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
+78 -1
View File
@@ -1,8 +1,84 @@
import { type NodeDefinition, RoofNode as RoofNodeSchema } from '@pascal-app/core'
import {
type AnyNodeId,
type HandleDescriptor,
type NodeDefinition,
RoofNode as RoofNodeSchema,
type RoofNode as RoofNodeType,
type RoofSegmentNode,
type SceneApi,
} from '@pascal-app/core'
import { buildRoofFloorplan } from './floorplan'
import { roofParametrics } from './parametrics'
import { RoofNode } from './schema'
const MOVE_FRONT_OFFSET = 0.35
const MIN_ROOF_FOOTPRINT = 1
type RoofFootprintBounds = {
maxX: number
maxZ: number
minX: number
minZ: number
}
function getRoofFootprintBounds(node: RoofNodeType, sceneApi: SceneApi): RoofFootprintBounds {
let bounds: RoofFootprintBounds | null = null
for (const childId of node.children ?? []) {
const segment = sceneApi.get<RoofSegmentNode>(childId as AnyNodeId)
if (segment?.type !== 'roof-segment') continue
const halfWidth = Math.max(segment.width, MIN_ROOF_FOOTPRINT) / 2
const halfDepth = Math.max(segment.depth, MIN_ROOF_FOOTPRINT) / 2
const cos = Math.cos(segment.rotation ?? 0)
const sin = Math.sin(segment.rotation ?? 0)
const corners = [
[-halfWidth, -halfDepth],
[halfWidth, -halfDepth],
[halfWidth, halfDepth],
[-halfWidth, halfDepth],
] as const
for (const [x, z] of corners) {
const localX = segment.position[0] + x * cos + z * sin
const localZ = segment.position[2] - x * sin + z * cos
bounds =
bounds === null
? { maxX: localX, maxZ: localZ, minX: localX, minZ: localZ }
: {
maxX: Math.max(bounds.maxX, localX),
maxZ: Math.max(bounds.maxZ, localZ),
minX: Math.min(bounds.minX, localX),
minZ: Math.min(bounds.minZ, localZ),
}
}
}
return bounds ?? { maxX: 0.5, maxZ: 0.5, minX: -0.5, minZ: -0.5 }
}
function roofMoveHandle(): HandleDescriptor<RoofNodeType> {
return {
kind: 'translate',
placement: {
position: (node, sceneApi) => {
const bounds = getRoofFootprintBounds(node, sceneApi)
return [(bounds.minX + bounds.maxX) / 2, 0.02, bounds.maxZ + MOVE_FRONT_OFFSET]
},
},
apply: (_node, position) => ({ position: [position[0], position[1], position[2]] }),
snapExtents: (node, sceneApi) => {
const bounds = getRoofFootprintBounds(node, sceneApi)
const width = Math.max(bounds.maxX - bounds.minX, MIN_ROOF_FOOTPRINT)
const depth = Math.max(bounds.maxZ - bounds.minZ, MIN_ROOF_FOOTPRINT)
const swap = Math.abs(Math.sin(node.rotation ?? 0)) > 0.9
return [swap ? depth : width, swap ? width : depth]
},
}
}
const roofHandles: HandleDescriptor<RoofNodeType>[] = [roofMoveHandle()]
/**
* Roof — Stage A registration. Wrap-exports the legacy `RoofRenderer`
* + `RoofSystem` (geometry generation via `getRoofSegmentBrushes` +
@@ -43,6 +119,7 @@ export const roofDefinition: NodeDefinition<typeof RoofNode> = {
},
parametrics: roofParametrics,
handles: roofHandles,
floorplan: buildRoofFloorplan,
renderer: {