Dwinzo_dev/app/src/modules/builder/groups/zoneGroup.tsx

734 lines
22 KiB
TypeScript

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,
useDeleteTool,
useDeletePointOrLine,
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";
const ZoneGroup: React.FC = () => {
const { camera, pointer, gl, raycaster, scene, controls } = useThree();
const [startPoint, setStartPoint] = useState<THREE.Vector3 | null>(null);
const [endPoint, setEndPoint] = useState<THREE.Vector3 | null>(null);
const { zones, setZones } = useZones();
const { zonePoints, setZonePoints } = useZonePoints();
const [isDragging, setIsDragging] = useState(false);
const { selectedZone } = useSelectedZoneStore();
const [draggedSphere, setDraggedSphere] = useState<THREE.Vector3 | null>(
null
);
const plane = useMemo(
() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),
[]
);
const { toggleView } = useToggleView();
const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine();
const { removedLayer, setRemovedLayer } = useRemovedLayer();
const { toolMode } = useToolMode();
const { setDeleteTool } = useDeleteTool();
const { activeLayer } = useActiveLayer();
const { socket } = useSocketStore();
const groupsRef = useRef<any>();
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 uColor;
void main(){
float alpha = 1.0 - vUv.y;
gl_FragColor = vec4(uColor, alpha);
}
`,
uniforms: {
uColor: { 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);
if (data.data && data.data.length > 0) {
const fetchedZones = data.data.map((zone: any) => ({
zoneId: zone.zoneId,
zoneName: zone.zoneName,
points: zone.points,
viewPortCenter: zone.viewPortCenter,
viewPortposition: zone.viewPortposition,
layer: zone.layer,
}));
setZones(fetchedZones);
const fetchedPoints = data.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.zoneId);
});
setRemovedLayer(null);
}
}, [removedLayer]);
useEffect(() => {
if (toolMode !== "Zone") {
setStartPoint(null);
setEndPoint(null);
} else {
setDeletePointOrLine(false);
setDeleteTool(false);
}
if (!toggleView) {
setStartPoint(null);
setEndPoint(null);
}
}, [toolMode, toggleView]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const addZoneToBackend = async (zone: {
zoneId: 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) return;
const position = [target[0], 10, target[2]];
const input = {
userId: userId,
organization: organization,
zoneData: {
zoneName: zone.zoneName,
zoneId: zone.zoneId,
points: zone.points,
viewPortCenter: target,
viewPortposition: position,
layer: zone.layer,
},
};
socket.emit("v2:zone:set", input);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const updateZoneToBackend = async (zone: {
zoneId: 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) return;
const position = [target[0], 10, target[2]];
const input = {
userId: userId,
organization: organization,
zoneData: {
zoneName: zone.zoneName,
zoneId: zone.zoneId,
points: zone.points,
viewPortCenter: target,
viewPortposition: position,
layer: zone.layer,
},
};
socket.emit("v2:zone:set", input);
};
const deleteZoneFromBackend = async (zoneId: string) => {
const email = localStorage.getItem("email");
const userId = localStorage.getItem("userId");
const organization = email!.split("@")[1].split(".")[0];
const input = {
userId: userId,
organization: organization,
zoneId: zoneId,
};
socket.emit("v2:zone:delete", input);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleDeleteZone = (zoneId: string) => {
const updatedZones = zones.filter((zone: any) => zone.zoneId !== zoneId);
setZones(updatedZones);
const zoneIndex = zones.findIndex((zone: any) => zone.zoneId === zoneId);
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(zoneId);
};
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 && !deletePointOrLine) {
isLeftMouseDown = false;
if (!startPoint && toolMode !== "move") {
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 && toolMode !== "move") {
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 zoneId = THREE.MathUtils.generateUUID();
const newZone = {
zoneId,
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 &&
deletePointOrLine
) {
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 zoneId = zones[zoneIndex].zoneId;
handleDeleteZone(zoneId);
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 (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" || deletePointOrLine || 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,
deletePointOrLine,
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 (
<group ref={groupsRef} name="zoneGroup">
<group name="zones" visible={!toggleView}>
{zones.map((zone: any) => (
<group
key={zone.zoneId}
name={zone.zoneName}
visible={zone.zoneId === selectedZone.zoneId}
>
{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 (
<mesh
name="zonePlane"
key={index}
position={midpoint}
rotation={[0, -angle, 0]}
>
<planeGeometry args={[planeWidth, planeHeight]} />
<primitive
object={zoneMaterial.clone()}
attach="material"
/>
</mesh>
);
})}
{!toggleView &&
(() => {
const points3D = zone.points || [];
const coords2D = points3D.map((p: any) => [p[0], p[2]]);
// Ensure the polygon is closed
if (
coords2D.length >= 3 &&
(coords2D[0][0] !== coords2D[coords2D.length - 1][0] ||
coords2D[0][1] !== coords2D[coords2D.length - 1][1])
) {
coords2D.push(coords2D[0]);
}
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 (
<Html
// data
key={zone.zoneId}
position={htmlPosition}
// class
className="zone-name-wrapper"
// others
center
>
<div className="zone-name">{zone.zoneName}</div>
</Html>
);
})()}
</group>
))}
</group>
<group name="zoneLines" visible={toggleView}>
{zones
.filter((zone: any) => zone.layer === activeLayer)
.map((zone: any) => (
<Line
key={zone.zoneId}
points={zone.points}
color="#007BFF"
lineWidth={3}
onClick={(e) => {
e.stopPropagation();
if (deletePointOrLine) {
handleDeleteZone(zone.zoneId);
}
}}
/>
))}
</group>
<group name="zoneArea" visible={toggleView}>
{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 < 3 ||
coords2D[0][0] !== coords2D[coords2D.length - 1][0] ||
coords2D[0][1] !== coords2D[coords2D.length - 1][1]
) {
coords2D.push(coords2D[0]);
}
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)}`;
const htmlPosition: [number, number, number] = [
center2D[0],
avgY + CONSTANTS.zoneConfig.height,
center2D[1],
];
return (
<Html
// data
key={`${index}-${zone}`}
position={htmlPosition}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
center
sprite
>
<div
className={`distance area line-${zone}`}
key={`${index}-${zone}`}
>
{zone.zoneName} ({formattedArea})
</div>
</Html>
);
})}
</group>
<group name="zonePoints" visible={toggleView}>
{zones
.filter((zone: any) => zone.layer === activeLayer)
.flatMap((zone: any) =>
zone.points.slice(0, 4).map((point: any, pointIndex: number) => (
<Sphere
key={`${zone.zoneId}-point-${pointIndex}`}
position={new THREE.Vector3(...point)}
args={[0.3, 16, 16]}
name={`point-${zone.zoneId}-${pointIndex}`}
>
<meshBasicMaterial color="red" />
</Sphere>
))
)}
</group>
<group name="tempGroup" visible={toggleView}>
{startPoint && endPoint && (
<Line
points={[
[startPoint.x, 0.15, startPoint.z],
[endPoint.x, 0.15, startPoint.z],
[endPoint.x, 0.15, endPoint.z],
[startPoint.x, 0.15, endPoint.z],
[startPoint.x, 0.15, startPoint.z],
]}
color="#C164FF"
lineWidth={3}
/>
)}
</group>
</group>
);
};
export default ZoneGroup;