Switch to VectorEngine gpt-image-2 and edits
Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
This commit is contained in:
@@ -14,6 +14,10 @@ import * as match3dWorksService from '../../services/match3d-works';
|
||||
import { clearMatch3DGeneratedModelBytesCache } from '../../services/match3dGeneratedModelCache';
|
||||
import { Match3DResultView } from './Match3DResultView';
|
||||
|
||||
const match3dSpritesheetParser = vi.hoisted(() => ({
|
||||
loadMatch3DSpritesheetAssetRegions: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../ResolvedAssetImage', () => ({
|
||||
ResolvedAssetImage: ({
|
||||
src,
|
||||
@@ -45,9 +49,20 @@ vi.mock('../../services/match3d-works', () => ({
|
||||
updateMatch3DWork: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/match3dSpritesheetParser', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../services/match3dSpritesheetParser')>();
|
||||
return {
|
||||
...actual,
|
||||
loadMatch3DSpritesheetAssetRegions:
|
||||
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearMatch3DGeneratedModelBytesCache();
|
||||
vi.clearAllMocks();
|
||||
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions.mockReset();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
@@ -687,8 +702,10 @@ describe('Match3DResultView', () => {
|
||||
expect(screen.queryByRole('button', { name: '音乐' })).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
expect(screen.getByRole('button', { name: '物品' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: 'UI' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: 'UI素材' })).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
|
||||
expect(screen.getAllByRole('button', { name: /打开.+物品素材/u }))
|
||||
.toHaveLength(20);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '打开水果核心物件物品素材' }),
|
||||
@@ -1075,7 +1092,7 @@ describe('Match3DResultView', () => {
|
||||
);
|
||||
});
|
||||
expect(screen.getByText('63 件')).toBeTruthy();
|
||||
expect(screen.getAllByText('21 种').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('20 种').length).toBeGreaterThan(0);
|
||||
expect(onSaved).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
clearCount: 21,
|
||||
@@ -1323,13 +1340,32 @@ describe('Match3DResultView', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('素材配置 UI 子 Tab 展示默认提示词并支持预览UI页面', () => {
|
||||
test('素材配置 UI素材子 Tab 仅预览背景图和UI spritesheet', () => {
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={createProfile({
|
||||
backgroundPrompt: '果园主题抓大鹅竖屏背景',
|
||||
backgroundImageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/background.png',
|
||||
generatedBackgroundAsset: {
|
||||
prompt: '果园主题抓大鹅竖屏背景',
|
||||
levelScenePrompt: '果园完整关卡画面',
|
||||
levelSceneImageSrc:
|
||||
'/generated-match3d-assets/session/profile/level-scene/scene.png',
|
||||
levelSceneImageObjectKey: null,
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/background.png',
|
||||
imageObjectKey: null,
|
||||
uiSpritesheetPrompt: 'UI spritesheet',
|
||||
uiSpritesheetImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||||
uiSpritesheetImageObjectKey: null,
|
||||
containerPrompt: null,
|
||||
containerImageSrc: null,
|
||||
containerImageObjectKey: null,
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
@@ -1337,15 +1373,18 @@ describe('Match3DResultView', () => {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'UI' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
|
||||
|
||||
expect(screen.getByAltText('游戏背景图').getAttribute('src')).toBe(
|
||||
'/generated-match3d-assets/session/profile/background/background.png',
|
||||
);
|
||||
expect(screen.getByLabelText('UI背景图画面描述提示词')).toHaveProperty(
|
||||
'value',
|
||||
'果园主题抓大鹅竖屏背景',
|
||||
expect(screen.getByAltText('UI素材图').getAttribute('src')).toBe(
|
||||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||||
);
|
||||
expect(
|
||||
screen.queryByLabelText('UI背景图画面描述提示词'),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
|
||||
expect(screen.getByRole('dialog', { name: 'UI预览' })).toBeTruthy();
|
||||
@@ -1359,7 +1398,7 @@ describe('Match3DResultView', () => {
|
||||
'img[src="/match3d-background-references/pot-fused-reference.png"]',
|
||||
);
|
||||
expect(containerImage).toBeTruthy();
|
||||
expect(containerImage?.className).toContain('w-[min(108vw,38rem)]');
|
||||
expect(containerImage?.className).toContain('w-[min(116vw,42rem)]');
|
||||
expect(containerImage?.className).toContain('-translate-x-1/2');
|
||||
expect(
|
||||
document.querySelector('.animate-spin, [class*="border-l-transparent"]'),
|
||||
@@ -1371,7 +1410,7 @@ describe('Match3DResultView', () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('素材配置 UI 子 Tab 从物品挂载资产展示生成背景和容器', async () => {
|
||||
test('素材配置 UI素材子 Tab 从物品挂载资产展示生成背景和UI spritesheet', async () => {
|
||||
const onStartTestRun = vi.fn();
|
||||
const profile = createProfile({
|
||||
backgroundPrompt: null,
|
||||
@@ -1388,11 +1427,14 @@ describe('Match3DResultView', () => {
|
||||
'/generated-match3d-assets/session/profile/background/background.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/background.png',
|
||||
containerPrompt: '果园容器',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/container.png',
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/container.png',
|
||||
uiSpritesheetPrompt: '果园UI素材',
|
||||
uiSpritesheetImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||||
uiSpritesheetImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||||
containerPrompt: null,
|
||||
containerImageSrc: null,
|
||||
containerImageObjectKey: null,
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
@@ -1417,15 +1459,16 @@ describe('Match3DResultView', () => {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'UI' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
|
||||
|
||||
expect(screen.getByAltText('游戏背景图').getAttribute('src')).toBe(
|
||||
'/generated-match3d-assets/session/profile/background/background.png',
|
||||
);
|
||||
expect(screen.getByLabelText('UI背景图画面描述提示词')).toHaveProperty(
|
||||
'value',
|
||||
'果园背景',
|
||||
expect(screen.getByAltText('UI素材图').getAttribute('src')).toBe(
|
||||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||||
);
|
||||
expect(screen.queryByLabelText('UI背景图画面描述提示词')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
|
||||
expect(
|
||||
@@ -1435,14 +1478,14 @@ describe('Match3DResultView', () => {
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector(
|
||||
'img[src="/generated-match3d-assets/session/profile/ui-container/container.png"]',
|
||||
'img[src="/generated-match3d-assets/session/profile/ui-spritesheet/ui.png"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector(
|
||||
'img[src="/match3d-background-references/pot-fused-reference.png"]',
|
||||
),
|
||||
).toBeNull();
|
||||
).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||||
|
||||
@@ -1455,8 +1498,8 @@ describe('Match3DResultView', () => {
|
||||
generatedBackgroundAsset: expect.objectContaining({
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/background.png',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/container.png',
|
||||
uiSpritesheetImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||||
}),
|
||||
}),
|
||||
{
|
||||
@@ -1466,7 +1509,81 @@ describe('Match3DResultView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('素材配置 UI 子 Tab 修改提示词后调用背景图生成接口并刷新素材', async () => {
|
||||
test('素材配置 UI素材子 Tab 预览物品spritesheet解析结果', async () => {
|
||||
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions.mockResolvedValue(
|
||||
Array.from({ length: 100 }, (_, index) => ({
|
||||
label: `素材${index + 1}`,
|
||||
x: index * 2,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
sheetWidth: 200,
|
||||
sheetHeight: 2,
|
||||
imageSrc: `data:image/png;base64,item-${index + 1}`,
|
||||
})),
|
||||
);
|
||||
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={createProfile({
|
||||
generatedItemAssets: [
|
||||
{ ...createReadyGeneratedItemAsset(1), itemName: '草莓' },
|
||||
{ ...createReadyGeneratedItemAsset(2), itemName: '苹果' },
|
||||
],
|
||||
generatedBackgroundAsset: {
|
||||
prompt: '果园主题抓大鹅竖屏背景',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/background.png',
|
||||
imageObjectKey: null,
|
||||
uiSpritesheetPrompt: 'UI spritesheet',
|
||||
uiSpritesheetImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||||
uiSpritesheetImageObjectKey: null,
|
||||
itemSpritesheetPrompt: '物品 spritesheet',
|
||||
itemSpritesheetImageSrc:
|
||||
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
|
||||
itemSpritesheetImageObjectKey: null,
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
|
||||
|
||||
expect(screen.getByAltText('物品素材图').getAttribute('src')).toBe(
|
||||
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('match3d-item-spritesheet-preview-0-0')
|
||||
.getAttribute('src'),
|
||||
).toBe('data:image/png;base64,item-1');
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('match3d-item-spritesheet-preview-1-4')
|
||||
.getAttribute('src'),
|
||||
).toBe('data:image/png;base64,item-10');
|
||||
});
|
||||
expect(screen.getByText('草莓')).toBeTruthy();
|
||||
expect(screen.getByText('苹果')).toBeTruthy();
|
||||
expect(
|
||||
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
maxRegions: 100,
|
||||
source:
|
||||
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('素材配置 UI素材子 Tab 不提供背景或容器重新生成入口', () => {
|
||||
const profile = createProfile({
|
||||
generatedItemAssets: [
|
||||
{
|
||||
@@ -1500,267 +1617,24 @@ describe('Match3DResultView', () => {
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
const nextProfile = createProfile({
|
||||
...profile,
|
||||
backgroundPrompt: '新背景提示词',
|
||||
backgroundImageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/new/background.png',
|
||||
generatedItemAssets: [
|
||||
{
|
||||
...profile.generatedItemAssets![0]!,
|
||||
backgroundAsset: {
|
||||
prompt: '新背景提示词',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/new/background.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/new/background.png',
|
||||
containerPrompt: '新容器提示词',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
generatedBackgroundAsset: {
|
||||
prompt: '新背景提示词',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/new/background.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/new/background.png',
|
||||
containerPrompt: '新容器提示词',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
const onSaved = vi.fn();
|
||||
vi.mocked(
|
||||
match3dWorksService.generateMatch3DBackgroundImage,
|
||||
).mockResolvedValue({
|
||||
item: nextProfile,
|
||||
backgroundImageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/new/background.png',
|
||||
backgroundImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/new/background.png',
|
||||
generatedBackgroundAsset: nextProfile.generatedBackgroundAsset!,
|
||||
prompt: '新背景提示词',
|
||||
});
|
||||
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={profile}
|
||||
onBack={() => {}}
|
||||
onSaved={onSaved}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'UI' }));
|
||||
fireEvent.change(screen.getByLabelText('UI背景图画面描述提示词'), {
|
||||
target: { value: '新背景提示词' },
|
||||
});
|
||||
expect(
|
||||
screen.getByRole('button', { name: /重新生成 · 2泥点/u }),
|
||||
).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole('button', { name: /重新生成/u }));
|
||||
expect(screen.getByRole('dialog', { name: '确认消耗泥点' })).toBeTruthy();
|
||||
confirmPointCost();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
match3dWorksService.generateMatch3DBackgroundImage,
|
||||
).toHaveBeenCalledWith(profile.profileId, {
|
||||
prompt: '新背景提示词',
|
||||
});
|
||||
expect(onSaved).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
generatedItemAssets: [
|
||||
expect.objectContaining({
|
||||
itemId: 'match3d-item-1',
|
||||
backgroundAsset: expect.objectContaining({
|
||||
prompt: '新背景提示词',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/new/background.png',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('素材配置 UI 子 Tab 重新生成后显示90秒倒计时进度', async () => {
|
||||
const deferred =
|
||||
createDeferred<
|
||||
Awaited<
|
||||
ReturnType<typeof match3dWorksService.generateMatch3DBackgroundImage>
|
||||
>
|
||||
>();
|
||||
vi.mocked(
|
||||
match3dWorksService.generateMatch3DBackgroundImage,
|
||||
).mockReturnValue(deferred.promise);
|
||||
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={createProfile({
|
||||
generatedBackgroundAsset: {
|
||||
prompt: '旧背景提示词',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/old/background.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/old/background.png',
|
||||
containerPrompt: '旧容器提示词',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/old/container.png',
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/old/container.png',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'UI' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /重新生成/u }));
|
||||
confirmPointCost();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('progressbar', { name: 'UI背景图生成进度' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('预计剩余 90 秒')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('素材配置容器形象子 Tab 单独调用容器图生成接口并刷新素材', async () => {
|
||||
const profile = createProfile({
|
||||
generatedBackgroundAsset: {
|
||||
prompt: '旧背景提示词',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/old/background.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/old/background.png',
|
||||
containerPrompt: '旧容器提示词',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/old/container.png',
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/old/container.png',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
generatedItemAssets: [
|
||||
{
|
||||
...createReadyGeneratedItemAsset(1),
|
||||
backgroundAsset: {
|
||||
prompt: '旧背景提示词',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/old/background.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/old/background.png',
|
||||
containerPrompt: '旧容器提示词',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/old/container.png',
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/old/container.png',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const nextBackgroundAsset = {
|
||||
prompt: '旧背景提示词',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/old/background.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/old/background.png',
|
||||
containerPrompt: '新容器提示词',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
};
|
||||
const nextProfile = createProfile({
|
||||
...profile,
|
||||
generatedBackgroundAsset: nextBackgroundAsset,
|
||||
generatedItemAssets: [
|
||||
{
|
||||
...profile.generatedItemAssets![0]!,
|
||||
backgroundAsset: nextBackgroundAsset,
|
||||
},
|
||||
],
|
||||
});
|
||||
const onSaved = vi.fn();
|
||||
vi.mocked(
|
||||
match3dWorksService.generateMatch3DContainerImage,
|
||||
).mockResolvedValue({
|
||||
item: nextProfile,
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
generatedBackgroundAsset: nextBackgroundAsset,
|
||||
prompt: '新容器提示词',
|
||||
});
|
||||
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={profile}
|
||||
onBack={() => {}}
|
||||
onSaved={onSaved}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '容器形象' }));
|
||||
fireEvent.change(screen.getByLabelText('容器形象画面描述提示词'), {
|
||||
target: { value: '新容器提示词' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /重新生成/u }));
|
||||
confirmPointCost();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
match3dWorksService.generateMatch3DContainerImage,
|
||||
).toHaveBeenCalledWith(profile.profileId, {
|
||||
prompt: '新容器提示词',
|
||||
});
|
||||
expect(
|
||||
match3dWorksService.generateMatch3DBackgroundImage,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(onSaved).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
generatedItemAssets: [
|
||||
expect.objectContaining({
|
||||
itemId: 'match3d-item-1',
|
||||
backgroundAsset: expect.objectContaining({
|
||||
prompt: '旧背景提示词',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/old/background.png',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(screen.queryByLabelText('UI背景图画面描述提示词')).toBeNull();
|
||||
expect(screen.queryByLabelText('容器形象画面描述提示词')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '容器形象' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
|
||||
expect(match3dWorksService.generateMatch3DBackgroundImage).not.toHaveBeenCalled();
|
||||
expect(match3dWorksService.generateMatch3DContainerImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('历史草稿同时带旧 draft 和 profile 素材时以 profile 多视角素材补齐试玩资产', async () => {
|
||||
|
||||
@@ -32,8 +32,6 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import { isGeneratedLegacyPath } from '../../services/assetReadUrlService';
|
||||
import {
|
||||
generateMatch3DBackgroundImage,
|
||||
generateMatch3DContainerImage,
|
||||
generateMatch3DCoverImage,
|
||||
generateMatch3DItemAssets,
|
||||
generateMatch3DWorkTags,
|
||||
@@ -48,6 +46,11 @@ import {
|
||||
resolveMatch3DGeneratedImageAssetSource,
|
||||
resolveMatch3DGeneratedModelAssetSource,
|
||||
} from '../../services/match3dGeneratedModelCache';
|
||||
import {
|
||||
buildMatch3DItemSpritesheetViewRegions,
|
||||
loadMatch3DSpritesheetAssetRegions,
|
||||
type Match3DDecodedSpritesheetRegion,
|
||||
} from '../../services/match3dSpritesheetParser';
|
||||
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import {
|
||||
@@ -83,7 +86,7 @@ type Match3DResultViewProps = {
|
||||
|
||||
type Match3DAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
type Match3DResultTab = 'work' | 'config' | 'assets';
|
||||
type Match3DAssetConfigTab = 'items' | 'ui' | 'container';
|
||||
type Match3DAssetConfigTab = 'items' | 'ui';
|
||||
type Match3DAssetTaskStatus =
|
||||
| 'idle'
|
||||
| 'submitting'
|
||||
@@ -102,6 +105,12 @@ type Match3DBatchItemGenerationState = {
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type Match3DItemSpritesheetPreviewGroup = {
|
||||
itemIndex: number;
|
||||
itemName: string;
|
||||
regions: Match3DDecodedSpritesheetRegion[];
|
||||
};
|
||||
|
||||
type Match3DTimedGenerationProgress = {
|
||||
startedAtMs: number;
|
||||
nowMs: number;
|
||||
@@ -158,11 +167,9 @@ type Match3DCoverReferenceDraft = {
|
||||
const MATCH3D_MIN_TAG_COUNT = 3;
|
||||
const MATCH3D_MAX_TAG_COUNT = 6;
|
||||
const MATCH3D_AUTOSAVE_DEBOUNCE_MS = 600;
|
||||
const MATCH3D_DEFAULT_ASSET_COUNT = 6;
|
||||
const MATCH3D_UI_BACKGROUND_POINTS_COST = 2;
|
||||
const MATCH3D_UI_BACKGROUND_GENERATION_ESTIMATE_SECONDS = 90;
|
||||
const MATCH3D_DEFAULT_ASSET_COUNT = 20;
|
||||
const MATCH3D_ITEM_ASSETS_POINTS_PER_BATCH = 2;
|
||||
const MATCH3D_ITEM_ASSETS_BILLING_BATCH_SIZE = 5;
|
||||
const MATCH3D_ITEM_ASSETS_BILLING_BATCH_SIZE = 20;
|
||||
const MATCH3D_COVER_REFERENCE_IMAGE_LIMIT = 6;
|
||||
|
||||
const MATCH3D_RESULT_TABS: Array<{ id: Match3DResultTab; label: string }> = [
|
||||
@@ -176,8 +183,7 @@ const MATCH3D_ASSET_CONFIG_TABS: Array<{
|
||||
label: string;
|
||||
}> = [
|
||||
{ id: 'items', label: '物品' },
|
||||
{ id: 'ui', label: 'UI' },
|
||||
{ id: 'container', label: '容器形象' },
|
||||
{ id: 'ui', label: 'UI素材' },
|
||||
];
|
||||
|
||||
// 中文注释:结果页难度配置必须与创作入口页保持同一组派生参数。
|
||||
@@ -202,7 +208,7 @@ const MATCH3D_DIFFICULTY_OPTIONS = [
|
||||
label: '硬核',
|
||||
clearCount: 21,
|
||||
difficulty: 8,
|
||||
itemTypeCount: 21,
|
||||
itemTypeCount: 20,
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -385,6 +391,56 @@ function resolveMatch3DContainerPreviewSource(
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMatch3DUiSpritesheetPreviewSource(
|
||||
profile: Match3DWorkProfile,
|
||||
draft: Match3DResultDraft | null,
|
||||
generatedItemAssets: readonly Match3DGeneratedItemAsset[],
|
||||
) {
|
||||
return (
|
||||
draft?.generatedBackgroundAsset?.uiSpritesheetImageSrc?.trim() ||
|
||||
draft?.generatedBackgroundAsset?.uiSpritesheetImageObjectKey?.trim() ||
|
||||
draft?.generatedBackgroundAsset?.containerImageSrc?.trim() ||
|
||||
draft?.generatedBackgroundAsset?.containerImageObjectKey?.trim() ||
|
||||
profile.generatedBackgroundAsset?.uiSpritesheetImageSrc?.trim() ||
|
||||
profile.generatedBackgroundAsset?.uiSpritesheetImageObjectKey?.trim() ||
|
||||
profile.generatedBackgroundAsset?.containerImageSrc?.trim() ||
|
||||
profile.generatedBackgroundAsset?.containerImageObjectKey?.trim() ||
|
||||
generatedItemAssets
|
||||
.map(
|
||||
(asset) =>
|
||||
asset.backgroundAsset?.uiSpritesheetImageSrc?.trim() ||
|
||||
asset.backgroundAsset?.uiSpritesheetImageObjectKey?.trim() ||
|
||||
asset.backgroundAsset?.containerImageSrc?.trim() ||
|
||||
asset.backgroundAsset?.containerImageObjectKey?.trim() ||
|
||||
'',
|
||||
)
|
||||
.find(Boolean) ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMatch3DItemSpritesheetPreviewSource(
|
||||
profile: Match3DWorkProfile,
|
||||
draft: Match3DResultDraft | null,
|
||||
generatedItemAssets: readonly Match3DGeneratedItemAsset[],
|
||||
) {
|
||||
return (
|
||||
draft?.generatedBackgroundAsset?.itemSpritesheetImageSrc?.trim() ||
|
||||
draft?.generatedBackgroundAsset?.itemSpritesheetImageObjectKey?.trim() ||
|
||||
profile.generatedBackgroundAsset?.itemSpritesheetImageSrc?.trim() ||
|
||||
profile.generatedBackgroundAsset?.itemSpritesheetImageObjectKey?.trim() ||
|
||||
generatedItemAssets
|
||||
.map(
|
||||
(asset) =>
|
||||
asset.backgroundAsset?.itemSpritesheetImageSrc?.trim() ||
|
||||
asset.backgroundAsset?.itemSpritesheetImageObjectKey?.trim() ||
|
||||
'',
|
||||
)
|
||||
.find(Boolean) ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMatch3DContainerPrompt(
|
||||
profile: Match3DWorkProfile,
|
||||
draft: Match3DResultDraft | null,
|
||||
@@ -589,6 +645,12 @@ function hasPersistableMatch3DGeneratedItemAsset(
|
||||
asset.subscriptionKey?.trim() ||
|
||||
asset.backgroundAsset?.imageSrc?.trim() ||
|
||||
asset.backgroundAsset?.imageObjectKey?.trim() ||
|
||||
asset.backgroundAsset?.levelSceneImageSrc?.trim() ||
|
||||
asset.backgroundAsset?.levelSceneImageObjectKey?.trim() ||
|
||||
asset.backgroundAsset?.uiSpritesheetImageSrc?.trim() ||
|
||||
asset.backgroundAsset?.uiSpritesheetImageObjectKey?.trim() ||
|
||||
asset.backgroundAsset?.itemSpritesheetImageSrc?.trim() ||
|
||||
asset.backgroundAsset?.itemSpritesheetImageObjectKey?.trim() ||
|
||||
asset.backgroundAsset?.containerImageSrc?.trim() ||
|
||||
asset.backgroundAsset?.containerImageObjectKey?.trim() ||
|
||||
asset.backgroundAsset?.prompt?.trim() ||
|
||||
@@ -627,8 +689,17 @@ function getMatch3DGeneratedItemAssetPersistenceSignature(
|
||||
asset.backgroundMusic?.taskId?.trim() ??
|
||||
'',
|
||||
asset.backgroundAsset?.prompt?.trim() ?? '',
|
||||
asset.backgroundAsset?.levelScenePrompt?.trim() ?? '',
|
||||
asset.backgroundAsset?.levelSceneImageSrc?.trim() ?? '',
|
||||
asset.backgroundAsset?.levelSceneImageObjectKey?.trim() ?? '',
|
||||
asset.backgroundAsset?.imageSrc?.trim() ?? '',
|
||||
asset.backgroundAsset?.imageObjectKey?.trim() ?? '',
|
||||
asset.backgroundAsset?.uiSpritesheetPrompt?.trim() ?? '',
|
||||
asset.backgroundAsset?.uiSpritesheetImageSrc?.trim() ?? '',
|
||||
asset.backgroundAsset?.uiSpritesheetImageObjectKey?.trim() ?? '',
|
||||
asset.backgroundAsset?.itemSpritesheetPrompt?.trim() ?? '',
|
||||
asset.backgroundAsset?.itemSpritesheetImageSrc?.trim() ?? '',
|
||||
asset.backgroundAsset?.itemSpritesheetImageObjectKey?.trim() ?? '',
|
||||
asset.backgroundAsset?.containerPrompt?.trim() ?? '',
|
||||
asset.backgroundAsset?.containerImageSrc?.trim() ?? '',
|
||||
asset.backgroundAsset?.containerImageObjectKey?.trim() ?? '',
|
||||
@@ -770,9 +841,22 @@ function createMatch3DAssetDrafts(
|
||||
name: `${theme}场景小物`,
|
||||
usage: '圆形空间周边装饰物',
|
||||
},
|
||||
].slice(0, MATCH3D_DEFAULT_ASSET_COUNT);
|
||||
];
|
||||
const fallbackSeeds = Array.from(
|
||||
{ length: MATCH3D_DEFAULT_ASSET_COUNT },
|
||||
(_, index) => {
|
||||
const seed = seeds[index];
|
||||
return (
|
||||
seed ?? {
|
||||
id: `generated-item-${index + 1}`,
|
||||
name: `${theme}物品${index + 1}`,
|
||||
usage: '局内点击消除物件',
|
||||
}
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return seeds.map((seed) => ({
|
||||
return fallbackSeeds.map((seed) => ({
|
||||
...seed,
|
||||
prompt: buildMatch3DAssetPrompt(profile, seed.name, seed.usage),
|
||||
referenceImageSrc: profile.referenceImageSrc ?? profile.coverImageSrc ?? '',
|
||||
@@ -2675,7 +2759,7 @@ function Match3DAssetConfigTabs({
|
||||
onChange: (tab: Match3DAssetConfigTab) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-3 grid grid-cols-3 gap-2 rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-1">
|
||||
<div className="mb-3 grid grid-cols-2 gap-2 rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-1">
|
||||
{MATCH3D_ASSET_CONFIG_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
@@ -2697,39 +2781,62 @@ function Match3DAssetConfigTabs({
|
||||
|
||||
function Match3DUIAssetsTab({
|
||||
backgroundPreviewSrc,
|
||||
containerPreviewSrc,
|
||||
backgroundPrompt,
|
||||
busy,
|
||||
isGenerating,
|
||||
uiSpritesheetPreviewSrc,
|
||||
itemSpritesheetPreviewSrc,
|
||||
itemNames,
|
||||
error,
|
||||
progressRuntime,
|
||||
onGenerate,
|
||||
}: {
|
||||
backgroundPreviewSrc: string;
|
||||
containerPreviewSrc: string;
|
||||
backgroundPrompt: string;
|
||||
busy: boolean;
|
||||
isGenerating: boolean;
|
||||
uiSpritesheetPreviewSrc: string;
|
||||
itemSpritesheetPreviewSrc: string;
|
||||
itemNames: readonly string[];
|
||||
error: string | null;
|
||||
progressRuntime: Match3DTimedGenerationProgress | null;
|
||||
onGenerate: (prompt: string) => void;
|
||||
}) {
|
||||
const [prompt, setPrompt] = useState(backgroundPrompt);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const [isCostConfirmOpen, setIsCostConfirmOpen] = useState(false);
|
||||
const [itemSpritesheetGroups, setItemSpritesheetGroups] = useState<
|
||||
Match3DItemSpritesheetPreviewGroup[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setPrompt(backgroundPrompt);
|
||||
}, [backgroundPrompt]);
|
||||
if (!itemSpritesheetPreviewSrc) {
|
||||
setItemSpritesheetGroups((current) =>
|
||||
current.length > 0 ? [] : current,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalizedPrompt = prompt.trim();
|
||||
const generationProgress =
|
||||
resolveMatch3DTimedGenerationProgress(progressRuntime);
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
void loadMatch3DSpritesheetAssetRegions({
|
||||
source: itemSpritesheetPreviewSrc,
|
||||
maxRegions: 100,
|
||||
minArea: 16,
|
||||
alphaThreshold: 8,
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((regions) => {
|
||||
if (!cancelled) {
|
||||
setItemSpritesheetGroups(
|
||||
buildMatch3DItemSpritesheetViewRegions(regions, itemNames),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setItemSpritesheetGroups([]);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [itemNames, itemSpritesheetPreviewSrc]);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<section className="platform-subpanel rounded-[1.35rem] p-4 sm:p-5">
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(14rem,0.72fr)_minmax(0,1fr)]">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPreviewOpen(true)}
|
||||
@@ -2744,66 +2851,67 @@ function Match3DUIAssetsTab({
|
||||
<span className="sr-only">打开UI页面预览</span>
|
||||
</button>
|
||||
|
||||
<div className="flex min-h-0 flex-col">
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
画面描述提示词
|
||||
</span>
|
||||
<textarea
|
||||
value={prompt}
|
||||
disabled={busy || isGenerating}
|
||||
rows={7}
|
||||
onChange={(event) => setPrompt(event.target.value)}
|
||||
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="UI背景图画面描述提示词"
|
||||
<div className="flex min-h-0 flex-col gap-3">
|
||||
<div className="aspect-square overflow-hidden rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/62 p-3 shadow-sm">
|
||||
<ResolvedAssetImage
|
||||
src={uiSpritesheetPreviewSrc}
|
||||
alt="UI素材图"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</label>
|
||||
<div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPreviewOpen(true)}
|
||||
className="platform-button platform-button--ghost min-h-11 justify-center gap-2 px-4 py-3"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
预览UI页面
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!normalizedPrompt || busy || isGenerating}
|
||||
onClick={() => setIsCostConfirmOpen(true)}
|
||||
className={`platform-button platform-button--primary min-h-11 justify-center gap-2 px-4 py-3 ${!normalizedPrompt || busy || isGenerating ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Wand2 className="h-4 w-4" />
|
||||
)}
|
||||
重新生成 · {MATCH3D_UI_BACKGROUND_POINTS_COST}泥点
|
||||
</button>
|
||||
</div>
|
||||
{isGenerating ? (
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-label="UI背景图生成进度"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={generationProgress.progressPercent}
|
||||
className="platform-progress-track relative mt-3 h-10 overflow-hidden rounded-full"
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-full bg-amber-600 transition-[width] duration-300"
|
||||
style={{ width: `${generationProgress.progressPercent}%` }}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center gap-2 px-4 text-xs font-bold text-white">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
预计剩余 {generationProgress.secondsLeft} 秒
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPreviewOpen(true)}
|
||||
className="platform-button platform-button--ghost min-h-11 justify-center gap-2 px-4 py-3"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
预览UI页面
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{itemSpritesheetPreviewSrc ? (
|
||||
<section className="platform-subpanel rounded-[1.35rem] p-4 sm:p-5">
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,18rem)_minmax(0,1fr)]">
|
||||
<div className="aspect-square overflow-hidden rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/62 p-3 shadow-sm">
|
||||
<ResolvedAssetImage
|
||||
src={itemSpritesheetPreviewSrc}
|
||||
alt="物品素材图"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
{itemSpritesheetGroups.length > 0 ? (
|
||||
<div className="grid max-h-[24rem] content-start gap-3 overflow-y-auto pr-1 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{itemSpritesheetGroups.map((group) => (
|
||||
<div
|
||||
key={`${group.itemIndex}-${group.itemName}`}
|
||||
className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-3"
|
||||
>
|
||||
<div className="mb-2 truncate text-xs font-black text-[var(--platform-text-strong)]">
|
||||
{group.itemName}
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-1.5">
|
||||
{group.regions.map((region, regionIndex) => (
|
||||
<img
|
||||
key={`${group.itemIndex}-${regionIndex}-${region.imageSrc}`}
|
||||
src={region.imageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
data-testid={`match3d-item-spritesheet-preview-${group.itemIndex}-${regionIndex}`}
|
||||
className="aspect-square w-full rounded-[0.55rem] border border-white/70 bg-white/82 object-contain p-1"
|
||||
draggable={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="platform-banner platform-banner--danger rounded-2xl text-sm leading-6">
|
||||
{error}
|
||||
@@ -2813,217 +2921,10 @@ function Match3DUIAssetsTab({
|
||||
{isPreviewOpen ? (
|
||||
<Match3DUIRuntimePreviewPanel
|
||||
backgroundPreviewSrc={backgroundPreviewSrc}
|
||||
containerPreviewSrc={containerPreviewSrc}
|
||||
containerPreviewSrc={MATCH3D_CONTAINER_REFERENCE_PREVIEW_SRC}
|
||||
onClose={() => setIsPreviewOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{isCostConfirmOpen ? (
|
||||
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="match3d-ui-point-cost-confirm-title"
|
||||
className="platform-modal-shell platform-remap-surface w-full max-w-xs rounded-[1.35rem] p-5 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
|
||||
>
|
||||
<div
|
||||
id="match3d-ui-point-cost-confirm-title"
|
||||
className="text-base font-black text-[var(--platform-text-strong)]"
|
||||
>
|
||||
确认消耗泥点
|
||||
</div>
|
||||
<div className="mt-2 text-sm font-semibold leading-6 text-[var(--platform-text-base)]">
|
||||
消耗 {MATCH3D_UI_BACKGROUND_POINTS_COST} 泥点
|
||||
</div>
|
||||
<div className="mt-5 grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCostConfirmOpen(false)}
|
||||
className="platform-button platform-button--secondary justify-center"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!normalizedPrompt || busy || isGenerating}
|
||||
onClick={() => {
|
||||
setIsCostConfirmOpen(false);
|
||||
onGenerate(normalizedPrompt);
|
||||
}}
|
||||
className={`platform-button platform-button--primary justify-center ${!normalizedPrompt || busy || isGenerating ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Match3DContainerAssetsTab({
|
||||
backgroundPreviewSrc,
|
||||
containerPreviewSrc,
|
||||
containerPrompt,
|
||||
busy,
|
||||
isGenerating,
|
||||
error,
|
||||
progressRuntime,
|
||||
onGenerate,
|
||||
}: {
|
||||
backgroundPreviewSrc: string;
|
||||
containerPreviewSrc: string;
|
||||
containerPrompt: string;
|
||||
busy: boolean;
|
||||
isGenerating: boolean;
|
||||
error: string | null;
|
||||
progressRuntime: Match3DTimedGenerationProgress | null;
|
||||
onGenerate: (prompt: string) => void;
|
||||
}) {
|
||||
const [prompt, setPrompt] = useState(containerPrompt);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const [isCostConfirmOpen, setIsCostConfirmOpen] = useState(false);
|
||||
const hasContainerPreview = Boolean(containerPreviewSrc.trim());
|
||||
|
||||
useEffect(() => {
|
||||
setPrompt(containerPrompt);
|
||||
}, [containerPrompt]);
|
||||
|
||||
const normalizedPrompt = prompt.trim();
|
||||
const generationProgress =
|
||||
resolveMatch3DTimedGenerationProgress(progressRuntime);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<section className="platform-subpanel rounded-[1.35rem] p-4 sm:p-5">
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(14rem,0.72fr)_minmax(0,1fr)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPreviewOpen(true)}
|
||||
className="mx-auto aspect-square w-full max-w-[18rem] overflow-hidden rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/62 p-3 text-left shadow-sm"
|
||||
aria-label="打开容器形象预览"
|
||||
>
|
||||
<ResolvedAssetImage
|
||||
src={containerPreviewSrc}
|
||||
alt="容器形象"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
<span className="sr-only">打开容器形象预览</span>
|
||||
</button>
|
||||
|
||||
<div className="flex min-h-0 flex-col">
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
容器形象提示词
|
||||
</span>
|
||||
<textarea
|
||||
value={prompt}
|
||||
disabled={busy || isGenerating}
|
||||
rows={7}
|
||||
onChange={(event) => setPrompt(event.target.value)}
|
||||
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="容器形象画面描述提示词"
|
||||
/>
|
||||
</label>
|
||||
<div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPreviewOpen(true)}
|
||||
className="platform-button platform-button--ghost min-h-11 justify-center gap-2 px-4 py-3"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
预览容器形象
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!normalizedPrompt || busy || isGenerating}
|
||||
onClick={() => setIsCostConfirmOpen(true)}
|
||||
className={`platform-button platform-button--primary min-h-11 justify-center gap-2 px-4 py-3 ${!normalizedPrompt || busy || isGenerating ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Wand2 className="h-4 w-4" />
|
||||
)}
|
||||
重新生成 · {MATCH3D_UI_BACKGROUND_POINTS_COST}泥点
|
||||
</button>
|
||||
</div>
|
||||
{isGenerating ? (
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-label="容器形象生成进度"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={generationProgress.progressPercent}
|
||||
className="platform-progress-track relative mt-3 h-10 overflow-hidden rounded-full"
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-full bg-amber-600 transition-[width] duration-300"
|
||||
style={{ width: `${generationProgress.progressPercent}%` }}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center gap-2 px-4 text-xs font-bold text-white">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
预计剩余 {generationProgress.secondsLeft} 秒
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{error ? (
|
||||
<div className="platform-banner platform-banner--danger rounded-2xl text-sm leading-6">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isPreviewOpen ? (
|
||||
<Match3DUIRuntimePreviewPanel
|
||||
backgroundPreviewSrc={backgroundPreviewSrc}
|
||||
containerPreviewSrc={hasContainerPreview ? containerPreviewSrc : ''}
|
||||
onClose={() => setIsPreviewOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{isCostConfirmOpen ? (
|
||||
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="match3d-container-point-cost-confirm-title"
|
||||
className="platform-modal-shell platform-remap-surface w-full max-w-xs rounded-[1.35rem] p-5 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
|
||||
>
|
||||
<div
|
||||
id="match3d-container-point-cost-confirm-title"
|
||||
className="text-base font-black text-[var(--platform-text-strong)]"
|
||||
>
|
||||
确认消耗泥点
|
||||
</div>
|
||||
<div className="mt-2 text-sm font-semibold leading-6 text-[var(--platform-text-base)]">
|
||||
消耗 {MATCH3D_UI_BACKGROUND_POINTS_COST} 泥点
|
||||
</div>
|
||||
<div className="mt-5 grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCostConfirmOpen(false)}
|
||||
className="platform-button platform-button--secondary justify-center"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!normalizedPrompt || busy || isGenerating}
|
||||
onClick={() => {
|
||||
setIsCostConfirmOpen(false);
|
||||
onGenerate(normalizedPrompt);
|
||||
}}
|
||||
className={`platform-button platform-button--primary justify-center ${!normalizedPrompt || busy || isGenerating ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3115,49 +3016,33 @@ function Match3DAssetConfigTab({
|
||||
activeAssetId,
|
||||
assetDrafts,
|
||||
backgroundPreviewSrc,
|
||||
containerPreviewSrc,
|
||||
backgroundPrompt,
|
||||
containerPrompt,
|
||||
uiSpritesheetPreviewSrc,
|
||||
itemSpritesheetPreviewSrc,
|
||||
itemNames,
|
||||
backgroundGenerationError,
|
||||
containerGenerationError,
|
||||
batchGenerationState,
|
||||
busy,
|
||||
backgroundGenerationProgress,
|
||||
containerGenerationProgress,
|
||||
isGeneratingBackground,
|
||||
isGeneratingContainer,
|
||||
onActiveAssetChange,
|
||||
onAddBatch,
|
||||
onRegenerateBatch,
|
||||
onAssetChange,
|
||||
onAssetConfigTabChange,
|
||||
onDeleteAsset,
|
||||
onGenerateBackground,
|
||||
onGenerateContainer,
|
||||
}: {
|
||||
activeAssetConfigTab: Match3DAssetConfigTab;
|
||||
activeAssetId: string | null;
|
||||
assetDrafts: Match3DItemAssetDraft[];
|
||||
backgroundPreviewSrc: string;
|
||||
containerPreviewSrc: string;
|
||||
backgroundPrompt: string;
|
||||
containerPrompt: string;
|
||||
uiSpritesheetPreviewSrc: string;
|
||||
itemSpritesheetPreviewSrc: string;
|
||||
itemNames: readonly string[];
|
||||
backgroundGenerationError: string | null;
|
||||
containerGenerationError: string | null;
|
||||
batchGenerationState: Match3DBatchItemGenerationState;
|
||||
busy: boolean;
|
||||
backgroundGenerationProgress: Match3DTimedGenerationProgress | null;
|
||||
containerGenerationProgress: Match3DTimedGenerationProgress | null;
|
||||
isGeneratingBackground: boolean;
|
||||
isGeneratingContainer: boolean;
|
||||
onActiveAssetChange: (assetId: string | null) => void;
|
||||
onAddBatch: () => void;
|
||||
onRegenerateBatch: () => void;
|
||||
onAssetChange: (asset: Match3DItemAssetDraft) => void;
|
||||
onAssetConfigTabChange: (tab: Match3DAssetConfigTab) => void;
|
||||
onDeleteAsset: (assetId: string) => void;
|
||||
onGenerateBackground: (prompt: string) => void;
|
||||
onGenerateContainer: (prompt: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-0">
|
||||
@@ -3180,25 +3065,10 @@ function Match3DAssetConfigTab({
|
||||
{activeAssetConfigTab === 'ui' ? (
|
||||
<Match3DUIAssetsTab
|
||||
backgroundPreviewSrc={backgroundPreviewSrc}
|
||||
containerPreviewSrc={containerPreviewSrc}
|
||||
backgroundPrompt={backgroundPrompt}
|
||||
busy={busy}
|
||||
isGenerating={isGeneratingBackground}
|
||||
uiSpritesheetPreviewSrc={uiSpritesheetPreviewSrc}
|
||||
itemSpritesheetPreviewSrc={itemSpritesheetPreviewSrc}
|
||||
itemNames={itemNames}
|
||||
error={backgroundGenerationError}
|
||||
progressRuntime={backgroundGenerationProgress}
|
||||
onGenerate={onGenerateBackground}
|
||||
/>
|
||||
) : null}
|
||||
{activeAssetConfigTab === 'container' ? (
|
||||
<Match3DContainerAssetsTab
|
||||
backgroundPreviewSrc={backgroundPreviewSrc}
|
||||
containerPreviewSrc={containerPreviewSrc}
|
||||
containerPrompt={containerPrompt}
|
||||
busy={busy}
|
||||
isGenerating={isGeneratingContainer}
|
||||
error={containerGenerationError}
|
||||
progressRuntime={containerGenerationProgress}
|
||||
onGenerate={onGenerateContainer}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -3255,18 +3125,9 @@ export function Match3DResultView({
|
||||
const [batchRegenerateError, setBatchRegenerateError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [isGeneratingBackground, setIsGeneratingBackground] = useState(false);
|
||||
const [backgroundGenerationError, setBackgroundGenerationError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [backgroundGenerationProgress, setBackgroundGenerationProgress] =
|
||||
useState<Match3DTimedGenerationProgress | null>(null);
|
||||
const [isGeneratingContainer, setIsGeneratingContainer] = useState(false);
|
||||
const [containerGenerationError, setContainerGenerationError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [containerGenerationProgress, setContainerGenerationProgress] =
|
||||
useState<Match3DTimedGenerationProgress | null>(null);
|
||||
const [autoSaveState, setAutoSaveState] =
|
||||
useState<Match3DAutoSaveState>('idle');
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
@@ -3299,24 +3160,6 @@ export function Match3DResultView({
|
||||
),
|
||||
[draft, generatedItemAssets, promotedProfile],
|
||||
);
|
||||
const backgroundPrompt = useMemo(
|
||||
() =>
|
||||
resolveMatch3DBackgroundPrompt(
|
||||
promotedProfile,
|
||||
draft,
|
||||
generatedItemAssets,
|
||||
),
|
||||
[draft, generatedItemAssets, promotedProfile],
|
||||
);
|
||||
const containerPrompt = useMemo(
|
||||
() =>
|
||||
resolveMatch3DContainerPrompt(
|
||||
promotedProfile,
|
||||
draft,
|
||||
generatedItemAssets,
|
||||
),
|
||||
[draft, generatedItemAssets, promotedProfile],
|
||||
);
|
||||
const containerPreviewSrc = useMemo(
|
||||
() =>
|
||||
resolveMatch3DContainerPreviewSource(
|
||||
@@ -3327,6 +3170,28 @@ export function Match3DResultView({
|
||||
MATCH3D_CONTAINER_REFERENCE_PREVIEW_SRC,
|
||||
[draft, generatedItemAssets, promotedProfile],
|
||||
);
|
||||
const uiSpritesheetPreviewSrc = useMemo(
|
||||
() =>
|
||||
resolveMatch3DUiSpritesheetPreviewSource(
|
||||
promotedProfile,
|
||||
draft,
|
||||
generatedItemAssets,
|
||||
),
|
||||
[draft, generatedItemAssets, promotedProfile],
|
||||
);
|
||||
const itemSpritesheetPreviewSrc = useMemo(
|
||||
() =>
|
||||
resolveMatch3DItemSpritesheetPreviewSource(
|
||||
promotedProfile,
|
||||
draft,
|
||||
generatedItemAssets,
|
||||
),
|
||||
[draft, generatedItemAssets, promotedProfile],
|
||||
);
|
||||
const generatedItemNames = useMemo(
|
||||
() => generatedItemAssets.map((asset) => asset.itemName),
|
||||
[generatedItemAssets],
|
||||
);
|
||||
const coverSourceAssets = useMemo(
|
||||
() =>
|
||||
resolveMatch3DCoverSourceAssets(
|
||||
@@ -3349,11 +3214,6 @@ export function Match3DResultView({
|
||||
setCoverAiRedraw(false);
|
||||
setCoverPanelError(null);
|
||||
setBackgroundGenerationError(null);
|
||||
setIsGeneratingBackground(false);
|
||||
setBackgroundGenerationProgress(null);
|
||||
setContainerGenerationError(null);
|
||||
setIsGeneratingContainer(false);
|
||||
setContainerGenerationProgress(null);
|
||||
// 中文注释:表单草稿只在切换作品或服务端更新时间变化时重置,避免父级重渲染打断本地编辑。
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [profile.profileId, profile.updatedAt]);
|
||||
@@ -3369,56 +3229,6 @@ export function Match3DResultView({
|
||||
profile.profileId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGeneratingBackground) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startedAtMs = Date.now();
|
||||
setBackgroundGenerationProgress({
|
||||
startedAtMs,
|
||||
nowMs: startedAtMs,
|
||||
estimateSeconds: MATCH3D_UI_BACKGROUND_GENERATION_ESTIMATE_SECONDS,
|
||||
});
|
||||
const timer = window.setInterval(() => {
|
||||
setBackgroundGenerationProgress((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
nowMs: Date.now(),
|
||||
}
|
||||
: current,
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [isGeneratingBackground]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGeneratingContainer) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startedAtMs = Date.now();
|
||||
setContainerGenerationProgress({
|
||||
startedAtMs,
|
||||
nowMs: startedAtMs,
|
||||
estimateSeconds: MATCH3D_UI_BACKGROUND_GENERATION_ESTIMATE_SECONDS,
|
||||
});
|
||||
const timer = window.setInterval(() => {
|
||||
setContainerGenerationProgress((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
nowMs: Date.now(),
|
||||
}
|
||||
: current,
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [isGeneratingContainer]);
|
||||
|
||||
useEffect(() => {
|
||||
const payload = buildSavePayload(editState);
|
||||
if (!payload) {
|
||||
@@ -3461,12 +3271,6 @@ export function Match3DResultView({
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (isGeneratingBackground) {
|
||||
return;
|
||||
}
|
||||
if (isGeneratingContainer) {
|
||||
return;
|
||||
}
|
||||
setAutoSaveState('error');
|
||||
setLocalError(
|
||||
saveError instanceof Error ? saveError.message : '自动保存失败。',
|
||||
@@ -3481,8 +3285,6 @@ export function Match3DResultView({
|
||||
}, [
|
||||
editState,
|
||||
generatedItemAssets,
|
||||
isGeneratingBackground,
|
||||
isGeneratingContainer,
|
||||
onSaved,
|
||||
profile,
|
||||
]);
|
||||
@@ -3906,92 +3708,6 @@ export function Match3DResultView({
|
||||
});
|
||||
};
|
||||
|
||||
const handleGenerateBackground = async (prompt: string) => {
|
||||
const normalizedPrompt = prompt.trim();
|
||||
if (!normalizedPrompt || isGeneratingBackground) {
|
||||
setBackgroundGenerationError('请填写画面描述提示词。');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGeneratingBackground(true);
|
||||
setBackgroundGenerationProgress({
|
||||
startedAtMs: Date.now(),
|
||||
nowMs: Date.now(),
|
||||
estimateSeconds: MATCH3D_UI_BACKGROUND_GENERATION_ESTIMATE_SECONDS,
|
||||
});
|
||||
setBackgroundGenerationError(null);
|
||||
try {
|
||||
const response = await generateMatch3DBackgroundImage(profile.profileId, {
|
||||
prompt: normalizedPrompt,
|
||||
});
|
||||
const nextGeneratedAssets = attachMatch3DGeneratedBackgroundAsset(
|
||||
response.item.generatedItemAssets?.length
|
||||
? response.item.generatedItemAssets
|
||||
: generatedItemAssets,
|
||||
response.generatedBackgroundAsset,
|
||||
);
|
||||
const refreshedProfile = attachMatch3DGeneratedItemAssets(
|
||||
response.item,
|
||||
nextGeneratedAssets,
|
||||
);
|
||||
setAssetDrafts(createMatch3DAssetDrafts(refreshedProfile, null));
|
||||
onSaved?.(refreshedProfile);
|
||||
setLocalError(null);
|
||||
} catch (caughtError) {
|
||||
setBackgroundGenerationError(
|
||||
caughtError instanceof Error
|
||||
? caughtError.message
|
||||
: 'UI背景图生成失败。',
|
||||
);
|
||||
} finally {
|
||||
setIsGeneratingBackground(false);
|
||||
setBackgroundGenerationProgress(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateContainer = async (prompt: string) => {
|
||||
const normalizedPrompt = prompt.trim();
|
||||
if (!normalizedPrompt || isGeneratingContainer) {
|
||||
setContainerGenerationError('请填写容器形象提示词。');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGeneratingContainer(true);
|
||||
setContainerGenerationProgress({
|
||||
startedAtMs: Date.now(),
|
||||
nowMs: Date.now(),
|
||||
estimateSeconds: MATCH3D_UI_BACKGROUND_GENERATION_ESTIMATE_SECONDS,
|
||||
});
|
||||
setContainerGenerationError(null);
|
||||
try {
|
||||
const response = await generateMatch3DContainerImage(profile.profileId, {
|
||||
prompt: normalizedPrompt,
|
||||
});
|
||||
const nextGeneratedAssets = attachMatch3DGeneratedBackgroundAsset(
|
||||
response.item.generatedItemAssets?.length
|
||||
? response.item.generatedItemAssets
|
||||
: generatedItemAssets,
|
||||
response.generatedBackgroundAsset,
|
||||
);
|
||||
const refreshedProfile = attachMatch3DGeneratedItemAssets(
|
||||
response.item,
|
||||
nextGeneratedAssets,
|
||||
);
|
||||
setAssetDrafts(createMatch3DAssetDrafts(refreshedProfile, null));
|
||||
onSaved?.(refreshedProfile);
|
||||
setLocalError(null);
|
||||
} catch (caughtError) {
|
||||
setContainerGenerationError(
|
||||
caughtError instanceof Error
|
||||
? caughtError.message
|
||||
: '容器形象生成失败。',
|
||||
);
|
||||
} finally {
|
||||
setIsGeneratingContainer(false);
|
||||
setContainerGenerationProgress(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartTestRun = async () => {
|
||||
if (!canStartTestRun || isStartingTestRun) {
|
||||
setLocalError(testRunBlockers[0] ?? null);
|
||||
@@ -4063,9 +3779,7 @@ export function Match3DResultView({
|
||||
isBusy ||
|
||||
isPublishing ||
|
||||
isStartingTestRun ||
|
||||
isGeneratingCover ||
|
||||
isGeneratingBackground ||
|
||||
isGeneratingContainer;
|
||||
isGeneratingCover;
|
||||
const workBusy = busy || isGeneratingTags;
|
||||
const displayError = error ?? localError;
|
||||
const dialogPublishError = hasAttemptedPublish ? error ?? localError : null;
|
||||
@@ -4104,17 +3818,11 @@ export function Match3DResultView({
|
||||
activeAssetId={activeAssetId}
|
||||
assetDrafts={assetDrafts}
|
||||
backgroundPreviewSrc={backgroundPreviewSrc}
|
||||
containerPreviewSrc={containerPreviewSrc}
|
||||
backgroundPrompt={backgroundPrompt}
|
||||
containerPrompt={containerPrompt}
|
||||
uiSpritesheetPreviewSrc={uiSpritesheetPreviewSrc}
|
||||
itemSpritesheetPreviewSrc={itemSpritesheetPreviewSrc}
|
||||
itemNames={generatedItemNames}
|
||||
backgroundGenerationError={backgroundGenerationError}
|
||||
containerGenerationError={containerGenerationError}
|
||||
batchGenerationState={batchGenerationState}
|
||||
busy={busy}
|
||||
backgroundGenerationProgress={backgroundGenerationProgress}
|
||||
containerGenerationProgress={containerGenerationProgress}
|
||||
isGeneratingBackground={isGeneratingBackground}
|
||||
isGeneratingContainer={isGeneratingContainer}
|
||||
onActiveAssetChange={setActiveAssetId}
|
||||
onAddBatch={() => {
|
||||
setBatchAddError(null);
|
||||
@@ -4137,12 +3845,6 @@ export function Match3DResultView({
|
||||
onDeleteAsset={(assetId) => {
|
||||
void handleDeleteAssetDraft(assetId);
|
||||
}}
|
||||
onGenerateBackground={(prompt) => {
|
||||
void handleGenerateBackground(prompt);
|
||||
}}
|
||||
onGenerateContainer={(prompt) => {
|
||||
void handleGenerateContainer(prompt);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user