diff --git a/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx b/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx index e157d07..8b05525 100644 --- a/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx +++ b/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx @@ -150,6 +150,7 @@ const ArmBotMechanics: React.FC = () => { modeluuid: updatedPath.modeluuid, eventData: { type: "ArmBot", points: updatedPath.points } } + console.log('data: ', data); socket.emit('v2:model-asset:updateEventData', data); } diff --git a/app/src/modules/builder/agv/pathNavigator.tsx b/app/src/modules/builder/agv/pathNavigator.tsx index 9a6fae7..a81925f 100644 --- a/app/src/modules/builder/agv/pathNavigator.tsx +++ b/app/src/modules/builder/agv/pathNavigator.tsx @@ -3,7 +3,10 @@ import * as THREE from "three"; import { useFrame, useThree } from "@react-three/fiber"; import { NavMeshQuery } from "@recast-navigation/core"; import { Line } from "@react-three/drei"; -import { useAnimationPlaySpeed, usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import { + useAnimationPlaySpeed, + usePlayButtonStore, +} from "../../../store/usePlayButtonStore"; import { usePlayAgv } from "../../../store/store"; interface PathNavigatorProps { @@ -11,7 +14,7 @@ interface PathNavigatorProps { pathPoints: any; id: string; speed: number; - globalSpeed: number, + globalSpeed: number; bufferTime: number; hitCount: number; processes: any[]; @@ -40,11 +43,21 @@ export default function PathNavigator({ }: PathNavigatorProps) { const [currentPhase, setCurrentPhase] = useState("initial"); const [path, setPath] = useState<[number, number, number][]>([]); - const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>([]); - const [pickupDropPath, setPickupDropPath] = useState<[number, number, number][]>([]); - const [dropPickupPath, setDropPickupPath] = useState<[number, number, number][]>([]); - const [initialPosition, setInitialPosition] = useState(null); - const [initialRotation, setInitialRotation] = useState(null); + const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>( + [] + ); + const [pickupDropPath, setPickupDropPath] = useState< + [number, number, number][] + >([]); + const [dropPickupPath, setDropPickupPath] = useState< + [number, number, number][] + >([]); + const [initialPosition, setInitialPosition] = useState( + null + ); + const [initialRotation, setInitialRotation] = useState( + null + ); const [boxVisible, setBoxVisible] = useState(false); const distancesRef = useRef([]); @@ -61,11 +74,14 @@ export default function PathNavigator({ const boxRef = useRef(null); - const baseMaterials = useMemo(() => ({ - Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), - Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), - Default: new THREE.MeshStandardMaterial({ color: 0xcccccc }) - }), []); + const baseMaterials = useMemo( + () => ({ + Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), + Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + Default: new THREE.MeshStandardMaterial({ color: 0xcccccc }), + }), + [] + ); useEffect(() => { const object = scene.getObjectByProperty("uuid", id); @@ -135,7 +151,11 @@ export default function PathNavigator({ const pickupToDropPath = computePath(pickup, drop); const dropToPickupPath = computePath(drop, pickup); - if (toPickupPath.length && pickupToDropPath.length && dropToPickupPath.length) { + if ( + toPickupPath.length && + pickupToDropPath.length && + dropToPickupPath.length + ) { setPickupDropPath(pickupToDropPath); setDropPickupPath(dropToPickupPath); setToPickupPath(toPickupPath); @@ -163,7 +183,10 @@ export default function PathNavigator({ }, [path]); function logAgvStatus(id: string, status: string) { - // console.log(`AGV ${id}: ${status}`); + // console.log( + // `AGV ${id}: ${status}` + + // ); } function findProcessByTargetModelUUID(processes: any, targetModelUUID: any) { @@ -223,7 +246,9 @@ export default function PathNavigator({ }, [processes, MaterialRef, boxVisible, scene, id, baseMaterials]); useFrame((_, delta) => { - const currentAgv = (agvRef.current || []).find((agv: AGVData) => agv.vehicleId === id); + const currentAgv = (agvRef.current || []).find( + (agv: AGVData) => agv.vehicleId === id + ); if (!scene || !id || !isPlaying) return; @@ -243,6 +268,7 @@ export default function PathNavigator({ const isAgvReady = () => { if (!agvRef.current || agvRef.current.length === 0) return false; if (!currentAgv) return false; + return currentAgv.isActive && hitCount >= currentAgv.maxHitCount; }; @@ -266,12 +292,19 @@ export default function PathNavigator({ } if (isPlaying && currentPhase === "initial" && !hasReachedPickup.current) { - const reached = moveAlongPath(object, path, distancesRef.current, speed, delta, progressRef); + const reached = moveAlongPath( + object, + path, + distancesRef.current, + speed, + delta, + progressRef + ); if (reached) { hasReachedPickup.current = true; if (currentAgv) { - currentAgv.status = 'picking'; + currentAgv.status = "picking"; } logAgvStatus(id, "Reached pickup point, Waiting for material"); } @@ -287,20 +320,28 @@ export default function PathNavigator({ progressRef.current = 0; logAgvStatus(id, "Started from pickup point, heading to drop point"); if (currentAgv) { - currentAgv.status = 'toDrop'; + currentAgv.status = "toDrop"; } - }, 0) + }, 0); return; } if (isPlaying && currentPhase === "toDrop") { - const reached = moveAlongPath(object, path, distancesRef.current, speed, delta, progressRef); + const reached = moveAlongPath( + object, + path, + distancesRef.current, + speed, + delta, + progressRef + ); if (reached && !isWaiting.current) { isWaiting.current = true; logAgvStatus(id, "Reached drop point"); if (currentAgv) { - currentAgv.status = 'droping'; + currentAgv.status = "droping"; + currentAgv.hitCount = currentAgv.hitCount--; } timeoutRef.current = setTimeout(() => { setPath([...dropPickupPath]); @@ -309,16 +350,26 @@ export default function PathNavigator({ isWaiting.current = false; setBoxVisible(false); if (currentAgv) { - currentAgv.status = 'toPickup'; + currentAgv.status = "toPickup"; } - logAgvStatus(id, "Started from droping point, heading to pickup point"); + logAgvStatus( + id, + "Started from droping point, heading to pickup point" + ); }, bufferTime * 1000); } return; } if (isPlaying && currentPhase === "toPickup") { - const reached = moveAlongPath(object, path, distancesRef.current, speed, delta, progressRef); + const reached = moveAlongPath( + object, + path, + distancesRef.current, + speed, + delta, + progressRef + ); if (reached) { if (currentAgv) { @@ -326,14 +377,21 @@ export default function PathNavigator({ } setCurrentPhase("initial"); if (currentAgv) { - currentAgv.status = 'picking'; + currentAgv.status = "picking"; } logAgvStatus(id, "Reached pickup point again, cycle complete"); } return; } - moveAlongPath(object, path, distancesRef.current, speed, delta, progressRef); + moveAlongPath( + object, + path, + distancesRef.current, + speed, + delta, + progressRef + ); }); function moveAlongPath( @@ -379,7 +437,11 @@ export default function PathNavigator({ const targetRotationY = Math.atan2(targetDirection.x, targetDirection.z); const rotationSpeed = Math.min(5 * delta, 1); - object.rotation.y = THREE.MathUtils.lerp(object.rotation.y, targetRotationY, rotationSpeed); + object.rotation.y = THREE.MathUtils.lerp( + object.rotation.y, + targetRotationY, + rotationSpeed + ); } return false; diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index c1063c3..65bc4c5 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -8,8 +8,7 @@ import crate from "../../../assets/gltf-glb/crate_box.glb"; import { useProcessAnimation } from "./useProcessAnimations"; import ProcessObject from "./processObject"; import { ProcessData } from "./types"; -import { useSimulationStates } from "../../../store/store"; -import { retrieveGLTF } from "../../../utils/indexDB/idbUtils"; + interface ProcessContainerProps { processes: ProcessData[]; @@ -44,19 +43,23 @@ const ProcessAnimator: React.FC = ({ checkAndCountTriggers, } = useProcessAnimation(processes, setProcesses, agvRef); - const baseMaterials = useMemo(() => ({ - Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), - Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), - Default: new THREE.MeshStandardMaterial(), - }), []); + 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 entry = { processId, objectId }; - const materialType = processState.spawnedObjects[objectId]?.currentMaterialType; + const materialType = + processState.spawnedObjects[objectId]?.currentMaterialType; if (!materialType) { return; @@ -65,13 +68,17 @@ const ProcessAnimator: React.FC = ({ const matRefArray = MaterialRef.current; // Find existing material group - const existing = matRefArray.find((entryGroup: { material: string; objects: any[] }) => - entryGroup.material === materialType + 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); + const alreadyExists = existing.objects.some( + (o: any) => + o.processId === entry.processId && o.objectId === entry.objectId + ); if (!alreadyExists) { existing.objects.push(entry); @@ -91,7 +98,8 @@ const ProcessAnimator: React.FC = ({ useFrame(() => { // Spawn logic frame - const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; + const currentTime = + clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; @@ -119,7 +127,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 } @@ -324,10 +335,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) { @@ -358,7 +372,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) { @@ -377,7 +392,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; diff --git a/app/src/modules/simulation/process/processCreator.tsx b/app/src/modules/simulation/process/processCreator.tsx index f7f9974..c46791f 100644 --- a/app/src/modules/simulation/process/processCreator.tsx +++ b/app/src/modules/simulation/process/processCreator.tsx @@ -1,450 +1,3 @@ -// import React, { -// useEffect, -// useMemo, -// useState, -// useCallback, -// useRef, -// } from "react"; -// import { useSimulationStates } from "../../../store/store"; -// import * as THREE from "three"; -// import { useThree } from "@react-three/fiber"; -// import { -// ConveyorEventsSchema, -// VehicleEventsSchema, -// } from "../../../types/world/worldTypes"; - -// // Type definitions -// export interface PointAction { -// uuid: string; -// name: string; -// type: string; -// material: string; -// delay: number | string; -// spawnInterval: string | number; -// isUsed: boolean; -// } - -// export interface PointTrigger { -// uuid: string; -// bufferTime: number; -// name: string; -// type: string; -// isUsed: boolean; -// } - -// export interface PathPoint { -// uuid: string; -// position: [number, number, number]; -// actions: PointAction[]; -// triggers: PointTrigger[]; -// connections: { -// targets: Array<{ modelUUID: string }>; -// }; -// } - -// export interface SimulationPath { -// type: string; -// modeluuid: string; -// points: PathPoint[]; -// pathPosition: [number, number, number]; -// speed?: number; -// } - -// export interface Process { -// id: string; -// paths: SimulationPath[]; -// animationPath: THREE.Vector3[]; -// pointActions: PointAction[][]; -// pointTriggers: PointTrigger[][]; -// speed: number; -// isActive: boolean; -// } - -// interface ProcessCreatorProps { -// onProcessesCreated: (processes: Process[]) => void; -// } - -// // Convert event schemas to SimulationPath -// function convertToSimulationPath( -// path: ConveyorEventsSchema | VehicleEventsSchema -// ): SimulationPath { -// const { modeluuid } = path; - -// // Normalized action handler -// const normalizeAction = (action: any): PointAction => { -// return { ...action }; // Return exact copy with no modifications -// }; - -// // Normalized trigger handler -// const normalizeTrigger = (trigger: any): PointTrigger => { -// return { ...trigger }; // Return exact copy with no modifications -// }; - -// if (path.type === "Conveyor") { -// return { -// type: path.type, -// modeluuid, -// points: path.points.map((point) => ({ -// uuid: point.uuid, -// position: point.position, -// actions: Array.isArray(point.actions) -// ? point.actions.map(normalizeAction) -// : point.actions -// ? [normalizeAction(point.actions)] -// : [], -// triggers: Array.isArray(point.triggers) -// ? point.triggers.map(normalizeTrigger) -// : point.triggers -// ? [normalizeTrigger(point.triggers)] -// : [], -// connections: { -// targets: point.connections.targets.map((target) => ({ -// modelUUID: target.modelUUID, -// })), -// }, -// })), -// pathPosition: path.position, -// speed: -// typeof path.speed === "string" -// ? parseFloat(path.speed) || 1 -// : path.speed || 1, -// }; -// } else { -// // For vehicle paths, handle the case where triggers might not exist -// return { -// type: path.type, -// modeluuid, -// points: [ -// { -// uuid: path.points.uuid, -// position: path.points.position, -// actions: Array.isArray(path.points.actions) -// ? path.points.actions.map(normalizeAction) -// : path.points.actions -// ? [normalizeAction(path.points.actions)] -// : [], -// // For vehicle paths, since triggers might not exist in the schema, -// // we always define default to an empty array -// triggers: [], -// connections: { -// targets: path.points.connections.targets.map((target) => ({ -// modelUUID: target.modelUUID, -// })), -// }, -// }, -// ], -// pathPosition: path.position, -// speed: path.points.speed || 1, -// }; -// } -// } - -// // Helper function to create an empty process -// const createEmptyProcess = (): Process => ({ -// id: `process-${Math.random().toString(36).substring(2, 11)}`, -// paths: [], -// animationPath: [], -// pointActions: [], -// pointTriggers: [], // Added point triggers array -// speed: 1, -// isActive: false, -// }); - -// // Enhanced connection checking function -// function shouldReverseNextPath( -// currentPath: SimulationPath, -// nextPath: SimulationPath -// ): boolean { -// if (nextPath.points.length !== 3) return false; - -// const currentLastPoint = currentPath.points[currentPath.points.length - 1]; -// const nextFirstPoint = nextPath.points[0]; -// const nextLastPoint = nextPath.points[nextPath.points.length - 1]; - -// // Check if current last connects to next last (requires reversal) -// const connectsToLast = currentLastPoint.connections.targets.some( -// (target) => -// target.modelUUID === nextPath.modeluuid && -// nextLastPoint.connections.targets.some( -// (t) => t.modelUUID === currentPath.modeluuid -// ) -// ); - -// // Check if current last connects to next first (no reversal needed) -// const connectsToFirst = currentLastPoint.connections.targets.some( -// (target) => -// target.modelUUID === nextPath.modeluuid && -// nextFirstPoint.connections.targets.some( -// (t) => t.modelUUID === currentPath.modeluuid -// ) -// ); - -// // Only reverse if connected to last point and not to first point -// return connectsToLast && !connectsToFirst; -// } - -// // Check if a point has a spawn action -// function hasSpawnAction(point: PathPoint): boolean { -// return point.actions.some((action) => action.type.toLowerCase() === "spawn"); -// } - -// // Ensure spawn point is always at the beginning of the path -// function ensureSpawnPointIsFirst(path: SimulationPath): SimulationPath { -// if (path.points.length !== 3) return path; - -// // If the third point has spawn action and first doesn't, reverse the array -// if (hasSpawnAction(path.points[2]) && !hasSpawnAction(path.points[0])) { -// return { -// ...path, -// points: [...path.points].reverse(), -// }; -// } - -// return path; -// } - -// // Updated path adjustment function -// function adjustPathPointsOrder(paths: SimulationPath[]): SimulationPath[] { -// if (paths.length < 1) return paths; - -// const adjustedPaths = [...paths]; - -// // First ensure all paths have spawn points at the beginning -// for (let i = 0; i < adjustedPaths.length; i++) { -// adjustedPaths[i] = ensureSpawnPointIsFirst(adjustedPaths[i]); -// } - -// // Then handle connections between paths -// for (let i = 0; i < adjustedPaths.length - 1; i++) { -// const currentPath = adjustedPaths[i]; -// const nextPath = adjustedPaths[i + 1]; - -// if (shouldReverseNextPath(currentPath, nextPath)) { -// const reversedPoints = [ -// nextPath.points[2], -// nextPath.points[1], -// nextPath.points[0], -// ]; - -// adjustedPaths[i + 1] = { -// ...nextPath, -// points: reversedPoints, -// }; -// } -// } - -// return adjustedPaths; -// } - -// // Main hook for process creation -// export function useProcessCreation() { -// const { scene } = useThree(); -// const [processes, setProcesses] = useState([]); - -// const hasSpawnAction = useCallback((path: SimulationPath): boolean => { -// if (path.type !== "Conveyor") return false; -// return path.points.some((point) => -// point.actions.some((action) => action.type.toLowerCase() === "spawn") -// ); -// }, []); - -// const createProcess = useCallback( -// (paths: SimulationPath[]): Process => { -// if (!paths || paths.length === 0) { -// return createEmptyProcess(); -// } - -// const animationPath: THREE.Vector3[] = []; -// const pointActions: PointAction[][] = []; -// const pointTriggers: PointTrigger[][] = []; // Added point triggers collection -// const processSpeed = paths[0]?.speed || 1; - -// for (const path of paths) { -// for (const point of path.points) { -// if (path.type === "Conveyor") { -// const obj = scene.getObjectByProperty("uuid", point.uuid); -// if (!obj) { -// console.warn(`Object with UUID ${point.uuid} not found in scene`); -// continue; -// } - -// const position = obj.getWorldPosition(new THREE.Vector3()); -// animationPath.push(position.clone()); -// pointActions.push(point.actions); -// pointTriggers.push(point.triggers); // Collect triggers for each point -// } -// } -// } - -// return { -// id: `process-${Math.random().toString(36).substring(2, 11)}`, -// paths, -// animationPath, -// pointActions, -// pointTriggers, -// speed: processSpeed, -// isActive: false, -// }; -// }, -// [scene] -// ); - -// const getAllConnectedPaths = useCallback( -// ( -// initialPath: SimulationPath, -// allPaths: SimulationPath[], -// visited: Set = new Set() -// ): SimulationPath[] => { -// const connectedPaths: SimulationPath[] = []; -// const queue: SimulationPath[] = [initialPath]; -// visited.add(initialPath.modeluuid); - -// const pathMap = new Map(); -// allPaths.forEach((path) => pathMap.set(path.modeluuid, path)); - -// while (queue.length > 0) { -// const currentPath = queue.shift()!; -// connectedPaths.push(currentPath); - -// // Process outgoing connections -// for (const point of currentPath.points) { -// for (const target of point.connections.targets) { -// if (!visited.has(target.modelUUID)) { -// const targetPath = pathMap.get(target.modelUUID); -// if (targetPath) { -// visited.add(target.modelUUID); -// queue.push(targetPath); -// } -// } -// } -// } - -// // Process incoming connections -// for (const [uuid, path] of pathMap) { -// if (!visited.has(uuid)) { -// const hasConnectionToCurrent = path.points.some((point) => -// point.connections.targets.some( -// (t) => t.modelUUID === currentPath.modeluuid -// ) -// ); -// if (hasConnectionToCurrent) { -// visited.add(uuid); -// queue.push(path); -// } -// } -// } -// } - -// return connectedPaths; -// }, -// [] -// ); - -// const createProcessesFromPaths = useCallback( -// (paths: SimulationPath[]): Process[] => { -// if (!paths || paths.length === 0) return []; - -// const visited = new Set(); -// const processes: Process[] = []; -// const pathMap = new Map(); -// paths.forEach((path) => pathMap.set(path.modeluuid, path)); - -// for (const path of paths) { -// if (!visited.has(path.modeluuid) && hasSpawnAction(path)) { -// const connectedPaths = getAllConnectedPaths(path, paths, visited); -// const adjustedPaths = adjustPathPointsOrder(connectedPaths); -// const process = createProcess(adjustedPaths); -// processes.push(process); -// } -// } - -// return processes; -// }, -// [createProcess, getAllConnectedPaths, hasSpawnAction] -// ); - -// return { -// processes, -// createProcessesFromPaths, -// setProcesses, -// }; -// } - -// const ProcessCreator: React.FC = React.memo( -// ({ onProcessesCreated }) => { -// const { simulationStates } = useSimulationStates(); -// const { createProcessesFromPaths } = useProcessCreation(); -// const prevPathsRef = useRef([]); -// const prevProcessesRef = useRef([]); - -// const convertedPaths = useMemo((): SimulationPath[] => { -// if (!simulationStates) return []; -// return simulationStates.map((path) => -// convertToSimulationPath( -// path as ConveyorEventsSchema | VehicleEventsSchema -// ) -// ); -// }, [simulationStates]); - -// // Enhanced dependency tracking that includes action and trigger types -// const pathsDependency = useMemo(() => { -// if (!convertedPaths) return null; -// return convertedPaths.map((path) => ({ -// id: path.modeluuid, -// // Track all action types for each point -// actionSignature: path.points -// .map((point, index) => -// point.actions.map((action) => `${index}-${action.type}`).join("|") -// ) -// .join(","), -// // Track all trigger types for each point -// triggerSignature: path.points -// .map((point, index) => -// point.triggers -// .map((trigger) => `${index}-${trigger.type}`) -// .join("|") -// ) -// .join(","), -// connections: path.points -// .flatMap((p: PathPoint) => -// p.connections.targets.map((t: { modelUUID: string }) => t.modelUUID) -// ) -// .join(","), -// isActive: false, -// })); -// }, [convertedPaths]); - -// // Force process recreation when paths change -// useEffect(() => { -// if (!convertedPaths || convertedPaths.length === 0) { -// if (prevProcessesRef.current.length > 0) { -// onProcessesCreated([]); -// prevProcessesRef.current = []; -// } -// return; -// } - -// // Always regenerate processes if the pathsDependency has changed -// // This ensures action and trigger type changes will be detected -// const newProcesses = createProcessesFromPaths(convertedPaths); -// prevPathsRef.current = convertedPaths; - -// // Always update processes when action or trigger types change -// onProcessesCreated(newProcesses); -// prevProcessesRef.current = newProcesses; -// }, [ -// pathsDependency, // This now includes action and trigger types -// onProcessesCreated, -// convertedPaths, -// createProcessesFromPaths, -// ]); - -// return null; -// } -// ); - -// export default ProcessCreator; - import React, { useEffect, useMemo, @@ -456,6 +9,7 @@ import { useSimulationStates } from "../../../store/store"; import * as THREE from "three"; import { useThree } from "@react-three/fiber"; import { + ArmBotEventsSchema, ConveyorEventsSchema, VehicleEventsSchema, } from "../../../types/simulation"; @@ -480,13 +34,14 @@ export interface PointTrigger { isUsed: boolean; } +// Update the connections type in your interfaces export interface PathPoint { uuid: string; position: [number, number, number]; actions: PointAction[]; triggers: PointTrigger[]; connections: { - targets: Array<{ modelUUID: string }>; + targets: Array<{ modelUUID: string; pointUUID?: string }>; }; } @@ -498,6 +53,14 @@ export interface SimulationPath { speed?: number; isActive: boolean; } +export interface ArmBot { + type: string; + modeluuid: string; + points: PathPoint[]; + pathPosition: [number, number, number]; + speed?: number; + isActive: boolean; +} export interface Process { id: string; @@ -515,7 +78,7 @@ interface ProcessCreatorProps { // Convert event schemas to SimulationPath function convertToSimulationPath( - path: ConveyorEventsSchema | VehicleEventsSchema + path: ConveyorEventsSchema | VehicleEventsSchema | ArmBotEventsSchema ): SimulationPath { const { modeluuid } = path; @@ -539,13 +102,13 @@ function convertToSimulationPath( actions: Array.isArray(point.actions) ? point.actions.map(normalizeAction) : point.actions - ? [normalizeAction(point.actions)] - : [], + ? [normalizeAction(point.actions)] + : [], triggers: Array.isArray(point.triggers) ? point.triggers.map(normalizeTrigger) : point.triggers - ? [normalizeTrigger(point.triggers)] - : [], + ? [normalizeTrigger(point.triggers)] + : [], connections: { targets: point.connections.targets.map((target) => ({ modelUUID: target.modelUUID, @@ -559,6 +122,36 @@ function convertToSimulationPath( : path.speed || 1, isActive: false, // Added missing property }; + } else if (path.type === "ArmBot") { + return { + type: path.type, + modeluuid, + points: [ + { + uuid: path.points.uuid, + position: path.points.position, + actions: Array.isArray(path.points.actions) + ? path.points.actions.map(normalizeAction) + : path.points.actions + ? [normalizeAction(path.points.actions)] + : [], + triggers: Array.isArray(path.points.triggers) + ? path.points.triggers.map(normalizeTrigger) + : path.points.triggers + ? [normalizeTrigger(path.points.triggers)] + : [], + connections: { + targets: path.points.connections.targets.map((target) => ({ + modelUUID: target.modelUUID, + pointUUID: target.pointUUID, // Include if available + })), + }, + }, + ], + pathPosition: path.position, + speed: path.points.actions?.speed || 1, + isActive: false, + }; } else { // For vehicle paths, handle the case where triggers might not exist return { @@ -571,8 +164,8 @@ function convertToSimulationPath( actions: Array.isArray(path.points.actions) ? path.points.actions.map(normalizeAction) : path.points.actions - ? [normalizeAction(path.points.actions)] - : [], + ? [normalizeAction(path.points.actions)] + : [], triggers: [], connections: { targets: path.points.connections.targets.map((target) => ({ @@ -831,7 +424,10 @@ const ProcessCreator: React.FC = React.memo( if (!simulationStates) return []; return simulationStates.map((path) => convertToSimulationPath( - path as ConveyorEventsSchema | VehicleEventsSchema + path as + | ConveyorEventsSchema + | VehicleEventsSchema + | ArmBotEventsSchema ) ); }, [simulationStates]); diff --git a/app/src/modules/simulation/process/types.ts b/app/src/modules/simulation/process/types.ts index 6f935fc..246e780 100644 --- a/app/src/modules/simulation/process/types.ts +++ b/app/src/modules/simulation/process/types.ts @@ -21,15 +21,15 @@ export interface PointAction { } export interface ProcessPoint { - uuid: string; + uuid: string; position: number[]; - rotation: number[]; - actions: PointAction[]; + rotation: number[]; + actions: PointAction[]; connections: { - source: { modelUUID: string; pointUUID: string }; - targets: { modelUUID: string; pointUUID: string }[]; + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; }; - triggers?: Trigger[]; + triggers?: Trigger[]; } export interface ProcessPath { modeluuid: string; @@ -38,8 +38,8 @@ export interface ProcessPath { pathPosition: number[]; pathRotation: number[]; speed: number; - type: "Conveyor" | "Vehicle"; - isActive: boolean + type: "Conveyor" | "Vehicle" | "ArmBot"; + isActive: boolean; } export interface ProcessData { diff --git a/app/src/modules/simulation/process/useProcessAnimations.tsx b/app/src/modules/simulation/process/useProcessAnimations.tsx index 032d013..232f17c 100644 --- a/app/src/modules/simulation/process/useProcessAnimations.tsx +++ b/app/src/modules/simulation/process/useProcessAnimations.tsx @@ -1,609 +1,685 @@ 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"; +interface ArmBotProcess { + triggerId: string; + startPoint: string; + endPoint: string; +} + // 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; } export const useProcessAnimation = ( - processes: ProcessData[], - setProcesses: React.Dispatch>, - agvRef: any + processes: ProcessData[], + setProcesses: React.Dispatch>, + agvRef: any ) => { - // 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< + Record + >({}); + const { speed } = useAnimationPlaySpeed(); + const prevIsPlaying = useRef(null); + const [internalResetFlag, setInternalResetFlag] = useState(false); + const [animationStates, setAnimationStates] = useState< + Record + >({}); + const speedRef = useRef(speed); + const armBotRef = useRef([]); + - // 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 - }); - } - } - }); - - // 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; - } - }); - }); - }); - - newStates[process.id] = { - spawnedObjects: {}, - nextSpawnTime: 0, - objectIdCounter: 0, - isProcessDelaying: false, - processDelayStartTime: 0, - processDelayDuration: 0, - triggerCounts, - triggerLogs: [], - }; - }); - - setAnimationStates(newStates); - animationStatesRef.current = newStates; - clockRef.current.start(); - } - }, [isPlaying, processes, internalResetFlag]); - - // 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; - }; - - 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] - ); - }; - - // 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] - ); - - // 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; - - // Find all vehicle paths for this process - const vehiclePaths = process.paths.filter( - (path) => path.type === "Vehicle" - ); - - // Check if any vehicle is active for this process - 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; - }); - - // Only count triggers if no vehicles are active for this process - if (activeVehicles.length === 0) { - onHitTriggers.forEach((trigger: Trigger) => { - const triggerKey = `${point.uuid}-${trigger.uuid}`; - newTriggerCounts[triggerKey] = (newTriggerCounts[triggerKey] || 0) + 1; - shouldLog = true; - newTriggerLogs.push({ - timestamp: currentTime, - pointId: point.uuid, - objectId, - triggerId: trigger.uuid, - }); - }); - } - - let processTotalHits = Object.values(newTriggerCounts).reduce((a, b) => a + b, 0); - - 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.status === 'picking') { - if (!vehicleEntry.isActive) { - vehicleEntry.hitCount = processTotalHits; - vehicleEntry.lastUpdated = currentTime; - - if (vehicleEntry.hitCount >= vehicleEntry.maxHitCount) { - vehicleEntry.isActive = true; - newTriggerCounts = {}; - processTotalHits = 0; - } - } - } - } - }); - } - - return { - ...prev, - [processId]: { - ...processState, - triggerCounts: newTriggerCounts, - triggerLogs: newTriggerLogs, - totalHits: processTotalHits, - }, - }; - }); - }, - [] - ); - - // Utility functions - const hasNonInheritActions = useCallback( - (actions: PointAction[] = []): boolean => { - return actions.some( - (action) => action.isUsed && action.type !== "Inherit" + // Check if this AGV already exists + const existingAgv = agvRef.current.find( + (v: any) => v.vehicleId === vehicleId && v.processId === processId ); - }, - [] + + 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, + }); + } + } + }); + + // 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; + } + }); + }); + }); + + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + isProcessDelaying: false, + processDelayStartTime: 0, + processDelayDuration: 0, + triggerCounts, + triggerLogs: [], + }; + }); + + setAnimationStates(newStates); + animationStatesRef.current = newStates; + clockRef.current.start(); + } + }, [isPlaying, processes, internalResetFlag]); + + // 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; + }; + + 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] + ); + + // 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; + + // Find all vehicle paths for this process + const vehiclePaths = process.paths.filter( + (path) => path.type === "Vehicle" + ); + + // Find all ArmBot paths for this process + const armBotPaths = process.paths.filter( + (path) => path.type === "ArmBot" + ); + + // Check if any vehicle is active for this process + 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 = armBotRef.current.find( + (a: any) => a.armBotId === armBotId && a.processId === processId + ); + return armBotEntry?.isActive; + }); + + // Only count triggers if no vehicles and no ArmBots are active for this process + if (activeVehicles.length === 0 && activeArmBots.length === 0) { + onHitTriggers.forEach((trigger: Trigger) => { + const triggerKey = `${point.uuid}-${trigger.uuid}`; + newTriggerCounts[triggerKey] = + (newTriggerCounts[triggerKey] || 0) + 1; + shouldLog = true; + newTriggerLogs.push({ + timestamp: currentTime, + pointId: point.uuid, + objectId, + 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) { + // Handle vehicle logic (existing code) + 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; - }, - [] - ); + // Handle ArmBot logic (new code) + armBotPaths.forEach((armBotPath) => { + if (armBotPath.points?.length > 0) { + const armBotPoint = armBotPath.points[0]; + const action = armBotPoint.actions?.[0]; + const maxHitCount = action?.hitCount; - const getTriggerCounts = useCallback((processId: string) => { - return animationStatesRef.current[processId]?.triggerCounts || {}; - }, []); + if (maxHitCount !== undefined) { + const armBotId = armBotPath.modeluuid; + let armBotEntry = armBotRef.current.find( + (a: any) => + a.armBotId === armBotId && a.processId === processId + ); - const getTriggerLogs = useCallback((processId: string) => { - return animationStatesRef.current[processId]?.triggerLogs || []; - }, []); + if (!armBotEntry) { + armBotEntry = { + processId, + armBotId, + maxHitCount: maxHitCount, + isActive: false, + hitCount: 0, + status: "stationed", + }; + armBotRef.current.push(armBotEntry); + console.log('armBotEntry: ', armBotEntry); + } - return { - animationStates, - setAnimationStates, - clockRef, - elapsedBeforePauseRef, - speedRef, - debugRef, - findSpawnPoint, - createSpawnedObject, - handlePointActions, - hasNonInheritActions, - getPointDataForAnimationIndex, - checkAndCountTriggers, - getTriggerCounts, - getTriggerLogs, - processes, - }; + if (!armBotEntry.isActive) { + armBotEntry.hitCount++; + armBotEntry.lastUpdated = currentTime; + + if (armBotEntry.hitCount >= armBotEntry.maxHitCount) { + armBotEntry.isActive = true; + // Reset trigger counts when ArmBot activates, similar to vehicle + newTriggerCounts = {}; + processTotalHits = 0; + } + } + } + } + }); + } + + return { + ...prev, + [processId]: { + ...processState, + triggerCounts: newTriggerCounts, + triggerLogs: newTriggerLogs, + totalHits: processTotalHits, + }, + }; + }); + }, + [] + ); + + // Utility functions + const hasNonInheritActions = useCallback( + (actions: PointAction[] = []): boolean => { + return actions.some( + (action) => action.isUsed && action.type !== "Inherit" + ); + }, + [] + ); + + 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 b1f961e..fb789ec 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -11,8 +11,10 @@ function Simulation() { const { activeModule } = useModuleStore(); const pathsGroupRef = useRef() as React.MutableRefObject; const [processes, setProcesses] = useState([]); + const agvRef = useRef([]); const MaterialRef = useRef([]); + const { simulationStates } = useSimulationStates(); return ( <>