Update Path Drawing Component to Match Wall Drawing Component Behavior
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
export const getPathPointByPoints = (point: any, paths: any) => {
|
||||||
|
for (const path of paths) {
|
||||||
|
if (
|
||||||
|
(path.pathPoints[0].pointId === point[0].pointId ||
|
||||||
|
path.pathPoints[1].pointId === point[0].pointId) &&
|
||||||
|
(path.pathPoints[0].pointId === point[1].pointId ||
|
||||||
|
path.pathPoints[1].pointId === point[1].pointId)
|
||||||
|
) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
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 setPathPositionProps = (
|
||||||
|
pointUuid: string,
|
||||||
|
position: [number, number, number],
|
||||||
|
setPaths: React.Dispatch<React.SetStateAction<PathData>>
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export const getPathsByPointId = (pointId: any, paths: PathData) => {
|
||||||
|
return paths.filter((a) => a.pathPoints.some((p) => p.pointId === pointId));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setPathPosition = (
|
||||||
|
pointUuid: string,
|
||||||
|
position: [number, number, number],
|
||||||
|
setPaths: React.Dispatch<React.SetStateAction<PathData>>
|
||||||
|
) => {
|
||||||
|
setPaths((prevPaths) =>
|
||||||
|
prevPaths.map((path: any) => {
|
||||||
|
if (path?.pathPoints.some((p: any) => p.pointId === pointUuid)) {
|
||||||
|
return {
|
||||||
|
...path,
|
||||||
|
pathPoints: path.pathPoints.map((p: any) =>
|
||||||
|
p.pointId === pointUuid ? { ...p, position } : p
|
||||||
|
) as [PointData, PointData], // 👈 force back to tuple
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,253 +1,4 @@
|
|||||||
// import { Line } from "@react-three/drei";
|
import { DragControls, Line } from "@react-three/drei";
|
||||||
// import { useThree } from "@react-three/fiber";
|
|
||||||
// import React, { useEffect, useMemo, useState } from "react";
|
|
||||||
// import { MathUtils, Plane, Vector3 } from "three";
|
|
||||||
// import { Vector3Array } from "../../../../types/world/worldTypes";
|
|
||||||
|
|
||||||
// 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[];
|
|
||||||
|
|
||||||
// export default function PathCreator() {
|
|
||||||
// const [paths, setPaths] = useState<PathData>([]);
|
|
||||||
// const [draftPoints, setDraftPoints] = useState<PointData[]>([]);
|
|
||||||
// const [mousePos, setMousePos] = useState<[number, number, number] | null>(
|
|
||||||
// null
|
|
||||||
// ); // 👈 track mouse for dashed line
|
|
||||||
|
|
||||||
// const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
|
|
||||||
// const { scene, raycaster, gl } = useThree();
|
|
||||||
// const [snappedPosition, setSnappedPosition] = useState<any>();
|
|
||||||
// // --- Helpers ---
|
|
||||||
// function getClosestIntersection(
|
|
||||||
// intersects: Vector3Array,
|
|
||||||
// point: Vector3
|
|
||||||
// ): Vector3 {
|
|
||||||
// let closestNewPoint: Vector3 = point;
|
|
||||||
// let minDistance = Infinity;
|
|
||||||
// for (const intersect of intersects) {
|
|
||||||
// const distance = point.distanceTo(intersect);
|
|
||||||
// if (distance < minDistance) {
|
|
||||||
// minDistance = distance;
|
|
||||||
// closestNewPoint = intersect;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return closestNewPoint;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const handleClick = () => {
|
|
||||||
// const intersectionPoint = new Vector3();
|
|
||||||
// let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
|
||||||
// if (!position) return;
|
|
||||||
|
|
||||||
// const pointIntersect = raycaster
|
|
||||||
// .intersectObjects(scene.children)
|
|
||||||
// .find((intersect) => intersect.object.name === "Path-Point");
|
|
||||||
|
|
||||||
// const pathIntersect = raycaster
|
|
||||||
// .intersectObjects(scene.children)
|
|
||||||
// .find((intersect) => intersect.object.name === "Path-Line");
|
|
||||||
|
|
||||||
// // ✅ Case 1: Split an existing path
|
|
||||||
// if (!pointIntersect && pathIntersect) {
|
|
||||||
// const hitLine = pathIntersect.object;
|
|
||||||
// const hitPathId = hitLine.userData.pathId;
|
|
||||||
|
|
||||||
// const hitPath = paths.find((p) => p.pathId === hitPathId);
|
|
||||||
// if (!hitPath) return;
|
|
||||||
|
|
||||||
// const [p1, p2] = hitPath.pathPoints;
|
|
||||||
|
|
||||||
// const point1Vec = new Vector3(...p1.position);
|
|
||||||
// const point2Vec = new Vector3(...p2.position);
|
|
||||||
|
|
||||||
// const lineDir = new Vector3()
|
|
||||||
// .subVectors(point2Vec, point1Vec)
|
|
||||||
// .normalize();
|
|
||||||
// const point1ToClick = new Vector3().subVectors(
|
|
||||||
// pathIntersect.point,
|
|
||||||
// point1Vec
|
|
||||||
// );
|
|
||||||
// const dot = point1ToClick.dot(lineDir);
|
|
||||||
|
|
||||||
// const projection = new Vector3()
|
|
||||||
// .copy(lineDir)
|
|
||||||
// .multiplyScalar(dot)
|
|
||||||
// .add(point1Vec);
|
|
||||||
|
|
||||||
// const lineLength = point1Vec.distanceTo(point2Vec);
|
|
||||||
// let t = point1Vec.distanceTo(projection) / lineLength;
|
|
||||||
// t = Math.max(0, Math.min(1, t));
|
|
||||||
|
|
||||||
// const closestPoint = new Vector3().lerpVectors(point1Vec, point2Vec, t);
|
|
||||||
|
|
||||||
// const newPoint: PointData = {
|
|
||||||
// pointId: MathUtils.generateUUID(),
|
|
||||||
// position: closestPoint.toArray() as [number, number, number],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const newPath1: PathDataInterface = {
|
|
||||||
// pathId: MathUtils.generateUUID(),
|
|
||||||
// pathPoints: [p1, newPoint],
|
|
||||||
// };
|
|
||||||
// const newPath2: PathDataInterface = {
|
|
||||||
// pathId: MathUtils.generateUUID(),
|
|
||||||
// pathPoints: [newPoint, p2],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// setPaths((prev) =>
|
|
||||||
// prev.filter((p) => p.pathId !== hitPathId).concat([newPath1, newPath2])
|
|
||||||
// );
|
|
||||||
|
|
||||||
// setDraftPoints([newPoint]);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// // if (snappedPosition && snappedPoint) {
|
|
||||||
// // newPoint.pointUuid = snappedPoint.pointUuid;
|
|
||||||
// // newPoint.position = snappedPosition;
|
|
||||||
// // newPoint.layer = snappedPoint.layer;
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // if (snappedPoint && snappedPoint.pointUuid === tempPoints[0]?.pointUuid) {
|
|
||||||
// // return;
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // if (snappedPosition && !snappedPoint) {
|
|
||||||
// // newPoint.position = snappedPosition;
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // if (pointIntersects && !snappedPoint) {
|
|
||||||
// // const point = getWallPointById(pointIntersects.object.uuid);
|
|
||||||
// // if (point) {
|
|
||||||
// // newPoint.pointUuid = point.pointUuid;
|
|
||||||
// // newPoint.position = point.position;
|
|
||||||
// // newPoint.layer = point.layer;
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // ✅ Case 2: Normal new path creation
|
|
||||||
// const newPoint: PointData = {
|
|
||||||
// pointId: MathUtils.generateUUID(),
|
|
||||||
// position: [position.x, position.y, position.z],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (draftPoints.length === 0) {
|
|
||||||
// setDraftPoints([newPoint]);
|
|
||||||
// } else {
|
|
||||||
// const newPath: PathDataInterface = {
|
|
||||||
// pathId: MathUtils.generateUUID(),
|
|
||||||
// pathPoints: [draftPoints[0], newPoint],
|
|
||||||
// };
|
|
||||||
// setPaths((prev) => [...prev, newPath]);
|
|
||||||
// setDraftPoints([newPoint]);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleContextMenu = (event: any) => {
|
|
||||||
// event.preventDefault();
|
|
||||||
// setDraftPoints([]);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // 👇 track mouse move for preview line
|
|
||||||
// const handleMouseMove = () => {
|
|
||||||
// const intersectionPoint = new Vector3();
|
|
||||||
// let pos = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
|
||||||
// if (pos) {
|
|
||||||
// setMousePos([pos.x, pos.y, pos.z]);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const canvasElement = gl.domElement;
|
|
||||||
|
|
||||||
// canvasElement.addEventListener("click", handleClick);
|
|
||||||
// canvasElement.addEventListener("mousemove", handleMouseMove);
|
|
||||||
// canvasElement.addEventListener("contextmenu", handleContextMenu);
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// canvasElement.removeEventListener("click", handleClick);
|
|
||||||
// canvasElement.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
// canvasElement.removeEventListener("contextmenu", handleContextMenu);
|
|
||||||
// };
|
|
||||||
// }, [gl, draftPoints, paths]);
|
|
||||||
|
|
||||||
// const allPoints = useMemo(() => {
|
|
||||||
// const points: PointData[] = [];
|
|
||||||
// const seenUuids = new Set<string>();
|
|
||||||
// paths?.forEach((wall) => {
|
|
||||||
// wall.pathPoints.forEach((point) => {
|
|
||||||
// if (!seenUuids.has(point.pointId)) {
|
|
||||||
// seenUuids.add(point.pointId);
|
|
||||||
// points.push(point);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// return points;
|
|
||||||
// }, [paths]);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// {/* Draft points (red) */}
|
|
||||||
// {draftPoints.map((point) => (
|
|
||||||
// <mesh key={point.pointId} position={point.position}>
|
|
||||||
// <sphereGeometry args={[0.2, 16, 16]} />
|
|
||||||
// <meshBasicMaterial color="red" />
|
|
||||||
// </mesh>
|
|
||||||
// ))}
|
|
||||||
|
|
||||||
// {/* All saved points */}
|
|
||||||
// {allPoints.map((point) => (
|
|
||||||
// <mesh
|
|
||||||
// key={point.pointId}
|
|
||||||
// position={point.position}
|
|
||||||
// name="Path-Point"
|
|
||||||
// userData={point}
|
|
||||||
// >
|
|
||||||
// <sphereGeometry args={[0.2, 16, 16]} />
|
|
||||||
// <meshBasicMaterial color="pink" />
|
|
||||||
// </mesh>
|
|
||||||
// ))}
|
|
||||||
|
|
||||||
// {/* 👇 Dashed preview line while drawing */}
|
|
||||||
// {draftPoints.length > 0 && mousePos && (
|
|
||||||
// <Line
|
|
||||||
// points={[draftPoints[0].position, mousePos]}
|
|
||||||
// color="orange"
|
|
||||||
// lineWidth={2}
|
|
||||||
// dashed
|
|
||||||
// />
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// {/* Permanent paths */}
|
|
||||||
// {paths.map((points) => (
|
|
||||||
// <Line
|
|
||||||
// name="Path-Line"
|
|
||||||
// key={points.pathId}
|
|
||||||
// points={[
|
|
||||||
// points.pathPoints[0].position,
|
|
||||||
// points.pathPoints[1].position,
|
|
||||||
// ]}
|
|
||||||
// color="purple"
|
|
||||||
// lineWidth={2}
|
|
||||||
// userData={points}
|
|
||||||
// />
|
|
||||||
// ))}
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
import { Line } from "@react-three/drei";
|
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -258,6 +9,10 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { LineCurve3, MathUtils, Plane, Vector3 } from "three";
|
import { LineCurve3, MathUtils, Plane, Vector3 } from "three";
|
||||||
import { Vector3Array } from "../../../../types/world/worldTypes";
|
import { Vector3Array } from "../../../../types/world/worldTypes";
|
||||||
|
import { useActiveSubTool, useToolMode } from "../../../../store/builder/store";
|
||||||
|
import PointHandler from "./pointHandler";
|
||||||
|
import { getPathPointByPoints } from "./function/getPathPointByPoints";
|
||||||
|
import PathHandler from "./pathHandler";
|
||||||
|
|
||||||
type PointData = {
|
type PointData = {
|
||||||
pointId: string;
|
pointId: string;
|
||||||
@@ -278,11 +33,11 @@ type PathData = PathDataInterface[];
|
|||||||
|
|
||||||
export default function PathCreator() {
|
export default function PathCreator() {
|
||||||
const [paths, setPaths] = useState<PathData>([]);
|
const [paths, setPaths] = useState<PathData>([]);
|
||||||
|
|
||||||
const [draftPoints, setDraftPoints] = useState<PointData[]>([]);
|
const [draftPoints, setDraftPoints] = useState<PointData[]>([]);
|
||||||
const [mousePos, setMousePos] = useState<[number, number, number] | null>(
|
const [mousePos, setMousePos] = useState<[number, number, number] | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const { toolMode } = useToolMode();
|
||||||
|
|
||||||
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
|
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
|
||||||
const { scene, raycaster, gl } = useThree();
|
const { scene, raycaster, gl } = useThree();
|
||||||
@@ -291,11 +46,17 @@ export default function PathCreator() {
|
|||||||
>(null);
|
>(null);
|
||||||
const [snappedPoint, setSnappedPoint] = useState<PointData | null>(null);
|
const [snappedPoint, setSnappedPoint] = useState<PointData | null>(null);
|
||||||
const finalPosition = useRef<[number, number, number] | null>(null);
|
const finalPosition = useRef<[number, number, number] | null>(null);
|
||||||
const [tempPath, setTempPath] = useState<PathDataInterface>();
|
const [hoveredLine, setHoveredLine] = useState<PathDataInterface | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [hoveredPoint, setHoveredPoint] = useState<PointData | null>(null);
|
||||||
|
|
||||||
|
const { activeSubTool } = useActiveSubTool();
|
||||||
const POINT_SNAP_THRESHOLD = 0.5;
|
const POINT_SNAP_THRESHOLD = 0.5;
|
||||||
const CAN_POINT_SNAP = true;
|
const CAN_POINT_SNAP = true;
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
}, [paths]);
|
||||||
const getAllOtherPathPoints = useCallback((): PointData[] => {
|
const getAllOtherPathPoints = useCallback((): PointData[] => {
|
||||||
if (draftPoints.length === 0) return [];
|
if (draftPoints.length === 0) return [];
|
||||||
return (
|
return (
|
||||||
@@ -366,108 +127,10 @@ export default function PathCreator() {
|
|||||||
draftPoints[0],
|
draftPoints[0],
|
||||||
{ pointId: "temp-point", position: finalPosition.current },
|
{ pointId: "temp-point", position: finalPosition.current },
|
||||||
];
|
];
|
||||||
setTempPath({
|
|
||||||
pathId: "temp-path",
|
|
||||||
pathPoints: paths,
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ Click handler
|
|
||||||
// const handleClick = () => {
|
|
||||||
// const intersectionPoint = new Vector3();
|
|
||||||
// const pos = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
|
||||||
// if (!pos) return;
|
|
||||||
|
|
||||||
// const pointIntersect = raycaster
|
|
||||||
// .intersectObjects(scene.children)
|
|
||||||
// .find((intersect) => intersect.object.name === "Path-Point");
|
|
||||||
|
|
||||||
// const pathIntersect = raycaster
|
|
||||||
// .intersectObjects(scene.children)
|
|
||||||
// .find((intersect) => intersect.object.name === "Path-Line");
|
|
||||||
|
|
||||||
// // Case 1: Split path
|
|
||||||
// if (!pointIntersect && pathIntersect) {
|
|
||||||
// const hitLine = pathIntersect.object;
|
|
||||||
// const hitPathId = hitLine.userData.pathId;
|
|
||||||
// const hitPath = paths.find((p) => p.pathId === hitPathId);
|
|
||||||
// if (!hitPath) return;
|
|
||||||
|
|
||||||
// const [p1, p2] = hitPath.pathPoints;
|
|
||||||
// const point1Vec = new Vector3(...p1.position);
|
|
||||||
// const point2Vec = new Vector3(...p2.position);
|
|
||||||
|
|
||||||
// const lineDir = new Vector3()
|
|
||||||
// .subVectors(point2Vec, point1Vec)
|
|
||||||
// .normalize();
|
|
||||||
// const point1ToClick = new Vector3().subVectors(
|
|
||||||
// pathIntersect.point,
|
|
||||||
// point1Vec
|
|
||||||
// );
|
|
||||||
// const dot = point1ToClick.dot(lineDir);
|
|
||||||
|
|
||||||
// const projection = new Vector3()
|
|
||||||
// .copy(lineDir)
|
|
||||||
// .multiplyScalar(dot)
|
|
||||||
// .add(point1Vec);
|
|
||||||
|
|
||||||
// const lineLength = point1Vec.distanceTo(point2Vec);
|
|
||||||
// let t = point1Vec.distanceTo(projection) / lineLength;
|
|
||||||
// t = Math.max(0, Math.min(1, t));
|
|
||||||
|
|
||||||
// const closestPoint = new Vector3().lerpVectors(point1Vec, point2Vec, t);
|
|
||||||
|
|
||||||
// const newPoint: PointData = {
|
|
||||||
// pointId: MathUtils.generateUUID(),
|
|
||||||
// position: closestPoint.toArray() as [number, number, number],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const newPath1: PathDataInterface = {
|
|
||||||
// pathId: MathUtils.generateUUID(),
|
|
||||||
// pathPoints: [p1, newPoint],
|
|
||||||
// };
|
|
||||||
// const newPath2: PathDataInterface = {
|
|
||||||
// pathId: MathUtils.generateUUID(),
|
|
||||||
// pathPoints: [newPoint, p2],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// setPaths((prev) =>
|
|
||||||
// prev.filter((p) => p.pathId !== hitPathId).concat([newPath1, newPath2])
|
|
||||||
// );
|
|
||||||
|
|
||||||
// setDraftPoints([newPoint]);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Case 2: Normal path creation
|
|
||||||
// const newPoint: PointData = {
|
|
||||||
// pointId: MathUtils.generateUUID(),
|
|
||||||
// position: [pos.x, pos.y, pos.z],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (snappedPosition) {
|
|
||||||
// newPoint.position = snappedPosition;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (snappedPoint && draftPoints[0]?.pointId === snappedPoint.pointId) {
|
|
||||||
// return; // prevent connecting point to itself
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (draftPoints.length === 0) {
|
|
||||||
// setDraftPoints([newPoint]);
|
|
||||||
// } else {
|
|
||||||
// const newPath: PathDataInterface = {
|
|
||||||
// pathId: MathUtils.generateUUID(),
|
|
||||||
// pathPoints: [draftPoints[0], newPoint],
|
|
||||||
// };
|
|
||||||
// setPaths((prev) => [...prev, newPath]);
|
|
||||||
// setDraftPoints([newPoint]);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// ✅ Click handler
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("paths", paths);
|
|
||||||
}, [paths]);
|
}, [paths]);
|
||||||
const getPathPointById = (uuid: any) => {
|
const getPathPointById = (uuid: any) => {
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
@@ -480,23 +143,11 @@ export default function PathCreator() {
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
const getPathPointByPoints = (point: any) => {
|
|
||||||
for (const path of paths) {
|
|
||||||
if (
|
|
||||||
(path.pathPoints[0].pointId === point[0].pointId ||
|
|
||||||
path.pathPoints[1].pointId === point[0].pointId) &&
|
|
||||||
(path.pathPoints[0].pointId === point[1].pointId ||
|
|
||||||
path.pathPoints[1].pointId === point[1].pointId)
|
|
||||||
) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("draftPoints", draftPoints);
|
|
||||||
}, [draftPoints]);
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
|
if (activeSubTool === "free-hand") return;
|
||||||
|
if (toolMode === "3D-Delete") return;
|
||||||
|
|
||||||
const intersectionPoint = new Vector3();
|
const intersectionPoint = new Vector3();
|
||||||
const pos = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
const pos = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||||
if (!pos) return;
|
if (!pos) return;
|
||||||
@@ -504,6 +155,8 @@ export default function PathCreator() {
|
|||||||
.intersectObjects(scene.children)
|
.intersectObjects(scene.children)
|
||||||
.find((intersect) => intersect.object.name === "Path-Point");
|
.find((intersect) => intersect.object.name === "Path-Point");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const pathIntersect = raycaster
|
const pathIntersect = raycaster
|
||||||
.intersectObjects(scene.children)
|
.intersectObjects(scene.children)
|
||||||
.find((intersect) => intersect.object.name === "Path-Line");
|
.find((intersect) => intersect.object.name === "Path-Line");
|
||||||
@@ -511,9 +164,13 @@ export default function PathCreator() {
|
|||||||
// --- Case 1: Split path ---
|
// --- Case 1: Split path ---
|
||||||
if (!pointIntersect && pathIntersect) {
|
if (!pointIntersect && pathIntersect) {
|
||||||
const hitLine = pathIntersect.object;
|
const hitLine = pathIntersect.object;
|
||||||
const clickedPath = getPathPointByPoints(hitLine.userData.pathPoints);
|
const clickedPath = getPathPointByPoints(
|
||||||
|
hitLine.userData.pathPoints,
|
||||||
|
paths
|
||||||
|
);
|
||||||
if (clickedPath) {
|
if (clickedPath) {
|
||||||
const hitPath = paths.find((p) => p.pathId === clickedPath.pathId);
|
const hitPath = paths.find((p) => p.pathId === clickedPath.pathId);
|
||||||
|
|
||||||
if (!hitPath) return;
|
if (!hitPath) return;
|
||||||
|
|
||||||
const [p1, p2] = clickedPath.pathPoints;
|
const [p1, p2] = clickedPath.pathPoints;
|
||||||
@@ -565,7 +222,6 @@ export default function PathCreator() {
|
|||||||
};
|
};
|
||||||
setDraftPoints([splitPoint]);
|
setDraftPoints([splitPoint]);
|
||||||
setPaths((prev) => [...prev, path1, path2]);
|
setPaths((prev) => [...prev, path1, path2]);
|
||||||
console.log("draftPoints:if ", path1, path2, draftPoints);
|
|
||||||
} else {
|
} else {
|
||||||
const newPath: PathDataInterface = {
|
const newPath: PathDataInterface = {
|
||||||
pathId: MathUtils.generateUUID(),
|
pathId: MathUtils.generateUUID(),
|
||||||
@@ -580,7 +236,6 @@ export default function PathCreator() {
|
|||||||
pathPoints: [point2, splitPoint],
|
pathPoints: [point2, splitPoint],
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("else ", newPath, firstPath, secondPath, draftPoints);
|
|
||||||
setPaths((prev) => [...prev, newPath, firstPath, secondPath]);
|
setPaths((prev) => [...prev, newPath, firstPath, secondPath]);
|
||||||
setDraftPoints([splitPoint]);
|
setDraftPoints([splitPoint]);
|
||||||
}
|
}
|
||||||
@@ -610,12 +265,11 @@ export default function PathCreator() {
|
|||||||
if (snappedPosition && !snappedPoint) {
|
if (snappedPosition && !snappedPoint) {
|
||||||
newPoint.position = snappedPosition;
|
newPoint.position = snappedPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pointIntersect && !snappedPoint) {
|
if (pointIntersect && !snappedPoint) {
|
||||||
const point = getPathPointById(pointIntersect.object.userData.pointId);
|
const point = getPathPointById(pointIntersect.object.userData.pointId);
|
||||||
console.log("point: ", point);
|
|
||||||
|
|
||||||
if (point) {
|
if (point) {
|
||||||
console.log("newPoint: ", newPoint);
|
|
||||||
newPoint.pointId = point.pointId;
|
newPoint.pointId = point.pointId;
|
||||||
newPoint.position = point.position;
|
newPoint.position = point.position;
|
||||||
}
|
}
|
||||||
@@ -658,7 +312,7 @@ export default function PathCreator() {
|
|||||||
canvasElement.removeEventListener("mousemove", handleMouseMove);
|
canvasElement.removeEventListener("mousemove", handleMouseMove);
|
||||||
canvasElement.removeEventListener("contextmenu", handleContextMenu);
|
canvasElement.removeEventListener("contextmenu", handleContextMenu);
|
||||||
};
|
};
|
||||||
}, [gl, draftPoints, paths]);
|
}, [gl, draftPoints, paths, toolMode]);
|
||||||
|
|
||||||
const allPoints = useMemo(() => {
|
const allPoints = useMemo(() => {
|
||||||
const points: PointData[] = [];
|
const points: PointData[] = [];
|
||||||
@@ -674,7 +328,6 @@ export default function PathCreator() {
|
|||||||
return points;
|
return points;
|
||||||
}, [paths]);
|
}, [paths]);
|
||||||
|
|
||||||
useEffect(() => {}, [paths]);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Draft points (red) */}
|
{/* Draft points (red) */}
|
||||||
@@ -687,19 +340,17 @@ export default function PathCreator() {
|
|||||||
|
|
||||||
{/* Saved points */}
|
{/* Saved points */}
|
||||||
{allPoints.map((point) => (
|
{allPoints.map((point) => (
|
||||||
<mesh
|
<PointHandler
|
||||||
key={point.pointId}
|
key={point.pointId}
|
||||||
position={point.position}
|
point={point}
|
||||||
name="Path-Point"
|
setPaths={setPaths}
|
||||||
userData={point}
|
paths={paths}
|
||||||
>
|
setHoveredPoint={setHoveredPoint}
|
||||||
<sphereGeometry args={[0.2, 16, 16]} />
|
hoveredLine={hoveredLine}
|
||||||
<meshBasicMaterial color="pink" />
|
hoveredPoint={hoveredPoint}
|
||||||
</mesh>
|
/>
|
||||||
))}
|
))}
|
||||||
{/* {tempPath && draftPoints.length > 0 && (
|
|
||||||
<ReferenceLine points={tempPath?.pathPoints} />
|
|
||||||
)} */}
|
|
||||||
{/* Preview line */}
|
{/* Preview line */}
|
||||||
{draftPoints.length > 0 && mousePos && (
|
{draftPoints.length > 0 && mousePos && (
|
||||||
<Line
|
<Line
|
||||||
@@ -712,378 +363,17 @@ export default function PathCreator() {
|
|||||||
|
|
||||||
{/* Permanent paths */}
|
{/* Permanent paths */}
|
||||||
{paths.map((path) => (
|
{paths.map((path) => (
|
||||||
<Line
|
<PathHandler
|
||||||
name="Path-Line"
|
|
||||||
key={path.pathId}
|
key={path.pathId}
|
||||||
points={[path.pathPoints[0].position, path.pathPoints[1].position]}
|
selectedPath={path}
|
||||||
color="purple"
|
setPaths={setPaths}
|
||||||
lineWidth={2}
|
paths={paths}
|
||||||
userData={path}
|
points={path.pathPoints}
|
||||||
|
setHoveredLine={setHoveredLine}
|
||||||
|
hoveredLine={hoveredLine}
|
||||||
|
hoveredPoint={hoveredPoint}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReferenceLine({ points }: any) {
|
|
||||||
const path = useMemo(() => {
|
|
||||||
const [start, end] = points?.map((p: any) => new Vector3(...p.position));
|
|
||||||
return new LineCurve3(start, end);
|
|
||||||
}, [points]);
|
|
||||||
|
|
||||||
const linePoints = useMemo(() => path.getPoints(1), [path]);
|
|
||||||
|
|
||||||
return <Line points={linePoints} color="orange" lineWidth={2} dashed />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// import { Line } from "@react-three/drei";
|
|
||||||
// import { useFrame, useThree } from "@react-three/fiber";
|
|
||||||
// import React, {
|
|
||||||
// useCallback,
|
|
||||||
// useEffect,
|
|
||||||
// useMemo,
|
|
||||||
// useRef,
|
|
||||||
// useState,
|
|
||||||
// } from "react";
|
|
||||||
// import { MathUtils, Plane, Vector3 } from "three";
|
|
||||||
// import { Vector3Array } from "../../../../types/world/worldTypes";
|
|
||||||
|
|
||||||
// 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 SnapObject = {
|
|
||||||
// pointId: string;
|
|
||||||
// position: [number, number, number];
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export default function PathCreator() {
|
|
||||||
// const [paths, setPaths] = useState<PathData>([]);
|
|
||||||
//
|
|
||||||
// const [draftPoints, setDraftPoints] = useState<PointData[]>([]);
|
|
||||||
// const [mousePos, setMousePos] = useState<[number, number, number] | null>(
|
|
||||||
// null
|
|
||||||
// ); // 👈 track mouse for dashed line
|
|
||||||
|
|
||||||
// const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
|
|
||||||
// const { scene, raycaster, gl } = useThree();
|
|
||||||
// const [snappedPosition, setSnappedPosition] = useState<
|
|
||||||
// [number, number, number] | null
|
|
||||||
// >(null);
|
|
||||||
// const [snappedPoint, setSnappedPoint] = useState<PointData | null>(null);
|
|
||||||
// const [tempWall, setTempWall] = useState<PathData | null>(null);
|
|
||||||
// const finalPosition = useRef<[number, number, number] | null>(null);
|
|
||||||
|
|
||||||
// const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
|
||||||
|
|
||||||
// const CAN_POINT_SNAP = true; // Whether snapping is enabled or not
|
|
||||||
|
|
||||||
// const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
|
||||||
|
|
||||||
// const CAN_ANGLE_SNAP = true; // Whether snapping is enabled or not
|
|
||||||
// // --- Helpers ---
|
|
||||||
// function getClosestIntersection(
|
|
||||||
// intersects: Vector3Array,
|
|
||||||
// point: Vector3
|
|
||||||
// ): Vector3 {
|
|
||||||
// let closestNewPoint: Vector3 = point;
|
|
||||||
// let minDistance = Infinity;
|
|
||||||
// for (const intersect of intersects) {
|
|
||||||
// const distance = point.distanceTo(intersect);
|
|
||||||
// if (distance < minDistance) {
|
|
||||||
// minDistance = distance;
|
|
||||||
// closestNewPoint = intersect;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return closestNewPoint;
|
|
||||||
// }
|
|
||||||
// const getAllOtherPathPoints = useCallback((): PointData[] => {
|
|
||||||
// if (draftPoints.length === 0) return [];
|
|
||||||
// return (
|
|
||||||
// paths?.flatMap((path) =>
|
|
||||||
// path.pathPoints.filter((pt) => pt.pointId !== draftPoints[0].pointId)
|
|
||||||
// ) ?? []
|
|
||||||
// );
|
|
||||||
// }, [paths, draftPoints]);
|
|
||||||
|
|
||||||
// // Snapping
|
|
||||||
// const snapPathPoint = useCallback(
|
|
||||||
// (position: [number, number, number], tempPoint?: PointData) => {
|
|
||||||
// if (draftPoints.length === 0 || !CAN_POINT_SNAP) {
|
|
||||||
// return {
|
|
||||||
// position,
|
|
||||||
// isSnapped: false,
|
|
||||||
// snappedPoint: null as PointData | null,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const otherPoints = [...getAllOtherPathPoints()];
|
|
||||||
// if (tempPoint) {
|
|
||||||
// otherPoints.push(tempPoint);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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, isSnapped: false, snappedPoint: null };
|
|
||||||
// },
|
|
||||||
// [draftPoints, getAllOtherPathPoints]
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Frame loop
|
|
||||||
// useFrame(() => {
|
|
||||||
// const intersectionPoint = new Vector3();
|
|
||||||
// raycaster.ray.intersectPlane(plane, intersectionPoint);
|
|
||||||
|
|
||||||
// if (!intersectionPoint) return;
|
|
||||||
|
|
||||||
// const snapped = snapPathPoint(
|
|
||||||
// [intersectionPoint.x, intersectionPoint.y, intersectionPoint.z],
|
|
||||||
// draftPoints[0] // ✅ use one point, not array
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if (snapped.isSnapped && snapped.snappedPoint) {
|
|
||||||
// finalPosition.current = snapped.position;
|
|
||||||
// setSnappedPosition(snapped.position); // ✅ correct type
|
|
||||||
// setSnappedPoint(snapped.snappedPoint);
|
|
||||||
// } else {
|
|
||||||
// finalPosition.current = [
|
|
||||||
// intersectionPoint.x,
|
|
||||||
// intersectionPoint.y,
|
|
||||||
// intersectionPoint.z,
|
|
||||||
// ];
|
|
||||||
// setSnappedPosition(null);
|
|
||||||
// setSnappedPoint(null);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// const getWallPointById: (uuid) => {
|
|
||||||
// for (const wall of get().paths) {
|
|
||||||
// const point = wall.pathPoints.find(p => p.pointUuid === uuid);
|
|
||||||
// if (point) return point;
|
|
||||||
// }
|
|
||||||
// return undefined;
|
|
||||||
// }
|
|
||||||
// const handleClick = () => {
|
|
||||||
// const intersectionPoint = new Vector3();
|
|
||||||
// let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
|
||||||
// if (!position) return;
|
|
||||||
|
|
||||||
// const pointIntersect = raycaster
|
|
||||||
// .intersectObjects(scene.children)
|
|
||||||
// .find((intersect) => intersect.object.name === "Path-Point");
|
|
||||||
|
|
||||||
// const pathIntersect = raycaster
|
|
||||||
// .intersectObjects(scene.children)
|
|
||||||
// .find((intersect) => intersect.object.name === "Path-Line");
|
|
||||||
|
|
||||||
// // ✅ Case 1: Split an existing path
|
|
||||||
// if (!pointIntersect && pathIntersect) {
|
|
||||||
// const hitLine = pathIntersect.object;
|
|
||||||
// const hitPathId = hitLine.userData.pathId;
|
|
||||||
|
|
||||||
// const hitPath = paths.find((p) => p.pathId === hitPathId);
|
|
||||||
// if (!hitPath) return;
|
|
||||||
|
|
||||||
// const [p1, p2] = hitPath.pathPoints;
|
|
||||||
|
|
||||||
// const point1Vec = new Vector3(...p1.position);
|
|
||||||
// const point2Vec = new Vector3(...p2.position);
|
|
||||||
|
|
||||||
// const lineDir = new Vector3()
|
|
||||||
// .subVectors(point2Vec, point1Vec)
|
|
||||||
// .normalize();
|
|
||||||
// const point1ToClick = new Vector3().subVectors(
|
|
||||||
// pathIntersect.point,
|
|
||||||
// point1Vec
|
|
||||||
// );
|
|
||||||
// const dot = point1ToClick.dot(lineDir);
|
|
||||||
|
|
||||||
// const projection = new Vector3()
|
|
||||||
// .copy(lineDir)
|
|
||||||
// .multiplyScalar(dot)
|
|
||||||
// .add(point1Vec);
|
|
||||||
|
|
||||||
// const lineLength = point1Vec.distanceTo(point2Vec);
|
|
||||||
// let t = point1Vec.distanceTo(projection) / lineLength;
|
|
||||||
// t = Math.max(0, Math.min(1, t));
|
|
||||||
|
|
||||||
// const closestPoint = new Vector3().lerpVectors(point1Vec, point2Vec, t);
|
|
||||||
|
|
||||||
// const newPoint: PointData = {
|
|
||||||
// pointId: MathUtils.generateUUID(),
|
|
||||||
// position: closestPoint.toArray() as [number, number, number],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const newPath1: PathDataInterface = {
|
|
||||||
// pathId: MathUtils.generateUUID(),
|
|
||||||
// pathPoints: [p1, newPoint],
|
|
||||||
// };
|
|
||||||
// const newPath2: PathDataInterface = {
|
|
||||||
// pathId: MathUtils.generateUUID(),
|
|
||||||
// pathPoints: [newPoint, p2],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// setPaths((prev) =>
|
|
||||||
// prev.filter((p) => p.pathId !== hitPathId).concat([newPath1, newPath2])
|
|
||||||
// );
|
|
||||||
|
|
||||||
// setDraftPoints([newPoint]);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const newPoint: PointData = {
|
|
||||||
// pointId: MathUtils.generateUUID(),
|
|
||||||
// position: [position.x, position.y, position.z],
|
|
||||||
// };
|
|
||||||
// if (snappedPosition && snappedPoint) {
|
|
||||||
// newPoint.pointId = snappedPoint.pointId;
|
|
||||||
// newPoint.position = snappedPosition;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (snappedPoint && snappedPoint.pointId === draftPoints[0].pointId) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (snappedPosition && !snappedPoint) {
|
|
||||||
// newPoint.position = snappedPosition;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (pointIntersect && !snappedPoint) {
|
|
||||||
// const point = getWallPointById(pointIntersects.object.uuid);
|
|
||||||
// if (point) {
|
|
||||||
// newPoint.pointUuid = point.pointUuid;
|
|
||||||
// newPoint.position = point.position;
|
|
||||||
// newPoint.layer = point.layer;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // ✅ Case 2: Normal new path creation
|
|
||||||
|
|
||||||
// if (draftPoints.length === 0) {
|
|
||||||
// setDraftPoints([newPoint]);
|
|
||||||
// } else {
|
|
||||||
// const newPath: PathDataInterface = {
|
|
||||||
// pathId: MathUtils.generateUUID(),
|
|
||||||
// pathPoints: [draftPoints[0], newPoint],
|
|
||||||
// };
|
|
||||||
// setPaths((prev) => [...prev, newPath]);
|
|
||||||
// setDraftPoints([newPoint]);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleContextMenu = (event: any) => {
|
|
||||||
// event.preventDefault();
|
|
||||||
// setDraftPoints([]);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // 👇 track mouse move for preview line
|
|
||||||
// const handleMouseMove = () => {
|
|
||||||
// const intersectionPoint = new Vector3();
|
|
||||||
// let pos = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
|
||||||
// if (pos) {
|
|
||||||
// setMousePos([pos.x, pos.y, pos.z]);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const canvasElement = gl.domElement;
|
|
||||||
|
|
||||||
// canvasElement.addEventListener("click", handleClick);
|
|
||||||
// canvasElement.addEventListener("mousemove", handleMouseMove);
|
|
||||||
// canvasElement.addEventListener("contextmenu", handleContextMenu);
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// canvasElement.removeEventListener("click", handleClick);
|
|
||||||
// canvasElement.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
// canvasElement.removeEventListener("contextmenu", handleContextMenu);
|
|
||||||
// };
|
|
||||||
// }, [gl, draftPoints, paths]);
|
|
||||||
|
|
||||||
// const allPoints = useMemo(() => {
|
|
||||||
// const points: PointData[] = [];
|
|
||||||
// const seenUuids = new Set<string>();
|
|
||||||
// paths?.forEach((wall) => {
|
|
||||||
// wall.pathPoints.forEach((point) => {
|
|
||||||
// if (!seenUuids.has(point.pointId)) {
|
|
||||||
// seenUuids.add(point.pointId);
|
|
||||||
// points.push(point);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// return points;
|
|
||||||
// }, [paths]);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// {/* Draft points (red) */}
|
|
||||||
// {draftPoints.map((point) => (
|
|
||||||
// <mesh key={point.pointId} position={point.position}>
|
|
||||||
// <sphereGeometry args={[0.2, 16, 16]} />
|
|
||||||
// <meshBasicMaterial color="red" />
|
|
||||||
// </mesh>
|
|
||||||
// ))}
|
|
||||||
|
|
||||||
// {/* All saved points */}
|
|
||||||
// {allPoints.map((point) => (
|
|
||||||
// <mesh
|
|
||||||
// key={point.pointId}
|
|
||||||
// position={point.position}
|
|
||||||
// name="Path-Point"
|
|
||||||
// userData={point}
|
|
||||||
// >
|
|
||||||
// <sphereGeometry args={[0.2, 16, 16]} />
|
|
||||||
// <meshBasicMaterial color="pink" />
|
|
||||||
// </mesh>
|
|
||||||
// ))}
|
|
||||||
|
|
||||||
// {/* 👇 Dashed preview line while drawing */}
|
|
||||||
// {draftPoints.length > 0 && mousePos && (
|
|
||||||
// <Line
|
|
||||||
// points={[draftPoints[0].position, mousePos]}
|
|
||||||
// color="orange"
|
|
||||||
// lineWidth={2}
|
|
||||||
// dashed
|
|
||||||
// />
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// {/* Permanent paths */}
|
|
||||||
// {paths.map((points) => (
|
|
||||||
// <Line
|
|
||||||
// name="Path-Line"
|
|
||||||
// key={points.pathId}
|
|
||||||
// points={[
|
|
||||||
// points.pathPoints[0].position,
|
|
||||||
// points.pathPoints[1].position,
|
|
||||||
// ]}
|
|
||||||
// color="purple"
|
|
||||||
// lineWidth={2}
|
|
||||||
// userData={points}
|
|
||||||
// />
|
|
||||||
// ))}
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|||||||
186
app/src/modules/simulation/vehicle/pathCreator/pathHandler.tsx
Normal file
186
app/src/modules/simulation/vehicle/pathCreator/pathHandler.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { DragControls, Line } from "@react-three/drei";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { useActiveSubTool, useToolMode } from "../../../../store/builder/store";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import { LineCurve3, Plane, Vector3 } from "three";
|
||||||
|
import { getPathsByPointId, setPathPosition } from "./function/getPaths";
|
||||||
|
import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors";
|
||||||
|
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 PathHandlerProps = {
|
||||||
|
selectedPath: PathDataInterface;
|
||||||
|
points: [PointData, PointData];
|
||||||
|
paths: PathData;
|
||||||
|
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
|
||||||
|
setHoveredLine: React.Dispatch<
|
||||||
|
React.SetStateAction<PathDataInterface | null>
|
||||||
|
>;
|
||||||
|
hoveredLine: PathDataInterface | null;
|
||||||
|
hoveredPoint: PointData | null;
|
||||||
|
};
|
||||||
|
export default function PathHandler({
|
||||||
|
selectedPath,
|
||||||
|
setPaths,
|
||||||
|
points,
|
||||||
|
paths,
|
||||||
|
setHoveredLine,
|
||||||
|
hoveredLine,
|
||||||
|
hoveredPoint,
|
||||||
|
}: PathHandlerProps) {
|
||||||
|
const { toolMode } = useToolMode();
|
||||||
|
const { scene, raycaster } = useThree();
|
||||||
|
const [dragOffset, setDragOffset] = useState<Vector3 | null>(null);
|
||||||
|
const [initialPositions, setInitialPositions] = useState<{
|
||||||
|
paths?: any;
|
||||||
|
}>({});
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const { activeSubTool } = useActiveSubTool();
|
||||||
|
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
|
||||||
|
const path = useMemo(() => {
|
||||||
|
const [start, end] = points.map((p) => new Vector3(...p.position));
|
||||||
|
return new LineCurve3(start, end);
|
||||||
|
}, [points]);
|
||||||
|
const removePath = (pathId: string) => {
|
||||||
|
setPaths((prevPaths) => prevPaths.filter((p) => p.pathId !== pathId));
|
||||||
|
};
|
||||||
|
const handlePathClick = (pointId: string) => {
|
||||||
|
if (toolMode === "3D-Delete") {
|
||||||
|
removePath(pointId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleDragStart = (points: [PointData, PointData]) => {
|
||||||
|
if (activeSubTool !== "free-hand") return;
|
||||||
|
const intersectionPoint = new Vector3();
|
||||||
|
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||||
|
if (hit) {
|
||||||
|
const start = new Vector3(...points[0].position);
|
||||||
|
const end = new Vector3(...points[1].position);
|
||||||
|
const midPoint = new Vector3().addVectors(start, end).multiplyScalar(0.5);
|
||||||
|
const offset = new Vector3().subVectors(midPoint, hit);
|
||||||
|
setDragOffset(offset);
|
||||||
|
const pathSet = getPathsByPointId(points[0].pointId, paths);
|
||||||
|
setInitialPositions({ paths: pathSet });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// const handleDrag = (points: [PointData, 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 start = new Vector3(...points[0].position);
|
||||||
|
// const end = new Vector3(...points[1].position);
|
||||||
|
// const midPoint = new Vector3()
|
||||||
|
// .addVectors(start, end)
|
||||||
|
// .multiplyScalar(0.5);
|
||||||
|
// const delta = new Vector3().subVectors(positionWithOffset, midPoint);
|
||||||
|
// const newStart = new Vector3().addVectors(start, delta);
|
||||||
|
// const newEnd = new Vector3().addVectors(end, delta);
|
||||||
|
// setPathPosition(
|
||||||
|
// points[0].pointId,
|
||||||
|
// [newStart.x, newStart.y, newStart.z],
|
||||||
|
// setPaths
|
||||||
|
// );
|
||||||
|
// setPathPosition(
|
||||||
|
// points[1].pointId,
|
||||||
|
// [newEnd.x, newEnd.y, newEnd.z],
|
||||||
|
// setPaths
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
const handleDrag = (points: [PointData, 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 start = new Vector3(...points[0].position);
|
||||||
|
const end = new Vector3(...points[1].position);
|
||||||
|
const midPoint = new Vector3()
|
||||||
|
.addVectors(start, end)
|
||||||
|
.multiplyScalar(0.5);
|
||||||
|
const delta = new Vector3().subVectors(positionWithOffset, midPoint);
|
||||||
|
|
||||||
|
const newStart: [number, number, number] = [
|
||||||
|
start.x + delta.x,
|
||||||
|
start.y + delta.y,
|
||||||
|
start.z + delta.z,
|
||||||
|
];
|
||||||
|
const newEnd: [number, number, number] = [
|
||||||
|
end.x + delta.x,
|
||||||
|
end.y + delta.y,
|
||||||
|
end.z + delta.z,
|
||||||
|
];
|
||||||
|
|
||||||
|
// ✅ Move both points separately (won’t overwrite other updates)
|
||||||
|
setPathPosition(points[0].pointId, newStart, setPaths);
|
||||||
|
setPathPosition(points[1].pointId, newEnd, setPaths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = (points: [PointData, PointData]) => {};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DragControls
|
||||||
|
axisLock="y"
|
||||||
|
autoTransform={false}
|
||||||
|
onDragStart={() => handleDragStart(points)}
|
||||||
|
onDrag={() => handleDrag(points)}
|
||||||
|
onDragEnd={() => handleDragEnd(points)}
|
||||||
|
>
|
||||||
|
<Line
|
||||||
|
name="Path-Line"
|
||||||
|
key={selectedPath.pathId}
|
||||||
|
points={[points[0].position, points[1].position]}
|
||||||
|
color="purple"
|
||||||
|
lineWidth={5}
|
||||||
|
userData={selectedPath}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePathClick(selectedPath.pathId);
|
||||||
|
}}
|
||||||
|
onPointerOver={(e) => {
|
||||||
|
if (e.buttons === 0 && !e.ctrlKey) {
|
||||||
|
setHoveredLine(selectedPath);
|
||||||
|
setIsHovered(true);
|
||||||
|
if (!hoveredPoint) {
|
||||||
|
// handleCanvasCursors("grab");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerOut={() => {
|
||||||
|
if (isHovered && hoveredLine) {
|
||||||
|
setHoveredLine(null);
|
||||||
|
if (!hoveredPoint) {
|
||||||
|
// handleCanvasCursors("default");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsHovered(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DragControls>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
299
app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx
Normal file
299
app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import { DragControls } from "@react-three/drei";
|
||||||
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
|
import { useActiveSubTool, useToolMode } from "../../../../store/builder/store";
|
||||||
|
import { Plane, Vector3 } from "three";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors";
|
||||||
|
import { getPathsByPointId, setPathPosition } from "./function/getPaths";
|
||||||
|
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();
|
||||||
|
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 { activeSubTool } = useActiveSubTool();
|
||||||
|
const [initialPositions, setInitialPositions] = useState<{
|
||||||
|
paths?: any;
|
||||||
|
}>({});
|
||||||
|
const [selectedPoints, setSelectedPoints] = useState();
|
||||||
|
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;
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
// };
|
||||||
|
|
||||||
|
const handlePointClick = (pointId: string) => {
|
||||||
|
if (toolMode === "3D-Delete") {
|
||||||
|
removePathByPoint(pointId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleDragStart = (point: PointData) => {
|
||||||
|
if (activeSubTool !== "free-hand") 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
handlePointClick(point.pointId);
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user