抽出编辑器生成提交流水线

新增 useImageCanvasGenerationSubmissionWorkflow 承载生成提交和结果落图副作用

补充生成提交流水线 hook 单测

精简 useImageCanvasGenerationWorkflow 的提交编排逻辑

更新 TRACKING.md 记录第四十三执行批次验证
This commit is contained in:
2026-06-17 19:54:41 +08:00
parent 7f573486bc
commit 489b0a7743
4 changed files with 1166 additions and 443 deletions

View File

@@ -7,19 +7,8 @@ import {
useState,
} from 'react';
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
import {
editEditorImage,
type EditorIconSpritesheetGenerationResult,
type EditorIconSpritesheetIconResult,
type EditorImageGenerationResult,
generateEditorCharacterAnimation,
generateEditorIconSpritesheet,
generateEditorImage,
} from '../../services/image-editor/editorProjectClient';
import type {
CanvasGenerationDialogState,
CanvasGenerationInputs,
CanvasLayer,
CanvasTool,
CanvasViewport,
@@ -50,25 +39,13 @@ import {
updateSpecFormDialogValue,
} from './ImageCanvasGenerationDialogModel';
import {
createGeneratedResultLayer,
createIconSpritesheetResultLayers,
createQuickEditResultLayer,
} from './ImageCanvasGenerationLayerModel';
import {
buildEditGenerationInputs,
buildQuickEditModelOptions,
buildQuickEditSizeOptions,
calculateCharacterAnimationPrice,
DEFAULT_ICON_DESCRIPTIONS,
DEFAULT_IMAGE_MODEL,
isCanvasGenerationDialog,
resolveImageGenerationErrorMessage,
} from './ImageCanvasGenerationModel';
import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
buildImageGenerationSubmissionPlan,
} from './ImageCanvasGenerationSubmissionModel';
import { useImageCanvasGenerationSubmissionWorkflow } from './useImageCanvasGenerationSubmissionWorkflow';
type CanvasSize = { width: number; height: number };
@@ -303,145 +280,6 @@ export function useImageCanvasGenerationWorkflow({
],
);
const addGeneratedResultLayer = useCallback(
(
generated: EditorImageGenerationResult,
options: {
sourceLayer?: CanvasLayer;
frame?: GenerateDialogState['placeholder'];
assetKind?: CanvasLayer['assetKind'];
title?: string;
dialogId?: string;
generationInputs?: CanvasGenerationInputs;
} = {},
) => {
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createGeneratedResultLayer({
generated,
generatedIndex,
canvasSize,
viewport,
sourceLayer: options.sourceLayer,
frame: options.frame,
assetKind: options.assetKind,
title: options.title,
generationInputs: options.generationInputs,
});
appendCanvasLayersWithResources([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (options.sourceLayer) {
setGenerateDialog(null);
setActiveTool('select');
} else if (options.dialogId) {
updateCanvasGenerationDialogById(options.dialogId, (currentDialog) =>
currentDialog.mode === 'character' || currentDialog.mode === 'icon'
? null
: {
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayer.id,
placeholder: undefined,
errorMessage: undefined,
},
);
}
if (options.sourceLayer) {
fitLayers([options.sourceLayer, nextLayer]);
}
},
[
appendCanvasLayersWithResources,
canvasSize,
fitLayers,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
setGenerateDialog,
updateCanvasGenerationDialogById,
viewport,
],
);
const addQuickEditResultLayer = useCallback(
(
generated: EditorImageGenerationResult,
sourceLayer: CanvasLayer,
generationInputs: CanvasGenerationInputs,
) => {
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createQuickEditResultLayer({
generated,
generatedIndex,
sourceLayer,
generationInputs,
});
appendCanvasLayersWithResources([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
setQuickEditPanel(null);
setActiveTool('select');
fitLayers([sourceLayer, nextLayer]);
},
[
appendCanvasLayersWithResources,
fitLayers,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
],
);
const addIconSpritesheetResultLayers = useCallback(
(
generated: EditorIconSpritesheetGenerationResult,
iconResults: EditorIconSpritesheetIconResult[],
generationInputs: CanvasGenerationInputs,
frame?: GenerateDialogState['placeholder'],
dialogId?: string,
) => {
const startIndex = layerCounterRef.current + 1;
const nextLayers = createIconSpritesheetResultLayers({
generated,
iconResults,
startIndex,
canvasSize,
viewport,
generationInputs,
frame,
});
if (!nextLayers.length) {
return;
}
layerCounterRef.current += nextLayers.length;
appendCanvasLayersWithResources(nextLayers);
selectSingleLayer(nextLayers[0]?.id ?? null);
setActiveSidebarPanel('layers');
if (dialogId) {
removeCanvasGenerationDialogById(dialogId);
}
setActiveTool('select');
},
[
appendCanvasLayersWithResources,
canvasSize,
layerCounterRef,
removeCanvasGenerationDialogById,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
viewport,
],
);
const pickCharacterSpecFromLayer = useCallback(
(layer: CanvasLayer) => {
setGenerateDialog((currentDialog) =>
@@ -493,197 +331,34 @@ export function useImageCanvasGenerationWorkflow({
setGenerateDialog(appendIconDescriptionToDialog);
}, [setGenerateDialog]);
const submitIconSpritesheetGeneration = useCallback(
async (dialog: GenerateDialogState) => {
if (dialog.mode !== 'icon') {
return;
}
const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
const setSubmittingIconDialog = (
nextDialog: CanvasGenerationDialogState,
) => {
updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog);
};
const submissionPlan =
buildIconSpritesheetGenerationSubmissionPlan(dialog);
if (!submissionPlan.ok) {
if (canvasDialog) {
setSubmittingIconDialog({
...canvasDialog,
status: 'failed',
composerOpen: true,
errorMessage: submissionPlan.errorMessage,
});
}
return;
}
if (!canvasDialog) {
return;
}
setSubmittingIconDialog({
...canvasDialog,
iconDescriptions: submissionPlan.iconDescriptions,
status: 'generating',
composerOpen: false,
errorMessage: undefined,
});
try {
const generated = await generateEditorIconSpritesheet(
submissionPlan.input,
);
setLastImageModel(submissionPlan.rememberImageModel);
addIconSpritesheetResultLayers(
generated,
generated.iconImageSrcs,
submissionPlan.generationInputs,
getGeneratingDialogPlaceholder(dialog),
canvasDialog.id,
);
} catch (error) {
setSubmittingIconDialog({
...canvasDialog,
iconDescriptions: submissionPlan.iconDescriptions,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
},
[
addIconSpritesheetResultLayers,
getGeneratingDialogPlaceholder,
updateCanvasGenerationDialogById,
],
);
const submitQuickEdit = useCallback(async () => {
if (!quickEditPanel || !quickEditSourceLayer) {
return;
}
const normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片';
setQuickEditPanel({
...quickEditPanel,
prompt: normalizedPrompt,
status: 'generating',
errorMessage: undefined,
});
try {
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
quickEditSourceLayer.src,
);
const generated = await generateEditorImage({
prompt: normalizedPrompt,
size: quickEditPanel.size,
kind: 'quick-edit',
model: quickEditPanel.model,
referenceImageSrcs: [referenceImageSrc],
});
addQuickEditResultLayer(
generated,
quickEditSourceLayer,
buildEditGenerationInputs(
'快速编辑提示词',
normalizedPrompt,
quickEditSourceLayer,
),
);
} catch (error) {
setQuickEditPanel({
...quickEditPanel,
prompt: normalizedPrompt,
status: 'failed',
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
}, [addQuickEditResultLayer, quickEditPanel, quickEditSourceLayer]);
const submitImageGeneration = useCallback(
async (dialog: GenerateDialogState) => {
const normalizedPrompt =
dialog.prompt.trim() ||
(dialog.mode === 'edit' ? '修改当前图片' : 'AI 生成图片');
const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
if (canvasDialog) {
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
...currentDialog,
prompt: normalizedPrompt,
status: 'generating',
composerOpen: false,
}));
} else {
setGenerateDialog({
...dialog,
prompt: normalizedPrompt,
status: 'generating',
composerOpen: dialog.mode === 'edit',
});
}
try {
const submissionPlan = buildImageGenerationSubmissionPlan({
dialog,
layers,
nextGeneratedIndex: layerCounterRef.current + 1,
});
if (submissionPlan.kind === 'edit') {
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
submissionPlan.sourceLayer.src,
);
const generated = await editEditorImage({
prompt: submissionPlan.normalizedPrompt,
sourceImageSrc: referenceImageSrc,
});
addGeneratedResultLayer(generated, {
sourceLayer: submissionPlan.sourceLayer,
generationInputs: submissionPlan.generationInputs,
});
} else {
const generated = await generateEditorImage(submissionPlan.input);
if (submissionPlan.rememberImageModel) {
setLastImageModel(submissionPlan.rememberImageModel);
}
addGeneratedResultLayer(generated, {
frame: getGeneratingDialogPlaceholder(dialog),
assetKind: submissionPlan.result.assetKind,
title: submissionPlan.result.title,
dialogId: canvasDialog?.id,
generationInputs: submissionPlan.result.generationInputs,
});
}
} catch (error) {
if (canvasDialog) {
updateCanvasGenerationDialogById(canvasDialog.id, () => ({
...canvasDialog,
prompt: normalizedPrompt,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
}));
} else {
setGenerateDialog({
...dialog,
prompt: normalizedPrompt,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
}
},
[
addGeneratedResultLayer,
getGeneratingDialogPlaceholder,
layerCounterRef,
layers,
setGenerateDialog,
updateCanvasGenerationDialogById,
],
);
const generationSubmissionWorkflow = useImageCanvasGenerationSubmissionWorkflow({
layers,
canvasSize,
viewport,
layerCounterRef,
quickEditPanel,
quickEditSourceLayer,
setQuickEditPanel,
characterAnimationPanel,
characterAnimationSourceLayer,
setCharacterAnimationPanel,
setGenerateDialog,
updateCanvasGenerationDialogById,
removeCanvasGenerationDialogById,
getGeneratingDialogPlaceholder,
appendCanvasLayersWithResources,
selectSingleLayer,
fitLayers,
setActiveTool,
setActiveSidebarPanel,
rememberImageModel: setLastImageModel,
});
const {
submitCharacterAnimation,
submitIconSpritesheetGeneration,
submitImageGeneration,
submitQuickEdit,
} = generationSubmissionWorkflow;
const updateSpecFormValue = useCallback(
(key: keyof SpecFormValues, value: string) => {
@@ -703,52 +378,6 @@ export function useImageCanvasGenerationWorkflow({
[],
);
const submitCharacterAnimation = useCallback(async () => {
if (!characterAnimationPanel || !characterAnimationSourceLayer) {
return;
}
const submissionPlan = buildCharacterAnimationSubmissionPlan({
panel: characterAnimationPanel,
sourceLayer: characterAnimationSourceLayer,
});
const nextPanel = {
...characterAnimationPanel,
promptText: submissionPlan.promptText,
status: 'generating' as const,
errorMessage: undefined,
result: undefined,
};
setCharacterAnimationPanel(nextPanel);
try {
const result = await generateEditorCharacterAnimation(
submissionPlan.input,
);
setCharacterAnimationPanel((currentPanel) =>
currentPanel
? {
...currentPanel,
status: 'completed',
result,
}
: currentPanel,
);
} catch (error) {
setCharacterAnimationPanel((currentPanel) =>
currentPanel
? {
...currentPanel,
status: 'failed',
errorMessage:
error instanceof Error && error.message.trim()
? error.message
: '生成角色动画失败',
}
: currentPanel,
);
}
}, [characterAnimationPanel, characterAnimationSourceLayer]);
const hideGeneratedLayerPanelAfterBlur = useCallback(() => {
setGenerateDialog((currentDialog) =>
hideGeneratedLayerComposerAfterBlur(currentDialog),