import { type Dispatch, type PointerEvent as ReactPointerEvent, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { PlatformProfileRechargeModal } from '../../../packages/shared/src/components/PlatformProfileRechargeModal'; import { PlatformProfileWalletLedgerModal } from '../../../packages/shared/src/components/PlatformProfileWalletLedgerModal'; import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration'; import { replaceAppHistoryPath } from '../../routing/activeAppPageRoutes'; import { loadFrontendRuntimeConfig } from '../../services/frontendRuntimeConfigService'; import { createEditorAsset, createEditorProjectResource, type EditorAssetSnapshot, type EditorProjectSnapshot, loadEditorGenerationPricing, loadEditorProject, } from '../../services/image-editor/editorProjectClient'; import { shouldShowRechargeEntry } from '../../services/payment/paymentPlatform'; import { usePlatformWalletStore } from '../../stores/usePlatformWalletStore'; import { useAuthUi } from '../auth/AuthUiContext'; import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog'; import { PlatformProfileRewardCodeRedeemModal } from '../platform-entry/PlatformProfileRewardCodeRedeemModal'; import { PlatformRechargePaymentConfirmationMask, PlatformRechargePaymentResultDialog, } from '../platform-entry/PlatformRechargePaymentStatusDialogs'; import { usePlatformProfileCenterController } from '../platform-entry/usePlatformProfileCenterController'; import type { ImageCanvasActionResult } from './ImageCanvasActionsContext'; import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider'; import { canvasAssetKindOrNull, DEFAULT_CANVAS_BACKGROUND_COLOR, generationInputsOrNull, isCanvasAssetKindOverrideCompatible, isInlineEditorMediaSource, resolveContextMenuPosition, resolveLayerResourceAssetKind, } from './ImageCanvasEditorModel'; import { ImageCanvasEditorShellView } from './ImageCanvasEditorShellView'; import type { AssetPointerDragState, CanvasAssetKind, CanvasContextMenuState, CanvasGenerationDialogState, CanvasHistoryAction, CanvasHistoryFeedback, CanvasLayer, CanvasTool, CanvasViewport, CharacterReferenceImage, EditorAsset, ImageContextMenuState, QuickEditPanelState, SidebarPanel, UploadTarget, } from './ImageCanvasEditorTypes'; import { getEditorUploadAccept, isImageFile } from './ImageCanvasFileModel'; import { createLayerGenerationDialogDraft, resolveSpecGenerationReferences, } from './ImageCanvasGenerationDialogModel'; import { hydrateCanvasGenerationInputsResult } from './ImageCanvasGenerationInputsModel'; import { applyEditorGenerationPricingConfig, isCanvasGenerationDialog, QUICK_EDIT_REFERENCE_LIMIT, resolveDialogExtraImageReferenceLimit, resolveExtraImageReferenceLimit, UI_EXTRA_REFERENCE_LIMIT, } from './ImageCanvasGenerationModel'; import { formatCanvasHistoryAction } from './ImageCanvasHistoryModel'; import { fitViewportToBounds } from './ImageCanvasInteractionModel'; import type { CanvasLayerCopyBlockReason } from './ImageCanvasLayerCommandModel'; import { isCanvasGenerationComposerVisible, resolveQuickEditFocusViewport, } from './ImageCanvasOverlayModel'; import { getCanvasGenerationSelectionId, getSelectedGenerationDialogIds, getSelectedLayerIds, } from './ImageCanvasSelectionModel'; import { ImageCanvasShortcutDialogView } from './ImageCanvasShortcutDialogView'; import { requiresGenerationDeleteConfirmation, useCanvasGenerationDialogs, } from './useCanvasGenerationDialogs'; import { useCanvasHistory } from './useCanvasHistory'; import { useImageCanvasAssetCanvasBridge, useImageCanvasAssetLayerCleanup, } from './useImageCanvasAssetCanvasBridge'; import { useImageCanvasAssetExportWorkflow } from './useImageCanvasAssetExportWorkflow'; import { useImageCanvasAssetLibrary } from './useImageCanvasAssetLibrary'; import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts'; import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome'; import { useImageCanvasGenerationSurface } from './useImageCanvasGenerationSurface'; import { useImageCanvasKeyboardShortcuts } from './useImageCanvasKeyboardShortcuts'; import { useImageCanvasLayerCommands } from './useImageCanvasLayerCommands'; import { useImageCanvasProjectPersistence } from './useImageCanvasProjectPersistence'; import { useImageCanvasStageController } from './useImageCanvasStageController'; import { useImageCanvasStageInteractions } from './useImageCanvasStageInteractions'; import { type ImageReferenceUploadCapacity, useImageCanvasUploadWorkflow, } from './useImageCanvasUploadWorkflow'; import { DEFAULT_IMAGE_CANVAS_VIEWPORT, useImageCanvasViewportControls, } from './useImageCanvasViewportControls'; import { useInlineGenerationPlaceholderExpiry } from './useInlineGenerationPlaceholderExpiry'; const TASK_FOCUS_HORIZONTAL_INSET = 28; const TASK_FOCUS_TOP_INSET = 82; const TASK_FOCUS_BOTTOM_INSET = 90; const TASK_FOCUS_DIALOG_BOTTOM_INSET = 280; const TASK_FOCUS_PADDING = 18; type CanvasStartupTool = | 'character-spec' | 'icon-spec' | 'ui-design' | 'background-music' | 'image' | 'publication-cover' | 'scene'; const CANVAS_STARTUP_TOOLS: CanvasStartupTool[] = [ 'character-spec', 'icon-spec', 'ui-design', 'background-music', 'image', 'publication-cover', 'scene', ]; type ImageCanvasEditorViewProps = { legacyWalletBalance?: number | null; onProjectAccessLost?: () => void; }; function isCanvasStartupTool(value: string | null): value is CanvasStartupTool { return CANVAS_STARTUP_TOOLS.includes(value as CanvasStartupTool); } function readCanvasStartupIntent() { if (typeof window === 'undefined') { return { showToolbarGuide: false, tool: null }; } const params = new URLSearchParams(window.location.search); const rawTool = params.get('tool'); return { showToolbarGuide: params.get('guide') === 'toolbar', tool: isCanvasStartupTool(rawTool) ? rawTool : null, }; } function clearCanvasStartupIntent() { if (typeof window === 'undefined') { return; } const params = new URLSearchParams(window.location.search); params.delete('tool'); params.delete('guide'); const search = params.toString(); window.history.replaceState( window.history.state, '', `${window.location.pathname}${search ? `?${search}` : ''}${window.location.hash}`, ); } function openCanvasStartupTool( tool: CanvasStartupTool, generationSurface: ReturnType, ) { if (tool === 'character-spec') { generationSurface.openSpecDialog('character'); return; } if (tool === 'icon-spec') { generationSurface.openSpecDialog('icon'); return; } if (tool === 'ui-design') { generationSurface.openUiDesignGenerationDialog(); return; } if (tool === 'background-music') { generationSurface.openBackgroundMusicGenerationDialog(); return; } if (tool === 'scene') { generationSurface.openSceneGenerationDialog(); return; } if (tool === 'publication-cover') { generationSurface.openPublicationGenerationDialog( 'publication-cover-image', ); return; } generationSurface.openGenerateDialog(); } function resolveLayerProjectResourceImageSrc(layer: CanvasLayer) { const objectKey = layer.objectKey?.trim(); if (objectKey) { return `/${objectKey.replace(/^\/+/u, '')}`; } if (isInlineEditorMediaSource(layer.src)) { return null; } return layer.src; } 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.id === sourceResourceId, ) ?? null ); } function isEditablePasteTarget(target: EventTarget | null) { const element = target instanceof HTMLElement ? target : null; if (!element) { return false; } return ( element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.isContentEditable ); } function getClipboardImageFiles(clipboardData: DataTransfer | null) { if (!clipboardData) { return []; } const files = Array.from(clipboardData.files ?? []).filter(isImageFile); if (files.length) { return files; } return Array.from(clipboardData.items ?? []) .filter((item) => item.kind === 'file' && item.type.startsWith('image/')) .map((item) => item.getAsFile()) .filter((file): file is File => file !== null && isImageFile(file)); } async function readSystemClipboardImageFiles() { const read = globalThis.navigator?.clipboard?.read; if (!read) { return []; } const items = await read(); const imageFiles: File[] = []; for (const item of items) { const imageType = item.types.find((type) => type.startsWith('image/')); if (!imageType) { continue; } const blob = await item.getType(imageType); imageFiles.push( new File([blob], `剪贴板图片.${imageType.split('/')[1] ?? 'png'}`, { type: imageType, }), ); } return imageFiles; } function createAssetActionLayer(asset: EditorAsset): CanvasLayer { return { id: `asset-download-${asset.id}`, resourceId: asset.sourceResourceId?.trim() || asset.assetObjectId?.trim() || asset.id, title: asset.label, src: asset.src, mediaType: asset.mediaType, thumbnailSrc: asset.thumbnailSrc, x: 0, y: 0, width: asset.width, height: asset.height, originalWidth: asset.width, originalHeight: asset.height, zIndex: 0, sourceType: asset.sourceType, prompt: asset.prompt ?? null, actualPrompt: asset.actualPrompt ?? null, model: asset.model ?? null, provider: asset.provider ?? null, taskId: asset.taskId ?? null, objectKey: asset.objectKey ?? null, assetObjectId: asset.assetObjectId ?? null, sourceResourceId: asset.sourceResourceId ?? null, sourceAssetId: asset.id, assetKind: asset.assetKind ?? null, generationInputs: asset.generationInputs ?? null, generationInputsHydrationState: asset.generationInputsHydrationState, imageSequenceFrames: asset.imageSequenceFrames, imageSequenceDurationMs: asset.imageSequenceDurationMs, }; } // 中文注释:服务端持久化顺序是 OSS PUT → asset object → project resource → editor asset // → 画布收口,非事务。看到孤儿 generating 占位只能说明最后一步没做完,前面几步可能已经 // 成功。所以不能断言「什么都没发生」,只能指路让用户自己核对素材库。 // 加载期剥离与页面打开期的到期清理共用这一条,两条路径对用户完全一致。 const DEAD_INLINE_PLACEHOLDER_NOTICE = '上次的完美像素处理未完成,画布占位已清理。请确认素材库是否已生成派生图。'; export function ImageCanvasEditorView({ legacyWalletBalance = null, onProjectAccessLost, }: ImageCanvasEditorViewProps = {}) { const authUi = useAuthUi(); const [, setGenerationPricingVersion] = useState(0); const walletOwnerUserId = usePlatformWalletStore( (state) => state.ownerUserId, ); const storedMudPointBalance = usePlatformWalletStore( (state) => state.mudPointBalance, ); const storedLegacyWalletBalance = usePlatformWalletStore( (state) => state.legacyWalletBalance, ); const storedMudPointBalanceStatus = usePlatformWalletStore( (state) => state.mudPointBalanceStatus, ); const storedMudPointBalanceError = usePlatformWalletStore( (state) => state.mudPointBalanceError, ); const onWalletBalanceMayHaveChanged = usePlatformWalletStore( (state) => state.onWalletBalanceMayHaveChanged, ); const currentWalletOwnerUserId = authUi?.canAccessProtectedData && authUi.user?.id ? authUi.user.id : null; const walletOwnerMatchesCurrentUser = Boolean(currentWalletOwnerUserId) && walletOwnerUserId === currentWalletOwnerUserId; const mudPointBalance = walletOwnerMatchesCurrentUser ? storedMudPointBalance : null; const mudPointBalanceError = walletOwnerMatchesCurrentUser ? storedMudPointBalanceError : ''; const walletBalance = mudPointBalance?.totalPoints ?? (walletOwnerMatchesCurrentUser ? (storedLegacyWalletBalance ?? legacyWalletBalance) : null); const isWalletBalanceLoading = Boolean(currentWalletOwnerUserId) && (!walletOwnerMatchesCurrentUser || storedMudPointBalanceStatus === 'idle' || storedMudPointBalanceStatus === 'loading'); const editorRootRef = useRef(null); const canvasViewportRef = useRef(null); const assetListRef = useRef(null); const assetPointerDragRef = useRef(null); const authUiRef = useRef(authUi); const hasRequestedLoginRef = useRef(false); const layerCounterRef = useRef(0); const layersRef = useRef([]); const canvasGenerationDialogsRef = useRef([]); const viewportRef = useRef(DEFAULT_IMAGE_CANVAS_VIEWPORT); const canvasBackgroundColorRef = useRef(DEFAULT_CANVAS_BACKGROUND_COLOR); const captureCanvasHistoryRef = useRef<(action: CanvasHistoryAction) => void>( () => {}, ); const discardCanvasHistoryEntriesContainingLayerRef = useRef< (matchesLayer: (layer: CanvasLayer) => boolean) => void >(() => {}); const layerResourcePersistenceVersionRef = useRef(new Map()); const resetCanvasInteractionStateRef = useRef<() => void>(() => {}); const closeGenerationTransientStateRef = useRef<() => void>(() => {}); const specToolWrapRef = useRef(null); const musicToolWrapRef = useRef(null); const publicationToolWrapRef = useRef(null); const characterSpecButtonRef = useRef(null); const characterReferenceButtonRef = useRef(null); const generationReferenceButtonRef = useRef(null); const publicationReferenceButtonRef = useRef(null); const iconSpecButtonRef = useRef(null); const startupIntentConsumedRef = useRef(false); const hasLoadedAgentRuntimeConfigRef = useRef(false); const selectedLayerIdRef = useRef(null); const showRechargeEntry = shouldShowRechargeEntry(); const currentEditorUserId = authUi?.user?.id ?? null; const [isToolbarGuideVisible, setIsToolbarGuideVisible] = useState(false); const [isAgentConversationEnabled, setIsAgentConversationEnabled] = useState(false); const [isAgentConversationOpen, setIsAgentConversationOpen] = useState(false); useEffect(() => { let isMounted = true; void loadEditorGenerationPricing() .then((pricing) => { if (!isMounted) { return; } applyEditorGenerationPricingConfig(pricing); setGenerationPricingVersion((version) => version + 1); }) .catch(() => { // 中文注释:价格接口异常时保留前端内置兜底,不阻断画板使用。 }); return () => { isMounted = false; }; }, []); useEffect(() => { let isMounted = true; let requestId = 0; const refreshAgentRuntimeConfig = () => { const currentRequestId = requestId + 1; requestId = currentRequestId; void loadFrontendRuntimeConfig() .then((config) => { if (!isMounted || currentRequestId !== requestId) { return; } const enabled = config.imageEditorAgentSidebarEnabled === true; const shouldAutoOpen = !hasLoadedAgentRuntimeConfigRef.current; hasLoadedAgentRuntimeConfigRef.current = true; setIsAgentConversationEnabled(enabled); setIsAgentConversationOpen((currentOpen) => { if (!enabled) { return false; } return shouldAutoOpen ? true : currentOpen; }); }) .catch(() => { if (!isMounted || currentRequestId !== requestId) { return; } hasLoadedAgentRuntimeConfigRef.current = true; setIsAgentConversationEnabled(false); setIsAgentConversationOpen(false); }); }; refreshAgentRuntimeConfig(); const handleWindowFocus = () => { refreshAgentRuntimeConfig(); }; const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { refreshAgentRuntimeConfig(); } }; window.addEventListener('focus', handleWindowFocus); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { isMounted = false; window.removeEventListener('focus', handleWindowFocus); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [currentEditorUserId]); const selectedLayerIdsRef = useRef([]); const setQuickEditPanelRef = useRef< Dispatch> >(() => {}); const appendUiAssetExtractionReferencesRef = useRef< (references: CharacterReferenceImage[]) => void >(() => {}); const resolveImageReferenceUploadCapacityRef = useRef< (target: UploadTarget) => ImageReferenceUploadCapacity | null >(() => null); const deleteLayerByIdRef = useRef<(targetLayerId: string | null) => void>( () => {}, ); const clearDeletedLayerGenerationStateRef = useRef< (targetLayerId: string) => void >(() => {}); const suppressAssetClickRef = useRef(false); const [layers, setLayers] = useState([]); const [selectedLayerId, setSelectedLayerId] = useState(null); const [selectedLayerIds, setSelectedLayerIds] = useState([]); const [hoveredLayerId, setHoveredLayerId] = useState(null); const [metadataLayer, setMetadataLayer] = useState(null); const [pendingGenerationDeleteDialog, setPendingGenerationDeleteDialog] = useState(null); const [imageContextMenu, setImageContextMenu] = useState(null); const [contextMenu, setContextMenu] = useState( null, ); const [isViewportInteracting, setIsViewportInteracting] = useState(false); const [uploadDropTarget, setUploadDropTarget] = useState< 'canvas' | 'assets' | null >(null); const [isShortcutDialogOpen, setIsShortcutDialogOpen] = useState(false); const captureViewportHistory = useCallback(() => { captureCanvasHistoryRef.current({ type: 'change-viewport' }); }, []); const { viewport, setViewport, animateViewportTo, canvasSize, minimapModel, updateScaleFromCenter, fitLayers, resolveCanvasPoint, getCanvasDropPoint, getCanvasPointFromClient, moveViewportFromMinimapPointer, updateViewportFromMinimapDrag, flushMinimapViewportDrag, } = useImageCanvasViewportControls({ canvasViewportRef, layers, captureCanvasHistory: captureViewportHistory, }); const beginViewportInteraction = useCallback(() => { setIsViewportInteracting(true); }, []); const endViewportInteraction = useCallback(() => { setIsViewportInteracting(false); }, []); selectedLayerIdRef.current = selectedLayerId; selectedLayerIdsRef.current = selectedLayerIds; layersRef.current = layers; viewportRef.current = viewport; authUiRef.current = authUi; const openEditorLoginModal = useCallback( (postLoginAction?: (() => void) | null) => { authUiRef.current?.openLoginModal(postLoginAction); }, [], ); const { buyRechargeProduct, closeNativeWechatPayment, confirmNativeWechatPayment, isLoadingRechargeCenter, isLoadingWalletLedger, isRechargeOpen, isRewardCodeOpen, isSubmittingRewardCode, isWalletLedgerOpen, loadRechargeCenter, loadWalletLedger, nativeWechatPayment, openRechargeOrRewardCodeModal, openWalletLedgerPanel, rechargeCenter, rechargeError, rechargePaymentResult, rewardCodeError, rewardCodeInput, rewardCodeSuccess, setIsRechargeOpen, setIsRewardCodeOpen, setIsWalletLedgerOpen, setRechargePaymentResult, setRewardCodeInput, submittingRechargeProductId, submitRewardCode, walletLedger, walletLedgerError, wechatRechargeOrderConfirmationState, } = usePlatformProfileCenterController({ activeTab: 'editor-canvas', isAuthenticated: Boolean(authUi?.user), showRechargeEntry, requestLogin: () => authUiRef.current?.openLoginModal(), currentUser: authUi?.user ?? null, }); const refreshEditorWalletState = useCallback(() => { if (!authUiRef.current?.canAccessProtectedData || !authUiRef.current.user) { return; } void onWalletBalanceMayHaveChanged(); }, [onWalletBalanceMayHaveChanged]); const isAccountPaymentModalOpen = isRewardCodeOpen || isRechargeOpen || isWalletLedgerOpen || Boolean(nativeWechatPayment) || Boolean(rechargePaymentResult) || Boolean(wechatRechargeOrderConfirmationState); useEffect(() => { if (!authUi || authUi.user || authUi.canAccessProtectedData) { hasRequestedLoginRef.current = false; return; } if (hasRequestedLoginRef.current) { return; } hasRequestedLoginRef.current = true; authUi.openLoginModal(() => { window.location.reload(); }); }, [authUi]); const { projectTitle, setProjectTitle, projectRenameValue, setProjectRenameValue, isRenamingProject, isProjectRenameSaving, projectRenameError, activeSidebarPanel, setActiveSidebarPanel, activeTool, setActiveTool, isZoomMenuOpen, isBackgroundSettingsOpen, isMinimapOpen, canvasBackgroundColor, canvasBackgroundHexValue, startProjectRename, cancelProjectRename, submitProjectRename, resetProjectRenameError, applyCanvasBackgroundColor, handleCanvasBackgroundHexChange, closeEditorChromePanels, toggleSidebarPanel, toggleZoomMenu, closeZoomMenu, toggleBackgroundSettings, toggleMinimap, } = useImageCanvasEditorChrome({ openEditorLoginModal }); canvasBackgroundColorRef.current = canvasBackgroundColor; const discardAssetRelatedCanvasHistory = useCallback( (matchesLayer: (layer: CanvasLayer) => boolean) => discardCanvasHistoryEntriesContainingLayerRef.current(matchesLayer), [], ); const canDeleteLayersRef = useRef<(targetLayerIds: string[]) => boolean>( () => true, ); const removeCanvasLayersLinkedToAssets = useImageCanvasAssetLayerCleanup({ layers, setLayers, setSelectedLayerId, setSelectedLayerIds, onDeleteLayerSideEffects: (layerId) => clearDeletedLayerGenerationStateRef.current(layerId), canDeleteLayers: (targetLayerIds) => canDeleteLayersRef.current(targetLayerIds), discardHistoryEntriesContainingLayer: discardAssetRelatedCanvasHistory, }); const { assetFolders, setAssetFolders, assets, setAssets, groupedAssets, renamingAsset, setRenamingAsset, renamingFolder, setRenamingFolder, creatingFolder, setCreatingFolder, newFolderName, setNewFolderName, activeUploadFolderId, setActiveUploadFolderId, isAssetSelectionMode, selectedAssetIds, selectedAssets, assetMarquee, assetPointerDrag, setAssetPointerDrag, assetMoveDropFolderId, pinnedAssetMoveFolderId, resolveAssetFolderId, updateAssetMoveDropFolder, startRenamingAsset, commitAssetRename, toggleAssetFolder, commitNewAssetFolder, deleteUploadedAsset, submitAssetToShowcase, startRenamingFolder, commitFolderRename, deleteAssetFolder, toggleAssetSelected, toggleAllAssetsSelected, deleteSelectedAssets, moveAssetToFolder, closeAssetSelectionMode, openAssetSelectionMode, handleAssetMarqueePointerDown, handleAssetMarqueePointerMove, handleAssetMarqueePointerUp, handleAssetMarqueePointerCancel, handleAssetMarqueeLostPointerCapture, refreshAssetLibrary, } = useImageCanvasAssetLibrary({ assetListRef, assetPointerDragRef, canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true, openEditorLoginModal, onDeleteAssets: removeCanvasLayersLinkedToAssets, onOpenSelectionMode: () => setUploadDropTarget(null), }); useEffect(() => { if (activeSidebarPanel !== 'assets' && isAssetSelectionMode) { closeAssetSelectionMode(); } }, [activeSidebarPanel, closeAssetSelectionMode, isAssetSelectionMode]); const pendingImageReferenceUploadCountRef = useRef(0); const isPendingReferenceUploadContextMutationLocked = useCallback( () => pendingImageReferenceUploadCountRef.current > 0, [], ); const rejectPendingReferenceUploadContextMutation = useCallback(() => { window.alert('参考图正在上传,请等待上传完成后再切换或关闭生成面板'); }, []); const handleActivateCanvasGenerationDialog = useCallback(() => { closeGenerationTransientStateRef.current(); setSelectedLayerId(null); setSelectedLayerIds([]); setImageContextMenu(null); setContextMenu(null); }, []); const { generateDialog, setGenerateDialog, generateDialogRef, setInactiveGenerateDialogs, inactiveGenerateDialogsRef, activeCanvasGenerationDialog, canvasGenerationDialogs, getCanvasGenerationDialogsSnapshot, openCanvasGenerationDialog, updateCanvasGenerationDialogById, removeCanvasGenerationDialogById, hasCanvasGenerationDialogById, getCanvasGenerationDialogById, archiveActiveCanvasGenerationDialog, activateCanvasGenerationDialog, restoreCanvasGenerationDialogs, removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder, } = useCanvasGenerationDialogs({ onActivate: handleActivateCanvasGenerationDialog, isContextMutationLocked: isPendingReferenceUploadContextMutationLocked, onContextMutationRejected: rejectPendingReferenceUploadContextMutation, }); canvasGenerationDialogsRef.current = canvasGenerationDialogs; const canvasHistoryRefs = useMemo( () => ({ layersRef, viewportRef, generateDialogRef, inactiveGenerateDialogsRef, selectedLayerIdRef, selectedLayerIdsRef, }), [generateDialogRef, inactiveGenerateDialogsRef], ); const canvasHistorySetters = useMemo( () => ({ setLayers, setViewport, setGenerateDialog, setInactiveGenerateDialogs, setSelectedLayerId, setSelectedLayerIds, }), [ setGenerateDialog, setInactiveGenerateDialogs, setLayers, setSelectedLayerId, setSelectedLayerIds, setViewport, ], ); const canvasHistoryResetters = useMemo( () => ({ setHoveredLayerId, setMetadataLayer, resetCanvasInteractionState: () => resetCanvasInteractionStateRef.current(), }), [], ); const { canUndo, canRedo, getCanvasHistorySnapshot, captureCanvasHistory, discardCanvasHistoryEntriesContainingLayer, undoCanvasChange, redoCanvasChange, } = useCanvasHistory({ refs: canvasHistoryRefs, setters: canvasHistorySetters, resetters: canvasHistoryResetters, }); captureCanvasHistoryRef.current = captureCanvasHistory; discardCanvasHistoryEntriesContainingLayerRef.current = discardCanvasHistoryEntriesContainingLayer; const [historyFeedback, setHistoryFeedback] = useState(null); useEffect(() => { if (!historyFeedback) { return; } const timer = window.setTimeout(() => setHistoryFeedback(null), 3000); return () => window.clearTimeout(timer); }, [historyFeedback]); const showCanvasLayerCopyWarning = useCallback( (reason: CanvasLayerCopyBlockReason) => { setHistoryFeedback((currentFeedback) => ({ id: (currentFeedback?.id ?? 0) + 1, tone: 'warning', text: reason === 'self-contained-local-unsupported' ? '历史序列帧素材暂不支持复制' : reason === 'unresolved-resource' ? '素材资源尚未登记,暂不能复制' : '素材仍在保存,请稍后重试', })); }, [], ); const handleUndoCanvasChange = useCallback(() => { const result = undoCanvasChange(); if (result.status === 'empty') { return; } const actionLabel = formatCanvasHistoryAction(result.action); setHistoryFeedback((currentFeedback) => ({ id: (currentFeedback?.id ?? 0) + 1, tone: result.status === 'blocked' ? 'warning' : 'neutral', text: result.status === 'blocked' ? `无法撤销:${actionLabel}操作,可能会使图片消失` : `已撤销:${actionLabel}操作`, })); }, [undoCanvasChange]); const handleRedoCanvasChange = useCallback(() => { const result = redoCanvasChange(); if (result.status === 'empty') { return; } const actionLabel = formatCanvasHistoryAction(result.action); setHistoryFeedback((currentFeedback) => ({ id: (currentFeedback?.id ?? 0) + 1, tone: result.status === 'blocked' ? 'warning' : 'neutral', text: result.status === 'blocked' ? `无法恢复:${actionLabel}操作,可能会使图片消失` : `已恢复:${actionLabel}操作`, })); }, [redoCanvasChange]); const selectSingleLayer = useCallback( (layerId: string | null) => { setSelectedLayerId(layerId); setSelectedLayerIds(layerId ? [layerId] : []); if (layerId) { setGenerateDialog((currentDialog) => isCanvasGenerationComposerVisible(currentDialog) ? { ...currentDialog, composerOpen: false, } : currentDialog, ); } }, [setGenerateDialog], ); const focusCanvasBounds = useCallback( ( bounds: { x: number; y: number; width: number; height: number }, options: { reserveDialogSpace?: boolean; animate?: boolean } = {}, ) => { const nextViewport = fitViewportToBounds({ bounds, canvasSize, insets: { left: TASK_FOCUS_HORIZONTAL_INSET, right: TASK_FOCUS_HORIZONTAL_INSET, top: TASK_FOCUS_TOP_INSET, bottom: TASK_FOCUS_BOTTOM_INSET + (options.reserveDialogSpace ? TASK_FOCUS_DIALOG_BOTTOM_INSET : 0), }, padding: TASK_FOCUS_PADDING, }); if (options.animate) { animateViewportTo(nextViewport); return; } setViewport(nextViewport); }, [animateViewportTo, canvasSize, setViewport], ); const focusCanvasResource = useCallback( (resourceId: string): ImageCanvasActionResult => { const normalizedResourceId = resourceId.trim(); if (!normalizedResourceId) { return { successed: false, reason: 'other' }; } // 同一 resourceId 对应多个画布图层时,定位顺序中的首个图层,保持单目标聚焦行为。 const targetLayer = layersRef.current.find( (layer) => layer.resourceId === normalizedResourceId, ); if (!targetLayer) { return { successed: false, reason: 'not-found-on-canva' }; } focusCanvasBounds(targetLayer, { animate: true }); return { successed: true }; }, [focusCanvasBounds], ); const focusCanvasLayerById = useCallback( (layerId: string) => { const targetLayer = layersRef.current.find( (layer) => layer.id === layerId, ); if (!targetLayer) { return; } selectSingleLayer(targetLayer.id); setActiveTool('select'); setActiveSidebarPanel('layers'); focusCanvasBounds(targetLayer); }, [ focusCanvasBounds, selectSingleLayer, setActiveSidebarPanel, setActiveTool, ], ); const focusCanvasGenerationDialog = useCallback( (dialog: CanvasGenerationDialogState) => { if ( dialog.mode === 'quick-edit' && dialog.sourceLayerId && !dialog.generatedLayerId ) { const sourceLayer = layersRef.current.find( (layer) => layer.id === dialog.sourceLayerId, ); if (sourceLayer) { setSelectedLayerId(sourceLayer.id); setSelectedLayerIds([sourceLayer.id]); setActiveTool('generate'); setActiveSidebarPanel('layers'); setViewport( resolveQuickEditFocusViewport({ sourceLayer, canvasSize }), ); return; } } if (dialog.generatedLayerId) { focusCanvasLayerById(dialog.generatedLayerId); return; } if (!dialog.placeholder) { return; } setSelectedLayerId(null); setSelectedLayerIds([getCanvasGenerationSelectionId(dialog.id)]); setActiveTool('select'); focusCanvasBounds(dialog.placeholder, { reserveDialogSpace: isCanvasGenerationComposerVisible(dialog), }); }, [ canvasSize, focusCanvasBounds, focusCanvasLayerById, setActiveSidebarPanel, setActiveTool, setViewport, ], ); const focusExternalGenerationTask = useCallback( (task: ExternalGenerationTaskRecord) => { const targetLayer = layersRef.current.find( (layer) => layer.taskId === task.jobId || layer.id === task.sourceEntityId || layer.resourceId === task.sourceEntityId || layer.sourceResourceId === task.sourceEntityId, ); if (targetLayer) { focusCanvasLayerById(targetLayer.id); return; } const targetDialog = canvasGenerationDialogsRef.current.find( (dialog) => dialog.id === task.sourceEntityId || dialog.sourceLayerId === task.sourceEntityId || dialog.generatedLayerId === task.sourceEntityId, ); if (targetDialog) { focusCanvasGenerationDialog(targetDialog); } }, [focusCanvasGenerationDialog, focusCanvasLayerById], ); const persistGeneratedAsset = useCallback( (layer: CanvasLayer) => { if (authUiRef.current && !authUiRef.current.canAccessProtectedData) { openEditorLoginModal(); return; } createEditorAsset({ folderId: activeUploadFolderId, label: layer.title, imageSrc: layer.src, objectKey: layer.objectKey, assetObjectId: layer.assetObjectId, width: layer.originalWidth, height: layer.originalHeight, sourceType: 'generated', prompt: layer.prompt, actualPrompt: layer.actualPrompt, model: layer.model, provider: layer.provider, taskId: layer.taskId, imageSequenceFrames: layer.imageSequenceFrames, imageSequenceDurationMs: layer.imageSequenceDurationMs, assetKind: layer.assetKind, generationInputs: layer.generationInputs, }) .then((asset) => { const persistedAssetKind = canvasAssetKindOrNull(asset.assetKind); if ( layer.assetKind === 'character-animation' && (persistedAssetKind !== 'character-animation' || (asset.imageSequenceFrames?.length ?? 0) < 2 || !( asset.imageSequenceDurationMs && asset.imageSequenceDurationMs > 0 )) ) { throw new Error('服务器未返回完整的角色动作正式字段'); } setAssets((currentAssets) => [ ...currentAssets.filter( (currentAsset) => currentAsset.id !== asset.assetId, ), { id: asset.assetId, label: asset.label, src: asset.imageSrc, mediaType: layer.mediaType, width: asset.width, height: asset.height, folderId: asset.folderId, sourceKind: 'uploaded', sourceType: asset.sourceType, persisted: true, prompt: asset.prompt ?? undefined, actualPrompt: asset.actualPrompt ?? undefined, model: asset.model ?? undefined, provider: asset.provider ?? undefined, taskId: asset.taskId ?? undefined, objectKey: asset.objectKey ?? undefined, assetObjectId: asset.assetObjectId ?? undefined, imageSequenceFrames: asset.imageSequenceFrames ?? undefined, imageSequenceDurationMs: asset.imageSequenceDurationMs ?? undefined, assetKind: persistedAssetKind, generationInputs: generationInputsOrNull(asset.generationInputs), generationInputsHydrationState: hydrateCanvasGenerationInputsResult(asset.generationInputs) .state, }, ]); setAssetFolders((currentFolders) => currentFolders.map((folder) => folder.id === asset.folderId ? { ...folder, collapsed: false, } : folder, ), ); setLayers((currentLayers) => currentLayers.map((currentLayer) => currentLayer.id === layer.id ? { ...currentLayer, sourceAssetId: asset.assetId, objectKey: asset.objectKey ?? currentLayer.objectKey, assetObjectId: asset.assetObjectId ?? currentLayer.assetObjectId, } : currentLayer, ), ); }) .catch((error: unknown) => { const isAuthError = error instanceof Error && 'status' in error && (error.status === 401 || error.status === 403); if (isAuthError) { openEditorLoginModal(); return; } window.alert( error instanceof Error && error.message.trim() ? error.message : '素材保存失败,请稍后重试', ); }); }, [activeUploadFolderId, openEditorLoginModal, setAssetFolders, setAssets], ); const upsertGeneratedAsset = useCallback( (asset: EditorAssetSnapshot) => { setAssets((currentAssets) => [ ...currentAssets.filter( (currentAsset) => currentAsset.id !== asset.assetId, ), { id: asset.assetId, label: asset.label, src: asset.imageSrc, width: asset.width, height: asset.height, folderId: asset.folderId, sourceKind: 'uploaded', sourceType: asset.sourceType, persisted: true, prompt: asset.prompt ?? undefined, actualPrompt: asset.actualPrompt ?? undefined, model: asset.model ?? undefined, provider: asset.provider ?? undefined, taskId: asset.taskId ?? undefined, objectKey: asset.objectKey ?? undefined, assetObjectId: asset.assetObjectId ?? undefined, thumbnailSrc: asset.thumbnailSrc ?? undefined, imageSequenceFrames: asset.imageSequenceFrames ?? undefined, imageSequenceDurationMs: asset.imageSequenceDurationMs ?? undefined, assetKind: canvasAssetKindOrNull(asset.assetKind), generationInputs: generationInputsOrNull(asset.generationInputs), generationInputsHydrationState: hydrateCanvasGenerationInputsResult( asset.generationInputs, ).state, }, ]); setAssetFolders((currentFolders) => currentFolders.some((folder) => folder.id === asset.folderId) ? currentFolders.map((folder) => folder.id === asset.folderId ? { ...folder, collapsed: false } : folder, ) : [ ...currentFolders, { id: asset.folderId, label: '项目素材', collapsed: false, systemDefault: true, persisted: true, }, ], ); }, [setAssetFolders, setAssets], ); const projectPersistenceRefs = useMemo( () => ({ layersRef, viewportRef, canvasGenerationDialogsRef, getCanvasGenerationDialogsSnapshot, canvasBackgroundColorRef, selectedLayerIdRef, selectedLayerIdsRef, }), [getCanvasGenerationDialogsSnapshot], ); const projectPersistenceSetters = useMemo( () => ({ setProjectTitle, setProjectRenameValue, setViewport, setLayers, setSelectedLayerId, setSelectedLayerIds, selectSingleLayer, setLayerCounter: (value: number) => { layerCounterRef.current = value; }, restoreCanvasGenerationDialogs, applyCanvasBackgroundColor, }), [ applyCanvasBackgroundColor, setSelectedLayerId, setSelectedLayerIds, restoreCanvasGenerationDialogs, selectSingleLayer, setLayers, setProjectRenameValue, setProjectTitle, setViewport, ], ); const { projectId, isProjectReady, assetKindFallbackNotice, appendCanvasLayersWithResources, applyProjectSnapshot, flushProjectPersistence, deadInlinePlaceholderDropCount, } = useImageCanvasProjectPersistence({ refs: projectPersistenceRefs, setters: projectPersistenceSetters, layers, canvasGenerationDialogs, viewport, canvasSize, canvasBackgroundColor, isViewportInteracting, canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true, currentUserId: currentEditorUserId, openEditorLoginModal, onProjectAccessLost, }); const returnToProjects = useCallback(async () => { await flushProjectPersistence(); replaceAppHistoryPath('/project'); window.dispatchEvent(new PopStateEvent('popstate')); }, [flushProjectPersistence]); const setEditorProjectContextId = useImageCanvasContextStore( (state) => state.setProjectId, ); useEffect(() => { setEditorProjectContextId(projectId); }, [projectId, setEditorProjectContextId]); const applyGeneratedProjectSnapshot = useCallback( ( project: EditorProjectSnapshot, action: CanvasHistoryAction = { type: 'generate-image', count: 1 }, ) => { captureCanvasHistory(action); applyProjectSnapshot(project); if (action.type !== 'perfect-pixel') { void refreshAssetLibrary(); } }, [applyProjectSnapshot, captureCanvasHistory, refreshAssetLibrary], ); const refreshCanvas = useCallback(() => { if (!projectId) { return; } void loadEditorProject(projectId) .then(applyGeneratedProjectSnapshot) .catch(() => undefined); }, [applyGeneratedProjectSnapshot, projectId]); const persistUpdatedLayerResource = useCallback( (layer: CanvasLayer) => { const requestVersion = (layerResourcePersistenceVersionRef.current.get(layer.id) ?? 0) + 1; layerResourcePersistenceVersionRef.current.set(layer.id, requestVersion); if (!projectId) { return; } const imageSrc = resolveLayerProjectResourceImageSrc(layer); if (!imageSrc) { return; } const resourceAssetKind = resolveLayerResourceAssetKind(layer); void createEditorProjectResource(projectId, { 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, imageSequenceFrames: layer.imageSequenceFrames, imageSequenceDurationMs: layer.imageSequenceDurationMs, sourceResourceId: layer.sourceResourceId, assetKind: resourceAssetKind, generationInputs: layer.generationInputs, }) .then((resource) => { if ( layerResourcePersistenceVersionRef.current.get(layer.id) !== requestVersion ) { return; } const confirmedResourceAssetKind = canvasAssetKindOrNull(resource.assetKind) ?? resourceAssetKind; setLayers((currentLayers) => { const nextLayers = currentLayers.map((currentLayer) => currentLayer.id === layer.id ? { ...currentLayer, resourceId: resource.resourceId, resourcePersistenceState: 'registered' as const, resourceAssetKind: confirmedResourceAssetKind, assetKind: currentLayer.assetKindOverride ?? confirmedResourceAssetKind, } : currentLayer, ); layersRef.current = nextLayers; return nextLayers; }); setMetadataLayer((currentLayer) => currentLayer?.id === layer.id ? { ...currentLayer, resourceId: resource.resourceId, resourcePersistenceState: 'registered' as const, resourceAssetKind: confirmedResourceAssetKind, assetKind: currentLayer.assetKindOverride ?? confirmedResourceAssetKind, } : currentLayer, ); }) .catch(() => undefined); }, [projectId], ); const { assetExportStatus, isExportingAssets, exportCanvasAssets, exportSelectedAssets, exportLayerImage, reportAssetError, } = useImageCanvasAssetExportWorkflow({ layers, projectId, projectTitle, }); const downloadAsset = useCallback( (asset: EditorAsset) => { exportLayerImage(createAssetActionLayer(asset)); }, [exportLayerImage], ); const downloadSelectedAssets = useCallback(() => { const selectedLayers = selectedAssets.map((asset, index) => ({ ...createAssetActionLayer(asset), zIndex: index, })); return exportSelectedAssets(selectedLayers); }, [exportSelectedAssets, selectedAssets]); const { uploadInputRef, uploadTarget, pendingImageReferenceUploadCount, requestUpload, handleUploadInputChange, addUploadedFiles, } = useImageCanvasUploadWorkflow({ canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true, openEditorLoginModal, projectId, assetFolders, activeUploadFolderId, canvasSize, viewport, activeTool, allocateUploadIndex: () => { layerCounterRef.current += 1; return layerCounterRef.current; }, setAssetFolders, setAssets, setLayers, setGenerateDialog, setQuickEditPanel: (updater) => setQuickEditPanelRef.current(updater), resolveImageReferenceUploadCapacity: (target) => resolveImageReferenceUploadCapacityRef.current(target), appendUiAssetExtractionReferences: (references) => appendUiAssetExtractionReferencesRef.current(references), appendCanvasLayersWithResources, captureCanvasHistory, selectSingleLayer, refreshAssetLibrary, }); pendingImageReferenceUploadCountRef.current = pendingImageReferenceUploadCount; const generationSurface = useImageCanvasGenerationSurface({ layers, canvasSize, viewport, setViewport, canvasViewportRef, setLayers, layerCounterRef, specToolWrapRef, musicToolWrapRef, publicationToolWrapRef, characterSpecButtonRef, characterReferenceButtonRef, generationReferenceButtonRef, publicationReferenceButtonRef, iconSpecButtonRef, hasPendingImageReferenceUploads: pendingImageReferenceUploadCount > 0, generateDialog, setGenerateDialog, activeCanvasGenerationDialog, canvasGenerationDialogs, openCanvasGenerationDialog, activateCanvasGenerationDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, getCanvasGenerationDialogById, archiveActiveCanvasGenerationDialog, removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, selectSingleLayer, fitLayers, captureCanvasHistory, setActiveTool, setActiveSidebarPanel, setMetadataLayer, setImageContextMenu, requestUpload, projectAssets: assets, persistGeneratedAsset, persistUpdatedLayerResource, projectId, currentUserId: currentEditorUserId, assetFolderId: activeUploadFolderId, upsertGeneratedAsset, applyProjectSnapshot: applyGeneratedProjectSnapshot, applyProjectSnapshotWithoutHistory: applyProjectSnapshot, flushProjectPersistence, refreshAssetLibrary, onWalletBalanceMayHaveChanged: refreshEditorWalletState, }); const handleEditorAgentConfirmSent = useCallback(() => { generationSurface.refreshTaskList(); }, [generationSurface]); const showGenerationWarning = generationSurface.showGenerationWarning; useEffect(() => { if (assetKindFallbackNotice) { showGenerationWarning(assetKindFallbackNotice.message); } }, [assetKindFallbackNotice, showGenerationWarning]); useEffect(() => { if (deadInlinePlaceholderDropCount === 0) { return; } showGenerationWarning(DEAD_INLINE_PLACEHOLDER_NOTICE); }, [deadInlinePlaceholderDropCount, showGenerationWarning]); const handleInlinePlaceholdersExpired = useCallback(() => { showGenerationWarning(DEAD_INLINE_PLACEHOLDER_NOTICE); }, [showGenerationWarning]); const handleExternalGenerationTasksCompleted = useCallback( (tasks: ExternalGenerationTaskRecord[]) => { if (!projectId || tasks.length === 0) { return; } const warning = tasks .find((task) => task.warning?.trim()) ?.warning?.trim(); if (warning) { showGenerationWarning(warning); } }, [projectId, showGenerationWarning], ); const effectiveIsAgentConversationOpen = isAgentConversationEnabled && isAgentConversationOpen; const toggleAgentConversation = useCallback(() => { if (!isAgentConversationEnabled) { return; } const nextOpen = !isAgentConversationOpen; if (nextOpen && generationSurface.isTaskSidebarOpen) { generationSurface.toggleTaskSidebar(); } setIsAgentConversationOpen(nextOpen); }, [generationSurface, isAgentConversationEnabled, isAgentConversationOpen]); const toggleTaskSidebar = useCallback(() => { if ( !generationSurface.isTaskSidebarOpen && effectiveIsAgentConversationOpen ) { setIsAgentConversationOpen(false); } generationSurface.toggleTaskSidebar(); }, [effectiveIsAgentConversationOpen, generationSurface]); const openQuickEditPanelWithAvailableCanvas = useCallback( (layer: CanvasLayer) => { if (generationSurface.isTaskSidebarOpen) { generationSurface.toggleTaskSidebar(); } setIsAgentConversationOpen(false); generationSurface.openQuickEditPanel(layer); }, [generationSurface], ); const toggleCanvasSidebarPanel = useCallback( (panel: SidebarPanel) => { toggleSidebarPanel(panel); }, [toggleSidebarPanel], ); useEffect(() => { if (!isAgentConversationEnabled && isAgentConversationOpen) { setIsAgentConversationOpen(false); return; } if ( isAgentConversationEnabled && generationSurface.isTaskSidebarOpen && isAgentConversationOpen ) { generationSurface.toggleTaskSidebar(); } }, [ generationSurface, generationSurface.isTaskSidebarOpen, isAgentConversationEnabled, isAgentConversationOpen, ]); const { setQuickEditPanel, setCropExpandPanel, setCharacterAnimationPanel, setIsSpecMenuOpen, setIsGenerationReferenceMenuOpen, setIsPublicationReferenceMenuOpen, setIsCharacterSpecMenuOpen, setIsCharacterReferenceMenuOpen, isPickingGenerationReferenceFromCanvas, setIsPickingGenerationReferenceFromCanvas, isPickingQuickEditReferenceFromCanvas, setIsPickingQuickEditReferenceFromCanvas, isPickingPublicationReferenceFromCanvas, setIsPickingPublicationReferenceFromCanvas, isPickingCharacterSpecFromCanvas, setIsPickingCharacterSpecFromCanvas, isPickingCharacterReferenceFromCanvas, setIsPickingCharacterReferenceFromCanvas, setIsIconSpecMenuOpen, isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas, setIsUiDesignSpecMenuOpen, isPickingUiDesignSpecFromCanvas, setIsPickingUiDesignSpecFromCanvas, openCharacterAnimationPanel, openRedrawPanel, openCropExpandPanel, removeSelectedLayerBackground, snapSelectedLayerToPerfectPixels, activeInlineGenerationDialogOwnership, perfectPixelLayerIds, pendingPerfectPixelLayerIds, splitSelectedIconSpritesheet, splittingIconSpritesheetLayerIds, extractUiDesignAssets, pickCharacterSpecFromLayer, pickGenerationReferenceFromLayer, pickQuickEditReferenceFromLayer, pickPublicationReferenceFromLayer, pickCharacterReferenceFromLayer, pickIconSpecFromLayer, pickUiDesignSpecFromLayer, hideGeneratedLayerPanelAfterBlur, clearDeletedLayerGenerationState, uiAssetExtractionState, uiAssetExtractionSourceLayer, quickEditPanel, quickEditSelectionState, quickEditSelectionSourceLayer, changeUiAssetExtractionTool, changeUiAssetExtractionModel, changeQuickEditSelectionTool, startUiAssetExtractionPointer, startQuickEditSelectionPointer, moveUiAssetExtractionPointer, moveQuickEditSelectionPointer, endUiAssetExtractionPointer, endQuickEditSelectionPointer, cancelUiAssetExtractionPointer, cancelQuickEditSelectionPointer, cancelUiAssetExtraction, appendUiAssetExtractionReferences, removeUiAssetExtractionReference, submitUiAssetExtraction, generationComposerStyle, generationComposerNode, switchGenerationTool, } = generationSurface; closeGenerationTransientStateRef.current = () => { setIsSpecMenuOpen(false); setIsGenerationReferenceMenuOpen(false); setIsPublicationReferenceMenuOpen(false); setIsPickingGenerationReferenceFromCanvas(false); setIsPickingQuickEditReferenceFromCanvas(false); setIsPickingPublicationReferenceFromCanvas(false); setIsCharacterSpecMenuOpen(false); setIsCharacterReferenceMenuOpen(false); setIsPickingCharacterSpecFromCanvas(false); setIsPickingCharacterReferenceFromCanvas(false); setIsIconSpecMenuOpen(false); setIsPickingIconSpecFromCanvas(false); setIsUiDesignSpecMenuOpen(false); setIsPickingUiDesignSpecFromCanvas(false); setImageContextMenu(null); setContextMenu(null); }; setQuickEditPanelRef.current = setQuickEditPanel; clearDeletedLayerGenerationStateRef.current = clearDeletedLayerGenerationState; appendUiAssetExtractionReferencesRef.current = appendUiAssetExtractionReferences; resolveImageReferenceUploadCapacityRef.current = (target) => { const imageReferenceCount = (references: CharacterReferenceImage[] = []) => references.filter( (reference) => (reference.mediaType ?? 'image') === 'image', ).length; const generateDialogContextId = generateDialog ? (generateDialog.id ?? `${generateDialog.mode}:${generateDialog.sourceLayerId ?? 'draft'}`) : undefined; if (target === 'spec-reference') { return generateDialog?.mode === 'spec' ? { label: '规范参考图', currentCount: generateDialog.specType === 'icon' ? 0 : imageReferenceCount( resolveSpecGenerationReferences(generateDialog), ), limit: generateDialog.specType === 'icon' ? 1 : resolveDialogExtraImageReferenceLimit(generateDialog), contextId: generateDialogContextId, } : null; } if (target === 'character-spec') { if (generateDialog?.mode !== 'character') { return null; } const referencesFit = imageReferenceCount(generateDialog.characterReferences) <= resolveDialogExtraImageReferenceLimit(generateDialog); return { label: '角色主图', currentCount: 0, limit: referencesFit ? 1 : 0, contextId: generateDialogContextId, }; } if (target === 'icon-spec') { if (generateDialog?.mode !== 'icon') { return null; } const referencesFit = imageReferenceCount(generateDialog.generationReferences) <= resolveDialogExtraImageReferenceLimit(generateDialog); return { label: '图标规范图', currentCount: 0, limit: referencesFit ? 1 : 0, contextId: generateDialogContextId, }; } if (target === 'ui-design-icon-spec') { if (generateDialog?.mode !== 'ui-design') { return null; } const referencesFit = imageReferenceCount(generateDialog.generationReferences) <= resolveDialogExtraImageReferenceLimit(generateDialog); return { label: 'UI设计规范图', currentCount: 0, limit: referencesFit ? 1 : 0, contextId: generateDialogContextId, }; } if (target === 'character-reference') { return generateDialog?.mode === 'character' ? { label: '角色参考图', currentCount: imageReferenceCount( generateDialog.characterReferences, ), limit: resolveDialogExtraImageReferenceLimit(generateDialog), contextId: generateDialogContextId, } : null; } if (target === 'generation-reference') { return generateDialog && generateDialog.mode !== 'video' ? { label: '生成参考图', currentCount: imageReferenceCount( generateDialog.mode === 'spec' ? resolveSpecGenerationReferences(generateDialog) : generateDialog.generationReferences, ), limit: resolveDialogExtraImageReferenceLimit(generateDialog), contextId: generateDialogContextId, } : null; } if (target === 'publication-reference') { return generateDialog?.mode === 'publication' ? { label: '宣发参考图', currentCount: imageReferenceCount( generateDialog.publicationReferences, ), limit: resolveDialogExtraImageReferenceLimit(generateDialog), contextId: generateDialogContextId, } : null; } if (target === 'quick-edit-reference') { return quickEditPanel ? { label: '快速编辑参考图', currentCount: imageReferenceCount( quickEditPanel.quickEditReferences, ), limit: resolveExtraImageReferenceLimit( quickEditPanel.model, QUICK_EDIT_REFERENCE_LIMIT, 1, ), contextId: quickEditPanel.referenceUploadContextId, } : null; } if (target === 'ui-asset-extraction-reference') { return uiAssetExtractionState ? { label: 'UI素材参考图', currentCount: imageReferenceCount( uiAssetExtractionState.references, ), limit: resolveExtraImageReferenceLimit( uiAssetExtractionState.model, UI_EXTRA_REFERENCE_LIMIT, 1, ), contextId: uiAssetExtractionState.referenceUploadContextId, } : null; } return null; }; useEffect(() => { if (!isProjectReady || startupIntentConsumedRef.current) { return; } const startupIntent = readCanvasStartupIntent(); if (!startupIntent.showToolbarGuide && !startupIntent.tool) { startupIntentConsumedRef.current = true; return; } startupIntentConsumedRef.current = true; clearCanvasStartupIntent(); if (startupIntent.tool) { setIsToolbarGuideVisible(false); openCanvasStartupTool(startupIntent.tool, generationSurface); return; } if ( startupIntent.showToolbarGuide && layersRef.current.length === 0 && canvasGenerationDialogsRef.current.length === 0 ) { setIsToolbarGuideVisible(true); } }, [generationSurface, isProjectReady]); useEffect(() => { if (isToolbarGuideVisible && isCanvasGenerationDialog(generateDialog)) { setIsToolbarGuideVisible(false); } }, [generateDialog, isToolbarGuideVisible]); const clearGenerationPanelsAfterBlur = useCallback(() => { setQuickEditPanel(null); setCropExpandPanel(null); setCharacterAnimationPanel(null); }, [setCharacterAnimationPanel, setCropExpandPanel, setQuickEditPanel]); const { selectedLayer, selectedToolbarStyle, imageContextMenuLayer, contextShouldShowLayer, contextShouldUnlockLayer, clearCanvasFocus, handleCanvasContextMenu, handleLayerContextMenu, handleGenerationFrameContextMenu, } = useImageCanvasStageController({ layers, canvasGenerationDialogs, selectedLayerId, selectedLayerIds, setSelectedLayerId, setSelectedLayerIds, imageContextMenu, setImageContextMenu, contextMenu, setContextMenu, viewport, canvasSize, selectSingleLayer, hideGeneratedLayerPanelAfterBlur, clearGenerationPanelsAfterBlur, getCanvasPointFromClient, }); const openLayerGenerationDialog = useCallback( (layer: CanvasLayer) => { if (layer.generationInputsHydrationState === 'invalid-versioned') { showGenerationWarning( '保存的生成参数版本不受支持或数据损坏,无法改造。', ); return false; } const existingDialog = [...canvasGenerationDialogsRef.current] .reverse() .find((dialog) => dialog.generatedLayerId === layer.id); if (existingDialog) { activateCanvasGenerationDialog(existingDialog); setSelectedLayerId(layer.id); setSelectedLayerIds([layer.id]); return true; } const restoredDraft = createLayerGenerationDialogDraft({ sourceLayer: layer, canvasSize, viewport: viewportRef.current, sourceAnimationLayer: findSourceAnimationLayer( layersRef.current, layer, ), availableLayers: layersRef.current, }); if (!restoredDraft) { return false; } openCanvasGenerationDialog(restoredDraft); setSelectedLayerId(layer.id); setSelectedLayerIds([layer.id]); return true; }, [ activateCanvasGenerationDialog, canvasSize, openCanvasGenerationDialog, setSelectedLayerId, setSelectedLayerIds, showGenerationWarning, ], ); const openGeneratedLayerGenerationDialog = useCallback( (layer: CanvasLayer) => { // TODO(产品确认): 已完成的生成结果被普通点击时,是否仍应恢复并显示其生成浮层? // 当前实现会这样做,但现有文档只定义点击图层后的选中/工具栏行为,以及用户显式 // 点击“改造”时的配方恢复,未定义这一隐式恢复入口。确认前保留既有行为。 if (layer.sourceType !== 'generated') { return false; } return openLayerGenerationDialog(layer); }, [openLayerGenerationDialog], ); const removeCanvasGenerationDialog = useCallback( (dialogId: string) => { captureCanvasHistory({ type: 'delete-generation-result', count: 1 }); removeCanvasGenerationDialogById(dialogId); setSelectedLayerId(null); setSelectedLayerIds([]); setImageContextMenu(null); setContextMenu(null); setActiveTool('select'); }, [ captureCanvasHistory, removeCanvasGenerationDialogById, setActiveTool, setContextMenu, setImageContextMenu, setSelectedLayerId, setSelectedLayerIds, ], ); const requestRemoveCanvasGenerationDialog = useCallback( (dialog: CanvasGenerationDialogState) => { if (requiresGenerationDeleteConfirmation(dialog)) { activateCanvasGenerationDialog(dialog); setPendingGenerationDeleteDialog(dialog); return; } removeCanvasGenerationDialog(dialog.id); }, [activateCanvasGenerationDialog, removeCanvasGenerationDialog], ); const contextMenuLayer = contextMenu?.kind === 'layer' ? (layers.find((layer) => layer.id === contextMenu.layerId) ?? null) : null; const canDeleteLayersDuringReferenceUpload = useCallback( (targetLayerIds: string[]) => { if (pendingImageReferenceUploadCountRef.current <= 0) { return true; } const protectedSourceLayerIds = [ generateDialog?.sourceLayerId, generationSurface.quickEditPanel?.sourceLayerId, uiAssetExtractionState?.sourceLayerId, ].filter((layerId): layerId is string => Boolean(layerId)); if ( !targetLayerIds.some((layerId) => protectedSourceLayerIds.includes(layerId), ) ) { return true; } window.alert('参考图正在上传,请等待上传完成后再删除当前生成源图'); return false; }, [ generateDialog?.sourceLayerId, generationSurface.quickEditPanel?.sourceLayerId, uiAssetExtractionState?.sourceLayerId, ], ); canDeleteLayersRef.current = canDeleteLayersDuringReferenceUpload; const { canvasClipboard, canCopyContextLayers, contextLayerCopyBlockReason, isCanvasClipboardCopyBlocked, pasteCanvasClipboard, copyContextLayers, copySelectedLayers, duplicateContextLayers, duplicateSelectedLayers, moveContextLayers, moveSelectedLayers, groupContextLayers, ungroupContextLayers, toggleContextLayerVisibility, toggleContextLayerLock, flipContextLayers, deleteContextLayers, exportContextLayer, deleteLayerById, deleteSelectedLayer, groupSelectedLayers, } = useImageCanvasLayerCommands({ layers, contextMenu, selectedLayerId, selectedLayerIds, canvasGenerationDialogs, setLayers, appendCanvasLayersWithResources, setSelectedLayerId, setSelectedLayerIds, setHoveredLayerId, setMetadataLayer, setContextMenu, setImageContextMenu, setActiveTool, captureCanvasHistory, selectSingleLayer, onDeleteLayerSideEffects: clearDeletedLayerGenerationState, onDeleteGenerationDialogSideEffects: removeCanvasGenerationDialogById, onRequestDeleteGenerationDialog: requestRemoveCanvasGenerationDialog, exportLayerImage, onCanvasLayerCopyBlocked: showCanvasLayerCopyWarning, canDeleteLayers: (targetLayerIds) => canDeleteLayersRef.current(targetLayerIds), }); const { canvasMarquee, isPanning, snapGuide, effectiveTool, setIsSpacePanning, setShiftPressed, clearActiveInteraction, handleCanvasPointerDown, handleLayerPointerDown, handleLayerClick, handleGenerationFramePointerDown, handleMinimapPointerDown, handlePointerMove, finishDrag, } = useImageCanvasStageInteractions({ canvasViewportRef, activeTool, layers, setLayers, viewport, setViewport, selectedLayerIds, setSelectedLayerId, setSelectedLayerIds, generateDialog, canvasGenerationDialogs, setGenerateDialog, isPickingCharacterSpecFromCanvas, isPickingGenerationReferenceFromCanvas, isPickingQuickEditReferenceFromCanvas, isPickingPublicationReferenceFromCanvas, isPickingCharacterReferenceFromCanvas, isPickingIconSpecFromCanvas, isPickingUiDesignSpecFromCanvas, clearCanvasFocus, pickCharacterSpecFromLayer, pickGenerationReferenceFromLayer, pickQuickEditReferenceFromLayer, pickPublicationReferenceFromLayer, pickCharacterReferenceFromLayer, pickIconSpecFromLayer, pickUiDesignSpecFromLayer, openLayerGenerationDialog: openGeneratedLayerGenerationDialog, activateCanvasGenerationDialog, updateCanvasGenerationDialogById, moveViewportFromMinimapPointer, updateViewportFromMinimapDrag, flushMinimapViewportDrag, minimapScale: minimapModel?.scale ?? 1, onViewportInteractionStart: beginViewportInteraction, onViewportInteractionEnd: endViewportInteraction, onCloseImageContextMenu: () => setImageContextMenu(null), getCanvasHistorySnapshot, captureCanvasHistory, }); resetCanvasInteractionStateRef.current = clearActiveInteraction; useEffect(() => { if (!isAccountPaymentModalOpen) { return; } clearActiveInteraction(); cancelUiAssetExtractionPointer(); cancelQuickEditSelectionPointer(); }, [ cancelQuickEditSelectionPointer, cancelUiAssetExtractionPointer, clearActiveInteraction, isAccountPaymentModalOpen, ]); const openAccountPaymentModal = useCallback(() => { clearActiveInteraction(); cancelUiAssetExtractionPointer(); cancelQuickEditSelectionPointer(); openRechargeOrRewardCodeModal(); }, [ cancelQuickEditSelectionPointer, cancelUiAssetExtractionPointer, clearActiveInteraction, openRechargeOrRewardCodeModal, ]); const handleCanvasPointerDownWithUiExtractionDismiss = useCallback( (event: ReactPointerEvent) => { if (uiAssetExtractionState) { event.preventDefault(); cancelUiAssetExtraction(); return; } handleCanvasPointerDown(event); }, [cancelUiAssetExtraction, handleCanvasPointerDown, uiAssetExtractionState], ); const deleteLayerByIdFromShortcut = useCallback( (layerId: string | null) => deleteLayerByIdRef.current(layerId), [], ); // 中文注释:加载期的剥离只跑一次,当时未到期而被保留的孤儿占位需要这里补上到期清理, // 否则它会一直转到用户下一次加载。两条路径共用同一条文案,用户感知一致。 useInlineGenerationPlaceholderExpiry({ canvasGenerationDialogs, activeInlineGenerationDialogOwnership, removeCanvasGenerationDialogById, onPlaceholdersExpired: handleInlinePlaceholdersExpired, }); const confirmRemoveGeneratingDialog = useCallback(() => { const dialog = pendingGenerationDeleteDialog; if (!dialog) { return; } setPendingGenerationDeleteDialog(null); removeCanvasGenerationDialog(dialog.id); }, [pendingGenerationDeleteDialog, removeCanvasGenerationDialog]); const updateLayerAssetKind = useCallback( (layerId: string, assetKind: CanvasAssetKind | null) => { const targetLayer = layersRef.current.find( (layer) => layer.id === layerId, ); if (!targetLayer || targetLayer.assetKind === assetKind) { return; } const resourceAssetKind = resolveLayerResourceAssetKind(targetLayer); if (!isCanvasAssetKindOverrideCompatible(resourceAssetKind, assetKind)) { showGenerationWarning('该标签与素材的媒体类型不兼容,已保留原标签。'); return; } captureCanvasHistory({ type: 'change-asset-kind', count: 1, layerIds: [layerId], }); const assetKindOverride = assetKind === resourceAssetKind ? null : assetKind; const nextLayer = { ...targetLayer, resourceAssetKind, assetKindOverride, assetKind: assetKindOverride ?? resourceAssetKind, }; setLayers((currentLayers) => { const nextLayers = currentLayers.map((layer) => layer.id === layerId ? nextLayer : layer, ); layersRef.current = nextLayers; return nextLayers; }); setMetadataLayer((currentLayer) => currentLayer?.id === layerId ? nextLayer : currentLayer, ); setImageContextMenu(null); setContextMenu(null); }, [captureCanvasHistory, showGenerationWarning], ); const selectAllCanvasObjects = useCallback(() => { const layerSelectionIds = layersRef.current .filter((layer) => !layer.hidden) .map((layer) => layer.id); const layerIds = new Set(layersRef.current.map((layer) => layer.id)); const generationSelectionIds = canvasGenerationDialogsRef.current .filter((dialog) => { if (!dialog.placeholder) { return false; } return !( dialog.generatedLayerId && layerIds.has(dialog.generatedLayerId) ); }) .map((dialog) => getCanvasGenerationSelectionId(dialog.id)); const nextSelectionIds = [...layerSelectionIds, ...generationSelectionIds]; setSelectedLayerIds(nextSelectionIds); setSelectedLayerId(layerSelectionIds[0] ?? null); setImageContextMenu(null); setContextMenu(null); setGenerateDialog((currentDialog) => isCanvasGenerationComposerVisible(currentDialog) ? { ...currentDialog, composerOpen: false, } : currentDialog, ); }, [setGenerateDialog]); const clearCanvasSelection = useCallback(() => { setSelectedLayerId(null); setSelectedLayerIds([]); setImageContextMenu(null); setContextMenu(null); }, []); const nudgeSelectedCanvasObjects = useCallback( (delta: { x: number; y: number }) => { const selectionIds = selectedLayerIdsRef.current.length ? selectedLayerIdsRef.current : selectedLayerIdRef.current ? [selectedLayerIdRef.current] : []; const targetLayerIds = getSelectedLayerIds(selectionIds); const targetDialogIds = getSelectedGenerationDialogIds(selectionIds); if (!targetLayerIds.length && !targetDialogIds.length) { return; } captureCanvasHistory({ type: targetLayerIds.length ? 'move-image' : 'move-generation-result', count: targetLayerIds.length || targetDialogIds.length, }); if (targetLayerIds.length) { setLayers((currentLayers) => currentLayers.map((layer) => targetLayerIds.includes(layer.id) ? { ...layer, x: layer.x + delta.x, y: layer.y + delta.y, } : layer, ), ); } targetDialogIds.forEach((dialogId) => { updateCanvasGenerationDialogById(dialogId, (dialog) => dialog.placeholder ? { ...dialog, placeholder: { ...dialog.placeholder, x: dialog.placeholder.x + delta.x, y: dialog.placeholder.y + delta.y, }, } : dialog, ); }); setImageContextMenu(null); setContextMenu(null); }, [captureCanvasHistory, updateCanvasGenerationDialogById], ); const exportCanvasAssetsFromShortcut = useCallback(() => { if (!layersRef.current.some((layer) => layer.src.trim().length > 0)) { return; } void exportCanvasAssets(); }, [exportCanvasAssets]); const zoomViewportBy = useCallback( (scaleFactor: number) => { updateScaleFromCenter(viewportRef.current.scale * scaleFactor); }, [updateScaleFromCenter], ); const switchTool = useCallback( (tool: CanvasTool) => { clearActiveInteraction(); if (tool === 'upload') { requestUpload('asset', { addToCanvas: true }); return; } if (switchGenerationTool(tool)) { return; } setActiveTool(tool); }, [ clearActiveInteraction, requestUpload, setActiveTool, switchGenerationTool, ], ); useImageCanvasKeyboardShortcuts({ isInteractionPaused: isAccountPaymentModalOpen, generateDialogRef, selectedLayerIdRef, selectedLayerIdsRef, undoCanvasChange: handleUndoCanvasChange, redoCanvasChange: handleRedoCanvasChange, deleteLayerById: deleteLayerByIdFromShortcut, deleteSelectedCanvasObjects: deleteSelectedLayer, selectAllCanvasObjects, clearCanvasSelection, copySelectedCanvasLayers: copySelectedLayers, pasteCanvasClipboard, duplicateSelectedCanvasLayers: duplicateSelectedLayers, moveSelectedCanvasLayers: moveSelectedLayers, nudgeSelectedCanvasObjects, requestRemoveCanvasGenerationDialog, setActiveTool, switchTool, updateScaleFromCenter, zoomViewportBy, fitLayers, toggleSidebarPanel, toggleMinimap, startProjectRename, exportCanvasAssets: exportCanvasAssetsFromShortcut, setGenerateDialog, setImageContextMenu, setContextMenu, setQuickEditPanel, closeEditorChromePanels, setIsSpecMenuOpen, setIsGenerationReferenceMenuOpen, setIsPublicationReferenceMenuOpen: generationSurface.setIsPublicationReferenceMenuOpen, setIsPickingGenerationReferenceFromCanvas, setIsPickingQuickEditReferenceFromCanvas, setIsPickingPublicationReferenceFromCanvas, setIsCharacterSpecMenuOpen, setIsCharacterReferenceMenuOpen, setIsPickingCharacterSpecFromCanvas, setIsPickingCharacterReferenceFromCanvas, setIsIconSpecMenuOpen, setIsPickingIconSpecFromCanvas, setIsUiDesignSpecMenuOpen, setIsPickingUiDesignSpecFromCanvas, setIsSpacePanning, setShiftPressed, }); const pasteCanvasOrSystemClipboard = useCallback( (canvasPoint?: { x: number; y: number }) => { if (pasteCanvasClipboard(canvasPoint)) { return; } void readSystemClipboardImageFiles() .then((imageFiles) => { if (imageFiles.length) { addUploadedFiles(imageFiles, { addToCanvas: true, canvasPoint }); } }) .catch(() => { // 系统剪贴板读取受浏览器权限限制,失败时保留原生粘贴路径。 }); }, [addUploadedFiles, pasteCanvasClipboard], ); useEffect(() => { const handleClipboardPaste = (event: ClipboardEvent) => { if (isEditablePasteTarget(event.target)) { return; } if (isAccountPaymentModalOpen) { return; } const didPasteCanvasClipboard = pasteCanvasClipboard(); if (didPasteCanvasClipboard) { event.preventDefault(); return; } const imageFiles = getClipboardImageFiles(event.clipboardData); if (imageFiles.length) { event.preventDefault(); addUploadedFiles(imageFiles, { addToCanvas: true }); return; } }; window.addEventListener('paste', handleClipboardPaste); return () => { window.removeEventListener('paste', handleClipboardPaste); }; }, [addUploadedFiles, isAccountPaymentModalOpen, pasteCanvasClipboard]); useEffect(() => { const blockBrowserZoom = (event: WheelEvent) => { const editorElement = editorRootRef.current; if ( editorElement && event.target instanceof Node && editorElement.contains(event.target) && (event.ctrlKey || event.metaKey) ) { event.preventDefault(); } }; window.addEventListener('wheel', blockBrowserZoom, { capture: true, passive: false, }); return () => { window.removeEventListener('wheel', blockBrowserZoom, { capture: true }); }; }, []); const { addAssetLayer, handleCanvasDragOver, handleCanvasDragLeave, handleCanvasDrop, } = useImageCanvasAssetCanvasBridge({ assetPointerDragRef, suppressAssetClickRef, assets, assetFolders, layerCounterRef, viewport, canvasSize, resolveAssetFolderId, resolveCanvasPoint, getCanvasDropPoint, setAssetPointerDrag, setActiveUploadFolderId, setUploadDropTarget, setHoveredLayerId, updateAssetMoveDropFolder, moveAssetToFolder, captureCanvasHistory, appendCanvasLayersWithResources, selectSingleLayer, addUploadedFiles, onAssetError: reportAssetError, }); deleteLayerByIdRef.current = deleteLayerById; const assetDragPreview = assetPointerDrag?.active ? { x: assetPointerDrag.currentClientX, y: assetPointerDrag.currentClientY, label: assets.find((asset) => asset.id === assetPointerDrag.assetId) ?.label ?? '素材', } : null; const sidebarProps = { activeSidebarPanel, assetListRef, assetPointerDragRef, suppressAssetClickRef, assets, groupedAssets, assetFolders, layers, selectedLayerId, selectedLayerIds, isAssetSelectionMode, selectedAssetIds, assetMoveDropFolderId, pinnedAssetMoveFolderId, creatingFolder, newFolderName, renamingFolder, renamingAsset, isExportingSelectedAssets: isExportingAssets, assetMarquee, setCreatingFolder, setNewFolderName, setRenamingFolder, setRenamingAsset, setActiveUploadFolderId, setUploadDropTarget, setAssetPointerDrag, setImageContextMenu, setContextMenu, onAssetMarqueePointerDown: handleAssetMarqueePointerDown, onAssetMarqueePointerMove: handleAssetMarqueePointerMove, onAssetMarqueePointerUp: handleAssetMarqueePointerUp, onAssetMarqueePointerCancel: handleAssetMarqueePointerCancel, onAssetMarqueeLostPointerCapture: handleAssetMarqueeLostPointerCapture, updateAssetMoveDropFolder, addUploadedFiles, requestUpload, moveAssetToFolder, commitNewAssetFolder, toggleAssetFolder, startRenamingFolder, commitFolderRename, deleteAssetFolder, startRenamingAsset, commitAssetRename, deleteUploadedAsset, submitAssetToShowcase, toggleAssetSelected, addAssetLayer, onDownloadAsset: downloadAsset, toggleAllAssetsSelected, downloadSelectedAssets, deleteSelectedAssets, closeAssetSelectionMode, openAssetSelectionMode, groupSelectedLayers, selectSingleLayer, resolveContextMenuPosition, getCanvasPointFromClient, }; const topbarProps = { projectTitle, projectRenameValue, isRenamingProject, isProjectRenameSaving, projectRenameError, layers, walletBalance, walletBreakdown: mudPointBalance, isWalletBalanceLoading, isWalletDetailsLoading: isLoadingRechargeCenter, walletDetailsError: rechargeError || mudPointBalanceError || null, currentUser: authUi?.user, assetExportStatus, isExportingAssets, setProjectRenameValue, startProjectRename, cancelProjectRename, submitProjectRename, resetProjectRenameError, exportCanvasAssets, onOpenShortcuts: () => setIsShortcutDialogOpen(true), onRequestWalletDetails: loadRechargeCenter, onRecharge: openAccountPaymentModal, onOpenWalletLedger: openWalletLedgerPanel, onReturnToProjects: returnToProjects, onOpenAccount: () => { if (authUi?.user) { authUi.openAccountModal(); return; } authUi?.openLoginModal(); }, }; const stageProps = { canvasViewportRef, specToolWrapRef, musicToolWrapRef, publicationToolWrapRef, isInteractionPaused: isAccountPaymentModalOpen, isPanning, effectiveTool, canvasBackgroundColor, canvasBackgroundHexValue, viewport, snapGuide, layers, selectedLayer, selectedLayerIds, hoveredLayerId, canvasMarquee, canvasGenerationDialogs, editorAgentAssets: assets, isToolbarGuideVisible, taskListRefreshKey: generationSurface.taskListRefreshKey, isTaskSidebarOpen: generationSurface.isTaskSidebarOpen, isAgentConversationEnabled, isAgentConversationOpen: effectiveIsAgentConversationOpen, generateDialog, cropExpandPanel: generationSurface.cropExpandPanel, cropExpandSourceLayer: generationSurface.cropExpandSourceLayer, uiAssetExtractionState: isAccountPaymentModalOpen ? null : uiAssetExtractionState, uiAssetExtractionSourceLayer: isAccountPaymentModalOpen ? null : uiAssetExtractionSourceLayer, quickEditSelectionState, quickEditSelectionSourceLayer, hasPendingImageReferenceUploads: pendingImageReferenceUploadCount > 0, generationComposerStyle, selectedToolbarStyle, perfectPixelLayerIds, pendingPerfectPixelLayerIds, splittingIconSpritesheetLayerIds, uploadDropTarget, contextMenu, canvasClipboard, imageContextMenu, imageContextMenuLayer, contextShouldShowLayer, contextShouldUnlockLayer, canCopyContextLayers, contextLayerCopyBlockReason, isCanvasClipboardCopyBlocked, canUndo, canRedo, historyFeedback, isZoomMenuOpen, isBackgroundSettingsOpen, activeSidebarPanel, isMinimapOpen, minimapModel, onCanvasPointerDown: handleCanvasPointerDownWithUiExtractionDismiss, onCanvasPointerMove: handlePointerMove, onCanvasPointerUp: finishDrag, onCanvasDragOver: handleCanvasDragOver, onCanvasDragLeave: handleCanvasDragLeave, onCanvasDrop: handleCanvasDrop, onCanvasContextMenu: handleCanvasContextMenu, onLayerPointerDown: handleLayerPointerDown, onLayerClick: handleLayerClick, onSelectLayer: selectSingleLayer, onLayerContextMenu: handleLayerContextMenu, onLayerMouseEnter: setHoveredLayerId, onLayerMouseLeave: (layerId: string) => setHoveredLayerId((currentId) => currentId === layerId ? null : currentId, ), onOpenLayerMetadata: (layer: CanvasLayer) => { setMetadataLayer(layer); selectSingleLayer(layer.id); }, onUpdateLayerAssetKind: updateLayerAssetKind, onGenerationFramePointerDown: handleGenerationFramePointerDown, onGenerationFrameContextMenu: handleGenerationFrameContextMenu, onActivateGenerationDialog: activateCanvasGenerationDialog, onFocusExternalTask: focusExternalGenerationTask, onExternalTasksCompleted: handleExternalGenerationTasksCompleted, onExternalTaskWalletMayHaveChanged: refreshEditorWalletState, onEditorAgentConfirmSent: handleEditorAgentConfirmSent, onToggleTaskSidebar: toggleTaskSidebar, onToggleAgentConversation: toggleAgentConversation, onCropExpandHandlePointerDown: generationSurface.startCropExpandFrameResize, onOpenQuickEditPanel: openQuickEditPanelWithAvailableCanvas, onOpenRedrawPanel: openRedrawPanel, onOpenCropExpandPanel: openCropExpandPanel, onRemoveBackground: removeSelectedLayerBackground, onPerfectPixel: (layer: CanvasLayer) => { void snapSelectedLayerToPerfectPixels(layer); }, onSplitIconSpritesheet: (layer: CanvasLayer) => { void flushProjectPersistence().then(() => splitSelectedIconSpritesheet(layer), ); }, onExtractUiDesignAssets: extractUiDesignAssets, onUiAssetExtractionToolChange: changeUiAssetExtractionTool, onUiAssetExtractionModelChange: changeUiAssetExtractionModel, onUiAssetExtractionPointerStart: startUiAssetExtractionPointer, onUiAssetExtractionPointerMove: moveUiAssetExtractionPointer, onUiAssetExtractionPointerEnd: endUiAssetExtractionPointer, onRequestUiAssetExtractionReferenceUpload: () => requestUpload('ui-asset-extraction-reference'), onRemoveUiAssetExtractionReference: removeUiAssetExtractionReference, onSubmitUiAssetExtraction: () => void submitUiAssetExtraction(), contextMenuLayer, onQuickEditSelectionToolChange: changeQuickEditSelectionTool, onQuickEditSelectionPointerStart: startQuickEditSelectionPointer, onQuickEditSelectionPointerMove: moveQuickEditSelectionPointer, onQuickEditSelectionPointerEnd: endQuickEditSelectionPointer, onOpenCharacterAnimationPanel: openCharacterAnimationPanel, onDownloadLayer: exportLayerImage, onPasteCanvasClipboard: pasteCanvasOrSystemClipboard, onCopyContextLayers: copyContextLayers, onDuplicateContextLayers: duplicateContextLayers, onMoveContextLayers: moveContextLayers, onGroupContextLayers: groupContextLayers, onUngroupContextLayers: ungroupContextLayers, onToggleContextLayerVisibility: toggleContextLayerVisibility, onToggleContextLayerLock: toggleContextLayerLock, onFlipContextLayers: flipContextLayers, onExportContextLayer: exportContextLayer, onDeleteContextLayers: deleteContextLayers, onDeleteLayerById: deleteLayerById, onCloseContextMenu: () => setContextMenu(null), onCloseImageContextMenu: () => setImageContextMenu(null), onUpdateScaleFromCenter: updateScaleFromCenter, onFitLayers: fitLayers, onUndoCanvasChange: handleUndoCanvasChange, onRedoCanvasChange: handleRedoCanvasChange, onToggleZoomMenu: toggleZoomMenu, onCloseZoomMenu: closeZoomMenu, onToggleBackgroundSettings: toggleBackgroundSettings, onApplyCanvasBackgroundColor: applyCanvasBackgroundColor, onCanvasBackgroundHexChange: handleCanvasBackgroundHexChange, onToggleSidebarPanel: toggleCanvasSidebarPanel, onToggleMinimap: toggleMinimap, onMinimapPointerDown: handleMinimapPointerDown, onSwitchTool: switchTool, onOpenToolOptions: generationSurface.openToolbarOptionMenu, onCloseToolOptions: generationSurface.closeToolbarOptionMenu, children: generationComposerNode, }; return ( setMetadataLayer(null), }} /> setPendingGenerationDeleteDialog(null)} onConfirm={confirmRemoveGeneratingDialog} > 删除后会停止在画布中显示这个生成任务,已消耗的泥点不会返还。 setIsShortcutDialogOpen(false)} /> {isRewardCodeOpen ? ( setIsRewardCodeOpen(false)} /> ) : null} {isRechargeOpen ? ( setIsRechargeOpen(false)} onRetry={loadRechargeCenter} onBuy={buyRechargeProduct} onConfirmNativePayment={confirmNativeWechatPayment} onCloseNativePayment={closeNativeWechatPayment} /> ) : null} {isWalletLedgerOpen ? ( setIsWalletLedgerOpen(false)} onRetry={loadWalletLedger} /> ) : null} {rechargePaymentResult ? ( setRechargePaymentResult(null)} /> ) : null} {wechatRechargeOrderConfirmationState ? ( ) : null} ); } export default ImageCanvasEditorView;