抽出编辑器生成提交流水线
新增 useImageCanvasGenerationSubmissionWorkflow 承载生成提交和结果落图副作用 补充生成提交流水线 hook 单测 精简 useImageCanvasGenerationWorkflow 的提交编排逻辑 更新 TRACKING.md 记录第四十三执行批次验证
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user