拆分图片画布图层命令工作流
新增图层命令 hook 和独立单测 主视图改为通过 hook 处理复制、剪切、粘贴、层级、分组、显隐、锁定、翻转、删除和导出委托 更新图片画布前端拆分文档和 TRACKING 回归记录
This commit is contained in:
422
src/components/image-editor/useImageCanvasLayerCommands.ts
Normal file
422
src/components/image-editor/useImageCanvasLayerCommands.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user