361 lines
11 KiB
TypeScript
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]);
|