调整图片编辑器参考图选择交互

- 常规参考图入口改为先弹出来源菜单,支持从画布选择和上传图片。

- 角色规范、图标规范和常规参考图来源菜单统一向上弹出。

- 画布参考图选择拦截普通图层选中逻辑,保持生成面板不隐藏。

- 补充图片编辑器交互测试与技术文档说明。
This commit is contained in:
2026-06-17 14:08:26 +08:00
parent d0ad8402de
commit e970d34574
12 changed files with 602 additions and 76 deletions

View File

@@ -2914,7 +2914,7 @@ describe('ImageCanvasEditorView', () => {
expect(generateEditorImageMock).toHaveBeenCalledWith({
kind: 'spec',
model: 'gpt-image-2',
model: 'gemini-3.1-flash-image-preview',
size: '2048x1152',
prompt: expect.stringContaining('玩法设计:平台跳跃玩法'),
});
@@ -3009,23 +3009,29 @@ describe('ImageCanvasEditorView', () => {
name: '生成角色形象',
});
expect(within(characterPanel).getByText('画面比例')).toBeTruthy();
expect(within(characterPanel).getByText('大小尺寸')).toBeTruthy();
expect(within(characterPanel).getByText('模型')).toBeTruthy();
expect(
within(characterPanel).getByRole('button', { name: '1:1' }),
).toBeTruthy();
expect(
within(characterPanel).getByRole('button', { name: 'GPT Image' }),
within(characterPanel).getByRole('button', { name: '1K' }),
).toBeTruthy();
expect(
within(characterPanel).getByRole('button', { name: 'nanobanana2' }),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '生成图标素材' }));
const iconPanel = screen.getByRole('dialog', { name: '生成图标素材' });
expect(within(iconPanel).getByText('画面比例')).toBeTruthy();
expect(within(iconPanel).getByText('大小尺寸')).toBeTruthy();
expect(within(iconPanel).getByText('模型')).toBeTruthy();
expect(
within(iconPanel).getByRole('button', { name: 'nanobanana2' }),
).toBeTruthy();
});
it('submits character generation without legacy dimension options', async () => {
it('submits character generation with default model and dimension options', async () => {
generateEditorImageMock.mockResolvedValueOnce({
imageSrc: 'data:image/png;base64,character-model-options',
width: 1024,
@@ -3056,15 +3062,104 @@ describe('ImageCanvasEditorView', () => {
expect.objectContaining({
kind: 'character',
prompt: '高个子游侠',
model: 'gemini-3.1-flash-image-preview',
aspectRatio: '1:1',
imageSize: '1K',
}),
);
});
expect(generateEditorImageMock.mock.calls[0]?.[0]).not.toEqual(
expect.objectContaining({
aspectRatio: expect.any(String),
imageSize: expect.any(String),
}),
});
it('remembers the last selected image model for character and icon generation', async () => {
generateEditorImageMock.mockResolvedValueOnce({
imageSrc: 'data:image/png;base64,character-gpt-model',
width: 1024,
height: 1024,
sourceType: 'generated',
prompt: '蓝衣剑士',
actualPrompt: '蓝衣剑士',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'character-gpt-model-1',
});
generateEditorIconSpritesheetMock.mockResolvedValueOnce({
spritesheetImageSrc: 'data:image/png;base64,sheet-gpt-model',
spritesheetWidth: 1024,
spritesheetHeight: 1024,
iconImageSrcs: [
{
name: '返回按钮',
imageSrc: 'data:image/png;base64,back',
width: 128,
height: 128,
},
],
prompt: '图标 prompt',
actualPrompt: '图标 prompt',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'icon-gpt-model-1',
});
render(<ImageCanvasEditorView />);
await screen.findByAltText('画布图片:拼图素材');
fireEvent.click(screen.getByRole('button', { name: '生成角色形象' }));
const characterPanel = screen.getByRole('dialog', {
name: '生成角色形象',
});
fireEvent.click(
within(characterPanel).getByRole('button', { name: 'gpt-image-2' }),
);
fireEvent.click(within(characterPanel).getByRole('button', { name: '2:3' }));
fireEvent.click(within(characterPanel).getByRole('button', { name: '2K' }));
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
target: { value: '蓝衣剑士' },
});
fireEvent.click(
within(characterPanel).getByRole('button', { name: '生成' }),
);
await waitFor(() => {
expect(generateEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'character',
prompt: '蓝衣剑士',
model: 'gpt-image-2',
aspectRatio: '2:3',
imageSize: '2K',
}),
);
});
fireEvent.click(screen.getByRole('button', { name: '生成图标素材' }));
const iconPanel = screen.getByRole('dialog', { name: '生成图标素材' });
expect(
within(iconPanel).getByRole('button', { name: 'gpt-image-2' }),
).toBeTruthy();
fireEvent.click(
within(iconPanel).getByRole('button', { name: '图标素材规范' }),
);
fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' }));
await userEvent.upload(
screen.getByLabelText('上传图片文件'),
new File(['icon-spec'], '图标规范.png', { type: 'image/png' }),
);
fireEvent.click(
within(screen.getByRole('dialog', { name: '生成图标素材' })).getByRole(
'button',
{ name: '生成' },
),
);
await waitFor(() => {
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
expect.objectContaining({
model: 'gpt-image-2',
aspectRatio: '1:1',
imageSize: '1K',
}),
);
});
});
it('keeps the bottom AI toolbar visible while generation panels are open', () => {
@@ -3239,6 +3334,18 @@ describe('ImageCanvasEditorView', () => {
const sourceMenu = screen.getByRole('menu', { name: '角色形象规范来源' });
expect(referenceRow?.contains(sourceMenu)).toBe(false);
expect(sourceMenu.className).toContain('platform-floating-menu--top-start');
fireEvent.click(
within(characterPanel).getByRole('button', { name: '上传常规参考图' }),
);
const regularReferenceMenu = screen.getByRole('menu', {
name: '常规参考图来源',
});
expect(referenceRow?.contains(regularReferenceMenu)).toBe(false);
expect(regularReferenceMenu.className).toContain(
'platform-floating-menu--top-start',
);
});
it('uses Lovart-style reference tiles in the character generation panel', () => {
@@ -3395,7 +3502,7 @@ describe('ImageCanvasEditorView', () => {
expect(generateEditorImageMock).toHaveBeenCalledWith({
kind: 'spec',
model: 'gpt-image-2',
model: 'gemini-3.1-flash-image-preview',
size: '2048x1152',
prompt: expect.stringContaining('生成一张完整游戏UI规范汇总设定展板'),
});
@@ -3442,7 +3549,7 @@ describe('ImageCanvasEditorView', () => {
expect(generateEditorImageMock).toHaveBeenCalledWith({
kind: 'spec',
model: 'gpt-image-2',
model: 'gemini-3.1-flash-image-preview',
size: '2048x1152',
prompt: '生成一张武器图标规范展板',
});
@@ -3504,15 +3611,51 @@ describe('ImageCanvasEditorView', () => {
screen.queryByText('请选择画布中的图片作为角色形象规范,按 Esc 退出'),
).toBeNull();
const canvasReferenceLayer = screen
.getByAltText('画布图片:大鱼素材')
.closest('button')!;
expect(canvasReferenceLayer.className).not.toContain(
'image-canvas-editor__layer--selected',
);
fireEvent.click(
within(characterPanel).getByRole('button', { name: '上传常规参考图' }),
);
const regularReferenceMenu = screen.getByRole('menu', {
name: '常规参考图来源',
});
fireEvent.click(
within(regularReferenceMenu).getByRole('menuitem', {
name: '从画布中选择',
}),
);
expect(
screen.getByText('请选择画布中的图片作为常规参考图,按 Esc 退出'),
).toBeTruthy();
fireEvent.pointerDown(canvasReferenceLayer, {
button: 0,
pointerId: 171,
clientX: 180,
clientY: 120,
});
expect(
screen.queryByText('请选择画布中的图片作为常规参考图,按 Esc 退出'),
).toBeNull();
expect(screen.getByRole('dialog', { name: '生成角色形象' })).toBeTruthy();
expect(canvasReferenceLayer.className).not.toContain(
'image-canvas-editor__layer--selected',
);
expect(within(characterPanel).getByText('1')).toBeTruthy();
fireEvent.click(
within(characterPanel).getByRole('button', { name: '上传常规参考图' }),
);
fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' }));
await userEvent.upload(
screen.getByLabelText('上传图片文件'),
new File(['reference'], '常规参考.png', { type: 'image/png' }),
);
await waitFor(() => {
expect(within(characterPanel).getByText('1')).toBeTruthy();
expect(within(characterPanel).getByText('2')).toBeTruthy();
});
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
@@ -3525,8 +3668,12 @@ describe('ImageCanvasEditorView', () => {
expect(generateEditorImageMock).toHaveBeenCalledWith({
kind: 'character',
prompt: '银发游侠,蓝色披风,弓箭手,适合像素风战棋。',
model: 'gemini-3.1-flash-image-preview',
aspectRatio: '1:1',
imageSize: '1K',
referenceImageSrcs: [
'/creation-type-references/puzzle.webp',
'/creation-type-references/big-fish.webp',
expect.stringMatching(/^data:image\/png;base64,/u),
],
});
@@ -3554,6 +3701,8 @@ describe('ImageCanvasEditorView', () => {
expect(within(characterInfoPanel).getByText('角色形象规范')).toBeTruthy();
expect(within(characterInfoPanel).getByText('拼图素材')).toBeTruthy();
expect(within(characterInfoPanel).getByText('常规参考图 1')).toBeTruthy();
expect(within(characterInfoPanel).getByText('大鱼素材')).toBeTruthy();
expect(within(characterInfoPanel).getByText('常规参考图 2')).toBeTruthy();
expect(within(characterInfoPanel).getByText('常规参考.png')).toBeTruthy();
await waitFor(() => {
expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith(
@@ -3752,6 +3901,9 @@ describe('ImageCanvasEditorView', () => {
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith({
referenceImageSrc: 'data:image/png;base64,icon-spec',
iconDescriptions: ['返回按钮', '设置按钮'],
model: 'gemini-3.1-flash-image-preview',
aspectRatio: '1:1',
imageSize: '1K',
});
await waitFor(() => {