From a69fd2af92b078487167c00e364491d6e3e264df Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Wed, 4 Jun 2025 15:19:37 +0530 Subject: [PATCH] feat: Add wall classification and geometry handling, update wall rendering logic --- app/src/modules/builder/builder.tsx | 2 +- .../builder/geomentries/walls/loadWalls.ts | 13 +- .../modules/builder/groups/floorPlanGroup.tsx | 2 +- app/src/modules/builder/point/point.tsx | 14 +- .../instance/helpers/useWallClassification.ts | 176 ++++++++++++++++++ .../instance/helpers/useWallGeometry.ts | 64 +++++++ .../builder/wall/Instances/instance/wall.tsx | 92 +++++++++ .../wall/Instances/instance/wallInstance.tsx | 13 +- .../builder/wall/Instances/wallInstances.tsx | 29 ++- app/src/store/builder/useBuilderStore.ts | 2 +- 10 files changed, 387 insertions(+), 20 deletions(-) create mode 100644 app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts create mode 100644 app/src/modules/builder/wall/Instances/instance/helpers/useWallGeometry.ts create mode 100644 app/src/modules/builder/wall/Instances/instance/wall.tsx diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index 2a09bef..3f37de9 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -280,7 +280,7 @@ export default function Builder() { plane={plane} /> - + {/* */} diff --git a/app/src/modules/builder/geomentries/walls/loadWalls.ts b/app/src/modules/builder/geomentries/walls/loadWalls.ts index d594e5a..14d72c4 100644 --- a/app/src/modules/builder/geomentries/walls/loadWalls.ts +++ b/app/src/modules/builder/geomentries/walls/loadWalls.ts @@ -121,9 +121,20 @@ async function loadWalls( } }); setWalls(Walls); - }else{ + } else { setWalls([]); } } export default loadWalls; + + +// A----- B----- C +// | | | +// | | | +// | | | +// F----- E----- D + +// 1. A -> B, B -> C, C -> D, D -> E, E -> F, F -> A, B -> E + +// 2. E -> F, F -> A, A -> B, B -> E, E -> D, D -> C, C -> B \ No newline at end of file diff --git a/app/src/modules/builder/groups/floorPlanGroup.tsx b/app/src/modules/builder/groups/floorPlanGroup.tsx index 6859f74..74f0783 100644 --- a/app/src/modules/builder/groups/floorPlanGroup.tsx +++ b/app/src/modules/builder/groups/floorPlanGroup.tsx @@ -157,7 +157,7 @@ const FloorPlanGroup = ({ floorPlanGroup, floorPlanGroupLine, floorPlanGroupPoin } if (toolMode === "Wall") { - // drawWall(raycaster, plane, floorPlanGroupPoint, snappedPoint, isSnapped, isSnappedUUID, line, ispreSnapped, anglesnappedPoint, isAngleSnapped, lines, floorPlanGroupLine, floorPlanGroup, ReferenceLineMesh, LineCreated, currentLayerPoint, dragPointControls, setNewLines, setDeletedLines, activeLayer, socket); + drawWall(raycaster, plane, floorPlanGroupPoint, snappedPoint, isSnapped, isSnappedUUID, line, ispreSnapped, anglesnappedPoint, isAngleSnapped, lines, floorPlanGroupLine, floorPlanGroup, ReferenceLineMesh, LineCreated, currentLayerPoint, dragPointControls, setNewLines, setDeletedLines, activeLayer, socket); } if (toolMode === "Floor") { diff --git a/app/src/modules/builder/point/point.tsx b/app/src/modules/builder/point/point.tsx index 0a79e7e..a2d17cd 100644 --- a/app/src/modules/builder/point/point.tsx +++ b/app/src/modules/builder/point/point.tsx @@ -8,6 +8,7 @@ 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'; function Point({ point }: { readonly point: Point }) { const materialRef = useRef(null); @@ -15,7 +16,8 @@ function Point({ point }: { readonly point: Point }) { const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); const [isHovered, setIsHovered] = useState(false); const { toolMode } = useToolMode(); - const { setPosition, removePoint } = useAisleStore(); + const { setPosition: setAislePosition, removePoint: removeAislePoint } = useAisleStore(); + const { setPosition: setWallPosition, removePoint: removeWallPoint } = useWallStore(); const { snapPosition } = useAislePointSnapping(point); const { checkSnapForAisle } = usePointSnapping({ uuid: point.pointUuid, pointType: point.pointType, position: point.position }); const { hoveredPoint, setHoveredPoint } = useBuilderStore(); @@ -95,7 +97,13 @@ function Point({ point }: { readonly point: Point }) { const aisleSnappedPosition = snapPosition(newPosition); const finalSnappedPosition = checkSnapForAisle(aisleSnappedPosition.position); - setPosition(point.pointUuid, finalSnappedPosition.position); + setAislePosition(point.pointUuid, finalSnappedPosition.position); + } + } else if (point.pointType === 'Wall') { + if (position) { + const newPosition: [number, number, number] = [position.x, position.y, position.z]; + + setWallPosition(point.pointUuid, newPosition); } } } @@ -109,7 +117,7 @@ function Point({ point }: { readonly point: Point }) { const handlePointClick = (point: Point) => { if (deletePointOrLine) { if (point.pointType === 'Aisle') { - const removedAisles = removePoint(point.pointUuid); + const removedAisles = removeAislePoint(point.pointUuid); if (removedAisles.length > 0) { setHoveredPoint(null); } diff --git a/app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts b/app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts new file mode 100644 index 0000000..0e11aff --- /dev/null +++ b/app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts @@ -0,0 +1,176 @@ +import { useMemo } from 'react'; +import * as THREE from 'three'; +import * as turf from '@turf/turf'; + +export function useWallClassification(walls: Walls) { + // Find all rooms from the given walls + const findRooms = () => { + if (walls.length < 3) return []; + + const pointMap = new Map(); + const connections = new Map(); + + // Build connection graph + walls.forEach(wall => { + const [p1, p2] = wall.points; + if (!pointMap.has(p1.pointUuid)) pointMap.set(p1.pointUuid, p1); + if (!pointMap.has(p2.pointUuid)) pointMap.set(p2.pointUuid, p2); + + if (!connections.has(p1.pointUuid)) connections.set(p1.pointUuid, []); + if (!connections.has(p2.pointUuid)) connections.set(p2.pointUuid, []); + + connections.get(p1.pointUuid)?.push(p2.pointUuid); + connections.get(p2.pointUuid)?.push(p1.pointUuid); + }); + console.log('connections: ', connections); + + const visited = new Set(); + const rooms: Point[][] = []; + + // Modified DFS to find all possible cycles + const findAllCycles = (current: string, path: string[]) => { + visited.add(current); + path.push(current); + + const neighbors = connections.get(current) || []; + for (const neighbor of neighbors) { + if (path.length > 2 && neighbor === path[0]) { + // Found a cycle (potential room) + const roomPoints = [...path, neighbor].map(uuid => pointMap.get(uuid)!); + + // Check if this room is valid and not already found + if (isValidRoom(roomPoints) && !roomAlreadyExists(rooms, roomPoints)) { + rooms.push(roomPoints); + } + continue; + } + + if (!path.includes(neighbor)) { + findAllCycles(neighbor, [...path]); + } + } + }; + + // Start from each point to find all possible cycles + for (const [pointUuid] of connections) { + if (!visited.has(pointUuid)) { + findAllCycles(pointUuid, []); + } + } + + return rooms; + }; + + // Helper function to check if room is valid + const isValidRoom = (roomPoints: Point[]) => { + if (roomPoints.length < 4) return false; + const coordinates = roomPoints.map(p => [p.position[0], p.position[2]]); + const polygon = turf.polygon([coordinates]); + return turf.booleanValid(polygon); + }; + + // Helper function to check for duplicate rooms + const roomAlreadyExists = (existingRooms: Point[][], newRoom: Point[]) => { + const newRoomIds = newRoom.map(p => p.pointUuid).sort().join('-'); + return existingRooms.some(room => { + const roomIds = room.map(p => p.pointUuid).sort().join('-'); + return roomIds === newRoomIds; + }); + }; + + const rooms = useMemo(() => findRooms(), [walls]); + + // Modified to track all rooms a wall belongs to + const getWallOrientation = (wall: Wall) => { + const [p1, p2] = wall.points; + const orientations: Array<{ isOutsideFacing: boolean }> = []; + + for (const room of rooms) { + for (let i = 0; i < room.length - 1; i++) { + const roomP1 = room[i]; + const roomP2 = room[i + 1]; + + // Check both directions + if ((p1.pointUuid === roomP1.pointUuid && p2.pointUuid === roomP2.pointUuid) || + (p1.pointUuid === roomP2.pointUuid && p2.pointUuid === roomP1.pointUuid)) { + + const roomPoints = room.map(p => new THREE.Vector3(p.position[0], 0, p.position[2])); + const centroid = new THREE.Vector3(); + roomPoints.forEach(p => centroid.add(p)); + centroid.divideScalar(roomPoints.length); + + const wallVector = new THREE.Vector3( + p2.position[0] - p1.position[0], + 0, + p2.position[2] - p1.position[2] + ); + + // Normal depends on wall direction + let normal = new THREE.Vector3(-wallVector.z, 0, wallVector.x).normalize(); + if (p1.pointUuid === roomP2.pointUuid) { + normal = new THREE.Vector3(wallVector.z, 0, -wallVector.x).normalize(); + } + + const testPoint = new THREE.Vector3( + (p1.position[0] + p2.position[0]) / 2 + normal.x, + 0, + (p1.position[2] + p2.position[2]) / 2 + normal.z + ); + + const pointInside = turf.booleanPointInPolygon( + turf.point([testPoint.x, testPoint.z]), + turf.polygon([room.map(p => [p.position[0], p.position[2]])]) + ); + + orientations.push({ + isOutsideFacing: !pointInside + }); + } + } + } + + return { + isRoomWall: orientations.length > 0, + orientations // Now tracks all orientations for walls in multiple rooms + }; + }; + + const getWallMaterialSide = (wall: Wall) => { + const orientation = getWallOrientation(wall); + + if (!orientation.isRoomWall) { + return { front: 'inside', back: 'inside' }; // Both sides same for segment walls + } + + // For walls in multiple rooms, we need to determine which side faces which room + if (orientation.orientations.length === 2) { + // Wall is between two rooms - one side faces each room's interior + return { + front: 'inside', + back: 'inside' + }; + } else if (orientation.orientations.length === 1) { + // Wall is part of only one room (exterior wall) + return orientation.orientations[0].isOutsideFacing + ? { front: 'outside', back: 'inside' } + : { front: 'inside', back: 'outside' }; + } + + // Default case (shouldn't normally happen) + return { front: 'inside', back: 'inside' }; + }; + + // Rest of the functions remain the same + const getWallType = (wall: Wall) => getWallOrientation(wall).isRoomWall ? 'room' : 'segment'; + const isRoomWall = (wall: Wall) => getWallOrientation(wall).isRoomWall; + const isSegmentWall = (wall: Wall) => !getWallOrientation(wall).isRoomWall; + + return { + rooms, + getWallType, + isRoomWall, + isSegmentWall, + getWallMaterialSide, + findRooms, + }; +} \ No newline at end of file diff --git a/app/src/modules/builder/wall/Instances/instance/helpers/useWallGeometry.ts b/app/src/modules/builder/wall/Instances/instance/helpers/useWallGeometry.ts new file mode 100644 index 0000000..29341d6 --- /dev/null +++ b/app/src/modules/builder/wall/Instances/instance/helpers/useWallGeometry.ts @@ -0,0 +1,64 @@ +import * as THREE from 'three'; +import { useMemo } from 'react'; + +function useWallGeometry(wallLength: number, wallHeight: number, wallThickness: number) { + return useMemo(() => { + const geometry = new THREE.BufferGeometry(); + + const halfLength = wallLength / 2; + const halfThickness = wallThickness / 2; + const height = wallHeight; + + const vertices = [ + -halfLength, -height / 2, halfThickness, + -halfLength, height / 2, halfThickness, + halfLength, height / 2, halfThickness, + halfLength, -height / 2, halfThickness, + -halfLength, -height / 2, -halfThickness, + -halfLength, height / 2, -halfThickness, + halfLength, height / 2, -halfThickness, + halfLength, -height / 2, -halfThickness, + -halfLength, height / 2, halfThickness, + -halfLength, height / 2, -halfThickness, + halfLength, height / 2, -halfThickness, + halfLength, height / 2, halfThickness, + -halfLength, -height / 2, halfThickness, + -halfLength, -height / 2, -halfThickness, + halfLength, -height / 2, -halfThickness, + halfLength, -height / 2, halfThickness, + ]; + + const indices = [ + 0, 1, 2, 0, 2, 3, + 4, 6, 5, 4, 7, 6, + 0, 4, 5, 0, 5, 1, + 3, 2, 6, 3, 6, 7, + 8, 9, 10, 8, 10, 11, + 12, 14, 13, 12, 15, 14 + ]; + + geometry.setIndex(indices); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry.setAttribute('uv', new THREE.Float32BufferAttribute([ + 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 0 + ], 2)); + + geometry.computeVertexNormals(); + + geometry.addGroup(0, 6, 0); // Front + geometry.addGroup(6, 6, 1); // Back + geometry.addGroup(12, 6, 2); // Left + geometry.addGroup(18, 6, 3); // Right + geometry.addGroup(24, 6, 4); // Top + geometry.addGroup(30, 6, 5); // Bottom + + return geometry; + }, [wallLength, wallHeight, wallThickness]); +} + +export default useWallGeometry; \ 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 new file mode 100644 index 0000000..1dea961 --- /dev/null +++ b/app/src/modules/builder/wall/Instances/instance/wall.tsx @@ -0,0 +1,92 @@ +import * as THREE from 'three'; +import { useMemo } 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 useWallGeometry from './helpers/useWallGeometry'; +import { useWallStore } from '../../../../../store/builder/useWallStore'; +import { useWallClassification } from './helpers/useWallClassification'; + +function Wall({ wall }: { readonly wall: Wall }) { + const { walls } = useWallStore(); + const { getWallMaterialSide, isRoomWall, rooms } = useWallClassification(walls); + console.log('rooms: ', rooms); + const materialSide = getWallMaterialSide(wall); + const [startPoint, endPoint] = wall.points; + + const startX = startPoint.position[0]; + const startZ = startPoint.position[2]; + const endX = endPoint.position[0]; + const endZ = endPoint.position[2]; + + const wallLength = Math.sqrt((endX - startX) ** 2 + (endZ - startZ) ** 2); + const angle = Math.atan2(endZ - startZ, endX - startX); + + const centerX = (startX + endX) / 2; + const centerZ = (startZ + endZ) / 2; + const centerY = wall.wallHeight / 2; + + const textureLoader = new THREE.TextureLoader(); + + const [insideWallTexture, outsideWallTexture] = useMemo(() => { + const inside = textureLoader.load(insideMaterial); + inside.wrapS = inside.wrapT = THREE.RepeatWrapping; + inside.repeat.set(wallLength / 10, wall.wallHeight / 10); + inside.colorSpace = THREE.SRGBColorSpace; + + const outside = textureLoader.load(outsideMaterial); + outside.wrapS = outside.wrapT = THREE.RepeatWrapping; + outside.repeat.set(wallLength / 10, wall.wallHeight / 10); + outside.colorSpace = THREE.SRGBColorSpace; + + return [inside, outside]; + }, [wallLength, wall.wallHeight, textureLoader]); + + const materials = useMemo(() => { + // For segment walls (not in a room), use inside material on both sides + const frontMaterial = isRoomWall(wall) + ? (materialSide.front === 'inside' ? insideWallTexture : outsideWallTexture) + : insideWallTexture; + + const backMaterial = isRoomWall(wall) + ? (materialSide.back === 'inside' ? insideWallTexture : outsideWallTexture) + : insideWallTexture; + + return [ + new THREE.MeshStandardMaterial({ + color: Constants.wallConfig.defaultColor, + side: THREE.DoubleSide, + map: frontMaterial + }), + new THREE.MeshStandardMaterial({ + color: Constants.wallConfig.defaultColor, + side: THREE.DoubleSide, + map: backMaterial + }), + 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, materialSide, isRoomWall, wall]); + + const geometry = useWallGeometry(wallLength, wall.wallHeight, wall.wallThickness); + + return ( + + + {materials.map((material, index) => ( + + ))} + + + ); +} + +export default Wall; \ No newline at end of file diff --git a/app/src/modules/builder/wall/Instances/instance/wallInstance.tsx b/app/src/modules/builder/wall/Instances/instance/wallInstance.tsx index f31f3c2..53b6ecd 100644 --- a/app/src/modules/builder/wall/Instances/instance/wallInstance.tsx +++ b/app/src/modules/builder/wall/Instances/instance/wallInstance.tsx @@ -1,18 +1,13 @@ -import Line from '../../../line/line' -import Point from '../../../point/point'; -import { useToggleView } from '../../../../../store/builder/store'; +import { useToggleView } from "../../../../../store/builder/store"; +import Wall from "./wall" function WallInstance({ wall }: { readonly wall: Wall }) { const { toggleView } = useToggleView(); return ( <> - {toggleView && ( - <> - - - - + {!toggleView && ( + )} ) diff --git a/app/src/modules/builder/wall/Instances/wallInstances.tsx b/app/src/modules/builder/wall/Instances/wallInstances.tsx index b9c5499..41beaa0 100644 --- a/app/src/modules/builder/wall/Instances/wallInstances.tsx +++ b/app/src/modules/builder/wall/Instances/wallInstances.tsx @@ -1,9 +1,14 @@ -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useWallStore } from '../../../../store/builder/useWallStore' import WallInstance from './instance/wallInstance'; +import Line from '../../line/line'; +import Point from '../../point/point'; +import { useToggleView } from '../../../../store/builder/store'; +import { Geometry } from '@react-three/csg'; function WallInstances() { const { walls } = useWallStore(); + const { toggleView } = useToggleView(); useEffect(() => { // console.log('walls: ', walls); @@ -11,9 +16,25 @@ function WallInstances() { return ( <> - {walls.map((wall) => ( - - ))} + + + {walls.map((wall) => ( + + ))} + + + + {toggleView && ( + <> + {walls.map((wall) => ( + + + + + + ))} + + )} ) } diff --git a/app/src/store/builder/useBuilderStore.ts b/app/src/store/builder/useBuilderStore.ts index 29f1753..245bf1d 100644 --- a/app/src/store/builder/useBuilderStore.ts +++ b/app/src/store/builder/useBuilderStore.ts @@ -80,7 +80,7 @@ export const useBuilderStore = create()( // Wall - wallThickness: 2, + wallThickness: 0.1, wallHeight: 7, setWallThickness: (thickness: number) => {