52 lines
4.7 KiB
TypeScript
52 lines
4.7 KiB
TypeScript
/**
|
|
* 文档结构修改的纯规划函数:删除拥有子树,以及重挂后的连线 owner 修复。
|
|
* 只返回条件 Change,不直接发布 editor 状态;调用方把它们合入一次事务再校验。
|
|
*/
|
|
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);
|
|
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);}
|
|
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. */
|
|
/** 端点重挂后,把边移到两端所属 canvas 的最近共同上级,保持边 ID/incarnation。
|
|
* 曲线坐标属于旧 owner 空间,因此 owner 与 path 必须作为同一操作一起转换。 */
|
|
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});
|
|
}
|
|
return freeze(changes);
|
|
}
|