diff --git a/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx b/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx index 10bf6fa..6ce66fe 100644 --- a/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx +++ b/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx @@ -177,7 +177,7 @@ function ReferenceAisle({ tempPoints }: Readonly) { useEffect(() => { setTempAisle(null); - }, [toolMode, toggleView, tempPoints.length, aisleType, aisleWidth, aisleColor]); + }, [toolMode, toggleView, tempPoints.length, aisleType, aisleWidth, aisleColor, activeLayer]); if (!tempAisle) return null; diff --git a/app/src/modules/builder/floor/floorCreator/referenceFloor.tsx b/app/src/modules/builder/floor/floorCreator/referenceFloor.tsx index 3c9d593..dde2002 100644 --- a/app/src/modules/builder/floor/floorCreator/referenceFloor.tsx +++ b/app/src/modules/builder/floor/floorCreator/referenceFloor.tsx @@ -83,7 +83,7 @@ function ReferenceFloor({ tempPoints }: Readonly) { useEffect(() => { setTempFloor(null); - }, [toolMode, toggleView, tempPoints.length, floorDepth, bevelStrength, isBeveled]); + }, [toolMode, toggleView, tempPoints.length, floorDepth, bevelStrength, isBeveled, activeLayer]); const allLines = useMemo(() => { if (!tempFloor || tempFloor.points.length < 2) return []; diff --git a/app/src/modules/builder/point/helpers/usePointSnapping.tsx b/app/src/modules/builder/point/helpers/usePointSnapping.tsx index 0d8f378..69f5e72 100644 --- a/app/src/modules/builder/point/helpers/usePointSnapping.tsx +++ b/app/src/modules/builder/point/helpers/usePointSnapping.tsx @@ -11,10 +11,11 @@ const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5; // Distance threshold for snapping i const CAN_ANGLE_SNAP = true; // Whether snapping is enabled or not export const usePointSnapping = (currentPoint: { uuid: string, pointType: string, position: [number, number, number] } | null) => { - const { aisleStore, wallStore, floorStore } = useSceneContext(); + const { aisleStore, wallStore, floorStore, zoneStore } = useSceneContext(); const { aisles, getConnectedPoints: getConnectedAislePoints } = aisleStore(); const { walls, getConnectedPoints: getConnectedWallPoints } = wallStore(); const { floors, getConnectedPoints: getConnectedFloorPoints } = floorStore(); + const { zones, getConnectedPoints: getConnectedZonePoints } = zoneStore(); // Wall Snapping @@ -278,6 +279,93 @@ export const usePointSnapping = (currentPoint: { uuid: string, pointType: string }; }, [currentPoint, getConnectedFloorPoints]); + // Zone Snapping + + const getAllOtherZonePoints = useCallback(() => { + if (!currentPoint) return []; + + return zones.flatMap(zone => + zone.points.filter(point => point.pointUuid !== currentPoint.uuid) + ); + }, [zones, currentPoint]); + + const snapZonePoint = useCallback((position: [number, number, number], tempPoints?: Point[] | []) => { + if (!currentPoint || !CAN_POINT_SNAP) return { position: position, isSnapped: false, snappedPoint: null }; + + const otherPoints = getAllOtherZonePoints(); + if (tempPoints) { + otherPoints.concat(tempPoints); + } + 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 <= POINT_SNAP_THRESHOLD && currentPoint.pointType === 'Zone') { + return { position: point.position, isSnapped: true, snappedPoint: point }; + } + } + + return { position: position, isSnapped: false, snappedPoint: null }; + }, [currentPoint, getAllOtherZonePoints]); + + const snapZoneAngle = useCallback((newPosition: [number, number, number]): { + position: [number, number, number], + isSnapped: boolean, + snapSources: THREE.Vector3[] + } => { + if (!currentPoint || !CAN_ANGLE_SNAP) return { position: newPosition, isSnapped: false, snapSources: [] }; + + const connectedPoints = getConnectedZonePoints(currentPoint.uuid); + if (connectedPoints.length === 0) { + return { position: newPosition, isSnapped: false, snapSources: [] }; + } + + const newPos = new THREE.Vector3(...newPosition); + let closestX: { pos: THREE.Vector3, dist: number } | null = null; + let closestZ: { pos: THREE.Vector3, dist: number } | null = null; + + for (const connectedPoint of connectedPoints) { + const cPos = new THREE.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: THREE.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 + }; + }, [currentPoint, getConnectedZonePoints]); + return { snapAislePoint, snapAisleAngle, @@ -285,5 +373,7 @@ export const usePointSnapping = (currentPoint: { uuid: string, pointType: string snapWallAngle, snapFloorPoint, snapFloorAngle, + snapZonePoint, + snapZoneAngle, }; }; \ No newline at end of file diff --git a/app/src/modules/builder/wall/wallCreator/referenceWall.tsx b/app/src/modules/builder/wall/wallCreator/referenceWall.tsx index 278fe67..905c60d 100644 --- a/app/src/modules/builder/wall/wallCreator/referenceWall.tsx +++ b/app/src/modules/builder/wall/wallCreator/referenceWall.tsx @@ -82,7 +82,7 @@ function ReferenceWall({ tempPoints }: Readonly) { useEffect(() => { setTempWall(null); - }, [toolMode, toggleView, tempPoints.length, wallHeight, wallThickness]); + }, [toolMode, toggleView, tempPoints.length, wallHeight, wallThickness, activeLayer]); if (!tempWall) return null; diff --git a/app/src/modules/builder/zone/zoneCreator/referenceZone.tsx b/app/src/modules/builder/zone/zoneCreator/referenceZone.tsx new file mode 100644 index 0000000..940134f --- /dev/null +++ b/app/src/modules/builder/zone/zoneCreator/referenceZone.tsx @@ -0,0 +1,163 @@ +import { useEffect, useRef, useState, useMemo } from 'react'; +import * as THREE from 'three'; +import { useFrame, useThree } from '@react-three/fiber'; +import { Extrude, Html } from '@react-three/drei'; +import { useBuilderStore } from '../../../../store/builder/useBuilderStore'; +import { useActiveLayer, useToolMode, useToggleView } from '../../../../store/builder/store'; +import { useDirectionalSnapping } from '../../point/helpers/useDirectionalSnapping'; +import { usePointSnapping } from '../../point/helpers/usePointSnapping'; +import ReferenceLine from '../../line/reference/referenceLine'; + +interface ReferenceZoneProps { + tempPoints: Point[]; +} + +function ReferenceZone({ tempPoints }: Readonly) { + const { zoneColor, zoneHeight, 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 [tempZone, setTempZone] = useState(null); + const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position); + + const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[tempPoints.length - 1]?.position || null); + const { snapZonePoint } = usePointSnapping({ uuid: 'temp-Zone', pointType: 'Zone', position: directionalSnap.position || [0, 0, 0], }); + + useFrame(() => { + if (toolMode === 'Zone' && toggleView && tempPoints.length > 0) { + raycaster.setFromCamera(pointer, camera); + const intersectionPoint = new THREE.Vector3(); + raycaster.ray.intersectPlane(plane, intersectionPoint); + + setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]); + + if (!intersectionPoint) return; + const snapped = snapZonePoint([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z], tempPoints.slice(0, -2)); + + 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; + + const zonePoints: Point[] = [ + ...tempPoints, + { + pointUuid: 'temp-point', + pointType: 'Zone', + position: finalPosition.current, + layer: activeLayer, + }, + ]; + + setTempZone({ + zoneUuid: 'temp-zone', + zoneName: 'temp-zone', + points: zonePoints, + zoneColor, + zoneHeight, + viewPortPosition: [0, 0, 0], + viewPortTarget: [0, 0, 0], + }); + + } else if (tempZone !== null) { + setTempZone(null); + } + }); + + useEffect(() => { + setTempZone(null); + }, [toolMode, toggleView, tempPoints.length, zoneColor, zoneHeight, activeLayer]); + + const allLines = useMemo(() => { + if (!tempZone || tempZone.points.length < 2) return []; + + const lines: [Point, Point][] = []; + const pts = tempZone.points; + + for (let i = 0; i < pts.length - 1; i++) { + lines.push([pts[i], pts[i + 1]]); + } + + return lines; + }, [tempZone]); + + if (!tempZone) return null; + + return ( + + {allLines.map(([p1, p2], idx) => { + const v1 = new THREE.Vector3(...p1.position); + const v2 = new THREE.Vector3(...p2.position); + const mid = new THREE.Vector3().addVectors(v1, v2).multiplyScalar(0.5); + const dist = v1.distanceTo(v2); + + return ( + + + {toggleView && ( + +
{dist.toFixed(2)} m
+ + )} +
+ ); + })} + + {tempPoints.length >= 3 && ( + + )} +
+ ); +} + +export default ReferenceZone; + + +function Zone({ zone }: { zone: Point[] }) { + const savedTheme: string | null = localStorage.getItem('theme'); + + const shape = useMemo(() => { + const shape = new THREE.Shape(); + const points = zone.map(p => new THREE.Vector2(p.position[0], p.position[2])); + if (points.length < 3) return null; + shape.moveTo(points[0].x, points[0].y); + points.forEach((pt) => { shape.lineTo(pt.x, pt.y); }); + return shape; + }, [zone]); + + if (!shape) return null; + + return ( + + + + + + ); +} diff --git a/app/src/modules/builder/zone/zoneCreator/zoneCreator.tsx b/app/src/modules/builder/zone/zoneCreator/zoneCreator.tsx index a237b59..af0fea2 100644 --- a/app/src/modules/builder/zone/zoneCreator/zoneCreator.tsx +++ b/app/src/modules/builder/zone/zoneCreator/zoneCreator.tsx @@ -1,4 +1,14 @@ -import React from 'react' +import * as THREE from 'three' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useThree } from '@react-three/fiber'; +import { useActiveLayer, useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store'; +import { useSceneContext } from '../../../scene/sceneContext'; +import { useBuilderStore } from '../../../../store/builder/useBuilderStore'; +import { useParams } from 'react-router-dom'; +import { useVersionContext } from '../../version/versionContext'; +import { getUserData } from '../../../../functions/getUserData'; +import ReferencePoint from '../../point/reference/referencePoint'; +import ReferenceZone from './referenceZone'; function ZoneCreator() { return ( diff --git a/app/src/store/builder/useZoneStore.ts b/app/src/store/builder/useZoneStore.ts index 39ff2a0..148e88c 100644 --- a/app/src/store/builder/useZoneStore.ts +++ b/app/src/store/builder/useZoneStore.ts @@ -15,6 +15,7 @@ interface ZoneStore { setViewPort: (uuid: string, position: [number, number, number], target: [number, number, number]) => void; getZoneById: (uuid: string) => Zone | undefined; + getConnectedPoints: (uuid: string) => Point[]; } export const createZoneStore = () => { @@ -83,6 +84,17 @@ export const createZoneStore = () => { getZoneById: (uuid) => { return get().zones.find(z => z.zoneUuid === uuid); }, + + getConnectedPoints: (pointUuid) => { + const connected: Point[] = []; + for (const zone of get().zones) { + if (zone.points.some(p => p.pointUuid === pointUuid)) { + connected.push(...zone.points.filter(p => p.pointUuid !== pointUuid)); + } + } + return connected; + } + })) ); };