From 1082b625521d3fccd5e6a56e0a8164d5a503fe66 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Fri, 15 May 2026 17:29:04 -0400 Subject: [PATCH] useDragAction: activation-click grace + curve fence commit sfx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy CurveFenceTool ignored grid:click for 150ms after mount — otherwise the very click that activates a tool (e.g. the floating menu "curve" button) cascades through the R3F drei portal into the grid, fires grid:click on the just-mounted tool, and commits the drag before any preview move runs. The new useDragAction was missing this guard, so the Stage D fence curve port "click → place sfx → exit" without ever letting the user adjust. Adds `activationGraceMs` (default 150) on useDragAction; ports the sfx:item-place commit emission into FenceCurveTool so the kind-owned tool matches legacy UX. Same guard will cover the upcoming Stage D ports (endpoint move, whole-fence move, placement, plus slab/ceiling/wall D). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/editor/src/hooks/use-drag-action.ts | 19 ++++++++++++++++++- packages/nodes/src/fence/curve-tool.tsx | 9 +++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/hooks/use-drag-action.ts b/packages/editor/src/hooks/use-drag-action.ts index c66b2bd3..97674f79 100644 --- a/packages/editor/src/hooks/use-drag-action.ts +++ b/packages/editor/src/hooks/use-drag-action.ts @@ -42,6 +42,14 @@ export type UseDragActionArgs = { onCommit?: () => void /** Fires once after `action.cancel` (Esc, unmount, or commit-returns-false). */ onCancel?: () => void + /** + * Milliseconds after activation during which `grid:click` is swallowed. + * Stops the very click that mounted this tool (a DOM button or 3D + * handle elsewhere) from cascading into the grid and immediately + * committing the drag. Defaults to 150ms — matches the legacy guard + * used by every kind-owned tool entered via a click. + */ + activationGraceMs?: number } /** @@ -72,12 +80,21 @@ export function useDragAction(args: UseDragActionArgs) { session.start(argsRef.current.initial) + const activatedAt = Date.now() + const graceMs = argsRef.current.activationGraceMs ?? 150 + const onMove = (event: GridEvent) => { const point: readonly [number, number] = [event.localPosition[0], event.localPosition[2]] session.move(point, modifiersFromGridEvent(event)) } - const onClick = (_event: GridEvent) => { + const onClick = (event: GridEvent) => { + // Swallow the click that mounted this tool — otherwise the very + // first grid:click cascades into commit() before any move(). + if (Date.now() - activatedAt < graceMs) { + event.nativeEvent?.stopPropagation?.() + return + } session.commit() } diff --git a/packages/nodes/src/fence/curve-tool.tsx b/packages/nodes/src/fence/curve-tool.tsx index ed7e642f..63a7525a 100644 --- a/packages/nodes/src/fence/curve-tool.tsx +++ b/packages/nodes/src/fence/curve-tool.tsx @@ -1,7 +1,7 @@ 'use client' import { type FenceNode, getWallMidpointHandlePoint, useScene } from '@pascal-app/core' -import { CursorSphere, useDragAction, useEditor } from '@pascal-app/editor' +import { CursorSphere, triggerSFX, useDragAction, useEditor } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useEffect, useState } from 'react' import { curveFenceDragAction } from './actions/curve' @@ -31,7 +31,8 @@ export const FenceCurveTool: React.FC<{ node: FenceNode }> = ({ node }) => { initialHandle.y, ]) - const exitCurveMode = () => { + const exitCurveMode = (committed: boolean) => { + if (committed) triggerSFX('sfx:item-place') useViewer.getState().setSelection({ selectedIds: [node.id] }) useEditor.getState().setCurvingFence(null) } @@ -46,8 +47,8 @@ export const FenceCurveTool: React.FC<{ node: FenceNode }> = ({ node }) => { // placeholder until the first grid:move fires. point: [initialHandle.x, initialHandle.y], }, - onCommit: exitCurveMode, - onCancel: exitCurveMode, + onCommit: () => exitCurveMode(true), + onCancel: () => exitCurveMode(false), }) // Mirror the active curveOffset back into the cursor position. The