Merge branch 'master' into editor-agent-more-tools
Project CI / Repository checks (pull_request) Failing after 20s
Project CI / Frontend tests (pull_request) Failing after 20s
Project CI / Backend tests (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 20s

This commit is contained in:
2026-07-22 10:20:08 +08:00
14 changed files with 223 additions and 53 deletions
@@ -2320,15 +2320,24 @@ describe('ImageCanvasEditorView', () => {
}).not.toThrow();
});
it('uses normal wheel for vertical canvas scroll and ctrl wheel for zoom', () => {
it('uses normal wheel for two-axis canvas pan and ctrl wheel for zoom', () => {
render(<ImageCanvasEditorView />);
const viewport = screen.getByLabelText('画布工作区');
const world = screen.getByTestId('image-canvas-world') as HTMLElement;
expect(
screen.getByRole('button', { name: '当前缩放比例 100%' }),
).toBeTruthy();
fireEvent.wheel(viewport, { deltaY: 120, clientX: 400, clientY: 280 });
fireEvent.wheel(viewport, {
deltaX: 80,
deltaY: 120,
clientX: 400,
clientY: 280,
});
expect(world.style.transform).toBe(
'translate(-80px, -120px) scale(0.5)',
);
expect(
screen.getByRole('button', { name: '当前缩放比例 100%' }),
).toBeTruthy();
@@ -19,8 +19,8 @@ import {
moveViewportFromMinimapDrag,
moveViewportFromMinimapPointer,
moveViewportFromPan,
panViewportFromWheel,
scaleViewportFromScreenPoint,
scrollViewportVertically,
selectCanvasObjectsInsideMarquee,
selectLayersInsideMarquee,
zoomViewportFromWheel,
@@ -133,11 +133,13 @@ describe('ImageCanvasInteractionModel', () => {
).toEqual({ x: 100, y: 60 });
});
it('scrolls vertically and zooms around a screen point', () => {
it('pans from two-axis wheel input and zooms around a screen point', () => {
const viewport = { x: 10, y: 20, scale: 1 };
expect(scrollViewportVertically(viewport, 120)).toEqual({
x: 10,
expect(
panViewportFromWheel({ viewport, deltaX: 40, deltaY: 120 }),
).toEqual({
x: -30,
y: -100,
scale: 1,
});
@@ -212,12 +212,18 @@ export function scaleViewportFromScreenPoint({
};
}
export function scrollViewportVertically(
viewport: CanvasViewport,
deltaY: number,
) {
export function panViewportFromWheel({
viewport,
deltaX,
deltaY,
}: {
viewport: CanvasViewport;
deltaX: number;
deltaY: number;
}) {
return {
...viewport,
x: viewport.x - deltaX,
y: viewport.y - deltaY,
};
}
@@ -29,7 +29,8 @@ export const IMAGE_CANVAS_SHORTCUT_SECTIONS: ImageCanvasShortcutSection[] = [
{
title: '视图',
items: [
{ action: '纵向滚动画布', keys: ['鼠标滚轮'], status: 'ready' },
{ action: '平移画布', keys: ['鼠标滚轮 / 触控板'], status: 'ready' },
{ action: '横向平移画布', keys: ['Shift', '鼠标滚轮'], status: 'ready' },
{ action: '以鼠标位置缩放', keys: ['Ctrl', '鼠标滚轮'], status: 'ready' },
{ action: '显示全部元素', keys: ['Ctrl', '0'], status: 'new' },
{ action: '缩放至 100%', keys: ['Ctrl', '1'], status: 'new' },
@@ -1083,6 +1083,22 @@ describe('useImageCanvasStageInteractions', () => {
});
expect(screen.getByTestId('panning').textContent).toBe('true');
act(() => {
fireEvent(
viewport,
new MouseEvent('pointermove', {
bubbles: true,
clientX: 140,
clientY: 70,
button: 1,
buttons: 4,
}),
);
});
expect(screen.getByTestId('viewport-state').textContent).toBe(
'40.0,-30.0,1',
);
act(() => {
fireEvent(
viewport,
@@ -158,19 +158,12 @@ describe('useImageCanvasViewportControls', () => {
expect(captureCanvasHistory).toHaveBeenCalledTimes(2);
});
it('handles vertical wheel scroll, ctrl wheel zoom and minimap movement', () => {
const rafCallbacks: FrameRequestCallback[] = [];
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(
(callback: FrameRequestCallback) => {
rafCallbacks.push(callback);
return rafCallbacks.length;
},
);
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {});
it('keeps ordinary mouse wheel vertical and maps Shift wheel to horizontal', () => {
const { result, viewportElement } = renderViewportControls();
act(() => {
fireEvent.wheel(viewportElement, {
deltaX: 0,
deltaY: 120,
clientX: 260,
clientY: 190,
@@ -182,6 +175,47 @@ describe('useImageCanvasViewportControls', () => {
scale: 0.5,
});
act(() => {
fireEvent.wheel(viewportElement, {
shiftKey: true,
deltaX: 0,
deltaY: 80,
clientX: 260,
clientY: 190,
});
});
expectViewport(result.current.viewport, {
x: -340,
y: -50,
scale: 0.5,
});
});
it('handles native two-axis wheel pan, ctrl wheel zoom and minimap movement', () => {
const rafCallbacks: FrameRequestCallback[] = [];
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(
(callback: FrameRequestCallback) => {
rafCallbacks.push(callback);
return rafCallbacks.length;
},
);
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {});
const { result, viewportElement } = renderViewportControls();
act(() => {
fireEvent.wheel(viewportElement, {
deltaX: 40,
deltaY: 120,
clientX: 260,
clientY: 190,
});
});
expectViewport(result.current.viewport, {
x: -300,
y: -50,
scale: 0.5,
});
act(() => {
fireEvent.wheel(viewportElement, {
ctrlKey: true,
@@ -191,7 +225,7 @@ describe('useImageCanvasViewportControls', () => {
});
});
expectViewport(result.current.viewport, {
x: -312,
x: -356,
y: -74,
scale: 0.55,
});
@@ -23,8 +23,8 @@ import {
getWorldPointFromClient,
moveViewportFromMinimapDrag as resolveViewportFromMinimapDrag,
moveViewportFromMinimapPointer as resolveViewportFromMinimapPointer,
panViewportFromWheel,
scaleViewportFromScreenPoint,
scrollViewportVertically,
zoomViewportFromWheel,
} from './ImageCanvasInteractionModel';
@@ -260,8 +260,16 @@ export function useImageCanvasViewportControls({
}
if (!event.ctrlKey && !event.metaKey) {
const useShiftWheelHorizontalFallback =
event.shiftKey && event.deltaX === 0;
setViewport((currentViewport) =>
scrollViewportVertically(currentViewport, event.deltaY),
panViewportFromWheel({
viewport: currentViewport,
deltaX: useShiftWheelHorizontalFallback
? event.deltaY
: event.deltaX,
deltaY: useShiftWheelHorizontalFallback ? 0 : event.deltaY,
}),
);
return;
}