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:
2026-05-14 20:34:45 +08:00
parent d33c937ebc
commit 548db78ca7
103 changed files with 6687 additions and 3270 deletions

View File

@@ -150,6 +150,14 @@ function stubCanvas(dataUrl: string, drawImage = vi.fn()) {
return drawImage;
}
function confirmPuzzlePointCost() {
const confirmDialog = screen.getByRole('dialog', {
name: '确认消耗泥点',
});
expect(within(confirmDialog).getByText('消耗 2 泥点')).toBeTruthy();
fireEvent.click(within(confirmDialog).getByRole('button', { name: '确定' }));
}
test('puzzle workspace submits the work form instead of agent chat', () => {
const onCreateFromForm = vi.fn();
@@ -174,6 +182,9 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
});
fireEvent.click(screen.getByRole('button', { name: /稿/u }));
expect(onCreateFromForm).not.toHaveBeenCalled();
confirmPuzzlePointCost();
expect(onCreateFromForm).toHaveBeenCalledWith({
seedText: '一只猫在雨夜灯牌下回头。',
pictureDescription: '一只猫在雨夜灯牌下回头。',
@@ -243,6 +254,7 @@ test('puzzle workspace keeps the reference image upload as a primary panel', ()
target: { value: '一只猫在阳光窗台上看着毛线球。' },
});
fireEvent.click(screen.getByRole('button', { name: /稿/u }));
confirmPuzzlePointCost();
expect(onCreateFromForm).toHaveBeenCalledWith(
expect.objectContaining({
pictureDescription: '一只猫在阳光窗台上看着毛线球。',
@@ -303,6 +315,7 @@ test('puzzle workspace selects a history image from the upload card', async () =
target: { value: '保留历史图里的主体,改成晴天花园。' },
});
fireEvent.click(screen.getByRole('button', { name: /稿/u }));
confirmPuzzlePointCost();
expect(onCreateFromForm).toHaveBeenCalledWith({
seedText: '保留历史图里的主体,改成晴天花园。',
@@ -356,6 +369,7 @@ test('puzzle workspace falls back to compile action for restored sessions', () =
);
fireEvent.click(screen.getByRole('button', { name: /稿/u }));
confirmPuzzlePointCost();
expect(onCreateFromForm).not.toHaveBeenCalled();
expect(onExecuteAction).toHaveBeenCalledWith({
@@ -389,6 +403,7 @@ test('puzzle workspace switches the image model from the description box', () =>
expect(screen.queryByRole('menuitemradio', { name: '原模型' })).toBeNull();
fireEvent.click(screen.getByRole('menuitemradio', { name: 'nanobanana2' }));
fireEvent.click(screen.getByRole('button', { name: /稿/u }));
confirmPuzzlePointCost();
expect(onCreateFromForm).toHaveBeenCalledWith(
expect.objectContaining({
@@ -538,6 +553,7 @@ test('puzzle workspace submits uploaded reference image when AI redraw is on', a
target: { value: '保留上传画面的主体和构图,改成雨夜灯街。' },
});
fireEvent.click(screen.getByRole('button', { name: /稿/u }));
confirmPuzzlePointCost();
expect(onCreateFromForm).toHaveBeenCalledWith({
seedText: '保留上传画面的主体和构图,改成雨夜灯街。',