perf(canvas): 大画布渲染与媒体 - 布局不变时复用空间索引,合并重复的资源与封面请求
连线索引只存端点 ID,文本更新不再重建几何。小地图一次遍历隐藏折叠帧子节点。资源 Blob 与签名刷新合并在途请求,视频封面优先复用已缓存文件。
This commit is contained in:
1 parent
b7416b8346
commit
631a85b4fe
10 files changed
+286
-107
No files matched your search
@@ -6,14 +6,16 @@ import { sameNodeSemanticData } from "@/lib/canvas/canvas-project-domain";
|
||||
import { canvasNodeRenderBudget, canvasNodeRenderPadding, CANVAS_MAX_RENDERED_CONNECTIONS, shouldReduceCanvasMediaEffects } from "@/lib/canvas/canvas-performance-mode";
|
||||
import { buildCanvasNodeMentionReferenceMap, buildCanvasResourceReferences, buildToolMentionReference, parseToolMentionTokens } from "@/lib/canvas/canvas-resource-references";
|
||||
import { buildSkillMentionReferences } from "@/lib/canvas/canvas-skill-mentions";
|
||||
import { buildCanvasSpatialIndex, canvasNodeBounds, type CanvasSpatialIndex, type CanvasSpatialIndexEntry } from "@/lib/canvas/canvas-spatial-index";
|
||||
import { buildCanvasConnectionIndex, type CanvasIndexedConnection } from "@/lib/canvas/canvas-connection-index";
|
||||
import { retainCanvasLayoutNodes } from "@/lib/canvas/canvas-layout-snapshot";
|
||||
import { buildCanvasSpatialIndex, canvasNodeBounds, type CanvasSpatialIndex } from "@/lib/canvas/canvas-spatial-index";
|
||||
import { canvasOverviewMode, resolveCanvasNodeLOD, type CanvasNodeRenderLOD } from "@/lib/canvas/canvas-node-lod";
|
||||
import { nodeGenerationPrompt } from "@/lib/canvas/generation-contract";
|
||||
import { intersectsCanvasBounds, selectCanvasVisibleNodes } from "@/lib/canvas/canvas-node-visibility";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import type { Asset, ImageAsset } from "@/stores/use-asset-store";
|
||||
import type { PrevisScene } from "@/types/previs";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasDisplayConnection, type CanvasMediaPerformanceMode, type CanvasNodeData, type ContextMenuState, type ViewportTransform } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasMediaPerformanceMode, type CanvasNodeData, type ContextMenuState, type ViewportTransform } from "@/types/canvas";
|
||||
|
||||
type DragPreview = { x: number; y: number; nodeIds: Set<string> } | null;
|
||||
|
||||
@@ -90,9 +92,10 @@ export function useCanvasRenderModel({
|
||||
const renderHiddenNodeIds = new Set<string>();
|
||||
const frameChildrenById = new Map<string, CanvasNodeData[]>();
|
||||
const canvasImageNodes: CanvasNodeData[] = [];
|
||||
const batchRoots: CanvasNodeData[] = [];
|
||||
const batchChildNodes: CanvasNodeData[] = [];
|
||||
const batchMotionById = new Map<string, { x: number; y: number; index: number }>();
|
||||
const batchChildIndexByRootId = new Map<string, Map<string, number>>();
|
||||
const batchChildCountById = new Map<string, number>();
|
||||
|
||||
for (const node of nodes) {
|
||||
const rootId = node.metadata?.batchRootId;
|
||||
@@ -103,6 +106,12 @@ export function useCanvasRenderModel({
|
||||
}
|
||||
|
||||
if (rootId && collapsingBatchIds.has(rootId)) renderHiddenNodeIds.delete(node.id);
|
||||
if (rootId) batchChildNodes.push(node);
|
||||
const childIndex = root?.metadata?.isBatchRoot
|
||||
? batchChildIndexByRootId.get(root.id) || new Map((root.metadata.batchChildIds || []).map((childId, index) => [childId, index]))
|
||||
: undefined;
|
||||
if (root?.metadata?.isBatchRoot && childIndex && !batchChildIndexByRootId.has(root.id)) batchChildIndexByRootId.set(root.id, childIndex);
|
||||
if (root && childIndex?.has(node.id)) batchChildCountById.set(root.id, (batchChildCountById.get(root.id) || 0) + 1);
|
||||
const parent = node.parentId ? nodeById.get(node.parentId) : undefined;
|
||||
if (parent && isFrameNode(parent)) {
|
||||
const children = frameChildrenById.get(parent.id);
|
||||
@@ -111,17 +120,16 @@ export function useCanvasRenderModel({
|
||||
if (parent.metadata?.frame?.collapsed) renderHiddenNodeIds.add(node.id);
|
||||
}
|
||||
|
||||
if (node.metadata?.isBatchRoot) batchRoots.push(node);
|
||||
if (node.metadata?.isBatchRoot) {
|
||||
if (!batchChildIndexByRootId.has(node.id)) batchChildIndexByRootId.set(node.id, new Map((node.metadata.batchChildIds || []).map((childId, index) => [childId, index])));
|
||||
if (!batchChildCountById.has(node.id)) batchChildCountById.set(node.id, 0);
|
||||
}
|
||||
if (node.type === CanvasNodeType.Image && node.metadata?.content && !collapsedBatchChildIds.has(node.id) && !(parent && isFrameNode(parent) && parent.metadata?.frame?.collapsed)) {
|
||||
canvasImageNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of batchRoots) {
|
||||
const childIndex = new Map((root.metadata?.batchChildIds || []).map((childId, index) => [childId, index]));
|
||||
batchChildIndexByRootId.set(root.id, childIndex);
|
||||
}
|
||||
for (const node of nodes) {
|
||||
for (const node of batchChildNodes) {
|
||||
const rootId = node.metadata?.batchRootId;
|
||||
if (!rootId) continue;
|
||||
const root = nodeById.get(rootId);
|
||||
@@ -131,13 +139,6 @@ export function useCanvasRenderModel({
|
||||
batchMotionById.set(node.id, { x: stackX - node.position.x, y: stackY - node.position.y, index: Math.max(index, 0) });
|
||||
}
|
||||
|
||||
const batchChildCountById = new Map<string, number>();
|
||||
for (const root of batchRoots) {
|
||||
const childIndex = batchChildIndexByRootId.get(root.id);
|
||||
const liveChildCount = [...(childIndex?.keys() || [])].filter((childId) => nodeById.get(childId)?.metadata?.batchRootId === root.id).length;
|
||||
batchChildCountById.set(root.id, liveChildCount);
|
||||
}
|
||||
|
||||
return { batchChildCountById, batchMotionById, canvasImageNodes, collapsedBatchChildIds, frameChildrenById, renderHiddenNodeIds };
|
||||
}, [collapsingBatchIds, nodeById, nodes]);
|
||||
const { batchChildCountById, batchMotionById, canvasImageNodes, collapsedBatchChildIds, frameChildrenById, renderHiddenNodeIds } = nodeDerivedData;
|
||||
@@ -162,21 +163,20 @@ export function useCanvasRenderModel({
|
||||
retain: { left: viewLeft - retainPadding, top: viewTop - retainPadding, right: viewLeft + viewWidth + retainPadding, bottom: viewTop + viewHeight + retainPadding },
|
||||
};
|
||||
}, [reduceMediaEffects, viewport.k, viewport.x, viewport.y, viewportSize.height, viewportSize.width]);
|
||||
const layoutNodesRef = useRef<CanvasNodeData[]>(nodes);
|
||||
const layoutNodes = useMemo(() => {
|
||||
const retained = retainCanvasLayoutNodes(layoutNodesRef.current, nodes);
|
||||
layoutNodesRef.current = retained;
|
||||
return retained;
|
||||
}, [nodes]);
|
||||
const nodeSpatialIndexRef = useRef<{ source: CanvasNodeData[]; index: CanvasSpatialIndex<string> } | null>(null);
|
||||
const nodeSpatialIndex = useMemo(() => {
|
||||
const previous = nodeSpatialIndexRef.current;
|
||||
const geometryUnchanged =
|
||||
previous &&
|
||||
previous.source.length === nodes.length &&
|
||||
nodes.every((node, index) => {
|
||||
const old = previous.source[index];
|
||||
return old.id === node.id && old.position.x === node.position.x && old.position.y === node.position.y && old.width === node.width && old.height === node.height;
|
||||
});
|
||||
if (geometryUnchanged) return previous.index;
|
||||
const index = buildCanvasSpatialIndex(nodes.map((node) => ({ id: node.id, bounds: canvasNodeBounds(node), value: node.id })));
|
||||
nodeSpatialIndexRef.current = { source: nodes, index };
|
||||
if (previous?.source === layoutNodes) return previous.index;
|
||||
const index = buildCanvasSpatialIndex(layoutNodes.map((node) => ({ id: node.id, bounds: canvasNodeBounds(node), value: node.id })));
|
||||
nodeSpatialIndexRef.current = { source: layoutNodes, index };
|
||||
return index;
|
||||
}, [nodes]);
|
||||
}, [layoutNodes]);
|
||||
const renderedNodeIdsRef = useRef<Set<string>>(new Set());
|
||||
const forcedRenderNodeIds = useMemo(() => new Set([...selectedNodeIds, ...(dragPreview?.nodeIds || [])]), [dragPreview, selectedNodeIds]);
|
||||
const candidateNodes = useMemo(() => {
|
||||
@@ -328,35 +328,9 @@ export function useCanvasRenderModel({
|
||||
});
|
||||
return { nodeIds, connectionIds };
|
||||
}, [activeNodeId, connections]);
|
||||
const connectionSpatialIndex = useMemo(() => {
|
||||
const entries: CanvasSpatialIndexEntry<CanvasDisplayConnection>[] = [];
|
||||
const connectionIdsByNodeId = new Map<string, Set<string>>();
|
||||
connections.forEach((connection) => {
|
||||
if (collapsedBatchChildIds.has(connection.fromNodeId) || collapsedBatchChildIds.has(connection.toNodeId)) return;
|
||||
const fromNode = nodeById.get(connection.fromNodeId);
|
||||
const toNode = nodeById.get(connection.toNodeId);
|
||||
if (!fromNode || !toNode) return;
|
||||
const fromParent = fromNode.parentId ? nodeById.get(fromNode.parentId) : null;
|
||||
const toParent = toNode.parentId ? nodeById.get(toNode.parentId) : null;
|
||||
const displayFrom = fromParent && isFrameNode(fromParent) && fromParent.metadata?.frame?.collapsed ? fromParent : fromNode;
|
||||
const displayTo = toParent && isFrameNode(toParent) && toParent.metadata?.frame?.collapsed ? toParent : toNode;
|
||||
if (displayFrom.id === displayTo.id) return;
|
||||
const left = Math.min(displayFrom.position.x, displayTo.position.x);
|
||||
const top = Math.min(displayFrom.position.y, displayTo.position.y);
|
||||
const right = Math.max(displayFrom.position.x + displayFrom.width, displayTo.position.x + displayTo.width);
|
||||
const bottom = Math.max(displayFrom.position.y + displayFrom.height, displayTo.position.y + displayTo.height);
|
||||
const value = { connection, from: displayFrom, to: displayTo };
|
||||
entries.push({ id: connection.id, bounds: { left, top, right, bottom }, value });
|
||||
for (const nodeId of new Set([connection.fromNodeId, connection.toNodeId, displayFrom.id, displayTo.id])) {
|
||||
const ids = connectionIdsByNodeId.get(nodeId) || new Set<string>();
|
||||
ids.add(connection.id);
|
||||
connectionIdsByNodeId.set(nodeId, ids);
|
||||
}
|
||||
});
|
||||
return { index: buildCanvasSpatialIndex(entries), connectionIdsByNodeId, entriesById: new Map(entries.map((entry) => [entry.id, entry.value])) };
|
||||
}, [collapsedBatchChildIds, connections, nodeById]);
|
||||
const connectionSpatialIndex = useMemo(() => buildCanvasConnectionIndex(layoutNodes, connections), [connections, layoutNodes]);
|
||||
const displayConnections = useMemo(() => {
|
||||
const candidateById = new Map<string, CanvasDisplayConnection>();
|
||||
const candidateById = new Map<string, CanvasIndexedConnection>();
|
||||
connectionSpatialIndex.index.query(renderBounds.retain, CANVAS_MAX_RENDERED_CONNECTIONS).forEach((display) => candidateById.set(display.connection.id, display));
|
||||
dragPreview?.nodeIds.forEach((nodeId) => {
|
||||
connectionSpatialIndex.connectionIdsByNodeId.get(nodeId)?.forEach((connectionId) => {
|
||||
@@ -364,9 +338,12 @@ export function useCanvasRenderModel({
|
||||
if (display) candidateById.set(connectionId, display);
|
||||
});
|
||||
});
|
||||
return [...candidateById.values()].flatMap(({ connection, from: sourceFrom, to: sourceTo }) => {
|
||||
const from = dragPreview?.nodeIds.has(sourceFrom.id) ? { ...sourceFrom, position: { x: sourceFrom.position.x + dragPreview.x, y: sourceFrom.position.y + dragPreview.y } } : sourceFrom;
|
||||
const to = dragPreview?.nodeIds.has(sourceTo.id) ? { ...sourceTo, position: { x: sourceTo.position.x + dragPreview.x, y: sourceTo.position.y + dragPreview.y } } : sourceTo;
|
||||
return [...candidateById.values()].flatMap(({ connection, fromId, toId }) => {
|
||||
const sourceFrom = nodeById.get(fromId);
|
||||
const sourceTo = nodeById.get(toId);
|
||||
if (!sourceFrom || !sourceTo) return [];
|
||||
const from = dragPreview?.nodeIds.has(fromId) ? { ...sourceFrom, position: { x: sourceFrom.position.x + dragPreview.x, y: sourceFrom.position.y + dragPreview.y } } : sourceFrom;
|
||||
const to = dragPreview?.nodeIds.has(toId) ? { ...sourceTo, position: { x: sourceTo.position.x + dragPreview.x, y: sourceTo.position.y + dragPreview.y } } : sourceTo;
|
||||
const connectionLeft = Math.min(from.position.x, to.position.x);
|
||||
const connectionTop = Math.min(from.position.y, to.position.y);
|
||||
const connectionRight = Math.max(from.position.x + from.width, to.position.x + to.width);
|
||||
@@ -374,7 +351,7 @@ export function useCanvasRenderModel({
|
||||
if (connectionRight <= renderBounds.retain.left || connectionLeft >= renderBounds.retain.right || connectionBottom <= renderBounds.retain.top || connectionTop >= renderBounds.retain.bottom) return [];
|
||||
return [{ connection, from, to }];
|
||||
});
|
||||
}, [connectionSpatialIndex, dragPreview, renderBounds]);
|
||||
}, [connectionSpatialIndex, dragPreview, nodeById, renderBounds]);
|
||||
|
||||
const configInputsById = useMemo(() => {
|
||||
const map = new Map<string, NodeGenerationInput[]>();
|
||||
|
||||
Reference in new issue
Block a user