feat: 重写泄漏区逻辑

This commit is contained in:
黎智洲 2022-03-02 22:07:27 +08:00
parent 5893ef2938
commit 3170bd77ec
21 changed files with 2209 additions and 1999 deletions

View File

@ -1,11 +1,11 @@
SV.registerLayout('BinaryTree', { SV.registerLayout('BinaryTree', {
defineOptions() { defineOptions() {
return { return {
element: { node: {
default: { default: {
type: 'binary-tree-node', type: 'binary-tree-node',
size: [60, 30], size: [60, 30],
label: '[data]', label: '[id]',
style: { style: {
fill: '#b83b5e', fill: '#b83b5e',
stroke: '#333', stroke: '#333',
@ -58,7 +58,7 @@ SV.registerLayout('BinaryTree', {
/** /**
* 对子树进行递归布局 * 对子树进行递归布局
*/ */
layoutItem(node, parent, index, layoutOptions) { layoutItem(node, layoutOptions) {
// 次双亲不进行布局 // 次双亲不进行布局
if (!node) { if (!node) {
return null; return null;
@ -69,43 +69,54 @@ SV.registerLayout('BinaryTree', {
height = bound.height, height = bound.height,
group = new Group(node), group = new Group(node),
leftGroup = null, leftGroup = null,
rightGroup = null; rightGroup = null,
leftBound = null,
rightBound = null;
if (node.visited) { if (node.visited) {
return null; return null;
} }
if (node.child && node.child[0]) { 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]) { if (node.child && node.child[1]) {
rightGroup = this.layoutItem(node.child[1], node, 1, layoutOptions); rightGroup = this.layoutItem(node.child[1], layoutOptions);
} }
// 处理左右子树相交问题 if (leftGroup) {
if (leftGroup && rightGroup) { leftBound = leftGroup.getBound();
let intersection = Bound.intersect(leftGroup.getBound(), rightGroup.getBound()), node.set('y', leftBound.y - layoutOptions.yInterval - height);
move = 0; }
if (intersection && intersection.width > 0) { if(rightGroup) {
move = (intersection.width + layoutOptions.xInterval) / 2; rightBound = rightGroup.getBound();
leftGroup.translate(-move, 0);
rightGroup.translate(move, 0); 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) { if (leftGroup) {
let leftBound = leftGroup.getBound(); leftBound = leftGroup.getBound();
node.set('y', leftBound.y - layoutOptions.yInterval - height);
node.set('x', leftBound.x + leftBound.width + layoutOptions.xInterval / 2 - width); node.set('x', leftBound.x + leftBound.width + layoutOptions.xInterval / 2 - width);
} }
if(rightGroup) { if(rightGroup) {
let rightBound = rightGroup.getBound(); rightBound = rightGroup.getBound();
node.set('y', rightBound.y - layoutOptions.yInterval - height);
node.set('x', rightBound.x - layoutOptions.xInterval / 2 - width); node.set('x', rightBound.x - layoutOptions.xInterval / 2 - width);
} }
@ -129,7 +140,7 @@ SV.registerLayout('BinaryTree', {
*/ */
layout(elements, layoutOptions) { layout(elements, layoutOptions) {
let root = elements[0]; let root = elements[0];
this.layoutItem(root, null, -1, layoutOptions); this.layoutItem(root, layoutOptions);
}, },
}); });

View File

@ -77,25 +77,37 @@ SV.registerLayout('LinkList', {
layout: { layout: {
xInterval: 50, xInterval: 50,
yInterval: 50 yInterval: 50
},
behavior: {
dragNode: false
} }
}; };
}, },
layout(elements, layoutOptions) { /**
for (let i = 0; i < elements.length; i++) { * 对子树进行递归布局
let node = elements[i], * @param node
prev = elements[1 - 1], * @param parent
width = node.get('size')[0]; */
layoutItem(node, prev, layoutOptions) {
if (prev) { if (!node) {
node.set('y', prev.get('y')); return null;
node.set('x', prev.get('x') + layoutOptions.xInterval + width);
}
} }
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);
} }
}); });

View File

@ -20,7 +20,7 @@
element: { element: {
default: { default: {
type: 'link-list-node', type: 'link-list-node',
label: '[id]', label: '[data]',
size: [60, 30], size: [60, 30],
style: { style: {
stroke: '#333', stroke: '#333',

120
demoV2/data.js Normal file
View File

@ -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,
},
];

View File

@ -1,237 +1,151 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DEMO</title>
<style>
* {
padding: 0;
margin: 0;
user-select: none;
}
<head> .container {
<meta charset="UTF-8" /> background-color: #fafafa;
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> border: 1px solid #ccc;
<title>DEMO</title> position: relative;
<style> }
* {
padding: 0;
margin: 0;
user-select: none;
}
.container { .down {
background-color: #fafafa; display: flex;
border: 1px solid #ccc; margin-top: 20px;
position: relative; }
}
.down { #container {
display: flex; width: 100%;
margin-top: 20px; height: 500px;
} position: relative;
overflow: hidden;
}
#container { #leak {
width: 100%; position: absolute;
height: 500px; left: 0;
position: relative; opacity: 0;
overflow: hidden; 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;
}
#leak { #leak > span {
position: absolute; color: #000;
left: 0; }
opacity: 0; </style>
top: 100px; </head>
width: 100%;
box-sizing: border-box;
padding: 4px;
border-top: 1px dashed #000;
pointer-events: none;
transition: opacity 0.75s ease-in-out;
}
#leak>span { <body>
color: #000; <div class="container" id="container">
} <div id="leak">
</style> <span>泄漏区</span>
</head> </div>
</div>
<body> <button id="btn-prev">prev</button>
<div class="container" id="container"> <button id="btn-next">next</button>
<div id="leak"> <button id="resize">resize</button>
<span>泄漏区</span> <button id="relayout">relayout</button>
</div> <button id="switch-mode">switch mode</button>
</div> <button id="brush-select">brush-select</button>
<span id="pos"></span>
<button id="btn-prev">prev</button> <script src="./../dist/sv.js"></script>
<button id="btn-next">next</button> <script>
<button id="resize">resize</button> const Group = SV.Group,
<button id="relayout">relayout</button> Bound = SV.Bound,
<button id="switch-mode">switch mode</button> G6 = SV.G6,
<button id="brush-select">brush-select</button> Vector = SV.Vector;
<span id="pos"></span> </script>
<script src="./Layouter/LinkList.js"></script>
<script src="./Layouter/BinaryTree.js"></script>
<script src="./Layouter/Stack.js"></script>
<script src="./Layouter/LinkQueue.js"></script>
<script src="./Layouter/GeneralizedList.js"></script>
<script src="./Layouter/ChainHashTable.js"></script>
<script src="./Layouter/Array.js"></script>
<script src="./Layouter/HashTable.js"></script>
<script src="./Layouter/LinkStack.js"></script>
<script src="./Layouter/AdjoinMatrixGraph.js"></script>
<script src="./Layouter/AdjoinTableGraph.js"></script>
<script src="./Layouter/SqQueue.js"></script>
<script src="./Layouter/PTree.js"></script>
<script src="./Layouter/PCTree.js"></script>
<script src="./data.js"></script>
<script src="./../dist/sv.js"></script> <script>
<script> let cur = SV(document.getElementById('container'), {
const Group = SV.Group, view: {
Bound = SV.Bound, leakAreaHeight: 130,
G6 = SV.G6, groupPadding: 40,
Vector = SV.Vector; },
</script> });
<script src="./Layouter/LinkList.js"></script>
<script src="./Layouter/BinaryTree.js"></script>
<script src="./Layouter/Stack.js"></script>
<script src="./Layouter/LinkQueue.js"></script>
<script src="./Layouter/GeneralizedList.js"></script>
<script src="./Layouter/ChainHashTable.js"></script>
<script src="./Layouter/Array.js"></script>
<script src="./Layouter/HashTable.js"></script>
<script src="./Layouter/LinkStack.js"></script>
<script src="./Layouter/AdjoinMatrixGraph.js"></script>
<script src="./Layouter/AdjoinTableGraph.js"></script>
<script src="./Layouter/SqQueue.js"></script>
<script src="./Layouter/PTree.js"></script>
<script src="./Layouter/PCTree.js"></script>
<script> let dataIndex = 0,
let cur = SV(document.getElementById('container'), { curData = SOURCES_DATA[dataIndex];
view: {
leakAreaHeight: 130,
groupPadding: 40,
},
});
let data = [{ let enableBrushSelect = false;
"BinaryTree0": {
"data": [{
"external": [
"T1",
"r"
],
"child": [
"0x617ee0",
"0x617f10"
],
"id": "0x617eb0",
"name": "T1",
"data": "Z",
"root": true,
"type": "default"
}, {
"child": [
"0x0",
"0x0"
],
"id": "0x617ee0",
"name": "T1->lchild",
"data": "A",
"type": "default"
}, {
"child": [
"0x0",
"0x617f40"
],
"id": "0x617f10",
"name": "T1->rchild",
"data": "B",
"type": "default",
"external": [
"t"
]
}, ],
"layouter": "BinaryTree"
},
"isEnterFunction": false
}, {
"BinaryTree1": {
"data": [{
"external": [
"T1",
"r"
],
"child": [
"0x617ee0",
"0x617f10"
],
"id": "0x617eb0",
"name": "T1",
"data": "Z",
"root": true,
"type": "default"
}, {
"child": [
"0x0",
"0x0"
],
"id": "0x617ee0",
"name": "T1->lchild",
"data": "A",
freed: 'freed',
"type": "default"
}, {
"child": [
"0x0",
"0x617f40"
],
"id": "0x617f10",
"name": "T1->rchild",
"data": "B",
"type": "default",
"external": [
"t"
]
}, ],
"layouter": "BinaryTree"
},
"isEnterFunction": false
}, ];
let dataIndex = 0, const container = document.getElementById('container'),
curData = data[dataIndex]; pos = document.getElementById('pos');
let enableBrushSelect = false; const leak = document.getElementById('leak');
const container = document.getElementById('container'), cur.render(curData);
pos = document.getElementById('pos');
const leak = document.getElementById('leak'); document.getElementById('btn-next').addEventListener('click', e => {
curData = SOURCES_DATA[++dataIndex];
cur.render(curData);
});
cur.render(curData); document.getElementById('btn-prev').addEventListener('click', e => {
curData = SOURCES_DATA[--dataIndex];
cur.render(curData);
});
document.getElementById('btn-next').addEventListener('click', e => { document.getElementById('resize').addEventListener('click', e => {
curData = data[++dataIndex]; container.style.height = 800 + 'px';
cur.render(curData); cur.resize(container.offsetWidth, container.offsetHeight);
}); });
document.getElementById('btn-prev').addEventListener('click', e => { document.getElementById('relayout').addEventListener('click', e => {
curData = data[--dataIndex]; cur.reLayout();
cur.render(curData); });
});
document.getElementById('resize').addEventListener('click', e => { document.getElementById('switch-mode').addEventListener('click', e => {
container.style.height = 800 + 'px'; cur.updateStyle('Array', newArrayOption);
cur.resize(container.offsetWidth, container.offsetHeight); });
});
document.getElementById('relayout').addEventListener('click', e => { document.getElementById('brush-select').addEventListener('click', e => {
cur.reLayout(); enableBrushSelect = !enableBrushSelect;
}); cur.switchBrushSelect(enableBrushSelect);
});
document.getElementById('switch-mode').addEventListener('click', e => { cur.on('onLeakAreaUpdate', payload => {
cur.updateStyle('Array', newArrayOption); leak.style.opacity = payload.hasLeak ? 1 : 0;
}); leak.style.top = payload.leakAreaY - 40 + 'px';
});
document.getElementById('brush-select').addEventListener('click', e => { // -------------------------------------------------------------------------------------------------------
enableBrushSelect = !enableBrushSelect;
cur.switchBrushSelect(enableBrushSelect);
});
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}`;
});
</script>
</body>
container.addEventListener('mousemove', e => {
let x = e.offsetX,
y = e.offsetY;
pos.innerHTML = `${x},${y}`;
});
</script>
</body>
</html> </html>

View File

@ -76,7 +76,7 @@ export function SolveNodeAppendagesDrag(viewContainer: ViewContainer) {
item.setSelectedState(false); item.setSelectedState(false);
if (item instanceof SVNode) { if (item instanceof SVNode) {
item.appendages.forEach(appendage => appendage.setSelectedState(false)); item.getAppendagesList().forEach(appendage => appendage.setSelectedState(false));
} }
}); });
viewContainer.brushSelectedModels.length = 0; viewContainer.brushSelectedModels.length = 0;
@ -100,7 +100,7 @@ export function SolveNodeAppendagesDrag(viewContainer: ViewContainer) {
y: node.G6Item.getModel().y y: node.G6Item.getModel().y
}); });
node.appendages.forEach(item => { node.getAppendagesList().forEach(item => {
item.setSelectedState(false); item.setSelectedState(false);
item.set({ item.set({
x: item.G6Item.getModel().x, x: item.G6Item.getModel().x,
@ -116,7 +116,7 @@ export function SolveNodeAppendagesDrag(viewContainer: ViewContainer) {
}); });
if(item instanceof SVNode) { if(item instanceof SVNode) {
item.appendages.forEach(appendage => { item.getAppendagesList().forEach(appendage => {
appendage.set({ appendage.set({
x: appendage.G6Item.getModel().x, x: appendage.G6Item.getModel().x,
y: appendage.G6Item.getModel().y y: appendage.G6Item.getModel().y

View File

@ -45,7 +45,7 @@ export function InitG6Behaviors(engine: Engine, viewContainer: ViewContainer): M
// 这里之所以要把节点和其 appendages 的选中状态设置为true是因为 g6 处理拖拽节点的逻辑是将所以已选中的元素一起拖动, // 这里之所以要把节点和其 appendages 的选中状态设置为true是因为 g6 处理拖拽节点的逻辑是将所以已选中的元素一起拖动,
// 这样 appendages 就可以很自然地跟着节点动(我是看源码才知道的) // 这样 appendages 就可以很自然地跟着节点动(我是看源码才知道的)
node.setSelectedState(true); node.setSelectedState(true);
node.appendages.forEach(item => { node.getAppendagesList().forEach(item => {
item.setSelectedState(true); item.setSelectedState(true);
}); });

View File

@ -1,152 +1,161 @@
import { Vector } from "./vector"; import { Vector } from './vector';
// 包围盒类型 // 包围盒类型
export type BoundingRect = { export type BoundingRect = {
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
}; };
// 包围盒操作 // 包围盒操作
export const Bound = { export const Bound = {
/**
*
* @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];
* @param points if (item[0] < minX) minX = item[0];
*/ if (item[1] > maxY) maxY = item[1];
fromPoints(points: Array<[number, number]>): BoundingRect { if (item[1] < minY) minY = item[1];
let maxX = -Infinity, });
minX = Infinity,
maxY = -Infinity,
minY = Infinity;
points.map(item => { return {
if(item[0] > maxX) maxX = item[0]; x: minX,
if(item[0] < minX) minX = item[0]; y: minY,
if(item[1] > maxY) maxY = item[1]; width: maxX - minX,
if(item[1] < minY) minY = item[1]; height: maxY - minY,
}); };
},
return { /**
x: minX, *
y: minY, * @param bound
width: maxX - minX, */
height: maxY - minY 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 * @param arg
*/ */
toPoints(bound: BoundingRect): Array<[number, number]> { union(...arg: BoundingRect[]): BoundingRect {
return [ if (arg.length === 0) {
[bound.x, bound.y], return {
[bound.x + bound.width, bound.y], x: 0,
[bound.x + bound.width, bound.y + bound.height], y: 0,
[bound.x, bound.y + bound.height] width: 0,
]; height: 0,
}, };
}
/** return arg.length > 1
* ? arg.reduce((total, cur) => {
* @param arg let minX = Math.min(total.x, cur.x),
*/ maxX = Math.max(total.x + total.width, cur.x + cur.width),
union(...arg: BoundingRect[]): BoundingRect { minY = Math.min(total.y, cur.y),
return arg.length > 1? maxY = Math.max(total.y + total.height, cur.y + cur.height);
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 { return {
x: minX, x: minX,
y: minY, y: minY,
width: maxX - minX, width: maxX - minX,
height: maxY - minY height: maxY - minY,
}; };
}): arg[0]; })
}, : arg[0];
},
/** /**
* *
* @param b1 * @param b1
* @param b2 * @param b2
*/ */
intersect(b1: BoundingRect, b2: BoundingRect): BoundingRect { intersect(b1: BoundingRect, b2: BoundingRect): BoundingRect {
let x, y, let x,
maxX, maxY, y,
overlapsX, maxX,
overlapsY; 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 = Math.max(b1x, b2x);
x = b1.x < b2.x? b2.x: b1.x; maxX = Math.min(b1mx, b2mx);
// maxX = b1.x + b1.width < b2.x + b2.width? b1.x + b1.width: b2.x + b2.width; overlapsX = maxX - x;
maxX = b1.x + b1.width;
overlapsX = maxX - x;
}
if(b1.y < b2.y + b2.height && b1.y + b1.height > b2.y) { y = Math.max(b1y, b2y);
y = b1.y < b2.y? b2.y: b1.y; maxY = Math.min(b1my, b2my);
maxY = b1.y + b1.height < b2.y + b2.height? b1.y + b1.height: b2.y + b2.height; overlapsY = maxY - y;
overlapsY = maxY - y;
}
if(!overlapsX || !overlapsY) return null; if (!overlapsX || !overlapsY) return null;
return { return {
x, x,
y, y,
width: overlapsX, width: overlapsX,
height: overlapsY height: overlapsY,
}; };
}, },
/** /**
* *
* @param bound * @param bound
* @param dx * @param dx
* @param dy * @param dy
*/ */
translate(bound: BoundingRect, dx: number, dy: number) { translate(bound: BoundingRect, dx: number, dy: number) {
bound.x += dx; bound.x += dx;
bound.y += dy; bound.y += dy;
}, },
/** /**
* *
* @param bound * @param bound
* @param rot * @param rot
*/ */
rotation(bound: BoundingRect, rot: number): BoundingRect { rotation(bound: BoundingRect, rot: number): BoundingRect {
let cx = bound.x + bound.width / 2, let cx = bound.x + bound.width / 2,
cy = bound.y + bound.height / 2; cy = bound.y + bound.height / 2;
return Bound.fromPoints(Bound.toPoints(bound).map(item => Vector.rotation(rot, item, [cx, cy]))); return Bound.fromPoints(Bound.toPoints(bound).map(item => Vector.rotation(rot, item, [cx, cy])));
}, },
/** /**
* *
* @param b1 * @param b1
* @param b2 * @param b2
*/ */
isOverlap(b1: BoundingRect, b2: BoundingRect): boolean { isOverlap(b1: BoundingRect, b2: BoundingRect): boolean {
let maxX1 = b1.x + b1.width, let maxX1 = b1.x + b1.width,
maxY1 = b1.y + b1.height, maxY1 = b1.y + b1.height,
maxX2 = b2.x + b2.width, maxX2 = b2.x + b2.width,
maxY2 = b2.y + b2.height; maxY2 = b2.y + b2.height;
if (b1.x < maxX2 && b2.x < maxX1 && b1.y < maxY2 && b2.y < maxY1) { if (b1.x < maxX2 && b2.x < maxX1 && b1.y < maxY2 && b2.y < maxY1) {
return true; return true;
} }
return false; return false;
} },
}; };

View File

@ -122,21 +122,6 @@ export const Util = {
return list; 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 * modelList G6Data
* @param modelList * @param modelList

View File

@ -49,4 +49,11 @@ export class SVLink extends SVModel {
curveOffset: options.curveOffset 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;
}
}; };

View File

@ -1,219 +1,204 @@
import { Util } from "../Common/util"; import { Util } from '../Common/util';
import { Style } from "../options"; import { Style } from '../options';
import { BoundingRect } from "../Common/boundingRect"; import { BoundingRect } from '../Common/boundingRect';
import { EdgeConfig, Item, NodeConfig } from "@antv/g6-core"; import { EdgeConfig, Item, NodeConfig } from '@antv/g6-core';
import { Graph } from "@antv/g6-pc"; import { Graph } from '@antv/g6-pc';
import merge from 'merge'; import merge from 'merge';
import { ModelConstructor } from "./modelConstructor";
export class SVModel { export class SVModel {
public id: string; public id: string;
public sourceType: string; public sourceType: string;
public g6Instance: Graph; public g6Instance: Graph;
public shadowG6Instance: Graph; public shadowG6Instance: Graph;
public group: string; public group: string;
public layout: string; public layout: string;
public G6ModelProps: NodeConfig | EdgeConfig; public G6ModelProps: NodeConfig | EdgeConfig;
public shadowG6Item: Item; public shadowG6Item: Item;
public G6Item: Item; public G6Item: Item;
public preLayout: boolean; // 是否进入预备布局阶段 public preLayout: boolean; // 是否进入预备布局阶段
public discarded: boolean; public discarded: boolean;
public freed: boolean; public freed: boolean;
public leaked: boolean; public leaked: boolean;
public generalStyle: Partial<Style>; public generalStyle: Partial<Style>;
private transformMatrix: number[]; private transformMatrix: number[];
private modelType: string; private modelType: string;
public layoutX: number; public layoutX: number;
public layoutY: number; public layoutY: number;
constructor(id: string, type: string, group: string, layout: string, modelType: string) { constructor(id: string, type: string, group: string, layout: string, modelType: string) {
this.id = id; this.id = id;
this.sourceType = type; this.sourceType = type;
this.group = group; this.group = group;
this.layout = layout; this.layout = layout;
this.shadowG6Item = null; this.shadowG6Item = null;
this.G6Item = null;
this.preLayout = false;
this.discarded = false;
this.freed = false;
this.leaked = false;
this.transformMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
this.modelType = modelType;
}
/**
* @override
* G6 model
* @param option
*/
generateG6ModelProps(options: unknown): NodeConfig | EdgeConfig {
return null;
}
/**
* G6 model
* @param attr
*/
get(attr: string): any {
return this.G6ModelProps[attr];
}
/**
* G6 model
* @param attr
* @param value
* @returns
*/
set(attr: string | object, value?: any) {
if (this.discarded) {
return;
}
if (typeof attr === 'object') {
Object.keys(attr).map(item => {
this.set(item, attr[item]);
});
return;
}
if (this.G6ModelProps[attr] === value) {
return;
}
if (attr === 'style' || attr === 'labelCfg') {
this.G6ModelProps[attr] = merge(this.G6ModelProps[attr] || {}, value);
} else {
this.G6ModelProps[attr] = value;
}
if (attr === 'rotation') {
const matrix = Util.calcRotateMatrix(this.getMatrix(), value);
this.setMatrix(matrix);
}
// 更新G6Item
if (this.G6Item) {
if (this.preLayout) {
const G6ItemModel = this.G6Item.getModel();
G6ItemModel[attr] = value;
} else {
this.g6Instance.updateItem(this.G6Item, this.G6ModelProps);
}
}
// 更新shadowG6Item
if (this.shadowG6Item) {
this.shadowG6Instance.updateItem(this.shadowG6Item, this.G6ModelProps);
}
}
/**
*
* @param G6ModelProps
*/
updateG6ModelStyle(G6ModelProps: NodeConfig | EdgeConfig) {
const newG6ModelProps = {
style: {
...G6ModelProps.style,
},
labelCfg: {
...G6ModelProps.labelCfg,
},
};
this.G6ModelProps = merge.recursive(this.G6ModelProps, newG6ModelProps);
if (this.G6Item) {
this.g6Instance.updateItem(this.G6Item, this.G6ModelProps);
}
if (this.shadowG6Item) {
this.shadowG6Instance.updateItem(this.shadowG6Item, this.G6ModelProps);
}
}
/**
*
* @returns
*/
getBound(): BoundingRect {
return this.shadowG6Item.getBBox();
}
/**
*
*/
getMatrix(): number[] {
return [...this.transformMatrix];
}
/**
*
* @param matrix
*/
setMatrix(matrix: number[]) {
this.transformMatrix = matrix;
this.set('style', { matrix });
}
/**
*
* @param isSelected
*/
setSelectedState(isSelected: boolean) {
if (this.G6Item === null) {
return;
}
this.G6Item.setState('selected', isSelected);
}
/**
*
* @returns
*/
getG6ModelProps(): NodeConfig | EdgeConfig {
return Util.objectClone(this.G6ModelProps);
}
/**
* model
* @returns
*/
getModelType(): string {
return this.modelType;
}
/**
* modelSVNode
*/
isNode(): boolean {
return false;
}
beforeDestroy () {}
destroy() {
this.G6Item = null; this.G6Item = null;
this.preLayout = false; this.shadowG6Instance = null;
this.discarded = false; this.shadowG6Item = null;
this.freed = false;
this.leaked = false;
this.transformMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
this.modelType = modelType;
}
/**
* @override
* G6 model
* @param option
*/
generateG6ModelProps(options: unknown): NodeConfig | EdgeConfig {
return null;
}
/**
* G6 model
* @param attr
*/
get(attr: string): any {
return this.G6ModelProps[attr];
}
/**
* G6 model
* @param attr
* @param value
* @returns
*/
set(attr: string | object, value?: any) {
if (this.discarded) {
return;
}
if (typeof attr === 'object') {
Object.keys(attr).map(item => {
this.set(item, attr[item]);
});
return;
}
if (this.G6ModelProps[attr] === value) {
return;
}
if (attr === 'style' || attr === 'labelCfg') {
this.G6ModelProps[attr] = merge(this.G6ModelProps[attr] || {}, value);
}
else {
this.G6ModelProps[attr] = value;
}
if (attr === 'rotation') {
const matrix = Util.calcRotateMatrix(this.getMatrix(), value);
this.setMatrix(matrix);
}
// 更新G6Item
if (this.G6Item) {
if (this.preLayout) {
const G6ItemModel = this.G6Item.getModel();
G6ItemModel[attr] = value;
}
else {
this.g6Instance.updateItem(this.G6Item, this.G6ModelProps);
}
}
// 更新shadowG6Item
if (this.shadowG6Item) {
this.shadowG6Instance.updateItem(this.shadowG6Item, this.G6ModelProps);
}
}
/**
*
* @param G6ModelProps
*/
updateG6ModelStyle(G6ModelProps: NodeConfig | EdgeConfig) {
const newG6ModelProps = {
style: {
...G6ModelProps.style
},
labelCfg: {
...G6ModelProps.labelCfg
}
};
this.G6ModelProps = merge.recursive(this.G6ModelProps, newG6ModelProps);
if (this.G6Item) {
this.g6Instance.updateItem(this.G6Item, this.G6ModelProps);
}
if (this.shadowG6Item) {
this.shadowG6Instance.updateItem(this.shadowG6Item, this.G6ModelProps);
}
}
/**
*
* @returns
*/
getBound(): BoundingRect {
return this.shadowG6Item.getBBox();
}
/**
*
*/
getMatrix(): number[] {
return [...this.transformMatrix];
}
/**
*
* @param matrix
*/
setMatrix(matrix: number[]) {
this.transformMatrix = matrix;
this.set('style', { matrix });
}
/**
*
* @param isSelected
*/
setSelectedState(isSelected: boolean) {
if(this.G6Item === null) {
return;
}
this.G6Item.setState('selected', isSelected);
}
/**
*
* @returns
*/
getG6ModelProps(): NodeConfig | EdgeConfig {
return Util.objectClone(this.G6ModelProps);
}
/**
* model
* @returns
*/
getModelType(): string {
return this.modelType;
}
/**
* modelSVNode
*/
isNode(): boolean {
return false;
} }
} }

View File

@ -5,7 +5,7 @@ import { SourceNode } from '../sources';
import { ModelConstructor } from './modelConstructor'; import { ModelConstructor } from './modelConstructor';
import { SVLink } from './SVLink'; import { SVLink } from './SVLink';
import { SVModel } from './SVModel'; import { SVModel } from './SVModel';
import { SVAddressLabel, SVFreedLabel, SVIndexLabel, SVMarker, SVNodeAppendage } from './SVNodeAppendage'; import { SVNodeAppendage } from './SVNodeAppendage';
export class SVNode extends SVModel { export class SVNode extends SVModel {
public sourceId: string; public sourceId: string;
@ -17,17 +17,7 @@ export class SVNode extends SVModel {
private label: string | string[]; private label: string | string[];
private disable: boolean; private disable: boolean;
public appendages: { [key: string]: SVNodeAppendage[] };
public shadowG6Item: INode;
public G6Item: INode;
public marker: SVMarker;
public freedLabel: SVFreedLabel;
public indexLabel: SVIndexLabel;
public addressLabel: SVAddressLabel;
public appendages: SVNodeAppendage[];
public modelConstructor: ModelConstructor;
constructor( constructor(
id: string, id: string,
@ -53,7 +43,7 @@ export class SVNode extends SVModel {
this.sourceId = sourceNode.id.toString(); this.sourceId = sourceNode.id.toString();
this.links = { inDegree: [], outDegree: [] }; this.links = { inDegree: [], outDegree: [] };
this.appendages = []; this.appendages = {};
this.sourceNode = sourceNode; this.sourceNode = sourceNode;
this.label = label; this.label = label;
this.G6ModelProps = this.generateG6ModelProps(options); this.G6ModelProps = this.generateG6ModelProps(options);
@ -94,7 +84,7 @@ export class SVNode extends SVModel {
} }
this.G6Item.setState('selected', isSelected); this.G6Item.setState('selected', isSelected);
this.appendages.forEach(item => { this.getAppendagesList().forEach(item => {
item.setSelectedState(isSelected); item.setSelectedState(isSelected);
}); });
} }
@ -103,11 +93,28 @@ export class SVNode extends SVModel {
return this.sourceId; return this.sourceId;
} }
getAppendagesList(): SVNodeAppendage[] {
const list = [];
Object.entries(this.appendages).forEach(item => {
list.push(...item[1]);
});
return list;
}
/** /**
* group * group
* @param node * @param node
*/ */
isSameGroup(node: SVNode): boolean { isSameGroup(node: SVNode): boolean {
return this.modelConstructor.isSameGroup(this, node); return ModelConstructor.isSameGroup(this, node);
}
beforeDestroy(): void {
this.sourceNode = null;
this.links.inDegree.length = 0;
this.links.outDegree.length = 0;
this.appendages = {};
} }
} }

View File

@ -1,182 +1,208 @@
import { INode, NodeConfig, EdgeConfig } from "@antv/g6-core"; import { INode, NodeConfig, EdgeConfig } from '@antv/g6-core';
import { Util } from "../Common/util"; import { Bound, BoundingRect } from '../Common/boundingRect';
import { AddressLabelOption, IndexLabelOption, MarkerOption, NodeLabelOption, Style } from "../options"; import { Util } from '../Common/util';
import { SVModel } from "./SVModel"; import { AddressLabelOption, IndexLabelOption, MarkerOption, NodeLabelOption, Style } from '../options';
import { SVNode } from "./SVNode"; import { SVModel } from './SVModel';
import { SVNode } from './SVNode';
export class SVNodeAppendage extends SVModel { export class SVNodeAppendage extends SVModel {
public target: SVNode; public target: SVNode;
constructor(id: string, type: string, group: string, layout: string, modelType: string, target: SVNode) { constructor(id: string, type: string, group: string, layout: string, modelType: string, target: SVNode) {
super(id, type, group, layout, modelType); super(id, type, group, layout, modelType);
this.target = target; this.target = target;
this.target.appendages.push(this);
} if (this.target.appendages[modelType] === undefined) {
this.target.appendages[modelType] = [];
}
this.target.appendages[modelType].push(this);
}
beforeDestroy(): void {
const targetAppendageList = this.target.appendages[this.getModelType()];
if (targetAppendageList) {
Util.removeFromList(targetAppendageList, item => item.id === this.id);
}
this.target = null;
}
} }
/** /**
* *
*/ */
export class SVFreedLabel extends SVNodeAppendage { export class SVFreedLabel extends SVNodeAppendage {
constructor(id: string, type: string, group: string, layout: string, target: SVNode) { constructor(id: string, type: string, group: string, layout: string, target: SVNode) {
super(id, type, group, layout, 'freedLabel', target); super(id, type, group, layout, 'freedLabel', target);
this.G6ModelProps = this.generateG6ModelProps();
}
this.target.freedLabel = this; generateG6ModelProps() {
this.G6ModelProps = this.generateG6ModelProps(); return {
} id: this.id,
x: 0,
generateG6ModelProps() { y: 0,
return { type: 'rect',
id: this.id, label: '已释放',
x: 0, labelCfg: {
y: 0, style: {
type: 'rect', fill: '#b83b5e',
label: '已释放', opacity: 0.6,
labelCfg: { },
style: { },
fill: '#b83b5e', size: [0, 0],
opacity: 0.6 style: {
} opacity: 0,
}, stroke: null,
size: [0, 0], fill: 'transparent',
style: { },
opacity: 0, };
stroke: null, }
fill: 'transparent'
}
};
}
} }
/** /**
* *
*/ */
export class SVAddressLabel extends SVNodeAppendage { export class SVAddressLabel extends SVNodeAppendage {
private sourceId: string; private sourceId: string;
constructor(id: string, type: string, group: string, layout: string, target: SVNode, options: AddressLabelOption) { constructor(id: string, type: string, group: string, layout: string, target: SVNode, options: AddressLabelOption) {
super(id, type, group, layout, 'addressLabel', target); super(id, type, group, layout, 'addressLabel', target);
this.sourceId = target.sourceId; this.sourceId = target.sourceId;
this.target.addressLabel = this; this.G6ModelProps = this.generateG6ModelProps(options);
this.G6ModelProps = this.generateG6ModelProps(options); }
}
generateG6ModelProps(options: AddressLabelOption) { getBound(): BoundingRect {
return { const textBound = this.shadowG6Item.getContainer().getChildren()[1].getBBox(),
id: this.id, keyBound = this.shadowG6Item.getBBox();
x: 0, return {
y: 0, x: keyBound.x + textBound.x,
type: 'rect', y: keyBound.y + textBound.y,
label: this.sourceId, width: textBound.width,
labelCfg: { height: textBound.height,
style: { };
fill: '#666', }
fontSize: 16,
...options.style generateG6ModelProps(options: AddressLabelOption) {
} return {
}, id: this.id,
size: [0, 0], x: 0,
style: { y: 0,
stroke: null, type: 'rect',
fill: 'transparent' label: this.sourceId,
} labelCfg: {
}; style: {
} fill: '#666',
fontSize: 16,
...options.style,
},
},
size: [0, 0],
style: {
stroke: null,
fill: 'transparent',
},
};
}
} }
/** /**
* *
*/ */
export class SVIndexLabel extends SVNodeAppendage { export class SVIndexLabel extends SVNodeAppendage {
private value: string; private value: string;
constructor(id: string, indexName: string, group: string, layout: string, value: string, target: SVNode, options: IndexLabelOption) { constructor(
super(id, indexName, group, layout, 'indexLabel', target); id: string,
indexName: string,
group: string,
layout: string,
value: string,
target: SVNode,
options: IndexLabelOption
) {
super(id, indexName, group, layout, 'indexLabel', target);
this.value = value;
this.G6ModelProps = this.generateG6ModelProps(options) as NodeConfig;
}
this.target.indexLabel = this; generateG6ModelProps(options: IndexLabelOption): NodeConfig | EdgeConfig {
this.value = value; return {
this.G6ModelProps = this.generateG6ModelProps(options) as NodeConfig; id: this.id,
} x: 0,
y: 0,
generateG6ModelProps(options: IndexLabelOption): NodeConfig | EdgeConfig { type: 'rect',
return { label: this.value,
id: this.id, labelCfg: {
x: 0, style: {
y: 0, fill: '#bbb',
type: 'rect', textAlign: 'center',
label: this.value, textBaseline: 'middle',
labelCfg: { fontSize: 14,
style: { fontStyle: 'italic',
fill: '#bbb', ...options.style,
textAlign: 'center', },
textBaseline: 'middle', },
fontSize: 14, size: [0, 0],
fontStyle: 'italic', style: {
...options.style stroke: null,
} fill: 'transparent',
}, },
size: [0, 0], };
style: { }
stroke: null,
fill: 'transparent'
}
};
}
} }
/** /**
* *
*/ */
export class SVMarker extends SVNodeAppendage { export class SVMarker extends SVNodeAppendage {
public label: string | string[]; public label: string | string[];
public anchor: number; public anchor: number;
public shadowG6Item: INode; public shadowG6Item: INode;
public G6Item: INode; public G6Item: INode;
constructor(id: string, type: string, group: string, layout: string, label: string | string[], target: SVNode, options: MarkerOption) { constructor(
super(id, type, group, layout, 'marker', target); id: string,
type: string,
group: string,
layout: string,
label: string | string[],
target: SVNode,
options: MarkerOption
) {
super(id, type, group, layout, 'marker', target);
this.label = label; this.label = label;
this.G6ModelProps = this.generateG6ModelProps(options);
}
this.target.marker = this; generateG6ModelProps(options: MarkerOption): NodeConfig {
this.G6ModelProps = this.generateG6ModelProps(options); this.anchor = options.anchor;
}
generateG6ModelProps(options: MarkerOption): NodeConfig { const type = options.type,
this.anchor = options.anchor; defaultSize: [number, number] = type === 'pointer' ? [8, 30] : [12, 12];
const type = options.type, return {
defaultSize: [number, number] = type === 'pointer' ? [8, 30] : [12, 12]; id: this.id,
x: 0,
y: 0,
rotation: 0,
type: options.type || 'marker',
size: options.size || defaultSize,
anchorPoints: null,
label: typeof this.label === 'string' ? this.label : this.label.join(', '),
style: Util.objectClone<Style>(options.style),
labelCfg: Util.objectClone<NodeLabelOption>(options.labelOptions),
};
}
return { public getLabelSizeRadius(): number {
id: this.id, const { width, height } = this.shadowG6Item.getContainer().getChildren()[2].getBBox();
x: 0, return Math.max(width, height);
y: 0, }
rotation: 0, }
type: options.type || 'marker',
size: options.size || defaultSize,
anchorPoints: null,
label: typeof this.label === 'string' ? this.label : this.label.join(', '),
style: Util.objectClone<Style>(options.style),
labelCfg: Util.objectClone<NodeLabelOption>(options.labelOptions)
};
}
public getLabelSizeRadius(): number {
const { width, height } = this.shadowG6Item.getContainer().getChildren()[2].getBBox();
return Math.max(width, height);
}
};

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,6 @@ import { Vector } from "./Common/vector";
import { EngineOptions, LayoutCreator } from "./options"; import { EngineOptions, LayoutCreator } from "./options";
import { SourceNode } from "./sources"; import { SourceNode } from "./sources";
import { Util } from "./Common/util"; import { Util } from "./Common/util";
import { SVModel } from "./Model/SVModel";
import { SVNode } from "./Model/SVNode"; import { SVNode } from "./Model/SVNode";

View File

@ -1,10 +1,10 @@
import { IPoint } from '@antv/g6-core'; import { IPoint, INode } from '@antv/g6-core';
import { Bound, BoundingRect } from '../Common/boundingRect'; import { Bound, BoundingRect } from '../Common/boundingRect';
import { Group } from '../Common/group'; import { Group } from '../Common/group';
import { Util } from '../Common/util'; import { Util } from '../Common/util';
import { Vector } from '../Common/vector'; import { Vector } from '../Common/vector';
import { Engine } from '../engine'; import { Engine } from '../engine';
import { LayoutGroupTable } from '../Model/modelConstructor'; import { LayoutGroupTable, ModelConstructor } from '../Model/modelConstructor';
import { SVModel } from '../Model/SVModel'; import { SVModel } from '../Model/SVModel';
import { SVAddressLabel, SVFreedLabel, SVIndexLabel, SVMarker } from '../Model/SVNodeAppendage'; import { SVAddressLabel, SVFreedLabel, SVIndexLabel, SVMarker } from '../Model/SVNodeAppendage';
import { AddressLabelOption, IndexLabelOption, LayoutOptions, MarkerOption, ViewOptions } from '../options'; import { AddressLabelOption, IndexLabelOption, LayoutOptions, MarkerOption, ViewOptions } from '../options';
@ -14,6 +14,8 @@ export class LayoutProvider {
private engine: Engine; private engine: Engine;
private viewOptions: ViewOptions; private viewOptions: ViewOptions;
private viewContainer: ViewContainer; private viewContainer: ViewContainer;
private leakAreaXoffset: number = 20;
private leakClusterXInterval = 25;
constructor(engine: Engine, viewContainer: ViewContainer) { constructor(engine: Engine, viewContainer: ViewContainer) {
this.engine = engine; this.engine = engine;
@ -67,7 +69,7 @@ export class LayoutProvider {
let target = item.target, let target = item.target,
targetBound: BoundingRect = target.getBound(), targetBound: BoundingRect = target.getBound(),
g6AnchorPosition = item.target.shadowG6Item.getAnchorPoints()[anchor] as IPoint, g6AnchorPosition = (<INode>item.target.shadowG6Item).getAnchorPoints()[anchor] as IPoint,
center: [number, number] = [ center: [number, number] = [
targetBound.x + targetBound.width / 2, targetBound.x + targetBound.width / 2,
targetBound.y + targetBound.height / 2, targetBound.y + targetBound.height / 2,
@ -214,10 +216,15 @@ export class LayoutProvider {
indexLabelOptions = group.options.indexLabel || {}, indexLabelOptions = group.options.indexLabel || {},
addressLabelOption = group.options.addressLabel || {}; addressLabelOption = group.options.addressLabel || {};
this.layoutIndexLabel(group.indexLabel, indexLabelOptions); const indexLabel = group.appendage.filter(item => item instanceof SVIndexLabel) as SVIndexLabel[],
this.layoutFreedLabel(group.freedLabel); freedLabel = group.appendage.filter(item => item instanceof SVFreedLabel) as SVFreedLabel[],
this.layoutAddressLabel(group.addressLabel, addressLabelOption); addressLabel = group.appendage.filter(item => item instanceof SVAddressLabel) as SVAddressLabel[],
this.layoutMarker(group.marker, markerOptions); // 布局外部指针 marker = group.appendage.filter(item => item instanceof SVMarker) as SVMarker[];
this.layoutIndexLabel(indexLabel, indexLabelOptions);
this.layoutFreedLabel(freedLabel);
this.layoutAddressLabel(addressLabel, addressLabelOption);
this.layoutMarker(marker, markerOptions); // 布局外部指针
}); });
return modelGroupList; return modelGroupList;
@ -225,45 +232,36 @@ export class LayoutProvider {
/** /**
* *
* @param leakModels
* @param accumulateLeakModels * @param accumulateLeakModels
* // todo: 部分元素被抽离后的泄漏区
*/ */
private layoutLeakModels(leakModels: SVModel[], accumulateLeakModels: SVModel[]) { private layoutLeakArea(accumulateLeakModels: SVModel[]) {
const group: Group = new Group(), const containerHeight = this.viewContainer.getG6Instance().getHeight(),
containerHeight = this.viewContainer.getG6Instance().getHeight(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight, leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
leakAreaY = containerHeight - leakAreaHeight, leakAreaY = containerHeight - leakAreaHeight;
xOffset = 60;
let prevBound: BoundingRect; let prevBound: BoundingRect;
// 避免在泄漏前拖拽节点导致的位置变化,先把节点位置重置为布局后的标准位置 // 避免在泄漏前拖拽节点导致的位置变化,先把节点位置重置为布局后的标准位置
leakModels.forEach(item => { accumulateLeakModels.forEach(item => {
item.set({ item.set({
x: item.layoutX, x: item.layoutX,
y: item.layoutY, y: item.layoutY,
}); });
}); });
const globalLeakGroupBound: BoundingRect = accumulateLeakModels.length const clusters = ModelConstructor.getClusters(accumulateLeakModels);
? Bound.union(...accumulateLeakModels.map(item => item.getBound()))
: { x: 0, y: leakAreaY, width: 0, height: 0 };
const layoutGroups = Util.groupBy(leakModels, 'group'); // 每一个簇从左往右布局就完事了,比之前的方法简单稳定很多
Object.keys(layoutGroups).forEach(key => { clusters.forEach(item => {
group.add(...layoutGroups[key]); const bound = item.getBound(),
x = prevBound ? prevBound.x + prevBound.width + this.leakClusterXInterval : this.leakAreaXoffset,
dx = x - bound.x,
dy = leakAreaY - bound.y;
const currentBound: BoundingRect = group.getBound(), item.translate(dx, dy);
prevBoundEnd = prevBound ? prevBound.x + prevBound.width : 0, Bound.translate(bound, dx, dy);
{ x: groupX, y: groupY } = currentBound, prevBound = bound;
dx = globalLeakGroupBound.x + globalLeakGroupBound.width + prevBoundEnd + xOffset - groupX,
dy = globalLeakGroupBound.y - groupY;
group.translate(dx, dy);
group.clear();
Bound.translate(currentBound, dx, dy);
prevBound = currentBound;
}); });
} }
@ -320,7 +318,7 @@ export class LayoutProvider {
if (this.viewContainer.hasLeak) { if (this.viewContainer.hasLeak) {
const boundBottomY = viewBound.y + viewBound.height; const boundBottomY = viewBound.y + viewBound.height;
dy = height - leakAreaHeight - 100 - boundBottomY; dy = height - leakAreaHeight - 130 - boundBottomY;
} else { } else {
const boundCenterY = viewBound.y + viewBound.height / 2; const boundCenterY = viewBound.y + viewBound.height / 2;
dy = centerY - boundCenterY; dy = centerY - boundCenterY;
@ -332,19 +330,15 @@ export class LayoutProvider {
/** /**
* *
* @param layoutGroupTable * @param layoutGroupTable
* @param leakModels * @param accumulateLeakModels
* @param hasLeak
* @param needFitCenter
*/ */
public layoutAll(layoutGroupTable: LayoutGroupTable, accumulateLeakModels: SVModel[], leakModels: SVModel[]) { public layoutAll(layoutGroupTable: LayoutGroupTable, accumulateLeakModels: SVModel[]) {
this.preLayoutProcess(layoutGroupTable); this.preLayoutProcess(layoutGroupTable);
const modelGroupList: Group[] = this.layoutModels(layoutGroupTable); const modelGroupList: Group[] = this.layoutModels(layoutGroupTable);
const generalGroup: Group = this.layoutGroups(modelGroupList); const generalGroup: Group = this.layoutGroups(modelGroupList);
if (leakModels.length) { this.layoutLeakArea(accumulateLeakModels);
this.layoutLeakModels(leakModels, accumulateLeakModels);
}
this.fitCenter(generalGroup); this.fitCenter(generalGroup);
this.postLayoutProcess(layoutGroupTable); this.postLayoutProcess(layoutGroupTable);

View File

@ -1,430 +1,461 @@
import { EventBus } from "../Common/eventBus"; import { EventBus } from '../Common/eventBus';
import { Util } from "../Common/util"; import { Util } from '../Common/util';
import { Engine } from "../engine"; import { Engine } from '../engine';
import { LayoutGroupTable } from "../Model/modelConstructor"; import { LayoutGroupTable } from '../Model/modelConstructor';
import { SVLink } from "../Model/SVLink"; import { SVLink } from '../Model/SVLink';
import { SVModel } from "../Model/SVModel"; import { SVModel } from '../Model/SVModel';
import { SVNode } from "../Model/SVNode"; import { SVNode } from '../Model/SVNode';
import { SVAddressLabel, SVMarker, SVNodeAppendage } from "../Model/SVNodeAppendage"; import { SVAddressLabel, SVMarker, SVNodeAppendage } from '../Model/SVNodeAppendage';
import { Animations } from "./animation"; import { Animations } from './animation';
import { Renderer } from "./renderer"; import { Renderer } from './renderer';
export interface DiffResult { export interface DiffResult {
CONTINUOUS: SVModel[]; CONTINUOUS: SVModel[];
APPEND: SVModel[]; APPEND: SVModel[];
REMOVE: SVModel[]; REMOVE: SVModel[];
FREED: SVNode[]; FREED: SVNode[];
LEAKED: SVModel[]; LEAKED: SVModel[];
UPDATE: SVModel[]; UPDATE: SVModel[];
ACCUMULATE_LEAK: SVModel[]; ACCUMULATE_LEAK: SVModel[];
} }
export class Reconcile { export class Reconcile {
private engine: Engine;
private engine: Engine; private renderer: Renderer;
private renderer: Renderer; private isFirstPatch: boolean;
private isFirstPatch: boolean;
constructor(engine: Engine, renderer: Renderer) {
constructor(engine: Engine, renderer: Renderer) { this.engine = engine;
this.engine = engine; this.renderer = renderer;
this.renderer = renderer; this.isFirstPatch = true;
this.isFirstPatch = true; }
}
/**
/** * models
* models * @param prevModelList
* @param prevModelList * @param modelList
* @param modelList */
*/ private getContinuousModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] {
private getContinuousModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] { const continuousModels = modelList.filter(item => prevModelList.find(prevModel => item.id === prevModel.id));
const continuousModels = modelList.filter(item => prevModelList.find(prevModel => item.id === prevModel.id)); return continuousModels;
return continuousModels; }
}
/**
/** *
* * @param prevModelList
* @param prevModelList * @param modelList
* @param modelList * @param accumulateLeakModels
* @param accumulateLeakModels * @returns
* @returns */
*/ private getAppendModels(
private getAppendModels(prevModelList: SVModel[], modelList: SVModel[], accumulateLeakModels: SVModel[]): SVModel[] { prevModelList: SVModel[],
const appendModels = modelList.filter(item => !prevModelList.find(model => model.id === item.id)); modelList: SVModel[],
accumulateLeakModels: SVModel[],
appendModels.forEach(item => { prevStep: boolean
let removeIndex = accumulateLeakModels.findIndex(leakModel => item.id === leakModel.id); ): SVModel[] {
const appendModels = modelList.filter(item => !prevModelList.find(model => model.id === item.id));
if (removeIndex > -1) {
accumulateLeakModels.splice(removeIndex, 1); // 看看新增的节点是不是从泄漏区来的
} // 目前的判断方式比较傻看看泄漏区有没有相同id的节点但是发现这个方法可能不可靠不知道还有没有更好的办法
}); appendModels.forEach(item => {
const removeIndex = accumulateLeakModels.findIndex(leakModel => item.id === leakModel.id),
return appendModels; svModel = accumulateLeakModels[removeIndex];
}
if (removeIndex > -1) {
/** svModel.leaked = false;
* accumulateLeakModels.splice(removeIndex, 1);
* @param layoutGroupTable }
* @param prevModelList });
* @param modelList
* @returns return appendModels;
*/ }
private getLeakModels(layoutGroupTable: LayoutGroupTable, prevModelList: SVModel[], modelList: SVModel[]): SVModel[] {
let potentialLeakModels: SVModel[] = prevModelList.filter(item => /**
!modelList.find(model => model.id === item.id) && !item.freed *
); * @param layoutGroupTable
const leakModels: SVModel[] = []; * @param prevModelList
* @param modelList
// 先把节点拿出来 * @returns
const potentialLeakNodes = potentialLeakModels.filter(item => item.isNode()) as SVNode[], */
groups = Util.groupBy<SVNode>(potentialLeakNodes, 'group'); private getLeakModels(
layoutGroupTable: LayoutGroupTable,
// 再把非节点的model拿出来 prevModelList: SVModel[],
potentialLeakModels = potentialLeakModels.filter(item => item.isNode() === false); modelList: SVModel[]
): SVModel[] {
Object.keys(groups).forEach(key => { let potentialLeakModels: SVModel[] = prevModelList.filter(
const leakRule = layoutGroupTable.get(key).layoutCreator.defineLeakRule; item => !modelList.find(model => model.id === item.id) && !item.freed
if(leakRule && typeof leakRule === 'function') { );
potentialLeakModels.push(...leakRule(groups[key])); const leakModels: SVModel[] = [];
}
}); // 先把节点拿出来
const potentialLeakNodes = potentialLeakModels.filter(item => item.isNode()) as SVNode[],
potentialLeakModels.forEach(item => { groups = Util.groupBy<SVNode>(potentialLeakNodes, 'group');
if (item instanceof SVNode) {
item.leaked = true; // 再把非节点的model拿出来
leakModels.push(item); potentialLeakModels = potentialLeakModels.filter(item => item.isNode() === false);
item.appendages.forEach(appendage => { Object.keys(groups).forEach(key => {
appendage.leaked = true; const leakRule = layoutGroupTable.get(key).layoutCreator.defineLeakRule;
leakModels.push(appendage); if (leakRule && typeof leakRule === 'function') {
}); potentialLeakModels.push(...leakRule(groups[key]));
} }
}); });
potentialLeakModels.forEach(item => { potentialLeakModels.forEach(item => {
if (item instanceof SVLink && item.node.leaked !== false && item.target.leaked !== false) { if (item instanceof SVNode) {
item.leaked = true; item.leaked = true;
leakModels.push(item); leakModels.push(item);
}
}); item.getAppendagesList().forEach(appendage => {
// 外部指针先不加入泄漏区(这个需要讨论一下,我觉得不应该)
leakModels.forEach(item => { if (appendage instanceof SVMarker) {
// 不能用上次的G6item了不然布局的时候会没有动画 return;
item.G6Item = null; }
});
appendage.leaked = true;
return leakModels; leakModels.push(appendage);
} });
}
/** });
*
* @param prevModelList potentialLeakModels.forEach(item => {
* @param modelList if (item instanceof SVLink && item.node.leaked !== false && item.target.leaked !== false) {
*/ item.leaked = true;
private getRemoveModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] { leakModels.push(item);
let removedModels: SVModel[] = []; }
});
for (let i = 0; i < prevModelList.length; i++) {
let prevModel = prevModelList[i], return leakModels;
target = modelList.find(item => item.id === prevModel.id); }
if (target === undefined && !prevModel.leaked) { /**
removedModels.push(prevModel); *
} * @param prevModelList
} * @param modelList
*/
return removedModels; private getRemoveModels(
} prevModelList: SVModel[],
modelList: SVModel[],
accumulateLeakModels: SVModel[]
/** ): SVModel[] {
* let removedModels: SVModel[] = [];
* @param prevModelList
* @param modelList for (let i = 0; i < prevModelList.length; i++) {
* @returns let prevModel = prevModelList[i],
*/ target = modelList.find(item => item.id === prevModel.id);
private getReTargetMarkers(prevModelList: SVModel[], modelList: SVModel[]): SVMarker[] {
const prevMarkers: SVMarker[] = prevModelList.filter(item => item instanceof SVMarker) as SVMarker[], if (target === undefined && !prevModel.leaked) {
markers: SVMarker[] = modelList.filter(item => item instanceof SVMarker) as SVMarker[]; removedModels.push(prevModel);
}
return markers.filter(item => prevMarkers.find(prevItem => { }
return prevItem.id === item.id && prevItem.target.id !== item.target.id
})); // 假如某个节点从泄漏区移回可视化区域,那么与原来泄漏结构的连线应该消失掉
} for (let i = 0; i < accumulateLeakModels.length; i++) {
let leakModel = accumulateLeakModels[i];
/** if (leakModel instanceof SVLink) {
* label model if (leakModel.node.leaked === false || leakModel.target.leaked === false) {
* @param prevModelList accumulateLeakModels.splice(i, 1);
* @param modelList i--;
* @returns removedModels.push(leakModel);
*/ }
private getLabelChangeModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] { }
let labelChangeModels: SVModel[] = []; }
modelList.forEach(item => { removedModels.forEach(item => {
const prevItem = prevModelList.find(prevItem => prevItem.id === item.id); item.beforeDestroy();
});
if (prevItem === undefined) {
return; return removedModels;
} }
const prevLabel = prevItem.get('label'), /**
label = item.get('label'); *
* @param prevModelList
if (String(prevLabel) !== String(label)) { * @param modelList
labelChangeModels.push(item); * @returns
} */
}); private getReTargetMarkers(prevModelList: SVModel[], modelList: SVModel[]): SVMarker[] {
const prevMarkers: SVMarker[] = prevModelList.filter(item => item instanceof SVMarker) as SVMarker[],
return labelChangeModels; markers: SVMarker[] = modelList.filter(item => item instanceof SVMarker) as SVMarker[];
}
return markers.filter(item =>
/** prevMarkers.find(prevItem => {
* free return prevItem.id === item.id && prevItem.target.id !== item.target.id;
* @param prevModelList })
* @param modelList );
* @returns }
*/
private getFreedModels(prevModelList: SVModel[], modelList: SVModel[]): SVNode[] { /**
const freedNodes = modelList.filter(item => item instanceof SVNode && item.freed) as SVNode[]; * label model
* @param prevModelList
freedNodes.forEach(item => { * @param modelList
const prev = prevModelList.find(prevModel => item.id === prevModel.id); * @returns
*/
if (prev) { private getLabelChangeModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] {
item.set('label', prev.get('label')); let labelChangeModels: SVModel[] = [];
}
}); modelList.forEach(item => {
const prevItem = prevModelList.find(prevItem => prevItem.id === item.id);
return freedNodes;
} if (prevItem === undefined) {
return;
// ------------------------------------------------------------------------------------------------ }
/** const prevLabel = prevItem.get('label'),
* models label = item.get('label');
* @param continuousModels
*/ if (String(prevLabel) !== String(label)) {
private handleContinuousModels(continuousModels: SVModel[]) { labelChangeModels.push(item);
for (let i = 0; i < continuousModels.length; i++) { }
let model = continuousModels[i]; });
if (model instanceof SVNode) { return labelChangeModels;
const group = model.G6Item.getContainer(); }
group.attr({ opacity: 1 });
} /**
} * free
} * @param prevModelList
* @param modelList
/** * @returns
* models */
* @param appendData private getFreedModels(prevModelList: SVModel[], modelList: SVModel[]): SVNode[] {
*/ const freedNodes = modelList.filter(item => item instanceof SVNode && item.freed) as SVNode[];
private handleAppendModels(appendModels: SVModel[]) {
let { duration, timingFunction } = this.engine.animationOptions; freedNodes.forEach(item => {
const prev = prevModelList.find(prevModel => item.id === prevModel.id);
appendModels.forEach(item => {
if (item instanceof SVNodeAppendage) { if (prev) {
// 先不显示泄漏区节点上面的地址文本 item.set('label', prev.get('label'));
if (item instanceof SVAddressLabel) { }
// 先将透明度改为0隐藏掉 });
const AddressLabelG6Group = item.G6Item.getContainer();
AddressLabelG6Group.attr({ opacity: 0 }); return freedNodes;
} }
else {
Animations.FADE_IN(item.G6Item, { // ------------------------------------------------------------------------------------------------
duration,
timingFunction /**
}); * models
} * @param continuousModels
} */
else { private handleContinuousModels(continuousModels: SVModel[]) {
Animations.APPEND(item.G6Item, { for (let i = 0; i < continuousModels.length; i++) {
duration, let model = continuousModels[i];
timingFunction
}); if (model instanceof SVNode) {
} const group = model.G6Item.getContainer();
}); group.attr({ opacity: 1 });
} }
}
/** }
* models
* @param removeData /**
*/ * models
private handleRemoveModels(removeModels: SVModel[]) { * @param appendData
let { duration, timingFunction } = this.engine.animationOptions; */
private handleAppendModels(appendModels: SVModel[]) {
removeModels.forEach(item => { let { duration, timingFunction } = this.engine.animationOptions;
Animations.REMOVE(item.G6Item, {
duration, appendModels.forEach(item => {
timingFunction, if (item instanceof SVNodeAppendage) {
callback: () => { // 先不显示泄漏区节点上面的地址文本
this.renderer.removeModel(item); if (item instanceof SVAddressLabel) {
} // 先将透明度改为0隐藏掉
}); const AddressLabelG6Group = item.G6Item.getContainer();
}); AddressLabelG6Group.attr({ opacity: 0 });
} } else {
Animations.FADE_IN(item.G6Item, {
/** duration,
* models timingFunction,
* @param leakModels });
*/ }
private handleLeakModels(leakModels: SVModel[]) { } else {
let { duration, timingFunction } = this.engine.animationOptions; Animations.APPEND(item.G6Item, {
duration,
leakModels.forEach(item => { timingFunction,
if (item instanceof SVAddressLabel) { });
Animations.FADE_IN(item.G6Item, { }
duration, });
timingFunction }
});
} /**
* models
item.G6Item.enableCapture(false); * @param removeData
}); */
private handleRemoveModels(removeModels: SVModel[]) {
EventBus.emit('onLeak', leakModels); let { duration, timingFunction } = this.engine.animationOptions;
}
removeModels.forEach(item => {
/** Animations.REMOVE(item.G6Item, {
* models duration,
* @param accumulateModels timingFunction,
*/ callback: () => {
private handleAccumulateLeakModels(accumulateModels: SVModel[]) { this.renderer.removeModel(item);
accumulateModels.forEach(item => { },
if (item.generalStyle) { });
item.set('style', { ...item.generalStyle }); });
} }
});
} /**
* models
* @param leakModels
/** */
* models private handleLeakModels(leakModels: SVModel[]) {
* @param freedModes let { duration, timingFunction } = this.engine.animationOptions;
*/
private handleFreedModels(freedModes: SVNode[]) { leakModels.forEach(item => {
const { duration, timingFunction } = this.engine.animationOptions, if (item instanceof SVAddressLabel) {
alpha = 0.4; Animations.FADE_IN(item.G6Item, {
duration,
freedModes.forEach(item => { timingFunction,
const nodeGroup = item.G6Item.getContainer(); });
}
item.set('style', { fill: '#ccc' });
nodeGroup.attr({ opacity: alpha }); item.G6Item.enableCapture(false);
});
if (item.marker) {
const markerGroup = item.marker.G6Item.getContainer(); EventBus.emit('onLeak', leakModels);
item.marker.set('style', { fill: '#ccc' }); }
markerGroup.attr({ opacity: alpha + 0.5 });
} /**
* models
item.freedLabel.G6Item.toFront(); * @param accumulateModels
Animations.FADE_IN(item.freedLabel.G6Item, { duration, timingFunction }); */
}); private handleAccumulateLeakModels(accumulateModels: SVModel[]) {
accumulateModels.forEach(item => {
EventBus.emit('onFreed', freedModes); if (item.generalStyle) {
} item.set('style', { ...item.generalStyle });
}
/** });
* models }
* @param models
*/ /**
private handleChangeModels(models: SVModel[]) { * models
const changeHighlightColor: string = this.engine.viewOptions.updateHighlight; * @param freedModes
*/
if (!changeHighlightColor || typeof changeHighlightColor !== 'string') { private handleFreedModels(freedModes: SVNode[]) {
return; const { duration, timingFunction } = this.engine.animationOptions,
} alpha = 0.4;
models.forEach(item => { freedModes.forEach(item => {
if (item.generalStyle === undefined) { const nodeGroup = item.G6Item.getContainer();
item.generalStyle = Util.objectClone(item.G6ModelProps.style);
} item.set('style', { fill: '#ccc' });
nodeGroup.attr({ opacity: alpha });
if (item instanceof SVLink) {
item.set('style', { if (item.appendages.marker) {
stroke: changeHighlightColor item.appendages.marker.forEach(marker => {
}); const markerGroup = marker.G6Item.getContainer();
} marker.set('style', { fill: '#ccc' });
else { markerGroup.attr({ opacity: alpha + 0.5 });
item.set('style', { });
fill: changeHighlightColor }
});
} if (item.appendages.freedLabel) {
}); item.appendages.freedLabel.forEach(freedLabel => {
} freedLabel.G6Item.toFront();
Animations.FADE_IN(freedLabel.G6Item, { duration, timingFunction });
});
/** }
* diff });
* @param layoutGroupTable
* @param prevModelList EventBus.emit('onFreed', freedModes);
* @param modelList }
* @param accumulateLeakModels
* @returns /**
*/ * models
public diff(layoutGroupTable: LayoutGroupTable, prevModelList: SVModel[], modelList: SVModel[], accumulateLeakModels: SVModel[], isEnterFunction: boolean): DiffResult { * @param models
const continuousModels: SVModel[] = this.getContinuousModels(prevModelList, modelList); */
const leakModels: SVModel[] = isEnterFunction? []: this.getLeakModels(layoutGroupTable, prevModelList, modelList); private handleChangeModels(models: SVModel[]) {
const appendModels: SVModel[] = this.getAppendModels(prevModelList, modelList, accumulateLeakModels); const changeHighlightColor: string = this.engine.viewOptions.updateHighlight;
const removeModels: SVModel[] = this.getRemoveModels(prevModelList, modelList);
const updateModels: SVModel[] = [ if (!changeHighlightColor || typeof changeHighlightColor !== 'string') {
...this.getReTargetMarkers(prevModelList, modelList), return;
...this.getLabelChangeModels(prevModelList, modelList), }
...appendModels,
...leakModels models.forEach(item => {
]; if (item.generalStyle === undefined) {
const freedModels: SVNode[] = this.getFreedModels(prevModelList, modelList); item.generalStyle = Util.objectClone(item.G6ModelProps.style);
}
return {
CONTINUOUS: continuousModels, if (item instanceof SVLink) {
APPEND: appendModels, item.set('style', {
REMOVE: removeModels, stroke: changeHighlightColor,
FREED: freedModels, });
LEAKED: leakModels, } else {
UPDATE: updateModels, item.set('style', {
ACCUMULATE_LEAK: [...accumulateLeakModels] fill: changeHighlightColor,
}; });
} }
});
}
/**
* /**
* @param diffResult * diff
* @param isFirstRender * @param layoutGroupTable
*/ * @param prevModelList
public patch(diffResult: DiffResult) { * @param modelList
const { * @param accumulateLeakModels
APPEND, * @returns
REMOVE, */
FREED, public diff(
LEAKED, layoutGroupTable: LayoutGroupTable,
UPDATE, prevModelList: SVModel[],
CONTINUOUS, modelList: SVModel[],
ACCUMULATE_LEAK accumulateLeakModels: SVModel[],
} = diffResult; isEnterFunction: boolean,
prevStep: boolean
this.handleAccumulateLeakModels(ACCUMULATE_LEAK); ): DiffResult {
const continuousModels: SVModel[] = this.getContinuousModels(prevModelList, modelList);
// 第一次渲染的时候不高亮变化的元素 const leakModels: SVModel[] = isEnterFunction
if (this.isFirstPatch === false) { ? []
this.handleChangeModels(UPDATE); : this.getLeakModels(layoutGroupTable, prevModelList, modelList);
} const appendModels: SVModel[] = this.getAppendModels(prevModelList, modelList, accumulateLeakModels, prevStep);
const removeModels: SVModel[] = this.getRemoveModels(prevModelList, modelList, accumulateLeakModels);
this.handleContinuousModels(CONTINUOUS); const updateModels: SVModel[] = [
this.handleFreedModels(FREED); ...this.getReTargetMarkers(prevModelList, modelList),
this.handleAppendModels(APPEND); ...this.getLabelChangeModels(prevModelList, modelList),
this.handleLeakModels(LEAKED); ...appendModels,
this.handleRemoveModels(REMOVE); ...leakModels,
];
if (this.isFirstPatch) { const freedModels: SVNode[] = this.getFreedModels(prevModelList, modelList);
this.isFirstPatch = false;
} return {
} CONTINUOUS: continuousModels,
APPEND: appendModels,
public destroy() { } REMOVE: removeModels,
FREED: freedModels,
LEAKED: leakModels,
UPDATE: updateModels,
ACCUMULATE_LEAK: [...accumulateLeakModels],
};
}
/**
*
* @param diffResult
* @param isFirstRender
*/
public patch(diffResult: DiffResult) {
const { APPEND, REMOVE, FREED, LEAKED, UPDATE, CONTINUOUS, ACCUMULATE_LEAK } = diffResult;
this.handleAccumulateLeakModels(ACCUMULATE_LEAK);
// 第一次渲染的时候不高亮变化的元素
if (this.isFirstPatch === false) {
this.handleChangeModels(UPDATE);
}
this.handleContinuousModels(CONTINUOUS);
this.handleFreedModels(FREED);
this.handleAppendModels(APPEND);
this.handleLeakModels(LEAKED);
this.handleRemoveModels(REMOVE);
if (this.isFirstPatch) {
this.isFirstPatch = false;
}
}
public destroy() {}
} }

View File

@ -4,165 +4,164 @@ import { Util } from '../Common/util';
import { Tooltip, Graph, GraphData, Modes } from '@antv/g6'; import { Tooltip, Graph, GraphData, Modes } from '@antv/g6';
import { InitG6Behaviors } from '../BehaviorHelper/initG6Behaviors'; import { InitG6Behaviors } from '../BehaviorHelper/initG6Behaviors';
export interface RenderModelPack { export interface RenderModelPack {
leaKModels: SVModel[]; leaKModels: SVModel[];
generalModel: SVModel[]; generalModel: SVModel[];
} }
export type g6Behavior =
export type g6Behavior = string | { type: string; shouldBegin?: Function; shouldUpdate?: Function; shouldEnd?: Function; }; | string
| { type: string; shouldBegin?: Function; shouldUpdate?: Function; shouldEnd?: Function };
export class Renderer { export class Renderer {
private engine: Engine; private engine: Engine;
private g6Instance: Graph; // g6 实例 private g6Instance: Graph; // g6 实例
private shadowG6Instance: Graph; private shadowG6Instance: Graph;
constructor(engine: Engine, DOMContainer: HTMLElement, behaviorsModes: Modes) { constructor(engine: Engine, DOMContainer: HTMLElement, behaviorsModes: Modes) {
this.engine = engine; this.engine = engine;
const enable: boolean = this.engine.animationOptions.enable, const enable: boolean = this.engine.animationOptions.enable,
duration: number = this.engine.animationOptions.duration, duration: number = this.engine.animationOptions.duration,
timingFunction: string = this.engine.animationOptions.timingFunction; timingFunction: string = this.engine.animationOptions.timingFunction;
const tooltip = new Tooltip({ const tooltip = new Tooltip({
offsetX: 10, offsetX: 10,
offsetY: 20, offsetY: 20,
shouldBegin(event) { shouldBegin(event) {
return event.item['SVModel'].isNode(); return event.item['SVModel'].isNode();
}, },
getContent: event => this.getTooltipContent(event.item['SVModel'], { address: 'sourceId', data: 'data' }), getContent: event => this.getTooltipContent(event.item['SVModel'], { address: 'sourceId', data: 'data' }),
itemTypes: ['node'] itemTypes: ['node'],
}); });
this.shadowG6Instance = new Graph({ this.shadowG6Instance = new Graph({
container: DOMContainer.cloneNode() as HTMLElement container: DOMContainer.cloneNode() as HTMLElement,
}); });
// 初始化g6实例 // 初始化g6实例
this.g6Instance = new Graph({ this.g6Instance = new Graph({
container: DOMContainer, container: DOMContainer,
width: DOMContainer.offsetWidth, width: DOMContainer.offsetWidth,
height: DOMContainer.offsetHeight, height: DOMContainer.offsetHeight,
groupByTypes: false, groupByTypes: false,
animate: enable, animate: enable,
animateCfg: { animateCfg: {
duration: duration, duration: duration,
easing: timingFunction easing: timingFunction,
}, },
fitView: false, fitView: false,
modes: behaviorsModes, modes: behaviorsModes,
plugins: [tooltip] plugins: [tooltip],
}); });
} }
/** /**
* tooltip元素 * tooltip元素
* @param model * @param model
* @param items * @param items
* @returns * @returns
*/ */
private getTooltipContent(model: SVModel, items: { [key: string]: string }): HTMLDivElement { private getTooltipContent(model: SVModel, items: { [key: string]: string }): HTMLDivElement {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
if (model === null || model === undefined) { if (model === null || model === undefined) {
return wrapper; return wrapper;
} }
Object.keys(items).map(key => { Object.keys(items).map(key => {
let value = model[items[key]]; let value = model[items[key]];
if (value !== undefined && value !== null) { if (value !== undefined && value !== null) {
let item = document.createElement('div'); let item = document.createElement('div');
item.innerHTML = `${key}${value !== '' ? value : model.G6ModelProps['label']}`; item.innerHTML = `${key}${value !== '' ? value : model.G6ModelProps['label']}`;
wrapper.appendChild(item); wrapper.appendChild(item);
} }
}); });
if (model.freed) { if (model.freed) {
let item = document.createElement('div'); let item = document.createElement('div');
item.innerHTML = '(freed)'; item.innerHTML = '(freed)';
wrapper.appendChild(item); wrapper.appendChild(item);
} }
return wrapper; return wrapper;
} }
/** /**
* model Canvas G6 item * model Canvas G6 item
* @param renderModelList * @param renderModelList
*/ */
public build(renderModelList: SVModel[]) { public build(renderModelList: SVModel[]) {
const g6Data: GraphData = Util.convertModelList2G6Data(renderModelList); const g6Data: GraphData = Util.convertModelList2G6Data(renderModelList);
this.shadowG6Instance.clear(); this.shadowG6Instance.clear();
this.shadowG6Instance.read(g6Data); this.shadowG6Instance.read(g6Data);
renderModelList.forEach(item => { renderModelList.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id); item.G6Item = null;
item.shadowG6Instance = this.shadowG6Instance; item.shadowG6Item = this.shadowG6Instance.findById(item.id);
}); item.shadowG6Instance = this.shadowG6Instance;
} });
}
/** /**
* *
* @param renderModelList * @param renderModelList
* @param isFirstRender * @param isFirstRender
*/ */
public render(renderModelList: SVModel[]) { public render(renderModelList: SVModel[]) {
const renderData: GraphData = Util.convertModelList2G6Data(renderModelList); const renderData: GraphData = Util.convertModelList2G6Data(renderModelList);
this.g6Instance.changeData(renderData); this.g6Instance.changeData(renderData);
renderModelList.forEach(item => { renderModelList.forEach(item => {
item.g6Instance = this.g6Instance; item.g6Instance = this.g6Instance;
item.G6Item = this.g6Instance.findById(item.id); item.G6Item = this.g6Instance.findById(item.id);
item.G6Item['SVModel'] = item; item.G6Item['SVModel'] = item;
}); });
this.g6Instance.getEdges().forEach(item => item.toFront()); this.g6Instance.getEdges().forEach(item => item.toFront());
this.g6Instance.paint(); this.g6Instance.paint();
} }
/** /**
* Model * Model
* @param model * @param model
*/ */
public removeModel(model: SVModel) { public removeModel(model: SVModel) {
this.g6Instance.removeItem(model.G6Item); this.g6Instance.removeItem(model.G6Item);
this.shadowG6Instance.removeItem(model.shadowG6Item); this.shadowG6Instance.removeItem(model.shadowG6Item);
} }
/** /**
* G6 * G6
*/ */
public getG6Instance() { public getG6Instance() {
return this.g6Instance; return this.g6Instance;
} }
/** /**
* *
*/ */
public refresh() { public refresh() {
this.g6Instance.refresh(); this.g6Instance.refresh();
this.shadowG6Instance.refresh(); this.shadowG6Instance.refresh();
} }
/** /**
* *
* @param width * @param width
* @param height * @param height
*/ */
public changeSize(width: number, height: number) { public changeSize(width: number, height: number) {
this.g6Instance.changeSize(width, height); this.g6Instance.changeSize(width, height);
this.shadowG6Instance.changeSize(width, height); this.shadowG6Instance.changeSize(width, height);
} }
/** /**
* *
*/ */
public destroy() { public destroy() {
this.shadowG6Instance.destroy(); this.shadowG6Instance.destroy();
this.g6Instance.destroy(); this.g6Instance.destroy();
} }
} }

View File

@ -1,226 +1,214 @@
import { Engine } from "../engine"; import { Engine } from '../engine';
import { LayoutProvider } from "./layoutProvider"; import { LayoutProvider } from './layoutProvider';
import { LayoutGroupTable } from "../Model/modelConstructor"; import { LayoutGroupTable } from '../Model/modelConstructor';
import { Util } from "../Common/util"; import { Util } from '../Common/util';
import { SVModel } from "../Model/SVModel"; import { SVModel } from '../Model/SVModel';
import { Renderer } from "./renderer"; import { Renderer } from './renderer';
import { Reconcile } from "./reconcile"; import { Reconcile } from './reconcile';
import { EventBus } from "../Common/eventBus"; import { EventBus } from '../Common/eventBus';
import { Group } from "../Common/group"; import { Group } from '../Common/group';
import { Graph, Modes } from "@antv/g6-pc"; import { Graph, Modes } from '@antv/g6-pc';
import { InitG6Behaviors } from "../BehaviorHelper/initG6Behaviors"; import { InitG6Behaviors } from '../BehaviorHelper/initG6Behaviors';
import { SVNode } from "../Model/SVNode"; import { SVNode } from '../Model/SVNode';
import { SolveBrushSelectDrag, SolveDragCanvasWithLeak, SolveNodeAppendagesDrag, SolveZoomCanvasWithLeak } from "../BehaviorHelper/behaviorIssueHelper"; import {
SolveBrushSelectDrag,
SolveDragCanvasWithLeak,
SolveNodeAppendagesDrag,
SolveZoomCanvasWithLeak,
} from '../BehaviorHelper/behaviorIssueHelper';
export class ViewContainer { export class ViewContainer {
private engine: Engine; private engine: Engine;
private layoutProvider: LayoutProvider; private layoutProvider: LayoutProvider;
private reconcile: Reconcile; private reconcile: Reconcile;
public renderer: Renderer; public renderer: Renderer;
private layoutGroupTable: LayoutGroupTable; private layoutGroupTable: LayoutGroupTable;
private prevModelList: SVModel[]; private prevModelList: SVModel[];
private accumulateLeakModels: SVModel[]; private accumulateLeakModels: SVModel[];
public hasLeak: boolean; public hasLeak: boolean;
public leakAreaY: number; public leakAreaY: number;
public brushSelectedModels: SVModel[]; // 保存框选过程中被选中的节点 public brushSelectedModels: SVModel[]; // 保存框选过程中被选中的节点
public clickSelectNode: SVNode; // 点击选中的节点 public clickSelectNode: SVNode; // 点击选中的节点
constructor(engine: Engine, DOMContainer: HTMLElement) {
const behaviorsModes: Modes = InitG6Behaviors(engine, this);
constructor(engine: Engine, DOMContainer: HTMLElement) { this.engine = engine;
const behaviorsModes: Modes = InitG6Behaviors(engine, this); this.layoutProvider = new LayoutProvider(engine, this);
this.renderer = new Renderer(engine, DOMContainer, behaviorsModes);
this.reconcile = new Reconcile(engine, this.renderer);
this.layoutGroupTable = new Map();
this.prevModelList = [];
this.accumulateLeakModels = [];
this.hasLeak = false; // 判断是否已经发生过泄漏
this.brushSelectedModels = [];
this.clickSelectNode = null;
this.engine = engine; const g6Instance = this.renderer.getG6Instance(),
this.layoutProvider = new LayoutProvider(engine, this); leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
this.renderer = new Renderer(engine, DOMContainer, behaviorsModes); height = this.getG6Instance().getHeight(),
this.reconcile = new Reconcile(engine, this.renderer); { drag, zoom } = this.engine.behaviorOptions;
this.layoutGroupTable = new Map();
this.prevModelList = [];
this.accumulateLeakModels = [];
this.hasLeak = false; // 判断是否已经发生过泄漏
this.brushSelectedModels = [];
this.clickSelectNode = null;
const g6Instance = this.renderer.getG6Instance(), this.leakAreaY = height - leakAreaHeight;
leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
height = this.getG6Instance().getHeight(),
{ drag, zoom } = this.engine.behaviorOptions;
this.leakAreaY = height - leakAreaHeight; SolveNodeAppendagesDrag(this);
SolveBrushSelectDrag(this);
drag && SolveDragCanvasWithLeak(this);
zoom && SolveZoomCanvasWithLeak(this);
}
SolveNodeAppendagesDrag(this); // ----------------------------------------------------------------------------------------------
SolveBrushSelectDrag(this);
drag && SolveDragCanvasWithLeak(this);
zoom && SolveZoomCanvasWithLeak(this);
}
/**
*
*/
reLayout() {
const g6Instance = this.getG6Instance(),
group = g6Instance.getGroup(),
matrix = group.getMatrix();
// ---------------------------------------------------------------------------------------------- if (matrix) {
let dx = matrix[6],
dy = matrix[7];
/** g6Instance.translate(-dx, -dy);
* }
*/
reLayout() {
const g6Instance = this.getG6Instance(),
group = g6Instance.getGroup(),
matrix = group.getMatrix();
if (matrix) { this.layoutProvider.layoutAll(this.layoutGroupTable, this.accumulateLeakModels);
let dx = matrix[6], g6Instance.refresh();
dy = matrix[7]; }
g6Instance.translate(-dx, -dy); /**
} * g6
*/
getG6Instance(): Graph {
return this.renderer.getG6Instance();
}
this.layoutProvider.layoutAll(this.layoutGroupTable, this.accumulateLeakModels, []); /**
g6Instance.refresh(); *
} * @returns
*/
getAccumulateLeakModels(): SVModel[] {
return this.accumulateLeakModels;
}
/**
*
*/
getLayoutGroupTable(): LayoutGroupTable {
return this.layoutGroupTable;
}
/** /**
* g6 *
*/ */
getG6Instance(): Graph { refresh() {
return this.renderer.getG6Instance(); this.renderer.getG6Instance().refresh();
} }
/** /**
* *
* @returns * @param width
*/ * @param height
getAccumulateLeakModels(): SVModel[] { */
return this.accumulateLeakModels; resize(width: number, height: number) {
} const g6Instance = this.getG6Instance(),
prevContainerHeight = g6Instance.getHeight(),
globalGroup: Group = new Group();
/** globalGroup.add(...this.prevModelList, ...this.accumulateLeakModels);
* this.renderer.changeSize(width, height);
*/
getLayoutGroupTable(): LayoutGroupTable {
return this.layoutGroupTable;
}
/** const containerHeight = g6Instance.getHeight(),
* dy = containerHeight - prevContainerHeight;
*/
refresh() {
this.renderer.getG6Instance().refresh();
}
/** globalGroup.translate(0, dy);
* this.renderer.refresh();
* @param width
* @param height
*/
resize(width: number, height: number) {
const g6Instance = this.getG6Instance(),
prevContainerHeight = g6Instance.getHeight(),
globalGroup: Group = new Group();
globalGroup.add(...this.prevModelList, ...this.accumulateLeakModels); this.leakAreaY += dy;
this.renderer.changeSize(width, height); EventBus.emit('onLeakAreaUpdate', {
leakAreaY: this.leakAreaY,
hasLeak: this.hasLeak,
});
}
const containerHeight = g6Instance.getHeight(), /**
dy = containerHeight - prevContainerHeight; *
* @param models
* @param layoutFn
*/
render(layoutGroupTable: LayoutGroupTable, isEnterFunction: boolean, prevStep: boolean) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable),
diffResult = this.reconcile.diff(
this.layoutGroupTable,
this.prevModelList,
modelList,
this.accumulateLeakModels,
isEnterFunction,
prevStep
),
renderModelList = [...modelList, ...diffResult.REMOVE, ...diffResult.LEAKED, ...diffResult.ACCUMULATE_LEAK];
globalGroup.translate(0, dy); if (this.hasLeak === true && this.accumulateLeakModels.length === 0) {
this.renderer.refresh(); this.hasLeak = false;
EventBus.emit('onLeakAreaUpdate', {
leakAreaY: this.leakAreaY,
hasLeak: this.hasLeak,
});
}
this.leakAreaY += dy; if (diffResult.LEAKED.length) {
EventBus.emit('onLeakAreaUpdate', { this.hasLeak = true;
leakAreaY: this.leakAreaY, EventBus.emit('onLeakAreaUpdate', {
hasLeak: this.hasLeak leakAreaY: this.leakAreaY,
}); hasLeak: this.hasLeak,
} });
}
/** this.accumulateLeakModels.push(...diffResult.LEAKED); // 对泄漏节点进行向后累积
* this.renderer.build(renderModelList); // 首先在离屏canvas渲染先
* @param models this.layoutProvider.layoutAll(layoutGroupTable, this.accumulateLeakModels); // 进行布局设置model的xy样式等
* @param layoutFn
*/
render(layoutGroupTable: LayoutGroupTable, isEnterFunction: boolean) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable),
diffResult = this.reconcile.diff(this.layoutGroupTable, this.prevModelList, modelList, this.accumulateLeakModels, isEnterFunction),
renderModelList = [
...modelList,
...diffResult.REMOVE,
...diffResult.LEAKED,
...diffResult.ACCUMULATE_LEAK
];
if (this.hasLeak === true && this.accumulateLeakModels.length === 0) { this.beforeRender();
this.hasLeak = false; this.renderer.render(renderModelList); // 渲染视图
EventBus.emit('onLeakAreaUpdate', { this.reconcile.patch(diffResult); // 对视图上的某些变化进行对应的动作,比如:节点创建动画,节点消失动画等
leakAreaY: this.leakAreaY, this.afterRender();
hasLeak: this.hasLeak
});
}
if (diffResult.LEAKED.length) { this.layoutGroupTable = layoutGroupTable;
this.hasLeak = true; this.prevModelList = modelList;
EventBus.emit('onLeakAreaUpdate', { }
leakAreaY: this.leakAreaY,
hasLeak: this.hasLeak
});
}
this.renderer.build(renderModelList); // 首先在离屏canvas渲染先 /**
this.layoutProvider.layoutAll(layoutGroupTable, this.accumulateLeakModels, diffResult.LEAKED); // 进行布局设置model的xy样式等 *
*/
destroy() {
this.renderer.destroy();
this.reconcile.destroy();
this.layoutProvider = null;
this.layoutGroupTable = null;
this.prevModelList.length = 0;
this.accumulateLeakModels.length = 0;
this.brushSelectedModels.length = 0;
}
this.beforeRender(); // ------------------------------------------------------------------------------
this.renderer.render(renderModelList); // 渲染视图
this.reconcile.patch(diffResult); // 对视图上的某些变化进行对应的动作,比如:节点创建动画,节点消失动画等
this.afterRender();
this.accumulateLeakModels.push(...diffResult.LEAKED); // 对泄漏节点进行累积 /**
*
*/
private afterRender() {
this.prevModelList.forEach(item => {
if (item.leaked === false) {
item.discarded = true;
}
});
}
this.layoutGroupTable = layoutGroupTable; /**
this.prevModelList = modelList; *
} */
private beforeRender() {}
/**
*
*/
destroy() {
this.renderer.destroy();
this.reconcile.destroy();
this.layoutProvider = null;
this.layoutGroupTable = null;
this.prevModelList.length = 0;
this.accumulateLeakModels.length = 0;
this.brushSelectedModels.length = 0;
}
// ------------------------------------------------------------------------------
/**
*
*/
private afterRender() {
this.prevModelList.forEach(item => {
if (item.leaked === false) {
item.discarded = true;
}
});
}
/**
*
*/
private beforeRender() { }
} }

View File

@ -49,15 +49,15 @@ export class Engine {
/** /**
* *
* @param sources * @param sources
* @param force * @param prevStep
*/ */
public render(source: Sources, force: boolean = false) { public render(source: Sources, prevStep: boolean = false) {
if (source === undefined || source === null) { if (source === undefined || source === null) {
return; return;
} }
`` ``
let stringSource = JSON.stringify(source); let stringSource = JSON.stringify(source);
if (force === false && this.prevStringSource === stringSource) { if (this.prevStringSource === stringSource) {
return; return;
} }
@ -67,7 +67,7 @@ export class Engine {
const layoutGroupTable = this.modelConstructor.construct(source); const layoutGroupTable = this.modelConstructor.construct(source);
// 2 渲染使用g6进行渲染 // 2 渲染使用g6进行渲染
this.viewContainer.render(layoutGroupTable, source.isEnterFunction as boolean); this.viewContainer.render(layoutGroupTable, source.isEnterFunction as boolean, prevStep);
} }

View File

@ -17,6 +17,5 @@ module.exports = {
loader: 'ts-loader' loader: 'ts-loader'
} }
] ]
}, }
// devtool: 'eval-source-map'
}; };