From e0082cb55a4ea64a7562f97ee16c10f763776445 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Wed, 16 Apr 2025 11:39:03 +0530 Subject: [PATCH] feat: Enhance ArmBotState with connections and isActive properties - Updated ArmBotState interface across multiple files to include connections (source and targets) and isActive properties. - Implemented logic in ProcessAnimator to check if processes are connected to active ArmBots, preventing future spawns if connected. - Adjusted animation state handling to account for active ArmBots, stopping animations and resetting states as necessary. - Refactored related functions for better clarity and maintainability. --- app/src/modules/simulation/armbot/ArmBot.tsx | 8 +- .../simulation/armbot/ArmBotInstances.tsx | 15 +- .../armbot/IKAnimationController.tsx | 79 +- .../modules/simulation/armbot/IkInstances.tsx | 17 +- .../simulation/process/processAnimator.tsx | 84 +- .../simulation/process/processContainer.tsx | 5 + .../process/useProcessAnimations.tsx | 1174 +++++++++-------- app/src/modules/simulation/simulation.tsx | 5 + .../staticMachine/staticMachine.tsx | 6 +- 9 files changed, 755 insertions(+), 638 deletions(-) diff --git a/app/src/modules/simulation/armbot/ArmBot.tsx b/app/src/modules/simulation/armbot/ArmBot.tsx index a6d8d23..1c7bb83 100644 --- a/app/src/modules/simulation/armbot/ArmBot.tsx +++ b/app/src/modules/simulation/armbot/ArmBot.tsx @@ -12,7 +12,12 @@ interface ArmBotState { 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 StaticMachineState { @@ -45,7 +50,8 @@ const ArmBot = ({ armBots, setArmBots, setStaticMachines }: ArmBotProps) => { status: "idle", material: "default", triggerId: '', - actions: bot.points.actions + actions: bot.points.actions, + connections: bot.points.connections })); setArmBots(initialStates); }, [simulationStates]); diff --git a/app/src/modules/simulation/armbot/ArmBotInstances.tsx b/app/src/modules/simulation/armbot/ArmBotInstances.tsx index 10a49f6..8d20a87 100644 --- a/app/src/modules/simulation/armbot/ArmBotInstances.tsx +++ b/app/src/modules/simulation/armbot/ArmBotInstances.tsx @@ -18,16 +18,12 @@ interface ArmBotState { status: string; material: string; triggerId: string; - actions: { - uuid: string; - name: string; - speed: number; - processes: { - triggerId: string; - startPoint: string; - endPoint: 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 StaticMachineState { @@ -87,6 +83,7 @@ export const ArmbotInstances: React.FC = ({ index, armBot, rotation={armBot.rotation} processes={processes} armBot={armBot} + setArmBots={setArmBots} setStaticMachines={setStaticMachines} updateArmBotStatus={updateArmBotStatus} /> diff --git a/app/src/modules/simulation/armbot/IKAnimationController.tsx b/app/src/modules/simulation/armbot/IKAnimationController.tsx index 9f19797..d0aaec1 100644 --- a/app/src/modules/simulation/armbot/IKAnimationController.tsx +++ b/app/src/modules/simulation/armbot/IKAnimationController.tsx @@ -20,16 +20,12 @@ interface ArmBotState { status: string; material: string; triggerId: string; - actions: { - uuid: string; - name: string; - speed: number; - processes: { - triggerId: string; - startPoint: string; - endPoint: 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; } type IKAnimationControllerProps = { @@ -46,6 +42,7 @@ type IKAnimationControllerProps = { logStatus: (status: string) => void; groupRef: React.RefObject; armBot: ArmBotState; + setArmBots: React.Dispatch>; setStaticMachines: React.Dispatch>; updateArmBotStatus: (status: string) => void; } @@ -59,6 +56,7 @@ const IKAnimationController = ({ logStatus, groupRef, armBot, + setArmBots, setStaticMachines, updateArmBotStatus }: IKAnimationControllerProps) => { @@ -120,30 +118,61 @@ const IKAnimationController = ({ if (prev.selectedTrigger !== selectedTrigger) { logStatus(`[Arm ${uuid}] Processing new trigger: ${selectedTrigger}`); - const currentProcess = process.find(p => p.triggerId === prev.selectedTrigger); if (currentProcess) { const triggerId = currentProcess.triggerId; const endPoint = armBot.actions.processes.find((process) => process.triggerId === triggerId)?.endPoint; - // Search simulationStates for a StaticMachine that has a point matching this endPointId - const matchedStaticMachine = simulationStates.find( - (state) => - state.type === "StaticMachine" && - state.points?.uuid === endPoint// check for static machine with matching point uuid - ) as any; + // Search simulationStates for a StaticMachine or Conveyor that has a point matching this endPointId + const matchedMachine = simulationStates.find((state) => { + if (state.type === "Conveyor") { + // For Conveyor, points is an array + return (state).points.some( + (point) => point.uuid === endPoint + ); + } else if (state.type === "StaticMachine") { + // For StaticMachine, points is an object + return state.points.uuid === endPoint; + } + return false; + }); - if (matchedStaticMachine) { - setStaticMachines((machines) => { - return machines.map((machine) => { - if (machine.uuid === matchedStaticMachine.modeluuid) { - return { ...machine, status: "running" }; - } else { - return machine; - } + if (matchedMachine) { + // Log if the end point is a conveyor + if (matchedMachine.type === "Conveyor") { + logStatus(`[Arm ${uuid}] Reached end point which is a conveyor (${matchedMachine.modelName})`); + } else { + logStatus(`[Arm ${uuid}] Reached end point which is a static machine (${matchedMachine.modelName})`); + } + + if (matchedMachine.type === "StaticMachine") { + setStaticMachines((machines) => { + return machines.map((machine) => { + if (machine.uuid === matchedMachine.modeluuid) { + return { ...machine, status: "running" }; + } else { + return machine; + } + }); }); - }); + } + + if (matchedMachine.type === "Conveyor") { + setArmBots((prev) => + prev.map((arm) => { + if (arm.uuid === uuid) { + return { + ...arm, + isActive: false + }; + } + else { + return arm; + } + }) + ); + } } } } diff --git a/app/src/modules/simulation/armbot/IkInstances.tsx b/app/src/modules/simulation/armbot/IkInstances.tsx index 50b8ffb..5a0d23b 100644 --- a/app/src/modules/simulation/armbot/IkInstances.tsx +++ b/app/src/modules/simulation/armbot/IkInstances.tsx @@ -23,16 +23,12 @@ interface ArmBotState { status: string; material: string; triggerId: string; - actions: { - uuid: string; - name: string; - speed: number; - processes: { - triggerId: string; - startPoint: string; - endPoint: 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; } const IkInstances = ({ @@ -43,6 +39,7 @@ const IkInstances = ({ position, rotation, armBot, + setArmBots, setStaticMachines, updateArmBotStatus }: { @@ -53,6 +50,7 @@ const IkInstances = ({ position: [number, number, number]; rotation: [number, number, number]; armBot: ArmBotState; + setArmBots: React.Dispatch>; setStaticMachines: React.Dispatch>; updateArmBotStatus: (status: string) => void; }) => { @@ -141,6 +139,7 @@ const IkInstances = ({ logStatus={logStatus} groupRef={groupRef} armBot={armBot} + setArmBots={setArmBots} setStaticMachines={setStaticMachines} updateArmBotStatus={updateArmBotStatus} /> diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index 460de49..f01e799 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useMemo } from "react"; +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"; @@ -18,9 +18,13 @@ interface ArmBotState { 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>; @@ -103,9 +107,33 @@ const ProcessAnimator: React.FC = ({ // 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: any) => { + // 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] + ); + + // In processAnimator.tsx - only the relevant spawn logic part that needs fixes + useFrame(() => { // Spawn logic frame - const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; + const currentTime = + clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; @@ -119,6 +147,14 @@ const ProcessAnimator: React.FC = ({ return; } + if (isConnectedToActiveArmBot(process.id)) { + newStates[process.id] = { + ...processState, + nextSpawnTime: Infinity, // Prevent future spawns + }; + return; + } + const spawnPoint = findSpawnPoint(process); if (!spawnPoint || !spawnPoint.actions) return; @@ -133,7 +169,10 @@ const ProcessAnimator: React.FC = ({ : parseFloat(spawnAction.spawnInterval as string) || 0; // Check if this is a zero interval spawn and we already spawned an object - if (spawnInterval === 0 && processState.hasSpawnedZeroIntervalObject === true) { + if ( + spawnInterval === 0 && + processState.hasSpawnedZeroIntervalObject === true + ) { return; // Don't spawn more objects for zero interval } @@ -183,6 +222,29 @@ const ProcessAnimator: React.FC = ({ const processState = newStates[process.id]; if (!processState) return; + if (isConnectedToActiveArmBot(process.id)) { + 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; + } + if (processState.isProcessDelaying) { const effectiveDelayTime = processState.processDelayDuration / speedRef.current; @@ -338,10 +400,13 @@ const ProcessAnimator: React.FC = ({ if (isLastPoint) { const isAgvPicking = agvRef.current.some( - (agv: any) => agv.processId === process.id && agv.status === "picking" + (agv: any) => + agv.processId === process.id && agv.status === "picking" ); - const shouldHide = !currentPointData?.actions || !hasNonInheritActions(currentPointData.actions); + const shouldHide = + !currentPointData?.actions || + !hasNonInheritActions(currentPointData.actions); if (shouldHide) { if (isAgvPicking) { @@ -372,7 +437,8 @@ const ProcessAnimator: React.FC = ({ if (tempStackedObjectsRef.current[objectId]) { const isAgvPicking = agvRef.current.some( - (agv: any) => agv.processId === process.id && agv.status === "picking" + (agv: any) => + agv.processId === process.id && agv.status === "picking" ); if (isAgvPicking) { @@ -391,7 +457,8 @@ const ProcessAnimator: React.FC = ({ if (!isLastPoint) { const nextPoint = path[nextPointIdx]; - const distance = path[stateRef.currentIndex].distanceTo(nextPoint); + const distance = + path[stateRef.currentIndex].distanceTo(nextPoint); const effectiveSpeed = stateRef.speed * speedRef.current; const movement = effectiveSpeed * delta; @@ -442,7 +509,6 @@ const ProcessAnimator: React.FC = ({ return newStates; }); }); - if (!processedProcesses || processedProcesses.length === 0) { return null; } diff --git a/app/src/modules/simulation/process/processContainer.tsx b/app/src/modules/simulation/process/processContainer.tsx index 4cc7edf..0bcdb13 100644 --- a/app/src/modules/simulation/process/processContainer.tsx +++ b/app/src/modules/simulation/process/processContainer.tsx @@ -9,7 +9,12 @@ interface ArmBotState { 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 { diff --git a/app/src/modules/simulation/process/useProcessAnimations.tsx b/app/src/modules/simulation/process/useProcessAnimations.tsx index 8c0f20d..5fc96dc 100644 --- a/app/src/modules/simulation/process/useProcessAnimations.tsx +++ b/app/src/modules/simulation/process/useProcessAnimations.tsx @@ -1,645 +1,651 @@ import { useCallback, useEffect, useRef, useState } from "react"; import * as THREE from "three"; import { - ProcessData, - ProcessAnimationState, - SpawnedObject, - AnimationState, - ProcessPoint, - PointAction, - Trigger, + ProcessData, + ProcessAnimationState, + SpawnedObject, + AnimationState, + ProcessPoint, + PointAction, + Trigger, } from "./types"; import { - useAnimationPlaySpeed, - usePauseButtonStore, - usePlayButtonStore, - useResetButtonStore, + useAnimationPlaySpeed, + usePauseButtonStore, + usePlayButtonStore, + useResetButtonStore, } from "../../../store/usePlayButtonStore"; import { usePlayAgv } from "../../../store/store"; // Enhanced ProcessAnimationState with trigger tracking interface EnhancedProcessAnimationState extends ProcessAnimationState { - triggerCounts: Record; - triggerLogs: Array<{ - timestamp: number; - pointId: string; - objectId: string; - triggerId: string; - }>; + triggerCounts: Record; + triggerLogs: Array<{ + timestamp: number; + pointId: string; + objectId: string; + triggerId: string; + }>; } interface ProcessContainerProps { - processes: ProcessData[]; - setProcesses: React.Dispatch>; - agvRef: any; + processes: ProcessData[]; + setProcesses: React.Dispatch>; + agvRef: any; } interface PlayAgvState { - playAgv: Record; - setPlayAgv: (data: any) => void; + playAgv: Record; + setPlayAgv: (data: any) => void; } interface ArmBotState { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - status: string; - material: string; - triggerId: string; - actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + 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; } export const useProcessAnimation = ( - processes: ProcessData[], - setProcesses: React.Dispatch>, - agvRef: any, - armBots: ArmBotState[], - setArmBots: React.Dispatch> + processes: ProcessData[], + setProcesses: React.Dispatch>, + agvRef: any, + armBots: ArmBotState[], + setArmBots: React.Dispatch> ) => { - // State and refs initialization - const { isPlaying, setIsPlaying } = usePlayButtonStore(); - const { isPaused, setIsPaused } = usePauseButtonStore(); - const { isReset, setReset } = useResetButtonStore(); - const debugRef = useRef(false); - const clockRef = useRef(new THREE.Clock()); - const pauseTimeRef = useRef(0); - const elapsedBeforePauseRef = useRef(0); - const animationStatesRef = useRef>({}); - const { speed } = useAnimationPlaySpeed(); - const prevIsPlaying = useRef(null); - const [internalResetFlag, setInternalResetFlag] = useState(false); - const [animationStates, setAnimationStates] = useState>({}); - const speedRef = useRef(speed); - const { PlayAgv, setPlayAgv } = usePlayAgv(); + // State and refs initialization + const { isPlaying, setIsPlaying } = usePlayButtonStore(); + const { isPaused, setIsPaused } = usePauseButtonStore(); + const { isReset, setReset } = useResetButtonStore(); + const debugRef = useRef(false); + const clockRef = useRef(new THREE.Clock()); + const pauseTimeRef = useRef(0); + const elapsedBeforePauseRef = useRef(0); + const animationStatesRef = useRef>({}); + const { speed } = useAnimationPlaySpeed(); + const prevIsPlaying = useRef(null); + const [internalResetFlag, setInternalResetFlag] = useState(false); + const [animationStates, setAnimationStates] = useState>({}); + const speedRef = useRef(speed); + const { PlayAgv, setPlayAgv } = usePlayAgv(); - // Effect hooks - useEffect(() => { - speedRef.current = speed; - }, [speed]); + // Effect hooks + useEffect(() => { + speedRef.current = speed; + }, [speed]); - useEffect(() => { - if (prevIsPlaying.current !== null || !isPlaying) { - setAnimationStates({}); - } - prevIsPlaying.current = isPlaying; - }, [isPlaying]); + useEffect(() => { + if (prevIsPlaying.current !== null || !isPlaying) { + setAnimationStates({}); + } + prevIsPlaying.current = isPlaying; + }, [isPlaying]); - useEffect(() => { - animationStatesRef.current = animationStates; - }, [animationStates]); + useEffect(() => { + animationStatesRef.current = animationStates; + }, [animationStates]); - // Reset handler - useEffect(() => { - if (isReset) { - setInternalResetFlag(true); - setIsPlaying(false); - setIsPaused(false); - setAnimationStates({}); - animationStatesRef.current = {}; - clockRef.current = new THREE.Clock(); - elapsedBeforePauseRef.current = 0; - pauseTimeRef.current = 0; - setReset(false); - setTimeout(() => { - setInternalResetFlag(false); - setIsPlaying(true); - }, 0); - } - }, [isReset, setReset, setIsPlaying, setIsPaused]); + // Reset handler + useEffect(() => { + if (isReset) { + setInternalResetFlag(true); + setIsPlaying(false); + setIsPaused(false); + setAnimationStates({}); + animationStatesRef.current = {}; + clockRef.current = new THREE.Clock(); + elapsedBeforePauseRef.current = 0; + pauseTimeRef.current = 0; + setReset(false); + setTimeout(() => { + setInternalResetFlag(false); + setIsPlaying(true); + }, 0); + } + }, [isReset, setReset, setIsPlaying, setIsPaused]); - // Pause handler - useEffect(() => { - if (isPaused) { - pauseTimeRef.current = clockRef.current.getElapsedTime(); - } else if (pauseTimeRef.current > 0) { - const pausedDuration = clockRef.current.getElapsedTime() - pauseTimeRef.current; - elapsedBeforePauseRef.current += pausedDuration; - } - }, [isPaused]); + // Pause handler + 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 with trigger tracking - useEffect(() => { - if (isPlaying && !internalResetFlag) { - const newStates: Record = {}; + // Initialize animation states with trigger tracking + useEffect(() => { + if (isPlaying && !internalResetFlag) { + const newStates: Record = {}; - processes.forEach((process) => { - const triggerCounts: Record = {}; + processes.forEach((process) => { + const triggerCounts: Record = {}; - // Initialize trigger counts for all On-Hit triggers - process.paths?.forEach((path) => { - path.points?.forEach((point) => { - point.triggers?.forEach((trigger: Trigger) => { - if (trigger.type === "On-Hit" && trigger.isUsed) { - triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; - } - }); - }); - }); - - newStates[process.id] = { - spawnedObjects: {}, - nextSpawnTime: 0, - objectIdCounter: 0, - isProcessDelaying: false, - processDelayStartTime: 0, - processDelayDuration: 0, - triggerCounts, - triggerLogs: [], - }; + // Initialize trigger counts for all On-Hit triggers + process.paths?.forEach((path) => { + path.points?.forEach((point) => { + point.triggers?.forEach((trigger: Trigger) => { + if (trigger.type === "On-Hit" && trigger.isUsed) { + triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; + } }); + }); + }); - setAnimationStates(newStates); - animationStatesRef.current = newStates; - clockRef.current.start(); - } - }, [isPlaying, processes, internalResetFlag]); + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + isProcessDelaying: false, + processDelayStartTime: 0, + processDelayDuration: 0, + triggerCounts, + triggerLogs: [], + }; + }); - useEffect(() => { - if (isPlaying && !internalResetFlag) { - const newStates: Record = {}; + setAnimationStates(newStates); + animationStatesRef.current = newStates; + clockRef.current.start(); + } + }, [isPlaying, processes, internalResetFlag]); - // Initialize AGVs for each process first - processes.forEach((process) => { - // Find all vehicle paths for this process - const vehiclePaths = process.paths?.filter( - (path) => path.type === "Vehicle" - ) || []; + useEffect(() => { + if (isPlaying && !internalResetFlag) { + const newStates: Record = {}; - // Initialize AGVs for each vehicle path - vehiclePaths.forEach((vehiclePath) => { - if (vehiclePath.points?.length > 0) { - const vehiclePoint = vehiclePath.points[0]; - const action = vehiclePoint.actions?.[0]; - const maxHitCount = action?.hitCount; + // Initialize AGVs for each process first + processes.forEach((process) => { + // Find all vehicle paths for this process + const vehiclePaths = process.paths?.filter( + (path) => path.type === "Vehicle" + ) || []; - const vehicleId = vehiclePath.modeluuid; - const processId = process.id; + // Initialize AGVs for each vehicle path + vehiclePaths.forEach((vehiclePath) => { + if (vehiclePath.points?.length > 0) { + const vehiclePoint = vehiclePath.points[0]; + const action = vehiclePoint.actions?.[0]; + const maxHitCount = action?.hitCount; - // Check if this AGV already exists - const existingAgv = agvRef.current.find( - (v: any) => v.vehicleId === vehicleId && v.processId === processId - ); + const vehicleId = vehiclePath.modeluuid; + const processId = process.id; - if (!existingAgv) { - // Initialize the AGV in a stationed state - agvRef.current.push({ - processId, - vehicleId, - maxHitCount: maxHitCount || 0, - isActive: false, - hitCount: 0, - status: 'stationed', - lastUpdated: 0 - }); - } - } - }); + // Check if this AGV already exists + const existingAgv = agvRef.current.find( + (v: any) => v.vehicleId === vehicleId && v.processId === processId + ); - // Then initialize trigger counts as before - const triggerCounts: Record = {}; - process.paths?.forEach((path) => { - path.points?.forEach((point) => { - point.triggers?.forEach((trigger: Trigger) => { - if (trigger.type === "On-Hit" && trigger.isUsed) { - triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; - } - }); - }); - }); + if (!existingAgv) { + // Initialize the AGV in a stationed state + agvRef.current.push({ + processId, + vehicleId, + maxHitCount: maxHitCount || 0, + isActive: false, + hitCount: 0, + status: 'stationed', + lastUpdated: 0 + }); + } + } + }); - newStates[process.id] = { - spawnedObjects: {}, - nextSpawnTime: 0, - objectIdCounter: 0, - isProcessDelaying: false, - processDelayStartTime: 0, - processDelayDuration: 0, - triggerCounts, - triggerLogs: [], - }; + // Then initialize trigger counts as before + const triggerCounts: Record = {}; + process.paths?.forEach((path) => { + path.points?.forEach((point) => { + point.triggers?.forEach((trigger: Trigger) => { + if (trigger.type === "On-Hit" && trigger.isUsed) { + triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; + } }); + }); + }); - setAnimationStates(newStates); - animationStatesRef.current = newStates; - clockRef.current.start(); - } - }, [isPlaying, processes, internalResetFlag]); + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + isProcessDelaying: false, + processDelayStartTime: 0, + processDelayDuration: 0, + triggerCounts, + triggerLogs: [], + }; + }); - // Helper functions - 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; - }; + setAnimationStates(newStates); + animationStatesRef.current = newStates; + clockRef.current.start(); + } + }, [isPlaying, processes, internalResetFlag]); - 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] + // Helper functions + 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" ); - }; - - // Optimized object creation - const createSpawnedObject = useCallback( - ( - process: ProcessData, - currentTime: number, - materialType: string, - spawnPoint: ProcessPoint, - baseMaterials: Record - ): SpawnedObject => { - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const spawnPosition = findAnimationPathPoint(process, spawnPoint); - const material = - processMaterials[materialType as keyof typeof processMaterials] || - baseMaterials.Default; - - return { - ref: { current: null }, - state: { - currentIndex: 0, - progress: 0, - isAnimating: true, - speed: process.speed || 1, - isDelaying: false, - delayStartTime: 0, - currentDelayDuration: 0, - delayComplete: false, - currentPathIndex: 0, - }, - visible: true, - material: material, - currentMaterialType: materialType, - spawnTime: currentTime, - position: spawnPosition, - }; - }, - [] - ); - - // Material handling - const handleMaterialSwap = useCallback( - ( - processId: string, - objectId: string, - materialType: string, - processes: ProcessData[], - baseMaterials: Record - ) => { - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState || !processState.spawnedObjects[objectId]) - return prev; - - const process = processes.find((p) => p.id === processId); - if (!process) return prev; - - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const newMaterial = - processMaterials[materialType as keyof typeof processMaterials]; - if (!newMaterial) return prev; - - return { - ...prev, - [processId]: { - ...processState, - spawnedObjects: { - ...processState.spawnedObjects, - [objectId]: { - ...processState.spawnedObjects[objectId], - material: newMaterial, - currentMaterialType: materialType, - }, - }, - }, - }; - }); - }, - [] - ); - - // Point action handler with trigger counting - const handlePointActions = useCallback( - ( - processId: string, - objectId: string, - actions: PointAction[] = [], - currentTime: number, - processes: ProcessData[], - baseMaterials: Record - ): 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, - 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, - processes, - baseMaterials - ); - } - break; - - default: - break; - } - }); - - return shouldStopAnimation; - }, - [handleMaterialSwap] - ); - - const deferredArmBotUpdates = useRef<{ uuid: string; triggerId: string }[]>([]); - - // Trigger counting system - const checkAndCountTriggers = useCallback( - ( - processId: string, - objectId: string, - currentPointIndex: number, - processes: ProcessData[], - currentTime: number - ) => { - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState) return prev; - - const process = processes.find((p) => p.id === processId); - if (!process) return prev; - - const point = getPointDataForAnimationIndex(process, currentPointIndex); - if (!point?.triggers) return prev; - - const onHitTriggers = point.triggers.filter((t: Trigger) => t.type === "On-Hit" && t.isUsed); - - if (onHitTriggers.length === 0) return prev; - - let newTriggerCounts = { ...processState.triggerCounts }; - const newTriggerLogs = [...processState.triggerLogs]; - let shouldLog = false; - - const vehiclePaths = process.paths.filter((path) => path.type === "Vehicle"); - const armBotPaths = process.paths.filter((path) => path.type === "ArmBot"); - - const activeVehicles = vehiclePaths.filter((path) => { - const vehicleId = path.modeluuid; - const vehicleEntry = agvRef.current.find((v: any) => v.vehicleId === vehicleId && v.processId === processId); - return vehicleEntry?.isActive; - }); - - // Check if any ArmBot is active for this process - // const activeArmBots = armBotPaths.filter((path) => { - // const armBotId = path.modeluuid; - // const armBotEntry = armBots.find((a: any) => a.uuid === armBotId); - // return armBotEntry; - // }); - - // Only count triggers if no vehicles and no ArmBots are active for this process - - if (activeVehicles.length === 0) { - onHitTriggers.forEach((trigger: Trigger) => { - const connections = point.connections?.targets || []; - - connections.forEach((connection) => { - const connectedModelUUID = connection.modelUUID; - - const matchingArmPath = armBotPaths.find((path) => path.modeluuid === connectedModelUUID); - - if (matchingArmPath) { - deferredArmBotUpdates.current.push({ - uuid: connectedModelUUID, - triggerId: trigger.uuid, - }); - } - }); - }); - } - - let processTotalHits = Object.values(newTriggerCounts).reduce((a, b) => a + b, 0); - - // Handle logic for vehicles and ArmBots when a trigger is hit - if (shouldLog) { - vehiclePaths.forEach((vehiclePath) => { - if (vehiclePath.points?.length > 0) { - const vehiclePoint = vehiclePath.points[0]; - const action = vehiclePoint.actions?.[0]; - const maxHitCount = action?.hitCount; - - if (maxHitCount !== undefined) { - const vehicleId = vehiclePath.modeluuid; - let vehicleEntry = agvRef.current.find( - (v: any) => - v.vehicleId === vehicleId && v.processId === processId - ); - - if (!vehicleEntry) { - vehicleEntry = { - processId, - vehicleId, - maxHitCount: maxHitCount, - isActive: false, - hitCount: 0, - status: "stationed", - }; - agvRef.current.push(vehicleEntry); - } - - if (!vehicleEntry.isActive) { - vehicleEntry.hitCount++; - vehicleEntry.lastUpdated = currentTime; - - if (vehicleEntry.hitCount >= vehicleEntry.maxHitCount) { - vehicleEntry.isActive = true; - newTriggerCounts = {}; - processTotalHits = 0; - } - } - } - } - }); - } - - return { - ...prev, - [processId]: { - ...processState, - triggerCounts: newTriggerCounts, - triggerLogs: newTriggerLogs, - totalHits: processTotalHits, - }, - }; - }); - }, []); - - useEffect(() => { - if (deferredArmBotUpdates.current.length > 0) { - const updates = [...deferredArmBotUpdates.current]; - deferredArmBotUpdates.current = []; - - setArmBots((prev) => - prev.map((bot) => { - const update = updates.find((u) => u.uuid === bot.uuid); - return update ? { ...bot, triggerId: update.triggerId } : bot; - }) - ); + if (spawnAction) { + return point; } - }, [animationStates]); + } + } + return null; + }; - // Utility functions - const hasNonInheritActions = useCallback( - (actions: PointAction[] = []): boolean => { - return actions.some( - (action) => action.isUsed && action.type !== "Inherit" - ); - }, - [] + 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 getPointDataForAnimationIndex = useCallback( - (process: ProcessData, index: number): ProcessPoint | null => { - if (!process.paths) return null; + // Optimized object creation + const createSpawnedObject = useCallback( + ( + process: ProcessData, + currentTime: number, + materialType: string, + spawnPoint: ProcessPoint, + baseMaterials: Record + ): SpawnedObject => { + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; - let cumulativePoints = 0; - for (const path of process.paths) { - const pointCount = path.points?.length || 0; + const spawnPosition = findAnimationPathPoint(process, spawnPoint); + const material = + processMaterials[materialType as keyof typeof processMaterials] || + baseMaterials.Default; - if (index < cumulativePoints + pointCount) { - const pointIndex = index - cumulativePoints; - return path.points?.[pointIndex] || null; + return { + ref: { current: null }, + state: { + currentIndex: 0, + progress: 0, + isAnimating: true, + speed: process.speed || 1, + isDelaying: false, + delayStartTime: 0, + currentDelayDuration: 0, + delayComplete: false, + currentPathIndex: 0, + }, + visible: true, + material: material, + currentMaterialType: materialType, + spawnTime: currentTime, + position: spawnPosition, + }; + }, + [] + ); + + // Material handling + const handleMaterialSwap = useCallback( + ( + processId: string, + objectId: string, + materialType: string, + processes: ProcessData[], + baseMaterials: Record + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState || !processState.spawnedObjects[objectId]) + return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; + + const newMaterial = + processMaterials[materialType as keyof typeof processMaterials]; + if (!newMaterial) return prev; + + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + material: newMaterial, + currentMaterialType: materialType, + }, + }, + }, + }; + }); + }, + [] + ); + + // Point action handler with trigger counting + const handlePointActions = useCallback( + ( + processId: string, + objectId: string, + actions: PointAction[] = [], + currentTime: number, + processes: ProcessData[], + baseMaterials: Record + ): 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, + 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, + processes, + baseMaterials + ); + } + break; + + default: + break; + } + }); + + return shouldStopAnimation; + }, + [handleMaterialSwap] + ); + + const deferredArmBotUpdates = useRef<{ uuid: string; triggerId: string }[]>([]); + + // Trigger counting system + const checkAndCountTriggers = useCallback( + ( + processId: string, + objectId: string, + currentPointIndex: number, + processes: ProcessData[], + currentTime: number + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState) return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const point = getPointDataForAnimationIndex(process, currentPointIndex); + if (!point?.triggers) return prev; + + const onHitTriggers = point.triggers.filter((t: Trigger) => t.type === "On-Hit" && t.isUsed); + + if (onHitTriggers.length === 0) return prev; + + let newTriggerCounts = { ...processState.triggerCounts }; + const newTriggerLogs = [...processState.triggerLogs]; + let shouldLog = false; + + const vehiclePaths = process.paths.filter((path) => path.type === "Vehicle"); + const armBotPaths = process.paths.filter((path) => path.type === "ArmBot"); + + const activeVehicles = vehiclePaths.filter((path) => { + const vehicleId = path.modeluuid; + const vehicleEntry = agvRef.current.find((v: any) => v.vehicleId === vehicleId && v.processId === processId); + return vehicleEntry?.isActive; + }); + + // Check if any ArmBot is active for this process + // const activeArmBots = armBotPaths.filter((path) => { + // const armBotId = path.modeluuid; + // const armBotEntry = armBots.find((a: any) => a.uuid === armBotId); + // return armBotEntry; + // }); + + // Only count triggers if no vehicles and no ArmBots are active for this process + + if (activeVehicles.length === 0) { + onHitTriggers.forEach((trigger: Trigger) => { + const connections = point.connections?.targets || []; + + connections.forEach((connection) => { + const connectedModelUUID = connection.modelUUID; + + const matchingArmPath = armBotPaths.find((path) => path.modeluuid === connectedModelUUID); + + if (matchingArmPath) { + deferredArmBotUpdates.current.push({ + uuid: connectedModelUUID, + triggerId: trigger.uuid, + }); + } + }); + }); + } + + let processTotalHits = Object.values(newTriggerCounts).reduce((a, b) => a + b, 0); + + // Handle logic for vehicles and ArmBots when a trigger is hit + if (shouldLog) { + vehiclePaths.forEach((vehiclePath) => { + if (vehiclePath.points?.length > 0) { + const vehiclePoint = vehiclePath.points[0]; + const action = vehiclePoint.actions?.[0]; + const maxHitCount = action?.hitCount; + + if (maxHitCount !== undefined) { + const vehicleId = vehiclePath.modeluuid; + let vehicleEntry = agvRef.current.find( + (v: any) => + v.vehicleId === vehicleId && v.processId === processId + ); + + if (!vehicleEntry) { + vehicleEntry = { + processId, + vehicleId, + maxHitCount: maxHitCount, + isActive: false, + hitCount: 0, + status: "stationed", + }; + agvRef.current.push(vehicleEntry); } - cumulativePoints += pointCount; + if (!vehicleEntry.isActive) { + vehicleEntry.hitCount++; + vehicleEntry.lastUpdated = currentTime; + + if (vehicleEntry.hitCount >= vehicleEntry.maxHitCount) { + vehicleEntry.isActive = true; + newTriggerCounts = {}; + processTotalHits = 0; + } + } + } } + }); + } - return null; - }, - [] - ); - - const getTriggerCounts = useCallback((processId: string) => { - return animationStatesRef.current[processId]?.triggerCounts || {}; + return { + ...prev, + [processId]: { + ...processState, + triggerCounts: newTriggerCounts, + triggerLogs: newTriggerLogs, + totalHits: processTotalHits, + }, + }; + }); }, []); - const getTriggerLogs = useCallback((processId: string) => { - return animationStatesRef.current[processId]?.triggerLogs || []; + useEffect(() => { + if (deferredArmBotUpdates.current.length > 0) { + const updates = [...deferredArmBotUpdates.current]; + deferredArmBotUpdates.current = []; + + setArmBots((prev) => + prev.map((bot) => { + const update = updates.find((u) => u.uuid === bot.uuid); + + return update + ? { ...bot, triggerId: update.triggerId, isActive: true } + : bot; + }) + ); + } + }, [animationStates]); + + // Utility functions + const hasNonInheritActions = useCallback( + (actions: PointAction[] = []): boolean => { + return actions.some( + (action) => action.isUsed && action.type !== "Inherit" + ); }, []); - return { - animationStates, - setAnimationStates, - clockRef, - elapsedBeforePauseRef, - speedRef, - debugRef, - findSpawnPoint, - createSpawnedObject, - handlePointActions, - hasNonInheritActions, - getPointDataForAnimationIndex, - checkAndCountTriggers, - getTriggerCounts, - getTriggerLogs, - processes, - }; + const getPointDataForAnimationIndex = useCallback( + (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; + }, + [] + ); + + const getTriggerCounts = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerCounts || {}; + }, []); + + const getTriggerLogs = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerLogs || []; + }, []); + + return { + animationStates, + setAnimationStates, + clockRef, + elapsedBeforePauseRef, + speedRef, + debugRef, + findSpawnPoint, + createSpawnedObject, + handlePointActions, + hasNonInheritActions, + getPointDataForAnimationIndex, + checkAndCountTriggers, + getTriggerCounts, + getTriggerLogs, + processes, + }; }; diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx index 2f66ab8..84d3651 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -15,7 +15,12 @@ interface ArmBotState { 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 StaticMachineState { diff --git a/app/src/modules/simulation/staticMachine/staticMachine.tsx b/app/src/modules/simulation/staticMachine/staticMachine.tsx index ba9b4f0..26760eb 100644 --- a/app/src/modules/simulation/staticMachine/staticMachine.tsx +++ b/app/src/modules/simulation/staticMachine/staticMachine.tsx @@ -10,9 +10,13 @@ interface ArmBotState { 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 StaticMachineState { uuid: string; status: string;