import { DragControls, Line } from "@react-three/drei"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { useActiveTool, useCreatedPaths, useToolMode, } from "../../../../store/builder/store"; import { CubicBezierCurve3, Plane, Quaternion, Vector3 } from "three"; import { useFrame, useThree } from "@react-three/fiber"; import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors"; import { getPathsByPointId, setPathPosition } from "./function/getPaths"; import { aStar } from "../structuredPath/functions/aStar"; import { useAnimationPlaySpeed, usePlayButtonStore, } from "../../../../store/usePlayButtonStore"; import { useSceneContext } from "../../../scene/sceneContext"; import { usePathManager } from "./function/usePathManager"; import { useProductContext } from "../../products/productContext"; import { useSelectedEventSphere } from "../../../../store/simulation/useSimulationStore"; type PointData = { pointId: string; position: [number, number, number]; isCurved?: boolean; handleA?: [number, number, number] | null; handleB?: [number, number, number] | null; neighbors?: string[]; }; interface PathDataInterface { pathId: string; isActive?: boolean; isCurved?: boolean; pathPoints: [PointData, PointData]; } type PathData = PathDataInterface[]; type PointHandlerProps = { point: PointData; hoveredPoint: PointData | null; setPaths: React.Dispatch>; paths: PathDataInterface[]; setHoveredPoint: React.Dispatch>; hoveredLine: PathDataInterface | null; pointIndex: any; points: PointData[]; selected: number[]; setSelected: React.Dispatch>; }; function dist(a: PointData, b: PointData): number { return Math.sqrt( (a.position[0] - b.position[0]) ** 2 + (a.position[1] - b.position[1]) ** 2 + (a.position[2] - b.position[2]) ** 2 ); } type SegmentPoint = { position: Vector3; originalPoint?: PointData; pathId?: string; startId?: string; endId?: string; }; /** --- A* Algorithm --- */ type AStarResult = { pointIds: string[]; distance: number; }; function aStarShortestPath( startId: string, goalId: string, points: PointData[], paths: PathData ): AStarResult | null { const pointById = new Map(points.map((p) => [p.pointId, p])); const start = pointById.get(startId); const goal = pointById.get(goalId); if (!start || !goal) return null; const openSet = new Set([startId]); const cameFrom: Record = {}; const gScore: Record = {}; const fScore: Record = {}; for (const p of points) { cameFrom[p.pointId] = null; gScore[p.pointId] = Infinity; fScore[p.pointId] = Infinity; } gScore[startId] = 0; fScore[startId] = dist(start, goal); const neighborsOf = (id: string): { id: string; cost: number }[] => { const me = pointById.get(id)!; const out: { id: string; cost: number }[] = []; for (const edge of paths) { const [a, b] = edge.pathPoints; if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) }); else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) }); } return out; }; while (openSet.size > 0) { let current: string = [...openSet].reduce((a, b) => fScore[a] < fScore[b] ? a : b ); if (current === goalId) { const ids: string[] = []; let node: string | null = current; while (node) { ids.unshift(node); node = cameFrom[node]; } return { pointIds: ids, distance: gScore[goalId] }; } openSet.delete(current); for (const nb of neighborsOf(current)) { const tentativeG = gScore[current] + nb.cost; if (tentativeG < gScore[nb.id]) { cameFrom[nb.id] = current; gScore[nb.id] = tentativeG; fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal); openSet.add(nb.id); } } } return null; } /** --- Convert node path to edges --- */ function nodePathToEdges( pointIds: string[], points: PointData[], paths: PathData ): PathData { const byId = new Map(points.map((p) => [p.pointId, p])); const edges: PathData = []; for (let i = 0; i < pointIds.length - 1; i++) { const a = pointIds[i]; const b = pointIds[i + 1]; const edge = paths.find( (p) => (p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) || (p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a) ); if (edge) { const [p1, p2] = edge.pathPoints; edges.push({ pathId: edge.pathId, pathPoints: p1.pointId === a ? ([p1, p2] as [PointData, PointData]) : ([p2, p1] as [PointData, PointData]), }); } else { const pa = byId.get(a)!; const pb = byId.get(b)!; edges.push({ pathId: `synthetic-${a}-${b}`, pathPoints: [pa, pb], }); } } return edges; } type VehicleDetails = { vehicleId: string; vehiclePosition: [number, number, number]; }; type Manager = { pathId: string; vehicleId: string; }; export default function PointHandler({ point, // setPaths, // paths, setHoveredPoint, hoveredLine, hoveredPoint, pointIndex, points, setSelected, selected, }: PointHandlerProps) { const { isPlaying } = usePlayButtonStore(); const [multiPaths, setMultiPaths] = useState< { id: number; path: PathData }[] >([]); const { vehicleStore, productStore } = useSceneContext(); const { vehicles, getVehicleById } = vehicleStore(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); const { updateEvent, updateAction } = productStore(); const { selectedEventSphere } = useSelectedEventSphere(); const pathIdRef = useRef(1); // To ensure unique incremental IDs const { toolMode } = useToolMode(); const { activeTool } = useActiveTool(); const { scene, raycaster } = useThree(); const [isHovered, setIsHovered] = useState(false); const [dragOffset, setDragOffset] = useState(null); const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []); const [initialPositions, setInitialPositions] = useState<{ paths?: any; }>({}); const [shortestPaths, setShortestPaths] = useState([]); const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters const [vehicleUuids, setVehicleUuids] = useState(); const CAN_POINT_SNAP = true; const CAN_ANGLE_SNAP = true; const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5; const [selectedPointIndices, setSelectedPointIndices] = useState( [] ); const [shortestEdges, setShortestEdges] = useState([]); const { speed } = useAnimationPlaySpeed(); const { assetStore } = useSceneContext(); const { assets } = assetStore(); const vehicleMovementState = useRef({}); const [activeVehicleIndex, setActiveVehicleIndex] = useState(0); const [vehicleData, setVehicleData] = useState([]); const { paths, setPaths } = useCreatedPaths(); const [managerData, setManagerData] = useState(); useEffect(() => { const findVehicle = assets .filter((val) => val.eventData?.type === "Vehicle") ?.map((val) => val.modelUuid); const findVehicleDatas = assets .filter((val) => val.eventData?.type === "Vehicle") ?.map((val) => val); findVehicleDatas.forEach((val) => { const vehicledId = val.modelUuid; const vehiclePosition: [number, number, number] = val.position; setVehicleData((prev) => [ ...prev, { vehicleId: vehicledId, vehiclePosition }, ]); }); setVehicleUuids(findVehicle); setActiveVehicleIndex(0); // Reset to first vehicle vehicleMovementState.current = {}; findVehicle.forEach((uuid) => { vehicleMovementState.current[uuid] = { index: 0, progress: 0, hasStarted: false, }; }); }, [assets]); const removePathByPoint = (pointId: string): PathDataInterface[] => { const removedPaths: PathDataInterface[] = []; const newPaths = paths.filter((path: PathDataInterface) => { const hasPoint = path.pathPoints.some( (p: PointData) => p.pointId === pointId ); if (hasPoint) { removedPaths.push(JSON.parse(JSON.stringify(path))); // keep a copy return false; // remove this path } return true; // keep this path }); setPaths(newPaths); return removedPaths; }; const getConnectedPoints = (uuid: string): PointData[] => { const connected: PointData[] = []; for (const path of paths) { for (const point of path.pathPoints) { if (point.pointId === uuid) { connected.push( ...path.pathPoints.filter((p: PointData) => p.pointId !== uuid) ); } } } return connected; }; const snapPathAngle = useCallback( ( newPosition: [number, number, number], pointId: string ): { position: [number, number, number]; isSnapped: boolean; snapSources: Vector3[]; } => { if (!pointId || !CAN_ANGLE_SNAP) { return { position: newPosition, isSnapped: false, snapSources: [] }; } const connectedPoints: PointData[] = getConnectedPoints(pointId) || []; if (connectedPoints.length === 0) { return { position: newPosition, isSnapped: false, snapSources: [], }; } const newPos = new Vector3(...newPosition); let closestX: { pos: Vector3; dist: number } | null = null; let closestZ: { pos: Vector3; dist: number } | null = null; for (const connectedPoint of connectedPoints) { const cPos = new Vector3(...connectedPoint.position); const xDist = Math.abs(newPos.x - cPos.x); const zDist = Math.abs(newPos.z - cPos.z); if (xDist < ANGLE_SNAP_DISTANCE_THRESHOLD) { if (!closestX || xDist < closestX.dist) { closestX = { pos: cPos, dist: xDist }; } } if (zDist < ANGLE_SNAP_DISTANCE_THRESHOLD) { if (!closestZ || zDist < closestZ.dist) { closestZ = { pos: cPos, dist: zDist }; } } } const snappedPos = newPos.clone(); const snapSources: Vector3[] = []; if (closestX) { snappedPos.x = closestX.pos.x; snapSources.push(closestX.pos.clone()); } if (closestZ) { snappedPos.z = closestZ.pos.z; snapSources.push(closestZ.pos.clone()); } const isSnapped = snapSources.length > 0; return { position: [snappedPos.x, snappedPos.y, snappedPos.z], isSnapped, snapSources, }; }, [] ); const getAllOtherPathPoints = useCallback((): PointData[] => { return ( paths?.flatMap((path: PathDataInterface) => path.pathPoints.filter((pt: PointData) => pt.pointId !== point.pointId) ) ?? [] ); }, [paths]); const snapPathPoint = useCallback( (position: [number, number, number], pointId?: string) => { if (!CAN_POINT_SNAP) return { position: position, isSnapped: false, snappedPoint: null }; const otherPoints = getAllOtherPathPoints(); const currentVec = new Vector3(...position); for (const point of otherPoints) { const pointVec = new Vector3(...point.position); const distance = currentVec.distanceTo(pointVec); if (distance <= POINT_SNAP_THRESHOLD) { return { position: point.position, isSnapped: true, snappedPoint: point, }; } } return { position: position, isSnapped: false, snappedPoint: null }; }, [getAllOtherPathPoints] ); const handlePointClick = (e: any, point: PointData) => { e.stopPropagation(); if (toolMode === "3D-Delete") { removePathByPoint(point.pointId); return; } }; let selectedVehiclePaths: Array< Array<{ vehicleId: string; pathId: string; isActive?: boolean; isCurved?: boolean; pathPoints: [PointData, PointData]; }> > = []; function assignPathToSelectedVehicle( selectedVehicleId: string, currentPath: PathData ) { const vehiclePathSegments = currentPath.map((path) => ({ vehicleId: selectedVehicleId, ...path, })); return selectedVehiclePaths.push(vehiclePathSegments); } const handleContextMenu = (e: any, point: PointData) => { // if (e.shiftKey && e.button === 2) { const pointIndex = points.findIndex((p) => p.pointId === point.pointId); if (pointIndex === -1) { return; } setSelected((prev) => { if (prev.length === 0) { return [pointIndex]; } // if (prev.length === 1) { // setTimeout(() => { // // const prevPoint = points[prev[0]]; // // const newPoint = points[pointIndex]; // // const result = aStarShortestPath( // prevPoint.pointId, // newPoint.pointId, // points, // paths // ); // if (result) { // const edges = nodePathToEdges(result.pointIds, points, paths); // // setShortestPaths(edges); // setShortestEdges(edges); // } else { // setShortestPaths([]); // setShortestEdges([]); // } // if (prevPoint.pointId === newPoint.pointId) { // return prev; // } // }, 0); // return [prev[0], pointIndex]; // } // More than two points — reset if (prev.length === 1) { setTimeout(() => { const prevPoint = points[prev[0]]; const newPoint = points[pointIndex]; console.log( "selectedEventSphere?.userData.modelUuid: ", selectedEventSphere?.userData.modelUuid ); if (selectedEventSphere?.userData.modelUuid) { const updatedVehicle = getVehicleById( selectedEventSphere.userData.modelUuid ); const startPoint = new Vector3(...prevPoint.position); const endPoint = new Vector3(...newPoint.position); if (updatedVehicle && startPoint && endPoint) { if (updatedVehicle.type === "vehicle") { const event = updateAction( selectedProduct.productUuid, updatedVehicle.point?.action.actionUuid, { pickUpPoint: { position: { x: startPoint.x, y: 0, z: startPoint.z, }, rotation: { x: updatedVehicle.point.action.pickUpPoint?.rotation.x ?? 0, y: updatedVehicle.point.action.pickUpPoint?.rotation.y ?? 0, z: updatedVehicle.point.action.pickUpPoint?.rotation.z ?? 0, }, }, unLoadPoint: { position: { x: endPoint.x, y: endPoint.y, z: endPoint.z, }, rotation: { x: updatedVehicle.point.action.unLoadPoint?.rotation.x ?? 0, y: updatedVehicle.point.action.unLoadPoint?.rotation.y ?? 0, z: updatedVehicle.point.action.unLoadPoint?.rotation.z ?? 0, }, }, } ); if (prevPoint.pointId === newPoint.pointId) return; const result = aStarShortestPath( prevPoint.pointId, newPoint.pointId, points, paths ); if (result) { const edges = nodePathToEdges(result.pointIds, points, paths); // Create a new path object/ const newPathObj = { id: pathIdRef.current++, path: edges, }; const shortPath = assignPathToSelectedVehicle( updatedVehicle?.modelUuid, edges ); console.log("shortPath: ", shortPath); setShortestPaths(edges); setShortestEdges(edges); // Append it to the list of paths setMultiPaths((prevPaths) => [...prevPaths, newPathObj]); } } } } // Reset selection to allow new pair selection }, 0); return [prev[0], pointIndex]; } setShortestPaths([]); return [pointIndex]; }); // } }; const handleDragStart = (point: PointData) => { if (activeTool !== "cursor") return; const intersectionPoint = new Vector3(); const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); if (hit) { const currentPosition = new Vector3(...point.position); const offset = new Vector3().subVectors(currentPosition, hit); setDragOffset(offset); const pathIntersection = getPathsByPointId(point.pointId, paths); setInitialPositions({ paths: pathIntersection }); } }; const handleDrag = (point: PointData) => { if (isHovered && dragOffset) { const intersectionPoint = new Vector3(); const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); if (hit) { // handleCanvasCursors("grabbing"); const positionWithOffset = new Vector3().addVectors(hit, dragOffset); const newPosition: [number, number, number] = [ positionWithOffset.x, positionWithOffset.y, positionWithOffset.z, ]; // ✅ Pass newPosition and pointId const pathSnapped = snapPathAngle(newPosition, point.pointId); const finalSnapped = snapPathPoint(pathSnapped.position); setPathPosition(point.pointId, finalSnapped.position, setPaths, paths); } } }; const handleDragEnd = (point: PointData) => { const pathIntersection = getPathsByPointId(point.pointId, paths); if (pathIntersection && pathIntersection.length > 0) { pathIntersection.forEach((update) => {}); } }; const pathSegments = useMemo(() => { if (!shortestPaths || shortestPaths.length === 0) return []; const segments: SegmentPoint[] = []; shortestPaths.forEach((path) => { const [start, end] = path.pathPoints; const startPos = new Vector3(...start.position); const endPos = new Vector3(...end.position); segments.push( { position: startPos, originalPoint: start, startId: start.pointId }, { position: endPos, originalPoint: end, endId: end.pointId } ); }); return segments.filter( (v, i, arr) => i === 0 || !v.position.equals(arr[i - 1].position) ); }, [shortestPaths]); function getPathIdByPoints( startId: string | undefined, endId: string | undefined, shortestPaths: any[] ) { for (const path of shortestPaths) { for (let i = 0; i < path.pathPoints.length - 1; i++) { const s = path.pathPoints[i]; const e = path.pathPoints[i + 1]; if ( (s.pointId === startId && e.pointId === endId) || (s.pointId === endId && e.pointId === startId) // handle both directions ) { return path.pathId; } } } return null; // not found } return ( <> handleDragStart(point)} onDrag={() => handleDrag(point)} onDragEnd={() => handleDragEnd(point)} > { handlePointClick(e, point); }} // onContextMenu={(e) => handleContextMenu(e, point)} onPointerOver={(e) => { if (!hoveredPoint && e.buttons === 0 && !e.ctrlKey) { setHoveredPoint(point); setIsHovered(true); // handleCanvasCursors("default"); } }} onPointerOut={() => { if (hoveredPoint) { setHoveredPoint(null); if (!hoveredLine) { // handleCanvasCursors("default"); } } setIsHovered(false); }} > {shortestEdges.map((edge) => ( p.position)} color="yellow" lineWidth={3} /> ))} ); }