Update Match3D/image-generation docs & code
Adds/updates documentation, assets and implementation for Match3D and puzzle image generation workflows. Key changes: decision logs and pitfalls updated to prefer VectorEngine Gemini for Match3D material sheets and to require edits (multipart) for 1:1 container reference images; guidance added for when to use APIMart vs VectorEngine. .env.example clarified APIMart/Responses config. Many new public assets and PPT visuals added. Code changes across frontend and backend: updated shared contracts, server-rs match3d/puzzle/image-generation handlers, VectorEngine/OpenAI image generation clients, and multiple React components/tests to handle UI/background/container image signing, edits workflow, and puzzle UI background resolution. Added src/services/puzzle-runtime/puzzleUiBackgroundSource.ts and related test updates. Includes notes about multipart HTTP/1.1 requirement and test/verification commands in docs.
This commit is contained in:
@@ -542,7 +542,7 @@ describe('PuzzleResultView', () => {
|
||||
const publishDialog = screen.getByRole('dialog', { name: '发布拼图作品' });
|
||||
expect(within(publishDialog).getByText('还有关卡画面正在生成。')).toBeTruthy();
|
||||
expect(
|
||||
within(publishDialog).getByRole('button', { name: '发布到广场' }),
|
||||
within(publishDialog).getByRole('button', { name: /发布到广场/u }),
|
||||
).toHaveProperty('disabled', true);
|
||||
});
|
||||
|
||||
@@ -561,7 +561,7 @@ describe('PuzzleResultView', () => {
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '发布拼图作品' })).getByRole(
|
||||
'button',
|
||||
{ name: '发布到广场' },
|
||||
{ name: /发布到广场/u },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -598,7 +598,7 @@ describe('PuzzleResultView', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /发布/u }));
|
||||
const dialog = screen.getByRole('dialog', { name: '发布拼图作品' });
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole('button', { name: '发布到广场' }),
|
||||
within(dialog).getByRole('button', { name: /发布到广场/u }),
|
||||
);
|
||||
|
||||
rerender(
|
||||
@@ -715,6 +715,56 @@ describe('PuzzleResultView', () => {
|
||||
expect(within(preview).getByLabelText('拼图区边界')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('UI背景只有 objectKey 时草稿页仍显示生成图', () => {
|
||||
const base = createSession();
|
||||
const level = base.draft!.levels![0]!;
|
||||
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession({
|
||||
draft: {
|
||||
...base.draft!,
|
||||
levels: [
|
||||
{
|
||||
...level,
|
||||
uiBackgroundPrompt: '雨夜猫街竖屏拼图UI背景',
|
||||
uiBackgroundImageSrc: null,
|
||||
uiBackgroundImageObjectKey:
|
||||
'generated-puzzle-assets/session/ui/background-object-key.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
|
||||
expect(screen.getByAltText('拼图UI背景图').getAttribute('src')).toBe(
|
||||
'/generated-puzzle-assets/session/ui/background-object-key.png',
|
||||
);
|
||||
expect(screen.getByRole('button', { name: /重新生成/u })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('does not display local fallback as saved UI background prompt', () => {
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession()}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
|
||||
expect(screen.getByLabelText('拼图UI背景提示词')).toHaveProperty(
|
||||
'value',
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
test('generates UI background with edited prompt and current levels snapshot', () => {
|
||||
const onExecuteAction = vi.fn();
|
||||
|
||||
@@ -732,6 +782,11 @@ describe('PuzzleResultView', () => {
|
||||
});
|
||||
expect(screen.getByRole('button', { name: /生成UI背景 · 2泥点/u })).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole('button', { name: /生成UI背景/u }));
|
||||
const confirmDialog = screen.getByRole('dialog', {
|
||||
name: '确认消耗泥点',
|
||||
});
|
||||
expect(within(confirmDialog).getByText('消耗 2 泥点')).toBeTruthy();
|
||||
fireEvent.click(within(confirmDialog).getByRole('button', { name: '确定' }));
|
||||
|
||||
expect(onExecuteAction).toHaveBeenCalledWith({
|
||||
action: 'generate_puzzle_ui_background',
|
||||
@@ -752,7 +807,7 @@ describe('PuzzleResultView', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('素材配置背景音乐试听使用签名地址', () => {
|
||||
test('素材配置隐藏背景音乐入口', () => {
|
||||
const base = createSession();
|
||||
const level = base.draft!.levels![0]!;
|
||||
|
||||
@@ -784,15 +839,12 @@ describe('PuzzleResultView', () => {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '背景音乐' }));
|
||||
expect(screen.getByRole('button', { name: /重新生成音乐 · 5泥点/u })).toBeTruthy();
|
||||
|
||||
expect(screen.getByLabelText('拼图背景音乐').getAttribute('src')).toBe(
|
||||
'https://signed.example.com/generated-puzzle-assets/session/audio/music.mp3',
|
||||
);
|
||||
expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /重新生成音乐/u })).toBeNull();
|
||||
expect(screen.queryByLabelText('拼图背景音乐')).toBeNull();
|
||||
});
|
||||
|
||||
test('生成完成回包合并音乐和UI背景后试玩使用最新资源', () => {
|
||||
test('生成完成回包合并历史音乐和UI背景后试玩使用最新资源', () => {
|
||||
const onStartTestRun = vi.fn();
|
||||
const base = createSession();
|
||||
const localLevel = {
|
||||
@@ -857,10 +909,7 @@ describe('PuzzleResultView', () => {
|
||||
expect(screen.getByAltText('拼图UI背景图').getAttribute('src')).toBe(
|
||||
'/generated-puzzle-assets/session/ui/fruit-background.png',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '背景音乐' }));
|
||||
expect(screen.getByLabelText('拼图背景音乐').getAttribute('src')).toBe(
|
||||
'https://signed.example.com/generated-puzzle-assets/session/audio/fruit.mp3',
|
||||
);
|
||||
expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user