import { Graph as GraphLib } from '@antv/graphlib'; import { isNil, isNumber, uniq } from '@antv/util'; import { COMBO_KEY, ChangeType, TREE_KEY } from '../constants'; import type { ComboData, EdgeData, GraphData, NodeData } from '../spec'; import type { DataAdded, DataChange, DataID, DataRemoved, DataUpdated, ElementDatum, HierarchyKey, ID, NodeLikeData, PartialEdgeData, PartialGraphData, PartialNodeLikeData, Point, State, } from '../types'; import type { EdgeDirection } from '../types/edge'; import type { ElementType } from '../types/element'; import { isCollapsed } from '../utils/collapsibility'; import { cloneElementData, isElementDataEqual, mergeElementsData } from '../utils/data'; import { arrayDiff } from '../utils/diff'; import { toG6Data, toGraphlibData } from '../utils/graphlib'; import { idOf, parentIdOf } from '../utils/id'; import { positionOf } from '../utils/position'; import { format, print } from '../utils/print'; import { dfs } from '../utils/traverse'; import { add } from '../utils/vector'; export class DataController { public model: GraphLib; /** * 最近一次删除的 combo 的 id * * The ids of the last deleted combos * @remarks * 当删除 combo 后,会将其 id 从 comboIds 中移除,此时根据 Graphlib 的 changes 事件获取到的 NodeRemoved 无法区分是 combo 还是 node。 * 因此需要记录最近一次删除的 combo 的 id,并用于 isCombo 的判断 * * When the combo is deleted, its id will be removed from comboIds. At this time, the NodeRemoved obtained according to the changes event of Graphlib cannot distinguish whether it is a combo or a node. * Therefore, it is necessary to record the id of the last deleted combo and use it to judge isCombo */ protected latestRemovedComboIds = new Set(); protected comboIds = new Set(); /** * 获取详细数据变更 * * Get detailed data changes */ private changes: DataChange[] = []; /** * 批处理计数器 * * Batch processing counter */ private batchCount = 0; /** * 是否处于无痕模式 * * Whether it is in traceless mode */ private isTraceless = false; constructor() { this.model = new GraphLib(); } private pushChange(change: DataChange) { if (this.isTraceless) return; const { type } = change; if (type === ChangeType.NodeUpdated || type === ChangeType.EdgeUpdated || type === ChangeType.ComboUpdated) { const { value, original } = change; this.changes.push({ value: cloneElementData(value), original: cloneElementData(original), type } as DataUpdated); } else { this.changes.push({ value: cloneElementData(change.value), type } as DataAdded | DataRemoved); } } public getChanges(): DataChange[] { return this.changes; } public clearChanges() { this.changes = []; } public batch(callback: () => void) { this.batchCount++; this.model.batch(callback); this.batchCount--; } protected isBatching() { return this.batchCount > 0; } /** * 执行操作而不会留下记录 * * Perform operations without leaving records * @param callback - 回调函数 | callback function * @remarks * 通常用于运行时调整元素并同步数据,避免触发数据变更导致重绘 * * Usually used to adjust elements at runtime and synchronize data to avoid triggering data changes and causing redraws */ public silence(callback: () => void) { this.isTraceless = true; callback(); this.isTraceless = false; } public isCombo(id: ID) { return this.comboIds.has(id) || this.latestRemovedComboIds.has(id); } public getData() { return { nodes: this.getNodeData(), edges: this.getEdgeData(), combos: this.getComboData(), }; } public getNodeData(ids?: ID[]) { return this.model.getAllNodes().reduce((acc, node) => { const data = toG6Data(node); if (this.isCombo(idOf(data))) return acc; if (ids === undefined) acc.push(data); else ids.includes(idOf(data)) && acc.push(data); return acc; }, [] as NodeData[]); } public getEdgeDatum(id: ID) { return toG6Data(this.model.getEdge(id)); } public getEdgeData(ids?: ID[]) { return this.model.getAllEdges().reduce((acc, edge) => { const data = toG6Data(edge); if (ids === undefined) acc.push(data); else ids.includes(idOf(data)) && acc.push(data); return acc; }, [] as EdgeData[]); } public getComboData(ids?: ID[]) { return this.model.getAllNodes().reduce((acc, combo) => { const data = toG6Data(combo); if (!this.isCombo(idOf(data))) return acc; if (ids === undefined) acc.push(data as ComboData); else ids.includes(idOf(data)) && acc.push(data as ComboData); return acc; }, [] as ComboData[]); } public getRootsData(hierarchyKey: HierarchyKey = TREE_KEY) { return this.model.getRoots(hierarchyKey).map(toG6Data); } public getAncestorsData(id: ID, hierarchyKey: HierarchyKey): NodeLikeData[] { const { model } = this; if (!model.hasNode(id) || !model.hasTreeStructure(hierarchyKey)) return []; return model.getAncestors(id, hierarchyKey).map(toG6Data); } public getDescendantsData(id: ID): NodeLikeData[] { const root = this.getElementDataById(id) as NodeLikeData; const data: NodeLikeData[] = []; dfs( root, (node) => { if (node !== root) data.push(node); }, (node) => this.getChildrenData(idOf(node)), 'TB', ); return data; } public getParentData(id: ID, hierarchyKey: HierarchyKey): NodeLikeData | undefined { const { model } = this; if (!hierarchyKey) { print.warn('The hierarchy structure key is not specified'); return undefined; } if (!model.hasNode(id) || !model.hasTreeStructure(hierarchyKey)) return undefined; const parent = model.getParent(id, hierarchyKey); return parent ? toG6Data(parent) : undefined; } public getChildrenData(id: ID): NodeLikeData[] { const structureKey = this.getElementType(id) === 'node' ? TREE_KEY : COMBO_KEY; const { model } = this; if (!model.hasNode(id) || !model.hasTreeStructure(structureKey)) return []; return model.getChildren(id, structureKey).map(toG6Data); } /** * 获取指定类型元素的数据 * * Get the data of the specified type of element * @param elementType - 元素类型 | element type * @returns 元素数据 | element data */ public getElementsDataByType(elementType: ElementType) { if (elementType === 'node') return this.getNodeData(); if (elementType === 'edge') return this.getEdgeData(); if (elementType === 'combo') return this.getComboData(); return []; } /** * 根据 ID 获取元素的数据,不用关心元素的类型 * * Get the data of the element by ID, no need to care about the type of the element * @param id - 元素 ID 数组 | element ID array * @returns 元素数据 | data of the element */ public getElementDataById(id: ID): ElementDatum { const type = this.getElementType(id); if (type === 'edge') return this.getEdgeDatum(id); return this.getNodeLikeDatum(id); } /** * 获取节点的数据 * * Get node data * @param id - 节点 ID | node ID * @returns 节点数据 | node data */ public getNodeLikeDatum(id: ID) { const data = this.model.getNode(id); return toG6Data(data); } /** * 获取所有节点和 combo 的数据 * * Get all node and combo data * @param ids - 节点和 combo ID 数组 | node and combo ID array * @returns 节点和 combo 的数据 | node and combo data */ public getNodeLikeData(ids?: ID[]) { return this.model.getAllNodes().reduce((acc, node) => { const data = toG6Data(node); if (ids) ids.includes(idOf(data)) && acc.push(data); else acc.push(data); return acc; }, [] as NodeLikeData[]); } public getElementDataByState(elementType: ElementType, state: string) { const elementData = this.getElementsDataByType(elementType); return elementData.filter((datum) => datum.states?.includes(state)); } public getElementState(id: ID): State[] { return this.getElementDataById(id)?.states || []; } public hasNode(id: ID) { return this.model.hasNode(id) && !this.isCombo(id); } public hasEdge(id: ID) { return this.model.hasEdge(id); } public hasCombo(id: ID) { return this.model.hasNode(id) && this.isCombo(id); } public getRelatedEdgesData(id: ID, direction: EdgeDirection = 'both') { return this.model.getRelatedEdges(id, direction).map(toG6Data) as EdgeData[]; } public getNeighborNodesData(id: ID) { return this.model.getNeighbors(id).map(toG6Data); } public setData(data: GraphData) { const { nodes: modifiedNodes = [], edges: modifiedEdges = [], combos: modifiedCombos = [] } = data; const { nodes: originalNodes, edges: originalEdges, combos: originalCombos } = this.getData(); const nodeDiff = arrayDiff(originalNodes, modifiedNodes, (node) => idOf(node), isElementDataEqual); const edgeDiff = arrayDiff(originalEdges, modifiedEdges, (edge) => idOf(edge), isElementDataEqual); const comboDiff = arrayDiff(originalCombos, modifiedCombos, (combo) => idOf(combo), isElementDataEqual); this.batch(() => { const dataToAdd = { nodes: nodeDiff.enter, edges: edgeDiff.enter, combos: comboDiff.enter, }; this.addData(dataToAdd); this.computeZIndex(dataToAdd, 'add', true); const dataToUpdate = { nodes: nodeDiff.update, edges: edgeDiff.update, combos: comboDiff.update, }; this.updateData(dataToUpdate); this.computeZIndex(dataToUpdate, 'update', true); const dataToRemove = { nodes: nodeDiff.exit.map(idOf), edges: edgeDiff.exit.map(idOf), combos: comboDiff.exit.map(idOf), }; this.removeData(dataToRemove); }); } public addData(data: GraphData) { const { nodes, edges, combos } = data; this.batch(() => { // add combo first this.addComboData(combos); this.addNodeData(nodes); this.addEdgeData(edges); }); this.computeZIndex(data, 'add'); } public addNodeData(nodes: NodeData[] = []) { if (!nodes.length) return; this.model.addNodes( nodes.map((node) => { this.pushChange({ value: node, type: ChangeType.NodeAdded }); return toGraphlibData(node); }), ); this.updateNodeLikeHierarchy(nodes); this.computeZIndex({ nodes }, 'add'); } public addEdgeData(edges: EdgeData[] = []) { if (!edges.length) return; this.model.addEdges( edges.map((edge) => { this.pushChange({ value: edge, type: ChangeType.EdgeAdded }); return toGraphlibData(edge); }), ); this.computeZIndex({ edges }, 'add'); } public addComboData(combos: ComboData[] = []) { if (!combos.length) return; const { model } = this; if (!model.hasTreeStructure(COMBO_KEY)) { model.attachTreeStructure(COMBO_KEY); } model.addNodes( combos.map((combo) => { this.comboIds.add(idOf(combo)); this.pushChange({ value: combo, type: ChangeType.ComboAdded }); return toGraphlibData(combo); }), ); this.updateNodeLikeHierarchy(combos); this.computeZIndex({ combos }, 'add'); } public addChildrenData(parentId: ID, childrenData: NodeData[]) { const parentData = this.getNodeLikeDatum(parentId) as NodeData; const childrenId = childrenData.map(idOf); this.addNodeData(childrenData); this.updateNodeData([{ id: parentId, children: [...(parentData.children || []), ...childrenId] }]); this.addEdgeData(childrenId.map((childId) => ({ source: parentId, target: childId }))); } /** * 计算 zIndex * * Calculate zIndex * @param data - 新增的数据 | newly added data * @param type - 操作类型 | operation type * @param force - 忽略批处理 | ignore batch processing * @remarks * 调用该函数的情况: * - 新增元素 * - 更新节点/组合的 combo * - 更新节点的 children * * The situation of calling this function: * - Add element * - Update the combo of the node/combo * - Update the children of the node */ protected computeZIndex(data: PartialGraphData, type: 'add' | 'update', force = false) { if (!force && this.isBatching()) return; this.batch(() => { const { nodes = [], edges = [], combos = [] } = data; combos.forEach((combo) => { const id = idOf(combo); if (type === 'add' && isNumber(combo.style?.zIndex)) return; if (type === 'update' && !('combo' in combo)) return; const parent = this.getParentData(id, COMBO_KEY); const zIndex = parent ? (parent.style?.zIndex ?? 0) + 1 : 0; this.preventUpdateNodeLikeHierarchy(() => { this.updateComboData([{ id, style: { zIndex } }]); }); }); nodes.forEach((node) => { const id = idOf(node); if (type === 'add' && isNumber(node.style?.zIndex)) return; if (type === 'update' && !('combo' in node) && !('children' in node)) return; let zIndex = 0; const comboParent = this.getParentData(id, COMBO_KEY); if (comboParent) { zIndex = (comboParent.style?.zIndex || 0) + 1; } else { const nodeParent = this.getParentData(id, TREE_KEY); if (nodeParent) zIndex = nodeParent?.style?.zIndex || 0; } this.preventUpdateNodeLikeHierarchy(() => { this.updateNodeData([{ id, style: { zIndex } }]); }); }); edges.forEach((edge) => { if (isNumber(edge.style?.zIndex)) return; let { id, source, target } = edge; if (!id) id = idOf(edge); else { const datum = this.getEdgeDatum(id); source = datum.source; target = datum.target; } if (!source || !target) return; const sourceZIndex = this.getNodeLikeDatum(source)?.style?.zIndex || 0; const targetZIndex = this.getNodeLikeDatum(target)?.style?.zIndex || 0; this.updateEdgeData([{ id: idOf(edge), style: { zIndex: Math.max(sourceZIndex, targetZIndex) - 1 } }]); }); }); } /** * 计算元素置顶后的 zIndex * * Calculate the zIndex after the element is placed on top * @param id - 元素 ID | ID of the element * @returns zIndex | zIndex */ public getFrontZIndex(id: ID) { const elementType = this.getElementType(id); const elementData = this.getElementDataById(id); const data = this.getData(); // 排除当前元素 / Exclude the current element Object.assign(data, { [`${elementType}s`]: data[`${elementType}s`].filter((element) => idOf(element) !== id), }); if (elementType === 'combo') { // 如果 combo 展开,则排除 combo 的子节点/combo 及内部边 // If the combo is expanded, exclude the child nodes/combos of the combo and the internal edges if (!isCollapsed(elementData as ComboData)) { const ancestorIds = new Set(this.getAncestorsData(id, COMBO_KEY).map(idOf)); data.nodes = data.nodes.filter((element) => !ancestorIds.has(idOf(element))); data.combos = data.combos.filter((element) => !ancestorIds.has(idOf(element))); data.edges = data.edges.filter(({ source, target }) => !ancestorIds.has(source) && !ancestorIds.has(target)); } } return Math.max( elementData.style?.zIndex || 0, 0, ...Object.values(data) .flat() .map((datum) => (datum?.style?.zIndex || 0) + 1), ); } protected updateNodeLikeHierarchy(data: NodeLikeData[]) { if (!this.enableUpdateNodeLikeHierarchy) return; const { model } = this; data.forEach((datum) => { const id = idOf(datum); const parent = parentIdOf(datum); if (parent !== undefined) { if (!model.hasTreeStructure(COMBO_KEY)) model.attachTreeStructure(COMBO_KEY); // 解除原父节点的子节点关系,更新原父节点及其祖先的数据 // Remove the child relationship of the original parent node, update the data of the original parent node and its ancestors if (parent === null) { this.refreshComboData(id); } this.setParent(id, parentIdOf(datum), COMBO_KEY); } const children = (datum as NodeData).children || []; if (children.length) { if (!model.hasTreeStructure(TREE_KEY)) model.attachTreeStructure(TREE_KEY); const _children = children.filter((child) => model.hasNode(child)); _children.forEach((child) => this.setParent(child, id, TREE_KEY)); if (_children.length !== children.length) { // 从数据中移除不存在的子节点 // Remove non-existent child nodes from the data this.updateNodeData([{ id, children: _children }]); } } }); } private enableUpdateNodeLikeHierarchy = true; /** * 执行变更时不要更新节点层次结构 * * Do not update the node hierarchy when executing changes * @param callback - 变更函数 | change function */ public preventUpdateNodeLikeHierarchy(callback: () => void) { this.enableUpdateNodeLikeHierarchy = false; callback(); this.enableUpdateNodeLikeHierarchy = true; } public updateData(data: PartialGraphData) { const { nodes, edges, combos } = data; this.batch(() => { this.updateNodeData(nodes); this.updateComboData(combos); this.updateEdgeData(edges); }); this.computeZIndex(data, 'update'); } public updateNodeData(nodes: PartialNodeLikeData[] = []) { if (!nodes.length) return; const { model } = this; this.batch(() => { const modifiedNodes: NodeData[] = []; nodes.forEach((modifiedNode) => { const id = idOf(modifiedNode); const originalNode = toG6Data(model.getNode(id)); if (isElementDataEqual(originalNode, modifiedNode)) return; const value = mergeElementsData(originalNode, modifiedNode); this.pushChange({ value, original: originalNode, type: ChangeType.NodeUpdated }); model.mergeNodeData(id, value); modifiedNodes.push(value); }); this.updateNodeLikeHierarchy(modifiedNodes); }); this.computeZIndex({ nodes }, 'update'); } /** * 将所有数据提交到变更记录中以进行重绘 * * Submit all data to the change record for redrawing */ public refreshData() { const { nodes, edges, combos } = this.getData(); nodes.forEach((node) => { this.pushChange({ value: node, original: node, type: ChangeType.NodeUpdated }); }); edges.forEach((edge) => { this.pushChange({ value: edge, original: edge, type: ChangeType.EdgeUpdated }); }); combos.forEach((combo) => { this.pushChange({ value: combo, original: combo, type: ChangeType.ComboUpdated }); }); } public syncNodeLikeDatum(datum: PartialNodeLikeData) { const { model } = this; const id = idOf(datum); if (!model.hasNode(id)) return; const original = toG6Data(model.getNode(id)); const value = mergeElementsData(original, datum); model.mergeNodeData(id, value); } public syncEdgeDatum(datum: PartialEdgeData) { const { model } = this; const id = idOf(datum); if (!model.hasEdge(id)) return; const original = toG6Data(model.getEdge(id)); const value = mergeElementsData(original, datum); model.mergeEdgeData(id, value); } public updateEdgeData(edges: PartialEdgeData[] = []) { if (!edges.length) return; const { model } = this; this.batch(() => { edges.forEach((modifiedEdge) => { const id = idOf(modifiedEdge); const originalEdge = toG6Data(model.getEdge(id)); if (isElementDataEqual(originalEdge, modifiedEdge)) return; if (modifiedEdge.source && originalEdge.source !== modifiedEdge.source) { model.updateEdgeSource(id, modifiedEdge.source); } if (modifiedEdge.target && originalEdge.target !== modifiedEdge.target) { model.updateEdgeTarget(id, modifiedEdge.target); } const updatedData = mergeElementsData(originalEdge, modifiedEdge); this.pushChange({ value: updatedData, original: originalEdge, type: ChangeType.EdgeUpdated }); model.mergeEdgeData(id, updatedData); }); }); this.computeZIndex({ edges }, 'update'); } public updateComboData(combos: PartialNodeLikeData[] = []) { if (!combos.length) return; const { model } = this; model.batch(() => { const modifiedCombos: ComboData[] = []; combos.forEach((modifiedCombo) => { const id = idOf(modifiedCombo); const originalCombo = toG6Data(model.getNode(id)) as ComboData; if (isElementDataEqual(originalCombo, modifiedCombo)) return; const value = mergeElementsData(originalCombo, modifiedCombo); this.pushChange({ value, original: originalCombo, type: ChangeType.ComboUpdated }); model.mergeNodeData(id, value); modifiedCombos.push(value); }); this.updateNodeLikeHierarchy(modifiedCombos); }); this.computeZIndex({ combos }, 'update'); } /** * 设置节点的父节点 * * Set the parent node of the node * @param id - 节点 ID | node ID * @param parent - 父节点 ID | parent node ID * @param hierarchyKey - 层次结构类型 | hierarchy type * @param update - 添加新/旧父节点数据更新记录 | add new/old parent node data update record */ public setParent(id: ID, parent: ID | undefined | null, hierarchyKey: HierarchyKey, update: boolean = true) { if (id === parent) return; const elementData = this.getNodeLikeDatum(id); const originalParentId = parentIdOf(elementData); if (originalParentId !== parent && hierarchyKey === COMBO_KEY) { const modifiedDatum = { id, combo: parent }; if (this.isCombo(id)) this.syncNodeLikeDatum(modifiedDatum); else this.syncNodeLikeDatum(modifiedDatum); } this.model.setParent(id, parent, hierarchyKey); if (update && hierarchyKey === COMBO_KEY) { uniq([originalParentId, parent]).forEach((pId) => { if (pId !== undefined) this.refreshComboData(pId); }); } } /** * 刷新 combo 数据 * * Refresh combo data * @param id - combo ID | combo ID * @remarks * 不会更改数据,但会触发数据变更事件 * * Will not change the data, but will trigger data change events */ public refreshComboData(id: ID) { const combo = this.getComboData([id])[0]; const ancestors = this.getAncestorsData(id, COMBO_KEY) as ComboData[]; if (combo) this.pushChange({ value: combo, original: combo, type: ChangeType.ComboUpdated }); ancestors.forEach((value) => { this.pushChange({ value: value, original: value, type: ChangeType.ComboUpdated }); }); } public getElementPosition(id: ID): Point { const datum = this.getElementDataById(id) as NodeLikeData; return positionOf(datum); } public translateNodeLikeBy(id: ID, offset: Point) { if (this.isCombo(id)) this.translateComboBy(id, offset); else this.translateNodeBy(id, offset); } public translateNodeLikeTo(id: ID, position: Point) { if (this.isCombo(id)) this.translateComboTo(id, position); else this.translateNodeTo(id, position); } public translateNodeBy(id: ID, offset: Point) { const curr = this.getElementPosition(id); const position = add(curr, [...offset, 0].slice(0, 3) as Point); this.translateNodeTo(id, position); } public translateNodeTo(id: ID, position: Point) { const [x = 0, y = 0, z = 0] = position; this.preventUpdateNodeLikeHierarchy(() => { this.updateNodeData([{ id, style: { x, y, z } }]); }); } public translateComboBy(id: ID, offset: Point) { const [dx = 0, dy = 0, dz = 0] = offset; if ([dx, dy, dz].some(isNaN) || [dx, dy, dz].every((o) => o === 0)) return; const combo = this.getComboData([id])[0]; if (!combo) return; const seenNodeLikeIds = new Set(); dfs( combo, (succeed) => { const succeedID = idOf(succeed); if (seenNodeLikeIds.has(succeedID)) return; seenNodeLikeIds.add(succeedID); const [x, y, z] = positionOf(succeed); const value = mergeElementsData(succeed, { style: { x: x + dx, y: y + dy, z: z + dz }, }); this.pushChange({ value, // @ts-ignore original: succeed, type: this.isCombo(succeedID) ? ChangeType.ComboUpdated : ChangeType.NodeUpdated, }); this.model.mergeNodeData(succeedID, value); }, (node) => this.getChildrenData(idOf(node)), 'BT', ); } public translateComboTo(id: ID, position: Point) { if (position.some(isNaN)) return; const [tx = 0, ty = 0, tz = 0] = position; const combo = this.getComboData([id])?.[0]; if (!combo) return; const [comboX, comboY, comboZ] = positionOf(combo); const dx = tx - comboX; const dy = ty - comboY; const dz = tz - comboZ; dfs( combo, (succeed) => { const succeedId = idOf(succeed); const [x, y, z] = positionOf(succeed); const value = mergeElementsData(succeed, { style: { x: x + dx, y: y + dy, z: z + dz }, }); this.pushChange({ value, // @ts-ignore original: succeed, type: this.isCombo(succeedId) ? ChangeType.ComboUpdated : ChangeType.NodeUpdated, }); this.model.mergeNodeData(succeedId, value); }, (node) => this.getChildrenData(idOf(node)), 'BT', ); } public removeData(data: DataID) { const { nodes, edges, combos } = data; this.batch(() => { // remove edges first this.removeEdgeData(edges); this.removeNodeData(nodes); this.removeComboData(combos); this.latestRemovedComboIds = new Set(combos); }); } public removeNodeData(ids: ID[] = []) { if (!ids.length) return; this.batch(() => { ids.forEach((id) => { // 移除关联边、子节点 // remove related edges and child nodes this.removeEdgeData(this.getRelatedEdgesData(id).map(idOf)); // TODO 树图情况下移除子节点 this.pushChange({ value: this.getNodeData([id])[0], type: ChangeType.NodeRemoved }); this.removeNodeLikeHierarchy(id); }); this.model.removeNodes(ids); }); } public removeEdgeData(ids: ID[] = []) { if (!ids.length) return; ids.forEach((id) => this.pushChange({ value: this.getEdgeData([id])[0], type: ChangeType.EdgeRemoved })); this.model.removeEdges(ids); } public removeComboData(ids: ID[] = []) { if (!ids.length) return; this.batch(() => { ids.forEach((id) => { this.pushChange({ value: this.getComboData([id])[0], type: ChangeType.ComboRemoved }); this.removeNodeLikeHierarchy(id); this.comboIds.delete(id); }); this.model.removeNodes(ids); }); } /** * 移除节点层次结构,将其子节点移动到父节点的 children 列表中 * * Remove the node hierarchy and move its child nodes to the parent node's children list * @param id - 待处理的节点 | node to be processed */ protected removeNodeLikeHierarchy(id: ID) { if (this.model.hasTreeStructure(COMBO_KEY)) { const grandParent = parentIdOf(this.getNodeLikeDatum(id)); // 从父节点的 children 列表中移除 // remove from its parent's children list // 调用 graphlib.setParent,不需要更新数据 this.setParent(id, undefined, COMBO_KEY, false); // 将子节点移动到父节点的 children 列表中 // move the children to the grandparent's children list this.model.getChildren(id, COMBO_KEY).forEach((child) => { const childData = toG6Data(child); const childId = idOf(childData); this.setParent(idOf(childData), grandParent, COMBO_KEY, false); const value = mergeElementsData(childData, { id: idOf(childData), combo: grandParent, }); this.pushChange({ value, original: childData, type: this.isCombo(childId) ? ChangeType.ComboUpdated : ChangeType.NodeUpdated, }); this.model.mergeNodeData(idOf(childData), value); }); if (!isNil(grandParent)) this.refreshComboData(grandParent); } } /** * 获取元素的类型 * * Get the type of the element * @param id - 元素 ID | ID of the element * @returns 元素类型 | type of the element */ public getElementType(id: ID): ElementType { if (this.model.hasNode(id)) { if (this.isCombo(id)) return 'combo'; return 'node'; } if (this.model.hasEdge(id)) return 'edge'; throw new Error(format(`Unknown element type of id: ${id}`)); } public destroy() { const { model } = this; const nodes = model.getAllNodes(); const edges = model.getAllEdges(); model.removeEdges(edges.map((edge) => edge.id)); model.removeNodes(nodes.map((node) => node.id)); // @ts-expect-error force delete this.context = {}; } }