import * as THREE from "three"; import { useThree } from "@react-three/fiber"; import React, { useEffect, useRef, useState } from "react"; import { useAsset3dWidget, useSocketStore, useWidgetSubOption } from "../../../../store/store"; import useModuleStore from "../../../../store/useModuleStore"; import { ThreeState } from "../../../../types/world/worldTypes"; import { useSelectedZoneStore } from "../../../../store/visualization/useZoneStore"; import { useEditWidgetOptionsStore, useLeftData, useRightClickSelected, useRightSelected, useTopData, useZoneWidgetStore } from "../../../../store/visualization/useZone3DWidgetStore"; import { use3DWidget } from "../../../../store/visualization/useDroppedObjectsStore"; import { get3dWidgetZoneData } from "../../../../services/visulization/zone/get3dWidgetData"; import { generateUniqueId } from "../../../../functions/generateUniqueId"; import ProductionCapacity from "./cards/ProductionCapacity"; import ReturnOfInvestment from "./cards/ReturnOfInvestment"; import StateWorking from "./cards/StateWorking"; import Throughput from "./cards/Throughput"; import { useWidgetStore } from "../../../../store/useWidgetStore"; import useChartStore from "../../../../store/visualization/useChartStore"; type WidgetData = { id: string; type: string; position: [number, number, number]; rotation?: [number, number, number]; tempPosition?: [number, number, number]; }; export default function Dropped3dWidgets() { const { widgetSelect } = useAsset3dWidget(); const { activeModule } = useModuleStore(); const { raycaster, gl, scene, mouse, camera }: ThreeState = useThree(); const { widgetSubOption } = useWidgetSubOption(); const { selectedZone } = useSelectedZoneStore(); const { top, setTop } = useTopData(); const { left, setLeft } = useLeftData(); const { rightSelect, setRightSelect } = useRightSelected(); const { editWidgetOptions, setEditWidgetOptions } = useEditWidgetOptionsStore(); const { zoneWidgetData, setZoneWidgetData, addWidget, updateWidgetPosition, updateWidgetRotation, tempWidget, tempWidgetPosition } = useZoneWidgetStore(); const { setWidgets3D } = use3DWidget(); const { visualizationSocket } = useSocketStore(); const { rightClickSelected, setRightClickSelected } = useRightClickSelected(); const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)); // Floor plane for horizontal move const verticalPlane = useRef(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0)); // Vertical plane for vertical move const planeIntersect = useRef(new THREE.Vector3()); const rotationStartRef = useRef<[number, number, number]>([0, 0, 0]); const mouseStartRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); const { setSelectedChartId } = useWidgetStore(); const { measurements, duration} = useChartStore(); let [floorPlanesVertical, setFloorPlanesVertical] = useState( new THREE.Plane(new THREE.Vector3(0, 1, 0)) ); const [intersectcontextmenu, setintersectcontextmenu] = useState(); const [horizontalX, setHorizontalX] = useState(); const [horizontalZ, setHorizontalZ] = useState(); const activeZoneWidgets = zoneWidgetData[selectedZone.zoneId] || []; useEffect(() => { if (activeModule !== "visualization") return; if (!selectedZone.zoneId) return; const email = localStorage.getItem("email") || ""; const organization = email?.split("@")[1]?.split(".")[0]; async function get3dWidgetData() { const result = await get3dWidgetZoneData( selectedZone.zoneId, organization ); setWidgets3D(result); if (result.length < 0) return const formattedWidgets = result?.map((widget: WidgetData) => ({ id: widget.id, type: widget.type, position: widget.position, rotation: widget.rotation || [0, 0, 0], })); setZoneWidgetData(selectedZone.zoneId, formattedWidgets); } get3dWidgetData(); }, [selectedZone.zoneId, activeModule]); const createdWidgetRef = useRef(null); useEffect(() => { if (activeModule !== "visualization") return; if (widgetSubOption === "Floating" || widgetSubOption === "2D") return; if (selectedZone.zoneName === "") return; const canvasElement = document.getElementById("real-time-vis-canvas"); if (!canvasElement) return; const hasEntered = { current: false }; const handleDragEnter = (event: DragEvent) => { event.preventDefault(); event.stopPropagation(); if (hasEntered.current || !widgetSelect.startsWith("ui")) return; hasEntered.current = true; const group1 = scene.getObjectByName("itemsGroup"); if (!group1) return; const rect = canvasElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster .intersectObjects(scene.children, true) .filter( (intersect) => !intersect.object.name.includes("Roof") && !intersect.object.name.includes("agv-collider") && !intersect.object.name.includes("MeasurementReference") && !intersect.object.userData.isPathObject && !(intersect.object.type === "GridHelper") ); if (intersects.length > 0) { const { x, y, z } = intersects[0].point; const newWidget: WidgetData = { id: generateUniqueId(), type: widgetSelect, position: [x, y, z], rotation: [0, 0, 0], }; createdWidgetRef.current = newWidget; tempWidget(selectedZone.zoneId, newWidget); // temp add in UI } }; const handleDragOver = (event: DragEvent) => { event.preventDefault(); event.stopPropagation(); event.dataTransfer!.dropEffect = "move"; // ✅ Add this line const widget = createdWidgetRef.current; if (!widget) return; const rect = canvasElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster .intersectObjects(scene.children, true) .filter( (intersect) => !intersect.object.name.includes("Roof") && !intersect.object.name.includes("agv-collider") && !intersect.object.name.includes("MeasurementReference") && !intersect.object.userData.isPathObject && !(intersect.object.type === "GridHelper") ); // Update widget's position in memory if (intersects.length > 0) { const { x, y, z } = intersects[0].point; tempWidgetPosition(selectedZone.zoneId, widget.id, [x, y, z]); widget.position = [x, y, z]; } }; 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 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); useZoneWidgetStore.setState((state) => ({ zoneWidgetData: { ...state.zoneWidgetData, [selectedZone.zoneId]: cleanedWidgets, }, })); // (Optional) Prevent adding if dropped outside zone // if (!isInside) { // createdWidgetRef.current = null; // return; // } // ✅ 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); canvasElement.addEventListener("dragover", handleDragOver); canvasElement.addEventListener("drop", onDrop); return () => { canvasElement.removeEventListener("dragenter", handleDragEnter); canvasElement.removeEventListener("dragover", handleDragOver); canvasElement.removeEventListener("drop", onDrop); }; }, [widgetSelect, activeModule, selectedZone.zoneId, widgetSubOption, camera,]); useEffect(() => { if (!rightClickSelected) return; const email = localStorage.getItem("email") || ""; const organization = email?.split("@")[1]?.split(".")[0]; if (rightSelect === "Duplicate") { async function duplicateWidget() { const widgetToDuplicate = activeZoneWidgets.find( (w: WidgetData) => w.id === rightClickSelected ); console.log("3d widget to duplecate", widgetToDuplicate); if (!widgetToDuplicate) return; const newWidget: any = { id: generateUniqueId(), type: widgetToDuplicate.type, position: [ widgetToDuplicate.position[0] + 0.5, widgetToDuplicate.position[1], widgetToDuplicate.position[2] + 0.5, ], rotation: widgetToDuplicate.rotation || [0, 0, 0], Data:{ measurements: measurements, duration: duration }, }; const adding3dWidget = { organization: organization, widget: newWidget, zoneId: selectedZone.zoneId, }; if (visualizationSocket) { visualizationSocket.emit("v2:viz-3D-widget:add", adding3dWidget); } // let response = await adding3dWidgets(selectedZone.zoneId, organization, newWidget) // addWidget(selectedZone.zoneId, newWidget); setRightSelect(null); setRightClickSelected(null); } duplicateWidget(); } if (rightSelect === "Delete") { const deleteWidgetApi = async () => { try { const deleteWidget = { organization, id: rightClickSelected, zoneId: selectedZone.zoneId, }; if (visualizationSocket) { visualizationSocket.emit("v2:viz-3D-widget:delete", deleteWidget); } // Call the API to delete the widget // const response = await delete3dWidgetApi(selectedZone.zoneId, organization, rightClickSelected); setZoneWidgetData( selectedZone.zoneId, activeZoneWidgets.filter( (w: WidgetData) => w.id !== rightClickSelected ) ); } catch (error) { } finally { setRightClickSelected(null); setRightSelect(null); } }; deleteWidgetApi(); } }, [rightSelect, rightClickSelected]); function isPointInPolygon( point: [number, number], polygon: Array<[number, number]> ): boolean { const [x, z] = point; let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const [xi, zi] = polygon[i]; const [xj, zj] = polygon[j]; const intersect = zi > z !== zj > z && x < ((xj - xi) * (z - zi)) / (zj - zi) + xi; if (intersect) inside = !inside; } 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); if (rightSelect === "RotateX" || rightSelect === "RotateY") { mouseStartRef.current = { x: event.clientX, y: event.clientY }; 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) { rotationStartRef.current = selectedWidget.rotation || [0, 0, 0]; } } }; const handleMouseMove = (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; const rect = gl.domElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); 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") { 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], // ]; // 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); // } // } }; const handleMouseUp = () => { if (!rightClickSelected || !rightSelect) return; 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; // Format values to 2 decimal places const formatValues = (vals: number[]) => vals.map((val) => parseFloat(val.toFixed(2))); if ( rightSelect === "Horizontal Move" || rightSelect === "Vertical Move" ) { let lastPosition = formatValues(selectedWidget.position) as [ number, number, number ]; // (async () => { // let response = await update3dWidget(selectedZoneId, organization, rightClickSelected, lastPosition); // // if (response) { // // } // })(); let updatingPosition = { organization: organization, zoneId: selectedZoneId, id: rightClickSelected, position: lastPosition, }; if (visualizationSocket) { visualizationSocket.emit( "v2:viz-3D-widget:modifyPositionRotation", updatingPosition ); } } else if (rightSelect.includes("Rotate")) { const rotation = selectedWidget.rotation || [0, 0, 0]; let lastRotation = formatValues(rotation) as [number, number, number]; // (async () => { // let response = await update3dWidgetRotation(selectedZoneId, organization, rightClickSelected, lastRotation); // // if (response) { // // } // })(); let updatingRotation = { organization: organization, zoneId: selectedZoneId, id: rightClickSelected, rotation: lastRotation, }; if (visualizationSocket) { visualizationSocket.emit( "v2:viz-3D-widget:modifyPositionRotation", updatingRotation ); } } // Reset selection setTimeout(() => { setRightClickSelected(null); setRightSelect(null); }, 50); }; window.addEventListener("mousedown", handleMouseDown); window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); return () => { window.removeEventListener("mousedown", handleMouseDown); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; }, [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( ({ id, type, position, Data, rotation = [0, 0, 0] }: any) => { const handleRightClick = (event: React.MouseEvent, id: string) => { setSelectedChartId({ id: id, type: type }) 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); handleRightClick3d(event, id) }; switch (type) { case "ui-Widget 1": return ( handleRightClick(e, id)} /> ); case "ui-Widget 2": return ( handleRightClick(e, id)} /> ); case "ui-Widget 3": return ( handleRightClick(e, id)} /> ); case "ui-Widget 4": return ( handleRightClick(e, id)} /> ); default: return null; } } )} ); }