Files
Endoleap/src/lib/canvas-core/structure.ts
T

58 lines
5.4 KiB
TypeScript

/**
* 文档结构修改的纯规划函数:删除拥有子树,以及重挂后的连线 owner 修复。
* 只返回条件 Change,不直接发布 editor 状态;调用方把它们合入一次事务再校验。
* 文档应是有效、已捕获的不可变快照;返回 before 保留记录引用,freeze 不提供任意外部输入隔离。
*/
import type {DocumentQueries} from './queries.ts';
import type { Change, Document, Id, Target } from './types.ts';
import { commonCanvas, targetElement } from './document.ts';
import { canvasTransform, convertPath } from './coordinates.ts';
import { equal, freeze, fault } from './values.ts';
/** Own the subtree explicitly; geometry/visibility never selects which descendants are deleted. */
/** 删除目标、由它作为入口拥有的全部子 canvas/成员,以及这些成员关联的边。
* queries 是可选的同版本关系索引;没有索引时扫描规范化文档,删除范围不依赖 DOM。 */
export function removalChanges(document:Document,target:Target,queries?:DocumentQueries):readonly Change[] {
// 索引快路径:先核对 snapshot 身份,再用显式 DFS stack 遍历拥有树。
if(queries){
if(queries.document!==document)throw fault('stale_query_snapshot');const element=targetElement(document,target),elements=new Set<Id>([element.id]),canvases=new Set<Id>(),child=queries.relations.childCanvasByEntry.get(element.id),stack=child?[child]:[];
while(stack.length){const id=stack.pop()!;if(canvases.has(id))continue;canvases.add(id);stack.push(...queries.childCanvases(id));for(const member of queries.members(id))elements.add(member);}
// 关联边可能属于更上级 canvas,即使不在子树成员表中也必须随端点删除。
for(const id of [...elements])for(const edge of queries.incident(id))elements.add(edge);
// 按原表顺序输出元素删除,再输出 canvas 删除;最终关系合法性在事务整批完成后检查。
return freeze([...queries.orderedElements(elements).map(id=>({kind:'element' as const,id,before:document.elements.get(id)!,after:null})),...queries.orderedCanvases(canvases).map(id=>({kind:'canvas' as const,id,before:document.canvases.get(id)!,after:null}))]);
}
// 扫描路径构造 parent→children 临时表;Set 去重,stack 避免 JS 递归调用栈增长。
const element=targetElement(document,target),elements=new Set<Id>([element.id]),canvases=new Set<Id>(),children=new Map<Id,Id[]>();
for(const canvas of document.canvases.values())if(canvas.parent){const list=children.get(canvas.parent.canvasId)??[];list.push(canvas.id);children.set(canvas.parent.canvasId,list);}
// 只从“目标是入口”的子 canvas 开始,不因节点视觉上处在某个矩形内就扩大删除范围。
const stack=[...document.canvases.values()].filter(canvas=>canvas.parent?.entryElementId===element.id).map(canvas=>canvas.id);
while(stack.length){const id=stack.pop()!;if(canvases.has(id))continue;canvases.add(id);for(const child of children.get(id)??[])stack.push(child);}
// 先收集全部被删成员,再找相关边,防止边扫描先后顺序影响删除结果。
for(const record of document.elements.values())if(canvases.has(record.ownerCanvasId))elements.add(record.id);
for(const record of document.elements.values())if(record.structure==='connection'&&(elements.has(record.source.elementId)||elements.has(record.target.elementId)))elements.add(record.id);
return freeze([
...[...document.elements.values()].filter(record=>elements.has(record.id)).map(record=>({kind:'element' as const,id:record.id,before:record,after:null})),
...[...document.canvases.values()].filter(canvas=>canvases.has(canvas.id)).map(canvas=>({kind:'canvas' as const,id:canvas.id,before:canvas,after:null})),
]);
}
/** Ownership changes and their curve coordinates are one operation, with the same stable edge identity. */
/** 端点或端点 owner 改变后,把边移到两端所属 canvas 的最近共同上级,保持边 ID/incarnation。
* 曲线坐标属于旧 owner 空间,因此需要转换时 owner 与 path 必须作为同一操作一起提交。
* 只修复 owner 发生变化的边;不会仅因祖先几何变了就重写同 owner 的局部路径。 */
export function repairConnectionOwners(before:Document,document:Document):readonly Change[] {
const changes:Change[]=[];
for(const edge of document.elements.values())if(edge.structure==='connection'){
// 缺端点暂时跳过,交给整图校验拒绝;此规划器不会自行补端点或删除悬空边。
const source=document.elements.get(edge.source.elementId),target=document.elements.get(edge.target.elementId);if(!source||!target)continue;
const owner=commonCanvas(document,source.ownerCanvasId,target.ownerCanvasId);if(owner===edge.ownerCanvasId)continue;
// 旧 owner 可能已从新文档移除:优先用 before 的变换解释旧曲线,再转到新 owner。
const path=convertPath(edge.path,canvasTransform(before.canvases.has(edge.ownerCanvasId)?before:document,edge.ownerCanvasId),canvasTransform(document,owner));
changes.push({kind:'field',target:{id:edge.id,incarnation:edge.incarnation},field:'ownerCanvasId',before:edge.ownerCanvasId,after:owner});
if(!equal(edge.path,path))changes.push({kind:'field',target:{id:edge.id,incarnation:edge.incarnation},field:'path',before:edge.path,after:path});
}
// 这里只返回补偿变化;document 参数不在本函数内被替换或发布。
return freeze(changes);
}