diff --git a/app/src/modules/simulation/vehicle/structuredPath/functions/pathMouseHandler.ts b/app/src/modules/simulation/vehicle/structuredPath/functions/pathMouseHandler.ts index 41b106b..d6f8004 100644 --- a/app/src/modules/simulation/vehicle/structuredPath/functions/pathMouseHandler.ts +++ b/app/src/modules/simulation/vehicle/structuredPath/functions/pathMouseHandler.ts @@ -1,6 +1,6 @@ import * as THREE from "three"; -export const POLYGON_CLOSE_THRESHOLD = 0.3; +export const POLYGON_CLOSE_THRESHOLD = 0.1; export const SNAP_POINT_THRESHOLD = 0.2; export const SNAP_LINE_THRESHOLD = 0.2; type PointData = { diff --git a/app/src/modules/simulation/vehicle/structuredPath/lineSegment.tsx b/app/src/modules/simulation/vehicle/structuredPath/lineSegment.tsx index 8fc610d..312f8f1 100644 --- a/app/src/modules/simulation/vehicle/structuredPath/lineSegment.tsx +++ b/app/src/modules/simulation/vehicle/structuredPath/lineSegment.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useThree } from "@react-three/fiber"; import * as THREE from "three"; import { Line } from "@react-three/drei"; @@ -21,6 +21,7 @@ type PathData = PathDataInterface[]; interface LineSegmentProps { index: number; paths: PathDataInterface[]; + pathIndex: number; setPaths: React.Dispatch>; insertPoint?: (pathIndex: number, point: THREE.Vector3) => void; } @@ -30,33 +31,12 @@ export default function LineSegment({ paths, setPaths, insertPoint, + pathIndex, }: 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 [curve, setCurve] = useState(); + const [curveState, setCurveState] = useState(""); const curvePoints = useMemo(() => { if (!paths || index >= paths.length) return []; @@ -67,18 +47,210 @@ export default function LineSegment({ const start = new THREE.Vector3(...current.position); const end = new THREE.Vector3(...next.position); + // 1️⃣ Case 1: use predefined handles 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; + if (useCurve) { + 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); + const curve = new THREE.CubicBezierCurve3(start, hB, hA, end); + return curve.getPoints(100); + } - return curve.getPoints(useCurve ? 100 : 2); - }, [paths, index]); + // 2️⃣ Case 2: use curveState-generated curve + if (curveState) { + const direction = new THREE.Vector3().subVectors(end, start).normalize(); - return ; + const up = new THREE.Vector3(0, 1, 0); + const perpendicular = new THREE.Vector3() + .crossVectors(direction, up) + .normalize(); + + const distance = start.distanceTo(end); + const controlDistance = distance / 6; + + let controlPoint1, controlPoint2; + + // if (curveState === "arc") { + // const direction = new THREE.Vector3() + // .subVectors(end, start) + // .normalize(); + + // const perpendicular = new THREE.Vector3( + // -direction.z, + // 0, + // direction.x + // ).normalize(); + + // controlPoint1 = new THREE.Vector3().addVectors( + // start, + // perpendicular.clone().multiplyScalar(-controlDistance) // negative fixes to "C" + // ); + + // controlPoint2 = new THREE.Vector3().addVectors( + // end, + // perpendicular.clone().multiplyScalar(-controlDistance) + // ); + // } + if (curveState === "arc") { + const direction = new THREE.Vector3() + .subVectors(end, start) + .normalize(); + + // Perpendicular direction in XZ plane + const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x); + + const distance = start.distanceTo(end); + const controlDistance = distance / 4; + + const controlPoint1 = new THREE.Vector3() + .addVectors(start, direction.clone().multiplyScalar(distance / 3)) + .add(perpendicular.clone().multiplyScalar(-controlDistance)); // ← flipped + + const controlPoint2 = new THREE.Vector3() + .addVectors(end, direction.clone().multiplyScalar(-distance / 3)) + .add(perpendicular.clone().multiplyScalar(-controlDistance)); // ← flipped + + const curve = new THREE.CubicBezierCurve3( + start, + controlPoint1, + controlPoint2, + end + ); + return curve.getPoints(64); + } + + // if (curveState === "arc") { + // const direction = new THREE.Vector3() + // .subVectors(end, start) + // .normalize(); + + // // XZ-plane perpendicular + // const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x); + + // const distance = start.distanceTo(end); + // const controlDistance = distance / 6; // ← increase this for more curvature + + // const controlPoint1 = new THREE.Vector3().addVectors( + // start, + // perpendicular.clone().multiplyScalar(-controlDistance) + // ); + + // const controlPoint2 = new THREE.Vector3().addVectors( + // end, + // perpendicular.clone().multiplyScalar(-controlDistance) + // ); + + // const curve = new THREE.CubicBezierCurve3( + // start, + // controlPoint1, + // controlPoint2, + // end + // ); + // return curve.getPoints(64); + // } + + const curve = new THREE.CubicBezierCurve3( + start, + controlPoint1, + controlPoint2, + end + ); + return curve.getPoints(32); + } + + // 3️⃣ Case 3: fallback straight line + const line = new THREE.LineCurve3(start, end); + return line.getPoints(2); + }, [paths, index, curveState]); + + // 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); + + // // 1️⃣ Case 1: Use predefined curve handles if present + // const useCurve = + // (current.isCurved && current.handleB) || (next.isCurved && next.handleA); + + // if (useCurve) { + // const handleB = current.handleB + // ? new THREE.Vector3(...current.handleB) + // : start; + // const handleA = next.handleA ? new THREE.Vector3(...next.handleA) : end; + + // const curve = new THREE.CubicBezierCurve3(start, handleB, handleA, end); + // return curve.getPoints(100); + // } + + // // 2️⃣ Case 2: Use curveState-generated arc (gentle C-shaped) + // // if (curveState === "arc") { + // // const direction = new THREE.Vector3().subVectors(end, start).normalize(); + // // const distance = start.distanceTo(end); + + // // // Get perpendicular in XZ plane + // // const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x); + + // // const controlOffset = perpendicular.multiplyScalar(distance / 8); + + // // // Create gentle symmetric control points for a "C" arc + // // const controlPoint1 = start.clone().add(controlOffset.clone().negate()); + // // const controlPoint2 = end.clone().add(controlOffset.clone().negate()); + + // // const curve = new THREE.CubicBezierCurve3( + // // start, + // // controlPoint1, + // // controlPoint2, + // // end + // // ); + + // // return curve.getPoints(64); // 64 for smoother shape + // // } + // if (curveState === "arc") { + // const direction = new THREE.Vector3().subVectors(end, start).normalize(); + // const distance = start.distanceTo(end); + // // const curveHeight = distance * 0.25; // 25% of distance + + // // 🔺 Control height: Raise control points on Y-axis + // const curveHeight = distance / 4; // adjust 4 → higher = taller arc + + // // Control points directly above the midpoint + // const mid = start.clone().add(end).multiplyScalar(0.5); + // const controlPoint = mid + // .clone() + // .add(new THREE.Vector3(0, curveHeight, 0)); + + // // Use Quadratic Bezier for simple arc + // const curve = new THREE.QuadraticBezierCurve3(start, controlPoint, end); + // return curve.getPoints(64); + // } + + // // 3️⃣ Case 3: Fallback to straight line + // const line = new THREE.LineCurve3(start, end); + // return line.getPoints(2); + // }, [paths, index, curveState]); + + const handleClick = (evt: any) => { + if (evt.ctrlKey) { + setCurveState("arc"); + } + }; + // const bendFactor = 1 / 10; // tweak this dynamically + // const controlOffset = perpendicular.multiplyScalar(distance * bendFactor); + return ( + + ); } diff --git a/app/src/modules/simulation/vehicle/structuredPath/pointHandlers.tsx b/app/src/modules/simulation/vehicle/structuredPath/pointHandlers.tsx index 5c1d786..233450a 100644 --- a/app/src/modules/simulation/vehicle/structuredPath/pointHandlers.tsx +++ b/app/src/modules/simulation/vehicle/structuredPath/pointHandlers.tsx @@ -587,7 +587,7 @@ function nodePathToEdges( } /** --- React Component --- */ -export default function PointHandlersRenamed({ +export default function PointHandlers({ point, pointIndex, points, @@ -602,7 +602,7 @@ export default function PointHandlersRenamed({ const handleARef = useRef(null); const handleBRef = useRef(null); const lineRef = useRef(null); - const { camera, gl, controls } = useThree(); + const { camera, gl, controls, raycaster } = useThree(); const [dragging, setDragging] = useState< null | "main" | "handleA" | "handleB" @@ -671,7 +671,12 @@ export default function PointHandlersRenamed({ : target === "handleA" ? handleARef.current : handleBRef.current; - if (targetRef) dragOffset.current.copy(targetRef.position).sub(e.point); + if (targetRef && targetRef.position) { + dragOffset.current + .copy(new THREE.Vector3(targetRef.position.x, 0, targetRef.position.z)) + .sub(e.point); + } + if (controls) (controls as any).enabled = false; gl.domElement.style.cursor = "grabbing"; }; @@ -683,10 +688,10 @@ export default function PointHandlersRenamed({ }; /** Update position in useFrame */ - useFrame(({ raycaster, mouse }) => { + useFrame(({ 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); @@ -700,16 +705,20 @@ export default function PointHandlersRenamed({ .clone() .sub(new THREE.Vector3().fromArray(p.position)); p.position = [newPos.x, 0, newPos.z]; - if (p.handleA) + + if (p.handleA) { p.handleA = new THREE.Vector3() .fromArray(p.handleA) - .add(delta) + .add(new THREE.Vector3(delta.x, 0, delta.z)) .toArray() as [number, number, number]; - if (p.handleB) + } + + if (p.handleB) { p.handleB = new THREE.Vector3() .fromArray(p.handleB) - .add(delta) + .add(new THREE.Vector3(delta.x, 0, delta.z)) .toArray() as [number, number, number]; + } } else { p[dragging] = [newPos.x, 0, newPos.z]; if (p.isCurved) { @@ -718,6 +727,7 @@ export default function PointHandlersRenamed({ const mirrorHandle = mainPos .clone() .sub(thisHandle.clone().sub(mainPos)); + console.log("mirrorHandle: ", mirrorHandle); if (dragging === "handleA") p.handleB = mirrorHandle.toArray() as [number, number, number]; if (dragging === "handleB") @@ -765,7 +775,7 @@ export default function PointHandlersRenamed({ onPointerDown={(e) => startDrag("main", e)} onPointerUp={stopDrag} > - + diff --git a/app/src/modules/simulation/vehicle/structuredPath/structuredPath.tsx b/app/src/modules/simulation/vehicle/structuredPath/structuredPath.tsx index 8e2618b..ee82855 100644 --- a/app/src/modules/simulation/vehicle/structuredPath/structuredPath.tsx +++ b/app/src/modules/simulation/vehicle/structuredPath/structuredPath.tsx @@ -27,6 +27,8 @@ type PointData = { interface PathDataInterface { pathId: string; + isActive?: boolean; + isCurved?: boolean; pathPoints: [PointData, PointData]; } @@ -35,355 +37,7 @@ 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( @@ -397,6 +51,7 @@ export default function StructuredPath() { // --- State Variables --- const [pathPointsList, setPathPointsList] = useState([]); const [allPaths, setAllPaths] = useState([]); + console.log("allPaths: ", allPaths); const [computedShortestPath, setComputedShortestPath] = useState( [] ); @@ -426,28 +81,55 @@ export default function StructuredPath() { computedShortestPath.forEach((path) => { const [start, end] = path.pathPoints; + const startPos = new THREE.Vector3(...start.position); + const endPos = new THREE.Vector3(...end.position); + + // Start point has curve handles if (start.isCurved && start.handleA && start.handleB) { + const handleA = new THREE.Vector3(...start.handleA); + const handleB = new THREE.Vector3(...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) + startPos, + handleA, + handleB, + endPos ); - const points = curve - .getPoints(20) - .map((pos) => ({ position: pos, originalPoint: start })); + const points = curve.getPoints(20).map((pos) => ({ + position: pos, + originalPoint: start, + })); segments.push(...points); - } else { + } + + // End point has curve handles + else if (end.isCurved && end.handleA && end.handleB) { + const handleA = new THREE.Vector3(...end.handleA); + const handleB = new THREE.Vector3(...end.handleB); + + const curve = new THREE.CubicBezierCurve3( + startPos, + handleA, + handleB, + endPos + ); + const points = curve.getPoints(20).map((pos) => ({ + position: pos, + originalPoint: end, + })); + segments.push(...points); + } + + // No curves — just straight line + else { segments.push( - { - position: new THREE.Vector3(...start.position), - originalPoint: start, - }, - { position: new THREE.Vector3(...end.position), originalPoint: end } + { position: startPos, originalPoint: start }, + { position: endPos, originalPoint: end } ); } }); + // Filter out duplicate consecutive points return segments.filter( (v, i, arr) => i === 0 || !v.position.equals(arr[i - 1].position) ); @@ -543,134 +225,6 @@ export default function StructuredPath() { }); }; - - // --- 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; @@ -725,6 +279,7 @@ export default function StructuredPath() {