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:
@@ -70,12 +70,16 @@ export const ParametricNodeRenderer = ({ node }: { node: AnyNode }) => {
|
||||
|
||||
const position = liveTransform?.position ?? overridePosition ?? n.position ?? [0, 0, 0]
|
||||
const rawRotation = overrideRotation ?? n.rotation
|
||||
const baseRotation: [number, number, number] =
|
||||
typeof rawRotation === 'number' ? [0, rawRotation, 0] : (rawRotation ?? [0, 0, 0])
|
||||
// The live transform carries only the plan-view Y rotation; keep the
|
||||
// node's own X/Z so 3D-oriented kinds (e.g. a duct-fitting riser at
|
||||
// X=π/2) don't visually flatten to horizontal mid-drag. Matches the
|
||||
// move tool's commit, which also replaces only the Y component.
|
||||
const rotation: [number, number, number] =
|
||||
liveTransform?.rotation !== undefined
|
||||
? [0, liveTransform.rotation, 0]
|
||||
: typeof rawRotation === 'number'
|
||||
? [0, rawRotation, 0]
|
||||
: (rawRotation ?? [0, 0, 0])
|
||||
? [baseRotation[0], liveTransform.rotation, baseRotation[2]]
|
||||
: baseRotation
|
||||
|
||||
return (
|
||||
<group
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user