Skip to content

fix: nearest-first hover/selection — hidden walls win over the furniture behind them - #697

Merged
Snoopy147 merged 7 commits into
mainfrom
fix/nearest-first-selection
Aug 21, 2026
Merged

fix: nearest-first hover/selection — hidden walls win over the furniture behind them#697
Snoopy147 merged 7 commits into
mainfrom
fix/nearest-first-selection

Conversation

@Snoopy147

@Snoopy147 Snoopy147 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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):

  1. fix: hidden walls no longer swallow pointer events #683's blanket pointer-transparency removed hidden walls from hover/selection candidacy entirely — the ray fell through to whatever was behind.
  2. The first fix attempt exposed a deeper truth: R3F's event raycast recurses through level/building wrapper groups, so passive overlay meshes (Bones framing instances) ride event.intersections at 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.
  3. Scenes LOADED with the X-ray already active never built wall collision geometry — RegisteredSystems derived 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.
  4. Hidden walls had NO hover affordance — hovered hidden walls now glow (indigo emissive stipple variant; constants tuned after pixel-diff QA).

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: RegisteredSystems re-derives on registry version and isolates each system in its own Suspense; WallSystem reads live dirtyNodes and 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.

Snoopy147 and others added 7 commits August 20, 2026 15:55
…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>
@Snoopy147
Snoopy147 merged commit a046ef1 into main Aug 21, 2026
3 checks passed
@Snoopy147
Snoopy147 deleted the fix/nearest-first-selection branch August 21, 2026 06:02

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 655a6f7. Configure here.

Snoopy147 added a commit that referenced this pull request Aug 21, 2026
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>
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