From fe95ea8d0b77d5fbc1dcc4e03f49b6751fab114f Mon Sep 17 00:00:00 2001 From: Poovizhi Date: Mon, 25 Aug 2025 15:25:48 +0530 Subject: [PATCH] Update Path Drawing Component to Match Wall Drawing Component Behavior --- .../function/getPathPointByPoints.ts | 13 + .../vehicle/pathCreator/function/getPaths.ts | 47 + .../vehicle/pathCreator/pathCreator.tsx | 800 +----------------- .../vehicle/pathCreator/pathHandler.tsx | 186 ++++ .../vehicle/pathCreator/pointHandler.tsx | 299 +++++++ 5 files changed, 590 insertions(+), 755 deletions(-) create mode 100644 app/src/modules/simulation/vehicle/pathCreator/function/getPathPointByPoints.ts create mode 100644 app/src/modules/simulation/vehicle/pathCreator/function/getPaths.ts create mode 100644 app/src/modules/simulation/vehicle/pathCreator/pathHandler.tsx create mode 100644 app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx diff --git a/app/src/modules/simulation/vehicle/pathCreator/function/getPathPointByPoints.ts b/app/src/modules/simulation/vehicle/pathCreator/function/getPathPointByPoints.ts new file mode 100644 index 0000000..93a3157 --- /dev/null +++ b/app/src/modules/simulation/vehicle/pathCreator/function/getPathPointByPoints.ts @@ -0,0 +1,13 @@ +export const getPathPointByPoints = (point: any, paths: any) => { + for (const path of paths) { + if ( + (path.pathPoints[0].pointId === point[0].pointId || + path.pathPoints[1].pointId === point[0].pointId) && + (path.pathPoints[0].pointId === point[1].pointId || + path.pathPoints[1].pointId === point[1].pointId) + ) { + return path; + } + } + return undefined; +}; diff --git a/app/src/modules/simulation/vehicle/pathCreator/function/getPaths.ts b/app/src/modules/simulation/vehicle/pathCreator/function/getPaths.ts new file mode 100644 index 0000000..af6836e --- /dev/null +++ b/app/src/modules/simulation/vehicle/pathCreator/function/getPaths.ts @@ -0,0 +1,47 @@ +import { useCallback } from "react"; + +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved?: boolean; + handleA?: [number, number, number] | null; + handleB?: [number, number, number] | null; +}; + +interface PathDataInterface { + pathId: string; + isActive?: boolean; + isCurved?: boolean; + pathPoints: [PointData, PointData]; +} + +type PathData = PathDataInterface[]; +type setPathPositionProps = ( + pointUuid: string, + position: [number, number, number], + setPaths: React.Dispatch> +) => void; + +export const getPathsByPointId = (pointId: any, paths: PathData) => { + return paths.filter((a) => a.pathPoints.some((p) => p.pointId === pointId)); +}; + +export const setPathPosition = ( + pointUuid: string, + position: [number, number, number], + setPaths: React.Dispatch> +) => { + setPaths((prevPaths) => + prevPaths.map((path: any) => { + if (path?.pathPoints.some((p: any) => p.pointId === pointUuid)) { + return { + ...path, + pathPoints: path.pathPoints.map((p: any) => + p.pointId === pointUuid ? { ...p, position } : p + ) as [PointData, PointData], // πŸ‘ˆ force back to tuple + }; + } + return path; + }) + ); +}; diff --git a/app/src/modules/simulation/vehicle/pathCreator/pathCreator.tsx b/app/src/modules/simulation/vehicle/pathCreator/pathCreator.tsx index f3b7644..16f487c 100644 --- a/app/src/modules/simulation/vehicle/pathCreator/pathCreator.tsx +++ b/app/src/modules/simulation/vehicle/pathCreator/pathCreator.tsx @@ -1,253 +1,4 @@ -// import { Line } from "@react-three/drei"; -// import { useThree } from "@react-three/fiber"; -// import React, { useEffect, useMemo, useState } from "react"; -// import { MathUtils, Plane, Vector3 } from "three"; -// import { Vector3Array } from "../../../../types/world/worldTypes"; - -// type PointData = { -// pointId: string; -// position: [number, number, number]; -// isCurved?: boolean; -// handleA?: [number, number, number] | null; -// handleB?: [number, number, number] | null; -// }; - -// interface PathDataInterface { -// pathId: string; -// isActive?: boolean; -// isCurved?: boolean; -// pathPoints: [PointData, PointData]; -// } - -// type PathData = PathDataInterface[]; - -// export default function PathCreator() { -// const [paths, setPaths] = useState([]); -// const [draftPoints, setDraftPoints] = useState([]); -// const [mousePos, setMousePos] = useState<[number, number, number] | null>( -// null -// ); // πŸ‘ˆ track mouse for dashed line - -// const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []); -// const { scene, raycaster, gl } = useThree(); -// const [snappedPosition, setSnappedPosition] = useState(); -// // --- Helpers --- -// function getClosestIntersection( -// intersects: Vector3Array, -// point: Vector3 -// ): Vector3 { -// let closestNewPoint: Vector3 = point; -// let minDistance = Infinity; -// for (const intersect of intersects) { -// const distance = point.distanceTo(intersect); -// if (distance < minDistance) { -// minDistance = distance; -// closestNewPoint = intersect; -// } -// } -// return closestNewPoint; -// } - -// const handleClick = () => { -// const intersectionPoint = new Vector3(); -// let position = raycaster.ray.intersectPlane(plane, intersectionPoint); -// if (!position) return; - -// const pointIntersect = raycaster -// .intersectObjects(scene.children) -// .find((intersect) => intersect.object.name === "Path-Point"); - -// const pathIntersect = raycaster -// .intersectObjects(scene.children) -// .find((intersect) => intersect.object.name === "Path-Line"); - -// // βœ… Case 1: Split an existing path -// if (!pointIntersect && pathIntersect) { -// const hitLine = pathIntersect.object; -// const hitPathId = hitLine.userData.pathId; - -// const hitPath = paths.find((p) => p.pathId === hitPathId); -// if (!hitPath) return; - -// const [p1, p2] = hitPath.pathPoints; - -// const point1Vec = new Vector3(...p1.position); -// const point2Vec = new Vector3(...p2.position); - -// const lineDir = new Vector3() -// .subVectors(point2Vec, point1Vec) -// .normalize(); -// const point1ToClick = new Vector3().subVectors( -// pathIntersect.point, -// point1Vec -// ); -// const dot = point1ToClick.dot(lineDir); - -// const projection = new Vector3() -// .copy(lineDir) -// .multiplyScalar(dot) -// .add(point1Vec); - -// const lineLength = point1Vec.distanceTo(point2Vec); -// let t = point1Vec.distanceTo(projection) / lineLength; -// t = Math.max(0, Math.min(1, t)); - -// const closestPoint = new Vector3().lerpVectors(point1Vec, point2Vec, t); - -// const newPoint: PointData = { -// pointId: MathUtils.generateUUID(), -// position: closestPoint.toArray() as [number, number, number], -// }; - -// const newPath1: PathDataInterface = { -// pathId: MathUtils.generateUUID(), -// pathPoints: [p1, newPoint], -// }; -// const newPath2: PathDataInterface = { -// pathId: MathUtils.generateUUID(), -// pathPoints: [newPoint, p2], -// }; - -// setPaths((prev) => -// prev.filter((p) => p.pathId !== hitPathId).concat([newPath1, newPath2]) -// ); - -// setDraftPoints([newPoint]); -// return; -// } -// // if (snappedPosition && snappedPoint) { -// // newPoint.pointUuid = snappedPoint.pointUuid; -// // newPoint.position = snappedPosition; -// // newPoint.layer = snappedPoint.layer; -// // } - -// // if (snappedPoint && snappedPoint.pointUuid === tempPoints[0]?.pointUuid) { -// // return; -// // } - -// // if (snappedPosition && !snappedPoint) { -// // newPoint.position = snappedPosition; -// // } - -// // if (pointIntersects && !snappedPoint) { -// // const point = getWallPointById(pointIntersects.object.uuid); -// // if (point) { -// // newPoint.pointUuid = point.pointUuid; -// // newPoint.position = point.position; -// // newPoint.layer = point.layer; -// // } -// // } -// // βœ… Case 2: Normal new path creation -// const newPoint: PointData = { -// pointId: MathUtils.generateUUID(), -// position: [position.x, position.y, position.z], -// }; - -// if (draftPoints.length === 0) { -// setDraftPoints([newPoint]); -// } else { -// const newPath: PathDataInterface = { -// pathId: MathUtils.generateUUID(), -// pathPoints: [draftPoints[0], newPoint], -// }; -// setPaths((prev) => [...prev, newPath]); -// setDraftPoints([newPoint]); -// } -// }; - -// const handleContextMenu = (event: any) => { -// event.preventDefault(); -// setDraftPoints([]); -// }; - -// // πŸ‘‡ track mouse move for preview line -// const handleMouseMove = () => { -// const intersectionPoint = new Vector3(); -// let pos = raycaster.ray.intersectPlane(plane, intersectionPoint); -// if (pos) { -// setMousePos([pos.x, pos.y, pos.z]); -// } -// }; - -// useEffect(() => { -// const canvasElement = gl.domElement; - -// canvasElement.addEventListener("click", handleClick); -// canvasElement.addEventListener("mousemove", handleMouseMove); -// canvasElement.addEventListener("contextmenu", handleContextMenu); - -// return () => { -// canvasElement.removeEventListener("click", handleClick); -// canvasElement.removeEventListener("mousemove", handleMouseMove); -// canvasElement.removeEventListener("contextmenu", handleContextMenu); -// }; -// }, [gl, draftPoints, paths]); - -// const allPoints = useMemo(() => { -// const points: PointData[] = []; -// const seenUuids = new Set(); -// paths?.forEach((wall) => { -// wall.pathPoints.forEach((point) => { -// if (!seenUuids.has(point.pointId)) { -// seenUuids.add(point.pointId); -// points.push(point); -// } -// }); -// }); -// return points; -// }, [paths]); - -// return ( -// <> -// {/* Draft points (red) */} -// {draftPoints.map((point) => ( -// -// -// -// -// ))} - -// {/* All saved points */} -// {allPoints.map((point) => ( -// -// -// -// -// ))} - -// {/* πŸ‘‡ Dashed preview line while drawing */} -// {draftPoints.length > 0 && mousePos && ( -// -// )} - -// {/* Permanent paths */} -// {paths.map((points) => ( -// -// ))} -// -// ); -// } -import { Line } from "@react-three/drei"; +import { DragControls, Line } from "@react-three/drei"; import { useFrame, useThree } from "@react-three/fiber"; import React, { useCallback, @@ -258,6 +9,10 @@ import React, { } from "react"; import { LineCurve3, MathUtils, Plane, Vector3 } from "three"; import { Vector3Array } from "../../../../types/world/worldTypes"; +import { useActiveSubTool, useToolMode } from "../../../../store/builder/store"; +import PointHandler from "./pointHandler"; +import { getPathPointByPoints } from "./function/getPathPointByPoints"; +import PathHandler from "./pathHandler"; type PointData = { pointId: string; @@ -278,11 +33,11 @@ type PathData = PathDataInterface[]; export default function PathCreator() { const [paths, setPaths] = useState([]); - const [draftPoints, setDraftPoints] = useState([]); const [mousePos, setMousePos] = useState<[number, number, number] | null>( null ); + const { toolMode } = useToolMode(); const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []); const { scene, raycaster, gl } = useThree(); @@ -291,11 +46,17 @@ export default function PathCreator() { >(null); const [snappedPoint, setSnappedPoint] = useState(null); const finalPosition = useRef<[number, number, number] | null>(null); - const [tempPath, setTempPath] = useState(); + const [hoveredLine, setHoveredLine] = useState( + null + ); + const [hoveredPoint, setHoveredPoint] = useState(null); + const { activeSubTool } = useActiveSubTool(); const POINT_SNAP_THRESHOLD = 0.5; const CAN_POINT_SNAP = true; - + useEffect(() => { + + }, [paths]); const getAllOtherPathPoints = useCallback((): PointData[] => { if (draftPoints.length === 0) return []; return ( @@ -366,108 +127,10 @@ export default function PathCreator() { draftPoints[0], { pointId: "temp-point", position: finalPosition.current }, ]; - setTempPath({ - pathId: "temp-path", - pathPoints: paths, - }); }); - // βœ… Click handler - // const handleClick = () => { - // const intersectionPoint = new Vector3(); - // const pos = raycaster.ray.intersectPlane(plane, intersectionPoint); - // if (!pos) return; - - // const pointIntersect = raycaster - // .intersectObjects(scene.children) - // .find((intersect) => intersect.object.name === "Path-Point"); - - // const pathIntersect = raycaster - // .intersectObjects(scene.children) - // .find((intersect) => intersect.object.name === "Path-Line"); - - // // Case 1: Split path - // if (!pointIntersect && pathIntersect) { - // const hitLine = pathIntersect.object; - // const hitPathId = hitLine.userData.pathId; - // const hitPath = paths.find((p) => p.pathId === hitPathId); - // if (!hitPath) return; - - // const [p1, p2] = hitPath.pathPoints; - // const point1Vec = new Vector3(...p1.position); - // const point2Vec = new Vector3(...p2.position); - - // const lineDir = new Vector3() - // .subVectors(point2Vec, point1Vec) - // .normalize(); - // const point1ToClick = new Vector3().subVectors( - // pathIntersect.point, - // point1Vec - // ); - // const dot = point1ToClick.dot(lineDir); - - // const projection = new Vector3() - // .copy(lineDir) - // .multiplyScalar(dot) - // .add(point1Vec); - - // const lineLength = point1Vec.distanceTo(point2Vec); - // let t = point1Vec.distanceTo(projection) / lineLength; - // t = Math.max(0, Math.min(1, t)); - - // const closestPoint = new Vector3().lerpVectors(point1Vec, point2Vec, t); - - // const newPoint: PointData = { - // pointId: MathUtils.generateUUID(), - // position: closestPoint.toArray() as [number, number, number], - // }; - - // const newPath1: PathDataInterface = { - // pathId: MathUtils.generateUUID(), - // pathPoints: [p1, newPoint], - // }; - // const newPath2: PathDataInterface = { - // pathId: MathUtils.generateUUID(), - // pathPoints: [newPoint, p2], - // }; - - // setPaths((prev) => - // prev.filter((p) => p.pathId !== hitPathId).concat([newPath1, newPath2]) - // ); - - // setDraftPoints([newPoint]); - // return; - // } - - // // Case 2: Normal path creation - // const newPoint: PointData = { - // pointId: MathUtils.generateUUID(), - // position: [pos.x, pos.y, pos.z], - // }; - - // if (snappedPosition) { - // newPoint.position = snappedPosition; - // } - - // if (snappedPoint && draftPoints[0]?.pointId === snappedPoint.pointId) { - // return; // prevent connecting point to itself - // } - - // if (draftPoints.length === 0) { - // setDraftPoints([newPoint]); - // } else { - // const newPath: PathDataInterface = { - // pathId: MathUtils.generateUUID(), - // pathPoints: [draftPoints[0], newPoint], - // }; - // setPaths((prev) => [...prev, newPath]); - // setDraftPoints([newPoint]); - // } - // }; - // βœ… Click handler - useEffect(() => { - console.log("paths", paths); + }, [paths]); const getPathPointById = (uuid: any) => { for (const path of paths) { @@ -480,29 +143,19 @@ export default function PathCreator() { } return undefined; }; - const getPathPointByPoints = (point: any) => { - for (const path of paths) { - if ( - (path.pathPoints[0].pointId === point[0].pointId || - path.pathPoints[1].pointId === point[0].pointId) && - (path.pathPoints[0].pointId === point[1].pointId || - path.pathPoints[1].pointId === point[1].pointId) - ) { - return path; - } - } - return undefined; - }; - useEffect(() => { - console.log("draftPoints", draftPoints); - }, [draftPoints]); + const handleClick = () => { + if (activeSubTool === "free-hand") return; + if (toolMode === "3D-Delete") return; + const intersectionPoint = new Vector3(); const pos = raycaster.ray.intersectPlane(plane, intersectionPoint); if (!pos) return; const pointIntersect = raycaster .intersectObjects(scene.children) .find((intersect) => intersect.object.name === "Path-Point"); + + const pathIntersect = raycaster .intersectObjects(scene.children) @@ -511,9 +164,13 @@ export default function PathCreator() { // --- Case 1: Split path --- if (!pointIntersect && pathIntersect) { const hitLine = pathIntersect.object; - const clickedPath = getPathPointByPoints(hitLine.userData.pathPoints); + const clickedPath = getPathPointByPoints( + hitLine.userData.pathPoints, + paths + ); if (clickedPath) { const hitPath = paths.find((p) => p.pathId === clickedPath.pathId); + if (!hitPath) return; const [p1, p2] = clickedPath.pathPoints; @@ -565,7 +222,6 @@ export default function PathCreator() { }; setDraftPoints([splitPoint]); setPaths((prev) => [...prev, path1, path2]); - console.log("draftPoints:if ", path1, path2, draftPoints); } else { const newPath: PathDataInterface = { pathId: MathUtils.generateUUID(), @@ -580,7 +236,6 @@ export default function PathCreator() { pathPoints: [point2, splitPoint], }; - console.log("else ", newPath, firstPath, secondPath, draftPoints); setPaths((prev) => [...prev, newPath, firstPath, secondPath]); setDraftPoints([splitPoint]); } @@ -610,12 +265,11 @@ export default function PathCreator() { if (snappedPosition && !snappedPoint) { newPoint.position = snappedPosition; } + if (pointIntersect && !snappedPoint) { const point = getPathPointById(pointIntersect.object.userData.pointId); - console.log("point: ", point); if (point) { - console.log("newPoint: ", newPoint); newPoint.pointId = point.pointId; newPoint.position = point.position; } @@ -658,7 +312,7 @@ export default function PathCreator() { canvasElement.removeEventListener("mousemove", handleMouseMove); canvasElement.removeEventListener("contextmenu", handleContextMenu); }; - }, [gl, draftPoints, paths]); + }, [gl, draftPoints, paths, toolMode]); const allPoints = useMemo(() => { const points: PointData[] = []; @@ -674,7 +328,6 @@ export default function PathCreator() { return points; }, [paths]); - useEffect(() => {}, [paths]); return ( <> {/* Draft points (red) */} @@ -687,19 +340,17 @@ export default function PathCreator() { {/* Saved points */} {allPoints.map((point) => ( - - - - + point={point} + setPaths={setPaths} + paths={paths} + setHoveredPoint={setHoveredPoint} + hoveredLine={hoveredLine} + hoveredPoint={hoveredPoint} + /> ))} - {/* {tempPath && draftPoints.length > 0 && ( - - )} */} + {/* Preview line */} {draftPoints.length > 0 && mousePos && ( ( - ))} ); } - -function ReferenceLine({ points }: any) { - const path = useMemo(() => { - const [start, end] = points?.map((p: any) => new Vector3(...p.position)); - return new LineCurve3(start, end); - }, [points]); - - const linePoints = useMemo(() => path.getPoints(1), [path]); - - return ; -} - -// import { Line } from "@react-three/drei"; -// import { useFrame, useThree } from "@react-three/fiber"; -// import React, { -// useCallback, -// useEffect, -// useMemo, -// useRef, -// useState, -// } from "react"; -// import { MathUtils, Plane, Vector3 } from "three"; -// import { Vector3Array } from "../../../../types/world/worldTypes"; - -// type PointData = { -// pointId: string; -// position: [number, number, number]; -// isCurved?: boolean; -// handleA?: [number, number, number] | null; -// handleB?: [number, number, number] | null; -// }; - -// interface PathDataInterface { -// pathId: string; -// isActive?: boolean; -// isCurved?: boolean; -// pathPoints: [PointData, PointData]; -// } - -// type PathData = PathDataInterface[]; - -// type SnapObject = { -// pointId: string; -// position: [number, number, number]; -// }; - -// export default function PathCreator() { -// const [paths, setPaths] = useState([]); -// -// const [draftPoints, setDraftPoints] = useState([]); -// const [mousePos, setMousePos] = useState<[number, number, number] | null>( -// null -// ); // πŸ‘ˆ track mouse for dashed line - -// const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []); -// const { scene, raycaster, gl } = useThree(); -// const [snappedPosition, setSnappedPosition] = useState< -// [number, number, number] | null -// >(null); -// const [snappedPoint, setSnappedPoint] = useState(null); -// const [tempWall, setTempWall] = useState(null); -// const finalPosition = useRef<[number, number, number] | null>(null); - -// const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters - -// const CAN_POINT_SNAP = true; // Whether snapping is enabled or not - -// const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5; // Distance threshold for snapping in meters - -// const CAN_ANGLE_SNAP = true; // Whether snapping is enabled or not -// // --- Helpers --- -// function getClosestIntersection( -// intersects: Vector3Array, -// point: Vector3 -// ): Vector3 { -// let closestNewPoint: Vector3 = point; -// let minDistance = Infinity; -// for (const intersect of intersects) { -// const distance = point.distanceTo(intersect); -// if (distance < minDistance) { -// minDistance = distance; -// closestNewPoint = intersect; -// } -// } -// return closestNewPoint; -// } -// const getAllOtherPathPoints = useCallback((): PointData[] => { -// if (draftPoints.length === 0) return []; -// return ( -// paths?.flatMap((path) => -// path.pathPoints.filter((pt) => pt.pointId !== draftPoints[0].pointId) -// ) ?? [] -// ); -// }, [paths, draftPoints]); - -// // Snapping -// const snapPathPoint = useCallback( -// (position: [number, number, number], tempPoint?: PointData) => { -// if (draftPoints.length === 0 || !CAN_POINT_SNAP) { -// return { -// position, -// isSnapped: false, -// snappedPoint: null as PointData | null, -// }; -// } - -// const otherPoints = [...getAllOtherPathPoints()]; -// if (tempPoint) { -// otherPoints.push(tempPoint); -// } - -// const currentVec = new Vector3(...position); - -// for (const point of otherPoints) { -// const pointVec = new Vector3(...point.position); -// const distance = currentVec.distanceTo(pointVec); - -// if (distance <= POINT_SNAP_THRESHOLD) { -// return { -// position: point.position, -// isSnapped: true, -// snappedPoint: point, -// }; -// } -// } - -// return { position, isSnapped: false, snappedPoint: null }; -// }, -// [draftPoints, getAllOtherPathPoints] -// ); - -// // Frame loop -// useFrame(() => { -// const intersectionPoint = new Vector3(); -// raycaster.ray.intersectPlane(plane, intersectionPoint); - -// if (!intersectionPoint) return; - -// const snapped = snapPathPoint( -// [intersectionPoint.x, intersectionPoint.y, intersectionPoint.z], -// draftPoints[0] // βœ… use one point, not array -// ); - -// if (snapped.isSnapped && snapped.snappedPoint) { -// finalPosition.current = snapped.position; -// setSnappedPosition(snapped.position); // βœ… correct type -// setSnappedPoint(snapped.snappedPoint); -// } else { -// finalPosition.current = [ -// intersectionPoint.x, -// intersectionPoint.y, -// intersectionPoint.z, -// ]; -// setSnappedPosition(null); -// setSnappedPoint(null); -// } -// }); -// const getWallPointById: (uuid) => { -// for (const wall of get().paths) { -// const point = wall.pathPoints.find(p => p.pointUuid === uuid); -// if (point) return point; -// } -// return undefined; -// } -// const handleClick = () => { -// const intersectionPoint = new Vector3(); -// let position = raycaster.ray.intersectPlane(plane, intersectionPoint); -// if (!position) return; - -// const pointIntersect = raycaster -// .intersectObjects(scene.children) -// .find((intersect) => intersect.object.name === "Path-Point"); - -// const pathIntersect = raycaster -// .intersectObjects(scene.children) -// .find((intersect) => intersect.object.name === "Path-Line"); - -// // βœ… Case 1: Split an existing path -// if (!pointIntersect && pathIntersect) { -// const hitLine = pathIntersect.object; -// const hitPathId = hitLine.userData.pathId; - -// const hitPath = paths.find((p) => p.pathId === hitPathId); -// if (!hitPath) return; - -// const [p1, p2] = hitPath.pathPoints; - -// const point1Vec = new Vector3(...p1.position); -// const point2Vec = new Vector3(...p2.position); - -// const lineDir = new Vector3() -// .subVectors(point2Vec, point1Vec) -// .normalize(); -// const point1ToClick = new Vector3().subVectors( -// pathIntersect.point, -// point1Vec -// ); -// const dot = point1ToClick.dot(lineDir); - -// const projection = new Vector3() -// .copy(lineDir) -// .multiplyScalar(dot) -// .add(point1Vec); - -// const lineLength = point1Vec.distanceTo(point2Vec); -// let t = point1Vec.distanceTo(projection) / lineLength; -// t = Math.max(0, Math.min(1, t)); - -// const closestPoint = new Vector3().lerpVectors(point1Vec, point2Vec, t); - -// const newPoint: PointData = { -// pointId: MathUtils.generateUUID(), -// position: closestPoint.toArray() as [number, number, number], -// }; - -// const newPath1: PathDataInterface = { -// pathId: MathUtils.generateUUID(), -// pathPoints: [p1, newPoint], -// }; -// const newPath2: PathDataInterface = { -// pathId: MathUtils.generateUUID(), -// pathPoints: [newPoint, p2], -// }; - -// setPaths((prev) => -// prev.filter((p) => p.pathId !== hitPathId).concat([newPath1, newPath2]) -// ); - -// setDraftPoints([newPoint]); -// return; -// } - -// const newPoint: PointData = { -// pointId: MathUtils.generateUUID(), -// position: [position.x, position.y, position.z], -// }; -// if (snappedPosition && snappedPoint) { -// newPoint.pointId = snappedPoint.pointId; -// newPoint.position = snappedPosition; -// } - -// if (snappedPoint && snappedPoint.pointId === draftPoints[0].pointId) { -// return; -// } - -// if (snappedPosition && !snappedPoint) { -// newPoint.position = snappedPosition; -// } - -// if (pointIntersect && !snappedPoint) { -// const point = getWallPointById(pointIntersects.object.uuid); -// if (point) { -// newPoint.pointUuid = point.pointUuid; -// newPoint.position = point.position; -// newPoint.layer = point.layer; -// } -// } -// // βœ… Case 2: Normal new path creation - -// if (draftPoints.length === 0) { -// setDraftPoints([newPoint]); -// } else { -// const newPath: PathDataInterface = { -// pathId: MathUtils.generateUUID(), -// pathPoints: [draftPoints[0], newPoint], -// }; -// setPaths((prev) => [...prev, newPath]); -// setDraftPoints([newPoint]); -// } -// }; - -// const handleContextMenu = (event: any) => { -// event.preventDefault(); -// setDraftPoints([]); -// }; - -// // πŸ‘‡ track mouse move for preview line -// const handleMouseMove = () => { -// const intersectionPoint = new Vector3(); -// let pos = raycaster.ray.intersectPlane(plane, intersectionPoint); -// if (pos) { -// setMousePos([pos.x, pos.y, pos.z]); -// } -// }; - -// useEffect(() => { -// const canvasElement = gl.domElement; - -// canvasElement.addEventListener("click", handleClick); -// canvasElement.addEventListener("mousemove", handleMouseMove); -// canvasElement.addEventListener("contextmenu", handleContextMenu); - -// return () => { -// canvasElement.removeEventListener("click", handleClick); -// canvasElement.removeEventListener("mousemove", handleMouseMove); -// canvasElement.removeEventListener("contextmenu", handleContextMenu); -// }; -// }, [gl, draftPoints, paths]); - -// const allPoints = useMemo(() => { -// const points: PointData[] = []; -// const seenUuids = new Set(); -// paths?.forEach((wall) => { -// wall.pathPoints.forEach((point) => { -// if (!seenUuids.has(point.pointId)) { -// seenUuids.add(point.pointId); -// points.push(point); -// } -// }); -// }); -// return points; -// }, [paths]); - -// return ( -// <> -// {/* Draft points (red) */} -// {draftPoints.map((point) => ( -// -// -// -// -// ))} - -// {/* All saved points */} -// {allPoints.map((point) => ( -// -// -// -// -// ))} - -// {/* πŸ‘‡ Dashed preview line while drawing */} -// {draftPoints.length > 0 && mousePos && ( -// -// )} - -// {/* Permanent paths */} -// {paths.map((points) => ( -// -// ))} -// -// ); -// } diff --git a/app/src/modules/simulation/vehicle/pathCreator/pathHandler.tsx b/app/src/modules/simulation/vehicle/pathCreator/pathHandler.tsx new file mode 100644 index 0000000..a75c35d --- /dev/null +++ b/app/src/modules/simulation/vehicle/pathCreator/pathHandler.tsx @@ -0,0 +1,186 @@ +import { DragControls, Line } from "@react-three/drei"; +import React, { useMemo, useState } from "react"; +import { useActiveSubTool, useToolMode } from "../../../../store/builder/store"; +import { useThree } from "@react-three/fiber"; +import { LineCurve3, Plane, Vector3 } from "three"; +import { getPathsByPointId, setPathPosition } from "./function/getPaths"; +import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors"; +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved?: boolean; + handleA?: [number, number, number] | null; + handleB?: [number, number, number] | null; +}; + +interface PathDataInterface { + pathId: string; + isActive?: boolean; + isCurved?: boolean; + pathPoints: [PointData, PointData]; +} + +type PathData = PathDataInterface[]; + +type PathHandlerProps = { + selectedPath: PathDataInterface; + points: [PointData, PointData]; + paths: PathData; + setPaths: React.Dispatch>; + setHoveredLine: React.Dispatch< + React.SetStateAction + >; + hoveredLine: PathDataInterface | null; + hoveredPoint: PointData | null; +}; +export default function PathHandler({ + selectedPath, + setPaths, + points, + paths, + setHoveredLine, + hoveredLine, + hoveredPoint, +}: PathHandlerProps) { + const { toolMode } = useToolMode(); + const { scene, raycaster } = useThree(); + const [dragOffset, setDragOffset] = useState(null); + const [initialPositions, setInitialPositions] = useState<{ + paths?: any; + }>({}); + const [isHovered, setIsHovered] = useState(false); + const { activeSubTool } = useActiveSubTool(); + const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []); + const path = useMemo(() => { + const [start, end] = points.map((p) => new Vector3(...p.position)); + return new LineCurve3(start, end); + }, [points]); + const removePath = (pathId: string) => { + setPaths((prevPaths) => prevPaths.filter((p) => p.pathId !== pathId)); + }; + const handlePathClick = (pointId: string) => { + if (toolMode === "3D-Delete") { + removePath(pointId); + } + }; + const handleDragStart = (points: [PointData, PointData]) => { + if (activeSubTool !== "free-hand") return; + const intersectionPoint = new Vector3(); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (hit) { + const start = new Vector3(...points[0].position); + const end = new Vector3(...points[1].position); + const midPoint = new Vector3().addVectors(start, end).multiplyScalar(0.5); + const offset = new Vector3().subVectors(midPoint, hit); + setDragOffset(offset); + const pathSet = getPathsByPointId(points[0].pointId, paths); + setInitialPositions({ paths: pathSet }); + } + }; + // const handleDrag = (points: [PointData, PointData]) => { + // if (isHovered && dragOffset) { + // const intersectionPoint = new Vector3(); + // const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + + // if (hit) { + // handleCanvasCursors("grabbing"); + // const positionWithOffset = new Vector3().addVectors(hit, dragOffset); + // const start = new Vector3(...points[0].position); + // const end = new Vector3(...points[1].position); + // const midPoint = new Vector3() + // .addVectors(start, end) + // .multiplyScalar(0.5); + // const delta = new Vector3().subVectors(positionWithOffset, midPoint); + // const newStart = new Vector3().addVectors(start, delta); + // const newEnd = new Vector3().addVectors(end, delta); + // setPathPosition( + // points[0].pointId, + // [newStart.x, newStart.y, newStart.z], + // setPaths + // ); + // setPathPosition( + // points[1].pointId, + // [newEnd.x, newEnd.y, newEnd.z], + // setPaths + // ); + // } + // } + // }; + const handleDrag = (points: [PointData, PointData]) => { + if (isHovered && dragOffset) { + const intersectionPoint = new Vector3(); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (hit) { + handleCanvasCursors("grabbing"); + const positionWithOffset = new Vector3().addVectors(hit, dragOffset); + + const start = new Vector3(...points[0].position); + const end = new Vector3(...points[1].position); + const midPoint = new Vector3() + .addVectors(start, end) + .multiplyScalar(0.5); + const delta = new Vector3().subVectors(positionWithOffset, midPoint); + + const newStart: [number, number, number] = [ + start.x + delta.x, + start.y + delta.y, + start.z + delta.z, + ]; + const newEnd: [number, number, number] = [ + end.x + delta.x, + end.y + delta.y, + end.z + delta.z, + ]; + + // βœ… Move both points separately (won’t overwrite other updates) + setPathPosition(points[0].pointId, newStart, setPaths); + setPathPosition(points[1].pointId, newEnd, setPaths); + } + } + }; + + const handleDragEnd = (points: [PointData, PointData]) => {}; + return ( + <> + handleDragStart(points)} + onDrag={() => handleDrag(points)} + onDragEnd={() => handleDragEnd(points)} + > + { + e.stopPropagation(); + handlePathClick(selectedPath.pathId); + }} + onPointerOver={(e) => { + if (e.buttons === 0 && !e.ctrlKey) { + setHoveredLine(selectedPath); + setIsHovered(true); + if (!hoveredPoint) { + // handleCanvasCursors("grab"); + } + } + }} + onPointerOut={() => { + if (isHovered && hoveredLine) { + setHoveredLine(null); + if (!hoveredPoint) { + // handleCanvasCursors("default"); + } + } + setIsHovered(false); + }} + /> + + + ); +} diff --git a/app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx b/app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx new file mode 100644 index 0000000..9812a9b --- /dev/null +++ b/app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx @@ -0,0 +1,299 @@ +import { DragControls } from "@react-three/drei"; +import React, { useCallback, useMemo, useState } from "react"; +import { useActiveSubTool, useToolMode } from "../../../../store/builder/store"; +import { Plane, Vector3 } from "three"; +import { useThree } from "@react-three/fiber"; +import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors"; +import { getPathsByPointId, setPathPosition } from "./function/getPaths"; +type PointData = { + pointId: string; + position: [number, number, number]; + isCurved?: boolean; + handleA?: [number, number, number] | null; + handleB?: [number, number, number] | null; +}; +interface PathDataInterface { + pathId: string; + isActive?: boolean; + isCurved?: boolean; + pathPoints: [PointData, PointData]; +} + +type PathData = PathDataInterface[]; +type PointHandlerProps = { + point: PointData; + hoveredPoint: PointData | null; + setPaths: React.Dispatch>; + paths: PathDataInterface[]; + setHoveredPoint: React.Dispatch>; + hoveredLine: PathDataInterface | null; +}; +export default function PointHandler({ + point, + setPaths, + paths, + setHoveredPoint, + hoveredLine, + hoveredPoint, +}: PointHandlerProps) { + const { toolMode } = useToolMode(); + const { scene, raycaster } = useThree(); + const [isHovered, setIsHovered] = useState(false); + const [dragOffset, setDragOffset] = useState(null); + const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []); + const { activeSubTool } = useActiveSubTool(); + const [initialPositions, setInitialPositions] = useState<{ + paths?: any; + }>({}); + const [selectedPoints, setSelectedPoints] = useState(); + const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters + + const CAN_POINT_SNAP = true; + const CAN_ANGLE_SNAP = true; + const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5; + const removePathByPoint = (pointId: string): PathDataInterface[] => { + const removedPaths: PathDataInterface[] = []; + + setPaths((prevPaths) => + prevPaths.filter((path) => { + const hasPoint = path.pathPoints.some((p) => p.pointId === pointId); + if (hasPoint) { + removedPaths.push(JSON.parse(JSON.stringify(path))); // keep a copy + return false; // remove this path + } + return true; // keep this path + }) + ); + + return removedPaths; + }; + const getConnectedPoints = (uuid: string): PointData[] => { + const connected: PointData[] = []; + + for (const path of paths) { + for (const point of path.pathPoints) { + if (point.pointId === uuid) { + connected.push( + ...path.pathPoints.filter((p: PointData) => p.pointId !== uuid) + ); + } + } + } + + return connected; + }; + const snapPathAngle = useCallback( + ( + newPosition: [number, number, number], + pointId: string + ): { + position: [number, number, number]; + isSnapped: boolean; + snapSources: Vector3[]; + } => { + if (!pointId || !CAN_ANGLE_SNAP) { + return { position: newPosition, isSnapped: false, snapSources: [] }; + } + + const connectedPoints: PointData[] = getConnectedPoints(pointId) || []; + + if (connectedPoints.length === 0) { + return { + position: newPosition, + isSnapped: false, + snapSources: [], + }; + } + + const newPos = new Vector3(...newPosition); + + let closestX: { pos: Vector3; dist: number } | null = null; + let closestZ: { pos: Vector3; dist: number } | null = null; + + for (const connectedPoint of connectedPoints) { + const cPos = new Vector3(...connectedPoint.position); + + const xDist = Math.abs(newPos.x - cPos.x); + const zDist = Math.abs(newPos.z - cPos.z); + + if (xDist < ANGLE_SNAP_DISTANCE_THRESHOLD) { + if (!closestX || xDist < closestX.dist) { + closestX = { pos: cPos, dist: xDist }; + } + } + + if (zDist < ANGLE_SNAP_DISTANCE_THRESHOLD) { + if (!closestZ || zDist < closestZ.dist) { + closestZ = { pos: cPos, dist: zDist }; + } + } + } + + const snappedPos = newPos.clone(); + const snapSources: Vector3[] = []; + + if (closestX) { + snappedPos.x = closestX.pos.x; + snapSources.push(closestX.pos.clone()); + } + + if (closestZ) { + snappedPos.z = closestZ.pos.z; + snapSources.push(closestZ.pos.clone()); + } + + const isSnapped = snapSources.length > 0; + + return { + position: [snappedPos.x, snappedPos.y, snappedPos.z], + isSnapped, + snapSources, + }; + }, + [] + ); + const getAllOtherPathPoints = useCallback((): PointData[] => { + return ( + paths?.flatMap((path) => + path.pathPoints.filter((pt) => pt.pointId !== point.pointId) + ) ?? [] + ); + }, [paths]); + + const snapPathPoint = useCallback( + (position: [number, number, number], pointId?: string) => { + if (!CAN_POINT_SNAP) + return { position: position, isSnapped: false, snappedPoint: null }; + + const otherPoints = getAllOtherPathPoints(); + + const currentVec = new Vector3(...position); + for (const point of otherPoints) { + const pointVec = new Vector3(...point.position); + const distance = currentVec.distanceTo(pointVec); + if (distance <= POINT_SNAP_THRESHOLD) { + return { + position: point.position, + isSnapped: true, + snappedPoint: point, + }; + } + } + return { position: position, isSnapped: false, snappedPoint: null }; + }, + [getAllOtherPathPoints] + ); + + // const setPathPosition = useCallback( + // ( + // pointUuid: string, + // position: [number, number, number], + // setPaths: React.Dispatch> + // ) => { + // setPaths((prevPaths) => + // prevPaths.map((path) => { + // if (path.pathPoints.some((p) => p.pointId === pointUuid)) { + // return { + // ...path, + // pathPoints: path.pathPoints.map((p) => + // p.pointId === pointUuid ? { ...p, position } : p + // ) as [PointData, PointData], // πŸ‘ˆ force back to tuple + // }; + // } + // return path; + // }) + // ); + // }, + // [setPaths] + // ); + + // const getPathsByPointId = (pointId: any) => { + // return paths.filter((a) => a.pathPoints.some((p) => p.pointId === pointId)); + // }; + + const handlePointClick = (pointId: string) => { + if (toolMode === "3D-Delete") { + removePathByPoint(pointId); + } + }; + const handleDragStart = (point: PointData) => { + if (activeSubTool !== "free-hand") return; + const intersectionPoint = new Vector3(); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (hit) { + const currentPosition = new Vector3(...point.position); + const offset = new Vector3().subVectors(currentPosition, hit); + setDragOffset(offset); + const pathIntersection = getPathsByPointId(point.pointId, paths); + setInitialPositions({ paths: pathIntersection }); + } + }; + const handleDrag = (point: PointData) => { + if (isHovered && dragOffset) { + const intersectionPoint = new Vector3(); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (hit) { + // handleCanvasCursors("grabbing"); + const positionWithOffset = new Vector3().addVectors(hit, dragOffset); + const newPosition: [number, number, number] = [ + positionWithOffset.x, + positionWithOffset.y, + positionWithOffset.z, + ]; + + // βœ… Pass newPosition and pointId + const pathSnapped = snapPathAngle(newPosition, point.pointId); + const finalSnapped = snapPathPoint(pathSnapped.position); + setPathPosition(point.pointId, finalSnapped.position, setPaths); + } + } + }; + + const handleDragEnd = (point: PointData) => { + const pathIntersection = getPathsByPointId(point.pointId, paths); + if (pathIntersection && pathIntersection.length > 0) { + pathIntersection.forEach((update) => {}); + } + }; + return ( + <> + handleDragStart(point)} + onDrag={() => handleDrag(point)} + onDragEnd={() => handleDragEnd(point)} + > + { + e.stopPropagation(); + handlePointClick(point.pointId); + }} + onPointerOver={(e) => { + if (!hoveredPoint && e.buttons === 0 && !e.ctrlKey) { + setHoveredPoint(point); + setIsHovered(true); + // handleCanvasCursors("default"); + } + }} + onPointerOut={() => { + if (hoveredPoint) { + setHoveredPoint(null); + if (!hoveredLine) { + // handleCanvasCursors("default"); + } + } + setIsHovered(false); + }} + > + + + + + + ); +}