Adds two new MEP node families (HVAC ductwork, DWV plumbing) built on a shared port-connectivity model. Co-authored by @sudhir9297.
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
'use client'
|
|
|
|
import {
|
|
type AnyNodeId,
|
|
type CupolaNode,
|
|
type RoofSegmentNode,
|
|
useLiveNodeOverrides,
|
|
useRegistry,
|
|
useScene,
|
|
} from '@pascal-app/core'
|
|
import {
|
|
type ColorPreset,
|
|
createMaterial,
|
|
createMaterialFromPresetRef,
|
|
createSurfaceRoleMaterial,
|
|
useNodeEvents,
|
|
useViewer,
|
|
} from '@pascal-app/viewer'
|
|
import { useEffect, useMemo, useRef } from 'react'
|
|
import * as THREE from 'three'
|
|
import { getAnalyticalNormal, surfaceQuatFromNormal } from '../shared/roof-surface'
|
|
import { buildCupolaGeometry } from './geometry'
|
|
|
|
const defaultMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0xff_ff_ff,
|
|
roughness: 0.7,
|
|
metalness: 0.2,
|
|
})
|
|
|
|
/**
|
|
* Cupola renderer. Same transform stack as the box vent — the cupola is
|
|
* parented to a roof-segment, so this reads the segment directly and
|
|
* reproduces the segment-local transform (segment position → rotation →
|
|
* cupola position → slope tilt → cupola yaw → mesh). No animation.
|
|
*/
|
|
const CupolaRenderer = ({ node: storeNode }: { node: CupolaNode }) => {
|
|
const ref = useRef<THREE.Group>(null!)
|
|
useRegistry(storeNode.id, 'cupola', ref)
|
|
const handlers = useNodeEvents(storeNode, 'cupola')
|
|
const shading = useViewer((s) => s.shading)
|
|
const textures = useViewer((s) => s.textures)
|
|
const colorPreset: ColorPreset = useViewer((s) => s.colorPreset)
|
|
const sceneTheme = useViewer((s) => s.sceneTheme)
|
|
|
|
const overrides = useLiveNodeOverrides(
|
|
(s) => s.get(storeNode.id as AnyNodeId) as Partial<CupolaNode> | undefined,
|
|
)
|
|
const node: CupolaNode = overrides ? ({ ...storeNode, ...overrides } as CupolaNode) : storeNode
|
|
|
|
const segment = useScene((state) =>
|
|
node.roofSegmentId
|
|
? (state.nodes[node.roofSegmentId as AnyNodeId] as RoofSegmentNode | undefined)
|
|
: undefined,
|
|
)
|
|
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: deps deliberately list the build inputs; depending on the whole object would rebuild on unrelated field changes.
|
|
const geometry = useMemo(
|
|
() => buildCupolaGeometry(node),
|
|
[node.width, node.depth, node.height, node.roofStyle, node.finial],
|
|
)
|
|
useEffect(() => () => geometry.dispose(), [geometry])
|
|
|
|
const surfaceQuat = useMemo(() => {
|
|
if (!segment) return new THREE.Quaternion()
|
|
const normal = getAnalyticalNormal(node.position[0] ?? 0, node.position[2] ?? 0, segment)
|
|
return surfaceQuatFromNormal(normal, new THREE.Quaternion())
|
|
}, [segment, node.position[0], node.position[2]])
|
|
|
|
const material = useMemo(() => {
|
|
if (!textures || (!node.material && !node.materialPreset)) {
|
|
return createSurfaceRoleMaterial('roof', colorPreset, THREE.FrontSide, sceneTheme)
|
|
}
|
|
return node.material
|
|
? createMaterial(node.material, shading)
|
|
: (createMaterialFromPresetRef(node.materialPreset, shading) ?? defaultMaterial)
|
|
}, [textures, colorPreset, sceneTheme, shading, node.material, node.materialPreset])
|
|
|
|
const yAxis = useMemo(() => new THREE.Vector3(0, 1, 0), [])
|
|
const composedQuat = useMemo(() => {
|
|
const yawQuat = new THREE.Quaternion().setFromAxisAngle(yAxis, node.rotation ?? 0)
|
|
return new THREE.Quaternion().copy(surfaceQuat).multiply(yawQuat)
|
|
}, [surfaceQuat, node.rotation, yAxis])
|
|
|
|
if (!segment) return null
|
|
|
|
const segPos = segment.position ?? [0, 0, 0]
|
|
const segRotY = segment.rotation ?? 0
|
|
|
|
return (
|
|
<group position={segPos} rotation-y={segRotY}>
|
|
<group
|
|
position={[node.position[0] ?? 0, node.position[1] ?? 0, node.position[2] ?? 0]}
|
|
quaternion={composedQuat}
|
|
ref={ref}
|
|
visible={node.visible}
|
|
>
|
|
<mesh
|
|
castShadow
|
|
geometry={geometry}
|
|
material={material}
|
|
name="cupola-surface"
|
|
receiveShadow
|
|
{...handlers}
|
|
/>
|
|
</group>
|
|
</group>
|
|
)
|
|
}
|
|
|
|
export default CupolaRenderer
|