优化画布小地图拖拽手感

小地图拖拽改为基于按下时视图计算位移

降低小地图拖拽灵敏度并保留单击定位

补充反向拖拽不沿旧方向漂移的回归测试
This commit is contained in:
2026-06-16 16:48:51 +08:00
parent 94841d4360
commit d249548013
2 changed files with 92 additions and 2 deletions

View File

@@ -2192,6 +2192,53 @@ describe('ImageCanvasEditorView', () => {
expect(screen.getByRole('button', { name: '画布小地图' })).toBeTruthy(); 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 () => { it('persists layer groups in the canvas layer snapshot', async () => {
await renderLoadedEditor(); await renderLoadedEditor();

View File

@@ -324,6 +324,11 @@ type DragState =
| { | {
kind: 'minimap'; kind: 'minimap';
pointerId: number; pointerId: number;
startClientX: number;
startClientY: number;
startViewport: CanvasViewport;
minimapScale: number;
moved: boolean;
}; };
const EDITOR_ASSET_FOLDERS: EditorAssetFolder[] = [ const EDITOR_ASSET_FOLDERS: EditorAssetFolder[] = [
@@ -347,6 +352,7 @@ const SNAP_THRESHOLD_SCREEN_PX = 18;
const FIT_VIEW_PADDING = 10; const FIT_VIEW_PADDING = 10;
const MINIMAP_SIZE = { width: 132, height: 84 }; const MINIMAP_SIZE = { width: 132, height: 84 };
const MINIMAP_PADDING = 8; const MINIMAP_PADDING = 8;
const MINIMAP_DRAG_SENSITIVITY = 0.3;
const CONTEXT_MENU_SIZE = { const CONTEXT_MENU_SIZE = {
blank: { width: 176, height: 148 }, blank: { width: 176, height: 148 },
layer: { width: 176, height: 444 }, layer: { width: 176, height: 444 },
@@ -3519,6 +3525,28 @@ export function ImageCanvasEditorView() {
})); }));
}; };
const moveViewportFromMinimapDrag = (
dragState: Extract<DragState, { kind: 'minimap' }>,
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 = ( const handleMinimapPointerDown = (
event: ReactPointerEvent<HTMLButtonElement>, event: ReactPointerEvent<HTMLButtonElement>,
) => { ) => {
@@ -3529,10 +3557,14 @@ export function ImageCanvasEditorView() {
dragStateRef.current = { dragStateRef.current = {
kind: 'minimap', kind: 'minimap',
pointerId: getPointerId(event), pointerId: getPointerId(event),
startClientX: pointer.x,
startClientY: pointer.y,
startViewport: { ...viewportRef.current },
minimapScale: minimapModel?.scale ?? 1,
moved: false,
}; };
captureCanvasHistory(); captureCanvasHistory();
dragHistoryCapturedRef.current = true; dragHistoryCapturedRef.current = true;
moveViewportFromMinimapPointer(pointer.x, pointer.y);
}; };
const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => { const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
@@ -3625,7 +3657,14 @@ export function ImageCanvasEditorView() {
dragHistoryCapturedRef.current = true; dragHistoryCapturedRef.current = true;
} }
const pointer = getPointerClient(event); 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; return;
} }
@@ -3692,6 +3731,10 @@ export function ImageCanvasEditorView() {
dragState && dragState &&
(dragState.pointerId < 0 || pointerId < 0 || dragState.pointerId === pointerId) (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; dragStateRef.current = null;
dragHistoryCapturedRef.current = false; dragHistoryCapturedRef.current = false;
setIsPanning(false); setIsPanning(false);