import { useCallback, useEffect, useRef, useState } from 'react'; import VehicleAnimator from '../animator/vehicleAnimator'; import * as THREE from 'three'; import { NavMeshQuery } from '@recast-navigation/core'; import { useNavMesh } from '../../../../../store/builder/store'; import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore'; import { useProductStore } from '../../../../../store/simulation/useProductStore'; import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler'; import MaterialAnimator from '../animator/materialAnimator'; import { useSceneContext } from '../../../../scene/sceneContext'; import { useProductContext } from '../../../products/productContext'; function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) { const { navMesh } = useNavMesh(); const { isPlaying } = usePlayButtonStore(); const { materialStore, armBotStore, conveyorStore, vehicleStore, storageUnitStore } = useSceneContext(); const { removeMaterial, setEndTime } = materialStore(); const { getStorageUnitById } = storageUnitStore(); const { getArmBotById } = armBotStore(); const { getConveyorById } = conveyorStore(); const { triggerPointActions } = useTriggerHandler(); const { getActionByUuid, getEventByModelUuid, getTriggerByUuid } = useProductStore(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); const { vehicles, setVehicleActive, setVehicleState, setVehiclePicking, clearCurrentMaterials, setVehicleLoad, decrementVehicleLoad, removeLastMaterial, getLastMaterial, incrementIdleTime, incrementActiveTime, resetTime } = vehicleStore(); const [currentPhase, setCurrentPhase] = useState('stationed'); const [path, setPath] = useState<[number, number, number][]>([]); const pauseTimeRef = useRef(null); const idleTimeRef = useRef(0); const activeTimeRef = useRef(0); const isPausedRef = useRef(false); const isSpeedRef = useRef(0); let startTime: number; let fixedInterval: number; const { speed } = useAnimationPlaySpeed(); const { isPaused } = usePauseButtonStore(); const previousTimeRef = useRef(null); const animationFrameIdRef = useRef(null); useEffect(() => { isPausedRef.current = isPaused; }, [isPaused]); useEffect(() => { isSpeedRef.current = speed; }, [speed]); const computePath = useCallback( (start: any, end: any) => { console.log('end: ', end); try { const navMeshQuery = new NavMeshQuery(navMesh); const { path: segmentPath } = navMeshQuery.computePath(start, end); if ( segmentPath.length > 0 && Math.round(segmentPath[segmentPath.length - 1].x) == Math.round(end.x) && Math.round(segmentPath[segmentPath.length - 1].z) == Math.round(end.z) ) { console.log('if ', segmentPath); return segmentPath?.map(({ x, y, z }) => [x, 0, z] as [number, number, number]) || []; } else { console.log("There is no path here...Choose valid path") const { path: segmentPaths } = navMeshQuery.computePath(start, start); return segmentPaths.map(({ x, y, z }) => [x, 0, z] as [number, number, number]) || []; } } catch { console.error("Failed to compute path"); return []; } }, [navMesh] ); function vehicleStatus(modelId: string, status: string) { // console.log(`${modelId} , ${status}`); } // Function to reset everything function reset() { setCurrentPhase('stationed'); setVehicleActive(agvDetail.modelUuid, false); setVehiclePicking(agvDetail.modelUuid, false); setVehicleState(agvDetail.modelUuid, 'idle'); setVehicleLoad(agvDetail.modelUuid, 0); setPath([]); startTime = 0; isPausedRef.current = false; pauseTimeRef.current = 0; resetTime(agvDetail.modelUuid) activeTimeRef.current = 0 idleTimeRef.current = 0 previousTimeRef.current = null if (animationFrameIdRef.current !== null) { cancelAnimationFrame(animationFrameIdRef.current) animationFrameIdRef.current = null } } useEffect(() => { if (isPlaying) { if (!agvDetail.point.action.unLoadPoint || !agvDetail.point.action.pickUpPoint) return; if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'stationed') { const toPickupPath = computePath( new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]), agvDetail?.point?.action?.pickUpPoint?.position ); // const toPickupPath = computePath( // new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]), // new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]) // ); setPath(toPickupPath); setCurrentPhase('stationed-pickup'); setVehicleState(agvDetail.modelUuid, 'running'); setVehiclePicking(agvDetail.modelUuid, false); setVehicleActive(agvDetail.modelUuid, true); vehicleStatus(agvDetail.modelUuid, 'Started from station, heading to pickup'); return; } else if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'picking') { if (agvDetail.currentLoad === agvDetail.point.action.loadCapacity && agvDetail.currentMaterials.length > 0) { if (agvDetail.point.action.pickUpPoint && agvDetail.point.action.unLoadPoint) { const toDrop = computePath( agvDetail.point.action.pickUpPoint.position, agvDetail.point.action.unLoadPoint.position ); setPath(toDrop); setCurrentPhase('pickup-drop'); setVehicleState(agvDetail.modelUuid, 'running'); setVehiclePicking(agvDetail.modelUuid, false); setVehicleActive(agvDetail.modelUuid, true); vehicleStatus(agvDetail.modelUuid, 'Started from pickup point, heading to drop point'); } } } else if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'dropping' && agvDetail.currentLoad === 0) { if (agvDetail.point.action.pickUpPoint && agvDetail.point.action.unLoadPoint) { const dropToPickup = computePath( agvDetail.point.action.unLoadPoint.position, agvDetail.point.action.pickUpPoint.position ); setPath(dropToPickup); setCurrentPhase('drop-pickup'); setVehicleState(agvDetail.modelUuid, 'running'); setVehiclePicking(agvDetail.modelUuid, false); setVehicleActive(agvDetail.modelUuid, true); vehicleStatus(agvDetail.modelUuid, 'Started from dropping point, heading to pickup point'); } } } else { reset() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [vehicles, currentPhase, path, isPlaying]); function animate(currentTime: number) { if (previousTimeRef.current === null) { previousTimeRef.current = currentTime; } const deltaTime = (currentTime - previousTimeRef.current) / 1000; previousTimeRef.current = currentTime; if (agvDetail.isActive) { if (!isPausedRef.current) { activeTimeRef.current += deltaTime * isSpeedRef.current; } } else { if (!isPausedRef.current) { idleTimeRef.current += deltaTime * isSpeedRef.current; // Scale idle time by speed } } animationFrameIdRef.current = requestAnimationFrame(animate); } useEffect(() => { if (!isPlaying) return if (!agvDetail.isActive) { const roundedActiveTime = Math.round(activeTimeRef.current); // console.log('Final Active Time:', roundedActiveTime, 'seconds'); incrementActiveTime(agvDetail.modelUuid, roundedActiveTime); activeTimeRef.current = 0; } else { const roundedIdleTime = Math.round(idleTimeRef.current); // console.log('Final Idle Time:', roundedIdleTime, 'seconds'); incrementIdleTime(agvDetail.modelUuid, roundedIdleTime); idleTimeRef.current = 0; } if (animationFrameIdRef.current === null) { animationFrameIdRef.current = requestAnimationFrame(animate); } return () => { if (animationFrameIdRef.current !== null) { cancelAnimationFrame(animationFrameIdRef.current); animationFrameIdRef.current = null; } }; }, [agvDetail, isPlaying]); function handleCallBack() { if (currentPhase === 'stationed-pickup') { setCurrentPhase('picking'); setVehicleState(agvDetail.modelUuid, 'idle'); setVehiclePicking(agvDetail.modelUuid, true); setVehicleActive(agvDetail.modelUuid, false); vehicleStatus(agvDetail.modelUuid, 'Reached pickup point, waiting for material'); setPath([]); } else if (currentPhase === 'pickup-drop') { setCurrentPhase('dropping'); setVehicleState(agvDetail.modelUuid, 'idle'); setVehiclePicking(agvDetail.modelUuid, false); setVehicleActive(agvDetail.modelUuid, false); vehicleStatus(agvDetail.modelUuid, 'Reached drop point'); setPath([]); } else if (currentPhase === 'drop-pickup') { setCurrentPhase('picking'); setVehicleState(agvDetail.modelUuid, 'idle'); setVehiclePicking(agvDetail.modelUuid, true); setVehicleActive(agvDetail.modelUuid, false); setPath([]); clearCurrentMaterials(agvDetail.modelUuid) vehicleStatus(agvDetail.modelUuid, 'Reached pickup point again, cycle complete'); } } function startUnloadingProcess() { if (agvDetail.point.action.triggers.length > 0) { const trigger = getTriggerByUuid(selectedProduct.productUuid, agvDetail.point.action.triggers[0]?.triggerUuid); const model = getEventByModelUuid(selectedProduct.productUuid, trigger?.triggeredAsset?.triggeredModel?.modelUuid || ''); if (trigger && model) { if (model.type === 'transfer') { const action = getActionByUuid(selectedProduct.productUuid, agvDetail.point.action.actionUuid); if (action) { handleMaterialDropToConveyor(model); } } else if (model.type === 'machine') { // } else if (model.type === 'roboticArm') { const action = getActionByUuid(selectedProduct.productUuid, agvDetail.point.action.actionUuid); if (action) { handleMaterialDropToArmBot(model); } } else if (model.type === 'storageUnit') { const action = getActionByUuid(selectedProduct.productUuid, agvDetail.point.action.actionUuid); if (action) { handleMaterialDropToStorageUnit(model); } } } else { const droppedMaterial = agvDetail.currentLoad; startTime = performance.now(); handleMaterialDropByDefault(droppedMaterial); } } else { const droppedMaterial = agvDetail.currentLoad; startTime = performance.now(); handleMaterialDropByDefault(droppedMaterial); } } function handleMaterialDropToStorageUnit(model: StorageEventSchema) { if (model) { if (model.point.action.actionType === 'store') { loopMaterialDropToStorage( agvDetail.modelUuid, agvDetail.currentLoad, agvDetail.point.action.unLoadDuration, model.modelUuid, model.point.action.storageCapacity, agvDetail.point.action ); } } } function loopMaterialDropToStorage( vehicleId: string, vehicleCurrentLoad: number, unLoadDuration: number, storageUnitId: string, storageMaxCapacity: number, action: VehicleAction ) { startTime = performance.now(); const fixedInterval = ((unLoadDuration / vehicleCurrentLoad) * (1000 / isSpeedRef.current)); const unloadLoop = () => { if (isPausedRef.current) { pauseTimeRef.current ??= performance.now(); requestAnimationFrame(unloadLoop); return; } if (pauseTimeRef.current) { const pauseDuration = performance.now() - pauseTimeRef.current; startTime += pauseDuration; pauseTimeRef.current = null; } const elapsedTime = performance.now() - startTime; const storageUnit = getStorageUnitById(storageUnitId); if (elapsedTime >= fixedInterval) { if (storageUnit && agvDetail && storageUnit.currentLoad < storageMaxCapacity && vehicleCurrentLoad > 0) { decrementVehicleLoad(vehicleId, 1); vehicleCurrentLoad -= 1; const material = removeLastMaterial(vehicleId); if (material) { triggerPointActions(action, material.materialId); } if (vehicleCurrentLoad > 0 && storageUnit.currentLoad < storageMaxCapacity) { startTime = performance.now(); requestAnimationFrame(unloadLoop); } } } else { requestAnimationFrame(unloadLoop); } }; const storageUnit = getStorageUnitById(storageUnitId); if (storageUnit && vehicleCurrentLoad > 0 && storageUnit?.currentLoad < storageMaxCapacity) { unloadLoop(); } } function handleMaterialDropToConveyor(model: ConveyorEventSchema) { const conveyor = getConveyorById(model.modelUuid); if (conveyor) { loopMaterialDropToConveyor( agvDetail.modelUuid, agvDetail.currentLoad, conveyor.modelUuid, agvDetail.point.action.unLoadDuration, agvDetail.point.action ); } } function loopMaterialDropToConveyor( vehicleId: string, vehicleCurrentLoad: number, conveyorId: string, unLoadDuration: number, action: VehicleAction ) { let lastIncrementTime = performance.now(); let pauseStartTime: number | null = null; let totalPausedDuration = 0; const fixedInterval = (unLoadDuration * 1000) / speed; const dropLoop = (currentTime: number) => { const conveyor = getConveyorById(conveyorId); if (isPausedRef.current || (conveyor && conveyor.isPaused)) { if (pauseStartTime === null) { pauseStartTime = currentTime; } requestAnimationFrame(dropLoop); return; } // If we were paused but now resumed if (pauseStartTime !== null) { totalPausedDuration += currentTime - pauseStartTime; pauseStartTime = null; } // Adjust for paused time const adjustedCurrentTime = currentTime - totalPausedDuration; const elapsedSinceLastIncrement = adjustedCurrentTime - lastIncrementTime; if (elapsedSinceLastIncrement >= fixedInterval) { if (conveyor && vehicleCurrentLoad > 0) { decrementVehicleLoad(vehicleId, 1); vehicleCurrentLoad -= 1; const material = removeLastMaterial(vehicleId); if (material) { triggerPointActions(action, material.materialId); } // Update the last increment time (using adjusted time) lastIncrementTime = adjustedCurrentTime; } } // Continue the loop if there's more load to drop if (vehicleCurrentLoad > 0) { requestAnimationFrame(dropLoop); } }; requestAnimationFrame(dropLoop); } function handleMaterialDropToArmBot(model: RoboticArmEventSchema) { const armBot = getArmBotById(model.modelUuid); if (armBot && armBot.state === 'idle' && !armBot.isActive) { loopMaterialDropToArmBot( agvDetail.modelUuid, agvDetail.currentLoad, agvDetail.point.action.unLoadDuration, model.modelUuid, agvDetail.point.action ); } } function loopMaterialDropToArmBot( vehicleId: string, vehicleCurrentLoad: number, unLoadDuration: number, armBotId: string, action: VehicleAction ) { startTime = performance.now(); const armBot = getArmBotById(armBotId); if (!armBot || armBot.state !== 'idle' || armBot.isActive || vehicleCurrentLoad <= 0) { return; } const checkIdleDuration = () => { if (isPausedRef.current) { pauseTimeRef.current ??= performance.now(); requestAnimationFrame(checkIdleDuration); return; } if (pauseTimeRef.current) { const pauseDuration = performance.now() - pauseTimeRef.current; startTime += pauseDuration; pauseTimeRef.current = null; } const elapsedTime = performance.now() - startTime; if (elapsedTime >= unLoadDuration * (1000 / speed)) { const material = getLastMaterial(vehicleId); if (material) { vehicleCurrentLoad -= 1; triggerPointActions(action, material.materialId); if (vehicleCurrentLoad > 0) { setTimeout(() => { const waitForNextTransfer = () => { const currentArmBot = getArmBotById(armBotId); if (currentArmBot && currentArmBot.state === 'idle' && !currentArmBot.isActive) { startTime = performance.now(); loopMaterialDropToArmBot(vehicleId, vehicleCurrentLoad, unLoadDuration, armBotId, action); } else { requestAnimationFrame(waitForNextTransfer); } }; waitForNextTransfer(); }, 0) } } } else { requestAnimationFrame(checkIdleDuration); } }; checkIdleDuration(); } function handleMaterialDropByDefault(droppedMaterial: number) { if (isPausedRef.current) { pauseTimeRef.current ??= performance.now(); requestAnimationFrame(() => handleMaterialDropByDefault(droppedMaterial)); return; } if (pauseTimeRef.current) { const pauseDuration = performance.now() - pauseTimeRef.current; startTime += pauseDuration; pauseTimeRef.current = null; } const elapsedTime = performance.now() - startTime; const unLoadDuration = agvDetail.point.action.unLoadDuration; fixedInterval = ((unLoadDuration / agvDetail.currentLoad) * (1000 / isSpeedRef.current)); if (elapsedTime >= fixedInterval) { let droppedMat = droppedMaterial - 1; decrementVehicleLoad(agvDetail.modelUuid, 1); const material = removeLastMaterial(agvDetail.modelUuid); if (material) { setEndTime(material.materialId, performance.now()); removeMaterial(material.materialId); } if (droppedMat > 0) { startTime = performance.now(); requestAnimationFrame(() => handleMaterialDropByDefault(droppedMat)); } else { return; } } else { requestAnimationFrame(() => handleMaterialDropByDefault(droppedMaterial)); } } return ( <> ); } export default VehicleInstance;