diff --git a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts index f1a365d0..6861dcd0 100644 --- a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts +++ b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts @@ -404,6 +404,40 @@ export class SpatialGridManager { return maxElevation } + /** + * Get the slab elevation for a wall by sampling its start, mid, and end points. + * Returns the highest slab elevation found, or 0 if none. + */ + getSlabElevationForWall( + levelId: string, + start: [number, number], + end: [number, number], + ): number { + const slabMap = this.slabsByLevel.get(levelId) + if (!slabMap) return 0 + + const samples: [number, number][] = [ + start, + [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2], + end, + ] + + let maxElevation = 0 + for (const slab of slabMap.values()) { + if (slab.polygon.length < 3) continue + for (const [sx, sz] of samples) { + if (pointInPolygon(sx, sz, slab.polygon)) { + const elevation = slab.elevation ?? 0.05 + if (elevation > maxElevation) { + maxElevation = elevation + } + break // This slab matched, no need to check more sample points + } + } + } + return maxElevation + } + clearLevel(levelId: string) { this.floorGrids.delete(levelId) this.wallGrids.delete(levelId) diff --git a/packages/core/src/systems/slab/slab-system.tsx b/packages/core/src/systems/slab/slab-system.tsx index 1240ab8a..e62dda56 100644 --- a/packages/core/src/systems/slab/slab-system.tsx +++ b/packages/core/src/systems/slab/slab-system.tsx @@ -40,11 +40,65 @@ function updateSlabGeometry(node: SlabNode, mesh: THREE.Mesh) { mesh.geometry = newGeo } +/** Half of default wall thickness — used to extend slab geometry under walls */ +const SLAB_OUTSET = 0.05 + +/** + * Expand a polygon outward by a uniform distance. + * Offsets each edge outward then intersects consecutive offset edges. + */ +function outsetPolygon(polygon: Array<[number, number]>, amount: number): Array<[number, number]> { + const n = polygon.length + if (n < 3) return polygon + + // Determine winding via signed area + let area2 = 0 + for (let i = 0; i < n; i++) { + const j = (i + 1) % n + area2 += polygon[i]![0] * polygon[j]![1] - polygon[j]![0] * polygon[i]![1] + } + const s = area2 >= 0 ? 1 : -1 + + // Offset each edge outward by amount + const offEdges: Array<[number, number, number, number]> = [] + for (let i = 0; i < n; i++) { + const j = (i + 1) % n + const dx = polygon[j]![0] - polygon[i]![0] + const dz = polygon[j]![1] - polygon[i]![1] + const len = Math.sqrt(dx * dx + dz * dz) + if (len < 1e-9) { + offEdges.push([polygon[i]![0], polygon[i]![1], dx, dz]) + continue + } + const nx = (s * dz / len) * amount + const nz = (s * -dx / len) * amount + offEdges.push([polygon[i]![0] + nx, polygon[i]![1] + nz, dx, dz]) + } + + // Intersect consecutive offset edges to get new vertices + const result: Array<[number, number]> = [] + for (let i = 0; i < n; i++) { + const j = (i + 1) % n + const [ax, az, adx, adz] = offEdges[i]! + const [bx, bz, bdx, bdz] = offEdges[j]! + const denom = adx * bdz - adz * bdx + if (Math.abs(denom) < 1e-9) { + // Parallel edges — use offset endpoint + result.push([ax + adx, az + adz]) + } else { + const t = ((bx - ax) * bdz - (bz - az) * bdx) / denom + result.push([ax + t * adx, az + t * adz]) + } + } + + return result +} + /** * Generates extruded slab geometry from polygon */ export function generateSlabGeometry(slabNode: SlabNode): THREE.BufferGeometry { - const polygon = slabNode.polygon + const polygon = outsetPolygon(slabNode.polygon, SLAB_OUTSET) const elevation = slabNode.elevation ?? 0.05 if (polygon.length < 3) { diff --git a/packages/core/src/systems/wall/wall-system.tsx b/packages/core/src/systems/wall/wall-system.tsx index 1c207182..264788ae 100644 --- a/packages/core/src/systems/wall/wall-system.tsx +++ b/packages/core/src/systems/wall/wall-system.tsx @@ -2,6 +2,8 @@ import { useFrame } from '@react-three/fiber' import * as THREE from 'three' import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg' import { sceneRegistry } from '../../hooks/scene-registry/scene-registry' +import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager' +import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync' import type { AnyNode, AnyNodeId, WallNode } from '../../schema' import useScene from '../../store/use-scene' import { @@ -95,18 +97,22 @@ function getLevelWalls(levelId: string): WallNode[] { * Updates the geometry for a single wall */ function updateWallGeometry(wallId: string, miterData: WallMiterData) { - const node = useScene.getState().nodes[wallId as WallNode['id']] + const nodes = useScene.getState().nodes + const node = nodes[wallId as WallNode['id']] if (!node || node.type !== 'wall') return const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh if (!mesh) return + const levelId = resolveLevelId(node, nodes) + const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end) + const childrenIds = node.children || [] const childrenNodes = childrenIds - .map((childId) => useScene.getState().nodes[childId]) + .map((childId) => nodes[childId]) .filter((n): n is AnyNode => n !== undefined) - const newGeo = generateExtrudedWall(node, childrenNodes, miterData) + const newGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation) mesh.geometry.dispose() mesh.geometry = newGeo @@ -114,7 +120,7 @@ function updateWallGeometry(wallId: string, miterData: WallMiterData) { // Update collision mesh const collisionMesh = mesh.getObjectByName('collision-mesh') as THREE.Mesh if (collisionMesh) { - const collisionGeo = generateExtrudedWall(node, [], miterData) + const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation) collisionMesh.geometry.dispose() collisionMesh.geometry = collisionGeo } @@ -134,12 +140,13 @@ export function generateExtrudedWall( wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, + slabElevation = 0, ) { const { junctionData } = miterData const wallStart: Point2D = { x: wallNode.start[0], y: wallNode.start[1] } const wallEnd: Point2D = { x: wallNode.end[0], y: wallNode.end[1] } - const height = wallNode.height ?? 2.5 + const height = (wallNode.height ?? 2.5) - slabElevation const thickness = wallNode.thickness ?? 0.1 const halfT = thickness / 2 @@ -232,6 +239,9 @@ export function generateExtrudedWall( // Rotate so extrusion direction (Z) becomes height direction (Y) geometry.rotateX(-Math.PI / 2) + if (slabElevation > 0) { + geometry.translate(0, slabElevation, 0) + } geometry.computeVertexNormals() // Apply CSG subtraction for cutouts (doors/windows)