387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
215 lines
5.0 KiB
TypeScript
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 });
|
|
});
|
|
});
|