6f181b36a1
生成队列按稳定请求标识去重并保持外部幂等哈希兼容 统一拒绝参考图超限并同步前端、后端、Provider 与 OpenAPI 契约 按真实归属重建生成引用并阻止直接持久化伪造来源 锁定参考图在途上传上下文并保留批量部分成功结果 关闭内部生成 POST 自动重试并补齐回归测试与项目文档 修正最新主线开发者密钥弹窗的导入排序门禁
844 lines
24 KiB
TypeScript
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,
|
|
};
|
|
}
|