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; } interface ProcessAnimationState { spawnedObjects: { [objectId: string]: SpawnedObject }; nextSpawnTime: number; objectIdCounter: number; } const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ processes, }) => { console.log("processes: ", 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; }; // Create a new spawned object const createSpawnedObject = ( process: ProcessData, currentTime: number, materialType: string ): SpawnedObject => { const processMaterials = { ...baseMaterials, ...(process.customMaterials || {}), }; 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, }; }; // 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 : 0; if (currentTime >= processState.nextSpawnTime) { const objectId = `obj-${process.id}-${processState.objectIdCounter}`; newStates[process.id] = { ...processState, spawnedObjects: { ...processState.spawnedObjects, [objectId]: createSpawnedObject( process, currentTime, spawnAction.material || "Default" ), }, 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; 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); console.log("process: ", process); const renderAs = process?.renderAs || "custom"; return renderAs === "box" ? ( } material={obj.material} > ) : ( gltf?.scene && ( } > ) ); }) )} ); }; export default ProcessAnimator;