Files
Endoleap/src/lib/canvas-bindings/projected-paint.ts
T

82 lines
8.3 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.
/**
* 节点额外绘制范围的保守索引。原生 renderer 的未旋转矩形不一定包含旋转/自定义组件的全部笔画,
* 因此用 geometry.paint bounds 补充超出的范围;无法解释的节点标 unknown,不能当作已证明不可见。
*/
import {clone,equal} from '../canvas-core/values.ts';
import {rectangleGeometry} from '../canvas-core/geometry.ts';
import {toLocal,toWorld,type Transform} from '../canvas-core/coordinates.ts';
import type {ElementRecord,Geometry,GeometryProtocol,Registry} from '../canvas-core/types.ts';
import type {projectCanvas,ProjectionNode} from './projection.ts';
import {createPaintIndex,type PaintRegion} from './paint-index.ts';
/** Extra node paint extents beyond native unrotated React Flow bounds. */
/** 对每个显示节点还原局部几何,调用组件 paint 边界,再把四角变到场景取轴对齐包围盒。
* 只记录超出原生矩形的节点;单节点协议失败记 unknown,其余节点继续处理。 */
export function projectedPaintOverflow(projection:ReturnType<typeof projectCanvas>,registry:Registry){
const regions:PaintRegion[]=[],unknown:string[]=[];
for(const node of projection.nodes)try{
const element=projection.document.elements.get(node.id),transform=element&&projection.transforms.get(element.ownerCanvasId);if(!element||element.structure!=='node'||!transform)throw Error('missing_geometry');
const width=node.width!/transform.scale,height=node.height!/transform.scale,center=toLocal(transform,{x:node.position.x+node.width!/2,y:node.position.y+node.height!/2}),geometry={x:center.x-width/2,y:center.y-height/2,width,height,rotation:((node.data.rotation??0)-transform.rotation+360)%360};
// 未知/降级节点用标准矩形边界;有效组件可声明更宽的笔画或旋转后的范围。
const definition=!node.data.__headlessFallback&&registry.resolve(element)==='known'?registry.components.get(element.componentId)?.geometry:undefined,protocol=definition?.kind==='node'?definition:rectangleGeometry;
const local=protocol.bounds({element,geometry,role:'paint'});if(![local.x,local.y,local.width,local.height].every(Number.isFinite)||local.width<0||local.height<0)throw Error('invalid_paint_bounds');
// 局部边界再经分区变换,必须考虑父级旋转/缩放,不能只给原矩形加一个固定像素边距。
const points=[{x:local.x,y:local.y},{x:local.x+local.width,y:local.y},{x:local.x,y:local.y+local.height},{x:local.x+local.width,y:local.y+local.height}].map(p=>toWorld(transform,p)),x=Math.min(...points.map(p=>p.x)),y=Math.min(...points.map(p=>p.y)),right=Math.max(...points.map(p=>p.x)),bottom=Math.max(...points.map(p=>p.y));
if(![x,y,right,bottom,right-x,bottom-y].every(Number.isFinite))throw Error('invalid_paint_bounds');
if(x<node.position.x||y<node.position.y||right>node.position.x+node.width!||bottom>node.position.y+node.height!)regions.push({id:node.id,bounds:{x,y,width:right-x,height:bottom-y}});
}catch{unknown.push(node.id);}
return{regions,unknown};
}
/** 只接节点几何协议;连线范围有独立的曲线/线宽路径。 */
type NodeProtocol=Extract<GeometryProtocol,{kind:'node'}>;
/** 缓存签名覆盖生命周期、模型几何、显示预览、变换、协议身份和其声明的数据依赖。 */
type PaintSignature={incarnation?:number;owner?:string;component?:string;version?:number;geometry?:Geometry;transform?:Transform;x:number;y:number;width?:number;height?:number;rotation:unknown;protocol:NodeProtocol;data:unknown};
/** 可缺失几何按全数值比较,外部对象换引用不等于显示几何改变。 */
const sameGeometry=(a:Geometry|undefined,b:Geometry|undefined)=>a===undefined?b===undefined:b!==undefined&&a.x===b.x&&a.y===b.y&&a.width===b.width&&a.height===b.height&&a.rotation===b.rotation;
/** 变换值比较避免无关根发布反复计算 paint 边界。 */
const sameTransform=(a:Transform|undefined,b:Transform|undefined)=>a===undefined?b===undefined:b!==undefined&&a.x===b.x&&a.y===b.y&&a.scale===b.scale&&a.rotation===b.rotation;
/** 验证缓存输入是否等价;协议未声明 dataDependencies 时保守比较整个 element.data,
* 显式声明后只比较列出的字段,声明遗漏会由协议实现者负责,不能由缓存自动推导。 */
function sameSignature(old:PaintSignature,node:ProjectionNode,element:ElementRecord|undefined,t:Transform|undefined,protocol:NodeProtocol){
if(old.incarnation!==element?.incarnation||old.owner!==element?.ownerCanvasId||old.component!==element?.componentId||old.version!==element?.dataVersion||old.protocol!==protocol||!sameGeometry(old.geometry,element?.structure==='node'?element.geometry:undefined)||!sameTransform(old.transform,t)||old.x!==node.position.x||old.y!==node.position.y||old.width!==node.width||old.height!==node.height||old.rotation!==node.data.rotation)return false;
const dependencies=protocol.dataDependencies;
if(dependencies===undefined)return equal(old.data,element?.data);
const values=old.data as unknown[];
for(let i=0;i<dependencies.length;i++)if(!equal(values[i],element?.data[dependencies[i]]))return false;
return true;
}
/** 复制实际依赖值而非持有调用方可变子对象;后续比较用捕获值发现内容变化。 */
function captureSignature(node:ProjectionNode,element:ElementRecord|undefined,t:Transform|undefined,protocol:NodeProtocol):PaintSignature{
const dependencies=protocol.dataDependencies;
return{incarnation:element?.incarnation,owner:element?.ownerCanvasId,component:element?.componentId,version:element?.dataVersion,geometry:element?.structure==='node'?{...element.geometry}:undefined,transform:t?{...t}:undefined,x:node.position.x,y:node.position.y,width:node.width,height:node.height,rotation:node.data.rotation,protocol,data:dependencies===undefined?clone(element?.data):dependencies.map(key=>clone(element?.data[key]))};
}
/** View-owned incremental paint grid; unchanged rows retain their captured dependencies. */
/** 视图级增量 paint 网格,每个节点缓存边界结果,只重新计算签名改变的条目。 */
export function createProjectedPaintIndex(){
let registryOwner:Registry|undefined,canvas:string|undefined,project:string|undefined,computed=0,captured=0;
type Projection=ReturnType<typeof projectCanvas>;
type Entry={signature:PaintSignature;result:ReturnType<typeof projectedPaintOverflow>};
const entries=new Map<string,Entry>(),index=createPaintIndex();
return Object.freeze({
/** 切 registry/project/canvas 清空缓存;逐节点收集额外范围与 unknown,再同步网格。 */
sync(projection:Projection,registry:Registry){
if(registryOwner!==registry||canvas!==projection.canvasId||project!==projection.document.projectId){entries.clear();index.clear();registryOwner=registry;canvas=projection.canvasId;project=projection.document.projectId;}
const seen=new Set<string>(),regions:PaintRegion[]=[],unknown:string[]=[];
for(const node of projection.nodes){seen.add(node.id);const element=projection.document.elements.get(node.id),t=element&&projection.transforms.get(element.ownerCanvasId),old=entries.get(node.id),declared=element&&!node.data.__headlessFallback&&registry.resolve(element)==='known'?registry.components.get(element.componentId)?.geometry:undefined,protocol=declared?.kind==='node'?declared:rectangleGeometry;
let entry=old;
// 单节点计算复用完整算法,保证增量缓存与无缓存查询使用相同几何规则。
if(!entry||!sameSignature(entry.signature,node,element,t,protocol)){
const signature=captureSignature(node,element,t,protocol),result=projectedPaintOverflow({...projection,nodes:[node]},registry);captured++;computed++;entry={signature,result};entries.set(node.id,entry);
}
regions.push(...entry.result.regions);unknown.push(...entry.result.unknown);
}
// 删除已不在场景中的缓存,返回可查询索引与冻结的未知列表;调用方须保守处理 unknown。
for(const id of entries.keys())if(!seen.has(id))entries.delete(id);index.sync(regions);return Object.freeze({query:index.query,unknown:Object.freeze(unknown)});
},
/** 累计实际计算/捕获次数和当前项数,仅供诊断,不是帧耗时或文档 revision。 */
stats:()=>Object.freeze({computed,captured,entries:entries.size}),
/** 解除场景与 registry 身份引用并清空网格;累计诊断计数仍保留。 */
clear(){entries.clear();index.clear();registryOwner=undefined;canvas=undefined;project=undefined;},
});
}