import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback, useMemo, useState, } from 'react'; import type { CanvasGenerationDialogState, CanvasLayer, CanvasTool, CanvasViewport, CharacterAnimationPanelState, GenerateDialogState, ImageContextMenuState, QuickEditPanelState, SidebarPanel, SpecFormValues, SpecGenerationType, } from './ImageCanvasEditorTypes'; import { appendCharacterReference, appendGenerationReference, appendIconDescriptionToDialog, assignUiDesignSpecReference, assignCharacterSpecReference, assignIconSpecReference, closeGenerateComposerDialog, createCharacterAnimationPanelDraft, createCharacterGenerationDialogDraft, createEditDialogDraft, createGenerateDialogDraft, createIconGenerationDialogDraft, createQuickEditPanelDraft, createSpecDialogDraft, createUiDesignGenerationDialogDraft, createVideoGenerationDialogDraft, hideGeneratedLayerComposerAfterBlur, updateCharacterAnimationDurationPanel, updateIconDescriptionInDialog, updateSpecFormDialogValue, } from './ImageCanvasGenerationDialogModel'; import { centerViewportOnPlacement, chooseGenerationPlacement, } from './ImageCanvasGenerationPlacementModel'; import { buildQuickEditModelOptions, buildQuickEditSizeOptions, calculateCharacterAnimationPrice, DEFAULT_ICON_DESCRIPTIONS, DEFAULT_IMAGE_MODEL, } from './ImageCanvasGenerationModel'; import { useImageCanvasGenerationSubmissionWorkflow } from './useImageCanvasGenerationSubmissionWorkflow'; type CanvasSize = { width: number; height: number }; type CanvasGenerationDialogUpdater = ( dialog: CanvasGenerationDialogState, ) => CanvasGenerationDialogState | null; type GenerationWorkflowOptions = { layers: CanvasLayer[]; canvasSize: CanvasSize; viewport: CanvasViewport; setViewport: Dispatch>; canvasGenerationDialogs: CanvasGenerationDialogState[]; layerCounterRef: MutableRefObject; generateDialog: GenerateDialogState | null; setGenerateDialog: Dispatch>; openCanvasGenerationDialog: ( dialog: Omit, ) => void; updateCanvasGenerationDialogById: ( dialogId: string, updater: CanvasGenerationDialogUpdater, ) => 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>; persistGeneratedAsset?: (layer: CanvasLayer) => void; }; export function useImageCanvasGenerationWorkflow({ layers, canvasSize, viewport, setViewport, canvasGenerationDialogs, layerCounterRef, generateDialog, setGenerateDialog, openCanvasGenerationDialog, updateCanvasGenerationDialogById, removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, selectSingleLayer, fitLayers, setActiveTool, setActiveSidebarPanel, setMetadataLayer, setImageContextMenu, persistGeneratedAsset, }: GenerationWorkflowOptions) { const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false); const [isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen] = useState(false); const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(false); const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] = useState(false); const [ isPickingGenerationReferenceFromCanvas, setIsPickingGenerationReferenceFromCanvas, ] = useState(false); const [ isPickingCharacterSpecFromCanvas, setIsPickingCharacterSpecFromCanvas, ] = useState(false); const [ isPickingCharacterReferenceFromCanvas, setIsPickingCharacterReferenceFromCanvas, ] = useState(false); const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(false); const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] = useState(false); const [isUiDesignSpecMenuOpen, setIsUiDesignSpecMenuOpen] = useState(false); const [isPickingUiDesignSpecFromCanvas, setIsPickingUiDesignSpecFromCanvas] = 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 = useMemo( () => quickEditPanel ? buildQuickEditSizeOptions(quickEditPanel.size) : [], [quickEditPanel], ); const quickEditModelOptions = useMemo( () => quickEditPanel ? buildQuickEditModelOptions(quickEditPanel.model) : [], [quickEditPanel], ); const characterAnimationPrice = characterAnimationPanel ? calculateCharacterAnimationPrice( characterAnimationPanel.resolution, characterAnimationPanel.durationSeconds, ) : 0; const iconDescriptionValues = generateDialog?.mode === 'icon' ? (generateDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS) : DEFAULT_ICON_DESCRIPTIONS; const closeGenerationTransientState = useCallback(() => { setIsSpecMenuOpen(false); setIsGenerationReferenceMenuOpen(false); setIsCharacterSpecMenuOpen(false); setIsCharacterReferenceMenuOpen(false); setIsPickingGenerationReferenceFromCanvas(false); setIsPickingCharacterSpecFromCanvas(false); setIsPickingCharacterReferenceFromCanvas(false); setIsIconSpecMenuOpen(false); setIsPickingIconSpecFromCanvas(false); setIsUiDesignSpecMenuOpen(false); setIsPickingUiDesignSpecFromCanvas(false); setImageContextMenu(null); }, [setImageContextMenu]); const openPlacedCanvasGenerationDialog = useCallback( (draft: Omit) => { const draftPlaceholder = draft.placeholder; if (!draftPlaceholder) { openCanvasGenerationDialog(draft); return; } // 中文注释:所有画布生成入口统一先走 placement 模型,避免新占位压住已有图层或生成占位。 const placement = chooseGenerationPlacement({ canvasSize, viewport, frame: draftPlaceholder, layers, generationDialogs: canvasGenerationDialogs, }); openCanvasGenerationDialog({ ...draft, placeholder: placement, }); setViewport( centerViewportOnPlacement({ canvasSize, viewport, placement, }), ); }, [ canvasGenerationDialogs, canvasSize, layers, openCanvasGenerationDialog, setViewport, viewport, ], ); const activateCanvasGenerationEntry = useCallback( (activeTool: CanvasTool) => { closeGenerationTransientState(); setActiveTool(activeTool); selectSingleLayer(null); setQuickEditPanel(null); setCharacterAnimationPanel(null); }, [closeGenerationTransientState, selectSingleLayer, setActiveTool], ); const openGenerateDialog = useCallback(() => { openPlacedCanvasGenerationDialog( createGenerateDialogDraft({ canvasSize, viewport }), ); activateCanvasGenerationEntry('generate'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ]); const openSpecDialog = useCallback( (specType: SpecGenerationType) => { openPlacedCanvasGenerationDialog( createSpecDialogDraft({ canvasSize, viewport, specType }), ); activateCanvasGenerationEntry('generate'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ], ); const openCharacterAnimationPanel = useCallback( (layer: CanvasLayer) => { const nextPanel = createCharacterAnimationPanelDraft(layer); if (!nextPanel) { return; } setImageContextMenu(null); setQuickEditPanel(null); setCharacterAnimationPanel(nextPanel); selectSingleLayer(layer.id); }, [selectSingleLayer, setImageContextMenu], ); const openCharacterGenerationDialog = useCallback(() => { openPlacedCanvasGenerationDialog( createCharacterGenerationDialogDraft({ canvasSize, viewport, imageModel: lastImageModel, }), ); activateCanvasGenerationEntry('character'); }, [ activateCanvasGenerationEntry, canvasSize, lastImageModel, openPlacedCanvasGenerationDialog, viewport, ]); const openIconGenerationDialog = useCallback(() => { openPlacedCanvasGenerationDialog( createIconGenerationDialogDraft({ canvasSize, viewport, imageModel: lastImageModel, }), ); activateCanvasGenerationEntry('icon'); }, [ activateCanvasGenerationEntry, canvasSize, lastImageModel, openPlacedCanvasGenerationDialog, viewport, ]); const openVideoGenerationDialog = useCallback(() => { openPlacedCanvasGenerationDialog( createVideoGenerationDialogDraft({ canvasSize, viewport }), ); activateCanvasGenerationEntry('video'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ]); const openUiDesignGenerationDialog = useCallback(() => { openPlacedCanvasGenerationDialog( createUiDesignGenerationDialogDraft({ canvasSize, viewport, imageModel: 'gpt-image-2', }), ); activateCanvasGenerationEntry('ui-design'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ]); const openEditDialog = useCallback( (sourceLayer: CanvasLayer) => { setMetadataLayer(null); setImageContextMenu(null); setQuickEditPanel(null); setGenerateDialog(createEditDialogDraft(sourceLayer)); setActiveTool('generate'); }, [setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer], ); const openQuickEditPanel = useCallback( (sourceLayer: CanvasLayer) => { setImageContextMenu(null); setMetadataLayer(null); setGenerateDialog(null); setCharacterAnimationPanel(null); setQuickEditPanel(createQuickEditPanelDraft(sourceLayer)); selectSingleLayer(sourceLayer.id); setActiveTool('generate'); }, [ selectSingleLayer, setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer, ], ); const pickCharacterSpecFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => assignCharacterSpecReference(currentDialog, layer), ); setIsPickingCharacterSpecFromCanvas(false); setIsCharacterSpecMenuOpen(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const pickGenerationReferenceFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => appendGenerationReference(currentDialog, layer), ); setIsPickingGenerationReferenceFromCanvas(false); setIsGenerationReferenceMenuOpen(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const pickCharacterReferenceFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => appendCharacterReference(currentDialog, layer), ); setIsPickingCharacterReferenceFromCanvas(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const pickIconSpecFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => assignIconSpecReference(currentDialog, layer), ); if (layer.assetKind !== 'icon-spec') { return; } setIsPickingIconSpecFromCanvas(false); setIsIconSpecMenuOpen(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const pickUiDesignSpecFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => assignUiDesignSpecReference(currentDialog, layer), ); setIsPickingUiDesignSpecFromCanvas(false); setIsUiDesignSpecMenuOpen(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const updateIconDescription = useCallback( (index: number, value: string) => { setGenerateDialog((currentDialog) => updateIconDescriptionInDialog(currentDialog, index, value), ); }, [setGenerateDialog], ); const addIconDescription = useCallback(() => { setGenerateDialog(appendIconDescriptionToDialog); }, [setGenerateDialog]); const generationSubmissionWorkflow = useImageCanvasGenerationSubmissionWorkflow({ layers, canvasSize, viewport, layerCounterRef, quickEditPanel, quickEditSourceLayer, setQuickEditPanel, characterAnimationPanel, characterAnimationSourceLayer, setCharacterAnimationPanel, setGenerateDialog, updateCanvasGenerationDialogById, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, selectSingleLayer, fitLayers, setActiveTool, setActiveSidebarPanel, rememberImageModel: setLastImageModel, persistGeneratedAsset, }); const { submitCharacterAnimation, submitIconSpritesheetGeneration, submitImageGeneration, submitQuickEdit, } = generationSubmissionWorkflow; const updateSpecFormValue = useCallback( (key: keyof SpecFormValues, value: string) => { setGenerateDialog((currentDialog) => updateSpecFormDialogValue(currentDialog, key, value), ); }, [setGenerateDialog], ); const updateCharacterAnimationDuration = useCallback( (frameCountValue: string) => { setCharacterAnimationPanel((currentPanel) => updateCharacterAnimationDurationPanel(currentPanel, frameCountValue), ); }, [], ); const hideGeneratedLayerPanelAfterBlur = useCallback(() => { setGenerateDialog((currentDialog) => hideGeneratedLayerComposerAfterBlur(currentDialog), ); }, [setGenerateDialog]); const closeGenerateComposer = useCallback(() => { setGenerateDialog(closeGenerateComposerDialog); 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, isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen, isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen, isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen, isPickingGenerationReferenceFromCanvas, setIsPickingGenerationReferenceFromCanvas, isPickingCharacterSpecFromCanvas, setIsPickingCharacterSpecFromCanvas, isPickingCharacterReferenceFromCanvas, setIsPickingCharacterReferenceFromCanvas, isIconSpecMenuOpen, setIsIconSpecMenuOpen, isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas, isUiDesignSpecMenuOpen, setIsUiDesignSpecMenuOpen, isPickingUiDesignSpecFromCanvas, setIsPickingUiDesignSpecFromCanvas, openGenerateDialog, openSpecDialog, openCharacterAnimationPanel, openCharacterGenerationDialog, openIconGenerationDialog, openVideoGenerationDialog, openUiDesignGenerationDialog, openEditDialog, openQuickEditPanel, pickCharacterSpecFromLayer, pickGenerationReferenceFromLayer, pickCharacterReferenceFromLayer, pickIconSpecFromLayer, pickUiDesignSpecFromLayer, submitIconSpritesheetGeneration, submitQuickEdit, submitImageGeneration, updateSpecFormValue, updateIconDescription, addIconDescription, updateCharacterAnimationDuration, rememberImageModel: setLastImageModel, submitCharacterAnimation, hideGeneratedLayerPanelAfterBlur, closeGenerateComposer, clearDeletedLayerGenerationState, }), [ addIconDescription, characterAnimationPanel, characterAnimationPrice, characterAnimationSourceLayer, clearDeletedLayerGenerationState, closeGenerateComposer, hideGeneratedLayerPanelAfterBlur, iconDescriptionValues, isCharacterReferenceMenuOpen, isCharacterSpecMenuOpen, isIconSpecMenuOpen, isGenerationReferenceMenuOpen, isPickingCharacterReferenceFromCanvas, isPickingCharacterSpecFromCanvas, isPickingGenerationReferenceFromCanvas, isPickingIconSpecFromCanvas, isPickingUiDesignSpecFromCanvas, isUiDesignSpecMenuOpen, isSpecMenuOpen, openCharacterAnimationPanel, openCharacterGenerationDialog, openEditDialog, openGenerateDialog, openIconGenerationDialog, openUiDesignGenerationDialog, openQuickEditPanel, openSpecDialog, openVideoGenerationDialog, pickCharacterReferenceFromLayer, pickCharacterSpecFromLayer, pickGenerationReferenceFromLayer, pickIconSpecFromLayer, pickUiDesignSpecFromLayer, quickEditModelOptions, quickEditPanel, quickEditSizeOptions, quickEditSourceLayer, submitCharacterAnimation, submitIconSpritesheetGeneration, submitImageGeneration, submitQuickEdit, updateCharacterAnimationDuration, updateIconDescription, updateSpecFormValue, ], ); }