cd80c085ff
统一前后端 SFX Prompt Unicode canonicalization、字符计数和 2048 边界 固化 52 个音效预设、追加规则及跨语言测试向量 演进共享音频请求响应、duration、Loop 和 V2 metadata 契约 增加 External model 输入矩阵纯函数并保持正式接线归属 T5 补齐前端提交、读取、深拷贝与请求边界回归测试 更新共享计划并标记 T1 完成且当前不可发布
260 lines
6.3 KiB
TypeScript
260 lines
6.3 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';
|
|
|
|
export type CanvasLayerCopyBlockReason =
|
|
| 'pending-resource'
|
|
| 'self-contained-local-unsupported'
|
|
| 'unresolved-resource';
|
|
|
|
type CanvasPoint = {
|
|
x: number;
|
|
y: number;
|
|
};
|
|
|
|
function cloneLayer(layer: CanvasLayer): CanvasLayer {
|
|
return {
|
|
...layer,
|
|
generationInputs: layer.generationInputs
|
|
? {
|
|
...layer.generationInputs,
|
|
fields: layer.generationInputs.fields.map((field) => ({ ...field })),
|
|
references: layer.generationInputs.references.map((reference) => ({
|
|
...reference,
|
|
})),
|
|
...(layer.generationInputs.soundEffect
|
|
? { soundEffect: { ...layer.generationInputs.soundEffect } }
|
|
: {}),
|
|
}
|
|
: 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 getCanvasLayerCopyBlockReason(
|
|
layers: CanvasLayer[],
|
|
): CanvasLayerCopyBlockReason | null {
|
|
for (const layer of layers) {
|
|
if (layer.resourcePersistenceState === 'registered') {
|
|
continue;
|
|
}
|
|
if (layer.resourcePersistenceState === 'pending') {
|
|
return 'pending-resource';
|
|
}
|
|
if (layer.resourcePersistenceState === 'self-contained-local') {
|
|
return 'self-contained-local-unsupported';
|
|
}
|
|
if (
|
|
layer.resourcePersistenceState === 'unresolved-local' ||
|
|
layer.resourceId.startsWith('local-')
|
|
) {
|
|
return 'unresolved-resource';
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function canCopyCanvasLayers(layers: CanvasLayer[]) {
|
|
return layers.length > 0 && getCanvasLayerCopyBlockReason(layers) === null;
|
|
}
|
|
|
|
export function duplicateCanvasLayers({
|
|
sourceLayers,
|
|
allLayers,
|
|
canvasPoint,
|
|
renameCopies = true,
|
|
stamp = Date.now(),
|
|
}: {
|
|
sourceLayers: CanvasLayer[];
|
|
allLayers: CanvasLayer[];
|
|
canvasPoint?: CanvasPoint;
|
|
renameCopies?: boolean;
|
|
stamp?: number | string;
|
|
}) {
|
|
if (!canCopyCanvasLayers(sourceLayers)) {
|
|
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}`,
|
|
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 (!canCopyCanvasLayers(targetLayers)) {
|
|
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,
|
|
},
|
|
);
|
|
}
|