Files
Endoleap/src/lib/canvas-bindings/scene-projection.ts
T

54 lines
4.3 KiB
TypeScript

/**
* 一个视图拥有的派生场景协调器。组合几何缓存、可信 changes 提示和完整/增量投影,
* 读取固定的 session 快照;不把 viewport、工具状态或同步任务状态当成场景重算条件。
*/
import {fault} from '../canvas-core/values.ts';
import type {Change,Document,Registry} from '../canvas-core/types.ts';
import type {SessionSnapshot,ViewState} from '../canvas-core/gestures.ts';
import type {DocumentQueries} from '../canvas-core/queries.ts';
import {createGeometryProjectionCache} from './geometry-cache.ts';
import {projectCanvas} from './projection.ts';
import {indexScene,patchScene,type SceneChangeHint,type SceneInputs} from './scene-changes.ts';
import type {RendererCatalog} from './renderers.ts';
/** One view's derived scene. Viewport/tool/mode and task status do not change scene inputs. */
/** 每个视图独立创建;advance 接 editor 发布,project 接当前显示输入,clear 释放全部派生缓存。 */
export function createSceneProjection(){
let geometry=createGeometryProjectionCache();
let previous:SceneInputs|undefined,value:ReturnType<typeof projectCanvas>|undefined,layout:ReturnType<typeof indexScene>|undefined,pending:SceneChangeHint|undefined;
return Object.freeze({
/** 累积从上次投影文档到当前文档的连续写集合;丢发布、无 changes 的替换或大范围改动退回全量。 */
advance(before:Document,after:Document,changes:readonly Change[]){
if(before===after)return;
if(!previous||previous.document===after){pending=undefined;return;}
// 只有链尾接上 before 才能继续累计;不连贯时仅从恰好匹配的旧场景重新起链。
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;}
// canvas 结构改动或超过16384个脏 ID 时不再扩充集合,标拓扑失效并清空大集合。
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();
},
/** 固定 document/registry/catalog/显示 canvas 和捕获的 session 输入;相同引用直接复用场景。
* 可兼容才尝试 patch,失败正常回退 projectCanvas;查询必须与同一份文档/registry 绑定。 */
project(owner:object,document:Document,view:ViewState,sessions:SessionSnapshot,registry:Registry,catalog:RendererCatalog,queries?:DocumentQueries){
if(queries&&queries.document!==document)throw fault('stale_query_snapshot');
if(queries&&queries.registry!==registry)throw fault('query_registry_changed');
const preview=sessions.gesture?.preview,connection=sessions.connectionGesture;
const next:SceneInputs={owner,document,registry,catalog,canvasId:view.canvasId,selection:view.selection,preview:preview?.size?preview:undefined,edgeId:connection?.target.id,path:connection?.path};
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.
// owner/registry/renderer catalog/根 canvas 改变意味着解释上下文变了,不能沿用局部补丁。
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,queries):undefined;
const result=patched??projectCanvas(document,view.canvasId,registry,view.selection,id=>next.preview?.get(id),connection,catalog,{queries,geometry,owner});
// 全量重建或节点数量改变才重建布局索引;成功采用结果后消费并清掉 changes 提示。
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();},
});
}