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
@@ -3,8 +3,11 @@ import type {
|
||||
DoorNode as DoorNodeType,
|
||||
HandleDescriptor,
|
||||
NodeDefinition,
|
||||
RoofSegmentNode,
|
||||
WallNode,
|
||||
} from '@pascal-app/core'
|
||||
import { readRoofFaceHeightMax, readRoofFaceWidthMax } from '../shared/roof-opening-host'
|
||||
import { buildRoofWallOpeningCut } from '../shared/roof-wall-opening-cut'
|
||||
import { scaleHandleHeight } from './door-math'
|
||||
import { buildDoorFloorplan } from './floorplan'
|
||||
import { doorWidthAffordance } from './floorplan-affordances'
|
||||
@@ -42,7 +45,13 @@ function doorWidthHandle(side: 'left' | 'right'): HandleDescriptor<DoorNodeType>
|
||||
// 'max' = +X edge anchored (left arrow grows the -X edge outward).
|
||||
anchor: side === 'right' ? 'min' : 'max',
|
||||
min: MIN_DOOR_WIDTH,
|
||||
max: (n, scene) => readWallLength(n, scene),
|
||||
max: (n, scene) => {
|
||||
// Roof-hosted doors clamp against the face profile (the wall-based
|
||||
// limits read Infinity when wallId is unset).
|
||||
const roofMax = readRoofFaceWidthMax(n, scene, sign)
|
||||
if (roofMax !== null) return Math.max(MIN_DOOR_WIDTH, roofMax)
|
||||
return readWallLength(n, scene)
|
||||
},
|
||||
currentValue: (n) => n.width,
|
||||
apply: (initial, newWidth) => {
|
||||
// Anchored edge stays fixed in wall-local coords. Door rotation is
|
||||
@@ -80,6 +89,8 @@ function doorHeightHandle(): HandleDescriptor<DoorNodeType> {
|
||||
anchor: 'min', // bottom anchored at wall-local Y = position[1] - height/2
|
||||
min: MIN_DOOR_HEIGHT,
|
||||
max: (n, scene) => {
|
||||
const roofMax = readRoofFaceHeightMax(n, scene, 1)
|
||||
if (roofMax !== null) return Math.max(MIN_DOOR_HEIGHT, roofMax)
|
||||
const bottom = n.position[1] - n.height / 2
|
||||
return Math.max(MIN_DOOR_HEIGHT, readWallHeight(n, scene) - bottom)
|
||||
},
|
||||
@@ -147,10 +158,22 @@ export const doorDefinition: NodeDefinition<typeof DoorNode> = {
|
||||
duplicable: true,
|
||||
deletable: true,
|
||||
wallOpeningPlacement: true,
|
||||
// `wallId` ties the door to its host wall and is re-derived from
|
||||
// the wall under the cursor when a preset is placed. Host apps
|
||||
// strip this at preset-save time via `getHostRefFields(def)`.
|
||||
hostRefFields: ['wallId'],
|
||||
// Doors also host on roof-segment wall faces (base walls under the
|
||||
// roof, gable ends). `buildCut` punches the opening into the
|
||||
// segment's wall brush; `cascadesViaHostSegment` keeps the roof-merge
|
||||
// loop from consuming door dirty marks (DoorSystem owns them and
|
||||
// already cascades to the host via parentId).
|
||||
roofAccessory: {
|
||||
buildCut: (node, hostSegment) =>
|
||||
buildRoofWallOpeningCut(node as DoorNodeType, hostSegment as RoofSegmentNode),
|
||||
cutScope: 'wall',
|
||||
cascadesViaHostSegment: true,
|
||||
},
|
||||
// `wallId` / `roofSegmentId` tie the door to its host and are
|
||||
// re-derived from the surface under the cursor when a preset is
|
||||
// placed. Host apps strip these at preset-save time via
|
||||
// `getHostRefFields(def)`.
|
||||
hostRefFields: ['wallId', 'roofSegmentId'],
|
||||
},
|
||||
|
||||
parametrics: doorParametrics,
|
||||
|
||||
Reference in New Issue
Block a user