feat(canvas): 画布工作台 - 新增生成副本并修复大型画布性能、素材同步与删除校验

统一图片与视频副本的生成落点并保留参考连接。\n优化大型画布节点虚拟化、媒体懒加载、拖拽与详情进入路径。\n修复素材主版本 ID 同步及删除时的画布独立引用校验。
This commit is contained in:
ddCat committed 2026-09-01 14:57:05 +08:00
1 parent b3b360481c
commit 3a21e0ebfc
45 files changed
+1348 -286

No files matched your search

+35 -3
View File
@@ -50,10 +50,15 @@ export function normalizeCanvasNodeTimestamps(nodes: CanvasNodeData[], fallback:
export function stampCanvasNodeChanges(previousNodes: CanvasNodeData[], nextNodes: CanvasNodeData[], now = new Date().toISOString()) {
if (previousNodes === nextNodes) return previousNodes;
const previousById = new Map(previousNodes.map((node) => [node.id, node]));
// Most canvas edits preserve order and replace one node. Avoid allocating a
// 50k-entry id map and comparing metadata for every untouched node in that
// hot path. The map fallback still handles inserts, deletes and reorders.
const sameOrder = previousNodes.length === nextNodes.length && previousNodes.every((node, index) => node.id === nextNodes[index]?.id);
const previousById = sameOrder ? null : new Map(previousNodes.map((node) => [node.id, node]));
let changed = false;
const stamped = nextNodes.map((node) => {
const previous = previousById.get(node.id);
const stamped = nextNodes.map((node, index) => {
const previous = sameOrder ? previousNodes[index] : previousById?.get(node.id);
if (previous === node) return node;
const createdAt = canvasNodeCreatedAt(node, canvasNodeCreatedAt(previous || node, now)) || now;
const meaningfulChange = previous ? canvasNodeMeaningfullyChanged(previous, node) : false;
const updatedAt = previous
@@ -68,6 +73,33 @@ export function stampCanvasNodeChanges(previousNodes: CanvasNodeData[], nextNode
return changed ? stamped : nextNodes;
}
/**
* Update one node without routing through the generic full-array timestamp
* reconciliation. This is used by media metadata callbacks, which can fire
* in bursts while a large canvas is first becoming visible.
*/
export function updateCanvasNode(nodes: CanvasNodeData[], nodeId: string, update: (node: CanvasNodeData) => CanvasNodeData, now = new Date().toISOString()) {
return updateCanvasNodes(nodes, new Map([[nodeId, update]]), now);
}
export function updateCanvasNodes(nodes: CanvasNodeData[], updates: ReadonlyMap<string, (node: CanvasNodeData) => CanvasNodeData>, now = new Date().toISOString()) {
if (!updates.size) return nodes;
let changed = false;
const next = nodes.map((current) => {
const update = updates.get(current.id);
if (!update) return current;
const updated = update(current);
if (updated === current) return current;
changed = true;
const createdAt = canvasNodeCreatedAt(updated, canvasNodeCreatedAt(current, now)) || now;
const updatedAt = canvasNodeMeaningfullyChanged(current, updated)
? now
: canvasNodeUpdatedAt(updated, canvasNodeUpdatedAt(current, createdAt)) || createdAt;
return updated.createdAt === createdAt && updated.updatedAt === updatedAt ? updated : { ...updated, createdAt, updatedAt };
});
return changed ? next : nodes;
}
function canvasNodeMeaningfullyChanged(previous: CanvasNodeData, next: CanvasNodeData) {
if (
previous.type !== next.type