finding shortest path
This commit is contained in:
@@ -57,7 +57,7 @@ export default function PathCreator() {
|
|||||||
|
|
||||||
const { activeSubTool } = useActiveSubTool();
|
const { activeSubTool } = useActiveSubTool();
|
||||||
const { activeTool } = useActiveTool();
|
const { activeTool } = useActiveTool();
|
||||||
|
const [pathPointsList, setPathPointsList] = useState<PointData[]>([]);
|
||||||
const POINT_SNAP_THRESHOLD = 0.5;
|
const POINT_SNAP_THRESHOLD = 0.5;
|
||||||
const CAN_POINT_SNAP = true;
|
const CAN_POINT_SNAP = true;
|
||||||
useEffect(() => {}, [paths]);
|
useEffect(() => {}, [paths]);
|
||||||
@@ -275,7 +275,11 @@ export default function PathCreator() {
|
|||||||
newPoint.position = point.position;
|
newPoint.position = point.position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setPathPointsList((prev) => {
|
||||||
|
if (!prev.find((p) => p.pointId === newPoint.pointId))
|
||||||
|
return [...prev, newPoint];
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
if (draftPoints.length === 0) {
|
if (draftPoints.length === 0) {
|
||||||
setDraftPoints([newPoint]);
|
setDraftPoints([newPoint]);
|
||||||
} else {
|
} else {
|
||||||
@@ -329,6 +333,9 @@ export default function PathCreator() {
|
|||||||
return points;
|
return points;
|
||||||
}, [paths]);
|
}, [paths]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("allPoints: ", allPoints);
|
||||||
|
}, [paths]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Draft points (red) */}
|
{/* Draft points (red) */}
|
||||||
@@ -340,8 +347,10 @@ export default function PathCreator() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Saved points */}
|
{/* Saved points */}
|
||||||
{allPoints.map((point) => (
|
{allPoints.map((point: PointData, i: any) => (
|
||||||
<PointHandler
|
<PointHandler
|
||||||
|
points={allPoints}
|
||||||
|
pointIndex={i}
|
||||||
key={point.pointId}
|
key={point.pointId}
|
||||||
point={point}
|
point={point}
|
||||||
setPaths={setPaths}
|
setPaths={setPaths}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type PointData = {
|
|||||||
isCurved?: boolean;
|
isCurved?: boolean;
|
||||||
handleA?: [number, number, number] | null;
|
handleA?: [number, number, number] | null;
|
||||||
handleB?: [number, number, number] | null;
|
handleB?: [number, number, number] | null;
|
||||||
|
neighbors?: string[];
|
||||||
};
|
};
|
||||||
interface PathDataInterface {
|
interface PathDataInterface {
|
||||||
pathId: string;
|
pathId: string;
|
||||||
@@ -28,7 +29,130 @@ type PointHandlerProps = {
|
|||||||
paths: PathDataInterface[];
|
paths: PathDataInterface[];
|
||||||
setHoveredPoint: React.Dispatch<React.SetStateAction<PointData | null>>;
|
setHoveredPoint: React.Dispatch<React.SetStateAction<PointData | null>>;
|
||||||
hoveredLine: PathDataInterface | null;
|
hoveredLine: PathDataInterface | null;
|
||||||
|
pointIndex: any;
|
||||||
|
points: PointData[];
|
||||||
};
|
};
|
||||||
|
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;
|
||||||
|
}
|
||||||
export default function PointHandler({
|
export default function PointHandler({
|
||||||
point,
|
point,
|
||||||
setPaths,
|
setPaths,
|
||||||
@@ -36,6 +160,8 @@ export default function PointHandler({
|
|||||||
setHoveredPoint,
|
setHoveredPoint,
|
||||||
hoveredLine,
|
hoveredLine,
|
||||||
hoveredPoint,
|
hoveredPoint,
|
||||||
|
pointIndex,
|
||||||
|
points,
|
||||||
}: PointHandlerProps) {
|
}: PointHandlerProps) {
|
||||||
const { toolMode } = useToolMode();
|
const { toolMode } = useToolMode();
|
||||||
const { activeTool } = useActiveTool();
|
const { activeTool } = useActiveTool();
|
||||||
@@ -47,9 +173,11 @@ export default function PointHandler({
|
|||||||
paths?: any;
|
paths?: any;
|
||||||
}>({});
|
}>({});
|
||||||
const [selectedPoints, setSelectedPoints] = useState<PointData[]>([]);
|
const [selectedPoints, setSelectedPoints] = useState<PointData[]>([]);
|
||||||
|
const [shortestEdges, setShortestEdges] = useState<PathData>([]);
|
||||||
const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
||||||
|
const [selectedPointIndices, setSelectedPointIndices] = useState<number[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
const CAN_POINT_SNAP = true;
|
const CAN_POINT_SNAP = true;
|
||||||
const CAN_ANGLE_SNAP = true;
|
const CAN_ANGLE_SNAP = true;
|
||||||
const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5;
|
const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5;
|
||||||
@@ -70,6 +198,7 @@ export default function PointHandler({
|
|||||||
|
|
||||||
return removedPaths;
|
return removedPaths;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getConnectedPoints = (uuid: string): PointData[] => {
|
const getConnectedPoints = (uuid: string): PointData[] => {
|
||||||
const connected: PointData[] = [];
|
const connected: PointData[] = [];
|
||||||
|
|
||||||
@@ -85,6 +214,7 @@ export default function PointHandler({
|
|||||||
|
|
||||||
return connected;
|
return connected;
|
||||||
};
|
};
|
||||||
|
|
||||||
const snapPathAngle = useCallback(
|
const snapPathAngle = useCallback(
|
||||||
(
|
(
|
||||||
newPosition: [number, number, number],
|
newPosition: [number, number, number],
|
||||||
@@ -155,6 +285,7 @@ export default function PointHandler({
|
|||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getAllOtherPathPoints = useCallback((): PointData[] => {
|
const getAllOtherPathPoints = useCallback((): PointData[] => {
|
||||||
return (
|
return (
|
||||||
paths?.flatMap((path) =>
|
paths?.flatMap((path) =>
|
||||||
@@ -186,42 +317,103 @@ export default function PointHandler({
|
|||||||
},
|
},
|
||||||
[getAllOtherPathPoints]
|
[getAllOtherPathPoints]
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
let isHandlingShiftClick = false;
|
||||||
console.log("selectedPoints: ", selectedPoints);
|
|
||||||
}, [selectedPoints]);
|
// const handlePointClick = (e: any, point: PointData) => {
|
||||||
// const setPathPosition = useCallback(
|
// if (toolMode === "3D-Delete") {
|
||||||
// (
|
// removePathByPoint(point.pointId);
|
||||||
// pointUuid: string,
|
// }
|
||||||
// position: [number, number, number],
|
// if (e.shiftKey) {
|
||||||
// setPaths: React.Dispatch<React.SetStateAction<PathData>>
|
// setSelectedPointIndices((prev) => {
|
||||||
// ) => {
|
// if (prev.length === 0) return [pointIndex];
|
||||||
// setPaths((prevPaths) =>
|
// if (prev.length === 1) {
|
||||||
// prevPaths.map((path) => {
|
// // defer shortest path calculation
|
||||||
// if (path.pathPoints.some((p) => p.pointId === pointUuid)) {
|
// setTimeout(() => {
|
||||||
// return {
|
// console.log("points: ", points);
|
||||||
// ...path,
|
// const p1 = points[prev[0]];
|
||||||
// pathPoints: path.pathPoints.map((p) =>
|
// console.log(' p1: ', p1);
|
||||||
// p.pointId === pointUuid ? { ...p, position } : p
|
// const p2 = points[pointIndex];
|
||||||
// ) as [PointData, PointData], // 👈 force back to tuple
|
// console.log('p2: ', p2);
|
||||||
// };
|
// const result = aStarShortestPath(
|
||||||
|
// p1.pointId,
|
||||||
|
// p2.pointId,
|
||||||
|
// points,
|
||||||
|
// paths
|
||||||
|
// );
|
||||||
|
// if (result) {
|
||||||
|
// const edges = nodePathToEdges(result.pointIds, points, paths);
|
||||||
|
// console.log("edges: ", edges);
|
||||||
|
// setShortestEdges(edges);
|
||||||
|
// } else {
|
||||||
|
// setShortestEdges([]);
|
||||||
// }
|
// }
|
||||||
// return path;
|
// }, 0);
|
||||||
// })
|
// return [prev[0], pointIndex];
|
||||||
// );
|
// }
|
||||||
// },
|
// return [pointIndex];
|
||||||
// [setPaths]
|
// });
|
||||||
// );
|
// }
|
||||||
|
// };
|
||||||
// const getPathsByPointId = (pointId: any) => {
|
const handlePointClick = (e: any, point: PointData) => {
|
||||||
// return paths.filter((a) => a.pathPoints.some((p) => p.pointId === pointId));
|
e.stopPropagation();
|
||||||
// };
|
|
||||||
|
|
||||||
const handlePointClick = (e: any, pointId: string) => {
|
|
||||||
if (toolMode === "3D-Delete") {
|
if (toolMode === "3D-Delete") {
|
||||||
removePathByPoint(pointId);
|
removePathByPoint(point.pointId);
|
||||||
} else if (e.ctrlKey) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
if (isHandlingShiftClick) return; // prevent double-handling
|
||||||
|
isHandlingShiftClick = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isHandlingShiftClick = false;
|
||||||
|
}, 100); // reset the flag after a short delay
|
||||||
|
|
||||||
|
setSelectedPointIndices((prev) => {
|
||||||
|
// console.log("Clicked point index:", pointIndex);
|
||||||
|
console.log("Previous selection:", prev);
|
||||||
|
|
||||||
|
if (prev.length === 0) {
|
||||||
|
console.log("first works");
|
||||||
|
return [pointIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev.length === 1) {
|
||||||
|
if (prev[0] === pointIndex) {
|
||||||
|
console.log("Same point selected twice — ignoring");
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p1 = points[prev[0]];
|
||||||
|
const p2 = points[pointIndex];
|
||||||
|
|
||||||
|
console.log("Point 1:", p1);
|
||||||
|
console.log("Point 2:", p2);
|
||||||
|
|
||||||
|
if (p1 && p2) {
|
||||||
|
const result = aStarShortestPath(
|
||||||
|
p1.pointId,
|
||||||
|
p2.pointId,
|
||||||
|
points,
|
||||||
|
paths
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const edges = nodePathToEdges(result.pointIds, points, paths);
|
||||||
|
setShortestEdges(edges);
|
||||||
|
} else {
|
||||||
|
setShortestEdges([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [prev[0], pointIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pointIndex];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStart = (point: PointData) => {
|
const handleDragStart = (point: PointData) => {
|
||||||
if (activeTool !== "cursor") return;
|
if (activeTool !== "cursor") return;
|
||||||
const intersectionPoint = new Vector3();
|
const intersectionPoint = new Vector3();
|
||||||
@@ -261,6 +453,11 @@ export default function PointHandler({
|
|||||||
pathIntersection.forEach((update) => {});
|
pathIntersection.forEach((update) => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("selectedPoints: ", selectedPoints);
|
||||||
|
}, [selectedPoints]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DragControls
|
<DragControls
|
||||||
@@ -276,8 +473,7 @@ export default function PointHandler({
|
|||||||
name="Path-Point"
|
name="Path-Point"
|
||||||
userData={point}
|
userData={point}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
handlePointClick(e, point);
|
||||||
handlePointClick(e, point.pointId);
|
|
||||||
}}
|
}}
|
||||||
onPointerOver={(e) => {
|
onPointerOver={(e) => {
|
||||||
if (!hoveredPoint && e.buttons === 0 && !e.ctrlKey) {
|
if (!hoveredPoint && e.buttons === 0 && !e.ctrlKey) {
|
||||||
@@ -303,3 +499,30 @@ export default function PointHandler({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const setPathPosition = useCallback(
|
||||||
|
// (
|
||||||
|
// pointUuid: string,
|
||||||
|
// position: [number, number, number],
|
||||||
|
// setPaths: React.Dispatch<React.SetStateAction<PathData>>
|
||||||
|
// ) => {
|
||||||
|
// setPaths((prevPaths) =>
|
||||||
|
// prevPaths.map((path) => {
|
||||||
|
// if (path.pathPoints.some((p) => p.pointId === pointUuid)) {
|
||||||
|
// return {
|
||||||
|
// ...path,
|
||||||
|
// pathPoints: path.pathPoints.map((p) =>
|
||||||
|
// p.pointId === pointUuid ? { ...p, position } : p
|
||||||
|
// ) as [PointData, PointData], // 👈 force back to tuple
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// return path;
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// [setPaths]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const getPathsByPointId = (pointId: any) => {
|
||||||
|
// return paths.filter((a) => a.pathPoints.some((p) => p.pointId === pointId));
|
||||||
|
// };
|
||||||
|
|||||||
@@ -602,7 +602,7 @@ export default function PointHandlers({
|
|||||||
const handleARef = useRef<THREE.Mesh>(null);
|
const handleARef = useRef<THREE.Mesh>(null);
|
||||||
const handleBRef = useRef<THREE.Mesh>(null);
|
const handleBRef = useRef<THREE.Mesh>(null);
|
||||||
const lineRef = useRef<any>(null);
|
const lineRef = useRef<any>(null);
|
||||||
const { camera, gl, controls, raycaster } = useThree();
|
const { gl, controls, raycaster } = useThree();
|
||||||
|
|
||||||
const [dragging, setDragging] = useState<
|
const [dragging, setDragging] = useState<
|
||||||
null | "main" | "handleA" | "handleB"
|
null | "main" | "handleA" | "handleB"
|
||||||
@@ -620,6 +620,7 @@ export default function PointHandlers({
|
|||||||
if (prev.length === 1) {
|
if (prev.length === 1) {
|
||||||
// defer shortest path calculation
|
// defer shortest path calculation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log('points: ', points);
|
||||||
const p1 = points[prev[0]];
|
const p1 = points[prev[0]];
|
||||||
const p2 = points[pointIndex];
|
const p2 = points[pointIndex];
|
||||||
const result = aStarShortestPath(
|
const result = aStarShortestPath(
|
||||||
@@ -630,6 +631,7 @@ export default function PointHandlers({
|
|||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
const edges = nodePathToEdges(result.pointIds, points, paths);
|
const edges = nodePathToEdges(result.pointIds, points, paths);
|
||||||
|
console.log('edges: ', edges);
|
||||||
setShortestEdges(edges);
|
setShortestEdges(edges);
|
||||||
setShortestPath(edges);
|
setShortestPath(edges);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function StructuredPath() {
|
|||||||
// --- State Variables ---
|
// --- State Variables ---
|
||||||
const [pathPointsList, setPathPointsList] = useState<PointData[]>([]);
|
const [pathPointsList, setPathPointsList] = useState<PointData[]>([]);
|
||||||
const [allPaths, setAllPaths] = useState<PathData>([]);
|
const [allPaths, setAllPaths] = useState<PathData>([]);
|
||||||
console.log("allPaths: ", allPaths);
|
|
||||||
const [computedShortestPath, setComputedShortestPath] = useState<PathData>(
|
const [computedShortestPath, setComputedShortestPath] = useState<PathData>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user