From c3659acdf81b7546dbaa26718f41321654ecb938 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Thu, 23 Jul 2026 09:58:28 -0400 Subject: [PATCH] fix(editor): don't flag camera as dragging on ACTION.NONE controlstart (#536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit camera-controls fires controlstart for every pointerdown — including buttons mapped to ACTION.NONE (plain left click in edit mode). Since #535 that set cameraDragging=true with no rest/sleep ever following to clear it, so every canvas click (selection, wall placement) was suppressed once the camera was at rest. Only flag dragging when currentAction actually drives the camera, and clear the flag on controlend for mapped-button taps with zero movement (no wake -> no rest/sleep). Co-authored-by: Claude Fable 5 --- .../editor/custom-camera-controls.tsx | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/components/editor/custom-camera-controls.tsx b/packages/editor/src/components/editor/custom-camera-controls.tsx index d415fb97..1b4ce7e3 100644 --- a/packages/editor/src/components/editor/custom-camera-controls.tsx +++ b/packages/editor/src/components/editor/custom-camera-controls.tsx @@ -457,11 +457,14 @@ export const CustomCameraControls = () => { } }, [freezeActivePoseInterpolation]) - const beginLocalCameraInteraction = useCallback(() => { - cancelPoseApplication() - cameraDraggingLifecycle.begin() - emitter.emit('camera-controls:interaction-start', undefined) - }, [cameraDraggingLifecycle, cancelPoseApplication]) + const beginLocalCameraInteraction = useCallback( + ({ dragging = true }: { dragging?: boolean } = {}) => { + cancelPoseApplication() + if (dragging) cameraDraggingLifecycle.begin() + emitter.emit('camera-controls:interaction-start', undefined) + }, + [cameraDraggingLifecycle, cancelPoseApplication], + ) const applyPendingPose = useCallback(() => { if (isFirstPersonMode) { @@ -1123,10 +1126,18 @@ export const CustomCameraControls = () => { // Cancel any in-progress 2D-origin navigation pose when the user starts // dragging (right-click orbit, middle-click pan, touch). `controlstart` // fires only for user pointer interactions — not for programmatic - // moveTo/rotateTo which emit `transitionstart` instead. + // moveTo/rotateTo which emit `transitionstart` instead. It also fires for + // pointerdowns whose button is mapped to ACTION.NONE (plain left click in + // edit mode); those must not flag the camera as dragging — no rest/sleep + // ever follows to clear the flag, which would leave canvas clicks + // (selection, placement) suppressed until the next real camera move. const handleControlStart = useCallback(() => { clearPendingFloorplanNavigationPose() - beginLocalCameraInteraction() + beginLocalCameraInteraction({ + dragging: controls.current + ? controls.current.currentAction !== CameraControlsImpl.ACTION.NONE + : false, + }) }, [beginLocalCameraInteraction, clearPendingFloorplanNavigationPose]) // Preview mode: auto-navigate camera to selected node (viewer behavior) @@ -1435,6 +1446,16 @@ export const CustomCameraControls = () => { cameraDraggingLifecycle.end() }, [cameraDraggingLifecycle]) + const onControlEnd = useCallback(() => { + // A mapped-button tap with zero camera movement never wakes the + // controls, so no rest/sleep follows — clear the dragging flag on + // release. While damping is still settling (`active`), rest/sleep + // clears it instead. + if (!controls.current?.active) { + cameraDraggingLifecycle.end() + } + }, [cameraDraggingLifecycle]) + // Preset capture mode frames a single subtree (often a 0.3–2m preset), // so the default 2m minDistance prevents the user from getting close // enough to compose a good thumbnail. Relax the clamp to 0.5m while @@ -1455,6 +1476,7 @@ export const CustomCameraControls = () => { minDistance={minDistance} minPolarAngle={0} mouseButtons={mouseButtons} + onControlEnd={onControlEnd} onControlStart={handleControlStart} onUpdate={handleCameraUpdate} onRest={onRest}