import React, { useEffect, useState, useRef, useMemo } from "react"; 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 { usePlayButtonStore } from "../../../store/usePlayButtonStore"; import { usePlayAgv } from "../../../store/store"; import crate from "../../../assets/gltf-glb/crate_box.glb"; interface PathNavigatorProps { navMesh: any; pathPoints: any; id: string; speed: number; bufferTime: number; hitCount: number; processes: any[]; agvRef: any; MaterialRef: any; } interface AGVData { processId: string; vehicleId: string; hitCount: number; totalHits: number; } type Phase = "initial" | "toDrop" | "toPickup"; type MaterialType = "Box" | "Crate"; export default function PathNavigator({ navMesh, pathPoints, id, speed, bufferTime, hitCount, processes, agvRef, MaterialRef, }: PathNavigatorProps) { const [currentPhase, setCurrentPhase] = useState("initial"); // const [path, setPath] = useState<[number, number, number][]>([]); const PickUpDrop = useRef([]); // const [currentPhase, setCurrentPhase] = useState<"initial" | "loop">( // "initial" // ); 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([]); const totalDistanceRef = useRef(0); const progressRef = useRef(0); const isWaiting = useRef(false); const timeoutRef = useRef(null); const hasStarted = useRef(false); const { scene } = useThree(); const { isPlaying } = usePlayButtonStore(); const { PlayAgv, setPlayAgv } = usePlayAgv(); useEffect(() => { const object = scene.getObjectByProperty("uuid", id); if (object) { setInitialPosition(object.position.clone()); setInitialRotation(object.rotation.clone()); } }, [scene, id]); const computePath = (start: any, end: any) => { try { const navMeshQuery = new NavMeshQuery(navMesh); const { path: segmentPath } = navMeshQuery.computePath(start, end); return ( segmentPath?.map( ({ x, y, z }) => [x, y + 0.1, z] as [number, number, number] ) || [] ); } catch { return []; } }; const resetState = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setPath([]); setCurrentPhase("initial"); setPickupDropPath([]); setDropPickupPath([]); distancesRef.current = []; totalDistanceRef.current = 0; progressRef.current = 0; isWaiting.current = false; if (initialPosition && initialRotation) { const object = scene.getObjectByProperty("uuid", id); if (object) { object.position.copy(initialPosition); object.rotation.copy(initialRotation); } } }; useEffect(() => { if (!isPlaying) { resetState(); } if (!navMesh || pathPoints.length < 2) return; const [pickup, drop] = pathPoints.slice(-2); PickUpDrop.current = pathPoints.slice(-2); const object = scene.getObjectByProperty("uuid", id); if (!object) return; const currentPosition = { x: object.position.x, y: object.position.y, z: object.position.z, }; const toPickupPath = computePath(currentPosition, pickup); const pickupToDropPath = computePath(pickup, drop); const dropToPickupPath = computePath(drop, pickup); if ( toPickupPath.length && pickupToDropPath.length && dropToPickupPath.length ) { setPickupDropPath(pickupToDropPath); setDropPickupPath(dropToPickupPath); setToPickupPath(toPickupPath); setPath(toPickupPath); setCurrentPhase("initial"); } }, [navMesh, pathPoints, hitCount, isPlaying, PlayAgv]); useEffect(() => { if (path.length < 2) return; let total = 0; const segmentDistances = path.slice(0, -1).map((point, i) => { const dist = new THREE.Vector3(...point).distanceTo( new THREE.Vector3(...path[i + 1]) ); total += dist; return dist; }); distancesRef.current = segmentDistances; totalDistanceRef.current = total; progressRef.current = 0; isWaiting.current = false; }, [path]); // Add these refs outside the useFrame if not already present: const startPointReached = useRef(false); const baseMaterials = useMemo( () => ({ Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), // Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), Default: new THREE.MeshStandardMaterial(), }), [] ); // Example usage: const targetModelUUID = id; // Replace with your target ID const matchedProcess = findProcessByTargetModelUUID( processes, targetModelUUID ); function findProcessByTargetModelUUID(processes: any, targetModelUUID: any) { for (const process of processes) { for (const path of process.paths) { for (const point of path.points) { if ( point.connections?.targets?.some( (target: any) => target.modelUUID === targetModelUUID ) ) { // return process.id; // Return the process if a match is found } } } } return null; // Return null if no match is found } useFrame((_, delta) => {}); const boxRef = useRef(null); useEffect(() => { if (!scene || !boxRef || !processes) return; // 1. Find the existing object by UUID const existingObject = scene.getObjectByProperty("uuid", id); if (!existingObject) return; // 2. Find the process that targets this object's modelUUID if (boxVisible) { const matchedProcess = findProcessByTargetModelUUID(processes, id); // 3. Determine the material (from materialref) if a process is matched let materialType = "Default"; if (matchedProcess) { const materialEntry = MaterialRef.current.find((item: any) => item.objects.some((obj: any) => obj.processId === matchedProcess) ); console.log("materialEntry: ", materialEntry); if (materialEntry) { materialType = materialEntry.material; // "Box" or "Crate" } } // 4. Create the box mesh with the assigned material const boxGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5); const boxMaterial = baseMaterials[materialType as MaterialType]; const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial); boxMesh.position.y = 1; boxMesh.name = `box-${id}`; // 5. Add to scene and cleanup existingObject.add(boxMesh); boxRef.current = boxMesh; } if (!startPointReached.current && boxVisible) { setBoxVisible(false); } return () => { if (boxRef.current?.parent) { boxRef.current.parent.remove(boxRef.current); } boxRef.current = null; }; }, [ processes, MaterialRef, boxVisible, findProcessByTargetModelUUID, startPointReached, ]); useFrame((_, delta) => { const currentAgv = (agvRef.current || []).find( (agv: AGVData) => agv.vehicleId === id ); if (!scene || !id || !isPlaying) return; const object = scene.getObjectByProperty("uuid", id); if (!object) return; if (isPlaying && !hasStarted.current) { hasStarted.current = false; startPointReached.current = false; progressRef.current = 0; isWaiting.current = false; if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } } const isAgvReady = () => { if (!agvRef.current || agvRef.current.length === 0) return false; if (!currentAgv) return false; return hitCount === currentAgv.expectedHitCount; }; // Step 1: Snap to start point on first play if (isPlaying && !hasStarted.current && toPickupPath.length > 0) { setBoxVisible(false); const startPoint = new THREE.Vector3(...toPickupPath[0]); object.position.copy(startPoint); if (toPickupPath.length > 1) { const nextPoint = new THREE.Vector3(...toPickupPath[1]); const direction = nextPoint.clone().sub(startPoint).normalize(); object.rotation.y = Math.atan2(direction.x, direction.z); } hasStarted.current = true; startPointReached.current = true; progressRef.current = 0; setToPickupPath(toPickupPath.slice(-1)); return; } // Step 2: Wait at start point for AGV readiness if (isPlaying && startPointReached.current && path.length === 0) { if (!isAgvReady()) { return; // Prevent transitioning to the next phase if AGV is not ready } setBoxVisible(true); setPath([...toPickupPath]); // Start path transition once the AGV is ready setCurrentPhase("toDrop"); progressRef.current = 0; console.log("startPointReached: ", startPointReached.current); startPointReached.current = false; return; } if (path.length < 2) return; progressRef.current += delta * speed; let covered = progressRef.current; let accumulated = 0; let index = 0; if (distancesRef.current.length !== path.length - 1) { distancesRef.current = []; totalDistanceRef.current = 0; for (let i = 0; i < path.length - 1; i++) { const start = new THREE.Vector3(...path[i]); const end = new THREE.Vector3(...path[i + 1]); const distance = start.distanceTo(end); distancesRef.current.push(distance); totalDistanceRef.current += distance; } } while ( index < distancesRef.current.length && covered > accumulated + distancesRef.current[index] ) { accumulated += distancesRef.current[index]; index++; } // AGV has completed its path if (index >= distancesRef.current.length) { progressRef.current = totalDistanceRef.current; timeoutRef.current = setTimeout(() => { if (!isAgvReady()) { isWaiting.current = false; return; } let nextPath = []; let nextPhase = currentPhase; if (currentPhase === "toDrop") { nextPath = dropPickupPath; nextPhase = "toPickup"; setBoxVisible(false); } else if (currentPhase === "toPickup") { // When returning to start point (toPickup phase completed) // Set position to toPickupPath[1] instead of [0] if (toPickupPath.length > 1) { object.position.copy(new THREE.Vector3(...toPickupPath[1])); // Also set rotation towards the next point if available if (toPickupPath.length > 2) { const nextPoint = new THREE.Vector3(...toPickupPath[2]); const direction = nextPoint .clone() .sub(object.position) .normalize(); object.rotation.y = Math.atan2(direction.x, direction.z); } } // Reset the path and mark start point as reached setPath([]); startPointReached.current = true; progressRef.current = 0; distancesRef.current = []; // Stop the AGV by setting isplaying to false if (currentAgv) { currentAgv.isplaying = false; } // Clear timeout and return to prevent further movement if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } return; } else { nextPath = pickupDropPath; nextPhase = "toDrop"; setBoxVisible(true); } setPath([...nextPath]); setCurrentPhase(nextPhase); progressRef.current = 0; isWaiting.current = false; distancesRef.current = []; // Reset hit count for the next cycle if (agvRef.current) { agvRef.current = agvRef.current.map((agv: AGVData) => agv.vehicleId === id ? { ...agv, hitCount: 0 } : agv ); } }, bufferTime * 1000); return; } // Step 4: Interpolate position and rotation const start = new THREE.Vector3(...path[index]); const end = new THREE.Vector3(...path[index + 1]); const dist = distancesRef.current[index]; if (!dist || dist === 0) return; const t = THREE.MathUtils.clamp((covered - accumulated) / dist, 0, 1); object.position.copy(start.clone().lerp(end, t)); const direction = new THREE.Vector3().subVectors(end, start).normalize(); object.rotation.y = Math.atan2(direction.x, direction.z); }); useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); return ( {toPickupPath.length > 0 && ( )} {pickupDropPath.length > 0 && ( )} {dropPickupPath.length > 0 && ( )} ); }