feat: doors and windows on roof-segment wall faces
Openings now host on the walls a roof segment generates — the base walls under the roof and the coplanar gable/shed/gambrel end faces, so a window can sit in a gable pediment. - core: roof-segment-walls.ts models the four vertical faces as 2D frames (u along face, v height) with convex profile polygons that mirror the wall volume getRoofSegmentBrushes builds; rect-in-profile clamping and anchored resize limits via half-plane algebra. - schemas: optional roofSegmentId on door/window; position is the segment-local wall mid-plane center, rotation[1] the face yaw. - cut: reuses capabilities.roofAccessory.buildCut; new cutScope: 'wall' subtracts from the wall brush only. cascadesViaHostSegment keeps the roof-merge loop from consuming door/window dirty marks (their own systems cascade via parentId). - tools: roof:* handlers in door/window tool + move-tool (the Build-tab preset path), with roofSegmentId cleared/restored across every roof<->wall re-anchor and revert; shared hit resolver normalizes normals through world space (merged mesh vs painted segment frames). - fix: RoofSystem no longer rebuilds per-segment CSG in accessory-reveal mode — the uncut rebuild used to draw over the merged shell's fresh opening until deselect. - fix: the walkthrough collider world now prunes by renderer-effective visibility; stale uncut segment CSG inside the hidden segments-wrapper blocked the player at openings the merged shell had cut through. Known gap: painted segments render per-segment CSG without accessory cuts (pre-existing, also affects skylight/dormer). Twice Codex-reviewed; details in private-editor plans/editor-roof-wall-openings.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1487328ec7
commit
bdfee058bd
@@ -0,0 +1,42 @@
|
||||
import type { RoofSegmentNode } from '@pascal-app/core'
|
||||
import * as THREE from 'three'
|
||||
|
||||
type RoofWallOpening = {
|
||||
roofSegmentId?: string
|
||||
position: [number, number, number]
|
||||
rotation: [number, number, number]
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
/**
|
||||
* CSG cut for a door / window hosted on a roof-segment wall face
|
||||
* (`capabilities.roofAccessory.buildCut`). A box through the wall plane,
|
||||
* oriented by the opening's face yaw, in segment-local coords — the
|
||||
* roof-merge loop subtracts it from the segment's wall brush.
|
||||
*
|
||||
* Returns null for wall-hosted openings (no `roofSegmentId`): their cut
|
||||
* is handled by the wall system's own cutout pipeline.
|
||||
*/
|
||||
export function buildRoofWallOpeningCut(
|
||||
node: RoofWallOpening,
|
||||
hostSegment: RoofSegmentNode,
|
||||
): THREE.BufferGeometry | null {
|
||||
if (!node.roofSegmentId) return null
|
||||
|
||||
const wallThickness = hostSegment.wallThickness ?? 0.1
|
||||
// Through the wall both ways, but well short of the rake/eave overhang
|
||||
// so the cut never nicks the soffit or fascia bands.
|
||||
const depth = wallThickness * 2 + 0.04
|
||||
|
||||
// A door's cut bottom is coplanar with the wall brush base — extend it
|
||||
// slightly downward so three-bvh-csg never has to clip coplanar faces.
|
||||
const bottom = node.position[1] - node.height / 2
|
||||
const bottomPad = bottom < 0.005 ? 0.02 : 0
|
||||
|
||||
const geo = new THREE.BoxGeometry(node.width, node.height + bottomPad, depth)
|
||||
geo.translate(0, -bottomPad / 2, 0)
|
||||
geo.rotateY(node.rotation[1] ?? 0)
|
||||
geo.translate(node.position[0], node.position[1], node.position[2])
|
||||
return geo
|
||||
}
|
||||
Reference in New Issue
Block a user