Files
editor/packages/nodes/src/shared/roof-wall-opening-cut.ts
T
Aymeric RabotandGitHub aab48e053f fix(editor): harden editor interactions and WebGPU rendering
Fix editor bug sweep regressions, WebGPU CSG/material crashes, Shift snap bypass behavior, arrow handle drag projection, and the wall preview null guard covered by the Sentry follow-up PRs.
2026-06-11 13:03:34 -04:00

75 lines
2.7 KiB
TypeScript

import type { DoorNode, RoofSegmentNode, WindowNode } from '@pascal-app/core'
import { getRoofWallFaceFrame, roofFacePointToSegment } from '@pascal-app/core'
import { buildOpeningCutoutGeometry, hasFlatOpeningCutoutBottom } from '@pascal-app/viewer'
import * as THREE from 'three'
/**
* CSG cut for a door / window hosted on a roof-segment wall face
* (`capabilities.roofAccessory.buildCut`). The cut goes through the wall
* mid-plane, derived from the CURRENT host geometry (the opening stores
* face-local coords), so the hole follows segment resizes for free.
* Plain rectangles cut a box; shaped openings (arch / rounded /
* frameless `opening` kind) reuse the wall pipeline's cutout profile so
* roof-hosted holes match wall-hosted ones.
*
* Returns null for wall-hosted openings: their cut is handled by the
* wall system's own cutout pipeline.
*/
export function buildRoofWallOpeningCut(
node: DoorNode | WindowNode,
hostSegment: RoofSegmentNode,
): THREE.BufferGeometry | null {
if (!node.roofSegmentId || !node.roofFace) 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.
// Only a flat bottom chord may extend; a rounded bottom is never
// coplanar and shifting it would distort the profile.
const bottom = node.position[1] - node.height / 2
const bottomPad = bottom < 0.005 && hasFlatOpeningCutoutBottom(node) ? 0.02 : 0
const center = roofFacePointToSegment(hostSegment, node.roofFace, [
node.position[0],
node.position[1],
0,
])
const { yaw } = getRoofWallFaceFrame(hostSegment, node.roofFace)
const geo = buildCutGeometry(node, wallThickness, depth, bottomPad)
geo.rotateY(yaw)
geo.translate(center[0], center[1], center[2])
return geo
}
function buildCutGeometry(
node: DoorNode | WindowNode,
wallThickness: number,
depth: number,
bottomPad: number,
): THREE.BufferGeometry {
const shaped =
node.openingKind === 'opening' ||
node.openingShape === 'arch' ||
node.openingShape === 'rounded'
if (!shaped) {
const geo = new THREE.BoxGeometry(node.width, node.height + bottomPad, depth)
geo.translate(0, -bottomPad / 2, 0)
return geo
}
const halfWidth = node.width / 2
const halfHeight = node.height / 2
return buildOpeningCutoutGeometry(
node,
{ left: -halfWidth, right: halfWidth, bottom: -halfHeight - bottomPad, top: halfHeight },
depth,
wallThickness,
)
}