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

37 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 视图局部的数值/路径投影缓存。按元素代次、几何和变换值复用计算,既不缓存可编辑记录,也不决定可见性。
* 每次 begin/end 标记本帧访问项,剔除不再出现的条目;切换 scope/project/canvas 时整体清空。
*/
import type {Geometry,Id,Json} from '../canvas-core/types.ts';
import {childCanvasTransform,toWorld,transformPath,type Transform} from '../canvas-core/coordinates.ts';
import {equal,clone,freeze} from '../canvas-core/values.ts';
/** 仅比较投影使用的四个变换量,普通对象引用变化不必导致重新计算。 */
const sameTransform=(a:Transform,b:Transform)=>a.x===b.x&&a.y===b.y&&a.scale===b.scale&&a.rotation===b.rotation;
/** 节点几何的全部数值字段都参与缓存条件。 */
const sameGeometry=(a:Geometry,b:Geometry)=>a.x===b.x&&a.y===b.y&&a.width===b.width&&a.height===b.height&&a.rotation===b.rotation;
/** 子画布变换只依赖 presentation 中 contentScale/unitScale,忽略无关样式字段。 */
const units=(value:Json|undefined)=>value&&typeof value==='object'&&!Array.isArray(value)?[value.contentScale,value.unitScale]:[];
/** View-local numeric/path projection only. No editable records, history, visibility or task facts. */
/** 缓存实例应归一个显示上下文;计数器累计实际计算次数供性能诊断,不是模型版本。 */
export function createGeometryProjectionCache(){
let owner:object|undefined,project:Id|undefined,canvas:Id|undefined,nodesComputed=0,childrenComputed=0,edgesComputed=0;
type NodeResult={position:{x:number;y:number};width:number;height:number;rotation:number};
const nodes=new Map<Id,{incarnation:number;geometry:Geometry;transform:Transform;value:NodeResult}>(),children=new Map<Id,{incarnation:number;geometry:Geometry;transform:Transform;units:unknown;value:Transform}>(),edges=new Map<Id,{incarnation:number;path:Json;transform:Transform;value:Json}>();let seenNodes=new Set<Id>(),seenChildren=new Set<Id>(),seenEdges=new Set<Id>();
return Object.freeze({
/** 开始一轮投影并重置 seen;owner 身份隔离不同 renderer/editor,即使 projectId 相同也不能混用。 */
begin(scope:object,projectId:Id,canvasId:Id){if(owner!==scope||project!==projectId||canvas!==canvasId){nodes.clear();children.clear();edges.clear();owner=scope;project=projectId;canvas=canvasId;}seenNodes=new Set();seenChildren=new Set();seenEdges=new Set();},
/** 将节点中心转到场景,按变换比例缩放宽高并合成旋转;缓存复制输入数值防止外部修改污染键。 */
node(id:Id,incarnation:number,g:Geometry,t:Transform):NodeResult{seenNodes.add(id);const before=nodes.get(id);if(before&&before.incarnation===incarnation&&sameGeometry(before.geometry,g)&&sameTransform(before.transform,t))return before.value;const center=toWorld(t,{x:g.x+g.width/2,y:g.y+g.height/2}),width=g.width*t.scale,height=g.height*t.scale,value=freeze({position:{x:center.x-width/2,y:center.y-height/2},width,height,rotation:(g.rotation+t.rotation)%360});nodes.set(id,{incarnation,geometry:{...g},transform:{...t},value});nodesComputed++;return value;},
/** 计算分区内部坐标到场景的变换,单位比例变化也会使缓存失效。 */
child(id:Id,incarnation:number,g:Geometry,t:Transform,presentation:Json|undefined):Transform{seenChildren.add(id);const before=children.get(id),currentUnits=units(presentation);if(before&&before.incarnation===incarnation&&sameGeometry(before.geometry,g)&&sameTransform(before.transform,t)&&equal(before.units,currentUnits))return before.value;const value=freeze(childCanvasTransform(t,g,presentation));children.set(id,{incarnation,geometry:{...g},transform:{...t},units:currentUnits,value});childrenComputed++;return value;},
/** JSON 路径按值比较;先复制/冻结输入再投影,不持有调用方可变路径作为历史缓存。 */
edge(id:Id,incarnation:number,path:Json,t:Transform):Json{seenEdges.add(id);const before=edges.get(id);if(before&&before.incarnation===incarnation&&equal(before.path,path)&&sameTransform(before.transform,t))return before.value;const captured=freeze(clone(path)),value=freeze(transformPath(captured,t));edges.set(id,{incarnation,path:captured,transform:{...t},value});edgesComputed++;return value;},
/** 删除本轮未访问的缓存项,避免浏览多个分区后无限保留旧节点和路径。 */
end(){for(const id of nodes.keys())if(!seenNodes.has(id))nodes.delete(id);for(const id of children.keys())if(!seenChildren.has(id))children.delete(id);for(const id of edges.keys())if(!seenEdges.has(id))edges.delete(id);},
/** 返回当前缓存大小和累计计算量的冻结快照;begin 不重置累计计数。 */
stats:()=>Object.freeze({nodesComputed,childrenComputed,edgesComputed,nodes:nodes.size,children:children.size,edges:edges.size}),
});
}
/** 由工厂推导的缓存接口,供投影器注入复用。 */
export type GeometryProjectionCache=ReturnType<typeof createGeometryProjectionCache>;