From e48195db98c6fbc545a7de755ae2c0bed1a4e5a5 Mon Sep 17 00:00:00 2001 From: SreeNath14 <153710861+SreeNath14@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:21:24 +0530 Subject: [PATCH] "updated Animation" --- app/src/modules/builder/agv/agv.tsx | 141 ++-- app/src/modules/builder/agv/pathNavigator.tsx | 490 +++++++++----- .../geomentries/assets/addAssetModel.ts | 3 + .../scene/IntialLoad/loadInitialFloorItems.ts | 1 + app/src/modules/simulation/process/mesh.tsx | 7 - .../simulation/process/processAnimator.tsx | 603 +++--------------- .../simulation/process/processContainer.tsx | 17 +- .../simulation/process/processCreator.tsx | 262 +++++--- .../simulation/process/processObject.tsx | 58 ++ .../process/processObjectRender.tsx | 114 ---- app/src/modules/simulation/process/types.ts | 123 ++-- .../process/useProcessAnimations.tsx | 542 ++++++++++++++++ app/src/modules/simulation/simulation.tsx | 23 +- app/src/store/store.ts | 37 +- app/src/types/world/worldTypes.d.ts | 357 +++++++---- 15 files changed, 1609 insertions(+), 1169 deletions(-) delete mode 100644 app/src/modules/simulation/process/mesh.tsx create mode 100644 app/src/modules/simulation/process/processObject.tsx delete mode 100644 app/src/modules/simulation/process/processObjectRender.tsx create mode 100644 app/src/modules/simulation/process/useProcessAnimations.tsx diff --git a/app/src/modules/builder/agv/agv.tsx b/app/src/modules/builder/agv/agv.tsx index 48f4306..78434d9 100644 --- a/app/src/modules/builder/agv/agv.tsx +++ b/app/src/modules/builder/agv/agv.tsx @@ -1,68 +1,107 @@ 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; +} -const Agv = () => { - const [pathPoints, setPathPoints] = useState([]); - const { simulationStates } = useSimulationStates(); - const { navMesh } = useNavMesh(); - const { isPlaying } = usePlayButtonStore(); +const Agv: React.FC = ({ processes, agvRef }) => { + const [pathPoints, setPathPoints] = useState([]); + const { simulationStates } = useSimulationStates(); + const { navMesh } = useNavMesh(); + const { isPlaying } = usePlayButtonStore(); + const { PlayAgv, setPlayAgv } = usePlayAgv(); - useEffect(() => { - if (simulationStates.length > 0) { + useEffect(() => { + console.log("agvRef: ", agvRef); + }, [agvRef]); - const agvModels = simulationStates.filter((val) => val.modelName === "agv" && val.type === "Vehicle"); + 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 }, - ], - })); + 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, + }, + ], + })); - setPathPoints(newPathPoints); - } - }, [simulationStates]); + setPathPoints(newPathPoints); + } + }, [simulationStates]); - return ( - <> - {pathPoints.map((pair, i) => ( - - + return ( + <> + {pathPoints.map((pair, i) => ( + + - {pair.points.slice(1).map((point, idx) => ( - - - - - ))} - - ))} - - ); + {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 7d7984c..7d17ee6 100644 --- a/app/src/modules/builder/agv/pathNavigator.tsx +++ b/app/src/modules/builder/agv/pathNavigator.tsx @@ -4,206 +4,368 @@ 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"; 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; } +interface AGVData { + processId: string; + vehicleId: string; + hitCount: number; + totalHits: number; +} +type Phase = "initial" | "toDrop" | "toPickup"; export default function PathNavigator({ - navMesh, - pathPoints, - id, - speed, - bufferTime, - hitCount + navMesh, + pathPoints, + id, + speed, + bufferTime, + hitCount, + processes, + agvRef, }: 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 [currentPhase, setCurrentPhase] = useState("initial"); + // console.log('agvRef: ', agvRef); - const distancesRef = useRef([]); - const totalDistanceRef = useRef(0); - const progressRef = useRef(0); - const isWaiting = useRef(false); - const timeoutRef = useRef(null); + const [path, setPath] = useState<[number, number, number][]>([]); - const { scene } = useThree(); - const { isPlaying } = usePlayButtonStore(); + // 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 + ); - useEffect(() => { - const object = scene.getObjectByProperty("uuid", id); - if (object) { - setInitialPosition(object.position.clone()); - setInitialRotation(object.rotation.clone()); - } - }, [scene, id]); + const distancesRef = useRef([]); + const totalDistanceRef = useRef(0); + const progressRef = useRef(0); + const isWaiting = useRef(false); + const timeoutRef = useRef(null); + const hasStarted = useRef(false); - 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 { scene } = useThree(); + const { isPlaying } = usePlayButtonStore(); + const { PlayAgv, setPlayAgv } = usePlayAgv(); - const resetState = () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } + useEffect(() => { + const object = scene.getObjectByProperty("uuid", id); + if (object) { + setInitialPosition(object.position.clone()); + setInitialRotation(object.rotation.clone()); + } + }, [scene, id]); - setPath([]); - setCurrentPhase('initial'); - setPickupDropPath([]); - setDropPickupPath([]); - distancesRef.current = []; - totalDistanceRef.current = 0; - progressRef.current = 0; - isWaiting.current = false; + 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 []; + } + }; - if (initialPosition && initialRotation) { - const object = scene.getObjectByProperty("uuid", id); - if (object) { - object.position.copy(initialPosition); - object.rotation.copy(initialRotation); - } - } - }; + const resetState = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } - useEffect(() => { - if (!isPlaying) { - resetState(); - } + setPath([]); + setCurrentPhase("initial"); + setPickupDropPath([]); + setDropPickupPath([]); + distancesRef.current = []; + totalDistanceRef.current = 0; + progressRef.current = 0; + isWaiting.current = false; - if (!navMesh || pathPoints.length < 2) return; + if (initialPosition && initialRotation) { + const object = scene.getObjectByProperty("uuid", id); + if (object) { + object.position.copy(initialPosition); + object.rotation.copy(initialRotation); + } + } + }; - const [pickup, drop] = pathPoints.slice(-2); - const object = scene.getObjectByProperty("uuid", id); - if (!object) return; + useEffect(() => { + if (!isPlaying) { + resetState(); + } - const currentPosition = { x: object.position.x, y: object.position.y, z: object.position.z }; + if (!navMesh || pathPoints.length < 2) return; - const toPickupPath = computePath(currentPosition, pickup); - const pickupToDropPath = computePath(pickup, drop); - const dropToPickupPath = computePath(drop, pickup); + const [pickup, drop] = pathPoints.slice(-2); + const object = scene.getObjectByProperty("uuid", id); - if (toPickupPath.length && pickupToDropPath.length && dropToPickupPath.length) { - setPickupDropPath(pickupToDropPath); - setDropPickupPath(dropToPickupPath); - setToPickupPath(toPickupPath); - setPath(toPickupPath); - setCurrentPhase('initial'); - } - }, [navMesh, pathPoints, hitCount, isPlaying]); + if (!object) return; - useEffect(() => { - if (path.length < 2) return; + const currentPosition = { + x: object.position.x, + y: object.position.y, + z: object.position.z, + }; - 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 toPickupPath = computePath(currentPosition, pickup); + const pickupToDropPath = computePath(pickup, drop); + const dropToPickupPath = computePath(drop, pickup); - distancesRef.current = segmentDistances; - totalDistanceRef.current = total; - progressRef.current = 0; - isWaiting.current = false; - }, [path]); + if ( + toPickupPath.length && + pickupToDropPath.length && + dropToPickupPath.length + ) { + setPickupDropPath(pickupToDropPath); + setDropPickupPath(dropToPickupPath); + setToPickupPath(toPickupPath); + setPath(toPickupPath); + setCurrentPhase("initial"); + } + }, [navMesh, pathPoints, hitCount, isPlaying, PlayAgv]); - useFrame((_, delta) => { - if (!isPlaying || path.length < 2 || !scene || !id) return; + useEffect(() => { + if (path.length < 2) return; - const object = scene.getObjectByProperty("uuid", id); - if (!object) return; + 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 speedFactor = speed; - progressRef.current += delta * speedFactor; + distancesRef.current = segmentDistances; + totalDistanceRef.current = total; + progressRef.current = 0; + isWaiting.current = false; + }, [path]); - let covered = progressRef.current; - let accumulated = 0; - let index = 0; + // Add these refs outside the useFrame if not already present: + const startPointReached = useRef(false); - while ( - index < distancesRef.current.length && - covered > accumulated + distancesRef.current[index] - ) { - accumulated += distancesRef.current[index]; - index++; - } + useFrame((_, delta) => {}); - if (index >= distancesRef.current.length) { - progressRef.current = totalDistanceRef.current; + useFrame((_, delta) => { + const currentAgv = (agvRef.current || []).find( + (agv: AGVData) => agv.vehicleId === id + ); + console.log("currentAgv: ", currentAgv?.isplaying); - if (!isWaiting.current) { - isWaiting.current = true; + if (!scene || !id || !isPlaying) return; - timeoutRef.current = setTimeout(() => { - if (currentPhase === 'initial') { - setPath(pickupDropPath); - setCurrentPhase('loop'); - } else { - setPath(prevPath => - prevPath === pickupDropPath ? dropPickupPath : pickupDropPath - ); - } + const object = scene.getObjectByProperty("uuid", id); + if (!object) return; - progressRef.current = 0; - isWaiting.current = false; - }, bufferTime * 1000); - } - 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 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); - const position = start.clone().lerp(end, t); + const isAgvReady = () => { + if (!agvRef.current || agvRef.current.length === 0) return false; + if (!currentAgv) return false; + return hitCount === currentAgv.expectedHitCount; + }; - object.position.copy(position); + // Step 1: Snap to start point on first play + if (isPlaying && !hasStarted.current && toPickupPath.length > 0) { + const startPoint = new THREE.Vector3(...toPickupPath[0]); + object.position.copy(startPoint); - const direction = new THREE.Vector3().subVectors(end, start).normalize(); - const targetRotationY = Math.atan2(direction.x, direction.z); + 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); + } - let angleDifference = targetRotationY - object.rotation.y; - angleDifference = ((angleDifference + Math.PI) % (Math.PI * 2)) - Math.PI; - object.rotation.y += angleDifference * 0.1; - }); + hasStarted.current = true; + startPointReached.current = true; + progressRef.current = 0; - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); + return; + } - return ( - - {toPickupPath.length > 0 && ( - - )} + // Step 2: Wait at start point for AGV readiness (only if expected hit count is not met) + if ( + isPlaying && + startPointReached.current && + path.length === 0 && + currentAgv?.isplaying + ) { + if (!isAgvReady()) { + return; // Prevent transitioning to the next phase if AGV is not ready + } - {pickupDropPath.length > 0 && ( - - )} + setPath([...toPickupPath]); // Start path transition once the AGV is ready + setCurrentPhase("toDrop"); + progressRef.current = 0; + startPointReached.current = false; - {dropPickupPath.length > 0 && ( - - )} - - ); -} \ No newline at end of file + 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"; + } else if (currentPhase === "toPickup") { + nextPath = pickupDropPath; + nextPhase = "toDrop"; + } else { + nextPath = pickupDropPath; + nextPhase = "toDrop"; + } + + setPath([...nextPath]); + setCurrentPhase(nextPhase); + progressRef.current = 0; + isWaiting.current = false; + distancesRef.current = []; + + // Decrease the expected count if AGV is ready and has completed its path + if (currentAgv) { + currentAgv.expectedCount = Math.max(0, currentAgv.expectedCount - 1); // Decrease but ensure it's not negative + console.log( + "Decreased expected count to: ", + currentAgv.expectedCount + ); + } + + if (agvRef.current) { + agvRef.current = agvRef.current.map((agv: AGVData) => + agv.vehicleId === id ? { ...agv, hitCount: null } : agv + ); + } + + // 🔁 Reset and wait again after reaching start + if (currentPhase === "toPickup" && currentAgv) { + currentAgv.isplaying = false; + setPath([]); + startPointReached.current = true; + progressRef.current = 0; + } + }, 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 9fc6199..0284245 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") { @@ -324,6 +326,7 @@ async function handleModelLoad( return updatedItems; }); + console.log('data: ', data); socket.emit("v2:model-asset:add", data); } diff --git a/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts b/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts index ec6033b..3cafc32 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/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..a790d19 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -1,134 +1,45 @@ -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"; + +interface ProcessContainerProps { + processes: ProcessData[]; + setProcesses: React.Dispatch>; + agvRef: 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, }) => { 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 }), @@ -138,365 +49,22 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ [] ); - // 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, - }; - }); - 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" - ); - 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] - ); - }; - - 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, - }, - }, - }, - }; - }); - }; - - 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 +81,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 +97,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 +110,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 +124,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 +197,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 +233,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 } }; @@ -691,8 +267,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 +282,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 +318,31 @@ 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..a0378db 100644 --- a/app/src/modules/simulation/process/processContainer.tsx +++ b/app/src/modules/simulation/process/processContainer.tsx @@ -2,15 +2,22 @@ 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; +} +const ProcessContainer: React.FC = ({ + processes, + setProcesses, + agvRef +}) => { + console.log("processes: ", processes); 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..9280a78 --- /dev/null +++ b/app/src/modules/simulation/process/useProcessAnimations.tsx @@ -0,0 +1,542 @@ +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; + hasSpawnedZeroIntervalObject?: boolean; + }>; +} + +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: [], + hasSpawnedZeroIntervalObject: false, // Initialize the new property + }; + }); + + 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 + 1; + } + + // 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..bd3eb58 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -16,23 +16,8 @@ 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([]); return ( <> @@ -41,8 +26,8 @@ function Simulation() { <> - - + + )} diff --git a/app/src/store/store.ts b/app/src/store/store.ts index 725180b..3c7e4a9 100644 --- a/app/src/store/store.ts +++ b/app/src/store/store.ts @@ -305,9 +305,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, })), })); @@ -347,12 +345,29 @@ export const useSelectedPath = create((set: any) => ({ })); interface SimulationPathsStore { - simulationStates: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[]; + simulationStates: ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + )[]; setSimulationStates: ( paths: - | (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[] - | ((prev: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[] - ) => (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[]) + | ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + )[] + | (( + prev: ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + )[] + ) => ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + )[]) ) => void; } @@ -363,7 +378,7 @@ export const useSimulationStates = create((set) => ({ simulationStates: typeof paths === "function" ? paths(state.simulationStates) : paths, })), -})) +})); export const useNavMesh = create((set: any) => ({ navMesh: null, @@ -446,3 +461,9 @@ export const useTileDistance = create((set: any) => ({ planeValue: { ...state.planeValue, ...value }, })), })); + +export const usePlayAgv = create((set, get) => ({ + PlayAgv: [], + setPlayAgv: (updateFn: (prev: any[]) => any[]) => + set({ PlayAgv: updateFn(get().PlayAgv) }), +})); diff --git a/app/src/types/world/worldTypes.d.ts b/app/src/types/world/worldTypes.d.ts index 7baf5e0..811b7de 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,120 +269,206 @@ 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]; - 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]; + 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]; - 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 }[] }; - }; + modeluuid: string; + modelName: string; + type: "StaticMachine"; + points: { + uuid: string; position: [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]; - 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]; + 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]; + actions: { uuid: string; - position: [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; + 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; }; - }; -} + }; +};