/** * React Flow 输入对象的结构共享缓存,与计算场景的 scene-projection 分工不同。 * 按元素代次与 owner 判定同一生命周期,复用值相等的 data/style/position 和整个数组,减少显示刷新。 * 缓存属于一个 owner/project/视图 canvas,既不是业务文档,也不是跨项目身份仓库。 */ import type {Document,Id} from '../canvas-core/types.ts'; import {equal,fault} from '../canvas-core/values.ts'; import type {projectCanvas} from './projection.ts'; /** 只处理已经生成的投影,不在这里解释元素组件或计算坐标。 */ type Projection=ReturnType; /** 原对象的生命周期证据;同 ID 的重建或移动 owner 后不能直接继承旧显示对象。 */ type Retained={incarnation:number;ownerCanvasId:Id;value:T}; /** View-owned rendering cache, never a document store. Keep unchanged React Flow input identities. */ /** 创建视图专属结构共享器;reuse 会就地改传入 nodes/edges 数组,调用方应提供自有可变派生数组。 * 不深克隆/冻结显示对象;缓存的对象须按不可变约定使用,不能传入业务记录数组或原地修改旧投影。 */ export function createProjectionCache(){ let scope:object|undefined,projectId:Id|undefined,canvasId:Id|undefined; let nodeArray:Projection['nodes']|undefined,edgeArray:Projection['edges']|undefined; /** 数组长度与逐项引用完全一致时沿用旧数组,顺序改变也会产生新数组身份。 */ const stable=(previous:T[]|undefined,next:T[])=>previous?.length===next.length&&next.every((value,index)=>value===previous[index])?previous:next; let nodes=new Map>(),edges=new Map>(); /** 每个显示 ID 须有文档记录;生命周期键只核 incarnation+owner,不另含 component/dataVersion,显示差异由值比较处理。 * 仅 data/style/position 三个嵌套字段单独等价共享,再比较整个显示对象;新表只含本帧项。 * 不查投影 ID 重复、不检查 node/edge 与记录的全部对应语义;这依赖上游合法投影。 */ function retain(document:Document,values:T[],previous:Map>){ const next=new Map>(); for(let index=0;index)[key],before=(old.value as Record)[key];if(Object.hasOwn(shared,key)&&Object.hasOwn(old.value,key)&&!Object.is(incoming,before)&&equal(incoming,before))shared={...shared,[key]:before};} const retained=sameLifetime&&equal(old.value,shared)?old.value:shared; values[index]=retained;next.set(value.id,{incarnation:record.incarnation,ownerCanvasId:record.ownerCanvasId,value:retained}); } return next; } return Object.freeze({ /** owner/project/canvas 改变先清旧表/数组,再分别 retain 节点和边,逐项引用相同才复用整个旧数组。 * 总返回新的投影外壳,nodes/edges 可沿用旧引用;中途抛错可能已改传入数组,不提供事务回滚。 */ reuse(owner:object,document:Document,viewCanvasId:Id,projection:Projection):Projection{ if(scope!==owner||projectId!==document.projectId||canvasId!==viewCanvasId){nodes.clear();edges.clear();nodeArray=undefined;edgeArray=undefined;scope=owner;projectId=document.projectId;canvasId=viewCanvasId;} const nextNodes=retain(document,projection.nodes,nodes),nextEdges=retain(document,projection.edges,edges); nodes=nextNodes;edges=nextEdges;nodeArray=stable(nodeArray,projection.nodes);edgeArray=stable(edgeArray,projection.edges); return{...projection,nodes:nodeArray,edges:edgeArray}; }, /** 读取本轮保留的节点显示对象,供 renderer 测量等显示流程使用。 */ getNode(id:Id){return nodes.get(id)?.value;}, /** 核上下文、incarnation/owner、renderer type、width/height 与 data 等价,决定能否借旧测量。 * 不要求 position/style/整个 node 等价,也不执行 DOM 测量;调用者仍负责应用测量结果的 UI 规则。 */ canRetainMeasurement(owner:object,document:Document,viewCanvasId:Id,value:Projection['nodes'][number]){ const old=nodes.get(value.id),record=document.elements.get(value.id); return scope===owner&&projectId===document.projectId&&canvasId===viewCanvasId&&!!old&&!!record&&old.incarnation===record.incarnation&&old.ownerCanvasId===record.ownerCanvasId&&old.value.type===value.type&&old.value.width===value.width&&old.value.height===value.height&&equal(old.value.data,value.data); }, /** 清空对象/数组和范围身份,调用方无需也不应据此清除业务文档。 */ clear(){nodes.clear();edges.clear();nodeArray=undefined;edgeArray=undefined;scope=undefined;projectId=undefined;canvasId=undefined;}, }); }