Skip to content

feat: HVAC ductwork + DWV plumbing systems#402

Open
sudhir9297 wants to merge 66 commits into
pascalorg:mainfrom
sudhir9297:fix/wed-jun-10
Open

feat: HVAC ductwork + DWV plumbing systems#402
sudhir9297 wants to merge 66 commits into
pascalorg:mainfrom
sudhir9297:fix/wed-jun-10

Conversation

@sudhir9297

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 / 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 / wyes / sanitary tees), plumbing fixtures (toilets / sinks / tubs with drain ports), pipe-trap, a riser diagram + validation services, and a pipe-fitting placement tool.
  • 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. DWV — pick DWV Pipe and draw a segment; confirm it draws level by default and only slopes when you hold/press S. Confirm bends/wyes/sanitary tees auto-mint at junctions, place a fixture (toilet/sink/tub) and confirm its drain port mates to the pipe.
  4. Select any MEP node and confirm the system pill appears; move a connected node and confirm mated fittings/segments follow.
  5. Run bun check-types — all 12 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 30 commits June 11, 2026 18:21
Every equipment type now exposes one refrigerant port on its +X
service-valve face (condenser/air-handler near the bottom third,
furnace up at the cased A-coil), advertised at the nominal 7/8"
suction OD so a lineset run mates cleanly. Geometry grows the matching
service-valve detail (copper stubs + valve bodies), and the duct
collar defaults drop to 8" round.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The refrigerant-side sibling of duct-segment: a copper suction +
liquid line pair as a polyline, joining a split system's outdoor
condenser to the indoor coil. Same two-click draw model and
selection-time path handles, but it snaps onto refrigerant service
ports instead of duct collars. Schema + event wiring in core,
geometry / floorplan / tool / handle system in nodes, StructureTool id
and a Build tab tile in the editor app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drawing a new run off another run's open end at an angle now mints a
correctly-oriented elbow fitting at the joint instead of leaving a butt
joint. planElbowAtPort (shared/auto-fitting.ts) plans the elbow purely
from the joint port and the leaving direction: angle = the actual turn
(15-90°, vertical turns included via a full 3D basis transfer), inlet
collar exactly on the run's port, and the new duct pulled back to the
outlet collar so duct meets metal.

The draw tool tracks which port each segment end snapped to and commits
duct + fittings as one createNodes batch. Joints onto fittings /
equipment / terminals stay direct connections — only run-to-run corners
get a fitting. Straight continuations and back-turns past 90° are left
as plain joints.

Tests round-trip every plan through the fitting kind's own port math to
prove both collars mate (position + direction) in horizontal, 45°, and
riser cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The elbow's junction was placed one leg beyond the existing run's end,
bulging the corner outward past where the user clicked. Now the
junction centers exactly on the drawn corner: the plan returns a
trimmedPortPoint one leg back along the existing run, the draw tool
shortens that run to it (skipping the fitting when the trim would
consume the run), and the trim + elbow + new duct commit through
applyNodeChanges as a single undo step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Starting a duct on another run's BODY (centerline snap, new
findNearestRunBodyXZ) now splits the trunk and mints a tee at the tap
point: the original node keeps the upstream half trimmed one run-leg
short, a new duct-segment carries the downstream half starting one leg
after, and the branch leaves square from the tee's collar (the drawn
direction is projected perpendicular to the trunk axis — vertical
drops included). Split + tee + branch commit through applyNodeChanges
as a single undo step.

Taps with no room for the run legs (too near a segment end) or drawn
parallel to the trunk fall back to plain placement; run end ports keep
priority over body hits so elbow joints still win near an open end.

planTeeAtRunBody round-trips through the fitting kind's own port math
in tests: all three collars mate, split halves end exactly on the run
collars, polyline trunks split inside the hit segment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ending (or starting) a duct on an existing elbow's open collar left
the elbow at its old orientation — a sloped run arriving at a riser
elbow butt-joined at the wrong angle. planElbowRealign now patches the
elbow in place: junction and the mated collar stay exactly where they
were (the other run stays connected), the free collar swings to face
the drawn direction, and the elbow's angle adjusts to the turn that
requires. The drawn duct starts/ends at the swung collar.

Realigns outside the elbow's 15-90° range, non-elbow fittings, and
unknown ports are left as plain joints. Patch ships in the same
applyNodeChanges as the new duct — one undo step.

Tests verify the mated collar is bit-identical before/after and the
free collar lands on the returned point facing the run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The doc's "System" primitive: buildPortComponents groups every
port-bearing node into connected components via coinciding ports
(union-find, same tolerance as port-connectivity), and
summarizeSystemFor reports a component's distribution loops, run
count + total length, fitting / terminal / equipment counts, and the
key health signal — connectedToEquipment.

