perf(canvas): 画布渲染 - 增加节点渲染预算与性能模式控制

This commit is contained in:
ddcat666
2026-10-11 22:20:33 +08:00
parent 5f359f2b1d
commit a8edb6f27f
12 changed files with 57 additions and 156 deletions

View File

@@ -26,7 +26,7 @@ test("world-space approach is independent of viewport scaling", () => {
test("only connection targets receive approach data; memo tracks it and selection does not activate tilt", () => {
const source = readFileSync(new URL("../src/components/canvas/canvas-node.tsx", import.meta.url), "utf8");
const layers = readFileSync(new URL("../src/pages/canvas/canvas-project-world-layers.tsx", import.meta.url), "utf8");
expect(source).toContain("isConnectionTarget && !reduceMediaEffects && !dragOffset");
expect(source).toContain("isConnectionTarget && !disableMediaEffects && !dragOffset");
expect(source).toContain("previous.connectionApproach?.x === next.connectionApproach?.x");
expect(source).toContain('data-connection-tilt={connectionTilt ? "true" : undefined}');
expect(layers).toContain("props.connectionApproach?.nodeId === node.id ? props.connectionApproach.point : undefined");

View File

@@ -3,7 +3,7 @@ import { resolve } from "node:path";
import { describe, expect, test } from "bun:test";
import { canvasNodeRenderPadding, resolveActiveCanvasMediaNodeId } from "../src/lib/canvas/canvas-performance-mode";
import { canvasNodeRenderPadding } from "../src/lib/canvas/canvas-render-budget";
import { videoMetadata } from "../src/lib/canvas/canvas-generation-task-sync";
import { canvasNodeVideoPreviewReference, canvasNodeVideoPreviewUrl, canvasVideoAssetPreviewUrl } from "../src/lib/canvas/canvas-media-preview";
import { collectImageStorageKeys } from "../src/services/image-storage";
@@ -34,23 +34,6 @@ function node(id: string, type: CanvasNodeType): CanvasNodeData {
return { id, type, title: id, position: { x: 0, y: 0 }, width: 320, height: 180, metadata: {} };
}
describe("canvas media activation", () => {
const nodes = [node("video", CanvasNodeType.Video), node("audio", CanvasNodeType.Audio), node("image", CanvasNodeType.Image)];
const nodeById = new Map(nodes.map((item) => [item.id, item]));
test("activates exactly one selected video or audio node", () => {
expect(resolveActiveCanvasMediaNodeId(new Set(["video"]), nodeById)).toBe("video");
expect(resolveActiveCanvasMediaNodeId(new Set(["audio"]), nodeById)).toBe("audio");
});
test("keeps media inactive for empty, multi-selection, and non-media selection", () => {
expect(resolveActiveCanvasMediaNodeId(new Set(), nodeById)).toBeNull();
expect(resolveActiveCanvasMediaNodeId(new Set(["video", "audio"]), nodeById)).toBeNull();
expect(resolveActiveCanvasMediaNodeId(new Set(["image"]), nodeById)).toBeNull();
expect(resolveActiveCanvasMediaNodeId(new Set(["missing"]), nodeById)).toBeNull();
});
});
describe("canvas dimension header rendering", () => {
test("tracks live viewport scale without React width commits", () => {
expect(canvasNodeSource).toContain("calc(var(--canvas-node-width) * var(--canvas-live-scale, 1))");
@@ -60,9 +43,8 @@ describe("canvas dimension header rendering", () => {
describe("large canvas media rendering", () => {
test("keeps rendered nodes mounted longer than newly entering nodes", () => {
expect(canvasNodeRenderPadding(true, false)).toBe(128);
expect(canvasNodeRenderPadding(true, true)).toBe(640);
expect(canvasNodeRenderPadding(false, true)).toBeGreaterThan(canvasNodeRenderPadding(false, false));
expect(canvasNodeRenderPadding(false)).toBe(192);
expect(canvasNodeRenderPadding(true)).toBe(384);
});
test("does not eagerly load or resize LibTV thumbnails", () => {
@@ -76,6 +58,12 @@ describe("large canvas media rendering", () => {
expect(canvasProjectSource).toContain("<CanvasNodeActionContext.Provider value={canvasNodeActions}>");
});
test("decodes canvas images synchronously so zoom re-rasterization never paints blank frames", () => {
const retainedImageSource = canvasNodeContentSource.match(/export function RetainedCanvasImage[\s\S]*?\n}\n/)?.[0] || "";
expect(retainedImageSource).toContain('decoding="sync"');
expect(retainedImageSource).not.toContain('decoding="async"');
});
test("keeps node shells visible while panning and zooming", () => {
expect(globalStylesSource).not.toMatch(/\[data-canvas-viewport-interacting="true"\]\s+\.canvas-node-shell\s*\{[^}]*content-visibility:\s*hidden/);
});