Skip to content

feat: HVAC ductwork + DWV plumbing systems#402

Merged
Aymericr merged 76 commits into
pascalorg:mainfrom
sudhir9297:fix/wed-jun-10
Jun 16, 2026
Merged

feat: HVAC ductwork + DWV plumbing systems#402
Aymericr merged 76 commits into
pascalorg:mainfrom
sudhir9297:fix/wed-jun-10

Conversation

@sudhir9297

@sudhir9297 sudhir9297 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds two new MEP node families to the editor — HVAC ductwork and DWV (drain-waste-vent) plumbing — built on a shared port-connectivity model.

  • CoreNodePort / def.ports on the node registry, a port-connectivity service, system-scoped port queries, cursorAttached + portSnap options on the movable capability, and parametric derive/reconcile inspector hooks.
  • HVAC nodesduct-segment (draw tool, drag handles, floorplan, ceiling mode, diameter stepping, rectangular trunk cross-sections), duct-fitting (elbow / tee / cross / reducer with typed ports, auto-elbow insertion at corners, tee taps, profile inheritance, mitered rect elbows), duct-terminal (registers / diffusers / return grilles that mount and snap to duct ports), hvac-equipment (furnace / air-handler / condenser with detailed models + refrigerant service ports), and lineset (refrigerant suction + liquid pair).
  • DWV plumbing nodespipe-segment (drains level by default, ¼"/ft slope is an S-key opt-in), auto-minted DWV fittings (bends / sanitary tees / crosses), plumbing fixtures (toilets / sinks / tubs with drain ports), pipe-trap, a riser diagram + validation services, and a pipe-fitting placement tool.
  • Auto tee / cross fittings — drawing a run that ends ON or passes straight THROUGH the side of an existing run now auto-inserts the joint: ducts mint a tee (side / end tap) or a 4-way cross (pass-through); DWV pipes mint a square sanitary tee (previously a 45° wye) or a new cross fitting. Side taps step along the run with grid snap, with Shift to free smooth placement.
  • Editor — system graph + system pill on selected MEP nodes, port snap / cursor attach / connectivity in the move tool, a rotation-axis pill above the floating action menu, and Build-tab tiles + Add-Fitting panels for the HVAC and DWV tools.
  • Merges latest origin/main (roof wall openings, direct-manipulation, snap work) into the branch — two conflicts in build-tab.tsx and move-registry-node-tool.tsx were resolved to keep both sides' behavior.

How to test

  1. bun dev and open the editor at localhost:3002.
  2. HVAC — switch to the Build tab, pick Duct, and draw a run; verify auto-elbows land on corners and diameter stepping works. Use Add Fitting to drop a tee/elbow and confirm it rides the cursor and snaps to duct ports. Drop a Register and an HVAC Unit; confirm the register snaps to a duct end and the unit exposes refrigerant ports. Draw a Lineset between equipment.
  3. Auto tee / cross (duct) — draw a duct run whose END lands on the side of an existing run and confirm a tee is minted; draw one that passes straight THROUGH a run and confirm a 4-way cross is minted with the drawn run split into two halves. Confirm the tap point steps along the trunk with grid snap (hold Shift for smooth).
  4. DWV — pick DWV Pipe and draw a segment; confirm it draws level by default and only slopes when you hold/press S. Tap a pipe's side and confirm a SQUARE sanitary tee is minted (not a 45° wye); draw a pipe straight through another and confirm a cross fitting is minted. Confirm bends auto-mint at corners, and place a fixture (toilet/sink/tub) and confirm its drain port mates to the pipe.
  5. Select any MEP node and confirm the system pill appears; move a connected node and confirm mated fittings/segments follow.
  6. Run bun check-types — all packages should pass.

Screenshots / screen recording

N/A in this description — recording strongly encouraged given the interactive 3D tooling; reviewer should exercise the Build-tab HVAC/DWV tools per the steps above.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

sudhir9297 and others added 30 commits May 19, 2026 02:59
Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Typed connection points (level-local position, outward direction,
diameter, supply/return tag) that kinds expose via def.ports. Placement
tools snap to them; a future system graph walks them for connectivity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four new kinds wired into AnyNode and the event bus:
- duct-segment: round duct run as a 3D polyline (diameter, material,
  insulation R, supply/return)
- duct-fitting: elbow / tee / reducer with position + euler rotation
- duct-terminal: supply register / diffuser / return grille with
  floor / ceiling / wall mount
- hvac-equipment: furnace / air handler / condenser cabinet

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reusable prefix/value pill (with optional signed deltas and an
emphasised primary part) so node tools can show the same themed readout
the wall H/L/T pill uses. MeasurementPill now delegates to it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- StructureTool gains duct-segment / duct-fitting / duct-terminal /
  hvac-equipment so the Build tab can arm the registry tools.
- useEditor.rotationAxis + cycleRotationAxis(): the world axis R/T
  rotates fully-3D kinds (duct fittings) around. Lives on the editor
  store so both the nodes package and the floating action menu can
  share it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a duct fitting is selected, show the active R/T rotation axis in a
DimensionPill-styled chip stacked directly above the move / duplicate /
delete menu — same slot the wall height pill uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round duct runs as a registry-driven kind:
- geometry: capped cylinder sections + sphere joints + translucent
  insulation shell; shared buildSection/createDuctMaterial helpers
- def.ports: run start/end exposed as typed ports (outward tangents)
- shared/ports.ts: scene-wide port query + XZ nearest-port snap used by
  every HVAC tool
- tool: one-segment-per-two-clicks placement with 45° XZ angle lock
  (Shift = free), Alt-drag vertical risers, port snapping, and a
  DimensionPill delta readout
- system: selection-time path-point drag handles portaled into the
  duct's scene group — axis-constrained by default, Alt = free plane,
  Shift = no grid snap, endpoint port-snap, single-undo commits
- floorplan: real-width line + system-tinted dashed centerline; risers
  render as circles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First kind to expose def.ports. Click-place tool snaps the ghost onto
any scene port (position AND orientation, pivoting on the inlet collar);
R/T rotates ±45° around the shared useEditor.rotationAxis, Alt cycles
the axis (also for a selected fitting via def.keyboardActions + a
listener-only def.system). Floorplan renders the projected port-stub
symbol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mount-aware (floor / ceiling / wall) face orientation with a single
collar port so duct runs end onto a terminal. Frame + louver geometry,
yaw click-place tool with R/T rotation, system-tinted floorplan symbol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Floor-placed cabinet with supply/return collar ports (condenser has
none), giving duct runs a real origin. Cabinet + collar geometry,
condenser fan detail, yaw click-place tool with R/T rotation,
floorplan footprint with equipment diagonal and collar dots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
duct-segment, duct-fitting, duct-terminal, hvac-equipment join the
registry and are re-exported from the package root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Duct, Duct Fitting, Register, and HVAC Unit tiles (placeholder icons
borrowed from existing assets) arming the registry placement tools.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebuild the hvac-equipment geometry so each unit reads as real gear:

- Furnace: hollow sheet-metal cabinet built from butt-jointed plates
  (no more z-fighting), open front cut exposing a blue squirrel-cage
  blower and orange burner manifold/gas valve, plus a front gas line
  with drip leg. Supply (top) and return (side) walls now carry real
  circular holes with open-ended collars so ducts pass through.
- Air handler: tall white cabinet with two stacked guarded axial fans
  and finned coil bands down the sides.
- Condenser: white coil cabinet with vertical louvered fins on all four
  faces, dark base/top frame and corner posts, and a top fan under a
  radial wire guard over a recessed throat.
- Shared white cabinet color across all three units.
- Default supply/return collar diameter dropped to 8" so duct holes
  match typical runs.

Also: duct-terminal gains floor/ceiling/wall mounting (M to cycle) and
the duct-segment snap indicator is smaller.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reads the mated-joint relationship back out of coinciding ports so an
edit to one node carries its neighbours along: a moved fitting
stretches the duct endpoints touching its collars and rigidly drags
fittings mated collar-to-collar. Propagation is deliberately one hop —
no runaway network rearrangement. Pure logic (def.ports + arithmetic),
consumed by the editor move tool and the duct-segment handle system.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lity

- cursorAttached: pin the dragged node to the cursor instead of the
  offset-preserving drag — small connector-like kinds (duct fittings)
  read as lagging behind the mouse otherwise.
- portSnap: magnetically shift the dragged node so its closest own port
  mates onto a nearby scene port (optionally filtered by distribution
  system), e.g. a register collar onto a duct run end. Alt bypasses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sudhir9297 and others added 27 commits June 11, 2026 20:25
plumbing-fixture kind: toilet / lavatory / kitchen-sink / tub /
washer, each a recognizable simple silhouette with one WASTE port at
its floor rough-in — so drain runs are drawn FROM a fixture, the way
DWV systems actually start. FIXTURE_SPECS centralizes per-type
footprint, drain rough-in position, drain size, and IPC Table 709.1
DFU values.

- Click-place tool: Q cycles the fixture type (ghost rebuilds live),
  R/T rotate, pill shows the type + its DFU.
- System graph sums drainage fixture units per component
  (summary.fixtureUnits) and the action-menu pill shows "N DFU" —
  the load number the upcoming pipe-sizing validators read.
- Floorplan: footprint rect + drain dot (toilets get the conventional
  bowl ellipse); Build tab gains a Fixture tile (lucide:bath).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Swaps the fixture node kind (toilets/sinks/tubs) for a pipe-trap node
across the schema union, event bus, node registry, and editor menus. The
DFU load accounting that depended on fixtures is removed from the system
graph; trap-based DWV modeling supersedes the fixture approach.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends ParametricDescriptor with a patch-aware `derive(next, patch)` and
a cross-node `reconcile(prev, next)` companion. The inspector folds the
derive result into the same update and applies reconcile's other-node
patches in one gesture, so editing one field can keep dependent fields
and neighbouring nodes consistent (e.g. duct runs re-trimmed onto a
resized fitting's collars). Direct store/MCP writes bypass it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds flat-oval cross-sections to duct segments and fittings (elbows sweep
a stadium ring through the mitered solid; tees carry oval run/branch
profiles), with roll continuity so a riser's profile stays continuous
through a fitting. Tees gain a `branchAngle` (45–135°): 90° square tap,
<90° a lateral leaning downstream toward flow, >90° leaning upstream.
Auto-fitting sizes oval joints by area-equivalent diameter, and the Build
tab arms the fitting tool from a Duct sub-panel (also drops the removed
fixture tile).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A freely placed drain start is now raised so the 1:48 fall lands on the
grid plane (nothing clips below), while a port/body-snapped start keeps
its fixed height and the end drops instead. Adds a selection-time system
module exposing path-point handles to edit a committed run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds two core services: `buildRiserDiagram` projects a plumbing network
to an isometric riser diagram (lines/markers), and `validateDwv` reports
DWV code findings by severity. Surfaces them in the editor via a
toggleable riser-diagram overlay panel and a view-toggle button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The automatic ¼"/ft drain fall made freshly drawn waste runs read as
bent/crooked. Runs now draw level; S toggles slope mode while the tool
is armed. Angle-locked ends also snap run LENGTH along the ray instead
of per-axis, so off-grid starts (port/body snaps) stay on the 45° ray.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Click-place tool for DWV fittings mirroring the duct-fitting pattern:
ghost preview, DWV port mating, R/T rotation with Alt axis cycling,
selection-time axis pill. Armed from an Add Fitting button under the
DWV Pipe tile, same as the Duct panel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolve the blockers, suggestions, and nits from the architecture review:

- Mount fitting selection affordances via def.affordanceTools.selection
  instead of def.system; rename the per-kind system.tsx files to
  selection.tsx and add a SelectionAffordanceManager in the editor.
- Add a distributionRole field to NodeDefinition (run/fitting/terminal/
  equipment) and key system-graph summarization off it instead of
  branching on node.type.
- Lift getLevelHeight/DEFAULT_LEVEL_HEIGHT into core's level-height
  service so viewer and nodes share one implementation.
- Split riser-diagram and floating-action-menu panels so the full-node
  subscription lives in a child mounted only when needed.
- Bundle inspector reconcile writes into a single updateNodes call.
- Move shared fitting-rotation helpers to nodes/src/shared.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts:
#	apps/editor/components/build-tab.tsx
#	packages/editor/src/components/tools/registry/move-registry-node-tool.tsx
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply safe biome fixes (formatting, import sort, unused-import removal)
to the branch's own duct/lineset/pipe-trap files so `bun run check`
passes. Unsafe useExhaustiveDependencies hints left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…al alignment

Duct, duct-fitting, pipe, and lineset now duplicate and move with a
translucent ghost that rides the cursor and only lands on the commit
click — nothing is inserted into the scene before that. Each kind ships
its own ghost+box mover (affordanceTools.move) and routes through a
pure-draft branch in the 3D floating action menu; the MoveTool dispatcher
prefers affordanceTools.move over capabilities.movable so duct-fitting
uses its ghost. The 2D floorplan overlay drives the same drag for any
path kind generically.

Dragged runs/fittings now show a footprint bounding box (DragBoundingBox
in 3D, an SVG rect in 2D) with Figma-style alignment guides drawn
relative to the box.

Every kind now contributes alignment anchors: nodeAlignmentAnchors emits
path vertices, typed-port positions, and the position centre, so dragging
or placing any item snaps to ducts, fittings, pipes, and linesets across
all collectAlignmentAnchors consumers (3D mover, ghost movers, fresh
placement, surface snap). The 2D overlay no longer skips thin run lines
as candidates.

Move tools hide the real 3D mesh imperatively (not the store `visible`
flag) so a node never vanishes from the 2D floorplan during a drag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Icons
- Add raster icons (HVAC, duct, duct-fitting, registers, dwv-pipes,
  lineset) and wire them through each node's presentation.icon as
  `kind: 'url'`, the Build tab, and the action-menu structure tools.
  Lineset drops the `lucide:cable` placeholder for its own PNG.

3D draw cursor
- Register, lineset, DWV pipe, and the duct/pipe fittings now render the
  shared CursorSphere (ground ring + vertical line + tool-icon badge) in
  the 3D view, matching the duct tool and the 2D floorplan overlay. The
  icon resolves from the active structure-tools entry. Previously only
  the 2D floorplan drew this, so the indicator was absent in 3D.

Alignment & path editing
- Add shared draw-alignment helper: Figma-style guides layered onto the
  HVAC/DWV draw cursors so runs line up with other nodes while being
  drawn (published to both the 2D plan and the 3D view).
- Add shared path-point-affordance: 2D floor-plan drag handles for
  polyline path vertices (duct/pipe/lineset), the plan counterpart of
  their 3D selection handles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `liquid-line` node — the thin bare-copper rail split out of the
lineset — as its own drawable MEP run, plus a Follow mode that traces it
alongside an existing lineset.

- New node under packages/nodes/src/liquid-line: schema, single-centerline
  geometry, floorplan, parametrics, endpoint-fold connect, selection +
  ghost move/duplicate affordances, and a draw tool (same model as the
  lineset tool: 45° lock, Shift free, Alt vertical, refrigerant-port snap).
- Follow mode: arm "Follow lineset" (Build → MEP panel toggle or `F`), then
  click a lineset to lay a liquid line beside it, tracing its whole path at
  a small clear-air gap on the cursor's side. Backed by a shared
  useLiquidLineToolOptions store so panel and tool stay in sync.
- Shared path-offset helper (parallel miter offset) drives the trace.
- Lineset geometry now draws a single centerline pipe (suction + optional
  jacket), dropping the parallel liquid rail it used to render.
- Register the kind across core schema/events, the nodes plugin, the editor
  StructureTool union + lookup table, the floating-action-menu path-kind
  branch, and the Build tab's MEP group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/core/src/services/index.ts
Auto-insert a fitting when a drawn run taps or crosses an existing run:
ducts mint a tee (end/side tap) or 4-way cross (pass-through); DWV pipes
mint a square sanitary tee (was a 45° wye) or a new cross fitting. Body
taps now step along the run with grid snap (Shift frees to smooth).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Floor-mounted registers ignored slab thickness and sat at y=0, sinking
into the slab. Declare the `floorPlaced` capability so the generic
FloorElevationSystem lifts the committed mesh onto the slab surface
(footprint-driven, multi-slab aware), and lift the placement ghost via
getFloorStackPreviewPosition so the preview matches.

Ceiling-mounted registers snapped to a global "tallest ceiling, else
wall, else 2.5m" plane. Resolve the actual ceiling the cursor ray hits
(point-in-polygon, holes excluded) and take that surface's own height;
refuse placement when no real ceiling is under the cursor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Wrap loadExampleFile/handleFile/loadAndConvert in useCallback and
  reorder before their useEffect in IfcConverter to fix
  noInvalidUseBeforeDeclaration
- Remove unused biome-ignore comments for noConsole (rule is off) in
  dormer/csg-geometry.ts and noArrayIndexKey (rule is off) in
  dormer/window-assembly.tsx
- Remove misplaced useExhaustiveDependencies suppression in
  dormer/panel-position-section.tsx (rule not firing on that hook)
- Add correct suppression for resolvedRadii spread-dep pattern in
  dormer/window-assembly.tsx
- Biome auto-fixed excess/missing hook deps across 30+ node files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The biome --unsafe autofix stripped [shading, textures, colorPreset,
sceneTheme] from the mount effect's dep array, treating them as unused.
They are deliberate re-run TRIGGERS: the effect re-marks every
def.geometry node dirty whenever an appearance value changes, so the
builders re-run and pick up the new shading / textures / color preset /
scene theme. Without them, switching render mode, toggling textures, or
changing the preset no longer rebuilds existing registry-driven geometry.

All four are primitives (stable by value), so listing them is safe;
added a scoped biome-ignore so the trigger-only deps survive future
autofixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier `biome check --write --unsafe` pass (commit a3b5e4d) did far
more than satisfy the failing check. `useExhaustiveDependencies` is
info-level and never failed `bun run check` — the only error-level
failure was a formatter diff. But --unsafe rewrote ~25 hook dependency
arrays, and several of those rewrites are regressions:

  - dormer/panel-position-section: replaced the stable `roofChildrenKey`
    signature with raw `roof.children` / `roof` captures, which both
    broke memoization AND introduced a TS error (`'roof' is possibly
    'undefined'`) that failed `tsc --build` for @pascal-app/nodes.
  - gutter/downspout renderers: replaced deliberate `JSON.stringify`
    value-comparison deps (there to avoid expensive CSG rebuilds) with
    broad object-identity deps → CSG re-runs on every node mutation.
  - turbine-vent: added `node` to specific-property lists, defeating the
    narrow geometry-rebuild triggers.
  - floorplan-panel / liquid-line: dropped real deps (`levelId`,
    `follow`) → stale-closure risk.

Reverts every such file to its pre-lint state (74b7351), restoring the
authors' intentional dependency arrays. The only genuinely-required fix
from that pass is kept: the duct-terminal import formatting (the actual
error-level failure) and the GeometrySystem appearance-dep restoration
(committed separately in c1bfb88).

Verified: `bun run check` exits 0, all 9 packages typecheck, and the
test suite is unchanged (1228 pass / 20 pre-existing env failures).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file is Next.js typegen output ("should not be edited") and was
inadvertently swept into the previous commit by `git add -A` after a
local `next typegen` run rewrote its route-types import path. Restore
the branch's committed version so the lint cleanup doesn't carry an
unrelated, environment-specific change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ain)

