946308b75e
将生成器对话框作为画布布局项序列化和恢复 生成成功后保留生成器快照并锚定到成品图层 图片类生成结果同步写入账号素材库 补充生成器持久化测试和浏览器回归相关文档
886 lines
26 KiB
TypeScript
886 lines
26 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import { createEditorAsset } from '../../services/image-editor/editorProjectClient';
|
|
import { useAuthUi } from '../auth/AuthUiContext';
|
|
import { ImageCanvasEditorShellView } from './ImageCanvasEditorShellView';
|
|
import { resolveContextMenuPosition } from './ImageCanvasEditorModel';
|
|
import { isCanvasGenerationComposerVisible } from './ImageCanvasOverlayModel';
|
|
import type {
|
|
AssetPointerDragState,
|
|
CanvasGenerationDialogState,
|
|
CanvasContextMenuState,
|
|
CanvasLayer,
|
|
CanvasTool,
|
|
CanvasViewport,
|
|
ImageContextMenuState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { useCanvasHistory } from './useCanvasHistory';
|
|
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
|
|
import { useImageCanvasAssetLibrary } from './useImageCanvasAssetLibrary';
|
|
import {
|
|
useImageCanvasAssetCanvasBridge,
|
|
useImageCanvasAssetLayerCleanup,
|
|
} from './useImageCanvasAssetCanvasBridge';
|
|
import { useImageCanvasAssetExportWorkflow } from './useImageCanvasAssetExportWorkflow';
|
|
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';
|
|
|
|
export function ImageCanvasEditorView() {
|
|
const authUi = useAuthUi();
|
|
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 characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const characterReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const generationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const selectedLayerIdRef = useRef<string | null>(null);
|
|
const selectedLayerIdsRef = useRef<string[]>([]);
|
|
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 [imageContextMenu, setImageContextMenu] =
|
|
useState<ImageContextMenuState | null>(null);
|
|
const [contextMenu, setContextMenu] = useState<CanvasContextMenuState | null>(
|
|
null,
|
|
);
|
|
const [uploadDropTarget, setUploadDropTarget] = useState<
|
|
'canvas' | 'assets' | null
|
|
>(null);
|
|
const captureViewportHistory = useCallback(() => {
|
|
captureCanvasHistoryRef.current();
|
|
}, []);
|
|
const {
|
|
viewport,
|
|
setViewport,
|
|
canvasSize,
|
|
minimapModel,
|
|
updateScaleFromCenter,
|
|
fitLayers,
|
|
resolveCanvasPoint,
|
|
getCanvasDropPoint,
|
|
getCanvasPointFromClient,
|
|
moveViewportFromMinimapPointer,
|
|
updateViewportFromMinimapDrag,
|
|
} = useImageCanvasViewportControls({
|
|
canvasViewportRef,
|
|
layers,
|
|
captureCanvasHistory: captureViewportHistory,
|
|
});
|
|
|
|
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);
|
|
},
|
|
[],
|
|
);
|
|
|
|
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 });
|
|
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,
|
|
startRenamingFolder,
|
|
commitFolderRename,
|
|
deleteAssetFolder,
|
|
toggleAssetSelected,
|
|
toggleAllAssetsSelected,
|
|
deleteSelectedAssets,
|
|
moveAssetToFolder,
|
|
closeAssetSelectionMode,
|
|
handleAssetMarqueePointerDown,
|
|
handleAssetMarqueePointerMove,
|
|
handleAssetMarqueePointerUp,
|
|
} = useImageCanvasAssetLibrary({
|
|
assetListRef,
|
|
canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true,
|
|
openEditorLoginModal,
|
|
onDeleteAssets: removeCanvasLayersLinkedToAssets,
|
|
});
|
|
|
|
const handleActivateCanvasGenerationDialog = useCallback(() => {
|
|
setSelectedLayerId(null);
|
|
setSelectedLayerIds([]);
|
|
setImageContextMenu(null);
|
|
}, []);
|
|
const {
|
|
generateDialog,
|
|
setGenerateDialog,
|
|
generateDialogRef,
|
|
inactiveGenerateDialogs,
|
|
setInactiveGenerateDialogs,
|
|
inactiveGenerateDialogsRef,
|
|
activeCanvasGenerationDialog,
|
|
canvasGenerationDialogs,
|
|
openCanvasGenerationDialog,
|
|
updateCanvasGenerationDialogById,
|
|
activateCanvasGenerationDialog,
|
|
restoreCanvasGenerationDialogs,
|
|
removeCanvasGenerationDialogsByLayerId,
|
|
getGeneratingDialogPlaceholder,
|
|
} = useCanvasGenerationDialogs({
|
|
onActivate: handleActivateCanvasGenerationDialog,
|
|
});
|
|
canvasGenerationDialogsRef.current = canvasGenerationDialogs;
|
|
const canvasHistoryRefs = useMemo(
|
|
() => ({
|
|
layersRef,
|
|
viewportRef,
|
|
generateDialogRef,
|
|
inactiveGenerateDialogsRef,
|
|
selectedLayerIdRef,
|
|
selectedLayerIdsRef,
|
|
}),
|
|
[],
|
|
);
|
|
const canvasHistorySetters = useMemo(
|
|
() => ({
|
|
setLayers,
|
|
setViewport,
|
|
setGenerateDialog,
|
|
setInactiveGenerateDialogs,
|
|
setSelectedLayerId,
|
|
setSelectedLayerIds,
|
|
}),
|
|
[],
|
|
);
|
|
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: currentDialog.generatedLayerId === layerId,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
}
|
|
}, []);
|
|
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,
|
|
})
|
|
.then((asset) => {
|
|
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,
|
|
},
|
|
]);
|
|
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 projectPersistenceRefs = useMemo(
|
|
() => ({
|
|
layersRef,
|
|
viewportRef,
|
|
canvasGenerationDialogsRef,
|
|
}),
|
|
[],
|
|
);
|
|
const projectPersistenceSetters = useMemo(
|
|
() => ({
|
|
setProjectTitle,
|
|
setProjectRenameValue,
|
|
setViewport,
|
|
setLayers,
|
|
selectSingleLayer,
|
|
setLayerCounter: (value: number) => {
|
|
layerCounterRef.current = value;
|
|
},
|
|
restoreCanvasGenerationDialogs,
|
|
}),
|
|
[restoreCanvasGenerationDialogs, selectSingleLayer],
|
|
);
|
|
const { projectId, appendCanvasLayersWithResources } =
|
|
useImageCanvasProjectPersistence({
|
|
refs: projectPersistenceRefs,
|
|
setters: projectPersistenceSetters,
|
|
layers,
|
|
canvasGenerationDialogs,
|
|
viewport,
|
|
canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true,
|
|
openEditorLoginModal,
|
|
});
|
|
const {
|
|
assetExportStatus,
|
|
isExportingAssets,
|
|
exportCanvasAssets,
|
|
exportLayerImage,
|
|
} = useImageCanvasAssetExportWorkflow({
|
|
layers,
|
|
projectId,
|
|
projectTitle,
|
|
});
|
|
const {
|
|
uploadInputRef,
|
|
setUploadTarget,
|
|
requestUpload,
|
|
handleUploadInputChange,
|
|
addUploadedFiles,
|
|
} = useImageCanvasUploadWorkflow({
|
|
canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true,
|
|
openEditorLoginModal,
|
|
assetFolders,
|
|
activeUploadFolderId,
|
|
canvasSize,
|
|
viewport,
|
|
activeTool,
|
|
allocateUploadIndex: () => {
|
|
layerCounterRef.current += 1;
|
|
return layerCounterRef.current;
|
|
},
|
|
setAssetFolders,
|
|
setAssets,
|
|
setLayers,
|
|
setGenerateDialog,
|
|
appendCanvasLayersWithResources,
|
|
selectSingleLayer,
|
|
});
|
|
const generationSurface = useImageCanvasGenerationSurface({
|
|
layers,
|
|
canvasSize,
|
|
viewport,
|
|
setViewport,
|
|
layerCounterRef,
|
|
specToolWrapRef,
|
|
characterSpecButtonRef,
|
|
characterReferenceButtonRef,
|
|
generationReferenceButtonRef,
|
|
iconSpecButtonRef,
|
|
generateDialog,
|
|
setGenerateDialog,
|
|
activeCanvasGenerationDialog,
|
|
canvasGenerationDialogs,
|
|
openCanvasGenerationDialog,
|
|
updateCanvasGenerationDialogById,
|
|
removeCanvasGenerationDialogsByLayerId,
|
|
getGeneratingDialogPlaceholder,
|
|
appendCanvasLayersWithResources,
|
|
selectSingleLayer,
|
|
fitLayers,
|
|
setActiveTool,
|
|
setActiveSidebarPanel,
|
|
setMetadataLayer,
|
|
setImageContextMenu,
|
|
requestUpload,
|
|
persistGeneratedAsset,
|
|
});
|
|
const {
|
|
quickEditPanel,
|
|
setQuickEditPanel,
|
|
setIsSpecMenuOpen,
|
|
setIsGenerationReferenceMenuOpen,
|
|
setIsCharacterSpecMenuOpen,
|
|
setIsCharacterReferenceMenuOpen,
|
|
isPickingGenerationReferenceFromCanvas,
|
|
setIsPickingGenerationReferenceFromCanvas,
|
|
isPickingCharacterSpecFromCanvas,
|
|
setIsPickingCharacterSpecFromCanvas,
|
|
isPickingCharacterReferenceFromCanvas,
|
|
setIsPickingCharacterReferenceFromCanvas,
|
|
setIsIconSpecMenuOpen,
|
|
isPickingIconSpecFromCanvas,
|
|
setIsPickingIconSpecFromCanvas,
|
|
setIsUiDesignSpecMenuOpen,
|
|
isPickingUiDesignSpecFromCanvas,
|
|
setIsPickingUiDesignSpecFromCanvas,
|
|
openCharacterAnimationPanel,
|
|
openEditDialog,
|
|
openQuickEditPanel,
|
|
pickCharacterSpecFromLayer,
|
|
pickGenerationReferenceFromLayer,
|
|
pickCharacterReferenceFromLayer,
|
|
pickIconSpecFromLayer,
|
|
pickUiDesignSpecFromLayer,
|
|
hideGeneratedLayerPanelAfterBlur,
|
|
clearDeletedLayerGenerationState,
|
|
generationComposerStyle,
|
|
generationComposerNode,
|
|
switchGenerationTool,
|
|
} = generationSurface;
|
|
const {
|
|
selectedLayer,
|
|
selectedToolbarStyle,
|
|
imageContextMenuLayer,
|
|
contextShouldShowLayer,
|
|
contextShouldUnlockLayer,
|
|
clearCanvasFocus,
|
|
handleCanvasContextMenu,
|
|
handleLayerContextMenu,
|
|
} = useImageCanvasStageController({
|
|
layers,
|
|
selectedLayerId,
|
|
selectedLayerIds,
|
|
imageContextMenu,
|
|
setImageContextMenu,
|
|
contextMenu,
|
|
setContextMenu,
|
|
viewport,
|
|
canvasSize,
|
|
selectSingleLayer,
|
|
hideGeneratedLayerPanelAfterBlur,
|
|
getCanvasPointFromClient,
|
|
});
|
|
const {
|
|
canvasClipboard,
|
|
pasteCanvasClipboard,
|
|
copyContextLayers,
|
|
duplicateContextLayers,
|
|
moveContextLayers,
|
|
groupContextLayers,
|
|
ungroupContextLayers,
|
|
toggleContextLayerVisibility,
|
|
toggleContextLayerLock,
|
|
flipContextLayers,
|
|
deleteContextLayers,
|
|
exportContextLayer,
|
|
deleteLayerById,
|
|
deleteSelectedLayer,
|
|
groupSelectedLayers,
|
|
} = useImageCanvasLayerCommands({
|
|
layers,
|
|
contextMenu,
|
|
selectedLayerId,
|
|
selectedLayerIds,
|
|
setLayers,
|
|
setSelectedLayerId,
|
|
setSelectedLayerIds,
|
|
setHoveredLayerId,
|
|
setMetadataLayer,
|
|
setContextMenu,
|
|
setImageContextMenu,
|
|
setActiveTool,
|
|
captureCanvasHistory,
|
|
selectSingleLayer,
|
|
onDeleteLayerSideEffects: clearDeletedLayerGenerationState,
|
|
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,
|
|
setGenerateDialog,
|
|
isPickingCharacterSpecFromCanvas,
|
|
isPickingGenerationReferenceFromCanvas,
|
|
isPickingCharacterReferenceFromCanvas,
|
|
isPickingIconSpecFromCanvas,
|
|
isPickingUiDesignSpecFromCanvas,
|
|
clearCanvasFocus,
|
|
pickCharacterSpecFromLayer,
|
|
pickGenerationReferenceFromLayer,
|
|
pickCharacterReferenceFromLayer,
|
|
pickIconSpecFromLayer,
|
|
pickUiDesignSpecFromLayer,
|
|
activateCanvasGenerationDialog,
|
|
updateCanvasGenerationDialogById,
|
|
moveViewportFromMinimapPointer,
|
|
updateViewportFromMinimapDrag,
|
|
minimapScale: minimapModel?.scale ?? 1,
|
|
onCloseImageContextMenu: () => setImageContextMenu(null),
|
|
});
|
|
resetCanvasInteractionStateRef.current = clearActiveInteraction;
|
|
const deleteLayerByIdFromShortcut = useCallback(
|
|
(layerId: string | null) => deleteLayerByIdRef.current(layerId),
|
|
[],
|
|
);
|
|
|
|
useImageCanvasKeyboardShortcuts({
|
|
generateDialogRef,
|
|
selectedLayerIdRef,
|
|
redoCanvasChange,
|
|
undoCanvasChange,
|
|
deleteLayerById: deleteLayerByIdFromShortcut,
|
|
setActiveTool,
|
|
setGenerateDialog,
|
|
setImageContextMenu,
|
|
setContextMenu,
|
|
setQuickEditPanel,
|
|
closeEditorChromePanels,
|
|
setIsSpecMenuOpen,
|
|
setIsGenerationReferenceMenuOpen,
|
|
setIsPickingGenerationReferenceFromCanvas,
|
|
setIsCharacterSpecMenuOpen,
|
|
setIsCharacterReferenceMenuOpen,
|
|
setIsPickingCharacterSpecFromCanvas,
|
|
setIsPickingCharacterReferenceFromCanvas,
|
|
setIsIconSpecMenuOpen,
|
|
setIsPickingIconSpecFromCanvas,
|
|
setIsUiDesignSpecMenuOpen,
|
|
setIsPickingUiDesignSpecFromCanvas,
|
|
setIsSpacePanning,
|
|
setShiftPressed,
|
|
});
|
|
|
|
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 switchTool = (tool: CanvasTool) => {
|
|
clearActiveInteraction();
|
|
if (tool === 'upload') {
|
|
requestUpload('asset');
|
|
return;
|
|
}
|
|
if (switchGenerationTool(tool)) {
|
|
return;
|
|
}
|
|
setActiveTool(tool);
|
|
};
|
|
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,
|
|
toggleAssetSelected,
|
|
addAssetLayer,
|
|
toggleAllAssetsSelected,
|
|
deleteSelectedAssets,
|
|
closeAssetSelectionMode,
|
|
groupSelectedLayers,
|
|
selectSingleLayer,
|
|
resolveContextMenuPosition,
|
|
getCanvasPointFromClient,
|
|
};
|
|
const topbarProps = {
|
|
projectId,
|
|
projectTitle,
|
|
projectRenameValue,
|
|
isRenamingProject,
|
|
isProjectRenameSaving,
|
|
projectRenameError,
|
|
layers,
|
|
assetExportStatus,
|
|
isExportingAssets,
|
|
setProjectRenameValue,
|
|
startProjectRename,
|
|
cancelProjectRename,
|
|
submitProjectRename,
|
|
resetProjectRenameError,
|
|
exportCanvasAssets,
|
|
};
|
|
const stageProps = {
|
|
canvasViewportRef,
|
|
specToolWrapRef,
|
|
isPanning,
|
|
effectiveTool,
|
|
canvasBackgroundColor,
|
|
canvasBackgroundHexValue,
|
|
viewport,
|
|
snapGuide,
|
|
layers,
|
|
selectedLayer,
|
|
selectedLayerIds,
|
|
hoveredLayerId,
|
|
canvasMarquee,
|
|
canvasGenerationDialogs,
|
|
generateDialog,
|
|
quickEditPanel,
|
|
generationComposerStyle,
|
|
selectedToolbarStyle,
|
|
uploadDropTarget,
|
|
contextMenu,
|
|
canvasClipboard,
|
|
imageContextMenu,
|
|
imageContextMenuLayer,
|
|
contextShouldShowLayer,
|
|
contextShouldUnlockLayer,
|
|
canUndo,
|
|
canRedo,
|
|
isZoomMenuOpen,
|
|
isBackgroundSettingsOpen,
|
|
activeSidebarPanel,
|
|
isMinimapOpen,
|
|
minimapModel,
|
|
onCanvasPointerDown: handleCanvasPointerDown,
|
|
onCanvasPointerMove: handlePointerMove,
|
|
onCanvasPointerUp: finishDrag,
|
|
onCanvasDragOver: handleCanvasDragOver,
|
|
onCanvasDragLeave: handleCanvasDragLeave,
|
|
onCanvasDrop: handleCanvasDrop,
|
|
onCanvasContextMenu: handleCanvasContextMenu,
|
|
onLayerPointerDown: handleLayerPointerDown,
|
|
onLayerClick: handleLayerClick,
|
|
onLayerContextMenu: handleLayerContextMenu,
|
|
onLayerMouseEnter: setHoveredLayerId,
|
|
onLayerMouseLeave: (layerId: string) =>
|
|
setHoveredLayerId((currentId) =>
|
|
currentId === layerId ? null : currentId,
|
|
),
|
|
onOpenLayerMetadata: (layer: CanvasLayer) => {
|
|
setMetadataLayer(layer);
|
|
selectSingleLayer(layer.id);
|
|
},
|
|
onGenerationFramePointerDown: handleGenerationFramePointerDown,
|
|
onActivateGenerationDialog: activateCanvasGenerationDialog,
|
|
onDeleteSelectedLayer: deleteSelectedLayer,
|
|
onOpenQuickEditPanel: openQuickEditPanel,
|
|
onOpenEditDialog: openEditDialog,
|
|
onOpenCharacterAnimationPanel: openCharacterAnimationPanel,
|
|
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,
|
|
children: generationComposerNode,
|
|
};
|
|
|
|
return (
|
|
<ImageCanvasEditorShellView
|
|
editorRootRef={editorRootRef}
|
|
uploadInputRef={uploadInputRef}
|
|
onUploadInputChange={handleUploadInputChange}
|
|
assetDragPreview={assetDragPreview}
|
|
sidebarProps={sidebarProps}
|
|
topbarProps={topbarProps}
|
|
stageProps={stageProps}
|
|
metadataProps={{
|
|
layer: metadataLayer,
|
|
onClose: () => setMetadataLayer(null),
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default ImageCanvasEditorView;
|