00db7d39ea
移除生成器对话框打开后的安全范围自动拉动视口。 取消快速编辑、裁扩和角色动画面板的画布范围夹取。 保留创建生成器时聚焦新占位的行为并更新回归测试。
226 lines
6.3 KiB
TypeScript
226 lines
6.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasLayer,
|
|
CharacterAnimationPanelState,
|
|
CropExpandPanelState,
|
|
QuickEditPanelState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import {
|
|
isCanvasGenerationComposerVisible,
|
|
resolveCharacterAnimationPanelStyle,
|
|
resolveCropExpandPanelStyle,
|
|
resolveGenerationAnchor,
|
|
resolveGenerationComposerStyle,
|
|
resolveIconComposerStyle,
|
|
resolveQuickEditFocusViewport,
|
|
resolveQuickEditPanelStyle,
|
|
resolveSelectedToolbarStyle,
|
|
} from './ImageCanvasOverlayModel';
|
|
|
|
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
|
return {
|
|
id: 'layer-a',
|
|
resourceId: 'resource-a',
|
|
title: '图层A',
|
|
src: 'data:image/png;base64,layer',
|
|
x: 100,
|
|
y: 80,
|
|
width: 240,
|
|
height: 120,
|
|
originalWidth: 240,
|
|
originalHeight: 120,
|
|
zIndex: 1,
|
|
sourceType: 'uploaded',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createDialog(
|
|
overrides: Partial<CanvasGenerationDialogState> = {},
|
|
): CanvasGenerationDialogState {
|
|
return {
|
|
id: 'dialog-a',
|
|
mode: 'generate',
|
|
prompt: '',
|
|
status: 'idle',
|
|
placeholder: {
|
|
x: 300,
|
|
y: 200,
|
|
width: 360,
|
|
height: 260,
|
|
originalWidth: 1024,
|
|
originalHeight: 1024,
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('ImageCanvasOverlayModel', () => {
|
|
it('anchors generation composer to generated layers before placeholders', () => {
|
|
const generatedLayer = createLayer({ x: 40, y: 60, width: 320, height: 180 });
|
|
const dialog = createDialog({ generatedLayerId: generatedLayer.id });
|
|
const anchor = resolveGenerationAnchor({ dialog, generatedLayer });
|
|
|
|
expect(anchor).toBe(generatedLayer);
|
|
expect(
|
|
resolveGenerationComposerStyle({
|
|
dialog,
|
|
anchor,
|
|
viewport: { x: 10, y: 20, scale: 2 },
|
|
}),
|
|
).toEqual({
|
|
left: 410,
|
|
top: 510,
|
|
});
|
|
});
|
|
|
|
it('hides generation composer while generating or explicitly closed', () => {
|
|
const placeholder = createDialog().placeholder ?? null;
|
|
|
|
expect(
|
|
resolveGenerationComposerStyle({
|
|
dialog: createDialog({ status: 'generating' }),
|
|
anchor: placeholder,
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
resolveGenerationComposerStyle({
|
|
dialog: createDialog({ composerOpen: false }),
|
|
anchor: placeholder,
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('keeps icon composer aligned to the base generation composer width', () => {
|
|
expect(
|
|
resolveIconComposerStyle({
|
|
dialog: createDialog({ mode: 'icon' }),
|
|
composerStyle: { left: 100, top: 200 },
|
|
}),
|
|
).toEqual({
|
|
left: 100,
|
|
top: 200,
|
|
});
|
|
|
|
expect(
|
|
resolveIconComposerStyle({
|
|
dialog: createDialog({ mode: 'generate' }),
|
|
composerStyle: { left: 100, top: 200 },
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('lets selected layer toolbar move past the horizontal canvas edges', () => {
|
|
expect(
|
|
resolveSelectedToolbarStyle({
|
|
selectedLayer: createLayer({ x: -500, y: -100 }),
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
canvasSize: { width: 900, height: 640 },
|
|
}),
|
|
).toEqual({ left: -380, top: 10 });
|
|
|
|
expect(
|
|
resolveSelectedToolbarStyle({
|
|
selectedLayer: createLayer({ x: 1200, y: 100 }),
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
canvasSize: { width: 900, height: 640 },
|
|
}),
|
|
).toEqual({ left: 1320, top: 88 });
|
|
});
|
|
|
|
it('fits tall quick edit sources above the reserved bottom composer area', () => {
|
|
const sourceLayer = createLayer({ x: 100, y: 50, width: 360, height: 1600 });
|
|
const viewport = resolveQuickEditFocusViewport({
|
|
sourceLayer,
|
|
canvasSize: { width: 900, height: 640 },
|
|
});
|
|
const sourceTop = viewport.y + sourceLayer.y * viewport.scale;
|
|
const sourceBottom = sourceTop + sourceLayer.height * viewport.scale;
|
|
|
|
expect(viewport.scale).toBeCloseTo(0.1925, 4);
|
|
expect(sourceTop).toBeCloseTo(56, 2);
|
|
expect(sourceBottom).toBeLessThanOrEqual(364);
|
|
expect(sourceBottom + 12 + 260).toBeLessThanOrEqual(640);
|
|
});
|
|
|
|
it('positions generation panels from their anchors without clamping to viewport bounds', () => {
|
|
const sourceLayer = createLayer({ x: 740, y: 460, width: 240, height: 180 });
|
|
const quickEditPanel: QuickEditPanelState = {
|
|
sourceLayerId: sourceLayer.id,
|
|
prompt: '',
|
|
size: '1024x1024',
|
|
model: 'gpt-image-2',
|
|
status: 'idle',
|
|
};
|
|
const characterPanel: CharacterAnimationPanelState = {
|
|
sourceLayerId: sourceLayer.id,
|
|
promptText: '',
|
|
resolution: '480p',
|
|
ratio: 'same',
|
|
frameCount: 32,
|
|
durationSeconds: 4,
|
|
status: 'idle',
|
|
};
|
|
const cropExpandPanel: CropExpandPanelState = {
|
|
sourceLayerId: sourceLayer.id,
|
|
frame: {
|
|
x: sourceLayer.x,
|
|
y: sourceLayer.y,
|
|
width: sourceLayer.width,
|
|
height: sourceLayer.height,
|
|
},
|
|
ratio: 'free',
|
|
status: 'idle',
|
|
};
|
|
|
|
expect(
|
|
resolveQuickEditPanelStyle({
|
|
panel: quickEditPanel,
|
|
sourceLayer,
|
|
viewport: { x: 20, y: 10, scale: 1 },
|
|
canvasSize: { width: 900, height: 640 },
|
|
}),
|
|
).toEqual({ left: 880, top: 662 });
|
|
expect(
|
|
resolveCropExpandPanelStyle({
|
|
panel: cropExpandPanel,
|
|
sourceLayer,
|
|
viewport: { x: 20, y: 10, scale: 1 },
|
|
canvasSize: { width: 900, height: 640 },
|
|
}),
|
|
).toEqual({ left: 1012, top: 470 });
|
|
expect(
|
|
resolveCharacterAnimationPanelStyle({
|
|
panel: characterPanel,
|
|
sourceLayer,
|
|
viewport: { x: 20, y: 10, scale: 1 },
|
|
canvasSize: { width: 900, height: 640 },
|
|
}),
|
|
).toEqual({ left: 1012, top: 470 });
|
|
});
|
|
|
|
it('recognizes canvas generation composer dialog modes', () => {
|
|
expect(isCanvasGenerationComposerVisible(createDialog({ mode: 'generate' }))).toBe(
|
|
true,
|
|
);
|
|
expect(isCanvasGenerationComposerVisible(createDialog({ mode: 'spec' }))).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
isCanvasGenerationComposerVisible(createDialog({ mode: 'publication' })),
|
|
).toBe(true);
|
|
expect(
|
|
isCanvasGenerationComposerVisible({
|
|
mode: 'edit',
|
|
prompt: '',
|
|
status: 'idle',
|
|
}),
|
|
).toBe(false);
|
|
expect(isCanvasGenerationComposerVisible(null)).toBe(false);
|
|
});
|
|
});
|