bun run check already passed (0 errors); these were warn-level:
- Remove 5 dead biome-ignore comments — noConsole (csg-geometry, 3×) and
  noArrayIndexKey (window-assembly, 2×) are both disabled in biome.jsonc,
  so the suppressions had no effect. The console calls / index keys are
  unchanged (the rules are off); only the dead comments are gone.
- Relocate the useExhaustiveDependencies suppression in
  panel-position-section from inside the useMemo body (where it covered
  nothing) to directly above the useMemo call, so it actually suppresses
  the diagnostic. Preserves the intentional `roofChildrenKey`
  stable-signature dep array — no behavior change.
- panel.tsx: `!(node && node.roofSegmentId)` → `!node?.roofSegmentId`
  (useOptionalChain), equivalence-preserving.

bun run check now reports 0 errors and 0 warnings. The remaining 92 are
info-level useExhaustiveDependencies hints (intentional parametric
narrow-rebuild deps); left untouched — broadening them reintroduces the
CSG-rebuild churn reverted earlier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An exhaustive audit of the useExhaustiveDependencies info-level hints
(28 files) confirmed two were genuine, user-visible stale-closure bugs
(the other ~90 are the intentional narrow-rebuild pattern, left as-is).

1. chimney/renderer.tsx — `segmentBrushes` memoised on 9 hand-listed
   segment fields, omitting `gambrelLowerWidthRatio` /
   `mansardSteepWidthRatio` / `dutchHipWidthRatio`, which
   `getRoofSegmentBrushes` reads to shape the trim brushes. Editing one
   of those ratios re-identifies `segment` (so `geo`/`trimmedBody`
   re-CSG) but NOT the brushes — the chimney then trims against the old
   roof outline. Depend on `[segment]` directly, matching the sibling
   `geo` memo whose comment already argues whole-object deps avoid
   exactly this forgotten-field class of bug. `segment` is a useScene
   selector ref, stable except when the segment's own data changes, so
   a chimney slider drag still hits the cache.

