新增 ImageCanvasLayerCommandModel 收口右键图层复制、粘贴、层级、分组、显隐、锁定、翻转和删除规则 主视图保留历史、选中态、菜单关闭、元数据清理和导出副作用 补充图片右键菜单真实浏览器冒泡回归测试 更新图片画布前端拆分计划和 TRACKING 验证记录
196 lines
6.0 KiB
TypeScript
196 lines
6.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
createCanvasLayerClipboard,
|
|
duplicateCanvasLayers,
|
|
flipCanvasLayers,
|
|
getCanvasLayersByIds,
|
|
groupCanvasLayers,
|
|
moveCanvasLayers,
|
|
removeCanvasLayers,
|
|
resolveContextTargetLayerIds,
|
|
toggleCanvasLayersLock,
|
|
toggleCanvasLayersVisibility,
|
|
ungroupCanvasLayers,
|
|
} from './ImageCanvasLayerCommandModel';
|
|
import type {
|
|
CanvasContextMenuState,
|
|
CanvasLayer,
|
|
} from './ImageCanvasEditorTypes';
|
|
|
|
function createLayer(overrides: Partial<CanvasLayer>): CanvasLayer {
|
|
const id = overrides.id ?? 'layer-a';
|
|
return {
|
|
id,
|
|
resourceId: `resource-${id}`,
|
|
title: id,
|
|
src: `data:image/png;base64,${id}`,
|
|
x: 10,
|
|
y: 20,
|
|
width: 100,
|
|
height: 80,
|
|
originalWidth: 100,
|
|
originalHeight: 80,
|
|
zIndex: 1,
|
|
sourceType: 'uploaded',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('ImageCanvasLayerCommandModel', () => {
|
|
it('resolves context menu targets from the current multi-selection', () => {
|
|
const menu: CanvasContextMenuState = {
|
|
kind: 'layer',
|
|
x: 0,
|
|
y: 0,
|
|
layerId: 'layer-b',
|
|
canvasPoint: { x: 10, y: 20 },
|
|
};
|
|
|
|
expect(resolveContextTargetLayerIds(menu, ['layer-a', 'layer-b'])).toEqual([
|
|
'layer-a',
|
|
'layer-b',
|
|
]);
|
|
expect(resolveContextTargetLayerIds(menu, ['layer-a'])).toEqual(['layer-b']);
|
|
expect(
|
|
resolveContextTargetLayerIds(
|
|
{ kind: 'blank', x: 0, y: 0, canvasPoint: { x: 0, y: 0 } },
|
|
['layer-a'],
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('duplicates layers to a canvas point while preserving relative offsets', () => {
|
|
const first = createLayer({ id: 'first', title: '第一张', x: 20, y: 30 });
|
|
const second = createLayer({
|
|
id: 'second',
|
|
title: '第二张',
|
|
x: 70,
|
|
y: 90,
|
|
zIndex: 4,
|
|
groupId: 'group-old',
|
|
});
|
|
|
|
const duplicated = duplicateCanvasLayers({
|
|
sourceLayers: [first, second],
|
|
allLayers: [first, second],
|
|
canvasPoint: { x: 300, y: 200 },
|
|
stamp: 'test',
|
|
});
|
|
|
|
expect(duplicated).toMatchObject([
|
|
{
|
|
id: 'layer-copy-test-0',
|
|
resourceId: 'local-resource-copy-test-0',
|
|
title: '第一张 副本',
|
|
x: 300,
|
|
y: 200,
|
|
zIndex: 5,
|
|
groupId: null,
|
|
},
|
|
{
|
|
id: 'layer-copy-test-1',
|
|
resourceId: 'local-resource-copy-test-1',
|
|
title: '第二张 副本',
|
|
x: 350,
|
|
y: 260,
|
|
zIndex: 6,
|
|
groupId: null,
|
|
},
|
|
]);
|
|
|
|
const cutPaste = duplicateCanvasLayers({
|
|
sourceLayers: [first],
|
|
allLayers: [first, second],
|
|
renameCopies: false,
|
|
stamp: 'cut',
|
|
});
|
|
expect(cutPaste[0]?.title).toBe('第一张');
|
|
expect(cutPaste[0]?.x).toBe(first.x + 32);
|
|
});
|
|
|
|
it('creates a cloned clipboard and removes target layers', () => {
|
|
const layers = [
|
|
createLayer({ id: 'first' }),
|
|
createLayer({ id: 'second', generationInputs: { fields: [], references: [] } }),
|
|
];
|
|
|
|
const clipboard = createCanvasLayerClipboard(layers, ['second'], 'copy');
|
|
|
|
expect(clipboard?.mode).toBe('copy');
|
|
expect(clipboard?.layers).toEqual([layers[1]]);
|
|
expect(clipboard?.layers[0]).not.toBe(layers[1]);
|
|
expect(getCanvasLayersByIds(layers, ['first'])).toEqual([layers[0]]);
|
|
expect(removeCanvasLayers(layers, ['first'])).toEqual([layers[1]]);
|
|
});
|
|
|
|
it('moves layer z-indexes with the same commands as the context menu', () => {
|
|
const layers = [
|
|
createLayer({ id: 'bottom', zIndex: 1 }),
|
|
createLayer({ id: 'middle', zIndex: 3 }),
|
|
createLayer({ id: 'top', zIndex: 8 }),
|
|
];
|
|
|
|
expect(moveCanvasLayers(layers, ['middle'], 'up')[1]?.zIndex).toBe(4);
|
|
expect(moveCanvasLayers(layers, ['middle'], 'down')[1]?.zIndex).toBe(2);
|
|
expect(moveCanvasLayers(layers, ['bottom', 'middle'], 'top')).toMatchObject([
|
|
{ id: 'bottom', zIndex: 9 },
|
|
{ id: 'middle', zIndex: 10 },
|
|
{ id: 'top', zIndex: 8 },
|
|
]);
|
|
expect(
|
|
moveCanvasLayers(layers, ['bottom', 'middle'], 'bottom'),
|
|
).toMatchObject([
|
|
{ id: 'bottom', zIndex: -2 },
|
|
{ id: 'middle', zIndex: -1 },
|
|
{ id: 'top', zIndex: 8 },
|
|
]);
|
|
});
|
|
|
|
it('groups, ungroups, toggles visibility and lock, and flips layers', () => {
|
|
const layers = [
|
|
createLayer({ id: 'first', hidden: true, locked: true }),
|
|
createLayer({ id: 'second' }),
|
|
createLayer({ id: 'third' }),
|
|
];
|
|
const targetIds = ['first', 'second'];
|
|
|
|
const groupedLayers = groupCanvasLayers(layers, targetIds, 'group-next');
|
|
expect(groupedLayers[0]?.groupId).toBe('group-next');
|
|
expect(groupedLayers[1]?.groupId).toBe('group-next');
|
|
expect(groupedLayers[2]?.groupId).toBeUndefined();
|
|
|
|
const ungroupedLayers = ungroupCanvasLayers(groupedLayers, targetIds);
|
|
expect(ungroupedLayers[0]?.groupId).toBeNull();
|
|
expect(ungroupedLayers[1]?.groupId).toBeNull();
|
|
expect(ungroupedLayers[2]?.groupId).toBeUndefined();
|
|
|
|
const hiddenLayers = toggleCanvasLayersVisibility(layers, targetIds);
|
|
expect(hiddenLayers[0]?.hidden).toBe(true);
|
|
expect(hiddenLayers[1]?.hidden).toBe(true);
|
|
expect(hiddenLayers[2]?.hidden).toBeUndefined();
|
|
|
|
const shownLayers = toggleCanvasLayersVisibility([
|
|
createLayer({ id: 'first', hidden: true }),
|
|
createLayer({ id: 'second', hidden: true }),
|
|
], targetIds);
|
|
expect(shownLayers[0]?.hidden).toBe(false);
|
|
expect(shownLayers[1]?.hidden).toBe(false);
|
|
|
|
const lockedLayers = toggleCanvasLayersLock(layers, targetIds);
|
|
expect(lockedLayers[0]?.locked).toBe(true);
|
|
expect(lockedLayers[1]?.locked).toBe(true);
|
|
expect(lockedLayers[2]?.locked).toBeUndefined();
|
|
|
|
const flippedXLayers = flipCanvasLayers(layers, targetIds, 'x');
|
|
expect(flippedXLayers[0]?.flipX).toBe(true);
|
|
expect(flippedXLayers[1]?.flipX).toBe(true);
|
|
expect(flippedXLayers[2]?.flipX).toBeUndefined();
|
|
|
|
const flippedYLayers = flipCanvasLayers(layers, targetIds, 'y');
|
|
expect(flippedYLayers[0]?.flipY).toBe(true);
|
|
expect(flippedYLayers[1]?.flipY).toBe(true);
|
|
expect(flippedYLayers[2]?.flipY).toBeUndefined();
|
|
});
|
|
});
|