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([]); 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() { ))} - + {tempPoints.length > 0 && + + } } 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) { - 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(null); - const mousePosRef = useRef(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) { 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) { { 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) { { 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) { { 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) { { 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(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(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: (uuid: string) => T | undefined; } @@ -183,6 +184,18 @@ export const useAisleStore = create()( 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: (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()( // Default values hoveredPoint: null, + snappedPoint: null, + snappedPosition: null, selectedAisle: null, @@ -74,6 +80,18 @@ export const useBuilderStore = create()( }); }, + 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;