nodes: add MEP movement controls and DWV parity#438
Conversation
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>
Duct draw tool's ceiling mode now hangs each path point just below the ceiling actually covering it (per-room heights tracked), with a translucent surface highlight and a plumb line to the floor so the in-flight point reads clearly from any angle. Dragging a duct corner that sits on a fitting now carries the fitting's other ducts along (port-connectivity second hop), so the joint moves together instead of tearing apart. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Alt detaches a dragged duct/pipe endpoint or fitting from its connected joint (no elbow re-aim, no connectivity follow); Ctrl/Cmd drives vertical riser movement on the fitting move. Behavioral parity across 2D and 3D. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bring pipe-segment endpoint drags and pipe-fitting moves to parity with duct: free-drag endpoints, Alt-detach, Ctrl/Cmd-vertical riser, elbow re-aim, and connectivity follow. Generalizes the shared elbow-reaim and auto-fitting helpers to dispatch by run kind so 2D and 3D share one path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add violet directional arrow affordances to duct-fitting selection (height, move cross, rotate arc) mirroring the duct-segment rig: portaled into the parent frame to stay out of the selection outline, rendered via the shared HandleArrow, and carrying mated-run connectivity through the single-undo dance. The move cross engages press-drag-release (placementDragMode) the same way the floating drag does, so the markup hit-areas go inert and the fitting move tool commits on pointer-up. Also re-export the HandleArrow primitives from @pascal-app/editor and extend the duct-segment side-move/floorplan affordances. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the hover-reveal / multi-handle selection rigs with a single click-to-latch cube that opens a directional cluster, shared between duct segments and fittings via a new selection-handles module (HandleCube / MoveChevron / RotateArc, all sized to the roof pitch cube). - Duct segment: per-vertex + run-center cubes reveal axis-locked move chevrons (down arrow always shown), plus a roll arc at the run center. - Duct fitting: center cube reveals six ±XYZ move arrows and three per-axis rotation arcs (oriented in place), replacing the old height/move/rotate trio with axis-cycling. - Rotation (fitting arcs + duct roll) snaps to 45° steps; Shift = smooth. - thin chevron profile + press-drag-release commit retained.
Build a fully-determined basis for the duct roll gizmo so the curved arrow always seats at the top-outer 45° corner regardless of run direction, instead of an arbitrary apex from a single setFromUnitVectors. The selection-rig ±Y arrows now own vertical movement, so the redundant Ctrl-modifier riser drag is removed from the fitting move tool. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Align the duct run-center cube + horizontal arrows to the run axis (matching the per-vertex handles) while keeping whole-run translate. Endpoint side / up-down swing arrows now follow grid snap points and port-snap onto nearby collars (Shift sweeps smoothly). Relax elbow realign + fitting schemas to flatten to a straight 0° coupling. Surface HVAC-specific hints in the select-mode helper panel for duct / fitting. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Linesets and liquid lines now commit one independent two-point node per drawn segment instead of folding into a single mitered polyline, so each line selects and deletes on its own. Endpoint caps fill shared-coordinate joints so connected segments still read as continuous pipe. Dragging a shared endpoint carries mated segments along via port connectivity (Alt detaches), so a run still edits as one welded piece. Liquid-line follow mode traces the whole connected lineset run, laying a per-segment parallel line down its full assembled length. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lifting/lowering a connected run with the run-center cube now keeps each connected end welded to its stationary partner instead of dragging the whole network. Run-to-run ends get the classic S/Z offset (two elbows + plumb riser, partner trimmed back one leg); elbow-connected ends form a clean L — the existing elbow stays put and re-aims its collar vertical, with one new top elbow + riser reconnecting to the lifted endpoint. The offset is ghosted live and minted as a single undo step on release. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Measure roof accessory placement against the active roof face using visible surface bounds and preview geometry footprints. Add dormer-local guides and special linear handling for ridge vents and gutters.
# Conflicts: # apps/editor/components/build-tab.tsx # apps/editor/components/viewer-toolbar.tsx # packages/editor/src/index.tsx # packages/viewer/src/systems/wall/wall-materials.ts
- auto-fitting: tee branch now follows the drawn lateral angle; update the stale square-tee test + doc comment that contradicted the rewrite - duct-segment: re-enable the vertical auto-offset rewind (the disabled stub left mintedIds empty, so re-dragging a tagged duct stranded old elbows/risers and stacked duplicates); remove the dead stub - duct-segment: strip the stale auto-offset tag on manual corner/roll commits so the horizontal-move path no longer trusts an out-of-date base - chimney: resume history before mutating segment children arrays so a cross-segment move reparents in one tracked transaction (undo stays consistent) - dormer: align schema test with the new windowSill=false default
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a20298f. Configure here.
| className="size-4 object-contain" | ||
| height={16} | ||
| src="/icons/dwv-pipes.png" | ||
| width={16} |
There was a problem hiding this comment.
Trap button broken icon path
Low Severity
The new Add Trap control loads /icons/dwv-pipes.png, but the app only ships dwv-pipes.webp (same path as the DWV pipe tile). The trap affordance can show a missing image while the rest of the pipe UI uses the webp asset.
Reviewed by Cursor Bugbot for commit a20298f. Configure here.
| { id: nodeId, data: { path: translationPlan.ductPath } as Partial<AnyNode> }, | ||
| ...translationPlan.updates, | ||
| ], | ||
| }) |
There was a problem hiding this comment.
Translation move skips offset invalidation
Medium Severity
When a whole-run move commits via planRunTranslationOffsets and applyNodeChanges, the batch updates paths and mints fittings but never runs autoOffsetInvalidationUpdates. Ducts still carrying an auto-offset tag can keep stale minted/base metadata after auto-routed elbows are inserted, unlike the connectivity-only commit path used elsewhere in this PR.
Reviewed by Cursor Bugbot for commit a20298f. Configure here.
| ? 'run' | ||
| : single?.type === 'duct-fitting' || single?.type === 'pipe-fitting' | ||
| ? 'fitting' | ||
| : null |
There was a problem hiding this comment.
Lineset help hints missing
Low Severity
The new MEP contextual-help path only treats duct-segment, pipe-segment, duct-fitting, and pipe-fitting as MEP selections. Selecting a lineset or liquid-line (which this PR adds endpoint handle editing for) still gets generic select hints instead of the run-style “click handle dot / Alt detach” guidance.
Reviewed by Cursor Bugbot for commit a20298f. Configure here.
| if (metadata) data.metadata = metadata as DuctSegmentNode['metadata'] | ||
| updates.push({ id: mate.duct.id, data }) | ||
| } | ||
| return [...updates, ...invalidations] |
There was a problem hiding this comment.
Duplicate onDelete patches clobber
Medium Severity
When both endpoints of the same run mate one deleted elbow, onDelete emits two full path patches for the same node id. deleteNodesAction applies them in order with shallow merge, so the second patch replaces the first and only one endpoint is moved back to the junction.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit a20298f. Configure here.


