Add frameless door openings and first-person collider support
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user