refactor(canvas): 预演台 - 重构工作台与场景交互链路
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user