99 lines
9.4 KiB
TypeScript
99 lines
9.4 KiB
TypeScript
/**
|
||
* 场景的受检增量修补。只在能够证明拓扑、身份和坐标关系仍兼容时替换少量显示对象;
|
||
* 不确定就返回 undefined,让调用方完整投影。缓存命中率不能高于模型与显示一致性的优先级。
|
||
*/
|
||
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';
|
||
import {projectNode,projectEdge,type projectCanvas} from './projection.ts';
|
||
import type {RendererCatalog} from './renderers.ts';
|
||
|
||
/** 与完整 projectCanvas 保持同一结果结构,避免增量路径自行定义另一套显示规则。 */
|
||
type Projection=ReturnType<typeof projectCanvas>;
|
||
/** from/to 按 Document 对象身份连接连续发布,ids 是宿主提供的完整写集合;这里不重算其覆盖证明。
|
||
* topology=true 禁用局部修补,Set 可由 scene coordinator 累加,不是持久 Change 日志或业务事件队列。 */
|
||
export type SceneChangeHint={from:Document;to:Document;ids:Set<Id>;topology:boolean};
|
||
/** 几何手势的瞬态预览,不将 pointermove 变成文档修改。 */
|
||
type Preview=NonNullable<SessionSnapshot['gesture']>['preview']|undefined;
|
||
/** 真正影响场景内容的输入;viewport/tool/mode 等状态由别的显示通道处理。 */
|
||
export type SceneInputs={owner:object;document:Document;registry:Registry;catalog:RendererCatalog;canvasId:Id;selection:ViewState['selection'];preview:Preview;edgeId:Id|undefined;path:NonNullable<SessionSnapshot['connectionGesture']>['path']|undefined};
|
||
/** 建上一帧 ID→数组下标、入口 elementId→子 canvasId、显示 canvas 深度索引。
|
||
* 深度沿 transforms 迭代顺序累计,依赖完整投影按父先子后产出;不在这里重新排序/校验整棵树。
|
||
* unsupported 标记当前已有 fallback node/unsupported edge,供 patchScene 直接回退。 */
|
||
export function indexScene(projection:Projection){
|
||
const nodes=new Map(projection.nodes.map((node,index)=>[node.id,index])),edges=new Map(projection.edges.map((edge,index)=>[edge.id,index])),entries=new Map<Id,Id>(),depths=new Map<Id,number>([[projection.canvasId,0]]);
|
||
for(const canvas of projection.document.canvases.values())if(canvas.parent)entries.set(canvas.parent.entryElementId,canvas.id);
|
||
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')};
|
||
}
|
||
/** Reuse existing projections for checked field edits and ordinary node insertions. */
|
||
/** 纯候选投影修补:合并文档/选择/预览脏项,先核对全体脏项再构造新数组。
|
||
* 允许普通节点新增与受检字段编辑;删除、代次/类型/owner/连线端点变更交给完整路径。
|
||
* 调用者须先核 owner/registry/catalog/canvas 兼容,layout 对应 base,queries 对应 next.document;本函数不重复全部护栏。
|
||
* undefined 表示本次不适合快路径,不是业务拒绝;投影器/外部服务抛错则直接传播,没有错误吞并或业务发布。 */
|
||
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>(),created=new Set<Id>();
|
||
if(document!==base.document){
|
||
// 匹配 from/to 的 hint 按完整写集合合同直接取 ids,不额外全表证明“没有遗漏其它修改”。
|
||
// 缺可信 hint 才比较 canvas 表及 element 表大小/顺序/内容;这条扫描路径本身不支持插入或删除。
|
||
if(hint?.from===base.document&&hint.to===document){if(hint.topology)return;for(const id of hint.ids)dirty.add(id);}
|
||
else{
|
||
// Canonical replacement and missed publications carry no trustworthy write set.
|
||
if(document.canvases.size!==base.document.canvases.size)return;
|
||
const canvasOrder=base.document.canvases.keys();
|
||
for(const[id,canvas]of document.canvases)if(id!==canvasOrder.next().value||!equal(canvas,base.document.canvases.get(id)))return;
|
||
if(document.elements.size!==base.document.elements.size)return;
|
||
const elementOrder=base.document.elements.keys();
|
||
for(const[id,record]of document.elements){if(id!==elementOrder.next().value)return;if(record!==base.document.elements.get(id)&&!equal(record,base.document.elements.get(id)))dirty.add(id);}
|
||
}
|
||
}
|
||
// 选择变化取对称差;预览变化同时检查新旧集合,取消预览也必须重画旧目标。
|
||
if(selection!==oldSelection){for(const id of oldSelection)if(!selection.has(id))dirty.add(id);for(const id of selection)if(!oldSelection.has(id))dirty.add(id);}
|
||
if(preview!==oldPreview){for(const[id,g]of oldPreview??[])if(!equal(g,preview?.get(id)))dirty.add(id);for(const[id,g]of preview??[])if(!equal(g,oldPreview?.get(id)))dirty.add(id);}
|
||
// 连线预览换目标或结束时,把旧边和新边都标脏,以去掉上一条线的预览。
|
||
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);
|
||
// 新节点暂记 created;未改分区拓扑且所属 canvas 已有变换时才可能插入当前场景。
|
||
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;
|
||
}
|
||
// 先通过脏项形状检查再懒复制受影响数组,未变对象仍共享;所有新节点/边调用完整路径共用投影函数。
|
||
// base.transforms 只在拓扑兼容时复用;未显示 canvas 的普通变更可以只更新返回 document 而不增加显示节点。
|
||
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&&!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(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;
|
||
}
|
||
}
|
||
// 插入后按显示根的深度优先成员序重排引用,不能直接 append 而破坏嵌套绘制层序。
|
||
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;
|
||
/** 按 owner 成员顺序深度优先重排节点引用,只进已在 transforms 显示的子 canvas。
|
||
* 缺任一所需投影就令 complete=false 回退;依赖已验证树,无独立循环/递归深度预算。 */
|
||
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};
|
||
}
|