feat(canvas): 画布工作台 - 新增生成副本并修复大型画布性能、素材同步与删除校验

统一图片与视频副本的生成落点并保留参考连接。\n优化大型画布节点虚拟化、媒体懒加载、拖拽与详情进入路径。\n修复素材主版本 ID 同步及删除时的画布独立引用校验。
This commit is contained in:
ddCat
2026-09-01 14:57:05 +08:00
parent 434f63e153
commit 3f4a9ea3d0
45 changed files with 1348 additions and 286 deletions

View File

@@ -1,6 +1,8 @@
import { describe, expect, test } from "bun:test";
import { nextCopiedNodeTitle } from "@/lib/canvas/canvas-node-copy";
import { isolateCopiedNodeMetadata, nextCopiedNodeTitle } from "@/lib/canvas/canvas-node-copy";
import { buildNodeGenerationContext } from "@/components/canvas/canvas-node-generation";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
describe("canvas node copy title", () => {
test("首次复制使用 copy1,后续按已有最大序号递增", () => {
@@ -25,3 +27,57 @@ describe("canvas node copy title", () => {
expect([first, second]).toEqual(["节点_copy2", "节点_copy3"]);
});
});
describe("canvas generation copy metadata", () => {
test("媒体副本保留提示词与参考字段,并明确原地回填生成结果", () => {
const source: CanvasNodeData = {
id: "source",
type: CanvasNodeType.Video,
title: "镜头",
position: { x: 0, y: 0 },
width: 340,
height: 240,
metadata: {
content: "video-url",
prompt: "相同提示词",
videoStartFrameNodeId: "reference-start",
videoEndFrameNodeId: "reference-end",
generationResultPlacement: "new-version",
},
};
const metadata = isolateCopiedNodeMetadata(source, new Map([[source.id, "copy"]]));
expect(metadata.prompt).toBe("相同提示词");
expect(metadata.videoStartFrameNodeId).toBe("reference-start");
expect(metadata.videoEndFrameNodeId).toBe("reference-end");
expect(metadata.copiedFromNodeId).toBe("source");
expect(metadata.generationResultPlacement).toBe("replace-node");
});
test("复制到副本的入边继续解析为同一张参考图", () => {
const reference: CanvasNodeData = {
id: "reference",
type: CanvasNodeType.Image,
title: "角色参考",
position: { x: 0, y: 0 },
width: 340,
height: 240,
metadata: { content: "reference-url", mimeType: "image/png" },
};
const copy: CanvasNodeData = {
id: "copy",
type: CanvasNodeType.Image,
title: "结果_copy1",
position: { x: 400, y: 0 },
width: 340,
height: 240,
metadata: { content: "old-result", prompt: "保持角色一致", copiedFromNodeId: "source", generationResultPlacement: "replace-node" },
};
const context = buildNodeGenerationContext(copy.id, [reference, copy], [{ id: "copy-reference", fromNodeId: reference.id, toNodeId: copy.id }], copy.metadata!.prompt!, []);
expect(context.prompt).toBe("保持角色一致");
expect(context.referenceImages.map((image) => image.id)).toEqual([reference.id]);
});
});