2025-08-25 15:25:48 +05:30
|
|
|
import { DragControls } from "@react-three/drei";
|
2025-08-25 16:21:54 +05:30
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
|
|
import { useActiveTool, useToolMode } from "../../../../store/builder/store";
|
2025-08-25 15:25:48 +05:30
|
|
|
import { Plane, Vector3 } from "three";
|
|
|
|
|
import { useThree } from "@react-three/fiber";
|
|
|
|
|
import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors";
|
|
|
|
|
import { getPathsByPointId, setPathPosition } from "./function/getPaths";
|
2025-08-25 16:21:54 +05:30
|
|
|
import { aStar } from "../structuredPath/functions/aStar";
|
2025-08-25 15:25:48 +05:30
|
|
|
type PointData = {
|
|
|
|
|
pointId: string;
|
|
|
|
|
position: [number, number, number];
|
|
|
|
|
isCurved?: boolean;
|
|
|
|
|
handleA?: [number, number, number] | null;
|
|
|
|
|
handleB?: [number, number, number] | null;
|
|
|
|
|
};
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
export default function PointHandler({
|
|
|
|
|
point,
|
|
|
|
|
setPaths,
|
|
|
|
|
paths,
|
|
|
|
|
setHoveredPoint,
|
|
|
|
|
hoveredLine,
|
|
|
|
|
hoveredPoint,
|
|
|
|
|
}: PointHandlerProps) {
|
|
|
|
|
const { toolMode } = useToolMode();
|
2025-08-25 16:21:54 +05:30
|
|
|
const { activeTool } = useActiveTool();
|
2025-08-25 15:25:48 +05:30
|
|
|
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;
|
|
|
|
|
}>({});
|
2025-08-25 16:21:54 +05:30
|
|
|
const [selectedPoints, setSelectedPoints] = useState<PointData[]>([]);
|
|
|
|
|
|
2025-08-25 15:25:48 +05:30
|
|
|
const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
|
|
|
|
|
|
|
|
|
const CAN_POINT_SNAP = true;
|
|
|
|
|
const CAN_ANGLE_SNAP = true;
|
|
|
|
|
const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5;
|
2025-08-25 16:21:54 +05:30
|
|
|
|
2025-08-25 15:25:48 +05:30
|
|
|
const removePathByPoint = (pointId: string): PathDataInterface[] => {
|
|
|
|
|
const removedPaths: PathDataInterface[] = [];
|
|
|
|
|
|
|
|
|
|
setPaths((prevPaths) =>
|
|
|
|
|
prevPaths.filter((path) => {
|
|
|
|
|
const hasPoint = path.pathPoints.some((p) => 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
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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) =>
|
|
|
|
|
path.pathPoints.filter((pt) => 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]
|
|
|
|
|
);
|
2025-08-25 16:21:54 +05:30
|
|
|
useEffect(() => {
|
|
|
|
|
console.log("selectedPoints: ", selectedPoints);
|
|
|
|
|
}, [selectedPoints]);
|
2025-08-25 15:25:48 +05:30
|
|
|
// 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));
|
|
|
|
|
// };
|
|
|
|
|
|
2025-08-25 16:21:54 +05:30
|
|
|
const handlePointClick = (e: any, pointId: string) => {
|
2025-08-25 15:25:48 +05:30
|
|
|
if (toolMode === "3D-Delete") {
|
|
|
|
|
removePathByPoint(pointId);
|
2025-08-25 16:21:54 +05:30
|
|
|
} else if (e.ctrlKey) {
|
2025-08-25 15:25:48 +05:30
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const handleDragStart = (point: PointData) => {
|
2025-08-25 16:21:54 +05:30
|
|
|
if (activeTool !== "cursor") return;
|
2025-08-25 15:25:48 +05:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragEnd = (point: PointData) => {
|
|
|
|
|
const pathIntersection = getPathsByPointId(point.pointId, paths);
|
|
|
|
|
if (pathIntersection && pathIntersection.length > 0) {
|
|
|
|
|
pathIntersection.forEach((update) => {});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
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) => {
|
|
|
|
|
e.stopPropagation();
|
2025-08-25 16:21:54 +05:30
|
|
|
handlePointClick(e, point.pointId);
|
2025-08-25 15:25:48 +05:30
|
|
|
}}
|
|
|
|
|
onPointerOver={(e) => {
|
|
|
|
|
if (!hoveredPoint && e.buttons === 0 && !e.ctrlKey) {
|
|
|
|
|
setHoveredPoint(point);
|
|
|
|
|
setIsHovered(true);
|
2025-08-25 16:21:54 +05:30
|
|
|
// handleCanvasCursors("default");
|
2025-08-25 15:25:48 +05:30
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onPointerOut={() => {
|
|
|
|
|
if (hoveredPoint) {
|
|
|
|
|
setHoveredPoint(null);
|
|
|
|
|
if (!hoveredLine) {
|
|
|
|
|
// handleCanvasCursors("default");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setIsHovered(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<sphereGeometry args={[0.3, 16, 16]} />
|
|
|
|
|
<meshBasicMaterial color="pink" />
|
|
|
|
|
</mesh>
|
|
|
|
|
</DragControls>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|