fix: hidden walls stay ray targets while a door/window tool is active - #689
Merged
Conversation
…ol is active #683 made walls hidden by the wall-mode pass ('down' X-ray mode, cutaway faces, auto-mode interior partitions) pointer-transparent so clicks reach the visible device/service boxes behind their invisible full-height collision meshes. But the door/window MOVE and PLACE tools drive their whole cursor model from the wall pointer events those handlers emit (wall:enter/move/click/leave): with hidden walls silent, the ray falls through to the ground grid, grid:move free-follows, and the opening detaches from its wall as a red world-axis-aligned ghost ("rotates in place in red 90 degrees") instead of sliding along the wall. Before #683 those drags worked in X-ray only by accident of the same defect that misfired outlet clicks. Fix: a counter-based hidden-wall pointer HOLD in core (holdHiddenWallPointerEvents / hiddenWallPointerEventsHeld). The four wall-opening tools (door + window, move + place) acquire it for their active lifetime; the wall renderer's gated handlers keep events flowing while a hold is live. No tool active means no hold, so plain selection clicks still pass through hidden walls — the night-5 D4 outlet fix is intact (MoveRegistryNodeTool never touches the hold; its cursor is the ground plane). Delete-mode hover keeps its existing exception. The gate predicate is extracted to wallPointerEventsSuppressed so the truth table is pinned by tests without an R3F rig. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Snoopy147
added a commit
that referenced
this pull request
Aug 20, 2026
…gesture (#694) * fix(nodes): door/window MOVE drags ride the node's own wall and undo as one step Two night-6 QA defects in the opening move tools, both fallout around #689's hidden-wall pointer hold: 1. Wrong-wall capture: with every hidden wall a ray target, nearest-hit-wins let a hidden wall interposed between the camera and the dragged opening's own wall catch the wall:move stream — the drag rode a wall the user cannot see and the commit silently re-parented the opening onto it. New `shouldIgnoreWallEventForOpeningMove` gate (shared/opening-move-wall-gate): while MOVING an existing opening, a hidden wall may drive the drag only if it is the node's own wall (grab wall / current mid-drag host); visible walls always pass, so cross-wall re-parenting needs an explicit, visible target. Ignored events don't stop propagation, so the ray falls through to the own wall behind. PLACE / isNew duplicates keep the all-walls behavior. 2. Multi-entry undo per drag gesture: the tools paused history with a RAW `temporal.pause()`, invisible to the refcounted getSceneHistoryPauseDepth. zundo reads isTracking AFTER a write's subscribers run, so a cooperating system's balanced pauseSceneHistory/resumeSceneHistory pair (the space-detection sync, on any mid-drag reparent that touches wall children) zeroed the refcount and resumed tracking mid-gesture — the mid-drag writes became their own undo entries (transient states: commit fired at drag-arm, door hidden/reparented, orphan opening at the drop spot). New `beginOpeningMoveHistorySession` (shared/opening-move-history) holds the refcounted LEASE for the gesture and opens one deliberate tracking window (`commitStep`) for the drop write: mid-drag tracks nothing, drop records exactly one entry whose past state is the restored pre-drag baseline, cancel records nothing. Store-level tests pin the one-entry contract, the exact-baseline undo, lease composition, and the raw-pause leak this replaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(nodes): biome format for the opening-move-history test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Snoopy147
added a commit
that referenced
this pull request
Aug 20, 2026
…irst
INVESTIGATION (user report: with the Bones X-ray on, mousing over a wall
highlights/selects the furniture BEHIND it):
(a) The hover + click-select path is R3F's per-mesh pointer events
(useNodeEvents on each node's meshes -> mitt emitter wall:enter /
item:click / ... -> SelectionManager's select-mode subscriptions, which
set hoveredId / selection and stopPropagation). R3F delivers through
the distance-sorted intersection list until propagation stops.
(b) Why walls lost: the Bones framing renderer auto-switches the host
wallMode to 'down'; WallCutout stamps userData.wallHidden=true on every
wall; the wall renderer's #683 gate then early-returned EVERY pointer
event (blanket pointer transparency, no stopPropagation), so R3F fell
through to the furniture behind. The framing members that visually
occupy the wall's volume are handler-less InstancedMeshes (never
raycast candidates), so nothing at the wall's depth could win.
(c) SOLID / visible walls do NOT lose: wallPointerEventsSuppressed returns
false for visible walls, the wall is the nearest interactive hit, and
SelectionManager stops propagation on it — no ordering bug in the
selection raycast itself. The defect is exclusively the hidden-wall
blanket transparency.
FIX: nearest-first with wall-furniture priority. A hidden wall's gated
handlers now reduce the event's intersection list (extractWallSelectionRay)
and handle the event unless something outranks the wall:
- its own hosted subtree (doors / windows / wall-mounted children) at ANY
depth gap — immune to grazing-angle inflation;
- any non-wall hit at <= wallHit + 0.35m (devices flush/proud/recessed at
the face, objects in front of the wall);
- wall-MOUNTED hits further down the ray — non-wall hits within epsilon
of another wall's collision hit (the #683 night-5 D4 receptacle behind
an interposed hidden wall keeps winning).
Other walls' hits never compete directly, so parallel hidden walls can't
both yield and drop the event into the room behind — the nearest wall wins
by delivery order. Events without ray data fall back to #683 transparency.
Unchanged: delete-mode hover, the #689 hidden-wall pointer hold (door /
window MOVE+PLACE tools — #694's own-wall gate keeps filtering those
downstream), visible walls never suppress.
Trade-off (pure host-side rule, no plugin presence flag): in a manual
'down' wall mode with no overlay rendering at the wall, the wall strip is
hover/selectable again even though it draws nothing there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Snoopy147
added a commit
that referenced
this pull request
Aug 21, 2026
…ure behind them (#697) * fix(nodes/wall): hidden walls join hover/selection raycasts nearest-first INVESTIGATION (user report: with the Bones X-ray on, mousing over a wall highlights/selects the furniture BEHIND it): (a) The hover + click-select path is R3F's per-mesh pointer events (useNodeEvents on each node's meshes -> mitt emitter wall:enter / item:click / ... -> SelectionManager's select-mode subscriptions, which set hoveredId / selection and stopPropagation). R3F delivers through the distance-sorted intersection list until propagation stops. (b) Why walls lost: the Bones framing renderer auto-switches the host wallMode to 'down'; WallCutout stamps userData.wallHidden=true on every wall; the wall renderer's #683 gate then early-returned EVERY pointer event (blanket pointer transparency, no stopPropagation), so R3F fell through to the furniture behind. The framing members that visually occupy the wall's volume are handler-less InstancedMeshes (never raycast candidates), so nothing at the wall's depth could win. (c) SOLID / visible walls do NOT lose: wallPointerEventsSuppressed returns false for visible walls, the wall is the nearest interactive hit, and SelectionManager stops propagation on it — no ordering bug in the selection raycast itself. The defect is exclusively the hidden-wall blanket transparency. FIX: nearest-first with wall-furniture priority. A hidden wall's gated handlers now reduce the event's intersection list (extractWallSelectionRay) and handle the event unless something outranks the wall: - its own hosted subtree (doors / windows / wall-mounted children) at ANY depth gap — immune to grazing-angle inflation; - any non-wall hit at <= wallHit + 0.35m (devices flush/proud/recessed at the face, objects in front of the wall); - wall-MOUNTED hits further down the ray — non-wall hits within epsilon of another wall's collision hit (the #683 night-5 D4 receptacle behind an interposed hidden wall keeps winning). Other walls' hits never compete directly, so parallel hidden walls can't both yield and drop the event into the room behind — the nearest wall wins by delivery order. Events without ray data fall back to #683 transparency. Unchanged: delete-mode hover, the #689 hidden-wall pointer hold (door / window MOVE+PLACE tools — #694's own-wall gate keeps filtering those downstream), visible walls never suppress. Trade-off (pure host-side rule, no plugin presence flag): in a manual 'down' wall mode with no overlay rendering at the wall, the wall strip is hover/selectable again even though it draws nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(nodes/wall): reduce the pointer ray lazily — visible walls skip it extractWallSelectionRay walks the event's intersection list and each hit's parent chain; visible walls never suppress, so doing that per hover move over every visible wall was wasted work. Gate the reduction on the wallHidden stamp the predicate already consumes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(nodes/wall): rank hidden-wall selection by hit OWNERSHIP, not raw depth QA f2 browser round falsified the premise that overlay meshes never reach event.intersections: R3F's event raycast recurses through the level / building wrapper groups (they carry pointer handlers), so Bones framing InstancedMeshes land in the list at the wall's own depth (probe6/probe7: d 4.246-4.422 vs wall 4.246) — all inside the epsilon tie-break, so the previous rule yielded everywhere framing renders and the click still fell through to the Double Bed. The wall's own RENDER mesh rides the same list at identical depth, which would have made the old hostedByThisWall subtree test self-defeating the moment walls build real geometry. Every hit is now classified by its NEAREST sceneRegistry-registered ancestor (selection-hit-owner.ts): - 'self-wall' (own collision/render/trim) -> neutral - 'other-wall' -> anchor only, never a direct competitor - 'selectable' (built-in selectable kinds + registry capabilities.selectable, e.g. bones:device) -> real competitor - 'passive' (bones:framing members, level/ building wrappers, zones, gizmos, grid, unregistered ancestry) -> never outranks Competitors win via: hosted-by-this-wall (any depth), <= wallHit + 0.35m, or within epsilon of another wall's hit (D4 interposed-wall receptacle). A hosted door resolves to the DOOR (registered deeper than its host wall), so 'self-wall' never swallows it. The reverse Object3D->id lookup rebuilds lazily off sceneRegistry.revision. This is robust standalone: even before the plugin-side raycast stub (plugin-bones d8bcc5d) lands, framing hits classify passive; any future overlay without the selectable capability behaves the same. Tests replay probe7 session B's exact hit shape (framing at wall depth + bed behind -> wall wins) plus the classifier truth table (self/other-wall, hosted door, framing passive, device selectable, wrapper/zone passive, deleted-node passive, revision-following lookup). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(viewer): walls can never stay degenerate — system resilience + self-heal QA f2 probe5/probe6: a scene LOADED with the Bones X-ray already active kept all 24 wall collision meshes as degenerate placeholder points with no wallHidden stamps and untouched base materials — geometry rebuild, cutout stamping, AND batching were all inert for 16+ seconds while other useFrame consumers (camera-controls) demonstrably ran. All three live in the ONE registry-mounted WallSystems bundle, so the failure is the bundle not running, not a per-wall skip. Toggling the X-ray mid-session (probe7) works, so the trigger is load-time mounting/ordering. Three fixes close the class: 1. RegisteredSystems re-derives its kind list on useRegistryVersion(). The list was snapshotted ONCE at mount (useMemo []), so any kind registering after that render — async plugin discovery, HMR — never mounted its system; a first render before ANY kinds register mounted nothing, permanently. Same staleness class SelectionManager already guards against. 2. Per-kind Suspense boundaries. One shared boundary meant any pending or failing lazy system chunk unmounted EVERY system (wall pipeline included) while it hung. 3. Wall self-heal sweep (wall-placeholder-sweep.ts, every 30 frames): any registered wall still on its mount-time placeholder geometry (stamped userData.placeholder, 3-vertex fallback signature) with no dirty mark is re-marked, so the normal rebuild path converges no matter what consumed the mount-time mark or when the system came up. The frame body now also reads the LIVE dirty set instead of a render-closure copy — a store-level set REPLACEMENT (scene load, plugin install) in the window before React commits no longer hides fresh marks. Walls now build their collision geometry regardless of initial visibility or load order — the invariant the hidden-wall selection gate needs to engage at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(viewer): biome import order for the wall-system sweep import Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer): hovered hidden walls glow — the X-ray hover affordance QA f2 (REVISE): hidden walls became hover/selection ray targets (nearest-first), but an invisible wall gave NO feedback under the cursor — what the user saw lighting up on hover was the furniture BEHIND it. WallCutout now tracks the select-mode hovered wall (useViewer.hoveredId, hoverHighlightMode 'default' only, so the paint-preview snapshot/restore flows never interleave) and draws a hovered HIDDEN wall with a hover variant of its invisible stipple film: the same indigo emissive treatment as the wall selection highlight at a softer blend/intensity (0.28/0.07 vs 0.4/0.12), so hover reads as "this will select" and still steps up on click. Visible and translucent walls keep their existing hover affordance (the post-processing outline pass) — no double-highlight. Mechanics: the per-wall material choice is extracted into a pure resolveWallMaterialVariant truth table (delete > selection > hover > base, hover arm only for the invisible base) consumed via materialsForVariant; getSelectionHighlightMaterial generalizes into getEmissiveHighlightMaterial with per-variant cache + profile, adding getHoverHighlightMaterials. The hovered wall id joins WallCutout's highlightKey so hover changes refresh the material pass immediately. Known limit (pre-existing, noted per QA): the canvas cursor stays 'auto' over walls even in solid mode — not addressed here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * tune(viewer): hover glow visible at normal zoom (QA: was ~2-5/255) The F2 browser round measured the hidden-wall hover glow at mean delta 0.13/255 over the viewport — functionally correct, invisible without A/B flipping. Blend 0.28->0.4, intensity 0.07->0.2 per the QA's 2-3x recommendation; selection emphasis (0.4/0.12) still reads stronger via its blend+dot treatment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User report with Bones' X-ray on: dragging a door/window "disconnects, rotates in place in red 90° from the wall".
Root cause: #683 made hidden walls pointer-transparent (the outlet-click fix) — but the door/window MOVE and PLACE tools drive their entire cursor model from the wall pointer events those handlers emit. With hidden walls silent, the ray falls through to the ground grid,
grid:movefree-follows, and the opening detaches as a red world-axis-aligned ghost instead of sliding along its wall. Before #683 those drags worked in X-ray only by accident of the same defect that misfired outlet clicks.Fix: a counter-based hidden-wall pointer HOLD in core (
holdHiddenWallPointerEvents). The four wall-opening tools acquire it for their active lifetime; the wall renderer keeps pointer events flowing while a hold is live. No tool active = no hold, so plain selection clicks still pass through hidden walls — the night-5 outlet fix is intact (MoveRegistryNodeTool never touches the hold). Delete-mode hover keeps its exception. The gate predicate is extracted (wallPointerEventsSuppressed) so the truth table is pinned by tests without an R3F rig.Tests: hold compose/idempotence + the suppression truth table. Verified live in X-ray: doors slide along their wall and commit with framing recomputing around the new opening; outlet click-through unchanged.
🤖 Generated with Claude Code
Note
Medium Risk
Touches editor pointer routing on hidden wall collision meshes across four tools and the wall renderer; behavior is test-backed but regressions could affect X-ray selection vs opening placement.
Overview
Fixes door/window move and place tools breaking in X-ray when walls are visually hidden but still have collision: those tools depend on
wall:*events, so pointer-transparent hidden walls caused floor free-follow and a detached red ghost instead of sliding along the wall.Adds a counter-based
holdHiddenWallPointerEvents/hiddenWallPointerEventsHeldAPI in core (exported from@pascal-app/core). All four opening tools acquire a hold for their effect lifetime and release it on cleanup (idempotent release for safe React teardown).Refactors the wall renderer gate into
wallPointerEventsSuppressed: hidden walls stay pointer-transparent for plain selection (outlet click-through unchanged), but keep events when delete hover mode is active or any hidden-wall hold is live. Unit tests cover hold compose/idempotence and the suppression truth table.Reviewed by Cursor Bugbot for commit 96d518e. Bugbot is set up for automated code reviews on this repo. Configure here.