721 lines
21 KiB
TypeScript
721 lines
21 KiB
TypeScript
import { DragControls, Line } from "@react-three/drei";
|
|
import React, {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import {
|
|
useActiveTool,
|
|
useCreatedPaths,
|
|
useToolMode,
|
|
} from "../../../../store/builder/store";
|
|
import { CubicBezierCurve3, Plane, Quaternion, Vector3 } from "three";
|
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors";
|
|
import { getPathsByPointId, setPathPosition } from "./function/getPaths";
|
|
import { aStar } from "../structuredPath/functions/aStar";
|
|
import {
|
|
useAnimationPlaySpeed,
|
|
usePlayButtonStore,
|
|
} from "../../../../store/usePlayButtonStore";
|
|
import { useSceneContext } from "../../../scene/sceneContext";
|
|
import { usePathManager } from "./function/usePathManager";
|
|
import { useProductContext } from "../../products/productContext";
|
|
import { useSelectedEventSphere } from "../../../../store/simulation/useSimulationStore";
|
|
type PointData = {
|
|
pointId: string;
|
|
position: [number, number, number];
|
|
isCurved?: boolean;
|
|
handleA?: [number, number, number] | null;
|
|
handleB?: [number, number, number] | null;
|
|
neighbors?: string[];
|
|
};
|
|
interface PathDataInterface {
|
|
pathId: string;
|
|
isActive?: boolean;
|
|
isCurved?: boolean;
|
|
pathPoints: [PointData, PointData];
|
|
}
|
|
|
|
type PathData = PathDataInterface[];
|
|
type PointHandlerProps = {
|
|
point: PointData;
|
|
hoveredPoint: PointData | null;
|
|
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
|
|
paths: PathDataInterface[];
|
|
setHoveredPoint: React.Dispatch<React.SetStateAction<PointData | null>>;
|
|
hoveredLine: PathDataInterface | null;
|
|
pointIndex: any;
|
|
points: PointData[];
|
|
selected: number[];
|
|
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
|
};
|
|
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
|
|
);
|
|
}
|
|
type SegmentPoint = {
|
|
position: Vector3;
|
|
originalPoint?: PointData;
|
|
pathId?: string;
|
|
startId?: string;
|
|
endId?: string;
|
|
};
|
|
|
|
/** --- 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;
|
|
}
|
|
type VehicleDetails = {
|
|
vehicleId: string;
|
|
vehiclePosition: [number, number, number];
|
|
};
|
|
type Manager = {
|
|
pathId: string;
|
|
vehicleId: string;
|
|
};
|
|
export default function PointHandler({
|
|
point,
|
|
// setPaths,
|
|
// paths,
|
|
setHoveredPoint,
|
|
hoveredLine,
|
|
hoveredPoint,
|
|
pointIndex,
|
|
points,
|
|
setSelected,
|
|
selected,
|
|
}: PointHandlerProps) {
|
|
const { isPlaying } = usePlayButtonStore();
|
|
const [multiPaths, setMultiPaths] = useState<
|
|
{ id: number; path: PathData }[]
|
|
>([]);
|
|
const { vehicleStore, productStore } = useSceneContext();
|
|
const { vehicles, getVehicleById } = vehicleStore();
|
|
const { selectedProductStore } = useProductContext();
|
|
const { selectedProduct } = selectedProductStore();
|
|
const { updateEvent, updateAction } = productStore();
|
|
const { selectedEventSphere } = useSelectedEventSphere();
|
|
const pathIdRef = useRef(1); // To ensure unique incremental IDs
|
|
const { toolMode } = useToolMode();
|
|
const { activeTool } = useActiveTool();
|
|
const { scene, raycaster } = useThree();
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const [dragOffset, setDragOffset] = useState<Vector3 | null>(null);
|
|
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
|
|
const [initialPositions, setInitialPositions] = useState<{
|
|
paths?: any;
|
|
}>({});
|
|
const [shortestPaths, setShortestPaths] = useState<PathData>([]);
|
|
const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
|
const [vehicleUuids, setVehicleUuids] = useState<any>();
|
|
const CAN_POINT_SNAP = true;
|
|
const CAN_ANGLE_SNAP = true;
|
|
const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5;
|
|
const [selectedPointIndices, setSelectedPointIndices] = useState<number[]>(
|
|
[]
|
|
);
|
|
const [shortestEdges, setShortestEdges] = useState<PathData>([]);
|
|
const { speed } = useAnimationPlaySpeed();
|
|
const { assetStore } = useSceneContext();
|
|
const { assets } = assetStore();
|
|
const vehicleMovementState = useRef<any>({});
|
|
const [activeVehicleIndex, setActiveVehicleIndex] = useState(0);
|
|
const [vehicleData, setVehicleData] = useState<VehicleDetails[]>([]);
|
|
const { paths, setPaths } = useCreatedPaths();
|
|
const [managerData, setManagerData] = useState<Manager>();
|
|
|
|
useEffect(() => {
|
|
const findVehicle = assets
|
|
.filter((val) => val.eventData?.type === "Vehicle")
|
|
?.map((val) => val.modelUuid);
|
|
const findVehicleDatas = assets
|
|
.filter((val) => val.eventData?.type === "Vehicle")
|
|
?.map((val) => val);
|
|
findVehicleDatas.forEach((val) => {
|
|
const vehicledId = val.modelUuid;
|
|
const vehiclePosition: [number, number, number] = val.position;
|
|
|
|
setVehicleData((prev) => [
|
|
...prev,
|
|
{ vehicleId: vehicledId, vehiclePosition },
|
|
]);
|
|
});
|
|
|
|
setVehicleUuids(findVehicle);
|
|
setActiveVehicleIndex(0); // Reset to first vehicle
|
|
|
|
vehicleMovementState.current = {};
|
|
findVehicle.forEach((uuid) => {
|
|
vehicleMovementState.current[uuid] = {
|
|
index: 0,
|
|
progress: 0,
|
|
hasStarted: false,
|
|
};
|
|
});
|
|
}, [assets]);
|
|
|
|
const removePathByPoint = (pointId: string): PathDataInterface[] => {
|
|
const removedPaths: PathDataInterface[] = [];
|
|
|
|
const newPaths = paths.filter((path: PathDataInterface) => {
|
|
const hasPoint = path.pathPoints.some(
|
|
(p: PointData) => p.pointId === pointId
|
|
);
|
|
if (hasPoint) {
|
|
removedPaths.push(JSON.parse(JSON.stringify(path))); // keep a copy
|
|
return false; // remove this path
|
|
}
|
|
return true; // keep this path
|
|
});
|
|
|
|
setPaths(newPaths);
|
|
|
|
return removedPaths;
|
|
};
|
|
|
|
const getConnectedPoints = (uuid: string): PointData[] => {
|
|
const connected: PointData[] = [];
|
|
|
|
for (const path of paths) {
|
|
for (const point of path.pathPoints) {
|
|
if (point.pointId === uuid) {
|
|
connected.push(
|
|
...path.pathPoints.filter((p: PointData) => p.pointId !== uuid)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return connected;
|
|
};
|
|
|
|
const snapPathAngle = useCallback(
|
|
(
|
|
newPosition: [number, number, number],
|
|
pointId: string
|
|
): {
|
|
position: [number, number, number];
|
|
isSnapped: boolean;
|
|
snapSources: Vector3[];
|
|
} => {
|
|
if (!pointId || !CAN_ANGLE_SNAP) {
|
|
return { position: newPosition, isSnapped: false, snapSources: [] };
|
|
}
|
|
|
|
const connectedPoints: PointData[] = getConnectedPoints(pointId) || [];
|
|
|
|
if (connectedPoints.length === 0) {
|
|
return {
|
|
position: newPosition,
|
|
isSnapped: false,
|
|
snapSources: [],
|
|
};
|
|
}
|
|
|
|
const newPos = new Vector3(...newPosition);
|
|
|
|
let closestX: { pos: Vector3; dist: number } | null = null;
|
|
let closestZ: { pos: Vector3; dist: number } | null = null;
|
|
|
|
for (const connectedPoint of connectedPoints) {
|
|
const cPos = new Vector3(...connectedPoint.position);
|
|
|
|
const xDist = Math.abs(newPos.x - cPos.x);
|
|
const zDist = Math.abs(newPos.z - cPos.z);
|
|
|
|
if (xDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
|
|
if (!closestX || xDist < closestX.dist) {
|
|
closestX = { pos: cPos, dist: xDist };
|
|
}
|
|
}
|
|
|
|
if (zDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
|
|
if (!closestZ || zDist < closestZ.dist) {
|
|
closestZ = { pos: cPos, dist: zDist };
|
|
}
|
|
}
|
|
}
|
|
|
|
const snappedPos = newPos.clone();
|
|
const snapSources: Vector3[] = [];
|
|
|
|
if (closestX) {
|
|
snappedPos.x = closestX.pos.x;
|
|
snapSources.push(closestX.pos.clone());
|
|
}
|
|
|
|
if (closestZ) {
|
|
snappedPos.z = closestZ.pos.z;
|
|
snapSources.push(closestZ.pos.clone());
|
|
}
|
|
|
|
const isSnapped = snapSources.length > 0;
|
|
|
|
return {
|
|
position: [snappedPos.x, snappedPos.y, snappedPos.z],
|
|
isSnapped,
|
|
snapSources,
|
|
};
|
|
},
|
|
[]
|
|
);
|
|
|
|
const getAllOtherPathPoints = useCallback((): PointData[] => {
|
|
return (
|
|
paths?.flatMap((path: PathDataInterface) =>
|
|
path.pathPoints.filter((pt: PointData) => pt.pointId !== point.pointId)
|
|
) ?? []
|
|
);
|
|
}, [paths]);
|
|
|
|
const snapPathPoint = useCallback(
|
|
(position: [number, number, number], pointId?: string) => {
|
|
if (!CAN_POINT_SNAP)
|
|
return { position: position, isSnapped: false, snappedPoint: null };
|
|
|
|
const otherPoints = getAllOtherPathPoints();
|
|
|
|
const currentVec = new Vector3(...position);
|
|
for (const point of otherPoints) {
|
|
const pointVec = new Vector3(...point.position);
|
|
const distance = currentVec.distanceTo(pointVec);
|
|
if (distance <= POINT_SNAP_THRESHOLD) {
|
|
return {
|
|
position: point.position,
|
|
isSnapped: true,
|
|
snappedPoint: point,
|
|
};
|
|
}
|
|
}
|
|
return { position: position, isSnapped: false, snappedPoint: null };
|
|
},
|
|
[getAllOtherPathPoints]
|
|
);
|
|
|
|
const handlePointClick = (e: any, point: PointData) => {
|
|
e.stopPropagation();
|
|
if (toolMode === "3D-Delete") {
|
|
removePathByPoint(point.pointId);
|
|
return;
|
|
}
|
|
};
|
|
let selectedVehiclePaths: Array<
|
|
Array<{
|
|
vehicleId: string;
|
|
pathId: string;
|
|
isActive?: boolean;
|
|
isCurved?: boolean;
|
|
pathPoints: [PointData, PointData];
|
|
}>
|
|
> = [];
|
|
|
|
function assignPathToSelectedVehicle(
|
|
selectedVehicleId: string,
|
|
currentPath: PathData
|
|
) {
|
|
const vehiclePathSegments = currentPath.map((path) => ({
|
|
vehicleId: selectedVehicleId,
|
|
...path,
|
|
}));
|
|
|
|
return selectedVehiclePaths.push(vehiclePathSegments);
|
|
}
|
|
|
|
const handleContextMenu = (e: any, point: PointData) => {
|
|
// if (e.shiftKey && e.button === 2) {
|
|
const pointIndex = points.findIndex((p) => p.pointId === point.pointId);
|
|
if (pointIndex === -1) {
|
|
return;
|
|
}
|
|
|
|
setSelected((prev) => {
|
|
if (prev.length === 0) {
|
|
return [pointIndex];
|
|
}
|
|
// if (prev.length === 1) {
|
|
// setTimeout(() => {
|
|
//
|
|
// const prevPoint = points[prev[0]];
|
|
//
|
|
// const newPoint = points[pointIndex];
|
|
//
|
|
// const result = aStarShortestPath(
|
|
// prevPoint.pointId,
|
|
// newPoint.pointId,
|
|
// points,
|
|
// paths
|
|
// );
|
|
|
|
// if (result) {
|
|
// const edges = nodePathToEdges(result.pointIds, points, paths);
|
|
//
|
|
// setShortestPaths(edges);
|
|
// setShortestEdges(edges);
|
|
// } else {
|
|
// setShortestPaths([]);
|
|
// setShortestEdges([]);
|
|
// }
|
|
// if (prevPoint.pointId === newPoint.pointId) {
|
|
// return prev;
|
|
// }
|
|
// }, 0);
|
|
|
|
// return [prev[0], pointIndex];
|
|
// }
|
|
|
|
// More than two points — reset
|
|
if (prev.length === 1) {
|
|
setTimeout(() => {
|
|
const prevPoint = points[prev[0]];
|
|
const newPoint = points[pointIndex];
|
|
console.log(
|
|
"selectedEventSphere?.userData.modelUuid: ",
|
|
selectedEventSphere?.userData.modelUuid
|
|
);
|
|
if (selectedEventSphere?.userData.modelUuid) {
|
|
const updatedVehicle = getVehicleById(
|
|
selectedEventSphere.userData.modelUuid
|
|
);
|
|
|
|
const startPoint = new Vector3(...prevPoint.position);
|
|
const endPoint = new Vector3(...newPoint.position);
|
|
if (updatedVehicle && startPoint && endPoint) {
|
|
if (updatedVehicle.type === "vehicle") {
|
|
const event = updateAction(
|
|
selectedProduct.productUuid,
|
|
updatedVehicle.point?.action.actionUuid,
|
|
{
|
|
pickUpPoint: {
|
|
position: {
|
|
x: startPoint.x,
|
|
y: 0,
|
|
z: startPoint.z,
|
|
},
|
|
rotation: {
|
|
x:
|
|
updatedVehicle.point.action.pickUpPoint?.rotation.x ??
|
|
0,
|
|
y:
|
|
updatedVehicle.point.action.pickUpPoint?.rotation.y ??
|
|
0,
|
|
z:
|
|
updatedVehicle.point.action.pickUpPoint?.rotation.z ??
|
|
0,
|
|
},
|
|
},
|
|
unLoadPoint: {
|
|
position: {
|
|
x: endPoint.x,
|
|
y: endPoint.y,
|
|
z: endPoint.z,
|
|
},
|
|
rotation: {
|
|
x:
|
|
updatedVehicle.point.action.unLoadPoint?.rotation.x ??
|
|
0,
|
|
y:
|
|
updatedVehicle.point.action.unLoadPoint?.rotation.y ??
|
|
0,
|
|
z:
|
|
updatedVehicle.point.action.unLoadPoint?.rotation.z ??
|
|
0,
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
if (prevPoint.pointId === newPoint.pointId) return;
|
|
|
|
const result = aStarShortestPath(
|
|
prevPoint.pointId,
|
|
newPoint.pointId,
|
|
points,
|
|
paths
|
|
);
|
|
|
|
if (result) {
|
|
const edges = nodePathToEdges(result.pointIds, points, paths);
|
|
|
|
// Create a new path object/
|
|
const newPathObj = {
|
|
id: pathIdRef.current++,
|
|
path: edges,
|
|
};
|
|
const shortPath = assignPathToSelectedVehicle(
|
|
updatedVehicle?.modelUuid,
|
|
edges
|
|
);
|
|
console.log("shortPath: ", shortPath);
|
|
|
|
setShortestPaths(edges);
|
|
setShortestEdges(edges);
|
|
// Append it to the list of paths
|
|
setMultiPaths((prevPaths) => [...prevPaths, newPathObj]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset selection to allow new pair selection
|
|
}, 0);
|
|
|
|
return [prev[0], pointIndex];
|
|
}
|
|
|
|
setShortestPaths([]);
|
|
return [pointIndex];
|
|
});
|
|
// }
|
|
};
|
|
|
|
const handleDragStart = (point: PointData) => {
|
|
if (activeTool !== "cursor") return;
|
|
const intersectionPoint = new Vector3();
|
|
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
|
if (hit) {
|
|
const currentPosition = new Vector3(...point.position);
|
|
const offset = new Vector3().subVectors(currentPosition, hit);
|
|
setDragOffset(offset);
|
|
const pathIntersection = getPathsByPointId(point.pointId, paths);
|
|
setInitialPositions({ paths: pathIntersection });
|
|
}
|
|
};
|
|
|
|
const handleDrag = (point: PointData) => {
|
|
if (isHovered && dragOffset) {
|
|
const intersectionPoint = new Vector3();
|
|
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
|
if (hit) {
|
|
// handleCanvasCursors("grabbing");
|
|
const positionWithOffset = new Vector3().addVectors(hit, dragOffset);
|
|
const newPosition: [number, number, number] = [
|
|
positionWithOffset.x,
|
|
positionWithOffset.y,
|
|
positionWithOffset.z,
|
|
];
|
|
|
|
// ✅ Pass newPosition and pointId
|
|
const pathSnapped = snapPathAngle(newPosition, point.pointId);
|
|
const finalSnapped = snapPathPoint(pathSnapped.position);
|
|
setPathPosition(point.pointId, finalSnapped.position, setPaths, paths);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleDragEnd = (point: PointData) => {
|
|
const pathIntersection = getPathsByPointId(point.pointId, paths);
|
|
if (pathIntersection && pathIntersection.length > 0) {
|
|
pathIntersection.forEach((update) => {});
|
|
}
|
|
};
|
|
|
|
const pathSegments = useMemo(() => {
|
|
if (!shortestPaths || shortestPaths.length === 0) return [];
|
|
|
|
const segments: SegmentPoint[] = [];
|
|
|
|
shortestPaths.forEach((path) => {
|
|
const [start, end] = path.pathPoints;
|
|
|
|
const startPos = new Vector3(...start.position);
|
|
const endPos = new Vector3(...end.position);
|
|
|
|
segments.push(
|
|
{ position: startPos, originalPoint: start, startId: start.pointId },
|
|
{ position: endPos, originalPoint: end, endId: end.pointId }
|
|
);
|
|
});
|
|
|
|
return segments.filter(
|
|
(v, i, arr) => i === 0 || !v.position.equals(arr[i - 1].position)
|
|
);
|
|
}, [shortestPaths]);
|
|
|
|
|
|
function getPathIdByPoints(
|
|
startId: string | undefined,
|
|
endId: string | undefined,
|
|
shortestPaths: any[]
|
|
) {
|
|
for (const path of shortestPaths) {
|
|
for (let i = 0; i < path.pathPoints.length - 1; i++) {
|
|
const s = path.pathPoints[i];
|
|
const e = path.pathPoints[i + 1];
|
|
|
|
if (
|
|
(s.pointId === startId && e.pointId === endId) ||
|
|
(s.pointId === endId && e.pointId === startId) // handle both directions
|
|
) {
|
|
return path.pathId;
|
|
}
|
|
}
|
|
}
|
|
return null; // not found
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<DragControls
|
|
axisLock="y"
|
|
autoTransform={false}
|
|
onDragStart={() => handleDragStart(point)}
|
|
onDrag={() => handleDrag(point)}
|
|
onDragEnd={() => handleDragEnd(point)}
|
|
>
|
|
<mesh
|
|
key={point.pointId}
|
|
position={point.position}
|
|
name="Path-Point"
|
|
userData={point}
|
|
onClick={(e) => {
|
|
handlePointClick(e, point);
|
|
}}
|
|
// onContextMenu={(e) => handleContextMenu(e, point)}
|
|
onPointerOver={(e) => {
|
|
if (!hoveredPoint && e.buttons === 0 && !e.ctrlKey) {
|
|
setHoveredPoint(point);
|
|
setIsHovered(true);
|
|
// handleCanvasCursors("default");
|
|
}
|
|
}}
|
|
onPointerOut={() => {
|
|
if (hoveredPoint) {
|
|
setHoveredPoint(null);
|
|
if (!hoveredLine) {
|
|
// handleCanvasCursors("default");
|
|
}
|
|
}
|
|
setIsHovered(false);
|
|
}}
|
|
>
|
|
<sphereGeometry args={[0.3, 16, 16]} />
|
|
<meshBasicMaterial color="pink" />
|
|
</mesh>
|
|
</DragControls>
|
|
{shortestEdges.map((edge) => (
|
|
<Line
|
|
key={`sp-${edge.pathId}`}
|
|
points={edge.pathPoints.map((p) => p.position)}
|
|
color="yellow"
|
|
lineWidth={3}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|