slab outset + wall pushing up
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user