From 5254bbd8dfe785ef1949551c25da43fc04a4a3bb Mon Sep 17 00:00:00 2001
From: Jerald-Golden-B <jerald@hexrfactory.com>
Date: Fri, 30 May 2025 16:27:28 +0530
Subject: [PATCH] refactor: Implement point snapping functionality and enhance
 aisle management with new snapping logic and state management

---
 .../aisle/aisleCreator/aisleCreator.tsx       | 22 +++++--
 .../aisle/aisleCreator/referenceAisle.tsx     | 43 ++++++++++---
 .../point/helpers/useAisleDragSnap.tsx        | 61 +++++++++++++++++++
 .../point/helpers/useDirectionalSnapping.tsx  | 53 ++++++++++++++++
 .../point/helpers/usePointSnapping.tsx        | 41 +++++++++++++
 app/src/modules/builder/point/point.tsx       |  6 +-
 .../builder/point/usePointSnapping.tsx        | 55 -----------------
 app/src/store/builder/useAisleStore.ts        | 13 ++++
 app/src/store/builder/useBuilderStore.ts      | 18 ++++++
 9 files changed, 242 insertions(+), 70 deletions(-)
 create mode 100644 app/src/modules/builder/point/helpers/useAisleDragSnap.tsx
 create mode 100644 app/src/modules/builder/point/helpers/useDirectionalSnapping.tsx
 create mode 100644 app/src/modules/builder/point/helpers/usePointSnapping.tsx
 delete mode 100644 app/src/modules/builder/point/usePointSnapping.tsx

diff --git a/app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx b/app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx
index baaf9df..0b14967 100644
--- a/app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx
+++ b/app/src/modules/builder/aisle/aisleCreator/aisleCreator.tsx
@@ -18,7 +18,7 @@ function AisleCreator() {
 
     const [tempPoints, setTempPoints] = useState<Point[]>([]);
     const [isCreating, setIsCreating] = useState(false);
-    const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, } = useBuilderStore();
+    const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, snappedPosition, snappedPoint } = useBuilderStore();
 
     // useEffect(() => {
     //     if (tempPoints.length > 0) {
@@ -57,7 +57,7 @@ function AisleCreator() {
 
             raycaster.setFromCamera(pointer, camera);
             const intersectionPoint = new THREE.Vector3();
-            const position = raycaster.ray.intersectPlane(plane, intersectionPoint);
+            let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
             if (!position) return;
 
             const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Aisle-Point');
@@ -69,7 +69,17 @@ function AisleCreator() {
                 layer: activeLayer
             };
 
-            if (intersects) {
+            if (snappedPosition && snappedPoint) {
+                newPoint.uuid = snappedPoint.uuid;
+                newPoint.position = snappedPosition;
+                newPoint.layer = snappedPoint.layer;
+            }
+
+            if (snappedPosition && !snappedPoint) {
+                newPoint.position = snappedPosition;
+            }
+
+            if (intersects && !snappedPoint) {
                 const point = getAislePointById(intersects.object.uuid);
                 if (point) {
                     newPoint.uuid = point.uuid;
@@ -210,7 +220,7 @@ function AisleCreator() {
             canvasElement.removeEventListener("click", onMouseClick);
             canvasElement.removeEventListener("contextmenu", onContext);
         };
-    }, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addAisle, aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength]);
+    }, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addAisle, aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, snappedPosition, snappedPoint]);
 
     return (
         <>
@@ -222,7 +232,9 @@ function AisleCreator() {
                         ))}
                     </group>
 
-                    <ReferenceAisle tempPoints={tempPoints} />
+                    {tempPoints.length > 0 &&
+                        <ReferenceAisle tempPoints={tempPoints} />
+                    }
                 </>
             }
         </>
diff --git a/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx b/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx
index bf02755..3479ea2 100644
--- a/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx
+++ b/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx
@@ -5,21 +5,31 @@ import { useActiveLayer, useToolMode, useToggleView } from '../../../../store/bu
 import * as Constants from '../../../../types/world/worldConstants';
 import { Extrude } from '@react-three/drei';
 import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
+import { useDirectionalSnapping } from '../../point/helpers/useDirectionalSnapping';
+import { usePointSnapping } from '../../point/helpers/usePointSnapping';
 
 interface ReferenceAisleProps {
     tempPoints: Point[];
 }
 
 function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
