/** * 一个视图拥有的派生场景协调器。组合几何缓存、可信 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. */ /** 每个视图一份 previous/value/layout/pending 与几何缓存;advance 只积写集合,project 才按需算场景。 * 缓存借用不可变文档/选择/预览/registry/catalog 身份,不能在原对象中改字段后期待自动发现失效。 * 实例不订阅 editor、不请求帧,宿主负责接线 advance/project 与视图 clear 生命周期。 */ export function createSceneProjection(){ let geometry=createGeometryProjectionCache(); let previous:SceneInputs|undefined,value:ReturnType|undefined,layout:ReturnType|undefined,pending:SceneChangeHint|undefined; return Object.freeze({ /** 累积上次投影文档→当前文档的连续发布链;同一文档对象直接忽略,未有场景时无需积累。 * 断链只在 previous.document===before 时能重起;空 changes 丢提示,canvas 改动/脏 ID>16384 只标 topology。 * 这里只影响快路径选择,不重放 Change、不写 doc,也不保证输入 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,undefined 正常回退 projectCanvas;异常不自动吞成另一份成功结果。 * queries 先查 document/registry 身份;其对象身份不在场景键中,单独更换 queries 不触发重算。 * 空 preview 规范为 undefined,viewport/tool/mode 不在键中;传入的 sessions 是捕获帧,不读取更新的 live 会话。 */ 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}); // 全量结果或节点数变化才重建 layout,其余兼容 patch 沿用数组下标/拓扑索引。 // 成功后一次推进 previous/value 并清 pending;异常不写这三项,但本轮几何缓存可能已有计算,不承诺整缓存回滚。 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();}, }); }