import { type Dispatch, type MutableRefObject, type PointerEvent as ReactPointerEvent, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import type { EditorAssetSnapshot, EditorProjectSnapshot, } from '../../services/image-editor/editorProjectClient'; import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel'; import type { CanvasGenerationDialogState, CanvasLayer, CanvasTool, CanvasViewport, CharacterAnimationPanelState, CharacterReferenceImage, CropExpandPanelState, CropExpandResizeHandle, GenerateDialogState, ImageContextMenuState, PublicationMaterialsWorkflowId, QuickEditPanelState, SidebarPanel, SpecFormValues, SpecGenerationType, } from './ImageCanvasEditorTypes'; import { appendCharacterReference, appendGenerationReference, appendPublicationReference, appendQuickEditReference, assignCharacterSpecReference, assignIconSpecReference, assignUiDesignSpecReference, closeGenerateComposerDialog, createAudioRedrawGenerationDialogDraft, createBackgroundMusicGenerationDialogDraft, createCharacterAnimationGenerationDialogDraft, createCharacterGenerationDialogDraft, createEditDialogDraft, createGenerateDialogDraft, createIconGenerationDialogDraft, createPublicationGenerationDialogDraft, createQuickEditPanelDraft, createRedrawPanelDraft, createSoundEffectGenerationDialogDraft, createSpecDialogDraft, createUiDesignGenerationDialogDraft, createVideoGenerationDialogDraft, hideGeneratedLayerComposerAfterBlur, isCharacterSpecReferenceLayer, isIconSpecReferenceLayer, updateCharacterAnimationDurationPanel, updateIconDescriptionsTextInDialog, updateSpecFormDialogValue, } from './ImageCanvasGenerationDialogModel'; import { calculateCharacterAnimationPrice, CHARACTER_ANIMATION_DURATION_OPTIONS, CHARACTER_ANIMATION_MODEL, DEFAULT_ICON_DESCRIPTIONS, DEFAULT_IMAGE_MODEL, ICON_DESCRIPTION_LIMIT, IMAGE_MODEL_GPT_IMAGE_2, isCanvasGenerationDialog, } from './ImageCanvasGenerationModel'; import { centerViewportOnPlacement, chooseGenerationPlacement, } from './ImageCanvasGenerationPlacementModel'; import { resolveQuickEditFocusViewport } from './ImageCanvasOverlayModel'; import { buildCropExpandInsetsFromFrame, removeImageBackground, renderCropExpandImage, } from './ImageCanvasRasterEditModel'; import { createUiAssetExtractionDraftMark, createUiAssetExtractionState, normalizeUiAssetExtractionMark, type UiAssetExtractionState, type UiAssetExtractionTool, updateUiAssetExtractionDraftMark, } from './ImageCanvasUiAssetExtractionModel'; import { useImageCanvasGenerationSubmissionWorkflow } from './useImageCanvasGenerationSubmissionWorkflow'; type CanvasSize = { width: number; height: number }; type CropExpandResizeDragState = { pointerId: number; handle: CropExpandResizeHandle; sourceLayerId: string; startClientX: number; startClientY: number; startScale: number; startFrame: CropExpandPanelState['frame']; ratio: CropExpandPanelState['ratio']; }; type CanvasGenerationDialogUpdater = ( dialog: CanvasGenerationDialogState, ) => CanvasGenerationDialogState | null; const LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY = 'genarrative.imageCanvas.lastCharacterSpecReference'; const LAST_ICON_SPEC_REFERENCE_CACHE_KEY = 'genarrative.imageCanvas.lastIconSpecReference'; const REFERENCE_PICK_WARNING_DISMISS_MS = 2600; const INVALID_CHARACTER_SPEC_WARNING = '选择的图片不是角色规范图,请选择生成规范里的角色规范。'; const INVALID_ICON_SPEC_WARNING = '选择的图片不是图标规范图,请选择生成规范里的图标规范。'; function appendQuickEditSelectionPrompt(prompt: string, markNumber: number) { const nextLine = `对${markNumber}号红色圈选框里的内容做以下修改:`; const trimmedPrompt = prompt.trimEnd(); return trimmedPrompt ? `${trimmedPrompt}\n${nextLine}` : nextLine; } function getLocalGenerationReferenceStorage() { if (typeof window === 'undefined') { return null; } try { return window.localStorage; } catch { return null; } } function readOptionalCachedString( record: Record, key: keyof CharacterReferenceImage, ) { const value = record[key]; return typeof value === 'string' ? value : undefined; } function readOptionalCachedNullableString( record: Record, key: keyof CharacterReferenceImage, ) { const value = record[key]; if (value === null) { return null; } return typeof value === 'string' ? value : undefined; } function readCachedGenerationReference( key: string, ): CharacterReferenceImage | null { const storage = getLocalGenerationReferenceStorage(); if (!storage) { return null; } try { const rawValue = storage.getItem(key); if (!rawValue) { return null; } const parsedValue: unknown = JSON.parse(rawValue); if (!parsedValue || typeof parsedValue !== 'object') { return null; } const record = parsedValue as Record; const id = readOptionalCachedString(record, 'id'); const label = readOptionalCachedString(record, 'label'); const src = readOptionalCachedString(record, 'src'); if (!id || !label || !src) { return null; } const reference: CharacterReferenceImage = { id, label, src }; const mediaType = readOptionalCachedString(record, 'mediaType'); if ( mediaType === 'image' || mediaType === 'video' || mediaType === 'audio' ) { reference.mediaType = mediaType; } const mimeType = readOptionalCachedString(record, 'mimeType'); if (mimeType) { reference.mimeType = mimeType; } const sizeBytes = record.sizeBytes; if (typeof sizeBytes === 'number') { reference.sizeBytes = sizeBytes; } const durationSeconds = record.durationSeconds; if (typeof durationSeconds === 'number') { reference.durationSeconds = durationSeconds; } for (const keyName of [ 'objectKey', 'assetObjectId', 'resourceId', 'sourceAssetId', ] as const) { const value = readOptionalCachedNullableString(record, keyName); if (value !== undefined) { reference[keyName] = value; } } return reference; } catch { return null; } } function writeCachedGenerationReference( key: string, reference: CharacterReferenceImage, ) { const storage = getLocalGenerationReferenceStorage(); if (!storage) { return; } try { storage.setItem(key, JSON.stringify(reference)); } catch { // 中文注释:本地缓存只提升下次新建素材的便捷性,写入失败不阻断生成流程。 } } function createCharacterAnimationPanelFromDialog( dialog: CanvasGenerationDialogState | null, ): CharacterAnimationPanelState | null { if (dialog?.mode !== 'character-animation' || !dialog.sourceLayerId) { return null; } return { sourceLayerId: dialog.sourceLayerId, promptText: dialog.prompt, resolution: dialog.characterAnimationResolution ?? '480p', ratio: dialog.characterAnimationRatio ?? 'same', frameCount: dialog.characterAnimationFrameCount ?? 32, durationSeconds: dialog.characterAnimationDurationSeconds ?? 4, status: dialog.status === 'generating' ? 'generating' : dialog.status === 'failed' ? 'failed' : dialog.characterAnimationResult ? 'completed' : 'idle', errorMessage: dialog.errorMessage, result: dialog.characterAnimationResult, }; } function applyCharacterAnimationPanelToDialog( dialog: CanvasGenerationDialogState, panel: CharacterAnimationPanelState | null, ): CanvasGenerationDialogState { if (dialog.mode !== 'character-animation') { return dialog; } if (!panel) { return { ...dialog, composerOpen: false, }; } return { ...dialog, prompt: panel.promptText, characterAnimationResolution: panel.resolution, characterAnimationRatio: panel.ratio, characterAnimationFrameCount: panel.frameCount, characterAnimationDurationSeconds: panel.durationSeconds, characterAnimationResult: panel.result, status: panel.status === 'generating' ? 'generating' : panel.status === 'failed' ? 'failed' : 'idle', errorMessage: panel.errorMessage, }; } type GenerationWorkflowOptions = { layers: CanvasLayer[]; canvasSize: CanvasSize; viewport: CanvasViewport; setViewport: Dispatch>; setLayers: Dispatch>; canvasGenerationDialogs: CanvasGenerationDialogState[]; layerCounterRef: MutableRefObject; generateDialog: GenerateDialogState | null; setGenerateDialog: Dispatch>; openCanvasGenerationDialog: ( dialog: Omit, ) => string; updateCanvasGenerationDialogById: ( dialogId: string, updater: CanvasGenerationDialogUpdater, ) => void; hasCanvasGenerationDialogById: (dialogId: string) => boolean; archiveActiveCanvasGenerationDialog: () => void; removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void; getGeneratingDialogPlaceholder: ( dialog: GenerateDialogState, ) => GenerateDialogState['placeholder']; appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void; selectSingleLayer: (layerId: string | null) => void; fitLayers: (targetLayers?: CanvasLayer[]) => void; captureCanvasHistory: () => void; setActiveTool: Dispatch>; setActiveSidebarPanel: Dispatch>; setMetadataLayer: Dispatch>; setImageContextMenu: Dispatch>; persistGeneratedAsset?: (layer: CanvasLayer) => void; persistUpdatedLayerResource?: (layer: CanvasLayer) => void; projectId?: string | null; assetFolderId?: string | null; upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void; applyProjectSnapshot?: (project: EditorProjectSnapshot) => void; onWalletBalanceMayHaveChanged?: () => void; }; export function useImageCanvasGenerationWorkflow({ layers, canvasSize, viewport, setViewport, setLayers, canvasGenerationDialogs, layerCounterRef, generateDialog, setGenerateDialog, openCanvasGenerationDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, archiveActiveCanvasGenerationDialog, removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, selectSingleLayer, fitLayers, captureCanvasHistory, setActiveTool, setActiveSidebarPanel, setMetadataLayer, setImageContextMenu, persistGeneratedAsset, persistUpdatedLayerResource, projectId, assetFolderId, upsertGeneratedAsset, applyProjectSnapshot, onWalletBalanceMayHaveChanged, }: 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 [ isPickingQuickEditReferenceFromCanvas, setIsPickingQuickEditReferenceFromCanvas, ] = 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 [isMusicMenuOpen, setIsMusicMenuOpen] = useState(false); const [isPublicationMenuOpen, setIsPublicationMenuOpen] = useState(false); const [isPublicationReferenceMenuOpen, setIsPublicationReferenceMenuOpen] = useState(false); const [ isPickingPublicationReferenceFromCanvas, setIsPickingPublicationReferenceFromCanvas, ] = useState(false); const [quickEditPanel, setQuickEditPanel] = useState(null); const [referencePickWarning, setReferencePickWarning] = useState< string | null >(null); const [cropExpandPanel, setCropExpandPanel] = useState(null); const [uiAssetExtractionState, setUiAssetExtractionState] = useState(null); const [quickEditSelectionState, setQuickEditSelectionState] = useState(null); const quickEditSelectionStateRef = useRef(null); const cropExpandPanelRef = useRef(null); const cropExpandResizeDragRef = useRef( null, ); const uiAssetExtractionMarkCounterRef = useRef(0); const quickEditSelectionMarkCounterRef = useRef(0); const [characterAnimationPanel, setCharacterAnimationPanel] = useState(null); const [lastImageModel, setLastImageModel] = useState(DEFAULT_IMAGE_MODEL); const [initialCharacterSpecReference] = useState(() => readCachedGenerationReference(LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY), ); const [initialIconSpecReference] = useState( () => readCachedGenerationReference(LAST_ICON_SPEC_REFERENCE_CACHE_KEY), ); const lastCharacterSpecReferenceRef = useRef( initialCharacterSpecReference, ); const lastIconSpecReferenceRef = useRef( initialIconSpecReference, ); const characterAnimationDialog = isCanvasGenerationDialog(generateDialog) && generateDialog.mode === 'character-animation' ? generateDialog : null; const characterAnimationDialogPanel = createCharacterAnimationPanelFromDialog( characterAnimationDialog, ); const effectiveCharacterAnimationPanel = characterAnimationDialogPanel ?? characterAnimationPanel; cropExpandPanelRef.current = cropExpandPanel; quickEditSelectionStateRef.current = quickEditSelectionState; const updateQuickEditSelectionState = useCallback( ( updater: ( currentState: UiAssetExtractionState | null, ) => UiAssetExtractionState | null, ) => { const nextState = updater(quickEditSelectionStateRef.current); quickEditSelectionStateRef.current = nextState; setQuickEditSelectionState(nextState); return nextState; }, [], ); useEffect(() => { if ( generateDialog?.mode === 'character' && generateDialog.characterSpecReference ) { lastCharacterSpecReferenceRef.current = generateDialog.characterSpecReference; writeCachedGenerationReference( LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY, generateDialog.characterSpecReference, ); return; } if (generateDialog?.mode === 'icon' && generateDialog.iconSpecReference) { lastIconSpecReferenceRef.current = generateDialog.iconSpecReference; writeCachedGenerationReference( LAST_ICON_SPEC_REFERENCE_CACHE_KEY, generateDialog.iconSpecReference, ); return; } if ( generateDialog?.mode === 'ui-design' && generateDialog.uiDesignSpecReference ) { lastIconSpecReferenceRef.current = generateDialog.uiDesignSpecReference; writeCachedGenerationReference( LAST_ICON_SPEC_REFERENCE_CACHE_KEY, generateDialog.uiDesignSpecReference, ); } }, [generateDialog]); useEffect(() => { if (!referencePickWarning) { return undefined; } const warningTimer = window.setTimeout(() => { setReferencePickWarning(null); }, REFERENCE_PICK_WARNING_DISMISS_MS); return () => window.clearTimeout(warningTimer); }, [referencePickWarning]); const quickEditSourceLayer = quickEditPanel ? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ?? null) : null; const cropExpandSourceLayer = cropExpandPanel ? (layers.find((layer) => layer.id === cropExpandPanel.sourceLayerId) ?? null) : null; const uiAssetExtractionSourceLayer = uiAssetExtractionState ? (layers.find( (layer) => layer.id === uiAssetExtractionState.sourceLayerId, ) ?? null) : null; const quickEditSelectionSourceLayer = quickEditSelectionState ? (layers.find( (layer) => layer.id === quickEditSelectionState.sourceLayerId, ) ?? null) : null; const characterAnimationSourceLayer = effectiveCharacterAnimationPanel ? (layers.find( (layer) => layer.id === effectiveCharacterAnimationPanel.sourceLayerId, ) ?? null) : null; const characterAnimationPrice = effectiveCharacterAnimationPanel ? calculateCharacterAnimationPrice( CHARACTER_ANIMATION_MODEL, effectiveCharacterAnimationPanel.resolution, effectiveCharacterAnimationPanel.durationSeconds, ) : 0; const iconDescriptionValues = generateDialog?.mode === 'icon' ? generateDialog.prompt.trim() ? generateDialog.prompt .split(/[\r\n,,、;;/|]+/u) .map((description) => description.trim()) .filter(Boolean) .slice(0, ICON_DESCRIPTION_LIMIT) : (generateDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS) : DEFAULT_ICON_DESCRIPTIONS; const closeGenerationTransientState = useCallback(() => { setIsSpecMenuOpen(false); setIsGenerationReferenceMenuOpen(false); setIsPickingQuickEditReferenceFromCanvas(false); setIsCharacterSpecMenuOpen(false); setIsCharacterReferenceMenuOpen(false); setIsPickingGenerationReferenceFromCanvas(false); setIsPickingCharacterSpecFromCanvas(false); setIsPickingCharacterReferenceFromCanvas(false); setIsIconSpecMenuOpen(false); setIsPickingIconSpecFromCanvas(false); setIsUiDesignSpecMenuOpen(false); setIsPickingUiDesignSpecFromCanvas(false); setIsMusicMenuOpen(false); setIsPublicationMenuOpen(false); setIsPublicationReferenceMenuOpen(false); setIsPickingPublicationReferenceFromCanvas(false); setUiAssetExtractionState(null); setQuickEditSelectionState(null); 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); setCropExpandPanel(null); setCharacterAnimationPanel(null); setUiAssetExtractionState(null); setQuickEditSelectionState(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 draft = createCharacterAnimationGenerationDialogDraft({ canvasSize, viewport, layer, }); if (!draft) { return; } openPlacedCanvasGenerationDialog(draft); activateCanvasGenerationEntry('character'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ], ); const openCharacterGenerationDialog = useCallback(() => { const draft = createCharacterGenerationDialogDraft({ canvasSize, viewport, imageModel: lastImageModel, }); openPlacedCanvasGenerationDialog({ ...draft, characterSpecReference: lastCharacterSpecReferenceRef.current ?? draft.characterSpecReference, }); activateCanvasGenerationEntry('character'); }, [ activateCanvasGenerationEntry, canvasSize, lastImageModel, openPlacedCanvasGenerationDialog, viewport, ]); const openIconGenerationDialog = useCallback(() => { const draft = createIconGenerationDialogDraft({ canvasSize, viewport, imageModel: lastImageModel, }); openPlacedCanvasGenerationDialog({ ...draft, iconSpecReference: lastIconSpecReferenceRef.current ?? draft.iconSpecReference, }); activateCanvasGenerationEntry('icon'); }, [ activateCanvasGenerationEntry, canvasSize, lastImageModel, openPlacedCanvasGenerationDialog, viewport, ]); const openPublicationGenerationDialog = useCallback( (workflowId: PublicationMaterialsWorkflowId) => { openPlacedCanvasGenerationDialog( createPublicationGenerationDialogDraft({ canvasSize, viewport, workflowId, }), ); activateCanvasGenerationEntry('publication'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ], ); const openVideoGenerationDialog = useCallback(() => { openPlacedCanvasGenerationDialog( createVideoGenerationDialogDraft({ canvasSize, viewport }), ); activateCanvasGenerationEntry('video'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ]); const openUiDesignGenerationDialog = useCallback(() => { const draft = createUiDesignGenerationDialogDraft({ canvasSize, viewport, imageModel: IMAGE_MODEL_GPT_IMAGE_2, }); openPlacedCanvasGenerationDialog({ ...draft, uiDesignSpecReference: lastIconSpecReferenceRef.current ?? draft.uiDesignSpecReference, }); activateCanvasGenerationEntry('ui-design'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ]); const openSoundEffectGenerationDialog = useCallback(() => { openPlacedCanvasGenerationDialog( createSoundEffectGenerationDialogDraft({ canvasSize, viewport }), ); activateCanvasGenerationEntry('music'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ]); const openBackgroundMusicGenerationDialog = useCallback(() => { openPlacedCanvasGenerationDialog( createBackgroundMusicGenerationDialogDraft({ canvasSize, viewport }), ); activateCanvasGenerationEntry('music'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, viewport, ]); const openEditDialog = useCallback( (sourceLayer: CanvasLayer) => { setMetadataLayer(null); setImageContextMenu(null); setQuickEditPanel(null); setCropExpandPanel(null); setUiAssetExtractionState(null); archiveActiveCanvasGenerationDialog(); setGenerateDialog(createEditDialogDraft(sourceLayer)); setActiveTool('generate'); }, [ archiveActiveCanvasGenerationDialog, setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer, ], ); const openQuickEditPanel = useCallback( (sourceLayer: CanvasLayer) => { setImageContextMenu(null); setMetadataLayer(null); setUiAssetExtractionState(null); archiveActiveCanvasGenerationDialog(); setGenerateDialog(null); setCropExpandPanel(null); setCharacterAnimationPanel(null); setQuickEditPanel(createQuickEditPanelDraft(sourceLayer)); const nextSelectionState = createUiAssetExtractionState(sourceLayer.id, { initialTool: null, }); quickEditSelectionStateRef.current = nextSelectionState; setQuickEditSelectionState(nextSelectionState); quickEditSelectionMarkCounterRef.current = 0; selectSingleLayer(sourceLayer.id); setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize })); setActiveTool('generate'); }, [ canvasSize, selectSingleLayer, archiveActiveCanvasGenerationDialog, setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer, setViewport, ], ); const updateSourceLayer = useCallback( (sourceLayerId: string, updater: (layer: CanvasLayer) => CanvasLayer) => { const sourceLayer = layers.find((layer) => layer.id === sourceLayerId); if (!sourceLayer) { return; } const updatedLayer = updater(sourceLayer); captureCanvasHistory(); setLayers((currentLayers) => currentLayers.map((layer) => layer.id === sourceLayerId ? updatedLayer : layer, ), ); persistUpdatedLayerResource?.(updatedLayer); selectSingleLayer(sourceLayerId); fitLayers([updatedLayer]); }, [ captureCanvasHistory, fitLayers, layers, persistUpdatedLayerResource, selectSingleLayer, setLayers, ], ); const openRedrawPanel = useCallback( (sourceLayer: CanvasLayer) => { setImageContextMenu(null); setMetadataLayer(null); setCropExpandPanel(null); setCharacterAnimationPanel(null); setUiAssetExtractionState(null); setQuickEditSelectionState(null); if (sourceLayer.mediaType === 'audio') { const audioDraft = createAudioRedrawGenerationDialogDraft(sourceLayer); if (!audioDraft) { return; } setQuickEditPanel(null); selectSingleLayer(sourceLayer.id); openCanvasGenerationDialog(audioDraft); setActiveTool('music'); return; } archiveActiveCanvasGenerationDialog(); setGenerateDialog(null); setQuickEditPanel(createRedrawPanelDraft(sourceLayer)); selectSingleLayer(sourceLayer.id); setActiveTool('generate'); }, [ archiveActiveCanvasGenerationDialog, openCanvasGenerationDialog, selectSingleLayer, setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer, ], ); const openCropExpandPanel = useCallback( (sourceLayer: CanvasLayer) => { setImageContextMenu(null); setMetadataLayer(null); setUiAssetExtractionState(null); archiveActiveCanvasGenerationDialog(); setGenerateDialog(null); setQuickEditPanel(null); setCharacterAnimationPanel(null); setCropExpandPanel({ sourceLayerId: sourceLayer.id, frame: { x: sourceLayer.x, y: sourceLayer.y, width: sourceLayer.width, height: sourceLayer.height, }, ratio: 'free', status: 'idle', }); selectSingleLayer(sourceLayer.id); setActiveTool('select'); }, [ archiveActiveCanvasGenerationDialog, selectSingleLayer, setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer, ], ); const startCropExpandFrameResize = useCallback( ( event: ReactPointerEvent, handle: CropExpandResizeHandle, ) => { const currentPanel = cropExpandPanelRef.current; if (!currentPanel) { return; } event.preventDefault(); event.stopPropagation(); cropExpandResizeDragRef.current = { pointerId: event.pointerId, handle, sourceLayerId: currentPanel.sourceLayerId, startClientX: event.clientX, startClientY: event.clientY, startScale: Number.isFinite(viewport.scale) && viewport.scale > 0 ? viewport.scale : 1, startFrame: currentPanel.frame, ratio: currentPanel.ratio, }; selectSingleLayer(currentPanel.sourceLayerId); }, [selectSingleLayer, viewport.scale], ); useEffect(() => { const handlePointerMove = (event: PointerEvent) => { const dragState = cropExpandResizeDragRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; } event.preventDefault(); const deltaX = (event.clientX - dragState.startClientX) / dragState.startScale; const deltaY = (event.clientY - dragState.startClientY) / dragState.startScale; const nextFrame = resizeCropExpandFrame({ frame: dragState.startFrame, handle: dragState.handle, deltaX, deltaY, ratio: dragState.ratio, }); setCropExpandPanel((currentPanel) => currentPanel?.sourceLayerId === dragState.sourceLayerId ? { ...currentPanel, frame: nextFrame, status: currentPanel.status === 'failed' ? 'idle' : currentPanel.status, errorMessage: currentPanel.status === 'failed' ? undefined : currentPanel.errorMessage, } : currentPanel, ); }; const handlePointerUp = (event: PointerEvent) => { const dragState = cropExpandResizeDragRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; } cropExpandResizeDragRef.current = null; }; window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointercancel', handlePointerUp); return () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointercancel', handlePointerUp); }; }, [setCropExpandPanel]); const submitCropExpand = useCallback(async () => { if (!cropExpandPanel || !cropExpandSourceLayer) { return; } try { const insets = buildCropExpandInsetsFromFrame({ layer: cropExpandSourceLayer, frame: cropExpandPanel.frame, }); setCropExpandPanel({ ...cropExpandPanel, status: 'processing', errorMessage: undefined, }); const result = await renderCropExpandImage({ source: cropExpandSourceLayer.src, insets, }); layerCounterRef.current += 1; const cropExpandIndex = layerCounterRef.current; const nextLayer: CanvasLayer = { ...cropExpandSourceLayer, id: `layer-crop-expand-${cropExpandIndex}`, resourceId: `local-resource-crop-expand-${cropExpandIndex}`, title: `${cropExpandSourceLayer.title} 裁扩`, src: result.imageSrc, x: cropExpandSourceLayer.x + cropExpandSourceLayer.width + 32, y: cropExpandSourceLayer.y, width: result.width, height: result.height, originalWidth: result.width, originalHeight: result.height, zIndex: cropExpandIndex + 10, sourceType: 'generated', sourceResourceId: cropExpandSourceLayer.resourceId, objectKey: null, assetObjectId: null, sourceAssetId: null, }; captureCanvasHistory(); appendCanvasLayersWithResources([nextLayer]); persistGeneratedAsset?.(nextLayer); selectSingleLayer(nextLayer.id); fitLayers([cropExpandSourceLayer, nextLayer]); setCropExpandPanel(null); setActiveSidebarPanel('layers'); } catch (error) { setCropExpandPanel({ ...cropExpandPanel, status: 'failed', errorMessage: error instanceof Error && error.message.trim() ? error.message : '裁扩图片失败', }); } }, [ cropExpandPanel, cropExpandSourceLayer, appendCanvasLayersWithResources, captureCanvasHistory, fitLayers, layerCounterRef, persistGeneratedAsset, selectSingleLayer, setActiveSidebarPanel, ]); const removeSelectedLayerBackground = useCallback( async (sourceLayer: CanvasLayer) => { setImageContextMenu(null); setMetadataLayer(null); setCropExpandPanel(null); setQuickEditPanel(null); try { const result = await removeImageBackground(sourceLayer.src); updateSourceLayer(sourceLayer.id, (layer) => ({ ...layer, src: result.imageSrc, width: result.width, height: result.height, originalWidth: result.width, originalHeight: result.height, objectKey: null, assetObjectId: null, sourceAssetId: null, })); setActiveSidebarPanel('layers'); } catch (error) { window.alert( error instanceof Error && error.message.trim() ? error.message : '去除背景失败', ); } }, [ setActiveSidebarPanel, setImageContextMenu, setMetadataLayer, updateSourceLayer, ], ); const pickCharacterSpecFromLayer = useCallback( (layer: CanvasLayer) => { if (!isCharacterSpecReferenceLayer(layer)) { setReferencePickWarning(INVALID_CHARACTER_SPEC_WARNING); return; } setReferencePickWarning(null); setGenerateDialog((currentDialog) => { const nextDialog = assignCharacterSpecReference(currentDialog, layer); if ( nextDialog?.mode === 'character' && nextDialog.characterSpecReference ) { lastCharacterSpecReferenceRef.current = nextDialog.characterSpecReference; } return nextDialog; }); 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 pickQuickEditReferenceFromLayer = useCallback( (layer: CanvasLayer) => { setQuickEditPanel((currentPanel) => appendQuickEditReference(currentPanel, layer), ); setIsPickingQuickEditReferenceFromCanvas(false); setImageContextMenu(null); }, [setImageContextMenu], ); const pickCharacterReferenceFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => appendCharacterReference(currentDialog, layer), ); setIsPickingCharacterReferenceFromCanvas(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const pickIconSpecFromLayer = useCallback( (layer: CanvasLayer) => { if (!isIconSpecReferenceLayer(layer)) { setReferencePickWarning(INVALID_ICON_SPEC_WARNING); return; } setReferencePickWarning(null); const nextDialog = assignIconSpecReference(generateDialog, layer); if (nextDialog?.mode === 'icon' && nextDialog.iconSpecReference) { lastIconSpecReferenceRef.current = nextDialog.iconSpecReference; } setGenerateDialog(nextDialog); if (nextDialog === generateDialog) { return; } setIsPickingIconSpecFromCanvas(false); setIsIconSpecMenuOpen(false); setImageContextMenu(null); }, [generateDialog, setGenerateDialog, setImageContextMenu], ); const pickUiDesignSpecFromLayer = useCallback( (layer: CanvasLayer) => { if (!isIconSpecReferenceLayer(layer)) { setReferencePickWarning(INVALID_ICON_SPEC_WARNING); return; } setReferencePickWarning(null); const nextDialog = assignUiDesignSpecReference(generateDialog, layer); if ( nextDialog?.mode === 'ui-design' && nextDialog.uiDesignSpecReference ) { lastIconSpecReferenceRef.current = nextDialog.uiDesignSpecReference; } setGenerateDialog(nextDialog); if (nextDialog === generateDialog) { return; } setIsPickingUiDesignSpecFromCanvas(false); setIsUiDesignSpecMenuOpen(false); setImageContextMenu(null); }, [generateDialog, setGenerateDialog, setImageContextMenu], ); const pickPublicationReferenceFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => appendPublicationReference(currentDialog, layer), ); setIsPickingPublicationReferenceFromCanvas(false); setIsPublicationReferenceMenuOpen(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const updateIconDescriptionsText = useCallback( (value: string) => { setGenerateDialog((currentDialog) => updateIconDescriptionsTextInDialog(currentDialog, value), ); }, [setGenerateDialog], ); const generationSubmissionWorkflow = useImageCanvasGenerationSubmissionWorkflow({ layers, canvasSize, viewport, layerCounterRef, quickEditPanel, quickEditSourceLayer, quickEditSelectionState, canvasGenerationDialogs, setQuickEditPanel, characterAnimationPanel: effectiveCharacterAnimationPanel, characterAnimationDialog, characterAnimationSourceLayer, setCharacterAnimationPanel, setGenerateDialog, openCanvasGenerationDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, selectSingleLayer, fitLayers, setActiveTool, setActiveSidebarPanel, rememberImageModel: setLastImageModel, projectId, assetFolderId, upsertGeneratedAsset, applyProjectSnapshot, onWalletBalanceMayHaveChanged, }); const setEffectiveCharacterAnimationPanel: Dispatch< SetStateAction > = useCallback( (updater) => { setGenerateDialog((currentDialog) => { if ( !isCanvasGenerationDialog(currentDialog) || currentDialog.mode !== 'character-animation' ) { return currentDialog; } const currentPanel = createCharacterAnimationPanelFromDialog(currentDialog); const nextPanel = typeof updater === 'function' ? updater(currentPanel) : updater; return applyCharacterAnimationPanelToDialog(currentDialog, nextPanel); }); setCharacterAnimationPanel((currentPanel) => { if (characterAnimationDialog) { return currentPanel; } return typeof updater === 'function' ? updater(currentPanel) : updater; }); }, [characterAnimationDialog, setGenerateDialog], ); const { extractUiDesignAssets: submitUiDesignAssetExtraction, submitCharacterAnimation, submitIconSpritesheetGeneration, submitImageGeneration, submitQuickEdit, } = generationSubmissionWorkflow; const extractUiDesignAssets = useCallback( (sourceLayer: CanvasLayer) => { if (sourceLayer.assetKind !== 'ui-design') { return; } closeGenerationTransientState(); setMetadataLayer(null); setQuickEditPanel(null); setCropExpandPanel(null); setCharacterAnimationPanel(null); selectSingleLayer(sourceLayer.id); setActiveTool('select'); setUiAssetExtractionState( createUiAssetExtractionState(sourceLayer.id, { initialTool: 'rect' }), ); setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize })); }, [ canvasSize, closeGenerationTransientState, selectSingleLayer, setActiveTool, setCharacterAnimationPanel, setCropExpandPanel, setMetadataLayer, setQuickEditPanel, ], ); const changeUiAssetExtractionTool = useCallback( (tool: UiAssetExtractionTool | null) => { setUiAssetExtractionState((currentState) => currentState ? { ...currentState, tool: currentState.tool === tool ? null : tool, draftMark: null, status: currentState.status === 'failed' ? 'idle' : currentState.status, errorMessage: currentState.status === 'failed' ? undefined : currentState.errorMessage, } : currentState, ); }, [], ); const changeQuickEditSelectionTool = useCallback( (tool: UiAssetExtractionTool | null) => { updateQuickEditSelectionState((currentState) => currentState ? { ...currentState, tool: currentState.tool === tool ? null : tool, draftMark: null, status: 'idle', errorMessage: undefined, } : currentState, ); }, [updateQuickEditSelectionState], ); const startUiAssetExtractionPointer = useCallback( (point: { x: number; y: number }) => { setUiAssetExtractionState((currentState) => { if ( !currentState || !currentState.tool || currentState.status === 'extracting' ) { return currentState; } uiAssetExtractionMarkCounterRef.current += 1; return { ...currentState, draftMark: createUiAssetExtractionDraftMark({ tool: currentState.tool, id: `ui-extraction-mark-${uiAssetExtractionMarkCounterRef.current}`, point, }), status: 'idle', errorMessage: undefined, }; }); }, [], ); const startQuickEditSelectionPointer = useCallback( (point: { x: number; y: number }) => { updateQuickEditSelectionState((currentState) => { if (!currentState || !currentState.tool) { return currentState; } quickEditSelectionMarkCounterRef.current += 1; return { ...currentState, draftMark: createUiAssetExtractionDraftMark({ tool: currentState.tool, id: `quick-edit-selection-mark-${quickEditSelectionMarkCounterRef.current}`, point, }), status: 'idle', errorMessage: undefined, }; }); }, [updateQuickEditSelectionState], ); const moveUiAssetExtractionPointer = useCallback( (point: { x: number; y: number }) => { setUiAssetExtractionState((currentState) => currentState?.draftMark ? { ...currentState, draftMark: updateUiAssetExtractionDraftMark( currentState.draftMark, point, ), } : currentState, ); }, [], ); const moveQuickEditSelectionPointer = useCallback( (point: { x: number; y: number }) => { updateQuickEditSelectionState((currentState) => currentState?.draftMark ? { ...currentState, draftMark: updateUiAssetExtractionDraftMark( currentState.draftMark, point, ), } : currentState, ); }, [updateQuickEditSelectionState], ); const endUiAssetExtractionPointer = useCallback(() => { setUiAssetExtractionState((currentState) => { if (!currentState?.draftMark) { return currentState; } const normalizedMark = normalizeUiAssetExtractionMark( currentState.draftMark, ); return { ...currentState, draftMark: null, marks: normalizedMark ? [...currentState.marks, normalizedMark] : currentState.marks, }; }); }, []); const endQuickEditSelectionPointer = useCallback(() => { const currentSelectionState = quickEditSelectionStateRef.current; if (!currentSelectionState?.draftMark) { return; } const normalizedMark = normalizeUiAssetExtractionMark( currentSelectionState.draftMark, ); if (!normalizedMark) { updateQuickEditSelectionState(() => ({ ...currentSelectionState, draftMark: null, })); return; } const nextMarkNumber = currentSelectionState.marks.length + 1; updateQuickEditSelectionState(() => ({ ...currentSelectionState, draftMark: null, marks: [...currentSelectionState.marks, normalizedMark], })); setQuickEditPanel((currentPanel) => currentPanel ? { ...currentPanel, status: currentPanel.status === 'failed' ? 'idle' : currentPanel.status, errorMessage: currentPanel.status === 'failed' ? undefined : currentPanel.errorMessage, prompt: appendQuickEditSelectionPrompt( currentPanel.prompt, nextMarkNumber, ), } : currentPanel, ); }, [updateQuickEditSelectionState]); useEffect(() => { if (!quickEditPanel && quickEditSelectionState) { quickEditSelectionStateRef.current = null; setQuickEditSelectionState(null); } }, [quickEditPanel, quickEditSelectionState]); const cancelUiAssetExtraction = useCallback(() => { setUiAssetExtractionState(null); }, []); const submitUiAssetExtraction = useCallback(async () => { const currentState = uiAssetExtractionState; const sourceLayer = uiAssetExtractionSourceLayer; if (!currentState || !sourceLayer || !currentState.marks.length) { return; } setUiAssetExtractionState({ ...currentState, status: 'extracting', errorMessage: undefined, }); try { await submitUiDesignAssetExtraction(sourceLayer, { marks: currentState.marks, suppressAlert: true, }); setUiAssetExtractionState(null); } catch (error) { setUiAssetExtractionState((latestState) => latestState?.sourceLayerId === currentState.sourceLayerId ? { ...latestState, status: 'failed', errorMessage: error instanceof Error && error.message.trim() ? error.message : '提取素材失败', } : latestState, ); } }, [ submitUiDesignAssetExtraction, uiAssetExtractionSourceLayer, uiAssetExtractionState, ]); const updateSpecFormValue = useCallback( (key: keyof SpecFormValues, value: string) => { setGenerateDialog((currentDialog) => updateSpecFormDialogValue(currentDialog, key, value), ); }, [setGenerateDialog], ); const updateCharacterAnimationDuration = useCallback( (frameCountValue: string) => { const option = CHARACTER_ANIMATION_DURATION_OPTIONS.find( (item) => String(item.frameCount) === frameCountValue, ); if (option) { setGenerateDialog((currentDialog) => currentDialog?.mode === 'character-animation' ? { ...currentDialog, characterAnimationFrameCount: option.frameCount, characterAnimationDurationSeconds: option.durationSeconds, status: currentDialog.status === 'failed' ? 'idle' : currentDialog.status, errorMessage: currentDialog.status === 'failed' ? undefined : currentDialog.errorMessage, } : currentDialog, ); } setCharacterAnimationPanel((currentPanel) => updateCharacterAnimationDurationPanel(currentPanel, frameCountValue), ); }, [setGenerateDialog], ); 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, ); setQuickEditPanel((currentPanel) => currentPanel ? { ...currentPanel, quickEditReferences: ( currentPanel.quickEditReferences ?? [] ).filter( (reference) => reference.id !== `canvas-${targetLayerId}`, ), } : currentPanel, ); setCropExpandPanel((currentPanel) => currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel, ); setUiAssetExtractionState((currentState) => currentState?.sourceLayerId === targetLayerId ? null : currentState, ); setQuickEditSelectionState((currentState) => currentState?.sourceLayerId === targetLayerId ? null : currentState, ); 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, cropExpandPanel, setCropExpandPanel, cropExpandSourceLayer, uiAssetExtractionState, uiAssetExtractionSourceLayer, quickEditSelectionState, quickEditSelectionSourceLayer, changeUiAssetExtractionTool, changeQuickEditSelectionTool, startUiAssetExtractionPointer, startQuickEditSelectionPointer, moveUiAssetExtractionPointer, moveQuickEditSelectionPointer, endUiAssetExtractionPointer, endQuickEditSelectionPointer, cancelUiAssetExtraction, submitUiAssetExtraction, characterAnimationPanel: effectiveCharacterAnimationPanel, setCharacterAnimationPanel: setEffectiveCharacterAnimationPanel, characterAnimationSourceLayer, characterAnimationPrice, iconDescriptionValues, isSpecMenuOpen, setIsSpecMenuOpen, isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen, isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen, isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen, isPickingGenerationReferenceFromCanvas, setIsPickingGenerationReferenceFromCanvas, isPickingQuickEditReferenceFromCanvas, setIsPickingQuickEditReferenceFromCanvas, isPickingCharacterSpecFromCanvas, setIsPickingCharacterSpecFromCanvas, isPickingCharacterReferenceFromCanvas, setIsPickingCharacterReferenceFromCanvas, isIconSpecMenuOpen, setIsIconSpecMenuOpen, isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas, isUiDesignSpecMenuOpen, setIsUiDesignSpecMenuOpen, isPickingUiDesignSpecFromCanvas, setIsPickingUiDesignSpecFromCanvas, isMusicMenuOpen, setIsMusicMenuOpen, isPublicationMenuOpen, setIsPublicationMenuOpen, isPublicationReferenceMenuOpen, setIsPublicationReferenceMenuOpen, isPickingPublicationReferenceFromCanvas, setIsPickingPublicationReferenceFromCanvas, referencePickWarning, clearReferencePickWarning: () => setReferencePickWarning(null), openGenerateDialog, openSpecDialog, openCharacterAnimationPanel, openCharacterGenerationDialog, openIconGenerationDialog, openPublicationGenerationDialog, openVideoGenerationDialog, openUiDesignGenerationDialog, openSoundEffectGenerationDialog, openBackgroundMusicGenerationDialog, openEditDialog, openQuickEditPanel, openRedrawPanel, openCropExpandPanel, startCropExpandFrameResize, removeSelectedLayerBackground, extractUiDesignAssets, pickCharacterSpecFromLayer, pickGenerationReferenceFromLayer, pickQuickEditReferenceFromLayer, pickCharacterReferenceFromLayer, pickIconSpecFromLayer, pickUiDesignSpecFromLayer, pickPublicationReferenceFromLayer, submitIconSpritesheetGeneration, submitQuickEdit, submitCropExpand, submitImageGeneration, updateSpecFormValue, updateIconDescriptionsText, updateCharacterAnimationDuration, rememberImageModel: setLastImageModel, submitCharacterAnimation, hideGeneratedLayerPanelAfterBlur, closeGenerateComposer, clearDeletedLayerGenerationState, }), [ effectiveCharacterAnimationPanel, characterAnimationPrice, characterAnimationSourceLayer, clearDeletedLayerGenerationState, closeGenerateComposer, cropExpandPanel, cropExpandSourceLayer, uiAssetExtractionState, uiAssetExtractionSourceLayer, quickEditSelectionState, quickEditSelectionSourceLayer, changeUiAssetExtractionTool, changeQuickEditSelectionTool, startUiAssetExtractionPointer, startQuickEditSelectionPointer, moveUiAssetExtractionPointer, moveQuickEditSelectionPointer, endUiAssetExtractionPointer, endQuickEditSelectionPointer, cancelUiAssetExtraction, submitUiAssetExtraction, setEffectiveCharacterAnimationPanel, hideGeneratedLayerPanelAfterBlur, iconDescriptionValues, isCharacterReferenceMenuOpen, isCharacterSpecMenuOpen, isIconSpecMenuOpen, isGenerationReferenceMenuOpen, isPickingCharacterReferenceFromCanvas, isPickingCharacterSpecFromCanvas, isPickingGenerationReferenceFromCanvas, isPickingQuickEditReferenceFromCanvas, isPickingIconSpecFromCanvas, isPickingUiDesignSpecFromCanvas, isUiDesignSpecMenuOpen, isMusicMenuOpen, isPublicationMenuOpen, isPublicationReferenceMenuOpen, isSpecMenuOpen, isPickingPublicationReferenceFromCanvas, referencePickWarning, openBackgroundMusicGenerationDialog, openCharacterAnimationPanel, openCharacterGenerationDialog, openCropExpandPanel, openEditDialog, extractUiDesignAssets, openGenerateDialog, openIconGenerationDialog, openPublicationGenerationDialog, openRedrawPanel, openUiDesignGenerationDialog, openQuickEditPanel, openSpecDialog, startCropExpandFrameResize, openSoundEffectGenerationDialog, openVideoGenerationDialog, pickCharacterReferenceFromLayer, pickCharacterSpecFromLayer, pickGenerationReferenceFromLayer, pickQuickEditReferenceFromLayer, pickIconSpecFromLayer, pickPublicationReferenceFromLayer, pickUiDesignSpecFromLayer, quickEditPanel, quickEditSourceLayer, removeSelectedLayerBackground, submitCharacterAnimation, submitCropExpand, submitIconSpritesheetGeneration, submitImageGeneration, submitQuickEdit, updateCharacterAnimationDuration, updateIconDescriptionsText, updateSpecFormValue, ], ); }