Files
Genarrative/src/components/image-editor/ImageCanvasGenerationPlacementModel.test.ts
T
JenkenB 8956ed6fe6 Unify canvas generation placement and UI changes
Add unified placement and UI updates for canvas generation: docs require ImageCanvasGenerationPlacementModel to compute placeholder positions (avoid overlaps, center viewport on placement) and limit frontend video model entries. Refactor UI components and tests: expose div props on PlatformFloatingMenu, make PlatformInlineOptionButton forwardRef, replace inline placeholder actions with ImageCanvasGenerationImageOptionsView in basic generation composer, enhance character animation panel with parameter menu portal and option choices, add reference chip UI, and update many tests to match new interactions and labels. These changes align generation workflows, accessibility labels, and positioning behavior across editor panels.
2026-06-18 11:40:19 +08:00

215 lines
5.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
chooseGenerationPlacement,
centerViewportOnPlacement,
} from './ImageCanvasGenerationPlacementModel';
import type {
CanvasGenerationDialogState,
CanvasLayer,
CanvasViewport,
} from './ImageCanvasEditorTypes';
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('centers 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 });
});
});