Retain projected paint geometry and grid across view updates

This commit is contained in:
2026-11-09 17:47:44 +08:00
parent c6d77bdefd
commit 46fabafca8
5 changed files with 36 additions and 6 deletions
+1 -1
View File
@@ -17,4 +17,4 @@ export type {FrameClock} from './frame-queue.ts';
export {createSessionFrameSource} from './session-frames.ts';
export {createPaintIndex} from './paint-index.ts';
export type {PaintRegion} from './paint-index.ts';
export {projectedPaintOverflow} from './projected-paint.ts';
export {projectedPaintOverflow,createProjectedPaintIndex} from './projected-paint.ts';
+22 -1
View File
@@ -2,7 +2,7 @@ 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 type {PaintRegion} from './paint-index.ts';
import {createPaintIndex,type PaintRegion} from './paint-index.ts';
/** Extra node paint extents beyond native unrotated React Flow bounds. */
export function projectedPaintOverflow(projection:ReturnType<typeof projectCanvas>,registry:Registry){
const regions:PaintRegion[]=[],unknown:string[]=[];
@@ -17,3 +17,24 @@ export function projectedPaintOverflow(projection:ReturnType<typeof projectCanva
}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<typeof projectCanvas>;
type Entry={node:Projection['nodes'][number];element:unknown;transform:string;result:ReturnType<typeof projectedPaintOverflow>};
const entries=new Map<string,Entry>(),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<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),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;},
});
}