refactor: Implement point snapping functionality and enhance aisle management with new snapping logic and state management

This commit is contained in:
Jerald-Golden-B 2025-05-30 16:27:28 +05:30
parent 63bb7c84aa
commit 5254bbd8df
9 changed files with 242 additions and 70 deletions

View File

@ -18,7 +18,7 @@ function AisleCreator() {
const [tempPoints, setTempPoints] = useState<Point[]>([]); const [tempPoints, setTempPoints] = useState<Point[]>([]);
const [isCreating, setIsCreating] = useState(false); 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(() => { // useEffect(() => {
// if (tempPoints.length > 0) { // if (tempPoints.length > 0) {
@ -57,7 +57,7 @@ function AisleCreator() {
raycaster.setFromCamera(pointer, camera); raycaster.setFromCamera(pointer, camera);
const intersectionPoint = new THREE.Vector3(); const intersectionPoint = new THREE.Vector3();
const position = raycaster.ray.intersectPlane(plane, intersectionPoint); let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (!position) return; if (!position) return;
const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Aisle-Point'); const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Aisle-Point');
@ -69,7 +69,17 @@ function AisleCreator() {
layer: activeLayer 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); const point = getAislePointById(intersects.object.uuid);
if (point) { if (point) {
newPoint.uuid = point.uuid; newPoint.uuid = point.uuid;
@ -210,7 +220,7 @@ function AisleCreator() {
canvasElement.removeEventListener("click", onMouseClick); canvasElement.removeEventListener("click", onMouseClick);
canvasElement.removeEventListener("contextmenu", onContext); 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 ( return (
<> <>
@ -222,7 +232,9 @@ function AisleCreator() {
))} ))}
</group> </group>
<ReferenceAisle tempPoints={tempPoints} /> {tempPoints.length > 0 &&
<ReferenceAisle tempPoints={tempPoints} />
}
</> </>
} }
</> </>

View File

@ -5,21 +5,31 @@ import { useActiveLayer, useToolMode, useToggleView } from '../../../../store/bu
import * as Constants from '../../../../types/world/worldConstants'; import * as Constants from '../../../../types/world/worldConstants';
import { Extrude } from '@react-three/drei'; import { Extrude } from '@react-three/drei';
import { useBuilderStore } from '../../../../store/builder/useBuilderStore'; import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
import { useDirectionalSnapping } from '../../point/helpers/useDirectionalSnapping';
import { usePointSnapping } from '../../point/helpers/usePointSnapping';
interface ReferenceAisleProps { interface ReferenceAisleProps {
tempPoints: Point[]; tempPoints: Point[];
} }
function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) { 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 { pointer, raycaster, camera } = useThree();
const { toolMode } = useToolMode(); const { toolMode } = useToolMode();
const { toggleView } = useToggleView(); const { toggleView } = useToggleView();
const { activeLayer } = useActiveLayer(); const { activeLayer } = useActiveLayer();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); 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 [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(() => { useFrame(() => {
if (toolMode === "Aisle" && toggleView && tempPoints.length === 1) { if (toolMode === "Aisle" && toggleView && tempPoints.length === 1) {
@ -27,8 +37,27 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
const intersectionPoint = new THREE.Vector3(); const intersectionPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersectionPoint); raycaster.ray.intersectPlane(plane, intersectionPoint);
setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
if (intersectionPoint) { 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') { if (aisleType === 'solid-aisle' || aisleType === 'stripped-aisle') {
setTempAisle({ setTempAisle({
@ -38,7 +67,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
{ {
uuid: 'temp-point', uuid: 'temp-point',
pointType: 'Aisle', pointType: 'Aisle',
position: [mousePosRef.current.x, mousePosRef.current.y, mousePosRef.current.z], position: finalPosition.current,
layer: activeLayer layer: activeLayer
} }
], ],
@ -56,7 +85,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
{ {
uuid: 'temp-point', uuid: 'temp-point',
pointType: 'Aisle', pointType: 'Aisle',
position: [mousePosRef.current.x, mousePosRef.current.y, mousePosRef.current.z], position: finalPosition.current,
layer: activeLayer layer: activeLayer
} }
], ],
@ -76,7 +105,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
{ {
uuid: 'temp-point', uuid: 'temp-point',
pointType: 'Aisle', pointType: 'Aisle',
position: [mousePosRef.current.x, mousePosRef.current.y, mousePosRef.current.z], position: finalPosition.current,
layer: activeLayer layer: activeLayer
} }
], ],
@ -97,7 +126,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
{ {
uuid: 'temp-point', uuid: 'temp-point',
pointType: 'Aisle', pointType: 'Aisle',
position: [mousePosRef.current.x, mousePosRef.current.y, mousePosRef.current.z], position: finalPosition.current,
layer: activeLayer layer: activeLayer
} }
], ],

View File

@ -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 };
}

View File

@ -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]);
};

