Files
editor/packages/nodes/src/roof-segment/renderer.tsx
T
Sudhir YadavandGitHub 5551500d98 feat: HVAC ductwork + DWV plumbing systems (#402)
Adds two new MEP node families (HVAC ductwork, DWV plumbing) built on a shared port-connectivity model. Co-authored by @sudhir9297.
2026-06-16 15:30:39 -04:00

130 lines
4.5 KiB
TypeScript

'use client'
import {
type AnyNodeId,
getEffectiveRoofSurfaceMaterial,
getEffectiveSegmentSurfaceMaterial,
type RoofNode,
type RoofSegmentNode,
type RoofSegmentSurfaceMaterialRole,
useRegistry,
useScene,
} from '@pascal-app/core'
import {
createMaterial,
createMaterialFromPresetRef,
getRoofMaterialArray,
useNodeEvents,
useViewer,
} from '@pascal-app/viewer'
import { useEffect, useMemo, useRef } from 'react'
import * as THREE from 'three'
import { getRoofDebugMaterials, getRoofMaterials } from '../roof/roof-materials'
import { createPlaceholderGeometry } from '../shared/placeholder-geometry'
export const RoofSegmentRenderer = ({ node }: { node: RoofSegmentNode }) => {
const ref = useRef<THREE.Mesh>(null!)
const nodes = useScene((state) => state.nodes)
useRegistry(node.id, 'roof-segment', ref)
const handlers = useNodeEvents(node, 'roof-segment')
const debugColors = useViewer((s) => s.debugColors)
const shading = useViewer((s) => s.shading)
const textures = useViewer((s) => s.textures)
const colorPreset = useViewer((s) => s.colorPreset)
const sceneTheme = useViewer((s) => s.sceneTheme)
const parentNode = node.parentId
? (nodes[node.parentId as AnyNodeId] as RoofNode | undefined)
: undefined
// 4 groups map 1:1 to the roof's 4-material array (see getRoofMaterialArray).
const placeholderGeometry = useMemo(() => createPlaceholderGeometry(4), [])
// Segment material precedence, per-role:
// 1. Segment's role-specific override (topMaterial, edgeMaterial, wallMaterial).
// 2. Segment's catch-all `material` (legacy single-slot paint).
// 3. Parent roof's role-specific material.
// 4. Parent roof's catch-all material.
// 5. Default `roofMaterials` (handled at the `material =` line below).
//
// The 4-slot layout matches getRoofMaterialArray:
// slot 0 → 'edge' (wall/trim & rake bands)
// slot 1 → 'wall' (deck top & shingle eave bands)
// slot 2 → 'wall' (interior)
// slot 3 → 'top' (shingle / roof surface)
// biome-ignore lint/correctness/useExhaustiveDependencies: deps deliberately list the build inputs; depending on the whole object would rebuild on unrelated field changes.
const customMaterial = useMemo(() => {
const resolveSlot = (role: RoofSegmentSurfaceMaterialRole): THREE.Material | null => {
const parentSpec = parentNode ? getEffectiveRoofSurfaceMaterial(parentNode, role) : undefined
const spec = getEffectiveSegmentSurfaceMaterial(node, role, parentSpec)
if (typeof spec.materialPreset === 'string') {
const resolved = createMaterialFromPresetRef(spec.materialPreset, shading)
if (resolved) return resolved
}
if (spec.material !== undefined) {
return createMaterial(spec.material, shading)
}
return null
}
// Themed parent-roof array (per-role scene-theme colours) — used both as the
// full fallback and to fill any individual untextured slot below.
const themedArray = parentNode
? getRoofMaterialArray(parentNode, shading, textures, colorPreset, sceneTheme)
: null
const edge = resolveSlot('edge')
const wall = resolveSlot('wall')
const top = resolveSlot('top')
if (!(edge || wall || top)) {
return themedArray
}
// Some slots have explicit materials; fill the rest from the themed array so
// an untextured slot still picks up the scene-theme role colour, not blank white.
// Per-role only, then the themed parent slot — no cross-role fallback, so
// painting one segment surface never bleeds onto its other surfaces.
const slot = (i: number) => themedArray?.[i] ?? new THREE.MeshStandardMaterial()
return [edge ?? slot(0), wall ?? slot(1), wall ?? slot(2), top ?? slot(3)] as THREE.Material[]
}, [
node.material,
node.materialPreset,
node.topMaterial,
node.topMaterialPreset,
node.edgeMaterial,
node.edgeMaterialPreset,
node.wallMaterial,
node.wallMaterialPreset,
parentNode,
shading,
textures,
colorPreset,
sceneTheme,
])
const material = debugColors
? getRoofDebugMaterials(shading)
: customMaterial || getRoofMaterials(shading, textures, colorPreset)
useEffect(() => {
return () => {
placeholderGeometry.dispose()
}
}, [placeholderGeometry])
return (
<mesh
geometry={placeholderGeometry}
material={material}
position={node.position}
ref={ref}
rotation-y={node.rotation}
visible={node.visible}
{...handlers}
/>
)
}
export default RoofSegmentRenderer