import { type Dispatch, type MutableRefObject, type PointerEvent as ReactPointerEvent, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference'; import { uploadEditorMediaAssetFile } from '../../services/image-editor/editorMediaAssetUploadClient'; import { createEditorProjectResource, type EditorAssetSnapshot, type EditorProjectLayerSnapshot, type EditorProjectResourceSnapshot, type EditorProjectSnapshot, splitEditorIconSpritesheet, } from '../../services/image-editor/editorProjectClient'; import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel'; import type { CanvasGenerationDialogState, CanvasHistoryAction, CanvasLayer, CanvasTool, CanvasViewport, CharacterAnimationPanelState, CharacterReferenceImage, CropExpandPanelState, CropExpandResizeHandle, GenerateDialogState, ImageContextMenuState, PublicationMaterialsWorkflowId, QuickEditPanelState, SidebarPanel, SpecFormValues, SpecGenerationType, } from './ImageCanvasEditorTypes'; import { appendCharacterReference, appendGenerationReference, appendPublicationReference, assignCharacterSpecReference, assignIconSpecReference, assignUiDesignSpecReference, closeGenerateComposerDialog, createAudioRedrawGenerationDialogDraft, createBackgroundMusicGenerationDialogDraft, createCharacterAnimationGenerationDialogDraft, createCharacterGenerationDialogDraft, createEditDialogDraft, createGenerateDialogDraft, createIconGenerationDialogDraft, createPublicationGenerationDialogDraft, createQuickEditGenerationDialogDraft, createQuickEditPanelDraft, createRedrawPanelDraft, createSameSourceGenerationDialogDraft, createSoundEffectGenerationDialogDraft, createSpecDialogDraft, createUiDesignGenerationDialogDraft, createVideoGenerationDialogDraft, createVideoRedrawGenerationDialogDraft, hideGeneratedLayerComposerAfterBlur, isCharacterSpecReferenceLayer, isIconSpecReferenceLayer, updateCharacterAnimationDurationPanel, updateIconDescriptionsTextInDialog, updateSpecFormDialogValue, } from './ImageCanvasGenerationDialogModel'; import { calculateCharacterAnimationPrice, CHARACTER_ANIMATION_DURATION_OPTIONS, CHARACTER_ANIMATION_MODEL, DEFAULT_IMAGE_MODEL, EDITOR_IMAGE_DIMENSION_OPTIONS, IMAGE_MODEL_GPT_IMAGE_2, isCanvasGenerationDialog, isQuickEditUnsupportedAssetKind, normalizeEditorImageModel, resizeGenerationPlaceholderToImageSelection, resolveEditorImageGenerationPixelSize, } 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 { applyQueuedEditorGenerationProject, createEditorGenerationMediaUploadId, resolveEditorGenerationMediaReference, 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; type RememberedImageGenerationOptions = { model: string; aspectRatio: string; imageSize: string; }; function dataUrlToImageFile(dataUrl: string, fileName: string) { const [header = '', payload = ''] = dataUrl.split(','); const mimeMatch = /^data:([^;]+)(;base64)?$/iu.exec(header); const type = mimeMatch?.[1] ?? 'image/png'; const isBase64 = Boolean(mimeMatch?.[2]); const binary = isBase64 ? typeof atob === 'function' ? atob(payload) : Buffer.from(payload, 'base64').toString('binary') : decodeURIComponent(payload); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return new File([bytes], fileName, { type }); } function createProjectResourceSnapshotFromLayer( projectId: string, layer: CanvasLayer, ): EditorProjectResourceSnapshot | null { const imageSrc = layer.objectKey?.trim() ? `/${layer.objectKey.trim().replace(/^\/+/u, '')}` : layer.src.trim(); if (!imageSrc) { return null; } return { resourceId: layer.resourceId, projectId, label: layer.title, imageSrc, objectKey: layer.objectKey, assetObjectId: layer.assetObjectId, width: layer.originalWidth, height: layer.originalHeight, sourceType: layer.sourceType, prompt: layer.prompt, actualPrompt: layer.actualPrompt, model: layer.model, provider: layer.provider, taskId: layer.taskId, durationSeconds: layer.durationSeconds, sourceResourceId: layer.sourceResourceId, assetKind: layer.assetKind, generationInputs: layer.generationInputs, }; } function createProjectLayerSnapshotFromLayer( layer: CanvasLayer, ): EditorProjectLayerSnapshot { return { layerId: layer.id, resourceId: layer.resourceId, title: layer.title, x: layer.x, y: layer.y, width: layer.width, height: layer.height, originalWidth: layer.originalWidth, originalHeight: layer.originalHeight, zIndex: layer.zIndex, sourceType: layer.sourceType, sourceResourceId: layer.sourceResourceId, assetKind: layer.assetKind, mediaType: layer.mediaType, objectKey: layer.objectKey, assetObjectId: layer.assetObjectId, durationSeconds: layer.durationSeconds, }; } function preserveSourceLayerInProjectSnapshot( project: EditorProjectSnapshot, sourceLayer: CanvasLayer, ): EditorProjectSnapshot { if ( project.layers.some((layer) => layer.layerId === sourceLayer.id) || sourceLayer.resourceId.startsWith('generation-dialog:') ) { return project; } const sourceResource = project.resources.some( (resource) => resource.resourceId === sourceLayer.resourceId, ) ? null : createProjectResourceSnapshotFromLayer(project.projectId, sourceLayer); return { ...project, resources: sourceResource ? [...project.resources, sourceResource] : project.resources, layers: [ createProjectLayerSnapshotFromLayer(sourceLayer), ...project.layers, ], }; } function findSourceGenerationDialog( dialogs: CanvasGenerationDialogState[], sourceLayer: CanvasLayer, ) { return [...dialogs] .reverse() .find( (dialog) => dialog.generatedLayerId === sourceLayer.id || // sourceLayerId 也用于派生生成器的输入关系,类型不匹配时不能当作图层来源。 (!dialog.generatedLayerId && dialog.sourceLayerId === sourceLayer.id && isGenerationDialogModeCompatibleWithSourceLayer( dialog.mode, sourceLayer, )), ); } function isGenerationDialogModeCompatibleWithSourceLayer( mode: CanvasGenerationDialogState['mode'], sourceLayer: CanvasLayer, ) { if (mode === 'quick-edit') { return false; } if (sourceLayer.assetKind === 'character') { return mode === 'character'; } if (sourceLayer.assetKind === 'character-animation') { return mode === 'character-animation'; } if (sourceLayer.assetKind === 'ui-design') { return mode === 'ui-design'; } if (sourceLayer.assetKind === 'publication-material') { return mode === 'publication'; } if ( sourceLayer.assetKind === 'icon' || sourceLayer.assetKind === 'icon-spritesheet' ) { return mode === 'icon'; } if ( sourceLayer.assetKind === 'spec' || sourceLayer.assetKind === 'icon-spec' ) { return mode === 'spec'; } if (sourceLayer.assetKind === 'video') { return mode === 'video'; } if (sourceLayer.assetKind === 'sound-effect') { return mode === 'audio-sound-effect'; } if (sourceLayer.assetKind === 'background-music') { return mode === 'audio-background-music'; } return true; } function findSourceAnimationLayer( layers: CanvasLayer[], sourceLayer: CanvasLayer, ) { if (sourceLayer.assetKind !== 'character-animation') { return null; } const sourceResourceId = sourceLayer.sourceResourceId?.trim(); if (!sourceResourceId) { return null; } return ( layers.find( (layer) => layer.resourceId === sourceResourceId && layer.assetKind === 'character', ) ?? null ); } function getCanvasToolForGenerationMode( mode: CanvasGenerationDialogState['mode'], ): CanvasTool { if (mode === 'video') { return 'video'; } if (mode === 'audio-sound-effect' || mode === 'audio-background-music') { return 'music'; } if (mode === 'character' || mode === 'character-animation') { return 'character'; } if (mode === 'icon') { return 'icon'; } if (mode === 'publication') { return 'publication'; } if (mode === 'ui-design') { return 'ui-design'; } if (mode === 'spec') { return 'spec'; } return 'generate'; } const LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY = 'genarrative.imageCanvas.lastCharacterSpecReference'; const LAST_ICON_SPEC_REFERENCE_CACHE_KEY = 'genarrative.imageCanvas.lastIconSpecReference'; const INVALID_CHARACTER_SPEC_WARNING = '选择的图片不是角色规范图,请选择生成规范里的角色规范。'; const INVALID_ICON_SPEC_WARNING = '选择的图片不是图标规范图,请选择生成规范里的图标规范。'; function normalizeRememberedImageGenerationOptions({ model, aspectRatio, imageSize, }: Partial): RememberedImageGenerationOptions { const normalizedModel = normalizeEditorImageModel(model); const dimensionOptions = EDITOR_IMAGE_DIMENSION_OPTIONS[ normalizedModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS ] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL]; const aspectRatios = dimensionOptions.aspectRatios as readonly string[]; const imageSizes = dimensionOptions.imageSizes as readonly string[]; return { model: normalizedModel, aspectRatio: aspectRatio && aspectRatios.includes(aspectRatio) ? aspectRatio : (dimensionOptions.aspectRatios[0] ?? '1:1'), imageSize: imageSize && imageSizes.includes(imageSize) ? imageSize : (dimensionOptions.imageSizes.find((size) => size === '1K') ?? dimensionOptions.imageSizes[0] ?? '1K'), }; } function applyRememberedImageGenerationOptions( draft: Omit, options: RememberedImageGenerationOptions, ): Omit { return resizeGenerationPlaceholderToImageSelection({ ...draft, ...normalizeRememberedImageGenerationOptions(options), }) as Omit; } function shouldRememberImageGenerationOptions( dialog: GenerateDialogState | null, ): dialog is GenerateDialogState & { mode: 'generate' | 'character' | 'icon'; } { return ( dialog?.mode === 'generate' || dialog?.mode === 'character' || dialog?.mode === 'icon' ); } 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, currentUserId?: string | null, ): 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 normalizedCurrentUserId = currentUserId?.trim(); const ownerUserId = typeof record.ownerUserId === 'string' ? record.ownerUserId.trim() : ''; if (normalizedCurrentUserId && ownerUserId !== normalizedCurrentUserId) { storage.removeItem(key); return null; } 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, currentUserId?: string | null, ) { const storage = getLocalGenerationReferenceStorage(); if (!storage) { return; } try { const ownerUserId = currentUserId?.trim(); storage.setItem( key, JSON.stringify(ownerUserId ? { ...reference, ownerUserId } : 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, assetLabel: dialog.assetLabel, 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, assetLabel: panel.assetLabel, 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[], options?: { captureHistory?: boolean }, ) => void; captureCanvasHistory: (action: CanvasHistoryAction) => void; setActiveTool: Dispatch>; setActiveSidebarPanel: Dispatch>; setMetadataLayer: Dispatch>; setImageContextMenu: Dispatch>; persistGeneratedAsset?: (layer: CanvasLayer) => void; persistUpdatedLayerResource?: (layer: CanvasLayer) => void; projectId?: string | null; currentUserId?: string | null; assetFolderId?: string | null; upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void; applyProjectSnapshot?: ( project: EditorProjectSnapshot, action?: CanvasHistoryAction, ) => 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, currentUserId, assetFolderId, upsertGeneratedAsset, applyProjectSnapshot, onWalletBalanceMayHaveChanged, }: GenerationWorkflowOptions) { const [isTaskSidebarOpen, setIsTaskSidebarOpen] = useState(false); const [taskListRefreshKey, setTaskListRefreshKey] = useState(0); const refreshTaskListForQueuedGeneration = useCallback(() => { setIsTaskSidebarOpen(true); setTaskListRefreshKey((key) => key + 1); }, []); const refreshTaskList = useCallback(() => { setTaskListRefreshKey((key) => key + 1); }, []); const previousTaskCountRef = useRef(canvasGenerationDialogs.length); const splittingIconSpritesheetLayerIdsRef = useRef(new Set()); const [splittingIconSpritesheetLayerIds, setSplittingIconSpritesheetLayerIds] = useState>(() => new Set()); 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 [generationWarning, setGenerationWarning] = useState( null, ); const [generationWarningVersion, setGenerationWarningVersion] = useState(0); const showGenerationWarning = useCallback((warning: string) => { setGenerationWarning(warning); setGenerationWarningVersion((version) => version + 1); }, []); useEffect(() => { if (!generationWarning) { return; } const timer = window.setTimeout(() => { setGenerationWarning(null); }, 3_000); return () => { window.clearTimeout(timer); }; }, [generationWarning, generationWarningVersion]); 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); useEffect(() => { if (canvasGenerationDialogs.length > previousTaskCountRef.current) { setIsTaskSidebarOpen(true); } previousTaskCountRef.current = canvasGenerationDialogs.length; }, [canvasGenerationDialogs.length]); const quickEditSelectionMarkCounterRef = useRef(0); const [characterAnimationPanel, setCharacterAnimationPanel] = useState(null); const [rememberedImageOptions, setRememberedImageOptions] = useState(() => normalizeRememberedImageGenerationOptions({ model: DEFAULT_IMAGE_MODEL }), ); const [initialCharacterSpecReference] = useState(() => readCachedGenerationReference( LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY, currentUserId, ), ); const [initialIconSpecReference] = useState( () => readCachedGenerationReference( LAST_ICON_SPEC_REFERENCE_CACHE_KEY, currentUserId, ), ); const currentUserIdRef = useRef(currentUserId); 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; useEffect(() => { currentUserIdRef.current = currentUserId; lastCharacterSpecReferenceRef.current = readCachedGenerationReference( LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY, currentUserId, ); lastIconSpecReferenceRef.current = readCachedGenerationReference( LAST_ICON_SPEC_REFERENCE_CACHE_KEY, currentUserId, ); }, [currentUserId]); 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, currentUserIdRef.current, ); return; } if (generateDialog?.mode === 'icon' && generateDialog.iconSpecReference) { lastIconSpecReferenceRef.current = generateDialog.iconSpecReference; writeCachedGenerationReference( LAST_ICON_SPEC_REFERENCE_CACHE_KEY, generateDialog.iconSpecReference, currentUserIdRef.current, ); return; } if ( generateDialog?.mode === 'ui-design' && generateDialog.uiDesignSpecReference ) { lastIconSpecReferenceRef.current = generateDialog.uiDesignSpecReference; writeCachedGenerationReference( LAST_ICON_SPEC_REFERENCE_CACHE_KEY, generateDialog.uiDesignSpecReference, currentUserIdRef.current, ); } }, [generateDialog]); useEffect(() => { if (!shouldRememberImageGenerationOptions(generateDialog)) { return; } const nextOptions = normalizeRememberedImageGenerationOptions({ model: generateDialog.imageModel, aspectRatio: generateDialog.aspectRatio, imageSize: generateDialog.imageSize, }); setRememberedImageOptions((currentOptions) => currentOptions.model === nextOptions.model && currentOptions.aspectRatio === nextOptions.aspectRatio && currentOptions.imageSize === nextOptions.imageSize ? currentOptions : nextOptions, ); }, [generateDialog]); 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 = quickEditPanel?.status !== 'generating' && 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 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) { return { dialogId: openCanvasGenerationDialog(draft), placeholder: undefined, }; } // 中文注释:所有画布生成入口统一先走 placement 模型,避免新占位压住已有图层或生成占位。 const placement = chooseGenerationPlacement({ canvasSize, viewport, frame: draftPlaceholder, layers, generationDialogs: canvasGenerationDialogs, }); const dialogId = openCanvasGenerationDialog({ ...draft, placeholder: placement, }); setViewport( centerViewportOnPlacement({ canvasSize, viewport, placement, }), ); return { dialogId, placeholder: 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( applyRememberedImageGenerationOptions( createGenerateDialogDraft({ canvasSize, viewport }), rememberedImageOptions, ), ); activateCanvasGenerationEntry('generate'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, rememberedImageOptions, 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 = applyRememberedImageGenerationOptions( createCharacterGenerationDialogDraft({ canvasSize, viewport, imageModel: rememberedImageOptions.model, }), rememberedImageOptions, ); openPlacedCanvasGenerationDialog({ ...draft, characterSpecReference: lastCharacterSpecReferenceRef.current ?? draft.characterSpecReference, }); activateCanvasGenerationEntry('character'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, rememberedImageOptions, viewport, ]); const openIconGenerationDialog = useCallback(() => { const draft = applyRememberedImageGenerationOptions( createIconGenerationDialogDraft({ canvasSize, viewport, imageModel: rememberedImageOptions.model, }), rememberedImageOptions, ); openPlacedCanvasGenerationDialog({ ...draft, iconSpecReference: lastIconSpecReferenceRef.current ?? draft.iconSpecReference, }); activateCanvasGenerationEntry('icon'); }, [ activateCanvasGenerationEntry, canvasSize, openPlacedCanvasGenerationDialog, rememberedImageOptions, 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 createQuickEditModeDraft = useCallback( (sourceLayer: CanvasLayer) => { const sourceDialog = findSourceGenerationDialog( canvasGenerationDialogs, sourceLayer, ); return createQuickEditPanelDraft(sourceLayer, { imageModel: sourceDialog?.imageModel, aspectRatio: sourceDialog?.aspectRatio, imageSize: sourceDialog?.imageSize, }); }, [canvasGenerationDialogs], ); const openQuickEditPanel = useCallback( (sourceLayer: CanvasLayer) => { if (isQuickEditUnsupportedAssetKind(sourceLayer)) { return; } setImageContextMenu(null); setMetadataLayer(null); setUiAssetExtractionState(null); setCropExpandPanel(null); setCharacterAnimationPanel(null); const quickEditDraft = createQuickEditModeDraft(sourceLayer); archiveActiveCanvasGenerationDialog(); setGenerateDialog(null); setQuickEditPanel(quickEditDraft); selectSingleLayer(sourceLayer.id); const nextSelectionState = createUiAssetExtractionState(sourceLayer.id, { initialTool: null, model: DEFAULT_IMAGE_MODEL, }); quickEditSelectionStateRef.current = nextSelectionState; setQuickEditSelectionState(nextSelectionState); quickEditSelectionMarkCounterRef.current = 0; setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize })); setActiveTool('generate'); }, [ archiveActiveCanvasGenerationDialog, canvasSize, createQuickEditModeDraft, selectSingleLayer, setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer, setQuickEditPanel, setViewport, ], ); const updateSourceLayer = useCallback( ( sourceLayerId: string, updater: (layer: CanvasLayer) => CanvasLayer, options: { fit?: boolean; persist?: boolean } = {}, ) => { const sourceLayer = layers.find((layer) => layer.id === sourceLayerId); if (!sourceLayer) { return; } const updatedLayer = updater(sourceLayer); captureCanvasHistory({ type: 'replace-image', count: 1 }); setLayers((currentLayers) => currentLayers.map((layer) => layer.id === sourceLayerId ? updatedLayer : layer, ), ); if (options.persist !== false) { persistUpdatedLayerResource?.(updatedLayer); } selectSingleLayer(sourceLayerId); if (options.fit !== false) { fitLayers([updatedLayer], { captureHistory: false }); } }, [ 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; } if (sourceLayer.mediaType === 'video') { const videoDraft = createVideoRedrawGenerationDialogDraft(sourceLayer); if (!videoDraft) { return; } setQuickEditPanel(null); selectSingleLayer(sourceLayer.id); openCanvasGenerationDialog(videoDraft); setActiveTool('video'); return; } if ( sourceLayer.mediaType === undefined || sourceLayer.mediaType === 'image' || sourceLayer.assetKind === 'character-animation' ) { const sourceDialog = findSourceGenerationDialog( canvasGenerationDialogs, sourceLayer, ); const sourceAnimationLayer = findSourceAnimationLayer( layers, sourceLayer, ); const sameSourceDraft = createSameSourceGenerationDialogDraft({ sourceLayer, canvasSize, viewport, sourceDialog, mode: 'redraw', sourceAnimationLayer, }); if (sameSourceDraft) { openPlacedCanvasGenerationDialog(sameSourceDraft); setQuickEditPanel(null); selectSingleLayer(null); setActiveTool(getCanvasToolForGenerationMode(sameSourceDraft.mode)); return; } if (sourceLayer.assetKind === 'character-animation') { showGenerationWarning('未找到角色动作关联的原角色图层,无法改造。'); return; } const redrawPanel = createRedrawPanelDraft(sourceLayer, { imageModel: sourceDialog?.imageModel, aspectRatio: sourceDialog?.aspectRatio, imageSize: sourceDialog?.imageSize, }); const redrawSize = resolveEditorImageGenerationPixelSize({ model: redrawPanel.model, aspectRatio: redrawPanel.aspectRatio, imageSize: redrawPanel.imageSize, }); openPlacedCanvasGenerationDialog({ ...createQuickEditGenerationDialogDraft({ sourceLayer, prompt: redrawPanel.prompt, model: redrawPanel.model, aspectRatio: redrawPanel.aspectRatio, imageSize: redrawPanel.imageSize, frame: { width: redrawSize.width, height: redrawSize.height, }, }), composerOpen: true, }); setQuickEditPanel(null); selectSingleLayer(null); setActiveTool('generate'); return; } }, [ openCanvasGenerationDialog, openPlacedCanvasGenerationDialog, selectSingleLayer, setActiveTool, setImageContextMenu, setMetadataLayer, showGenerationWarning, canvasGenerationDialogs, canvasSize, layers, viewport, ], ); 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 sourceImageSrc = await resolveEditorImageReferenceDataUrl( cropExpandSourceLayer.objectKey?.trim() || cropExpandSourceLayer.src, ); const result = await renderCropExpandImage({ source: sourceImageSrc, insets, }); layerCounterRef.current += 1; const cropExpandIndex = layerCounterRef.current; const cropExpandTitle = `${cropExpandSourceLayer.title} 裁扩`; const cropExpandProjectId = projectId?.trim(); let cropExpandResourceId = `local-resource-crop-expand-${cropExpandIndex}`; let cropExpandImageSrc = result.imageSrc; let cropExpandObjectKey: string | null = null; let cropExpandAssetObjectId: string | null = null; let cropExpandSourceResourceId: string | null = cropExpandSourceLayer.resourceId; let cropExpandAssetKind = cropExpandSourceLayer.assetKind; if (cropExpandProjectId) { const cropExpandUploadId = createEditorGenerationMediaUploadId(); const cropExpandFile = dataUrlToImageFile( result.imageSrc, `crop-expand-${cropExpandIndex}-${cropExpandUploadId}.png`, ); const uploadedCropExpand = await uploadEditorMediaAssetFile( cropExpandFile, 'image', { assetKind: 'editor_crop_expand_image', pathSegments: [ 'editor', 'crop-expand', cropExpandProjectId, cropExpandUploadId, ], entityId: cropExpandProjectId, metadata: { editor_project_id: cropExpandProjectId, source_resource_id: cropExpandSourceLayer.resourceId, }, }, ); const cropExpandResource = await createEditorProjectResource( cropExpandProjectId, { imageSrc: uploadedCropExpand.legacyPublicPath, objectKey: uploadedCropExpand.objectKey, assetObjectId: uploadedCropExpand.assetObjectId, width: result.width, height: result.height, sourceType: 'generated', prompt: cropExpandSourceLayer.prompt, actualPrompt: cropExpandSourceLayer.actualPrompt, model: cropExpandSourceLayer.model, provider: cropExpandSourceLayer.provider, taskId: cropExpandSourceLayer.taskId, sourceResourceId: cropExpandSourceLayer.resourceId, assetKind: cropExpandSourceLayer.assetKind, generationInputs: cropExpandSourceLayer.generationInputs, }, ); cropExpandResourceId = cropExpandResource.resourceId; cropExpandImageSrc = cropExpandResource.imageSrc; cropExpandObjectKey = cropExpandResource.objectKey ?? uploadedCropExpand.objectKey; cropExpandAssetObjectId = cropExpandResource.assetObjectId ?? uploadedCropExpand.assetObjectId; cropExpandSourceResourceId = cropExpandResource.sourceResourceId ?? cropExpandSourceLayer.resourceId; cropExpandAssetKind = (cropExpandResource.assetKind as CanvasLayer['assetKind']) ?? cropExpandSourceLayer.assetKind; } const nextLayer: CanvasLayer = { ...cropExpandSourceLayer, id: `layer-crop-expand-${cropExpandIndex}`, resourceId: cropExpandResourceId, title: cropExpandTitle, src: cropExpandImageSrc, 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: cropExpandSourceResourceId, objectKey: cropExpandObjectKey, assetObjectId: cropExpandAssetObjectId, sourceAssetId: null, assetKind: cropExpandAssetKind, }; captureCanvasHistory({ type: 'expand-image', count: 1 }); appendCanvasLayersWithResources([nextLayer]); persistGeneratedAsset?.(nextLayer); selectSingleLayer(nextLayer.id); fitLayers([cropExpandSourceLayer, nextLayer], { captureHistory: false }); 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, projectId, selectSingleLayer, setActiveSidebarPanel, ]); const removeSelectedLayerBackground = useCallback( async (sourceLayer: CanvasLayer) => { setImageContextMenu(null); setMetadataLayer(null); setCropExpandPanel(null); setQuickEditPanel(null); const assetLabel = `${sourceLayer.title} 去背景`; const backgroundRemovalDialog = projectId && applyProjectSnapshot ? createQuickEditGenerationDialogDraft({ sourceLayer, prompt: '去除背景', status: 'generating', frame: { width: sourceLayer.width, height: sourceLayer.height, }, }) : null; const backgroundRemovalPlacement = backgroundRemovalDialog ? openPlacedCanvasGenerationDialog({ ...backgroundRemovalDialog, composerOpen: false, }) : undefined; const backgroundRemovalDialogId = backgroundRemovalPlacement?.dialogId; const applyBackgroundRemovalProjectSnapshot = projectId && applyProjectSnapshot ? (project: EditorProjectSnapshot) => { applyProjectSnapshot( preserveSourceLayerInProjectSnapshot(project, sourceLayer), ); } : undefined; try { const sourceImageSrc = await resolveEditorGenerationMediaReference( sourceLayer, 'image', projectId, ); const result = await removeImageBackground({ sourceImageSrc, projectId, targetLayerId: sourceLayer.id, assetKind: sourceLayer.assetKind, generationInputs: sourceLayer.generationInputs, assetFolderId, assetLabel, sourceResourceId: sourceLayer.resourceId, ...(backgroundRemovalPlacement?.placeholder ? { canvasCompletion: { dialogId: backgroundRemovalDialogId, title: assetLabel, placeholder: backgroundRemovalPlacement.placeholder, }, } : {}), }); await applyQueuedEditorGenerationProject( result, projectId, (project) => applyProjectSnapshot?.(project, { type: 'remove-background', count: 1, }), refreshTaskListForQueuedGeneration, onWalletBalanceMayHaveChanged, showGenerationWarning, backgroundRemovalDialogId, applyBackgroundRemovalProjectSnapshot ? (project) => preserveSourceLayerInProjectSnapshot(project, sourceLayer) : undefined, ); } catch (error) { if (backgroundRemovalDialogId) { updateCanvasGenerationDialogById( backgroundRemovalDialogId, (dialog) => ({ ...dialog, status: 'failed', errorMessage: error instanceof Error && error.message.trim() ? error.message : '去除背景失败', }), ); } throw error; } }, [ applyProjectSnapshot, assetFolderId, onWalletBalanceMayHaveChanged, openPlacedCanvasGenerationDialog, projectId, refreshTaskListForQueuedGeneration, setImageContextMenu, setMetadataLayer, showGenerationWarning, updateCanvasGenerationDialogById, ], ); const splitSelectedIconSpritesheet = useCallback( async (sourceLayer: CanvasLayer) => { if ( sourceLayer.assetKind !== 'icon-spritesheet' || !projectId || !applyProjectSnapshot || splittingIconSpritesheetLayerIdsRef.current.has(sourceLayer.id) ) { return; } splittingIconSpritesheetLayerIdsRef.current.add(sourceLayer.id); setSplittingIconSpritesheetLayerIds((currentLayerIds) => { const nextLayerIds = new Set(currentLayerIds); nextLayerIds.add(sourceLayer.id); return nextLayerIds; }); closeGenerationTransientState(); setImageContextMenu(null); setMetadataLayer(null); setCropExpandPanel(null); setQuickEditPanel(null); try { const result = await splitEditorIconSpritesheet({ projectId, sourceResourceId: sourceLayer.resourceId, assetFolderId, canvasCompletion: { title: '拆分图集', placeholder: { x: sourceLayer.x, y: sourceLayer.y, width: sourceLayer.width, height: sourceLayer.height, originalWidth: sourceLayer.originalWidth, originalHeight: sourceLayer.originalHeight, }, }, }); applyProjectSnapshot(result.project, { type: 'split-atlas', count: 1, }); setActiveTool('select'); setActiveSidebarPanel('layers'); } catch (error) { window.alert( error instanceof Error && error.message.trim() ? error.message : '拆分图集失败', ); } finally { splittingIconSpritesheetLayerIdsRef.current.delete(sourceLayer.id); setSplittingIconSpritesheetLayerIds((currentLayerIds) => { if (!currentLayerIds.has(sourceLayer.id)) { return currentLayerIds; } const nextLayerIds = new Set(currentLayerIds); nextLayerIds.delete(sourceLayer.id); return nextLayerIds; }); } }, [ applyProjectSnapshot, assetFolderId, closeGenerationTransientState, projectId, setActiveSidebarPanel, setActiveTool, setCropExpandPanel, setImageContextMenu, setMetadataLayer, setQuickEditPanel, ], ); const pickCharacterSpecFromLayer = useCallback( (layer: CanvasLayer) => { if (!isCharacterSpecReferenceLayer(layer)) { showGenerationWarning(INVALID_CHARACTER_SPEC_WARNING); return; } setGenerationWarning(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, showGenerationWarning], ); const pickGenerationReferenceFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => appendGenerationReference(currentDialog, layer), ); setIsPickingGenerationReferenceFromCanvas(false); setIsGenerationReferenceMenuOpen(false); setImageContextMenu(null); }, [setGenerateDialog, setImageContextMenu], ); const pickQuickEditReferenceFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => appendGenerationReference(currentDialog, layer), ); setIsPickingQuickEditReferenceFromCanvas(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) => { if (!isIconSpecReferenceLayer(layer)) { showGenerationWarning(INVALID_ICON_SPEC_WARNING); return; } setGenerationWarning(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, showGenerationWarning, ], ); const pickUiDesignSpecFromLayer = useCallback( (layer: CanvasLayer) => { if (!isIconSpecReferenceLayer(layer)) { showGenerationWarning(INVALID_ICON_SPEC_WARNING); return; } setGenerationWarning(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, showGenerationWarning, ], ); 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 rememberImageModel = useCallback((model: string) => { setRememberedImageOptions((currentOptions) => normalizeRememberedImageGenerationOptions({ ...currentOptions, model, }), ); }, []); const generationSubmissionWorkflow = useImageCanvasGenerationSubmissionWorkflow({ layers, canvasSize, viewport, layerCounterRef, quickEditPanel, quickEditSourceLayer, quickEditSelectionState, canvasGenerationDialogs, setQuickEditPanel, characterAnimationPanel: effectiveCharacterAnimationPanel, characterAnimationDialog, characterAnimationSourceLayer, setCharacterAnimationPanel, setGenerateDialog, openCanvasGenerationDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, captureCanvasHistory, updateSourceLayer, selectSingleLayer, fitLayers, setActiveTool, setActiveSidebarPanel, rememberImageModel, projectId, assetFolderId, upsertGeneratedAsset, applyProjectSnapshot, onQueuedGenerationTask: refreshTaskListForQueuedGeneration, onWalletBalanceMayHaveChanged, onGenerationWarning: showGenerationWarning, }); 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', model: DEFAULT_IMAGE_MODEL, }), ); setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize })); }, [ canvasSize, closeGenerationTransientState, selectSingleLayer, setActiveTool, setCharacterAnimationPanel, setCropExpandPanel, setMetadataLayer, setQuickEditPanel, setViewport, ], ); 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 changeUiAssetExtractionModel = useCallback( (model: string) => { const normalizedModel = normalizeEditorImageModel(model); rememberImageModel(normalizedModel); setUiAssetExtractionState((currentState) => currentState ? { ...currentState, model: normalizedModel, status: currentState.status === 'failed' ? 'idle' : currentState.status, errorMessage: currentState.status === 'failed' ? undefined : currentState.errorMessage, } : currentState, ); }, [rememberImageModel], ); const appendUiAssetExtractionReferences = useCallback( (references: CharacterReferenceImage[]) => { if (!references.length) { return; } setUiAssetExtractionState((currentState) => currentState ? { ...currentState, references: [...currentState.references, ...references], status: currentState.status === 'failed' ? 'idle' : currentState.status, errorMessage: currentState.status === 'failed' ? undefined : currentState.errorMessage, } : currentState, ); }, [], ); const removeUiAssetExtractionReference = useCallback( (referenceId: string) => { setUiAssetExtractionState((currentState) => currentState ? { ...currentState, references: currentState.references.filter( (reference) => reference.id !== referenceId, ), 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 cancelUiAssetExtractionPointer = useCallback(() => { setUiAssetExtractionState((currentState) => currentState?.draftMark ? { ...currentState, draftMark: null, } : currentState, ); }, []); 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?.sourceLayerId === currentSelectionState.sourceLayerId ? { ...currentPanel, status: currentPanel.status === 'failed' ? 'idle' : currentPanel.status, errorMessage: currentPanel.status === 'failed' ? undefined : currentPanel.errorMessage, prompt: appendQuickEditSelectionPrompt( currentPanel.prompt, nextMarkNumber, ), } : currentPanel, ); setGenerateDialog((currentDialog) => currentDialog?.mode === 'quick-edit' && currentDialog.sourceLayerId === currentSelectionState.sourceLayerId ? { ...currentDialog, status: currentDialog.status === 'failed' ? 'idle' : currentDialog.status, errorMessage: currentDialog.status === 'failed' ? undefined : currentDialog.errorMessage, prompt: appendQuickEditSelectionPrompt( currentDialog.prompt, nextMarkNumber, ), composerOpen: true, } : currentDialog, ); }, [setGenerateDialog, setQuickEditPanel, updateQuickEditSelectionState]); const cancelQuickEditSelectionPointer = useCallback(() => { updateQuickEditSelectionState((currentState) => currentState?.draftMark ? { ...currentState, draftMark: null, } : currentState, ); }, [updateQuickEditSelectionState]); useEffect(() => { if (!quickEditSelectionState) { return; } const hasActiveQuickEditDialog = generateDialog?.mode === 'quick-edit' && generateDialog.sourceLayerId === quickEditSelectionState.sourceLayerId; if (!hasActiveQuickEditDialog && !quickEditPanel) { quickEditSelectionStateRef.current = null; setQuickEditSelectionState(null); } }, [generateDialog, 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, model: currentState.model, references: currentState.references, 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, changeUiAssetExtractionModel, appendUiAssetExtractionReferences, removeUiAssetExtractionReference, changeQuickEditSelectionTool, startUiAssetExtractionPointer, startQuickEditSelectionPointer, moveUiAssetExtractionPointer, moveQuickEditSelectionPointer, endUiAssetExtractionPointer, endQuickEditSelectionPointer, cancelUiAssetExtractionPointer, cancelQuickEditSelectionPointer, cancelUiAssetExtraction, submitUiAssetExtraction, characterAnimationPanel: effectiveCharacterAnimationPanel, setCharacterAnimationPanel: setEffectiveCharacterAnimationPanel, characterAnimationSourceLayer, characterAnimationPrice, 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, generationWarning, generationWarningVersion, showGenerationWarning, clearGenerationWarning: () => setGenerationWarning(null), openGenerateDialog, openSpecDialog, openCharacterAnimationPanel, openCharacterGenerationDialog, openIconGenerationDialog, openPublicationGenerationDialog, openVideoGenerationDialog, openUiDesignGenerationDialog, openSoundEffectGenerationDialog, openBackgroundMusicGenerationDialog, openEditDialog, openQuickEditPanel, openRedrawPanel, openCropExpandPanel, startCropExpandFrameResize, removeSelectedLayerBackground, splitSelectedIconSpritesheet, splittingIconSpritesheetLayerIds, taskListRefreshKey, refreshTaskList, isTaskSidebarOpen, toggleTaskSidebar: () => setIsTaskSidebarOpen((open) => !open), extractUiDesignAssets, pickCharacterSpecFromLayer, pickGenerationReferenceFromLayer, pickQuickEditReferenceFromLayer, pickCharacterReferenceFromLayer, pickIconSpecFromLayer, pickUiDesignSpecFromLayer, pickPublicationReferenceFromLayer, submitIconSpritesheetGeneration, submitQuickEdit, submitCropExpand, submitImageGeneration, updateSpecFormValue, updateIconDescriptionsText, updateCharacterAnimationDuration, rememberImageModel, submitCharacterAnimation, hideGeneratedLayerPanelAfterBlur, closeGenerateComposer, clearDeletedLayerGenerationState, }), [ effectiveCharacterAnimationPanel, characterAnimationPrice, characterAnimationSourceLayer, clearDeletedLayerGenerationState, closeGenerateComposer, cropExpandPanel, cropExpandSourceLayer, uiAssetExtractionState, uiAssetExtractionSourceLayer, quickEditSelectionState, quickEditSelectionSourceLayer, changeUiAssetExtractionTool, changeUiAssetExtractionModel, appendUiAssetExtractionReferences, removeUiAssetExtractionReference, changeQuickEditSelectionTool, startUiAssetExtractionPointer, startQuickEditSelectionPointer, moveUiAssetExtractionPointer, moveQuickEditSelectionPointer, endUiAssetExtractionPointer, endQuickEditSelectionPointer, cancelUiAssetExtractionPointer, cancelQuickEditSelectionPointer, cancelUiAssetExtraction, submitUiAssetExtraction, setEffectiveCharacterAnimationPanel, hideGeneratedLayerPanelAfterBlur, isCharacterReferenceMenuOpen, isCharacterSpecMenuOpen, isIconSpecMenuOpen, isGenerationReferenceMenuOpen, isPickingCharacterReferenceFromCanvas, isPickingCharacterSpecFromCanvas, isPickingGenerationReferenceFromCanvas, isPickingQuickEditReferenceFromCanvas, isPickingIconSpecFromCanvas, isPickingUiDesignSpecFromCanvas, isUiDesignSpecMenuOpen, isMusicMenuOpen, isTaskSidebarOpen, taskListRefreshKey, refreshTaskList, isPublicationMenuOpen, isPublicationReferenceMenuOpen, isSpecMenuOpen, isPickingPublicationReferenceFromCanvas, generationWarning, generationWarningVersion, 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, splitSelectedIconSpritesheet, splittingIconSpritesheetLayerIds, submitCharacterAnimation, submitCropExpand, submitIconSpritesheetGeneration, submitImageGeneration, submitQuickEdit, updateCharacterAnimationDuration, updateIconDescriptionsText, updateSpecFormValue, rememberImageModel, showGenerationWarning, ], ); }