新增 ImageCanvasLayerCommandModel 收口右键图层复制、粘贴、层级、分组、显隐、锁定、翻转和删除规则 主视图保留历史、选中态、菜单关闭、元数据清理和导出副作用 补充图片右键菜单真实浏览器冒泡回归测试 更新图片画布前端拆分计划和 TRACKING 验证记录
220 lines
5.0 KiB
TypeScript
220 lines
5.0 KiB
TypeScript
import type {
|
|
CanvasClipboard,
|
|
CanvasContextMenuState,
|
|
CanvasLayer,
|
|
} from './ImageCanvasEditorTypes';
|
|
|
|
export type CanvasLayerMoveMode = 'up' | 'down' | 'top' | 'bottom';
|
|
|
|
export type CanvasLayerFlipAxis = 'x' | 'y';
|
|
|
|
type CanvasPoint = {
|
|
x: number;
|
|
y: number;
|
|
};
|
|
|
|
function cloneLayer(layer: CanvasLayer): CanvasLayer {
|
|
return {
|
|
...layer,
|
|
generationInputs: layer.generationInputs
|
|
? {
|
|
fields: layer.generationInputs.fields.map((field) => ({ ...field })),
|
|
references: layer.generationInputs.references.map((reference) => ({
|
|
...reference,
|
|
})),
|
|
}
|
|
: layer.generationInputs,
|
|
};
|
|
}
|
|
|
|
export function resolveContextTargetLayerIds(
|
|
menu: CanvasContextMenuState | null,
|
|
selectedLayerIds: string[],
|
|
) {
|
|
if (menu?.kind !== 'layer') {
|
|
return [];
|
|
}
|
|
return selectedLayerIds.includes(menu.layerId)
|
|
? [...selectedLayerIds]
|
|
: [menu.layerId];
|
|
}
|
|
|
|
export function getCanvasLayersByIds(
|
|
layers: CanvasLayer[],
|
|
targetIds: string[],
|
|
) {
|
|
return layers.filter((layer) => targetIds.includes(layer.id));
|
|
}
|
|
|
|
export function duplicateCanvasLayers({
|
|
sourceLayers,
|
|
allLayers,
|
|
canvasPoint,
|
|
renameCopies = true,
|
|
stamp = Date.now(),
|
|
}: {
|
|
sourceLayers: CanvasLayer[];
|
|
allLayers: CanvasLayer[];
|
|
canvasPoint?: CanvasPoint;
|
|
renameCopies?: boolean;
|
|
stamp?: number | string;
|
|
}) {
|
|
if (!sourceLayers.length) {
|
|
return [];
|
|
}
|
|
|
|
const minX = Math.min(...sourceLayers.map((layer) => layer.x));
|
|
const minY = Math.min(...sourceLayers.map((layer) => layer.y));
|
|
const maxZIndex = allLayers.reduce(
|
|
(maxZ, layer) => Math.max(maxZ, layer.zIndex),
|
|
0,
|
|
);
|
|
|
|
return sourceLayers.map((layer, index) => ({
|
|
...cloneLayer(layer),
|
|
id: `layer-copy-${stamp}-${index}`,
|
|
resourceId: `local-resource-copy-${stamp}-${index}`,
|
|
title: renameCopies ? `${layer.title} 副本` : layer.title,
|
|
x: canvasPoint ? canvasPoint.x + (layer.x - minX) : layer.x + 32,
|
|
y: canvasPoint ? canvasPoint.y + (layer.y - minY) : layer.y + 32,
|
|
zIndex: maxZIndex + index + 1,
|
|
groupId: null,
|
|
}));
|
|
}
|
|
|
|
export function createCanvasLayerClipboard(
|
|
layers: CanvasLayer[],
|
|
targetIds: string[],
|
|
mode: CanvasClipboard['mode'],
|
|
): CanvasClipboard | null {
|
|
const targetLayers = getCanvasLayersByIds(layers, targetIds);
|
|
if (!targetLayers.length) {
|
|
return null;
|
|
}
|
|
return {
|
|
layers: targetLayers.map(cloneLayer),
|
|
mode,
|
|
};
|
|
}
|
|
|
|
export function removeCanvasLayers(layers: CanvasLayer[], targetIds: string[]) {
|
|
if (!targetIds.length) {
|
|
return layers;
|
|
}
|
|
return layers.filter((layer) => !targetIds.includes(layer.id));
|
|
}
|
|
|
|
export function updateCanvasLayersByIds(
|
|
layers: CanvasLayer[],
|
|
targetIds: string[],
|
|
updater: (layer: CanvasLayer, targetIds: string[]) => CanvasLayer,
|
|
) {
|
|
if (!targetIds.length) {
|
|
return layers;
|
|
}
|
|
return layers.map((layer) =>
|
|
targetIds.includes(layer.id) ? updater(layer, targetIds) : layer,
|
|
);
|
|
}
|
|
|
|
export function moveCanvasLayers(
|
|
layers: CanvasLayer[],
|
|
targetIds: string[],
|
|
mode: CanvasLayerMoveMode,
|
|
) {
|
|
if (!targetIds.length) {
|
|
return layers;
|
|
}
|
|
const maxZIndex = layers.reduce(
|
|
(maxZ, layer) => Math.max(maxZ, layer.zIndex),
|
|
0,
|
|
);
|
|
const minZIndex = layers.reduce(
|
|
(minZ, layer) => Math.min(minZ, layer.zIndex),
|
|
0,
|
|
);
|
|
let offsetIndex = 0;
|
|
|
|
return updateCanvasLayersByIds(layers, targetIds, (layer) => {
|
|
if (mode === 'up') {
|
|
return { ...layer, zIndex: layer.zIndex + 1 };
|
|
}
|
|
if (mode === 'down') {
|
|
return { ...layer, zIndex: layer.zIndex - 1 };
|
|
}
|
|
offsetIndex += 1;
|
|
if (mode === 'top') {
|
|
return { ...layer, zIndex: maxZIndex + offsetIndex };
|
|
}
|
|
return {
|
|
...layer,
|
|
zIndex: minZIndex - (targetIds.length - offsetIndex + 1),
|
|
};
|
|
});
|
|
}
|
|
|
|
export function groupCanvasLayers(
|
|
layers: CanvasLayer[],
|
|
targetIds: string[],
|
|
groupId: string,
|
|
) {
|
|
return updateCanvasLayersByIds(layers, targetIds, (layer) => ({
|
|
...layer,
|
|
groupId,
|
|
}));
|
|
}
|
|
|
|
export function ungroupCanvasLayers(
|
|
layers: CanvasLayer[],
|
|
targetIds: string[],
|
|
) {
|
|
return updateCanvasLayersByIds(layers, targetIds, (layer) => ({
|
|
...layer,
|
|
groupId: null,
|
|
}));
|
|
}
|
|
|
|
export function toggleCanvasLayersVisibility(
|
|
layers: CanvasLayer[],
|
|
targetIds: string[],
|
|
) {
|
|
const shouldHide = getCanvasLayersByIds(layers, targetIds).some(
|
|
(layer) => !layer.hidden,
|
|
);
|
|
return updateCanvasLayersByIds(layers, targetIds, (layer) => ({
|
|
...layer,
|
|
hidden: shouldHide,
|
|
}));
|
|
}
|
|
|
|
export function toggleCanvasLayersLock(
|
|
layers: CanvasLayer[],
|
|
targetIds: string[],
|
|
) {
|
|
const shouldLock = getCanvasLayersByIds(layers, targetIds).some(
|
|
(layer) => !layer.locked,
|
|
);
|
|
return updateCanvasLayersByIds(layers, targetIds, (layer) => ({
|
|
...layer,
|
|
locked: shouldLock,
|
|
}));
|
|
}
|
|
|
|
export function flipCanvasLayers(
|
|
layers: CanvasLayer[],
|
|
targetIds: string[],
|
|
axis: CanvasLayerFlipAxis,
|
|
) {
|
|
return updateCanvasLayersByIds(layers, targetIds, (layer) =>
|
|
axis === 'x'
|
|
? {
|
|
...layer,
|
|
flipX: !layer.flipX,
|
|
}
|
|
: {
|
|
...layer,
|
|
flipY: !layer.flipY,
|
|
},
|
|
);
|
|
}
|