feat: enhance wall classification and add decal management in wall store

This commit is contained in:
Jerald-Golden-B 2025-06-10 11:22:52 +05:30
parent 796a4ace45
commit 81664ba765
7 changed files with 292 additions and 202 deletions

View File

@ -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<string>();
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<number>();
const getPolygon = (points: Point[]) =>
turf.polygon([points.map(p => [p.position[0], p.position[2]])]);
const getWallSet = (room: Point[]) => {
const set = new Set<string>();
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<Wall>();
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,
};
}

View File

@ -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<any>();
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 (
<group
name={`Wall-${wall.wallUuid}`}
userData={wall}
position={[centerX, centerY, centerZ]}
rotation={[0, -angle, 0]}
visible={visible}
>
<mesh geometry={geometry}>
<Base ref={meshRef} geometry={geometry} visible>
{materials.map((material, index) => (
<primitive key={index} object={material} attach={`material-${index}`} />
))}
</mesh>
{wall.decals.map((decal) => {
return (
<Decal
// debug
position={[decal.decalPosition[0], decal.decalPosition[1], wall.wallThickness / 2]}
rotation={[0, 0, decal.decalRotation]}
scale={[decal.decalScale, decal.decalScale, 0.001]}
>
<meshBasicMaterial
map={outsideWallTexture}
side={THREE.DoubleSide}
polygonOffset
polygonOffsetFactor={-1}
/>
</Decal>
)
})}
</Base>
</group>
);
}
export default Wall;
// A--------------------------B--------------G
// | | |
// | | |
// | | |
// | | |
// | | |
// | | |
// F--------------------------C--------------H
// | |
// | |
// | |
// | |
// | |
// | |
// E--------------------------D

View File

@ -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<ReferenceWallProps>) {
inSideMaterial: 'default',
wallThickness: wallThickness,
wallHeight: wallHeight,
decals: []
})
}

View File

@ -90,7 +90,8 @@ function WallCreator() {
outSideMaterial: 'default',
inSideMaterial: 'default',
wallThickness: wallThickness,
wallHeight: wallHeight
wallHeight: wallHeight,
decals: []
};
addWall(wall);
setTempPoints([newPoint]);

View File

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

View File

@ -7,6 +7,12 @@ interface WallStore {
addWall: (wall: Wall) => void;
updateWall: (uuid: string, updated: Partial<Wall>) => 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<WallStore>()(
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) => {

View File

@ -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[];