Files
Dwinzo_Demo/app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx

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}
/>
))}
</>
);
}