import { useCallback, useMemo, useState, type Dispatch, type MutableRefObject, type SetStateAction, } from 'react'; import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference'; import { editEditorImage, generateEditorCharacterAnimation, generateEditorIconSpritesheet, generateEditorImage, type EditorIconSpritesheetGenerationResult, type EditorIconSpritesheetIconResult, type EditorImageGenerationResult, } from '../../services/image-editor/editorProjectClient'; import { createGeneratedResultLayer, createIconSpritesheetResultLayers, createQuickEditResultLayer, } from './ImageCanvasGenerationLayerModel'; import { CHARACTER_ANIMATION_DURATION_OPTIONS, CHARACTER_ANIMATION_MODEL, CHARACTER_FRAME_DISPLAY_SIZE, CHARACTER_FRAME_ORIGINAL_SIZE, DEFAULT_ICON_DESCRIPTIONS, DEFAULT_IMAGE_MODEL, DEFAULT_SPEC_FORM_VALUES, EDITOR_IMAGE_DIMENSION_OPTIONS, ICON_DESCRIPTION_LIMIT, ICON_FRAME_DISPLAY_SIZE, ICON_FRAME_ORIGINAL_SIZE, SPEC_FRAME_DISPLAY_SIZE, SPEC_FRAME_ORIGINAL_SIZE, SPEC_GENERATION_SIZE, SPEC_TYPE_LABEL, buildCharacterGenerationInputs, buildEditGenerationInputs, buildIconGenerationInputs, buildImageGenerationInputs, buildQuickEditModelOptions, buildQuickEditSizeOptions, buildSpecGenerationInputs, buildSpecPrompt, calculateCharacterAnimationPrice, createCanvasLayerReference, isCanvasGenerationDialog, resolveCharacterAnimationSourceImageSrc, resolveImageGenerationErrorMessage, } from './ImageCanvasGenerationModel'; import { formatImageSizeValue } from './ImageCanvasEditorModel'; import type { CanvasGenerationDialogState, CanvasGenerationInputs, CanvasLayer, CanvasTool, CanvasViewport, CharacterAnimationPanelState, GenerateDialogState, ImageContextMenuState, QuickEditPanelState, SidebarPanel, SpecFormValues, SpecGenerationType, } from './ImageCanvasEditorTypes'; type CanvasSize = { width: number; height: number }; type CanvasGenerationDialogUpdater = ( dialog: CanvasGenerationDialogState, ) => CanvasGenerationDialogState | null; type GenerationWorkflowOptions = { layers: CanvasLayer[]; canvasSize: CanvasSize; viewport: CanvasViewport; layerCounterRef: MutableRefObject; generateDialog: GenerateDialogState | null; setGenerateDialog: Dispatch>; openCanvasGenerationDialog: ( dialog: Omit, ) => void; updateCanvasGenerationDialogById: ( dialogId: string, updater: CanvasGenerationDialogUpdater, ) => void; removeCanvasGenerationDialogById: (dialogId: string) => void; removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void; getGeneratingDialogPlaceholder: ( dialog: GenerateDialogState, ) => GenerateDialogState['placeholder']; appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void; selectSingleLayer: (layerId: string | null) => void; fitLayers: (targetLayers?: CanvasLayer[]) => void; setActiveTool: Dispatch>; setActiveSidebarPanel: Dispatch>; setMetadataLayer: Dispatch>; setImageContextMenu: Dispatch>; }; function getViewportWorldCenter({ canvasSize, viewport, }: { canvasSize: CanvasSize; viewport: CanvasViewport; }) { const safeScale = viewport.scale > 0 ? viewport.scale : 1; return { x: (canvasSize.width / 2 - viewport.x) / safeScale, y: (canvasSize.height / 2 - viewport.y) / safeScale, }; } function setFailedCharacterGenerationIdle(dialog: GenerateDialogState) { return { ...dialog, status: dialog.status === 'failed' ? 'idle' : dialog.status, errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage, }; } function setFailedIconGenerationIdle(dialog: GenerateDialogState) { return { ...dialog, status: dialog.status === 'failed' ? 'idle' : dialog.status, errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage, }; } export function useImageCanvasGenerationWorkflow({ layers, canvasSize, viewport, layerCounterRef, generateDialog, setGenerateDialog, openCanvasGenerationDialog, updateCanvasGenerationDialogById, removeCanvasGenerationDialogById, removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, selectSingleLayer, fitLayers, setActiveTool, setActiveSidebarPanel, setMetadataLayer, setImageContextMenu, }: GenerationWorkflowOptions) { const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false); const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(false); const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] = useState(false); const [ isPickingCharacterSpecFromCanvas, setIsPickingCharacterSpecFromCanvas, ] = useState(false); const [ isPickingCharacterReferenceFromCanvas, setIsPickingCharacterReferenceFromCanvas, ] = useState(false); const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(false); const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] = useState(false); const [quickEditPanel, setQuickEditPanel] = useState(null); const [characterAnimationPanel, setCharacterAnimationPanel] = useState(null); const [lastImageModel, setLastImageModel] = useState(DEFAULT_IMAGE_MODEL); const quickEditSourceLayer = quickEditPanel ? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ?? null) : null; const characterAnimationSourceLayer = characterAnimationPanel ? (layers.find( (layer) => layer.id === characterAnimationPanel.sourceLayerId, ) ?? null) : null; const quickEditSizeOptions = quickEditPanel ? buildQuickEditSizeOptions(quickEditPanel.size) : []; const quickEditModelOptions = quickEditPanel ? buildQuickEditModelOptions(quickEditPanel.model) : []; const characterAnimationPrice = characterAnimationPanel ? calculateCharacterAnimationPrice( characterAnimationPanel.resolution, characterAnimationPanel.durationSeconds, ) : 0; const iconDescriptionValues = generateDialog?.mode === 'icon' ? (generateDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS) : DEFAULT_ICON_DESCRIPTIONS; const openGenerateDialog = useCallback(() => { const placeholderWidth = 420; const placeholderHeight = 420; const worldCenter = getViewportWorldCenter({ canvasSize, viewport }); openCanvasGenerationDialog({ mode: 'generate', prompt: '', status: 'idle', composerOpen: true, placeholder: { x: worldCenter.x - placeholderWidth / 2, y: worldCenter.y - placeholderHeight / 2, width: placeholderWidth, height: placeholderHeight, originalWidth: 2048, originalHeight: 2048, }, }); setActiveTool('generate'); selectSingleLayer(null); setQuickEditPanel(null); }, [ canvasSize, openCanvasGenerationDialog, selectSingleLayer, setActiveTool, viewport, ]); const openSpecDialog = useCallback( (specType: SpecGenerationType) => { const worldCenter = getViewportWorldCenter({ canvasSize, viewport }); openCanvasGenerationDialog({ mode: 'spec', prompt: '', status: 'idle', composerOpen: true, specType, specValues: { ...DEFAULT_SPEC_FORM_VALUES[specType] }, placeholder: { x: worldCenter.x - SPEC_FRAME_DISPLAY_SIZE.width / 2, y: worldCenter.y - SPEC_FRAME_DISPLAY_SIZE.height / 2, width: SPEC_FRAME_DISPLAY_SIZE.width, height: SPEC_FRAME_DISPLAY_SIZE.height, originalWidth: SPEC_FRAME_ORIGINAL_SIZE.width, originalHeight: SPEC_FRAME_ORIGINAL_SIZE.height, }, }); setIsSpecMenuOpen(false); setActiveTool('generate'); selectSingleLayer(null); setQuickEditPanel(null); }, [ canvasSize, openCanvasGenerationDialog, selectSingleLayer, setActiveTool, viewport, ], ); const openCharacterAnimationPanel = useCallback( (layer: CanvasLayer) => { if (layer.assetKind !== 'character') { return; } setImageContextMenu(null); setQuickEditPanel(null); setCharacterAnimationPanel({ sourceLayerId: layer.id, promptText: '', resolution: '480p', ratio: 'same', frameCount: 32, durationSeconds: 4, status: 'idle', }); selectSingleLayer(layer.id); }, [selectSingleLayer, setImageContextMenu], ); const openCharacterGenerationDialog = useCallback(() => { const worldCenter = getViewportWorldCenter({ canvasSize, viewport }); setIsSpecMenuOpen(false); setIsCharacterReferenceMenuOpen(false); setIsPickingCharacterSpecFromCanvas(false); setIsPickingCharacterReferenceFromCanvas(false); const dimensionOptions = EDITOR_IMAGE_DIMENSION_OPTIONS[ lastImageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS ] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL]; openCanvasGenerationDialog({ mode: 'character', prompt: '', status: 'idle', composerOpen: true, characterSpecReference: null, characterReferences: [], imageModel: lastImageModel, aspectRatio: dimensionOptions.aspectRatios[0], imageSize: dimensionOptions.imageSizes.find((size) => size === '1K') ?? dimensionOptions.imageSizes[0], placeholder: { x: worldCenter.x - CHARACTER_FRAME_DISPLAY_SIZE.width / 2, y: worldCenter.y - CHARACTER_FRAME_DISPLAY_SIZE.height / 2, width: CHARACTER_FRAME_DISPLAY_SIZE.width, height: CHARACTER_FRAME_DISPLAY_SIZE.height, originalWidth: CHARACTER_FRAME_ORIGINAL_SIZE.width, originalHeight: CHARACTER_FRAME_ORIGINAL_SIZE.height, }, }); setActiveTool('character'); selectSingleLayer(null); setQuickEditPanel(null); }, [ canvasSize, lastImageModel, openCanvasGenerationDialog, selectSingleLayer, setActiveTool, viewport, ]); const openIconGenerationDialog = useCallback(() => { const worldCenter = getViewportWorldCenter({ canvasSize, viewport }); setIsSpecMenuOpen(false); setIsCharacterReferenceMenuOpen(false); setIsPickingCharacterSpecFromCanvas(false); setIsPickingCharacterReferenceFromCanvas(false); setIsPickingIconSpecFromCanvas(false); const dimensionOptions = EDITOR_IMAGE_DIMENSION_OPTIONS[ lastImageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS ] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL]; openCanvasGenerationDialog({ mode: 'icon', prompt: '', status: 'idle', composerOpen: true, iconSpecReference: null, iconDescriptions: [...DEFAULT_ICON_DESCRIPTIONS], imageModel: lastImageModel, aspectRatio: dimensionOptions.aspectRatios[0], imageSize: dimensionOptions.imageSizes.find((size) => size === '1K') ?? dimensionOptions.imageSizes[0], placeholder: { x: worldCenter.x - ICON_FRAME_DISPLAY_SIZE.width / 2, y: worldCenter.y - ICON_FRAME_DISPLAY_SIZE.height / 2, width: ICON_FRAME_DISPLAY_SIZE.width, height: ICON_FRAME_DISPLAY_SIZE.height, originalWidth: ICON_FRAME_ORIGINAL_SIZE.width, originalHeight: ICON_FRAME_ORIGINAL_SIZE.height, }, }); setActiveTool('icon'); selectSingleLayer(null); setQuickEditPanel(null); setCharacterAnimationPanel(null); }, [ canvasSize, lastImageModel, openCanvasGenerationDialog, selectSingleLayer, setActiveTool, viewport, ]); const openEditDialog = useCallback( (sourceLayer: CanvasLayer) => { setMetadataLayer(null); setImageContextMenu(null); setQuickEditPanel(null); setGenerateDialog({ mode: 'edit', prompt: sourceLayer.prompt ? `${sourceLayer.prompt},在保持主体结构的基础上优化画面细节` : '', status: 'idle', composerOpen: true, sourceLayerId: sourceLayer.id, }); setActiveTool('generate'); }, [setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer], ); const openQuickEditPanel = useCallback( (sourceLayer: CanvasLayer) => { setImageContextMenu(null); setMetadataLayer(null); setGenerateDialog(null); setCharacterAnimationPanel(null); setQuickEditPanel({ sourceLayerId: sourceLayer.id, prompt: '', size: formatImageSizeValue( sourceLayer.originalWidth, sourceLayer.originalHeight, ), model: sourceLayer.model?.trim() || DEFAULT_IMAGE_MODEL, status: 'idle', }); selectSingleLayer(sourceLayer.id); setActiveTool('generate'); }, [ selectSingleLayer, setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer, ], ); 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) => currentDialog?.mode === 'character' ? { ...setFailedCharacterGenerationIdle(currentDialog), characterSpecReference: createCanvasLayerReference(layer), composerOpen: true, } : currentDialog, ); setIsPickingCharacterSpecFromCanvas(false); setIsCharacterSpecMenuOpen(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const pickCharacterReferenceFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => currentDialog?.mode === 'character' ? { ...setFailedCharacterGenerationIdle(currentDialog), characterReferences: [ ...(currentDialog.characterReferences ?? []), createCanvasLayerReference(layer), ], composerOpen: true, } : currentDialog, ); setIsPickingCharacterReferenceFromCanvas(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const pickIconSpecFromLayer = useCallback( (layer: CanvasLayer) => { if (layer.assetKind !== 'icon-spec') { return; } setGenerateDialog((currentDialog) => currentDialog?.mode === 'icon' ? { ...setFailedIconGenerationIdle(currentDialog), iconSpecReference: createCanvasLayerReference(layer), composerOpen: true, } : currentDialog, ); setIsPickingIconSpecFromCanvas(false); setIsIconSpecMenuOpen(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const updateIconDescription = useCallback( (index: number, value: string) => { setGenerateDialog((currentDialog) => currentDialog?.mode === 'icon' ? { ...setFailedIconGenerationIdle(currentDialog), iconDescriptions: ( currentDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS ).map((description, descriptionIndex) => descriptionIndex === index ? value : description, ), } : currentDialog, ); }, [setGenerateDialog], ); const addIconDescription = useCallback(() => { setGenerateDialog((currentDialog) => { if (currentDialog?.mode !== 'icon') { return currentDialog; } const descriptions = currentDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS; if (descriptions.length >= ICON_DESCRIPTION_LIMIT) { return currentDialog; } return { ...setFailedIconGenerationIdle(currentDialog), iconDescriptions: [...descriptions, ''], }; }); }, [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 iconDescriptions = ( dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS ) .map((description) => description.trim()) .filter(Boolean); if (!dialog.iconSpecReference) { if (canvasDialog) { setSubmittingIconDialog({ ...canvasDialog, status: 'failed', composerOpen: true, errorMessage: '请选择图标素材规范', }); } return; } if (!iconDescriptions.length) { if (canvasDialog) { setSubmittingIconDialog({ ...canvasDialog, status: 'failed', composerOpen: true, errorMessage: '请填写素材描述', }); } return; } if (!canvasDialog) { return; } setSubmittingIconDialog({ ...canvasDialog, iconDescriptions, status: 'generating', composerOpen: false, errorMessage: undefined, }); try { const generated = await generateEditorIconSpritesheet({ referenceImageSrc: dialog.iconSpecReference.src, iconDescriptions, model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL, aspectRatio: dialog.aspectRatio ?? '1:1', imageSize: dialog.imageSize ?? '1K', }); setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL); addIconSpritesheetResultLayers( generated, generated.iconImageSrcs, buildIconGenerationInputs(iconDescriptions, dialog.iconSpecReference), getGeneratingDialogPlaceholder(dialog), canvasDialog.id, ); } catch (error) { setSubmittingIconDialog({ ...canvasDialog, 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 { if (dialog.mode === 'edit') { const sourceLayer = layers.find( (layer) => layer.id === dialog.sourceLayerId, ); if (!sourceLayer) { throw new Error('未找到要修改的图片'); } const referenceImageSrc = await resolveEditorImageReferenceDataUrl( sourceLayer.src, ); const generated = await editEditorImage({ prompt: normalizedPrompt, sourceImageSrc: referenceImageSrc, }); addGeneratedResultLayer(generated, { sourceLayer, generationInputs: buildEditGenerationInputs( '修改要求', normalizedPrompt, sourceLayer, ), }); } else if (dialog.mode === 'spec') { const specType = dialog.specType ?? 'custom'; const specValues = dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType]; const specPrompt = buildSpecPrompt( specType, specValues, Boolean(dialog.specReference?.src), ); const generated = await generateEditorImage({ prompt: specPrompt, size: SPEC_GENERATION_SIZE, model: DEFAULT_IMAGE_MODEL, kind: 'spec', ...(dialog.specReference?.src ? { referenceImageSrcs: [dialog.specReference.src] } : {}), }); addGeneratedResultLayer(generated, { frame: getGeneratingDialogPlaceholder(dialog), assetKind: specType === 'icon' ? 'icon-spec' : 'spec', title: `${SPEC_TYPE_LABEL[specType]} ${layerCounterRef.current + 1}`, dialogId: canvasDialog?.id, generationInputs: buildSpecGenerationInputs( specType, specValues, dialog.specReference, ), }); } else if (dialog.mode === 'character') { const referenceImageSrcs = [ dialog.characterSpecReference?.src, ...(dialog.characterReferences ?? []).map( (reference) => reference.src, ), ].filter((src): src is string => Boolean(src)); const generated = await generateEditorImage({ prompt: normalizedPrompt, kind: 'character', model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL, aspectRatio: dialog.aspectRatio ?? '1:1', imageSize: dialog.imageSize ?? '1K', ...(referenceImageSrcs.length ? { referenceImageSrcs } : {}), }); setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL); addGeneratedResultLayer(generated, { frame: getGeneratingDialogPlaceholder(dialog), assetKind: 'character', title: `角色形象 ${layerCounterRef.current + 1}`, dialogId: canvasDialog?.id, generationInputs: buildCharacterGenerationInputs( normalizedPrompt, dialog.characterSpecReference, dialog.characterReferences, ), }); } else { const generated = await generateEditorImage({ prompt: normalizedPrompt, }); addGeneratedResultLayer(generated, { frame: getGeneratingDialogPlaceholder(dialog), dialogId: canvasDialog?.id, generationInputs: buildImageGenerationInputs(normalizedPrompt), }); } } 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 updateSpecFormValue = useCallback( (key: keyof SpecFormValues, value: string) => { setGenerateDialog((currentDialog) => { if (currentDialog?.mode !== 'spec') { return currentDialog; } const specType = currentDialog.specType ?? 'custom'; return { ...currentDialog, specValues: { ...DEFAULT_SPEC_FORM_VALUES[specType], ...currentDialog.specValues, [key]: value, }, status: currentDialog.status === 'failed' ? 'idle' : currentDialog.status, errorMessage: currentDialog.status === 'failed' ? undefined : currentDialog.errorMessage, }; }); }, [setGenerateDialog], ); const updateCharacterAnimationDuration = useCallback( (frameCountValue: string) => { const option = CHARACTER_ANIMATION_DURATION_OPTIONS.find( (item) => String(item.frameCount) === frameCountValue, ); if (!option) { return; } setCharacterAnimationPanel((currentPanel) => currentPanel ? { ...currentPanel, frameCount: option.frameCount, durationSeconds: option.durationSeconds, status: currentPanel.status === 'failed' ? 'idle' : currentPanel.status, errorMessage: currentPanel.status === 'failed' ? undefined : currentPanel.errorMessage, } : currentPanel, ); }, [], ); const submitCharacterAnimation = useCallback(async () => { if (!characterAnimationPanel || !characterAnimationSourceLayer) { return; } const promptText = characterAnimationPanel.promptText.trim(); const nextPanel = { ...characterAnimationPanel, promptText, status: 'generating' as const, errorMessage: undefined, result: undefined, }; setCharacterAnimationPanel(nextPanel); try { const result = await generateEditorCharacterAnimation({ sourceLayerId: characterAnimationSourceLayer.id, sourceImageSrc: resolveCharacterAnimationSourceImageSrc( characterAnimationSourceLayer, ), sourceWidth: characterAnimationSourceLayer.originalWidth, sourceHeight: characterAnimationSourceLayer.originalHeight, promptText, resolution: nextPanel.resolution, ratio: nextPanel.ratio, frameCount: nextPanel.frameCount, durationSeconds: nextPanel.durationSeconds, priceMudPoints: calculateCharacterAnimationPrice( nextPanel.resolution, nextPanel.durationSeconds, ), model: CHARACTER_ANIMATION_MODEL, }); 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) => (currentDialog?.mode === 'generate' || currentDialog?.mode === 'spec' || currentDialog?.mode === 'character' || currentDialog?.mode === 'icon') && currentDialog.status !== 'generating' ? { ...currentDialog, composerOpen: false, } : currentDialog, ); }, [setGenerateDialog]); const closeGenerateComposer = useCallback(() => { setGenerateDialog((currentDialog) => currentDialog?.mode === 'generate' ? { ...currentDialog, composerOpen: false, } : currentDialog, ); setActiveTool('select'); }, [setActiveTool, setGenerateDialog]); const clearDeletedLayerGenerationState = useCallback( (targetLayerId: string) => { setQuickEditPanel((currentPanel) => currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel, ); setCharacterAnimationPanel((currentPanel) => currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel, ); setGenerateDialog((currentDialog) => currentDialog?.mode === 'edit' && currentDialog.sourceLayerId === targetLayerId ? null : currentDialog, ); removeCanvasGenerationDialogsByLayerId(targetLayerId); }, [removeCanvasGenerationDialogsByLayerId, setGenerateDialog], ); return useMemo( () => ({ quickEditPanel, setQuickEditPanel, quickEditSourceLayer, quickEditSizeOptions, quickEditModelOptions, characterAnimationPanel, setCharacterAnimationPanel, characterAnimationSourceLayer, characterAnimationPrice, iconDescriptionValues, isSpecMenuOpen, setIsSpecMenuOpen, isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen, isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen, isPickingCharacterSpecFromCanvas, setIsPickingCharacterSpecFromCanvas, isPickingCharacterReferenceFromCanvas, setIsPickingCharacterReferenceFromCanvas, isIconSpecMenuOpen, setIsIconSpecMenuOpen, isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas, openGenerateDialog, openSpecDialog, openCharacterAnimationPanel, openCharacterGenerationDialog, openIconGenerationDialog, openEditDialog, openQuickEditPanel, pickCharacterSpecFromLayer, pickCharacterReferenceFromLayer, pickIconSpecFromLayer, submitIconSpritesheetGeneration, submitQuickEdit, submitImageGeneration, updateSpecFormValue, updateIconDescription, addIconDescription, updateCharacterAnimationDuration, rememberImageModel: setLastImageModel, submitCharacterAnimation, hideGeneratedLayerPanelAfterBlur, closeGenerateComposer, clearDeletedLayerGenerationState, }), [ addIconDescription, characterAnimationPanel, characterAnimationPrice, characterAnimationSourceLayer, clearDeletedLayerGenerationState, closeGenerateComposer, hideGeneratedLayerPanelAfterBlur, iconDescriptionValues, isCharacterReferenceMenuOpen, isCharacterSpecMenuOpen, isIconSpecMenuOpen, isPickingCharacterReferenceFromCanvas, isPickingCharacterSpecFromCanvas, isPickingIconSpecFromCanvas, isSpecMenuOpen, openCharacterAnimationPanel, openCharacterGenerationDialog, openEditDialog, openGenerateDialog, openIconGenerationDialog, openQuickEditPanel, openSpecDialog, pickCharacterReferenceFromLayer, pickCharacterSpecFromLayer, pickIconSpecFromLayer, quickEditModelOptions, quickEditPanel, quickEditSizeOptions, quickEditSourceLayer, submitCharacterAnimation, submitIconSpritesheetGeneration, submitImageGeneration, submitQuickEdit, updateCharacterAnimationDuration, updateIconDescription, updateSpecFormValue, ], ); }