Files
Genarrative/packages/image-canvas-core/src/sharedCanvasCore.test.ts
T
menghao 03a4410272
Project CI / Native shell tests (push) Failing after 11m56s
Project CI / Repository checks (push) Successful in 1m5s
Project CI / Frontend tests (push) Successful in 3m20s
Project CI / Backend tests (push) Successful in 4m1s
game agent无限画布开发 (#136)
主要工作是共享同一套“画布内核与通用 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>
2026-08-12 10:54:16 +08:00

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',
});
});
});