From 5f603e83c412a71e2b83106929f75cda3ad65ca0 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Sat, 27 Jun 2026 20:08:17 -0400 Subject: [PATCH] fix(editor): unify wall-endpoint move activation; stop wall-move from co-firing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes to the wall-endpoint reshape interaction. selection-manager: a node:click is synthesized on R3F pointer-up, so an endpoint handle that sits on the wall body lets the wall mesh (raycast-hit behind it from a 3D angle) emit its own click on the same release — selecting the wall and arming its move tool on top of the endpoint move. Ignore the body click while an `endpoint` reshape owns the pointer. Scoped to `endpoint` so hole-edit (which relies on node clicks to exit) is unaffected. move-endpoint-tool: a press-drag committed on release but a tap dismissed, and whether the tap's release ran at all raced the window pointer-up listener mounting a tick after the handle's pointerdown ("works once, then needs a long press"). Unify on one rule: commit only when the endpoint actually moved, otherwise stay armed. A tap now grabs the endpoint (it follows the cursor; the next click after a move commits) exactly like a press-drag — both engage identically. Drops the now-dead hasDraggedRef. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/editor/selection-manager.tsx | 10 ++++++++++ .../nodes/src/wall/move-endpoint-tool.tsx | 20 +++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/components/editor/selection-manager.tsx b/packages/editor/src/components/editor/selection-manager.tsx index 443cd444..728c214e 100644 --- a/packages/editor/src/components/editor/selection-manager.tsx +++ b/packages/editor/src/components/editor/selection-manager.tsx @@ -1418,6 +1418,16 @@ export const SelectionManager = () => { // Skip if box-select just completed (drag ended over a node) if (boxSelectHandled) return + // node:click is synthesized on pointer-up (use-node-events). A wall/fence + // endpoint handle sits ON the wall body, so from a 3D angle the wall mesh + // is raycast-hit behind it and the SAME pointer-up also emits the wall's + // click — which would select + arm the wall move tool on top of the + // endpoint move. While an endpoint reshape owns the pointer, ignore the + // body click so only the reshape tool handles the release. (Scoped to + // `endpoint`: hole-edit relies on node clicks to exit, just below.) + const activeScope = useInteractionScope.getState().scope + if (activeScope.kind === 'reshaping' && activeScope.reshape === 'endpoint') return + const node = event.node // A ceiling is selectable only through its corner handles, never via diff --git a/packages/nodes/src/wall/move-endpoint-tool.tsx b/packages/nodes/src/wall/move-endpoint-tool.tsx index cf6aa0e5..cb17ec66 100644 --- a/packages/nodes/src/wall/move-endpoint-tool.tsx +++ b/packages/nodes/src/wall/move-endpoint-tool.tsx @@ -177,7 +177,6 @@ function getLinkedWallUpdates( } export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ target }) => { - const hasDraggedRef = useRef(false) const previousGridPosRef = useRef(null) const altPressedRef = useRef(false) const nodeIdRef = useRef(target.wall.id) @@ -366,7 +365,6 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ triggerSFX('sfx:grid-snap') } previousGridPosRef.current = alignedPoint - hasDraggedRef.current = true // Stand the magnetic beacon at the endpoint when it locked onto existing // wall geometry (corner / midpoint / crossing / wall body). @@ -390,20 +388,22 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ // through to the selection manager and arms the wall MOVE tool, a mode the // user never asked for. swallowNextClick() - // Press-release without drag: dismiss the tool without committing. - if (!hasDraggedRef.current) { - useViewer.getState().setSelection({ selectedIds: [nodeId] }) - setAngleLabel(null) - exitMoveMode() - return - } const preview = previewRef.current ?? { start: originalStart, end: originalEnd } const hasChanged = !( samePoint(preview.start, originalStart) && samePoint(preview.end, originalEnd) ) - if (hasChanged && isSegmentLongEnough(preview.start, preview.end)) { + // Endpoint still at its original spot: this release is the *grab* of a + // click-to-move (a tap on the handle, or a press that never dragged). Stay + // armed so the endpoint keeps following the cursor — the next release after + // an actual move commits. A press-drag and a click thus engage identically; + // previously the no-drag branch dismissed the tool, and whether it even ran + // raced the window pointer-up listener mounting (hence "works once, then + // needs a long press"). + if (!hasChanged) return + + if (isSegmentLongEnough(preview.start, preview.end)) { wasCommitted = true const linkedUpdates = altPressedRef.current