added predined path

This commit is contained in:
2025-08-20 10:09:34 +05:30
parent 5e025224d6
commit 3f808f167d
15 changed files with 2280 additions and 698 deletions

View File

@@ -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);

View File

@@ -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<React.SetStateAction<any[][]>>;
// }
// export default function EditablePoint({
// point,
// pointIndex,
// groupIndex,
// selected,
// setPointsGroups,
// }: PointProps) {
// const meshRef = useRef<THREE.Mesh>(null);
// const handleARef = useRef<THREE.Mesh>(null);
// const handleBRef = useRef<THREE.Mesh>(null);
// const lineRef = useRef<THREE.Line>(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 */}
// <mesh
// ref={meshRef}
// position={point.position}
// onClick={onPointClick}
// onPointerDown={(e) => startDrag("main", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.3, 16, 16]} />
// <meshStandardMaterial
// color={selected.includes(pointIndex) ? "red" : "pink"}
// />
// </mesh>
// {/* Handles + line */}
// {point.isCurved && point.handleA && point.handleB && (
// <>
// {/* Line between handles */}
// {point.handleA && point.handleB && (
// <Line
// points={[point.handleA, point.handleB]}
// color="gray"
// lineWidth={1} // optional, works in WebGL2
// />
// )}
// {/* Handle A */}
// <mesh
// ref={handleARef}
// position={point.handleA}
// onPointerDown={(e) => startDrag("handleA", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.15, 8, 8]} />
// <meshStandardMaterial color="orange" />
// </mesh>
// {/* Handle B */}
// <mesh
// ref={handleBRef}
// position={point.handleB}
// onPointerDown={(e) => startDrag("handleB", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.15, 8, 8]} />
// <meshStandardMaterial color="green" />
// </mesh>
// </>
// )}
// </>
// );
// }
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<React.SetStateAction<number[]>>;
pointsGroups: any[][];
setPointsGroups: React.Dispatch<React.SetStateAction<any[][]>>;
@@ -260,7 +35,6 @@ export default function PointHandle({
shortestPath,
setShortestPath,
}: PointProps) {
const meshRef = useRef<THREE.Mesh>(null);
const handleARef = useRef<THREE.Mesh>(null);
const handleBRef = useRef<THREE.Mesh>(null);
@@ -553,7 +327,7 @@ export default function PointHandle({
// path.unshift(u);
// u = previous[u];
// }
//
//
// return path;
// };

View File

@@ -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<Vector3 | null>(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 (
// <Line
// points={curvePoints}
// color="purple"
// lineWidth={2}
// onPointerDown={onClickLine}
// onPointerUp={onPointerUp}
// />
// );
// }
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<Vector3 | null>(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 (
<Line
points={curvePoints}

View File

@@ -18,335 +18,6 @@ import * as THREE from "three";
import PointHandle from "./PointHandle";
import LineSegment from "./lineSegment";
// export default function PreDefinedPath() {
// const { gl, raycaster } = useThree();
// const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0));
// const [points, setPoints] = useState<[number, number, number][]>([]);
// const [selected, setSelected] = useState<number[]>([]);
// 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) => (
// <mesh
// key={idx}
// position={pos}
// onClick={() => {
// setSelected((prev) => (prev.length === 2 ? [idx] : [...prev, idx]));
// }}
// >
// <sphereGeometry args={[0.3, 16, 16]} />
// <meshStandardMaterial
// color={selected.includes(idx) ? "red" : "pink"}
// />
// </mesh>
// ))}
// {points.map((pos, i) => {
// if (i < points.length - 1) {
// return (
// <DraggableLineSegment
// key={i}
// index={i}
// start={new THREE.Vector3(...points[i])}
// end={new THREE.Vector3(...points[i + 1])}
// updatePoints={updatePoints}
// insertPoint={insertPoint}
// />
// );
// }
// 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<number[]>([]);
// 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) => (
// <React.Fragment key={gIdx}>
// {group.map((pos, idx) => (
// <mesh
// key={idx}
// position={pos}
// onClick={(e) => {
// e.stopPropagation();
// setSelected((prev) =>
// prev.length === 2 ? [idx] : [...prev, idx]
// );
// }}
// >
// <sphereGeometry args={[0.3, 16, 16]} />
// <meshStandardMaterial
// color={selected.includes(idx) ? "red" : "pink"}
// />
// </mesh>
// ))}
// {group.map((pos, i) => {
// if (i < group.length - 1) {
// return (
// <DraggableLineSegment
// key={i}
// index={i}
// start={new THREE.Vector3(...group[i])}
// end={new THREE.Vector3(...group[i + 1])}
// updatePoints={(i0, p0, i1, p1) =>
// updatePoints(gIdx, i0, p0, i1, p1)
// }
// insertPoint={(index, point) =>
// insertPoint(gIdx, index, point)
// }
// />
// );
// }
// return null;
// })}
// </React.Fragment>
// ))}
// </>
// );
// }
// 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<PointData[][]>([]);
console.log("mainShapeOnly: ", mainShapeOnly);
const [pointsGroups, setPointsGroups] = useState<PointData[][]>([[]]);
console.log("pointsGroups: ", pointsGroups);
const [definedPath, setDefinedPath] = useState<PointData[][] | PointData[]>(
[]
);
console.log("definedPath: ", definedPath);
const [selected, setSelected] = useState<number[]>([]);
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<number[]>([]);
const [shortestDistance, setShortestDistance] = useState<number>(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]
// );

View File

@@ -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<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
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
}

View File

@@ -0,0 +1,7 @@
export function handleContextMenu(
evt: MouseEvent,
setCurrentTempPath: (val: any[]) => void
) {
evt.preventDefault();
setCurrentTempPath([]);
}

View File

@@ -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<React.SetStateAction<PathData>>;
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);
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
export function handleMouseMove(
isLeftClickDown: { current: boolean },
isDragging: { current: boolean }
) {
if (isLeftClickDown.current) isDragging.current = true;
}

View File

@@ -0,0 +1,6 @@
export function handleMouseUp(
evt: MouseEvent,
isLeftClickDown: { current: boolean }
) {
if (evt.button === 0) isLeftClickDown.current = false;
}

View File

@@ -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<boolean>,
isDragging: React.MutableRefObject<boolean>
) {
if (evt.button === 0) {
if (evt.ctrlKey || evt.shiftKey) return;
isLeftClickDown.current = true;
isDragging.current = false;
}
}
export function handleMouseUp(
evt: MouseEvent,
isLeftClickDown: React.MutableRefObject<boolean>
) {
if (evt.button === 0) isLeftClickDown.current = false;
}
export function handleMouseMove(
isLeftClickDown: React.MutableRefObject<boolean>,
isDragging: React.MutableRefObject<boolean>
) {
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<boolean>;
raycaster: THREE.Raycaster;
plane: THREE.Plane;
pointer: { x: number; y: number };
currentTempPath: any[];
setCurrentTempPath: (val: any[]) => void;
pathPointsList: any[];
allPaths: PathData;
setAllPaths: React.Dispatch<React.SetStateAction<PathData>>;
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([]);
}

View File

@@ -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<React.SetStateAction<PathData>>;
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 <Line points={curvePoints} color="purple" lineWidth={3} />;
}

View File

@@ -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<React.SetStateAction<PointData[]>>;
// setPaths: React.Dispatch<React.SetStateAction<PathData>>;
// paths: PathData;
// selected: number[];
// setSelected: React.Dispatch<React.SetStateAction<number[]>>;
// setShortestPath: React.Dispatch<React.SetStateAction<PathData>>;
// }
// /** --- 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<string>([startId]);
// const cameFrom: Record<string, string | null> = {};
// const gScore: Record<string, number> = {};
// const fScore: Record<string, number> = {};
// 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<THREE.Mesh>(null);
// const handleARef = useRef<THREE.Mesh>(null);
// const handleBRef = useRef<THREE.Mesh>(null);
// const lineRef = useRef<any>(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<PathData>([]);
// /** 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 */}
// <mesh
// ref={meshRef}
// position={point.position}
// onClick={onPointClick}
// onPointerDown={(e) => startDrag("main", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.3, 16, 16]} />
// <meshStandardMaterial
// color={selected.includes(pointIndex) ? "red" : "pink"}
// />
// </mesh>
// {/* Curve handles */}
// {point.isCurved && point.handleA && point.handleB && (
// <>
// <Line
// ref={lineRef}
// points={[point.handleA, point.handleB]}
// color="gray"
// lineWidth={1}
// />
// <mesh
// ref={handleARef}
// position={point.handleA}
// onPointerDown={(e) => startDrag("handleA", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.15, 8, 8]} />
// <meshStandardMaterial color="orange" />
// </mesh>
// <mesh
// ref={handleBRef}
// position={point.handleB}
// onPointerDown={(e) => startDrag("handleB", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.15, 8, 8]} />
// <meshStandardMaterial color="green" />
// </mesh>
// </>
// )}
// {/* Draw connected paths */}
// {/* Highlight shortest path */}
// {shortestEdges.map((edge) => (
// <Line
// key={`sp-${edge.pathId}`}
// points={edge.pathPoints.map((p) => 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<React.SetStateAction<PointData[]>>;
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
paths: PathData;
selected: number[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
setShortestPath: React.Dispatch<React.SetStateAction<PathData>>;
}
/** --- 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<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
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<THREE.Mesh>(null);
const handleARef = useRef<THREE.Mesh>(null);
const handleBRef = useRef<THREE.Mesh>(null);
const lineRef = useRef<any>(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<PathData>([]);
/** 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 */}
<mesh
ref={meshRef}
position={point.position}
onClick={onPointClick}
onPointerDown={(e) => startDrag("main", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.3, 16, 16]} />
<meshStandardMaterial
color={selected.includes(pointIndex) ? "red" : "pink"}
/>
</mesh>
{/* Curve handles */}
{point.isCurved && point.handleA && point.handleB && (
<>
<Line
ref={lineRef}
points={[point.handleA, point.handleB]}
color="gray"
lineWidth={1}
/>
<mesh
ref={handleARef}
position={point.handleA}
onPointerDown={(e) => startDrag("handleA", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="orange" />
</mesh>
<mesh
ref={handleBRef}
position={point.handleB}
onPointerDown={(e) => startDrag("handleB", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="green" />
</mesh>
</>
)}
{/* Highlight shortest path */}
{shortestEdges.map((edge) => (
<Line
key={`sp-${edge.pathId}`}
points={edge.pathPoints.map((p) => p.position)}
color="yellow"
lineWidth={3}
/>
))}
</>
);
}

View File

@@ -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<PointData[]>([]);
// const [paths, setPaths] = useState<PathData>([]);
// const [shortestPath, setShortestPath] = useState<PathData>([]);
// useEffect(() => {
// console.log("paths: ", paths);
// }, [paths]);
// const [tempPath, setTempPath] = useState<PointData[]>([]);
// const [mousePos, setMousePos] = useState<[number, number, number] | null>(
// null
// );
// const [selected, setSelected] = useState<number[]>([]);
// const POLYGON_CLOSE_THRESHOLD = 0.3;
// const isLeftMouseDown = useRef(false);
// const drag = useRef(false);
// const SNAP_POINT_THRESHOLD = 0.2;
// const [assetUuid, setAssetUuid] = useState<any>();
// const SNAP_LINE_THRESHOLD = 0.2;
// const movementState = useRef<any>({});
// 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) => (
// <LineSegment
// key={path.pathId}
// index={pathIndex}
// paths={paths}
// setPaths={setPaths}
// />
// ))}
// {/* Interactive PointHandles */}
// {points.map((point, index) => (
// <PointHandles
// key={point.pointId}
// point={point}
// pointIndex={index}
// points={points}
// setPoints={setPoints}
// paths={paths}
// setPaths={setPaths}
// setShortestPath={setShortestPath}
// selected={selected}
// setSelected={setSelected}
// />
// ))}
// {tempPath.length > 0 && mousePos && (
// <Line
// points={[
// new THREE.Vector3(...tempPath[tempPath.length - 1].position),
// new THREE.Vector3(...mousePos),
// ]}
// color="orange"
// lineWidth={2}
// dashed
// />
// )}
// </>
// );
// }
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<PointData[]>([]);
const [allPaths, setAllPaths] = useState<PathData>([]);
const [computedShortestPath, setComputedShortestPath] = useState<PathData>(
[]
);
const [currentTempPath, setCurrentTempPath] = useState<PointData[]>([]);
const [currentMousePos, setCurrentMousePos] = useState<
[number, number, number] | null
>(null);
const [selectedPointIndices, setSelectedPointIndices] = useState<number[]>(
[]
);
const [vehicleUuids, setVehicleUuids] = useState<any>();
// --- Constants & Refs ---
const isLeftClickDown = useRef<boolean>(false);
const isDragging = useRef<boolean>(false);
const vehicleMovementState = useRef<any>({});
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) => (
<LineSegment
key={path.pathId}
index={pathIndex}
paths={allPaths}
setPaths={setAllPaths}
/>
))}
{pathPointsList.map((point, index) => (
<PointHandles
key={point.pointId}
point={point}
pointIndex={index}
points={pathPointsList}
setPoints={setPathPointsList}
paths={allPaths}
setPaths={setAllPaths}
setShortestPath={setComputedShortestPath}
selected={selectedPointIndices}
setSelected={setSelectedPointIndices}
/>
))}
{currentTempPath.length > 0 && currentMousePos && (
<Line
points={[
new THREE.Vector3(
...currentTempPath[currentTempPath.length - 1].position
),
new THREE.Vector3(...currentMousePos),
]}
color="orange"
lineWidth={2}
dashed
/>
)}
</>
);
}

View File

@@ -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 (
<>
<PreDefinedPath />
<StructuredPath />
{/* <PreDefinedPath /> */}
{/* <VehicleInstances /> */}
{isVehicleSelected && selectedEventSphere && !isPlaying && <VehicleUI />}
</>