Files
Dwinzo_Demo/app/src/modules/simulation/vehicle/preDefinedPath/PointHandle.tsx
2025-08-20 10:09:34 +05:30

361 lines
11 KiB
TypeScript

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[][]>>;
shortestPath: number[]; // <- add this
setShortestPath: React.Dispatch<React.SetStateAction<number[]>>; // <- add this
setShortestDistance?: React.Dispatch<React.SetStateAction<number>>; // optional
}
export default function PointHandle({
point,
pointIndex,
groupIndex,
selected,
setSelected,
pointsGroups,
setPointsGroups,
setShortestDistance,
shortestPath,
setShortestPath,
}: 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 pathLineRef = useRef<THREE.Line>(null!);
const { camera, gl, controls } = useThree();
const [dragging, setDragging] = useState<
null | "main" | "handleA" | "handleB"
>(null);
const dragOffset = useRef(new THREE.Vector3());
// const [shortestPath, setShortestPath] = useState<number[]>([]);
/** Shift-click or ctrl-click handling */
const onPointClick = (e: any) => {
e.stopPropagation();
if (e.ctrlKey) {
// Toggle 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;
});
} else if (e.shiftKey) {
// Shift-click for multi-select
setSelected((prev) => {
if (prev.includes(pointIndex)) return prev; // keep selection
const newSelection = [...prev, pointIndex];
return newSelection.slice(-2); // keep only 2 points
});
} else {
// Single selection
setSelected([pointIndex]);
}
};
/** 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)) {
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];
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];
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 handle lines */
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;
}
});
useEffect(() => {
if (selected.length === 2) {
const groupPoints = pointsGroups[groupIndex];
if (!groupPoints) return;
const pathPoints = selected
.map((i) => groupPoints[i])
.filter((p) => p !== undefined)
.map((p) => p.position);
setShortestPath(pathPoints);
// compute distance
let totalDistance = 0;
for (let i = 0; i < pathPoints.length - 1; i++) {
const p1 = new THREE.Vector3().fromArray(pathPoints[i]);
const p2 = new THREE.Vector3().fromArray(pathPoints[i + 1]);
totalDistance += p1.distanceTo(p2);
}
setShortestDistance?.(totalDistance);
} else {
setShortestPath([]);
setShortestDistance?.(0);
}
}, [selected, pointsGroups]);
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
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 */}
{shortestPath.length > 1 && (
<Line
points={shortestPath} // <- just use the positions array
color="blue"
lineWidth={2}
/>
)}
</>
);
}
/** Build adjacency list for shortest path */
// const buildGraph = (points: any[]) => {
// const graph: Record<number, { neighbor: number; distance: number }[]> = {};
// points.forEach((p, idx) => {
// graph[idx] = [];
// points.forEach((q, j) => {
// if (idx !== j) {
// const d = new THREE.Vector3()
// .fromArray(p.position)
// .distanceTo(new THREE.Vector3().fromArray(q.position));
// graph[idx].push({ neighbor: j, distance: d });
// }
// });
// });
// return graph;
// };
// /** Dijkstra shortest path */
// const findShortestPath = (graph: any, startIdx: number, endIdx: number) => {
// const distances: number[] = Array(Object.keys(graph).length).fill(Infinity);
// const previous: (number | null)[] = Array(distances.length).fill(null);
// distances[startIdx] = 0;
// const queue = new Set(Object.keys(graph).map(Number));
// while (queue.size) {
// let current = [...queue].reduce((a, b) =>
// distances[a] < distances[b] ? a : b
// );
// if (current === endIdx) break;
// queue.delete(current);
// for (const { neighbor, distance } of graph[current]) {
// const alt = distances[current] + distance;
// if (alt < distances[neighbor]) {
// distances[neighbor] = alt;
// previous[neighbor] = current;
// }
// }
// }
// const path: number[] = [];
// let u: number | null = endIdx;
// while (u !== null) {
// path.unshift(u);
// u = previous[u];
// }
//
// return path;
// };
// /** Calculate shortest path when 2 points are selected */
// useEffect(() => {
// if (selected.length === 2) {
// const groupPoints = pointsGroups[groupIndex];
// const graph = buildGraph(groupPoints);
// const path = findShortestPath(graph, selected[0], selected[1]);
// setShortestPath(path);
// // Calculate distance
// if (setShortestDistance) {
// let totalDistance = 0;
// for (let i = 0; i < path.length - 1; i++) {
// const p1 = new THREE.Vector3().fromArray(
// groupPoints[path[i]].position
// );
// const p2 = new THREE.Vector3().fromArray(
// groupPoints[path[i + 1]].position
// );
// totalDistance += p1.distanceTo(p2);
// }
// setShortestDistance?.(totalDistance);
// }
// } else {
// setShortestPath([]);
// if (setShortestDistance) setShortestDistance(0);
// }
// }, [selected, pointsGroups]);