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 { 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; } const ProcessAnimator: React.FC = ({ processes, setProcesses, agvRef, }) => { const gltf = useLoader(GLTFLoader, crate) as GLTF; const groupRef = useRef(null); const { animationStates, setAnimationStates, clockRef, elapsedBeforePauseRef, speedRef, debugRef, findSpawnPoint, createSpawnedObject, handlePointActions, hasNonInheritActions, getPointDataForAnimationIndex, processes: processedProcesses, checkAndCountTriggers, } = useProcessAnimation(processes, setProcesses, agvRef); const baseMaterials = useMemo( () => ({ Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), }), [] ); // In processAnimator.tsx - only the relevant spawn logic part that needs fixes useFrame(() => { // Spawn logic frame const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; processedProcesses.forEach((process) => { const processState = newStates[process.id]; if (!processState) return; if (processState.isProcessDelaying) { // Existing delay handling logic... return; } const spawnPoint = findSpawnPoint(process); if (!spawnPoint || !spawnPoint.actions) return; const spawnAction = spawnPoint.actions.find( (a) => a.isUsed && a.type === "Spawn" ); if (!spawnAction) return; const spawnInterval = typeof spawnAction.spawnInterval === "number" ? spawnAction.spawnInterval : parseFloat(spawnAction.spawnInterval as string) || 0; // 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) { const objectId = `obj-${process.id}-${processState.objectIdCounter}`; const newObject = createSpawnedObject( process, currentTime, spawnAction.material || "Default", spawnPoint, baseMaterials ); // Update state with the new object and flag for zero interval newStates[process.id] = { ...processState, spawnedObjects: { ...processState.spawnedObjects, [objectId]: newObject, }, objectIdCounter: processState.objectIdCounter + 1, nextSpawnTime: currentTime + effectiveSpawnInterval, // Mark that we've spawned an object for zero interval case hasSpawnedZeroIntervalObject: spawnInterval === 0 ? true : processState.hasSpawnedZeroIntervalObject, }; } }); return newStates; }); }); useFrame((_, delta) => { // Animation logic frame const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; processedProcesses.forEach((process) => { const processState = newStates[process.id]; if (!processState) return; if (processState.isProcessDelaying) { 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.005 : obj.state.progress, }, }, }), {} ), }; return newStates; } else { return newStates; } } const path = process.animationPath?.map((p) => new THREE.Vector3(p.x, p.y, p.z)) || []; if (path.length < 2) return; const updatedObjects = { ...processState.spawnedObjects }; Object.entries(processState.spawnedObjects).forEach( ([objectId, obj]) => { if (!obj.visible) return; const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; if (!currentRef) return; if ( obj.position && obj.state.currentIndex === 0 && obj.state.progress === 0 ) { currentRef.position.copy(obj.position); } const stateRef = obj.state; if (stateRef.isDelaying) { const effectiveDelayTime = stateRef.currentDelayDuration / speedRef.current; if (currentTime - stateRef.delayStartTime >= effectiveDelayTime) { stateRef.isDelaying = false; stateRef.delayComplete = true; stateRef.isAnimating = true; if (stateRef.progress === 0) { stateRef.progress = 0.005; } const nextPointIdx = stateRef.currentIndex + 1; if (nextPointIdx < path.length) { const slightProgress = Math.max(stateRef.progress, 0.005); currentRef.position.lerpVectors( path[stateRef.currentIndex], nextPointIdx < path.length ? path[nextPointIdx] : path[stateRef.currentIndex], slightProgress ); } } else { updatedObjects[objectId] = { ...obj, state: { ...stateRef } }; return; } } if (!stateRef.isAnimating) return; const currentPointData = getPointDataForAnimationIndex( process, stateRef.currentIndex ); // Handle point actions when first arriving at point if (stateRef.progress === 0 && currentPointData?.actions) { const shouldStop = handlePointActions( process.id, objectId, currentPointData.actions, currentTime, processedProcesses, baseMaterials ); if (shouldStop) { updatedObjects[objectId] = { ...obj, state: { ...stateRef } }; return; } } const nextPointIdx = stateRef.currentIndex + 1; const isLastPoint = nextPointIdx >= path.length; if (isLastPoint) { if (currentPointData?.actions) { const shouldStop = !hasNonInheritActions( currentPointData.actions ); if (shouldStop) { return; } } } if (!isLastPoint) { const nextPoint = path[nextPointIdx]; const distance = path[stateRef.currentIndex].distanceTo(nextPoint); const effectiveSpeed = stateRef.speed * speedRef.current; const movement = effectiveSpeed * delta; if (stateRef.delayComplete && stateRef.progress < 0.01) { stateRef.progress = 0.05; stateRef.delayComplete = false; } else { stateRef.progress += movement / distance; } if (stateRef.progress >= 1) { stateRef.currentIndex = nextPointIdx; 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 ); } else { currentRef.position.lerpVectors( path[stateRef.currentIndex], nextPoint, stateRef.progress ); } } updatedObjects[objectId] = { ...obj, state: { ...stateRef } }; } ); newStates[process.id] = { ...processState, spawnedObjects: updatedObjects, }; }); return newStates; }); }); 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 = processedProcesses.find((p) => p.id === processId); const renderAs = process?.renderAs || "custom"; return ( ); }) )} ); }; export default ProcessAnimator;