Reuse unchanged projection arrays and limit extra viewport retention

This commit is contained in:
2026-11-09 18:45:57 +08:00
parent 0813b8df53
commit a9bc6e7d29
7 changed files with 39 additions and 9 deletions
@@ -12,6 +12,7 @@ import { equal } from "@/lib/canvas-core/values";
import {createGeometryProjectionCache} from "@/lib/canvas-bindings/geometry-cache";
import {createPaintIndex} from "@/lib/canvas-bindings/paint-index";
import {projectedConnectionPaintOverflow} from "@/lib/canvas-bindings/connection-paint";
import {nativeRectVisible} from "@/lib/canvas-bindings/visibility";
import {createOverlayIndex} from "@/lib/canvas-bindings/overlays";
import {CanvasOverlays} from "./canvas-overlays";
import {createProjectedPaintIndex} from "@/lib/canvas-bindings/projected-paint";
@@ -82,11 +83,14 @@ function CanvasView({ engine, board, viewId, renderers,fitLoading=true }: { engi
const overlayScope=useMemo(()=>({index:overlays,engine}),[overlays,engine]);
const overlaySnapshot=useSyncExternalStore(overlays.subscribe,overlays.getSnapshot,overlays.getSnapshot);
const surfaceBounds=surface.current?.getBoundingClientRect();
const overlayVisible=!!overlaySnapshot.length&&(!surfaceBounds||overlays.visible(surfaceBounds,target=>root.document.elements.get(target.id)?.incarnation===target.incarnation).length>0);
const paintCache=useMemo(()=>createProjectedPaintIndex(),[engine,view.canvasId]);
const paint=useMemo(()=>paintCache.sync(projection,engine.editor.registry),[paintCache,projection,engine]);
const viewport=useViewport(),surfaceWidth=useStore(state=>state.width),surfaceHeight=useStore(state=>state.height);
const paintVisible=paint.unknown.length>0||!surfaceWidth||!surfaceHeight||paint.query({x:-viewport.x/viewport.zoom,y:-viewport.y/viewport.zoom,width:surfaceWidth/viewport.zoom,height:surfaceHeight/viewport.zoom}).size>0;
const sceneViewport={x:-viewport.x/viewport.zoom,y:-viewport.y/viewport.zoom,width:surfaceWidth/viewport.zoom,height:surfaceHeight/viewport.zoom};
const nativeVisible=(id:Id)=>{const node=flow.getInternalNode(id);if(!node||!node.internals.handleBounds||(node as typeof node&{dragging?:boolean}).dragging)return true;return nativeRectVisible({x:node.internals.positionAbsolute.x,y:node.internals.positionAbsolute.y,width:node.measured.width??node.width??0,height:node.measured.height??node.height??0},sceneViewport);};
const overlayVisible=!!overlaySnapshot.length&&(!surfaceBounds||overlays.visible(surfaceBounds,target=>root.document.elements.get(target.id)?.incarnation===target.incarnation).some(entry=>!nativeVisible(entry.target.id)));
const paintVisible=!surfaceWidth||!surfaceHeight||paint.unknown.some(id=>!nativeVisible(id))||[...paint.query(sceneViewport)].some(id=>!nativeVisible(id));
const nativeNodes=useStore(state=>state.nodes);
const connectionPaint=useMemo(()=>{const result=projectedConnectionPaintOverflow(projection,engine.editor.registry,{anchor:(node,handle,end,edge)=>{const measured=flow.getInternalNode(node.id);return node.data.__headlessRecord||edge.type==='unsupported'?measuredConnector(measured,handle,end):compatConnector(node,handle,end==='source'?'right':'left');}}),index=createPaintIndex();index.sync(result.regions);return{index,unknown:result.unknown};},[projection,engine,flow,nativeNodes]);
const connectionPaintVisible=connectionPaint.unknown.length>0||!surfaceWidth||!surfaceHeight||connectionPaint.index.query({x:-viewport.x/viewport.zoom,y:-viewport.y/viewport.zoom,width:surfaceWidth/viewport.zoom,height:surfaceHeight/viewport.zoom}).size>0;
+7 -4
View File
@@ -7,6 +7,8 @@ type Retained<T>={incarnation:number;ownerCanvasId:Id;value:T};
/** View-owned rendering cache, never a document store. Keep unchanged React Flow input identities. */
export function createProjectionCache(){
let scope:object|undefined,projectId:Id|undefined,canvasId:Id|undefined;
let nodeArray:Projection['nodes']|undefined,edgeArray:Projection['edges']|undefined;
const stable=<T>(previous:T[]|undefined,next:T[])=>previous?.length===next.length&&next.every((value,index)=>value===previous[index])?previous:next;
let nodes=new Map<Id,Retained<Projection['nodes'][number]>>(),edges=new Map<Id,Retained<Projection['edges'][number]>>();
function retain<T extends{id:Id}>(document:Document,values:T[],previous:Map<Id,Retained<T>>){
const next=new Map<Id,Retained<T>>();
@@ -19,15 +21,16 @@ export function createProjectionCache(){
}
return Object.freeze({
reuse(owner:object,document:Document,viewCanvasId:Id,projection:Projection):Projection{
if(scope!==owner||projectId!==document.projectId||canvasId!==viewCanvasId){nodes.clear();edges.clear();scope=owner;projectId=document.projectId;canvasId=viewCanvasId;}
nodes=retain(document,projection.nodes,nodes);edges=retain(document,projection.edges,edges);
return projection;
if(scope!==owner||projectId!==document.projectId||canvasId!==viewCanvasId){nodes.clear();edges.clear();nodeArray=undefined;edgeArray=undefined;scope=owner;projectId=document.projectId;canvasId=viewCanvasId;}
const nextNodes=retain(document,projection.nodes,nodes),nextEdges=retain(document,projection.edges,edges);
nodes=nextNodes;edges=nextEdges;nodeArray=stable(nodeArray,projection.nodes);edgeArray=stable(edgeArray,projection.edges);
return{...projection,nodes:nodeArray,edges:edgeArray};
},
getNode(id:Id){return nodes.get(id)?.value;},
canRetainMeasurement(owner:object,document:Document,viewCanvasId:Id,value:Projection['nodes'][number]){
const old=nodes.get(value.id),record=document.elements.get(value.id);
return scope===owner&&projectId===document.projectId&&canvasId===viewCanvasId&&!!old&&!!record&&old.incarnation===record.incarnation&&old.ownerCanvasId===record.ownerCanvasId&&old.value.type===value.type&&old.value.width===value.width&&old.value.height===value.height&&equal(old.value.data,value.data);
},
clear(){nodes.clear();edges.clear();scope=undefined;projectId=undefined;canvasId=undefined;},
clear(){nodes.clear();edges.clear();nodeArray=undefined;edgeArray=undefined;scope=undefined;projectId=undefined;canvasId=undefined;},
});
}
+3
View File
@@ -0,0 +1,3 @@
import type {Rect} from '../canvas-core/geometry.ts';
/** Positive-area native node visibility. Degenerate nodes are retained for measurement. */
export function nativeRectVisible(node:Rect,viewport:Rect){if(node.width<=0||node.height<=0)return true;return Math.min(node.x+node.width,viewport.x+viewport.width)>Math.max(node.x,viewport.x)&&Math.min(node.y+node.height,viewport.y+viewport.height)>Math.max(node.y,viewport.y);}