refactor(canvas): 预演台 - 重构工作台与场景交互链路

This commit is contained in:
ddcat666
2026-10-06 09:06:43 +08:00
parent 1dfc8ce7ac
commit 112916587c
128 changed files with 42372 additions and 5011 deletions

View File

@@ -1,16 +1,16 @@
import { describe, expect, test } from "bun:test";
import { gateDirectorPreviewFailure, resolveDirectorActiveShot, resolveDirectorPreviewSource, type DirectorNodeContentReader } from "../src/lib/canvas/director/director-preview";
import { createDirectorScene } from "../src/lib/canvas/director/director-scene";
import type { DirectorScene, DirectorShot } from "../src/types/director";
import { gatePrevisPreviewFailure, resolvePrevisActiveShot, resolvePrevisPreviewSource, type PrevisNodeContentReader } from "../src/lib/canvas/previs/previs-preview";
import { createPrevisScene } from "../src/lib/canvas/previs/previs-scene";
import type { PrevisScene, PrevisShot } from "../src/types/previs";
function scene(shots?: DirectorShot[]): DirectorScene {
const base = createDirectorScene("镜头 1");
function scene(shots?: PrevisShot[]): PrevisScene {
const base = createPrevisScene("镜头 1");
return shots ? { ...base, shots, activeShotId: shots[0]?.id || base.activeShotId } : base;
}
/** 画布节点表;未登记的 id 视为已删除。 */
function reader(nodes: Record<string, string | undefined>): DirectorNodeContentReader {
function reader(nodes: Record<string, string | undefined>): PrevisNodeContentReader {
return (nodeId) => (nodeId ? nodes[nodeId] : undefined);
}
@@ -18,7 +18,7 @@ describe("预览来源优先级", () => {
test("metadata preview 优先于 shot fallback", () => {
const current = scene();
const shot = { ...current.shots[0], previewNodeId: "from-shot" };
const source = resolveDirectorPreviewSource({
const source = resolvePrevisPreviewSource({
scene: current,
shot,
previewNodeId: "from-metadata",
@@ -30,7 +30,7 @@ describe("预览来源优先级", () => {
test("metadata 缺失时回落到 shot.previewNodeId", () => {
const current = scene();
const shot = { ...current.shots[0], previewNodeId: "from-shot" };
const source = resolveDirectorPreviewSource({
const source = resolvePrevisPreviewSource({
scene: current,
shot,
previewNodeId: undefined,
@@ -42,7 +42,7 @@ describe("预览来源优先级", () => {
test("metadata 指向的节点已删除时回落到 shot", () => {
const current = scene();
const shot = { ...current.shots[0], previewNodeId: "from-shot" };
const source = resolveDirectorPreviewSource({
const source = resolvePrevisPreviewSource({
scene: current,
shot,
previewNodeId: "deleted-node",
@@ -53,7 +53,7 @@ describe("预览来源优先级", () => {
test("节点已删除且无 shot 预览时为空态,绝不产出 image", () => {
const current = scene();
const source = resolveDirectorPreviewSource({
const source = resolvePrevisPreviewSource({
scene: current,
shot: current.shots[0],
previewNodeId: "deleted-node",
@@ -66,7 +66,7 @@ describe("预览来源优先级", () => {
const current = scene();
const shot = { ...current.shots[0], previewNodeId: "blank-shot" };
expect(
resolveDirectorPreviewSource({
resolvePrevisPreviewSource({
scene: current,
shot,
previewNodeId: "blank-meta",
@@ -78,7 +78,7 @@ describe("预览来源优先级", () => {
test("首尾空白被剥离,URL 不会畸形地进入 <img src>", () => {
const current = scene();
expect(
resolveDirectorPreviewSource({
resolvePrevisPreviewSource({
scene: current,
shot: current.shots[0],
previewNodeId: "padded-meta",
@@ -91,7 +91,7 @@ describe("预览来源优先级", () => {
const current = scene();
const shot = { ...current.shots[0], previewNodeId: "padded-shot" };
expect(
resolveDirectorPreviewSource({
resolvePrevisPreviewSource({
scene: current,
shot,
previewNodeId: undefined,
@@ -102,7 +102,7 @@ describe("预览来源优先级", () => {
test("仅有空白的正文仍然落空,不因 trim 变成空字符串 URL", () => {
const current = scene();
const source = resolveDirectorPreviewSource({
const source = resolvePrevisPreviewSource({
scene: current,
shot: current.shots[0],
previewNodeId: "ws-only",
@@ -113,12 +113,12 @@ describe("预览来源优先级", () => {
});
test("scene 为 null 时是准备态而不是空态", () => {
expect(resolveDirectorPreviewSource({ scene: null, shot: undefined, previewNodeId: undefined, readNodeContent: reader({}) })).toEqual({ kind: "loading" });
expect(resolvePrevisPreviewSource({ scene: null, shot: undefined, previewNodeId: undefined, readNodeContent: reader({}) })).toEqual({ kind: "loading" });
});
test("scene 为 null 但已有真实预览时仍优先显示图", () => {
expect(
resolveDirectorPreviewSource({
resolvePrevisPreviewSource({
scene: null,
shot: undefined,
previewNodeId: "from-metadata",
@@ -129,18 +129,18 @@ describe("预览来源优先级", () => {
test("有场景但从未回写构图时是诚实空态", () => {
const current = scene();
expect(resolveDirectorPreviewSource({ scene: current, shot: current.shots[0], previewNodeId: undefined, readNodeContent: reader({}) })).toEqual({ kind: "empty" });
expect(resolvePrevisPreviewSource({ scene: current, shot: current.shots[0], previewNodeId: undefined, readNodeContent: reader({}) })).toEqual({ kind: "empty" });
});
test("Apply 写入 metadata 后由空态转为 image", () => {
const current = scene();
const before = resolveDirectorPreviewSource({ scene: current, shot: current.shots[0], previewNodeId: undefined, readNodeContent: reader({}) });
const before = resolvePrevisPreviewSource({ scene: current, shot: current.shots[0], previewNodeId: undefined, readNodeContent: reader({}) });
expect(before).toEqual({ kind: "empty" });
const after = resolveDirectorPreviewSource({
const after = resolvePrevisPreviewSource({
scene: current,
shot: current.shots[0],
previewNodeId: "image-director-1",
readNodeContent: reader({ "image-director-1": "data:image/png;base64,APPLIED" }),
previewNodeId: "image-previs-1",
readNodeContent: reader({ "image-previs-1": "data:image/png;base64,APPLIED" }),
});
expect(after).toEqual({ kind: "image", url: "data:image/png;base64,APPLIED" });
});
@@ -150,58 +150,58 @@ describe("加载失败门控", () => {
const image = { kind: "image", url: "data:image/png;base64,A" } as const;
test("失败 URL 与当前 URL 一致时立即变空态", () => {
expect(gateDirectorPreviewFailure(image, image.url)).toEqual({ kind: "empty" });
expect(gatePrevisPreviewFailure(image, image.url)).toEqual({ kind: "empty" });
});
test("URL 变化后自动恢复,无需清理布尔标记", () => {
expect(gateDirectorPreviewFailure({ kind: "image", url: "data:image/png;base64,B" }, image.url)).toEqual({ kind: "image", url: "data:image/png;base64,B" });
expect(gatePrevisPreviewFailure({ kind: "image", url: "data:image/png;base64,B" }, image.url)).toEqual({ kind: "image", url: "data:image/png;base64,B" });
});
test("无失败记录时原样通过", () => {
expect(gateDirectorPreviewFailure(image, null)).toBe(image);
expect(gatePrevisPreviewFailure(image, null)).toBe(image);
});
test("非 image 来源不受门控影响", () => {
expect(gateDirectorPreviewFailure({ kind: "loading" }, image.url)).toEqual({ kind: "loading" });
expect(gateDirectorPreviewFailure({ kind: "empty" }, image.url)).toEqual({ kind: "empty" });
expect(gatePrevisPreviewFailure({ kind: "loading" }, image.url)).toEqual({ kind: "loading" });
expect(gatePrevisPreviewFailure({ kind: "empty" }, image.url)).toEqual({ kind: "empty" });
});
test("失败后 Apply 出新图仍能显示(同一次会话内)", () => {
const next = resolveDirectorPreviewSource({
const next = resolvePrevisPreviewSource({
scene: scene(),
shot: undefined,
previewNodeId: "n1",
readNodeContent: reader({ n1: "data:image/png;base64,NEW" }),
});
expect(gateDirectorPreviewFailure(next, "data:image/png;base64,OLD")).toEqual({ kind: "image", url: "data:image/png;base64,NEW" });
expect(gatePrevisPreviewFailure(next, "data:image/png;base64,OLD")).toEqual({ kind: "image", url: "data:image/png;base64,NEW" });
});
});
describe("当前镜头解析", () => {
test("优先 metadata 指定的 shot", () => {
const first = createDirectorScene("s").shots[0];
const second: DirectorShot = { ...first, id: "shot-2", name: "镜头 2", previewNodeId: "p2" };
const first = createPrevisScene("s").shots[0];
const second: PrevisShot = { ...first, id: "shot-2", name: "镜头 2", previewNodeId: "p2" };
const current = scene([first, second]);
expect(resolveDirectorActiveShot(current, "shot-2")?.id).toBe("shot-2");
expect(resolvePrevisActiveShot(current, "shot-2")?.id).toBe("shot-2");
});
test("metadata 未指定或未命中时退回第一个 shot", () => {
const current = scene();
expect(resolveDirectorActiveShot(current, undefined)?.id).toBe(current.shots[0].id);
expect(resolveDirectorActiveShot(current, "missing")?.id).toBe(current.shots[0].id);
expect(resolvePrevisActiveShot(current, undefined)?.id).toBe(current.shots[0].id);
expect(resolvePrevisActiveShot(current, "missing")?.id).toBe(current.shots[0].id);
});
test("scene 为 null 时没有镜头", () => {
expect(resolveDirectorActiveShot(null, "shot-1")).toBeUndefined();
expect(resolvePrevisActiveShot(null, "shot-1")).toBeUndefined();
});
test("指定 shot 的 previewNodeId 参与回落,而不是第一个 shot 的", () => {
const first = { ...createDirectorScene("s").shots[0], previewNodeId: "p1" };
const second: DirectorShot = { ...first, id: "shot-2", previewNodeId: "p2" };
const first = { ...createPrevisScene("s").shots[0], previewNodeId: "p1" };
const second: PrevisShot = { ...first, id: "shot-2", previewNodeId: "p2" };
const current = scene([first, second]);
const shot = resolveDirectorActiveShot(current, "shot-2");
const shot = resolvePrevisActiveShot(current, "shot-2");
expect(
resolveDirectorPreviewSource({
resolvePrevisPreviewSource({
scene: current,
shot,
previewNodeId: undefined,