View File

@ -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,
};
};

View File

@ -6,7 +6,7 @@ import { DragControls } from '@react-three/drei';
import { useAisleStore } from '../../../store/builder/useAisleStore'; import { useAisleStore } from '../../../store/builder/useAisleStore';
import { useThree } from '@react-three/fiber'; import { useThree } from '@react-three/fiber';
import { useBuilderStore } from '../../../store/builder/useBuilderStore'; import { useBuilderStore } from '../../../store/builder/useBuilderStore';
import { usePointSnapping } from './usePointSnapping'; import { usePointSnapping } from './helpers/usePointSnapping';
function Point({ point }: { readonly point: Point }) { function Point({ point }: { readonly point: Point }) {
const materialRef = useRef<THREE.ShaderMaterial>(null); const materialRef = useRef<THREE.ShaderMaterial>(null);
@ -15,7 +15,7 @@ function Point({ point }: { readonly point: Point }) {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const { toolMode } = useToolMode(); const { toolMode } = useToolMode();
const { setPosition, removePoint } = useAisleStore(); 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 { hoveredPoint, setHoveredPoint } = useBuilderStore();
const { deletePointOrLine } = useDeletePointOrLine(); 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 newPosition: [number, number, number] = [position.x, position.y, position.z];
const snappedPosition = checkSnapForAisle(newPosition); const snappedPosition = checkSnapForAisle(newPosition);
setPosition(point.uuid, snappedPosition); setPosition(point.uuid, snappedPosition.position);
} }
} }
} }

View File

@ -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
};
};

View File

@ -34,6 +34,7 @@ interface AisleStore {
getAisleById: (uuid: string) => Aisle | undefined; getAisleById: (uuid: string) => Aisle | undefined;
getAislePointById: (uuid: string) => Point | undefined; getAislePointById: (uuid: string) => Point | undefined;
getConnectedPoints: (uuid: string) => Point[] | [];
getAisleType: <T extends AisleType>(uuid: string) => T | undefined; getAisleType: <T extends AisleType>(uuid: string) => T | undefined;
} }
@ -183,6 +184,18 @@ export const useAisleStore = create<AisleStore>()(
return undefined; 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) => { getAisleType: <T extends AisleType>(uuid: string) => {
const aisle = get().aisles.find(a => a.uuid === uuid); const aisle = get().aisles.find(a => a.uuid === uuid);
return aisle?.type as T | undefined; return aisle?.type as T | undefined;

View File

@ -6,6 +6,8 @@ interface BuilderState {
// Common properties // Common properties
hoveredPoint: Point | null; hoveredPoint: Point | null;
snappedPoint: Point | null;
snappedPosition: [number, number, number] | null;
selectedAisle: Object3D | null; selectedAisle: Object3D | null;
@ -26,6 +28,8 @@ interface BuilderState {
// Setters for common properties // Setters for common properties
setHoveredPoint: (point: Point | null) => void; setHoveredPoint: (point: Point | null) => void;
setSnappedPoint: (point: Point | null) => void;
setSnappedPosition: (position: [number, number, number] | null) => void;
setSelectedAisle: (aisle: Object3D | null) => void; setSelectedAisle: (aisle: Object3D | null) => void;
@ -55,6 +59,8 @@ export const useBuilderStore = create<BuilderState>()(
// Default values // Default values
hoveredPoint: null, hoveredPoint: null,
snappedPoint: null,
snappedPosition: null,
selectedAisle: 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) => { setSelectedAisle: (aisle: Object3D | null) => {
set((state) => { set((state) => {
state.selectedAisle = aisle; state.selectedAisle = aisle;