fix: nearest-first hover/selection — hidden walls win over the furniture behind them - #697
Conversation
…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>
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>
… 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>
…lf-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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 655a6f7. Configure here.
| // them). Same indigo so hover reads as "this will select", weaker so a | ||
| // hovered-then-selected wall still steps up on click. | ||
| const HOVER_EMISSIVE_BLEND = 0.4 | ||
| const HOVER_EMISSIVE_INTENSITY = 0.2 |
There was a problem hiding this comment.
Hover glow stronger than selection
Medium Severity
The new hidden-wall hover glow is documented as weaker than selection so a click “steps up,” but HOVER_EMISSIVE_INTENSITY (0.2) is higher than SELECTION_EMISSIVE_INTENSITY (0.12) while both use the same blend. Invisible wall materials also default emissiveIntensity to 1, so Math.max leaves both highlights at full intensity and the hover→select step-up never appears on X-ray walls.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 655a6f7. Configure here.
Julien's three feedbacks complete (line-set routed to the AH with cross-trade awareness; under-floor DWV tree with frost sleeves; selection fix pairs with editor #697), wave-2 blockers B17 slab truth + B19 return-air truth, MEP glyph de-collision. 1054 tests at the pin; 3D visual round APPROVE with numeric evidence. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>


User report: with the Bones X-ray on, hovering a wall highlighted/selected the furniture BEHIND it — walls were unselectable where framing rendered.
Root causes (browser-verified):
event.intersectionsat the wall's own depth — any depth/epsilon rule starves. Intersections are now ranked by hit ownership (selection-hit-owner.ts): passive hits (framing members, wrappers, zones, gizmos) never outrank the wall; selectables (devices, openings, items) win via hosted / ≤0.35m epsilon / wall-anchored rules.RegisteredSystemsderived once on mount (late-registering kinds never mounted their systems), one bad lazy chunk unmounted every system, and WallSystem read a stale dirty-set closure. All fixed + a placeholder self-heal sweep every 30 frames.Browser QA: APPROVE, 7/7 — bay/stud clicks select the wall (bonus: nearest hidden wall wins with two interposed), cold-load-with-X-ray selects immediately and at +30s, receptacle priority through interposed walls, furniture from inside, door MOVE + PLACE on hidden walls, solid mode. 37 new tests across
selection-hit-owner,pointer-transparency(incl. the exact probe7 hit-shape replay),wall-placeholder-sweep,wall-material-variant. Root suite 3623 pass / 0 fail; turbo 13/13; biome clean.Pairs with plugin-bones
d8bcc5d(framing meshes raycast-transparent) — robust standalone without it.Known trade-off (deliberate): manually-hidden walls (wallMode 'down' without Bones) are hoverable again. Pre-existing, untouched: empty-space click doesn't clear selection (identical in solid baseline); canvas cursor stays 'auto' over walls.
🤖 Generated with Claude Code
Note
Medium Risk
Changes core wall pointer/selection ranking and geometry rebuild self-heal. Wrong ownership or epsilon rules can mis-route hover/click between walls, openings, and furniture.
Overview
Hidden walls in X-ray / down mode now win hover and selection against furniture behind them, instead of being blanket pointer-transparent. Hits are ranked by ownership (self-wall, other-wall, selectable, passive): framing overlays and wrappers never steal the wall; hosted openings, face-mounted devices (0.35 m epsilon), and wall-mounted gear on a wall further down the ray still win.
Hovered hidden walls get a visible indigo stipple glow (
hover-invisible). Visible/translucent walls keep the outline pass.Also unsticks walls that never left placeholder geometry:
RegisteredSystemsre-derives on registry version and isolates each system in its own Suspense;WallSystemreads livedirtyNodesand periodically re-marks unbuilt placeholders.Reviewed by Cursor Bugbot for commit 655a6f7. Bugbot is set up for automated code reviews on this repo. Configure here.