-    const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, } = useBuilderStore();
+    const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, setSnappedPosition, setSnappedPoint } = useBuilderStore();
     const { pointer, raycaster, camera } = useThree();
     const { toolMode } = useToolMode();
     const { toggleView } = useToggleView();
     const { activeLayer } = useActiveLayer();
     const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
+    const finalPosition = useRef<[number, number, number] | null>(null);
 
     const [tempAisle, setTempAisle] = useState<Aisle | null>(null);
-    const mousePosRef = useRef<THREE.Vector3>(new THREE.Vector3());
+    const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position);
+
+    // Calculate directional snap based on current and previous points
+    const directionalSnap = useDirectionalSnapping(
+        currentPosition,
+        tempPoints[0]?.position || null
+    );
+    const { checkSnapForAisle } = usePointSnapping({ uuid: 'temp-aisle', pointType: 'Aisle', position: directionalSnap.position || [0, 0, 0] });
 
     useFrame(() => {
         if (toolMode === "Aisle" && toggleView && tempPoints.length === 1) {
@@ -27,8 +37,27 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
             const intersectionPoint = new THREE.Vector3();
             raycaster.ray.intersectPlane(plane, intersectionPoint);
 
+            setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
+
             if (intersectionPoint) {
-                mousePosRef.current.copy(intersectionPoint);
+
+                const snapped = checkSnapForAisle([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
+
+                if (snapped.isSnapped && snapped.snappedPoint) {
+                    finalPosition.current = snapped.position;
+                    setSnappedPosition(snapped.position);
+                    setSnappedPoint(snapped.snappedPoint);
+                } else if (directionalSnap.isSnapped) {
+                    finalPosition.current = directionalSnap.position;
+                    setSnappedPosition(directionalSnap.position);
+                    setSnappedPoint(null);
+                } else {
+                    finalPosition.current = [intersectionPoint.x, intersectionPoint.y, intersectionPoint.z];
+                    setSnappedPosition(null);
+                    setSnappedPoint(null);
+                }
+
+                if (!finalPosition.current) return;
 
                 if (aisleType === 'solid-aisle' || aisleType === 'stripped-aisle') {
                     setTempAisle({
@@ -38,7 +67,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
                             {
                                 uuid: 'temp-point',
                                 pointType: 'Aisle',
-                                position: [mousePosRef.current.x, mousePosRef.current.y, mousePosRef.current.z],
+                                position: finalPosition.current,
                                 layer: activeLayer
                             }
                         ],
@@ -56,7 +85,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
                             {
                                 uuid: 'temp-point',
                                 pointType: 'Aisle',
-                                position: [mousePosRef.current.x, mousePosRef.current.y, mousePosRef.current.z],
+                                position: finalPosition.current,
                                 layer: activeLayer
                             }
                         ],
@@ -76,7 +105,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
                             {
                                 uuid: 'temp-point',
                                 pointType: 'Aisle',
-                                position: [mousePosRef.current.x, mousePosRef.current.y, mousePosRef.current.z],
+                                position: finalPosition.current,
                                 layer: activeLayer
                             }
                         ],
@@ -97,7 +126,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
                             {
                                 uuid: 'temp-point',
                                 pointType: 'Aisle',
-                                position: [mousePosRef.current.x, mousePosRef.current.y, mousePosRef.current.z],
+                                position: finalPosition.current,
                                 layer: activeLayer
                             }
                         ],
diff --git a/app/src/modules/builder/point/helpers/useAisleDragSnap.tsx b/app/src/modules/builder/point/helpers/useAisleDragSnap.tsx
new file mode 100644
index 0000000..e12fc17
--- /dev/null
+++ b/app/src/modules/builder/point/helpers/useAisleDragSnap.tsx
@@ -0,0 +1,61 @@
+import { useCallback, useMemo } from 'react';
+import * as THREE from 'three';
+import { useAisleStore } from '../../../../store/builder/useAisleStore';
+
+const SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
+const SNAP_STRENGTH = 0.2; // How strongly the point snaps (0-1)
+
+export function useAislePointSnapping(point: Point) {
+    const { getConnectedPoints } = useAisleStore();
+
+    // Find all points connected to the current point via aisles
+    const connectedPoints = useMemo(() => {
+        return getConnectedPoints(point.uuid);
+    }, [point.uuid]);
+
+    // Snapping function
+    const snapPosition = useCallback((newPosition: [number, number, number]): [number, number, number] => {
+        if (connectedPoints.length === 0) return newPosition;
+
+        const newPos = new THREE.Vector3(...newPosition);
+        let snappedX = newPos.x;
+        let snappedZ = newPos.z;
+        let snapCount = 0;
+
+        // Check against all connected points
+        connectedPoints.forEach(connectedPoint => {
+            const connectedPos = new THREE.Vector3(...connectedPoint.position);
+
+            // Check X axis
+            if (Math.abs(newPos.x - connectedPos.x) < SNAP_THRESHOLD) {
+                // Apply soft snapping (lerp)
+                snappedX = THREE.MathUtils.lerp(
+                    newPos.x,
+                    connectedPos.x,
+                    SNAP_STRENGTH
+                );
+                snapCount++;
+            }
+
+            // Check Z axis
+            if (Math.abs(newPos.z - connectedPos.z) < SNAP_THRESHOLD) {
+                // Apply soft snapping (lerp)
+                snappedZ = THREE.MathUtils.lerp(
+                    newPos.z,
+                    connectedPos.z,
+                    SNAP_STRENGTH
+                );
+                snapCount++;
+            }
+        });
+
+        // Only return snapped position if we actually snapped to something
+        if (snapCount > 0) {
+            return [snappedX, newPos.y, snappedZ];
+        }
+
+        return newPosition;
+    }, [connectedPoints]);
+
+    return { snapPosition };
+}
\ No newline at end of file
diff --git a/app/src/modules/builder/point/helpers/useDirectionalSnapping.tsx b/app/src/modules/builder/point/helpers/useDirectionalSnapping.tsx
new file mode 100644
index 0000000..fffdc31
--- /dev/null
+++ b/app/src/modules/builder/point/helpers/useDirectionalSnapping.tsx
@@ -0,0 +1,53 @@
+import { useMemo } from 'react';
+import * as THREE from 'three';
+
+const SNAP_ANGLE_THRESHOLD = 15; // Degrees within which to snap to orthogonal direction
+
+const SNAP_DISTANCE_THRESHOLD = 0.5; // Minimum distance to consider for directional snap
+
+const CAN_SNAP = true; // Whether snapping is enabled or not
+
+export const useDirectionalSnapping = (
+    currentPoint: [number, number, number] | null,
+    previousPoint: [number, number, number] | null
+): { position: [number, number, number], isSnapped: boolean } => {
+    return useMemo(() => {
+        if (!currentPoint || !previousPoint || !CAN_SNAP) return { position: currentPoint || [0, 0, 0], isSnapped: false }; // No snapping if no points
+
+        const currentVec = new THREE.Vector2(currentPoint[0], currentPoint[2]);
+        const previousVec = new THREE.Vector2(previousPoint[0], previousPoint[2]);
+        const directionVec = new THREE.Vector2().subVectors(currentVec, previousVec);
+
+        const angle = THREE.MathUtils.radToDeg(directionVec.angle());
+        const normalizedAngle = (angle + 360) % 360;
+        const closestAngle = Math.round(normalizedAngle / 90) * 90 % 360;
+
+        const angleDiff = Math.abs(normalizedAngle - closestAngle);
+        const shortestDiff = Math.min(angleDiff, 360 - angleDiff);
+
+        if (shortestDiff <= SNAP_ANGLE_THRESHOLD) {
+            const distance = directionVec.length();
+            const snappedDirection = new THREE.Vector2(
+                Math.cos(THREE.MathUtils.degToRad(closestAngle)),
+                Math.sin(THREE.MathUtils.degToRad(closestAngle))
+            ).multiplyScalar(distance);
+
+            const newPosition = new THREE.Vector2(previousPoint[0] + snappedDirection.x, previousPoint[2] + snappedDirection.y)
+
+            if (newPosition.distanceTo(currentVec) > SNAP_DISTANCE_THRESHOLD) {
+                return { position: currentPoint, isSnapped: false };
+            }
+
+            return {
+                position: [
+                    previousPoint[0] + snappedDirection.x,
+                    currentPoint[1],
+                    previousPoint[2] + snappedDirection.y
+                ],
+                isSnapped: true
+            };
+        }
+
+        return { position: currentPoint, isSnapped: false };
+    }, [currentPoint, previousPoint]);
+};
\ No newline at end of file
diff --git a/app/src/modules/builder/point/helpers/usePointSnapping.tsx b/app/src/modules/builder/point/helpers/usePointSnapping.tsx
new file mode 100644
index 0000000..4d61b8a
--- /dev/null
+++ b/app/src/modules/builder/point/helpers/usePointSnapping.tsx
@@ -0,0 +1,41 @@
+import { useCallback } from 'react';
+import { useAisleStore } from '../../../../store/builder/useAisleStore';
+import * as THREE from 'three';
+
+const SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
+
+const CAN_SNAP = true; // Whether snapping is enabled or not
+
+export const usePointSnapping = (currentPoint: { uuid: string, pointType: string, position: [number, number, number] } | null) => {
+    const { aisles } = useAisleStore();
+
+    const getAllOtherPoints = useCallback(() => {
+        if (!currentPoint) return [];
+
+        return aisles.flatMap(aisle =>
+            aisle.points.filter(point => point.uuid !== currentPoint.uuid)
+        );
+    }, [aisles, currentPoint]);
+
+    const checkSnapForAisle = useCallback((position: [number, number, number]) => {
+        if (!currentPoint || !CAN_SNAP) return { position: position, isSnapped: false, snappedPoint: null };
+
+        const otherPoints = getAllOtherPoints();
+        const currentVec = new THREE.Vector3(...position);
+
+        for (const point of otherPoints) {
+            const pointVec = new THREE.Vector3(...point.position);
+            const distance = currentVec.distanceTo(pointVec);
+
+            if (distance <= SNAP_THRESHOLD && currentPoint.pointType === 'Aisle') {
+                return { position: point.position, isSnapped: true, snappedPoint: point };
+            }
+        }
+
+        return { position: position, isSnapped: false, snappedPoint: null };
+    }, [currentPoint, getAllOtherPoints]);
+
+    return {
+        checkSnapForAisle,
+    };
+};
\ No newline at end of file
diff --git a/app/src/modules/builder/point/point.tsx b/app/src/modules/builder/point/point.tsx
index c61a3db..9155b86 100644
--- a/app/src/modules/builder/point/point.tsx
+++ b/app/src/modules/builder/point/point.tsx
@@ -6,7 +6,7 @@ import { DragControls } from '@react-three/drei';
 import { useAisleStore } from '../../../store/builder/useAisleStore';
 import { useThree } from '@react-three/fiber';
 import { useBuilderStore } from '../../../store/builder/useBuilderStore';
-import { usePointSnapping } from './usePointSnapping';
+import { usePointSnapping } from './helpers/usePointSnapping';
 
 function Point({ point }: { readonly point: Point }) {
     const materialRef = useRef<THREE.ShaderMaterial>(null);
@@ -15,7 +15,7 @@ function Point({ point }: { readonly point: Point }) {
     const [isHovered, setIsHovered] = useState(false);
     const { toolMode } = useToolMode();
     const { setPosition, removePoint } = useAisleStore();
-    const { checkSnapForAisle } = usePointSnapping(point);
+    const { checkSnapForAisle } = usePointSnapping({ uuid: point.uuid, pointType: point.pointType, position: point.position });
     const { hoveredPoint, setHoveredPoint } = useBuilderStore();
     const { deletePointOrLine } = useDeletePointOrLine();
 
@@ -60,7 +60,7 @@ function Point({ point }: { readonly point: Point }) {
                     const newPosition: [number, number, number] = [position.x, position.y, position.z];
                     const snappedPosition = checkSnapForAisle(newPosition);
 
-                    setPosition(point.uuid, snappedPosition);
+                    setPosition(point.uuid, snappedPosition.position);
                 }
             }
         }
diff --git a/app/src/modules/builder/point/usePointSnapping.tsx b/app/src/modules/builder/point/usePointSnapping.tsx
deleted file mode 100644
index f3e188b..0000000
--- a/app/src/modules/builder/point/usePointSnapping.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useCallback, useEffect, useState } from 'react';
-import { useAisleStore } from '../../../store/builder/useAisleStore';
-import * as THREE from 'three';
-
-const SNAP_THRESHOLD = 0.25; // Distance threshold for snapping in meters
-
-export const usePointSnapping = (currentPoint: Point | null) => {
-    const { aisles } = useAisleStore();
-    const [snappedPosition, setSnappedPosition] = useState<[number, number, number] | null>(null);
-    const [snappedPointId, setSnappedPointId] = useState<string | null>(null);
-
-    // Get all points from all aisles except the current point
-    const getAllOtherPoints = useCallback(() => {
-        if (!currentPoint) return [];
-
-        return aisles.flatMap(aisle =>
-            aisle.points.filter(point => point.uuid !== currentPoint.uuid)
-        );
-    }, [aisles, currentPoint]);
-
-    // Check if there's a nearby Aisle point to snap to
-    const checkSnapForAisle = useCallback((position: [number, number, number]) => {
-        if (!currentPoint) return position;
-
-        const otherPoints = getAllOtherPoints();
-        const currentVec = new THREE.Vector3(...position);
-
-        for (const point of otherPoints) {
-            const pointVec = new THREE.Vector3(...point.position);
-            const distance = currentVec.distanceTo(pointVec);
-
-            if (distance <= SNAP_THRESHOLD && point.pointType === 'Aisle') {
-                setSnappedPointId(point.uuid);
-                return point.position;
-            }
-        }
-
-        // No snap found
-        setSnappedPointId(null);
-        return position;
-    }, [currentPoint, getAllOtherPoints]);
-
-    // Reset snap state when current point changes
-    useEffect(() => {
-        setSnappedPosition(null);
-        setSnappedPointId(null);
-    }, [currentPoint]);
-
-    return {
-        snappedPosition,
-        snappedPointId,
-        checkSnapForAisle,
-        isSnapped: snappedPointId !== null
-    };
-};
\ No newline at end of file
diff --git a/app/src/store/builder/useAisleStore.ts b/app/src/store/builder/useAisleStore.ts
index 2bb17af..7f091a8 100644
--- a/app/src/store/builder/useAisleStore.ts
+++ b/app/src/store/builder/useAisleStore.ts
@@ -34,6 +34,7 @@ interface AisleStore {
 
     getAisleById: (uuid: string) => Aisle | undefined;
     getAislePointById: (uuid: string) => Point | undefined;
+    getConnectedPoints: (uuid: string) => Point[] | [];
     getAisleType: <T extends AisleType>(uuid: string) => T | undefined;
 }
 
@@ -183,6 +184,18 @@ export const useAisleStore = create<AisleStore>()(
             return undefined;
         },
 
+        getConnectedPoints: (uuid) => {
+            const connected: Point[] = [];
+            for (const aisle of get().aisles) {
+                for (const point of aisle.points) {
+                    if (point.uuid === uuid) {
+                        connected.push(...aisle.points.filter(p => p.uuid !== uuid));
+                    }
+                }
+            }
+            return connected;
+        },
+
         getAisleType: <T extends AisleType>(uuid: string) => {
             const aisle = get().aisles.find(a => a.uuid === uuid);
             return aisle?.type as T | undefined;
diff --git a/app/src/store/builder/useBuilderStore.ts b/app/src/store/builder/useBuilderStore.ts
index f8f52e7..dc3524e 100644
--- a/app/src/store/builder/useBuilderStore.ts
+++ b/app/src/store/builder/useBuilderStore.ts
@@ -6,6 +6,8 @@ interface BuilderState {
     // Common properties
 
     hoveredPoint: Point | null;
+    snappedPoint: Point | null;
+    snappedPosition: [number, number, number] | null;
 
     selectedAisle: Object3D | null;
 
@@ -26,6 +28,8 @@ interface BuilderState {
     // Setters for common properties
 
     setHoveredPoint: (point: Point | null) => void;
+    setSnappedPoint: (point: Point | null) => void;
+    setSnappedPosition: (position: [number, number, number] | null) => void;
 
     setSelectedAisle: (aisle: Object3D | null) => void;
 
@@ -55,6 +59,8 @@ export const useBuilderStore = create<BuilderState>()(
         // Default values
 
         hoveredPoint: null,
+        snappedPoint: null,
+        snappedPosition: null,
 
         selectedAisle: null,
 
@@ -74,6 +80,18 @@ export const useBuilderStore = create<BuilderState>()(
             });
         },
 
+        setSnappedPoint: (point: Point | null) => {
+            set((state) => {
+                state.snappedPoint = point;
+            });
+        },
+
+        setSnappedPosition: (position: [number, number, number] | null) => {
+            set((state) => {
+                state.snappedPosition = position;
+            });
+        },
+
         setSelectedAisle: (aisle: Object3D | null) => {
             set((state) => {
                 state.selectedAisle = aisle;