Skip to content

Panel-owned X-ray wall-mode takeover; export the headless engine surface - #2

Open
JonathanNus wants to merge 1 commit into
pascalorg:masterfrom
JonathanNus:fix/xray-wall-mode-lifecycle
Open

Panel-owned X-ray wall-mode takeover; export the headless engine surface#2
JonathanNus wants to merge 1 commit into
pascalorg:masterfrom
JonathanNus:fix/xray-wall-mode-lifecycle

Conversation

@JonathanNus

Copy link
Copy Markdown

The bug

Leaving the Bones panel keeps the host stuck in the X-ray view. The wall-mode takeover in framing/renderer.tsx writes useViewer.setWallMode('down') from an effect whose cleanup is tied to the node's lifetime, but the intent ("get the walls out of the way while I'm looking at the X-ray") is tied to the panel's. Sidebar tab switches unmount the panel, not the renderer, so nothing restores — and since wallMode is a persisted viewer preference, the stuck view survives page reloads. The sharpest edge: merely opening a saved scene that contains a bones:framing node blanks the host's walls before the user touches anything.

This also runs against the plugin appearance contract (#625): the renderer was writing host presentation state, which the contract says plugins subscribe to read-only.

The fix

  • The save/set/restore state machine moves to src/view-takeover.ts, viewer-import-free so it tests headlessly. Behaviours pinned by unit tests: restores the previous mode on release; takes no ownership when the user already had walls down; never stomps a mode the user picked while the panel was open; double-engage keeps the original mode.
  • panel.tsx engages it while a live X-ray exists (seeThrough !== false) and releases on unmount — i.e. exactly when the user leaves.
  • framing/renderer.tsx no longer touches host state; it only draws.

Long term, a scoped, owner-tokened presentation override on useViewer (push/pop) would be the contract-sanctioned way to do this — happy to discuss; this PR just makes the current behaviour match the current contract.

Also: headless engine exports

index.ts now exports the pure derivation surface (computeLevel, computeLevelUncached, computeTakeoff, cutList, row/member types, extractLevels, extractRoofs). The pipeline is already pure and fully tested headlessly; the exports let a host estimator compute the same quantities the panel shows — per level, without mounting anything. computeLevelUncached is exported so a whole-building rollup loop doesn't thrash the 1-deep memo the panel and 3D renderer share. No behaviour change for existing consumers.

Tests

499 pass, 0 fail (bun test); tsc --noEmit clean. New: src/view-takeover.test.ts (6 tests).

…ss engine

Two changes a host needs:

1. The wall-mode takeover ('down' while X-raying) moves from the 3D
   renderer to the panel. The renderer lives as long as the bones:framing
   node does, so leaving the Bones tab kept the host's walls hidden — and
   because wallMode is a persisted viewer preference, the stuck view
   survived reloads, and merely OPENING a scene that contained an X-ray
   node blanked the walls. The panel unmounts exactly when the user
   leaves, which is the lifetime this intent actually has. The state
   machine lives in view-takeover.ts (viewer-import-free, unit tested:
   restore on leave, never stomp a user-picked mode, no ownership when
   walls were already down). Also aligns with the plugin appearance
   contract (#625): the renderer no longer writes host presentation state.

2. index.ts exports the pure derivation surface — computeLevel,
   computeLevelUncached, computeTakeoff, cutList, the row/member types,
   extractLevels/extractRoofs — so a host estimator can compute the same
   quantities the panel shows without mounting anything.
   computeLevelUncached is exported because a whole-building rollup loop
   would otherwise thrash the 1-deep memo the panel and renderer share.
gorkking pushed a commit to gorkking/Pascal-Editor-plugin-bones that referenced this pull request Aug 23, 2026
…er condenser, mis-dragged heat-pump override all WARN

Condenser-honesty set (prod report pascalorg#2 'I don't see the heat pump'):

F1 compute.ts — a level whose zones can't feed the room-derived engines
composed ZERO hvac/plumbing output while framing/electrical rendered
fine, with zero words (hunt 7a walls+slab+no-zones, 7b garage+hallway,
7c outdoor-only, 7d building-parented zones, 4b/4c two-storey zone/level
mismatch, roof levels). One warning covering both systems, keyed off the
ACTUAL compose, three honest classes: no indoor zones / all zones
outdoor (the day-9 delta that honestly-but-silently removed a
previously-drawn unit) / no habitable rooms.

F2 compute.ts — each level mints its condenser at LEVEL-LOCAL grade, so
an upper-storey X-ray drew the 'outdoor' unit at facade height (hunt
4a). Warning route, not levelId remount: the renderer mounts foreign
MEMBERS only (buildGroups builds fixture-less foreign groups) — a
remount would orphan the condenser/disconnect fixtures at facade height
and the line-set still needs an unmodeled facade riser. Ground-storey
compose byte-equal (master-baseline recaptured identical).

F3 hvac.ts — a moved heat-pump service point won verbatim with no check
(hunt 5a living-room unit, 5f 13 m into the yard). The override still
WINS (A4); it now warns when it lands inside an indoor zone (named room;
wall-band guard kills the centerline-anchor false positive) or beyond
the NEC 210.63 25 ft service reach from every exterior wall — threshold
basis stated in-file (the 15 m line-set advisory is a run-length class
and would bless the 13 m drag).

Suite 1591 green; baseline byte-identical; hunt matrix re-run: every
class warns, healthy scenes unchanged.

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