import React, { useRef, useState, useEffect, useMemo } from "react"; import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore, } from "../../../store/usePlayButtonStore"; import { GLTFLoader } from "three-stdlib"; import { useLoader, useFrame } from "@react-three/fiber"; 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; } 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[] }> = ({ processes, }) => { 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); // Update the ref when speed changes useEffect(() => { speedRef.current = speed; }, [speed]); useEffect(() => { if (prevIsPlaying.current !== null) { setAnimationStates({}); } // Update ref to current isPlaying after effect prevIsPlaying.current = isPlaying; // setAnimationStates({}); }, [isPlaying]); // Sync ref with state useEffect(() => { animationStatesRef.current = animationStates; }, [animationStates]); // Base materials const baseMaterials = useMemo( () => ({ Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), }), [] ); // 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; }; useFrame(() => { if (!isPlaying || isPaused) return; const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; processes.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, }, }, }), {} ), }; } 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; // Apply global speed to spawn intervals (faster speed = more frequent spawns) 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 ); newStates[process.id] = { ...processState, spawnedObjects: { ...processState.spawnedObjects, [objectId]: newObject, }, objectIdCounter: processState.objectIdCounter + 1, nextSpawnTime: currentTime + effectiveSpawnInterval, }; } }); return newStates; }); }); useFrame((_, delta) => { if (!isPlaying || isPaused) return; const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; processes.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.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) { // Apply global speed to delays (faster speed = shorter delays) 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 ); 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 ); 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); // Apply both process-specific speed and global speed multiplier 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); 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], nextPoint, stateRef.progress ); } } updatedObjects[objectId] = { ...obj, state: { ...stateRef } }; } ); newStates[process.id] = { ...processState, spawnedObjects: updatedObjects, }; }); return newStates; }); }); if (!processes || processes.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 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; }) )} ); }; export default ProcessAnimator;