7452e3e3e2
统一右键图层命令后关闭画布菜单和图片菜单 补齐快速编辑入口的菜单关闭逻辑 让 Ctrl+V 优先粘贴画布内部剪贴板 补充右键菜单、图层命令和快捷键回归测试
1614 lines
50 KiB
TypeScript
1614 lines
50 KiB
TypeScript
import {
|
|
type Dispatch,
|
|
type PointerEvent as ReactPointerEvent,
|
|
type SetStateAction,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import {
|
|
createEditorAsset,
|
|
createEditorProjectResource,
|
|
type EditorAssetSnapshot,
|
|
type EditorProjectSnapshot,
|
|
loadEditorGenerationPricing,
|
|
} from '../../services/image-editor/editorProjectClient';
|
|
import { shouldShowRechargeEntry } from '../../services/payment/paymentPlatform';
|
|
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
|
import { useAuthUi } from '../auth/AuthUiContext';
|
|
import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog';
|
|
import { PlatformProfileRechargeModal } from '../platform-entry/PlatformProfileRechargeModal';
|
|
import { PlatformProfileRewardCodeRedeemModal } from '../platform-entry/PlatformProfileRewardCodeRedeemModal';
|
|
import {
|
|
PlatformRechargePaymentConfirmationMask,
|
|
PlatformRechargePaymentResultDialog,
|
|
} from '../platform-entry/PlatformRechargePaymentStatusDialogs';
|
|
import { usePlatformProfileCenterController } from '../platform-entry/usePlatformProfileCenterController';
|
|
import { formatDashboardCount } from '../rpg-entry/rpgEntryProfileDashboardPresentation';
|
|
import {
|
|
canvasAssetKindOrNull,
|
|
generationInputsOrNull,
|
|
isInlineEditorMediaSource,
|
|
resolveContextMenuPosition,
|
|
} from './ImageCanvasEditorModel';
|
|
import { ImageCanvasEditorShellView } from './ImageCanvasEditorShellView';
|
|
import type {
|
|
AssetPointerDragState,
|
|
CanvasAssetKind,
|
|
CanvasContextMenuState,
|
|
CanvasGenerationDialogState,
|
|
CanvasLayer,
|
|
CanvasTool,
|
|
CanvasViewport,
|
|
ImageContextMenuState,
|
|
QuickEditPanelState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { getEditorUploadAccept, isImageFile } from './ImageCanvasFileModel';
|
|
import { applyEditorGenerationPricingConfig } from './ImageCanvasGenerationModel';
|
|
import { isCanvasGenerationComposerVisible } from './ImageCanvasOverlayModel';
|
|
import {
|
|
getCanvasGenerationSelectionId,
|
|
getSelectedGenerationDialogIds,
|
|
getSelectedLayerIds,
|
|
} from './ImageCanvasSelectionModel';
|
|
import { ImageCanvasShortcutDialogView } from './ImageCanvasShortcutDialogView';
|
|
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
|
|
import { useCanvasHistory } from './useCanvasHistory';
|
|
import {
|
|
useImageCanvasAssetCanvasBridge,
|
|
useImageCanvasAssetLayerCleanup,
|
|
} from './useImageCanvasAssetCanvasBridge';
|
|
import { useImageCanvasAssetExportWorkflow } from './useImageCanvasAssetExportWorkflow';
|
|
import { useImageCanvasAssetLibrary } from './useImageCanvasAssetLibrary';
|
|
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 { useImageCanvasUploadWorkflow } from './useImageCanvasUploadWorkflow';
|
|
import {
|
|
DEFAULT_IMAGE_CANVAS_VIEWPORT,
|
|
useImageCanvasViewportControls,
|
|
} from './useImageCanvasViewportControls';
|
|
|
|
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 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));
|
|
}
|
|
|
|
export function ImageCanvasEditorView() {
|
|
const authUi = useAuthUi();
|
|
const [, setGenerationPricingVersion] = useState(0);
|
|
const [walletBalanceLabel, setWalletBalanceLabel] = useState<string | null>(
|
|
null,
|
|
);
|
|
const [isWalletBalanceLoading, setIsWalletBalanceLoading] = useState(false);
|
|
const editorRootRef = useRef<HTMLElement | null>(null);
|
|
const canvasViewportRef = useRef<HTMLDivElement | null>(null);
|
|
const assetListRef = useRef<HTMLDivElement | null>(null);
|
|
const assetPointerDragRef = useRef<AssetPointerDragState | null>(null);
|
|
const authUiRef = useRef(authUi);
|
|
const hasRequestedLoginRef = useRef(false);
|
|
const layerCounterRef = useRef(0);
|
|
const layersRef = useRef<CanvasLayer[]>([]);
|
|
const canvasGenerationDialogsRef = useRef<CanvasGenerationDialogState[]>([]);
|
|
const viewportRef = useRef<CanvasViewport>(DEFAULT_IMAGE_CANVAS_VIEWPORT);
|
|
const captureCanvasHistoryRef = useRef<() => void>(() => {});
|
|
const resetCanvasInteractionStateRef = useRef<() => void>(() => {});
|
|
const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
|
const musicToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
|
const publicationToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
|
const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const characterReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const generationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const publicationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const selectedLayerIdRef = useRef<string | null>(null);
|
|
const showRechargeEntry = shouldShowRechargeEntry();
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
void loadEditorGenerationPricing()
|
|
.then((pricing) => {
|
|
if (!isMounted) {
|
|
return;
|
|
}
|
|
applyEditorGenerationPricingConfig(pricing);
|
|
setGenerationPricingVersion((version) => version + 1);
|
|
})
|
|
.catch(() => {
|
|
// 中文注释:价格接口异常时保留前端内置兜底,不阻断画板使用。
|
|
});
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, []);
|
|
const selectedLayerIdsRef = useRef<string[]>([]);
|
|
const setQuickEditPanelRef = useRef<
|
|
Dispatch<SetStateAction<QuickEditPanelState | null>>
|
|
>(() => {});
|
|
const deleteLayerByIdRef = useRef<(targetLayerId: string | null) => void>(
|
|
() => {},
|
|
);
|
|
const suppressAssetClickRef = useRef(false);
|
|
const [layers, setLayers] = useState<CanvasLayer[]>([]);
|
|
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
|
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
|
const [hoveredLayerId, setHoveredLayerId] = useState<string | null>(null);
|
|
const [metadataLayer, setMetadataLayer] = useState<CanvasLayer | null>(null);
|
|
const [pendingGenerationDeleteDialog, setPendingGenerationDeleteDialog] =
|
|
useState<CanvasGenerationDialogState | null>(null);
|
|
const [imageContextMenu, setImageContextMenu] =
|
|
useState<ImageContextMenuState | null>(null);
|
|
const [contextMenu, setContextMenu] = useState<CanvasContextMenuState | null>(
|
|
null,
|
|
);
|
|
const [isViewportInteracting, setIsViewportInteracting] = useState(false);
|
|
const [uploadDropTarget, setUploadDropTarget] = useState<
|
|
'canvas' | 'assets' | null
|
|
>(null);
|
|
const [isShortcutDialogOpen, setIsShortcutDialogOpen] = useState(false);
|
|
const captureViewportHistory = useCallback(() => {
|
|
captureCanvasHistoryRef.current();
|
|
}, []);
|
|
const {
|
|
viewport,
|
|
setViewport,
|
|
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 refreshEditorWalletBalance = useCallback(() => {
|
|
if (!authUiRef.current?.canAccessProtectedData || !authUiRef.current.user) {
|
|
return;
|
|
}
|
|
void getPlatformProfileDashboard({
|
|
authImpact: 'local',
|
|
skipRefresh: true,
|
|
notifyAuthStateChange: false,
|
|
clearAuthOnUnauthorized: false,
|
|
})
|
|
.then((dashboard) => {
|
|
setWalletBalanceLabel(
|
|
`${formatDashboardCount(dashboard.walletBalance)}泥点`,
|
|
);
|
|
})
|
|
.catch(() => undefined);
|
|
}, []);
|
|
const {
|
|
activeRechargeTab,
|
|
buyRechargeProduct,
|
|
confirmNativeWechatPayment,
|
|
isLoadingRechargeCenter,
|
|
isRechargeOpen,
|
|
isRewardCodeOpen,
|
|
isSubmittingRewardCode,
|
|
loadRechargeCenter,
|
|
nativeWechatPayment,
|
|
openRechargeOrRewardCodeModal,
|
|
rechargeCenter,
|
|
rechargeError,
|
|
rechargePaymentResult,
|
|
rewardCodeError,
|
|
rewardCodeInput,
|
|
rewardCodeSuccess,
|
|
setActiveRechargeTab,
|
|
setIsRechargeOpen,
|
|
setIsRewardCodeOpen,
|
|
setRechargePaymentResult,
|
|
setRewardCodeInput,
|
|
submittingRechargeProductId,
|
|
submitRewardCode,
|
|
wechatRechargeOrderConfirmationState,
|
|
} = usePlatformProfileCenterController({
|
|
activeTab: 'editor-canvas',
|
|
isAuthenticated: Boolean(authUi?.user),
|
|
showRechargeEntry,
|
|
onRechargeSuccess: refreshEditorWalletBalance,
|
|
requestLogin: () => authUiRef.current?.openLoginModal(),
|
|
currentUser: authUi?.user ?? null,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!authUi || authUi.user || authUi.canAccessProtectedData) {
|
|
hasRequestedLoginRef.current = false;
|
|
return;
|
|
}
|
|
if (hasRequestedLoginRef.current) {
|
|
return;
|
|
}
|
|
hasRequestedLoginRef.current = true;
|
|
authUi.openLoginModal(() => {
|
|
window.location.reload();
|
|
});
|
|
}, [authUi]);
|
|
useEffect(() => {
|
|
if (!authUi?.canAccessProtectedData || !authUi.user) {
|
|
setWalletBalanceLabel(null);
|
|
setIsWalletBalanceLoading(false);
|
|
return;
|
|
}
|
|
|
|
let isMounted = true;
|
|
let requestId = 0;
|
|
|
|
const refreshWalletBalance = () => {
|
|
const currentRequestId = requestId + 1;
|
|
requestId = currentRequestId;
|
|
setIsWalletBalanceLoading(true);
|
|
void getPlatformProfileDashboard({
|
|
authImpact: 'local',
|
|
skipRefresh: true,
|
|
notifyAuthStateChange: false,
|
|
clearAuthOnUnauthorized: false,
|
|
})
|
|
.then((dashboard) => {
|
|
if (!isMounted || currentRequestId !== requestId) {
|
|
return;
|
|
}
|
|
setWalletBalanceLabel(`${formatDashboardCount(dashboard.walletBalance)}泥点`);
|
|
})
|
|
.catch(() => {
|
|
if (!isMounted || currentRequestId !== requestId) {
|
|
return;
|
|
}
|
|
setWalletBalanceLabel(null);
|
|
})
|
|
.finally(() => {
|
|
if (!isMounted || currentRequestId !== requestId) {
|
|
return;
|
|
}
|
|
setIsWalletBalanceLoading(false);
|
|
});
|
|
};
|
|
|
|
refreshWalletBalance();
|
|
|
|
const handleWindowFocus = () => {
|
|
refreshWalletBalance();
|
|
};
|
|
const handleVisibilityChange = () => {
|
|
if (document.visibilityState === 'visible') {
|
|
refreshWalletBalance();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('focus', handleWindowFocus);
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
window.removeEventListener('focus', handleWindowFocus);
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
};
|
|
}, [authUi?.canAccessProtectedData, authUi?.user?.id]);
|
|
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 });
|
|
const removeCanvasLayersLinkedToAssets = useImageCanvasAssetLayerCleanup({
|
|
layers,
|
|
setLayers,
|
|
setSelectedLayerId,
|
|
setSelectedLayerIds,
|
|
});
|
|
const {
|
|
assetFolders,
|
|
setAssetFolders,
|
|
assets,
|
|
setAssets,
|
|
groupedAssets,
|
|
allSelectableAssetsSelected,
|
|
renamingAsset,
|
|
setRenamingAsset,
|
|
renamingFolder,
|
|
setRenamingFolder,
|
|
creatingFolder,
|
|
setCreatingFolder,
|
|
newFolderName,
|
|
setNewFolderName,
|
|
activeUploadFolderId,
|
|
setActiveUploadFolderId,
|
|
isAssetSelectionMode,
|
|
setIsAssetSelectionMode,
|
|
selectedAssetIds,
|
|
setSelectedAssetIds,
|
|
assetMarquee,
|
|
assetPointerDrag,
|
|
setAssetPointerDrag,
|
|
assetMoveDropFolderId,
|
|
pinnedAssetMoveFolderId,
|
|
resolveAssetFolderId,
|
|
updateAssetMoveDropFolder,
|
|
startRenamingAsset,
|
|
commitAssetRename,
|
|
toggleAssetFolder,
|
|
commitNewAssetFolder,
|
|
deleteUploadedAsset,
|
|
setAssetPublicShowcaseEnabled,
|
|
startRenamingFolder,
|
|
commitFolderRename,
|
|
deleteAssetFolder,
|
|
toggleAssetSelected,
|
|
toggleAllAssetsSelected,
|
|
deleteSelectedAssets,
|
|
moveAssetToFolder,
|
|
closeAssetSelectionMode,
|
|
handleAssetMarqueePointerDown,
|
|
handleAssetMarqueePointerMove,
|
|
handleAssetMarqueePointerUp,
|
|
refreshAssetLibrary,
|
|
} = useImageCanvasAssetLibrary({
|
|
assetListRef,
|
|
canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true,
|
|
openEditorLoginModal,
|
|
onDeleteAssets: removeCanvasLayersLinkedToAssets,
|
|
});
|
|
|
|
const handleActivateCanvasGenerationDialog = useCallback(() => {
|
|
setSelectedLayerId(null);
|
|
setSelectedLayerIds([]);
|
|
setImageContextMenu(null);
|
|
}, []);
|
|
const {
|
|
generateDialog,
|
|
setGenerateDialog,
|
|
generateDialogRef,
|
|
setInactiveGenerateDialogs,
|
|
inactiveGenerateDialogsRef,
|
|
activeCanvasGenerationDialog,
|
|
canvasGenerationDialogs,
|
|
openCanvasGenerationDialog,
|
|
updateCanvasGenerationDialogById,
|
|
removeCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
archiveActiveCanvasGenerationDialog,
|
|
activateCanvasGenerationDialog,
|
|
restoreCanvasGenerationDialogs,
|
|
removeCanvasGenerationDialogsByLayerId,
|
|
getGeneratingDialogPlaceholder,
|
|
} = useCanvasGenerationDialogs({
|
|
onActivate: handleActivateCanvasGenerationDialog,
|
|
});
|
|
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,
|
|
captureCanvasHistory,
|
|
undoCanvasChange,
|
|
redoCanvasChange,
|
|
} = useCanvasHistory({
|
|
refs: canvasHistoryRefs,
|
|
setters: canvasHistorySetters,
|
|
resetters: canvasHistoryResetters,
|
|
});
|
|
captureCanvasHistoryRef.current = captureCanvasHistory;
|
|
const selectSingleLayer = useCallback(
|
|
(layerId: string | null) => {
|
|
setSelectedLayerId(layerId);
|
|
setSelectedLayerIds(layerId ? [layerId] : []);
|
|
if (layerId) {
|
|
setGenerateDialog((currentDialog) =>
|
|
isCanvasGenerationComposerVisible(currentDialog)
|
|
? {
|
|
...currentDialog,
|
|
composerOpen: false,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
}
|
|
},
|
|
[setGenerateDialog],
|
|
);
|
|
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,
|
|
durationSeconds: layer.durationSeconds,
|
|
assetKind: layer.assetKind,
|
|
generationInputs: layer.generationInputs,
|
|
})
|
|
.then((asset) => {
|
|
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,
|
|
durationSeconds: asset.durationSeconds ?? layer.durationSeconds,
|
|
assetKind: canvasAssetKindOrNull(asset.assetKind),
|
|
generationInputs: generationInputsOrNull(asset.generationInputs),
|
|
},
|
|
]);
|
|
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) => {
|
|
if (
|
|
error instanceof Error &&
|
|
'status' in error &&
|
|
(error.status === 401 || error.status === 403)
|
|
) {
|
|
openEditorLoginModal();
|
|
}
|
|
});
|
|
},
|
|
[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,
|
|
durationSeconds: asset.durationSeconds ?? undefined,
|
|
assetKind: canvasAssetKindOrNull(asset.assetKind),
|
|
generationInputs: generationInputsOrNull(asset.generationInputs),
|
|
},
|
|
]);
|
|
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,
|
|
}),
|
|
[],
|
|
);
|
|
const projectPersistenceSetters = useMemo(
|
|
() => ({
|
|
setProjectTitle,
|
|
setProjectRenameValue,
|
|
setViewport,
|
|
setLayers,
|
|
selectSingleLayer,
|
|
setLayerCounter: (value: number) => {
|
|
layerCounterRef.current = value;
|
|
},
|
|
restoreCanvasGenerationDialogs,
|
|
}),
|
|
[
|
|
restoreCanvasGenerationDialogs,
|
|
selectSingleLayer,
|
|
setLayers,
|
|
setProjectRenameValue,
|
|
setProjectTitle,
|
|
setViewport,
|
|
],
|
|
);
|
|
const { projectId, appendCanvasLayersWithResources, applyProjectSnapshot } =
|
|
useImageCanvasProjectPersistence({
|
|
refs: projectPersistenceRefs,
|
|
setters: projectPersistenceSetters,
|
|
layers,
|
|
canvasGenerationDialogs,
|
|
viewport,
|
|
isViewportInteracting,
|
|
canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true,
|
|
openEditorLoginModal,
|
|
});
|
|
const applyGeneratedProjectSnapshot = useCallback(
|
|
(project: EditorProjectSnapshot) => {
|
|
applyProjectSnapshot(project);
|
|
void refreshAssetLibrary();
|
|
},
|
|
[applyProjectSnapshot, refreshAssetLibrary],
|
|
);
|
|
const persistUpdatedLayerResource = useCallback(
|
|
(layer: CanvasLayer) => {
|
|
if (!projectId) {
|
|
return;
|
|
}
|
|
const imageSrc = resolveLayerProjectResourceImageSrc(layer);
|
|
if (!imageSrc) {
|
|
return;
|
|
}
|
|
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,
|
|
durationSeconds: layer.durationSeconds,
|
|
sourceResourceId: layer.sourceResourceId,
|
|
assetKind: layer.assetKind,
|
|
generationInputs: layer.generationInputs,
|
|
}).then((resource) => {
|
|
setLayers((currentLayers) => {
|
|
const nextLayers = currentLayers.map((currentLayer) =>
|
|
currentLayer.id === layer.id
|
|
? {
|
|
...currentLayer,
|
|
resourceId: resource.resourceId,
|
|
}
|
|
: currentLayer,
|
|
);
|
|
layersRef.current = nextLayers;
|
|
return nextLayers;
|
|
});
|
|
});
|
|
},
|
|
[projectId],
|
|
);
|
|
const {
|
|
assetExportStatus,
|
|
isExportingAssets,
|
|
exportCanvasAssets,
|
|
exportLayerImage,
|
|
} = useImageCanvasAssetExportWorkflow({
|
|
layers,
|
|
projectId,
|
|
projectTitle,
|
|
});
|
|
const {
|
|
uploadInputRef,
|
|
uploadTarget,
|
|
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),
|
|
appendCanvasLayersWithResources,
|
|
selectSingleLayer,
|
|
});
|
|
const generationSurface = useImageCanvasGenerationSurface({
|
|
layers,
|
|
canvasSize,
|
|
viewport,
|
|
setViewport,
|
|
canvasViewportRef,
|
|
setLayers,
|
|
layerCounterRef,
|
|
specToolWrapRef,
|
|
musicToolWrapRef,
|
|
publicationToolWrapRef,
|
|
characterSpecButtonRef,
|
|
characterReferenceButtonRef,
|
|
generationReferenceButtonRef,
|
|
publicationReferenceButtonRef,
|
|
iconSpecButtonRef,
|
|
generateDialog,
|
|
setGenerateDialog,
|
|
activeCanvasGenerationDialog,
|
|
canvasGenerationDialogs,
|
|
openCanvasGenerationDialog,
|
|
updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
archiveActiveCanvasGenerationDialog,
|
|
removeCanvasGenerationDialogsByLayerId,
|
|
getGeneratingDialogPlaceholder,
|
|
appendCanvasLayersWithResources,
|
|
selectSingleLayer,
|
|
fitLayers,
|
|
captureCanvasHistory,
|
|
setActiveTool,
|
|
setActiveSidebarPanel,
|
|
setMetadataLayer,
|
|
setImageContextMenu,
|
|
requestUpload,
|
|
persistGeneratedAsset,
|
|
persistUpdatedLayerResource,
|
|
projectId,
|
|
assetFolderId: activeUploadFolderId,
|
|
upsertGeneratedAsset,
|
|
applyProjectSnapshot: applyGeneratedProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged: refreshEditorWalletBalance,
|
|
});
|
|
const {
|
|
setQuickEditPanel,
|
|
setCropExpandPanel,
|
|
setCharacterAnimationPanel,
|
|
setIsSpecMenuOpen,
|
|
setIsGenerationReferenceMenuOpen,
|
|
setIsCharacterSpecMenuOpen,
|
|
setIsCharacterReferenceMenuOpen,
|
|
isPickingGenerationReferenceFromCanvas,
|
|
setIsPickingGenerationReferenceFromCanvas,
|
|
isPickingQuickEditReferenceFromCanvas,
|
|
setIsPickingQuickEditReferenceFromCanvas,
|
|
isPickingPublicationReferenceFromCanvas,
|
|
setIsPickingPublicationReferenceFromCanvas,
|
|
isPickingCharacterSpecFromCanvas,
|
|
setIsPickingCharacterSpecFromCanvas,
|
|
isPickingCharacterReferenceFromCanvas,
|
|
setIsPickingCharacterReferenceFromCanvas,
|
|
setIsIconSpecMenuOpen,
|
|
isPickingIconSpecFromCanvas,
|
|
setIsPickingIconSpecFromCanvas,
|
|
setIsUiDesignSpecMenuOpen,
|
|
isPickingUiDesignSpecFromCanvas,
|
|
setIsPickingUiDesignSpecFromCanvas,
|
|
openCharacterAnimationPanel,
|
|
openQuickEditPanel,
|
|
openRedrawPanel,
|
|
openCropExpandPanel,
|
|
removeSelectedLayerBackground,
|
|
extractUiDesignAssets,
|
|
pickCharacterSpecFromLayer,
|
|
pickGenerationReferenceFromLayer,
|
|
pickQuickEditReferenceFromLayer,
|
|
pickPublicationReferenceFromLayer,
|
|
pickCharacterReferenceFromLayer,
|
|
pickIconSpecFromLayer,
|
|
pickUiDesignSpecFromLayer,
|
|
hideGeneratedLayerPanelAfterBlur,
|
|
clearDeletedLayerGenerationState,
|
|
uiAssetExtractionState,
|
|
uiAssetExtractionSourceLayer,
|
|
quickEditSelectionState,
|
|
quickEditSelectionSourceLayer,
|
|
changeUiAssetExtractionTool,
|
|
changeQuickEditSelectionTool,
|
|
startUiAssetExtractionPointer,
|
|
startQuickEditSelectionPointer,
|
|
moveUiAssetExtractionPointer,
|
|
moveQuickEditSelectionPointer,
|
|
endUiAssetExtractionPointer,
|
|
endQuickEditSelectionPointer,
|
|
cancelUiAssetExtraction,
|
|
submitUiAssetExtraction,
|
|
generationComposerStyle,
|
|
generationComposerNode,
|
|
switchGenerationTool,
|
|
} = generationSurface;
|
|
setQuickEditPanelRef.current = setQuickEditPanel;
|
|
const clearGenerationPanelsAfterBlur = useCallback(() => {
|
|
setQuickEditPanel(null);
|
|
setCropExpandPanel(null);
|
|
setCharacterAnimationPanel(null);
|
|
}, [setCharacterAnimationPanel, setCropExpandPanel, setQuickEditPanel]);
|
|
const {
|
|
selectedLayer,
|
|
selectedToolbarStyle,
|
|
imageContextMenuLayer,
|
|
contextShouldShowLayer,
|
|
contextShouldUnlockLayer,
|
|
clearCanvasFocus,
|
|
handleCanvasContextMenu,
|
|
handleLayerContextMenu,
|
|
} = useImageCanvasStageController({
|
|
layers,
|
|
canvasGenerationDialogs,
|
|
selectedLayerId,
|
|
selectedLayerIds,
|
|
imageContextMenu,
|
|
setImageContextMenu,
|
|
contextMenu,
|
|
setContextMenu,
|
|
viewport,
|
|
canvasSize,
|
|
selectSingleLayer,
|
|
hideGeneratedLayerPanelAfterBlur,
|
|
clearGenerationPanelsAfterBlur,
|
|
getCanvasPointFromClient,
|
|
});
|
|
const contextMenuLayer =
|
|
contextMenu?.kind === 'layer'
|
|
? layers.find((layer) => layer.id === contextMenu.layerId) ?? null
|
|
: null;
|
|
const {
|
|
canvasClipboard,
|
|
pasteCanvasClipboard,
|
|
copyContextLayers,
|
|
copySelectedLayers,
|
|
duplicateContextLayers,
|
|
duplicateSelectedLayers,
|
|
moveContextLayers,
|
|
moveSelectedLayers,
|
|
groupContextLayers,
|
|
ungroupContextLayers,
|
|
toggleContextLayerVisibility,
|
|
toggleContextLayerLock,
|
|
flipContextLayers,
|
|
deleteContextLayers,
|
|
exportContextLayer,
|
|
deleteLayerById,
|
|
deleteSelectedLayer,
|
|
groupSelectedLayers,
|
|
} = useImageCanvasLayerCommands({
|
|
layers,
|
|
contextMenu,
|
|
selectedLayerId,
|
|
selectedLayerIds,
|
|
canvasGenerationDialogs,
|
|
setLayers,
|
|
setSelectedLayerId,
|
|
setSelectedLayerIds,
|
|
setHoveredLayerId,
|
|
setMetadataLayer,
|
|
setContextMenu,
|
|
setImageContextMenu,
|
|
setActiveTool,
|
|
captureCanvasHistory,
|
|
selectSingleLayer,
|
|
onDeleteLayerSideEffects: clearDeletedLayerGenerationState,
|
|
onDeleteGenerationDialogSideEffects: removeCanvasGenerationDialogById,
|
|
exportLayerImage,
|
|
});
|
|
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,
|
|
activateCanvasGenerationDialog,
|
|
updateCanvasGenerationDialogById,
|
|
moveViewportFromMinimapPointer,
|
|
updateViewportFromMinimapDrag,
|
|
flushMinimapViewportDrag,
|
|
minimapScale: minimapModel?.scale ?? 1,
|
|
onViewportInteractionStart: beginViewportInteraction,
|
|
onViewportInteractionEnd: endViewportInteraction,
|
|
onCloseImageContextMenu: () => setImageContextMenu(null),
|
|
});
|
|
resetCanvasInteractionStateRef.current = clearActiveInteraction;
|
|
const handleCanvasPointerDownWithUiExtractionDismiss = useCallback(
|
|
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
if (uiAssetExtractionState) {
|
|
event.preventDefault();
|
|
cancelUiAssetExtraction();
|
|
return;
|
|
}
|
|
handleCanvasPointerDown(event);
|
|
},
|
|
[cancelUiAssetExtraction, handleCanvasPointerDown, uiAssetExtractionState],
|
|
);
|
|
const deleteLayerByIdFromShortcut = useCallback(
|
|
(layerId: string | null) => deleteLayerByIdRef.current(layerId),
|
|
[],
|
|
);
|
|
const removeCanvasGenerationDialog = useCallback(
|
|
(dialogId: string) => {
|
|
captureCanvasHistory();
|
|
removeCanvasGenerationDialogById(dialogId);
|
|
setSelectedLayerId(null);
|
|
setSelectedLayerIds([]);
|
|
setImageContextMenu(null);
|
|
setContextMenu(null);
|
|
setActiveTool('select');
|
|
},
|
|
[
|
|
captureCanvasHistory,
|
|
removeCanvasGenerationDialogById,
|
|
setActiveTool,
|
|
setContextMenu,
|
|
setImageContextMenu,
|
|
setSelectedLayerId,
|
|
setSelectedLayerIds,
|
|
],
|
|
);
|
|
const requestRemoveCanvasGenerationDialog = useCallback(
|
|
(dialog: CanvasGenerationDialogState) => {
|
|
if (dialog.status === 'generating') {
|
|
activateCanvasGenerationDialog(dialog);
|
|
setPendingGenerationDeleteDialog(dialog);
|
|
return;
|
|
}
|
|
removeCanvasGenerationDialog(dialog.id);
|
|
},
|
|
[activateCanvasGenerationDialog, removeCanvasGenerationDialog],
|
|
);
|
|
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;
|
|
}
|
|
captureCanvasHistory();
|
|
const nextLayer = {
|
|
...targetLayer,
|
|
assetKind,
|
|
};
|
|
setLayers((currentLayers) => {
|
|
const nextLayers = currentLayers.map((layer) =>
|
|
layer.id === layerId ? { ...layer, assetKind } : layer,
|
|
);
|
|
layersRef.current = nextLayers;
|
|
return nextLayers;
|
|
});
|
|
setMetadataLayer((currentLayer) =>
|
|
currentLayer?.id === layerId ? nextLayer : currentLayer,
|
|
);
|
|
persistUpdatedLayerResource(nextLayer);
|
|
setImageContextMenu(null);
|
|
setContextMenu(null);
|
|
},
|
|
[captureCanvasHistory, persistUpdatedLayerResource],
|
|
);
|
|
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();
|
|
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({
|
|
generateDialogRef,
|
|
selectedLayerIdRef,
|
|
selectedLayerIdsRef,
|
|
redoCanvasChange,
|
|
undoCanvasChange,
|
|
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,
|
|
});
|
|
|
|
useEffect(() => {
|
|
const handleClipboardPaste = (event: ClipboardEvent) => {
|
|
if (isEditablePasteTarget(event.target)) {
|
|
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, 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,
|
|
});
|
|
|
|
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,
|
|
allSelectableAssetsSelected,
|
|
assetMarquee,
|
|
setIsAssetSelectionMode,
|
|
setCreatingFolder,
|
|
setNewFolderName,
|
|
setRenamingFolder,
|
|
setRenamingAsset,
|
|
setActiveUploadFolderId,
|
|
setUploadDropTarget,
|
|
setAssetPointerDrag,
|
|
setSelectedAssetIds,
|
|
setImageContextMenu,
|
|
setContextMenu,
|
|
onAssetMarqueePointerDown: handleAssetMarqueePointerDown,
|
|
onAssetMarqueePointerMove: handleAssetMarqueePointerMove,
|
|
onAssetMarqueePointerUp: handleAssetMarqueePointerUp,
|
|
updateAssetMoveDropFolder,
|
|
addUploadedFiles,
|
|
requestUpload,
|
|
moveAssetToFolder,
|
|
commitNewAssetFolder,
|
|
toggleAssetFolder,
|
|
startRenamingFolder,
|
|
commitFolderRename,
|
|
deleteAssetFolder,
|
|
startRenamingAsset,
|
|
commitAssetRename,
|
|
deleteUploadedAsset,
|
|
setAssetPublicShowcaseEnabled,
|
|
toggleAssetSelected,
|
|
addAssetLayer,
|
|
toggleAllAssetsSelected,
|
|
deleteSelectedAssets,
|
|
closeAssetSelectionMode,
|
|
groupSelectedLayers,
|
|
selectSingleLayer,
|
|
resolveContextMenuPosition,
|
|
getCanvasPointFromClient,
|
|
};
|
|
const topbarProps = {
|
|
projectId,
|
|
projectTitle,
|
|
projectRenameValue,
|
|
isRenamingProject,
|
|
isProjectRenameSaving,
|
|
projectRenameError,
|
|
layers,
|
|
walletBalanceLabel,
|
|
isWalletBalanceLoading,
|
|
assetExportStatus,
|
|
isExportingAssets,
|
|
setProjectRenameValue,
|
|
startProjectRename,
|
|
cancelProjectRename,
|
|
submitProjectRename,
|
|
resetProjectRenameError,
|
|
exportCanvasAssets,
|
|
onOpenShortcuts: () => setIsShortcutDialogOpen(true),
|
|
onOpenWallet: openRechargeOrRewardCodeModal,
|
|
};
|
|
const stageProps = {
|
|
canvasViewportRef,
|
|
specToolWrapRef,
|
|
musicToolWrapRef,
|
|
publicationToolWrapRef,
|
|
isPanning,
|
|
effectiveTool,
|
|
canvasBackgroundColor,
|
|
canvasBackgroundHexValue,
|
|
viewport,
|
|
snapGuide,
|
|
layers,
|
|
selectedLayer,
|
|
selectedLayerIds,
|
|
hoveredLayerId,
|
|
canvasMarquee,
|
|
canvasGenerationDialogs,
|
|
generateDialog,
|
|
cropExpandPanel: generationSurface.cropExpandPanel,
|
|
cropExpandSourceLayer: generationSurface.cropExpandSourceLayer,
|
|
uiAssetExtractionState,
|
|
uiAssetExtractionSourceLayer,
|
|
quickEditSelectionState,
|
|
quickEditSelectionSourceLayer,
|
|
generationComposerStyle,
|
|
selectedToolbarStyle,
|
|
uploadDropTarget,
|
|
contextMenu,
|
|
canvasClipboard,
|
|
imageContextMenu,
|
|
imageContextMenuLayer,
|
|
contextShouldShowLayer,
|
|
contextShouldUnlockLayer,
|
|
canUndo,
|
|
canRedo,
|
|
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,
|
|
onActivateGenerationDialog: activateCanvasGenerationDialog,
|
|
onCropExpandHandlePointerDown:
|
|
generationSurface.startCropExpandFrameResize,
|
|
onOpenQuickEditPanel: openQuickEditPanel,
|
|
onOpenRedrawPanel: openRedrawPanel,
|
|
onOpenCropExpandPanel: openCropExpandPanel,
|
|
onRemoveBackground: removeSelectedLayerBackground,
|
|
onExtractUiDesignAssets: extractUiDesignAssets,
|
|
onUiAssetExtractionToolChange: changeUiAssetExtractionTool,
|
|
onUiAssetExtractionPointerStart: startUiAssetExtractionPointer,
|
|
onUiAssetExtractionPointerMove: moveUiAssetExtractionPointer,
|
|
onUiAssetExtractionPointerEnd: endUiAssetExtractionPointer,
|
|
onSubmitUiAssetExtraction: () => void submitUiAssetExtraction(),
|
|
contextMenuLayer,
|
|
onQuickEditSelectionToolChange: changeQuickEditSelectionTool,
|
|
onQuickEditSelectionPointerStart: startQuickEditSelectionPointer,
|
|
onQuickEditSelectionPointerMove: moveQuickEditSelectionPointer,
|
|
onQuickEditSelectionPointerEnd: endQuickEditSelectionPointer,
|
|
onOpenCharacterAnimationPanel: openCharacterAnimationPanel,
|
|
onDownloadLayer: exportLayerImage,
|
|
onPasteCanvasClipboard: pasteCanvasClipboard,
|
|
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: undoCanvasChange,
|
|
onRedoCanvasChange: redoCanvasChange,
|
|
onToggleZoomMenu: toggleZoomMenu,
|
|
onCloseZoomMenu: closeZoomMenu,
|
|
onToggleBackgroundSettings: toggleBackgroundSettings,
|
|
onApplyCanvasBackgroundColor: applyCanvasBackgroundColor,
|
|
onCanvasBackgroundHexChange: handleCanvasBackgroundHexChange,
|
|
onToggleSidebarPanel: toggleSidebarPanel,
|
|
onToggleMinimap: toggleMinimap,
|
|
onMinimapPointerDown: handleMinimapPointerDown,
|
|
onSwitchTool: switchTool,
|
|
onOpenToolOptions: generationSurface.openToolbarOptionMenu,
|
|
onCloseToolOptions: generationSurface.closeToolbarOptionMenu,
|
|
children: generationComposerNode,
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<ImageCanvasEditorShellView
|
|
editorRootRef={editorRootRef}
|
|
uploadInputRef={uploadInputRef}
|
|
uploadAccept={getEditorUploadAccept(uploadTarget)}
|
|
onUploadInputChange={handleUploadInputChange}
|
|
assetDragPreview={assetDragPreview}
|
|
sidebarProps={sidebarProps}
|
|
topbarProps={topbarProps}
|
|
stageProps={stageProps}
|
|
metadataProps={{
|
|
layer: metadataLayer,
|
|
onClose: () => setMetadataLayer(null),
|
|
}}
|
|
/>
|
|
<PlatformDangerConfirmDialog
|
|
open={Boolean(pendingGenerationDeleteDialog)}
|
|
title="删除生成中的占位图"
|
|
confirmLabel="确认删除"
|
|
onClose={() => setPendingGenerationDeleteDialog(null)}
|
|
onConfirm={confirmRemoveGeneratingDialog}
|
|
>
|
|
删除后会停止在画布中显示这个生成任务,已消耗的泥点不会返还。
|
|
</PlatformDangerConfirmDialog>
|
|
<ImageCanvasShortcutDialogView
|
|
open={isShortcutDialogOpen}
|
|
onClose={() => setIsShortcutDialogOpen(false)}
|
|
/>
|
|
{isRewardCodeOpen ? (
|
|
<PlatformProfileRewardCodeRedeemModal
|
|
value={rewardCodeInput}
|
|
isSubmitting={isSubmittingRewardCode}
|
|
error={rewardCodeError}
|
|
success={rewardCodeSuccess}
|
|
onChange={setRewardCodeInput}
|
|
onSubmit={submitRewardCode}
|
|
onClose={() => setIsRewardCodeOpen(false)}
|
|
/>
|
|
) : null}
|
|
{isRechargeOpen ? (
|
|
<PlatformProfileRechargeModal
|
|
center={rechargeCenter}
|
|
isLoading={isLoadingRechargeCenter}
|
|
error={rechargeError}
|
|
submittingProductId={submittingRechargeProductId}
|
|
nativePayment={nativeWechatPayment}
|
|
activeTab={activeRechargeTab}
|
|
onTabChange={setActiveRechargeTab}
|
|
onClose={() => setIsRechargeOpen(false)}
|
|
onRetry={loadRechargeCenter}
|
|
onBuy={buyRechargeProduct}
|
|
onConfirmNativePayment={confirmNativeWechatPayment}
|
|
/>
|
|
) : null}
|
|
{rechargePaymentResult ? (
|
|
<PlatformRechargePaymentResultDialog
|
|
result={rechargePaymentResult}
|
|
onClose={() => setRechargePaymentResult(null)}
|
|
/>
|
|
) : null}
|
|
{wechatRechargeOrderConfirmationState ? (
|
|
<PlatformRechargePaymentConfirmationMask
|
|
orderId={wechatRechargeOrderConfirmationState.orderId}
|
|
/>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default ImageCanvasEditorView;
|