import React, { useRef, useEffect, useMemo, useCallback } 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"; interface ArmBotState { uuid: string; position: [number, number, number]; rotation: [number, number, number]; status: string; material: string; triggerId: string; connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[]; }; actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; isActive?: boolean; } interface ProcessContainerProps { processes: ProcessData[]; setProcesses: React.Dispatch>; agvRef: any; MaterialRef: any; armBots: ArmBotState[]; setArmBots: React.Dispatch>; } const ProcessAnimator: React.FC = ({ processes, setProcesses, agvRef, MaterialRef, armBots, setArmBots, }) => { const gltf = useLoader(GLTFLoader, crate) as GLTF; const groupRef = useRef(null); const tempStackedObjectsRef = useRef>({}); const { animationStates, setAnimationStates, clockRef, elapsedBeforePauseRef, speedRef, debugRef, findSpawnPoint, createSpawnedObject, handlePointActions, hasNonInheritActions, getPointDataForAnimationIndex, processes: processedProcesses, checkAndCountTriggers, } = useProcessAnimation(processes, setProcesses, agvRef, armBots, setArmBots); const baseMaterials = useMemo( () => ({ Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), Default: new THREE.MeshStandardMaterial(), }), [] ); useEffect(() => { // Update material references for all spawned objects Object.entries(animationStates).forEach(([processId, processState]) => { Object.keys(processState.spawnedObjects).forEach((objectId) => { const entry = { processId, objectId }; const materialType = processState.spawnedObjects[objectId]?.currentMaterialType; if (!materialType) { return; } const matRefArray = MaterialRef.current; // Find existing material group const existing = matRefArray.find( (entryGroup: { material: string; objects: any[] }) => entryGroup.material === materialType ); if (existing) { // Check if this processId + objectId already exists const alreadyExists = existing.objects.some( (o: any) => o.processId === entry.processId && o.objectId === entry.objectId ); if (!alreadyExists) { existing.objects.push(entry); } } else { // Create new group for this material type matRefArray.push({ material: materialType, objects: [entry], }); } }); }); }, [animationStates, MaterialRef, agvRef]); // In processAnimator.tsx - only the relevant spawn logic part that needs fixes // Add this function to ProcessAnimator component const isConnectedToActiveArmBot = useCallback( (processId: any) => { // Check if any active armbot is connected to this process return armBots.some((armbot) => { if (!armbot.isActive) return false; // Check if this armbot is connected to the process return armbot.connections?.targets?.some((connection) => { // Find the process that owns this modelUUID const connectedProcess = processes.find((p) => p.paths?.some((path) => path.modeluuid === connection.modelUUID) ); return connectedProcess?.id === processId; }); }); }, [armBots, processes] ); // First useFrame for spawn logic 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; // Check connection status const isConnected = isConnectedToActiveArmBot(process.id); if (processState.isProcessDelaying) { // Existing delay handling logic... return; } if (isConnected) { newStates[process.id] = { ...processState, nextSpawnTime: Infinity, // Prevent future spawns }; return; } const spawnPoint = findSpawnPoint(process); if (!spawnPoint || !spawnPoint.actions) { // console.log( // `Process ${process.id} has no valid spawn point or 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 || "0") || 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 ); // Initialize state properly to ensure animation newObject.state = { ...newObject.state, isAnimating: true, isDelaying: false, delayComplete: false, progress: 0.005, // Start with tiny progress to ensure animation begins }; // 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; }); }); // Second useFrame for animation logic 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; } // Check connection status with debugging const isConnected = isConnectedToActiveArmBot(process.id); // console.log( // `Process ${process.id} animation - connected:`, // isConnected // ); if (isConnected) { // Stop all animations when connected to active arm bot newStates[process.id] = { ...processState, spawnedObjects: Object.entries(processState.spawnedObjects).reduce( (acc, [id, obj]) => ({ ...acc, [id]: { ...obj, state: { ...obj.state, isAnimating: false, // Stop animation isDelaying: false, // Clear delays delayComplete: false, // Reset delays progress: 0, // Reset progress }, }, }), {} ), }; return; } // Process delay handling if (processState.isProcessDelaying) { const effectiveDelayTime = processState.processDelayDuration / speedRef.current; if ( currentTime - processState.processDelayStartTime >= effectiveDelayTime ) { // console.log( // `Process ${process.id} delay completed, resuming animation` // ); 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; } else { return; } } // Ensure we have a valid path to follow const path = process.animationPath?.map((p) => new THREE.Vector3(p.x, p.y, p.z)) || []; if (path.length < 2) { // console.log( // `Process ${process.id} has insufficient path points: ${path.length}` // ); return; } const updatedObjects = { ...processState.spawnedObjects }; let animationOccurring = false; // Track if any animation is happening Object.entries(processState.spawnedObjects).forEach( ([objectId, obj]) => { if (!obj.visible) { return; } const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; if (!currentRef) { // console.log( // `No reference for object ${objectId}, skipping animation` // ); return; } // Initialize position for new objects if ( obj.position && obj.state.currentIndex === 0 && obj.state.progress === 0 ) { currentRef.position.copy(obj.position); } const stateRef = obj.state; // Ensure animation state is properly set for objects if (!stateRef.isAnimating && !stateRef.isDelaying && !isConnected) { stateRef.isAnimating = true; stateRef.progress = stateRef.progress > 0 ? stateRef.progress : 0.005; } // Handle delay logic if (stateRef.isDelaying) { const effectiveDelayTime = stateRef.currentDelayDuration / speedRef.current; if (currentTime - stateRef.delayStartTime >= effectiveDelayTime) { // console.log( // `Delay complete for object ${objectId}, resuming animation` // ); 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; } } // Skip non-animating objects if (!stateRef.isAnimating) { // console.log( // `Object ${objectId} not animating, skipping animation updates` // ); return; } animationOccurring = true; // Mark that animation is happening // Handle point actions 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; // Handle objects at the last point if (isLastPoint) { const isAgvPicking = agvRef.current.some( (agv: any) => agv.processId === process.id && agv.status === "picking" ); const shouldHide = !currentPointData?.actions || !hasNonInheritActions(currentPointData.actions); if (shouldHide) { if (isAgvPicking) { // console.log( // `AGV picking at last point for object ${objectId}, hiding object` // ); updatedObjects[objectId] = { ...obj, visible: false, state: { ...stateRef, isAnimating: false, }, }; } else { tempStackedObjectsRef.current[objectId] = true; updatedObjects[objectId] = { ...obj, visible: true, state: { ...stateRef, isAnimating: true, }, }; } return; } } // Handle stacked objects when AGV picks if (tempStackedObjectsRef.current[objectId]) { const isAgvPicking = agvRef.current.some( (agv: any) => agv.processId === process.id && agv.status === "picking" ); if (isAgvPicking) { delete tempStackedObjectsRef.current[objectId]; updatedObjects[objectId] = { ...obj, visible: false, state: { ...stateRef, isAnimating: false, }, }; return; } } // Handle normal animation progress for objects not at last point if (!isLastPoint) { const nextPoint = path[nextPointIdx]; const distance = path[stateRef.currentIndex].distanceTo(nextPoint); const effectiveSpeed = stateRef.speed * speedRef.current; const movement = effectiveSpeed * delta; // Ensure progress is always moving forward if (stateRef.delayComplete && stateRef.progress < 0.01) { stateRef.progress = 0.05; stateRef.delayComplete = false; // console.log( // `Boosting progress for object ${objectId} after delay` // ); } else { stateRef.progress += movement / distance; // console.log( // `Object ${objectId} progress: ${stateRef.progress.toFixed(3)}` // ); } // Handle point transition 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 ); // No action needed with newPointData here - will be handled in next frame } else { // Update position with lerp currentRef.position.lerpVectors( path[stateRef.currentIndex], nextPoint, stateRef.progress ); } } updatedObjects[objectId] = { ...obj, state: { ...stateRef } }; } ); // Log if no animation is occurring when it should if (!animationOccurring && !isConnected) { // console.log( // `Warning: No animation occurring for process ${process.id} despite not being connected` // ); } 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;