1671d35995
让画布 Agent 消息统一走后端规划与 LLM 回复 修复生成图片在聊天、画布和素材库中的展示与恢复 补齐右键删除、画布焦点、聊天滚轮和面板避让交互 同步 Agent 事件契约、文档和定向回归测试
223 lines
5.2 KiB
TypeScript
223 lines
5.2 KiB
TypeScript
import type {
|
|
CanvasClipboard,
|
|
CanvasContextMenuState,
|
|
CanvasLayer,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { getCanvasGenerationSelectionId } from './ImageCanvasSelectionModel';
|
|
|
|
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 || menu.kind === 'blank') {
|
|
return [];
|
|
}
|
|
const targetId =
|
|
menu.kind === 'generation-dialog'
|
|
? getCanvasGenerationSelectionId(menu.dialogId)
|
|
: menu.layerId;
|
|
return selectedLayerIds.includes(targetId) ? [...selectedLayerIds] : [targetId];
|
|
}
|
|
|
|
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,
|
|
},
|
|
);
|
|
}
|