合并 master 的画布资源命名更新
合并 origin/master 的资源命名、图集拆分告警与相关后端契约更新 保留编辑 Agent 完成任务后的任务列表和画布刷新流程 保留编辑生成结果的轻量化持久化处理
This commit is contained in:
@@ -58,6 +58,7 @@ function BasicGenerationHarness({
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
<output aria-label="当前提示词">{dialog.prompt}</output>
|
||||
<output aria-label="当前资源名称">{dialog.assetLabel ?? '-'}</output>
|
||||
<output aria-label="当前比例">{dialog.aspectRatio}</output>
|
||||
<output aria-label="当前尺寸">{dialog.imageSize}</output>
|
||||
<output aria-label="当前模型">{dialog.imageModel}</output>
|
||||
@@ -86,17 +87,27 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '新的提示' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('资源名称'), {
|
||||
target: { value: '自定义主视觉' },
|
||||
});
|
||||
const panel = screen.getByRole('dialog', { name: '生成图片' });
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '添加参考图' }));
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
expect(screen.getByLabelText('当前提示词').textContent).toBe('新的提示');
|
||||
expect(screen.getByLabelText('当前资源名称').textContent).toBe(
|
||||
'自定义主视觉',
|
||||
);
|
||||
expect(screen.getByLabelText('当前状态').textContent).toBe('idle');
|
||||
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
|
||||
expect(requestUpload).toHaveBeenCalledWith('generation-reference');
|
||||
expect(submitGeneration).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ prompt: '新的提示', status: 'idle' }),
|
||||
expect.objectContaining({
|
||||
prompt: '新的提示',
|
||||
assetLabel: '自定义主视觉',
|
||||
status: 'idle',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -183,7 +194,7 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
|
||||
expect(submitButton.textContent).toBe('生成5泥点');
|
||||
});
|
||||
|
||||
it('keeps quick edit to one prompt box and model selection', () => {
|
||||
it('keeps quick edit to one prompt box with image parameters', () => {
|
||||
render(
|
||||
<BasicGenerationHarness
|
||||
initialDialog={createDialog({
|
||||
@@ -213,10 +224,10 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
|
||||
).toBeNull();
|
||||
expect(within(panel).queryByText('旧参考图')).toBeNull();
|
||||
expect(
|
||||
within(panel).queryByRole('button', {
|
||||
name: /快速编辑图片尺寸/u,
|
||||
within(panel).getByRole('button', {
|
||||
name: '快速编辑图片尺寸 1:1·1K',
|
||||
}),
|
||||
).toBeNull();
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByRole('button', {
|
||||
name: '快速编辑图片模型 gpt-image-2',
|
||||
@@ -234,22 +245,16 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
|
||||
name: '生成图片尺寸 16:9·1K',
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('menu', { name: '生成图片尺寸选项' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('menu', { name: '生成图片尺寸选项' })).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole('textbox', { name: '生成提示词' }));
|
||||
|
||||
expect(
|
||||
screen.queryByRole('menu', { name: '生成图片尺寸选项' }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('menu', { name: '生成图片尺寸选项' })).toBeNull();
|
||||
});
|
||||
|
||||
it('does not render a standalone close button', () => {
|
||||
render(<BasicGenerationHarness />);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '关闭生成图片' }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '关闭生成图片' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
GenerateDialogState,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasGenerationAssetNameField } from './ImageCanvasGenerationAssetNameField';
|
||||
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
||||
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
||||
import { calculateEditorImageGenerationPrice } from './ImageCanvasGenerationModel';
|
||||
@@ -140,19 +141,21 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
const references = dialog.generationReferences ?? [];
|
||||
const isQuickEdit = dialog.mode === 'quick-edit';
|
||||
const resolvedDialogLabel =
|
||||
dialogLabel ?? optionLabelPrefix ?? (isQuickEdit ? '快速编辑图片' : '生成图片');
|
||||
dialogLabel ??
|
||||
optionLabelPrefix ??
|
||||
(isQuickEdit ? '快速编辑图片' : '生成图片');
|
||||
const resolvedPromptLabel =
|
||||
promptLabel ?? (isQuickEdit ? '快速编辑提示词' : '生成提示词');
|
||||
const resolvedPromptPlaceholder =
|
||||
promptPlaceholder ??
|
||||
(isQuickEdit ? '写下每个编号要怎么改' : '今天想生成什么画面?');
|
||||
(isQuickEdit ? '你希望素材如何修改?' : '今天想生成什么画面?');
|
||||
const resolvedOptionLabelPrefix =
|
||||
optionLabelPrefix ?? (isQuickEdit ? '快速编辑图片' : '生成图片');
|
||||
const resolvedReferenceButtonLabel = referenceButtonLabel;
|
||||
const resolvedReferenceButtonAriaLabel =
|
||||
referenceButtonAriaLabel ?? `添加${resolvedReferenceButtonLabel}`;
|
||||
const shouldIncludeReferences = isQuickEdit ? false : includeReferences;
|
||||
const shouldIncludeDimensions = isQuickEdit ? false : includeDimensions;
|
||||
const shouldIncludeDimensions = includeDimensions;
|
||||
const resolvedSubmitLabel =
|
||||
isQuickEdit && submitLabel === '生成' ? '修改' : submitLabel;
|
||||
const resolvedSubmitAriaLabel =
|
||||
@@ -265,6 +268,20 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value={dialog.assetLabel}
|
||||
disabled={dialog.status === 'generating'}
|
||||
onChange={(assetLabel) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
assetLabel,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className={finalFooterClassName}>
|
||||
<ImageCanvasGenerationImageOptionsView
|
||||
dialog={dialog}
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
CanvasLayer,
|
||||
CharacterAnimationPanelState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasGenerationAssetNameField } from './ImageCanvasGenerationAssetNameField';
|
||||
import {
|
||||
CHARACTER_ANIMATION_ACTION_PROMPTS,
|
||||
CHARACTER_ANIMATION_DURATION_OPTIONS,
|
||||
@@ -211,6 +212,11 @@ export function ImageCanvasCharacterAnimationPanelView({
|
||||
updatePanel({ promptText: event.target.value.slice(0, 4000) })
|
||||
}
|
||||
/>
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value={panel.assetLabel}
|
||||
disabled={isGenerating}
|
||||
onChange={(assetLabel) => updatePanel({ assetLabel })}
|
||||
/>
|
||||
<div className="image-canvas-editor__character-animation-presets">
|
||||
{CHARACTER_ANIMATION_ACTION_PROMPTS.map((preset) => (
|
||||
<button
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
SpecGenerationType,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasGenerationAssetNameField } from './ImageCanvasGenerationAssetNameField';
|
||||
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
||||
import { calculateEditorImageGenerationPrice } from './ImageCanvasGenerationModel';
|
||||
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
|
||||
@@ -275,6 +276,20 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value={dialog.assetLabel}
|
||||
disabled={dialog.status === 'generating'}
|
||||
onChange={(assetLabel) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'character'
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
assetLabel,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
|
||||
@@ -265,4 +265,30 @@ describe('ImageCanvasContextMenusView', () => {
|
||||
expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(2);
|
||||
expect(props.onDeleteLayerById).toHaveBeenCalledWith(layer.id);
|
||||
});
|
||||
|
||||
it.each(['icon', 'icon-spritesheet'] as const)(
|
||||
'hides quick edit from the standalone menu for %s assets',
|
||||
(assetKind) => {
|
||||
const layer = createLayer({ assetKind });
|
||||
renderContextMenus({
|
||||
imageContextMenu: { layerId: layer.id, x: 20, y: 22 },
|
||||
imageContextMenuLayer: layer,
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('menuitem', { name: '快速编辑' })).toBeNull();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: '查看图片信息' }),
|
||||
).toBeTruthy();
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps quick edit in the standalone menu for icon specs', () => {
|
||||
const layer = createLayer({ assetKind: 'icon-spec' });
|
||||
renderContextMenus({
|
||||
imageContextMenu: { layerId: layer.id, x: 20, y: 22 },
|
||||
imageContextMenuLayer: layer,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('menuitem', { name: '快速编辑' })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
CanvasViewport,
|
||||
ImageContextMenuState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { isQuickEditUnsupportedAssetKind } from './ImageCanvasGenerationModel';
|
||||
|
||||
type ImageCanvasContextMenusViewProps = {
|
||||
viewport: CanvasViewport;
|
||||
@@ -477,17 +478,19 @@ export function ImageCanvasContextMenusView({
|
||||
<hr />
|
||||
{imageContextMenuLayer ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
onOpenQuickEditPanel(imageContextMenuLayer);
|
||||
onCloseContextMenu();
|
||||
onCloseImageContextMenu();
|
||||
}}
|
||||
>
|
||||
快速编辑
|
||||
</button>
|
||||
{!isQuickEditUnsupportedAssetKind(imageContextMenuLayer) ? (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
onOpenQuickEditPanel(imageContextMenuLayer);
|
||||
onCloseContextMenu();
|
||||
onCloseImageContextMenu();
|
||||
}}
|
||||
>
|
||||
快速编辑
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@@ -539,15 +542,17 @@ export function ImageCanvasContextMenusView({
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<PlatformFloatingMenu label="图片功能面板" placement="bottom-start">
|
||||
<PlatformFloatingMenuItem
|
||||
className="image-canvas-editor__context-menu-item"
|
||||
onClick={() => {
|
||||
onOpenQuickEditPanel(imageContextMenuLayer);
|
||||
onCloseImageContextMenu();
|
||||
}}
|
||||
>
|
||||
快速编辑
|
||||
</PlatformFloatingMenuItem>
|
||||
{!isQuickEditUnsupportedAssetKind(imageContextMenuLayer) ? (
|
||||
<PlatformFloatingMenuItem
|
||||
className="image-canvas-editor__context-menu-item"
|
||||
onClick={() => {
|
||||
onOpenQuickEditPanel(imageContextMenuLayer);
|
||||
onCloseImageContextMenu();
|
||||
}}
|
||||
>
|
||||
快速编辑
|
||||
</PlatformFloatingMenuItem>
|
||||
) : null}
|
||||
<PlatformFloatingMenuItem
|
||||
className="image-canvas-editor__context-menu-item"
|
||||
onClick={() => {
|
||||
|
||||
@@ -262,6 +262,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
phaseDetail: overrides.phaseDetail ?? '正在生成角色形象。',
|
||||
progress: overrides.progress ?? 40,
|
||||
error: overrides.error ?? null,
|
||||
warning: overrides.warning ?? null,
|
||||
priceMudPoints: overrides.priceMudPoints ?? 3,
|
||||
refundLedgerId: overrides.refundLedgerId ?? null,
|
||||
notificationAcknowledgedAt: overrides.notificationAcknowledgedAt ?? null,
|
||||
@@ -2446,7 +2447,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect(screen.getByAltText('画布图片:拼图素材')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('opens icon asset generation panel, only picks icon specs, and lays only the generated spritesheet on canvas', async () => {
|
||||
it('opens icon asset generation panel, only picks icon specs, and lays out the generated spritesheet slices', async () => {
|
||||
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId: 'editor-project-icons',
|
||||
title: '图标素材画布',
|
||||
@@ -2489,7 +2490,20 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
spritesheetImageSrc: 'data:image/png;base64,sheet',
|
||||
spritesheetWidth: 512,
|
||||
spritesheetHeight: 512,
|
||||
iconImageSrcs: [],
|
||||
iconImageSrcs: [
|
||||
{
|
||||
name: '返回按钮',
|
||||
imageSrc: 'data:image/png;base64,back',
|
||||
width: 128,
|
||||
height: 128,
|
||||
},
|
||||
{
|
||||
name: '设置按钮',
|
||||
imageSrc: 'data:image/png;base64,settings',
|
||||
width: 128,
|
||||
height: 128,
|
||||
},
|
||||
],
|
||||
prompt: '图标 prompt',
|
||||
actualPrompt: '图标 prompt',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
@@ -2566,7 +2580,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
await waitFor(() => {
|
||||
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
referenceImageSrc: 'data:image/png;base64,icon-spec',
|
||||
referenceImageSrc: 'resource-icon-spec',
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
@@ -2588,8 +2602,8 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByAltText(/图层缩略图:图标素材图集/u)).toBeTruthy();
|
||||
});
|
||||
expect(screen.queryByAltText('画布图片:返回按钮')).toBeNull();
|
||||
expect(screen.queryByAltText('画布图片:设置按钮')).toBeNull();
|
||||
expect(screen.getByAltText('画布图片:返回按钮')).toBeTruthy();
|
||||
expect(screen.getByAltText('画布图片:设置按钮')).toBeTruthy();
|
||||
expect(screen.queryByLabelText('图标素材生成占位图')).toBeNull();
|
||||
expect(screen.getByText('图集')).toBeTruthy();
|
||||
fireEvent.click(
|
||||
@@ -2882,6 +2896,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
phaseDetail: '生成已完成。',
|
||||
warning: '连通域数量不足',
|
||||
completedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAtMicros: 2,
|
||||
@@ -2921,6 +2936,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect(loadEditorProjectMock).toHaveBeenCalledWith(projectId);
|
||||
});
|
||||
expect(await screen.findByAltText('画布图片:刷新后生成结果')).toBeTruthy();
|
||||
expect((await screen.findByRole('alert')).textContent).toBe(
|
||||
'图集已生成,但自动拆分未完成:连通域数量不足',
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(loadEditorAssetLibraryMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -169,7 +169,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a layer from an account asset at the requested screen point', () => {
|
||||
it('creates a cascaded layer from an account asset near the requested screen point', () => {
|
||||
const asset: EditorAsset = {
|
||||
id: 'asset-1',
|
||||
label: '角色草图',
|
||||
@@ -210,6 +210,31 @@ describe('ImageCanvasEditorModel', () => {
|
||||
expect(layer.y).toBe(12);
|
||||
});
|
||||
|
||||
it('centers a dropped asset exactly at the requested screen point', () => {
|
||||
const asset: EditorAsset = {
|
||||
id: 'asset-drop',
|
||||
label: '投放素材',
|
||||
src: 'data:image/png;base64,drop',
|
||||
width: 640,
|
||||
height: 480,
|
||||
folderId: 'project',
|
||||
sourceKind: 'uploaded',
|
||||
sourceType: 'uploaded',
|
||||
persisted: true,
|
||||
};
|
||||
|
||||
const layer = createLayerFromAsset(
|
||||
asset,
|
||||
3,
|
||||
{ x: 20, y: 40, scale: 2 },
|
||||
{ x: 420, y: 340 },
|
||||
{ applyCascadeOffset: false },
|
||||
);
|
||||
|
||||
expect(layer.x + layer.width / 2).toBe(200);
|
||||
expect(layer.y + layer.height / 2).toBe(150);
|
||||
});
|
||||
|
||||
it('preserves source resource ids from the persisted asset library', () => {
|
||||
const library = normalizeAssetLibrary({
|
||||
folders: [
|
||||
|
||||
@@ -138,6 +138,7 @@ export function createLayerFromAsset(
|
||||
index: number,
|
||||
viewport: CanvasViewport,
|
||||
screenCenter: { x: number; y: number },
|
||||
options: { applyCascadeOffset?: boolean } = {},
|
||||
): CanvasLayer {
|
||||
const { width, height } = resolveLayerResolutionSize(
|
||||
asset.width,
|
||||
@@ -151,7 +152,7 @@ export function createLayerFromAsset(
|
||||
};
|
||||
const worldCenterX = (safeScreenCenter.x - viewport.x) / safeScale;
|
||||
const worldCenterY = (safeScreenCenter.y - viewport.y) / safeScale;
|
||||
const offset = index * 34;
|
||||
const offset = options.applyCascadeOffset === false ? 0 : index * 34;
|
||||
const assetKind: CanvasAssetKind | undefined =
|
||||
asset.mediaType === 'video'
|
||||
? 'video'
|
||||
@@ -1145,6 +1146,7 @@ export function canvasAssetKindOrNull(value: unknown): CanvasAssetKind | null {
|
||||
value === 'icon' ||
|
||||
value === 'icon-spritesheet' ||
|
||||
value === 'icon-spec' ||
|
||||
value === 'editor_green_screen_source' ||
|
||||
value === 'publication-material' ||
|
||||
value === 'ui-design' ||
|
||||
value === 'video' ||
|
||||
|
||||
@@ -194,6 +194,7 @@ function createStageProps(): ImageCanvasStageViewProps {
|
||||
onOpenRedrawPanel: vi.fn(),
|
||||
onOpenCropExpandPanel: vi.fn(),
|
||||
onRemoveBackground: vi.fn(),
|
||||
onSplitIconSpritesheet: vi.fn(),
|
||||
onExtractUiDesignAssets: vi.fn(),
|
||||
onUiAssetExtractionToolChange: vi.fn(),
|
||||
onUiAssetExtractionModelChange: vi.fn(),
|
||||
|
||||
@@ -20,6 +20,7 @@ export type CanvasAssetKind =
|
||||
| 'icon'
|
||||
| 'icon-spritesheet'
|
||||
| 'icon-spec'
|
||||
| 'editor_green_screen_source'
|
||||
| 'publication-material'
|
||||
| 'ui-design'
|
||||
| 'video'
|
||||
@@ -197,6 +198,7 @@ export type GenerateDialogState = {
|
||||
| 'audio-sound-effect'
|
||||
| 'audio-background-music';
|
||||
prompt: string;
|
||||
assetLabel?: string;
|
||||
status: 'idle' | 'generating' | 'failed';
|
||||
composerOpen?: boolean;
|
||||
sourceLayerId?: string;
|
||||
@@ -304,6 +306,7 @@ export type QuickEditPanelState = {
|
||||
mode?: 'quick-edit' | 'redraw';
|
||||
sourceLayerId: string;
|
||||
prompt: string;
|
||||
assetLabel?: string;
|
||||
size: string;
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
@@ -347,6 +350,7 @@ export type CropExpandResizeHandle =
|
||||
export type CharacterAnimationPanelState = {
|
||||
sourceLayerId: string;
|
||||
promptText: string;
|
||||
assetLabel?: string;
|
||||
resolution: EditorCharacterAnimationResolution;
|
||||
ratio: EditorCharacterAnimationRatio;
|
||||
frameCount: EditorCharacterAnimationFrameCount;
|
||||
|
||||
@@ -1758,6 +1758,23 @@ describe('ImageCanvasEditorView', () => {
|
||||
expect(screen.queryByLabelText('发送给画布 Agent')).toBeNull();
|
||||
});
|
||||
|
||||
it('closes the Agent conversation before opening quick edit', async () => {
|
||||
enableEditorAgentSidebarForTest();
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
expect(await screen.findByLabelText('发送给画布 Agent')).toBeTruthy();
|
||||
const sourceLayer = (
|
||||
await screen.findByAltText('画布图片:拼图素材')
|
||||
).closest('button')!;
|
||||
fireEvent.click(sourceLayer);
|
||||
fireEvent.click(screen.getByRole('button', { name: '快速编辑' }));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '快速编辑图片' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByLabelText('发送给画布 Agent')).toBeNull();
|
||||
});
|
||||
|
||||
it('closes the Agent conversation panel when runtime config disables it after focus', async () => {
|
||||
loadFrontendRuntimeConfigMock
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { EditorAgentGenerationResultEvent } from '../../../packages/shared/src/contracts/editorAgent';
|
||||
import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration';
|
||||
import { loadFrontendRuntimeConfig } from '../../services/frontendRuntimeConfigService';
|
||||
import {
|
||||
@@ -1098,18 +1097,6 @@ export function ImageCanvasEditorView({
|
||||
},
|
||||
[applyProjectSnapshot, refreshAssetLibrary],
|
||||
);
|
||||
const handleExternalGenerationTasksCompleted = useCallback(
|
||||
(tasks: ExternalGenerationTaskRecord[]) => {
|
||||
if (!projectId || tasks.length === 0) {
|
||||
return;
|
||||
}
|
||||
refreshEditorWalletBalance();
|
||||
void loadEditorProject(projectId)
|
||||
.then(applyGeneratedProjectSnapshot)
|
||||
.catch(() => undefined);
|
||||
},
|
||||
[applyGeneratedProjectSnapshot, projectId, refreshEditorWalletBalance],
|
||||
);
|
||||
const handleEditorAgentCanvasRefreshRequested = useCallback(() => {
|
||||
if (!projectId) {
|
||||
return;
|
||||
@@ -1256,6 +1243,30 @@ export function ImageCanvasEditorView({
|
||||
generationSurface.refreshTaskList();
|
||||
handleEditorAgentCanvasRefreshRequested();
|
||||
}, [generationSurface, handleEditorAgentCanvasRefreshRequested]);
|
||||
const showGenerationWarning = generationSurface.showGenerationWarning;
|
||||
const handleExternalGenerationTasksCompleted = useCallback(
|
||||
(tasks: ExternalGenerationTaskRecord[]) => {
|
||||
if (!projectId || tasks.length === 0) {
|
||||
return;
|
||||
}
|
||||
const warning = tasks.find((task) => task.warning?.trim())?.warning?.trim();
|
||||
if (warning) {
|
||||
showGenerationWarning(
|
||||
`图集已生成,但自动拆分未完成:${warning}`,
|
||||
);
|
||||
}
|
||||
refreshEditorWalletBalance();
|
||||
void loadEditorProject(projectId)
|
||||
.then(applyGeneratedProjectSnapshot)
|
||||
.catch(() => undefined);
|
||||
},
|
||||
[
|
||||
applyGeneratedProjectSnapshot,
|
||||
projectId,
|
||||
refreshEditorWalletBalance,
|
||||
showGenerationWarning,
|
||||
],
|
||||
);
|
||||
const effectiveIsAgentConversationOpen =
|
||||
isAgentConversationEnabled && isAgentConversationOpen;
|
||||
const toggleAgentConversation = useCallback(() => {
|
||||
@@ -1277,6 +1288,16 @@ export function ImageCanvasEditorView({
|
||||
}
|
||||
generationSurface.toggleTaskSidebar();
|
||||
}, [effectiveIsAgentConversationOpen, generationSurface]);
|
||||
const openQuickEditPanelWithAvailableCanvas = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
if (generationSurface.isTaskSidebarOpen) {
|
||||
generationSurface.toggleTaskSidebar();
|
||||
}
|
||||
setIsAgentConversationOpen(false);
|
||||
generationSurface.openQuickEditPanel(layer);
|
||||
},
|
||||
[generationSurface],
|
||||
);
|
||||
const toggleCanvasSidebarPanel = useCallback(
|
||||
(panel: SidebarPanel) => {
|
||||
toggleSidebarPanel(panel);
|
||||
@@ -1328,9 +1349,9 @@ export function ImageCanvasEditorView({
|
||||
setIsPickingUiDesignSpecFromCanvas,
|
||||
openCharacterAnimationPanel,
|
||||
openRedrawPanel,
|
||||
openQuickEditPanel,
|
||||
openCropExpandPanel,
|
||||
removeSelectedLayerBackground,
|
||||
splitSelectedIconSpritesheet,
|
||||
extractUiDesignAssets,
|
||||
pickCharacterSpecFromLayer,
|
||||
pickGenerationReferenceFromLayer,
|
||||
@@ -2138,10 +2159,11 @@ export function ImageCanvasEditorView({
|
||||
onToggleTaskSidebar: toggleTaskSidebar,
|
||||
onToggleAgentConversation: toggleAgentConversation,
|
||||
onCropExpandHandlePointerDown: generationSurface.startCropExpandFrameResize,
|
||||
onOpenQuickEditPanel: openQuickEditPanel,
|
||||
onOpenQuickEditPanel: openQuickEditPanelWithAvailableCanvas,
|
||||
onOpenRedrawPanel: openRedrawPanel,
|
||||
onOpenCropExpandPanel: openCropExpandPanel,
|
||||
onRemoveBackground: removeSelectedLayerBackground,
|
||||
onSplitIconSpritesheet: splitSelectedIconSpritesheet,
|
||||
onExtractUiDesignAssets: extractUiDesignAssets,
|
||||
onUiAssetExtractionToolChange: changeUiAssetExtractionTool,
|
||||
onUiAssetExtractionModelChange: changeUiAssetExtractionModel,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
IMAGE_CANVAS_GENERATION_ASSET_NAME_MAX_LENGTH,
|
||||
ImageCanvasGenerationAssetNameField,
|
||||
} from './ImageCanvasGenerationAssetNameField';
|
||||
|
||||
describe('ImageCanvasGenerationAssetNameField', () => {
|
||||
it('renders as a controlled optional name input', () => {
|
||||
const onChange = vi.fn();
|
||||
const { rerender } = render(
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value="角色立绘"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox', {
|
||||
name: '资源名称',
|
||||
}) as HTMLInputElement;
|
||||
expect(input.value).toBe('角色立绘');
|
||||
expect(input.getAttribute('maxlength')).toBe(
|
||||
String(IMAGE_CANVAS_GENERATION_ASSET_NAME_MAX_LENGTH),
|
||||
);
|
||||
|
||||
fireEvent.change(input, { target: { value: '新名称' } });
|
||||
expect(onChange).toHaveBeenCalledWith('新名称');
|
||||
expect(input.value).toBe('角色立绘');
|
||||
|
||||
rerender(
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value="新名称"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
expect(input.value).toBe('新名称');
|
||||
});
|
||||
|
||||
it('limits callback values to 80 characters and supports disabled state', () => {
|
||||
const onChange = vi.fn();
|
||||
render(
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value=""
|
||||
disabled
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox', {
|
||||
name: '资源名称',
|
||||
}) as HTMLInputElement;
|
||||
expect(input.disabled).toBe(true);
|
||||
|
||||
fireEvent.change(input, { target: { value: '名'.repeat(90) } });
|
||||
expect(onChange).toHaveBeenCalledWith('名'.repeat(80));
|
||||
|
||||
fireEvent.change(input, { target: { value: '🎨'.repeat(90) } });
|
||||
expect(onChange).toHaveBeenLastCalledWith('🎨'.repeat(80));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { EDITOR_GENERATED_ASSET_LABEL_MAX_CHARS } from './ImageCanvasGenerationSubmissionModel';
|
||||
|
||||
export const IMAGE_CANVAS_GENERATION_ASSET_NAME_MAX_LENGTH =
|
||||
EDITOR_GENERATED_ASSET_LABEL_MAX_CHARS;
|
||||
|
||||
type ImageCanvasGenerationAssetNameFieldProps = {
|
||||
value?: string | null;
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
export function ImageCanvasGenerationAssetNameField({
|
||||
value,
|
||||
disabled = false,
|
||||
onChange,
|
||||
}: ImageCanvasGenerationAssetNameFieldProps) {
|
||||
return (
|
||||
<label className="image-canvas-editor__generation-asset-name-field">
|
||||
<PlatformFieldLabel
|
||||
variant="form"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
资源名称
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
aria-label="资源名称"
|
||||
value={value ?? ''}
|
||||
maxLength={IMAGE_CANVAS_GENERATION_ASSET_NAME_MAX_LENGTH}
|
||||
disabled={disabled}
|
||||
placeholder="例如:勇者立绘"
|
||||
size="sm"
|
||||
density="compact"
|
||||
className="image-canvas-editor__generation-asset-name-input"
|
||||
onChange={(event) =>
|
||||
onChange(
|
||||
Array.from(event.target.value)
|
||||
.slice(0, IMAGE_CANVAS_GENERATION_ASSET_NAME_MAX_LENGTH)
|
||||
.join(''),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ function renderComposer(
|
||||
}
|
||||
|
||||
describe('ImageCanvasGenerationComposerView', () => {
|
||||
it('让快速编辑只保留提示词和模型选择', () => {
|
||||
it('让快速编辑显示提示词、尺寸和模型选择', () => {
|
||||
renderComposer({
|
||||
mode: 'quick-edit',
|
||||
prompt: '',
|
||||
@@ -146,10 +146,10 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
within(panel).queryByRole('button', { name: '添加参考图' }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
within(panel).queryByRole('button', {
|
||||
name: /快速编辑图片尺寸/u,
|
||||
within(panel).getByRole('button', {
|
||||
name: '快速编辑图片尺寸 1:1·1K',
|
||||
}),
|
||||
).toBeNull();
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByRole('button', {
|
||||
name: '快速编辑图片模型 gpt-image-2',
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
resizeGenerationPlaceholderToVideoSelection,
|
||||
SPEC_TYPE_LABEL,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { ImageCanvasGenerationAssetNameField } from './ImageCanvasGenerationAssetNameField';
|
||||
import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSpritesheetComposerView';
|
||||
import { ImageCanvasPublicationMaterialsDemoPanelView } from './ImageCanvasPublicationMaterialsDemoPanelView';
|
||||
import { getPublicationMaterialsWorkflow } from './ImageCanvasPublicationMaterialsModel';
|
||||
@@ -401,6 +402,11 @@ function ImageCanvasVideoGenerationComposerView({
|
||||
updateVideoDialog({ prompt: event.target.value })
|
||||
}
|
||||
/>
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value={dialog.assetLabel}
|
||||
disabled={isGenerating}
|
||||
onChange={(assetLabel) => updateVideoDialog({ assetLabel })}
|
||||
/>
|
||||
<div className="image-canvas-editor__generation-composer-footer">
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--dimensions">
|
||||
<PlatformInlineOptionButton
|
||||
@@ -787,6 +793,11 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) => updateAudioDialog({ prompt: event.target.value })}
|
||||
/>
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value={dialog.assetLabel}
|
||||
disabled={isGenerating}
|
||||
onChange={(assetLabel) => updateAudioDialog({ assetLabel })}
|
||||
/>
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
createIconGenerationDialogDraft,
|
||||
createLayerGenerationDialogDraft,
|
||||
createPublicationGenerationDialogDraft,
|
||||
createQuickEditGenerationDialogDraft,
|
||||
createQuickEditPanelDraft,
|
||||
createRedrawPanelDraft,
|
||||
createSameSourceGenerationDialogDraft,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
} from './ImageCanvasGenerationDialogModel';
|
||||
import {
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
IMAGE_MODEL_NANOBANANA2,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
@@ -195,28 +197,29 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
const canvasSize = { width: 960, height: 720 };
|
||||
const viewport = { x: 0, y: 0, scale: 1 };
|
||||
|
||||
expect(createVideoGenerationDialogDraft({ canvasSize, viewport }))
|
||||
.toMatchObject({
|
||||
mode: 'video',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
generationReferences: [],
|
||||
videoModel: 'seedance2.0-fast',
|
||||
videoAspectRatio: '16:9',
|
||||
videoResolution: '480p',
|
||||
videoDurationSeconds: 4,
|
||||
videoMode: 'std',
|
||||
videoSound: 'on',
|
||||
videoWebSearchEnabled: true,
|
||||
placeholder: {
|
||||
x: 53,
|
||||
y: 120,
|
||||
width: 854,
|
||||
height: 480,
|
||||
originalWidth: 854,
|
||||
originalHeight: 480,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
createVideoGenerationDialogDraft({ canvasSize, viewport }),
|
||||
).toMatchObject({
|
||||
mode: 'video',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
generationReferences: [],
|
||||
videoModel: 'seedance2.0-fast',
|
||||
videoAspectRatio: '16:9',
|
||||
videoResolution: '480p',
|
||||
videoDurationSeconds: 4,
|
||||
videoMode: 'std',
|
||||
videoSound: 'on',
|
||||
videoWebSearchEnabled: true,
|
||||
placeholder: {
|
||||
x: 53,
|
||||
y: 120,
|
||||
width: 854,
|
||||
height: 480,
|
||||
originalWidth: 854,
|
||||
originalHeight: 480,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
createUiDesignGenerationDialogDraft({
|
||||
canvasSize,
|
||||
@@ -242,37 +245,39 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
const canvasSize = { width: 960, height: 720 };
|
||||
const viewport = { x: 0, y: 0, scale: 1 };
|
||||
|
||||
expect(createSoundEffectGenerationDialogDraft({ canvasSize, viewport }))
|
||||
.toMatchObject({
|
||||
mode: 'audio-sound-effect',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
soundModel: 'audio1.0',
|
||||
soundDurationSeconds: 5,
|
||||
placeholder: {
|
||||
x: 270,
|
||||
y: 300,
|
||||
width: 420,
|
||||
height: 120,
|
||||
originalWidth: 420,
|
||||
originalHeight: 120,
|
||||
},
|
||||
});
|
||||
expect(createBackgroundMusicGenerationDialogDraft({ canvasSize, viewport }))
|
||||
.toMatchObject({
|
||||
mode: 'audio-background-music',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
makeInstrumental: true,
|
||||
placeholder: {
|
||||
x: 270,
|
||||
y: 300,
|
||||
width: 420,
|
||||
height: 120,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
createSoundEffectGenerationDialogDraft({ canvasSize, viewport }),
|
||||
).toMatchObject({
|
||||
mode: 'audio-sound-effect',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
soundModel: 'audio1.0',
|
||||
soundDurationSeconds: 5,
|
||||
placeholder: {
|
||||
x: 270,
|
||||
y: 300,
|
||||
width: 420,
|
||||
height: 120,
|
||||
originalWidth: 420,
|
||||
originalHeight: 120,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
createBackgroundMusicGenerationDialogDraft({ canvasSize, viewport }),
|
||||
).toMatchObject({
|
||||
mode: 'audio-background-music',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
makeInstrumental: true,
|
||||
placeholder: {
|
||||
x: 270,
|
||||
y: 300,
|
||||
width: 420,
|
||||
height: 120,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('creates audio remodel drafts from generated audio layers without references', () => {
|
||||
@@ -411,6 +416,20 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
expect(createCharacterAnimationPanelDraft(createLayer())).toBeNull();
|
||||
});
|
||||
|
||||
it('preserves the resolved asset label in quick-edit dialog snapshots', () => {
|
||||
expect(
|
||||
createQuickEditGenerationDialogDraft({
|
||||
sourceLayer: createLayer(),
|
||||
prompt: '让视频下雨',
|
||||
assetLabel: '雨天版本',
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'quick-edit',
|
||||
prompt: '让视频下雨',
|
||||
assetLabel: '雨天版本',
|
||||
});
|
||||
});
|
||||
|
||||
it('restores remodel prompts from user-visible generation input snapshots only', () => {
|
||||
const cases: Array<{
|
||||
fields: NonNullable<CanvasLayer['generationInputs']>['fields'];
|
||||
@@ -496,17 +515,17 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
const canvasSize = { width: 960, height: 720 };
|
||||
const viewport = { x: 0, y: 0, scale: 1 };
|
||||
const characterDraft = createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '角色设定', value: '红发骑士' },
|
||||
{ title: '抠图背景色', value: '暖浅桃色 #FFD6C2' },
|
||||
{ title: '抠图模型', value: '动漫风格 anime-seg' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
sourceLayer: createLayer({
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '角色设定', value: '红发骑士' },
|
||||
{ title: '抠图背景色', value: '暖浅桃色 #FFD6C2' },
|
||||
{ title: '抠图模型', value: '动漫风格 anime-seg' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
canvasSize,
|
||||
viewport,
|
||||
mode: 'redraw',
|
||||
@@ -653,21 +672,38 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('locks quick edit drafts to gpt-image-2 despite legacy source models', () => {
|
||||
it('inherits and normalizes quick edit model parameters from the source', () => {
|
||||
expect(
|
||||
createQuickEditPanelDraft(
|
||||
createLayer({
|
||||
model: 'nano-banana',
|
||||
}),
|
||||
).model,
|
||||
).toBe(IMAGE_MODEL_GPT_IMAGE_2);
|
||||
),
|
||||
).toMatchObject({
|
||||
model: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '4:3',
|
||||
imageSize: '1K',
|
||||
});
|
||||
expect(
|
||||
createQuickEditPanelDraft(createLayer({ model: 'nanobanana2' }), {
|
||||
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
}),
|
||||
).toMatchObject({
|
||||
model: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
});
|
||||
expect(
|
||||
createQuickEditPanelDraft(
|
||||
createLayer({
|
||||
model: 'nanobanana2',
|
||||
}),
|
||||
).model,
|
||||
).toBe(IMAGE_MODEL_GPT_IMAGE_2);
|
||||
createLayer({ originalWidth: 512, originalHeight: 512 }),
|
||||
{ imageModel: IMAGE_MODEL_GPT_IMAGE_2, imageSize: '0.5K' },
|
||||
).imageSize,
|
||||
).toBe('1K');
|
||||
expect(
|
||||
createQuickEditPanelDraft(createLayer({ model: 'legacy-image-model' })),
|
||||
).toMatchObject({ model: IMAGE_MODEL_NANOBANANA2 });
|
||||
});
|
||||
|
||||
it('creates character animation generation dialog drafts for character layers', () => {
|
||||
@@ -805,7 +841,8 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
title: '动作参考',
|
||||
src: 'https://signed.example.com/reference.mp4',
|
||||
mediaType: 'video',
|
||||
objectKey: 'generated-character-drafts/editor/seedance-references/video/reference.mp4',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/seedance-references/video/reference.mp4',
|
||||
assetObjectId: 'asset-object-video',
|
||||
});
|
||||
expect(
|
||||
@@ -864,7 +901,9 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
status: 'idle',
|
||||
generationReferences: [],
|
||||
};
|
||||
expect(appendGenerationReference(imageDialog, videoLayer)).toBe(imageDialog);
|
||||
expect(appendGenerationReference(imageDialog, videoLayer)).toBe(
|
||||
imageDialog,
|
||||
);
|
||||
const specDialog: GenerateDialogState = {
|
||||
mode: 'spec',
|
||||
prompt: '',
|
||||
@@ -978,7 +1017,9 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
errorMessage: '描述错误',
|
||||
iconDescriptions: ['旧描述'],
|
||||
};
|
||||
expect(updateIconDescriptionsTextInDialog(iconDialog, '剑\n盾、药水')).toEqual(
|
||||
expect(
|
||||
updateIconDescriptionsTextInDialog(iconDialog, '剑\n盾、药水'),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
status: 'idle',
|
||||
errorMessage: undefined,
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
DEFAULT_VIDEO_SOUND,
|
||||
DEFAULT_VIDEO_WEB_SEARCH_ENABLED,
|
||||
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||||
EDITOR_IMAGE_MODEL_OPTIONS,
|
||||
ICON_DESCRIPTION_LIMIT,
|
||||
ICON_FRAME_DISPLAY_SIZE,
|
||||
ICON_FRAME_ORIGINAL_SIZE,
|
||||
@@ -690,7 +691,8 @@ export function createLayerGenerationDialogDraft({
|
||||
return {
|
||||
...draft,
|
||||
prompt: draft.prompt || sourceLayer.prompt?.trim() || '',
|
||||
imageModel: sourceDialog?.imageModel ?? sourceLayer.model ?? draft.imageModel,
|
||||
imageModel:
|
||||
sourceDialog?.imageModel ?? sourceLayer.model ?? draft.imageModel,
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
generatedLayerId: sourceLayer.id,
|
||||
@@ -1176,19 +1178,41 @@ export function createQuickEditPanelDraft(
|
||||
imageSize?: string;
|
||||
} = {},
|
||||
): QuickEditPanelState {
|
||||
const aspectRatio =
|
||||
options.aspectRatio ??
|
||||
inferEditorImageAspectRatio(
|
||||
sourceLayer.originalWidth,
|
||||
sourceLayer.originalHeight,
|
||||
);
|
||||
const imageSize =
|
||||
options.imageSize ??
|
||||
inferEditorImageSizeLabel(
|
||||
sourceLayer.originalWidth,
|
||||
sourceLayer.originalHeight,
|
||||
);
|
||||
const model = IMAGE_MODEL_GPT_IMAGE_2;
|
||||
const normalizedModel = normalizeEditorImageModel(
|
||||
options.imageModel ?? sourceLayer.model,
|
||||
);
|
||||
const model = EDITOR_IMAGE_MODEL_OPTIONS.some(
|
||||
(option) => option.value === normalizedModel,
|
||||
)
|
||||
? normalizedModel
|
||||
: DEFAULT_IMAGE_MODEL;
|
||||
const dimensionOptions =
|
||||
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
||||
model as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||||
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
|
||||
const supportedAspectRatios =
|
||||
dimensionOptions.aspectRatios as readonly string[];
|
||||
const supportedImageSizes = dimensionOptions.imageSizes as readonly string[];
|
||||
const inferredAspectRatio = inferEditorImageAspectRatio(
|
||||
sourceLayer.originalWidth,
|
||||
sourceLayer.originalHeight,
|
||||
);
|
||||
const inferredImageSize = inferEditorImageSizeLabel(
|
||||
sourceLayer.originalWidth,
|
||||
sourceLayer.originalHeight,
|
||||
);
|
||||
const aspectRatio = supportedAspectRatios.includes(options.aspectRatio ?? '')
|
||||
? options.aspectRatio
|
||||
: supportedAspectRatios.includes(inferredAspectRatio)
|
||||
? inferredAspectRatio
|
||||
: (dimensionOptions.aspectRatios[0] ?? '1:1');
|
||||
const imageSize = supportedImageSizes.includes(options.imageSize ?? '')
|
||||
? options.imageSize
|
||||
: supportedImageSizes.includes(inferredImageSize)
|
||||
? inferredImageSize
|
||||
: (dimensionOptions.imageSizes.find((size) => size === '1K') ??
|
||||
dimensionOptions.imageSizes[0] ??
|
||||
'1K');
|
||||
return {
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: sourceLayer.id,
|
||||
@@ -1208,6 +1232,7 @@ export function createQuickEditPanelDraft(
|
||||
export function createQuickEditGenerationDialogDraft({
|
||||
sourceLayer,
|
||||
prompt,
|
||||
assetLabel,
|
||||
status = 'idle',
|
||||
references = [],
|
||||
aspectRatio,
|
||||
@@ -1216,6 +1241,7 @@ export function createQuickEditGenerationDialogDraft({
|
||||
}: {
|
||||
sourceLayer: CanvasLayer;
|
||||
prompt: string;
|
||||
assetLabel?: string;
|
||||
status?: CanvasGenerationDialogState['status'];
|
||||
references?: NonNullable<QuickEditPanelState['quickEditReferences']>;
|
||||
model?: string;
|
||||
@@ -1228,6 +1254,7 @@ export function createQuickEditGenerationDialogDraft({
|
||||
return {
|
||||
mode: 'quick-edit',
|
||||
prompt,
|
||||
assetLabel,
|
||||
status,
|
||||
composerOpen: false,
|
||||
sourceLayerId: sourceLayer.id,
|
||||
|
||||
@@ -313,14 +313,22 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
id: 'layer-source',
|
||||
title: '源图',
|
||||
src: 'data:image/png;base64,edited',
|
||||
width: 320,
|
||||
height: 240,
|
||||
originalWidth: 1537,
|
||||
originalHeight: 1025,
|
||||
x: -488,
|
||||
y: -252,
|
||||
width: 1536,
|
||||
height: 1024,
|
||||
originalWidth: 1536,
|
||||
originalHeight: 1024,
|
||||
resourceId: 'resource-edited',
|
||||
sourceResourceId: 'resource-source',
|
||||
objectKey: 'generated/edited.png',
|
||||
});
|
||||
expect(layer.x + layer.width / 2).toBe(
|
||||
sourceLayer.x + sourceLayer.width / 2,
|
||||
);
|
||||
expect(layer.y + layer.height / 2).toBe(
|
||||
sourceLayer.y + sourceLayer.height / 2,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates wrapped icon layers with icon metadata', () => {
|
||||
|
||||
@@ -39,6 +39,7 @@ type QuickEditResultLayerOptions = {
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
mode?: 'quick-edit' | 'redraw';
|
||||
frame?: GenerateDialogState['placeholder'];
|
||||
title?: string;
|
||||
};
|
||||
|
||||
type IconSpritesheetResultLayerOptions = {
|
||||
@@ -190,9 +191,13 @@ export function createQuickEditResultLayer({
|
||||
generationInputs,
|
||||
mode = 'quick-edit',
|
||||
frame,
|
||||
title,
|
||||
}: QuickEditResultLayerOptions): CanvasLayer {
|
||||
const originalWidth =
|
||||
frame?.originalWidth || generated.width || sourceLayer.originalWidth || 1024;
|
||||
frame?.originalWidth ||
|
||||
generated.width ||
|
||||
sourceLayer.originalWidth ||
|
||||
1024;
|
||||
const originalHeight =
|
||||
frame?.originalHeight ||
|
||||
generated.height ||
|
||||
@@ -219,7 +224,9 @@ export function createQuickEditResultLayer({
|
||||
{
|
||||
id: `layer-quick-edit-${generatedIndex}`,
|
||||
resourceId: `local-resource-quick-edit-${generatedIndex}`,
|
||||
title: `${sourceLayer.title} ${mode === 'redraw' ? '重绘' : '快速编辑'}`,
|
||||
title:
|
||||
title ??
|
||||
`${sourceLayer.title} ${mode === 'redraw' ? '重绘' : '快速编辑'}`,
|
||||
src: generated.imageSrc,
|
||||
x: frameX ?? sourceLayer.x + sourceLayer.width + 32,
|
||||
y: frameY ?? sourceLayer.y,
|
||||
@@ -247,14 +254,29 @@ export function applyImageEditResultToSourceLayer({
|
||||
sourceLayer: CanvasLayer;
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
}): CanvasLayer {
|
||||
const originalWidth = sourceLayer.originalWidth || sourceLayer.width;
|
||||
const originalHeight = sourceLayer.originalHeight || sourceLayer.height;
|
||||
const width = sourceLayer.width;
|
||||
const height = sourceLayer.height;
|
||||
const originalWidth =
|
||||
generated.resource?.width ||
|
||||
generated.width ||
|
||||
sourceLayer.originalWidth ||
|
||||
1;
|
||||
const originalHeight =
|
||||
generated.resource?.height ||
|
||||
generated.height ||
|
||||
sourceLayer.originalHeight ||
|
||||
1;
|
||||
const { width, height } = resolveLayerResolutionSize(
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
sourceLayer,
|
||||
);
|
||||
const centerX = sourceLayer.x + sourceLayer.width / 2;
|
||||
const centerY = sourceLayer.y + sourceLayer.height / 2;
|
||||
return applyGeneratedMetadata(
|
||||
{
|
||||
...sourceLayer,
|
||||
src: generated.imageSrc,
|
||||
x: centerX - width / 2,
|
||||
y: centerY - height / 2,
|
||||
width,
|
||||
height,
|
||||
originalWidth,
|
||||
@@ -280,10 +302,8 @@ export function createIconSpritesheetResultLayers({
|
||||
const spritesheetResource = generated.spritesheetResource;
|
||||
const spritesheetAsset = generated.spritesheetAsset;
|
||||
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
||||
const startX =
|
||||
frame?.x ?? worldCenter.x - ICON_FRAME_DISPLAY_SIZE.width / 2;
|
||||
const startY =
|
||||
frame?.y ?? worldCenter.y - ICON_FRAME_DISPLAY_SIZE.height / 2;
|
||||
const startX = frame?.x ?? worldCenter.x - ICON_FRAME_DISPLAY_SIZE.width / 2;
|
||||
const startY = frame?.y ?? worldCenter.y - ICON_FRAME_DISPLAY_SIZE.height / 2;
|
||||
const spacing = 24;
|
||||
const maxRowWidth = 560;
|
||||
const spritesheetOriginalWidth =
|
||||
@@ -319,8 +339,7 @@ export function createIconSpritesheetResultLayers({
|
||||
sourceAssetId: spritesheetAsset?.assetId,
|
||||
objectKey: spritesheetResource?.objectKey ?? undefined,
|
||||
assetObjectId: spritesheetResource?.assetObjectId ?? undefined,
|
||||
generationInputs:
|
||||
spritesheetResource?.generationInputs ?? generationInputs,
|
||||
generationInputs: spritesheetResource?.generationInputs ?? generationInputs,
|
||||
generatedAssetSnapshot: spritesheetAsset,
|
||||
};
|
||||
const iconStartX = startX + spritesheetDisplaySize.width + 32;
|
||||
@@ -348,7 +367,8 @@ export function createIconSpritesheetResultLayers({
|
||||
const generatedIndex = startIndex + index + 1;
|
||||
const layer: CanvasLayer = {
|
||||
id: `layer-icon-${generatedIndex}`,
|
||||
resourceId: resource?.resourceId ?? `local-resource-icon-${generatedIndex}`,
|
||||
resourceId:
|
||||
resource?.resourceId ?? `local-resource-icon-${generatedIndex}`,
|
||||
title: icon.name,
|
||||
src: icon.imageSrc,
|
||||
x: cursorX,
|
||||
@@ -412,11 +432,14 @@ export function createVideoResultLayer({
|
||||
|
||||
return {
|
||||
id: `layer-video-${generatedIndex}`,
|
||||
resourceId: resource?.resourceId ?? `local-resource-video-${generatedIndex}`,
|
||||
resourceId:
|
||||
resource?.resourceId ?? `local-resource-video-${generatedIndex}`,
|
||||
title,
|
||||
src: generated.videoSrc,
|
||||
mediaType: 'video',
|
||||
assetKind: (resource?.assetKind as CanvasLayer['assetKind'] | undefined) ?? assetKind,
|
||||
assetKind:
|
||||
(resource?.assetKind as CanvasLayer['assetKind'] | undefined) ??
|
||||
assetKind,
|
||||
x: frameX ?? worldCenter.x - width / 2,
|
||||
y: frameY ?? worldCenter.y - height / 2,
|
||||
width,
|
||||
@@ -432,7 +455,8 @@ export function createVideoResultLayer({
|
||||
taskId: generated.taskId,
|
||||
objectKey: resource?.objectKey ?? generated.objectKey,
|
||||
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
|
||||
thumbnailSrc: generated.thumbnailSrc ?? generated.asset?.thumbnailSrc ?? undefined,
|
||||
thumbnailSrc:
|
||||
generated.thumbnailSrc ?? generated.asset?.thumbnailSrc ?? undefined,
|
||||
sourceResourceId: sourceLayer?.resourceId,
|
||||
sourceAssetId: asset?.assetId,
|
||||
groupId: sourceLayer?.groupId,
|
||||
|
||||
@@ -250,9 +250,7 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
],
|
||||
),
|
||||
).toEqual({
|
||||
fields: [
|
||||
{ title: '角色设定', value: '主角骑士' },
|
||||
],
|
||||
fields: [{ title: '角色设定', value: '主角骑士' }],
|
||||
references: [
|
||||
{
|
||||
title: '角色规范',
|
||||
@@ -381,9 +379,17 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
buildSpecPrompt('custom', { ...blankSpecValues, customPrompt: '自定义' }),
|
||||
).toBe('自定义');
|
||||
expect(buildQuickEditModelOptions('nano-banana')).toEqual([
|
||||
{
|
||||
label: 'nanobanana2',
|
||||
value: 'gemini-3.1-flash-image-preview',
|
||||
},
|
||||
{ label: 'gpt-image-2', value: 'gpt-image-2' },
|
||||
]);
|
||||
expect(buildQuickEditModelOptions('nanobanana2')).toEqual([
|
||||
{
|
||||
label: 'nanobanana2',
|
||||
value: 'gemini-3.1-flash-image-preview',
|
||||
},
|
||||
{ label: 'gpt-image-2', value: 'gpt-image-2' },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -111,9 +111,7 @@ export const EDITOR_IMAGE_MODEL_OPTIONS = [
|
||||
{ label: 'nanobanana2', value: IMAGE_MODEL_NANOBANANA2 },
|
||||
{ label: 'gpt-image-2', value: IMAGE_MODEL_GPT_IMAGE_2 },
|
||||
] as const;
|
||||
export const QUICK_EDIT_MODEL_OPTIONS = [
|
||||
{ label: 'gpt-image-2', value: IMAGE_MODEL_GPT_IMAGE_2 },
|
||||
] as const;
|
||||
export const QUICK_EDIT_MODEL_OPTIONS = EDITOR_IMAGE_MODEL_OPTIONS;
|
||||
export const EDITOR_IMAGE_DIMENSION_OPTIONS = {
|
||||
[IMAGE_MODEL_NANOBANANA2]: {
|
||||
aspectRatios: ['1:1', '4:3', '3:2', '2:3', '9:16', '16:9'],
|
||||
@@ -557,6 +555,10 @@ export function buildQuickEditModelOptions(currentModel: string) {
|
||||
return options;
|
||||
}
|
||||
|
||||
export function isQuickEditUnsupportedAssetKind(layer: CanvasLayer) {
|
||||
return layer.assetKind === 'icon' || layer.assetKind === 'icon-spritesheet';
|
||||
}
|
||||
|
||||
export function buildCharacterSpecPrompt(values: SpecFormValues) {
|
||||
return [
|
||||
'生成2D 角色美术视觉规范设定图,纯白底板,整齐排布全身标准立绘;固定统一头身比例、勾线粗细恒定;展示待机行走攻击基础动作帧样例,重心对齐不变位,服饰配饰分层结构示意,搭配专属角色色卡标注色号,无多余杂物,精准尺寸标注,高清矢量规范稿',
|
||||
|
||||
@@ -61,6 +61,25 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('trims custom asset names and limits them to 80 characters', () => {
|
||||
const customName = ` ${'名'.repeat(81)} `;
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'generate',
|
||||
prompt: '一张发光主视觉',
|
||||
assetLabel: customName,
|
||||
status: 'idle',
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 3,
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
kind: 'image',
|
||||
result: { title: '名'.repeat(80) },
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes legacy nanobanana aliases before submitting image generation', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
@@ -646,6 +665,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
imageModel: 'gpt-image-2',
|
||||
aspectRatio: '3:2',
|
||||
imageSize: '2K',
|
||||
assetLabel: ' 冒险游戏图标 ',
|
||||
iconSpecReference: {
|
||||
id: 'icon-spec',
|
||||
label: '图标规范',
|
||||
@@ -694,6 +714,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
],
|
||||
},
|
||||
rememberImageModel: 'gpt-image-2',
|
||||
resultTitle: '冒险游戏图标',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -711,6 +732,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
panel: {
|
||||
sourceLayerId: 'character-layer',
|
||||
promptText: ' 循环奔跑动作 ',
|
||||
assetLabel: ' 勇者奔跑 ',
|
||||
resolution: '720p',
|
||||
ratio: 'same',
|
||||
frameCount: 48,
|
||||
@@ -722,6 +744,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
|
||||
expect(plan).toEqual({
|
||||
promptText: '循环奔跑动作',
|
||||
resultTitle: '勇者奔跑',
|
||||
input: {
|
||||
sourceLayerId: 'character-layer',
|
||||
sourceImageSrc: 'generated/character.png',
|
||||
@@ -734,6 +757,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
frameCount: 48,
|
||||
durationSeconds: 6,
|
||||
model: 'seedance2.0-fast',
|
||||
assetLabel: '勇者奔跑',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,20 @@ type ImageGenerationSubmissionOptions = {
|
||||
nextGeneratedIndex: number;
|
||||
};
|
||||
|
||||
export const EDITOR_GENERATED_ASSET_LABEL_MAX_CHARS = 80;
|
||||
|
||||
export function resolveGenerationAssetLabel(
|
||||
assetLabel: string | null | undefined,
|
||||
fallback: string,
|
||||
) {
|
||||
const normalized = assetLabel?.trim();
|
||||
return normalized
|
||||
? Array.from(normalized)
|
||||
.slice(0, EDITOR_GENERATED_ASSET_LABEL_MAX_CHARS)
|
||||
.join('')
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function isSeedanceVideoModel(model: string) {
|
||||
return model === 'seedance2.0' || model === 'seedance2.0-fast';
|
||||
}
|
||||
@@ -147,6 +161,7 @@ export type ImageGenerationSubmissionPlan =
|
||||
kind: 'edit';
|
||||
normalizedPrompt: string;
|
||||
sourceLayer: CanvasLayer;
|
||||
resultTitle: string;
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
}
|
||||
| {
|
||||
@@ -213,10 +228,12 @@ export type IconSpritesheetGenerationSubmissionPlan =
|
||||
input: EditorIconSpritesheetGenerationInput;
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
rememberImageModel: string;
|
||||
resultTitle: string;
|
||||
};
|
||||
|
||||
export type CharacterAnimationSubmissionPlan = {
|
||||
promptText: string;
|
||||
resultTitle: string;
|
||||
input: EditorCharacterAnimationGenerationInput;
|
||||
};
|
||||
|
||||
@@ -239,6 +256,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
kind: 'edit',
|
||||
normalizedPrompt,
|
||||
sourceLayer,
|
||||
resultTitle: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`${sourceLayer.title} 修改结果`,
|
||||
),
|
||||
generationInputs: buildEditGenerationInputs(
|
||||
'修改要求',
|
||||
normalizedPrompt,
|
||||
@@ -266,7 +287,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
model: imageModel,
|
||||
},
|
||||
result: {
|
||||
title: `${sourceLayer.title} 快速编辑`,
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`${sourceLayer.title} 快速编辑`,
|
||||
),
|
||||
assetKind: sourceLayer.assetKind,
|
||||
generationInputs: buildQuickEditGenerationInputs(
|
||||
'快速编辑提示词',
|
||||
@@ -308,7 +332,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
// 中文注释:生成规范菜单里的“图标规范”沿用历史 ui specType,但产物语义应作为图标规范供图标素材引用。
|
||||
assetKind:
|
||||
specType === 'ui' || specType === 'icon' ? 'icon-spec' : 'spec',
|
||||
title: `${SPEC_TYPE_LABEL[specType]} ${nextGeneratedIndex}`,
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`${SPEC_TYPE_LABEL[specType]} ${nextGeneratedIndex}`,
|
||||
),
|
||||
generationInputs: buildSpecGenerationInputs(
|
||||
specType,
|
||||
specValues,
|
||||
@@ -345,7 +372,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
},
|
||||
result: {
|
||||
assetKind: 'character',
|
||||
title: `角色形象 ${nextGeneratedIndex}`,
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`角色形象 ${nextGeneratedIndex}`,
|
||||
),
|
||||
generationInputs: buildCharacterGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.characterSpecReference,
|
||||
@@ -379,7 +409,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
},
|
||||
result: {
|
||||
assetKind: 'ui-design',
|
||||
title: `UI设计图 ${nextGeneratedIndex}`,
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`UI设计图 ${nextGeneratedIndex}`,
|
||||
),
|
||||
generationInputs: buildUiDesignGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.uiDesignSpecReference,
|
||||
@@ -421,7 +454,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
: {}),
|
||||
},
|
||||
result: {
|
||||
title: `${nextGeneratedIndex} 宣发素材`,
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`${nextGeneratedIndex} 宣发素材`,
|
||||
),
|
||||
generationInputs: buildPublicationMaterialsGenerationInputs(
|
||||
dialog.publicationGameInfo,
|
||||
dialog.publicationReferences,
|
||||
@@ -471,7 +507,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
: {}),
|
||||
},
|
||||
result: {
|
||||
title: `生成视频 ${nextGeneratedIndex}`,
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`生成视频 ${nextGeneratedIndex}`,
|
||||
),
|
||||
generationInputs: buildVideoGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.generationReferences,
|
||||
@@ -496,7 +535,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
duration: durationSeconds,
|
||||
},
|
||||
result: {
|
||||
title: `游戏音效 ${nextGeneratedIndex}`,
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`游戏音效 ${nextGeneratedIndex}`,
|
||||
),
|
||||
generationInputs: buildSoundEffectGenerationInputs(
|
||||
normalizedPrompt,
|
||||
soundModel,
|
||||
@@ -516,7 +558,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
makeInstrumental: true,
|
||||
},
|
||||
result: {
|
||||
title: `游戏背景音乐 ${nextGeneratedIndex}`,
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`游戏背景音乐 ${nextGeneratedIndex}`,
|
||||
),
|
||||
generationInputs:
|
||||
buildBackgroundMusicGenerationInputs(normalizedPrompt),
|
||||
},
|
||||
@@ -541,7 +586,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
: {}),
|
||||
},
|
||||
result: {
|
||||
title: `生成图片 ${nextGeneratedIndex}`,
|
||||
title: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`生成图片 ${nextGeneratedIndex}`,
|
||||
),
|
||||
generationInputs: buildImageGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.generationReferences,
|
||||
@@ -553,6 +601,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
|
||||
export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
dialog: GenerateDialogState,
|
||||
nextGeneratedIndex = 1,
|
||||
): IconSpritesheetGenerationSubmissionPlan {
|
||||
const iconDescriptionSource = dialog.prompt.trim()
|
||||
? dialog.prompt.split(/[\r\n,,、;;/|]+/u)
|
||||
@@ -610,6 +659,10 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
dialog.generationReferences,
|
||||
),
|
||||
rememberImageModel,
|
||||
resultTitle: resolveGenerationAssetLabel(
|
||||
dialog.assetLabel,
|
||||
`图标素材图集 ${nextGeneratedIndex}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -621,8 +674,10 @@ export function buildCharacterAnimationSubmissionPlan({
|
||||
sourceLayer: CanvasLayer;
|
||||
}): CharacterAnimationSubmissionPlan {
|
||||
const promptText = panel.promptText.trim();
|
||||
const resultTitle = resolveGenerationAssetLabel(panel.assetLabel, '角色动作');
|
||||
return {
|
||||
promptText,
|
||||
resultTitle,
|
||||
input: {
|
||||
sourceLayerId: sourceLayer.id,
|
||||
sourceImageSrc: resolveCharacterAnimationSourceImageSrc(sourceLayer),
|
||||
@@ -636,6 +691,7 @@ export function buildCharacterAnimationSubmissionPlan({
|
||||
frameCount: panel.frameCount,
|
||||
durationSeconds: panel.durationSeconds,
|
||||
model: CHARACTER_ANIMATION_MODEL,
|
||||
assetLabel: resultTitle,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user