perf(canvas): 大画布渲染与媒体 - 布局不变时复用空间索引,合并重复的资源与封面请求
连线索引只存端点 ID,文本更新不再重建几何。小地图一次遍历隐藏折叠帧子节点。资源 Blob 与签名刷新合并在途请求,视频封面优先复用已缓存文件。
This commit is contained in:
1 parent
b7416b8346
commit
631a85b4fe
10 files changed
+286
-107
No files matched your search
@@ -0,0 +1,45 @@
|
||||
import { isFrameNode } from "@/lib/canvas/canvas-frame";
|
||||
import { buildCanvasSpatialIndex, type CanvasSpatialIndexEntry } from "@/lib/canvas/canvas-spatial-index";
|
||||
import type { CanvasConnection, CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
export type CanvasIndexedConnection = { connection: CanvasConnection; fromId: string; toId: string };
|
||||
|
||||
/** Store endpoint IDs so text/task updates can use live nodes without rebuilding geometry. */
|
||||
export function buildCanvasConnectionIndex(nodes: CanvasNodeData[], connections: CanvasConnection[]) {
|
||||
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
||||
const entries: CanvasSpatialIndexEntry<CanvasIndexedConnection>[] = [];
|
||||
const connectionIdsByNodeId = new Map<string, Set<string>>();
|
||||
const collapsedBatchChild = (node: CanvasNodeData) => {
|
||||
const root = node.metadata?.batchRootId ? nodeById.get(node.metadata.batchRootId) : undefined;
|
||||
return root && !root.metadata?.imageBatchExpanded;
|
||||
};
|
||||
const endpoint = (node: CanvasNodeData) => {
|
||||
const parent = node.parentId ? nodeById.get(node.parentId) : undefined;
|
||||
return parent && isFrameNode(parent) && parent.metadata?.frame?.collapsed ? parent : node;
|
||||
};
|
||||
for (const connection of connections) {
|
||||
const source = nodeById.get(connection.fromNodeId);
|
||||
const target = nodeById.get(connection.toNodeId);
|
||||
if (!source || !target || collapsedBatchChild(source) || collapsedBatchChild(target)) continue;
|
||||
const from = endpoint(source),
|
||||
to = endpoint(target);
|
||||
if (from.id === to.id) continue;
|
||||
const value = { connection, fromId: from.id, toId: to.id };
|
||||
entries.push({
|
||||
id: connection.id,
|
||||
bounds: {
|
||||
left: Math.min(from.position.x, to.position.x),
|
||||
top: Math.min(from.position.y, to.position.y),
|
||||
right: Math.max(from.position.x + from.width, to.position.x + to.width),
|
||||
bottom: Math.max(from.position.y + from.height, to.position.y + to.height),
|
||||
},
|
||||
value,
|
||||
});
|
||||
for (const nodeId of new Set([source.id, target.id, from.id, to.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])) };
|
||||
}
|
||||
@@ -46,6 +46,16 @@ export function isNodeHiddenByCollapsedFrame(node: CanvasNodeData, nodes: Canvas
|
||||
return Boolean(getCollapsedParentFrame(node, nodes));
|
||||
}
|
||||
|
||||
/** Build minimap input in one pass; parent lookup must stay O(1) on large canvases. */
|
||||
export function visibleCanvasMinimapNodes(nodes: CanvasNodeData[]) {
|
||||
const byId = new Map(nodes.map((node) => [node.id, node]));
|
||||
const collapsedFrameIds = new Set(nodes.filter((node) => isFrameNode(node) && node.metadata?.frame?.collapsed).map((node) => node.id));
|
||||
return nodes.filter((node) => {
|
||||
const parent = node.parentId ? byId.get(node.parentId) : undefined;
|
||||
return !parent || !collapsedFrameIds.has(parent.id);
|
||||
});
|
||||
}
|
||||
|
||||
export function findFrameDropTarget(nodes: CanvasNodeData[], draggedNodeIds: Set<string>) {
|
||||
const dragged = nodes.filter((node) => draggedNodeIds.has(node.id));
|
||||
if (!dragged.length) return null;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
/** Only for geometry/topology consumers; never use this snapshot as node content. */
|
||||
export function retainCanvasLayoutNodes(previous: CanvasNodeData[], nodes: CanvasNodeData[]) {
|
||||
return previous === nodes || (previous.length === nodes.length && nodes.every((node, index) => sameLayout(node, previous[index]))) ? previous : nodes;
|
||||
}
|
||||
|
||||
function sameLayout(a: CanvasNodeData, b: CanvasNodeData) {
|
||||
return (
|
||||
a === b ||
|
||||
(a.id === b.id &&
|
||||
a.type === b.type &&
|
||||
a.parentId === b.parentId &&
|
||||
a.position.x === b.position.x &&
|
||||
a.position.y === b.position.y &&
|
||||
a.width === b.width &&
|
||||
a.height === b.height &&
|
||||
a.metadata?.batchRootId === b.metadata?.batchRootId &&
|
||||
a.metadata?.imageBatchExpanded === b.metadata?.imageBatchExpanded &&
|
||||
a.metadata?.frame?.collapsed === b.metadata?.frame?.collapsed)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user