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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
Reference in New Issue
Block a user