按来源生成器打开快速编辑和改造

快速编辑和改造优先恢复图层来源生成器类型

按原生成器草稿恢复普通图片、角色、图标、规范、UI、宣发和动作参数

补充同源生成器恢复与画布工作流回归测试
This commit is contained in:
2026-06-26 00:21:41 +08:00
parent bf09b84cb3
commit 607042a250
4 changed files with 780 additions and 24 deletions
@@ -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,
],
);