Skip to content

editor: fix wall item placement preview drifting off the item - #643

Merged
wass08 merged 2 commits into
mainfrom
fix/wall-placement-preview-frame
Aug 13, 2026
Merged

editor: fix wall item placement preview drifting off the item#643
wass08 merged 2 commits into
mainfrom
fix/wall-placement-preview-frame

Conversation

@wass08

@wass08 wass08 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Placing an item on a wall drew the green placement box in a different spot from the item it previews, and the item then committed somewhere neither of them was. Three independent causes:

  • The box and the node came from different quantities. wallStrategy snapped the node position in WALL-LOCAL coords but snapped the raw WORLD hit independently per axis for the cursor. Those are different lattices — wall-local X runs from wall.start, and wall-local Y is measured from the supporting slab's elevation (mesh.position.set(start[0], slabElevation, start[1])), not world zero. Every sibling strategy (floor / ceiling / roof-wall) already derives its cursor from the point it stores; wall was the odd one out. Both now come from resolveWallPlacementPose, which maps one wall-local point through the hit object's frame — the exact inverse of how localPosition was measured.

  • calculateCursorRotation returns the wall face yaw + π. Invisible for the z-symmetric door/window boxes it was written for, but it put the asymmetric wall-side item box (z ∈ [0, depth]) on the far side of the wall. The coordinator was already compensating for that π when publishing the 2D floorplan preview; that workaround goes away with the cause. The shared helper is left alone for door/window.

  • FloorElevationSystem never called floorPlaced.applies. Its own doc comment claims it "respects floorPlaced.applies so items with asset.attachTo (wall / ceiling mounted) are left alone", but only getFloorPlacedElevation did. For an opted-out node that resolver returns 0, so the system's write degenerated to mesh.position.y = position[1] — harmless while that's the node's host-local store value, but tools publish live transforms in WORLD space, so the moment a wall item started moving it copied the world Y into the mesh's wall-local Y slot every frame and floated the ghost off the wall. Hence the reported sequence: correct on wall:enter (no live transform yet), wrong throughout the move, correct again after commit (the coordinator clears the live transform first). Honouring applies also covers the other three opt-outs: ceiling items, cabinet modules inside a run, and non-floor duct terminals.

Two smaller consequences of deriving z from the hosting convention rather than the hit depth: wall-side items now store the face they mount on (matching ItemSystem's per-frame push, so no first-frame pop), and wall items stay centred in the thickness instead of being snapped clean out of a thick wall by snapToHalf.

How to test

  1. bun dev and open a scene with a room. A wall sitting on a slab (non-zero elevation) and a wall that is neither axis-aligned nor grid-aligned show the bug most clearly.
  2. Grab any attachTo: 'wall-side' catalog item and hover it over a wall face. The green box should wrap the item — same height off the floor, same spot along the wall, and extending out of the near face toward the room, not through the wall.
  3. Keep moving the cursor along the wall. The item must stay inside the box for the whole move (before this, it lifted off the wall by the slab elevation as soon as the pointer moved).
  4. Click to commit. The item lands exactly where the box was.
  5. Repeat with an attachTo: 'wall' item on a thick wall (≥ 0.6 m) — it should sit centred in the thickness rather than snapped out of it.
  6. Regression: place a door and a window on a wall; their preview boxes and commits are unchanged. Drag a floor item over an elevated slab; it still gets its floor-stack lift.

Screenshots / screen recording

N/A — no recording captured (the Chrome extension wasn't connected in this session). The before-state is the reported bug: box below and behind the item, item floating above it while moving.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

Note

Medium Risk
Changes core 3D placement and per-frame mesh Y during drags for wall-mounted items; behavior is localized with new tests but regressions could affect doors/windows or floor stacking if applies gates are wrong.

Overview
Fixes wall item placement where the green preview box, the moving ghost, and the committed position could disagree—especially on elevated slabs, diagonal walls, and wall-side assets.

Wall strategy introduces resolveWallPlacementPose so wallStrategy enter/move snap X/Y in wall-local space, set Z from hosting rules (wall-side on the hit face, wall centered in thickness) instead of snapping hit depth, and map the same point to world cursorPosition via the collision mesh. Preview rotation is now wall yaw + item rotation (replacing calculateCursorRotation on this path), so asymmetric wall-side boxes face outward and the 2D floorplan can use cursorRotationY directly.

Placement coordinator drops redundant world-position patching during wall drag offset correction; pose math lives in the strategy.

Floor elevation system skips nodes when floorPlaced.applies returns false, so live transforms published in world space no longer overwrite wall-local mesh Y during wall moves.

Adds placement-strategies.test.ts covering preview/commit alignment, face mounting, and validator-adjusted Y.

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

wass08 and others added 2 commits August 13, 2026 09:31
The placement wireframe and the item it previews were computed from two
different quantities: the node position snapped the hit in WALL-LOCAL
coords, while the cursor snapped the raw WORLD hit independently per axis.
Those are different lattices — wall-local X runs from `wall.start`, and
wall-local Y is measured from the supporting slab's elevation, not world
zero — so the box drifted off the item by the slab elevation plus up to a
grid step, and the commit (which follows the node) landed where the box
was not.

`calculateCursorRotation` also returns the wall face's yaw + π, which is
invisible for the z-symmetric door/window boxes it was written for but put
the asymmetric `wall-side` item box on the far side of the wall. The
coordinator already compensated for that π when publishing the 2D
floorplan preview; that workaround goes away with the cause.

Both now come from `resolveWallPlacementPose`, which maps one wall-local
point through the hit object's frame — the exact inverse of how
`localPosition` was measured. `z` follows the hosting convention rather
than the hit depth, so `wall-side` items store the face they mount on
(matching ItemSystem's per-frame push, no first-frame pop) and `wall`
items stay centred in the thickness instead of being snapped out of a
thick wall.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`FloorElevationSystem` documents that it "respects `floorPlaced.applies` so
items with `asset.attachTo` (wall / ceiling mounted) are left alone", but it
never called the predicate — only `getFloorPlacedElevation` did.

For an opted-out node that resolver returns 0, so the system's write
degenerated to `mesh.position.y = position[1]`. Harmless while `position` is
the node's own host-local store value, but tools publish live transforms in
WORLD space: the moment a wall item started moving, the system copied its
world Y into the mesh's wall-local Y slot every frame and the ghost floated
off the wall by the host frame's elevation. Hence the reported sequence —
correct on `wall:enter` (no live transform yet), wrong throughout the move,
correct again after commit (the coordinator clears the live transform first).

Honour `applies` before the write. Covers the other three opt-outs too:
ceiling items, cabinet modules inside a run, and non-floor duct terminals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wass08
wass08 merged commit aa6267c into main Aug 13, 2026
3 checks passed
@wass08
wass08 deleted the fix/wall-placement-preview-frame branch August 13, 2026 07:57
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