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
@@ -325,7 +325,6 @@ export function generateDormerGeometry(
const dormerBrushes = getRoofSegmentBrushes(virtualSegment)
if (!dormerBrushes) {
// biome-ignore lint/suspicious/noConsole: keep diagnostic — fallback path.
console.warn('[dormer] getRoofSegmentBrushes returned null; using fallback silhouette.')
return buildDormerFallbackGeometry(dormer)
}
@@ -472,7 +471,6 @@ export function generateDormerGeometry(
remapRoofShellFaces(resultGeo, virtualSegment)
splitDormerGableMaterial(resultGeo, dormer.height, DORMER_GABLE_MATERIAL_INDEX)
} catch (e) {
// biome-ignore lint/suspicious/noConsole: dormer CSG can throw; keep diagnostic.
console.error('[dormer] CSG failed, falling back to silhouette:', e)
if (dormerSolid) {
try {
@@ -492,7 +490,6 @@ export function generateDormerGeometry(
// dormer is at least visible.
const triCount = resultGeo.getIndex()?.count ?? resultGeo.getAttribute('position')?.count ?? 0
if (triCount === 0) {
// biome-ignore lint/suspicious/noConsole: keep diagnostic — empty CSG.
console.warn('[dormer] CSG produced empty geometry; using fallback silhouette.')
return buildDormerFallbackGeometry(dormer)
}
@@ -41,6 +41,7 @@ export function DormerPositionSection({
const segmentId = segment?.id
const roofChildrenKey = (roof?.children ?? []).join(',')
// biome-ignore lint/correctness/useExhaustiveDependencies: roofChildrenKey is the stable signature of `roof.children`; intentionally omitting `roof` (object identity) in favor of the joined ids.
const worldXform = useMemo(() => {
const dormerObj = sceneRegistry.nodes.get(selectedId)
let worldX = 0
@@ -79,7 +80,6 @@ export function DormerPositionSection({
if (Number.isFinite(lo_x)) bounds = { minX: lo_x, maxX: hi_x, minZ: lo_z, maxZ: hi_z }
}
return { worldX, worldZ, worldRotation, bounds }
// biome-ignore lint/correctness/useExhaustiveDependencies: roofChildrenKey is the stable signature of `roof.children`; intentionally omitting `roof` (object identity) in favor of the joined ids.
}, [selectedId, px, py, pz, nodeRotation, segmentId, roofChildrenKey])
const worldX_now = worldXform.worldX
+1 -1
View File
@@ -107,7 +107,7 @@ export default function DormerPanel() {
}, [node, selectedId, setMovingNode, setSelection])
const handleDuplicate = useCallback(() => {
if (!(node && node.roofSegmentId)) return
if (!node?.roofSegmentId) return
triggerSFX('sfx:item-pick')
// Deep clone and strip the id so the move tool's onClick branch
// (`isNew || !node.id`) takes the "create fresh" path. Setting
+1
View File
@@ -26,6 +26,7 @@ const invalidGhostMaterial = new THREE.MeshStandardMaterial({
const DormerPreview = ({ node, invalid }: { node: DormerNode; invalid?: boolean }) => {
const material = invalid ? invalidGhostMaterial : ghostMaterial
// biome-ignore lint/correctness/useExhaustiveDependencies: deps deliberately list the build inputs; depending on the whole object would rebuild on unrelated field changes.
const geo = useMemo(
() => buildDormerGhostGeometry(node),
[node.width, node.depth, node.height, node.roofHeight, node.roofType, node.wallSkirtHeight],
+2
View File
@@ -59,6 +59,7 @@ const DormerRenderer = ({ node: storeNode }: { node: DormerNode }) => {
// shingle, 4=Gable wall. Walls take the 'wall' role, the deck side and
// shingle take 'roof'. When textures are off, every slot snaps to its
// role colour regardless of explicit paint (the render-modes invariant).
// biome-ignore lint/correctness/useExhaustiveDependencies: deps deliberately list the build inputs; depending on the whole object would rebuild on unrelated field changes.
const material = useMemo(() => {
const wallRole = () => createSurfaceRoleMaterial('wall', colorPreset, undefined, sceneTheme)
const roofRole = () => createSurfaceRoleMaterial('roof', colorPreset, undefined, sceneTheme)
@@ -111,6 +112,7 @@ const DormerRenderer = ({ node: storeNode }: { node: DormerNode }) => {
[colorPreset, sceneTheme],
)
// 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(() => {
if (!segment) return null
if (isLiveDrag) return buildDormerFallbackGeometry(node)
@@ -28,6 +28,7 @@ const DormerWindowAssembly = ({
frameMaterial: THREE.Material
glassMaterial: THREE.Material
}) => {
// biome-ignore lint/correctness/useExhaustiveDependencies: deps deliberately list the build inputs; depending on the whole object would rebuild on unrelated field changes.
const skirtWin = useMemo(
() => getDormerSkirtWindowDims(node),
[
@@ -45,6 +46,7 @@ const DormerWindowAssembly = ({
const winShape: DormerWindowShape = node.windowShape
const resolvedRadii: [number, number, number, number] = [...node.windowCornerRadii]
// biome-ignore lint/correctness/useExhaustiveDependencies: deps deliberately list the build inputs; depending on the whole object would rebuild on unrelated field changes.
const winGeo = useMemo(
() =>
buildDormerWindowGeometries(
@@ -101,6 +103,7 @@ const DormerWindowAssembly = ({
)
useEffect(() => () => sillGeo?.dispose(), [sillGeo])
// biome-ignore lint/correctness/useExhaustiveDependencies: deps deliberately list the build inputs; depending on the whole object would rebuild on unrelated field changes.
const exposed = useMemo(
() => getDormerExposedFaces(node, segment),
[
@@ -142,7 +145,6 @@ const DormerWindowAssembly = ({
{winGeo.glassPanes.map((pane, i) => (
<mesh
geometry={pane.geo}
// biome-ignore lint/suspicious/noArrayIndexKey: glass panes are derived from grid indices, no stable id.
key={`${keyPrefix}-glass-${i}`}
material={glassMaterial}
name={`dormer-glass-${keyPrefix}-${i}`}
@@ -153,7 +155,6 @@ const DormerWindowAssembly = ({
<mesh
castShadow
geometry={bar.geo}
// biome-ignore lint/suspicious/noArrayIndexKey: frame bars are derived from grid indices, no stable id.
key={`${keyPrefix}-bar-${i}`}
material={frameMaterial}
name={`dormer-frame-${keyPrefix}-${i}`}