first commit

This commit is contained in:
2025-06-10 15:28:23 +05:30
commit e22a2dc275
699 changed files with 100382 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
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<string, string[]>();
const pointMap = new Map<string, Point>();
walls.forEach(wall => {
const [p1, p2] = wall.points;
pointMap.set(p1.pointUuid, p1);
pointMap.set(p2.pointUuid, p2);
// Add connection from p1 to p2
if (!graph.has(p1.pointUuid)) graph.set(p1.pointUuid, []);
graph.get(p1.pointUuid)?.push(p2.pointUuid);
// Add connection from p2 to p1
if (!graph.has(p2.pointUuid)) graph.set(p2.pointUuid, []);
graph.get(p2.pointUuid)?.push(p1.pointUuid);
});
// 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;
};
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)
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 = <T,>(a: Set<T>, b: Set<T>) => {
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<string>();
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];
const p2 = room[i + 1];
if ((wall.points[0].pointUuid === p1.pointUuid && wall.points[1].pointUuid === p2.pointUuid) ||
(wall.points[0].pointUuid === p2.pointUuid && wall.points[1].pointUuid === p1.pointUuid)) {
return true;
}
}
return false;
});
if (containingRooms.length === 0) {
return {
type: 'segment',
rooms: []
};
} else if (containingRooms.length === 1) {
return {
type: 'room',
rooms: containingRooms
};
} else {
return {
type: 'rooms',
rooms: containingRooms
};
}
};
// 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 => {
const type = findWallType(wall).type;
return type === 'room' || type === 'rooms';
};
const isSegmentWall = (wall: Wall): boolean => {
return findWallType(wall).type === 'segment';
};
return {
rooms,
getWallType,
isRoomWall,
isSegmentWall,
findRooms,
};
}

View File

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

View File

@@ -0,0 +1,128 @@
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 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 { 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];
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]);
const materials = useMemo(() => {
const frontMaterial = insideWallTexture;
const backMaterial = outsideWallTexture;
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, 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}
>
<Base ref={meshRef} geometry={geometry} visible>
{materials.map((material, index) => (
<primitive key={index} object={material} attach={`material-${index}`} />
))}
{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;

View File

@@ -0,0 +1,16 @@
import { useToggleView } from "../../../../../store/builder/store";
import Wall from "./wall"
function WallInstance({ wall }: { readonly wall: Wall }) {
const { toggleView } = useToggleView();
return (
<>
{!toggleView && (
<Wall wall={wall} />
)}
</>
)
}
export default WallInstance

View File

@@ -0,0 +1,79 @@
import React, { useEffect, useMemo, useRef } 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 { Base, Geometry, Subtraction } from '@react-three/csg';
import { BoxGeometry } from 'three';
function WallInstances() {
const { walls } = useWallStore();
const { toggleView } = useToggleView();
const ref = useRef<any>();
useEffect(() => {
// console.log('walls: ', walls);
}, [walls])
const allPoints = useMemo(() => {
const points: Point[] = [];
const seenUuids = new Set<string>();
walls.forEach(aisle => {
aisle.points.forEach(point => {
if (!seenUuids.has(point.pointUuid)) {
seenUuids.add(point.pointUuid);
points.push(point);
}
});
});
return points;
}, [walls]);
return (
<>
<group name='Walls-Group' ref={ref}>
<Geometry computeVertexNormals>
{walls.map((wall) => (
<WallInstance key={wall.wallUuid} wall={wall} />
))}
{/* <Base geometry={new BoxGeometry()} >
<meshStandardMaterial />
</Base>
<Geometry>
<Subtraction scale={[5, 11, 5]} >
<Geometry>
<Base geometry={new BoxGeometry()} />
</Geometry>
</Subtraction>
</Geometry> */}
</Geometry>
</group>
{toggleView && (
<>
<group name='Wall-Points-Group'>
{allPoints.map((point) => (
<Point key={point.pointUuid} point={point} />
))}
</group>
<group name='Wall-Lines-Group'>
{walls.map((wall) => (
<React.Fragment key={wall.wallUuid}>
<Line points={wall.points} />
</React.Fragment>
))}
</group>
</>
)}
</>
)
}
export default WallInstances