diff --git a/demoV2/Layouter/BinaryTree.js b/demoV2/Layouter/BinaryTree.js index 35aff49..3abff47 100644 --- a/demoV2/Layouter/BinaryTree.js +++ b/demoV2/Layouter/BinaryTree.js @@ -1,11 +1,11 @@ SV.registerLayout('BinaryTree', { defineOptions() { return { - element: { + node: { default: { type: 'binary-tree-node', size: [60, 30], - label: '[data]', + label: '[id]', style: { fill: '#b83b5e', stroke: '#333', @@ -58,7 +58,7 @@ SV.registerLayout('BinaryTree', { /** * 对子树进行递归布局 */ - layoutItem(node, parent, index, layoutOptions) { + layoutItem(node, layoutOptions) { // 次双亲不进行布局 if (!node) { return null; @@ -69,43 +69,54 @@ SV.registerLayout('BinaryTree', { height = bound.height, group = new Group(node), leftGroup = null, - rightGroup = null; + rightGroup = null, + leftBound = null, + rightBound = null; if (node.visited) { return null; } if (node.child && node.child[0]) { - leftGroup = this.layoutItem(node.child[0], node, 0, layoutOptions); + leftGroup = this.layoutItem(node.child[0], layoutOptions); } if (node.child && node.child[1]) { - rightGroup = this.layoutItem(node.child[1], node, 1, layoutOptions); + rightGroup = this.layoutItem(node.child[1], layoutOptions); } - // 处理左右子树相交问题 - if (leftGroup && rightGroup) { - let intersection = Bound.intersect(leftGroup.getBound(), rightGroup.getBound()), - move = 0; + if (leftGroup) { + leftBound = leftGroup.getBound(); + node.set('y', leftBound.y - layoutOptions.yInterval - height); + } - if (intersection && intersection.width > 0) { - move = (intersection.width + layoutOptions.xInterval) / 2; - leftGroup.translate(-move, 0); - rightGroup.translate(move, 0); + if(rightGroup) { + rightBound = rightGroup.getBound(); + + if(leftGroup) { + rightGroup.translate(0, leftBound.y - rightBound.y) + } + + rightBound = rightGroup.getBound(); + node.set('y', rightBound.y - layoutOptions.yInterval - height); + } + + // 处理左右子树相交问题 + if (leftGroup && rightGroup) { + let move = Math.abs(rightBound.x - layoutOptions.xInterval - leftBound.x - leftBound.width); + if (move > 0) { + leftGroup.translate(-move / 2, 0); + rightGroup.translate(move / 2, 0); } } if (leftGroup) { - let leftBound = leftGroup.getBound(); - - node.set('y', leftBound.y - layoutOptions.yInterval - height); + leftBound = leftGroup.getBound(); node.set('x', leftBound.x + leftBound.width + layoutOptions.xInterval / 2 - width); } if(rightGroup) { - let rightBound = rightGroup.getBound(); - - node.set('y', rightBound.y - layoutOptions.yInterval - height); + rightBound = rightGroup.getBound(); node.set('x', rightBound.x - layoutOptions.xInterval / 2 - width); } @@ -129,7 +140,7 @@ SV.registerLayout('BinaryTree', { */ layout(elements, layoutOptions) { let root = elements[0]; - this.layoutItem(root, null, -1, layoutOptions); + this.layoutItem(root, layoutOptions); }, }); diff --git a/demoV2/Layouter/LinkList.js b/demoV2/Layouter/LinkList.js index 951d011..a6dc7d8 100644 --- a/demoV2/Layouter/LinkList.js +++ b/demoV2/Layouter/LinkList.js @@ -77,25 +77,37 @@ SV.registerLayout('LinkList', { layout: { xInterval: 50, yInterval: 50 - }, - behavior: { - dragNode: false } }; }, - layout(elements, layoutOptions) { - for (let i = 0; i < elements.length; i++) { - let node = elements[i], - prev = elements[1 - 1], - width = node.get('size')[0]; - - if (prev) { - node.set('y', prev.get('y')); - node.set('x', prev.get('x') + layoutOptions.xInterval + width); - } + /** + * 对子树进行递归布局 + * @param node + * @param parent + */ + layoutItem(node, prev, layoutOptions) { + if (!node) { + return null; } + + let width = node.get('size')[0]; + + if (prev) { + node.set('y', prev.get('y')); + node.set('x', prev.get('x') + layoutOptions.xInterval + width); + } + + if (node.next) { + this.layoutItem(node.next, node, layoutOptions); + } + }, + + + layout(elements, layoutOptions) { + let root = elements[0]; + this.layoutItem(root, null, layoutOptions); } }); diff --git a/demoV2/Layouter/LinkStack.js b/demoV2/Layouter/LinkStack.js index 9634ddd..dc49a13 100644 --- a/demoV2/Layouter/LinkStack.js +++ b/demoV2/Layouter/LinkStack.js @@ -20,7 +20,7 @@ element: { default: { type: 'link-list-node', - label: '[id]', + label: '[data]', size: [60, 30], style: { stroke: '#333', diff --git a/demoV2/data.js b/demoV2/data.js new file mode 100644 index 0000000..40d5929 --- /dev/null +++ b/demoV2/data.js @@ -0,0 +1,120 @@ +const SOURCES_DATA = [ + { + LinkList0: { + data: [ + { + id: '0x617eb0', + data: 'Z', + next: '0x617ef0', + rootExternal: ['L'], + type: 'default', + }, + { + id: '0x617ef0', + data: 'A', + next: '0x617f10', + type: 'default', + }, + { + id: '0x617f10', + data: 'B', + next: '0x617f30', + type: 'default', + }, + { + id: '0x617f30', + data: 'C', + external: ['r', 't'], + next: null, + type: 'default', + }, + ], + layouter: 'LinkList', + }, + LinkList1: { + data: [ + { + id: '0x617ed0', + data: 'Y', + next: '0x617f50', + rootExternal: ['L2'], + type: 'default', + }, + { + id: '0x617f50', + data: 'a', + next: '0x617f70', + type: 'default', + }, + { + id: '0x617f70', + data: 'b', + external: ['r2', 't2'], + loopNext: 'LinkList0#0x617f30', + type: 'default', + }, + ], + layouter: 'LinkList', + }, + isEnterFunction: false, + }, + { + LinkList0: { + data: [ + { + id: '0x617eb0', + data: 'Z', + next: '0x617ef0', + rootExternal: ['L'], + type: 'default', + }, + { + id: '0x617ef0', + data: 'A', + next: '0x617f10', + type: 'default', + }, + { + id: '0x617f10', + data: 'B', + next: '0x617f30', + type: 'default', + }, + { + id: '0x617f30', + data: 'C', + external: ['r', 't'], + next: null, + type: 'default', + }, + ], + layouter: 'LinkList', + }, + LinkList1: { + data: [ + { + id: '0x617ed0', + data: 'Y', + next: '0x617f50', + rootExternal: ['L2'], + type: 'default', + }, + { + id: '0x617f50', + data: 'a', + next: '0x617f70', + type: 'default', + }, + { + id: '0x617f70', + data: 'b', + external: ['r2', 't2'], + next: null, + type: 'default', + }, + ], + layouter: 'LinkList', + }, + isEnterFunction: false, + }, +]; diff --git a/demoV2/demo2.html b/demoV2/demo2.html index 82e01d7..511956d 100644 --- a/demoV2/demo2.html +++ b/demoV2/demo2.html @@ -1,237 +1,151 @@ + + + + DEMO + - + .container { + background-color: #fafafa; + border: 1px solid #ccc; + position: relative; + } - -
-
- 泄漏区 -
-
+ .down { + display: flex; + margin-top: 20px; + } - - - - - - - + #container { + width: 100%; + height: 500px; + position: relative; + overflow: hidden; + } - - - - - - - - - - - - - - - - + #leak { + position: absolute; + left: 0; + opacity: 0; + top: 100px; + width: 100%; + box-sizing: border-box; + padding: 4px; + border-top: 1px dashed #000; + pointer-events: none; + transition: opacity 0.75s ease-in-out; + } - + + + + + + + + + + + + + + + + - const container = document.getElementById('container'), - pos = document.getElementById('pos'); + - + document.getElementById('brush-select').addEventListener('click', e => { + enableBrushSelect = !enableBrushSelect; + cur.switchBrushSelect(enableBrushSelect); + }); - \ No newline at end of file + cur.on('onLeakAreaUpdate', payload => { + leak.style.opacity = payload.hasLeak ? 1 : 0; + leak.style.top = payload.leakAreaY - 40 + 'px'; + }); + + // ------------------------------------------------------------------------------------------------------- + + container.addEventListener('mousemove', e => { + let x = e.offsetX, + y = e.offsetY; + pos.innerHTML = `${x},${y}`; + }); + + + diff --git a/src/BehaviorHelper/behaviorIssueHelper.ts b/src/BehaviorHelper/behaviorIssueHelper.ts index 299f460..cbd3ba3 100644 --- a/src/BehaviorHelper/behaviorIssueHelper.ts +++ b/src/BehaviorHelper/behaviorIssueHelper.ts @@ -76,7 +76,7 @@ export function SolveNodeAppendagesDrag(viewContainer: ViewContainer) { item.setSelectedState(false); if (item instanceof SVNode) { - item.appendages.forEach(appendage => appendage.setSelectedState(false)); + item.getAppendagesList().forEach(appendage => appendage.setSelectedState(false)); } }); viewContainer.brushSelectedModels.length = 0; @@ -100,7 +100,7 @@ export function SolveNodeAppendagesDrag(viewContainer: ViewContainer) { y: node.G6Item.getModel().y }); - node.appendages.forEach(item => { + node.getAppendagesList().forEach(item => { item.setSelectedState(false); item.set({ x: item.G6Item.getModel().x, @@ -116,7 +116,7 @@ export function SolveNodeAppendagesDrag(viewContainer: ViewContainer) { }); if(item instanceof SVNode) { - item.appendages.forEach(appendage => { + item.getAppendagesList().forEach(appendage => { appendage.set({ x: appendage.G6Item.getModel().x, y: appendage.G6Item.getModel().y diff --git a/src/BehaviorHelper/initG6Behaviors.ts b/src/BehaviorHelper/initG6Behaviors.ts index b7464bd..a3319ea 100644 --- a/src/BehaviorHelper/initG6Behaviors.ts +++ b/src/BehaviorHelper/initG6Behaviors.ts @@ -45,7 +45,7 @@ export function InitG6Behaviors(engine: Engine, viewContainer: ViewContainer): M // 这里之所以要把节点和其 appendages 的选中状态设置为true,是因为 g6 处理拖拽节点的逻辑是将所以已选中的元素一起拖动, // 这样 appendages 就可以很自然地跟着节点动(我是看源码才知道的) node.setSelectedState(true); - node.appendages.forEach(item => { + node.getAppendagesList().forEach(item => { item.setSelectedState(true); }); diff --git a/src/Common/boundingRect.ts b/src/Common/boundingRect.ts index af9a1fb..21bff70 100644 --- a/src/Common/boundingRect.ts +++ b/src/Common/boundingRect.ts @@ -1,152 +1,161 @@ -import { Vector } from "./vector"; - - +import { Vector } from './vector'; // 包围盒类型 export type BoundingRect = { - x: number; - y: number; - width: number; - height: number; + x: number; + y: number; + width: number; + height: number; }; - // 包围盒操作 export const Bound = { + /** + * 从点集生成包围盒 + * @param points + */ + fromPoints(points: Array<[number, number]>): BoundingRect { + let maxX = -Infinity, + minX = Infinity, + maxY = -Infinity, + minY = Infinity; - /** - * 从点集生成包围盒 - * @param points - */ - fromPoints(points: Array<[number, number]>): BoundingRect { - let maxX = -Infinity, - minX = Infinity, - maxY = -Infinity, - minY = Infinity; + points.map(item => { + if (item[0] > maxX) maxX = item[0]; + if (item[0] < minX) minX = item[0]; + if (item[1] > maxY) maxY = item[1]; + if (item[1] < minY) minY = item[1]; + }); - points.map(item => { - if(item[0] > maxX) maxX = item[0]; - if(item[0] < minX) minX = item[0]; - if(item[1] > maxY) maxY = item[1]; - if(item[1] < minY) minY = item[1]; - }); + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + }, - return { - x: minX, - y: minY, - width: maxX - minX, - height: maxY - minY - }; - }, + /** + * 由包围盒转化为四个顶点(顺时针) + * @param bound + */ + toPoints(bound: BoundingRect): Array<[number, number]> { + return [ + [bound.x, bound.y], + [bound.x + bound.width, bound.y], + [bound.x + bound.width, bound.y + bound.height], + [bound.x, bound.y + bound.height], + ]; + }, - /** - * 由包围盒转化为四个顶点(顺时针) - * @param bound - */ - toPoints(bound: BoundingRect): Array<[number, number]> { - return [ - [bound.x, bound.y], - [bound.x + bound.width, bound.y], - [bound.x + bound.width, bound.y + bound.height], - [bound.x, bound.y + bound.height] - ]; - }, + /** + * 求包围盒并集 + * @param arg + */ + union(...arg: BoundingRect[]): BoundingRect { + if (arg.length === 0) { + return { + x: 0, + y: 0, + width: 0, + height: 0, + }; + } - /** - * 求包围盒并集 - * @param arg - */ - union(...arg: BoundingRect[]): BoundingRect { - return arg.length > 1? - arg.reduce((total, cur) => { - let minX = total.x < cur.x? total.x: cur.x, - maxX = total.x + total.width < cur.x + cur.width? cur.x + cur.width: total.x + total.width, - minY = total.y < cur.y? total.y: cur.y, - maxY = total.y + total.height < cur.y + cur.height? cur.y + cur.height: total.y + total.height; + return arg.length > 1 + ? arg.reduce((total, cur) => { + let minX = Math.min(total.x, cur.x), + maxX = Math.max(total.x + total.width, cur.x + cur.width), + minY = Math.min(total.y, cur.y), + maxY = Math.max(total.y + total.height, cur.y + cur.height); - return { - x: minX, - y: minY, - width: maxX - minX, - height: maxY - minY - }; - }): arg[0]; - }, + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + }) + : arg[0]; + }, - /** - * 包围盒求交集 - * @param b1 - * @param b2 - */ - intersect(b1: BoundingRect, b2: BoundingRect): BoundingRect { - let x, y, - maxX, maxY, - overlapsX, - overlapsY; + /** + * 包围盒求交集 + * @param b1 + * @param b2 + */ + intersect(b1: BoundingRect, b2: BoundingRect): BoundingRect { + let x, + y, + maxX, + maxY, + overlapsX, + overlapsY, + b1x = b1.x, + b1mx = b1.x + b1.width, + b2x = b2.x, + b2mx = b2.x + b2.width, + b1y = b1.y, + b1my = b1.y + b1.height, + b2y = b2.y, + b2my = b2.y + b2.height; - if(b1.x < b2.x + b2.width && b1.x + b1.width > b2.x) { - x = b1.x < b2.x? b2.x: b1.x; - // maxX = b1.x + b1.width < b2.x + b2.width? b1.x + b1.width: b2.x + b2.width; - maxX = b1.x + b1.width; - overlapsX = maxX - x; - } - - if(b1.y < b2.y + b2.height && b1.y + b1.height > b2.y) { - y = b1.y < b2.y? b2.y: b1.y; - maxY = b1.y + b1.height < b2.y + b2.height? b1.y + b1.height: b2.y + b2.height; - overlapsY = maxY - y; - } + x = Math.max(b1x, b2x); + maxX = Math.min(b1mx, b2mx); + overlapsX = maxX - x; - if(!overlapsX || !overlapsY) return null; + y = Math.max(b1y, b2y); + maxY = Math.min(b1my, b2my); + overlapsY = maxY - y; - return { - x, - y, - width: overlapsX, - height: overlapsY - }; - }, + if (!overlapsX || !overlapsY) return null; - /** - * 位移包围盒 - * @param bound - * @param dx - * @param dy - */ - translate(bound: BoundingRect, dx: number, dy: number) { - bound.x += dx; - bound.y += dy; - }, + return { + x, + y, + width: overlapsX, + height: overlapsY, + }; + }, - /** - * 求包围盒旋转后新形成的包围盒 - * @param bound - * @param rot - */ - rotation(bound: BoundingRect, rot: number): BoundingRect { - let cx = bound.x + bound.width / 2, - cy = bound.y + bound.height / 2; + /** + * 位移包围盒 + * @param bound + * @param dx + * @param dy + */ + translate(bound: BoundingRect, dx: number, dy: number) { + bound.x += dx; + bound.y += dy; + }, - return Bound.fromPoints(Bound.toPoints(bound).map(item => Vector.rotation(rot, item, [cx, cy]))); - }, + /** + * 求包围盒旋转后新形成的包围盒 + * @param bound + * @param rot + */ + rotation(bound: BoundingRect, rot: number): BoundingRect { + let cx = bound.x + bound.width / 2, + cy = bound.y + bound.height / 2; - /** - * 判断两个包围盒是否相交 - * @param b1 - * @param b2 - */ - isOverlap(b1: BoundingRect, b2: BoundingRect): boolean { - let maxX1 = b1.x + b1.width, - maxY1 = b1.y + b1.height, - maxX2 = b2.x + b2.width, - maxY2 = b2.y + b2.height; + return Bound.fromPoints(Bound.toPoints(bound).map(item => Vector.rotation(rot, item, [cx, cy]))); + }, - if (b1.x < maxX2 && b2.x < maxX1 && b1.y < maxY2 && b2.y < maxY1) { - return true; - } - - return false; - } + /** + * 判断两个包围盒是否相交 + * @param b1 + * @param b2 + */ + isOverlap(b1: BoundingRect, b2: BoundingRect): boolean { + let maxX1 = b1.x + b1.width, + maxY1 = b1.y + b1.height, + maxX2 = b2.x + b2.width, + maxY2 = b2.y + b2.height; + + if (b1.x < maxX2 && b2.x < maxX1 && b1.y < maxY2 && b2.y < maxY1) { + return true; + } + + return false; + }, }; - - diff --git a/src/Common/util.ts b/src/Common/util.ts index a36d1e8..42f652f 100644 --- a/src/Common/util.ts +++ b/src/Common/util.ts @@ -122,21 +122,6 @@ export const Util = { return list; }, - /** - * G6 data 转换器 - * @param layoutGroup - * @returns - */ - convertG6Data(layoutGroup: LayoutGroup): GraphData { - let nodes = [...layoutGroup.node, ...layoutGroup.marker], - edges = layoutGroup.link; - - return { - nodes: nodes.map(item => item.getG6ModelProps()) as NodeConfig[], - edges: edges.map(item => item.getG6ModelProps()) as EdgeConfig[] - }; - }, - /** * 将 modelList 转换到 G6Data * @param modelList diff --git a/src/Model/SVLink.ts b/src/Model/SVLink.ts index aae2232..32398f3 100644 --- a/src/Model/SVLink.ts +++ b/src/Model/SVLink.ts @@ -49,4 +49,11 @@ export class SVLink extends SVModel { curveOffset: options.curveOffset }; } + + beforeDestroy(): void { + Util.removeFromList(this.target.links.inDegree, item => item.id === this.id); + Util.removeFromList(this.node.links.outDegree, item => item.id === this.id); + this.node = null; + this.target = null; + } }; diff --git a/src/Model/SVModel.ts b/src/Model/SVModel.ts index e650f7f..7fc61a6 100644 --- a/src/Model/SVModel.ts +++ b/src/Model/SVModel.ts @@ -1,219 +1,204 @@ -import { Util } from "../Common/util"; -import { Style } from "../options"; -import { BoundingRect } from "../Common/boundingRect"; -import { EdgeConfig, Item, NodeConfig } from "@antv/g6-core"; -import { Graph } from "@antv/g6-pc"; +import { Util } from '../Common/util'; +import { Style } from '../options'; +import { BoundingRect } from '../Common/boundingRect'; +import { EdgeConfig, Item, NodeConfig } from '@antv/g6-core'; +import { Graph } from '@antv/g6-pc'; import merge from 'merge'; -import { ModelConstructor } from "./modelConstructor"; - - - export class SVModel { - public id: string; - public sourceType: string; + public id: string; + public sourceType: string; - public g6Instance: Graph; - public shadowG6Instance: Graph; - public group: string; - public layout: string; - public G6ModelProps: NodeConfig | EdgeConfig; - public shadowG6Item: Item; - public G6Item: Item; + public g6Instance: Graph; + public shadowG6Instance: Graph; + public group: string; + public layout: string; + public G6ModelProps: NodeConfig | EdgeConfig; + public shadowG6Item: Item; + public G6Item: Item; - public preLayout: boolean; // 是否进入预备布局阶段 - public discarded: boolean; - public freed: boolean; - public leaked: boolean; - public generalStyle: Partial