33 lines
5.9 KiB
JavaScript
33 lines
5.9 KiB
JavaScript
/**
|
||
* bindings几何投影缓存测试。用缩放子图与跨层曲线比较缓存投影和直接投影,按计算次数验证局部失效。
|
||
* 统计用于确定性工作量检查,不是浏览器帧率基准;缓存不能把旧查询、旧生命周期或可变预览当成不变输入。
|
||
*/
|
||
import {test} from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {createDocument,createRegistry,builtinComponents,createEditor} from '../../src/lib/canvas-core/index.ts';
|
||
import {createGeometryProjectionCache} from '../../src/lib/canvas-bindings/geometry-cache.ts';
|
||
import {projectCanvas} from '../../src/lib/canvas-bindings/projection.ts';
|
||
// 最小节点/连线工厂保留owner和可选path,以同一registry构造对照。
|
||
const registry=createRegistry(builtinComponents()),node=(id,owner='P',componentId='text')=>({id,incarnation:1,ownerCanvasId:owner,componentId,dataVersion:1,structure:'node',data:{title:id,text:''},geometry:{x:10,y:20,width:200,height:100,rotation:0}}),edge=(id,source,target,owner,path=null)=>({id,incarnation:1,ownerCanvasId:owner,componentId:'connection',dataVersion:1,structure:'connection',data:{},source:{elementId:source},target:{elementId:target},path});
|
||
// P含缩放入口F,S含A/B;in边归P带路径,local边归S,祖先变化应影响不同几何缓存项。
|
||
const document=createDocument('project',[{id:'P',parent:null},{id:'S',parent:{canvasId:'P',entryElementId:'F'}}],[node('C'),{...node('F','P','frame'),data:{title:'F',text:'',presentation:{contentScale:2,unitScale:.75}}},node('A','S'),node('B','S'),edge('in','C','A','P',{type:'bezier-chain',knots:[{id:'k',mode:'corner',position:{x:500,y:200},in:{x:10,y:0},out:{x:20,y:0}}]}),edge('local','A','B','S')]);
|
||
|
||
// 每次draw与直接投影深比较;标题、选择与元数据不增计算,A局部变化只重算自身,F移动再影响后代及路径。
|
||
test('indexed renderer projection preserves legacy output and avoids unrelated geometry work on title, selection and ACK',()=>{
|
||
let id=0;const editor=createEditor({document,registry,createId:()=>`operation-${++id}`,runtime:0}),cache=createGeometryProjectionCache(),owner={},selected=new Set(),draw=()=>{const doc=editor.getSnapshot().document,actual=projectCanvas(doc,'P',registry,selected,()=>undefined,null,undefined,{queries:editor.getQueries(doc),geometry:cache,owner});assert.deepEqual(actual,projectCanvas(doc,'P',registry,selected,()=>undefined));return actual;};
|
||
draw();const before=cache.stats();assert.deepEqual({nodes:before.nodesComputed,children:before.childrenComputed,edges:before.edgesComputed},{nodes:4,children:1,edges:2});editor.dispatch({type:'element.data',target:{id:'A',incarnation:1},patch:{title:'only title'}});draw();assert.deepEqual(cache.stats(),before);selected.add('B');draw();assert.deepEqual(cache.stats(),before);editor.updateRuntime(()=>1);draw();assert.deepEqual(cache.stats(),before);
|
||
editor.dispatch({type:'element.geometry',target:{id:'A',incarnation:1},patch:{x:300,rotation:25}});draw();assert.equal(cache.stats().nodesComputed,before.nodesComputed+1);assert.equal(cache.stats().childrenComputed,before.childrenComputed);assert.equal(cache.stats().edgesComputed,before.edgesComputed);
|
||
editor.dispatch({type:'element.geometry',target:{id:'F',incarnation:1},patch:{x:200}});draw();assert.equal(cache.stats().nodesComputed,before.nodesComputed+4);assert.equal(cache.stats().childrenComputed,before.childrenComputed+1);assert.equal(cache.stats().edgesComputed,before.edgesComputed+1);
|
||
});
|
||
|
||
// 折叠清不可见缓存,旧query拒绝;预览进入/退出各重算,删除恢复incarnation=2后重新出现,不能沿用旧记录身份。
|
||
test('geometry cache prunes collapsed/deleted data, respects previews/lifecycles and rejects stale query snapshots',()=>{
|
||
let id=0;const editor=createEditor({document,registry,createId:()=>`operation-${++id}`}),cache=createGeometryProjectionCache(),owner={},draw=(geometry=()=>undefined)=>{const doc=editor.getSnapshot().document;return projectCanvas(doc,'P',registry,new Set(),geometry,null,undefined,{queries:editor.getQueries(doc),geometry:cache,owner});};draw();const old=editor.getQueries();editor.dispatch({type:'element.data',target:{id:'F',incarnation:1},patch:{presentation:{contentScale:2,unitScale:.75,collapsed:true}}});draw();assert.equal(cache.stats().nodes,2);assert.equal(cache.stats().children,0);assert.equal(cache.stats().edges,1);assert.throws(()=>projectCanvas(editor.getSnapshot().document,'P',registry,new Set(),()=>undefined,null,undefined,{queries:old}),/stale_query_snapshot/);
|
||
editor.history.undo();draw();assert.equal(cache.stats().nodes,4);const before=cache.stats().nodesComputed,preview={...editor.getSnapshot().document.elements.get('A').geometry,x:900};draw(id=>id==='A'?preview:undefined);assert.equal(cache.stats().nodesComputed,before+1);draw();assert.equal(cache.stats().nodesComputed,before+2);editor.dispatch({type:'element.remove',target:{id:'A',incarnation:1}});draw();assert.equal(cache.stats().nodes,3);editor.history.undo();draw();assert.equal(cache.stats().nodes,4);assert.equal(editor.getSnapshot().document.elements.get('A').incarnation,2);
|
||
});
|
||
|
||
// 原path后来修改不改变first结果,新输入按scale=2/x偏移1得到61;输出深冻结,换owner清旧边缓存。
|
||
test('path cache snapshots mutable preview inputs and geometry cache cannot be corrupted through returned values',()=>{
|
||
const cache=createGeometryProjectionCache(),owner={},t={x:1,y:2,scale:2,rotation:0},path={type:'bezier-chain',knots:[{id:'k',mode:'corner',position:{x:10,y:20},in:{x:0,y:0},out:{x:0,y:0}}]};cache.begin(owner,'project','P');const first=cache.edge('e',1,path,t);path.knots[0].position.x=30;const second=cache.edge('e',1,path,t);assert.notEqual(first,second);assert.equal(second.knots[0].position.x,61);assert.equal(first.knots[0].position.x,21);assert.ok(Object.isFrozen(second.knots[0].position));cache.end();cache.begin({},'project','P');cache.node('A',1,node('A').geometry,t);cache.end();assert.equal(cache.stats().edges,0);
|
||
});
|