import { DragControls } from "@react-three/drei"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useActiveTool, 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"; import { aStar } from "../structuredPath/functions/aStar"; 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>; paths: PathDataInterface[]; setHoveredPoint: React.Dispatch>; hoveredLine: PathDataInterface | null; }; export default function PointHandler({ point, setPaths, paths, setHoveredPoint, hoveredLine, hoveredPoint, }: PointHandlerProps) { const { toolMode } = useToolMode(); const { activeTool } = useActiveTool(); const { scene, raycaster } = useThree(); const [isHovered, setIsHovered] = useState(false); const [dragOffset, setDragOffset] = useState(null); const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []); 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] ); useEffect(() => { console.log("selectedPoints: ", selectedPoints); }, [selectedPoints]); // const setPathPosition = useCallback( // ( // pointUuid: string, // position: [number, number, number], // setPaths: React.Dispatch> // ) => { // 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 = (e: any, pointId: string) => { if (toolMode === "3D-Delete") { removePathByPoint(pointId); } else if (e.ctrlKey) { } }; 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); } } }; const handleDragEnd = (point: PointData) => { const pathIntersection = getPathsByPointId(point.pointId, paths); if (pathIntersection && pathIntersection.length > 0) { pathIntersection.forEach((update) => {}); } }; return ( <> handleDragStart(point)} onDrag={() => handleDrag(point)} onDragEnd={() => handleDragEnd(point)} > { e.stopPropagation(); handlePointClick(e, 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); }} > ); }