Limit connection paint node indexes to inspected endpoints

This commit is contained in:
2026-11-10 14:34:36 +08:00
parent aa72f9e484
commit 6b3986f89f
2 changed files with 17 additions and 1 deletions
+5 -1
View File
@@ -7,8 +7,12 @@ import {compatConnector} from './connectors.ts';
/** Conservative cubic control hull + half stroke, in the displayed scene space.
* Missing/custom geometry stays unknown so a host cannot interpret it as invisible. */
function connectionPaint(projection:ReturnType<typeof projectCanvas>,registry:Registry,services:HitServices={},maxEdges=4096,maxSegments=4096,cache?:Map<string,{signature:unknown;segments:number;region?:PaintRegion}>){
const regions:PaintRegion[]=[],unknown:string[]=[],nodes=new Map(projection.nodes.map(node=>[node.id,node]));
if(![maxEdges,maxSegments].every(n=>Number.isSafeInteger(n)&&n>0&&n<=1000000))throw Error('invalid_paint_budget');
const regions:PaintRegion[]=[],unknown:string[]=[],nodes=new Map<string,typeof projection.nodes[number]>(),endpoints=new Set<string>();
// Only inspected edges need node entries. Preserve last-ID-wins behavior without
// allocating an entry and pair array for every unrelated projected node.
for(let i=0;i<Math.min(maxEdges,projection.edges.length);i++){const edge=projection.edges[i];endpoints.add(edge.source);endpoints.add(edge.target);}
if(endpoints.size)for(const node of projection.nodes)if(endpoints.has(node.id))nodes.set(node.id,node);
let remainingSegments=maxSegments,computed=0;
for(let index=0;index<projection.edges.length;index++){
if(index>=maxEdges){unknown.push('paint_budget');break;}const edge=projection.edges[index];
+12
View File
@@ -12,3 +12,15 @@ import {createConnectionPaintIndex} from '../../src/lib/canvas-bindings/connecti
test('connection geometry cache skips equal ACKs and unrelated titles, invalidates measured anchors, stroke configuration and lifecycle',()=>{const cache=createConnectionPaintIndex();let geometryCalls=0,shift=0,width=2;const services={anchor:(node,_handle,end)=>({point:{x:node.position.x+(end==='source'?100:0)+shift,y:550},normal:{x:end==='source'?1:-1,y:0}}),edgeGeometry:(_edge,from,to)=>{geometryCalls++;return[[from.point,{x:200,y:550},{x:400,y:550},to.point]];},strokeWidth:()=>width};cache.sync(draw(),registry,services);assert.equal(geometryCalls,1);cache.sync(draw(),registry,services);assert.equal(geometryCalls,1);const p=draw();p.nodes[0].data.title='unrelated node title';cache.sync(p,registry,services);assert.equal(geometryCalls,1);shift=10;cache.sync(draw(),registry,services);assert.equal(geometryCalls,2);width=20;cache.sync(draw(),registry,services,'new-stroke-config');assert.equal(geometryCalls,3);const restored=projectCanvas(createDocument('p',[{id:'p',parent:null}],[node('a',0),node('b',500),{...edge(),incarnation:2}]),'p',registry,new Set(),()=>undefined);cache.sync(restored,registry,services,'new-stroke-config');assert.equal(geometryCalls,4);cache.sync({...restored,edges:[]},registry,services,'new-stroke-config');assert.equal(cache.stats().entries,0);});
test('cached segment costs still respect a smaller budget instead of claiming complete coverage',()=>{const cache=createConnectionPaintIndex(),projection=draw([edge('one'),edge('two')]),services={edgeGeometry:()=>[[{x:100,y:550},{x:200,y:550},{x:400,y:550},{x:500,y:550}]],strokeWidth:()=>2};cache.sync(projection,registry,services);const computed=cache.stats().computed,limited=cache.sync(projection,registry,services,undefined,{maxSegments:1});assert.ok(limited.unknown.includes('paint_budget'));assert.equal(cache.stats().computed,computed);cache.clear();assert.equal(cache.stats().entries,0);});
test('connection preview paths invalidate paint even while the committed connection record stays unchanged',()=>{const cache=createConnectionPaintIndex(),original=draw(),nearBow={x:250,y:-50,width:100,height:100};assert.equal(cache.sync(original,registry).query(nearBow).size,0);const preview=draw();preview.edges[0].data.path={type:'bezier-chain',knots:[{id:'preview-knot',mode:'corner',position:{x:300,y:-1000},in:{x:-100,y:0},out:{x:100,y:0}}]};const shown=cache.sync(preview,registry);assert.ok(shown.query(nearBow).has('edge'));assert.equal(preview.document.elements.get('edge').path,null);assert.equal(cache.sync(draw(),registry).query(nearBow).size,0);assert.equal(cache.stats().computed,3);});
test('an empty connection projection does not enumerate unrelated nodes',()=>{
const projection=draw();projection.edges=[];projection.nodes=new Proxy(projection.nodes,{get(target,key,receiver){if(key===Symbol.iterator||key==='map')throw Error('unrelated nodes enumerated');return Reflect.get(target,key,receiver);}});
assert.deepEqual(projectedConnectionPaintOverflow(projection,registry),{regions:[],unknown:[],computed:0});assert.equal(createConnectionPaintIndex().sync(projection,registry).unknown.length,0);
});
test('endpoint filtering preserves duplicate IDs, mutable replacement, missing targets and edge budgets',()=>{
const projection=draw([edge('one'),edge('two')]),last={...projection.nodes[0],position:{x:900,y:500}},positions=[];
projection.nodes.push(last);const services={anchor:(node,_handle,end)=>{positions.push(node.position.x);return{point:{x:node.position.x,y:550},normal:{x:end==='source'?1:-1,y:0}};}};
assert.deepEqual(projectedConnectionPaintOverflow(projection,registry,services,1).unknown,['paint_budget']);assert.deepEqual(positions,[900,500]);
projection.nodes[projection.nodes.length-1]={...last,position:{x:1200,y:500}};positions.length=0;projectedConnectionPaintOverflow(projection,registry,services,1);assert.deepEqual(positions,[1200,500]);
projection.nodes=projection.nodes.filter(node=>node.id!=='b');assert.deepEqual(projectedConnectionPaintOverflow(projection,registry).unknown,['one','two']);
});