fix: stop node tops lifting floor placements, and keep batched walls pickable - #686
Merged
Conversation
#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>
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.
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.tsflipped node-top election from opt-in to opt-out —options?.includeNodeTopSurfaces === falsebecame the only way out, so every caller that passed nothing started electing node tops. The same PR gaveitemacapabilities.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) andresolveFrozenFloorPlacementPatchfreezes it into the draft's authored Y, so the item sits at ceiling height instead of on the floor. Verified against the real function — electselevation: 2.7, sourceNodeId: ceilingwhere the previous behaviour returned0. 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
raycasttoday, 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_LAYERontoBATCHED_LAYERviahideBatchedWall, 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_LAYERalone, so a batched wall stops being hit: nowall:enter(no hover outline, no paint preview), nowall:move, nowall: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 addedsetSurfaceRaycastLayersfor the raycasters that module builds; the shared event raycaster was never opted in.PointerRaycastLayersenables the bit on it. Additive rather thansetSurfaceRaycastLayers, 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
door floor clearance/DrawingSheetNodefailures are pre-existing and order-dependent — they fail identically on6cfff809without this branch.check-typesgreen, biome clean.pointer-support-cap.test.ts; the batched-layer raycast invariant inscene-visibility.test.ts.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 viaincludeNodeTopSurfaces: 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
PointerRaycastLayersadditively enablesBATCHED_LAYERon React Three Fiber’s shared raycaster so walls moved offSCENE_LAYERwhen 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.