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
@@ -1,6 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { type DoorNode, useRegistry, useScene } from '@pascal-app/core'
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type DoorNode,
|
||||
type RoofSegmentNode,
|
||||
useRegistry,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useNodeEvents } from '@pascal-app/viewer'
|
||||
import { useLayoutEffect, useRef } from 'react'
|
||||
import { type Mesh, MeshBasicMaterial } from 'three'
|
||||
@@ -17,7 +23,17 @@ export const DoorRenderer = ({ node }: { node: DoorNode }) => {
|
||||
const handlers = useNodeEvents(node, 'door')
|
||||
const isTransient = !!(node.metadata as Record<string, unknown> | null)?.isTransient
|
||||
|
||||
return (
|
||||
// Roof-hosted doors mount under the roof's `roof-elements` group (roof
|
||||
// frame), so the host segment's transform is applied here — wall-hosted
|
||||
// doors get it for free from the wall mesh they're nested in.
|
||||
const segment = useScene((state) =>
|
||||
node.roofSegmentId
|
||||
? (state.nodes[node.roofSegmentId as AnyNodeId] as RoofSegmentNode | undefined)
|
||||
: undefined,
|
||||
)
|
||||
if (node.roofSegmentId && segment?.type !== 'roof-segment') return null
|
||||
|
||||
const mesh = (
|
||||
<mesh
|
||||
castShadow
|
||||
material={doorHitboxMaterial}
|
||||
@@ -31,6 +47,13 @@ export const DoorRenderer = ({ node }: { node: DoorNode }) => {
|
||||
<boxGeometry args={[0, 0, 0]} />
|
||||
</mesh>
|
||||
)
|
||||
|
||||
if (!segment) return mesh
|
||||
return (
|
||||
<group position={segment.position} rotation-y={segment.rotation}>
|
||||
{mesh}
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
export default DoorRenderer
|
||||
|
||||
Reference in New Issue
Block a user