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"; 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; } interface AnimationState { currentIndex: number; progress: number; isAnimating: boolean; speed: number; isDelaying: boolean; delayStartTime: number; currentDelayDuration: number; delayComplete: boolean; currentPathIndex: number; spawnPoints: Record< string, { position: THREE.Vector3; interval: number; lastSpawnTime: number; } >; } const MAX_SPAWNED_OBJECTS = 20; const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ processes, }) => { console.log("processes: ", processes); const gltf = useLoader(GLTFLoader, boxGltb) as GLTF; const { isPlaying, setIsPlaying } = usePlayButtonStore(); const groupRef = useRef(null); const meshRef = useRef(null); const [visible, setVisible] = useState(false); const spawnedObjectsRef = useRef([]); const materials = 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 }), }), [] ); const [currentMaterial, setCurrentMaterial] = useState( materials.Default ); const { animationPath, currentProcess } = useMemo(() => { const defaultProcess = { animationPath: [], pointActions: [], speed: 1, paths: [], }; const cp = processes?.[0] || defaultProcess; return { animationPath: cp.animationPath?.map((p) => new THREE.Vector3(p.x, p.y, p.z)) || [], currentProcess: cp, }; }, [processes]); const animationStateRef = useRef({ currentIndex: 0, progress: 0, isAnimating: false, speed: currentProcess.speed, isDelaying: false, delayStartTime: 0, currentDelayDuration: 0, delayComplete: false, currentPathIndex: 0, spawnPoints: {}, }); const getPointDataForAnimationIndex = (index: number) => { if (!processes[0]?.paths) return null; if (index < 3) { return processes[0].paths[0]?.points[index]; } else { const path2Index = index - 3; return processes[0].paths[1]?.points[path2Index]; } }; useEffect(() => { if (isPlaying) { setVisible(true); animationStateRef.current = { currentIndex: 0, progress: 0, isAnimating: true, speed: currentProcess.speed, isDelaying: false, delayStartTime: 0, currentDelayDuration: 0, delayComplete: false, currentPathIndex: 0, spawnPoints: {}, }; // Clear spawned objects if (groupRef.current) { spawnedObjectsRef.current.forEach((obj) => { if (groupRef.current?.children.includes(obj)) { groupRef.current.remove(obj); } if (obj instanceof THREE.Mesh) { obj.material.dispose(); } }); spawnedObjectsRef.current = []; } const currentRef = gltf?.scene ? groupRef.current : meshRef.current; if (currentRef && animationPath.length > 0) { currentRef.position.copy(animationPath[0]); } } else { animationStateRef.current.isAnimating = false; } }, [isPlaying, currentProcess, animationPath]); const handleMaterialSwap = (materialType: string) => { const newMaterial = materials[materialType as keyof typeof materials] || materials.Default; setCurrentMaterial(newMaterial); spawnedObjectsRef.current.forEach((obj) => { if (obj instanceof THREE.Mesh) { obj.material = newMaterial.clone(); } }); }; const hasNonInheritActions = (actions: PointAction[] = []) => { return actions.some((action) => action.isUsed && action.type !== "Inherit"); }; const handlePointActions = ( actions: PointAction[] = [], currentTime: number, currentPosition: THREE.Vector3 ) => { let shouldStopAnimation = false; actions.forEach((action) => { if (!action.isUsed) return; switch (action.type) { case "Delay": if ( !animationStateRef.current.isDelaying && !animationStateRef.current.delayComplete ) { const delayDuration = typeof action.delay === "number" ? action.delay : parseFloat(action.delay as string) || 0; if (delayDuration > 0) { animationStateRef.current.isDelaying = true; animationStateRef.current.delayStartTime = currentTime; animationStateRef.current.currentDelayDuration = delayDuration; shouldStopAnimation = true; } } break; case "Despawn": setVisible(false); setIsPlaying(false); animationStateRef.current.isAnimating = false; shouldStopAnimation = true; break; case "Spawn": const spawnInterval = typeof action.spawnInterval === "number" ? action.spawnInterval : parseFloat(action.spawnInterval as string) || 1; const positionKey = currentPosition.toArray().join(","); animationStateRef.current.spawnPoints[positionKey] = { position: currentPosition.clone(), interval: spawnInterval, lastSpawnTime: currentTime - spawnInterval, // Force immediate spawn }; break; case "Swap": if (action.material) { handleMaterialSwap(action.material); } break; case "Inherit": break; } }); return shouldStopAnimation; }; useFrame((state, delta) => { const currentRef = gltf?.scene ? groupRef.current : meshRef.current; if ( !currentRef || !animationStateRef.current.isAnimating || animationPath.length < 2 ) { return; } const currentTime = state.clock.getElapsedTime(); const path = animationPath; const stateRef = animationStateRef.current; if (stateRef.currentIndex === 3 && stateRef.currentPathIndex === 0) { stateRef.currentPathIndex = 1; } const currentPointData = getPointDataForAnimationIndex( stateRef.currentIndex ); if (stateRef.progress === 0 && currentPointData?.actions) { const shouldStop = handlePointActions( currentPointData.actions, currentTime, currentRef.position ); if (shouldStop) return; } if (stateRef.isDelaying) { if ( currentTime - stateRef.delayStartTime >= stateRef.currentDelayDuration ) { stateRef.isDelaying = false; stateRef.delayComplete = true; } else { return; } } // Handle spawning - this is the key updated part Object.entries(stateRef.spawnPoints).forEach(([key, spawnPoint]) => { if (currentTime - spawnPoint.lastSpawnTime >= spawnPoint.interval) { spawnPoint.lastSpawnTime = currentTime; if (gltf?.scene && groupRef?.current) { const newObject = gltf.scene.clone(); newObject.position.copy(spawnPoint.position); newObject.traverse((child) => { if (child instanceof THREE.Mesh) { child.material = currentMaterial.clone(); } }); groupRef.current.add(newObject); spawnedObjectsRef.current.push(newObject); // Clean up old objects if needed console.log( "spawnedObjectsRef.current.length: ", spawnedObjectsRef.current.length ); if (spawnedObjectsRef.current.length > MAX_SPAWNED_OBJECTS) { const oldest = spawnedObjectsRef.current.shift(); if (oldest && groupRef.current.children.includes(oldest)) { groupRef.current.remove(oldest); if (oldest instanceof THREE.Mesh) { oldest.material.dispose(); } } } } } }); 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]); setIsPlaying(false); stateRef.isAnimating = false; 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 ); } } }); useEffect(() => { return () => { if (groupRef.current) { spawnedObjectsRef.current.forEach((obj) => { if (groupRef.current?.children.includes(obj)) { groupRef.current.remove(obj); } if (obj instanceof THREE.Mesh) { obj.material.dispose(); } }); spawnedObjectsRef.current = []; } }; }, []); if (!processes || processes.length === 0) { return null; } if (!gltf?.scene) { return visible ? ( ) : null; } return visible ? ( ) : null; }; export default ProcessAnimator;