03a4410272
主要工作是共享同一套“画布内核与通用 UI 源码”,网站和 Tauri 只分别实现宿主适配层。 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Co-authored-by: kdletters <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/136 Co-authored-by: menghao <mh18530625731@163.com> Co-committed-by: menghao <mh18530625731@163.com>
212 lines
5.4 KiB
TypeScript
212 lines
5.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
type CanvasLayer,
|
|
createCanvasExportModel,
|
|
createMinimapModel,
|
|
fitViewportToLayers,
|
|
getWorldPointFromClient,
|
|
moveLayersFromDrag,
|
|
panViewportFromWheel,
|
|
resizeCanvasLayerBounds,
|
|
resolvePointerSelection,
|
|
resolveViewportFromWheel,
|
|
scaleViewportFromScreenPoint,
|
|
selectLayersInsideMarquee,
|
|
transformCanvasLayers,
|
|
} from './index';
|
|
|
|
function layer(id: string, overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
|
return {
|
|
id,
|
|
resourceId: `resource-${id}`,
|
|
title: id,
|
|
src: `media:${id}`,
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 80,
|
|
originalWidth: 100,
|
|
originalHeight: 80,
|
|
zIndex: 1,
|
|
sourceType: 'uploaded',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('shared image canvas core', () => {
|
|
it('keeps the world anchor stable while zooming and converts coordinates', () => {
|
|
const viewport = { x: 20, y: 30, scale: 0.5 };
|
|
const anchor = { x: 240, y: 180 };
|
|
const before = getWorldPointFromClient({
|
|
clientX: anchor.x,
|
|
clientY: anchor.y,
|
|
rect: { left: 0, top: 0, right: 800, bottom: 600 },
|
|
viewport,
|
|
});
|
|
const next = scaleViewportFromScreenPoint({
|
|
viewport,
|
|
nextScale: 1.25,
|
|
screenPoint: anchor,
|
|
});
|
|
const after = getWorldPointFromClient({
|
|
clientX: anchor.x,
|
|
clientY: anchor.y,
|
|
rect: { left: 0, top: 0, right: 800, bottom: 600 },
|
|
viewport: next,
|
|
});
|
|
expect(after).toEqual(before);
|
|
expect(panViewportFromWheel({ viewport, deltaX: 12, deltaY: -8 })).toEqual({
|
|
x: 8,
|
|
y: 38,
|
|
scale: 0.5,
|
|
});
|
|
});
|
|
|
|
it('shares pan, shift-pan and modifier-zoom wheel semantics', () => {
|
|
const viewport = { x: 20, y: 30, scale: 1 };
|
|
expect(
|
|
resolveViewportFromWheel({
|
|
viewport,
|
|
deltaX: 4,
|
|
deltaY: 10,
|
|
shiftKey: false,
|
|
ctrlKey: false,
|
|
metaKey: false,
|
|
screenPoint: { x: 100, y: 100 },
|
|
}),
|
|
).toEqual({ mode: 'pan', viewport: { x: 16, y: 20, scale: 1 } });
|
|
expect(
|
|
resolveViewportFromWheel({
|
|
viewport,
|
|
deltaX: 0,
|
|
deltaY: 10,
|
|
shiftKey: true,
|
|
ctrlKey: false,
|
|
metaKey: false,
|
|
screenPoint: { x: 100, y: 100 },
|
|
}),
|
|
).toEqual({ mode: 'pan', viewport: { x: 10, y: 30, scale: 1 } });
|
|
expect(
|
|
resolveViewportFromWheel({
|
|
viewport,
|
|
deltaX: 0,
|
|
deltaY: -10,
|
|
shiftKey: false,
|
|
ctrlKey: true,
|
|
metaKey: false,
|
|
screenPoint: { x: 100, y: 100 },
|
|
}).mode,
|
|
).toBe('zoom');
|
|
});
|
|
|
|
it('supports additive selection, marquee and multi-layer movement', () => {
|
|
expect(
|
|
resolvePointerSelection({
|
|
targetId: 'b',
|
|
selectedIds: ['a'],
|
|
isMultiSelectGesture: true,
|
|
}),
|
|
).toEqual(['a', 'b']);
|
|
expect(
|
|
resolvePointerSelection({
|
|
targetId: 'a',
|
|
selectedIds: ['a'],
|
|
isMultiSelectGesture: true,
|
|
}),
|
|
).toEqual(['a']);
|
|
expect(
|
|
resolvePointerSelection({
|
|
targetId: 'a',
|
|
selectedIds: ['a', 'b'],
|
|
isMultiSelectGesture: true,
|
|
}),
|
|
).toEqual(['b']);
|
|
const layers = [
|
|
layer('a', { x: 10, y: 10 }),
|
|
layer('b', { x: 140, y: 20 }),
|
|
];
|
|
expect(
|
|
selectLayersInsideMarquee({
|
|
layers,
|
|
marquee: {
|
|
pointerId: 1,
|
|
startX: 0,
|
|
startY: 0,
|
|
currentX: 0,
|
|
currentY: 0,
|
|
},
|
|
currentPoint: { x: 260, y: 120 },
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
}),
|
|
).toEqual(['a', 'b']);
|
|
const moved = moveLayersFromDrag({
|
|
layers,
|
|
dragState: {
|
|
kind: 'layer',
|
|
pointerId: 1,
|
|
layerId: 'a',
|
|
layerIds: ['a', 'b'],
|
|
startClientX: 0,
|
|
startClientY: 0,
|
|
startLayerX: 10,
|
|
startLayerY: 10,
|
|
startLayers: [
|
|
{ id: 'a', x: 10, y: 10 },
|
|
{ id: 'b', x: 140, y: 20 },
|
|
],
|
|
startScale: 1,
|
|
},
|
|
pointer: { x: 20, y: 30 },
|
|
});
|
|
expect(moved.layers.map(({ x, y }) => ({ x, y }))).toEqual([
|
|
{ x: 30, y: 40 },
|
|
{ x: 160, y: 50 },
|
|
]);
|
|
});
|
|
|
|
it('resizes layers and preserves locked layers during transforms', () => {
|
|
expect(
|
|
resizeCanvasLayerBounds({
|
|
initial: { x: 10, y: 20, width: 200, height: 100 },
|
|
handle: 'top-left',
|
|
deltaX: -40,
|
|
deltaY: -20,
|
|
preserveAspectRatio: true,
|
|
}),
|
|
).toEqual({ x: -30, y: 0, width: 240, height: 120 });
|
|
const locked = layer('locked', { locked: true });
|
|
expect(
|
|
transformCanvasLayers(
|
|
[locked],
|
|
new Map([['locked', { x: 99, width: 300 }]]),
|
|
)[0],
|
|
).toBe(locked);
|
|
});
|
|
|
|
it('fits content, projects the minimap and creates a host-neutral export', () => {
|
|
const layers = [
|
|
layer('a', { x: 100, y: 200 }),
|
|
layer('b', { x: 400, y: 500, width: 200, height: 120, zIndex: 2 }),
|
|
];
|
|
const viewport = fitViewportToLayers({
|
|
layers,
|
|
canvasSize: { width: 900, height: 640 },
|
|
});
|
|
expect(viewport).not.toBeNull();
|
|
expect(
|
|
createMinimapModel({
|
|
layers,
|
|
viewport: viewport!,
|
|
canvasSize: { width: 900, height: 640 },
|
|
})?.layers,
|
|
).toHaveLength(2);
|
|
expect(
|
|
createCanvasExportModel({ layers, backgroundColor: '#ffffff' }),
|
|
).toMatchObject({
|
|
bounds: { x: 100, y: 200, width: 500, height: 420 },
|
|
backgroundColor: '#ffffff',
|
|
});
|
|
});
|
|
});
|