按来源生成器打开快速编辑和改造
快速编辑和改造优先恢复图层来源生成器类型 按原生成器草稿恢复普通图片、角色、图标、规范、UI、宣发和动作参数 补充同源生成器恢复与画布工作流回归测试
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
createPublicationGenerationDialogDraft,
|
||||
createQuickEditPanelDraft,
|
||||
createRedrawPanelDraft,
|
||||
createSameSourceGenerationDialogDraft,
|
||||
createSoundEffectGenerationDialogDraft,
|
||||
createSpecDialogDraft,
|
||||
createUiDesignGenerationDialogDraft,
|
||||
@@ -490,6 +491,108 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('creates same-source generator drafts for generated image layers', () => {
|
||||
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: '红发骑士' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
canvasSize,
|
||||
viewport,
|
||||
mode: 'redraw',
|
||||
});
|
||||
|
||||
expect(characterDraft).toMatchObject({
|
||||
mode: 'character',
|
||||
prompt: '红发骑士',
|
||||
sourceLayerId: 'layer-source',
|
||||
placeholder: {
|
||||
x: 472,
|
||||
y: 140,
|
||||
},
|
||||
});
|
||||
|
||||
const publicationDraft = createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
assetKind: 'publication-material',
|
||||
originalWidth: 720,
|
||||
originalHeight: 1280,
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '游戏名', value: '星海远征' },
|
||||
{ title: '游戏分类', value: '冒险' },
|
||||
{ title: '一句话描述游戏', value: '驾驶飞船探索星云' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
canvasSize,
|
||||
viewport,
|
||||
mode: 'redraw',
|
||||
});
|
||||
|
||||
expect(publicationDraft).toMatchObject({
|
||||
mode: 'publication',
|
||||
publicationWorkflowId: 'publication-detail-gallery',
|
||||
publicationGameInfo: {
|
||||
gameName: '星海远征',
|
||||
gameCategories: '冒险',
|
||||
gameDescription: '驾驶飞船探索星云',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer(),
|
||||
canvasSize,
|
||||
viewport,
|
||||
mode: 'redraw',
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('restores the plain image generator when a source generation dialog exists', () => {
|
||||
const canvasSize = { width: 960, height: 720 };
|
||||
const viewport = { x: 0, y: 0, scale: 1 };
|
||||
|
||||
expect(
|
||||
createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
prompt: '【系统内置】普通提示词',
|
||||
generationInputs: {
|
||||
fields: [{ title: '生成提示词', value: '普通图' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
canvasSize,
|
||||
viewport,
|
||||
sourceDialog: {
|
||||
id: 'generation-dialog-1',
|
||||
mode: 'generate',
|
||||
prompt: '普通图',
|
||||
status: 'idle',
|
||||
generatedLayerId: 'layer-source',
|
||||
imageModel: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
},
|
||||
mode: 'redraw',
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'generate',
|
||||
prompt: '普通图',
|
||||
sourceLayerId: 'layer-source',
|
||||
imageModel: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes legacy nanobanana model aliases in quick edit drafts', () => {
|
||||
expect(
|
||||
createQuickEditPanelDraft(
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
CHARACTER_ANIMATION_FRAME_DISPLAY_SIZE,
|
||||
CHARACTER_ANIMATION_FRAME_ORIGINAL_SIZE,
|
||||
createCanvasLayerReference,
|
||||
DEFAULT_ICON_DESCRIPTIONS,
|
||||
DEFAULT_IMAGE_MODEL,
|
||||
DEFAULT_PUBLICATION_GAME_INFO,
|
||||
DEFAULT_SOUND_EFFECT_DURATION_SECONDS,
|
||||
@@ -45,6 +44,14 @@ import {
|
||||
import { getPublicationMaterialsWorkflow } from './ImageCanvasPublicationMaterialsModel';
|
||||
|
||||
type CanvasSize = { width: number; height: number };
|
||||
type SourceGenerationDialogDraftContext = {
|
||||
sourceLayer: CanvasLayer;
|
||||
canvasSize: CanvasSize;
|
||||
viewport: CanvasViewport;
|
||||
sourceDialog?: CanvasGenerationDialogState | null;
|
||||
mode: 'quick-edit' | 'redraw';
|
||||
sourceAnimationLayer?: CanvasLayer | null;
|
||||
};
|
||||
const VIDEO_REFERENCE_LIMITS = {
|
||||
image: 9,
|
||||
video: 3,
|
||||
@@ -156,6 +163,18 @@ function resolveImageDimensionDefaults(imageModel: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function getGenerationInputFieldValues(layer: CanvasLayer) {
|
||||
const values = new Map<string, string>();
|
||||
for (const field of layer.generationInputs?.fields ?? []) {
|
||||
const title = field.title.trim().toLowerCase();
|
||||
const value = field.value.trim();
|
||||
if (title && value && !values.has(title)) {
|
||||
values.set(title, value);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export function createGenerateDialogDraft({
|
||||
canvasSize,
|
||||
viewport,
|
||||
@@ -420,7 +439,7 @@ export function createBackgroundMusicGenerationDialogDraft({
|
||||
|
||||
function findGenerationInputFieldValue(
|
||||
layer: CanvasLayer,
|
||||
titles: string[],
|
||||
titles: readonly string[],
|
||||
) {
|
||||
const normalizedTitles = new Set(titles.map((title) => title.toLowerCase()));
|
||||
return (
|
||||
@@ -594,6 +613,423 @@ export function createVideoRedrawGenerationDialogDraft(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolveGeneratedSourceDialogMode({
|
||||
sourceLayer,
|
||||
sourceDialog,
|
||||
}: {
|
||||
sourceLayer: CanvasLayer;
|
||||
sourceDialog?: CanvasGenerationDialogState | null;
|
||||
}): CanvasGenerationDialogState['mode'] | null {
|
||||
if (sourceDialog) {
|
||||
return sourceDialog.mode;
|
||||
}
|
||||
if (sourceLayer.assetKind === 'character') {
|
||||
return 'character';
|
||||
}
|
||||
if (sourceLayer.assetKind === 'ui-design') {
|
||||
return 'ui-design';
|
||||
}
|
||||
if (sourceLayer.assetKind === 'publication-material') {
|
||||
return 'publication';
|
||||
}
|
||||
if (
|
||||
sourceLayer.assetKind === 'icon' ||
|
||||
sourceLayer.assetKind === 'icon-spritesheet'
|
||||
) {
|
||||
return 'icon';
|
||||
}
|
||||
if (
|
||||
sourceLayer.assetKind === 'spec' ||
|
||||
sourceLayer.assetKind === 'icon-spec'
|
||||
) {
|
||||
return 'spec';
|
||||
}
|
||||
if (sourceLayer.assetKind === 'character-animation') {
|
||||
return 'character-animation';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSourceDraftPrompt({
|
||||
sourceLayer,
|
||||
mode,
|
||||
}: {
|
||||
sourceLayer: CanvasLayer;
|
||||
mode: SourceGenerationDialogDraftContext['mode'];
|
||||
}) {
|
||||
if (mode === 'quick-edit') {
|
||||
return '';
|
||||
}
|
||||
return resolveUserGenerationPromptSnapshot(sourceLayer);
|
||||
}
|
||||
|
||||
function buildSourceSidePlaceholder(sourceLayer: CanvasLayer) {
|
||||
return {
|
||||
x: sourceLayer.x + sourceLayer.width + 32,
|
||||
y: sourceLayer.y,
|
||||
};
|
||||
}
|
||||
|
||||
function placeDraftBesideSourceLayer<T extends Omit<CanvasGenerationDialogState, 'id'>>(
|
||||
draft: T,
|
||||
sourceLayer: CanvasLayer,
|
||||
): T {
|
||||
if (!draft.placeholder) {
|
||||
return draft;
|
||||
}
|
||||
return {
|
||||
...draft,
|
||||
placeholder: {
|
||||
...draft.placeholder,
|
||||
...buildSourceSidePlaceholder(sourceLayer),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function restoreSharedImageOptions(
|
||||
draft: Omit<CanvasGenerationDialogState, 'id'>,
|
||||
sourceLayer: CanvasLayer,
|
||||
sourceDialog?: CanvasGenerationDialogState | null,
|
||||
): Omit<CanvasGenerationDialogState, 'id'> {
|
||||
return {
|
||||
...draft,
|
||||
imageModel: sourceDialog?.imageModel ?? draft.imageModel,
|
||||
aspectRatio:
|
||||
sourceDialog?.aspectRatio ??
|
||||
draft.aspectRatio ??
|
||||
inferEditorImageAspectRatio(
|
||||
sourceLayer.originalWidth,
|
||||
sourceLayer.originalHeight,
|
||||
),
|
||||
imageSize:
|
||||
sourceDialog?.imageSize ??
|
||||
draft.imageSize ??
|
||||
inferEditorImageSizeLabel(
|
||||
sourceLayer.originalWidth,
|
||||
sourceLayer.originalHeight,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSpecTypeFromSourceLayer(
|
||||
sourceLayer: CanvasLayer,
|
||||
sourceDialog?: CanvasGenerationDialogState | null,
|
||||
): SpecGenerationType {
|
||||
if (sourceDialog?.mode === 'spec' && sourceDialog.specType) {
|
||||
return sourceDialog.specType;
|
||||
}
|
||||
if (sourceLayer.assetKind === 'icon-spec' || isIconSpecReferenceLayer(sourceLayer)) {
|
||||
return 'icon';
|
||||
}
|
||||
const fields = getGenerationInputFieldValues(sourceLayer);
|
||||
if (fields.has('自定义规范提示词')) {
|
||||
return 'custom';
|
||||
}
|
||||
if (fields.has('头身比') || fields.has('角色视角')) {
|
||||
return 'character';
|
||||
}
|
||||
return 'ui';
|
||||
}
|
||||
|
||||
function restoreSpecValuesFromLayer(
|
||||
draft: Omit<CanvasGenerationDialogState, 'id'>,
|
||||
sourceLayer: CanvasLayer,
|
||||
specType: SpecGenerationType,
|
||||
sourceDialog?: CanvasGenerationDialogState | null,
|
||||
): Omit<CanvasGenerationDialogState, 'id'> {
|
||||
const fields = getGenerationInputFieldValues(sourceLayer);
|
||||
const defaultValues = DEFAULT_SPEC_FORM_VALUES[specType];
|
||||
return {
|
||||
...draft,
|
||||
specValues: {
|
||||
...defaultValues,
|
||||
...sourceDialog?.specValues,
|
||||
playSetting:
|
||||
fields.get('玩法设定') ??
|
||||
sourceDialog?.specValues?.playSetting ??
|
||||
defaultValues.playSetting,
|
||||
artStyle:
|
||||
fields.get('美术风格') ??
|
||||
sourceDialog?.specValues?.artStyle ??
|
||||
defaultValues.artStyle,
|
||||
bodyRatio:
|
||||
fields.get('头身比') ??
|
||||
sourceDialog?.specValues?.bodyRatio ??
|
||||
defaultValues.bodyRatio,
|
||||
characterView:
|
||||
fields.get('角色视角') ??
|
||||
sourceDialog?.specValues?.characterView ??
|
||||
defaultValues.characterView,
|
||||
customPrompt:
|
||||
fields.get('自定义规范提示词') ??
|
||||
sourceDialog?.specValues?.customPrompt ??
|
||||
defaultValues.customPrompt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePublicationWorkflowIdFromLayer(
|
||||
sourceLayer: CanvasLayer,
|
||||
sourceDialog?: CanvasGenerationDialogState | null,
|
||||
): PublicationMaterialsWorkflowId {
|
||||
if (sourceDialog?.mode === 'publication' && sourceDialog.publicationWorkflowId) {
|
||||
return sourceDialog.publicationWorkflowId;
|
||||
}
|
||||
const ratio = inferEditorImageAspectRatio(
|
||||
sourceLayer.originalWidth,
|
||||
sourceLayer.originalHeight,
|
||||
);
|
||||
if (ratio === '9:16') {
|
||||
return 'publication-detail-gallery';
|
||||
}
|
||||
if (ratio === '16:9') {
|
||||
return 'publication-promo-poster';
|
||||
}
|
||||
return 'publication-cover-image';
|
||||
}
|
||||
|
||||
function restorePublicationGameInfoFromLayer(
|
||||
draft: Omit<CanvasGenerationDialogState, 'id'>,
|
||||
sourceLayer: CanvasLayer,
|
||||
sourceDialog?: CanvasGenerationDialogState | null,
|
||||
): Omit<CanvasGenerationDialogState, 'id'> {
|
||||
const fields = getGenerationInputFieldValues(sourceLayer);
|
||||
return {
|
||||
...draft,
|
||||
publicationGameInfo: {
|
||||
...DEFAULT_PUBLICATION_GAME_INFO,
|
||||
...sourceDialog?.publicationGameInfo,
|
||||
gameName:
|
||||
fields.get('游戏名') ??
|
||||
sourceDialog?.publicationGameInfo?.gameName ??
|
||||
DEFAULT_PUBLICATION_GAME_INFO.gameName,
|
||||
gameCategories:
|
||||
fields.get('游戏分类') ??
|
||||
sourceDialog?.publicationGameInfo?.gameCategories ??
|
||||
DEFAULT_PUBLICATION_GAME_INFO.gameCategories,
|
||||
gameDescription:
|
||||
fields.get('一句话描述游戏') ??
|
||||
sourceDialog?.publicationGameInfo?.gameDescription ??
|
||||
DEFAULT_PUBLICATION_GAME_INFO.gameDescription,
|
||||
},
|
||||
publicationReferences: sourceDialog?.publicationReferences ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function restoreIconDescriptionsFromLayer(
|
||||
draft: Omit<CanvasGenerationDialogState, 'id'>,
|
||||
sourceLayer: CanvasLayer,
|
||||
sourceDialog?: CanvasGenerationDialogState | null,
|
||||
): Omit<CanvasGenerationDialogState, 'id'> {
|
||||
const descriptions = parseIconDescriptionsText(
|
||||
findGenerationInputFieldValue(sourceLayer, ['素材描述']) ??
|
||||
sourceDialog?.prompt ??
|
||||
'',
|
||||
);
|
||||
return {
|
||||
...draft,
|
||||
prompt: descriptions.join('\n'),
|
||||
iconDescriptions: descriptions.length
|
||||
? descriptions
|
||||
: (sourceDialog?.iconDescriptions ?? draft.iconDescriptions),
|
||||
iconSpecReference: sourceDialog?.iconSpecReference ?? draft.iconSpecReference,
|
||||
};
|
||||
}
|
||||
|
||||
function createSameSourceCharacterAnimationDraft({
|
||||
sourceLayer,
|
||||
canvasSize,
|
||||
viewport,
|
||||
sourceAnimationLayer,
|
||||
mode,
|
||||
}: SourceGenerationDialogDraftContext): Omit<CanvasGenerationDialogState, 'id'> | null {
|
||||
const animationSourceLayer = sourceAnimationLayer ?? sourceLayer;
|
||||
const draft = createCharacterAnimationGenerationDialogDraft({
|
||||
canvasSize,
|
||||
viewport,
|
||||
layer: animationSourceLayer,
|
||||
});
|
||||
if (!draft) {
|
||||
return null;
|
||||
}
|
||||
return placeDraftBesideSourceLayer(
|
||||
{
|
||||
...draft,
|
||||
prompt: getSourceDraftPrompt({ sourceLayer, mode }),
|
||||
sourceLayerId: animationSourceLayer.id,
|
||||
composerOpen: true,
|
||||
},
|
||||
sourceLayer,
|
||||
);
|
||||
}
|
||||
|
||||
export function createSameSourceGenerationDialogDraft({
|
||||
sourceLayer,
|
||||
canvasSize,
|
||||
viewport,
|
||||
sourceDialog,
|
||||
mode,
|
||||
sourceAnimationLayer,
|
||||
}: SourceGenerationDialogDraftContext): Omit<CanvasGenerationDialogState, 'id'> | null {
|
||||
const sourceMode = resolveGeneratedSourceDialogMode({
|
||||
sourceLayer,
|
||||
sourceDialog,
|
||||
});
|
||||
const prompt = getSourceDraftPrompt({ sourceLayer, mode });
|
||||
|
||||
if (sourceMode === 'quick-edit') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sourceMode === 'generate') {
|
||||
return placeDraftBesideSourceLayer(
|
||||
restoreSharedImageOptions(
|
||||
{
|
||||
...createGenerateDialogDraft({ canvasSize, viewport }),
|
||||
prompt,
|
||||
sourceLayerId: sourceLayer.id,
|
||||
generationReferences:
|
||||
sourceDialog?.mode === 'generate'
|
||||
? (sourceDialog.generationReferences ?? [])
|
||||
: [],
|
||||
},
|
||||
sourceLayer,
|
||||
sourceDialog,
|
||||
),
|
||||
sourceLayer,
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceMode === 'character-animation') {
|
||||
return createSameSourceCharacterAnimationDraft({
|
||||
sourceLayer,
|
||||
canvasSize,
|
||||
viewport,
|
||||
sourceDialog,
|
||||
mode,
|
||||
sourceAnimationLayer,
|
||||
});
|
||||
}
|
||||
|
||||
if (sourceMode === 'character') {
|
||||
return placeDraftBesideSourceLayer(
|
||||
restoreSharedImageOptions(
|
||||
{
|
||||
...createCharacterGenerationDialogDraft({
|
||||
canvasSize,
|
||||
viewport,
|
||||
imageModel: sourceDialog?.imageModel ?? sourceLayer.model ?? DEFAULT_IMAGE_MODEL,
|
||||
}),
|
||||
prompt,
|
||||
sourceLayerId: sourceLayer.id,
|
||||
characterSpecReference:
|
||||
sourceDialog?.mode === 'character'
|
||||
? sourceDialog.characterSpecReference
|
||||
: null,
|
||||
characterReferences:
|
||||
sourceDialog?.mode === 'character'
|
||||
? (sourceDialog.characterReferences ?? [])
|
||||
: [],
|
||||
},
|
||||
sourceLayer,
|
||||
sourceDialog,
|
||||
),
|
||||
sourceLayer,
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceMode === 'ui-design') {
|
||||
return placeDraftBesideSourceLayer(
|
||||
restoreSharedImageOptions(
|
||||
{
|
||||
...createUiDesignGenerationDialogDraft({
|
||||
canvasSize,
|
||||
viewport,
|
||||
imageModel: sourceDialog?.imageModel ?? IMAGE_MODEL_GPT_IMAGE_2,
|
||||
}),
|
||||
prompt,
|
||||
sourceLayerId: sourceLayer.id,
|
||||
uiDesignSpecReference:
|
||||
sourceDialog?.mode === 'ui-design'
|
||||
? sourceDialog.uiDesignSpecReference
|
||||
: null,
|
||||
},
|
||||
sourceLayer,
|
||||
sourceDialog,
|
||||
),
|
||||
sourceLayer,
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceMode === 'publication') {
|
||||
const workflowId = resolvePublicationWorkflowIdFromLayer(
|
||||
sourceLayer,
|
||||
sourceDialog,
|
||||
);
|
||||
return placeDraftBesideSourceLayer(
|
||||
restorePublicationGameInfoFromLayer(
|
||||
{
|
||||
...createPublicationGenerationDialogDraft({
|
||||
canvasSize,
|
||||
viewport,
|
||||
workflowId,
|
||||
}),
|
||||
prompt,
|
||||
sourceLayerId: sourceLayer.id,
|
||||
},
|
||||
sourceLayer,
|
||||
sourceDialog,
|
||||
),
|
||||
sourceLayer,
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceMode === 'icon') {
|
||||
return placeDraftBesideSourceLayer(
|
||||
restoreSharedImageOptions(
|
||||
restoreIconDescriptionsFromLayer(
|
||||
{
|
||||
...createIconGenerationDialogDraft({
|
||||
canvasSize,
|
||||
viewport,
|
||||
imageModel:
|
||||
sourceDialog?.imageModel ?? sourceLayer.model ?? DEFAULT_IMAGE_MODEL,
|
||||
}),
|
||||
sourceLayerId: sourceLayer.id,
|
||||
},
|
||||
sourceLayer,
|
||||
sourceDialog,
|
||||
),
|
||||
sourceLayer,
|
||||
sourceDialog,
|
||||
),
|
||||
sourceLayer,
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceMode === 'spec') {
|
||||
const specType = resolveSpecTypeFromSourceLayer(sourceLayer, sourceDialog);
|
||||
return placeDraftBesideSourceLayer(
|
||||
restoreSpecValuesFromLayer(
|
||||
{
|
||||
...createSpecDialogDraft({ canvasSize, viewport, specType }),
|
||||
prompt,
|
||||
sourceLayerId: sourceLayer.id,
|
||||
specReference:
|
||||
sourceDialog?.mode === 'spec' ? sourceDialog.specReference : null,
|
||||
},
|
||||
sourceLayer,
|
||||
specType,
|
||||
sourceDialog,
|
||||
),
|
||||
sourceLayer,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createUiDesignGenerationDialogDraft({
|
||||
canvasSize,
|
||||
viewport,
|
||||
|
||||
@@ -354,6 +354,72 @@ function GenerationWorkflowHarness({
|
||||
>
|
||||
打开用户快照改造
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
workflow.openQuickEditPanel(
|
||||
createLayer({
|
||||
id: 'layer-character-generated',
|
||||
title: '角色形象',
|
||||
assetKind: 'character',
|
||||
prompt: '【系统内置】角色提示词',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '红发骑士' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
快速编辑角色图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
workflow.openRedrawPanel(
|
||||
createLayer({
|
||||
id: 'layer-publication-generated',
|
||||
title: '宣发素材',
|
||||
assetKind: 'publication-material',
|
||||
originalWidth: 720,
|
||||
originalHeight: 1280,
|
||||
width: 180,
|
||||
height: 320,
|
||||
prompt: '【系统内置】宣发提示词',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '游戏名', value: '星海远征' },
|
||||
{ title: '游戏分类', value: '冒险' },
|
||||
{ title: '一句话描述游戏', value: '驾驶飞船探索星云' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
改造宣发图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
dialogs.openCanvasGenerationDialog({
|
||||
mode: 'generate',
|
||||
prompt: '普通图',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
generatedLayerId: 'layer-source',
|
||||
imageModel: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
})
|
||||
}
|
||||
>
|
||||
写入普通图来源生成器
|
||||
</button>
|
||||
<button type="button" onClick={() => workflow.openQuickEditPanel(layers[0]!)}>
|
||||
快速编辑来源普通图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => workflow.openCropExpandPanel(layers[0]!)}
|
||||
@@ -743,6 +809,69 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the source generator for generated character quick edit', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '快速编辑角色图' }));
|
||||
|
||||
expect(screen.getByTestId('tool').textContent).toBe('character');
|
||||
expect(screen.getByTestId('selected').textContent).toBe('-');
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'character:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('dialog-prompt').textContent).toBe('-');
|
||||
expect(screen.getByTestId('quick-edit-selection').textContent).toBe('-');
|
||||
});
|
||||
|
||||
it('opens the source generator for generated publication redraw', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '改造宣发图' }));
|
||||
|
||||
expect(screen.getByTestId('tool').textContent).toBe('publication');
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'publication:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('dialog-prompt').textContent).toBe(
|
||||
'游戏名:星海远征\n游戏分类:冒险\n一句话描述游戏:驾驶飞船探索星云',
|
||||
);
|
||||
expect(screen.getByTestId('dialog-image-options').textContent).toBe(
|
||||
'publication:gpt-image-2:9:16:1K',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the source generator for plain generated images with a source dialog', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '写入普通图来源生成器' }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '快速编辑来源普通图' }));
|
||||
|
||||
expect(screen.getByTestId('tool').textContent).toBe('generate');
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'generate:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('dialog-image-options').textContent).toBe(
|
||||
'generate:gpt-image-2:2:3:2K',
|
||||
);
|
||||
expect(screen.getByTestId('quick-edit-selection').textContent).toBe('-');
|
||||
});
|
||||
|
||||
it('keeps plain images without a source generator on quick edit generator', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
|
||||
|
||||
expect(screen.getByTestId('tool').textContent).toBe('generate');
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'quick-edit:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('quick-edit-selection').textContent).toBe(
|
||||
'layer-source:none:0',
|
||||
);
|
||||
});
|
||||
|
||||
it('calculates character animation button price from the model pricing config', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
@@ -885,10 +1014,10 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
|
||||
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'quick-edit:idle:open:-:placeholder',
|
||||
'generate:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('dialog-image-options').textContent).toBe(
|
||||
'quick-edit:gpt-image-2:2:3:2K',
|
||||
'generate:gpt-image-2:2:3:2K',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -911,10 +1040,10 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开图片改造' }));
|
||||
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'quick-edit:idle:open:-:placeholder',
|
||||
'generate:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('dialog-image-options').textContent).toBe(
|
||||
'quick-edit:gpt-image-2:2:3:2K',
|
||||
'generate:gpt-image-2:2:3:2K',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
|
||||
import type {
|
||||
EditorAssetSnapshot,
|
||||
EditorProjectSnapshot,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
|
||||
import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
@@ -52,11 +52,12 @@ import {
|
||||
createQuickEditGenerationDialogDraft,
|
||||
createQuickEditPanelDraft,
|
||||
createRedrawPanelDraft,
|
||||
createSameSourceGenerationDialogDraft,
|
||||
createSoundEffectGenerationDialogDraft,
|
||||
createSpecDialogDraft,
|
||||
createUiDesignGenerationDialogDraft,
|
||||
createVideoRedrawGenerationDialogDraft,
|
||||
createVideoGenerationDialogDraft,
|
||||
createVideoRedrawGenerationDialogDraft,
|
||||
hideGeneratedLayerComposerAfterBlur,
|
||||
isCharacterSpecReferenceLayer,
|
||||
isIconSpecReferenceLayer,
|
||||
@@ -121,6 +122,60 @@ type RememberedImageGenerationOptions = {
|
||||
imageSize: string;
|
||||
};
|
||||
|
||||
function findSourceGenerationDialog(
|
||||
dialogs: CanvasGenerationDialogState[],
|
||||
sourceLayer: CanvasLayer,
|
||||
) {
|
||||
return [...dialogs].reverse().find(
|
||||
(dialog) =>
|
||||
dialog.generatedLayerId === sourceLayer.id ||
|
||||
(!dialog.generatedLayerId && dialog.sourceLayerId === sourceLayer.id),
|
||||
);
|
||||
}
|
||||
|
||||
function findSourceAnimationLayer(layers: CanvasLayer[], sourceLayer: CanvasLayer) {
|
||||
if (sourceLayer.assetKind !== 'character-animation') {
|
||||
return null;
|
||||
}
|
||||
const sourceResourceId = sourceLayer.sourceResourceId?.trim();
|
||||
if (!sourceResourceId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
layers.find(
|
||||
(layer) =>
|
||||
layer.resourceId === sourceResourceId && layer.assetKind === 'character',
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function getCanvasToolForGenerationMode(
|
||||
mode: CanvasGenerationDialogState['mode'],
|
||||
): CanvasTool {
|
||||
if (mode === 'video') {
|
||||
return 'video';
|
||||
}
|
||||
if (mode === 'audio-sound-effect' || mode === 'audio-background-music') {
|
||||
return 'music';
|
||||
}
|
||||
if (mode === 'character' || mode === 'character-animation') {
|
||||
return 'character';
|
||||
}
|
||||
if (mode === 'icon') {
|
||||
return 'icon';
|
||||
}
|
||||
if (mode === 'publication') {
|
||||
return 'publication';
|
||||
}
|
||||
if (mode === 'ui-design') {
|
||||
return 'ui-design';
|
||||
}
|
||||
if (mode === 'spec') {
|
||||
return 'spec';
|
||||
}
|
||||
return 'generate';
|
||||
}
|
||||
|
||||
const LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY =
|
||||
'genarrative.imageCanvas.lastCharacterSpecReference';
|
||||
const LAST_ICON_SPEC_REFERENCE_CACHE_KEY =
|
||||
@@ -899,14 +954,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
|
||||
const createQuickEditModeDraft = useCallback(
|
||||
(sourceLayer: CanvasLayer) => {
|
||||
const sourceDialog = [...canvasGenerationDialogs]
|
||||
.reverse()
|
||||
.find(
|
||||
(dialog) =>
|
||||
dialog.generatedLayerId === sourceLayer.id ||
|
||||
(!dialog.generatedLayerId &&
|
||||
dialog.sourceLayerId === sourceLayer.id),
|
||||
);
|
||||
const sourceDialog = findSourceGenerationDialog(
|
||||
canvasGenerationDialogs,
|
||||
sourceLayer,
|
||||
);
|
||||
return createQuickEditPanelDraft(sourceLayer, {
|
||||
imageModel: sourceDialog?.imageModel,
|
||||
aspectRatio: sourceDialog?.aspectRatio,
|
||||
@@ -923,6 +974,26 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setUiAssetExtractionState(null);
|
||||
setCropExpandPanel(null);
|
||||
setCharacterAnimationPanel(null);
|
||||
const sourceDialog = findSourceGenerationDialog(
|
||||
canvasGenerationDialogs,
|
||||
sourceLayer,
|
||||
);
|
||||
const sameSourceDraft = createSameSourceGenerationDialogDraft({
|
||||
sourceLayer,
|
||||
canvasSize,
|
||||
viewport,
|
||||
sourceDialog,
|
||||
mode: 'quick-edit',
|
||||
sourceAnimationLayer: findSourceAnimationLayer(layers, sourceLayer),
|
||||
});
|
||||
if (sameSourceDraft) {
|
||||
openPlacedCanvasGenerationDialog(sameSourceDraft);
|
||||
setQuickEditPanel(null);
|
||||
setQuickEditSelectionState(null);
|
||||
selectSingleLayer(null);
|
||||
setActiveTool(getCanvasToolForGenerationMode(sameSourceDraft.mode));
|
||||
return;
|
||||
}
|
||||
const quickEditDraft = createQuickEditModeDraft(sourceLayer);
|
||||
const quickEditSize = resolveEditorImageGenerationPixelSize({
|
||||
model: quickEditDraft.model,
|
||||
@@ -958,12 +1029,15 @@ export function useImageCanvasGenerationWorkflow({
|
||||
[
|
||||
canvasSize,
|
||||
createQuickEditModeDraft,
|
||||
canvasGenerationDialogs,
|
||||
layers,
|
||||
openPlacedCanvasGenerationDialog,
|
||||
selectSingleLayer,
|
||||
setActiveTool,
|
||||
setImageContextMenu,
|
||||
setMetadataLayer,
|
||||
setViewport,
|
||||
viewport,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1028,14 +1102,25 @@ export function useImageCanvasGenerationWorkflow({
|
||||
sourceLayer.mediaType === undefined ||
|
||||
sourceLayer.mediaType === 'image'
|
||||
) {
|
||||
const sourceDialog = [...canvasGenerationDialogs]
|
||||
.reverse()
|
||||
.find(
|
||||
(dialog) =>
|
||||
dialog.generatedLayerId === sourceLayer.id ||
|
||||
(!dialog.generatedLayerId &&
|
||||
dialog.sourceLayerId === sourceLayer.id),
|
||||
);
|
||||
const sourceDialog = findSourceGenerationDialog(
|
||||
canvasGenerationDialogs,
|
||||
sourceLayer,
|
||||
);
|
||||
const sameSourceDraft = createSameSourceGenerationDialogDraft({
|
||||
sourceLayer,
|
||||
canvasSize,
|
||||
viewport,
|
||||
sourceDialog,
|
||||
mode: 'redraw',
|
||||
sourceAnimationLayer: findSourceAnimationLayer(layers, sourceLayer),
|
||||
});
|
||||
if (sameSourceDraft) {
|
||||
openPlacedCanvasGenerationDialog(sameSourceDraft);
|
||||
setQuickEditPanel(null);
|
||||
selectSingleLayer(null);
|
||||
setActiveTool(getCanvasToolForGenerationMode(sameSourceDraft.mode));
|
||||
return;
|
||||
}
|
||||
const redrawPanel = createRedrawPanelDraft(sourceLayer, {
|
||||
imageModel: sourceDialog?.imageModel,
|
||||
aspectRatio: sourceDialog?.aspectRatio,
|
||||
@@ -1074,6 +1159,9 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setImageContextMenu,
|
||||
setMetadataLayer,
|
||||
canvasGenerationDialogs,
|
||||
canvasSize,
|
||||
layers,
|
||||
viewport,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user