What does this PR do?
Adds a broad MEP interaction pass for ducts, DWV pipe, linesets, liquid lines, and roof accessories, with the branch now merged forward from current main. The PR introduces run-aligned cube/arrow handles, vertical and run-translation auto-routing, connected-joint endpoint movement, DWV pipe/fitting parity with duct movement controls, continuous drawing for DWV/refrigerant lines, shared endpoint controls for linesets/liquid lines, and delete repair for auto-inserted DWV bends so pipe lengths restore when the bend is removed.
How to test
bun run checkand confirm Biome exits successfully. Current main emits existing material-work warnings/infos, but the command exits cleanly.bun run buildand confirm the monorepo build completes.bun run check-typesand confirm all workspace typechecks complete.bun test packages/nodes/src/shared/pipe-vertical-offset.test.ts packages/nodes/src/shared/pipe-run-translation-offset.test.ts packages/nodes/src/shared/vertical-offset.test.ts packages/nodes/src/shared/run-translation-offset.test.ts packages/nodes/src/pipe-fitting/parametrics.test.tsand confirm the focused MEP routing suite passes.Screenshots / screen recording
Screen recording not attached yet — this is a visual/interactive editing change and should get a quick reviewer clip before merge.
Checklist
bun devbun checkto verify)mainbranchNote
High Risk
Large changes to port connectivity propagation and delete/reconcile paths affect core scene editing for all duct/pipe joints; mistakes could corrupt paths or undo stacks across connected MEP graphs.
Overview
MEP connectivity and fittings —
port-connectivitynow walks the full joint graph (not one hop): fittings translate rigidly, runs stretch along-axis and translate perpendicular without skew, with new tests. Duct fitting moves follow connected runs (Alt detaches); parametrics addonDeleteelbow repair, auto-offset tag handling, rect defaults, 0° coupling angles, body paint/slots, and inspector W/H swap. Schema adds optionalslotson duct nodes; pipe fittings/traps get similar angle/default tweaks.Editor handles and UX — Registry adds
LatchHandle,faceNormal, andlatchGroupfor collapsed arrow clusters; dormers get front/back depth handles, window latch cube, and placement guides on roof. Arrow handles gain a thinner chevron profile; contextual help surfaces MEP handle hints. Build tab adds Add Trap (pipe-trap); cursor sphere can show the dot at the tip for ceiling-height placement.Viewer and preview — Translucent wall mode cycles through toolbars, command palette, and preview overlay; preview adds first-person walkthrough (controls + pointer lock).
getCeilingAt/getCeilingHeightAtsupport ceiling-aware placement. Roof accessory move/place tools publish shared roof-surface placement guides (chimney move gets cancel/reparent fixes).Deletes —
deleteNodesinvokes per-kindparametrics.onDeletebefore removal so neighbours restore consistently in one undo step.Reviewed by Cursor Bugbot for commit a20298f. Bugbot is set up for automated code reviews on this repo. Configure here.