Files
Genarrative/src/components/image-editor/useImageCanvasLayerCommands.ts
kdletters f38493a07e 拆分图片画布图层命令工作流
新增图层命令 hook 和独立单测

主视图改为通过 hook 处理复制、剪切、粘贴、层级、分组、显隐、锁定、翻转、删除和导出委托

更新图片画布前端拆分文档和 TRACKING 回归记录
2026-06-17 07:38:37 +08:00

423 lines
12 KiB
TypeScript

import {
useCallback,
useState,
type Dispatch,
type SetStateAction,
} from 'react';
import type {
CanvasClipboard,
CanvasContextMenuState,
CanvasLayer,
} from './ImageCanvasEditorTypes';
import {
createCanvasLayerClipboard,
duplicateCanvasLayers,
flipCanvasLayers,
getCanvasLayersByIds,
groupCanvasLayers,
moveCanvasLayers,
removeCanvasLayers,
resolveContextTargetLayerIds,
toggleCanvasLayersLock,
toggleCanvasLayersVisibility,
ungroupCanvasLayers,
updateCanvasLayersByIds,
type CanvasLayerFlipAxis,
type CanvasLayerMoveMode,
} from './ImageCanvasLayerCommandModel';
type LayerCommandsOptions = {
layers: CanvasLayer[];
contextMenu: CanvasContextMenuState | null;
selectedLayerId: string | null;
selectedLayerIds: string[];
setLayers: Dispatch<SetStateAction<CanvasLayer[]>>;
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: () => void;
selectSingleLayer: (layerId: string | null) => void;
onDeleteLayerSideEffects: (targetLayerId: string) => void;
exportLayerImage: (layer: CanvasLayer | null) => void;
};
function createGroupId() {
return `layer-group-${Date.now()}`;
}
export function useImageCanvasLayerCommands({
layers,
contextMenu,
selectedLayerId,
selectedLayerIds,
setLayers,
setSelectedLayerId,
setSelectedLayerIds,
setHoveredLayerId,
setMetadataLayer,
setContextMenu,
setImageContextMenu,
setActiveTool,
captureCanvasHistory,
selectSingleLayer,
onDeleteLayerSideEffects,
exportLayerImage,
}: LayerCommandsOptions) {
const [canvasClipboard, setCanvasClipboard] =
useState<CanvasClipboard | null>(null);
const getContextTargetLayerIds = useCallback(
(menu: CanvasContextMenuState | null = contextMenu) =>
resolveContextTargetLayerIds(menu, selectedLayerIds),
[contextMenu, selectedLayerIds],
);
const duplicateLayersToPoint = useCallback(
(
sourceLayers: CanvasLayer[],
canvasPoint?: { x: number; y: number },
options: { renameCopies?: boolean } = {},
) =>
duplicateCanvasLayers({
sourceLayers,
allLayers: layers,
canvasPoint,
renameCopies: options.renameCopies !== false,
}),
[layers],
);
const pasteCanvasClipboard = useCallback(
(canvasPoint?: { x: number; y: number }) => {
if (!canvasClipboard?.layers.length) {
return;
}
const nextLayers = duplicateLayersToPoint(
canvasClipboard.layers,
canvasPoint,
{
renameCopies: canvasClipboard.mode !== 'cut',
},
);
if (!nextLayers.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
setSelectedLayerIds(nextLayers.map((layer) => layer.id));
setSelectedLayerId(nextLayers[0]?.id ?? null);
setActiveTool('select');
setContextMenu(null);
},
[
canvasClipboard,
captureCanvasHistory,
duplicateLayersToPoint,
setActiveTool,
setContextMenu,
setLayers,
setSelectedLayerId,
setSelectedLayerIds,
],
);
const copyContextLayers = useCallback(
(options: { cut?: boolean } = {}) => {
const targetIds = getContextTargetLayerIds();
const clipboard = createCanvasLayerClipboard(
layers,
targetIds,
options.cut ? 'cut' : 'copy',
);
if (!clipboard) {
return;
}
setCanvasClipboard(clipboard);
if (options.cut) {
captureCanvasHistory();
setLayers((currentLayers) =>
removeCanvasLayers(currentLayers, targetIds),
);
selectSingleLayer(null);
setMetadataLayer((currentLayer) =>
currentLayer && targetIds.includes(currentLayer.id)
? null
: currentLayer,
);
}
setContextMenu(null);
},
[
captureCanvasHistory,
getContextTargetLayerIds,
layers,
selectSingleLayer,
setContextMenu,
setLayers,
setMetadataLayer,
],
);
const duplicateContextLayers = useCallback(() => {
const targetIds = getContextTargetLayerIds();
const targetLayers = getCanvasLayersByIds(layers, targetIds);
const nextLayers = duplicateLayersToPoint(targetLayers);
if (!nextLayers.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
setSelectedLayerIds(nextLayers.map((layer) => layer.id));
setSelectedLayerId(nextLayers[0]?.id ?? null);
setContextMenu(null);
}, [
captureCanvasHistory,
duplicateLayersToPoint,
getContextTargetLayerIds,
layers,
setContextMenu,
setLayers,
setSelectedLayerId,
setSelectedLayerIds,
]);
const updateContextLayers = useCallback(
(updater: (layer: CanvasLayer, targetIds: string[]) => CanvasLayer) => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) =>
updateCanvasLayersByIds(currentLayers, targetIds, updater),
);
setContextMenu(null);
},
[captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers],
);
const moveContextLayers = useCallback(
(mode: CanvasLayerMoveMode) => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) =>
moveCanvasLayers(currentLayers, targetIds, mode),
);
setContextMenu(null);
},
[captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers],
);
const groupContextLayers = useCallback(() => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) =>
groupCanvasLayers(currentLayers, targetIds, createGroupId()),
);
setContextMenu(null);
}, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]);
const ungroupContextLayers = useCallback(() => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) => ungroupCanvasLayers(currentLayers, targetIds));
setContextMenu(null);
}, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]);
const toggleContextLayerVisibility = useCallback(() => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) =>
toggleCanvasLayersVisibility(currentLayers, targetIds),
);
setContextMenu(null);
}, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]);
const toggleContextLayerLock = useCallback(() => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) =>
toggleCanvasLayersLock(currentLayers, targetIds),
);
setContextMenu(null);
}, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]);
const flipContextLayers = useCallback(
(axis: CanvasLayerFlipAxis) => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) =>
flipCanvasLayers(currentLayers, targetIds, axis),
);
setContextMenu(null);
},
[captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers],
);
const deleteContextLayers = useCallback(() => {
const targetIds = getContextTargetLayerIds();
if (!targetIds.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) => removeCanvasLayers(currentLayers, targetIds));
selectSingleLayer(null);
setHoveredLayerId(null);
setMetadataLayer((currentLayer) =>
currentLayer && targetIds.includes(currentLayer.id) ? null : currentLayer,
);
setContextMenu(null);
}, [
captureCanvasHistory,
getContextTargetLayerIds,
selectSingleLayer,
setContextMenu,
setHoveredLayerId,
setLayers,
setMetadataLayer,
]);
const exportContextLayer = useCallback(() => {
const targetIds = getContextTargetLayerIds();
const targetLayer = layers.find((layer) => targetIds.includes(layer.id));
exportLayerImage(targetLayer ?? null);
setContextMenu(null);
}, [exportLayerImage, getContextTargetLayerIds, layers, setContextMenu]);
const deleteLayerById = useCallback(
(targetLayerId: string | null) => {
if (!targetLayerId) {
return;
}
setImageContextMenu(null);
setContextMenu(null);
captureCanvasHistory();
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,
onDeleteLayerSideEffects,
selectSingleLayer,
setContextMenu,
setHoveredLayerId,
setImageContextMenu,
setLayers,
setMetadataLayer,
],
);
const deleteSelectedLayer = useCallback(() => {
const targetIds = selectedLayerIds.length
? selectedLayerIds
: selectedLayerId
? [selectedLayerId]
: [];
if (targetIds.length <= 1) {
deleteLayerById(targetIds[0] ?? null);
return;
}
captureCanvasHistory();
setImageContextMenu(null);
setContextMenu(null);
setLayers((currentLayers) => {
const nextLayers = currentLayers.filter(
(layer) => !targetIds.includes(layer.id),
);
const nextSelectedLayer = nextLayers
.slice()
.sort((left, right) => right.zIndex - left.zIndex)[0];
selectSingleLayer(nextSelectedLayer?.id ?? null);
return nextLayers;
});
setHoveredLayerId(null);
setMetadataLayer((currentLayer) =>
currentLayer && targetIds.includes(currentLayer.id) ? null : currentLayer,
);
targetIds.forEach((targetId) => onDeleteLayerSideEffects(targetId));
}, [
captureCanvasHistory,
deleteLayerById,
onDeleteLayerSideEffects,
selectSingleLayer,
selectedLayerId,
selectedLayerIds,
setContextMenu,
setHoveredLayerId,
setImageContextMenu,
setLayers,
setMetadataLayer,
]);
const groupSelectedLayers = useCallback(() => {
const targetIds = selectedLayerIds.length
? selectedLayerIds
: selectedLayerId
? [selectedLayerId]
: [];
if (!targetIds.length) {
return;
}
captureCanvasHistory();
setLayers((currentLayers) =>
groupCanvasLayers(currentLayers, targetIds, createGroupId()),
);
}, [captureCanvasHistory, selectedLayerId, selectedLayerIds, setLayers]);
return {
canvasClipboard,
getContextTargetLayerIds,
pasteCanvasClipboard,
copyContextLayers,
duplicateContextLayers,
updateContextLayers,
moveContextLayers,
groupContextLayers,
ungroupContextLayers,
toggleContextLayerVisibility,
toggleContextLayerLock,
flipContextLayers,
deleteContextLayers,
exportContextLayer,
deleteLayerById,
deleteSelectedLayer,
groupSelectedLayers,
};
}