/** * 文档结构修改的纯规划函数:删除拥有子树,以及重挂后的连线 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([element.id]),canvases=new Set(),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([element.id]),canvases=new Set(),children=new Map(); 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); }