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.
This commit is contained in:
Sudhir Yadav
2026-06-16 15:30:39 -04:00
committed by GitHub
parent a0d3d9c701
commit 5551500d98
172 changed files with 17361 additions and 150 deletions
@@ -64,6 +64,14 @@ export const GeometrySystem = () => {
// shelf dirties the shelf without altering its boards.
const builtGeometryKeyRef = useRef<Map<string, string>>(new Map())
// Re-mark every geometry-backed node dirty whenever a viewer appearance
// value changes, so `def.geometry` builders re-run and pick up the new
// shading / texture / preset / theme. These four are deliberate re-run
// TRIGGERS, not values read in the body — the effect re-fires on any
// change. They're primitives (stable by value), so listing them is safe;
// biome flags them as "unnecessary" because the body doesn't reference
// them, but dropping them silently breaks appearance-mode switching.
// biome-ignore lint/correctness/useExhaustiveDependencies: shading/textures/colorPreset/sceneTheme are intentional re-run triggers; removing them stops geometry from rebuilding on appearance change.
useEffect(() => {
const nodes = useScene.getState().nodes
for (const node of Object.values(nodes)) {
@@ -1,8 +1,7 @@
import { type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
import { getLevelHeight, type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
import { useFrame } from '@react-three/fiber'
import { lerp } from 'three/src/math/MathUtils.js'
import useViewer from '../../store/use-viewer'
import { getLevelHeight } from './level-utils'
const EXPLODED_GAP = 5
@@ -1,53 +1,4 @@
import {
type CeilingNode,
type LevelNode,
sceneRegistry,
useScene,
type WallNode,
} from '@pascal-app/core'
export const DEFAULT_LEVEL_HEIGHT = 2.5
// Cache: levelId → computed height. Invalidated when the nodes reference changes.
// Zustand produces a new `nodes` object on every mutation, so reference equality
// is a zero-cost way to detect stale data without any subscription overhead.
const heightCache = new Map<string, number>()
let lastNodesRef: object | null = null
export function getLevelHeight(
levelId: string,
nodes: ReturnType<typeof useScene.getState>['nodes'],
): number {
if (nodes !== lastNodesRef) {
heightCache.clear()
lastNodesRef = nodes
}
if (heightCache.has(levelId)) return heightCache.get(levelId)!
const level = nodes[levelId as LevelNode['id']] as LevelNode | undefined
if (!level) return DEFAULT_LEVEL_HEIGHT
let maxTop = 0
for (const childId of level.children) {
const child = nodes[childId as keyof typeof nodes]
if (!child) continue
if (child.type === 'ceiling') {
const ch = (child as CeilingNode).height ?? DEFAULT_LEVEL_HEIGHT
if (ch > maxTop) maxTop = ch
} else if (child.type === 'wall') {
let meshY = sceneRegistry.nodes.get(childId as any)?.position.y ?? 0
if (meshY < 0) meshY = 0
const top = meshY + ((child as WallNode).height ?? DEFAULT_LEVEL_HEIGHT)
if (top > maxTop) maxTop = top
}
}
const height = maxTop > 0 ? maxTop : DEFAULT_LEVEL_HEIGHT
heightCache.set(levelId, height)
return height
}
import { getLevelHeight, type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
/**
* Instantly snaps all level Objects3D to their true stacked Y positions