Reuse existing scene projections when inserting ordinary canvas nodes
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type {DocumentQueries} from '../canvas-core/queries.ts';
|
||||
import type {Document,Id,Registry} from '../canvas-core/types.ts';
|
||||
import type {SessionSnapshot,ViewState} from '../canvas-core/gestures.ts';
|
||||
import {equal} from '../canvas-core/values.ts';
|
||||
@@ -14,12 +15,12 @@ export function indexScene(projection:Projection){
|
||||
for(const id of projection.transforms.keys())if(id!==projection.canvasId){const parent=projection.document.canvases.get(id)?.parent;if(parent&&depths.has(parent.canvasId))depths.set(id,depths.get(parent.canvasId)!+1);}
|
||||
return{nodes,edges,entries,depths,unsupported:projection.nodes.some(node=>!!node.data.__headlessFallback)||projection.edges.some(edge=>edge.type==='unsupported')};
|
||||
}
|
||||
/** A checked fixed-layout update. Structural/visibility changes keep the complete traversal. */
|
||||
export function patchScene(base:Projection,layout:ReturnType<typeof indexScene>,next:SceneInputs,previous:SceneInputs,connection:SessionSnapshot['connectionGesture'],hint?:SceneChangeHint):Projection|undefined{
|
||||
/** Reuse existing projections for checked field edits and ordinary node insertions. */
|
||||
export function patchScene(base:Projection,layout:ReturnType<typeof indexScene>,next:SceneInputs,previous:SceneInputs,connection:SessionSnapshot['connectionGesture'],hint?:SceneChangeHint,queries?:DocumentQueries):Projection|undefined{
|
||||
const {document,selection,preview,registry,catalog,canvasId}=next;
|
||||
const {selection:oldSelection,preview:oldPreview,edgeId:oldEdgeId,path:oldPath}=previous;
|
||||
if(layout.unsupported||document.projectId!==base.document.projectId)return;
|
||||
const dirty=new Set<Id>();
|
||||
const dirty=new Set<Id>(),created=new Set<Id>();
|
||||
if(document!==base.document){
|
||||
if(hint?.from===base.document&&hint.to===document){if(hint.topology)return;for(const id of hint.ids)dirty.add(id);}
|
||||
else{
|
||||
@@ -37,22 +38,33 @@ export function patchScene(base:Projection,layout:ReturnType<typeof indexScene>,
|
||||
if(connection?.target.id!==oldEdgeId||connection?.path!==oldPath){if(oldEdgeId)dirty.add(oldEdgeId);if(connection)dirty.add(connection.target.id);}
|
||||
// Validate the whole dirty footprint before constructing or publishing any replacement.
|
||||
for(const id of dirty){const before=base.document.elements.get(id),after=document.elements.get(id);
|
||||
if(!before&&after?.structure==='node'){created.add(id);continue;}
|
||||
if(!before||!after||before.incarnation!==after.incarnation||before.structure!==after.structure||before.componentId!==after.componentId||before.dataVersion!==after.dataVersion||before.ownerCanvasId!==after.ownerCanvasId)return;
|
||||
if(before.structure==='connection'&&after.structure==='connection'&&(!equal(before.source,after.source)||!equal(before.target,after.target)))return;
|
||||
if(before.structure==='node'&&after.structure==='node'&&layout.entries.has(id)&&(!equal(before.geometry,after.geometry)||!equal(before.data.presentation,after.data.presentation)||!equal(oldPreview?.get(id),preview?.get(id))))return;
|
||||
}
|
||||
let nodes=base.nodes,edges=base.edges;
|
||||
let nodes=base.nodes,edges=base.edges;const inserted=new Map<Id,Projection['nodes'][number]>();
|
||||
for(const id of dirty){const element=document.elements.get(id)!;
|
||||
if(element.structure==='node'){
|
||||
const index=layout.nodes.get(id);if(index===undefined)continue;
|
||||
const index=layout.nodes.get(id);if(index===undefined&&!created.has(id))continue;
|
||||
if(!base.transforms.has(element.ownerCanvasId))continue;
|
||||
const transform=base.transforms.get(element.ownerCanvasId)!,depth=layout.depths.get(element.ownerCanvasId)!;
|
||||
const node=projectNode(element,canvasId,registry,selection,preview?.get(id),transform,depth,layout.entries.get(id),catalog);
|
||||
if(nodes===base.nodes)nodes=[...nodes];nodes[index]=node;
|
||||
if(created.has(id)){if(node.data.__headlessFallback)return;inserted.set(id,node);}
|
||||
else{if(nodes===base.nodes)nodes=[...nodes];nodes[index!]=node;}
|
||||
}else{
|
||||
const index=layout.edges.get(id);if(index===undefined)continue;const prior=base.edges[index];
|
||||
const edge=projectEdge(element,prior.source,prior.target,registry,selection,base.transforms.get(element.ownerCanvasId)!,connection,catalog);
|
||||
if(edges===base.edges)edges=[...edges];edges[index]=edge;
|
||||
}
|
||||
}
|
||||
if(inserted.size){
|
||||
// Membership order is depth-first, not global insertion order. Reorder references
|
||||
// without reconstructing unaffected nodes, transforms or edge projections.
|
||||
const members=new Map<Id,Id[]>();if(!queries)for(const element of document.elements.values()){const list=members.get(element.ownerCanvasId)??[];list.push(element.id);members.set(element.ownerCanvasId,list);}
|
||||
const ordered:Projection['nodes']=[];let complete=true;
|
||||
const visit=(owner:Id)=>{for(const id of queries?queries.members(owner):members.get(owner)??[]){const element=document.elements.get(id)!;if(element.structure!=='node')continue;const projected=inserted.get(id)??nodes[layout.nodes.get(id)!];if(!projected){complete=false;return;}ordered.push(projected);const child=layout.entries.get(id);if(child&&base.transforms.has(child))visit(child);}};
|
||||
visit(canvasId);if(!complete)return;nodes=ordered;
|
||||
}
|
||||
return nodes===base.nodes&&edges===base.edges&&document===base.document?base:{...base,document,nodes,edges};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function createSceneProjection(){
|
||||
if(pending?.to!==before){pending=previous.document===before?{from:before,to:after,ids:new Set(),topology:false}:undefined;}
|
||||
if(!pending)return;pending.to=after;
|
||||
if(!changes.length){pending=undefined;return;}
|
||||
for(const change of changes){if(change.kind==='canvas'||change.kind==='element'){pending.topology=true;break;}pending.ids.add(change.target.id);if(pending.ids.size>16384){pending.topology=true;break;}}
|
||||
for(const change of changes){if(change.kind==='canvas'){pending.topology=true;break;}pending.ids.add(change.kind==='element'?change.id:change.target.id);if(pending.ids.size>16384){pending.topology=true;break;}}
|
||||
if(pending.topology)pending.ids.clear();
|
||||
},
|
||||
project(owner:object,document:Document,view:ViewState,sessions:SessionSnapshot,registry:Registry,catalog:RendererCatalog,queries?:DocumentQueries){
|
||||
@@ -30,9 +30,9 @@ export function createSceneProjection(){
|
||||
if(previous&&Object.keys(next).every(key=>Object.is(next[key as keyof SceneInputs],previous![key as keyof SceneInputs])))return value!;
|
||||
// Read the captured session, not a newer live gesture during an older React render.
|
||||
const compatible=previous&&value&&layout&&previous.owner===owner&&previous.registry===registry&&previous.catalog===catalog&&previous.canvasId===view.canvasId;
|
||||
const patched=compatible?patchScene(value!,layout!,next,previous!,connection,pending):undefined;
|
||||
const patched=compatible?patchScene(value!,layout!,next,previous!,connection,pending,queries):undefined;
|
||||
const result=patched??projectCanvas(document,view.canvasId,registry,view.selection,id=>next.preview?.get(id),connection,catalog,{queries,geometry,owner});
|
||||
if(!patched)layout=indexScene(result);
|
||||
if(!patched||result.nodes.length!==value!.nodes.length)layout=indexScene(result);
|
||||
previous=next;value=result;pending=undefined;return result;
|
||||
},
|
||||
clear(){previous=undefined;value=undefined;layout=undefined;pending=undefined;geometry=createGeometryProjectionCache();},
|
||||
|
||||
Reference in New Issue
Block a user