Retain visible connection paint beyond endpoint bounds

This commit is contained in:
2026-11-09 18:22:42 +08:00
parent 73b132f71e
commit e6b9999202
6 changed files with 61 additions and 1 deletions
@@ -0,0 +1,28 @@
import type {Registry} from '../canvas-core/types.ts';
import type {projectCanvas} from './projection.ts';
import type {HitServices} from './hit-query.ts';
import type {PaintRegion} from './paint-index.ts';
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. */
export function projectedConnectionPaintOverflow(projection:ReturnType<typeof projectCanvas>,registry:Registry,services:HitServices={},maxEdges=4096,maxSegments=4096){
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');
let remainingSegments=maxSegments;
for(let index=0;index<projection.edges.length;index++){
if(index>=maxEdges){unknown.push('paint_budget');break;}const edge=projection.edges[index];
try{const record=projection.document.elements.get(edge.id),source=nodes.get(edge.source),target=nodes.get(edge.target);if(!record||record.structure!=='connection'||!source||!target)throw Error();
const anchor=(node:typeof source,handle:string|null|undefined,end:'source'|'target')=>services.anchor?services.anchor(node,handle,end,edge):node.data.__headlessRecord?undefined:compatConnector(node,handle,end==='source'?'right':'left');
const from=anchor(source,edge.sourceHandle,'source'),to=anchor(target,edge.targetHandle,'target'),transform=projection.transforms.get(record.ownerCanvasId);if(!from||!to||!transform)throw Error();
const protocol=edge.type!=='unsupported'&&registry.resolve(record)==='known'?registry.components.get(record.componentId)?.geometry:undefined;
const segments=services.edgeGeometry?.(edge,from,to)??(protocol?.kind==='connection'?protocol.segments({element:record,source:from,target:to,displayPath:edge.data?.__headlessRecord?undefined:edge.data?.path??null,transform}):undefined),stroke=services.strokeWidth?.(edge)??(protocol?.kind==='connection'?protocol.strokeWidth({element:record,transform}):undefined);
if(!segments||!Array.isArray(segments)||segments.length>maxSegments||typeof stroke!=='number'||!Number.isFinite(stroke)||stroke<0)throw Error();if(!segments.length)continue;if(segments.length>remainingSegments){unknown.push('paint_budget');break;}remainingSegments-=segments.length;
let left=Infinity,top=Infinity,right=-Infinity,bottom=-Infinity;
for(const segment of segments){if(!Array.isArray(segment)||segment.length!==4)throw Error();for(const point of segment){if(!point||!Number.isFinite(point.x)||!Number.isFinite(point.y))throw Error();left=Math.min(left,point.x);top=Math.min(top,point.y);right=Math.max(right,point.x);bottom=Math.max(bottom,point.y);}}
const half=stroke/2,x=left-half,y=top-half,width=right-left+stroke,height=bottom-top+stroke;if(![x,y,width,height,x+width,y+height].every(Number.isFinite))throw Error();
const native={left:Math.min(source.position.x,target.position.x),top:Math.min(source.position.y,target.position.y),right:Math.max(source.position.x+source.width!,target.position.x+target.width!),bottom:Math.max(source.position.y+source.height!,target.position.y+target.height!)};
if(!Object.values(native).every(Number.isFinite))throw Error();if(x<native.left||y<native.top||x+width>native.right||y+height>native.bottom)regions.push({id:edge.id,bounds:{x,y,width,height}});
}catch{unknown.push(edge.id);}
}
return{regions,unknown};
}
+1
View File
@@ -18,3 +18,4 @@ export {createSessionFrameSource} from './session-frames.ts';
export {createPaintIndex} from './paint-index.ts';
export type {PaintRegion} from './paint-index.ts';
export {projectedPaintOverflow,createProjectedPaintIndex} from './projected-paint.ts';
export {projectedConnectionPaintOverflow} from './connection-paint.ts';