Improve roof node editing
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
hasSegmentMaterialOverride,
|
||||
type RoofNode,
|
||||
type RoofSegmentNode,
|
||||
useLiveNodeOverrides,
|
||||
useRegistry,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
@@ -14,8 +15,13 @@ import * as THREE from 'three'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { getRoofDebugMaterials, getRoofMaterials } from './roof-materials'
|
||||
|
||||
export const RoofRenderer = ({ node }: { node: RoofNode }) => {
|
||||
export const RoofRenderer = ({ node: rawNode }: { node: RoofNode }) => {
|
||||
const ref = useRef<THREE.Group>(null!)
|
||||
const liveOverride = useLiveNodeOverrides((s) => s.overrides.get(rawNode.id))
|
||||
const node = useMemo<RoofNode>(
|
||||
() => (liveOverride ? ({ ...rawNode, ...liveOverride } as RoofNode) : rawNode),
|
||||
[rawNode, liveOverride],
|
||||
)
|
||||
|
||||
useRegistry(node.id, 'roof', ref)
|
||||
useLayoutEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user