fix(editor): preserve grab offset when moving items (no teleport-to-cursor)#416
Merged
Conversation
…ursor) Grabbing a placed item to move it (move-cross handle or the Move affordance) snapped the item's origin to the cursor instead of keeping the offset between the grab point and the origin. - Floor (regression): the grab-offset rewrite patched event.localPosition, but floorStrategy.move reads event.position (the world cursor) for its world-grid snap on the default non-Shift path -- a read added after the offset fix landed. Now also correct event.position, derived from the corrected building-local point via buildingLocalToWorld. - Wall / ceiling / item-surface / shelf: the shared surface strategies snapped the origin straight to the cursor with no grab offset. Added per-surface grab anchors (start + (raw - anchor)) seeded on the first move per host and reset on leave/detach; host-resting items start in their surface, and the level-reparent effect skips intentionally-hosted drafts so the dragged mesh stays on its host. Rename preserveFloorDragOffset -> preserveDragOffset (it now governs all surfaces). 2D move paths already preserved the grab offset, so no sibling change is needed -- this brings 3D into parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Behaviour-preserving readability pass before shipping: - Extract the floor grab-offset IIFE into a named `applyFloorGrabOffset` helper (parity with `resolveHostSurfaceWorld`); drop redundant tuple casts via the return annotation and tighten the comment. - Wall move: always set `position` (raw hit when the wall mesh is absent) instead of a conditional object spread. A reviewed dedup of the three per-surface anchors was deliberately NOT taken — the wall (X/Y) / ceiling (X/Z) / host-surface frames are heterogeneous enough that unifying them adds more indirection than it removes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aymericr
added a commit
that referenced
this pull request
Jun 16, 2026
…ursor) (#416) * fix(editor): preserve grab offset when moving items (no teleport-to-cursor) Grabbing a placed item to move it (move-cross handle or the Move affordance) snapped the item's origin to the cursor instead of keeping the offset between the grab point and the origin. - Floor (regression): the grab-offset rewrite patched event.localPosition, but floorStrategy.move reads event.position (the world cursor) for its world-grid snap on the default non-Shift path -- a read added after the offset fix landed. Now also correct event.position, derived from the corrected building-local point via buildingLocalToWorld. - Wall / ceiling / item-surface / shelf: the shared surface strategies snapped the origin straight to the cursor with no grab offset. Added per-surface grab anchors (start + (raw - anchor)) seeded on the first move per host and reset on leave/detach; host-resting items start in their surface, and the level-reparent effect skips intentionally-hosted drafts so the dragged mesh stays on its host. Rename preserveFloorDragOffset -> preserveDragOffset (it now governs all surfaces). 2D move paths already preserved the grab offset, so no sibling change is needed -- this brings 3D into parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(editor): tidy grab-offset floor/wall paths (quality pass) Behaviour-preserving readability pass before shipping: - Extract the floor grab-offset IIFE into a named `applyFloorGrabOffset` helper (parity with `resolveHostSurfaceWorld`); drop redundant tuple casts via the return annotation and tighten the comment. - Wall move: always set `position` (raw hit when the wall mesh is absent) instead of a conditional object spread. A reviewed dedup of the three per-surface anchors was deliberately NOT taken — the wall (X/Y) / ceiling (X/Z) / host-surface frames are heterogeneous enough that unifying them adds more indirection than it removes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 4b7e0ec)
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.
What does this PR do?
Fixes the move-grab "teleport": grabbing an already-placed item to move it (the move-cross handle, or the Move affordance) snapped the item's origin to the cursor instead of preserving the offset between where you grabbed it and the item's origin. Grabbing a sofa by its left edge and nudging it shouldn't recenter it under the pointer.
Root cause was two distinct problems:
Floor items — a regression. The grab-offset correction rewrote
event.localPosition, butfloorStrategy.movereadsevent.position(the raw world cursor) for its world-grid snap on the default (non-Shift) path — that read was introduced when world-grid alignment landed (editor: building rotation, alt-click single wall, world-grid alignment, tap-to-engage move #388), after the offset fix (be0f491b). So the correction was silently bypassed and the item snapped its origin to the grid-snapped cursor. The correction now also rewritesevent.position, derived from the corrected building-local point via a newbuildingLocalToWorld(the exact inverse of the transform that producedlocalPosition; buildings rotate only about Y, so the affine identity holds exactly).Wall / ceiling / item-surface / shelf items — pre-existing. The shared surface strategies snap the item's origin straight to the cursor hit with no grab offset. Added per-surface grab anchors (seeded on the first move per host, reset on leave/detach) that offset every move by
start + (raw − anchor)in the surface's own frame — mirroring the floor path and the existing door/window move tools, with the world point re-derived so the visual cursor stays synced with the mesh. Items resting on a table/shelf now start in their host surface so the first move preserves the offset instead of snapping viaenter(), and the level-reparent effect skips intentionally-hosted items so the dragged mesh stays on its host.Renamed the coordinator's
preserveFloorDragOffset→preserveDragOffset(it now governs all surfaces).2D parity: the 2D move paths already preserve the grab offset (relative-mode
resolvePlanarCursorPosition), so no 2D sibling change is needed — this brings 3D into parity with 2D.Out of scope: roof-wall-mounted items (rare; the face-UV frame needs separate handling). 2D door/window keep
mode:'absolute'on purpose (a wall opening snaps to the wall nearest the cursor — documented).How to test
Notes for reviewers
Diagnosed and adversarially reviewed with an independent multi-agent review and Codex; both converged. The floor world↔local math was verified correct; one host-reparent issue (host-resting items being ejected to the level mid-drag) was found by both reviewers and fixed by gating the reparent effect.
Checklist
bun checkclean on the changed files)@pascal-app/editor+@pascal-app/nodes)mainbranch