82 lines
8.3 KiB
TypeScript
82 lines
8.3 KiB
TypeScript
/**
|
||
* 节点额外绘制范围的保守索引。原生 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&®istry.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&®istry.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;},
|
||
});
|
||
}
|