Improve roof node editing

This commit is contained in:
Aymeric Rabot
2026-06-07 00:24:15 -04:00
parent b7d6038348
commit f84910848b
11 changed files with 216 additions and 72 deletions
+3 -37
View File
@@ -1,6 +1,6 @@
import {
type AnyNodeId,
getActiveRoofHeight,
getRoofSegmentSurfaceY,
type RoofNode,
type RoofSegmentNode,
sceneRegistry,
@@ -17,40 +17,6 @@ export type RoofSegmentHit = {
localZ: number
}
/**
* Analytical surface Y for `seg` at segment-local (lx, lz). Mirrors
* the per-roof-type slope math in `shared/roof-surface.ts` so the
* disambiguator below stays free of cross-kind imports. Returns the
* roof's local surface height; the value is only used to compare
* candidates, never written to the scene.
*/
function analyticalSurfaceY(seg: RoofSegmentNode, lx: number, lz: number): number {
const rh = getActiveRoofHeight(seg)
const peakY = seg.wallHeight + rh
if (rh === 0) return seg.wallHeight
if (
seg.roofType === 'gable' ||
seg.roofType === 'gambrel' ||
seg.roofType === 'mansard' ||
seg.roofType === 'dutch'
) {
const t = seg.depth > 0 ? Math.abs(lz) / (seg.depth / 2) : 0
return peakY - t * rh
}
if (seg.roofType === 'shed') {
const t = (lz + seg.depth / 2) / (seg.depth || 1)
return peakY - t * rh
}
if (seg.roofType === 'hip') {
const fx = seg.width > 0 ? Math.abs(lx) / (seg.width / 2) : 0
const fz = seg.depth > 0 ? Math.abs(lz) / (seg.depth / 2) : 0
return peakY - Math.max(fx, fz) * rh
}
const t = seg.depth > 0 ? Math.abs(lz) / (seg.depth / 2) : 0
return peakY - t * rh
}
/**
* Resolve which roof-segment the user clicked. Used by every placement
* tool that drops a new node onto a roof (box-vent, ridge-vent,
@@ -61,7 +27,7 @@ function analyticalSurfaceY(seg: RoofSegmentNode, lx: number, lz: number): numbe
* point's (x, z) lies inside *every* segment's axis-aligned half-
* extents, so a naive first-match returns the wrong slope (typically
* segments[0]). We instead score each candidate by
* `|localY analyticalSurfaceY(localX, localZ)|` and pick the
* `|localY getRoofSegmentSurfaceY(localX, localZ)|` and pick the
* smallest — the slope the user actually clicked is the one whose
* sloped surface passes through the hit point.
*
@@ -101,7 +67,7 @@ export function resolveRoofSegmentHit(
const halfW = seg.width / 2 + overhang
const halfD = seg.depth / 2 + overhang
if (Math.abs(local.x) <= halfW && Math.abs(local.z) <= halfD) {
const surfaceY = analyticalSurfaceY(seg, local.x, local.z)
const surfaceY = getRoofSegmentSurfaceY(seg, local.x, local.z)
const score = Math.abs(local.y - surfaceY)
if (!best || score < best.score) {
best = {
+2 -21
View File
@@ -1,5 +1,5 @@
import {
getActiveRoofHeight,
getRoofSegmentSurfaceY,
getSegmentSlopeFrame,
ROOF_SHAPE_DEFAULTS,
type RoofSegmentNode,
@@ -13,26 +13,7 @@ import * as THREE from 'three'
// accessories don't reach across into a sibling kind for it.
export function getSurfaceY(lx: number, lz: number, seg: RoofSegmentNode): number {
const { roofType, wallHeight, depth, width } = seg
const rh = getActiveRoofHeight(seg)
const peakY = wallHeight + rh
if (rh === 0) return wallHeight
if (roofType === 'gable') {
const t = depth > 0 ? Math.abs(lz) / (depth / 2) : 0
return peakY - t * rh
}
if (roofType === 'shed') {
const t = (lz + depth / 2) / (depth || 1)
return peakY - t * rh
}
if (roofType === 'hip') {
const fx = width > 0 ? Math.abs(lx) / (width / 2) : 0
const fz = depth > 0 ? Math.abs(lz) / (depth / 2) : 0
return peakY - Math.max(fx, fz) * rh
}
const t = depth > 0 ? Math.abs(lz) / (depth / 2) : 0
return peakY - t * rh
return getRoofSegmentSurfaceY(seg, lx, lz)
}
// Outward normal for a roof surface tilting at angle θ in the horizontal