diff --git a/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx b/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx index 9a38111..10bf6fa 100644 --- a/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx +++ b/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx @@ -25,7 +25,7 @@ function ReferenceAisle({ tempPoints }: Readonly) { const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position); const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[0]?.position || null); - const { checkSnapForAisle } = usePointSnapping({ uuid: 'temp-aisle', pointType: 'Aisle', position: directionalSnap.position || [0, 0, 0] }); + const { snapAislePoint } = usePointSnapping({ uuid: 'temp-aisle', pointType: 'Aisle', position: directionalSnap.position || [0, 0, 0] }); useFrame(() => { if (toolMode === "Aisle" && toggleView && tempPoints.length === 1) { @@ -36,7 +36,7 @@ function ReferenceAisle({ tempPoints }: Readonly) { setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]); if (intersectionPoint) { - const snapped = checkSnapForAisle([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]); + const snapped = snapAislePoint([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]); if (snapped.isSnapped && snapped.snappedPoint) { finalPosition.current = snapped.position; diff --git a/app/src/modules/builder/geomentries/lines/getClosestIntersection.ts b/app/src/modules/builder/geomentries/lines/getClosestIntersection.ts index 587c472..afae9d5 100644 --- a/app/src/modules/builder/geomentries/lines/getClosestIntersection.ts +++ b/app/src/modules/builder/geomentries/lines/getClosestIntersection.ts @@ -5,11 +5,11 @@ import * as Types from "../../../../types/world/worldTypes"; function getClosestIntersection( intersects: Types.Vector3Array, point: Types.Vector3 -): Types.Vector3 | null { +): Types.Vector3 { ////////// A function that finds which point is closest from the intersects points that is given, Used in finding which point in a line is closest when clicked on a line during drawing ////////// - let closestNewPoint: THREE.Vector3 | null = null; + let closestNewPoint: THREE.Vector3 = point; let minDistance = Infinity; for (const intersect of intersects) { diff --git a/app/src/modules/builder/line/line.tsx b/app/src/modules/builder/line/line.tsx index 707e167..aa929d2 100644 --- a/app/src/modules/builder/line/line.tsx +++ b/app/src/modules/builder/line/line.tsx @@ -46,9 +46,10 @@ function Line({ points }: Readonly) { return ( diff --git a/app/src/modules/builder/point/helpers/useAisleDragSnap.tsx b/app/src/modules/builder/point/helpers/useAisleDragSnap.tsx deleted file mode 100644 index d97e9d4..0000000 --- a/app/src/modules/builder/point/helpers/useAisleDragSnap.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useCallback } from 'react'; -import * as THREE from 'three'; -import { useAisleStore } from '../../../../store/builder/useAisleStore'; - -const SNAP_DISTANCE_THRESHOLD = 0.5; // Distance threshold for snapping in meters - -const CAN_SNAP = true; // Whether snapping is enabled or not - -export function useAislePointSnapping(point: Point) { - const { getConnectedPoints } = useAisleStore(); - - const snapPosition = useCallback((newPosition: [number, number, number]): { - position: [number, number, number], - isSnapped: boolean, - snapSources: THREE.Vector3[] - } => { - if (!CAN_SNAP) return { position: newPosition, isSnapped: false, snapSources: [] }; - - const connectedPoints = getConnectedPoints(point.pointUuid); - 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 < SNAP_DISTANCE_THRESHOLD) { - if (!closestX || xDist < closestX.dist) { - closestX = { pos: cPos, dist: xDist }; - } - } - - if (zDist < 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 - }; - }, [point.pointUuid, getConnectedPoints]); - - return { snapPosition }; -} diff --git a/app/src/modules/builder/point/helpers/usePointSnapping.tsx b/app/src/modules/builder/point/helpers/usePointSnapping.tsx index 74328cb..4545fab 100644 --- a/app/src/modules/builder/point/helpers/usePointSnapping.tsx +++ b/app/src/modules/builder/point/helpers/usePointSnapping.tsx @@ -1,15 +1,106 @@ +import * as THREE from 'three'; import { useCallback } from 'react'; import { useAisleStore } from '../../../../store/builder/useAisleStore'; -import * as THREE from 'three'; import { useWallStore } from '../../../../store/builder/useWallStore'; -const SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters +const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters -const CAN_SNAP = true; // Whether snapping is enabled or not +const CAN_POINT_SNAP = true; // Whether snapping is enabled or not + +const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5; // Distance threshold for snapping in meters + +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 { aisles } = useAisleStore(); - const { walls } = useWallStore(); + const { aisles, getConnectedPoints: getConnectedAislePoints } = useAisleStore(); + const { walls, getConnectedPoints: getConnectedWallPoints } = useWallStore(); + + // Wall Snapping + + const getAllOtherWallPoints = useCallback(() => { + if (!currentPoint) return []; + return walls.flatMap(wall => + wall.points.filter(point => point.pointUuid !== currentPoint.uuid) + ); + }, [walls, currentPoint]); + + const snapWallPoint = useCallback((position: [number, number, number]) => { + if (!currentPoint || !CAN_POINT_SNAP) return { position: position, isSnapped: false, snappedPoint: null }; + + const otherPoints = getAllOtherWallPoints(); + 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 === 'Wall') { + return { position: point.position, isSnapped: true, snappedPoint: point }; + } + } + return { position: position, isSnapped: false, snappedPoint: null }; + }, [currentPoint, getAllOtherWallPoints]); + + const snapWallAngle = 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 = getConnectedWallPoints(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, getConnectedAislePoints]); + + // Aisle Snapping const getAllOtherAislePoints = useCallback(() => { if (!currentPoint) return []; @@ -19,24 +110,15 @@ export const usePointSnapping = (currentPoint: { uuid: string, pointType: string ); }, [aisles, currentPoint]); - const getAllOtherWallPoints = useCallback(() => { - if (!currentPoint) return []; - return walls.flatMap(wall => - wall.points.filter(point => point.pointUuid !== currentPoint.uuid) - ); - }, [walls, currentPoint]); - - const checkSnapForAisle = useCallback((position: [number, number, number]) => { - if (!currentPoint || !CAN_SNAP) return { position: position, isSnapped: false, snappedPoint: null }; + const snapAislePoint = useCallback((position: [number, number, number]) => { + if (!currentPoint || !CAN_POINT_SNAP) return { position: position, isSnapped: false, snappedPoint: null }; const otherPoints = getAllOtherAislePoints(); 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') { + if (distance <= POINT_SNAP_THRESHOLD && currentPoint.pointType === 'Aisle') { return { position: point.position, isSnapped: true, snappedPoint: point }; } } @@ -44,23 +126,71 @@ export const usePointSnapping = (currentPoint: { uuid: string, pointType: string return { position: position, isSnapped: false, snappedPoint: null }; }, [currentPoint, getAllOtherAislePoints]); - const checkSnapForWall = useCallback((position: [number, number, number]) => { - if (!currentPoint || !CAN_SNAP) return { position: position, isSnapped: false, snappedPoint: null }; + const snapAisleAngle = 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 otherPoints = getAllOtherWallPoints(); - 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 === 'Wall') { - return { position: point.position, isSnapped: true, snappedPoint: point }; + const connectedPoints = getConnectedAislePoints(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 }; + } } } - return { position: position, isSnapped: false, snappedPoint: null }; - }, [currentPoint, getAllOtherWallPoints]); + + 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, getConnectedAislePoints]); return { - checkSnapForAisle, - checkSnapForWall, + snapAislePoint, + snapAisleAngle, + snapWallPoint, + snapWallAngle, }; }; \ 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 c026f94..f92ab74 100644 --- a/app/src/modules/builder/point/point.tsx +++ b/app/src/modules/builder/point/point.tsx @@ -7,7 +7,6 @@ import { useAisleStore } from '../../../store/builder/useAisleStore'; import { useThree } from '@react-three/fiber'; import { useBuilderStore } from '../../../store/builder/useBuilderStore'; import { usePointSnapping } from './helpers/usePointSnapping'; -import { useAislePointSnapping } from './helpers/useAisleDragSnap'; import { useWallStore } from '../../../store/builder/useWallStore'; import { deleteAisleApi } from '../../../services/factoryBuilder/aisle/deleteAisleApi'; import { useParams } from 'react-router-dom'; @@ -21,8 +20,7 @@ function Point({ point }: { readonly point: Point }) { const { toolMode } = useToolMode(); const { setPosition: setAislePosition, removePoint: removeAislePoint, getAislesByPointId } = useAisleStore(); const { setPosition: setWallPosition, removePoint: removeWallPoint } = useWallStore(); - const { snapPosition } = useAislePointSnapping(point); - const { checkSnapForAisle } = usePointSnapping({ uuid: point.pointUuid, pointType: point.pointType, position: point.position }); + const { snapAislePoint, snapAisleAngle, snapWallPoint, snapWallAngle } = usePointSnapping({ uuid: point.pointUuid, pointType: point.pointType, position: point.position }); const { hoveredPoint, setHoveredPoint } = useBuilderStore(); const { projectId } = useParams(); const boxScale: [number, number, number] = Constants.pointConfig.boxScale; @@ -96,8 +94,8 @@ function Point({ point }: { readonly point: Point }) { if (point.pointType === 'Aisle') { if (position) { const newPosition: [number, number, number] = [position.x, position.y, position.z]; - const aisleSnappedPosition = snapPosition(newPosition); - const finalSnappedPosition = checkSnapForAisle(aisleSnappedPosition.position); + const aisleSnappedPosition = snapAisleAngle(newPosition); + const finalSnappedPosition = snapAislePoint(aisleSnappedPosition.position); setAislePosition(point.pointUuid, finalSnappedPosition.position); @@ -105,8 +103,10 @@ function Point({ point }: { readonly point: Point }) { } else if (point.pointType === 'Wall') { if (position) { const newPosition: [number, number, number] = [position.x, position.y, position.z]; + const wallSnappedPosition = snapWallAngle(newPosition); + const finalSnappedPosition = snapWallPoint(wallSnappedPosition.position); - setWallPosition(point.pointUuid, newPosition); + setWallPosition(point.pointUuid, finalSnappedPosition.position); } } } @@ -179,7 +179,7 @@ function Point({ point }: { readonly point: Point }) { } }} onPointerOut={() => { - if (hoveredPoint && hoveredPoint.pointUuid === point.pointUuid) { + if (hoveredPoint) { setHoveredPoint(null); } setIsHovered(false) diff --git a/app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts b/app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts index 4e815ad..5ae9a21 100644 --- a/app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts +++ b/app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts @@ -2,218 +2,130 @@ import { useMemo } from 'react'; import * as turf from '@turf/turf'; export function useWallClassification(walls: Walls) { - // Find all minimal rooms from the given walls + const findRooms = () => { if (walls.length < 3) return []; - // Build a graph of point connections - const graph = new Map(); - const pointMap = new Map(); + // Map pointUuid to list of connected line segments + const pointMap = new Map(); - walls.forEach(wall => { - const [p1, p2] = wall.points; - pointMap.set(p1.pointUuid, p1); - pointMap.set(p2.pointUuid, p2); + for (const wall of walls) { + for (const point of wall.points) { + const list = pointMap.get(point.pointUuid) || []; + list.push(wall); + pointMap.set(point.pointUuid, list); + } + } - // Add connection from p1 to p2 - if (!graph.has(p1.pointUuid)) graph.set(p1.pointUuid, []); - graph.get(p1.pointUuid)?.push(p2.pointUuid); + // Create graph of connected walls using pointUuid + const visited = new Set(); + const mergedLineStrings = []; - // Add connection from p2 to p1 - if (!graph.has(p2.pointUuid)) graph.set(p2.pointUuid, []); - graph.get(p2.pointUuid)?.push(p1.pointUuid); + const wallKey = (p1: Point, p2: Point) => `${p1.pointUuid}-${p2.pointUuid}`; + + for (const wall of walls) { + const key = wallKey(wall.points[0], wall.points[1]); + if (visited.has(key) || visited.has(wallKey(wall.points[1], wall.points[0]))) continue; + + let line: Point[] = [...wall.points]; + visited.add(key); + + // Try to extend the line forward and backward by matching endpoints + let extended = true; + while (extended) { + extended = false; + + const last = line[line.length - 1]; + const nextWalls = pointMap.get(last.pointUuid) || []; + + for (const next of nextWalls) { + const [n1, n2] = next.points; + const nextKey = wallKey(n1, n2); + if (visited.has(nextKey) || visited.has(wallKey(n2, n1))) continue; + + if (n1.pointUuid === last.pointUuid && n2.pointUuid !== line[line.length - 2]?.pointUuid) { + line.push(n2); + visited.add(nextKey); + extended = true; + break; + } else if (n2.pointUuid === last.pointUuid && n1.pointUuid !== line[line.length - 2]?.pointUuid) { + line.push(n1); + visited.add(nextKey); + extended = true; + break; + } + } + + const first = line[0]; + const prevWalls = pointMap.get(first.pointUuid) || []; + + for (const prev of prevWalls) { + const [p1, p2] = prev.points; + const prevKey = wallKey(p1, p2); + if (visited.has(prevKey) || visited.has(wallKey(p2, p1))) continue; + + if (p1.pointUuid === first.pointUuid && p2.pointUuid !== line[1]?.pointUuid) { + line.unshift(p2); + visited.add(prevKey); + extended = true; + break; + } else if (p2.pointUuid === first.pointUuid && p1.pointUuid !== line[1]?.pointUuid) { + line.unshift(p1); + visited.add(prevKey); + extended = true; + break; + } + } + } + + // Create merged LineString + const coords = line.map(p => [p.position[0], p.position[2]]); + mergedLineStrings.push(turf.lineString(coords, { + pointUuids: line.map(p => p.pointUuid) + })); + } + + const lineStrings = turf.featureCollection(mergedLineStrings); + + // Now polygonize merged line strings + const polygons = turf.polygonize(lineStrings); + + const rooms: Point[][] = []; + + polygons.features.forEach(feature => { + if (feature.geometry.type === 'Polygon') { + const coordinates = feature.geometry.coordinates[0]; + const roomPoints: Point[] = []; + + for (const [x, z] of coordinates) { + const matchingPoint = walls.flatMap(wall => wall.points) + .find(p => + p.position[0].toFixed(10) === x.toFixed(10) && + p.position[2].toFixed(10) === z.toFixed(10) + ); + + if (matchingPoint) { + roomPoints.push(matchingPoint); + } + } + + if (roomPoints.length > 0 && + roomPoints[0].pointUuid !== roomPoints[roomPoints.length - 1].pointUuid) { + roomPoints.push(roomPoints[0]); + } + + if (roomPoints.length >= 4) { + rooms.push(roomPoints); + } + } }); - // Find all minimal cycles (rooms) in the graph - const allCycles: string[][] = []; - - const findCycles = (startNode: string, currentNode: string, path: string[], depth = 0) => { - if (depth > 20) return; // Prevent infinite recursion - - path.push(currentNode); - - const neighbors = graph.get(currentNode) || []; - - for (const neighbor of neighbors) { - if (path.length > 2 && neighbor === startNode) { - // Found a cycle that returns to start - const cycle = [...path, neighbor]; - - // Check if this is a new unique cycle - if (!cycleExists(allCycles, cycle)) { - allCycles.push(cycle); - } - continue; - } - - if (!path.includes(neighbor)) { - findCycles(startNode, neighbor, [...path], depth + 1); - } - } - }; - - // Start cycle detection from each node - for (const [pointUuid] of graph) { - findCycles(pointUuid, pointUuid, []); - } - - // Convert cycles to Point arrays and validate them - const potentialRooms = allCycles - .map(cycle => cycle.map(uuid => pointMap.get(uuid)!)) - .filter(room => isValidRoom(room)); - - const uniqueRooms = removeDuplicateRooms(potentialRooms); - - // ✅ New logic that only removes redundant supersets - const filteredRooms = removeSupersetLikeRooms(uniqueRooms, walls); - - return filteredRooms; - + return rooms; }; - const removeSupersetLikeRooms = (rooms: Point[][], walls: Wall[]): Point[][] => { - const toRemove = new Set(); - - const getPolygon = (points: Point[]) => - turf.polygon([points.map(p => [p.position[0], p.position[2]])]); - - const getWallSet = (room: Point[]) => { - const set = new Set(); - for (let i = 0; i < room.length - 1; i++) { - const p1 = room[i].pointUuid; - const p2 = room[i + 1].pointUuid; - - const wall = walls.find(w => - (w.points[0].pointUuid === p1 && w.points[1].pointUuid === p2) || - (w.points[0].pointUuid === p2 && w.points[1].pointUuid === p1) - ); - - if (wall) { - set.add(wall.wallUuid); - } - } - return set; - }; - - const roomPolygons = rooms.map(getPolygon); - const roomAreas = roomPolygons.map(poly => turf.area(poly)); - const wallSets = rooms.map(getWallSet); - - // First, identify all rooms that are completely contained within others - for (let i = 0; i < rooms.length; i++) { - for (let j = 0; j < rooms.length; j++) { - if (i === j) continue; - - // If room i completely contains room j - if (turf.booleanContains(roomPolygons[i], roomPolygons[j])) { - // Check if the contained room shares most of its walls with the containing room - const sharedWalls = [...wallSets[j]].filter(w => wallSets[i].has(w)); - const shareRatio = sharedWalls.length / wallSets[j].size; - - // If they share more than 50% of walls, mark the larger one for removal - // UNLESS the smaller one is significantly smaller (likely a real room) - if (shareRatio > 0.5 && (roomAreas[i] / roomAreas[j] > 2)) { - toRemove.add(i); - } - } - } - } - - // Second pass: handle cases where a room is divided by segmented walls - for (let i = 0; i < rooms.length; i++) { - if (toRemove.has(i)) continue; - - for (let j = 0; j < rooms.length; j++) { - if (i === j || toRemove.has(j)) continue; - - // Check if these rooms share a significant portion of walls - const sharedWalls = [...wallSets[i]].filter(w => wallSets[j].has(w)); - const shareRatio = Math.max( - sharedWalls.length / wallSets[i].size, - sharedWalls.length / wallSets[j].size - ); - - // If they share more than 30% of walls and one is much larger - if (shareRatio > 0.3) { - const areaRatio = roomAreas[i] / roomAreas[j]; - if (areaRatio > 2) { - // The larger room is likely the undivided version - toRemove.add(i); - } else if (areaRatio < 0.5) { - // The smaller room might be an artifact - toRemove.add(j); - } - } - } - } - - return rooms.filter((_, idx) => !toRemove.has(idx)); - }; - - // Check if a cycle already exists in our list (considering different orders) - const cycleExists = (allCycles: string[][], newCycle: string[]) => { - const newSet = new Set(newCycle); - - return allCycles.some(existingCycle => { - if (existingCycle.length !== newCycle.length) return false; - const existingSet = new Set(existingCycle); - return setsEqual(newSet, existingSet); - }); - }; - - // Check if two sets are equal - const setsEqual = (a: Set, b: Set) => { - if (a.size !== b.size) return false; - for (const item of a) if (!b.has(item)) return false; - return true; - }; - - // Remove duplicate rooms (same set of points in different orders) - const removeDuplicateRooms = (rooms: Point[][]) => { - const uniqueRooms: Point[][] = []; - const roomHashes = new Set(); - - for (const room of rooms) { - // Create a consistent hash for the room regardless of point order - const hash = room - .map(p => p.pointUuid) - .sort() - .join('-'); - - if (!roomHashes.has(hash)) { - roomHashes.add(hash); - uniqueRooms.push(room); - } - } - - return uniqueRooms; - }; - - // Check if a room is valid (closed, non-self-intersecting polygon) - const isValidRoom = (points: Point[]): boolean => { - // Must have at least 4 points (first and last are same) - if (points.length < 4) return false; - - // Must be a closed loop - if (points[0].pointUuid !== points[points.length - 1].pointUuid) { - return false; - } - - try { - const coordinates = points.map(p => [p.position[0], p.position[2]]); - const polygon = turf.polygon([coordinates]); - return turf.booleanValid(polygon); - } catch (e) { - return false; - } - }; - - // Rest of the implementation remains the same... const rooms = useMemo(() => findRooms(), [walls]); - // Determine wall orientation relative to room const findWallType = (wall: Wall) => { - // Find all rooms that contain this wall const containingRooms = rooms.filter(room => { for (let i = 0; i < room.length - 1; i++) { const p1 = room[i]; @@ -244,11 +156,7 @@ export function useWallClassification(walls: Walls) { } }; - // Update the other functions to use the new return type - const getWallType = (wall: Wall): { - type: string; - rooms: Point[][]; - } => { + const getWallType = (wall: Wall) => { return findWallType(wall); }; @@ -261,11 +169,32 @@ export function useWallClassification(walls: Walls) { return findWallType(wall).type === 'segment'; }; + const isWallFlipped = (wall: Wall): boolean => { + const wallType = findWallType(wall); + if (wallType.type === 'segment') return false; + + for (const room of wallType.rooms) { + for (let i = 0; i < room.length - 1; i++) { + const p1 = room[i]; + const p2 = room[i + 1]; + if (wall.points[0].pointUuid === p1.pointUuid && wall.points[1].pointUuid === p2.pointUuid) { + return false; + } + if (wall.points[0].pointUuid === p2.pointUuid && wall.points[1].pointUuid === p1.pointUuid) { + return true; + } + } + } + return false; + }; + + return { rooms, getWallType, isRoomWall, isSegmentWall, findRooms, + isWallFlipped }; } \ No newline at end of file diff --git a/app/src/modules/builder/wall/Instances/instance/wall.tsx b/app/src/modules/builder/wall/Instances/instance/wall.tsx index c28366d..9f857df 100644 --- a/app/src/modules/builder/wall/Instances/instance/wall.tsx +++ b/app/src/modules/builder/wall/Instances/instance/wall.tsx @@ -2,8 +2,8 @@ import * as THREE from 'three'; import { useMemo, useRef, useState } from 'react'; import * as Constants from '../../../../../types/world/worldConstants'; -import insideMaterial from '../../../../../assets/textures/floor/wall-tex.png'; -import outsideMaterial from '../../../../../assets/textures/floor/factory wall texture.jpg'; +import defaultMaterial from '../../../../../assets/textures/floor/wall-tex.png'; +import material1 from '../../../../../assets/textures/floor/factory wall texture.jpg'; import useWallGeometry from './helpers/useWallGeometry'; import { useWallStore } from '../../../../../store/builder/useWallStore'; import { useWallClassification } from './helpers/useWallClassification'; @@ -14,14 +14,18 @@ import { Base } from '@react-three/csg'; function Wall({ wall }: { readonly wall: Wall }) { const { walls } = useWallStore(); - const { getWallType } = useWallClassification(walls); + const { getWallType, isWallFlipped } = useWallClassification(walls); const wallType = getWallType(wall); - const [startPoint, endPoint] = wall.points; const [visible, setVisible] = useState(true); const { wallVisibility } = useWallVisibility(); const meshRef = useRef(); const { camera } = useThree(); + const wallFlipped = isWallFlipped(wall); + + const [rawStart, rawEnd] = wall.points; + const [startPoint, endPoint] = wallFlipped ? [rawStart, rawEnd] : [rawEnd, rawStart]; + const startX = startPoint.position[0]; const startZ = startPoint.position[2]; const endX = endPoint.position[0]; @@ -36,13 +40,13 @@ function Wall({ wall }: { readonly wall: Wall }) { const textureLoader = new THREE.TextureLoader(); - const [insideWallTexture, outsideWallTexture] = useMemo(() => { - const inside = textureLoader.load(insideMaterial); + const [defaultWallTexture, material1WallTexture] = useMemo(() => { + const inside = textureLoader.load(defaultMaterial); inside.wrapS = inside.wrapT = THREE.RepeatWrapping; inside.repeat.set(wallLength / 10, wall.wallHeight / 10); inside.colorSpace = THREE.SRGBColorSpace; - const outside = textureLoader.load(outsideMaterial); + const outside = textureLoader.load(material1); outside.wrapS = outside.wrapT = THREE.RepeatWrapping; outside.repeat.set(wallLength / 10, wall.wallHeight / 10); outside.colorSpace = THREE.SRGBColorSpace; @@ -51,27 +55,24 @@ function Wall({ wall }: { readonly wall: Wall }) { }, [wallLength, wall.wallHeight]); const materials = useMemo(() => { - const frontMaterial = insideWallTexture; - - const backMaterial = outsideWallTexture; return [ new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide, - map: frontMaterial + map: wall.insideMaterial === 'Default Material' ? defaultWallTexture : material1WallTexture, }), new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide, - map: backMaterial + map: wall.outsideMaterial === 'Default Material' ? defaultWallTexture : material1WallTexture, }), new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Left new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Right new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Top new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }) // Bottom ]; - }, [insideWallTexture, outsideWallTexture, wall]); + }, [defaultWallTexture, material1WallTexture, wall]); const geometry = useWallGeometry(wallLength, wall.wallHeight, wall.wallThickness); @@ -83,6 +84,7 @@ function Wall({ wall }: { readonly wall: Wall }) { if (!wallVisibility && wallType.type === 'room') { meshRef.current.getWorldDirection(v); camera.getWorldDirection(u); + if (!u || !v) return; setVisible((2 * v.dot(u)) <= 0.1); } else { @@ -112,7 +114,7 @@ function Wall({ wall }: { readonly wall: Wall }) { scale={[decal.decalScale, decal.decalScale, 0.001]} > ) { const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position); const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[0]?.position || null); - const { checkSnapForWall } = usePointSnapping({ uuid: 'temp-wall', pointType: 'Wall', position: directionalSnap.position || [0, 0, 0] }); + const { snapWallPoint } = usePointSnapping({ uuid: 'temp-wall', pointType: 'Wall', position: directionalSnap.position || [0, 0, 0] }); useFrame(() => { if (toolMode === 'Wall' && toggleView && tempPoints.length === 1) { @@ -37,7 +37,7 @@ function ReferenceWall({ tempPoints }: Readonly) { setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]); if (intersectionPoint) { - const snapped = checkSnapForWall([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]); + const snapped = snapWallPoint([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]); if (snapped.isSnapped && snapped.snappedPoint) { finalPosition.current = snapped.position; @@ -68,8 +68,8 @@ function ReferenceWall({ tempPoints }: Readonly) { setTempWall({ wallUuid: 'temp-wall', points: wallPoints, - outSideMaterial: 'default', - inSideMaterial: 'default', + outsideMaterial: 'default', + insideMaterial: 'default', wallThickness: wallThickness, wallHeight: wallHeight, decals: [] diff --git a/app/src/modules/builder/wall/wallCreator/wallCreator.tsx b/app/src/modules/builder/wall/wallCreator/wallCreator.tsx index 7c4c6ce..53a0f9e 100644 --- a/app/src/modules/builder/wall/wallCreator/wallCreator.tsx +++ b/app/src/modules/builder/wall/wallCreator/wallCreator.tsx @@ -2,10 +2,12 @@ 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 * as Constants from '../../../../types/world/worldConstants'; import { useWallStore } from '../../../../store/builder/useWallStore'; import { useBuilderStore } from '../../../../store/builder/useBuilderStore'; import ReferencePoint from '../../point/reference/referencePoint'; import ReferenceWall from './referenceWall'; +import getClosestIntersection from '../../geomentries/lines/getClosestIntersection'; function WallCreator() { const { scene, camera, raycaster, gl, pointer } = useThree(); @@ -14,10 +16,10 @@ function WallCreator() { const { toolMode } = useToolMode(); const { activeLayer } = useActiveLayer(); const { socket } = useSocketStore(); - const { addWall, getWallPointById } = useWallStore(); + const { addWall, getWallPointById, removeWall, getWallByPoints } = useWallStore(); const drag = useRef(false); const isLeftMouseDown = useRef(false); - const { wallThickness, wallHeight, snappedPosition, snappedPoint } = useBuilderStore(); + const { wallThickness, wallHeight, insideMaterial, outsideMaterial, snappedPosition, snappedPoint } = useBuilderStore(); const [tempPoints, setTempPoints] = useState([]); const [isCreating, setIsCreating] = useState(false); @@ -52,7 +54,96 @@ function WallCreator() { let position = raycaster.ray.intersectPlane(plane, intersectionPoint); if (!position) return; - const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Wall-Point'); + const pointIntersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Wall-Point'); + + const wallIntersect = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Wall-Line'); + if (wallIntersect && !pointIntersects) { + const wall = getWallByPoints(wallIntersect.object.userData.points); + if (wall) { + const ThroughPoint = wallIntersect.object.userData.path.getPoints(Constants.lineConfig.lineIntersectionPoints); + let intersectionPoint = getClosestIntersection(ThroughPoint, wallIntersect.point); + const point1Vec = new THREE.Vector3(...wall.points[0].position); + const point2Vec = new THREE.Vector3(...wall.points[1].position); + + const lineDir = new THREE.Vector3().subVectors(point2Vec, point1Vec).normalize(); + + const point1ToIntersect = new THREE.Vector3().subVectors(intersectionPoint, point1Vec); + + const dotProduct = point1ToIntersect.dot(lineDir); + const projection = new THREE.Vector3().copy(lineDir).multiplyScalar(dotProduct).add(point1Vec); + + const lineLength = point1Vec.distanceTo(point2Vec); + let t = point1Vec.distanceTo(projection) / lineLength; + t = Math.max(0, Math.min(1, t)); + + const closestPoint = new THREE.Vector3().lerpVectors(point1Vec, point2Vec, t); + + removeWall(wall.wallUuid); + + const point1: Point = { + pointUuid: wall.points[0].pointUuid, + pointType: 'Wall', + position: wall.points[0].position, + layer: wall.points[0].layer + }; + + const point2: Point = { + pointUuid: wall.points[1].pointUuid, + pointType: 'Wall', + position: wall.points[1].position, + layer: wall.points[1].layer + }; + + const newPoint: Point = { + pointUuid: THREE.MathUtils.generateUUID(), + pointType: 'Wall', + position: closestPoint.toArray(), + layer: activeLayer + }; + + if (tempPoints.length === 0) { + setTempPoints([newPoint]); + setIsCreating(true); + } else { + const wall1: Wall = { + wallUuid: THREE.MathUtils.generateUUID(), + points: [tempPoints[0], newPoint], + outsideMaterial: insideMaterial, + insideMaterial: outsideMaterial, + wallThickness: wallThickness, + wallHeight: wallHeight, + decals: [] + }; + addWall(wall1); + + const wall2: Wall = { + wallUuid: THREE.MathUtils.generateUUID(), + points: [point1, newPoint], + outsideMaterial: insideMaterial, + insideMaterial: outsideMaterial, + wallThickness: wallThickness, + wallHeight: wallHeight, + decals: [] + } + addWall(wall2); + + const wall3: Wall = { + wallUuid: THREE.MathUtils.generateUUID(), + points: [point2, newPoint], + outsideMaterial: insideMaterial, + insideMaterial: outsideMaterial, + wallThickness: wallThickness, + wallHeight: wallHeight, + decals: [] + } + addWall(wall3); + + setTempPoints([newPoint]); + } + + return; + } + } const newPoint: Point = { pointUuid: THREE.MathUtils.generateUUID(), @@ -71,8 +162,8 @@ function WallCreator() { newPoint.position = snappedPosition; } - if (intersects && !snappedPoint) { - const point = getWallPointById(intersects.object.uuid); + if (pointIntersects && !snappedPoint) { + const point = getWallPointById(pointIntersects.object.uuid); if (point) { newPoint.pointUuid = point.pointUuid; newPoint.position = point.position; @@ -87,8 +178,8 @@ function WallCreator() { const wall: Wall = { wallUuid: THREE.MathUtils.generateUUID(), points: [tempPoints[0], newPoint], - outSideMaterial: 'default', - inSideMaterial: 'default', + outsideMaterial: insideMaterial, + insideMaterial: outsideMaterial, wallThickness: wallThickness, wallHeight: wallHeight, decals: [] @@ -114,10 +205,8 @@ function WallCreator() { canvasElement.addEventListener("click", onMouseClick); canvasElement.addEventListener("contextmenu", onContext); } else { - if (tempPoints.length > 0 || isCreating) { - setTempPoints([]); - setIsCreating(false); - } + setTempPoints([]); + setIsCreating(false); canvasElement.removeEventListener("mousedown", onMouseDown); canvasElement.removeEventListener("mouseup", onMouseUp); canvasElement.removeEventListener("mousemove", onMouseMove); diff --git a/app/src/store/builder/useBuilderStore.ts b/app/src/store/builder/useBuilderStore.ts index 245bf1d..a23198b 100644 --- a/app/src/store/builder/useBuilderStore.ts +++ b/app/src/store/builder/useBuilderStore.ts @@ -13,9 +13,12 @@ interface BuilderState { wallThickness: number; wallHeight: number; + outsideMaterial: string; + insideMaterial: string; setWallThickness: (thickness: number) => void; setWallHeight: (height: number) => void; + setWallMaterial: (material: string, side: 'inside' | 'outside') => void; // Aisle @@ -82,19 +85,28 @@ export const useBuilderStore = create()( wallThickness: 0.1, wallHeight: 7, + outsideMaterial: 'Default Material', + insideMaterial: 'Default Material', setWallThickness: (thickness: number) => { set((state) => { state.wallThickness = thickness; }) }, - + setWallHeight: (height: number) => { set((state) => { state.wallHeight = height; }) }, + setWallMaterial: (material: string, side: 'inside' | 'outside') => { + set((state) => { + if (side === 'outside') state.outsideMaterial = material; + else state.insideMaterial = material; + }); + }, + // Aisle selectedAisle: null, diff --git a/app/src/store/builder/useWallStore.tsx b/app/src/store/builder/useWallStore.tsx index 682cb8f..029ace6 100644 --- a/app/src/store/builder/useWallStore.tsx +++ b/app/src/store/builder/useWallStore.tsx @@ -19,6 +19,8 @@ interface WallStore { setLayer: (pointUuid: string, layer: number) => void; getWallById: (uuid: string) => Wall | undefined; + getWallByPointId: (uuid: string) => Wall | undefined; + getWallByPoints: (points: Point[]) => Wall | undefined; getWallPointById: (uuid: string) => Point | undefined; getConnectedPoints: (uuid: string) => Point[]; } @@ -135,6 +137,25 @@ export const useWallStore = create()( return get().walls.find(w => w.wallUuid === uuid); }, + getWallByPointId: (uuid) => { + for (const wall of get().walls) { + if (wall.points.some(p => p.pointUuid === uuid)) { + return wall; + } + } + return undefined; + }, + + getWallByPoints: (point) => { + for (const wall of get().walls) { + if (((wall.points[0].pointUuid === point[0].pointUuid) || (wall.points[1].pointUuid === point[0].pointUuid)) && + ((wall.points[0].pointUuid === point[1].pointUuid) || (wall.points[1].pointUuid === point[1].pointUuid))) { + return wall; + } + } + return undefined; + }, + getWallPointById: (uuid) => { for (const wall of get().walls) { const point = wall.points.find(p => p.pointUuid === uuid); diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts index 2cf06f0..5e91468 100644 --- a/app/src/types/builderTypes.d.ts +++ b/app/src/types/builderTypes.d.ts @@ -59,8 +59,8 @@ interface Decal { interface Wall { wallUuid: string; points: [Point, Point]; - outSideMaterial: string; - inSideMaterial: string; + outsideMaterial: string; + insideMaterial: string; wallThickness: number; wallHeight: number; decals: Decal[]