The floating action menu surfaces it: selecting any HVAC node shows a
pill above move/duplicate/delete with the system ("Supply · 7.2m ·
3 runs · 2 registers") and an amber "⚠ no equipment" warning when the
tree doesn't reach a furnace / air handler — orphaned subtrees are
visible at a glance. The fitting's rotation-axis pill stacks beneath
it in the same slot.

Core tests cover component grouping (tolerance, port-less bystanders)
and summaries (full tree stats, orphan detection) via stub registry
definitions, keeping core free of nodes-package imports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Duct segments gain shape: 'round' | 'rect' with width/height in inches
(default 14x8 trunk) — real US systems are a rect trunk with round
branches. Q toggles the shape in the draw tool (ghost preview becomes
a box, pill reads W/H instead of diameter, run commits named "Trunk");
the inspector switches between diameter and width/height fields.

Geometry builds box sections with a stable basis (width stays
horizontal — the minimal-rotation cylinder quaternion would roll the
profile on axis-aligned runs), cube joints at interior points, and a
matching insulation shell. The floor plan draws rect runs at their
actual width.

Joints stay round: rect ports advertise the area-equivalent diameter
(2*sqrt(wh/pi), clamped to the fitting ceiling), so tee taps on a rect
trunk mint a sensibly-sized round tee and split halves inherit the
rect profile. Old scenes parse unchanged via schema defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drawing off an existing node now CONTINUES its cross-section: snapping
the segment start onto a port inherits the owner's profile — a rect
trunk end keeps drawing rect at its exact W×H, a round run / fitting
collar keeps its diameter, equipment / terminal collars start round at
the port's size. The ghost preview, pill readout (W·H vs Ø), ceiling
offset, and committed node all follow. Body taps (tee branches) keep
the tool's own profile, since branches off trunks are round.

duct-fitting gains shape/width/height: rect elbows and tee run-legs
build as boxes at the trunk's profile with a cube junction (tee branch
collar stays round), inspector swaps diameter for W/H. The planners
carry profiles through — planElbowAtPort takes a DuctProfile (rect
elbows mint at the trunk's W×H with the area-equivalent diameter
driving leg lengths), planTeeAtRunBody stamps the trunk's shape onto
the tee. Old scenes parse unchanged via schema defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rect elbow was two box stubs pushed into an axis-aligned cube, so
the legs interpenetrated and the corner read as colliding boxes at any
angle off 90°. It's now ONE closed swept solid: the rect profile runs
from the inlet face through a miter ring on the corner's bisector
plane (2D miter-join offset, exact for the elbow's 15-90° range) to
the outlet face — a crisp seam like a folded sheet-metal square elbow.
Non-indexed triangles give flat face normals for the metal look.

The rect tee also drops its junction cube: one straight rect box
inlet→outlet with the round branch stub tapping its side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The five HVAC tiles were borrowing unrelated PNGs (wall, column,
window, elevator). BuildType gains an `iconify` alternative to
`iconSrc`, rendered via @iconify/react: wind (duct), git-branch
(fitting), air-vent (register), heater (HVAC unit), cable (lineset).
The duct-terminal registry presentation icon aligns to air-vent too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First kind of the research doc's Phase 2 (DWV plumbing): pipe-segment,
the plumbing sibling of duct-segment on the same polyline + typed-port
machinery, with SLOPE as the new ingredient.

- Schema: path / diameter (1¼–8" nominal) / pvc-abs-cast-iron /
  system waste|vent. Slope lives in the path Y coordinates; Y may go
  below the floor.
- Draw tool: two-click segments where WASTE runs fall automatically at
  the IPC default ¼" per foot of horizontal run (the pill's Y delta
  shows the live drop); vents stay level. Q toggles waste/vent, [ / ]
  steps nominal sizes, Alt-drag draws vertical stacks, 45° lock with
  Shift free, port snap scoped to the new DWV port systems so drains
  never mate onto duct or refrigerant collars.
- Geometry: capped PVC/ABS/cast-iron sections with coupling hubs;
  floor plan follows drafting convention (waste solid at pipe width,
  vents dashed, stacks as circles).
- System graph counts pipe runs, the floating-menu pill covers them,
  Build tab gains a DWV Pipe tile (lucide:droplets).

Deferred: DWV fittings (wye / sanitary tee), fixtures, traps, IPC
validators, riser view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pipe-fitting kind (elbow / wye / sanitary-tee) with typed ports and
the plumbing-correct joint vocabulary, minted automatically by the
pipe draw tool:

- Corner joints: a run drawn off another run's open end at an angle
  mints a BEND — planCornerJoint extracts the duct elbow's junction /
  trim / collar math into a domain-agnostic planner shared by both.
- Body taps: starting on a run's side splits it and mints the
  code-correct entry — a 45° WYE leaning downstream on horizontal
  drains, a square SANITARY TEE on vertical stacks (axis steeper than
  ~45° reads as a stack).
- findNearestRunBodyXZ takes a kind filter so pipe taps target pipes
  and duct taps target ducts.

Fittings carry pvc/abs/cast-iron material and waste/vent system from
the run they join; no placement tool (a loose DWV fitting isn't a real
workflow) but full inspector editing, floorplan symbol (45° branch at
true plan angle), system-graph counting, and the action-menu pill.
Joints round-trip through the fitting's own port math in tests.

Note: src/shared/floorplan-cursor.test.ts fails under bun from a
pre-existing three-bvh-csg import issue (ships in 8dc602c) —
unrelated to this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
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.

1 participant