165 lines
4.3 KiB
TypeScript
165 lines
4.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
CANVAS_WORLD_ORIGIN,
|
|
createLayerFromAsset,
|
|
hydrateLayer,
|
|
normalizeAssetLibrary,
|
|
normalizeCanvasBackgroundHex,
|
|
resolveSnappedLayerPosition,
|
|
serializeLayer,
|
|
} from './ImageCanvasEditorModel';
|
|
import type { CanvasLayer, EditorAsset } from './ImageCanvasEditorTypes';
|
|
|
|
describe('ImageCanvasEditorModel', () => {
|
|
it('normalizes valid canvas background hex values and rejects invalid input', () => {
|
|
expect(normalizeCanvasBackgroundHex(' #ABC ')).toBe('#aabbcc');
|
|
expect(normalizeCanvasBackgroundHex('#f8fafc')).toBe('#f8fafc');
|
|
expect(normalizeCanvasBackgroundHex('white')).toBeNull();
|
|
expect(normalizeCanvasBackgroundHex('#not-a-color')).toBeNull();
|
|
});
|
|
|
|
it('keeps only one default asset folder when normalizing the persisted library', () => {
|
|
const library = normalizeAssetLibrary({
|
|
folders: [
|
|
{
|
|
folderId: 'project',
|
|
label: '项目素材',
|
|
sortOrder: 0,
|
|
collapsed: false,
|
|
systemDefault: true,
|
|
},
|
|
{
|
|
folderId: 'project-duplicate',
|
|
label: '旧项目素材',
|
|
sortOrder: 1,
|
|
collapsed: false,
|
|
systemDefault: true,
|
|
},
|
|
],
|
|
assets: [],
|
|
});
|
|
|
|
expect(library.folders).toHaveLength(1);
|
|
expect(library.folders[0]?.id).toBe('project');
|
|
});
|
|
|
|
it('creates a layer from an account asset at the requested screen point', () => {
|
|
const asset: EditorAsset = {
|
|
id: 'asset-1',
|
|
label: '角色草图',
|
|
src: 'data:image/png;base64,one',
|
|
width: 640,
|
|
height: 480,
|
|
folderId: 'project',
|
|
sourceKind: 'uploaded',
|
|
sourceType: 'uploaded',
|
|
persisted: true,
|
|
objectKey: 'oss/asset-1.png',
|
|
assetObjectId: 'object-1',
|
|
};
|
|
|
|
const layer = createLayerFromAsset(
|
|
asset,
|
|
3,
|
|
{ x: 20, y: 40, scale: 2 },
|
|
{ x: 420, y: 340 },
|
|
);
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-asset-1-3',
|
|
title: '角色草图',
|
|
width: 640,
|
|
height: 480,
|
|
originalWidth: 640,
|
|
originalHeight: 480,
|
|
objectKey: 'oss/asset-1.png',
|
|
assetObjectId: 'object-1',
|
|
sourceAssetId: 'asset-1',
|
|
});
|
|
expect(layer.x).toBe(-18);
|
|
expect(layer.y).toBe(12);
|
|
});
|
|
|
|
it('serializes and hydrates canvas layer metadata without embedding image payloads', () => {
|
|
const layer: CanvasLayer = {
|
|
id: 'layer-generated',
|
|
resourceId: 'resource-generated',
|
|
title: '生成图',
|
|
src: 'data:image/png;base64,heavy',
|
|
x: 10,
|
|
y: 20,
|
|
width: 1024,
|
|
height: 768,
|
|
originalWidth: 1024,
|
|
originalHeight: 768,
|
|
zIndex: 9,
|
|
sourceType: 'generated',
|
|
objectKey: 'generated/object.png',
|
|
assetKind: 'character',
|
|
generationInputs: {
|
|
fields: [{ title: '角色设定', value: '骑士' }],
|
|
references: [],
|
|
},
|
|
locked: true,
|
|
};
|
|
|
|
const snapshot = serializeLayer(layer);
|
|
expect(snapshot).not.toHaveProperty('src');
|
|
|
|
const hydrated = hydrateLayer(
|
|
snapshot,
|
|
new Map([['resource-generated', { imageSrc: '/read/generated.png' }]]),
|
|
);
|
|
|
|
expect(hydrated).toMatchObject({
|
|
id: 'layer-generated',
|
|
src: '/read/generated.png',
|
|
sourceType: 'generated',
|
|
assetKind: 'character',
|
|
objectKey: 'generated/object.png',
|
|
locked: true,
|
|
});
|
|
});
|
|
|
|
it('snaps moving layers to nearby canvas and layer guides', () => {
|
|
const movingLayer: CanvasLayer = {
|
|
id: 'moving',
|
|
resourceId: 'resource-moving',
|
|
title: '移动图',
|
|
src: 'data:image/png;base64,moving',
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
originalWidth: 100,
|
|
originalHeight: 100,
|
|
zIndex: 1,
|
|
sourceType: 'uploaded',
|
|
};
|
|
const anchorLayer: CanvasLayer = {
|
|
...movingLayer,
|
|
id: 'anchor',
|
|
resourceId: 'resource-anchor',
|
|
x: 300,
|
|
y: 240,
|
|
zIndex: 2,
|
|
};
|
|
|
|
const snapped = resolveSnappedLayerPosition(
|
|
movingLayer,
|
|
CANVAS_WORLD_ORIGIN - 47,
|
|
238,
|
|
[movingLayer, anchorLayer],
|
|
1,
|
|
);
|
|
|
|
expect(snapped.x).toBe(CANVAS_WORLD_ORIGIN - movingLayer.width / 2);
|
|
expect(snapped.y).toBe(anchorLayer.y);
|
|
expect(snapped.guide).toEqual({
|
|
vertical: CANVAS_WORLD_ORIGIN,
|
|
horizontal: anchorLayer.y,
|
|
});
|
|
});
|
|
});
|