Files
Genarrative/src/components/image-editor/ImageCanvasGenerationPlacementModel.test.ts
T
kdletters 387a1c26e3 完成全量检查整改
清理已跟踪原始日志与个人本地配置
修复前后端有效门禁、测试和构建问题
补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束
收紧图片编辑器状态、附件与媒体引用测试
排除已下线旧创作入口测试并清理 warning
2026-07-17 16:56:46 +08:00

215 lines
5.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type {
CanvasGenerationDialogState,
CanvasLayer,
CanvasViewport,
} from './ImageCanvasEditorTypes';
import {
centerViewportOnPlacement,
chooseGenerationPlacement,
} from './ImageCanvasGenerationPlacementModel';
const canvasSize = { width: 900, height: 640 };
const viewport: CanvasViewport = { x: 10, y: 20, scale: 2 };
const frame = {
x: 0,
y: 0,
width: 420,
height: 420,
originalWidth: 2048,
originalHeight: 2048,
};
function layer(overrides: Partial<CanvasLayer>): CanvasLayer {
return {
id: 'layer-1',
resourceId: 'resource-1',
title: '图片',
src: '/image.png',
x: 0,
y: 0,
width: 100,
height: 100,
originalWidth: 100,
originalHeight: 100,
zIndex: 1,
sourceType: 'uploaded',
...overrides,
};
}
function generationDialog(
overrides: Partial<CanvasGenerationDialogState>,
): CanvasGenerationDialogState {
return {
id: 'dialog-1',
mode: 'generate',
prompt: '',
status: 'idle',
placeholder: {
x: 0,
y: 0,
width: 100,
height: 100,
originalWidth: 2048,
originalHeight: 2048,
},
...overrides,
};
}
describe('ImageCanvasGenerationPlacementModel', () => {
it('places an empty-canvas generation frame at the current viewport center', () => {
const placement = chooseGenerationPlacement({
canvasSize,
viewport,
frame,
layers: [],
generationDialogs: [],
});
expect(placement).toEqual({
x: 10,
y: -60,
width: 420,
height: 420,
originalWidth: 2048,
originalHeight: 2048,
});
});
it('keeps the nearest non-overlapping gap from visible layers and generation placeholders', () => {
const placement = chooseGenerationPlacement({
canvasSize,
viewport,
frame: { ...frame, width: 100, height: 100 },
layers: [
layer({ id: 'center', x: 170, y: 110, width: 120, height: 120 }),
layer({ id: 'right', x: 322, y: 110, width: 100, height: 120 }),
layer({
id: 'hidden',
hidden: true,
x: 18,
y: 118,
width: 100,
height: 100,
}),
],
generationDialogs: [
{
id: 'dialog-left',
mode: 'generate',
prompt: '',
status: 'idle',
placeholder: {
x: 18,
y: 118,
width: 100,
height: 100,
originalWidth: 2048,
originalHeight: 2048,
},
},
],
});
expect(placement.x).toBe(170);
expect(placement.y).toBe(-22);
});
it('does not treat hidden layers as generation placement blockers', () => {
const placement = chooseGenerationPlacement({
canvasSize,
viewport,
frame: { ...frame, width: 100, height: 100 },
layers: [
layer({
id: 'hidden-center',
hidden: true,
x: 170,
y: 110,
width: 100,
height: 100,
}),
],
generationDialogs: [],
});
expect(placement.x).toBe(170);
expect(placement.y).toBe(100);
});
it('chooses the nearest empty slot when multiple blockers surround the viewport center', () => {
const placement = chooseGenerationPlacement({
canvasSize: { width: 1000, height: 1000 },
viewport: { x: 500, y: 500, scale: 1 },
frame: { ...frame, width: 100, height: 100 },
layers: [
layer({
id: 'left-blocker',
x: -148,
y: -68,
width: 96,
height: 136,
}),
layer({
id: 'top-blocker',
x: -68,
y: -148,
width: 136,
height: 96,
}),
],
generationDialogs: [],
});
expect(placement.x).toBe(-20);
expect(placement.y).toBe(-20);
});
it('avoids existing generation placeholders when choosing a new placement', () => {
const placement = chooseGenerationPlacement({
canvasSize,
viewport,
frame: { ...frame, width: 100, height: 100 },
layers: [],
generationDialogs: [
generationDialog({
id: 'existing-placeholder',
placeholder: {
x: 170,
y: 110,
width: 100,
height: 100,
originalWidth: 2048,
originalHeight: 2048,
},
}),
],
});
expect(placement.x).toBe(170);
expect(placement.y).toBe(-22);
});
it('focuses the viewport on the chosen placement without changing zoom', () => {
const zoomedViewport = { ...viewport, scale: 1.75 };
const nextViewport = centerViewportOnPlacement({
canvasSize,
viewport: zoomedViewport,
placement: {
x: 170,
y: 262,
width: 100,
height: 100,
originalWidth: 2048,
originalHeight: 2048,
},
});
expect(nextViewport.scale).toBe(zoomedViewport.scale);
expect(nextViewport).toEqual({ x: 65, y: -226, scale: 1.75 });
});
});