diff --git a/app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx b/app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx index 2bd8656..e665f5b 100644 --- a/app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx +++ b/app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx @@ -28,8 +28,7 @@ function AisleCreator() { const { activeLayer } = useActiveLayer(); const { socket } = useSocketStore(); const { aisleStore, undoRedo2DStore } = useSceneContext(); - const { aisles, addAisle, getAislePointById } = aisleStore(); - console.log("aisles: ", aisles); + const { addAisle, getAislePointById } = aisleStore(); const { push2D } = undoRedo2DStore(); const drag = useRef(false); const isLeftMouseDown = useRef(false); diff --git a/app/src/modules/simulation/vehicle/preDefinedPath/PointHandle.tsx b/app/src/modules/simulation/vehicle/preDefinedPath/PointHandle.tsx index 5d22ff9..5d10164 100644 --- a/app/src/modules/simulation/vehicle/preDefinedPath/PointHandle.tsx +++ b/app/src/modules/simulation/vehicle/preDefinedPath/PointHandle.tsx @@ -1,245 +1,20 @@ -// import React, { useRef, useState } from "react"; -// import { useThree, useFrame } from "@react-three/fiber"; -// import * as THREE from "three"; -// import { Line } from "@react-three/drei"; - -// interface PointProps { -// point: any; -// pointIndex: number; -// groupIndex: number; -// selected: number[]; -// setPointsGroups: React.Dispatch>; -// } - -// export default function EditablePoint({ -// point, -// pointIndex, -// groupIndex, -// selected, -// setPointsGroups, -// }: PointProps) { -// const meshRef = useRef(null); -// const handleARef = useRef(null); -// const handleBRef = useRef(null); -// const lineRef = useRef(null!); - -// const { camera, gl, controls } = useThree(); -// const [dragging, setDragging] = useState< -// null | "main" | "handleA" | "handleB" -// >(null); -// const dragOffset = useRef(new THREE.Vector3()); - -// /** Handle clicking the point */ -// const onPointClick = (e: any) => { -// e.stopPropagation(); - -// if (e.ctrlKey) { -// // Toggle curve handles -// setPointsGroups((prev) => { -// const newGroups = [...prev]; -// const group = [...newGroups[groupIndex]]; -// const idx = group.findIndex((p) => p.pointId === point.pointId); -// const updated = { ...group[idx] }; - -// if (!updated.handleA && !updated.handleB) { -// updated.handleA = [ -// updated.position[0] + 1, -// updated.position[1], -// updated.position[2], -// ]; -// updated.handleB = [ -// updated.position[0] - 1, -// updated.position[1], -// updated.position[2], -// ]; -// updated.isCurved = true; -// } else { -// updated.handleA = null; -// updated.handleB = null; -// updated.isCurved = false; -// } - -// group[idx] = updated; -// newGroups[groupIndex] = group; -// return newGroups; -// }); -// } -// }; - -// /** Pointer down for dragging */ -// const startDrag = (target: "main" | "handleA" | "handleB", e: any) => { -// e.stopPropagation(); -// setDragging(target); -// const targetRef = -// target === "main" -// ? meshRef.current -// : target === "handleA" -// ? handleARef.current -// : handleBRef.current; -// if (targetRef) { -// dragOffset.current.copy(targetRef.position).sub(e.point); -// } -// if (controls) (controls as any).enabled = false; -// gl.domElement.style.cursor = "grabbing"; -// }; - -// /** Pointer up stops dragging */ -// const stopDrag = () => { -// setDragging(null); -// gl.domElement.style.cursor = "auto"; -// if (controls) (controls as any).enabled = true; -// }; - -// /** Handle dragging logic */ -// useFrame(({ raycaster, mouse }) => { -// if (!dragging) return; - -// const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); -// raycaster.setFromCamera(mouse, camera); -// const intersection = new THREE.Vector3(); - -// if (raycaster.ray.intersectPlane(plane, intersection)) { -// const newPos = intersection.add(dragOffset.current); - -// setPointsGroups((prev) => { -// const newGroups = [...prev]; -// const group = [...newGroups[groupIndex]]; -// const idx = group.findIndex((p) => p.pointId === point.pointId); -// const updated = { ...group[idx] }; - -// if (dragging === "main") { -// const delta = new THREE.Vector3() -// .fromArray(newPos.toArray()) -// .sub(new THREE.Vector3().fromArray(updated.position)); -// updated.position = newPos.toArray() as [number, number, number]; - -// // Move handles along with the main point -// if (updated.handleA) { -// updated.handleA = new THREE.Vector3() -// .fromArray(updated.handleA) -// .add(delta) -// .toArray() as [number, number, number]; -// } -// if (updated.handleB) { -// updated.handleB = new THREE.Vector3() -// .fromArray(updated.handleB) -// .add(delta) -// .toArray() as [number, number, number]; -// } -// } else { -// updated[dragging] = newPos.toArray() as [number, number, number]; - -// // Mirror opposite handle -// if (updated.isCurved) { -// const mainPos = new THREE.Vector3().fromArray(updated.position); -// const thisHandle = new THREE.Vector3().fromArray( -// updated[dragging]! -// ); -// const mirrorHandle = mainPos -// .clone() -// .sub(thisHandle.clone().sub(mainPos)); - -// if (dragging === "handleA") -// updated.handleB = mirrorHandle.toArray() as [ -// number, -// number, -// number -// ]; -// if (dragging === "handleB") -// updated.handleA = mirrorHandle.toArray() as [ -// number, -// number, -// number -// ]; -// } -// } - -// group[idx] = updated; -// newGroups[groupIndex] = group; -// return newGroups; -// }); -// } -// }); - -// /** Update line between handles each frame */ -// useFrame(() => { -// if (lineRef.current && point.handleA && point.handleB) { -// const positions = lineRef.current.geometry.attributes.position -// .array as Float32Array; -// positions[0] = point.handleA[0]; -// positions[1] = point.handleA[1]; -// positions[2] = point.handleA[2]; -// positions[3] = point.handleB[0]; -// positions[4] = point.handleB[1]; -// positions[5] = point.handleB[2]; -// lineRef.current.geometry.attributes.position.needsUpdate = true; -// } -// }); - -// return ( -// <> -// {/* Main point */} -// startDrag("main", e)} -// onPointerUp={stopDrag} -// > -// -// -// - -// {/* Handles + line */} -// {point.isCurved && point.handleA && point.handleB && ( -// <> -// {/* Line between handles */} -// {point.handleA && point.handleB && ( -// -// )} - -// {/* Handle A */} -// startDrag("handleA", e)} -// onPointerUp={stopDrag} -// > -// -// -// - -// {/* Handle B */} -// startDrag("handleB", e)} -// onPointerUp={stopDrag} -// > -// -// -// -// -// )} -// -// ); -// } import React, { useRef, useState, useEffect } from "react"; import { useThree, useFrame } from "@react-three/fiber"; import * as THREE from "three"; import { Line } from "@react-three/drei"; - +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved: boolean; + handleA: [number, number, number] | null; + handleB: [number, number, number] | null; +}; interface PointProps { point: any; pointIndex: number; groupIndex: number; selected: number[]; + mainShapeOnly?: PointData[]; setSelected: React.Dispatch>; pointsGroups: any[][]; setPointsGroups: React.Dispatch>; @@ -260,7 +35,6 @@ export default function PointHandle({ shortestPath, setShortestPath, }: PointProps) { - const meshRef = useRef(null); const handleARef = useRef(null); const handleBRef = useRef(null); @@ -553,7 +327,7 @@ export default function PointHandle({ // path.unshift(u); // u = previous[u]; // } -// +// // return path; // }; diff --git a/app/src/modules/simulation/vehicle/preDefinedPath/lineSegment.tsx b/app/src/modules/simulation/vehicle/preDefinedPath/lineSegment.tsx index d57e37e..71cc3ae 100644 --- a/app/src/modules/simulation/vehicle/preDefinedPath/lineSegment.tsx +++ b/app/src/modules/simulation/vehicle/preDefinedPath/lineSegment.tsx @@ -1,3 +1,90 @@ +// import { Line } from "@react-three/drei"; +// import { useThree } from "@react-three/fiber"; +// import { useEffect, useMemo, useRef } from "react"; +// import { CubicBezierCurve3, LineCurve3, Plane, Vector3 } from "three"; + +// export default function LineSegment({ +// index, +// createdPoints, +// updatePoints, +// insertPoint, +// }: { +// index: number; +// createdPoints: any[]; // Array of points with position, isCurved, handleA, handleB +// updatePoints: (i0: number, p0: Vector3, i1: number, p1: Vector3) => void; +// insertPoint?: (index: number, point: Vector3) => void; +// }) { +// const { gl, raycaster, camera, controls } = useThree(); +// const plane = new Plane(new Vector3(0, 1, 0), 0); +// const dragStart = useRef(null); + +// // ======== Curve or Line Points ======== +// const curvePoints = useMemo(() => { +// if (!createdPoints || index + 1 >= createdPoints.length) return []; + +// const current = createdPoints[index]; +// const next = createdPoints[index + 1]; + +// const starts = new Vector3(...current.position); +// const ends = new Vector3(...next.position); + +// const useCurve = +// (current.isCurved && current.handleB) || (next.isCurved && next.handleA); + +// const hB = current.handleB ? new Vector3(...current.handleB) : starts; +// const hA = next.handleA ? new Vector3(...next.handleA) : ends; + +// const curve = useCurve +// ? new CubicBezierCurve3(starts, hB, hA, ends) +// : new LineCurve3(starts, ends); + +// return curve.getPoints(useCurve ? 100 : 2); +// }, [createdPoints, index]); + +// // ======== Events ======== +// const onPointerUp = () => { +// dragStart.current = null; +// gl.domElement.style.cursor = "default"; +// if (controls) (controls as any).enabled = true; +// }; + +// const onClickLine = () => { +// const intersection = new Vector3(); +// if (raycaster.ray.intersectPlane(plane, intersection)) { +// const start = new Vector3(...createdPoints[index].position); +// const end = new Vector3(...createdPoints[index + 1].position); +// const segLen = start.distanceTo(end); +// const distToStart = start.distanceTo(intersection); +// const distToEnd = end.distanceTo(intersection); + +// if ( +// distToStart > 0.01 && +// distToEnd > 0.01 && +// distToStart + distToEnd <= segLen + 0.01 +// ) { +// insertPoint?.(index + 1, intersection); +// } +// } +// }; + +// useEffect(() => { +// gl.domElement.addEventListener("pointerup", onPointerUp); +// return () => { +// gl.domElement.removeEventListener("pointerup", onPointerUp); +// }; +// }, []); + +// // ======== Render ======== +// return ( +// +// ); +// } import { Line } from "@react-three/drei"; import { useThree } from "@react-three/fiber"; import { useEffect, useMemo, useRef } from "react"; @@ -10,7 +97,7 @@ export default function LineSegment({ insertPoint, }: { index: number; - createdPoints: any[]; // Array of points with position, isCurved, handleA, handleB + createdPoints: any[]; updatePoints: (i0: number, p0: Vector3, i1: number, p1: Vector3) => void; insertPoint?: (index: number, point: Vector3) => void; }) { @@ -18,21 +105,25 @@ export default function LineSegment({ const plane = new Plane(new Vector3(0, 1, 0), 0); const dragStart = useRef(null); - // ======== Curve or Line Points ======== const curvePoints = useMemo(() => { if (!createdPoints || index + 1 >= createdPoints.length) return []; const current = createdPoints[index]; const next = createdPoints[index + 1]; - const starts = new Vector3(...current.position); - const ends = new Vector3(...next.position); + // Force y = 0 + const starts = new Vector3(current.position[0], 0, current.position[2]); + const ends = new Vector3(next.position[0], 0, next.position[2]); const useCurve = (current.isCurved && current.handleB) || (next.isCurved && next.handleA); - const hB = current.handleB ? new Vector3(...current.handleB) : starts; - const hA = next.handleA ? new Vector3(...next.handleA) : ends; + const hB = current.handleB + ? new Vector3(current.handleB[0], 0, current.handleB[2]) + : starts; + const hA = next.handleA + ? new Vector3(next.handleA[0], 0, next.handleA[2]) + : ends; const curve = useCurve ? new CubicBezierCurve3(starts, hB, hA, ends) @@ -41,7 +132,6 @@ export default function LineSegment({ return curve.getPoints(useCurve ? 100 : 2); }, [createdPoints, index]); - // ======== Events ======== const onPointerUp = () => { dragStart.current = null; gl.domElement.style.cursor = "default"; @@ -51,8 +141,17 @@ export default function LineSegment({ const onClickLine = () => { const intersection = new Vector3(); if (raycaster.ray.intersectPlane(plane, intersection)) { - const start = new Vector3(...createdPoints[index].position); - const end = new Vector3(...createdPoints[index + 1].position); + const start = new Vector3( + createdPoints[index].position[0], + 0, + createdPoints[index].position[2] + ); + const end = new Vector3( + createdPoints[index + 1].position[0], + 0, + createdPoints[index + 1].position[2] + ); + const segLen = start.distanceTo(end); const distToStart = start.distanceTo(intersection); const distToEnd = end.distanceTo(intersection); @@ -74,7 +173,6 @@ export default function LineSegment({ }; }, []); - // ======== Render ======== return ( ([]); -// const [selected, setSelected] = useState([]); -// const downPosition = useRef<{ x: number; y: number } | null>(null); -// const hasClicked = useRef(false); - -// const handleMouseDown = useCallback((e: MouseEvent) => { -// hasClicked.current = false; -// downPosition.current = { x: e.clientX, y: e.clientY }; -// }, []); - -// const handleClick = useCallback( -// (e: MouseEvent) => { -// if (hasClicked.current) return; -// hasClicked.current = true; - -// if ( -// !downPosition.current || -// Math.abs(downPosition.current.x - e.clientX) > 2 || -// Math.abs(downPosition.current.y - e.clientY) > 2 -// ) { -// return; -// } - -// const intersection = new THREE.Vector3(); -// if (raycaster.ray.intersectPlane(plane.current, intersection)) { -// const pointArray = intersection.toArray() as [number, number, number]; -// const alreadyExists = points.some( -// (p) => -// Math.abs(p[0] - pointArray[0]) < 0.01 && -// Math.abs(p[1] - pointArray[1]) < 0.01 && -// Math.abs(p[2] - pointArray[2]) < 0.01 -// ); - -// if (!alreadyExists) { -// setPoints((prev) => [...prev, pointArray]); -// } -// } -// }, -// [raycaster, points] -// ); - -// useEffect(() => { -// const domElement = gl.domElement; -// domElement.addEventListener("mousedown", handleMouseDown); -// domElement.addEventListener("mouseup", handleClick); -// return () => { -// domElement.removeEventListener("mousedown", handleMouseDown); -// domElement.removeEventListener("mouseup", handleClick); -// }; -// }, [handleClick, handleMouseDown]); - -// // Update two existing points -// const updatePoints = ( -// i0: number, -// p0: THREE.Vector3, -// i1: number, -// p1: THREE.Vector3 -// ) => { -// const updated = [...points]; -// updated[i0] = p0.toArray() as [number, number, number]; -// updated[i1] = p1.toArray() as [number, number, number]; -// setPoints(updated); -// }; - -// const insertPoint = (index: number, point: THREE.Vector3) => { -// const updated = [...points]; -// updated.splice(index, 0, point.toArray() as [number, number, number]); -// setPoints(updated); -// }; - -// return ( -// <> -// {points.map((pos, idx) => ( -// { -// setSelected((prev) => (prev.length === 2 ? [idx] : [...prev, idx])); -// }} -// > -// -// -// -// ))} - -// {points.map((pos, i) => { -// if (i < points.length - 1) { -// return ( -// -// ); -// } -// return null; -// })} -// -// ); -// } - -///crcted -// export default function PreDefinedPath() { -// const { gl, raycaster } = useThree(); -// const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)); - -// const [pointsGroups, setPointsGroups] = useState< -// [number, number, number][][] -// >([[]]); -// -// const [selected, setSelected] = useState([]); -// const downPosition = useRef<{ x: number; y: number } | null>(null); -// const hasClicked = useRef(false); - -// const handleMouseDown = useCallback((e: MouseEvent) => { -// hasClicked.current = false; -// downPosition.current = { x: e.clientX, y: e.clientY }; -// }, []); - -// const handleClick = useCallback( -// (e: MouseEvent) => { -// // Right click → start new group -// if (e.button === 2) { -// setPointsGroups((prev) => [...prev, []]); -// setSelected([]); -// return; -// } - -// // Left click only -// if (e.button !== 0) return; -// if (hasClicked.current) return; -// hasClicked.current = true; - -// if ( -// !downPosition.current || -// Math.abs(downPosition.current.x - e.clientX) > 2 || -// Math.abs(downPosition.current.y - e.clientY) > 2 -// ) { -// return; -// } - -// const intersection = new THREE.Vector3(); -// if (raycaster.ray.intersectPlane(plane.current, intersection)) { -// const pointArray = intersection.toArray() as [number, number, number]; -// setPointsGroups((prev) => { -// const newGroups = [...prev]; -// const currentGroup = [...newGroups[newGroups.length - 1]]; - -// const alreadyExists = currentGroup.some( -// (p) => -// Math.abs(p[0] - pointArray[0]) < 0.01 && -// Math.abs(p[1] - pointArray[1]) < 0.01 && -// Math.abs(p[2] - pointArray[2]) < 0.01 -// ); -// if (alreadyExists) return prev; - -// if (selected.length === 2) { -// const [startIdx, endIdx] = selected; -// const insertIndex = startIdx < endIdx ? startIdx + 1 : endIdx + 1; - -// currentGroup.splice(insertIndex, 0, pointArray); -// newGroups[newGroups.length - 1] = currentGroup; -// return newGroups; -// } else { -// currentGroup.push(pointArray); -// newGroups[newGroups.length - 1] = currentGroup; -// return newGroups; -// } -// }); -// } -// }, -// [raycaster, selected] -// ); - -// useEffect(() => { -// const domElement = gl.domElement; -// domElement.addEventListener("contextmenu", (e) => e.preventDefault()); // disable browser menu -// domElement.addEventListener("mousedown", handleMouseDown); -// domElement.addEventListener("mouseup", handleClick); -// return () => { -// domElement.removeEventListener("mousedown", handleMouseDown); -// domElement.removeEventListener("mouseup", handleClick); -// }; -// }, [handleClick, handleMouseDown]); - -// const updatePoints = ( -// groupIndex: number, -// i0: number, -// p0: THREE.Vector3, -// i1: number, -// p1: THREE.Vector3 -// ) => { -// setPointsGroups((prev) => { -// const newGroups = [...prev]; -// const group = [...newGroups[groupIndex]]; -// group[i0] = p0.toArray() as [number, number, number]; -// group[i1] = p1.toArray() as [number, number, number]; -// newGroups[groupIndex] = group; -// return newGroups; -// }); -// }; - -// const insertPoint = ( -// groupIndex: number, -// index: number, -// point: THREE.Vector3 -// ) => { -// setPointsGroups((prev) => { -// const newGroups = [...prev]; -// const group = [...newGroups[groupIndex]]; -// group.splice(index, 0, point.toArray() as [number, number, number]); -// newGroups[groupIndex] = group; -// return newGroups; -// }); -// }; - -// return ( -// <> -// {pointsGroups.map((group, gIdx) => ( -// -// {group.map((pos, idx) => ( -// { -// e.stopPropagation(); -// setSelected((prev) => -// prev.length === 2 ? [idx] : [...prev, idx] -// ); -// }} -// > -// -// -// -// ))} - -// {group.map((pos, i) => { -// if (i < group.length - 1) { -// return ( -// -// updatePoints(gIdx, i0, p0, i1, p1) -// } -// insertPoint={(index, point) => -// insertPoint(gIdx, index, point) -// } -// /> -// ); -// } -// return null; -// })} -// -// ))} -// -// ); -// } -// const handleClick = useCallback( -// (e: any) => { -// if (e.ctrlKey) return; -// if (e.button === 2) { -// setPointsGroups((prev) => [...prev, []]); -// setSelected([]); -// return; -// } -// if (e.button !== 0) return; -// if (hasClicked.current) return; -// hasClicked.current = true; - -// if ( -// !downPosition.current || -// Math.abs(downPosition.current.x - e.clientX) > 2 || -// Math.abs(downPosition.current.y - e.clientY) > 2 -// ) -// return; - -// const intersection = new THREE.Vector3(); -// if (raycaster.ray.intersectPlane(plane.current, intersection)) { -// const pointArray = intersection.toArray() as [number, number, number]; - -// setPointsGroups((prev) => { -// const newGroups = [...prev]; -// const currentGroup = [...newGroups[newGroups.length - 1]]; - -// const alreadyExists = currentGroup.some( -// (p) => -// Math.abs(p.position[0] - pointArray[0]) < 0.01 && -// Math.abs(p.position[1] - pointArray[1]) < 0.01 && -// Math.abs(p.position[2] - pointArray[2]) < 0.01 -// ); -// if (alreadyExists) return prev; - -// const newPoint: PointData = { -// pointId: crypto.randomUUID(), -// position: pointArray, -// isCurved: false, -// handleA: null, -// handleB: null, -// }; - -// if (selected.length === 2) { -// const [startIdx, endIdx] = selected; -// const insertIndex = startIdx < endIdx ? startIdx + 1 : endIdx + 1; -// currentGroup.splice(insertIndex, 0, newPoint); -// } else { -// currentGroup.push(newPoint); -// } - -// newGroups[newGroups.length - 1] = currentGroup; -// return newGroups; -// }); -// } -// }, -// [raycaster, selected] -// ); - type PointData = { pointId: string; position: [number, number, number]; @@ -354,25 +25,29 @@ type PointData = { handleA: [number, number, number] | null; handleB: [number, number, number] | null; }; + +interface PathDataInterface { + pathId: string; + pathPoints: [PointData, PointData]; +} + +type PathData = PathDataInterface[]; + export default function PreDefinedPath() { const { gl, raycaster } = useThree(); const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)); const [mainShapeOnly, setMainShapeOnly] = useState([]); - console.log("mainShapeOnly: ", mainShapeOnly); const [pointsGroups, setPointsGroups] = useState([[]]); - console.log("pointsGroups: ", pointsGroups); const [definedPath, setDefinedPath] = useState( [] ); - console.log("definedPath: ", definedPath); const [selected, setSelected] = useState([]); const downPosition = useRef<{ x: number; y: number } | null>(null); const hasClicked = useRef(false); const handleMouseDown = useCallback((e: any) => { - console.log("e.ctrlKey: ", e.ctrlKey); hasClicked.current = false; downPosition.current = { x: e.clientX, y: e.clientY }; }, []); @@ -516,6 +191,7 @@ export default function PreDefinedPath() { }, [pointsGroups]); const [shortestPath, setShortestPath] = useState([]); const [shortestDistance, setShortestDistance] = useState(0); + useEffect(() => { const domElement = gl.domElement; domElement.addEventListener("contextmenu", (e) => e.preventDefault()); @@ -537,6 +213,7 @@ export default function PreDefinedPath() { point={point} groupIndex={gIdx} pointIndex={idx} + // mainShapeOnly={mainShapeOnly} setPointsGroups={setPointsGroups} pointsGroups={pointsGroups} // <-- pass the full groups selected={selected} @@ -570,52 +247,13 @@ export default function PreDefinedPath() { return newGroups; }); }} - // insertPoint={(index, pointVec) => { - // setPointsGroups((prev) => { - // const newGroups = [...prev]; - // const groupToSplit = newGroups[gIdx]; - - // // Create the new point - // const newPoint = { - // pointId: crypto.randomUUID(), - // position: pointVec.toArray() as [ - // number, - // number, - // number - // ], - // isCurved: false, - // handleA: null, - // handleB: null, - // }; - - // // First half: everything from start to clicked segment - // const firstHalf = [ - // ...groupToSplit.slice(0, index), - // newPoint, - // ]; - - // // Second half: new point + everything after clicked segment - // const secondHalf = [ - // newPoint, - // ...groupToSplit.slice(index), - // ]; - - // // Replace the original group with the first half - // newGroups[gIdx] = firstHalf; - - // // Insert the second half as a new group right after - // newGroups.splice(gIdx + 1, 0, secondHalf); - - // return newGroups; - // }); - // }} - insertPoint={(index: number, pointVec: THREE.Vector3) => { + insertPoint={(index, pointVec) => { setPointsGroups((prev) => { const newGroups = [...prev]; - const group = [...newGroups[gIdx]]; + const groupToSplit = newGroups[gIdx]; // Create the new point - const newPoint: PointData = { + const newPoint = { pointId: crypto.randomUUID(), position: pointVec.toArray() as [ number, @@ -627,9 +265,24 @@ export default function PreDefinedPath() { handleB: null, }; - // Find best place to insert based on index (insert between points) - group.splice(index, 0, newPoint); // insert at index - newGroups[gIdx] = group; + // First half: everything from start to clicked segment + const firstHalf = [ + ...groupToSplit.slice(0, index), + newPoint, + ]; + + // Second half: new point + everything after clicked segment + const secondHalf = [ + newPoint, + ...groupToSplit.slice(index), + ]; + + // Replace the original group with the first half + newGroups[gIdx] = firstHalf; + + // Insert the second half as a new group right after + newGroups.splice(gIdx + 1, 0, secondHalf); + return newGroups; }); }} @@ -643,75 +296,3 @@ export default function PreDefinedPath() { ); } - -// const handleClick = useCallback( -// (e: MouseEvent) => { -// if (e.ctrlKey) return; -// if (e.button === 2) { -// setPointsGroups((prev) => [...prev, []]); -// setSelected([]); -// return; -// } -// if (e.button !== 0) return; - -// // Check small movement -// if ( -// !downPosition.current || -// Math.abs(downPosition.current.x - e.clientX) > 2 || -// Math.abs(downPosition.current.y - e.clientY) > 2 -// ) { -// return; -// } - -// const intersection = new THREE.Vector3(); -// if (raycaster.ray.intersectPlane(plane.current, intersection)) { -// const pointArray = intersection.toArray() as [number, number, number]; - -// setPointsGroups((prev) => { -// const newGroups = [...prev]; -// const currentGroup = [...newGroups[newGroups.length - 1]]; - -// // Search for existing point in ALL groups -// let existingPoint: PointData | null = null; -// for (const group of prev) { -// for (const p of group) { -// if ( -// Math.abs(p.position[0] - pointArray[0]) < 0.01 && -// Math.abs(p.position[1] - pointArray[1]) < 0.01 && -// Math.abs(p.position[2] - pointArray[2]) < 0.01 -// ) { -// existingPoint = p; -// break; -// } -// } -// if (existingPoint) break; -// } - -// if (existingPoint) { -// // Just connect to existing point without duplicating -// if ( -// currentGroup.length === 0 || -// currentGroup[currentGroup.length - 1].pointId !== -// existingPoint.pointId -// ) { -// currentGroup.push(existingPoint); -// } -// } else { -// // Create new point -// const newPoint: PointData = { -// pointId: crypto.randomUUID(), -// position: pointArray, -// isCurved: false, -// handleA: null, -// handleB: null, -// }; -// currentGroup.push(newPoint); -// } - -// newGroups[newGroups.length - 1] = currentGroup; -// return newGroups; -// }); -// } -// }, -// [raycaster] -// ); diff --git a/app/src/modules/simulation/vehicle/structuredPath/functions/aStarAlgorithm.ts b/app/src/modules/simulation/vehicle/structuredPath/functions/aStarAlgorithm.ts new file mode 100644 index 0000000..1ea20eb --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/functions/aStarAlgorithm.ts @@ -0,0 +1,95 @@ +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved: boolean; + handleA: [number, number, number] | null; + handleB: [number, number, number] | null; +}; + +interface PathDataInterface { + pathId: string; + pathPoints: [PointData, PointData]; +} + +type PathData = PathDataInterface[]; + +function distance(a: PointData, b: PointData): number { + const dx = a.position[0] - b.position[0]; + const dy = a.position[1] - b.position[1]; + const dz = a.position[2] - b.position[2]; + return Math.sqrt(dx * dx + dy * dy + dz * dz); +} + +type AStarResult = { + path: PointData[]; // ordered list of points along the path + distance: number; // total distance +}; + +export function aStarShortestPath( + startId: string, + goalId: string, + points: PointData[], + paths: PathData +): AStarResult | null { + const openSet = new Set([startId]); + const cameFrom: Record = {}; + const gScore: Record = {}; + const fScore: Record = {}; + + points.forEach((p) => { + gScore[p.pointId] = Infinity; + fScore[p.pointId] = Infinity; + cameFrom[p.pointId] = null; + }); + + gScore[startId] = 0; + fScore[startId] = 0; + + while (openSet.size > 0) { + // Pick node with lowest fScore + let current = [...openSet].reduce((a, b) => + fScore[a] < fScore[b] ? a : b + ); + + if (current === goalId) { + // ✅ Reconstruct path + const path: PointData[] = []; + let node: string | null = current; + while (node) { + const pt = points.find((p) => p.pointId === node); + if (pt) path.unshift(pt); + node = cameFrom[node]; + } + + return { + path, + distance: gScore[goalId], + }; + } + + openSet.delete(current); + + // Find neighbors from paths + const neighbors = paths.filter((p) => + p.pathPoints.some((pt) => pt.pointId === current) + ); + + for (let n of neighbors) { + const [p1, p2] = n.pathPoints; + const neighbor = p1.pointId === current ? p2 : p1; + + const tentativeG = + gScore[current] + + distance(points.find((pt) => pt.pointId === current)!, neighbor); + + if (tentativeG < gScore[neighbor.pointId]) { + cameFrom[neighbor.pointId] = current; + gScore[neighbor.pointId] = tentativeG; + fScore[neighbor.pointId] = tentativeG; // no heuristic for now + openSet.add(neighbor.pointId); + } + } + } + + return null; // no path found +} diff --git a/app/src/modules/simulation/vehicle/structuredPath/functions/handleContextMenu.ts b/app/src/modules/simulation/vehicle/structuredPath/functions/handleContextMenu.ts new file mode 100644 index 0000000..a6e9034 --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/functions/handleContextMenu.ts @@ -0,0 +1,7 @@ +export function handleContextMenu( + evt: MouseEvent, + setCurrentTempPath: (val: any[]) => void +) { + evt.preventDefault(); + setCurrentTempPath([]); +} diff --git a/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseClick.ts b/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseClick.ts new file mode 100644 index 0000000..8eeb654 --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseClick.ts @@ -0,0 +1,130 @@ +import * as THREE from "three"; +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved: boolean; + handleA: [number, number, number] | null; + handleB: [number, number, number] | null; +}; + +interface PathDataInterface { + pathId: string; + pathPoints: [PointData, PointData]; // always two points +} + +type PathData = PathDataInterface[]; +export const POLYGON_CLOSE_THRESHOLD = 0.3; +export const SNAP_POINT_THRESHOLD = 0.2; +export const SNAP_LINE_THRESHOLD = 0.2; +export function handleMouseClick({ + evt, + isDragging, + raycaster, + plane, + pointer, + currentTempPath, + setCurrentTempPath, + pathPointsList, + allPaths, + setAllPaths, + addPointToCurrentTemp, +}: { + evt: MouseEvent; + isDragging: { current: boolean }; + raycaster: THREE.Raycaster; + plane: THREE.Plane; + pointer: { x: number; y: number }; + currentTempPath: any[]; + setCurrentTempPath: (val: any[]) => void; + pathPointsList: any[]; + allPaths: any[]; + setAllPaths: React.Dispatch>; + addPointToCurrentTemp: (point: any) => void; +}) { + if (isDragging.current) return; + if (evt.ctrlKey || evt.shiftKey) return; + + const intersectPoint = new THREE.Vector3(); + const pos = raycaster.ray.intersectPlane(plane, intersectPoint); + if (!pos) return; + + let clickedPoint = new THREE.Vector3(pos.x, pos.y, pos.z); + + let snapPoint: any = null; + for (let p of pathPointsList) { + const pVec = new THREE.Vector3(...p.position); + if (pVec.distanceTo(clickedPoint) < SNAP_POINT_THRESHOLD) { + snapPoint = p; + clickedPoint = pVec; + break; + } + } + + let newPoint = snapPoint ?? { + pointId: THREE.MathUtils.generateUUID(), + position: [clickedPoint.x, 0, clickedPoint.z], + isCurved: false, + handleA: null, + handleB: null, + }; + + if (currentTempPath.length > 2) { + const firstVec = new THREE.Vector3(...currentTempPath[0].position); + if (firstVec.distanceTo(clickedPoint) < POLYGON_CLOSE_THRESHOLD) { + const closingPoint = { ...currentTempPath[0] }; + console.log("closingPoint: ", closingPoint); + addPointToCurrentTemp(closingPoint); + setCurrentTempPath([]); + return; + } + } + + const getNearestPointOnLine = ( + a: THREE.Vector3, + b: THREE.Vector3, + p: THREE.Vector3 + ) => { + const ab = new THREE.Vector3().subVectors(b, a); + const t = Math.max( + 0, + Math.min(1, p.clone().sub(a).dot(ab) / ab.lengthSq()) + ); + return a.clone().add(ab.multiplyScalar(t)); + }; + + for (let path of allPaths) { + const a = new THREE.Vector3(...path.pathPoints[0].position); + const b = new THREE.Vector3(...path.pathPoints[1].position); + const closest = getNearestPointOnLine(a, b, clickedPoint); + + if (closest.distanceTo(clickedPoint) < SNAP_LINE_THRESHOLD) { + const splitPoint = { + pointId: THREE.MathUtils.generateUUID(), + position: closest.toArray() as [number, number, number], + isCurved: false, + handleA: null, + handleB: null, + }; + + setAllPaths((prev: any) => + prev + .filter((pa: any) => pa.pathId !== path.pathId) + .concat([ + { + pathId: THREE.MathUtils.generateUUID(), + pathPoints: [path.pathPoints[0], splitPoint], + }, + { + pathId: THREE.MathUtils.generateUUID(), + pathPoints: [splitPoint, path.pathPoints[1]], + }, + ]) + ); + + addPointToCurrentTemp(splitPoint); + return; + } + } + + addPointToCurrentTemp(newPoint); +} diff --git a/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseDown.ts b/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseDown.ts new file mode 100644 index 0000000..e886fe6 --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseDown.ts @@ -0,0 +1,13 @@ +export function handleMouseDown( + evt: MouseEvent, + isLeftClickDown: { current: boolean }, + isDragging: { current: boolean } +) { + if (evt.button === 0) { + if (evt.ctrlKey || evt.shiftKey) return; + isLeftClickDown.current = true; + isDragging.current = false; + } +} + + diff --git a/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseMove.ts b/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseMove.ts new file mode 100644 index 0000000..5100c13 --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseMove.ts @@ -0,0 +1,6 @@ +export function handleMouseMove( + isLeftClickDown: { current: boolean }, + isDragging: { current: boolean } +) { + if (isLeftClickDown.current) isDragging.current = true; +} diff --git a/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseUp.ts b/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseUp.ts new file mode 100644 index 0000000..59eae32 --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/functions/handleMouseUp.ts @@ -0,0 +1,6 @@ +export function handleMouseUp( + evt: MouseEvent, + isLeftClickDown: { current: boolean } +) { + if (evt.button === 0) isLeftClickDown.current = false; +} diff --git a/app/src/modules/simulation/vehicle/structuredPath/functions/pathMouseHandler.ts b/app/src/modules/simulation/vehicle/structuredPath/functions/pathMouseHandler.ts new file mode 100644 index 0000000..41b106b --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/functions/pathMouseHandler.ts @@ -0,0 +1,209 @@ +import * as THREE from "three"; + +export const POLYGON_CLOSE_THRESHOLD = 0.3; +export const SNAP_POINT_THRESHOLD = 0.2; +export const SNAP_LINE_THRESHOLD = 0.2; +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved: boolean; + handleA: [number, number, number] | null; + handleB: [number, number, number] | null; +}; + +interface PathDataInterface { + pathId: string; + pathPoints: [PointData, PointData]; // always two points +} + +type PathData = PathDataInterface[]; + +export function handleMouseDown( + evt: MouseEvent, + isLeftClickDown: React.MutableRefObject, + isDragging: React.MutableRefObject +) { + if (evt.button === 0) { + if (evt.ctrlKey || evt.shiftKey) return; + isLeftClickDown.current = true; + isDragging.current = false; + } +} + +export function handleMouseUp( + evt: MouseEvent, + isLeftClickDown: React.MutableRefObject +) { + if (evt.button === 0) isLeftClickDown.current = false; +} + +export function handleMouseMove( + isLeftClickDown: React.MutableRefObject, + isDragging: React.MutableRefObject +) { + if (isLeftClickDown.current) isDragging.current = true; +} + +export function handleMouseClick({ + evt, + isDragging, + raycaster, + plane, + pointer, + currentTempPath, + setCurrentTempPath, + pathPointsList, + allPaths, + setAllPaths, + addPointToCurrentTemp, +}: { + evt: MouseEvent; + isDragging: React.MutableRefObject; + raycaster: THREE.Raycaster; + plane: THREE.Plane; + pointer: { x: number; y: number }; + currentTempPath: any[]; + setCurrentTempPath: (val: any[]) => void; + pathPointsList: any[]; + allPaths: PathData; + setAllPaths: React.Dispatch>; + addPointToCurrentTemp: (point: any) => void; +}) { + if (isDragging.current) return; + if (evt.ctrlKey || evt.shiftKey) return; + + const intersectPoint = new THREE.Vector3(); + const pos = raycaster.ray.intersectPlane(plane, intersectPoint); + if (!pos) return; + + let clickedPoint = new THREE.Vector3(pos.x, 0, pos.z); // force y = 0 + + let snapPoint: any = null; + for (let p of pathPointsList) { + const pVec = new THREE.Vector3(p.position[0], 0, p.position[2]); // force y = 0 + if (pVec.distanceTo(clickedPoint) < SNAP_POINT_THRESHOLD) { + snapPoint = { + ...p, + position: [p.position[0], 0, p.position[2]], // force y = 0 + }; + clickedPoint = pVec; + break; + } + } + + let newPoint = snapPoint ?? { + pointId: THREE.MathUtils.generateUUID(), + position: [clickedPoint.x, 0, clickedPoint.z], // y = 0 + isCurved: false, + handleA: null, + handleB: null, + }; + + if (currentTempPath.length > 2) { + const firstVec = new THREE.Vector3( + currentTempPath[0].position[0], + 0, + currentTempPath[0].position[2] + ); // y = 0 + if (firstVec.distanceTo(clickedPoint) < POLYGON_CLOSE_THRESHOLD) { + const closingPoint = { + ...currentTempPath[0], + position: [ + currentTempPath[0].position[0], + 0, + currentTempPath[0].position[2], + ], // y = 0 + }; + addPointToCurrentTemp(closingPoint); + setCurrentTempPath([]); + return; + } + } + + const getNearestPointOnLine = ( + a: THREE.Vector3, + b: THREE.Vector3, + p: THREE.Vector3 + ) => { + const ab = new THREE.Vector3().subVectors(b, a); + const t = Math.max( + 0, + Math.min(1, p.clone().sub(a).dot(ab) / ab.lengthSq()) + ); + return a.clone().add(ab.multiplyScalar(t)); + }; + + for (let path of allPaths) { + const a = new THREE.Vector3( + path.pathPoints[0].position[0], + 0, + path.pathPoints[0].position[2] + ); + const b = new THREE.Vector3( + path.pathPoints[1].position[0], + 0, + path.pathPoints[1].position[2] + ); + const closest = getNearestPointOnLine(a, b, clickedPoint); + closest.y = 0; // force y = 0 + + if (closest.distanceTo(clickedPoint) < SNAP_LINE_THRESHOLD) { + const splitPoint = { + pointId: THREE.MathUtils.generateUUID(), + position: [closest.x, 0, closest.z], // y = 0 + isCurved: false, + handleA: null, + handleB: null, + }; + + setAllPaths((prev) => + prev + .filter((pa) => pa.pathId !== path.pathId) + .concat([ + { + pathId: THREE.MathUtils.generateUUID(), + pathPoints: [ + { + ...path.pathPoints[0], + position: [ + path.pathPoints[0].position[0], + 0, + path.pathPoints[0].position[2], + ] as [number, number, number], + }, + splitPoint, + ] as [PointData, PointData], + }, + { + pathId: THREE.MathUtils.generateUUID(), + pathPoints: [ + splitPoint, + { + ...path.pathPoints[1], + position: [ + path.pathPoints[1].position[0], + 0, + path.pathPoints[1].position[2], + ] as [number, number, number], + }, + ] as [PointData, PointData], + }, + ]) + ); + console.log("path.pathPoints[1]: ", path.pathPoints); + + addPointToCurrentTemp(splitPoint); + return; + } + } + + addPointToCurrentTemp(newPoint); +} + +export function handleContextMenu( + evt: MouseEvent, + setCurrentTempPath: (val: any[]) => void +) { + evt.preventDefault(); + setCurrentTempPath([]); +} diff --git a/app/src/modules/simulation/vehicle/structuredPath/lineSegment.tsx b/app/src/modules/simulation/vehicle/structuredPath/lineSegment.tsx new file mode 100644 index 0000000..8fc610d --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/lineSegment.tsx @@ -0,0 +1,84 @@ +import { useMemo, useRef } from "react"; +import { useThree } from "@react-three/fiber"; +import * as THREE from "three"; +import { Line } from "@react-three/drei"; + +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved: boolean; + handleA: [number, number, number] | null; + handleB: [number, number, number] | null; +}; + +interface PathDataInterface { + pathId: string; + pathPoints: [PointData, PointData]; +} + +type PathData = PathDataInterface[]; + +interface LineSegmentProps { + index: number; + paths: PathDataInterface[]; + setPaths: React.Dispatch>; + insertPoint?: (pathIndex: number, point: THREE.Vector3) => void; +} + +export default function LineSegment({ + index, + paths, + setPaths, + insertPoint, +}: LineSegmentProps) { + const { gl, raycaster, camera, controls } = useThree(); + const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); + + const segmentPoints = useMemo(() => { + if (!paths[index]) return []; + + const [startPoint, endPoint] = paths[index].pathPoints; + + const start = new THREE.Vector3(...startPoint.position); + const end = new THREE.Vector3(...endPoint.position); + + const useCurve = + (startPoint.isCurved && startPoint.handleB) || + (endPoint.isCurved && endPoint.handleA); + + const hB = startPoint.handleB + ? new THREE.Vector3(...startPoint.handleB) + : start; + const hA = endPoint.handleA ? new THREE.Vector3(...endPoint.handleA) : end; + + const curve = useCurve + ? new THREE.CubicBezierCurve3(start, hB, hA, end) + : new THREE.LineCurve3(start, end); + + return curve.getPoints(useCurve ? 100 : 2); + }, [paths, index]); + + const curvePoints = useMemo(() => { + if (!paths || index >= paths.length) return []; + + const path = paths[index]; + const [current, next] = path.pathPoints; + + const start = new THREE.Vector3(...current.position); + const end = new THREE.Vector3(...next.position); + + const useCurve = + (current.isCurved && current.handleB) || (next.isCurved && next.handleA); + + const hB = current.handleB ? new THREE.Vector3(...current.handleB) : start; + const hA = next.handleA ? new THREE.Vector3(...next.handleA) : end; + + const curve = useCurve + ? new THREE.CubicBezierCurve3(start, hB, hA, end) + : new THREE.LineCurve3(start, end); + + return curve.getPoints(useCurve ? 100 : 2); + }, [paths, index]); + + return ; +} diff --git a/app/src/modules/simulation/vehicle/structuredPath/pointHandlers.tsx b/app/src/modules/simulation/vehicle/structuredPath/pointHandlers.tsx new file mode 100644 index 0000000..5c1d786 --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/pointHandlers.tsx @@ -0,0 +1,815 @@ +// import { Line } from "@react-three/drei"; +// import { useFrame, useThree } from "@react-three/fiber"; +// import React, { useRef, useState } from "react"; +// import * as THREE from "three"; + +// /** --- Types --- */ +// type PointData = { +// pointId: string; +// position: [number, number, number]; +// isCurved: boolean; +// handleA: [number, number, number] | null; +// handleB: [number, number, number] | null; +// }; + +// interface PathDataInterface { +// pathId: string; +// pathPoints: [PointData, PointData]; // always two points +// } + +// type PathData = PathDataInterface[]; + +// interface PointHandleProps { +// point: PointData; +// pointIndex: number; +// points: PointData[]; +// setPoints: React.Dispatch>; +// setPaths: React.Dispatch>; +// paths: PathData; +// selected: number[]; +// setSelected: React.Dispatch>; +// setShortestPath: React.Dispatch>; +// } + +// /** --- Math helpers --- */ +// 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 +// ); +// } + +// /** --- 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) edges.push(edge); +// // else { +// // const pa = byId.get(a)!; +// // const pb = byId.get(b)!; +// // edges.push({ +// // pathId: `synthetic-${a}-${b}`, +// // pathPoints: [pa, pb], +// // }); +// // } +// // } +// // + +// // return 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) { +// // Ensure correct order in 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; +// } + +// /** --- React Component --- */ +// export default function PointHandlers({ +// point, +// pointIndex, +// points, +// setPoints, +// setPaths, +// paths, +// selected, +// setSelected, +// setShortestPath, +// }: PointHandleProps) { +// const meshRef = useRef(null); +// const handleARef = useRef(null); +// const handleBRef = useRef(null); +// const lineRef = useRef(null!); +// const { camera, gl, controls } = useThree(); + +// const [dragging, setDragging] = useState< +// null | "main" | "handleA" | "handleB" +// >(null); +// const dragOffset = useRef(new THREE.Vector3()); +// const [shortestEdges, setShortestEdges] = useState([]); + +// /** Click handling */ +// const onPointClick = (e: any) => { +// e.stopPropagation(); +// if (e.shiftKey) { +// setSelected((prev) => { +// if (prev.length === 0) return [pointIndex]; +// else if (prev.length === 1) { +// const p1 = points[prev[0]]; +// const p2 = points[pointIndex]; +// const result = aStarShortestPath( +// p1.pointId, +// p2.pointId, +// points, +// paths +// ); +// if (result) { +// const edges = nodePathToEdges(result.pointIds, points, paths); +// setShortestEdges(edges); +// setShortestPath(edges); +// } else { +// setShortestEdges([]); +// } +// return [prev[0], pointIndex]; +// } else { +// setShortestEdges([]); +// return [pointIndex]; +// } +// }); +// } else if (e.ctrlKey) { +// setPoints((prev) => { +// const updated = [...prev]; +// const p = { ...updated[pointIndex] }; + +// if (!p.handleA && !p.handleB) { +// p.handleA = [p.position[0] + 1, p.position[1], p.position[2]]; +// p.handleB = [p.position[0] - 1, p.position[1], p.position[2]]; +// p.isCurved = true; +// } else { +// p.handleA = null; +// p.handleB = null; +// p.isCurved = false; +// } + +// updated[pointIndex] = p; +// return updated; +// }); +// } +// }; + +// /** Dragging logic */ +// const startDrag = (target: "main" | "handleA" | "handleB", e: any) => { +// e.stopPropagation(); +// setDragging(target); +// const targetRef = +// target === "main" +// ? meshRef.current +// : target === "handleA" +// ? handleARef.current +// : handleBRef.current; +// if (targetRef) dragOffset.current.copy(targetRef.position).sub(e.point); +// if (controls) (controls as any).enabled = false; +// gl.domElement.style.cursor = "grabbing"; +// }; + +// const stopDrag = () => { +// setDragging(null); +// gl.domElement.style.cursor = "auto"; +// if (controls) (controls as any).enabled = true; +// }; + +// useFrame(({ raycaster, mouse }) => { +// if (!dragging) return; + +// const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); +// raycaster.setFromCamera(mouse, camera); + +// const intersection = new THREE.Vector3(); +// if (!raycaster.ray.intersectPlane(plane, intersection)) return; + +// const newPos = intersection.add(dragOffset.current); + +// setPoints((prevPoints) => { +// const updatedPoints = [...prevPoints]; +// const point = { ...updatedPoints[pointIndex] }; + +// if (dragging === "main") { +// // Calculate delta movement +// const delta = newPos +// .clone() +// .sub(new THREE.Vector3().fromArray(point.position)); + +// // Move main point +// point.position = newPos.toArray() as [number, number, number]; + +// // Move handles with main point +// if (point.handleA) +// point.handleA = new THREE.Vector3() +// .fromArray(point.handleA) +// .add(delta) +// .toArray() as [number, number, number]; +// if (point.handleB) +// point.handleB = new THREE.Vector3() +// .fromArray(point.handleB) +// .add(delta) +// .toArray() as [number, number, number]; +// } else { +// // Dragging a handle +// point[dragging] = newPos.toArray() as [number, number, number]; + +// if (point.isCurved) { +// // Mirror the opposite handle +// const mainPos = new THREE.Vector3().fromArray(point.position); +// const thisHandle = new THREE.Vector3().fromArray(point[dragging]!); +// const mirrorHandle = mainPos +// .clone() +// .sub(thisHandle.clone().sub(mainPos)); + +// if (dragging === "handleA") +// point.handleB = mirrorHandle.toArray() as [number, number, number]; +// if (dragging === "handleB") +// point.handleA = mirrorHandle.toArray() as [number, number, number]; +// } +// } + +// updatedPoints[pointIndex] = point; + +// // Update all paths that include this point +// setPaths((prevPaths: any) => +// prevPaths.map((path: any) => { +// const updatedPathPoints = path.pathPoints.map((p: any) => +// p.pointId === point.pointId ? point : p +// ); +// return { ...path, pathPoints: updatedPathPoints }; +// }) +// ); + +// return updatedPoints; +// }); +// }); + +// /** Update line between handles */ +// useFrame(() => { +// if (lineRef.current && point.handleA && point.handleB) { +// const positions = lineRef.current.geometry.attributes.position +// .array as Float32Array; +// positions[0] = point.handleA[0]; +// positions[1] = point.handleA[1]; +// positions[2] = point.handleA[2]; +// positions[3] = point.handleB[0]; +// positions[4] = point.handleB[1]; +// positions[5] = point.handleB[2]; +// lineRef.current.geometry.attributes.position.needsUpdate = true; +// } +// }); + +// return ( +// <> +// {/* Main point */} +// startDrag("main", e)} +// onPointerUp={stopDrag} +// > +// +// +// + +// {/* Curve handles */} +// {point.isCurved && point.handleA && point.handleB && ( +// <> +// +// startDrag("handleA", e)} +// onPointerUp={stopDrag} +// > +// +// +// +// startDrag("handleB", e)} +// onPointerUp={stopDrag} +// > +// +// +// +// +// )} + +// {/* Draw connected paths */} + +// {/* Highlight shortest path */} +// {shortestEdges.map((edge) => ( +// p.position)} +// color="yellow" +// lineWidth={3} +// /> +// ))} +// +// ); +// } + +/** --- Types --- */ +import { Line } from "@react-three/drei"; +import { useFrame, useThree } from "@react-three/fiber"; +import React, { useRef, useState } from "react"; +import * as THREE from "three"; + +/** --- Types --- */ +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved: boolean; + handleA: [number, number, number] | null; + handleB: [number, number, number] | null; +}; + +interface PathDataInterface { + pathId: string; + pathPoints: [PointData, PointData]; // always two points +} + +type PathData = PathDataInterface[]; + +interface PointHandleProps { + point: PointData; + pointIndex: number; + points: PointData[]; + setPoints: React.Dispatch>; + setPaths: React.Dispatch>; + paths: PathData; + selected: number[]; + setSelected: React.Dispatch>; + setShortestPath: React.Dispatch>; +} + +/** --- Math helpers --- */ +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 + ); +} + +/** --- 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; +} + +/** --- React Component --- */ +export default function PointHandlersRenamed({ + point, + pointIndex, + points, + setPoints, + setPaths, + paths, + selected, + setSelected, + setShortestPath, +}: PointHandleProps) { + const meshRef = useRef(null); + const handleARef = useRef(null); + const handleBRef = useRef(null); + const lineRef = useRef(null); + const { camera, gl, controls } = useThree(); + + const [dragging, setDragging] = useState< + null | "main" | "handleA" | "handleB" + >(null); + const dragOffset = useRef(new THREE.Vector3()); + const [shortestEdges, setShortestEdges] = useState([]); + + /** Click handling */ + const onPointClick = (e: any) => { + e.stopPropagation(); + + if (e.shiftKey) { + setSelected((prev) => { + if (prev.length === 0) return [pointIndex]; + if (prev.length === 1) { + // defer shortest path calculation + setTimeout(() => { + const p1 = points[prev[0]]; + const p2 = points[pointIndex]; + const result = aStarShortestPath( + p1.pointId, + p2.pointId, + points, + paths + ); + if (result) { + const edges = nodePathToEdges(result.pointIds, points, paths); + setShortestEdges(edges); + setShortestPath(edges); + } else { + setShortestEdges([]); + } + }, 0); + return [prev[0], pointIndex]; + } + return [pointIndex]; + }); + } else if (e.ctrlKey) { + setPoints((prev) => { + const updated = [...prev]; + const p = { ...updated[pointIndex] }; + + if (!p.handleA && !p.handleB) { + p.handleA = [p.position[0] + 1, 0, p.position[2]]; + p.handleB = [p.position[0] - 1, 0, p.position[2]]; + p.isCurved = true; + } else { + p.handleA = null; + p.handleB = null; + p.isCurved = false; + } + + updated[pointIndex] = p; + return updated; + }); + } + }; + + /** Dragging logic */ + const startDrag = (target: "main" | "handleA" | "handleB", e: any) => { + e.stopPropagation(); + setDragging(target); + const targetRef = + target === "main" + ? meshRef.current + : target === "handleA" + ? handleARef.current + : handleBRef.current; + if (targetRef) dragOffset.current.copy(targetRef.position).sub(e.point); + if (controls) (controls as any).enabled = false; + gl.domElement.style.cursor = "grabbing"; + }; + + const stopDrag = () => { + setDragging(null); + gl.domElement.style.cursor = "auto"; + if (controls) (controls as any).enabled = true; + }; + + /** Update position in useFrame */ + useFrame(({ raycaster, mouse }) => { + if (!dragging) return; + const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); + raycaster.setFromCamera(mouse, camera); + const intersection = new THREE.Vector3(); + if (!raycaster.ray.intersectPlane(plane, intersection)) return; + const newPos = intersection.add(dragOffset.current); + + setPoints((prevPoints) => { + const updatedPoints = [...prevPoints]; + const p = { ...updatedPoints[pointIndex] }; + + if (dragging === "main") { + const delta = newPos + .clone() + .sub(new THREE.Vector3().fromArray(p.position)); + p.position = [newPos.x, 0, newPos.z]; + if (p.handleA) + p.handleA = new THREE.Vector3() + .fromArray(p.handleA) + .add(delta) + .toArray() as [number, number, number]; + if (p.handleB) + p.handleB = new THREE.Vector3() + .fromArray(p.handleB) + .add(delta) + .toArray() as [number, number, number]; + } else { + p[dragging] = [newPos.x, 0, newPos.z]; + if (p.isCurved) { + const mainPos = new THREE.Vector3().fromArray(p.position); + const thisHandle = new THREE.Vector3().fromArray(p[dragging]!); + const mirrorHandle = mainPos + .clone() + .sub(thisHandle.clone().sub(mainPos)); + if (dragging === "handleA") + p.handleB = mirrorHandle.toArray() as [number, number, number]; + if (dragging === "handleB") + p.handleA = mirrorHandle.toArray() as [number, number, number]; + } + } + + updatedPoints[pointIndex] = p; + + setPaths((prevPaths: any) => + prevPaths.map((path: any) => ({ + ...path, + pathPoints: path.pathPoints.map((pp: any) => + pp.pointId === p.pointId ? p : pp + ), + })) + ); + + return updatedPoints; + }); + }); + + /** Update line between handles */ + useFrame(() => { + if (lineRef.current && point.handleA && point.handleB) { + const positions = lineRef.current.geometry.attributes.position + .array as Float32Array; + positions[0] = point.handleA[0]; + positions[1] = point.handleA[1]; + positions[2] = point.handleA[2]; + positions[3] = point.handleB[0]; + positions[4] = point.handleB[1]; + positions[5] = point.handleB[2]; + lineRef.current.geometry.attributes.position.needsUpdate = true; + } + }); + + return ( + <> + {/* Main point */} + startDrag("main", e)} + onPointerUp={stopDrag} + > + + + + + {/* Curve handles */} + {point.isCurved && point.handleA && point.handleB && ( + <> + + startDrag("handleA", e)} + onPointerUp={stopDrag} + > + + + + startDrag("handleB", e)} + onPointerUp={stopDrag} + > + + + + + )} + + {/* Highlight shortest path */} + {shortestEdges.map((edge) => ( + p.position)} + color="yellow" + lineWidth={3} + /> + ))} + + ); +} diff --git a/app/src/modules/simulation/vehicle/structuredPath/structuredPath.tsx b/app/src/modules/simulation/vehicle/structuredPath/structuredPath.tsx new file mode 100644 index 0000000..8e2618b --- /dev/null +++ b/app/src/modules/simulation/vehicle/structuredPath/structuredPath.tsx @@ -0,0 +1,763 @@ +import * as THREE from "three"; +import { useRef, useState, useMemo, useEffect } from "react"; +import { useThree, useFrame } from "@react-three/fiber"; +import { Line } from "@react-three/drei"; +import { useSceneContext } from "../../../scene/sceneContext"; +import { + useAnimationPlaySpeed, + usePlayButtonStore, +} from "../../../../store/usePlayButtonStore"; +import PointHandles from "./pointHandlers"; +import LineSegment from "./lineSegment"; +import { + handleContextMenu, + handleMouseClick, + handleMouseDown, + handleMouseMove, + handleMouseUp, +} from "./functions/pathMouseHandler"; + +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved: boolean; + handleA: [number, number, number] | null; + handleB: [number, number, number] | null; +}; + +interface PathDataInterface { + pathId: string; + pathPoints: [PointData, PointData]; +} + +type PathData = PathDataInterface[]; +type SegmentPoint = { + position: THREE.Vector3; + originalPoint?: PointData; +}; +// ///////////////////////////////////logic wise crct code (path and points) +// export default function StructuredPath() { +// const { scene, camera, raycaster, gl, pointer } = useThree(); +// const plane = useMemo( +// () => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), +// [] +// ); +// const { speed } = useAnimationPlaySpeed(); +// const { assetStore } = useSceneContext(); +// const { assets } = assetStore(); +// const [points, setPoints] = useState([]); +// const [paths, setPaths] = useState([]); +// const [shortestPath, setShortestPath] = useState([]); +// useEffect(() => { +// console.log("paths: ", paths); +// }, [paths]); +// const [tempPath, setTempPath] = useState([]); +// const [mousePos, setMousePos] = useState<[number, number, number] | null>( +// null +// ); +// const [selected, setSelected] = useState([]); +// const POLYGON_CLOSE_THRESHOLD = 0.3; +// const isLeftMouseDown = useRef(false); +// const drag = useRef(false); +// const SNAP_POINT_THRESHOLD = 0.2; +// const [assetUuid, setAssetUuid] = useState(); +// const SNAP_LINE_THRESHOLD = 0.2; +// const movementState = useRef({}); +// const activeIndexRef = useRef(0); +// const { isPlaying } = usePlayButtonStore(); + +// const pathSegments = useMemo(() => { +// if (!shortestPath || shortestPath.length === 0) return []; + +// const segments: SegmentPoint[] = []; + +// shortestPath.forEach((path) => { +// const [start, end] = path.pathPoints; + +// if (start.isCurved && start.handleA && start.handleB) { +// // Curved segment +// const curve = new THREE.CubicBezierCurve3( +// new THREE.Vector3(...start.position), +// new THREE.Vector3(...start.handleA), +// new THREE.Vector3(...start.handleB), +// new THREE.Vector3(...end.position) +// ); +// const points = curve +// .getPoints(20) +// .map((pos) => ({ position: pos, originalPoint: start })); +// segments.push(...points); +// } else { +// // Straight segment +// segments.push( +// { +// position: new THREE.Vector3(...start.position), +// originalPoint: start, +// }, +// { position: new THREE.Vector3(...end.position), originalPoint: end } +// ); +// } +// }); + +// // Remove duplicates +// return segments.filter( +// (v, i, arr) => i === 0 || !v.position.equals(arr[i - 1].position) +// ); +// }, [shortestPath]); + +// useEffect(() => { +// const findVehicle = assets +// .filter((val) => val.eventData?.type === "Vehicle") +// ?.map((val) => val.modelUuid); + +// setAssetUuid(findVehicle); + +// movementState.current = {}; +// findVehicle.forEach((uuid) => { +// movementState.current[uuid] = { +// index: 0, +// progress: 0, +// }; +// }); +// }, [assets]); + +// useFrame((_, delta) => { +// if (!isPlaying || pathSegments.length < 2) return; + +// const object = scene.getObjectByProperty( +// "uuid", +// assetUuid[activeIndexRef.current] +// ); +// if (!object) return; + +// const state = movementState.current[assetUuid[activeIndexRef.current]]; +// if (!state) return; + +// // Current start & end +// const startSeg = pathSegments[state.index]; +// const endSeg = pathSegments[state.index + 1]; + +// const segmentDistance = startSeg.position.distanceTo(endSeg.position); +// state.progress += (speed * delta) / segmentDistance; + +// if (state.progress >= 1) { +// state.progress = 0; +// state.index++; +// if (state.index >= pathSegments.length - 1) { +// state.index = 0; +// activeIndexRef.current = +// (activeIndexRef.current + 1) % assetUuid.length; +// } +// } + +// // Move object along sampled points +// const newPos = startSeg.position +// .clone() +// .lerp(endSeg.position, state.progress); +// object.position.copy(newPos); + +// // Smooth rotation +// const direction = endSeg.position +// .clone() +// .sub(startSeg.position) +// .normalize(); +// const forward = new THREE.Vector3(0, 0, 1); +// object.quaternion.setFromUnitVectors(forward, direction); + +// // Access handles if needed +// if (startSeg.originalPoint?.handleA) { +// const handleA = startSeg.originalPoint.handleA; +// const handleB = startSeg.originalPoint.handleB; +// // do something with handles here +// } +// }); + +// useFrame(() => { +// if (tempPath.length === 0) return; +// raycaster.setFromCamera(pointer, camera); +// const intersect = new THREE.Vector3(); +// if (raycaster.ray.intersectPlane(plane, intersect)) { +// setMousePos([intersect.x, intersect.y, intersect.z]); +// } +// }); + +// const getClosestPointOnLine = ( +// a: THREE.Vector3, +// b: THREE.Vector3, +// p: THREE.Vector3 +// ) => { +// const ab = new THREE.Vector3().subVectors(b, a); +// const t = Math.max( +// 0, +// Math.min(1, p.clone().sub(a).dot(ab) / ab.lengthSq()) +// ); +// return a.clone().add(ab.multiplyScalar(t)); +// }; + +// const addPointToTemp = (newPoint: PointData) => { +// // Add to tempPath +// setTempPath((prev) => { +// const updated = [...prev, newPoint]; + +// // Create new path from last point to this one +// if (prev.length > 0) { +// const lastPoint = prev[prev.length - 1]; +// const newPath: PathDataInterface = { +// pathId: THREE.MathUtils.generateUUID(), +// pathPoints: [lastPoint, newPoint], +// }; +// setPaths((prevPaths) => [...prevPaths, newPath]); +// } + +// return updated; +// }); + +// // Add to global points if not already exists +// setPoints((prev) => { +// if (!prev.find((p) => p.pointId === newPoint.pointId)) +// return [...prev, newPoint]; +// return prev; +// }); +// }; + +// useEffect(() => { +// const canvas = gl.domElement; + +// const onMouseDown = (evt: MouseEvent) => { +// if (evt.button === 0) { +// if (evt.ctrlKey || evt.shiftKey) return; +// isLeftMouseDown.current = true; +// drag.current = false; +// } +// }; + +// const onMouseUp = (evt: MouseEvent) => { +// if (evt.button === 0) isLeftMouseDown.current = false; +// }; + +// const onMouseMove = () => { +// if (isLeftMouseDown.current) drag.current = true; +// }; + +// const onClick = (evt: MouseEvent) => { +// if (drag.current) return; + +// if (evt.ctrlKey || evt.shiftKey) return; +// const intersectPoint = new THREE.Vector3(); +// const pointIntersects = raycaster +// .intersectObjects(scene.children) +// .find((intersect) => intersect.object.name === "path-line"); + +// const pos = raycaster.ray.intersectPlane(plane, intersectPoint); +// if (!pos) return; + +// let clickedPoint = new THREE.Vector3(pos.x, pos.y, pos.z); + +// // Snap to existing point if close +// let snapPoint: PointData | null = null; +// for (let p of points) { +// const pVec = new THREE.Vector3(...p.position); +// if (pVec.distanceTo(clickedPoint) < SNAP_POINT_THRESHOLD) { +// snapPoint = p; +// clickedPoint = pVec; +// break; +// } +// } + +// let newPoint: PointData = snapPoint ?? { +// pointId: THREE.MathUtils.generateUUID(), +// position: [clickedPoint.x, 0, clickedPoint.z], +// isCurved: false, +// handleA: null, +// handleB: null, +// }; + +// // Check if polygon can be closed +// if (tempPath.length > 2) { +// const firstVec = new THREE.Vector3(...tempPath[0].position); +// if (firstVec.distanceTo(clickedPoint) < POLYGON_CLOSE_THRESHOLD) { +// // Close polygon by connecting last point → first point +// const closingPoint = { ...tempPath[0] }; +// addPointToTemp(closingPoint); +// setTempPath([]); // Polygon finished +// return; +// } +// } + +// // Split existing line if clicked near it (same as before) +// for (let path of paths) { +// const a = new THREE.Vector3(...path.pathPoints[0].position); +// const b = new THREE.Vector3(...path.pathPoints[1].position); +// const closest = getClosestPointOnLine(a, b, clickedPoint); + +// if (closest.distanceTo(clickedPoint) < SNAP_LINE_THRESHOLD) { +// const splitPoint: PointData = { +// pointId: THREE.MathUtils.generateUUID(), +// position: closest.toArray() as [number, number, number], +// isCurved: false, +// handleA: null, +// handleB: null, +// }; + +// // Remove original path and replace with split paths +// setPaths((prev) => +// prev +// .filter((pa) => pa.pathId !== path.pathId) +// .concat([ +// { +// pathId: THREE.MathUtils.generateUUID(), +// pathPoints: [path.pathPoints[0], splitPoint], +// }, +// { +// pathId: THREE.MathUtils.generateUUID(), +// pathPoints: [splitPoint, path.pathPoints[1]], +// }, +// ]) +// ); + +// addPointToTemp(splitPoint); +// return; +// } +// } + +// // Normal add point +// addPointToTemp(newPoint); +// }; + +// const onContextMenu = (evt: MouseEvent) => { +// evt.preventDefault(); +// setTempPath([]); // Cancel current polygon +// }; + +// canvas.addEventListener("mousedown", onMouseDown); +// canvas.addEventListener("mouseup", onMouseUp); +// canvas.addEventListener("mousemove", onMouseMove); +// canvas.addEventListener("click", onClick); +// canvas.addEventListener("contextmenu", onContextMenu); + +// return () => { +// canvas.removeEventListener("mousedown", onMouseDown); +// canvas.removeEventListener("mouseup", onMouseUp); +// canvas.removeEventListener("mousemove", onMouseMove); +// canvas.removeEventListener("click", onClick); +// canvas.removeEventListener("contextmenu", onContextMenu); +// }; +// }, [gl, camera, raycaster, pointer, tempPath, points, paths, plane]); + +// return ( +// <> +// {paths.map((path, pathIndex) => ( +// +// ))} + +// {/* Interactive PointHandles */} +// {points.map((point, index) => ( +// +// ))} + +// {tempPath.length > 0 && mousePos && ( +// +// )} +// +// ); +// } +export default function StructuredPath() { + const { scene, camera, raycaster, gl, pointer } = useThree(); + const plane = useMemo( + () => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), + [] + ); + const { speed } = useAnimationPlaySpeed(); + const { assetStore } = useSceneContext(); + const { assets } = assetStore(); + + // --- State Variables --- + const [pathPointsList, setPathPointsList] = useState([]); + const [allPaths, setAllPaths] = useState([]); + const [computedShortestPath, setComputedShortestPath] = useState( + [] + ); + const [currentTempPath, setCurrentTempPath] = useState([]); + const [currentMousePos, setCurrentMousePos] = useState< + [number, number, number] | null + >(null); + const [selectedPointIndices, setSelectedPointIndices] = useState( + [] + ); + const [vehicleUuids, setVehicleUuids] = useState(); + + // --- Constants & Refs --- + const isLeftClickDown = useRef(false); + const isDragging = useRef(false); + const vehicleMovementState = useRef({}); + const activeVehicleIndexRef = useRef(0); + + const { isPlaying } = usePlayButtonStore(); + + // --- Computed Path Segments --- + const pathSegments = useMemo(() => { + if (!computedShortestPath || computedShortestPath.length === 0) return []; + + const segments: SegmentPoint[] = []; + + computedShortestPath.forEach((path) => { + const [start, end] = path.pathPoints; + + if (start.isCurved && start.handleA && start.handleB) { + const curve = new THREE.CubicBezierCurve3( + new THREE.Vector3(...start.position), + new THREE.Vector3(...start.handleA), + new THREE.Vector3(...start.handleB), + new THREE.Vector3(...end.position) + ); + const points = curve + .getPoints(20) + .map((pos) => ({ position: pos, originalPoint: start })); + segments.push(...points); + } else { + segments.push( + { + position: new THREE.Vector3(...start.position), + originalPoint: start, + }, + { position: new THREE.Vector3(...end.position), originalPoint: end } + ); + } + }); + + return segments.filter( + (v, i, arr) => i === 0 || !v.position.equals(arr[i - 1].position) + ); + }, [computedShortestPath]); + + // --- Initialize Vehicles --- + useEffect(() => { + const findVehicle = assets + .filter((val) => val.eventData?.type === "Vehicle") + ?.map((val) => val.modelUuid); + + setVehicleUuids(findVehicle); + + vehicleMovementState.current = {}; + findVehicle.forEach((uuid) => { + vehicleMovementState.current[uuid] = { index: 0, progress: 0 }; + }); + }, [assets]); + + // --- Vehicle Movement --- + useFrame((_, delta) => { + if (!isPlaying || pathSegments.length < 2) return; + + const object = scene.getObjectByProperty( + "uuid", + vehicleUuids[activeVehicleIndexRef.current] + ); + if (!object) return; + + const state = + vehicleMovementState.current[vehicleUuids[activeVehicleIndexRef.current]]; + if (!state) return; + + const startSeg = pathSegments[state.index]; + const endSeg = pathSegments[state.index + 1]; + + const segmentDistance = startSeg.position.distanceTo(endSeg.position); + state.progress += (speed * delta) / segmentDistance; + + if (state.progress >= 1) { + state.progress = 0; + state.index++; + if (state.index >= pathSegments.length - 1) { + state.index = 0; + activeVehicleIndexRef.current = + (activeVehicleIndexRef.current + 1) % vehicleUuids.length; + } + } + + const newPos = startSeg.position + .clone() + .lerp(endSeg.position, state.progress); + object.position.copy(newPos); + + const direction = endSeg.position + .clone() + .sub(startSeg.position) + .normalize(); + const forward = new THREE.Vector3(0, 0, 1); + object.quaternion.setFromUnitVectors(forward, direction); + }); + + // --- Update Mouse Position --- + useFrame(() => { + if (currentTempPath.length === 0) return; + raycaster.setFromCamera(pointer, camera); + const intersect = new THREE.Vector3(); + if (raycaster.ray.intersectPlane(plane, intersect)) { + setCurrentMousePos([intersect.x, intersect.y, intersect.z]); + } + }); + + const addPointToCurrentTemp = (newPoint: PointData) => { + setCurrentTempPath((prev) => { + const updated = [...prev, newPoint]; + + if (prev.length > 0) { + const lastPoint = prev[prev.length - 1]; + const newPath: PathDataInterface = { + pathId: THREE.MathUtils.generateUUID(), + pathPoints: [lastPoint, newPoint], + }; + setAllPaths((prevPaths) => [...prevPaths, newPath]); + } + + return updated; + }); + + setPathPointsList((prev) => { + if (!prev.find((p) => p.pointId === newPoint.pointId)) + return [...prev, newPoint]; + return prev; + }); + }; + + + // --- Event Handlers --- + // useEffect(() => { + // const canvas = gl.domElement; + + // const handleMouseDown = (evt: MouseEvent) => { + // if (evt.button === 0) { + // if (evt.ctrlKey || evt.shiftKey) return; + // isLeftClickDown.current = true; + // isDragging.current = false; + // } + // }; + + // const handleMouseUp = (evt: MouseEvent) => { + // if (evt.button === 0) isLeftClickDown.current = false; + // }; + + // const handleMouseMove = () => { + // if (isLeftClickDown.current) isDragging.current = true; + // }; + + // const handleMouseClick = (evt: MouseEvent) => { + // if (isDragging.current) return; + // if (evt.ctrlKey || evt.shiftKey) return; + + // const intersectPoint = new THREE.Vector3(); + // const pointIntersects = raycaster + // .intersectObjects(scene.children) + // .find((intersect) => intersect.object.name === "path-line"); + + // const pos = raycaster.ray.intersectPlane(plane, intersectPoint); + // if (!pos) return; + + // let clickedPoint = new THREE.Vector3(pos.x, pos.y, pos.z); + + // let snapPoint: PointData | null = null; + // for (let p of pathPointsList) { + // const pVec = new THREE.Vector3(...p.position); + // if (pVec.distanceTo(clickedPoint) < SNAP_POINT_THRESHOLD) { + // snapPoint = p; + // clickedPoint = pVec; + // break; + // } + // } + + // let newPoint: PointData = snapPoint ?? { + // pointId: THREE.MathUtils.generateUUID(), + // position: [clickedPoint.x, 0, clickedPoint.z], + // isCurved: false, + // handleA: null, + // handleB: null, + // }; + + // if (currentTempPath.length > 2) { + // const firstVec = new THREE.Vector3(...currentTempPath[0].position); + // if (firstVec.distanceTo(clickedPoint) < POLYGON_CLOSE_THRESHOLD) { + // const closingPoint = { ...currentTempPath[0] }; + // addPointToCurrentTemp(closingPoint); + // setCurrentTempPath([]); + // return; + // } + // } + + // for (let path of allPaths) { + // const a = new THREE.Vector3(...path.pathPoints[0].position); + // const b = new THREE.Vector3(...path.pathPoints[1].position); + // const closest = getNearestPointOnLine(a, b, clickedPoint); + + // if (closest.distanceTo(clickedPoint) < SNAP_LINE_THRESHOLD) { + // const splitPoint: PointData = { + // pointId: THREE.MathUtils.generateUUID(), + // position: closest.toArray() as [number, number, number], + // isCurved: false, + // handleA: null, + // handleB: null, + // }; + + // setAllPaths((prev) => + // prev + // .filter((pa) => pa.pathId !== path.pathId) + // .concat([ + // { + // pathId: THREE.MathUtils.generateUUID(), + // pathPoints: [path.pathPoints[0], splitPoint], + // }, + // { + // pathId: THREE.MathUtils.generateUUID(), + // pathPoints: [splitPoint, path.pathPoints[1]], + // }, + // ]) + // ); + + // addPointToCurrentTemp(splitPoint); + // return; + // } + // } + + // addPointToCurrentTemp(newPoint); + // }; + + // const handleContextMenu = (evt: MouseEvent) => { + // evt.preventDefault(); + // setCurrentTempPath([]); + // }; + + // canvas.addEventListener("mousedown", handleMouseDown); + // canvas.addEventListener("mouseup", handleMouseUp); + // canvas.addEventListener("mousemove", handleMouseMove); + // canvas.addEventListener("click", handleMouseClick); + // canvas.addEventListener("contextmenu", handleContextMenu); + + // return () => { + // canvas.removeEventListener("mousedown", handleMouseDown); + // canvas.removeEventListener("mouseup", handleMouseUp); + // canvas.removeEventListener("mousemove", handleMouseMove); + // canvas.removeEventListener("click", handleMouseClick); + // canvas.removeEventListener("contextmenu", handleContextMenu); + // }; + // }, [ + // gl, + // camera, + // raycaster, + // pointer, + // currentTempPath, + // pathPointsList, + // allPaths, + // plane, + // ]); + useEffect(() => { + const canvas = gl.domElement; + + const onMouseDown = (evt: MouseEvent) => + handleMouseDown(evt, isLeftClickDown, isDragging); + const onMouseUp = (evt: MouseEvent) => handleMouseUp(evt, isLeftClickDown); + const onMouseMove = () => handleMouseMove(isLeftClickDown, isDragging); + const onClick = (evt: MouseEvent) => + handleMouseClick({ + evt, + isDragging, + raycaster, + plane, + pointer, + currentTempPath, + setCurrentTempPath, + pathPointsList, + allPaths, + setAllPaths, + addPointToCurrentTemp, + }); + const onContextMenu = (evt: MouseEvent) => + handleContextMenu(evt, setCurrentTempPath); + + canvas.addEventListener("mousedown", onMouseDown); + canvas.addEventListener("mouseup", onMouseUp); + canvas.addEventListener("mousemove", onMouseMove); + canvas.addEventListener("click", onClick); + canvas.addEventListener("contextmenu", onContextMenu); + + return () => { + canvas.removeEventListener("mousedown", onMouseDown); + canvas.removeEventListener("mouseup", onMouseUp); + canvas.removeEventListener("mousemove", onMouseMove); + canvas.removeEventListener("click", onClick); + canvas.removeEventListener("contextmenu", onContextMenu); + }; + }, [ + gl, + camera, + raycaster, + pointer, + plane, + currentTempPath, + pathPointsList, + allPaths, + ]); + // --- Render --- + return ( + <> + {allPaths.map((path, pathIndex) => ( + + ))} + + {pathPointsList.map((point, index) => ( + + ))} + + {currentTempPath.length > 0 && currentMousePos && ( + + )} + + ); +} diff --git a/app/src/modules/simulation/vehicle/vehicles.tsx b/app/src/modules/simulation/vehicle/vehicles.tsx index 1b9ee4f..abcd659 100644 --- a/app/src/modules/simulation/vehicle/vehicles.tsx +++ b/app/src/modules/simulation/vehicle/vehicles.tsx @@ -5,6 +5,7 @@ import VehicleInstances from "./instances/vehicleInstances"; import VehicleUI from "../spatialUI/vehicle/vehicleUI"; import { useSceneContext } from "../../scene/sceneContext"; import PreDefinedPath from "./preDefinedPath/preDefinedPath"; +import StructuredPath from "./structuredPath/structuredPath"; function Vehicles() { const { vehicleStore } = useSceneContext(); @@ -28,7 +29,8 @@ function Vehicles() { return ( <> - + + {/* */} {/* */} {isVehicleSelected && selectedEventSphere && !isPlaying && }