统一画布生成资源命名
新增所有生成面板的资源名称输入并统一80字符边界 贯通画布标题、项目资源、素材库与生成请求的名称传播 为图标图集和角色动作补齐命名契约及中间产物后缀 补充前后端回归测试、OpenAPI与编辑器文档
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',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -267,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"
|
||||
|
||||
@@ -198,6 +198,7 @@ export type GenerateDialogState = {
|
||||
| 'audio-sound-effect'
|
||||
| 'audio-background-music';
|
||||
prompt: string;
|
||||
assetLabel?: string;
|
||||
status: 'idle' | 'generating' | 'failed';
|
||||
composerOpen?: boolean;
|
||||
sourceLayerId?: string;
|
||||
@@ -305,6 +306,7 @@ export type QuickEditPanelState = {
|
||||
mode?: 'quick-edit' | 'redraw';
|
||||
sourceLayerId: string;
|
||||
prompt: string;
|
||||
assetLabel?: string;
|
||||
size: string;
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
@@ -348,6 +350,7 @@ export type CropExpandResizeHandle =
|
||||
export type CharacterAnimationPanelState = {
|
||||
sourceLayerId: string;
|
||||
promptText: string;
|
||||
assetLabel?: string;
|
||||
resolution: EditorCharacterAnimationResolution;
|
||||
ratio: EditorCharacterAnimationRatio;
|
||||
frameCount: EditorCharacterAnimationFrameCount;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
@@ -415,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'];
|
||||
|
||||
@@ -1232,6 +1232,7 @@ export function createQuickEditPanelDraft(
|
||||
export function createQuickEditGenerationDialogDraft({
|
||||
sourceLayer,
|
||||
prompt,
|
||||
assetLabel,
|
||||
status = 'idle',
|
||||
references = [],
|
||||
aspectRatio,
|
||||
@@ -1240,6 +1241,7 @@ export function createQuickEditGenerationDialogDraft({
|
||||
}: {
|
||||
sourceLayer: CanvasLayer;
|
||||
prompt: string;
|
||||
assetLabel?: string;
|
||||
status?: CanvasGenerationDialogState['status'];
|
||||
references?: NonNullable<QuickEditPanelState['quickEditReferences']>;
|
||||
model?: string;
|
||||
@@ -1252,6 +1254,7 @@ export function createQuickEditGenerationDialogDraft({
|
||||
return {
|
||||
mode: 'quick-edit',
|
||||
prompt,
|
||||
assetLabel,
|
||||
status,
|
||||
composerOpen: false,
|
||||
sourceLayerId: sourceLayer.id,
|
||||
|
||||
@@ -39,6 +39,7 @@ type QuickEditResultLayerOptions = {
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
mode?: 'quick-edit' | 'redraw';
|
||||
frame?: GenerateDialogState['placeholder'];
|
||||
title?: string;
|
||||
};
|
||||
|
||||
type IconSpritesheetResultLayerOptions = {
|
||||
@@ -190,6 +191,7 @@ export function createQuickEditResultLayer({
|
||||
generationInputs,
|
||||
mode = 'quick-edit',
|
||||
frame,
|
||||
title,
|
||||
}: QuickEditResultLayerOptions): CanvasLayer {
|
||||
const originalWidth =
|
||||
frame?.originalWidth ||
|
||||
@@ -222,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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
SpecGenerationType,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasGenerationAssetNameField } from './ImageCanvasGenerationAssetNameField';
|
||||
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
||||
import {
|
||||
calculateEditorIconSpritesheetPrice,
|
||||
@@ -223,6 +224,28 @@ export function ImageCanvasIconSpritesheetComposerView({
|
||||
onChange={(event) => onUpdateIconDescriptionText(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value={dialog.assetLabel}
|
||||
disabled={dialog.status === 'generating'}
|
||||
onChange={(assetLabel) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'icon'
|
||||
? {
|
||||
...currentDialog,
|
||||
status:
|
||||
currentDialog.status === 'failed'
|
||||
? 'idle'
|
||||
: currentDialog.status,
|
||||
errorMessage:
|
||||
currentDialog.status === 'failed'
|
||||
? undefined
|
||||
: currentDialog.errorMessage,
|
||||
assetLabel,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
PublicationMaterialsGameInfo,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasGenerationAssetNameField } from './ImageCanvasGenerationAssetNameField';
|
||||
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
||||
import {
|
||||
buildPublicationMaterialsPrompt,
|
||||
@@ -286,6 +287,21 @@ export function ImageCanvasPublicationMaterialsDemoPanelView({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value={dialog.assetLabel}
|
||||
disabled={dialog.status === 'generating'}
|
||||
onChange={(assetLabel) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'publication'
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
assetLabel,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
|
||||
@@ -55,6 +55,7 @@ function createQuickEditDialog(
|
||||
status: panel.status,
|
||||
sourceLayerId: panel.sourceLayerId,
|
||||
generationReferences: panel.quickEditReferences,
|
||||
assetLabel: panel.assetLabel,
|
||||
imageModel: panel.model,
|
||||
aspectRatio: panel.aspectRatio,
|
||||
imageSize: panel.imageSize,
|
||||
@@ -76,6 +77,7 @@ function syncPanelFromDialogUpdate(
|
||||
imageSize: normalizedDialog.imageSize ?? normalizedPanel.imageSize ?? '1K',
|
||||
model: normalizedDialog.imageModel ?? normalizedPanel.model,
|
||||
quickEditReferences: normalizedDialog.generationReferences,
|
||||
assetLabel: normalizedDialog.assetLabel,
|
||||
status: normalizedDialog.status,
|
||||
errorMessage: normalizedDialog.errorMessage,
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
SpecGenerationType,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasGenerationAssetNameField } from './ImageCanvasGenerationAssetNameField';
|
||||
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
||||
import {
|
||||
calculateEditorSpecGenerationPrice,
|
||||
@@ -415,6 +416,30 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ImageCanvasGenerationAssetNameField
|
||||
value={dialog.assetLabel}
|
||||
disabled={isGenerating}
|
||||
onChange={(assetLabel) =>
|
||||
setGenerateDialog?.((currentDialog) =>
|
||||
currentDialog &&
|
||||
(currentDialog.mode === 'spec' ||
|
||||
currentDialog.mode === 'ui-design')
|
||||
? {
|
||||
...currentDialog,
|
||||
status:
|
||||
currentDialog.status === 'failed'
|
||||
? 'idle'
|
||||
: currentDialog.status,
|
||||
errorMessage:
|
||||
currentDialog.status === 'failed'
|
||||
? undefined
|
||||
: currentDialog.errorMessage,
|
||||
assetLabel,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
|
||||
@@ -574,7 +574,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-source:源图:resource-source:-:-:data:image/png;base64,generated',
|
||||
'layer-source:源图 快速编辑:resource-source:-:-:data:image/png;base64,generated',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
||||
@@ -584,6 +584,36 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(screen.getByTestId('fit-count').textContent).toBe('0');
|
||||
});
|
||||
|
||||
it('uses one custom name for quick-edit requests and local layer results', async () => {
|
||||
editEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '改成雨天' }),
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialQuickEditPanel={{
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '改成雨天',
|
||||
assetLabel: ' 雨天勇者 ',
|
||||
size: '1024x768',
|
||||
model: 'gpt-image-2',
|
||||
status: 'idle',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ assetLabel: '雨天勇者' }),
|
||||
);
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-source:雨天勇者:resource-source',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects direct quick edit submission for icon assets', async () => {
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
@@ -699,7 +729,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(generateEditorImageMock).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-source:源图:resource-source',
|
||||
'layer-source:源图 快速编辑:resource-source',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
||||
@@ -756,7 +786,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-source:源图:resource-source:-:-:data:image/png;base64,generated',
|
||||
'layer-source:源图 快速编辑:resource-source:-:-:data:image/png;base64,generated',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('layers').textContent).not.toContain(
|
||||
@@ -972,6 +1002,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
projectId="editor-project-1"
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
assetKind: 'character',
|
||||
@@ -982,6 +1013,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
mode: 'redraw',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: ' 角色换成蓝色披风 ',
|
||||
assetLabel: ' 蓝披风勇者 ',
|
||||
size: '1024x768',
|
||||
model: 'gpt-image-2',
|
||||
status: 'idle',
|
||||
@@ -1002,12 +1034,16 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
model: 'gpt-image-2',
|
||||
referenceImageSrcs: ['resource-source'],
|
||||
assetKind: 'character',
|
||||
assetLabel: '蓝披风勇者',
|
||||
canvasCompletion: expect.objectContaining({
|
||||
title: '蓝披风勇者',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-quick-edit-1:源图 重绘:resource-source:character',
|
||||
'layer-quick-edit-1:蓝披风勇者:resource-source:character',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
||||
@@ -1239,7 +1275,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(generateEditorVideoMock).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-source:角色动作:resource-source:character-animation:image-sequence:data:image/png;base64,action-frame-edited',
|
||||
'layer-source:角色动作 快速编辑:resource-source:character-animation:image-sequence:data:image/png;base64,action-frame-edited',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2125,10 +2161,12 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
projectId="editor-project-1"
|
||||
initialDialog={{
|
||||
id: 'dialog-icon',
|
||||
mode: 'icon',
|
||||
prompt: ' 返回按钮 \n\n 设置按钮 ',
|
||||
assetLabel: ' 复古操作图标 ',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
imageModel: 'gpt-image-2',
|
||||
@@ -2160,12 +2198,16 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
model: 'gpt-image-2',
|
||||
assetLabel: '复古操作图标',
|
||||
canvasCompletion: expect.objectContaining({
|
||||
title: '复古操作图标',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-icon-spritesheet-1:图标素材图集 1:-:icon-spritesheet',
|
||||
'layer-icon-spritesheet-1:复古操作图标:-:icon-spritesheet',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('layers').textContent).not.toContain(
|
||||
@@ -2611,6 +2653,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
projectId="editor-project-1"
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
id: 'character-layer',
|
||||
@@ -2624,6 +2667,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
mode: 'character-animation',
|
||||
sourceLayerId: 'character-layer',
|
||||
prompt: ' 挥手动作 ',
|
||||
assetLabel: ' 勇者挥手 ',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
characterAnimationResolution: '480p',
|
||||
@@ -2642,6 +2686,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
initialCharacterAnimationPanel={{
|
||||
sourceLayerId: 'character-layer',
|
||||
promptText: ' 挥手动作 ',
|
||||
assetLabel: ' 勇者挥手 ',
|
||||
resolution: '480p',
|
||||
ratio: 'same',
|
||||
frameCount: 32,
|
||||
@@ -2661,12 +2706,16 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
sourceImageSrc: 'generated/character.png',
|
||||
promptText: '挥手动作',
|
||||
model: 'seedance2.0-fast',
|
||||
assetLabel: '勇者挥手',
|
||||
canvasCompletion: expect.objectContaining({
|
||||
title: '勇者挥手',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-character-animation-1:角色动作:resource-source:character-animation:image-sequence:data:image/png;base64,action-frame:data:image/png;base64,action-frame:4',
|
||||
'layer-character-animation-1:勇者挥手:resource-source:character-animation:image-sequence:data:image/png;base64,action-frame:data:image/png;base64,action-frame:4',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
buildCharacterAnimationSubmissionPlan,
|
||||
buildIconSpritesheetGenerationSubmissionPlan,
|
||||
buildImageGenerationSubmissionPlan,
|
||||
resolveGenerationAssetLabel,
|
||||
} from './ImageCanvasGenerationSubmissionModel';
|
||||
import type {
|
||||
UiAssetExtractionMark,
|
||||
@@ -680,6 +681,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
options: {
|
||||
frame?: GenerateDialogState['placeholder'];
|
||||
dialogId?: string;
|
||||
title?: string;
|
||||
} = {},
|
||||
) => {
|
||||
if (
|
||||
@@ -697,6 +699,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
generationInputs,
|
||||
mode,
|
||||
frame: options.frame,
|
||||
title: options.title,
|
||||
});
|
||||
|
||||
addGeneratedLayersToCanvas([nextLayer]);
|
||||
@@ -752,15 +755,18 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
sourceLayer: CanvasLayer,
|
||||
generationInputs: CanvasGenerationInputs,
|
||||
dialogId?: string,
|
||||
title?: string,
|
||||
) => {
|
||||
updateSourceLayer(
|
||||
sourceLayer.id,
|
||||
(layer) =>
|
||||
applyImageEditResultToSourceLayer({
|
||||
(layer) => ({
|
||||
...applyImageEditResultToSourceLayer({
|
||||
generated,
|
||||
sourceLayer: layer,
|
||||
generationInputs,
|
||||
}),
|
||||
...(title ? { title } : {}),
|
||||
}),
|
||||
{ fit: false, persist: false },
|
||||
);
|
||||
setQuickEditPanel(null);
|
||||
@@ -1156,8 +1162,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
) => {
|
||||
updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog);
|
||||
};
|
||||
const submissionPlan =
|
||||
buildIconSpritesheetGenerationSubmissionPlan(dialog);
|
||||
const submissionPlan = buildIconSpritesheetGenerationSubmissionPlan(
|
||||
dialog,
|
||||
layerCounterRef.current + 1,
|
||||
);
|
||||
if (submissionPlan.ok === false) {
|
||||
if (canvasDialog) {
|
||||
setSubmittingIconDialog({
|
||||
@@ -1211,11 +1219,12 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
projectId,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: submissionPlan.resultTitle,
|
||||
...(projectId && canvasCompletionPlaceholder
|
||||
? {
|
||||
canvasCompletion: {
|
||||
dialogId: canvasDialog.id,
|
||||
title: '图标素材图集',
|
||||
title: submissionPlan.resultTitle,
|
||||
placeholder: canvasCompletionPlaceholder,
|
||||
},
|
||||
}
|
||||
@@ -1250,6 +1259,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
submissionPlan.generationInputs,
|
||||
canvasCompletionPlaceholder,
|
||||
canvasDialog.id,
|
||||
{ spritesheetTitle: submissionPlan.resultTitle },
|
||||
);
|
||||
} catch (error) {
|
||||
setSubmittingIconDialog({
|
||||
@@ -1265,6 +1275,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
addIconSpritesheetResultLayers,
|
||||
applyProjectSnapshot,
|
||||
getGeneratingDialogPlaceholder,
|
||||
layerCounterRef,
|
||||
rememberImageModel,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
@@ -1289,6 +1300,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}
|
||||
|
||||
const panelMode = quickEditPanel.mode ?? 'quick-edit';
|
||||
const quickEditResultTitle = resolveGenerationAssetLabel(
|
||||
quickEditPanel.assetLabel,
|
||||
`${quickEditSourceLayer.title} ${panelMode === 'redraw' ? '重绘' : '快速编辑'}`,
|
||||
);
|
||||
const quickEditReferences =
|
||||
panelMode === 'redraw' || panelMode === 'quick-edit'
|
||||
? []
|
||||
@@ -1340,6 +1355,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
width: quickEditOutputSize.width,
|
||||
height: quickEditOutputSize.height,
|
||||
},
|
||||
assetLabel: quickEditResultTitle,
|
||||
});
|
||||
const quickEditPlacement = quickEditDraft.placeholder
|
||||
? chooseGenerationPlacement({
|
||||
@@ -1436,12 +1452,12 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
sourceResourceId: quickEditSourceLayer.resourceId,
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
assetFolderId,
|
||||
assetLabel: `${quickEditSourceLayer.title} 快速编辑`,
|
||||
assetLabel: quickEditResultTitle,
|
||||
...(projectId && quickEditCanvasCompletionPlaceholder
|
||||
? {
|
||||
canvasCompletion: {
|
||||
dialogId: quickEditDialogId,
|
||||
title: `${quickEditSourceLayer.title} 快速编辑`,
|
||||
title: quickEditResultTitle,
|
||||
placeholder: quickEditCanvasCompletionPlaceholder,
|
||||
},
|
||||
}
|
||||
@@ -1469,7 +1485,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}
|
||||
addVideoResultLayer(
|
||||
generated,
|
||||
`${quickEditSourceLayer.title} 快速编辑`,
|
||||
quickEditResultTitle,
|
||||
generationInputs,
|
||||
quickEditFrame,
|
||||
quickEditDialogId,
|
||||
@@ -1538,7 +1554,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: `${quickEditSourceLayer.title} 快速编辑`,
|
||||
assetLabel: quickEditResultTitle,
|
||||
sourceResourceId: quickEditSourceLayer.resourceId,
|
||||
targetLayerId: quickEditSourceLayer.id,
|
||||
}),
|
||||
@@ -1560,13 +1576,13 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: `${quickEditSourceLayer.title} 重绘`,
|
||||
assetLabel: quickEditResultTitle,
|
||||
sourceResourceId: quickEditSourceLayer.resourceId,
|
||||
...(projectId && quickEditCanvasCompletionPlaceholder
|
||||
? {
|
||||
canvasCompletion: {
|
||||
dialogId: quickEditDialogId,
|
||||
title: `${quickEditSourceLayer.title} 重绘`,
|
||||
title: quickEditResultTitle,
|
||||
placeholder: quickEditCanvasCompletionPlaceholder,
|
||||
},
|
||||
}
|
||||
@@ -1605,6 +1621,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
quickEditSourceLayer,
|
||||
generationInputs,
|
||||
quickEditDialogId,
|
||||
quickEditResultTitle,
|
||||
);
|
||||
} else {
|
||||
addQuickEditResultLayer(
|
||||
@@ -1616,8 +1633,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
? {
|
||||
frame: quickEditFrame,
|
||||
dialogId: quickEditDialogId,
|
||||
title: quickEditResultTitle,
|
||||
}
|
||||
: {},
|
||||
: { title: quickEditResultTitle },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1728,13 +1746,13 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
assetKind: submissionPlan.sourceLayer.assetKind,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: `${submissionPlan.sourceLayer.title} 修改结果`,
|
||||
assetLabel: submissionPlan.resultTitle,
|
||||
sourceResourceId: submissionPlan.sourceLayer.resourceId,
|
||||
...(projectId && editCanvasCompletionPlaceholder
|
||||
? {
|
||||
canvasCompletion: {
|
||||
dialogId: canvasDialog?.id,
|
||||
title: `${submissionPlan.sourceLayer.title} 修改结果`,
|
||||
title: submissionPlan.resultTitle,
|
||||
placeholder: editCanvasCompletionPlaceholder,
|
||||
},
|
||||
}
|
||||
@@ -1759,6 +1777,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}
|
||||
addGeneratedResultLayer(generated, {
|
||||
sourceLayer: submissionPlan.sourceLayer,
|
||||
title: submissionPlan.resultTitle,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
});
|
||||
} else if (submissionPlan.kind === 'quick-edit') {
|
||||
@@ -1829,6 +1848,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
submissionPlan.sourceLayer,
|
||||
submissionPlan.result.generationInputs,
|
||||
canvasDialog?.id,
|
||||
submissionPlan.result.title,
|
||||
);
|
||||
} else if (submissionPlan.kind === 'video') {
|
||||
const canvasCompletionPlaceholder =
|
||||
@@ -2087,6 +2107,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
characterAnimationFrameCount: characterAnimationPanel.frameCount,
|
||||
characterAnimationDurationSeconds:
|
||||
characterAnimationPanel.durationSeconds,
|
||||
assetLabel: submissionPlan.resultTitle,
|
||||
characterAnimationResult: undefined,
|
||||
status: 'generating',
|
||||
composerOpen: false,
|
||||
@@ -2096,6 +2117,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const nextPanel = {
|
||||
...characterAnimationPanel,
|
||||
promptText: submissionPlan.promptText,
|
||||
assetLabel: submissionPlan.resultTitle,
|
||||
status: 'generating' as const,
|
||||
errorMessage: undefined,
|
||||
result: undefined,
|
||||
@@ -2133,7 +2155,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
? {
|
||||
canvasCompletion: {
|
||||
dialogId: canvasDialog.id,
|
||||
title: '角色动作',
|
||||
title: submissionPlan.resultTitle,
|
||||
placeholder: canvasCompletionPlaceholder,
|
||||
},
|
||||
}
|
||||
@@ -2159,7 +2181,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
if (canvasDialog) {
|
||||
addCharacterAnimationResultLayer(
|
||||
result,
|
||||
'角色动作',
|
||||
submissionPlan.resultTitle,
|
||||
generationInputs,
|
||||
canvasCompletionPlaceholder,
|
||||
canvasDialog.id,
|
||||
|
||||
@@ -282,6 +282,9 @@ function GenerationWorkflowHarness({
|
||||
? `${workflow.characterAnimationPanel.sourceLayerId}:${workflow.characterAnimationPanel.status}`
|
||||
: '-'}
|
||||
</span>
|
||||
<span data-testid="character-animation-name">
|
||||
{`${workflow.characterAnimationPanel?.assetLabel ?? '-'}:${activeDialog?.assetLabel ?? '-'}`}
|
||||
</span>
|
||||
<span data-testid="character-animation-price">
|
||||
{workflow.characterAnimationPrice}
|
||||
</span>
|
||||
@@ -535,6 +538,18 @@ function GenerationWorkflowHarness({
|
||||
>
|
||||
打开普通动画
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
workflow.setCharacterAnimationPanel((currentPanel) =>
|
||||
currentPanel
|
||||
? { ...currentPanel, assetLabel: '勇者挥手' }
|
||||
: currentPanel,
|
||||
)
|
||||
}
|
||||
>
|
||||
命名角色动画
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -1675,7 +1690,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-source:源图:resource-source:-:data:image/png;base64,generated:task-generated',
|
||||
'layer-source:源图 快速编辑:resource-source:-:data:image/png;base64,generated:task-generated',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
||||
@@ -2121,6 +2136,17 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect(screen.getByTestId('image-context').textContent).toBe('-');
|
||||
});
|
||||
|
||||
it('keeps character animation asset names synchronized with the dialog', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开角色动画' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '命名角色动画' }));
|
||||
|
||||
expect(screen.getByTestId('character-animation-name').textContent).toBe(
|
||||
'勇者挥手:勇者挥手',
|
||||
);
|
||||
});
|
||||
|
||||
it('hides non-generating canvas generation composers without deleting placeholders', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
|
||||
@@ -486,6 +486,7 @@ function createCharacterAnimationPanelFromDialog(
|
||||
return {
|
||||
sourceLayerId: dialog.sourceLayerId,
|
||||
promptText: dialog.prompt,
|
||||
assetLabel: dialog.assetLabel,
|
||||
resolution: dialog.characterAnimationResolution ?? '480p',
|
||||
ratio: dialog.characterAnimationRatio ?? 'same',
|
||||
frameCount: dialog.characterAnimationFrameCount ?? 32,
|
||||
@@ -519,6 +520,7 @@ function applyCharacterAnimationPanelToDialog(
|
||||
return {
|
||||
...dialog,
|
||||
prompt: panel.promptText,
|
||||
assetLabel: panel.assetLabel,
|
||||
characterAnimationResolution: panel.resolution,
|
||||
characterAnimationRatio: panel.ratio,
|
||||
characterAnimationFrameCount: panel.frameCount,
|
||||
|
||||
@@ -7246,6 +7246,18 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-asset-name-field {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
gap: 0.24rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-asset-name-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-prompt--borderless {
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
|
||||
@@ -912,6 +912,7 @@ describe('editorProjectClient', () => {
|
||||
referenceImageSrc: 'data:image/png;base64,spec',
|
||||
referenceImageSrcs: ['data:image/png;base64,icon-ref'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
assetLabel: '冒险游戏图标',
|
||||
});
|
||||
|
||||
expect(result.taskId).toBe('icon-spritesheet-task-1');
|
||||
@@ -929,6 +930,7 @@ describe('editorProjectClient', () => {
|
||||
referenceImageSrcs: ['data:image/png;base64,icon-ref'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
assetLabel: '冒险游戏图标',
|
||||
}),
|
||||
}),
|
||||
'生成图标素材失败',
|
||||
@@ -1256,6 +1258,7 @@ describe('editorProjectClient', () => {
|
||||
frameCount: 32,
|
||||
durationSeconds: 4,
|
||||
model: 'seedance2.0-fast',
|
||||
assetLabel: '勇者待机',
|
||||
});
|
||||
|
||||
expect(result.taskId).toBe('character-animation-1');
|
||||
@@ -1275,6 +1278,7 @@ describe('editorProjectClient', () => {
|
||||
frameCount: 32,
|
||||
durationSeconds: 4,
|
||||
model: 'seedance2.0-fast',
|
||||
assetLabel: '勇者待机',
|
||||
}),
|
||||
}),
|
||||
'生成角色动画失败',
|
||||
|
||||
@@ -203,6 +203,7 @@ export type EditorIconSpritesheetGenerationInput = {
|
||||
projectId?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
assetLabel?: string | null;
|
||||
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
|
||||
};
|
||||
|
||||
@@ -360,6 +361,7 @@ export type EditorCharacterAnimationGenerationInput = {
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
sourceResourceId?: string | null;
|
||||
assetFolderId?: string | null;
|
||||
assetLabel?: string | null;
|
||||
};
|
||||
|
||||
export type EditorCharacterAnimationFrameResult = {
|
||||
@@ -935,6 +937,7 @@ export async function generateEditorIconSpritesheet(
|
||||
? { generationInputs: input.generationInputs }
|
||||
: {}),
|
||||
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
|
||||
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
|
||||
...(input.canvasCompletion
|
||||
? { canvasCompletion: input.canvasCompletion }
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user