2. floorplan-panel.tsx — `handlePointerMove` calls `showOpeningGhost`
   (whose door-swing-arc vs window-panes glyph is bound to
   `isDoorBuildActive`) but omitted it from deps. A door→window tool
   switch changes none of the handler's other listed deps, so it kept
   the stale closure and floated a door symbol while the window tool was
   armed. Add `showOpeningGhost` to the dep array.

Verified: bun run check exits 0 (0 errors, 0 warnings; 88 intentional
infos remain), all 9 packages typecheck, tests unchanged (1228 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Most are the intentional tight-dep pattern in packages/nodes (geometry/
material memos capture the whole node but list only shape fields, so they
don't rebuild on name/visible flips) — suppressed with a reason rather than
widening the deps, plus a few intentional re-run triggers (follow, levelId,
mount-only effects).

Two were real and fixed:
- floorplan-panel handlePointerMove: depended on isOpeningPlacementActive
  (= isOpeningBuildActive || isOpeningMoveActive) while the body branches on
  the operands individually, so a build->move transition kept a stale closure.
  Now lists both individual flags.
- floorplan-panel showOpeningGhost: dropped three stable module-level schema
  imports from the dep array.

Also converts one string concat to a template literal (useTemplate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Aymericr Aymericr merged commit 5551500 into pascalorg:main Jun 16, 2026
2 checks passed
pull Bot pushed a commit to qili724427533-cmyk/editor that referenced this pull request Jun 16, 2026
- core: fix layer violation in level-height.ts — extract sceneRegistry
  import, replace with optional WallBaseYResolver callback so core stays
  pure (no Three.js mesh state); viewer callers pass resolver, headless
  callers (MCP/tests) get deterministic node-data-only result

- core: generalise port-connectivity service from duct-only to all
  distribution families — match partners by distributionRole ('run' →
  endpoint stretch, 'fitting' → rigid follow) instead of hard-coded
  duct-segment/duct-fitting type names; add system-compat guard so
  cross-system ports (e.g. supply duct vs waste pipe) don't fuse

- editor: fix port-snap rotation bug in move tool — pass preview node
  at live rotation into resolvePortSnap so own-port positions reflect
  any mid-drag R/T rotation before computing the snap delta

- editor: wire pipe-trap into UI — add to StructureTool union,
  MepToolKind, MEP_ITEMS Build-tab tile, and structure-tools action menu

- test: add port-connectivity-pipe.test.ts — 2 tests covering
  pipe-fitting → pipe-segment endpoint drag and cross-system isolation

- test: fix stale pipe-auto-fitting.test.ts wye expectation — author
  deliberately chose square sanitary-tee for DWV side-taps (documented
  in PR description and PipeFittingNode schema); update the one test
  that still expected wye to match the implemented behaviour

- nit: fix optional-chain biome warning in validate-dwv.ts
pull Bot pushed a commit to qili724427533-cmyk/editor that referenced this pull request Jun 16, 2026
- core: decouple drag-follow from distributionRole — add
  portConnectivityFollow flag to NodeDefinition; pipe-trap opts out
  (portConnectivityFollow: false) so dragging a connected pipe endpoint
  stretches the trap arm instead of yanking the anchored trap fixture

- core: remove the module-level getLevelHeight cache entirely — it was
  keyed only by nodes-object identity, which could return stale heights
  for in-place mutations by pure/headless callers. The function is now
  fully pure and deterministic; viewer hot path recomputes per frame as
  before (the cache only ever skipped the resolver-free branch)

- test: harden port-connectivity-pipe.test.ts — real DuctSegmentNode
  cross-family isolation case (was waste-vs-vent), new pipe-trap anchor
  case (run drag doesn't move trap; trap drag still stretches run), and
  beforeEach/afterEach registry reset instead of leaky beforeAll

- nit: biome format/import-order on all touched files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants