Add frameless door openings and first-person collider support

This commit is contained in:
sudhir
2026-05-01 11:35:26 +05:30
parent 915393d649
commit 09bfb0b484
8 changed files with 379 additions and 63 deletions
+8
View File
@@ -32,6 +32,13 @@ export const DoorNode = BaseNode.extend({
width: z.number().default(0.9),
height: z.number().default(2.1),
// Opening mode
openingKind: z.enum(['door', 'opening']).default('door'),
openingShape: z.enum(['rectangle', 'rounded', 'arch']).default('rectangle'),
cornerRadius: z.number().min(0).default(0.15),
archHeight: z.number().min(0).default(0.45),
openingRevealRadius: z.number().min(0).default(0.025),
// Frame
frameThickness: z.number().default(0.05),
frameDepth: z.number().default(0.07),
@@ -81,6 +88,7 @@ export const DoorNode = BaseNode.extend({
panicBarHeight: z.number().default(1.0),
}).describe(dedent`Door node - a parametric door placed on a wall
- position: center of the door in wall-local coordinate system (Y = height/2, always at floor)
- openingKind/openingShape: hinged door or frameless wall opening shape
- segments: rows stacked top to bottom, each defining its own columnRatios
- type 'empty' = no leaf fill for that segment, 'panel' = raised/recessed panel, 'glass' = glazed
- hingesSide/swingDirection/swingAngle: which way the door opens and how far it is currently open
@@ -78,6 +78,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
const {
width,
height,
openingKind,
frameThickness,
frameDepth,
threshold,
@@ -97,6 +98,11 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
const hasLeafContent = segments.some((seg) => seg.type !== 'empty')
const clampedSwingAngle = Math.max(0, Math.min(Math.PI / 2, swingAngle))
if (openingKind === 'opening') {
syncDoorCutout(node, mesh)
return
}
// Leaf occupies the full opening (no bottom frame bar — door opens to floor)
const leafW = width - 2 * frameThickness
const leafH = height - frameThickness // only top frame
@@ -306,6 +312,10 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeX, leafTop - 0.25, hingeZ)
}
syncDoorCutout(node, mesh)
}
function syncDoorCutout(node: DoorNode, mesh: THREE.Mesh) {
// ── Cutout (for wall CSG) — always full door dimensions, 1m deep ──
let cutout = mesh.getObjectByName('cutout') as THREE.Mesh | undefined
if (!cutout) {
+79 -1
View File
@@ -5,7 +5,7 @@ import { computeBoundsTree } from 'three-mesh-bvh'
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 type { AnyNode, AnyNodeId, DoorNode, WallNode } from '../../schema'
import useScene from '../../store/use-scene'
import { getWallCurveFrameAt, getWallSurfacePolygon, isCurvedWall } from './wall-curve'
import { DEFAULT_WALL_HEIGHT, getWallPlanFootprint, getWallThickness } from './wall-footprint'
@@ -546,6 +546,11 @@ function collectCutoutBrushes(
for (const child of childrenNodes) {
if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door') continue
if (child.type === 'door' && child.openingKind === 'opening') {
brushes.push(createDoorOpeningCutoutBrush(child, wallThickness))
continue
}
const childMesh = sceneRegistry.nodes.get(child.id)
if (!childMesh) continue
@@ -600,3 +605,76 @@ function collectCutoutBrushes(
return brushes
}
function createDoorOpeningCutoutBrush(door: DoorNode, wallThickness: number): Brush {
const shape = createDoorOpeningCutoutShape(door)
const depth = wallThickness * 2
const bevelSize =
door.openingShape === 'rounded'
? Math.min(
Math.max(door.openingRevealRadius ?? 0.025, 0),
Math.max(wallThickness * 0.45, 0.001),
Math.max((door.cornerRadius ?? 0.15) * 0.45, 0.001),
)
: 0
const geometry = new THREE.ExtrudeGeometry(shape, {
depth,
bevelEnabled: bevelSize > 0,
bevelSegments: bevelSize > 0 ? 8 : 0,
bevelSize,
bevelThickness: bevelSize,
curveSegments: 24,
})
geometry.translate(0, 0, -depth / 2)
geometry.computeBoundsTree = computeBoundsTree
geometry.computeBoundsTree({ maxLeafSize: 10 })
return new Brush(geometry)
}
function createDoorOpeningCutoutShape(door: DoorNode): THREE.Shape {
const halfWidth = door.width / 2
const bottom = door.position[1] - door.height / 2
const top = door.position[1] + door.height / 2
const centerX = door.position[0]
const left = centerX - halfWidth
const right = centerX + halfWidth
const width = Math.max(door.width, 1e-6)
const height = Math.max(door.height, 1e-6)
const shape = new THREE.Shape()
if (door.openingShape === 'arch') {
const archHeight = Math.min(Math.max(door.archHeight ?? width / 2, 0.01), height)
const springY = top - archHeight
shape.moveTo(left, bottom)
shape.lineTo(right, bottom)
shape.lineTo(right, springY)
shape.quadraticCurveTo(centerX, top, left, springY)
shape.lineTo(left, bottom)
shape.closePath()
return shape
}
if (door.openingShape === 'rounded') {
const radius = Math.min(Math.max(door.cornerRadius ?? 0.15, 0), width / 2, height)
shape.moveTo(left, bottom)
shape.lineTo(right, bottom)
shape.lineTo(right, top - radius)
shape.absarc(right - radius, top - radius, radius, 0, Math.PI / 2, false)
shape.lineTo(left + radius, top)
shape.absarc(left + radius, top - radius, radius, Math.PI / 2, Math.PI, false)
shape.lineTo(left, bottom)
shape.closePath()
return shape
}
shape.moveTo(left, bottom)
shape.lineTo(right, bottom)
shape.lineTo(right, top)
shape.lineTo(left, top)
shape.closePath()
return shape
}