Skip to content

fix: stop node tops lifting floor placements, and keep batched walls pickable - #686

Merged
wass08 merged 2 commits into
mainfrom
fix/placement-y-node-top-election
Aug 19, 2026
Merged

fix: stop node tops lifting floor placements, and keep batched walls pickable#686
wass08 merged 2 commits into
mainfrom
fix/placement-y-node-top-election

Conversation

@wass08

@wass08 wass08 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Two regressions that landed in main on 2026-08-18, both reproduced by hand in the community editor before and after the fix.

1. Items and presets pushed up the Y axis (#638)

pointer-support-cap.ts flipped node-top election from opt-in to opt-out — options?.includeNodeTopSurfaces === false became the only way out, so every caller that passed nothing started electing node tops. The same PR gave item a capabilities.surfaces.top, widening the candidate set from ['wall','item','column'] to wall, slab, ceiling, cabinet, column, item, shelf and block.

A ray aimed at a floor crosses every upward-facing face above that floor first. In a finished room that is the ceiling: placing an item at the centre of a ceilinged room elects the ceiling's top face (nearest hit, normal.y ≈ 1) and resolveFrozenFloorPlacementPatch freezes it into the draft's authored Y, so the item sits at ceiling height instead of on the floor. Verified against the real function — elects elevation: 2.7, sourceNodeId: ceiling where the previous behaviour returned 0. Walls the ray passes over do the same in a narrower band.

Restores the opt-in, keeping #638's registry-driven kind discovery (the list is still derived from capabilities.surfaces.top, not hardcoded). Only the tools that build on a surface ask for it: wall (already did), column, fence, stair, block. Item placement, registry move/presets and slab drafting go back to placing against the floor the pointer indicates.

Also excludes the node the active interaction is placing or moving. Its mesh rides the cursor, so electing its own top would raise it by its own height on every pointer move. Both tools happen to neuter the dragged mesh's raycast today, but that is each tool's private convention and async-mounted item children are only covered on the next frame — the election owns the invariant now.

2. Wall hover outline and paint preview dead (#608)

#608 sews a level's walls into one mesh once they settle (8+ walls, 180 ms quiet) and moves each sewn wall off SCENE_LAYER onto BATCHED_LAYER via hideBatchedWall, so it costs no draw call while staying in the graph with its R3F pointer handlers attached.

R3F picks with one shared raycaster whose default mask is SCENE_LAYER alone, so a batched wall stops being hit: no wall:enter (no hover outline, no paint preview), no wall:move, no wall:click. Selection is the circular case — a selected wall leaves the batch, but the click that would select it never lands. #608 saw this for measurement and added setSurfaceRaycastLayers for the raycasters that module builds; the shared event raycaster was never opted in.

PointerRaycastLayers enables the bit on it. Additive rather than setSurfaceRaycastLayers, which resets the mask — right for the private per-query raycasters it was written for, wrong for the one every pointer event goes through.

Testing

  • Full suite: 4100 pass, 0 new failures. The 11 door floor clearance / DrawingSheetNode failures are pre-existing and order-dependent — they fail identically on 6cfff809 without this branch.
  • check-types green, biome clean.
  • New coverage: the opt-in default and the interacting-node exclusion in pointer-support-cap.test.ts; the batched-layer raycast invariant in scene-visibility.test.ts.
  • Manual A/B in the community editor: both bugs reproduced on 6cfff809, both gone on this branch.

🤖 Generated with Claude Code


Note

Medium Risk
Touches core pointer placement and shared viewer raycasting; behavior is localized with explicit opt-in and tests, but wrong defaults would affect all floor placement and wall interaction.

Overview
Fixes two editor regressions: floor placements snapping to ceilings and batched walls losing pointer interaction.

Pointer support (resolvePointerSupportSurface) — Node-top election is opt-in again via includeNodeTopSurfaces: true (default off), so item/preset/slab placement uses the floor under the cursor instead of the nearest upward face along the ray. Structural tools that build on surfaces (stair, block, column, fence) pass the flag explicitly. The resolver also skips the node under active place/move (useInteractionScope) so a preview cannot lift itself on its own top.

Viewer picking — New PointerRaycastLayers additively enables BATCHED_LAYER on React Three Fiber’s shared raycaster so walls moved off SCENE_LAYER when merged into a level batch still receive hover, paint, and click.

Tests cover the default/off behavior, interacting-node exclusion, and batched-layer raycast matching.

Reviewed by Cursor Bugbot for commit f2eb6b6. Bugbot is set up for automated code reviews on this repo. Configure here.

wass08 and others added 2 commits August 18, 2026 16:36
#638 made the pointer support election consider node tops by default —
`options?.includeNodeTopSurfaces === false` opted out, so every caller that
passed nothing (item placement, registry move/presets, slab drafting) started
electing them. It also gave `item` a `surfaces.top`, widening the candidate set
from wall/item/column to wall, slab, ceiling, cabinet, column, item, shelf,
block.

A ray aimed at a floor crosses every upward-facing face above that floor first.
In a finished room that is the ceiling: placing an item at the room centre
elects the ceiling's top face (nearest hit, normal.y ≈ 1) and freezes it into
the draft's authored Y via `resolveFrozenFloorPlacementPatch`, so the item sits
at ceiling height instead of on the floor. Walls the ray passes over do the
same in a narrower band.

Restore the opt-in. Keep #638's registry-driven discovery — the kind list is
still derived from `capabilities.surfaces.top` rather than hardcoded — but only
the tools that build ON a surface ask for it: wall (already did), column, fence,
stair, block. Item placement, registry move and slab drafting go back to
placing against the floor the pointer indicates.

Also exclude the node the active interaction is placing or moving. Its mesh
rides the cursor, so electing its own top would raise it by its own height every
pointer move. The tools neuter the dragged mesh's `raycast` for their own
pointer routing, which happens to cover this today — but that is each tool's
private convention, and async-mounted item children are only neutered on the
next frame. The election owns the invariant now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#608 sews a level's walls into one mesh once they settle (8+ walls, 180ms
quiet). Each sewn wall is moved off SCENE_LAYER onto BATCHED_LAYER by
`hideBatchedWall` so it costs no draw call while staying in the graph — with
its R3F pointer handlers still attached.

R3F picks with one shared raycaster whose default mask is SCENE_LAYER alone, so
a batched wall stops being hit: no `wall:enter` (no hover outline, no paint
preview), no `wall:move`, no `wall:click`. Selection is the circular case — a
selected wall leaves the batch, but the click that would select it never lands.
#608 saw this for measurement and added `setSurfaceRaycastLayers` for the
raycasters that module builds; the shared event raycaster was never opted in.

Enable BATCHED_LAYER on it. Additive rather than `setSurfaceRaycastLayers`,
which resets the mask — right for the private per-query raycasters it was
written for, wrong for the one every pointer event goes through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wass08
wass08 merged commit 7e1990f into main Aug 19, 2026
2 of 3 checks passed
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