diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx index 7c966ef2..f361a2e1 100644 --- a/src/components/image-editor/ImageCanvasEditorView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx @@ -2192,6 +2192,53 @@ describe('ImageCanvasEditorView', () => { expect(screen.getByRole('button', { name: '画布小地图' })).toBeTruthy(); }); + it('keeps minimap drag direction stable after pausing and reversing', async () => { + await renderLoadedEditor(); + + const minimap = screen.getByRole('button', { name: '画布小地图' }); + vi.spyOn(minimap, 'getBoundingClientRect').mockReturnValue({ + x: 0, + y: 0, + left: 0, + top: 0, + right: 132, + bottom: 84, + width: 132, + height: 84, + toJSON: () => ({}), + }); + const world = screen + .getByLabelText('画布工作区') + .querySelector('.image-canvas-editor__world') as HTMLElement; + const readTranslateX = () => { + const match = /translate\(([-\d.]+)px,/u.exec(world.style.transform); + return match ? Number(match[1]) : 0; + }; + + dispatchPointerEvent(minimap, 'pointerdown', { + button: 0, + pointerId: 72, + clientX: 60, + clientY: 42, + }); + dispatchPointerEvent(screen.getByLabelText('画布工作区'), 'pointermove', { + button: 0, + pointerId: 72, + clientX: 120, + clientY: 42, + }); + const translateAfterRightDrag = readTranslateX(); + + dispatchPointerEvent(screen.getByLabelText('画布工作区'), 'pointermove', { + button: 0, + pointerId: 72, + clientX: 90, + clientY: 42, + }); + + expect(readTranslateX()).toBeGreaterThan(translateAfterRightDrag); + }); + it('persists layer groups in the canvas layer snapshot', async () => { await renderLoadedEditor(); diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index fce14e00..f058e85f 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -324,6 +324,11 @@ type DragState = | { kind: 'minimap'; pointerId: number; + startClientX: number; + startClientY: number; + startViewport: CanvasViewport; + minimapScale: number; + moved: boolean; }; const EDITOR_ASSET_FOLDERS: EditorAssetFolder[] = [ @@ -347,6 +352,7 @@ const SNAP_THRESHOLD_SCREEN_PX = 18; const FIT_VIEW_PADDING = 10; const MINIMAP_SIZE = { width: 132, height: 84 }; const MINIMAP_PADDING = 8; +const MINIMAP_DRAG_SENSITIVITY = 0.3; const CONTEXT_MENU_SIZE = { blank: { width: 176, height: 148 }, layer: { width: 176, height: 444 }, @@ -3519,6 +3525,28 @@ export function ImageCanvasEditorView() { })); }; + const moveViewportFromMinimapDrag = ( + dragState: Extract, + clientX: number, + clientY: number, + ) => { + const deltaWorldX = + ((clientX - dragState.startClientX) / dragState.minimapScale) * + MINIMAP_DRAG_SENSITIVITY; + const deltaWorldY = + ((clientY - dragState.startClientY) / dragState.minimapScale) * + MINIMAP_DRAG_SENSITIVITY; + setViewport({ + ...dragState.startViewport, + x: + dragState.startViewport.x - + deltaWorldX * dragState.startViewport.scale, + y: + dragState.startViewport.y - + deltaWorldY * dragState.startViewport.scale, + }); + }; + const handleMinimapPointerDown = ( event: ReactPointerEvent, ) => { @@ -3529,10 +3557,14 @@ export function ImageCanvasEditorView() { dragStateRef.current = { kind: 'minimap', pointerId: getPointerId(event), + startClientX: pointer.x, + startClientY: pointer.y, + startViewport: { ...viewportRef.current }, + minimapScale: minimapModel?.scale ?? 1, + moved: false, }; captureCanvasHistory(); dragHistoryCapturedRef.current = true; - moveViewportFromMinimapPointer(pointer.x, pointer.y); }; const handlePointerMove = (event: ReactPointerEvent) => { @@ -3625,7 +3657,14 @@ export function ImageCanvasEditorView() { dragHistoryCapturedRef.current = true; } const pointer = getPointerClient(event); - moveViewportFromMinimapPointer(pointer.x, pointer.y); + const deltaX = pointer.x - dragState.startClientX; + const deltaY = pointer.y - dragState.startClientY; + if (!dragState.moved && Math.hypot(deltaX, deltaY) >= 2) { + dragState.moved = true; + } + if (dragState.moved) { + moveViewportFromMinimapDrag(dragState, pointer.x, pointer.y); + } return; } @@ -3692,6 +3731,10 @@ export function ImageCanvasEditorView() { dragState && (dragState.pointerId < 0 || pointerId < 0 || dragState.pointerId === pointerId) ) { + if (dragState.kind === 'minimap' && !dragState.moved) { + const pointer = getPointerClient(event); + moveViewportFromMinimapPointer(pointer.x, pointer.y); + } dragStateRef.current = null; dragHistoryCapturedRef.current = false; setIsPanning(false);