Files
Genarrative/src/components/image-editor/useImageCanvasLayerCommands.ts
T
kdletters 6f181b36a1
Project CI / Frontend tests (push) Failing after 22s
Project CI / Repository checks (push) Successful in 58s
Project CI / Backend tests (push) Successful in 3m30s
Project CI / Native shell tests (push) Successful in 12m34s
修复编辑器生成幂等与参考图契约
生成队列按稳定请求标识去重并保持外部幂等哈希兼容
统一拒绝参考图超限并同步前端、后端、Provider 与 OpenAPI 契约
按真实归属重建生成引用并阻止直接持久化伪造来源
锁定参考图在途上传上下文并保留批量部分成功结果
关闭内部生成 POST 自动重试并补齐回归测试与项目文档
修正最新主线开发者密钥弹窗的导入排序门禁
2026-08-05 23:31:16 +08:00

844 lines
24 KiB
TypeScript

import {
type Dispatch,
type SetStateAction,
useCallback,
useRef,
useState,
} from 'react';
import type {
CanvasClipboard,
CanvasContextMenuState,
CanvasGenerationDialogState,
CanvasHistoryAction,
CanvasLayer,
} from './ImageCanvasEditorTypes';
import { readLayerImageBlob } from './ImageCanvasExportModel';
import {
canCopyCanvasLayers,
type CanvasLayerCopyBlockReason,
type CanvasLayerFlipAxis,
type CanvasLayerMoveMode,
createCanvasLayerClipboard,
duplicateCanvasLayers,
flipCanvasLayers,
getCanvasLayerCopyBlockReason,
getCanvasLayersByIds,
groupCanvasLayers,
moveCanvasLayers,
removeCanvasLayers,
resolveContextTargetLayerIds,
toggleCanvasLayersLock,
toggleCanvasLayersVisibility,
ungroupCanvasLayers,
updateCanvasLayersByIds,
} from './ImageCanvasLayerCommandModel';
import {
getCanvasGenerationDialogIdFromSelectionId,
getSelectedLayerIds,
} from './ImageCanvasSelectionModel';
import type { ImageSequenceExportMode } from './useImageCanvasAssetExportWorkflow';
type LayerCommandsOptions = {
layers: CanvasLayer[];
contextMenu: CanvasContextMenuState | null;
selectedLayerId: string | null;
selectedLayerIds: string[];
canvasGenerationDialogs?: CanvasGenerationDialogState[];
setLayers: Dispatch<SetStateAction<CanvasLayer[]>>;
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
setSelectedLayerId: Dispatch<SetStateAction<string | null>>;
setSelectedLayerIds: Dispatch<SetStateAction<string[]>>;
setHoveredLayerId: Dispatch<SetStateAction<string | null>>;
setMetadataLayer: Dispatch<SetStateAction<CanvasLayer | null>>;
setContextMenu: Dispatch<SetStateAction<CanvasContextMenuState | null>>;
setImageContextMenu: (menu: null) => void;
setActiveTool: (tool: 'select') => void;
captureCanvasHistory: (action: CanvasHistoryAction) => void;
selectSingleLayer: (layerId: string | null) => void;
onDeleteLayerSideEffects: (targetLayerId: string) => void;
onDeleteGenerationDialogSideEffects?: (targetDialogId: string) => void;
onRequestDeleteGenerationDialog?: (
targetDialog: CanvasGenerationDialogState,
) => void;
exportLayerImage: (
layer: CanvasLayer | null,
options?: { mode?: ImageSequenceExportMode },
) => void;
onCanvasLayerCopyBlocked?: (reason: CanvasLayerCopyBlockReason) => void;
canDeleteLayers?: (targetLayerIds: string[]) => boolean;
};
function createGroupId() {
return `layer-group-${Date.now()}`;
}
function isClipboardImageLayer(layer: CanvasLayer) {
return (
Boolean(layer.src.trim()) &&
layer.mediaType !== 'video' &&
layer.mediaType !== 'audio' &&
layer.mediaType !== 'image-sequence'
);
}
async function normalizeClipboardImageBlob(blob: Blob) {
if (blob.type === 'image/png') {
return blob;
}
if (typeof createImageBitmap === 'function') {
try {
const image = await createImageBitmap(blob);
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext('2d')?.drawImage(image, 0, 0);
const pngBlob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, 'image/png'),
);
if (pngBlob) {
return pngBlob;
}
} catch {
// 浏览器不支持当前图片解码时,回退到原始字节。
}
}
if (!blob.type || blob.type === 'application/octet-stream') {
return blob.slice(0, blob.size, 'image/png');
}
return blob;
}
async function readLayerClipboardPngBlob(layer: CanvasLayer) {
const blob = await normalizeClipboardImageBlob(
await readLayerImageBlob(layer),
);
return blob.type === 'image/png'
? blob
: blob.slice(0, blob.size, 'image/png');
}
function copyLayerImageToSystemClipboard(layers: CanvasLayer[]) {
const layer = layers.slice().reverse().find(isClipboardImageLayer);
const clipboard = globalThis.navigator?.clipboard;
const ClipboardItemCtor = globalThis.ClipboardItem;
if (!layer || !clipboard?.write || typeof ClipboardItemCtor !== 'function') {
return;
}
void clipboard
.write([
new ClipboardItemCtor({
'image/png': readLayerClipboardPngBlob(layer),
}),
])
.catch(() => {
// 系统剪贴板只是画布内复制的附加能力,失败不阻断内部粘贴。
});
}
export function useImageCanvasLayerCommands({
layers,
contextMenu,
selectedLayerId,
selectedLayerIds,
canvasGenerationDialogs = [],
setLayers,
appendCanvasLayersWithResources,
setSelectedLayerId,
setSelectedLayerIds,
setHoveredLayerId,
setMetadataLayer,
setContextMenu,
setImageContextMenu,
setActiveTool,
captureCanvasHistory,
selectSingleLayer,
onDeleteLayerSideEffects,
onDeleteGenerationDialogSideEffects,
onRequestDeleteGenerationDialog,
exportLayerImage,
onCanvasLayerCopyBlocked,
canDeleteLayers,
}: LayerCommandsOptions) {
const [canvasClipboard, setCanvasClipboard] =
useState<CanvasClipboard | null>(null);
const canvasClipboardRef = useRef<CanvasClipboard | null>(null);
const duplicateLayerSequenceRef = useRef(0);
const closeContextMenus = useCallback(() => {
setContextMenu(null);
setImageContextMenu(null);
}, [setContextMenu, setImageContextMenu]);
const getContextTargetLayerIds = useCallback(
(menu: CanvasContextMenuState | null = contextMenu) =>
resolveContextTargetLayerIds(menu, selectedLayerIds),
[contextMenu, selectedLayerIds],
);
const getSelectedTargetLayerIds = useCallback(() => {
const targetIds = selectedLayerIds.length
? selectedLayerIds
: selectedLayerId
? [selectedLayerId]
: [];
return getSelectedLayerIds(targetIds);
}, [selectedLayerId, selectedLayerIds]);
const duplicateLayersToPoint = useCallback(
(
sourceLayers: CanvasLayer[],
canvasPoint?: { x: number; y: number },
options: { renameCopies?: boolean } = {},
) => {
duplicateLayerSequenceRef.current += 1;
return duplicateCanvasLayers({
sourceLayers,
allLayers: layers,
canvasPoint,
renameCopies: options.renameCopies !== false,
stamp: `${Date.now()}-${duplicateLayerSequenceRef.current}`,
});
},
[layers],
);
const rejectPendingResourceCopy = useCallback(
(targetLayers: CanvasLayer[]) => {
const reason = getCanvasLayerCopyBlockReason(targetLayers);
if (!reason) {
return false;
}
if (targetLayers.length) {
onCanvasLayerCopyBlocked?.(reason);
}
return true;
},
[onCanvasLayerCopyBlocked],
);
const pasteCanvasClipboard = useCallback(
(canvasPoint?: { x: number; y: number }) => {
const clipboard = canvasClipboardRef.current;
if (!clipboard?.layers.length) {
return false;
}
if (rejectPendingResourceCopy(clipboard.layers)) {
return true;
}
const nextLayers = duplicateLayersToPoint(clipboard.layers, canvasPoint, {
renameCopies: clipboard.mode !== 'cut',
});
if (!nextLayers.length) {
return false;
}
captureCanvasHistory({ type: 'paste-image', count: nextLayers.length });
appendCanvasLayersWithResources(nextLayers);
setSelectedLayerIds(nextLayers.map((layer) => layer.id));
setSelectedLayerId(nextLayers[0]?.id ?? null);
setActiveTool('select');
closeContextMenus();
return true;
},
[
captureCanvasHistory,
closeContextMenus,
duplicateLayersToPoint,
appendCanvasLayersWithResources,
rejectPendingResourceCopy,
setActiveTool,
setSelectedLayerId,
setSelectedLayerIds,
],
);
const copyContextLayers = useCallback(
(options: { cut?: boolean } = {}) => {
const targetIds = getContextTargetLayerIds();
const targetLayers = getCanvasLayersByIds(layers, targetIds);
if (rejectPendingResourceCopy(targetLayers)) {
return;
}
if (options.cut && canDeleteLayers?.(targetIds) === false) {
closeContextMenus();
return;
}
const clipboard = createCanvasLayerClipboard(
layers,
targetIds,
options.cut ? 'cut' : 'copy',
);
if (!clipboard) {
return;
}
canvasClipboardRef.current = clipboard;
setCanvasClipboard(clipboard);
if (!options.cut) {
copyLayerImageToSystemClipboard(clipboard.layers);
}
if (options.cut) {
captureCanvasHistory({ type: 'cut-image', count: targetIds.length });
setLayers((currentLayers) =>
removeCanvasLayers(currentLayers, targetIds),
);
selectSingleLayer(null);
setMetadataLayer((currentLayer) =>
currentLayer && targetIds.includes(currentLayer.id)
? null
: currentLayer,
);
targetIds.forEach((targetId) => onDeleteLayerSideEffects(targetId));
}
closeContextMenus();
},
[
captureCanvasHistory,
canDeleteLayers,
closeContextMenus,
getContextTargetLayerIds,
layers,
onDeleteLayerSideEffects,
rejectPendingResourceCopy,
selectSingleLayer,
setLayers,
setMetadataLayer,
],
);
const copySelectedLayers = useCallback(
(options: { cut?: boolean } = {}) => {
const targetIds = getSelectedTargetLayerIds();
const targetLayers = getCanvasLayersByIds(layers, targetIds);
if (rejectPendingResourceCopy(targetLayers)) {
return;
}
if (options.cut && canDeleteLayers?.(targetIds) === false) {
setContextMenu(null);
setImageContextMenu(null);
return;
}
const clipboard = createCanvasLayerClipboard(
layers,
targetIds,
options.cut ? 'cut' : 'copy',
);
if (!clipboard) {
return;
}
canvasClipboardRef.current = clipboard;
setCanvasClipboard(clipboard);
if (!options.cut) {
copyLayerImageToSystemClipboard(clipboard.layers);
}
if (options.cut) {
captureCanvasHistory({ type: 'cut-image', count: targetIds.length });
setLayers((currentLayers) =>
removeCanvasLayers(currentLayers, targetIds),
);
selectSingleLayer(null);
setMetadataLayer((currentLayer) =>
currentLayer && targetIds.includes(currentLayer.id)
? null
: currentLayer,
);
targetIds.forEach((targetId) => onDeleteLayerSideEffects(targetId));
}
setContextMenu(null);
setImageContextMenu(null);
},
[
captureCanvasHistory,
canDeleteLayers,
getSelectedTargetLayerIds,
layers,
onDeleteLayerSideEffects,
rejectPendingResourceCopy,
selectSingleLayer,
setContextMenu,
setImageContextMenu,
setLayers,
setMetadataLayer,
],
);
const duplicateContextLayers = useCallback(() => {
const targetIds = getContextTargetLayerIds();
const targetLayers = getCanvasLayersByIds(layers, targetIds);
if (rejectPendingResourceCopy(targetLayers)) {
return;
}
const nextLayers = duplicateLayersToPoint(targetLayers);
if (!nextLayers.length) {
return;
}
captureCanvasHistory({ type: 'duplicate-image', count: nextLayers.length });
appendCanvasLayersWithResources(nextLayers);
setSelectedLayerIds(nextLayers.map((layer) => layer.id));
setSelectedLayerId(nextLayers[0]?.id ?? null);
closeContextMenus();
}, [
captureCanvasHistory,
closeContextMenus,
duplicateLayersToPoint,
getContextTargetLayerIds,
layers,
appendCanvasLayersWithResources,
rejectPendingResourceCopy,
setSelectedLayerId,
setSelectedLayerIds,
]);
const duplicateSelectedLayers = useCallback(() => {
const targetIds = getSelectedTargetLayerIds();
const targetLayers = getCanvasLayersByIds(layers, targetIds);
if (rejectPendingResourceCopy(targetLayers)) {
return;
}
const nextLayers = duplicateLayersToPoint(targetLayers);
if (!nextLayers.length) {
return;
}
captureCanvasHistory({ type: 'duplicate-image', count: nextLayers.length });
appendCanvasLayersWithResources(nextLayers);
setSelectedLayerIds(nextLayers.map((layer) => layer.id));
setSelectedLayerId(nextLayers[0]?.id ?? null);
setContextMenu(null);
setImageContextMenu(null);
}, [
captureCanvasHistory,
duplicateLayersToPoint,
getSelectedTargetLayerIds,
layers,
appendCanvasLayersWithResources,
rejectPendingResourceCopy,
setContextMenu,
setImageContextMenu,
setSelectedLayerId,
setSelectedLayerIds,
]);
const updateContextLayers = useCallback(
(updater: (layer: CanvasLayer, targetIds: string[]) => CanvasLayer) => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory({
type: 'change-asset-kind',
count: targetIds.length,
layerIds: targetIds,
});
setLayers((currentLayers) =>
updateCanvasLayersByIds(currentLayers, targetIds, updater),
);
closeContextMenus();
},
[
captureCanvasHistory,
closeContextMenus,
getContextTargetLayerIds,
setLayers,
],
);
const moveContextLayers = useCallback(
(mode: CanvasLayerMoveMode) => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory({
type: 'change-layer-order',
count: targetIds.length,
});
setLayers((currentLayers) =>
moveCanvasLayers(currentLayers, targetIds, mode),
);
closeContextMenus();
},
[
captureCanvasHistory,
closeContextMenus,
getContextTargetLayerIds,
setLayers,
],
);
const moveSelectedLayers = useCallback(
(mode: CanvasLayerMoveMode) => {
const targetIds = getSelectedTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory({
type: 'change-layer-order',
count: targetIds.length,
});
setLayers((currentLayers) =>
moveCanvasLayers(currentLayers, targetIds, mode),
);
setContextMenu(null);
setImageContextMenu(null);
},
[
captureCanvasHistory,
getSelectedTargetLayerIds,
setContextMenu,
setImageContextMenu,
setLayers,
],
);
const groupContextLayers = useCallback(() => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory({ type: 'group-images', count: targetIds.length });
setLayers((currentLayers) =>
groupCanvasLayers(currentLayers, targetIds, createGroupId()),
);
closeContextMenus();
}, [
captureCanvasHistory,
closeContextMenus,
getContextTargetLayerIds,
setLayers,
]);
const ungroupContextLayers = useCallback(() => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory({ type: 'ungroup-images', count: targetIds.length });
setLayers((currentLayers) => ungroupCanvasLayers(currentLayers, targetIds));
closeContextMenus();
}, [
captureCanvasHistory,
closeContextMenus,
getContextTargetLayerIds,
setLayers,
]);
const toggleContextLayerVisibility = useCallback(() => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
const shouldHide = getCanvasLayersByIds(layers, targetIds).some(
(layer) => !layer.hidden,
);
captureCanvasHistory({
type: shouldHide ? 'hide-image' : 'show-image',
count: targetIds.length,
});
setLayers((currentLayers) =>
toggleCanvasLayersVisibility(currentLayers, targetIds),
);
closeContextMenus();
}, [
captureCanvasHistory,
closeContextMenus,
getContextTargetLayerIds,
layers,
setLayers,
]);
const toggleContextLayerLock = useCallback(() => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
const shouldLock = getCanvasLayersByIds(layers, targetIds).some(
(layer) => !layer.locked,
);
captureCanvasHistory({
type: shouldLock ? 'lock-image' : 'unlock-image',
count: targetIds.length,
});
setLayers((currentLayers) =>
toggleCanvasLayersLock(currentLayers, targetIds),
);
closeContextMenus();
}, [
captureCanvasHistory,
closeContextMenus,
getContextTargetLayerIds,
layers,
setLayers,
]);
const flipContextLayers = useCallback(
(axis: CanvasLayerFlipAxis) => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory({ type: 'flip-image', count: targetIds.length });
setLayers((currentLayers) =>
flipCanvasLayers(currentLayers, targetIds, axis),
);
closeContextMenus();
},
[
captureCanvasHistory,
closeContextMenus,
getContextTargetLayerIds,
setLayers,
],
);
const deleteContextLayers = useCallback(() => {
const targetIds = getContextTargetLayerIds();
const targetLayerIds = getSelectedLayerIds(targetIds);
const targetDialogs = targetIds
.map(getCanvasGenerationDialogIdFromSelectionId)
.filter((dialogId): dialogId is string => Boolean(dialogId))
.flatMap((dialogId) => {
const dialog = canvasGenerationDialogs.find(
(candidate) => candidate.id === dialogId,
);
return dialog ? [dialog] : [];
});
if (
!targetLayerIds.length &&
targetDialogs.length &&
onRequestDeleteGenerationDialog
) {
closeContextMenus();
targetDialogs.forEach((dialog) => {
onRequestDeleteGenerationDialog(dialog);
});
return;
}
const targetDialogIds = targetDialogs.map((dialog) => dialog.id);
if (!targetLayerIds.length && !targetDialogIds.length) {
return;
}
if (targetLayerIds.length && canDeleteLayers?.(targetLayerIds) === false) {
closeContextMenus();
return;
}
captureCanvasHistory({
type: targetLayerIds.length ? 'delete-image' : 'delete-generation-result',
count: targetLayerIds.length || targetDialogIds.length,
});
if (targetLayerIds.length) {
setLayers((currentLayers) =>
removeCanvasLayers(currentLayers, targetLayerIds),
);
}
selectSingleLayer(null);
setHoveredLayerId(null);
setMetadataLayer((currentLayer) =>
currentLayer && targetLayerIds.includes(currentLayer.id)
? null
: currentLayer,
);
targetLayerIds.forEach((targetId) => onDeleteLayerSideEffects(targetId));
targetDialogIds.forEach((targetId) =>
onDeleteGenerationDialogSideEffects?.(targetId),
);
closeContextMenus();
}, [
captureCanvasHistory,
canDeleteLayers,
canvasGenerationDialogs,
closeContextMenus,
getContextTargetLayerIds,
onDeleteGenerationDialogSideEffects,
onDeleteLayerSideEffects,
onRequestDeleteGenerationDialog,
selectSingleLayer,
setHoveredLayerId,
setLayers,
setMetadataLayer,
]);
const exportContextLayer = useCallback(
(options: { mode?: ImageSequenceExportMode } = {}) => {
const targetIds = getContextTargetLayerIds();
const targetLayer = layers.find((layer) => targetIds.includes(layer.id));
exportLayerImage(targetLayer ?? null, options);
setContextMenu(null);
setImageContextMenu(null);
},
[
exportLayerImage,
getContextTargetLayerIds,
layers,
setContextMenu,
setImageContextMenu,
],
);
const deleteLayerById = useCallback(
(targetLayerId: string | null) => {
if (!targetLayerId) {
return;
}
if (canDeleteLayers?.([targetLayerId]) === false) {
setImageContextMenu(null);
setContextMenu(null);
return;
}
setImageContextMenu(null);
setContextMenu(null);
captureCanvasHistory({ type: 'delete-image', count: 1 });
setLayers((currentLayers) => {
const nextLayers = currentLayers.filter(
(layer) => layer.id !== targetLayerId,
);
const nextSelectedLayer = nextLayers
.slice()
.sort((left, right) => right.zIndex - left.zIndex)[0];
selectSingleLayer(nextSelectedLayer?.id ?? null);
return nextLayers;
});
setHoveredLayerId(null);
setMetadataLayer((currentLayer) =>
currentLayer?.id === targetLayerId ? null : currentLayer,
);
onDeleteLayerSideEffects(targetLayerId);
},
[
captureCanvasHistory,
canDeleteLayers,
onDeleteLayerSideEffects,
selectSingleLayer,
setContextMenu,
setHoveredLayerId,
setImageContextMenu,
setLayers,
setMetadataLayer,
],
);
const deleteSelectedLayer = useCallback(() => {
const targetIds = selectedLayerIds.length
? selectedLayerIds
: selectedLayerId
? [selectedLayerId]
: [];
const targetLayerIds = getSelectedLayerIds(targetIds);
const targetDialogIds = targetIds
.map(getCanvasGenerationDialogIdFromSelectionId)
.filter((dialogId): dialogId is string => Boolean(dialogId))
.filter((dialogId) =>
canvasGenerationDialogs.some((dialog) => dialog.id === dialogId),
);
if (!targetLayerIds.length && !targetDialogIds.length) {
return;
}
if (targetLayerIds.length && canDeleteLayers?.(targetLayerIds) === false) {
setImageContextMenu(null);
setContextMenu(null);
return;
}
if (targetLayerIds.length === 1 && !targetDialogIds.length) {
deleteLayerById(targetLayerIds[0] ?? null);
return;
}
captureCanvasHistory({
type: targetLayerIds.length ? 'delete-image' : 'delete-generation-result',
count: targetLayerIds.length || targetDialogIds.length,
});
setImageContextMenu(null);
setContextMenu(null);
if (targetLayerIds.length) {
setLayers((currentLayers) => {
const nextLayers = currentLayers.filter(
(layer) => !targetLayerIds.includes(layer.id),
);
if (!targetDialogIds.length) {
const nextSelectedLayer = nextLayers
.slice()
.sort((left, right) => right.zIndex - left.zIndex)[0];
selectSingleLayer(nextSelectedLayer?.id ?? null);
}
return nextLayers;
});
}
if (targetDialogIds.length || !targetLayerIds.length) {
selectSingleLayer(null);
}
setHoveredLayerId(null);
setMetadataLayer((currentLayer) =>
currentLayer && targetLayerIds.includes(currentLayer.id)
? null
: currentLayer,
);
targetLayerIds.forEach((targetId) => onDeleteLayerSideEffects(targetId));
targetDialogIds.forEach((targetId) =>
onDeleteGenerationDialogSideEffects?.(targetId),
);
}, [
captureCanvasHistory,
canDeleteLayers,
canvasGenerationDialogs,
deleteLayerById,
onDeleteGenerationDialogSideEffects,
onDeleteLayerSideEffects,
selectSingleLayer,
selectedLayerId,
selectedLayerIds,
setContextMenu,
setHoveredLayerId,
setImageContextMenu,
setLayers,
setMetadataLayer,
]);
const groupSelectedLayers = useCallback(() => {
const targetIds = selectedLayerIds.length
? selectedLayerIds
: selectedLayerId
? [selectedLayerId]
: [];
const targetLayerIds = getSelectedLayerIds(targetIds);
if (!targetLayerIds.length) {
return;
}
captureCanvasHistory({
type: 'group-images',
count: targetLayerIds.length,
});
setLayers((currentLayers) =>
groupCanvasLayers(currentLayers, targetLayerIds, createGroupId()),
);
}, [captureCanvasHistory, selectedLayerId, selectedLayerIds, setLayers]);
const contextTargetLayers = getCanvasLayersByIds(
layers,
getContextTargetLayerIds(),
);
return {
canvasClipboard,
canCopyContextLayers: canCopyCanvasLayers(contextTargetLayers),
contextLayerCopyBlockReason:
getCanvasLayerCopyBlockReason(contextTargetLayers),
isCanvasClipboardCopyBlocked: Boolean(
canvasClipboard && !canCopyCanvasLayers(canvasClipboard.layers),
),
getContextTargetLayerIds,
pasteCanvasClipboard,
copyContextLayers,
copySelectedLayers,
duplicateContextLayers,
duplicateSelectedLayers,
updateContextLayers,
moveContextLayers,
moveSelectedLayers,
groupContextLayers,
ungroupContextLayers,
toggleContextLayerVisibility,
toggleContextLayerLock,
flipContextLayers,
deleteContextLayers,
exportContextLayer,
deleteLayerById,
deleteSelectedLayer,
groupSelectedLayers,
};
}