Files
Endoleap/src/lib/canvas-bindings/projection-cache.ts
T

61 lines
5.4 KiB
TypeScript

/**
* 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<typeof projectCanvas>;
/** 原对象的生命周期证据;同 ID 的重建或移动 owner 后不能直接继承旧显示对象。 */
type Retained<T>={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=<T>(previous:T[]|undefined,next:T[])=>previous?.length===next.length&&next.every((value,index)=>value===previous[index])?previous:next;
let nodes=new Map<Id,Retained<Projection['nodes'][number]>>(),edges=new Map<Id,Retained<Projection['edges'][number]>>();
/** 每个显示 ID 须有文档记录;生命周期键只核 incarnation+owner,不另含 component/dataVersion,显示差异由值比较处理。
* 仅 data/style/position 三个嵌套字段单独等价共享,再比较整个显示对象;新表只含本帧项。
* 不查投影 ID 重复、不检查 node/edge 与记录的全部对应语义;这依赖上游合法投影。 */
function retain<T extends{id:Id}>(document:Document,values:T[],previous:Map<Id,Retained<T>>){
const next=new Map<Id,Retained<T>>();
for(let index=0;index<values.length;index++){
const value=values[index],record=document.elements.get(value.id);if(!record)throw fault('projection_identity_missing',[value.id]);
const old=previous.get(value.id),sameLifetime=old&&old.incarnation===record.incarnation&&old.ownerCanvasId===record.ownerCanvasId;
if(sameLifetime&&old.value===value){next.set(value.id,old);continue;}
let shared=value;
// 常见嵌套字段独立共享,使其它显示属性变化时也能保留未变化的 data/style/position。
if(sameLifetime)for(const key of ['data','style','position'] as const){const incoming=(shared as Record<string,unknown>)[key],before=(old.value as Record<string,unknown>)[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;},
});
}