diff --git a/app/package-lock.json b/app/package-lock.json index 2c1f714..5d92f68 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -48,6 +48,7 @@ "zustand": "^5.0.0-rc.2" }, "devDependencies": { + "@types/html2canvas": "^1.0.0", "@types/node": "^22.9.1", "@types/three": "^0.169.0", "axios": "^1.8.4", @@ -6490,6 +6491,17 @@ "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" }, + "node_modules/@types/html2canvas": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz", + "integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==", + "deprecated": "This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "html2canvas": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", diff --git a/app/package.json b/app/package.json index ce5c7d3..e555d5f 100644 --- a/app/package.json +++ b/app/package.json @@ -70,6 +70,7 @@ ] }, "devDependencies": { + "@types/html2canvas": "^1.0.0", "@types/node": "^22.9.1", "@types/three": "^0.169.0", "axios": "^1.8.4", diff --git a/app/src/components/layout/sidebarLeft/visualization/Templates.tsx b/app/src/components/layout/sidebarLeft/visualization/Templates.tsx index 4430bfc..e796101 100644 --- a/app/src/components/layout/sidebarLeft/visualization/Templates.tsx +++ b/app/src/components/layout/sidebarLeft/visualization/Templates.tsx @@ -95,10 +95,10 @@ const Templates = () => {
handleLoadTemplate(template)} + > {template?.snapshot && ( -
+
handleLoadTemplate(template)}> {`${template.name}(); + const [horizontalX, setHorizontalX] = useState(); + const [horizontalZ, setHorizontalZ] = useState(); const activeZoneWidgets = zoneWidgetData[selectedZone.zoneId] || []; useEffect(() => { @@ -181,26 +183,30 @@ export default function Dropped3dWidgets() { }; const onDrop = (event: any) => { - event.preventDefault(); event.stopPropagation(); - + hasEntered.current = false; - + const email = localStorage.getItem("email") || ""; const organization = email?.split("@")[1]?.split(".")[0]; - + const newWidget = createdWidgetRef.current; if (!newWidget || !widgetSelect.startsWith("ui")) return; - + // ✅ Extract 2D drop position - const [x, , z] = newWidget.position; - + let [x, y, z] = newWidget.position; + + // ✅ Clamp Y to at least 0 + y = Math.max(y, 0); + newWidget.position = [x, y, z]; + // ✅ Prepare polygon from selectedZone.points const points3D = selectedZone.points as Array<[number, number, number]>; const zonePolygonXZ = points3D.map(([x, , z]) => [x, z] as [number, number]); - + const isInside = isPointInPolygon([x, z], zonePolygonXZ); + // ✅ Remove temp widget const prevWidgets = useZoneWidgetStore.getState().zoneWidgetData[selectedZone.zoneId] || []; const cleanedWidgets = prevWidgets.filter(w => w.id !== newWidget.id); @@ -210,26 +216,29 @@ export default function Dropped3dWidgets() { [selectedZone.zoneId]: cleanedWidgets, }, })); + + // (Optional) Prevent adding if dropped outside zone // if (!isInside) { - // createdWidgetRef.current = null; - // return; // Stop here + // return; // } - // ✅ Add widget if inside polygon + + // ✅ Add widget addWidget(selectedZone.zoneId, newWidget); - + const add3dWidget = { organization, widget: newWidget, zoneId: selectedZone.zoneId, }; - + if (visualizationSocket) { visualizationSocket.emit("v2:viz-3D-widget:add", add3dWidget); } - + createdWidgetRef.current = null; }; + canvasElement.addEventListener("dragenter", handleDragEnter); @@ -341,28 +350,40 @@ export default function Dropped3dWidgets() { return inside; } const [prevX, setPrevX] = useState(0); - + useEffect(() => { const email = localStorage.getItem("email") || ""; const organization = email?.split("@")[1]?.split(".")[0]; const handleMouseDown = (event: MouseEvent) => { if (!rightClickSelected || !rightSelect) return; + const selectedZoneId = Object.keys(zoneWidgetData).find( + (zoneId: string) => + zoneWidgetData[zoneId].some( + (widget: WidgetData) => widget.id === rightClickSelected + ) + ); + if (!selectedZoneId) return; + const selectedWidget = zoneWidgetData[selectedZoneId].find( + (widget: WidgetData) => widget.id === rightClickSelected + ); + if (!selectedWidget) return + // let points = []; + // points.push(new THREE.Vector3(0, 0, 0)); + // points.push(new THREE.Vector3(0, selectedWidget.position[1], 0)); + // const newgeometry = new THREE.BufferGeometry().setFromPoints(points); + // let vector = new THREE.Vector3(); + // camera.getWorldDirection(vector); + // let cameraDirection = vector; + // let newPlane = new THREE.Plane(cameraDirection); + // floorPlanesVertical = newPlane; + // setFloorPlanesVertical(newPlane); + // const intersect1 = raycaster?.ray?.intersectPlane( + // floorPlanesVertical, + // planeIntersect.current + // ); + + // setintersectcontextmenu(intersect1.y); - const cameraDirection = new THREE.Vector3(); - camera.getWorldDirection(cameraDirection); - - // Plane normal should be perpendicular to screen (XZ move), so use screen right direction - const right = new THREE.Vector3(); - camera.getWorldDirection(cameraDirection); - cameraDirection.y = 0; - cameraDirection.normalize(); - - right.crossVectors(new THREE.Vector3(0, 1, 0), cameraDirection).normalize(); - - // Create a plane that allows vertical movement - const verticalPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(right, new THREE.Vector3(0, 0, 0)); - - setFloorPlanesVertical(verticalPlane); if (rightSelect === "RotateX" || rightSelect === "RotateY") { mouseStartRef.current = { x: event.clientX, y: event.clientY }; @@ -380,6 +401,7 @@ export default function Dropped3dWidgets() { rotationStartRef.current = selectedWidget.rotation || [0, 0, 0]; } } + }; const handleMouseMove = (event: MouseEvent) => { @@ -402,90 +424,110 @@ export default function Dropped3dWidgets() { raycaster.setFromCamera(mouse, camera); - if (rightSelect === "Horizontal Move" &&raycaster.ray.intersectPlane(plane.current, planeIntersect.current)) { - const points3D = selectedZone.points as Array<[number, number, number]>; - const zonePolygonXZ = points3D.map(([x, , z]) => [x, z] as [number, number]); - const newPosition: [number, number, number] = [ - planeIntersect.current.x, - selectedWidget.position[1], - planeIntersect.current.z, - ]; - const isInside = isPointInPolygon( - [newPosition[0], newPosition[2]], - zonePolygonXZ - ); - // if (isInside) { + if (rightSelect === "Horizontal Move") { + const intersect = raycaster.ray.intersectPlane(plane.current, planeIntersect.current); + if ( + intersect && + typeof horizontalX === "number" && + typeof horizontalZ === "number" + ) { + const selectedZoneId = Object.keys(zoneWidgetData).find(zoneId => + zoneWidgetData[zoneId].some(widget => widget.id === rightClickSelected) + ); + if (!selectedZoneId) return; + + const selectedWidget = zoneWidgetData[selectedZoneId].find(widget => widget.id === rightClickSelected); + if (!selectedWidget) return; + + const newPosition: [number, number, number] = [ + intersect.x + horizontalX, + selectedWidget.position[1], + intersect.z + horizontalZ, + ]; + + updateWidgetPosition(selectedZoneId, rightClickSelected, newPosition); - // } + } } + if (rightSelect === "Vertical Move") { - if (raycaster.ray.intersectPlane(floorPlanesVertical, planeIntersect.current)) { - const currentY = selectedWidget.position[1]; - const newY = planeIntersect.current.y; - console.log('planeIntersect.current: ', planeIntersect.current); - - const deltaY = newY - currentY; - - // Reject if jump is too large (safety check) - if (Math.abs(deltaY) > 200) return; - - // Clamp jump or apply smoothing - const clampedY = currentY + THREE.MathUtils.clamp(deltaY, -10, 10); - - if (clampedY > 0) { - updateWidgetPosition(selectedZoneId, rightClickSelected, [ - selectedWidget.position[0], - clampedY, - selectedWidget.position[2], - ]); - } + const intersect = raycaster.ray.intersectPlane(floorPlanesVertical, planeIntersect.current); + + if (intersect && typeof intersectcontextmenu === "number") { + const diff = intersect.y - intersectcontextmenu; + const unclampedY = selectedWidget.position[1] + diff; + const newY = Math.max(0, unclampedY); // Prevent going below floor (y=0) + + setintersectcontextmenu(intersect.y); + + const newPosition: [number, number, number] = [ + selectedWidget.position[0], + newY, + selectedWidget.position[2], + ]; + + updateWidgetPosition(selectedZoneId, rightClickSelected, newPosition); } } + + if (rightSelect?.startsWith("Rotate")) { + const axis = rightSelect.slice(-1).toLowerCase(); // "x", "y", or "z" + const currentX = event.pageX; + const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0; + setPrevX(currentX); + if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) { + const index = axis === "x" ? 0 : axis === "y" ? 1 : 2; + const currentRotation = selectedWidget.rotation as [number, number, number]; // assert type + const newRotation: [number, number, number] = [...currentRotation]; + newRotation[index] += 0.05 * sign; + updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation); + } + } + // if (rightSelect === "RotateX") { + // + // const currentX = event.pageX; + // const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0; + // + // setPrevX(currentX); + // if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) { + // + // const newRotation: [number, number, number] = [ + // selectedWidget.rotation[0] + 0.05 * sign, + // selectedWidget.rotation[1], + // selectedWidget.rotation[2], + // ]; + // updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation); + // } + // } + // if (rightSelect === "RotateY") { + // const currentX = event.pageX; + // const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0; + // setPrevX(currentX); + // if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) { + // const newRotation: [number, number, number] = [ + // selectedWidget.rotation[0], + // selectedWidget.rotation[1] + 0.05 * sign, + // selectedWidget.rotation[2], + // ]; - if (rightSelect === "RotateX") { - const currentX = event.pageX; - const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0; - setPrevX(currentX); - if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) { - const newRotation: [number, number, number] = [ - selectedWidget.rotation[0] + 0.05 * sign, - selectedWidget.rotation[1], - selectedWidget.rotation[2], - ]; - - updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation); - } - } + // updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation); + // } + // } + // if (rightSelect === "RotateZ") { + // const currentX = event.pageX; + // const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0; + // setPrevX(currentX); + // if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) { + // const newRotation: [number, number, number] = [ + // selectedWidget.rotation[0], + // selectedWidget.rotation[1], + // selectedWidget.rotation[2] + 0.05 * sign, + // ]; - if (rightSelect === "RotateY") { - const currentX = event.pageX; - const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0; - setPrevX(currentX); - if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) { - const newRotation: [number, number, number] = [ - selectedWidget.rotation[0] , - selectedWidget.rotation[1]+ 0.05 * sign, - selectedWidget.rotation[2], - ]; - - updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation); - } - } - if (rightSelect === "RotateZ") { - const currentX = event.pageX; - const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0; - setPrevX(currentX); - if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) { - const newRotation: [number, number, number] = [ - selectedWidget.rotation[0] , - selectedWidget.rotation[1], - selectedWidget.rotation[2]+ 0.05 * sign, - ]; - - updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation); - } - } + // updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation); + // } + // } }; const handleMouseUp = () => { if (!rightClickSelected || !rightSelect) return; @@ -535,7 +577,7 @@ export default function Dropped3dWidgets() { const rotation = selectedWidget.rotation || [0, 0, 0]; let lastRotation = formatValues(rotation) as [number, number, number]; - + // (async () => { // let response = await update3dWidgetRotation(selectedZoneId, organization, rightClickSelected, lastRotation); // @@ -574,6 +616,54 @@ export default function Dropped3dWidgets() { }; }, [rightClickSelected, rightSelect, zoneWidgetData, gl]); + const handleRightClick3d = (event: React.MouseEvent, id: string) => { + event.preventDefault(); + + const canvasElement = document.getElementById("real-time-vis-canvas"); + if (!canvasElement) throw new Error("Canvas element not found"); + + const canvasRect = canvasElement.getBoundingClientRect(); + const relativeX = event.clientX - canvasRect.left; + const relativeY = event.clientY - canvasRect.top; + + setEditWidgetOptions(true); + setRightClickSelected(id); + setTop(relativeY); + setLeft(relativeX); + + const selectedZoneId = Object.keys(zoneWidgetData).find(zoneId => + zoneWidgetData[zoneId].some(widget => widget.id === id) + ); + if (!selectedZoneId) return; + + const selectedWidget = zoneWidgetData[selectedZoneId].find(widget => widget.id === id); + if (!selectedWidget) return; + + const { top, left, width, height } = canvasElement.getBoundingClientRect(); + mouse.x = ((event.clientX - left) / width) * 2 - 1; + mouse.y = -((event.clientY - top) / height) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + + const cameraDirection = new THREE.Vector3(); + camera.getWorldDirection(cameraDirection); + const verticalPlane = new THREE.Plane(cameraDirection); + setFloorPlanesVertical(verticalPlane); + + const intersectPoint = raycaster.ray.intersectPlane(verticalPlane, planeIntersect.current); + if (intersectPoint) { + setintersectcontextmenu(intersectPoint.y); + } + const intersect2 = raycaster.ray.intersectPlane(plane.current, planeIntersect.current); + if (intersect2) { + const xDiff = -intersect2.x + selectedWidget.position[0]; + const zDiff = -intersect2.z + selectedWidget.position[2]; + setHorizontalX(xDiff); + setHorizontalZ(zDiff); + } + }; + + return ( <> {activeZoneWidgets.map( @@ -592,6 +682,7 @@ export default function Dropped3dWidgets() { setRightClickSelected(id); setTop(relativeY); setLeft(relativeX); + handleRightClick3d(event, id) }; switch (type) { diff --git a/app/src/components/ui/componets/RealTimeVisulization.tsx b/app/src/components/ui/componets/RealTimeVisulization.tsx index d70bfc3..0950536 100644 --- a/app/src/components/ui/componets/RealTimeVisulization.tsx +++ b/app/src/components/ui/componets/RealTimeVisulization.tsx @@ -286,7 +286,6 @@ const RealTimeVisulization: React.FC = () => { "Horizontal Move", "RotateX", "RotateY", - "RotateZ", "Delete", ]} />