From c5be50698e31b48e28b512a64d14aa55108681b1 Mon Sep 17 00:00:00 2001 From: Yunine Date: Fri, 17 Jul 2026 17:25:15 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=94=BB=E5=B8=83?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E4=BA=A4=E4=BA=92=E4=BB=85=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E9=BC=A0=E6=A0=87=E4=B8=AD=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除鼠标右键拖拽画布的交互处理 保留鼠标中键拖拽画布功能 同步更新相关交互测试 --- .../useImageCanvasStageInteractions.test.tsx | 48 +++++++++++++++++++ .../useImageCanvasStageInteractions.ts | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/components/image-editor/useImageCanvasStageInteractions.test.tsx b/src/components/image-editor/useImageCanvasStageInteractions.test.tsx index 0db87d40a..9c1ccdde9 100644 --- a/src/components/image-editor/useImageCanvasStageInteractions.test.tsx +++ b/src/components/image-editor/useImageCanvasStageInteractions.test.tsx @@ -920,4 +920,52 @@ describe('useImageCanvasStageInteractions', () => { expect(flushMinimapViewportDrag).toHaveBeenCalledTimes(1); expect(onViewportInteractionEnd).toHaveBeenCalledTimes(2); }); + + it('allows middle-button dragging and rejects right-button dragging', () => { + render(); + + const viewport = screen.getByTestId('viewport'); + + act(() => { + fireEvent( + viewport, + new MouseEvent('pointerdown',{ + bubbles: true, + clientX: 100, + clientY: 100, + button: 1, + buttons: 4, + }), + ); + }); + expect(screen.getByTestId('panning').textContent).toBe('true'); + + act(() => { + fireEvent( + viewport, + new MouseEvent('pointerup',{ + bubbles: true, + clientX: 100, + clientY: 100, + button: 1, + buttons: 0, + }), + ); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + + act(() => { + fireEvent( + viewport, + new MouseEvent('pointerdown',{ + bubbles: true, + clientX: 100, + clientY: 100, + button: 2, + buttons: 2, + }), + ); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + }); }); diff --git a/src/components/image-editor/useImageCanvasStageInteractions.ts b/src/components/image-editor/useImageCanvasStageInteractions.ts index 1b94876b2..5f19ae5d3 100644 --- a/src/components/image-editor/useImageCanvasStageInteractions.ts +++ b/src/components/image-editor/useImageCanvasStageInteractions.ts @@ -239,7 +239,7 @@ export function useImageCanvasStageInteractions({ const handleCanvasPointerDown = useCallback( (event: ReactPointerEvent) => { const button = getPointerButton(event); - if (button !== 0 || effectiveTool === 'hand') { + if (button === 1 ||(button !== 2 && effectiveTool === 'hand')) { startPan(event); return; } -- 2.52.0 From 0b037f46e9e798d377589444a9b82b7f76e152d6 Mon Sep 17 00:00:00 2001 From: Yunine Date: Fri, 17 Jul 2026 20:35:28 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=B3=E9=94=AE?= =?UTF-8?q?=E6=8C=89=E4=B8=8B=E6=97=B6=E4=B8=8D=E5=86=8D=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E5=B9=B3=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImageCanvasStageInteractionModel.ts | 39 ++++++++++++------- .../useImageCanvasStageInteractions.ts | 38 +++++++++++++++--- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/components/image-editor/ImageCanvasStageInteractionModel.ts b/src/components/image-editor/ImageCanvasStageInteractionModel.ts index 08ceb1a0d..19445fc06 100644 --- a/src/components/image-editor/ImageCanvasStageInteractionModel.ts +++ b/src/components/image-editor/ImageCanvasStageInteractionModel.ts @@ -30,10 +30,13 @@ type PointerSource = { }; }; -type CanvasRectLike = { - left?: number; - top?: number; -} | null | undefined; +type CanvasRectLike = + | { + left?: number; + top?: number; + } + | null + | undefined; const CANVAS_GENERATION_DIALOG_MODES = new Set([ 'generate', @@ -64,12 +67,22 @@ function hasCanvasGenerationDialogMode( export function getPointerButton(event: PointerSource) { const nativeButtons = Number(event.nativeEvent?.buttons); - if (Number.isFinite(nativeButtons) && (nativeButtons & 4) === 4) { - return 1; + if (Number.isFinite(nativeButtons)) { + if ((nativeButtons & 2) === 2) { + return 2; + } + if ((nativeButtons & 4) === 4) { + return 1; + } } const syntheticButtons = Number(event.buttons); - if (Number.isFinite(syntheticButtons) && (syntheticButtons & 4) === 4) { - return 1; + if (Number.isFinite(syntheticButtons)) { + if ((syntheticButtons & 2) === 2) { + return 2; + } + if ((syntheticButtons & 4) === 4) { + return 1; + } } const syntheticButton = Number(event.button); if (Number.isFinite(syntheticButton)) { @@ -200,9 +213,8 @@ export function createLayerDragStart({ isMultiSelectGesture, }); const selectedCanvasLayerIds = getSelectedLayerIds(nextSelectedLayerIds); - const selectedGenerationDialogIds = getSelectedGenerationDialogIds( - nextSelectedLayerIds, - ); + const selectedGenerationDialogIds = + getSelectedGenerationDialogIds(nextSelectedLayerIds); const dragLayerIds = selectedCanvasLayerIds.includes(layer.id) ? selectedCanvasLayerIds : [layer.id]; @@ -300,9 +312,8 @@ export function createGenerationFrameSelectionStart({ selectedIds: selectedLayerIds, isMultiSelectGesture, }); - const selectedGenerationDialogIds = getSelectedGenerationDialogIds( - nextSelectedLayerIds, - ); + const selectedGenerationDialogIds = + getSelectedGenerationDialogIds(nextSelectedLayerIds); const selectedCanvasLayerIds = getSelectedLayerIds(nextSelectedLayerIds); const dragDialogIds = selectedGenerationDialogIds.includes(dialog.id) ? selectedGenerationDialogIds diff --git a/src/components/image-editor/useImageCanvasStageInteractions.ts b/src/components/image-editor/useImageCanvasStageInteractions.ts index 5f19ae5d3..44145b9fd 100644 --- a/src/components/image-editor/useImageCanvasStageInteractions.ts +++ b/src/components/image-editor/useImageCanvasStageInteractions.ts @@ -92,9 +92,7 @@ type UseImageCanvasStageInteractionsOptions = { pickUiDesignSpecFromLayer: (layer: CanvasLayer) => void; pickPublicationReferenceFromLayer: (layer: CanvasLayer) => void; openLayerGenerationDialog?: (layer: CanvasLayer) => boolean; - activateCanvasGenerationDialog: ( - dialog: CanvasGenerationDialogState, - ) => void; + activateCanvasGenerationDialog: (dialog: CanvasGenerationDialogState) => void; updateCanvasGenerationDialogById: ( dialogId: string, updater: ( @@ -216,6 +214,15 @@ export function useImageCanvasStageInteractions({ } }, [finishViewportInteraction, flushMinimapViewportDrag]); + const rejectRightButtonInteraction = useCallback( + (event: ReactPointerEvent) => { + event.preventDefault(); + event.stopPropagation(); + clearActiveInteraction(); + }, + [clearActiveInteraction], + ); + const setShiftPressed = useCallback((pressed: boolean) => { isShiftPressedRef.current = pressed; }, []); @@ -239,12 +246,17 @@ export function useImageCanvasStageInteractions({ const handleCanvasPointerDown = useCallback( (event: ReactPointerEvent) => { const button = getPointerButton(event); - if (button === 1 ||(button !== 2 && effectiveTool === 'hand')) { + if (button === 2) { + rejectRightButtonInteraction(event); + return; + } + if (button === 1 || (button === 0 && effectiveTool === 'hand')) { startPan(event); return; } if (button !== 0) { + event.preventDefault(); return; } const target = event.target as HTMLElement; @@ -275,12 +287,17 @@ export function useImageCanvasStageInteractions({ const handleLayerPointerDown = useCallback( (event: ReactPointerEvent, layer: CanvasLayer) => { const button = getPointerButton(event); - if (button === 1 || effectiveTool === 'hand') { + if (button === 2) { + rejectRightButtonInteraction(event); + return; + } + if (button === 1 || (button === 0 && effectiveTool === 'hand')) { event.stopPropagation(); startPan(event); return; } if (button !== 0) { + event.preventDefault(); event.stopPropagation(); return; } @@ -481,12 +498,18 @@ export function useImageCanvasStageInteractions({ return; } const button = getPointerButton(event); - if (button === 1 || effectiveTool === 'hand') { + if (button === 2) { + rejectRightButtonInteraction(event); + return; + } + if (button === 1 || (button === 0 && effectiveTool === 'hand')) { event.stopPropagation(); startPan(event); return; } if (button !== 0) { + event.preventDefault(); + event.stopPropagation(); return; } @@ -554,6 +577,9 @@ export function useImageCanvasStageInteractions({ const handlePointerMove = useCallback( (event: ReactPointerEvent) => { + if ((event.buttons & 2) !== 0 && (event.buttons & (1 | 4)) === 0) { + return; + } if (canvasMarquee && canvasMarquee.pointerId === event.pointerId) { event.preventDefault(); const rect = canvasViewportRef.current?.getBoundingClientRect(); -- 2.52.0 From e7279166c9f49cb415486bfeccb601f521092569 Mon Sep 17 00:00:00 2001 From: Yunine Date: Fri, 17 Jul 2026 20:37:55 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImageCanvasStageInteractionModel.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/image-editor/ImageCanvasStageInteractionModel.test.ts b/src/components/image-editor/ImageCanvasStageInteractionModel.test.ts index 40625737c..c05ae0ec7 100644 --- a/src/components/image-editor/ImageCanvasStageInteractionModel.test.ts +++ b/src/components/image-editor/ImageCanvasStageInteractionModel.test.ts @@ -345,9 +345,9 @@ describe('ImageCanvasStageInteractionModel', () => { minimapScale: 0.4, moved: false, }); - expect( - updateMinimapDragMovement(minimapDrag, { x: 121, y: 90 }), - ).toBe(minimapDrag); + expect(updateMinimapDragMovement(minimapDrag, { x: 121, y: 90 })).toBe( + minimapDrag, + ); expect(updateMinimapDragMovement(minimapDrag, { x: 123, y: 90 })).toEqual({ ...minimapDrag, moved: true, @@ -385,7 +385,10 @@ describe('ImageCanvasStageInteractionModel', () => { dialog: anotherDialog, layers, generationDialogs: [dialog, anotherDialog], - selectedLayerIds: ['layer-a', getCanvasGenerationSelectionId('dialog-1')], + selectedLayerIds: [ + 'layer-a', + getCanvasGenerationSelectionId('dialog-1'), + ], isMultiSelectGesture: true, pointerId: 12, pointer: { x: 360, y: 260 }, -- 2.52.0 From 46db6055fa8fbf41ffe0cbb87e91f56e76f65718 Mon Sep 17 00:00:00 2001 From: Yunine Date: Sun, 19 Jul 2026 18:56:31 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=94=BB=E5=B8=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=BA=A4=E4=BA=92=E4=BA=8B=E4=BB=B6=E5=AF=BC?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useImageCanvasStageInteractions.test.tsx | 242 ++++++++++++++++-- 1 file changed, 223 insertions(+), 19 deletions(-) diff --git a/src/components/image-editor/useImageCanvasStageInteractions.test.tsx b/src/components/image-editor/useImageCanvasStageInteractions.test.tsx index 9c1ccdde9..acb03151a 100644 --- a/src/components/image-editor/useImageCanvasStageInteractions.test.tsx +++ b/src/components/image-editor/useImageCanvasStageInteractions.test.tsx @@ -1,6 +1,6 @@ /* @vitest-environment jsdom */ -import { act, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import { type PointerEvent as ReactPointerEvent, useRef, @@ -309,7 +309,10 @@ function StageInteractionsHarness({ {layers - .map((layer) => `${layer.id}:${layer.x.toFixed(1)},${layer.y.toFixed(1)}`) + .map( + (layer) => + `${layer.id}:${layer.x.toFixed(1)},${layer.y.toFixed(1)}`, + ) .join('|')} @@ -323,7 +326,9 @@ function StageInteractionsHarness({ {String(interaction.isPanning)} {interaction.effectiveTool} - {interaction.snapGuide?.vertical ?? interaction.snapGuide?.horizontal ?? '-'} + {interaction.snapGuide?.vertical ?? + interaction.snapGuide?.horizontal ?? + '-'} {generateDialog?.placeholder @@ -337,9 +342,7 @@ function StageInteractionsHarness({ {String(generateDialog?.composerOpen ?? false)} {clearCount} - - {imageMenuCloseCount} - + {imageMenuCloseCount} + + + + ); } @@ -778,7 +861,9 @@ describe('useImageCanvasStageInteractions', () => { act(() => { screen.getByRole('button', { name: '直接移动平移' }).click(); }); - expect(screen.getByTestId('viewport-state').textContent).toBe('30.0,25.0,1'); + expect(screen.getByTestId('viewport-state').textContent).toBe( + '30.0,25.0,1', + ); act(() => { screen.getByRole('button', { name: '清理交互' }).click(); }); @@ -800,9 +885,7 @@ describe('useImageCanvasStageInteractions', () => { render(); act(() => { - screen - .getByRole('button', { name: '直接贴近图层拖生成占位' }) - .click(); + screen.getByRole('button', { name: '直接贴近图层拖生成占位' }).click(); }); expect(screen.getByTestId('dialog-position').textContent).toBe( @@ -844,8 +927,9 @@ describe('useImageCanvasStageInteractions', () => { act(() => { screen.getByRole('button', { name: '直接追加生成器' }).click(); }); - const generationSelectionId = - screen.getByTestId('dialog-selection-id').textContent; + const generationSelectionId = screen.getByTestId( + 'dialog-selection-id', + ).textContent; expect(screen.getByTestId('selection').textContent).toBe( `first:first,${generationSelectionId}`, ); @@ -921,15 +1005,15 @@ describe('useImageCanvasStageInteractions', () => { expect(onViewportInteractionEnd).toHaveBeenCalledTimes(2); }); - it('allows middle-button dragging and rejects right-button dragging', () => { + it('allows middle-button dragging and rejects right-button dragging in select, hand, and Space modes', () => { render(); const viewport = screen.getByTestId('viewport'); act(() => { fireEvent( - viewport, - new MouseEvent('pointerdown',{ + viewport, + new MouseEvent('pointerdown', { bubbles: true, clientX: 100, clientY: 100, @@ -942,8 +1026,8 @@ describe('useImageCanvasStageInteractions', () => { act(() => { fireEvent( - viewport, - new MouseEvent('pointerup',{ + viewport, + new MouseEvent('pointerup', { bubbles: true, clientX: 100, clientY: 100, @@ -956,8 +1040,49 @@ describe('useImageCanvasStageInteractions', () => { act(() => { fireEvent( - viewport, - new MouseEvent('pointerdown',{ + viewport, + new MouseEvent('pointerdown', { + bubbles: true, + clientX: 100, + clientY: 100, + button: 2, + buttons: 2, + }), + ); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + + act(() => { + screen.getByRole('button', { name: '切抓手' }).click(); + }); + expect(screen.getByTestId('tool').textContent).toBe('hand'); + + act(() => { + fireEvent( + viewport, + new MouseEvent('pointerdown', { + bubbles: true, + clientX: 100, + clientY: 100, + button: 2, + buttons: 2, + }), + ); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + + act(() => { + screen.getByRole('button', { name: '松开空格' }).click(); + }); + act(() => { + screen.getByRole('button', { name: '按住空格' }).click(); + }); + expect(screen.getByTestId('tool').textContent).toBe('hand'); + + act(() => { + fireEvent( + viewport, + new MouseEvent('pointerdown', { bubbles: true, clientX: 100, clientY: 100, @@ -968,4 +1093,83 @@ describe('useImageCanvasStageInteractions', () => { }); expect(screen.getByTestId('panning').textContent).toBe('false'); }); + + it('rejects right-click panning on layers in select, hand, and Space modes', () => { + render(); + + act(() => { + screen.getByRole('button', { name: '右键选第一层' }).click(); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + expect(screen.getByTestId('selection').textContent).toBe('-:'); + + act(() => { + screen.getByRole('button', { name: '右键抓手模式选第一层' }).click(); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + expect(screen.getByTestId('tool').textContent).toBe('hand'); + expect(screen.getByTestId('selection').textContent).toBe('-:'); + + act(() => { + screen.getByRole('button', { name: '松开空格' }).click(); + }); + act(() => { + screen.getByRole('button', { name: '按住空格' }).click(); + }); + expect(screen.getByTestId('tool').textContent).toBe('hand'); + + act(() => { + const layer = screen.getByTestId('layer-first'); + fireEvent( + layer, + new MouseEvent('pointerdown', { + bubbles: true, + clientX: 40, + clientY: 40, + button: 2, + buttons: 2, + }), + ); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + expect(screen.getByTestId('selection').textContent).toBe('-:'); + }); + + it('rejects right-click panning on generation frames in select, hand, and Space modes', () => { + render(); + + act(() => { + screen.getByRole('button', { name: '右键拖生成占位' }).click(); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + + act(() => { + screen.getByRole('button', { name: '右键抓手模式拖生成占位' }).click(); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + expect(screen.getByTestId('tool').textContent).toBe('hand'); + + act(() => { + screen.getByRole('button', { name: '松开空格' }).click(); + }); + act(() => { + screen.getByRole('button', { name: '按住空格' }).click(); + }); + expect(screen.getByTestId('tool').textContent).toBe('hand'); + + act(() => { + const frame = screen.getByTestId('generation-frame'); + fireEvent( + frame, + new MouseEvent('pointerdown', { + bubbles: true, + clientX: 300, + clientY: 200, + button: 2, + buttons: 2, + }), + ); + }); + expect(screen.getByTestId('panning').textContent).toBe('false'); + }); }); -- 2.52.0 From 68b51aba226050ddf7acad16eb66e6c8000f1087 Mon Sep 17 00:00:00 2001 From: Yunine Date: Mon, 20 Jul 2026 13:38:41 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=B3=E9=94=AE?= =?UTF-8?q?=E5=9B=BE=E5=B1=82=E6=97=B6=20pointerdown=20=E8=A2=AB=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E5=AF=BC=E8=87=B4=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=89=93=E5=BC=80=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useImageCanvasStageInteractions.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/image-editor/useImageCanvasStageInteractions.ts b/src/components/image-editor/useImageCanvasStageInteractions.ts index 44145b9fd..63ad4c603 100644 --- a/src/components/image-editor/useImageCanvasStageInteractions.ts +++ b/src/components/image-editor/useImageCanvasStageInteractions.ts @@ -214,14 +214,10 @@ export function useImageCanvasStageInteractions({ } }, [finishViewportInteraction, flushMinimapViewportDrag]); - const rejectRightButtonInteraction = useCallback( - (event: ReactPointerEvent) => { - event.preventDefault(); - event.stopPropagation(); - clearActiveInteraction(); - }, - [clearActiveInteraction], - ); + const rejectRightButtonInteraction = useCallback(() => { + // Preserve the pointer event so the matching contextmenu event can still open. + clearActiveInteraction(); + }, [clearActiveInteraction]); const setShiftPressed = useCallback((pressed: boolean) => { isShiftPressedRef.current = pressed; @@ -247,7 +243,7 @@ export function useImageCanvasStageInteractions({ (event: ReactPointerEvent) => { const button = getPointerButton(event); if (button === 2) { - rejectRightButtonInteraction(event); + rejectRightButtonInteraction(); return; } if (button === 1 || (button === 0 && effectiveTool === 'hand')) { @@ -281,14 +277,20 @@ export function useImageCanvasStageInteractions({ } clearCanvasFocus(); }, - [canvasViewportRef, clearCanvasFocus, effectiveTool, startPan], + [ + canvasViewportRef, + clearCanvasFocus, + effectiveTool, + rejectRightButtonInteraction, + startPan, + ], ); const handleLayerPointerDown = useCallback( (event: ReactPointerEvent, layer: CanvasLayer) => { const button = getPointerButton(event); if (button === 2) { - rejectRightButtonInteraction(event); + rejectRightButtonInteraction(); return; } if (button === 1 || (button === 0 && effectiveTool === 'hand')) { @@ -426,6 +428,7 @@ export function useImageCanvasStageInteractions({ pickIconSpecFromLayer, pickPublicationReferenceFromLayer, pickUiDesignSpecFromLayer, + rejectRightButtonInteraction, selectedLayerIds, setGenerateDialog, setSelectedLayerId, @@ -499,7 +502,7 @@ export function useImageCanvasStageInteractions({ } const button = getPointerButton(event); if (button === 2) { - rejectRightButtonInteraction(event); + rejectRightButtonInteraction(); return; } if (button === 1 || (button === 0 && effectiveTool === 'hand')) { @@ -550,6 +553,7 @@ export function useImageCanvasStageInteractions({ canvasGenerationDialogs, effectiveTool, layers, + rejectRightButtonInteraction, selectedLayerIds, setSelectedLayerId, setSelectedLayerIds, -- 2.52.0