import {rectangleGeometry} from '../canvas-core/geometry.ts'; import {toLocal,toWorld} from '../canvas-core/coordinates.ts'; import type {Registry} from '../canvas-core/types.ts'; import type {projectCanvas} from './projection.ts'; import {createPaintIndex,type PaintRegion} from './paint-index.ts'; /** Extra node paint extents beyond native unrotated React Flow bounds. */ export function projectedPaintOverflow(projection:ReturnType,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(xnode.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}; } /** View-owned incremental paint grid; reuse unaffected node results across Root/session updates. */ export function createProjectedPaintIndex(){ let registryOwner:Registry|undefined,canvas:string|undefined,project:string|undefined,computed=0; type Projection=ReturnType; type Entry={node:Projection['nodes'][number];element:unknown;transform:string;result:ReturnType}; const entries=new Map(),index=createPaintIndex(); return Object.freeze({ 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(),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),transform=t?`${t.x},${t.y},${t.scale},${t.rotation}`:'missing',old=entries.get(node.id); const result=old&&old.node===node&&old.element===element&&old.transform===transform?old.result:projectedPaintOverflow({...projection,nodes:[node]},registry); if(result!==old?.result)computed++;entries.set(node.id,{node,element,transform,result});regions.push(...result.regions);unknown.push(...result.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)}); }, stats:()=>Object.freeze({computed,entries:entries.size}), clear(){entries.clear();index.clear();registryOwner=undefined;canvas=undefined;project=undefined;}, }); }