diff --git a/app/package-lock.json b/app/package-lock.json index 112c62c..8be748e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -29,6 +29,7 @@ "chartjs-plugin-annotation": "^3.1.0", "glob": "^11.0.0", "gsap": "^3.12.5", + "html2canvas": "^1.4.1", "leva": "^0.10.0", "mqtt": "^5.10.4", "postprocessing": "^6.36.4", @@ -8027,6 +8028,15 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -9082,6 +9092,15 @@ "postcss": "^8.4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -12469,6 +12488,19 @@ } } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -20414,6 +20446,15 @@ "node": "*" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -21200,6 +21241,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", diff --git a/app/package.json b/app/package.json index 66e3b39..ce5c7d3 100644 --- a/app/package.json +++ b/app/package.json @@ -24,6 +24,7 @@ "chartjs-plugin-annotation": "^3.1.0", "glob": "^11.0.0", "gsap": "^3.12.5", + "html2canvas": "^1.4.1", "leva": "^0.10.0", "mqtt": "^5.10.4", "postprocessing": "^6.36.4", diff --git a/app/src/modules/builder/agv/agv.tsx b/app/src/modules/builder/agv/agv.tsx index 48f4306..5b8a2e4 100644 --- a/app/src/modules/builder/agv/agv.tsx +++ b/app/src/modules/builder/agv/agv.tsx @@ -1,68 +1,111 @@ import { useEffect, useState } from "react"; import { Line } from "@react-three/drei"; -import { useNavMesh, useSimulationStates } from "../../../store/store"; +import { + useNavMesh, + usePlayAgv, + useSimulationStates, +} from "../../../store/store"; import PathNavigator from "./pathNavigator"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; type PathPoints = { - modelUuid: string; - modelSpeed: number; - bufferTime: number; - points: { x: number; y: number; z: number }[]; - hitCount: number; + modelUuid: string; + modelSpeed: number; + bufferTime: number; + points: { x: number; y: number; z: number }[]; + hitCount: number; }; +interface ProcessContainerProps { + processes: any[]; + agvRef: any; + MaterialRef: any; +} -const Agv = () => { - const [pathPoints, setPathPoints] = useState([]); - const { simulationStates } = useSimulationStates(); - const { navMesh } = useNavMesh(); - const { isPlaying } = usePlayButtonStore(); +const Agv: React.FC = ({ + processes, + agvRef, + MaterialRef, +}) => { + const [pathPoints, setPathPoints] = useState([]); + const { simulationStates } = useSimulationStates(); + const { navMesh } = useNavMesh(); + const { isPlaying } = usePlayButtonStore(); + const { PlayAgv, setPlayAgv } = usePlayAgv(); - useEffect(() => { - if (simulationStates.length > 0) { - const agvModels = simulationStates.filter((val) => val.modelName === "agv" && val.type === "Vehicle"); - const newPathPoints = agvModels.filter((model: any) => model.points && model.points.actions && typeof model.points.actions.start === "object" && typeof model.points.actions.end === "object" && "x" in model.points.actions.start && "y" in model.points.actions.start && "x" in model.points.actions.end && "y" in model.points.actions.end) - .map((model: any) => ({ - modelUuid: model.modeluuid, - modelSpeed: model.points.speed, - bufferTime: model.points.actions.buffer, - hitCount: model.points.actions.hitCount, - points: [ - { x: model.position[0], y: model.position[1], z: model.position[2] }, - { x: model.points.actions.start.x, y: 0, z: model.points.actions.start.y }, - { x: model.points.actions.end.x, y: 0, z: model.points.actions.end.y }, - ], - })); + useEffect(() => { + if (simulationStates.length > 0) { + const agvModels = simulationStates.filter( + (val) => val.modelName === "agv" && val.type === "Vehicle" + ); - setPathPoints(newPathPoints); - } - }, [simulationStates]); + const newPathPoints = agvModels + .filter( + (model: any) => + model.points && + model.points.actions && + typeof model.points.actions.start === "object" && + typeof model.points.actions.end === "object" && + "x" in model.points.actions.start && + "y" in model.points.actions.start && + "x" in model.points.actions.end && + "y" in model.points.actions.end + ) + .map((model: any) => ({ + modelUuid: model.modeluuid, + modelSpeed: model.points.speed, + bufferTime: model.points.actions.buffer, + hitCount: model.points.actions.hitCount, + points: [ + { + x: model.position[0], + y: model.position[1], + z: model.position[2], + }, + { + x: model.points.actions.start.x, + y: 0, + z: model.points.actions.start.y, + }, + { + x: model.points.actions.end.x, + y: 0, + z: model.points.actions.end.y, + }, + ], + })); - return ( - <> - {pathPoints.map((pair, i) => ( - - + setPathPoints(newPathPoints); + } + }, [simulationStates]); - {pair.points.slice(1).map((point, idx) => ( - - - - - ))} - - ))} - - ); + return ( + <> + {pathPoints.map((pair, i) => ( + + + + {pair.points.slice(1).map((point, idx) => ( + + + + + ))} + + ))} + + ); }; export default Agv; diff --git a/app/src/modules/builder/agv/pathNavigator.tsx b/app/src/modules/builder/agv/pathNavigator.tsx index 6369a87..d68da57 100644 --- a/app/src/modules/builder/agv/pathNavigator.tsx +++ b/app/src/modules/builder/agv/pathNavigator.tsx @@ -1,216 +1,485 @@ -import React, { useEffect, useState, useRef } from "react"; +import React, { useEffect, useState, useRef, useMemo } from "react"; import * as THREE from "three"; import { useFrame, useThree } from "@react-three/fiber"; import { NavMeshQuery } from "@recast-navigation/core"; import { Line } from "@react-three/drei"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import { usePlayAgv } from "../../../store/store"; +import crate from "../../../assets/gltf-glb/crate_box.glb"; interface PathNavigatorProps { - navMesh: any; - pathPoints: any; - id: string; - speed: number; - bufferTime: number; - hitCount: number; + navMesh: any; + pathPoints: any; + id: string; + speed: number; + bufferTime: number; + hitCount: number; + processes: any[]; + agvRef: any; + MaterialRef: any; } - +interface AGVData { + processId: string; + vehicleId: string; + hitCount: number; + totalHits: number; +} +type Phase = "initial" | "toDrop" | "toPickup"; +type MaterialType = "Box" | "Crate"; export default function PathNavigator({ - navMesh, - pathPoints, - id, - speed, - bufferTime, - hitCount + navMesh, + pathPoints, + id, + speed, + bufferTime, + hitCount, + processes, + agvRef, + MaterialRef, }: PathNavigatorProps) { - const [path, setPath] = useState<[number, number, number][]>([]); - const [currentPhase, setCurrentPhase] = useState<'initial' | 'loop'>('initial'); - const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>([]); - const [pickupDropPath, setPickupDropPath] = useState<[number, number, number][]>([]); - const [dropPickupPath, setDropPickupPath] = useState<[number, number, number][]>([]); - const [initialPosition, setInitialPosition] = useState(null); - const [initialRotation, setInitialRotation] = useState(null); - const [targetPosition] = useState(new THREE.Vector3()); - const [smoothPosition] = useState(new THREE.Vector3()); - const [targetQuaternion] = useState(new THREE.Quaternion()); - const distancesRef = useRef([]); - const totalDistanceRef = useRef(0); - const progressRef = useRef(0); - const isWaiting = useRef(false); - const timeoutRef = useRef(null); - const pathTransitionProgress = useRef(0); + const [currentPhase, setCurrentPhase] = useState("initial"); + // - const { scene } = useThree(); - const { isPlaying } = usePlayButtonStore(); + const [path, setPath] = useState<[number, number, number][]>([]); + const PickUpDrop = useRef([]); - useEffect(() => { - const object = scene.getObjectByProperty("uuid", id); - if (object) { - setInitialPosition(object.position.clone()); - setInitialRotation(object.rotation.clone()); - smoothPosition.copy(object.position.clone()); - targetPosition.copy(object.position.clone()); - targetQuaternion.setFromEuler(object.rotation.clone()); - } - }, [scene, id]); + // const [currentPhase, setCurrentPhase] = useState<"initial" | "loop">( + // "initial" + // ); + const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>( + [] + ); - const computePath = (start: any, end: any) => { - try { - const navMeshQuery = new NavMeshQuery(navMesh); - const { path: segmentPath } = navMeshQuery.computePath(start, end); - return segmentPath?.map(({ x, y, z }) => [x, y + 0.1, z] as [number, number, number]) || []; - } catch { - return []; - } - }; + const [pickupDropPath, setPickupDropPath] = useState< + [number, number, number][] + >([]); + const [dropPickupPath, setDropPickupPath] = useState< + [number, number, number][] + >([]); + const [initialPosition, setInitialPosition] = useState( + null + ); + const [initialRotation, setInitialRotation] = useState( + null + ); + const [boxVisible, setBoxVisible] = useState(false); - const resetState = () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } + const distancesRef = useRef([]); + const totalDistanceRef = useRef(0); + const progressRef = useRef(0); + const isWaiting = useRef(false); + const timeoutRef = useRef(null); + const hasStarted = useRef(false); - setPath([]); - setCurrentPhase('initial'); - distancesRef.current = []; - totalDistanceRef.current = 0; - progressRef.current = 0; - isWaiting.current = false; - pathTransitionProgress.current = 0; + const { scene } = useThree(); + const { isPlaying } = usePlayButtonStore(); + const { PlayAgv, setPlayAgv } = usePlayAgv(); - const object = scene.getObjectByProperty("uuid", id); - if (object && initialPosition && initialRotation) { - object.position.copy(initialPosition); - object.rotation.copy(initialRotation); - smoothPosition.copy(initialPosition); - targetPosition.copy(initialPosition); - targetQuaternion.setFromEuler(initialRotation); - } - }; + useEffect(() => { + const object = scene.getObjectByProperty("uuid", id); + if (object) { + setInitialPosition(object.position.clone()); + setInitialRotation(object.rotation.clone()); + } + }, [scene, id]); + const computePath = (start: any, end: any) => { + try { + const navMeshQuery = new NavMeshQuery(navMesh); + const { path: segmentPath } = navMeshQuery.computePath(start, end); + return ( + segmentPath?.map( + ({ x, y, z }) => [x, y + 0.1, z] as [number, number, number] + ) || [] + ); + } catch { + return []; + } + }; - useEffect(() => { - if (!isPlaying) { - resetState(); - } + const resetState = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } - if (!navMesh || pathPoints.length < 2) return; + setPath([]); + setCurrentPhase("initial"); + setPickupDropPath([]); + setDropPickupPath([]); + distancesRef.current = []; + totalDistanceRef.current = 0; + progressRef.current = 0; + isWaiting.current = false; - const [pickup, drop] = pathPoints.slice(-2); - const object = scene.getObjectByProperty("uuid", id); - if (!object) return; + if (initialPosition && initialRotation) { + const object = scene.getObjectByProperty("uuid", id); + if (object) { + object.position.copy(initialPosition); + object.rotation.copy(initialRotation); + } + } + }; - const currentPosition = { x: object.position.x, y: object.position.y, z: object.position.z }; + useEffect(() => { + if (!isPlaying) { + resetState(); + } - const toPickupPath = computePath(currentPosition, pickup); - const pickupToDropPath = computePath(pickup, drop); - const dropToPickupPath = computePath(drop, pickup); + if (!navMesh || pathPoints.length < 2) return; - if (toPickupPath.length && pickupToDropPath.length && dropToPickupPath.length) { - setPickupDropPath(pickupToDropPath); - setDropPickupPath(dropToPickupPath); - setToPickupPath(toPickupPath); - setPath(toPickupPath); - setCurrentPhase('initial'); - } - }, [navMesh, pathPoints, hitCount, isPlaying]); + const [pickup, drop] = pathPoints.slice(-2); - useEffect(() => { - if (path.length < 2) return; + PickUpDrop.current = pathPoints.slice(-2); - let total = 0; - const segmentDistances = path.slice(0, -1).map((point, i) => { - const dist = new THREE.Vector3(...point).distanceTo(new THREE.Vector3(...path[i + 1])); - total += dist; - return dist; - }); + const object = scene.getObjectByProperty("uuid", id); - distancesRef.current = segmentDistances; - totalDistanceRef.current = total; - progressRef.current = 0; - isWaiting.current = false; - }, [path]); + if (!object) return; - useFrame((_, delta) => { - if (!isPlaying || path.length < 2 || !scene || !id) return; + const currentPosition = { + x: object.position.x, + y: object.position.y, + z: object.position.z, + }; - const object = scene.getObjectByProperty("uuid", id); - if (!object) return; + const toPickupPath = computePath(currentPosition, pickup); + const pickupToDropPath = computePath(pickup, drop); + const dropToPickupPath = computePath(drop, pickup); - const speedFactor = speed; - progressRef.current += delta * speedFactor; + if ( + toPickupPath.length && + pickupToDropPath.length && + dropToPickupPath.length + ) { + setPickupDropPath(pickupToDropPath); + setDropPickupPath(dropToPickupPath); + setToPickupPath(toPickupPath); + setPath(toPickupPath); + setCurrentPhase("initial"); + } + }, [navMesh, pathPoints, hitCount, isPlaying, PlayAgv]); - let covered = progressRef.current; - let accumulated = 0; - let index = 0; + useEffect(() => { + if (path.length < 2) return; - while ( - index < distancesRef.current.length && - covered > accumulated + distancesRef.current[index] - ) { - accumulated += distancesRef.current[index]; - index++; - } + let total = 0; + const segmentDistances = path.slice(0, -1).map((point, i) => { + const dist = new THREE.Vector3(...point).distanceTo( + new THREE.Vector3(...path[i + 1]) + ); + total += dist; + return dist; + }); - if (index >= distancesRef.current.length) { - progressRef.current = totalDistanceRef.current; + distancesRef.current = segmentDistances; + totalDistanceRef.current = total; + progressRef.current = 0; + isWaiting.current = false; + }, [path]); - if (!isWaiting.current) { - isWaiting.current = true; + // Add these refs outside the useFrame if not already present: + const startPointReached = useRef(false); - timeoutRef.current = setTimeout(() => { - if (currentPhase === 'initial') { - setPath(pickupDropPath); - setCurrentPhase('loop'); - } else { - setPath(prevPath => - prevPath === pickupDropPath ? dropPickupPath : pickupDropPath - ); - } + const baseMaterials = useMemo( + () => ({ + Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), + Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + // Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + Default: new THREE.MeshStandardMaterial(), + }), + [] + ); - progressRef.current = 0; - isWaiting.current = false; - }, bufferTime * 1000); - } - return; - } + // Example usage: + const targetModelUUID = id; // Replace with your target ID + const matchedProcess = findProcessByTargetModelUUID( + processes, + targetModelUUID + ); - const start = new THREE.Vector3(...path[index]); - const end = new THREE.Vector3(...path[index + 1]); - const dist = distancesRef.current[index]; - const t = THREE.MathUtils.clamp((covered - accumulated) / dist, 0, 1); + function findProcessByTargetModelUUID(processes: any, targetModelUUID: any) { + for (const process of processes) { + for (const path of process.paths) { + for (const point of path.points) { + if ( + point.connections?.targets?.some( + (target: any) => target.modelUUID === targetModelUUID + ) + ) { + // + return process.id; // Return the process if a match is found + } + } + } + } + return null; // Return null if no match is found + } - targetPosition.copy(start).lerp(end, t); + useFrame((_, delta) => {}); + const boxRef = useRef(null); - smoothPosition.lerp(targetPosition, 0.1); - object.position.copy(smoothPosition); + useEffect(() => { + if (!scene || !boxRef || !processes) return; - const direction = new THREE.Vector3().subVectors(end, start).normalize(); - const targetRotationY = Math.atan2(direction.x, direction.z); - targetQuaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), targetRotationY); - object.quaternion.slerp(targetQuaternion, 0.1); - }); + // 1. Find the existing object by UUID + const existingObject = scene.getObjectByProperty("uuid", id); + if (!existingObject) return; - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); + // 2. Find the process that targets this object's modelUUID + if (boxVisible) { + const matchedProcess = findProcessByTargetModelUUID(processes, id); - return ( - - {toPickupPath.length > 0 && ( - - )} + // 3. Determine the material (from materialref) if a process is matched + let materialType = "Default"; - {pickupDropPath.length > 0 && ( - - )} + if (matchedProcess) { + const materialEntry = MaterialRef.current.find((item: any) => + item.objects.some((obj: any) => obj.processId === matchedProcess) + ); + console.log("materialEntry: ", materialEntry); + if (materialEntry) { + materialType = materialEntry.material; // "Box" or "Crate" + } + } - {dropPickupPath.length > 0 && ( - - )} - - ); -} \ No newline at end of file + // 4. Create the box mesh with the assigned material + const boxGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5); + const boxMaterial = baseMaterials[materialType as MaterialType]; + const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial); + boxMesh.position.y = 1; + boxMesh.name = `box-${id}`; + + // 5. Add to scene and cleanup + existingObject.add(boxMesh); + boxRef.current = boxMesh; + } + + if (!startPointReached.current && boxVisible) { + setBoxVisible(false); + } + return () => { + if (boxRef.current?.parent) { + boxRef.current.parent.remove(boxRef.current); + } + boxRef.current = null; + }; + }, [ + processes, + MaterialRef, + boxVisible, + findProcessByTargetModelUUID, + startPointReached, + ]); + + useFrame((_, delta) => { + const currentAgv = (agvRef.current || []).find( + (agv: AGVData) => agv.vehicleId === id + ); + + if (!scene || !id || !isPlaying) return; + + const object = scene.getObjectByProperty("uuid", id); + if (!object) return; + + if (isPlaying && !hasStarted.current) { + hasStarted.current = false; + startPointReached.current = false; + progressRef.current = 0; + isWaiting.current = false; + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + } + + const isAgvReady = () => { + if (!agvRef.current || agvRef.current.length === 0) return false; + if (!currentAgv) return false; + return hitCount === currentAgv.expectedHitCount; + }; + + // Step 1: Snap to start point on first play + if (isPlaying && !hasStarted.current && toPickupPath.length > 0) { + setBoxVisible(false); + const startPoint = new THREE.Vector3(...toPickupPath[0]); + object.position.copy(startPoint); + + if (toPickupPath.length > 1) { + const nextPoint = new THREE.Vector3(...toPickupPath[1]); + const direction = nextPoint.clone().sub(startPoint).normalize(); + object.rotation.y = Math.atan2(direction.x, direction.z); + } + + hasStarted.current = true; + startPointReached.current = true; + progressRef.current = 0; + + setToPickupPath(toPickupPath.slice(-1)); + + return; + } + + // Step 2: Wait at start point for AGV readiness + if (isPlaying && startPointReached.current && path.length === 0) { + if (!isAgvReady()) { + return; // Prevent transitioning to the next phase if AGV is not ready + } + + setBoxVisible(true); + + setPath([...toPickupPath]); // Start path transition once the AGV is ready + setCurrentPhase("toDrop"); + progressRef.current = 0; + console.log("startPointReached: ", startPointReached.current); + startPointReached.current = false; + + return; + } + + if (path.length < 2) return; + + progressRef.current += delta * speed; + + let covered = progressRef.current; + let accumulated = 0; + let index = 0; + + if (distancesRef.current.length !== path.length - 1) { + distancesRef.current = []; + totalDistanceRef.current = 0; + + for (let i = 0; i < path.length - 1; i++) { + const start = new THREE.Vector3(...path[i]); + const end = new THREE.Vector3(...path[i + 1]); + const distance = start.distanceTo(end); + distancesRef.current.push(distance); + totalDistanceRef.current += distance; + } + } + + while ( + index < distancesRef.current.length && + covered > accumulated + distancesRef.current[index] + ) { + accumulated += distancesRef.current[index]; + index++; + } + + // AGV has completed its path + if (index >= distancesRef.current.length) { + progressRef.current = totalDistanceRef.current; + + timeoutRef.current = setTimeout(() => { + if (!isAgvReady()) { + isWaiting.current = false; + return; + } + + let nextPath = []; + let nextPhase = currentPhase; + + if (currentPhase === "toDrop") { + nextPath = dropPickupPath; + nextPhase = "toPickup"; + setBoxVisible(false); + } else if (currentPhase === "toPickup") { + // When returning to start point (toPickup phase completed) + // Set position to toPickupPath[1] instead of [0] + if (toPickupPath.length > 1) { + object.position.copy(new THREE.Vector3(...toPickupPath[1])); + // Also set rotation towards the next point if available + if (toPickupPath.length > 2) { + const nextPoint = new THREE.Vector3(...toPickupPath[2]); + const direction = nextPoint + .clone() + .sub(object.position) + .normalize(); + object.rotation.y = Math.atan2(direction.x, direction.z); + } + } + + // Reset the path and mark start point as reached + setPath([]); + startPointReached.current = true; + progressRef.current = 0; + distancesRef.current = []; + + // Stop the AGV by setting isplaying to false + if (currentAgv) { + currentAgv.isplaying = false; + } + + // Clear timeout and return to prevent further movement + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + return; + } else { + nextPath = pickupDropPath; + nextPhase = "toDrop"; + setBoxVisible(true); + } + + setPath([...nextPath]); + setCurrentPhase(nextPhase); + progressRef.current = 0; + isWaiting.current = false; + distancesRef.current = []; + + // Reset hit count for the next cycle + if (agvRef.current) { + agvRef.current = agvRef.current.map((agv: AGVData) => + agv.vehicleId === id ? { ...agv, hitCount: 0 } : agv + ); + } + }, bufferTime * 1000); + + return; + } + + // Step 4: Interpolate position and rotation + const start = new THREE.Vector3(...path[index]); + const end = new THREE.Vector3(...path[index + 1]); + const dist = distancesRef.current[index]; + + if (!dist || dist === 0) return; + + const t = THREE.MathUtils.clamp((covered - accumulated) / dist, 0, 1); + object.position.copy(start.clone().lerp(end, t)); + + const direction = new THREE.Vector3().subVectors(end, start).normalize(); + object.rotation.y = Math.atan2(direction.x, direction.z); + }); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + return ( + + {toPickupPath.length > 0 && ( + + )} + + {pickupDropPath.length > 0 && ( + + )} + + {dropPickupPath.length > 0 && ( + + )} + + ); +} diff --git a/app/src/modules/builder/geomentries/assets/addAssetModel.ts b/app/src/modules/builder/geomentries/assets/addAssetModel.ts index d109eb8..1b1c959 100644 --- a/app/src/modules/builder/geomentries/assets/addAssetModel.ts +++ b/app/src/modules/builder/geomentries/assets/addAssetModel.ts @@ -150,6 +150,7 @@ async function handleModelLoad( const organization = email ? email.split("@")[1].split(".")[0] : ""; getAssetEventType(selectedItem.id, organization).then(async (res) => { + console.log('res: ', res); if (res.type === "Conveyor") { const pointUUIDs = res.points.map(() => THREE.MathUtils.generateUUID()); @@ -224,6 +225,7 @@ async function handleModelLoad( eventData as Types.ConveyorEventsSchema ]); + console.log('data: ', data); socket.emit("v2:model-asset:add", data); } else if (res.type === "Vehicle") { @@ -453,6 +455,7 @@ async function handleModelLoad( return updatedItems; }); + console.log('data: ', data); socket.emit("v2:model-asset:add", data); } diff --git a/app/src/modules/builder/geomentries/lines/distanceText/distanceText.tsx b/app/src/modules/builder/geomentries/lines/distanceText/distanceText.tsx index 39162ad..307d51f 100644 --- a/app/src/modules/builder/geomentries/lines/distanceText/distanceText.tsx +++ b/app/src/modules/builder/geomentries/lines/distanceText/distanceText.tsx @@ -1,143 +1,143 @@ -import { useEffect, useState } from "react"; -import { getLines } from "../../../../../services/factoryBuilder/lines/getLinesApi"; -import * as THREE from "three"; -import { - useActiveLayer, - useDeletedLines, - useNewLines, - useToggleView, -} from "../../../../../store/store"; -import objectLinesToArray from "../lineConvertions/objectLinesToArray"; -import { Html } from "@react-three/drei"; -import * as Types from "../../../../../types/world/worldTypes"; - -const DistanceText = () => { - const [lines, setLines] = useState< - { - distance: string; - position: THREE.Vector3; - userData: Types.Line; - layer: string; - }[] - >([]); - const { activeLayer } = useActiveLayer(); - const { toggleView } = useToggleView(); - const { newLines, setNewLines } = useNewLines(); - const { deletedLines, setDeletedLines } = useDeletedLines(); - - useEffect(() => { - const email = localStorage.getItem("email"); - if (!email) return; - const organization = email.split("@")[1].split(".")[0]; - - getLines(organization).then((data) => { - data = objectLinesToArray(data); - - const lines = data - .filter((line: Types.Line) => line[0][2] === activeLayer) - .map((line: Types.Line) => { - const point1 = new THREE.Vector3( - line[0][0].x, - line[0][0].y, - line[0][0].z - ); - const point2 = new THREE.Vector3( - line[1][0].x, - line[1][0].y, - line[1][0].z - ); - const distance = point1.distanceTo(point2); - const midpoint = new THREE.Vector3() - .addVectors(point1, point2) - .divideScalar(2); - return { - distance: distance.toFixed(1), - position: midpoint, - userData: line, - layer: activeLayer, - }; - }); - setLines(lines); - }); - }, [activeLayer]); - - useEffect(() => { - if (newLines.length > 0) { - if (newLines[0][0][2] !== activeLayer) return; - const newLinesData = newLines.map((line: Types.Line) => { - const point1 = new THREE.Vector3( - line[0][0].x, - line[0][0].y, - line[0][0].z - ); - const point2 = new THREE.Vector3( - line[1][0].x, - line[1][0].y, - line[1][0].z - ); - const distance = point1.distanceTo(point2); - const midpoint = new THREE.Vector3() - .addVectors(point1, point2) - .divideScalar(2); - - return { - distance: distance.toFixed(1), - position: midpoint, - userData: line, - layer: activeLayer, - }; - }); - setLines((prevLines) => [...prevLines, ...newLinesData]); - setNewLines([]); - } - }, [newLines, activeLayer]); - - useEffect(() => { - if ((deletedLines as Types.Lines).length > 0) { - setLines((prevLines) => - prevLines.filter( - (line) => - !deletedLines.some( - (deletedLine: any) => - deletedLine[0][1] === line.userData[0][1] && - deletedLine[1][1] === line.userData[1][1] - ) - ) - ); - setDeletedLines([]); - } - }, [deletedLines]); - - return ( - <> - {toggleView && ( - - {lines.map((text) => ( - -
- {text.distance} m -
- - ))} -
- )} - - ); -}; - -export default DistanceText; +import { useEffect, useState } from "react"; +import { getLines } from "../../../../../services/factoryBuilder/lines/getLinesApi"; +import * as THREE from "three"; +import { + useActiveLayer, + useDeletedLines, + useNewLines, + useToggleView, +} from "../../../../../store/store"; +import objectLinesToArray from "../lineConvertions/objectLinesToArray"; +import { Html } from "@react-three/drei"; +import * as Types from "../../../../../types/world/worldTypes"; + +const DistanceText = () => { + const [lines, setLines] = useState< + { + distance: string; + position: THREE.Vector3; + userData: Types.Line; + layer: string; + }[] + >([]); + const { activeLayer } = useActiveLayer(); + const { toggleView } = useToggleView(); + const { newLines, setNewLines } = useNewLines(); + const { deletedLines, setDeletedLines } = useDeletedLines(); + + useEffect(() => { + const email = localStorage.getItem("email"); + if (!email) return; + const organization = email.split("@")[1].split(".")[0]; + + getLines(organization).then((data) => { + data = objectLinesToArray(data); + + const lines = data + .filter((line: Types.Line) => line[0][2] === activeLayer) + .map((line: Types.Line) => { + const point1 = new THREE.Vector3( + line[0][0].x, + line[0][0].y, + line[0][0].z + ); + const point2 = new THREE.Vector3( + line[1][0].x, + line[1][0].y, + line[1][0].z + ); + const distance = point1.distanceTo(point2); + const midpoint = new THREE.Vector3() + .addVectors(point1, point2) + .divideScalar(2); + return { + distance: distance.toFixed(1), + position: midpoint, + userData: line, + layer: activeLayer, + }; + }); + setLines(lines); + }); + }, [activeLayer]); + + useEffect(() => { + if (newLines.length > 0) { + if (newLines[0][0][2] !== activeLayer) return; + const newLinesData = newLines.map((line: Types.Line) => { + const point1 = new THREE.Vector3( + line[0][0].x, + line[0][0].y, + line[0][0].z + ); + const point2 = new THREE.Vector3( + line[1][0].x, + line[1][0].y, + line[1][0].z + ); + const distance = point1.distanceTo(point2); + const midpoint = new THREE.Vector3() + .addVectors(point1, point2) + .divideScalar(2); + + return { + distance: distance.toFixed(1), + position: midpoint, + userData: line, + layer: activeLayer, + }; + }); + setLines((prevLines) => [...prevLines, ...newLinesData]); + setNewLines([]); + } + }, [newLines, activeLayer]); + + useEffect(() => { + if ((deletedLines as Types.Lines).length > 0) { + setLines((prevLines) => + prevLines.filter( + (line) => + !deletedLines.some( + (deletedLine: any) => + deletedLine[0][1] === line.userData[0][1] && + deletedLine[1][1] === line.userData[1][1] + ) + ) + ); + setDeletedLines([]); + } + }, [deletedLines]); + + return ( + <> + {toggleView && ( + + {lines.map((text) => ( + +
+ {text.distance} m +
+ + ))} +
+ )} + + ); +}; + +export default DistanceText; diff --git a/app/src/modules/builder/geomentries/lines/distanceText/referenceDistanceText.tsx b/app/src/modules/builder/geomentries/lines/distanceText/referenceDistanceText.tsx index 039d30f..7342b1e 100644 --- a/app/src/modules/builder/geomentries/lines/distanceText/referenceDistanceText.tsx +++ b/app/src/modules/builder/geomentries/lines/distanceText/referenceDistanceText.tsx @@ -1,71 +1,71 @@ -import * as THREE from "three"; -import { Html } from "@react-three/drei"; -import { useState, useEffect } from "react"; -import { useActiveLayer } from "../../../../../store/store"; - -const ReferenceDistanceText = ({ line }: { line: any }) => { - interface TextState { - distance: string; - position: THREE.Vector3; - userData: any; - layer: any; - } - - const [text, setTexts] = useState(null); - const { activeLayer } = useActiveLayer(); - - useEffect(() => { - if (line) { - if (line.parent === null) { - setTexts(null); - return; - } - const distance = line.userData.linePoints.cursorPosition.distanceTo( - line.userData.linePoints.startPoint - ); - const midpoint = new THREE.Vector3() - .addVectors( - line.userData.linePoints.cursorPosition, - line.userData.linePoints.startPoint - ) - .divideScalar(2); - const newTexts = { - distance: distance.toFixed(1), - position: midpoint, - userData: line, - layer: activeLayer, - }; - setTexts(newTexts); - } - }); - - return ( - - - {text !== null && ( - -
- {text.distance} m -
- - )} -
-
- ); -}; - -export default ReferenceDistanceText; +import * as THREE from "three"; +import { Html } from "@react-three/drei"; +import { useState, useEffect } from "react"; +import { useActiveLayer } from "../../../../../store/store"; + +const ReferenceDistanceText = ({ line }: { line: any }) => { + interface TextState { + distance: string; + position: THREE.Vector3; + userData: any; + layer: any; + } + + const [text, setTexts] = useState(null); + const { activeLayer } = useActiveLayer(); + + useEffect(() => { + if (line) { + if (line.parent === null) { + setTexts(null); + return; + } + const distance = line.userData.linePoints.cursorPosition.distanceTo( + line.userData.linePoints.startPoint + ); + const midpoint = new THREE.Vector3() + .addVectors( + line.userData.linePoints.cursorPosition, + line.userData.linePoints.startPoint + ) + .divideScalar(2); + const newTexts = { + distance: distance.toFixed(1), + position: midpoint, + userData: line, + layer: activeLayer, + }; + setTexts(newTexts); + } + }); + + return ( + + + {text !== null && ( + +
+ {text.distance} m +
+ + )} +
+
+ ); +}; + +export default ReferenceDistanceText; diff --git a/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts b/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts index ea5ea74..c93b660 100644 --- a/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts +++ b/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts @@ -20,6 +20,7 @@ async function loadInitialFloorItems( const organization = (email!.split("@")[1]).split(".")[0]; const items = await getFloorAssets(organization); + console.log('items: ', items); localStorage.setItem("FloorItems", JSON.stringify(items)); await initializeDB(); diff --git a/app/src/modules/scene/controls/selection/copyPasteControls.tsx b/app/src/modules/scene/controls/selection/copyPasteControls.tsx index 0c68da3..5c07649 100644 --- a/app/src/modules/scene/controls/selection/copyPasteControls.tsx +++ b/app/src/modules/scene/controls/selection/copyPasteControls.tsx @@ -266,7 +266,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -344,7 +344,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -422,7 +422,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, diff --git a/app/src/modules/scene/controls/selection/duplicationControls.tsx b/app/src/modules/scene/controls/selection/duplicationControls.tsx index dbd0b34..a127a30 100644 --- a/app/src/modules/scene/controls/selection/duplicationControls.tsx +++ b/app/src/modules/scene/controls/selection/duplicationControls.tsx @@ -245,7 +245,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -323,7 +323,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -401,7 +401,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, diff --git a/app/src/modules/simulation/path/pathCreation.tsx b/app/src/modules/simulation/path/pathCreation.tsx index c31da9b..345fff3 100644 --- a/app/src/modules/simulation/path/pathCreation.tsx +++ b/app/src/modules/simulation/path/pathCreation.tsx @@ -335,7 +335,6 @@ function PathCreation({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObjec key={path.modeluuid} ref={(el) => (groupRefs.current[path.modeluuid] = el!)} position={path.position} - rotation={path.rotation} onClick={(e) => { if (isConnecting || eyeDropMode) return; e.stopPropagation(); @@ -389,7 +388,6 @@ function PathCreation({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObjec key={path.modeluuid} ref={(el) => (groupRefs.current[path.modeluuid] = el!)} position={path.position} - rotation={path.rotation} onClick={(e) => { if (isConnecting || eyeDropMode) return; e.stopPropagation(); diff --git a/app/src/modules/simulation/process/mesh.tsx b/app/src/modules/simulation/process/mesh.tsx deleted file mode 100644 index f6b94bb..0000000 --- a/app/src/modules/simulation/process/mesh.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const Mesh: React.FC = () => { - return ; -}; - -export default Mesh; diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index c8cf07f..7b906d2 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -1,502 +1,119 @@ -import React, { useRef, useState, useEffect, useMemo } from "react"; -import { - useAnimationPlaySpeed, - usePauseButtonStore, - usePlayButtonStore, - useResetButtonStore, -} from "../../../store/usePlayButtonStore"; -import { GLTFLoader } from "three-stdlib"; +import React, { useRef, useEffect, useMemo } from "react"; import { useLoader, useFrame } from "@react-three/fiber"; +import { GLTFLoader } from "three-stdlib"; import * as THREE from "three"; import { GLTF } from "three-stdlib"; import crate from "../../../assets/gltf-glb/crate_box.glb"; -import box from "../../../assets/gltf-glb/box.glb"; -interface PointAction { - uuid: string; - name: string; - type: "Inherit" | "Spawn" | "Despawn" | "Delay" | "Swap"; - objectType: string; - material: string; - delay: string | number; - spawnInterval: string | number; - isUsed: boolean; +import { useProcessAnimation } from "./useProcessAnimations"; +import ProcessObject from "./processObject"; +import { ProcessData } from "./types"; +import { useSimulationStates } from "../../../store/store"; +import { retrieveGLTF } from "../../../utils/indexDB/idbUtils"; + +interface ProcessContainerProps { + processes: ProcessData[]; + setProcesses: React.Dispatch>; + agvRef: any; + MaterialRef: any; } -interface ProcessPoint { - uuid: string; - position: number[]; - rotation: number[]; - actions: PointAction[]; - connections: { - source: { modelUUID: string; pointUUID: string }; - targets: { modelUUID: string; pointUUID: string }[]; - }; -} - -interface ProcessPath { - modeluuid: string; - modelName: string; - points: ProcessPoint[]; - pathPosition: number[]; - pathRotation: number[]; - speed: number; -} - -interface ProcessData { - id: string; - paths: ProcessPath[]; - animationPath: { x: number; y: number; z: number }[]; - pointActions: PointAction[][]; - speed: number; - customMaterials?: Record; - renderAs?: "box" | "custom"; -} - -interface AnimationState { - currentIndex: number; - progress: number; - isAnimating: boolean; - speed: number; - isDelaying: boolean; - delayStartTime: number; - currentDelayDuration: number; - delayComplete: boolean; - currentPathIndex: number; -} - -interface SpawnedObject { - ref: React.RefObject; - state: AnimationState; - visible: boolean; - material: THREE.Material; - spawnTime: number; - currentMaterialType: string; - position: THREE.Vector3; -} - -interface ProcessAnimationState { - spawnedObjects: { [objectId: string]: SpawnedObject }; - nextSpawnTime: number; - objectIdCounter: number; - isProcessDelaying: boolean; - processDelayStartTime: number; - processDelayDuration: number; -} - -const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ +const ProcessAnimator: React.FC = ({ processes, + setProcesses, + agvRef, + MaterialRef, }) => { const gltf = useLoader(GLTFLoader, crate) as GLTF; - const { isPlaying, setIsPlaying } = usePlayButtonStore(); - const { isPaused, setIsPaused } = usePauseButtonStore(); - const { isReset, setReset } = useResetButtonStore(); const groupRef = useRef(null); - const debugRef = useRef(false); - const clockRef = useRef(new THREE.Clock()); - const pauseTimeRef = useRef(0); - const elapsedBeforePauseRef = useRef(0); - const animationStatesRef = useRef>({}); - const { speed, setSpeed } = useAnimationPlaySpeed(); - const prevIsPlaying = useRef(null); - const [internalResetFlag, setInternalResetFlag] = useState(false); - const [animationStates, setAnimationStates] = useState< - Record - >({}); - // Store the speed in a ref to access the latest value in animation frames - const speedRef = useRef(speed); + const { + animationStates, + setAnimationStates, + clockRef, + elapsedBeforePauseRef, + speedRef, + debugRef, + findSpawnPoint, + createSpawnedObject, + handlePointActions, + hasNonInheritActions, + getPointDataForAnimationIndex, + processes: processedProcesses, + checkAndCountTriggers, + } = useProcessAnimation(processes, setProcesses, agvRef); - // Update the ref when speed changes - useEffect(() => { - speedRef.current = speed; - }, [speed]); - - useEffect(() => { - if (prevIsPlaying.current !== null) { - setAnimationStates({}); - } - - // Update ref to current isPlaying after effect - prevIsPlaying.current = isPlaying; - - // setAnimationStates({}); - }, [isPlaying]); - - // Sync ref with state - useEffect(() => { - animationStatesRef.current = animationStates; - }, [animationStates]); - - // Base materials const baseMaterials = useMemo( () => ({ Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), - Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + // Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + Default: new THREE.MeshStandardMaterial(), }), [] ); - // Replace your reset effect with this: useEffect(() => { - if (isReset) { - // 1. Mark that we're doing an internal reset - setInternalResetFlag(true); - - // 2. Pause the animation first - setIsPlaying(false); - setIsPaused(false); - - // 3. Reset all animation states - setAnimationStates({}); - animationStatesRef.current = {}; - - // 4. Reset timing references - clockRef.current = new THREE.Clock(); - elapsedBeforePauseRef.current = 0; - pauseTimeRef.current = 0; - - // 5. Clear the external reset flag - setReset(false); - - // 6. After state updates are complete, restart - setTimeout(() => { - setInternalResetFlag(false); - setIsPlaying(true); - }, 0); - } - }, [isReset, setReset, setIsPlaying, setIsPaused]); - - // Handle pause state changes - useEffect(() => { - if (isPaused) { - pauseTimeRef.current = clockRef.current.getElapsedTime(); - } else if (pauseTimeRef.current > 0) { - const pausedDuration = - clockRef.current.getElapsedTime() - pauseTimeRef.current; - elapsedBeforePauseRef.current += pausedDuration; - } - }, [isPaused]); - - // Initialize animation states when processes or play state changes - useEffect(() => { - if (isPlaying && !internalResetFlag) { - const newStates: Record = {}; - processes.forEach((process) => { - newStates[process.id] = { - spawnedObjects: {}, - nextSpawnTime: 0, - objectIdCounter: 0, - isProcessDelaying: false, - processDelayStartTime: 0, - processDelayDuration: 0, + // Update material references for all spawned objects + Object.entries(animationStates).forEach(([processId, processState]) => { + Object.keys(processState.spawnedObjects).forEach((objectId) => { + const entry = { + processId, + objectId, }; - }); - setAnimationStates(newStates); - animationStatesRef.current = newStates; - clockRef.current.start(); - } - }, [isPlaying, processes, internalResetFlag]); - const findSpawnPoint = (process: ProcessData): ProcessPoint | null => { - for (const path of process.paths || []) { - for (const point of path.points || []) { - const spawnAction = point.actions?.find( - (a) => a.isUsed && a.type === "Spawn" + const materialType = + processState.spawnedObjects[objectId]?.currentMaterialType; + + if (!materialType) { + return; + } + + const matRefArray = MaterialRef.current; + + // Find existing material group + const existing = matRefArray.find( + (entryGroup: { material: string; objects: any[] }) => + entryGroup.material === materialType ); - if (spawnAction) { - return point; - } - } - } - return null; - }; - const findAnimationPathPoint = ( - process: ProcessData, - spawnPoint: ProcessPoint - ): THREE.Vector3 => { - if (process.animationPath && process.animationPath.length > 0) { - let pointIndex = 0; - for (const path of process.paths || []) { - for (let i = 0; i < (path.points?.length || 0); i++) { - const point = path.points?.[i]; - if (point && point.uuid === spawnPoint.uuid) { - if (process.animationPath[pointIndex]) { - const p = process.animationPath[pointIndex]; - return new THREE.Vector3(p.x, p.y, p.z); - } + if (existing) { + // Check if this processId + objectId already exists + const alreadyExists = existing.objects.some( + (o: any) => + o.processId === entry.processId && o.objectId === entry.objectId + ); + + if (!alreadyExists) { + existing.objects.push(entry); } - pointIndex++; + } else { + // Create new group for this material type + matRefArray.push({ + material: materialType, + objects: [entry], + }); } - } - } - return new THREE.Vector3( - spawnPoint.position[0], - spawnPoint.position[1], - spawnPoint.position[2] - ); - }; - - const createSpawnedObject = ( - process: ProcessData, - currentTime: number, - materialType: string, - spawnPoint: ProcessPoint - ): SpawnedObject => { - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const spawnPosition = findAnimationPathPoint(process, spawnPoint); - const material = - processMaterials[materialType as keyof typeof processMaterials] || - baseMaterials.Default; - - if (debugRef.current) { - console.log(`Creating object with material: ${materialType}`, material); - } - - return { - ref: React.createRef(), - state: { - currentIndex: 0, - progress: 0, - isAnimating: true, - speed: process.speed || 1, // Process base speed (will be multiplied by global speed) - isDelaying: false, - delayStartTime: 0, - currentDelayDuration: 0, - delayComplete: false, - currentPathIndex: 0, - }, - visible: true, - material: material, - currentMaterialType: materialType, - spawnTime: currentTime, - position: spawnPosition, - }; - }; - - const handleMaterialSwap = ( - processId: string, - objectId: string, - materialType: string - ) => { - if (debugRef.current) { - console.log(`Attempting material swap to: ${materialType}`); - } - - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState || !processState.spawnedObjects[objectId]) { - if (debugRef.current) console.log("Object not found for swap"); - return prev; - } - - const process = processes.find((p) => p.id === processId); - if (!process) { - if (debugRef.current) console.log("Process not found"); - return prev; - } - - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const newMaterial = - processMaterials[materialType as keyof typeof processMaterials]; - if (!newMaterial) { - if (debugRef.current) console.log(`Material ${materialType} not found`); - return prev; - } - - if (debugRef.current) { - console.log(`Swapping material for ${objectId} to ${materialType}`); - } - - return { - ...prev, - [processId]: { - ...processState, - spawnedObjects: { - ...processState.spawnedObjects, - [objectId]: { - ...processState.spawnedObjects[objectId], - material: newMaterial, - currentMaterialType: materialType, - }, - }, - }, - }; + }); }); - }; + }, [animationStates, MaterialRef, agvRef]); - const handlePointActions = ( - processId: string, - objectId: string, - actions: PointAction[] = [], - currentTime: number - ): boolean => { - let shouldStopAnimation = false; - - actions.forEach((action) => { - if (!action.isUsed) return; - - if (debugRef.current) { - console.log(`Processing action: ${action.type} for ${objectId}`); - } - - switch (action.type) { - case "Delay": - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState || processState.isProcessDelaying) { - return prev; - } - - const delayDuration = - typeof action.delay === "number" - ? action.delay - : parseFloat(action.delay as string) || 0; - - if (delayDuration > 0) { - return { - ...prev, - [processId]: { - ...processState, - isProcessDelaying: true, - processDelayStartTime: currentTime, - processDelayDuration: delayDuration, - spawnedObjects: { - ...processState.spawnedObjects, - [objectId]: { - ...processState.spawnedObjects[objectId], - state: { - ...processState.spawnedObjects[objectId].state, - isAnimating: false, - isDelaying: true, - delayStartTime: currentTime, - currentDelayDuration: delayDuration, - delayComplete: false, - }, - }, - }, - }, - }; - } - return prev; - }); - shouldStopAnimation = true; - break; - - case "Despawn": - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState) return prev; - - const newSpawnedObjects = { ...processState.spawnedObjects }; - delete newSpawnedObjects[objectId]; - - return { - ...prev, - [processId]: { - ...processState, - spawnedObjects: newSpawnedObjects, - }, - }; - }); - shouldStopAnimation = true; - break; - - case "Swap": - if (action.material) { - handleMaterialSwap(processId, objectId, action.material); - } - break; - - default: - break; - } - }); - - return shouldStopAnimation; - }; - - const hasNonInheritActions = (actions: PointAction[] = []): boolean => { - return actions.some((action) => action.isUsed && action.type !== "Inherit"); - }; - - const getPointDataForAnimationIndex = ( - process: ProcessData, - index: number - ): ProcessPoint | null => { - if (!process.paths) return null; - - let cumulativePoints = 0; - for (const path of process.paths) { - const pointCount = path.points?.length || 0; - - if (index < cumulativePoints + pointCount) { - const pointIndex = index - cumulativePoints; - return path.points?.[pointIndex] || null; - } - - cumulativePoints += pointCount; - } - - return null; - }; + // In processAnimator.tsx - only the relevant spawn logic part that needs fixes useFrame(() => { - if (!isPlaying || isPaused) return; - + // Spawn logic frame const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; - processes.forEach((process) => { + processedProcesses.forEach((process) => { const processState = newStates[process.id]; if (!processState) return; if (processState.isProcessDelaying) { - // Apply global speed to delays (faster speed = shorter delays) - const effectiveDelayTime = - processState.processDelayDuration / speedRef.current; - - if ( - currentTime - processState.processDelayStartTime >= - effectiveDelayTime - ) { - newStates[process.id] = { - ...processState, - isProcessDelaying: false, - spawnedObjects: Object.entries( - processState.spawnedObjects - ).reduce( - (acc, [id, obj]) => ({ - ...acc, - [id]: { - ...obj, - state: { - ...obj.state, - isDelaying: false, - delayComplete: true, - isAnimating: true, - progress: - obj.state.progress === 0 ? 0.001 : obj.state.progress, - }, - }, - }), - {} - ), - }; - } + // Existing delay handling logic... return; } @@ -513,7 +130,14 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ ? spawnAction.spawnInterval : parseFloat(spawnAction.spawnInterval as string) || 0; - // Apply global speed to spawn intervals (faster speed = more frequent spawns) + // Check if this is a zero interval spawn and we already spawned an object + if ( + spawnInterval === 0 && + processState.hasSpawnedZeroIntervalObject === true + ) { + return; // Don't spawn more objects for zero interval + } + const effectiveSpawnInterval = spawnInterval / speedRef.current; if (currentTime >= processState.nextSpawnTime) { @@ -522,9 +146,11 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ process, currentTime, spawnAction.material || "Default", - spawnPoint + spawnPoint, + baseMaterials ); + // Update state with the new object and flag for zero interval newStates[process.id] = { ...processState, spawnedObjects: { @@ -533,6 +159,11 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ }, objectIdCounter: processState.objectIdCounter + 1, nextSpawnTime: currentTime + effectiveSpawnInterval, + // Mark that we've spawned an object for zero interval case + hasSpawnedZeroIntervalObject: + spawnInterval === 0 + ? true + : processState.hasSpawnedZeroIntervalObject, }; } }); @@ -542,20 +173,18 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ }); useFrame((_, delta) => { - if (!isPlaying || isPaused) return; - + // Animation logic frame const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; - processes.forEach((process) => { + processedProcesses.forEach((process) => { const processState = newStates[process.id]; if (!processState) return; if (processState.isProcessDelaying) { - // Apply global speed to delays (faster speed = shorter delays) const effectiveDelayTime = processState.processDelayDuration / speedRef.current; @@ -617,7 +246,6 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ const stateRef = obj.state; if (stateRef.isDelaying) { - // Apply global speed to delays (faster speed = shorter delays) const effectiveDelayTime = stateRef.currentDelayDuration / speedRef.current; @@ -654,18 +282,15 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ stateRef.currentIndex ); + // Handle point actions when first arriving at point if (stateRef.progress === 0 && currentPointData?.actions) { - if (debugRef.current) { - console.log( - `At point ${stateRef.currentIndex} with actions:`, - currentPointData.actions - ); - } const shouldStop = handlePointActions( process.id, objectId, currentPointData.actions, - currentTime + currentTime, + processedProcesses, + baseMaterials ); if (shouldStop) { updatedObjects[objectId] = { ...obj, state: { ...stateRef } }; @@ -676,14 +301,39 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ const nextPointIdx = stateRef.currentIndex + 1; const isLastPoint = nextPointIdx >= path.length; + // if (isLastPoint) { + // if (currentPointData?.actions) { + // const shouldStop = !hasNonInheritActions( + // currentPointData.actions + // ); + // if (shouldStop) { + // return; + // } + // } + // } + if (isLastPoint) { if (currentPointData?.actions) { - const shouldStop = !hasNonInheritActions( + const hasNonInherit = hasNonInheritActions( currentPointData.actions ); - if (shouldStop) { + if (!hasNonInherit) { + // Remove the object if all actions are inherit + updatedObjects[objectId] = { + ...obj, + visible: false, + state: { ...stateRef, isAnimating: false }, + }; return; } + } else { + // No actions at last point - remove the object + updatedObjects[objectId] = { + ...obj, + visible: false, + state: { ...stateRef, isAnimating: false }, + }; + return; } } @@ -691,8 +341,6 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ const nextPoint = path[nextPointIdx]; const distance = path[stateRef.currentIndex].distanceTo(nextPoint); - - // Apply both process-specific speed and global speed multiplier const effectiveSpeed = stateRef.speed * speedRef.current; const movement = effectiveSpeed * delta; @@ -708,17 +356,19 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ stateRef.progress = 0; currentRef.position.copy(nextPoint); + // TRIGGER CHECK - When object arrives at new point + checkAndCountTriggers( + process.id, + objectId, + stateRef.currentIndex, // The new point index + processedProcesses, + currentTime + ); + const newPointData = getPointDataForAnimationIndex( process, stateRef.currentIndex ); - - if (newPointData?.actions && debugRef.current) { - console.log( - `Reached new point with actions:`, - newPointData.actions - ); - } } else { currentRef.position.lerpVectors( path[stateRef.currentIndex], @@ -742,56 +392,32 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ }); }); - if (!processes || processes.length === 0) { + if (!processedProcesses || processedProcesses.length === 0) { return null; } return ( - <> + {Object.entries(animationStates).flatMap(([processId, processState]) => Object.entries(processState.spawnedObjects) .filter(([_, obj]) => obj.visible) .map(([objectId, obj]) => { - const process = processes.find((p) => p.id === processId); + const process = processedProcesses.find((p) => p.id === processId); + const renderAs = process?.renderAs || "custom"; - if (renderAs === "box") { - return ( - } - material={obj.material} - position={obj.position} - > - - - ); - } - - if (gltf?.scene) { - // Clone the scene and apply the material to all meshes - const clonedScene = gltf.scene.clone(); - clonedScene.traverse((child) => { - if (child instanceof THREE.Mesh) { - child.material = obj.material; - } - }); - - return ( - } - position={obj.position} - > - - - ); - } - - return null; + return ( + + ); }) )} - + ); }; diff --git a/app/src/modules/simulation/process/processContainer.tsx b/app/src/modules/simulation/process/processContainer.tsx index bf8fa48..1cbc75b 100644 --- a/app/src/modules/simulation/process/processContainer.tsx +++ b/app/src/modules/simulation/process/processContainer.tsx @@ -2,15 +2,28 @@ import React, { useState } from "react"; import ProcessCreator from "./processCreator"; import ProcessAnimator from "./processAnimator"; -const ProcessContainer: React.FC = () => { - const [processes, setProcesses] = useState([]); - +interface ProcessContainerProps { + processes: any[]; + setProcesses: React.Dispatch>; + agvRef: any; + MaterialRef: any; +} +const ProcessContainer: React.FC = ({ + processes, + setProcesses, + agvRef, + MaterialRef, +}) => { return ( <> - {processes.length > 0 && } - + ); }; diff --git a/app/src/modules/simulation/process/processCreator.tsx b/app/src/modules/simulation/process/processCreator.tsx index 4403a71..cae152b 100644 --- a/app/src/modules/simulation/process/processCreator.tsx +++ b/app/src/modules/simulation/process/processCreator.tsx @@ -24,16 +24,26 @@ // isUsed: boolean; // } +// export interface PointTrigger { +// uuid: string; +// bufferTime: number; +// name: string; +// type: string; +// isUsed: boolean; +// } + // export interface PathPoint { // uuid: string; // position: [number, number, number]; // actions: PointAction[]; +// triggers: PointTrigger[]; // connections: { // targets: Array<{ modelUUID: string }>; // }; // } // export interface SimulationPath { +// type: string; // modeluuid: string; // points: PathPoint[]; // pathPosition: [number, number, number]; @@ -45,7 +55,9 @@ // paths: SimulationPath[]; // animationPath: THREE.Vector3[]; // pointActions: PointAction[][]; +// pointTriggers: PointTrigger[][]; // speed: number; +// isplaying: boolean; // } // interface ProcessCreatorProps { @@ -58,18 +70,33 @@ // ): SimulationPath { // const { modeluuid } = path; -// // Simplified normalizeAction function that preserves exact original properties +// // Normalized action handler // const normalizeAction = (action: any): PointAction => { // return { ...action }; // Return exact copy with no modifications // }; +// // Normalized trigger handler +// const normalizeTrigger = (trigger: any): PointTrigger => { +// return { ...trigger }; // Return exact copy with no modifications +// }; + // if (path.type === "Conveyor") { // return { +// type: path.type, // modeluuid, // points: path.points.map((point) => ({ // uuid: point.uuid, // position: point.position, -// actions: point.actions.map(normalizeAction), // Preserve exact actions +// actions: Array.isArray(point.actions) +// ? point.actions.map(normalizeAction) +// : point.actions +// ? [normalizeAction(point.actions)] +// : [], +// triggers: Array.isArray(point.triggers) +// ? point.triggers.map(normalizeTrigger) +// : point.triggers +// ? [normalizeTrigger(point.triggers)] +// : [], // connections: { // targets: point.connections.targets.map((target) => ({ // modelUUID: target.modelUUID, @@ -83,44 +110,44 @@ // : path.speed || 1, // }; // } else { +// // For vehicle paths, handle the case where triggers might not exist // return { +// type: path.type, // modeluuid, // points: [ // { -// uuid: path.point.uuid, -// position: path.point.position, -// actions: Array.isArray(path.point.actions) -// ? path.point.actions.map(normalizeAction) -// : [normalizeAction(path.point.actions)], +// uuid: path.points.uuid, +// position: path.points.position, +// actions: Array.isArray(path.points.actions) +// ? path.points.actions.map(normalizeAction) +// : path.points.actions +// ? [normalizeAction(path.points.actions)] +// : [], +// // For vehicle paths, since triggers might not exist in the schema, +// // we always define default to an empty array +// triggers: [], // connections: { -// targets: path.point.connections.targets.map((target) => ({ +// targets: path.points.connections.targets.map((target) => ({ // modelUUID: target.modelUUID, // })), // }, // }, // ], // pathPosition: path.position, -// speed: path.point.speed || 1, +// speed: path.points.speed || 1, // }; // } // } -// // Custom shallow comparison for arrays -// const areArraysEqual = (a: any[], b: any[]) => { -// if (a.length !== b.length) return false; -// for (let i = 0; i < a.length; i++) { -// if (a[i] !== b[i]) return false; -// } -// return true; -// }; - // // Helper function to create an empty process // const createEmptyProcess = (): Process => ({ // id: `process-${Math.random().toString(36).substring(2, 11)}`, // paths: [], // animationPath: [], // pointActions: [], +// pointTriggers: [], // Added point triggers array // speed: 1, +// isplaying: false, // }); // // Enhanced connection checking function @@ -156,12 +183,38 @@ // return connectsToLast && !connectsToFirst; // } +// // Check if a point has a spawn action +// function hasSpawnAction(point: PathPoint): boolean { +// return point.actions.some((action) => action.type.toLowerCase() === "spawn"); +// } + +// // Ensure spawn point is always at the beginning of the path +// function ensureSpawnPointIsFirst(path: SimulationPath): SimulationPath { +// if (path.points.length !== 3) return path; + +// // If the third point has spawn action and first doesn't, reverse the array +// if (hasSpawnAction(path.points[2]) && !hasSpawnAction(path.points[0])) { +// return { +// ...path, +// points: [...path.points].reverse(), +// }; +// } + +// return path; +// } + // // Updated path adjustment function // function adjustPathPointsOrder(paths: SimulationPath[]): SimulationPath[] { -// if (paths.length < 2) return paths; +// if (paths.length < 1) return paths; // const adjustedPaths = [...paths]; +// // First ensure all paths have spawn points at the beginning +// for (let i = 0; i < adjustedPaths.length; i++) { +// adjustedPaths[i] = ensureSpawnPointIsFirst(adjustedPaths[i]); +// } + +// // Then handle connections between paths // for (let i = 0; i < adjustedPaths.length - 1; i++) { // const currentPath = adjustedPaths[i]; // const nextPath = adjustedPaths[i + 1]; @@ -189,6 +242,7 @@ // const [processes, setProcesses] = useState([]); // const hasSpawnAction = useCallback((path: SimulationPath): boolean => { +// if (path.type !== "Conveyor") return false; // return path.points.some((point) => // point.actions.some((action) => action.type.toLowerCase() === "spawn") // ); @@ -202,19 +256,23 @@ // const animationPath: THREE.Vector3[] = []; // const pointActions: PointAction[][] = []; +// const pointTriggers: PointTrigger[][] = []; // Added point triggers collection // const processSpeed = paths[0]?.speed || 1; // for (const path of paths) { // for (const point of path.points) { -// const obj = scene.getObjectByProperty("uuid", point.uuid); -// if (!obj) { -// console.warn(`Object with UUID ${point.uuid} not found in scene`); -// continue; -// } +// if (path.type === "Conveyor") { +// const obj = scene.getObjectByProperty("uuid", point.uuid); +// if (!obj) { +// console.warn(`Object with UUID ${point.uuid} not found in scene`); +// continue; +// } -// const position = obj.getWorldPosition(new THREE.Vector3()); -// animationPath.push(position.clone()); -// pointActions.push(point.actions); +// const position = obj.getWorldPosition(new THREE.Vector3()); +// animationPath.push(position.clone()); +// pointActions.push(point.actions); +// pointTriggers.push(point.triggers); // Collect triggers for each point +// } // } // } @@ -223,7 +281,9 @@ // paths, // animationPath, // pointActions, +// pointTriggers, // speed: processSpeed, +// isplaying: false, // }; // }, // [scene] @@ -326,21 +386,35 @@ // ); // }, [simulationStates]); +// // Enhanced dependency tracking that includes action and trigger types // const pathsDependency = useMemo(() => { // if (!convertedPaths) return null; // return convertedPaths.map((path) => ({ // id: path.modeluuid, -// hasSpawn: path.points.some((p: PathPoint) => -// p.actions.some((a: PointAction) => a.type.toLowerCase() === "spawn") -// ), +// // Track all action types for each point +// actionSignature: path.points +// .map((point, index) => +// point.actions.map((action) => `${index}-${action.type}`).join("|") +// ) +// .join(","), +// // Track all trigger types for each point +// triggerSignature: path.points +// .map((point, index) => +// point.triggers +// .map((trigger) => `${index}-${trigger.type}`) +// .join("|") +// ) +// .join(","), // connections: path.points // .flatMap((p: PathPoint) => // p.connections.targets.map((t: { modelUUID: string }) => t.modelUUID) // ) // .join(","), +// isplaying: false, // })); // }, [convertedPaths]); +// // Force process recreation when paths change // useEffect(() => { // if (!convertedPaths || convertedPaths.length === 0) { // if (prevProcessesRef.current.length > 0) { @@ -350,42 +424,16 @@ // return; // } -// if (areArraysEqual(prevPathsRef.current, convertedPaths)) { -// return; -// } - -// prevPathsRef.current = convertedPaths; +// // Always regenerate processes if the pathsDependency has changed +// // This ensures action and trigger type changes will be detected // const newProcesses = createProcessesFromPaths(convertedPaths); +// prevPathsRef.current = convertedPaths; -// // console.log("--- Action Types in Paths ---"); -// // convertedPaths.forEach((path) => { -// // path.points.forEach((point) => { -// // point.actions.forEach((action) => { -// // console.log( -// // `Path ${path.modeluuid}, Point ${point.uuid}: ${action.type}` -// // ); -// // }); -// // }); -// // }); -// // console.log("New processes:", newProcesses); - -// if ( -// newProcesses.length !== prevProcessesRef.current.length || -// !newProcesses.every( -// (proc, i) => -// proc.paths.length === prevProcessesRef.current[i]?.paths.length && -// proc.paths.every( -// (path, j) => -// path.modeluuid === -// prevProcessesRef.current[i]?.paths[j]?.modeluuid -// ) -// ) -// ) { -// onProcessesCreated(newProcesses); -// // prevProcessesRef.current = newProcesses; -// } +// // Always update processes when action or trigger types change +// onProcessesCreated(newProcesses); +// prevProcessesRef.current = newProcesses; // }, [ -// pathsDependency, +// pathsDependency, // This now includes action and trigger types // onProcessesCreated, // convertedPaths, // createProcessesFromPaths, @@ -423,10 +471,19 @@ export interface PointAction { isUsed: boolean; } +export interface PointTrigger { + uuid: string; + bufferTime: number; + name: string; + type: string; + isUsed: boolean; +} + export interface PathPoint { uuid: string; position: [number, number, number]; actions: PointAction[]; + triggers: PointTrigger[]; connections: { targets: Array<{ modelUUID: string }>; }; @@ -438,6 +495,7 @@ export interface SimulationPath { points: PathPoint[]; pathPosition: [number, number, number]; speed?: number; + isplaying: boolean; } export interface Process { @@ -445,7 +503,9 @@ export interface Process { paths: SimulationPath[]; animationPath: THREE.Vector3[]; pointActions: PointAction[][]; + pointTriggers: PointTrigger[][]; speed: number; + isplaying: boolean; } interface ProcessCreatorProps { @@ -458,11 +518,16 @@ function convertToSimulationPath( ): SimulationPath { const { modeluuid } = path; - // Simplified normalizeAction function that preserves exact original properties + // Normalized action handler const normalizeAction = (action: any): PointAction => { return { ...action }; // Return exact copy with no modifications }; + // Normalized trigger handler + const normalizeTrigger = (trigger: any): PointTrigger => { + return { ...trigger }; // Return exact copy with no modifications + }; + if (path.type === "Conveyor") { return { type: path.type, @@ -470,7 +535,16 @@ function convertToSimulationPath( points: path.points.map((point) => ({ uuid: point.uuid, position: point.position, - actions: point.actions.map(normalizeAction), // Preserve exact actions + actions: Array.isArray(point.actions) + ? point.actions.map(normalizeAction) + : point.actions + ? [normalizeAction(point.actions)] + : [], + triggers: Array.isArray(point.triggers) + ? point.triggers.map(normalizeTrigger) + : point.triggers + ? [normalizeTrigger(point.triggers)] + : [], connections: { targets: point.connections.targets.map((target) => ({ modelUUID: target.modelUUID, @@ -482,8 +556,10 @@ function convertToSimulationPath( typeof path.speed === "string" ? parseFloat(path.speed) || 1 : path.speed || 1, + isplaying: false, // Added missing property }; } else { + // For vehicle paths, handle the case where triggers might not exist return { type: path.type, modeluuid, @@ -493,7 +569,10 @@ function convertToSimulationPath( position: path.points.position, actions: Array.isArray(path.points.actions) ? path.points.actions.map(normalizeAction) - : [normalizeAction(path.points.actions)], + : path.points.actions + ? [normalizeAction(path.points.actions)] + : [], + triggers: [], connections: { targets: path.points.connections.targets.map((target) => ({ modelUUID: target.modelUUID, @@ -503,26 +582,20 @@ function convertToSimulationPath( ], pathPosition: path.position, speed: path.points.speed || 1, + isplaying: false, }; } } -// Custom shallow comparison for arrays -const areArraysEqual = (a: any[], b: any[]) => { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - return true; -}; - // Helper function to create an empty process const createEmptyProcess = (): Process => ({ id: `process-${Math.random().toString(36).substring(2, 11)}`, paths: [], animationPath: [], pointActions: [], + pointTriggers: [], // Added point triggers array speed: 1, + isplaying: false, }); // Enhanced connection checking function @@ -631,19 +704,23 @@ export function useProcessCreation() { const animationPath: THREE.Vector3[] = []; const pointActions: PointAction[][] = []; + const pointTriggers: PointTrigger[][] = []; // Added point triggers collection const processSpeed = paths[0]?.speed || 1; for (const path of paths) { for (const point of path.points) { - const obj = scene.getObjectByProperty("uuid", point.uuid); - if (!obj) { - console.warn(`Object with UUID ${point.uuid} not found in scene`); - continue; - } + if (path.type === "Conveyor") { + const obj = scene.getObjectByProperty("uuid", point.uuid); + if (!obj) { + console.warn(`Object with UUID ${point.uuid} not found in scene`); + continue; + } - const position = obj.getWorldPosition(new THREE.Vector3()); - animationPath.push(position.clone()); - pointActions.push(point.actions); + const position = obj.getWorldPosition(new THREE.Vector3()); + animationPath.push(position.clone()); + pointActions.push(point.actions); + pointTriggers.push(point.triggers); // Collect triggers for each point + } } } @@ -652,7 +729,9 @@ export function useProcessCreation() { paths, animationPath, pointActions, + pointTriggers, speed: processSpeed, + isplaying: false, }; }, [scene] @@ -755,7 +834,7 @@ const ProcessCreator: React.FC = React.memo( ); }, [simulationStates]); - // Enhanced dependency tracking that includes action types + // Enhanced dependency tracking that includes action and trigger types const pathsDependency = useMemo(() => { if (!convertedPaths) return null; return convertedPaths.map((path) => ({ @@ -766,11 +845,20 @@ const ProcessCreator: React.FC = React.memo( point.actions.map((action) => `${index}-${action.type}`).join("|") ) .join(","), + // Track all trigger types for each point + triggerSignature: path.points + .map((point, index) => + point.triggers + .map((trigger) => `${index}-${trigger.type}`) + .join("|") + ) + .join(","), connections: path.points .flatMap((p: PathPoint) => p.connections.targets.map((t: { modelUUID: string }) => t.modelUUID) ) .join(","), + isplaying: false, })); }, [convertedPaths]); @@ -785,15 +873,15 @@ const ProcessCreator: React.FC = React.memo( } // Always regenerate processes if the pathsDependency has changed - // This ensures action type changes will be detected + // This ensures action and trigger type changes will be detected const newProcesses = createProcessesFromPaths(convertedPaths); prevPathsRef.current = convertedPaths; - // Always update processes when action types change + // Always update processes when action or trigger types change onProcessesCreated(newProcesses); prevProcessesRef.current = newProcesses; }, [ - pathsDependency, // This now includes action types + pathsDependency, // This now includes action and trigger types onProcessesCreated, convertedPaths, createProcessesFromPaths, diff --git a/app/src/modules/simulation/process/processObject.tsx b/app/src/modules/simulation/process/processObject.tsx new file mode 100644 index 0000000..01ef41d --- /dev/null +++ b/app/src/modules/simulation/process/processObject.tsx @@ -0,0 +1,58 @@ +import React, { useMemo } from "react"; +import * as THREE from "three"; +import { GLTF } from "three-stdlib"; +import { SpawnedObject } from "./types"; + +interface ProcessObjectProps { + objectId: string; + obj: SpawnedObject; + renderAs?: "box" | "custom"; + gltf?: GLTF; +} + +const ProcessObject: React.FC = ({ + objectId, + obj, + renderAs = "custom", + gltf, +}) => { + const renderedObject = useMemo(() => { + if (renderAs === "box") { + return ( + } + material={obj.material} + position={obj.position} + > + + + ); + } + + if (gltf?.scene) { + const clonedScene = gltf.scene.clone(); + clonedScene.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.material = obj.material; + } + }); + + return ( + } + position={obj.position} + > + + + ); + } + + return null; + }, [objectId, obj, renderAs, gltf]); + + return renderedObject; +}; + +export default ProcessObject; diff --git a/app/src/modules/simulation/process/processObjectRender.tsx b/app/src/modules/simulation/process/processObjectRender.tsx deleted file mode 100644 index 7f5a0cc..0000000 --- a/app/src/modules/simulation/process/processObjectRender.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React, { useRef, useEffect } from "react"; -import { useFrame } from "@react-three/fiber"; -import * as THREE from "three"; -import { GLTF } from "three-stdlib"; -import { Box3Helper } from "three"; -import { SpawnedObject, ProcessData } from "./types"; - -interface ProcessObjectRendererProps { - objectId: string; - object: SpawnedObject; - process: ProcessData; - gltf: GLTF; - showBoundingBox?: boolean; -} - -export const ProcessObjectRenderer: React.FC = ({ - objectId, - object, - process, - gltf, - showBoundingBox = false, -}) => { - const meshRef = useRef(null); - const boxHelperRef = useRef(null); - const boundingBoxRef = useRef(new THREE.Box3()); - - // Issue 1: Can't assign to ref.current as it's read-only - useEffect(() => { - if (object.ref && meshRef.current) { - // Instead of direct assignment, we need to store the mesh reference another way - // Option 1: If you can modify the SpawnedObject interface, add a setMesh method - if (typeof (object as any).setMesh === 'function') { - (object as any).setMesh(meshRef.current); - } - - // Option 2: Store the mesh in a property that isn't ref.current - // This requires modifying your SpawnedObject interface to include this property - (object as any).meshInstance = meshRef.current; - - // Option 3: If you need to maintain compatibility, you could use Object.defineProperty - // But this is a hack and not recommended - // Object.defineProperty(object.ref, 'current', { value: meshRef.current, writable: true }); - } - }, [object.ref]); - - // Create a bounding box helper for visualization - useFrame(() => { - if (meshRef.current && showBoundingBox) { - // Update the bounding box to match the mesh position - if (!boxHelperRef.current) { - // Get the size of the mesh - const size = new THREE.Vector3(1, 1, 1); - - // If the mesh has geometry, use its dimensions - if (meshRef.current.geometry) { - const box = new THREE.Box3().setFromObject(meshRef.current); - box.getSize(size); - } - - // Create a new bounding box centered on the mesh - boundingBoxRef.current = new THREE.Box3().setFromCenterAndSize( - meshRef.current.position, - size - ); - - // Create a helper to visualize the box - boxHelperRef.current = new Box3Helper( - boundingBoxRef.current, - new THREE.Color(0xff0000) - ); - - // Add the helper to the scene - meshRef.current.parent?.add(boxHelperRef.current); - } else { - // Update the box position to match the mesh - boundingBoxRef.current.setFromCenterAndSize( - meshRef.current.position, - boundingBoxRef.current.getSize(new THREE.Vector3()) - ); - - // Force the helper to update - boxHelperRef.current.updateMatrixWorld(true); - } - } - }); - - if (gltf?.scene) { - return ( - - ); - } - - // Issue 2: Material color type problem - return ( - - - - - ); -}; \ No newline at end of file diff --git a/app/src/modules/simulation/process/types.ts b/app/src/modules/simulation/process/types.ts index 64a3e6a..086b620 100644 --- a/app/src/modules/simulation/process/types.ts +++ b/app/src/modules/simulation/process/types.ts @@ -1,79 +1,86 @@ import * as THREE from "three"; -import React from "react"; -export interface ProcessPoint { +export interface Trigger { uuid: string; - position: number[]; - actions?: PointAction[]; + name: string; + type: string; + bufferTime: number; + isUsed: boolean; } export interface PointAction { - type: string; + uuid: string; + name: string; + type: "Inherit" | "Spawn" | "Despawn" | "Delay" | "Swap"; + objectType: string; + material: string; + delay: string | number; + spawnInterval: string | number; isUsed: boolean; - spawnInterval?: number | string; - material?: string; - delay?: number | string; + hitCount?: number; +} + +export interface ProcessPoint { + uuid: string; + position: number[]; + rotation: number[]; + actions: PointAction[]; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + triggers?: Trigger[]; +} +export interface ProcessPath { + modeluuid: string; + modelName: string; + points: ProcessPoint[]; + pathPosition: number[]; + pathRotation: number[]; + speed: number; + type: "Conveyor" | "Vehicle"; + isplaying: boolean } export interface ProcessData { id: string; - name: string; - paths?: { - points?: ProcessPoint[]; - }[]; - animationPath?: { x: number; y: number; z: number }[]; - speed?: number; + paths: ProcessPath[]; + animationPath: { x: number; y: number; z: number }[]; + pointActions: PointAction[][]; + speed: number; customMaterials?: Record; + renderAs?: "box" | "custom"; + pointTriggers: []; +} + +export interface AnimationState { + currentIndex: number; + progress: number; + isAnimating: boolean; + speed: number; + isDelaying: boolean; + delayStartTime: number; + currentDelayDuration: number; + delayComplete: boolean; + currentPathIndex: number; +} + +export interface SpawnedObject { + ref: React.RefObject; + state: AnimationState; + visible: boolean; + material: THREE.Material; + spawnTime: number; + currentMaterialType: string; + position: THREE.Vector3; } export interface ProcessAnimationState { - spawnedObjects: Record; + spawnedObjects: { [objectId: string]: SpawnedObject }; nextSpawnTime: number; objectIdCounter: number; isProcessDelaying: boolean; processDelayStartTime: number; processDelayDuration: number; - isCollisionPaused?: boolean; + hasSpawnedZeroIntervalObject?: boolean; } - -export interface SpawnedObject { - ref: React.RefObject; - state: { - currentIndex: number; - progress: number; - isAnimating: boolean; - speed: number; - isDelaying: boolean; - delayStartTime: number; - currentDelayDuration: number; - delayComplete: boolean; - currentPathIndex: number; - }; - visible: boolean; - material: THREE.Material; - currentMaterialType: string; - spawnTime: number; - position: THREE.Vector3; - collision?: { - boundingBox: THREE.Box3; - isColliding: boolean; - colliding: boolean; // Added this property - }; -} - -// For use in your processAnimator.tsx -// Update the CollisionState interface to include all required properties -interface CollisionState { - boundingBox: THREE.Box3; - isColliding: boolean; - colliding: boolean; // This was missing - collidingWith: string[]; - } -export interface SpawnedObjectWithCollision extends SpawnedObject { - collision: { - boundingBox: THREE.Box3; - isColliding: boolean; - colliding: boolean; - collidingWith: string[]; - }; - } diff --git a/app/src/modules/simulation/process/useProcessAnimations.tsx b/app/src/modules/simulation/process/useProcessAnimations.tsx new file mode 100644 index 0000000..7525c29 --- /dev/null +++ b/app/src/modules/simulation/process/useProcessAnimations.tsx @@ -0,0 +1,540 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import * as THREE from "three"; +import { + ProcessData, + ProcessAnimationState, + SpawnedObject, + AnimationState, + ProcessPoint, + PointAction, + Trigger, +} from "./types"; +import { + useAnimationPlaySpeed, + usePauseButtonStore, + usePlayButtonStore, + useResetButtonStore, +} from "../../../store/usePlayButtonStore"; +import { usePlayAgv } from "../../../store/store"; + +// Enhanced ProcessAnimationState with trigger tracking +interface EnhancedProcessAnimationState extends ProcessAnimationState { + triggerCounts: Record; + triggerLogs: Array<{ + timestamp: number; + pointId: string; + objectId: string; + triggerId: string; + }>; +} + +interface ProcessContainerProps { + processes: ProcessData[]; + setProcesses: React.Dispatch>; + agvRef: any; +} + +interface PlayAgvState { + playAgv: Record; + setPlayAgv: (data: any) => void; +} + +export const useProcessAnimation = ( + processes: ProcessData[], + setProcesses: React.Dispatch>, + agvRef: any +) => { + // State and refs initialization + const { isPlaying, setIsPlaying } = usePlayButtonStore(); + const { isPaused, setIsPaused } = usePauseButtonStore(); + const { isReset, setReset } = useResetButtonStore(); + const debugRef = useRef(false); + const clockRef = useRef(new THREE.Clock()); + const pauseTimeRef = useRef(0); + const elapsedBeforePauseRef = useRef(0); + const animationStatesRef = useRef< + Record + >({}); + const { speed } = useAnimationPlaySpeed(); + const prevIsPlaying = useRef(null); + const [internalResetFlag, setInternalResetFlag] = useState(false); + const [animationStates, setAnimationStates] = useState< + Record + >({}); + const speedRef = useRef(speed); + const { PlayAgv, setPlayAgv } = usePlayAgv(); + + // Effect hooks + useEffect(() => { + speedRef.current = speed; + }, [speed]); + + useEffect(() => { + if (prevIsPlaying.current !== null) { + setAnimationStates({}); + } + prevIsPlaying.current = isPlaying; + }, [isPlaying]); + + useEffect(() => { + animationStatesRef.current = animationStates; + }, [animationStates]); + + // Reset handler + useEffect(() => { + if (isReset) { + setInternalResetFlag(true); + setIsPlaying(false); + setIsPaused(false); + setAnimationStates({}); + animationStatesRef.current = {}; + clockRef.current = new THREE.Clock(); + elapsedBeforePauseRef.current = 0; + pauseTimeRef.current = 0; + setReset(false); + setTimeout(() => { + setInternalResetFlag(false); + setIsPlaying(true); + }, 0); + } + }, [isReset, setReset, setIsPlaying, setIsPaused]); + + // Pause handler + useEffect(() => { + if (isPaused) { + pauseTimeRef.current = clockRef.current.getElapsedTime(); + } else if (pauseTimeRef.current > 0) { + const pausedDuration = + clockRef.current.getElapsedTime() - pauseTimeRef.current; + elapsedBeforePauseRef.current += pausedDuration; + } + }, [isPaused]); + + // Initialize animation states with trigger tracking + useEffect(() => { + if (isPlaying && !internalResetFlag) { + const newStates: Record = {}; + + processes.forEach((process) => { + const triggerCounts: Record = {}; + + // Initialize trigger counts for all On-Hit triggers + process.paths?.forEach((path) => { + path.points?.forEach((point) => { + point.triggers?.forEach((trigger: Trigger) => { + if (trigger.type === "On-Hit" && trigger.isUsed) { + triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; + } + }); + }); + }); + + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + isProcessDelaying: false, + processDelayStartTime: 0, + processDelayDuration: 0, + triggerCounts, + triggerLogs: [], + }; + }); + + setAnimationStates(newStates); + animationStatesRef.current = newStates; + clockRef.current.start(); + } + }, [isPlaying, processes, internalResetFlag]); + + // Helper functions + const findSpawnPoint = (process: ProcessData): ProcessPoint | null => { + for (const path of process.paths || []) { + for (const point of path.points || []) { + const spawnAction = point.actions?.find( + (a) => a.isUsed && a.type === "Spawn" + ); + if (spawnAction) { + return point; + } + } + } + return null; + }; + + const findAnimationPathPoint = ( + process: ProcessData, + spawnPoint: ProcessPoint + ): THREE.Vector3 => { + if (process.animationPath && process.animationPath.length > 0) { + let pointIndex = 0; + for (const path of process.paths || []) { + for (let i = 0; i < (path.points?.length || 0); i++) { + const point = path.points?.[i]; + if (point && point.uuid === spawnPoint.uuid) { + if (process.animationPath[pointIndex]) { + const p = process.animationPath[pointIndex]; + return new THREE.Vector3(p.x, p.y, p.z); + } + } + pointIndex++; + } + } + } + return new THREE.Vector3( + spawnPoint.position[0], + spawnPoint.position[1], + spawnPoint.position[2] + ); + }; + + // Optimized object creation + const createSpawnedObject = useCallback( + ( + process: ProcessData, + currentTime: number, + materialType: string, + spawnPoint: ProcessPoint, + baseMaterials: Record + ): SpawnedObject => { + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; + + const spawnPosition = findAnimationPathPoint(process, spawnPoint); + const material = + processMaterials[materialType as keyof typeof processMaterials] || + baseMaterials.Default; + + return { + ref: { current: null }, + state: { + currentIndex: 0, + progress: 0, + isAnimating: true, + speed: process.speed || 1, + isDelaying: false, + delayStartTime: 0, + currentDelayDuration: 0, + delayComplete: false, + currentPathIndex: 0, + }, + visible: true, + material: material, + currentMaterialType: materialType, + spawnTime: currentTime, + position: spawnPosition, + }; + }, + [] + ); + + // Material handling + const handleMaterialSwap = useCallback( + ( + processId: string, + objectId: string, + materialType: string, + processes: ProcessData[], + baseMaterials: Record + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState || !processState.spawnedObjects[objectId]) + return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; + + const newMaterial = + processMaterials[materialType as keyof typeof processMaterials]; + if (!newMaterial) return prev; + + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + material: newMaterial, + currentMaterialType: materialType, + }, + }, + }, + }; + }); + }, + [] + ); + + // Point action handler with trigger counting + const handlePointActions = useCallback( + ( + processId: string, + objectId: string, + actions: PointAction[] = [], + currentTime: number, + processes: ProcessData[], + baseMaterials: Record + ): boolean => { + let shouldStopAnimation = false; + + actions.forEach((action) => { + if (!action.isUsed) return; + + switch (action.type) { + case "Delay": + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState || processState.isProcessDelaying) return prev; + + const delayDuration = + typeof action.delay === "number" + ? action.delay + : parseFloat(action.delay as string) || 0; + + if (delayDuration > 0) { + return { + ...prev, + [processId]: { + ...processState, + isProcessDelaying: true, + processDelayStartTime: currentTime, + processDelayDuration: delayDuration, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + state: { + ...processState.spawnedObjects[objectId].state, + isAnimating: false, + isDelaying: true, + delayStartTime: currentTime, + currentDelayDuration: delayDuration, + delayComplete: false, + }, + }, + }, + }, + }; + } + return prev; + }); + shouldStopAnimation = true; + break; + + case "Despawn": + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState) return prev; + + const newSpawnedObjects = { ...processState.spawnedObjects }; + delete newSpawnedObjects[objectId]; + + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: newSpawnedObjects, + }, + }; + }); + shouldStopAnimation = true; + break; + + case "Swap": + if (action.material) { + handleMaterialSwap( + processId, + objectId, + action.material, + processes, + baseMaterials + ); + } + break; + + default: + break; + } + }); + + return shouldStopAnimation; + }, + [handleMaterialSwap] + ); + + // Trigger counting system + const checkAndCountTriggers = useCallback( + ( + processId: string, + objectId: string, + currentPointIndex: number, + processes: ProcessData[], + currentTime: number + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState) return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const point = getPointDataForAnimationIndex(process, currentPointIndex); + if (!point?.triggers) return prev; + + const onHitTriggers = point.triggers.filter( + (t: Trigger) => t.type === "On-Hit" && t.isUsed + ); + if (onHitTriggers.length === 0) return prev; + + const newTriggerCounts = { ...processState.triggerCounts }; + const newTriggerLogs = [...processState.triggerLogs]; + let shouldLog = false; + + // Update counts for all valid triggers + onHitTriggers.forEach((trigger: Trigger) => { + const triggerKey = `${point.uuid}-${trigger.uuid}`; + newTriggerCounts[triggerKey] = + (newTriggerCounts[triggerKey] || 0) + 1; + shouldLog = true; + newTriggerLogs.push({ + timestamp: currentTime, + pointId: point.uuid, + objectId, + triggerId: trigger.uuid, + }); + }); + + const processTotalHits = Object.values(newTriggerCounts).reduce( + (a, b) => a + b, + 0 + ); + + if (shouldLog) { + const vehiclePaths = process.paths.filter( + (path) => path.type === "Vehicle" + ); + + vehiclePaths.forEach((vehiclePath) => { + if (vehiclePath.points?.length > 0) { + const vehiclePoint = vehiclePath.points[0]; + const action = vehiclePoint.actions?.[0]; + let expectedHitCount = action?.hitCount; + + if (expectedHitCount !== undefined) { + const vehicleId = vehiclePath.modeluuid; + + let vehicleEntry = agvRef.current.find( + (v: any) => + v.vehicleId === vehicleId && v.processId === processId + ); + + if (!vehicleEntry) { + vehicleEntry = { + processId, + vehicleId, + expectedHitCount, + isplaying: false, + hitCount: 0, // Initialize hitCount + }; + agvRef.current.push(vehicleEntry); + } + + // Increment expectedHitCount if not playing + if (!vehicleEntry.isplaying) { + vehicleEntry.expectedHitCount = processTotalHits; + } + + // Set vehicle's hitCount to the processTotalHits + vehicleEntry.hitCount = processTotalHits; + vehicleEntry.lastUpdated = currentTime; + + // If hitCount matches expectedHitCount, set isplaying to true + if (vehicleEntry.hitCount >= vehicleEntry.expectedHitCount) { + vehicleEntry.isplaying = true; + } + } + } + }); + } + + return { + ...prev, + [processId]: { + ...processState, + triggerCounts: newTriggerCounts, + triggerLogs: newTriggerLogs, + totalHits: processTotalHits, + }, + }; + }); + }, + [] + ); + + // Utility functions + const hasNonInheritActions = useCallback( + (actions: PointAction[] = []): boolean => { + return actions.some( + (action) => action.isUsed && action.type !== "Inherit" + ); + }, + [] + ); + + const getPointDataForAnimationIndex = useCallback( + (process: ProcessData, index: number): ProcessPoint | null => { + if (!process.paths) return null; + + let cumulativePoints = 0; + for (const path of process.paths) { + const pointCount = path.points?.length || 0; + + if (index < cumulativePoints + pointCount) { + const pointIndex = index - cumulativePoints; + return path.points?.[pointIndex] || null; + } + + cumulativePoints += pointCount; + } + + return null; + }, + [] + ); + + const getTriggerCounts = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerCounts || {}; + }, []); + + const getTriggerLogs = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerLogs || []; + }, []); + + return { + animationStates, + setAnimationStates, + clockRef, + elapsedBeforePauseRef, + speedRef, + debugRef, + findSpawnPoint, + createSpawnedObject, + handlePointActions, + hasNonInheritActions, + getPointDataForAnimationIndex, + checkAndCountTriggers, + getTriggerCounts, + getTriggerLogs, + processes, + }; +}; diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx index 98fd3dd..5bcf7d1 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -16,23 +16,9 @@ function Simulation() { const { activeModule } = useModuleStore(); const pathsGroupRef = useRef() as React.MutableRefObject; const { simulationStates, setSimulationStates } = useSimulationStates(); - const [processes, setProcesses] = useState([]); - - useEffect(() => { - // console.log('simulationStates: ', simulationStates); - }, [simulationStates]); - - // useEffect(() => { - // if (selectedActionSphere) { - // console.log('selectedActionSphere: ', selectedActionSphere); - // } - // }, [selectedActionSphere]); - - // useEffect(() => { - // if (selectedPath) { - // console.log('selectedPath: ', selectedPath); - // } - // }, [selectedPath]); + const [processes, setProcesses] = useState([]); + const agvRef = useRef([]); + const MaterialRef = useRef([]); return ( <> @@ -41,8 +27,17 @@ function Simulation() { <> - - + + )} diff --git a/app/src/store/store.ts b/app/src/store/store.ts index 8810dbe..3210afc 100644 --- a/app/src/store/store.ts +++ b/app/src/store/store.ts @@ -70,9 +70,8 @@ export const useZones = create((set: any) => ({ zones: [], setZones: (callback: any) => set((state: any) => ({ - zones: - typeof callback === 'function' ? callback(state.zones) : callback - })) + zones: typeof callback === "function" ? callback(state.zones) : callback, + })), })); interface ZonePointsState { @@ -314,9 +313,7 @@ export const useActiveUsers = create((set: any) => ({ setActiveUsers: (callback: (prev: any[]) => any[] | any[]) => set((state: { activeUsers: any[] }) => ({ activeUsers: - typeof callback === "function" - ? callback(state.activeUsers) - : callback, + typeof callback === "function" ? callback(state.activeUsers) : callback, })), })); @@ -356,12 +353,33 @@ export const useSelectedPath = create((set: any) => ({ })); interface SimulationPathsStore { - simulationStates: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[]; + simulationStates: ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + | Types.ArmBotEventsSchema + )[]; setSimulationStates: ( paths: - | (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[] - | ((prev: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[] - ) => (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[]) + | ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + | Types.ArmBotEventsSchema + )[] + | (( + prev: ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + | Types.ArmBotEventsSchema + )[] + ) => ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + | Types.ArmBotEventsSchema + )[]) ) => void; } @@ -372,7 +390,7 @@ export const useSimulationStates = create((set) => ({ simulationStates: typeof paths === "function" ? paths(state.simulationStates) : paths, })), -})) +})); export const useNavMesh = create((set: any) => ({ navMesh: null, @@ -456,6 +474,11 @@ export const useTileDistance = create((set: any) => ({ })), })); +export const usePlayAgv = create((set, get) => ({ + PlayAgv: [], + setPlayAgv: (updateFn: (prev: any[]) => any[]) => + set({ PlayAgv: updateFn(get().PlayAgv) }), +})); // Define the Asset type type Asset = { id: string; @@ -474,4 +497,4 @@ type ZoneAssetState = { export const useZoneAssetId = create((set) => ({ zoneAssetId: null, setZoneAssetId: (asset) => set({ zoneAssetId: asset }), -})); \ No newline at end of file +})); diff --git a/app/src/types/world/worldTypes.d.ts b/app/src/types/world/worldTypes.d.ts index fefca2a..3197198 100644 --- a/app/src/types/world/worldTypes.d.ts +++ b/app/src/types/world/worldTypes.d.ts @@ -1,13 +1,13 @@ // Importing core classes and types from THREE.js and @react-three/fiber import * as THREE from "three"; -import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; -import { DragControls } from 'three/examples/jsm/controls/DragControls'; -import { IntersectionEvent } from '@react-three/fiber/dist/declarations/src/core/events'; +import { TransformControls } from "three/examples/jsm/controls/TransformControls"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; +import { DragControls } from "three/examples/jsm/controls/DragControls"; +import { IntersectionEvent } from "@react-three/fiber/dist/declarations/src/core/events"; import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events"; import { RootState } from "@react-three/fiber"; import { CSM } from "three/examples/jsm/csm/CSM"; -import { CSMHelper } from 'three/examples/jsm/csm/CSMHelper'; +import { CSMHelper } from "three/examples/jsm/csm/CSMHelper"; import { CameraControls } from "@react-three/drei"; /** Core THREE.js and React-Fiber Event Types **/ @@ -15,7 +15,6 @@ import { CameraControls } from "@react-three/drei"; // Event type specific to pointer events in @react-three/fiber export type ThreeEvent = ThreeEvent; - /** Vector and Reference Types **/ // 2D Vector type from THREE.js @@ -33,7 +32,6 @@ export type RefVector3 = React.MutableRefObject; // Quaternion type for rotations, using the base structure from THREE.js export type QuaternionType = THREE.QuaternionLike; - /** Basic Object Types for Scene Management **/ // THREE.js mesh object @@ -49,7 +47,9 @@ export type Shape = THREE.Shape; export type IntersectionEvent = THREE.Intersection; // Array type for intersections with objects in the scene -export type IntersectsType = THREE.Intersection>[]; +export type IntersectsType = THREE.Intersection< + THREE.Object3D +>[]; // Event type for mesh interactions export type MeshEvent = IntersectionEvent; @@ -60,7 +60,6 @@ export type DragEvent = DragEvent; // Generic type for user data attached to objects export type UserData = any; - /** React Mutable References for Scene Objects **/ // Mutable reference to the scene, used in React-based projects @@ -92,7 +91,6 @@ export type Vector3Array = THREE.Vector3[]; export type DragControl = DragControls | null; export type RefDragControl = React.MutableRefObject; - /** Primitive Types with Mutable References **/ export type String = string; @@ -109,15 +107,15 @@ export type NumberArray = number[]; export type RefRaycaster = React.MutableRefObject; // Camera reference, supporting both perspective and basic cameras -export type RefCamera = React.MutableRefObject; - +export type RefCamera = React.MutableRefObject< + THREE.Camera | THREE.PerspectiveCamera +>; /** Three.js Root State Management **/ // Root state of the @react-three/fiber instance, providing context of the scene export type ThreeState = RootState; - /** Point and Line Types for Spatial Geometry **/ // Defines a point in 3D space with metadata for unique identification @@ -132,11 +130,16 @@ export type RefLine = React.MutableRefObject; // Collection of lines for structured geometry export type Lines = Array; - /** Wall and Room Types for 3D Space Management **/ // Defines a wall with its geometry, position, rotation, material, and layer information -export type Wall = [THREE.ExtrudeGeometry, [number, number, number], [number, number, number], string, number]; +export type Wall = [ + THREE.ExtrudeGeometry, + [number, number, number], + [number, number, number], + string, + number +]; // Collection of walls, useful in scene construction export type Walls = Array; @@ -145,15 +148,22 @@ export type Walls = Array; export type RefWalls = React.MutableRefObject; // Room type, containing coordinates and layer metadata for spatial management -export type Rooms = Array<{ coordinates: Array<{ position: THREE.Vector3, uuid: string }>, layer: number }>; +export type Rooms = Array<{ + coordinates: Array<{ position: THREE.Vector3; uuid: string }>; + layer: number; +}>; // Reference for room objects, enabling updates within React components -export type RefRooms = React.MutableRefObject, layer: number }>>; +export type RefRooms = React.MutableRefObject< + Array<{ + coordinates: Array<{ position: THREE.Vector3; uuid: string }>; + layer: number; + }> +>; // Reference for lines, supporting React-based state changes export type RefLines = React.MutableRefObject; - /** Floor Line Types for Layered Structures **/ // Floor line type for single lines on the floor level @@ -168,18 +178,16 @@ export type OnlyFloorLines = Array; // Reference for multi-level floor lines, allowing structured updates export type RefOnlyFloorLines = React.MutableRefObject; - /** GeoJSON Line Integration **/ // Structure for representing GeoJSON lines, integrating external data sources export type GeoJsonLine = { - line: any; - uuids: [string, string]; - layer: number; - type: string; + line: any; + uuids: [string, string]; + layer: number; + type: string; }; - /** State Management Types for React Components **/ // Dispatch types for number and boolean states, commonly used in React hooks @@ -189,68 +197,71 @@ export type BooleanState = React.Dispatch>; // Mutable reference for TubeGeometry, allowing dynamic geometry updates export type RefTubeGeometry = React.MutableRefObject; - /** Floor Item Configuration **/ // Type for individual items placed on the floor, with positioning and rotation metadata export type FloorItemType = { - modeluuid: string; - modelname: string; - position: [number, number, number]; - rotation: { x: number; y: number; z: number }; - modelfileID: string; - isLocked: boolean; - isVisible: boolean; + modeluuid: string; + modelname: string; + position: [number, number, number]; + rotation: { x: number; y: number; z: number }; + modelfileID: string; + isLocked: boolean; + isVisible: boolean; }; // Array of floor items for managing multiple objects on the floor export type FloorItems = Array; // Dispatch type for setting floor item state in React -export type setFloorItemSetState = React.Dispatch>; - +export type setFloorItemSetState = React.Dispatch< + React.SetStateAction +>; /** Asset Configuration for Loading and Positioning **/ // Configuration for assets, allowing model URLs, scaling, positioning, and types interface AssetConfiguration { - modelUrl: string; - scale?: [number, number, number]; - csgscale?: [number, number, number]; - csgposition?: [number, number, number]; - positionY?: (intersectionPoint: { point: THREE.Vector3 }) => number; - type?: "Fixed-Move" | "Free-Move"; + modelUrl: string; + scale?: [number, number, number]; + csgscale?: [number, number, number]; + csgposition?: [number, number, number]; + positionY?: (intersectionPoint: { point: THREE.Vector3 }) => number; + type?: "Fixed-Move" | "Free-Move"; } // Collection of asset configurations, keyed by unique identifiers export type AssetConfigurations = { - [key: string]: AssetConfiguration; + [key: string]: AssetConfiguration; }; - /** Wall Item Configuration **/ // Configuration for wall items, including model, scale, position, and rotation interface WallItem { - type: "Fixed-Move" | "Free-Move" | undefined; - model?: THREE.Group; - modeluuid?: string - modelname?: string; - scale?: [number, number, number]; - csgscale?: [number, number, number]; - csgposition?: [number, number, number]; - position?: [number, number, number]; - quaternion?: Types.QuaternionType; + type: "Fixed-Move" | "Free-Move" | undefined; + model?: THREE.Group; + modeluuid?: string; + modelname?: string; + scale?: [number, number, number]; + csgscale?: [number, number, number]; + csgposition?: [number, number, number]; + position?: [number, number, number]; + quaternion?: Types.QuaternionType; } // Collection of wall items, allowing for multiple items in a scene export type wallItems = Array; // Dispatch for setting wall item state in React -export type setWallItemSetState = React.Dispatch>; +export type setWallItemSetState = React.Dispatch< + React.SetStateAction +>; // Dispatch for setting vector3 state in React -export type setVector3State = React.Dispatch>; +export type setVector3State = React.Dispatch< + React.SetStateAction +>; // Dispatch for setting euler state in React export type setEulerState = React.Dispatch>; @@ -258,147 +269,250 @@ export type setEulerState = React.Dispatch>; // Reference type for wall items, allowing direct access to the mutable array export type RefWallItems = React.MutableRefObject; - /** Wall and Item Selection State Management **/ // State management for selecting, removing, and indexing wall items export type setRemoveLayerSetState = (layer: number | null) => void; export type setSelectedWallItemSetState = (item: THREE.Object3D | null) => void; -export type setSelectedFloorItemSetState = (item: THREE.Object3D | null) => void; +export type setSelectedFloorItemSetState = ( + item: THREE.Object3D | null +) => void; export type setSelectedItemsIndexSetState = (index: number | null) => void; export type RefCSM = React.MutableRefObject; export type RefCSMHelper = React.MutableRefObject; interface PathConnection { - fromModelUUID: string; - fromUUID: string; - toConnections: { - toModelUUID: string; - toUUID: string; - }[]; + fromModelUUID: string; + fromUUID: string; + toConnections: { + toModelUUID: string; + toUUID: string; + }[]; } interface ConnectionStore { - connections: PathConnection[]; - setConnections: (connections: PathConnection[]) => void; - addConnection: (newConnection: PathConnection) => void; - removeConnection: (fromUUID: string, toUUID: string) => void; + connections: PathConnection[]; + setConnections: (connections: PathConnection[]) => void; + addConnection: (newConnection: PathConnection) => void; + removeConnection: (fromUUID: string, toUUID: string) => void; } interface ConveyorEventsSchema { - modeluuid: string; - modelName: string; - type: 'Conveyor'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; type: string; material: string; delay: number | string; spawnInterval: number | string; isUsed: boolean }[] | []; - triggers: { uuid: string; name: string; type: string; isUsed: boolean; bufferTime: number }[] | []; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }[]; + modeluuid: string; + modelName: string; + type: "Conveyor"; + points: { + uuid: string; position: [number, number, number]; rotation: [number, number, number]; - speed: number | string; + actions: + | { + uuid: string; + name: string; + type: string; + material: string; + delay: number | string; + spawnInterval: number | string; + isUsed: boolean; + }[] + | []; + triggers: + | { + uuid: string; + name: string; + type: string; + isUsed: boolean; + bufferTime: number; + }[] + | []; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + }[]; + position: [number, number, number]; + rotation: [number, number, number]; + speed: number | string; } interface VehicleEventsSchema { - modeluuid: string; - modelName: string; - type: 'Vehicle'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; type: string; start: { x: number, y: number } | {}, hitCount: number, end: { x: number, y: number } | {}, buffer: number }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - speed: number; - }; + modeluuid: string; + modelName: string; + type: "Vehicle"; + points: { + uuid: string; position: [number, number, number]; - rotation: [number, number, number]; + actions: { + uuid: string; + name: string; + type: string; + start: { x: number; y: number } | {}; + hitCount: number; + end: { x: number; y: number } | {}; + buffer: number; + }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + speed: number; + isPlaying: boolean; + }; + + position: [number, number, number]; } interface StaticMachineEventsSchema { - modeluuid: string; - modelName: string; - type: 'StaticMachine'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; buffer: number; material: string; }; - triggers: { uuid: string; name: string; type: string }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }; + modeluuid: string; + modelName: string; + type: "StaticMachine"; + points: { + uuid: string; position: [number, number, number]; - rotation: [number, number, number]; + actions: { + uuid: string; + name: string; + buffer: number | string; + material: string; + isUsed: boolean; + }; + triggers: { uuid: string; name: string; type: string }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + }; + position: [number, number, number]; } interface ArmBotEventsSchema { - modeluuid: string; - modelName: string; - type: 'ArmBot'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[] }; - triggers: { uuid: string; name: string; type: string }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }; + modeluuid: string; + modelName: string; + type: "ArmBot"; + points: { + uuid: string; position: [number, number, number]; - rotation: [number, number, number]; + actions: { + uuid: string; + name: string; + speed: number; + processes: { triggerId: string; startPoint: string; endPoint: string }[]; + }; + triggers: { uuid: string; name: string; type: string }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + }; + position: [number, number, number]; } export type EventData = { - modeluuid: string; - modelname: string; - position: [number, number, number]; - rotation: { x: number; y: number; z: number }; - modelfileID: string; - isLocked: boolean; - isVisible: boolean; - eventData?: { - type: 'Conveyor'; + modeluuid: string; + modelname: string; + position: [number, number, number]; + rotation: { x: number; y: number; z: number }; + modelfileID: string; + isLocked: boolean; + isVisible: boolean; + eventData?: + | { + type: "Conveyor"; points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; type: string; material: string; delay: number | string; spawnInterval: number | string; isUsed: boolean }[] | []; - triggers: { uuid: string; name: string; type: string; isUsed: boolean; bufferTime: number }[] | []; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + actions: + | { + uuid: string; + name: string; + type: string; + material: string; + delay: number | string; + spawnInterval: number | string; + isUsed: boolean; + }[] + | []; + triggers: + | { + uuid: string; + name: string; + type: string; + isUsed: boolean; + bufferTime: number; + }[] + | []; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; }[]; speed: number | string; - } | { - type: 'Vehicle'; + } + | { + type: "Vehicle"; points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + actions: { uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; type: string; start: { x: number, y: number } | {}, hitCount: number, end: { x: number, y: number } | {}, buffer: number }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; + name: string; + type: string; + start: { x: number; y: number } | {}; + hitCount: number; + end: { x: number; y: number } | {}; + buffer: number; + }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + speed: number; + }; + } + | { + type: "StaticMachine"; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + actions: { + uuid: string; + name: string; + buffer: number; + material: string; + }; + triggers: { uuid: string; name: string; type: string }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + }; + } + | { + type: "ArmBot"; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + actions: { + uuid: string; + name: string; speed: number; + processes: { + triggerId: string; + startPoint: string; + endPoint: string; + }[]; + }; + triggers: { uuid: string; name: string; type: string }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; }; - } | { - type: 'StaticMachine'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; buffer: number; material: string; }; - triggers: { uuid: string; name: string; type: string }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }; - } | { - type: 'ArmBot'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[] }; - triggers: { uuid: string; name: string; type: string }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }; - }; -} + }; +}; + diff --git a/app/src/utils/shortcutkeys/handleShortcutKeys.ts b/app/src/utils/shortcutkeys/handleShortcutKeys.ts index e54b792..43ca553 100644 --- a/app/src/utils/shortcutkeys/handleShortcutKeys.ts +++ b/app/src/utils/shortcutkeys/handleShortcutKeys.ts @@ -1,198 +1,198 @@ -// Importing React and useEffect from React library -import React, { useEffect } from "react"; - -// Importing the necessary hooks and types from the application's state management stores -import useModuleStore, { useThreeDStore } from "../../store/useModuleStore"; -import useToggleStore from "../../store/useUIToggleStore"; -import { - useActiveSubTool, - useActiveTool, - useAddAction, - useDeleteTool, - useSelectedWallItem, - useToggleView, - useToolMode, -} from "../../store/store"; -import { usePlayButtonStore } from "../../store/usePlayButtonStore"; - -// Utility function to detect modifier keys (Ctrl, Alt, Shift) and key combinations -import { detectModifierKeys } from "./detectModifierKeys"; - -// KeyPressListener component to handle global keyboard shortcuts -const KeyPressListener: React.FC = () => { - // Accessing state and actions from different stores - const { activeModule, setActiveModule } = useModuleStore(); // Module management (e.g., builder, simulation, visualization) - const { setActiveSubTool } = useActiveSubTool(); // Sub-tool management - const { toggleUI, setToggleUI } = useToggleStore(); // UI visibility toggle - const { setToggleThreeD } = useThreeDStore(); // 3D view toggle - const { setToolMode } = useToolMode(); // Tool mode management - const { setIsPlaying } = usePlayButtonStore(); // Play button state management - - // Wall and tool-related actions - const { toggleView, setToggleView } = useToggleView(); // 2D/3D toggle state - const { setDeleteTool } = useDeleteTool(); // Delete tool action - const { setAddAction } = useAddAction(); // Add action management - const { setSelectedWallItem } = useSelectedWallItem(); // Selected wall item management - const { setActiveTool } = useActiveTool(); // Active tool management - - // useEffect to manage global keyboard shortcuts - useEffect(() => { - // Function to handle keydown events - const handleKeyPress = (event: KeyboardEvent) => { - // Identify the currently focused element - const activeElement = document.activeElement; - - // Check if the user is typing in an input field, textarea, or contenteditable element - const isTyping = - activeElement instanceof HTMLInputElement || - activeElement instanceof HTMLTextAreaElement || - (activeElement && activeElement.getAttribute("contenteditable") === "true"); - - if (isTyping) { - return; // Skip shortcut handling while typing - } - - // Detect the combination of keys pressed - const keyCombination = detectModifierKeys(event); - - // Allow browser default behavior for specific keys (e.g., F5 for refresh) - if (["F5", "F11", "F12"].includes(event.key) || keyCombination === "Ctrl+R") { - return; - } - - // Prevent the default browser action for other handled key presses - event.preventDefault(); - - if (keyCombination) { - // Switch between different modules (e.g., builder, simulation) - if (keyCombination === "1" && !toggleView) { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - setActiveModule("builder"); - } - if (keyCombination === "2" && !toggleView) { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - setActiveModule("simulation"); - } - if (keyCombination === "3" && !toggleView) { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - setActiveModule("visualization"); - } - if (keyCombination === "4" && !toggleView) { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - setToggleUI(false); - setActiveModule("market"); - } - - // Toggle UI visibility - if (keyCombination === "Ctrl+." && activeModule !== "market") { - setToggleUI(!toggleUI); - } - - // Tool selection shortcuts - if (keyCombination === "V") { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - } - if (keyCombination === "X") { - setActiveTool("delete"); - setActiveSubTool("delete"); - } - if (keyCombination === "H") { - setActiveTool("free-hand"); - setActiveSubTool("free-hand"); - } - - // Toggle play mode - if (keyCombination === "Ctrl+P" && !toggleView) { - setIsPlaying(true); - } - - // Builder-specific shortcuts - if (activeModule === "builder") { - if (keyCombination === "TAB") { - // Switch between 2D and 3D views - if (toggleView) { - setToggleView(false); - setToggleThreeD(true); - setActiveTool("cursor"); - } else { - setSelectedWallItem(null); - setDeleteTool(false); - setAddAction(null); - setToggleView(true); - setToggleThreeD(false); - setActiveTool("cursor"); - } - } - - // Wall-related tools - if (toggleView) { - if (keyCombination === "Q" || keyCombination === "6") { - setActiveTool("draw-wall"); - setToolMode("Wall"); - } - if (keyCombination === "R" || keyCombination === "7") { - setActiveTool("draw-aisle"); - setToolMode("Aisle"); - } - if (keyCombination === "E" || keyCombination === "8") { - setActiveTool("draw-zone"); - setToolMode("Zone"); - } - if (keyCombination === "T" || keyCombination === "9") { - setActiveTool("draw-floor"); - setToolMode("Floor"); - } - } - - // Measurement tool - if (keyCombination === "M") { - setActiveTool("measure"); - setToolMode("MeasurementScale"); - } - } - - // Undo and redo actions - if (keyCombination === "Ctrl+Z") { - // Handle undo action - } - if (keyCombination === "Ctrl+Y" || keyCombination === "Ctrl+Shift+Z") { - // Handle redo action - } - - // Helper actions - if (keyCombination === "Ctrl+H") { - // Open help - } - if (keyCombination === "Ctrl+F") { - // Open find functionality - } - if (keyCombination === "Ctrl+?") { - // Show shortcuts info - } - - // Reset to cursor tool and stop play mode - if (keyCombination === "ESCAPE") { - setActiveTool("cursor"); - setIsPlaying(false); - } - } - }; - - // Add keydown event listener - window.addEventListener("keydown", handleKeyPress); - - // Cleanup function to remove the event listener - return () => { - window.removeEventListener("keydown", handleKeyPress); - }; - }, [activeModule, toggleUI, toggleView]); // Dependencies to reapply effect if these values change - - return null; // This component does not render any UI -}; - -export default KeyPressListener; +// Importing React and useEffect from React library +import React, { useEffect } from "react"; + +// Importing the necessary hooks and types from the application's state management stores +import useModuleStore, { useThreeDStore } from "../../store/useModuleStore"; +import useToggleStore from "../../store/useUIToggleStore"; +import { + useActiveSubTool, + useActiveTool, + useAddAction, + useDeleteTool, + useSelectedWallItem, + useToggleView, + useToolMode, +} from "../../store/store"; +import { usePlayButtonStore } from "../../store/usePlayButtonStore"; + +// Utility function to detect modifier keys (Ctrl, Alt, Shift) and key combinations +import { detectModifierKeys } from "./detectModifierKeys"; + +// KeyPressListener component to handle global keyboard shortcuts +const KeyPressListener: React.FC = () => { + // Accessing state and actions from different stores + const { activeModule, setActiveModule } = useModuleStore(); // Module management (e.g., builder, simulation, visualization) + const { setActiveSubTool } = useActiveSubTool(); // Sub-tool management + const { toggleUI, setToggleUI } = useToggleStore(); // UI visibility toggle + const { setToggleThreeD } = useThreeDStore(); // 3D view toggle + const { setToolMode } = useToolMode(); // Tool mode management + const { setIsPlaying } = usePlayButtonStore(); // Play button state management + + // Wall and tool-related actions + const { toggleView, setToggleView } = useToggleView(); // 2D/3D toggle state + const { setDeleteTool } = useDeleteTool(); // Delete tool action + const { setAddAction } = useAddAction(); // Add action management + const { setSelectedWallItem } = useSelectedWallItem(); // Selected wall item management + const { setActiveTool } = useActiveTool(); // Active tool management + + // useEffect to manage global keyboard shortcuts + useEffect(() => { + // Function to handle keydown events + const handleKeyPress = (event: KeyboardEvent) => { + // Identify the currently focused element + const activeElement = document.activeElement; + + // Check if the user is typing in an input field, textarea, or contenteditable element + const isTyping = + activeElement instanceof HTMLInputElement || + activeElement instanceof HTMLTextAreaElement || + (activeElement && activeElement.getAttribute("contenteditable") === "true"); + + if (isTyping) { + return; // Skip shortcut handling while typing + } + + // Detect the combination of keys pressed + const keyCombination = detectModifierKeys(event); + + // Allow browser default behavior for specific keys (e.g., F5 for refresh) + if (["F5", "F11", "F12"].includes(event.key) || keyCombination === "Ctrl+R") { + return; + } + + // Prevent the default browser action for other handled key presses + event.preventDefault(); + + if (keyCombination) { + // Switch between different modules (e.g., builder, simulation) + if (keyCombination === "1" && !toggleView) { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + setActiveModule("builder"); + } + if (keyCombination === "2" && !toggleView) { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + setActiveModule("simulation"); + } + if (keyCombination === "3" && !toggleView) { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + setActiveModule("visualization"); + } + if (keyCombination === "4" && !toggleView) { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + setToggleUI(false); + setActiveModule("market"); + } + + // Toggle UI visibility + if (keyCombination === "Ctrl+." && activeModule !== "market") { + setToggleUI(!toggleUI); + } + + // Tool selection shortcuts + if (keyCombination === "V") { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + } + if (keyCombination === "X") { + setActiveTool("delete"); + setActiveSubTool("delete"); + } + if (keyCombination === "H") { + setActiveTool("free-hand"); + setActiveSubTool("free-hand"); + } + + // Toggle play mode + if (keyCombination === "Ctrl+P" && !toggleView) { + setIsPlaying(true); + } + + // Builder-specific shortcuts + if (activeModule === "builder") { + if (keyCombination === "TAB") { + // Switch between 2D and 3D views + if (toggleView) { + setToggleView(false); + setToggleThreeD(true); + setActiveTool("cursor"); + } else { + setSelectedWallItem(null); + setDeleteTool(false); + setAddAction(null); + setToggleView(true); + setToggleThreeD(false); + setActiveTool("cursor"); + } + } + + // Wall-related tools + if (toggleView) { + if (keyCombination === "Q" || keyCombination === "6") { + setActiveTool("draw-wall"); + setToolMode("Wall"); + } + if (keyCombination === "R" || keyCombination === "7") { + setActiveTool("draw-aisle"); + setToolMode("Aisle"); + } + if (keyCombination === "E" || keyCombination === "8") { + setActiveTool("draw-zone"); + setToolMode("Zone"); + } + if (keyCombination === "T" || keyCombination === "9") { + setActiveTool("draw-floor"); + setToolMode("Floor"); + } + } + + // Measurement tool + if (keyCombination === "M") { + setActiveTool("measure"); + setToolMode("MeasurementScale"); + } + } + + // Undo and redo actions + if (keyCombination === "Ctrl+Z") { + // Handle undo action + } + if (keyCombination === "Ctrl+Y" || keyCombination === "Ctrl+Shift+Z") { + // Handle redo action + } + + // Helper actions + if (keyCombination === "Ctrl+H") { + // Open help + } + if (keyCombination === "Ctrl+F") { + // Open find functionality + } + if (keyCombination === "Ctrl+?") { + // Show shortcuts info + } + + // Reset to cursor tool and stop play mode + if (keyCombination === "ESCAPE") { + setActiveTool("cursor"); + setIsPlaying(false); + } + } + }; + + // Add keydown event listener + window.addEventListener("keydown", handleKeyPress); + + // Cleanup function to remove the event listener + return () => { + window.removeEventListener("keydown", handleKeyPress); + }; + }, [activeModule, toggleUI, toggleView]); // Dependencies to reapply effect if these values change + + return null; // This component does not render any UI +}; + +export default KeyPressListener;