import React, { useState, useEffect, useMemo, useRef } from "react"; import { Html, Line, Sphere } from "@react-three/drei"; import { useThree, useFrame } from "@react-three/fiber"; import * as THREE from "three"; import { useActiveLayer, useSocketStore, useToggleView, useToolMode, useRemovedLayer, useZones, useZonePoints, } from "../../../store/builder/store"; import { getZonesApi } from "../../../services/factoryBuilder/zones/getZonesApi"; import * as CONSTANTS from "../../../types/world/worldConstants"; import * as turf from "@turf/turf"; import { computeArea } from "../functions/computeArea"; import { useSelectedZoneStore } from "../../../store/visualization/useZoneStore"; import { useParams } from "react-router-dom"; const ZoneGroup: React.FC = () => { const { camera, pointer, gl, raycaster, scene, controls } = useThree(); const [startPoint, setStartPoint] = useState(null); const [endPoint, setEndPoint] = useState(null); const { zones, setZones } = useZones(); const { zonePoints, setZonePoints } = useZonePoints(); const [isDragging, setIsDragging] = useState(false); const { selectedZone } = useSelectedZoneStore(); const [draggedSphere, setDraggedSphere] = useState( null ); const plane = useMemo( () => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), [] ); const { toggleView } = useToggleView(); const { removedLayer, setRemovedLayer } = useRemovedLayer(); const { toolMode } = useToolMode(); const { activeLayer } = useActiveLayer(); const { socket } = useSocketStore(); const { projectId } = useParams(); const groupsRef = useRef(); const zoneMaterial = useMemo( () => new THREE.ShaderMaterial({ side: THREE.DoubleSide, vertexShader: ` varying vec2 vUv; void main(){ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); vUv = uv; } `, fragmentShader: ` varying vec2 vUv; uniform vec3 uOuterColor; void main(){ float alpha = 1.0 - vUv.y; gl_FragColor = vec4(uOuterColor, alpha); } `, uniforms: { uOuterColor: { value: new THREE.Color(CONSTANTS.zoneConfig.color) }, }, transparent: true, depthWrite: false, }), [] ); useEffect(() => { const fetchZones = async () => { const email = localStorage.getItem("email"); if (!email) return; const organization = email.split("@")[1].split(".")[0]; const data = await getZonesApi(organization, projectId); // console.log('data: ', data); if (data.length > 0) { const fetchedZones = data.map((zone: any) => ({ zoneUuid: zone.zoneUuid, zoneName: zone.zoneName, points: zone.points, viewPortCenter: zone.viewPortCenter, viewPortposition: zone.viewPortposition, layer: zone.layer, })); setZones(fetchedZones); const fetchedPoints = data.flatMap((zone: any) => zone.points.slice(0, 4).map((point: [number, number, number]) => new THREE.Vector3(...point)) ); setZonePoints(fetchedPoints); } }; fetchZones(); }, []); useEffect(() => { localStorage.setItem("zones", JSON.stringify(zones)); }, [zones]); useEffect(() => { if (removedLayer) { const updatedZones = zones.filter((zone: any) => zone.layer !== removedLayer); setZones(updatedZones); const updatedzonePoints = zonePoints.filter((_: any, index: any) => { const zoneIndex = Math.floor(index / 4); return zones[zoneIndex]?.layer !== removedLayer; }); setZonePoints(updatedzonePoints); zones.filter((zone: any) => zone.layer === removedLayer).forEach((zone: any) => { deleteZoneFromBackend(zone.zoneUuid); }); setRemovedLayer(null); } }, [removedLayer]); useEffect(() => { if (toolMode !== "Zone") { setStartPoint(null); setEndPoint(null); } if (!toggleView) { setStartPoint(null); setEndPoint(null); } }, [toolMode, toggleView]); // eslint-disable-next-line react-hooks/exhaustive-deps const addZoneToBackend = async (zone: { zoneUuid: string; zoneName: string; points: [number, number, number][]; layer: string; }) => { const email = localStorage.getItem("email"); const userId = localStorage.getItem("userId"); const organization = email!.split("@")[1].split(".")[0]; const calculateCenter = (points: number[][]) => { if (!points || points.length === 0) return null; let sumX = 0, sumY = 0, sumZ = 0; const numPoints = points.length; points.forEach(([x, y, z]) => { sumX += x; sumY += y; sumZ += z; }); return [sumX / numPoints, sumY / numPoints, sumZ / numPoints] as [ number, number, number ]; }; const target: [number, number, number] | null = calculateCenter(zone.points); if (!target || zone.points.length < 4) return; const position = [target[0], 10, target[2]]; const input = { userId: userId, projectId, organization: organization, zoneData: { zoneName: zone.zoneName, zoneUuid: zone.zoneUuid, points: zone.points, viewPortCenter: target, viewPortposition: position, layer: zone.layer, }, }; socket.emit("v1:zone:set", input); }; // eslint-disable-next-line react-hooks/exhaustive-deps const updateZoneToBackend = async (zone: { zoneUuid: string; zoneName: string; points: [number, number, number][]; layer: string; }) => { const email = localStorage.getItem("email"); const userId = localStorage.getItem("userId"); const organization = email!.split("@")[1].split(".")[0]; const calculateCenter = (points: number[][]) => { if (!points || points.length === 0) return null; let sumX = 0, sumY = 0, sumZ = 0; const numPoints = points.length; points.forEach(([x, y, z]) => { sumX += x; sumY += y; sumZ += z; }); return [sumX / numPoints, sumY / numPoints, sumZ / numPoints] as [ number, number, number ]; }; const target: [number, number, number] | null = calculateCenter(zone.points); if (!target || zone.points.length < 4) return; const position = [target[0], 10, target[2]]; const input = { userId: userId, projectId, organization: organization, zoneData: { zoneName: zone.zoneName, zoneUuid: zone.zoneUuid, points: zone.points, viewPortCenter: target, viewPortposition: position, layer: zone.layer, }, }; socket.emit("v1:zone:set", input); }; const deleteZoneFromBackend = async (zoneUuid: string) => { const email = localStorage.getItem("email"); const userId = localStorage.getItem("userId"); const organization = email!.split("@")[1].split(".")[0]; const input = { userId: userId, projectId, organization: organization, zoneUuid: zoneUuid, }; socket.emit("v1:zone:delete", input); }; // eslint-disable-next-line react-hooks/exhaustive-deps const handleDeleteZone = (zoneUuid: string) => { const updatedZones = zones.filter((zone: any) => zone.zoneUuid !== zoneUuid); setZones(updatedZones); const zoneIndex = zones.findIndex((zone: any) => zone.zoneUuid === zoneUuid); if (zoneIndex !== -1) { const zonePointsToRemove = zonePoints.slice(zoneIndex * 4, zoneIndex * 4 + 4); zonePointsToRemove.forEach((point: any) => groupsRef.current.remove(point)); const updatedzonePoints = zonePoints.filter((_: any, index: any) => index < zoneIndex * 4 || index >= zoneIndex * 4 + 4); setZonePoints(updatedzonePoints); } deleteZoneFromBackend(zoneUuid); }; useEffect(() => { if (!camera || !toggleView) return; const canvasElement = gl.domElement; let drag = false; let isLeftMouseDown = false; const onMouseDown = (evt: any) => { if (evt.button === 0) { isLeftMouseDown = true; drag = false; raycaster.setFromCamera(pointer, camera); const intersects = raycaster.intersectObjects(groupsRef.current.children, true); if (intersects.length > 0 && toolMode === "move") { const clickedObject = intersects[0].object; const sphereIndex = zonePoints.findIndex((point: any) => point.equals(clickedObject.position) ); if (sphereIndex !== -1) { (controls as any).enabled = false; setDraggedSphere(zonePoints[sphereIndex]); setIsDragging(true); } } } }; const onMouseUp = (evt: any) => { if (evt.button === 0 && !drag && !isDragging && toolMode === 'Zone') { isLeftMouseDown = false; if (!startPoint) { raycaster.setFromCamera(pointer, camera); const intersectionPoint = new THREE.Vector3(); const point = raycaster.ray.intersectPlane(plane, intersectionPoint); if (point) { setStartPoint(point); setEndPoint(null); } } else if (startPoint) { raycaster.setFromCamera(pointer, camera); const intersectionPoint = new THREE.Vector3(); const point = raycaster.ray.intersectPlane(plane, intersectionPoint); if (!point) return; const points = [ [startPoint.x, 0.15, startPoint.z], [point.x, 0.15, startPoint.z], [point.x, 0.15, point.z], [startPoint.x, 0.15, point.z], [startPoint.x, 0.15, startPoint.z], ] as [number, number, number][]; const zoneName = `Zone ${zones.length + 1}`; const zoneUuid = THREE.MathUtils.generateUUID(); const newZone = { zoneUuid, zoneName, points: points, layer: activeLayer, }; const newZones = [...zones, newZone]; setZones(newZones); const newzonePoints = [ new THREE.Vector3(startPoint.x, 0.15, startPoint.z), new THREE.Vector3(point.x, 0.15, startPoint.z), new THREE.Vector3(point.x, 0.15, point.z), new THREE.Vector3(startPoint.x, 0.15, point.z), ]; const updatedZonePoints = [...zonePoints, ...newzonePoints]; setZonePoints(updatedZonePoints); addZoneToBackend(newZone); setStartPoint(null); setEndPoint(null); } } else if (evt.button === 0 && !drag && !isDragging && toolMode === '2D-Delete') { raycaster.setFromCamera(pointer, camera); const intersects = raycaster.intersectObjects( groupsRef.current.children, true ); if (intersects.length > 0) { const clickedObject = intersects[0].object; const sphereIndex = zonePoints.findIndex((point: any) => point.equals(clickedObject.position) ); if (sphereIndex !== -1) { const zoneIndex = Math.floor(sphereIndex / 4); const zoneUuid = zones[zoneIndex].zoneUuid; handleDeleteZone(zoneUuid); return; } } } if (evt.button === 0) { if (isDragging && draggedSphere) { setIsDragging(false); setDraggedSphere(null); const sphereIndex = zonePoints.findIndex((point: any) => point === draggedSphere); if (sphereIndex !== -1) { const zoneIndex = Math.floor(sphereIndex / 4); if (zoneIndex !== -1 && zones[zoneIndex]) { updateZoneToBackend(zones[zoneIndex]); } } } } }; const onMouseMove = () => { if (!groupsRef.current) return; if (isLeftMouseDown) { drag = true; } raycaster.setFromCamera(pointer, camera); const intersects = raycaster.intersectObjects(groupsRef.current.children, true); if (intersects.length > 0 && intersects[0].object.name.includes("point")) { gl.domElement.style.cursor = toolMode === "move" ? "pointer" : "default"; } else { gl.domElement.style.cursor = "default"; } if (isDragging && draggedSphere) { raycaster.setFromCamera(pointer, camera); const intersectionPoint = new THREE.Vector3(); const point = raycaster.ray.intersectPlane(plane, intersectionPoint); if (point) { draggedSphere.set(point.x, 0.15, point.z); const sphereIndex = zonePoints.findIndex((point: any) => point === draggedSphere); if (sphereIndex !== -1) { const zoneIndex = Math.floor(sphereIndex / 4); const cornerIndex = sphereIndex % 4; const updatedZones = zones.map((zone: any, index: number) => { if (index === zoneIndex) { const updatedPoints = [...zone.points]; updatedPoints[cornerIndex] = [point.x, 0.15, point.z]; updatedPoints[4] = updatedPoints[0]; return { ...zone, points: updatedPoints }; } return zone; }); setZones(updatedZones); } } } }; const onContext = (event: any) => { event.preventDefault(); setStartPoint(null); setEndPoint(null); }; if (toolMode === "Zone" || toolMode === '2D-Delete' || toolMode === "move") { canvasElement.addEventListener("mousedown", onMouseDown); canvasElement.addEventListener("mouseup", onMouseUp); canvasElement.addEventListener("mousemove", onMouseMove); canvasElement.addEventListener("contextmenu", onContext); } return () => { canvasElement.removeEventListener("mousedown", onMouseDown); canvasElement.removeEventListener("mouseup", onMouseUp); canvasElement.removeEventListener("mousemove", onMouseMove); canvasElement.removeEventListener("contextmenu", onContext); }; }, [gl, camera, startPoint, toggleView, scene, toolMode, zones, isDragging, zonePoints, draggedSphere, activeLayer, raycaster, pointer, controls, plane, setZones, setZonePoints, addZoneToBackend, handleDeleteZone, updateZoneToBackend,]); useFrame(() => { if (!startPoint) return; raycaster.setFromCamera(pointer, camera); const intersectionPoint = new THREE.Vector3(); const point = raycaster.ray.intersectPlane(plane, intersectionPoint); if (point) { setEndPoint(point); } }); return ( {zones.map((zone: any) => ( {zone.points .slice(0, -1) .map((point: [number, number, number], index: number) => { const nextPoint = zone.points[index + 1]; const point1 = new THREE.Vector3(point[0], point[1], point[2]); const point2 = new THREE.Vector3( nextPoint[0], nextPoint[1], nextPoint[2] ); const planeWidth = point1.distanceTo(point2); const planeHeight = CONSTANTS.zoneConfig.height; const midpoint = new THREE.Vector3( (point1.x + point2.x) / 2, CONSTANTS.zoneConfig.height / 2 + (zone.layer - 1) * CONSTANTS.zoneConfig.height, (point1.z + point2.z) / 2 ); const angle = Math.atan2( point2.z - point1.z, point2.x - point1.x ); return ( ); })} {!toggleView && (() => { const points3D = zone.points || []; const coords2D = points3D.map((p: any) => [p[0], p[2]]); // Ensure the polygon is closed if ( coords2D.length >= 4 && (coords2D[0][0] !== coords2D[coords2D.length - 1][0] || coords2D[0][1] !== coords2D[coords2D.length - 1][1]) ) { coords2D.push(coords2D[0]); } if (coords2D.length < 4) return null; const polygon = turf.polygon([coords2D]); const center2D = turf.center(polygon).geometry.coordinates; // Calculate the average Y value const sumY = points3D.reduce( (sum: number, p: any) => sum + p[1], 0 ); const avgY = points3D.length > 0 ? sumY / points3D.length : 0; const htmlPosition: [number, number, number] = [ center2D[0], avgY + (CONSTANTS.zoneConfig.height || 0) + 1.5, center2D[1], ]; return (
{zone.zoneName}
); })()}
))}
{zones .filter((zone: any) => zone.layer === activeLayer) .map((zone: any) => ( { e.stopPropagation(); if (toolMode === '2D-Delete') { handleDeleteZone(zone.zoneUuid); } }} /> ))} {zones.map((zone: any, index: any) => { if (!toggleView) return null; const points3D = zone.points; const coords2D = points3D.map((p: any) => [p[0], p[2]]); if ( coords2D.length < 4 || coords2D[0][0] !== coords2D[coords2D.length - 1][0] || coords2D[0][1] !== coords2D[coords2D.length - 1][1] ) { coords2D.push(coords2D[0]); } if (coords2D.length < 4) return null; const polygon = turf.polygon([coords2D]); const center2D = turf.center(polygon).geometry.coordinates; const sumY = points3D.reduce((sum: number, p: any) => sum + p[1], 0); const avgY = sumY / points3D.length; const area = computeArea(points3D, "zone"); const formattedArea = `${area.toFixed(2)} m²`; const htmlPosition: [number, number, number] = [ center2D[0], avgY + CONSTANTS.zoneConfig.height, center2D[1], ]; return (
{zone.zoneName} ({formattedArea})
); })}
{zones .filter((zone: any) => zone.layer === activeLayer) .flatMap((zone: any) => zone.points.slice(0, 4).map((point: any, pointIndex: number) => ( )) )} {startPoint && endPoint && ( )}
); }; export default ZoneGroup;