// import React, { useRef, useState, useEffect, useMemo } from "react"; // import { usePlayButtonStore } 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 boxGltb from "../../../assets/gltf-glb/crate_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: { pathUUID: string; pointUUID: string }; // targets: { pathUUID: 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; // The position of the object // } // interface ProcessAnimationState { // spawnedObjects: { [objectId: string]: SpawnedObject }; // nextSpawnTime: number; // objectIdCounter: number; // } // const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ // processes, // }) => { // // const gltf = useLoader(GLTFLoader, boxGltb) as GLTF; // const { isPlaying } = usePlayButtonStore(); // const groupRef = useRef(null); // const [animationStates, setAnimationStates] = useState< // Record // >({}); // // Base materials // const baseMaterials = useMemo( // () => ({ // Wood: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), // Box: new THREE.MeshStandardMaterial({ // color: 0xcccccc, // metalness: 0.8, // roughness: 0.2, // }), // Crate: new THREE.MeshStandardMaterial({ // color: 0x00aaff, // metalness: 0.1, // roughness: 0.5, // }), // Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), // }), // [] // ); // // Initialize animation states when processes or play state changes // useEffect(() => { // if (!isPlaying) { // setAnimationStates({}); // return; // } // const newStates: Record = {}; // processes.forEach((process) => { // newStates[process.id] = { // spawnedObjects: {}, // nextSpawnTime: 0, // objectIdCounter: 0, // }; // }); // setAnimationStates(newStates); // }, [isPlaying, processes]); // // Find spawn point in a process // 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; // }; // // Find the corresponding animation path point for a spawn point // const findAnimationPathPoint = (process: ProcessData, spawnPoint: ProcessPoint): THREE.Vector3 => { // // If we have an animation path, use the first point // if (process.animationPath && process.animationPath.length > 0) { // // Find the index of this point in the path // let pointIndex = 0; // // Try to find the corresponding point in the animation path // 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) { // // Found the matching point // if (process.animationPath[pointIndex]) { // const p = process.animationPath[pointIndex]; // return new THREE.Vector3(p.x, p.y, p.z); // } // } // pointIndex++; // } // } // // Fallback to the spawn point's position // return new THREE.Vector3( // spawnPoint.position[0], // spawnPoint.position[1], // spawnPoint.position[2] // ); // } // // If no animation path, use the spawn point's position // return new THREE.Vector3( // spawnPoint.position[0], // spawnPoint.position[1], // spawnPoint.position[2] // ); // }; // // Create a new spawned object // const createSpawnedObject = ( // process: ProcessData, // currentTime: number, // materialType: string, // spawnPoint: ProcessPoint // ): SpawnedObject => { // const processMaterials = { // ...baseMaterials, // ...(process.customMaterials || {}), // }; // // Get the position where we should spawn // const spawnPosition = findAnimationPathPoint(process, spawnPoint); // return { // ref: React.createRef(), // state: { // currentIndex: 0, // progress: 0, // isAnimating: true, // speed: process.speed || 1, // isDelaying: false, // delayStartTime: 0, // currentDelayDuration: 0, // delayComplete: false, // currentPathIndex: 0, // }, // visible: true, // material: // processMaterials[materialType as keyof typeof processMaterials] || // baseMaterials.Default, // currentMaterialType: materialType, // spawnTime: currentTime, // position: spawnPosition, // Store the position directly // }; // }; // // Handle material swap for an object // const handleMaterialSwap = ( // processId: string, // objectId: string, // materialType: string // ) => { // setAnimationStates((prev) => { // const processState = prev[processId]; // if (!processState || !processState.spawnedObjects[objectId]) return prev; // const process = processes.find((p) => p.id === processId); // const processMaterials = { // ...baseMaterials, // ...(process?.customMaterials || {}), // }; // const newMaterial = // processMaterials[materialType as keyof typeof processMaterials] || // baseMaterials.Default; // return { // ...prev, // [processId]: { // ...processState, // spawnedObjects: { // ...processState.spawnedObjects, // [objectId]: { // ...processState.spawnedObjects[objectId], // material: newMaterial, // currentMaterialType: materialType, // }, // }, // }, // }; // }); // }; // // Handle point actions for an object // const handlePointActions = ( // processId: string, // objectId: string, // actions: PointAction[] = [], // currentTime: number // ): 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.spawnedObjects[objectId] || // processState.spawnedObjects[objectId].state.isDelaying // ) { // return prev; // } // const delayDuration = // typeof action.delay === "number" // ? action.delay // : parseFloat(action.delay as string) || 0; // if (delayDuration > 0) { // return { // ...prev, // [processId]: { // ...processState, // spawnedObjects: { // ...processState.spawnedObjects, // [objectId]: { // ...processState.spawnedObjects[objectId], // state: { // ...processState.spawnedObjects[objectId].state, // 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; // }; // // Check if point has non-inherit actions // const hasNonInheritActions = (actions: PointAction[] = []): boolean => { // return actions.some((action) => action.isUsed && action.type !== "Inherit"); // }; // // Get point data for current animation index // 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; // }; // // Spawn objects for all processes // useFrame((state) => { // if (!isPlaying) return; // const currentTime = state.clock.getElapsedTime(); // setAnimationStates((prev) => { // const newStates = { ...prev }; // processes.forEach((process) => { // const processState = newStates[process.id]; // if (!processState) 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; // if (currentTime >= processState.nextSpawnTime) { // const objectId = `obj-${process.id}-${processState.objectIdCounter}`; // // Create the new object with the spawn point // const newObject = createSpawnedObject( // process, // currentTime, // spawnAction.material || "Default", // spawnPoint // ); // newStates[process.id] = { // ...processState, // spawnedObjects: { // ...processState.spawnedObjects, // [objectId]: newObject, // }, // objectIdCounter: processState.objectIdCounter + 1, // nextSpawnTime: currentTime + spawnInterval, // }; // } // }); // return newStates; // }); // }); // // Animate objects for all processes // useFrame((state, delta) => { // if (!isPlaying) return; // const currentTime = state.clock.getElapsedTime(); // setAnimationStates((prev) => { // const newStates = { ...prev }; // processes.forEach((process) => { // const processState = newStates[process.id]; // if (!processState) return; // 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 || !obj.state.isAnimating) return; // const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; // if (!currentRef) return; // // Set the position when the reference is first available // if (obj.position && obj.state.currentIndex === 0 && obj.state.progress === 0) { // currentRef.position.copy(obj.position); // } // const stateRef = obj.state; // // Get current point data // const currentPointData = getPointDataForAnimationIndex( // process, // stateRef.currentIndex // ); // // Execute actions when arriving at a new point // if (stateRef.progress === 0 && currentPointData?.actions) { // const shouldStop = handlePointActions( // process.id, // objectId, // currentPointData.actions, // currentTime // ); // if (shouldStop) return; // } // // Handle delays // if (stateRef.isDelaying) { // if ( // currentTime - stateRef.delayStartTime >= // stateRef.currentDelayDuration // ) { // stateRef.isDelaying = false; // stateRef.delayComplete = true; // } else { // return; // Keep waiting // } // } // const nextPointIdx = stateRef.currentIndex + 1; // const isLastPoint = nextPointIdx >= path.length; // if (isLastPoint) { // if (currentPointData?.actions) { // const shouldStop = !hasNonInheritActions( // currentPointData.actions // ); // if (shouldStop) { // currentRef.position.copy(path[stateRef.currentIndex]); // delete updatedObjects[objectId]; // return; // } // } // } // if (!isLastPoint) { // const nextPoint = path[nextPointIdx]; // const distance = // path[stateRef.currentIndex].distanceTo(nextPoint); // const movement = stateRef.speed * delta; // stateRef.progress += movement / distance; // if (stateRef.progress >= 1) { // stateRef.currentIndex = nextPointIdx; // stateRef.progress = 0; // stateRef.delayComplete = false; // currentRef.position.copy(nextPoint); // } 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"; // return renderAs === "box" ? ( // } // material={obj.material} // position={obj.position} // Set position directly in the JSX // > // // // ) : ( // gltf?.scene && ( // } // position={obj.position} // Set position directly in the JSX // > // // // ) // ); // }) // )} // // ); // }; // export default ProcessAnimator; import React, { useRef, useState, useEffect, useMemo } from "react"; import { usePlayButtonStore } 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 boxGltb from "../../../assets/gltf-glb/crate_box.glb"; import camera from "../../../assets/gltf-glb/camera face 2.gltf"; 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: { pathUUID: string; pointUUID: string }; targets: { pathUUID: 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; // The position of the object } interface ProcessAnimationState { spawnedObjects: { [objectId: string]: SpawnedObject }; nextSpawnTime: number; objectIdCounter: number; // New fields for process-wide delay isProcessDelaying: boolean; processDelayStartTime: number; processDelayDuration: number; } const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ processes, }) => { const gltf = useLoader(GLTFLoader, boxGltb) as GLTF; const { isPlaying } = usePlayButtonStore(); const groupRef = useRef(null); const [animationStates, setAnimationStates] = useState< Record >({}); // Base materials const baseMaterials = useMemo( () => ({ Wood: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), Box: new THREE.MeshPhongMaterial({ color: 0xcccccc, }), Crate: new THREE.MeshStandardMaterial({ color: 0x00aaff, metalness: 0.1, roughness: 0.5, }), Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), }), [] ); // Initialize animation states when processes or play state changes useEffect(() => { if (!isPlaying) { setAnimationStates({}); return; } const newStates: Record = {}; processes.forEach((process) => { newStates[process.id] = { spawnedObjects: {}, nextSpawnTime: 0, objectIdCounter: 0, // Initialize process-wide delay state isProcessDelaying: false, processDelayStartTime: 0, processDelayDuration: 0, }; }); setAnimationStates(newStates); }, [isPlaying, processes]); // Find spawn point in a process 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; }; // Find the corresponding animation path point for a spawn point const findAnimationPathPoint = ( process: ProcessData, spawnPoint: ProcessPoint ): THREE.Vector3 => { // If we have an animation path, use the first point if (process.animationPath && process.animationPath.length > 0) { // Find the index of this point in the path let pointIndex = 0; // Try to find the corresponding point in the animation path 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) { // Found the matching point if (process.animationPath[pointIndex]) { const p = process.animationPath[pointIndex]; return new THREE.Vector3(p.x, p.y, p.z); } } pointIndex++; } } // Fallback to the spawn point's position return new THREE.Vector3( spawnPoint.position[0], spawnPoint.position[1], spawnPoint.position[2] ); } // If no animation path, use the spawn point's position return new THREE.Vector3( spawnPoint.position[0], spawnPoint.position[1], spawnPoint.position[2] ); }; // Create a new spawned object const createSpawnedObject = ( process: ProcessData, currentTime: number, materialType: string, spawnPoint: ProcessPoint ): SpawnedObject => { const processMaterials = { ...baseMaterials, ...(process.customMaterials || {}), }; // Get the position where we should spawn const spawnPosition = findAnimationPathPoint(process, spawnPoint); return { ref: React.createRef(), state: { currentIndex: 0, progress: 0, isAnimating: true, speed: process.speed || 1, isDelaying: false, delayStartTime: 0, currentDelayDuration: 0, delayComplete: false, currentPathIndex: 0, }, visible: true, material: processMaterials[materialType as keyof typeof processMaterials] || baseMaterials.Default, currentMaterialType: materialType, spawnTime: currentTime, position: spawnPosition, // Store the position directly }; }; // Handle material swap for an object const handleMaterialSwap = ( processId: string, objectId: string, materialType: string ) => { setAnimationStates((prev) => { const processState = prev[processId]; if (!processState || !processState.spawnedObjects[objectId]) return prev; const process = processes.find((p) => p.id === processId); const processMaterials = { ...baseMaterials, ...(process?.customMaterials || {}), }; const newMaterial = processMaterials[materialType as keyof typeof processMaterials] || baseMaterials.Default; return { ...prev, [processId]: { ...processState, spawnedObjects: { ...processState.spawnedObjects, [objectId]: { ...processState.spawnedObjects[objectId], material: newMaterial, currentMaterialType: materialType, }, }, }, }; }); }; // Handle point actions for an object const handlePointActions = ( processId: string, objectId: string, actions: PointAction[] = [], currentTime: number ): 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, // Set process-wide delay instead of object-specific delay isProcessDelaying: true, processDelayStartTime: currentTime, processDelayDuration: delayDuration, // Update the specific object's state as well spawnedObjects: { ...processState.spawnedObjects, [objectId]: { ...processState.spawnedObjects[objectId], state: { ...processState.spawnedObjects[objectId].state, isAnimating: false, // Explicitly pause animation during delay 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; }; // Check if point has non-inherit actions const hasNonInheritActions = (actions: PointAction[] = []): boolean => { return actions.some((action) => action.isUsed && action.type !== "Inherit"); }; // Get point data for current animation index 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; }; // Spawn objects for all processes useFrame((state) => { if (!isPlaying) return; const currentTime = state.clock.getElapsedTime(); setAnimationStates((prev) => { const newStates = { ...prev }; processes.forEach((process) => { const processState = newStates[process.id]; if (!processState) return; // Skip spawning if the process is currently in a delay if (processState.isProcessDelaying) { // Check if delay is over if ( currentTime - processState.processDelayStartTime >= processState.processDelayDuration ) { // Reset process delay state newStates[process.id] = { ...processState, isProcessDelaying: false, // Reset delay state on all objects in this process spawnedObjects: Object.entries( processState.spawnedObjects ).reduce( (acc, [id, obj]) => ({ ...acc, [id]: { ...obj, state: { ...obj.state, isDelaying: false, delayComplete: true, isAnimating: true, // Ensure animation resumes // Force a small progress to ensure movement starts progress: obj.state.progress === 0 ? 0.001 : obj.state.progress, }, }, }), {} ), }; } return; // Skip spawning while delaying } 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; if (currentTime >= processState.nextSpawnTime) { const objectId = `obj-${process.id}-${processState.objectIdCounter}`; // Create the new object with the spawn point const newObject = createSpawnedObject( process, currentTime, spawnAction.material || "Default", spawnPoint ); newStates[process.id] = { ...processState, spawnedObjects: { ...processState.spawnedObjects, [objectId]: newObject, }, objectIdCounter: processState.objectIdCounter + 1, nextSpawnTime: currentTime + spawnInterval, }; } }); return newStates; }); }); // Animate objects for all processes useFrame((state, delta) => { if (!isPlaying) return; const currentTime = state.clock.getElapsedTime(); setAnimationStates((prev) => { const newStates = { ...prev }; processes.forEach((process) => { const processState = newStates[process.id]; if (!processState) return; // Check if the process-wide delay is active if (processState.isProcessDelaying) { // Check if the delay has completed if ( currentTime - processState.processDelayStartTime >= processState.processDelayDuration ) { // Reset process delay state AND resume animation newStates[process.id] = { ...processState, isProcessDelaying: false, // Reset delay state on all objects in this process AND ensure isAnimating is true spawnedObjects: Object.entries( processState.spawnedObjects ).reduce( (acc, [id, obj]) => ({ ...acc, [id]: { ...obj, state: { ...obj.state, isDelaying: false, delayComplete: true, isAnimating: true, // Ensure animation resumes // Important: Force progress to a small positive value to ensure movement progress: obj.state.progress === 0 ? 0.005 : obj.state.progress, }, }, }), {} ), }; // Skip the rest of the processing for this frame to allow the state update to take effect return newStates; } else { // If we're still in a process-wide delay, don't animate anything 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]) => { // Skip objects that are explicitly not visible if (!obj.visible) return; const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; if (!currentRef) return; // Set the position when the reference is first available if ( obj.position && obj.state.currentIndex === 0 && obj.state.progress === 0 ) { currentRef.position.copy(obj.position); } const stateRef = obj.state; // Check if we're delaying at the object level and update accordingly if (stateRef.isDelaying) { if ( currentTime - stateRef.delayStartTime >= stateRef.currentDelayDuration ) { // Delay is complete, resume animation stateRef.isDelaying = false; stateRef.delayComplete = true; stateRef.isAnimating = true; // Explicitly resume animation // Force movement from the current point by setting progress to a small value // if we're at the start of a segment if (stateRef.progress === 0) { stateRef.progress = 0.005; } // Force an immediate position update to ensure visually accurate position const nextPointIdx = stateRef.currentIndex + 1; if (nextPointIdx < path.length) { // Calculate the position slightly ahead of the current point const slightProgress = Math.max(stateRef.progress, 0.005); currentRef.position.lerpVectors( path[stateRef.currentIndex], nextPointIdx < path.length ? path[nextPointIdx] : path[stateRef.currentIndex], slightProgress ); } } else { // Still delaying, don't animate this object updatedObjects[objectId] = { ...obj, state: { ...stateRef } }; return; } } // Skip animation if the object shouldn't be animating if (!stateRef.isAnimating) return; // Get current point data const currentPointData = getPointDataForAnimationIndex( process, stateRef.currentIndex ); // Execute actions when arriving at a new point if (stateRef.progress === 0 && 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) { // uncomment this or write own logic to handle the object when reaching the last point of the process // currentRef.position.copy(path[stateRef.currentIndex]); // delete updatedObjects[objectId]; return; } } } if (!isLastPoint) { const nextPoint = path[nextPointIdx]; const distance = path[stateRef.currentIndex].distanceTo(nextPoint); const movement = stateRef.speed * delta; // If we just resumed from a delay, ensure we make actual progress if (stateRef.delayComplete && stateRef.progress < 0.01) { // Boost initial movement after delay to ensure visible progress stateRef.progress = 0.05; // Small but visible initial progress stateRef.delayComplete = false; // Reset flag so we don't do this again } else { // Normal progress calculation stateRef.progress += movement / distance; } if (stateRef.progress >= 1) { // We've reached the next point stateRef.currentIndex = nextPointIdx; stateRef.progress = 0; currentRef.position.copy(nextPoint); // Check if we need to execute actions at this new point const newPointData = getPointDataForAnimationIndex( process, stateRef.currentIndex ); if (newPointData?.actions) { // We've arrived at a new point with actions, handle them in the next frame // We don't call handlePointActions directly here to avoid state update issues // The actions will be handled in the next frame when progress is 0 } } else { // Normal path interpolation 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"; return renderAs === "box" ? ( } material={obj.material} position={obj.position} // Set position directly in the JSX > ) : ( gltf?.scene && ( } position={obj.position} // Set position directly in the JSX > ) ); }) )} ); }; export default ProcessAnimator;