From 81664ba765b064eb49be499ed0e189518d38a9ba Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Tue, 10 Jun 2025 11:22:52 +0530 Subject: [PATCH] feat: enhance wall classification and add decal management in wall store --- .../instance/helpers/useWallClassification.ts | 218 +++++++++--------- .../builder/wall/Instances/instance/wall.tsx | 83 ++++--- .../wall/wallCreator/referenceWall.tsx | 2 +- .../builder/wall/wallCreator/wallCreator.tsx | 3 +- .../selectionControls/selectionControls.tsx | 68 +++--- app/src/store/builder/useWallStore.tsx | 58 +++++ app/src/types/builderTypes.d.ts | 62 ++--- 7 files changed, 292 insertions(+), 202 deletions(-) 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 4a5c30d..4e815ad 100644 --- a/app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts +++ b/app/src/modules/builder/wall/Instances/instance/helpers/useWallClassification.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import * as THREE from 'three'; import * as turf from '@turf/turf'; export function useWallClassification(walls: Walls) { @@ -27,12 +26,10 @@ export function useWallClassification(walls: Walls) { // Find all minimal cycles (rooms) in the graph const allCycles: string[][] = []; - const visited = new Set(); const findCycles = (startNode: string, currentNode: string, path: string[], depth = 0) => { if (depth > 20) return; // Prevent infinite recursion - visited.add(currentNode); path.push(currentNode); const neighbors = graph.get(currentNode) || []; @@ -65,10 +62,92 @@ export function useWallClassification(walls: Walls) { .map(cycle => cycle.map(uuid => pointMap.get(uuid)!)) .filter(room => isValidRoom(room)); - // Filter out duplicate rooms (same set of points in different orders) const uniqueRooms = removeDuplicateRooms(potentialRooms); - return uniqueRooms; + // ✅ New logic that only removes redundant supersets + const filteredRooms = removeSupersetLikeRooms(uniqueRooms, walls); + + return filteredRooms; + + }; + + 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) @@ -132,48 +211,10 @@ export function useWallClassification(walls: Walls) { // Rest of the implementation remains the same... const rooms = useMemo(() => findRooms(), [walls]); - const createPolygon = (points: Point[]) => { - const coordinates = points.map(p => [p.position[0], p.position[2]]); - return turf.polygon([coordinates]); - }; - - - // Get all walls that form part of any room - const getRoomWalls = (): Wall[] => { - const roomWalls = new Set(); - - rooms.forEach(room => { - for (let i = 0; i < room.length - 1; i++) { - const p1 = room[i]; - const p2 = room[i + 1]; - - // Find the wall that connects these two points - const wall = walls.find(w => - (w.points[0].pointUuid === p1.pointUuid && w.points[1].pointUuid === p2.pointUuid) || - (w.points[0].pointUuid === p2.pointUuid && w.points[1].pointUuid === p1.pointUuid) - ); - - if (wall) roomWalls.add(wall); - } - }); - - return Array.from(roomWalls); - }; - // Determine wall orientation relative to room - const getWallOrientation = (wall: Wall) => { - const roomWalls = getRoomWalls(); - const isRoomWall = roomWalls.includes(wall); - - if (!isRoomWall) { - return { - isRoomWall: false, - isOutsideFacing: false - }; - } - - // Find which room this wall belongs to - const containingRoom = rooms.find(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]; const p2 = room[i + 1]; @@ -185,79 +226,39 @@ export function useWallClassification(walls: Walls) { return false; }); - if (!containingRoom) { + if (containingRooms.length === 0) { return { - isRoomWall: false, - isOutsideFacing: false + type: 'segment', + rooms: [] + }; + } else if (containingRooms.length === 1) { + return { + type: 'room', + rooms: containingRooms + }; + } else { + return { + type: 'rooms', + rooms: containingRooms }; } - - // Calculate the normal vector to determine outside direction - const roomPoints = containingRoom.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 [p1, p2] = wall.points; - const wallVector = new THREE.Vector3( - p2.position[0] - p1.position[0], - 0, - p2.position[2] - p1.position[2] - ); - const normal = new THREE.Vector3(-wallVector.z, 0, wallVector.x).normalize(); - - // Check if normal points away from centroid (outside) - 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]), - createPolygon(containingRoom) - ); - - // Determine if the wall is in the same order as the room's edge - let isSameOrder = false; - for (let i = 0; i < containingRoom.length - 1; i++) { - const roomP1 = containingRoom[i]; - const roomP2 = containingRoom[i + 1]; - if (p1.pointUuid === roomP1.pointUuid && p2.pointUuid === roomP2.pointUuid) { - isSameOrder = true; - break; - } - } - - return { - isRoomWall: true, - isOutsideFacing: isSameOrder ? !pointInside : pointInside - }; }; - // Rest of the functions remain the same... - const getWallType = (wall: Wall): 'room' | 'segment' => { - return getWallOrientation(wall).isRoomWall ? 'room' : 'segment'; + // Update the other functions to use the new return type + const getWallType = (wall: Wall): { + type: string; + rooms: Point[][]; + } => { + return findWallType(wall); }; const isRoomWall = (wall: Wall): boolean => { - return getWallOrientation(wall).isRoomWall; + const type = findWallType(wall).type; + return type === 'room' || type === 'rooms'; }; const isSegmentWall = (wall: Wall): boolean => { - return !getWallOrientation(wall).isRoomWall; - }; - - const getWallMaterialSide = (wall: Wall): { front: 'inside' | 'outside', back: 'inside' | 'outside' } => { - const orientation = getWallOrientation(wall); - - if (!orientation.isRoomWall) { - return { front: 'inside', back: 'outside' }; - } - - return orientation.isOutsideFacing - ? { front: 'outside', back: 'inside' } - : { front: 'inside', back: 'outside' }; + return findWallType(wall).type === 'segment'; }; return { @@ -265,7 +266,6 @@ export function useWallClassification(walls: Walls) { getWallType, isRoomWall, isSegmentWall, - getWallMaterialSide, findRooms, }; } \ 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 7045a76..c28366d 100644 --- a/app/src/modules/builder/wall/Instances/instance/wall.tsx +++ b/app/src/modules/builder/wall/Instances/instance/wall.tsx @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { useMemo } from 'react'; +import { useMemo, useRef, useState } from 'react'; import * as Constants from '../../../../../types/world/worldConstants'; import insideMaterial from '../../../../../assets/textures/floor/wall-tex.png'; @@ -7,13 +7,20 @@ import outsideMaterial from '../../../../../assets/textures/floor/factory wall t import useWallGeometry from './helpers/useWallGeometry'; import { useWallStore } from '../../../../../store/builder/useWallStore'; import { useWallClassification } from './helpers/useWallClassification'; +import { useFrame, useThree } from '@react-three/fiber'; +import { useWallVisibility } from '../../../../../store/builder/store'; +import { Decal } from '@react-three/drei'; +import { Base } from '@react-three/csg'; function Wall({ wall }: { readonly wall: Wall }) { const { walls } = useWallStore(); - const { getWallMaterialSide, isRoomWall, rooms } = useWallClassification(walls); - console.log('rooms: ', rooms); - const materialSide = getWallMaterialSide(wall); + const { getWallType } = 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 startX = startPoint.position[0]; const startZ = startPoint.position[2]; @@ -44,14 +51,9 @@ function Wall({ wall }: { readonly wall: Wall }) { }, [wallLength, wall.wallHeight]); 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 frontMaterial = insideWallTexture; - const backMaterial = isRoomWall(wall) - ? (materialSide.back === 'inside' ? insideWallTexture : outsideWallTexture) - : insideWallTexture; + const backMaterial = outsideWallTexture; return [ new THREE.MeshStandardMaterial({ @@ -69,41 +71,58 @@ function Wall({ wall }: { readonly wall: Wall }) { 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]); + }, [insideWallTexture, outsideWallTexture, wall]); const geometry = useWallGeometry(wallLength, wall.wallHeight, wall.wallThickness); + useFrame(() => { + if (!meshRef.current) return; + const v = new THREE.Vector3(); + const u = new THREE.Vector3(); + + if (!wallVisibility && wallType.type === 'room') { + meshRef.current.getWorldDirection(v); + camera.getWorldDirection(u); + setVisible((2 * v.dot(u)) <= 0.1); + + } else { + setVisible(true); + } + }) + return ( - + {materials.map((material, index) => ( ))} - + + {wall.decals.map((decal) => { + return ( + + + + ) + })} + ); } -export default Wall; - - -// A--------------------------B--------------G -// | | | -// | | | -// | | | -// | | | -// | | | -// | | | -// F--------------------------C--------------H -// | | -// | | -// | | -// | | -// | | -// | | -// E--------------------------D \ No newline at end of file +export default Wall; \ 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 7849705..42fa3d9 100644 --- a/app/src/modules/builder/wall/wallCreator/referenceWall.tsx +++ b/app/src/modules/builder/wall/wallCreator/referenceWall.tsx @@ -6,7 +6,6 @@ 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 * as Constants from '../../../../types/world/worldConstants'; import ReferenceLine from '../../line/reference/referenceLine'; interface ReferenceWallProps { @@ -73,6 +72,7 @@ function ReferenceWall({ tempPoints }: Readonly) { 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 b07b6d9..7c4c6ce 100644 --- a/app/src/modules/builder/wall/wallCreator/wallCreator.tsx +++ b/app/src/modules/builder/wall/wallCreator/wallCreator.tsx @@ -90,7 +90,8 @@ function WallCreator() { outSideMaterial: 'default', inSideMaterial: 'default', wallThickness: wallThickness, - wallHeight: wallHeight + wallHeight: wallHeight, + decals: [] }; addWall(wall); setTempPoints([newPoint]); diff --git a/app/src/modules/scene/controls/selectionControls/selectionControls.tsx b/app/src/modules/scene/controls/selectionControls/selectionControls.tsx index 0fb9c2f..955eecf 100644 --- a/app/src/modules/scene/controls/selectionControls/selectionControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/selectionControls.tsx @@ -35,32 +35,32 @@ const SelectionControls: React.FC = () => { const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]); const { projectId } = useParams(); + const isDragging = useRef(false); + const isLeftMouseDown = useRef(false); + const isSelecting = useRef(false); + const isRightClick = useRef(false); + const rightClickMoved = useRef(false); + const isCtrlSelecting = useRef(false); + const isShiftSelecting = useRef(false); + useEffect(() => { if (!camera || !scene || toggleView) return; const canvasElement = gl.domElement; canvasElement.tabIndex = 0; - let isDragging = false; - let isLeftMouseDown = false; - let isSelecting = false; - let isRightClick = false; - let rightClickMoved = false; - let isCtrlSelecting = false; - let isShiftSelecting = false; - const helper = new SelectionHelper(gl); const onPointerDown = (event: PointerEvent) => { if (event.button === 2) { - isRightClick = true; - rightClickMoved = false; + isRightClick.current = true; + rightClickMoved.current = false; } else if (event.button === 0) { - isSelecting = false; - isCtrlSelecting = event.ctrlKey; - isShiftSelecting = event.shiftKey; - isLeftMouseDown = true; - isDragging = false; + isSelecting.current = false; + isCtrlSelecting.current = event.ctrlKey; + isShiftSelecting.current = event.shiftKey; + isLeftMouseDown.current = true; + isDragging.current = false; if (event.ctrlKey && duplicatedObjects.length === 0) { if (controls) (controls as any).enabled = false; selectionBox.startPoint.set(pointer.x, pointer.y, 0); @@ -69,45 +69,46 @@ const SelectionControls: React.FC = () => { }; const onPointerMove = (event: PointerEvent) => { - if (isRightClick) { - rightClickMoved = true; + if (isRightClick.current) { + rightClickMoved.current = true; } - if (isLeftMouseDown) { - isDragging = true; + if (isLeftMouseDown.current) { + isDragging.current = true; } - isSelecting = true; - if (helper.isDown && event.ctrlKey && duplicatedObjects.length === 0 && isCtrlSelecting) { + isSelecting.current = true; + if (helper.isDown && event.ctrlKey && duplicatedObjects.length === 0 && isCtrlSelecting.current) { selectionBox.endPoint.set(pointer.x, pointer.y, 0); } }; const onPointerUp = (event: PointerEvent) => { if (event.button === 2 && !event.ctrlKey && !event.shiftKey) { - isRightClick = false; - if (!rightClickMoved) { + isRightClick.current = false; + rightClickMoved.current = false; + if (!rightClickMoved.current) { clearSelection(); } return; } - if (isSelecting && isCtrlSelecting) { - isCtrlSelecting = false; - isSelecting = false; + if (isSelecting.current && isCtrlSelecting.current) { + isCtrlSelecting.current = false; + isSelecting.current = false; if (event.ctrlKey && duplicatedObjects.length === 0) { selectAssets(); } - } else if (!isSelecting && selectedAssets.length > 0 && ((!event.ctrlKey && !event.shiftKey && pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) || event.button !== 0)) { + } else if (!isSelecting.current && selectedAssets.length > 0 && ((!event.ctrlKey && !event.shiftKey && pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) || event.button !== 0)) { clearSelection(); helper.enabled = true; - isCtrlSelecting = false; + isCtrlSelecting.current = false; } else if (controls) { (controls as any).enabled = true; } - if (!isDragging && isLeftMouseDown && isShiftSelecting && event.shiftKey) { - isShiftSelecting = false; - isLeftMouseDown = false; - isDragging = false; + if (!isDragging.current && isLeftMouseDown.current && isShiftSelecting.current && event.shiftKey) { + isShiftSelecting.current = false; + isLeftMouseDown.current = false; + isDragging.current = false; raycaster.setFromCamera(pointer, camera); const intersects = raycaster.intersectObjects(scene.children, true) @@ -167,7 +168,7 @@ const SelectionControls: React.FC = () => { const onContextMenu = (event: MouseEvent) => { event.preventDefault(); - if (!rightClickMoved) { + if (!rightClickMoved.current) { clearSelection(); } }; @@ -207,6 +208,7 @@ const SelectionControls: React.FC = () => { }, [activeModule]); useFrame(() => { + console.log(rightClickMoved.current); if (pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { selectionGroup.current.position.set(0, 0, 0); } diff --git a/app/src/store/builder/useWallStore.tsx b/app/src/store/builder/useWallStore.tsx index d1bd02d..682cb8f 100644 --- a/app/src/store/builder/useWallStore.tsx +++ b/app/src/store/builder/useWallStore.tsx @@ -7,6 +7,12 @@ interface WallStore { addWall: (wall: Wall) => void; updateWall: (uuid: string, updated: Partial) => void; removeWall: (uuid: string) => void; + addDecal: (wallUuid: string, decal: Decal) => void; + updateDecal: (decalUuid: string, decal: Decal) => void; + removeDecal: (decalUuid: string) => void; + updateDecalPosition: (decalUuid: string, position: [number, number, number]) => void; + updateDecalRotation: (decalUuid: string, rotation: number) => void; + updateDecalScale: (decalUuid: string, scale: number) => void; removePoint: (pointUuid: string) => Wall[]; setPosition: (pointUuid: string, position: [number, number, number]) => void; @@ -40,6 +46,58 @@ export const useWallStore = create()( state.walls = state.walls.filter(w => w.wallUuid !== uuid); }), + addDecal: (wallUuid, decal) => set((state) => { + const wallToUpdate = state.walls.find(w => w.wallUuid === wallUuid); + if (wallToUpdate) { + wallToUpdate.decals.push(decal); + } + }), + + updateDecal: (decalUuid, decal) => set((state) => { + for (const wall of state.walls) { + const decalToUpdate = wall.decals.find(d => d.decalUuid === decalUuid); + if (decalToUpdate) { + Object.assign(decalToUpdate, decal); + } + } + }), + + removeDecal: (decalUuid) => set((state) => { + for (const wall of state.walls) { + wall.decals = wall.decals.filter(d => d.decalUuid !== decalUuid); + } + }), + + updateDecalPosition: (decalUuid, position) => set((state) => { + for (const wall of state.walls) { + const decal = wall.decals.find(d => d.decalUuid === decalUuid); + if (decal) { + decal.decalPosition = position; + break; + } + } + }), + + updateDecalRotation: (decalUuid, rotation) => set((state) => { + for (const wall of state.walls) { + const decal = wall.decals.find(d => d.decalUuid === decalUuid); + if (decal) { + decal.decalRotation = rotation; + break; + } + } + }), + + updateDecalScale: (decalUuid, scale) => set((state) => { + for (const wall of state.walls) { + const decal = wall.decals.find(d => d.decalUuid === decalUuid); + if (decal) { + decal.decalScale = scale; + break; + } + } + }), + removePoint: (pointUuid) => { const removedWalls: Wall[] = []; set((state) => { diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts index 6d1e282..2cf06f0 100644 --- a/app/src/types/builderTypes.d.ts +++ b/app/src/types/builderTypes.d.ts @@ -1,33 +1,33 @@ // Asset interface Asset { - modelUuid: string; - modelName: string; - assetId: string; - position: [number, number, number]; - rotation: [number, number, number]; - isLocked: boolean; - isCollidable: boolean; - isVisible: boolean; - opacity: number; - animations?: string[]; - animationState?: { - current: string; - playing: boolean; - }; - eventData?: { - type: string; - point?: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; + modelUuid: string; + modelName: string; + assetId: string; + position: [number, number, number]; + rotation: [number, number, number]; + isLocked: boolean; + isCollidable: boolean; + isVisible: boolean; + opacity: number; + animations?: string[]; + animationState?: { + current: string; + playing: boolean; + }; + eventData?: { + type: string; + point?: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + }; + points?: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + }[]; }; - points?: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - }[]; - }; } type Assets = Asset[]; @@ -47,6 +47,15 @@ interface Point { // Wall +interface Decal { + decalUuid: string; + decalName: string; + decalId: string; + decalPosition: [number, number, number]; + decalRotation: number; + decalScale: number; +} + interface Wall { wallUuid: string; points: [Point, Point]; @@ -54,6 +63,7 @@ interface Wall { inSideMaterial: string; wallThickness: number; wallHeight: number; + decals: Decal[] } type Walls = Wall[];