Merge branch 'main' of https://gitlab.com/phenomLi/StructV2 into main

# Conflicts:
#	demo/Layouter/Force.js
#	src/Model/SVModel.ts
#	src/StructV.ts
#	src/View/renderer.ts
#	src/View/viewContainer.ts
#	src/engine.ts
This commit is contained in:
廖威敬 2022-04-06 11:33:48 +08:00
commit 7b9fffe0ab
51 changed files with 9550 additions and 19139 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules node_modules
test test
dist

View File

@ -1,6 +1,6 @@
const fs = require('fs'); const fs = require('fs');
const sourcePath = 'D:\\个人项目\\v\\StructV2\\dist\\sv.js'; const sourcePath = 'D:\\个人项目\\v\\StructV2\\dist\\sv.js';
const targetPath = 'D:\\个人项目\\anyview项目\\froend_student\\src\\pages\\student\\assets\\js\\sv.js' const targetPath = 'D:\\个人项目\\froend_student\\src\\pages\\student\\assets\\js\\sv.js'
function COPY(from, to) { function COPY(from, to) {

View File

@ -20,7 +20,6 @@ const isNeighbor = function (itemA, itemB) {
SV.registerLayout('AdjoinMatrixGraph', { SV.registerLayout('AdjoinMatrixGraph', {
sourcesPreprocess(sources) { sourcesPreprocess(sources) {
let dataLength = sources.length; let dataLength = sources.length;
let matrixNodeLength = dataLength * dataLength; let matrixNodeLength = dataLength * dataLength;

155
demo/Layouter/BinaryTree.js Normal file
View File

@ -0,0 +1,155 @@
SV.registerLayout('BinaryTree', {
defineOptions() {
return {
node: {
default: {
type: 'binary-tree-node',
size: [60, 30],
label: '[data]',
style: {
fill: '#b83b5e',
stroke: '#333',
cursor: 'pointer',
},
},
},
link: {
child: {
type: 'line',
sourceAnchor: index => (index === 0 ? 3 : 1),
targetAnchor: 0,
style: {
stroke: '#333',
lineAppendWidth: 6,
cursor: 'pointer',
endArrow: 'default',
startArrow: {
path: G6.Arrow.circle(2, -1),
fill: '#333',
},
},
},
},
marker: {
external: {
type: 'pointer',
anchor: 0,
offset: 14,
style: {
fill: '#f08a5d',
},
labelOptions: {
style: {
fill: '#000099',
},
},
},
},
layout: {
xInterval: 40,
yInterval: 40,
},
behavior: {
// dragNode: false
},
};
},
/**
* 对子树进行递归布局
*/
layoutItem(node, layoutOptions) {
// 次双亲不进行布局
if (!node) {
return null;
}
let bound = node.getBound(),
width = bound.width,
height = bound.height,
group = new Group(node),
leftGroup = null,
rightGroup = null,
leftBound = null,
rightBound = null;
if (node.visited) {
return null;
}
node.visited = true;
if (node.child && node.child[0]) {
leftGroup = this.layoutItem(node.child[0], layoutOptions);
}
if (node.child && node.child[1]) {
rightGroup = this.layoutItem(node.child[1], layoutOptions);
}
if (leftGroup) {
leftBound = leftGroup.getBound();
node.set('y', leftBound.y - layoutOptions.yInterval - height);
}
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) {
leftBound = leftGroup.getBound();
node.set('x', leftBound.x + leftBound.width + layoutOptions.xInterval / 2 - width);
}
if (rightGroup) {
rightBound = rightGroup.getBound();
node.set('x', rightBound.x - layoutOptions.xInterval / 2 - width);
}
if (leftGroup) {
group.add(leftGroup);
}
if (rightGroup) {
group.add(rightGroup);
}
return group;
},
/**
* 布局函数
* @param {*} elements
* @param {*} layoutOptions
*/
layout(elements, layoutOptions) {
let root = elements[0];
this.layoutItem(root, layoutOptions);
},
});
[{
id: 6385328,
data: '',
external: ['L'],
root: true,
after: null,
next: null,
}, ];

View File

@ -13,7 +13,7 @@ SV.registerLayout('ChainHashTable', {
element: { element: {
head: { head: {
type: 'two-cell-node', type: 'two-cell-node',
label: '[id]', label: '[data]',
size: [70, 40], size: [70, 40],
style: { style: {
stroke: '#333', stroke: '#333',
@ -22,7 +22,7 @@ SV.registerLayout('ChainHashTable', {
}, },
node: { node: {
type: 'link-list-node', type: 'link-list-node',
label: '[id]', label: '[data]',
size: [60, 30], size: [60, 30],
style: { style: {
stroke: '#333', stroke: '#333',
@ -64,12 +64,13 @@ SV.registerLayout('ChainHashTable', {
} }
} }
}, },
pointer: { marker: {
external: { external: {
anchor: 3, type: 'pointer',
anchor: 1,
offset: 8, offset: 8,
style: { style: {
fill: '#f08a5d' fill: '#f08a5d'
} }
} }
}, },

View File

@ -63,6 +63,40 @@ SV.registerShape('three-cell-node', {
}); });
} }
//节点没有原子时
if(!cfg.sub){
group.addShape('text', {
attrs: {
x: width,
y: height * ( 8 / 7),
textAlign: 'center',
textBaseline: 'middle',
text: '^',
fill: '#000',
fontSize: 20,
},
name: 'text',
draggable: true
});
}
//节点没有子表时
if(!cfg.next){
group.addShape('text', {
attrs: {
x: width * (4 / 3),
y: height * ( 8 / 7),
textAlign: 'center',
textBaseline: 'middle',
text: '^',
fill: '#000',
fontSize: 20,
},
name: 'text',
draggable: true
});
}
return wrapperRect; return wrapperRect;
}, },

View File

@ -1,17 +1,14 @@
SV.registerLayout('HashTable', { SV.registerLayout('HashTable', {
sourcesPreprocess(sources) { sourcesPreprocess(sources) {
const firstElement = sources[0]; const firstElement = sources[0];
if(firstElement.external) { if (firstElement.external) {
firstElement.headExternal = firstElement.external; firstElement.headExternal = firstElement.external;
delete firstElement.external; delete firstElement.external;
} }
if(firstElement.cursor) { if (firstElement.cursor) {
firstElement.headCursor = firstElement.cursor; firstElement.headCursor = firstElement.cursor;
delete firstElement.cursor; delete firstElement.cursor;
} }
@ -21,9 +18,9 @@ SV.registerLayout('HashTable', {
defineOptions() { defineOptions() {
return { return {
element: { element: {
default: { default: {
type: 'indexed-node', type: 'array-node',
label: '[data]', label: '[data]',
size: [60, 30], size: [60, 30],
style: { style: {
@ -71,25 +68,29 @@ SV.registerLayout('HashTable', {
layout(elements) { layout(elements) {
let arr = elements; let arr = elements;
for(let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
let width = arr[i].get('size')[0]; let width = arr[i].get('size')[0];
if(i > 0) { if (i > 0) {
arr[i].set('x', arr[i - 1].get('x') + width); arr[i].set('x', arr[i - 1].get('x') + width);
} }
if(arr[i].empty) { if (arr[i].empty) {
arr[i].set('style', { arr[i].set('style', {
fill: null fill: null
}); });
} }
if(arr[i].disable) { if (arr[i].disable) {
arr[i].set('style', { arr[i].set('style', {
fill: '#ccc' fill: null
}); });
arr[i].set('labelCfg', {
style: {
opacity: 0.4
}
})
} }
} }
} }
}); });

View File

@ -1,11 +1,11 @@
SV.registerLayout('LinkList', { SV.registerLayout('LinkList', {
sourcesPreprocess(sources) { sourcesPreprocess(sources) {
let root = sources[0]; let root = sources[0];
if(root.external) { if (root.external) {
root.rootExternal = root.external; root.rootExternal = root.external;
delete root.external; delete root.external;
} }
@ -15,7 +15,7 @@ SV.registerLayout('LinkList', {
defineOptions() { defineOptions() {
return { return {
node: { node: {
default: { default: {
type: 'link-list-node', type: 'link-list-node',
label: '[data]', label: '[data]',
@ -28,7 +28,7 @@ SV.registerLayout('LinkList', {
} }
}, },
link: { link: {
next: { next: {
type: 'line', type: 'line',
sourceAnchor: 2, sourceAnchor: 2,
targetAnchor: 6, targetAnchor: 6,
@ -36,7 +36,7 @@ SV.registerLayout('LinkList', {
stroke: '#333', stroke: '#333',
endArrow: 'default', endArrow: 'default',
startArrow: { startArrow: {
path: G6.Arrow.circle(2, -1), path: G6.Arrow.circle(2, -1),
fill: '#333' fill: '#333'
} }
} }
@ -50,7 +50,7 @@ SV.registerLayout('LinkList', {
stroke: '#333', stroke: '#333',
endArrow: 'default', endArrow: 'default',
startArrow: { startArrow: {
path: G6.Arrow.circle(2, -1), path: G6.Arrow.circle(2, -1),
fill: '#333' fill: '#333'
} }
} }
@ -77,9 +77,6 @@ SV.registerLayout('LinkList', {
layout: { layout: {
xInterval: 50, xInterval: 50,
yInterval: 50 yInterval: 50
},
behavior: {
dragNode: false
} }
}; };
}, },
@ -90,22 +87,22 @@ SV.registerLayout('LinkList', {
* @param node * @param node
* @param parent * @param parent
*/ */
layoutItem(node, prev, layoutOptions) { layoutItem(node, prev, layoutOptions) {
if(!node) { if (!node) {
return null; return null;
} }
let width = node.get('size')[0]; let width = node.get('size')[0];
if(prev) { if (prev) {
node.set('y', prev.get('y')); node.set('y', prev.get('y'));
node.set('x', prev.get('x') + layoutOptions.xInterval + width); node.set('x', prev.get('x') + layoutOptions.xInterval + width);
} }
if(node.next) { if (node.next) {
this.layoutItem(node.next, node, layoutOptions); this.layoutItem(node.next, node, layoutOptions);
} }
}, },
layout(elements, layoutOptions) { layout(elements, layoutOptions) {

View File

@ -72,6 +72,20 @@ SV.registerLayout('LinkQueue', {
fill: '#333' fill: '#333'
} }
} }
},
loopNext: {
type: 'quadratic',
curveOffset: -100,
sourceAnchor: 2,
targetAnchor: 7,
style: {
stroke: '#333',
endArrow: 'default',
startArrow: {
path: G6.Arrow.circle(2, -1),
fill: '#333'
}
}
} }
}, },
marker: { marker: {
@ -85,7 +99,7 @@ SV.registerLayout('LinkQueue', {
}, },
layout: { layout: {
xInterval: 50, xInterval: 50,
yInterval: 50 yInterval: 58
}, },
behavior: { behavior: {
dragNode: ['node'] dragNode: ['node']

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',
@ -44,6 +44,20 @@
fill: '#333' fill: '#333'
} }
} }
},
loopNext: {
type: 'quadratic',
curveOffset: -100,
sourceAnchor: 2,
targetAnchor: 7,
style: {
stroke: '#333',
endArrow: 'default',
startArrow: {
path: G6.Arrow.circle(2, -1),
fill: '#333'
}
}
} }
}, },
marker: { marker: {
@ -83,7 +97,7 @@
let height = node.get('size')[1]; let height = node.get('size')[1];
if(prev) { if(prev && node.isSameGroup(prev)) {
node.set('x', prev.get('x')); node.set('x', prev.get('x'));
node.set('y', prev.get('y') - layoutOptions.yInterval - height); node.set('y', prev.get('y') - layoutOptions.yInterval - height);
} }

197
demo/Layouter/PCTree.js Normal file
View File

@ -0,0 +1,197 @@
SV.registerLayout('PCTree', {
sourcesPreprocess(sources) {
let headNodes = sources.filter(item => item.type === 'PCTreeHead');
for(let i = 0; i < headNodes.length; i++){
let dataNode = {
type: 'PCTreePreHead',
id: headNodes[i].id + '_0',
data: headNodes[i].preData,
indexLeft: headNodes[i].index,
root: headNodes[i].root
}
if(dataNode.root){
dataNode.indexTop = 'data';
headNodes[i].indexTop = ' parent firstChild'
}
sources.push(dataNode)
}
return sources;
},
defineOptions() {
return {
node: {
PCTreePreHead: {
type: 'rect',
label: '[data]',
size: [60, 34],
labelOptions: {
style: { fontSize: 16 }
},
style: {
stroke: '#333',
fill: '#95e1d3',
offset: 25
}
},
PCTreeHead: {
type: 'two-cell-node',
label: '[data]',
size: [120, 34],
style: {
stroke: '#333',
fill: '#95e1d3'
}
},
PCTreeNode: {
type: 'link-list-node',
label: '[data]',
size: [60, 27],
style: {
stroke: '#333',
fill: '#00AF92'
}
}
},
indexLabel: {
indexTop: { position: 'top' },
indexLeft: { position: 'left' }
},
link: {
headNext: {
sourceAnchor: 1,
targetAnchor: 6,
style: {
stroke: '#333',
endArrow: {
path: G6.Arrow.triangle(8, 6, 0),
fill: '#333'
},
startArrow: {
path: G6.Arrow.circle(2, -1),
fill: '#333'
}
}
},
next: {
sourceAnchor: 2,
targetAnchor: 6,
style: {
stroke: '#333',
endArrow: {
path: G6.Arrow.triangle(8, 6, 0),
fill: '#333'
},
startArrow: {
path: G6.Arrow.circle(2, -1),
fill: '#333'
}
}
}
},
marker: {
external: {
type: 'pointer',
anchor: 0,
offset: 8,
style: {
fill: '#f08a5d'
}
}
},
layout: {
xInterval: 50,
yInterval: 86
},
behavior: {
dragNode: ['PCTreeNode']
}
};
},
//判断node节点是否之前布局过
isUnique(value, allNodeIdValue){
let re = new RegExp("" + value);
return !re.test(allNodeIdValue);
},
/**
* 对子树进行递归布局
* @param node
* @param parent
*/
layoutItem(node, prev, layoutOptions, allNodeId) {
if(!node) {
return null;
}
let width = node.get('size')[0],
idValue = node.id.split('(')[1].slice(0, -1);
//有y型链表的情况不用再布局
if(this.isUnique(idValue, allNodeId.value)){
if(prev) {
node.set('y', prev.get('y'));
node.set('x', prev.get('x') + layoutOptions.xInterval + width);
}
allNodeId.value += idValue;
if(node.next) {
this.layoutItem(node.next, node, layoutOptions, allNodeId);
}
}
},
layout(elements, layoutOptions) {
let headNode = elements.filter(item => item.type === 'PCTreeHead'),
preHeadNode = elements.filter(item => item.type === 'PCTreePreHead'),
roots = elements.filter(item => item.type === 'PCTreeNode' && item.root),
height = headNode[0].get('size')[1],
width = headNode[0].get('size')[0],
i,
allNodeId = { value: ''}; //引用类型用于传参
for(i = 0; i < headNode.length; i++) {
let node = headNode[i],
preNode = preHeadNode[i];
node.set({
x: 0,
y: i * height
});
preNode.set({
x: width / 4,
y: (i + 1) * height
})
if(node.headNext) {
let y = node.get('y') + height - node.headNext.get('size')[1],
x = width + layoutOptions.xInterval * 2;
node.headNext.set({ x, y });
this.layoutItem(node.headNext, null, layoutOptions, allNodeId);
}
}
for(i = 0; i < roots.length; i++) {
let nodeWidth = roots[0].get('size')[0],
nodeInternalSum = i * (nodeWidth + layoutOptions.xInterval);
roots[i].set({
x: headNode[0].get('x') + width + layoutOptions.xInterval * 2 + nodeInternalSum,
y: headNode[0].get('y') - layoutOptions.yInterval
})
}
}
});

View File

@ -35,8 +35,12 @@ SV.registerLayout('PTree', {
data: sources[i].parent data: sources[i].parent
}); });
} }
sources[0].indexLeft = 'data'; if(sources[0]){
parentNodes[0].indexLeft = 'parent'; sources[0].indexLeft = 'data';
}
if(parentNodes[0]){
parentNodes[0].indexLeft = 'parent';
}
sources.push(...parentNodes); sources.push(...parentNodes);
@ -70,14 +74,17 @@ SV.registerLayout('PTree', {
layout(elements) { layout(elements) {
let nodeLength = elements.length, let nodeLength = elements.length;
halfLength = nodeLength / 2,
if(nodeLength == 0) return;
let halfLength = nodeLength / 2,
size = elements[0].get('size')[0], size = elements[0].get('size')[0],
i; i;
for (i = 0; i < nodeLength; i++) { for (i = 0; i < nodeLength; i++) {
let x = (i % halfLength) * size; let x = (i % halfLength) * size,
y = Math.floor(i / halfLength) * size; y = Math.floor(i / halfLength) * size;
elements[i].set({ x, y }); elements[i].set({ x, y });

View File

@ -1,96 +1,115 @@
/** /**
* 三叉树 * 三叉树
*/ */
SV.registerLayout('TriTree', { SV.registerLayout('TriTree', {
defineOptions() { defineOptions() {
return { return {
/** node: {
* 结点
*/
element: {
default: { default: {
type: 'tri-tree-node', type: 'tri-tree-node',
size: [60, 30], size: [60, 30],
label: '[data]', label: '[data]',
style: { style: {
fill: '#ff2e63', fill: '#95e1d3',
stroke: "#333", stroke: '#333',
cursor: 'pointer' cursor: 'pointer',
backgroundFill: '#eee'
},
labelOptions: {
style: { fontSize: 16 }
} }
} }
}, },
/**
* 箭头
*/
link: { link: {
child: { child: {
type: 'line',
sourceAnchor: index => index, sourceAnchor: index => index,
targetAnchor: 3, targetAnchor: 3,
type: 'line',
style: { style: {
stroke: '#333', stroke: '#333',
//边的响应范围 lineAppendWidth: 10,
lineAppendWidth: 6, lineWidth: 1.6,
cursor: 'pointer',
endArrow: 'default', endArrow: 'default',
startArrow: { startArrow: {
//参数:半径、偏移量 path: G6.Arrow.circle(2, -1),
path: G6.Arrow.circle(2, -1),
fill: '#333' fill: '#333'
} }
} }
}, },
parent: { parent: {
type: 'line', type: 'line',
sourceAnchor: 4, sourceAnchor: 4,
targetAnchor: 2, targetAnchor: 6,
style: { style: {
stroke: '#A9A9A9', stroke: '#999',
//边的响应范围 lineAppendWidth: 10,
lineAppendWidth: 6, lineWidth: 1.6,
cursor: 'pointer',
endArrow: 'default', endArrow: 'default',
startArrow: { startArrow: {
//参数:半径、偏移量 path: G6.Arrow.circle(2, -1),
path: G6.Arrow.circle(2, -1), fill: '#999'
fill: '#333' }
}
},
r_parent: {
type: 'quadratic',
sourceAnchor: 4,
targetAnchor: 5,
curveOffset: -20,
style: {
stroke: '#999',
lineAppendWidth: 10,
lineWidth: 1.6,
endArrow: 'default',
startArrow: {
path: G6.Arrow.circle(2, -1),
fill: '#999'
}
}
},
l_parent: {
type: 'quadratic',
sourceAnchor: 4,
targetAnchor: 2,
curveOffset: 20,
style: {
stroke: '#999',
lineAppendWidth: 10,
lineWidth: 1.6,
endArrow: 'default',
startArrow: {
path: G6.Arrow.circle(2, -1),
fill: '#999'
} }
} }
}, },
}, },
/**
* 指针
*/
marker: { marker: {
external: { external: {
type: "pointer", type: 'pointer',
anchor: 3, anchor: 3,
offset: 14, offset: 14,
labelOffset: 2,
style: { style: {
fill: '#f08a5d' fill: '#f08a5d'
}, },
labelOptions: { labelOptions: {
style: { style: {
// stroke: fontSize: 15,
fill: '#999'
} }
} }
} }
}, },
/** addressLabel: {
* 布局 style: {
*/ fill: '#999'
}
},
layout: { layout: {
xInterval: 40, xInterval: 40,
yInterval: 40, yInterval: 40,
}, }
/**
* 动画
*/
//animation: {
// enable: true,
// duration: 750,
// timingFunction: 'easePolyOut'
//}
}; };
}, },
@ -99,44 +118,44 @@
*/ */
layoutItem(node, parent, index, layoutOptions) { layoutItem(node, parent, index, layoutOptions) {
// 次双亲不进行布局 // 次双亲不进行布局
if(!node) { if (!node) {
return null; return null;
} }
let bound = node.getBound(), //获取包围盒 let bound = node.getBound(), //获取包围盒
width = bound.width, width = bound.width,
height = bound.height, height = bound.height,
group = new Group(node); //创建分组 group = new Group(node); //创建分组
//有双亲,设置结点的位置 //有双亲,设置结点的位置
if(parent) { if (parent) {
// 纵坐标 // 纵坐标
node.set('y', parent.get('y') + layoutOptions.yInterval + height); node.set('y', parent.get('y') + layoutOptions.yInterval + height);
// 左节点横坐标 // 左节点横坐标
if(index === 0) { if (index === 0) {
node.set('x', parent.get('x') - layoutOptions.xInterval / 2 - width / 2); node.set('x', parent.get('x') - layoutOptions.xInterval / 2 - width / 2);
} }
// 右结点横坐标 // 右结点横坐标
if(index === 1) { if (index === 1) {
node.set('x', parent.get('x') + layoutOptions.xInterval / 2 + width / 2); node.set('x', parent.get('x') + layoutOptions.xInterval / 2 + width / 2);
} }
} }
//有孩子 //有孩子
if(node.child && (node.child[0] || node.child[1])) { if (node.child && (node.child[0] || node.child[1])) {
let leftChild = node.child[0], let leftChild = node.child[0],
rightChild = node.child[1], rightChild = node.child[1],
leftGroup = this.layoutItem(leftChild, node, 0, layoutOptions), leftGroup = this.layoutItem(leftChild, node, 0, layoutOptions),
rightGroup = this.layoutItem(rightChild, node, 1, layoutOptions), rightGroup = this.layoutItem(rightChild, node, 1, layoutOptions),
intersection = null, intersection = null,
move = 0; move = 0;
// 处理左子树中子树相交问题 // 处理左子树中子树相交问题
if(leftGroup && rightChild) { if (leftGroup && rightChild) {
//求出包围盒相交部分 //求出包围盒相交部分
intersection = Bound.intersect(leftGroup.getBound(), rightGroup.getBound()); intersection = Bound.intersect(leftGroup.getBound(), rightGroup.getBound());
move = 0; move = 0;
//处理 //处理
if(intersection && intersection.width > 0) { if (intersection && intersection.width > 0) {
move = (intersection.width + layoutOptions.xInterval) / 2; move = (intersection.width + layoutOptions.xInterval) / 2;
// console.log(move,intersection.width,layoutOptions.xInterval); // console.log(move,intersection.width,layoutOptions.xInterval);
leftGroup.translate(-move, 0); leftGroup.translate(-move, 0);
@ -145,17 +164,17 @@
} }
//加入分组 //加入分组
if(leftGroup) { if (leftGroup) {
group.add(leftGroup); group.add(leftGroup);
} }
if(rightGroup) { if (rightGroup) {
group.add(rightGroup) group.add(rightGroup)
} }
} }
//返回分组 //返回分组
return group; return group;
}, },
/** /**
* 布局函数 * 布局函数

292
demo/data.js Normal file
View File

@ -0,0 +1,292 @@
const SOURCES_DATA = [{
"TriTree0": {
"data": [{
"external": [
"T"
],
"parent": [
"0x0"
],
"child": [
"0xfd9ee0",
"0xfd9f10"
],
"id": "0xfd9eb0",
"name": "T",
"data": "A",
"root": true,
"type": "default"
},
{
"child": [
"0x0",
"0x0"
],
"id": "0xfd9ee0",
"name": "T->lchild",
"data": "B",
"type": "default",
"l_parent": [
"0xfd9eb0"
],
"external": [
"T1"
]
},
{
"child": [
"0x0",
"0x0"
],
"id": "0xfd9f10",
"name": "T->rchild",
"data": "C",
"type": "default",
"r_parent": [
"0xfd9eb0"
],
"external": [
"T2"
]
}
],
"layouter": "TriTree"
},
"TriTree3": {
"data": [{
"external": [
"T3"
],
"parent": [
"0x0"
],
"child": [
"0x0",
"0x0"
],
"id": "0xfd9f40",
"name": "T3",
"data": "D",
"root": true,
"type": "default"
}],
"layouter": "TriTree"
},
"TriTree4": {
"data": [{
"external": [
"T4"
],
"parent": [
"0x0"
],
"child": [
"0x0",
"0x0"
],
"id": "0xfd9f70",
"name": "T4",
"data": "E",
"root": true,
"type": "default"
}],
"layouter": "TriTree"
},
"handleUpdate": {
"isEnterFunction": true,
"isFirstDebug": true
}
}, {
"TriTree0": {
"data": [{
"external": [
"T"
],
"parent": [
"0x0"
],
"child": [
"0xfd9ee0",
"0xfd9f10"
],
"id": "0xfd9eb0",
"name": "T",
"data": "A",
"root": true,
"type": "default"
},
{
"child": [
"0x0",
"0x0"
],
"id": "0xfd9ee0",
"name": "T->lchild",
"data": "B",
"type": "default",
"l_parent": [
"0xfd9eb0"
],
"external": [
"T1"
]
},
{
"child": [
"0x0",
"0x0"
],
"id": "0xfd9f10",
"name": "T->rchild",
"data": "C",
"type": "default",
"r_parent": [
"0xfd9eb0"
],
"external": [
"T2"
]
}
],
"layouter": "TriTree"
},
"TriTree3": {
"data": [{
"external": [
"T3"
],
"parent": [
"0x0"
],
"child": [
"0x0",
"0x0"
],
"id": "0xfd9f40",
"name": "T3",
"data": "D",
"root": true,
"type": "default"
}],
"layouter": "TriTree"
},
"TriTree4": {
"data": [{
"external": [
"T4"
],
"parent": [
"0xfd9f40"
],
"child": [
"0x0",
"0x0"
],
"id": "0xfd9f70",
"name": "T4",
"data": "E",
"root": true,
"type": "default"
}],
"layouter": "TriTree"
},
"handleUpdate": {
"isEnterFunction": false,
"isFirstDebug": false
}
}, {
"TriTree0": {
"data": [{
"external": [
"T"
],
"parent": [
"0x0"
],
"child": [
"0xfd9ee0",
"0xfd9f10"
],
"id": "0xfd9eb0",
"name": "T",
"data": "A",
"root": true,
"type": "default"
},
{
"child": [
"0x0",
"0x0"
],
"id": "0xfd9ee0",
"name": "T->lchild",
"data": "B",
"type": "default",
"l_parent": [
"0xfd9eb0"
],
"external": [
"T1"
]
},
{
"child": [
"0x0",
"0x0"
],
"id": "0xfd9f10",
"name": "T->rchild",
"data": "C",
"type": "default",
"r_parent": [
"0xfd9eb0"
],
"external": [
"T2"
]
}
],
"layouter": "TriTree"
},
"TriTree3": {
"data": [{
"external": [
"T3"
],
"parent": [
"0x0"
],
"child": [
"0xfd9f70",
"0x0"
],
"id": "0xfd9f40",
"name": "T3",
"data": "D",
"root": true,
"type": "default"
},
{
"child": [
"0x0",
"0x0"
],
"id": "0xfd9f70",
"name": "T3->lchild",
"data": "E",
"type": "default",
"l_parent": [
"0xfd9f40"
],
"external": [
"T4"
]
}
],
"layouter": "TriTree"
},
"handleUpdate": {
"isEnterFunction": false,
"isFirstDebug": false
}
}];

View File

@ -2,8 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DEMO</title> <title>DEMO</title>
<style> <style>
* { * {
@ -11,25 +11,25 @@
margin: 0; margin: 0;
user-select: none; user-select: none;
} }
.container { .container {
background-color: #fafafa; background-color: #fafafa;
border: 1px solid #ccc; border: 1px solid #ccc;
position: relative; position: relative;
} }
.down { .down {
display: flex; display: flex;
margin-top: 20px; margin-top: 20px;
} }
#container { #container {
width: 100%; width: 100%;
height: 500px; height: 500px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
#leak { #leak {
position: absolute; position: absolute;
left: 0; left: 0;
@ -42,7 +42,7 @@
pointer-events: none; pointer-events: none;
transition: opacity 0.75s ease-in-out; transition: opacity 0.75s ease-in-out;
} }
#leak>span { #leak>span {
color: #000; color: #000;
} }
@ -50,7 +50,6 @@
</head> </head>
<body> <body>
<div class="container" id="container"> <div class="container" id="container">
<div id="leak"> <div id="leak">
<span>泄漏区</span> <span>泄漏区</span>
@ -67,15 +66,14 @@
<script src="./../dist/sv.js"></script> <script src="./../dist/sv.js"></script>
<script> <script>
const Group = SV.Group, const Group = SV.Group,
Bound = SV.Bound, Bound = SV.Bound,
G6 = SV.G6, G6 = SV.G6,
Vector = SV.Vector; Vector = SV.Vector;
</script> </script>
<script src="./Layouter/LinkList.js"></script> <script src="./Layouter/LinkList.js"></script>
<script src="./Layouter/BinaryTree.js"></script> <script src="./Layouter/BinaryTree.js"></script>
<script src="./Layouter/TriTree.js"></script>
<script src="./Layouter/Stack.js"></script> <script src="./Layouter/Stack.js"></script>
<script src="./Layouter/LinkQueue.js"></script> <script src="./Layouter/LinkQueue.js"></script>
<script src="./Layouter/GeneralizedList.js"></script> <script src="./Layouter/GeneralizedList.js"></script>
@ -87,9 +85,10 @@
<script src="./Layouter/AdjoinTableGraph.js"></script> <script src="./Layouter/AdjoinTableGraph.js"></script>
<script src="./Layouter/SqQueue.js"></script> <script src="./Layouter/SqQueue.js"></script>
<script src="./Layouter/PTree.js"></script> <script src="./Layouter/PTree.js"></script>
<script src="./Layouter/PCTree.js"></script>
<script src="./data.js"></script>
<script> <script>
let cur = SV(document.getElementById('container'), { let cur = SV(document.getElementById('container'), {
view: { view: {
leakAreaHeight: 130, leakAreaHeight: 130,
@ -97,133 +96,8 @@
}, },
}); });
let data = [{
"sqStack0": {
"data": [
{
"id": "0x617eb5",
"data": "",
"index": 5,
"cursor": "top"
},
{
"id": "0x617eb4",
"data": "2",
"index": 4
},
{
"id": "0x617eb3",
"data": "6",
"index": 3
},
{
"id": "0x617eb2",
"data": "7",
"index": 2
},
{
"id": "0x617eb1",
"data": "9",
"index": 1
},
{
"id": "0x617eb0",
"data": "1",
"index": 0,
"external": "S"
}
],
"layouter": "Stack"
}
}, {
"sqStack0": {
"data": [
{
"id": "0x617eb4",
"data": "2",
"index": 4,
"cursor": "top",
"type": "default"
},
{
"id": "0x617eb3",
"data": "6",
"index": 3,
"type": "default"
},
{
"id": "0x617eb2",
"data": "7",
"index": 2,
"type": "default"
},
{
"id": "0x617eb1",
"data": "9",
"index": 1,
"type": "default"
},
{
"id": "0x617eb0",
"data": "1",
"index": 0,
"bottomExternal": "S",
"type": "default"
}
],
"layouter": "Stack"
}
}, {
"data": [
{
"id": "0x617eb5",
"data": "",
"index": 5,
"cursor": "top",
"type": "default"
},
{
"id": "0x617eb4",
"data": "2",
"index": 4,
"type": "default"
},
{
"id": "0x617eb3",
"data": "6",
"index": 3,
"type": "default"
},
{
"id": "0x617eb2",
"data": "7",
"index": 2,
"type": "default"
},
{
"id": "0x617eb1",
"data": "9",
"index": 1,
"type": "default"
},
{
"id": "0x617eb0",
"data": "1",
"index": 0,
"bottomExternal": "S",
"type": "default"
}
],
"layouter": "Stack"
}];
let dataIndex = 0, let dataIndex = 0,
curData = data[dataIndex]; curData = SOURCES_DATA[dataIndex];
let enableBrushSelect = false; let enableBrushSelect = false;
@ -235,12 +109,12 @@
cur.render(curData); cur.render(curData);
document.getElementById('btn-next').addEventListener('click', e => { document.getElementById('btn-next').addEventListener('click', e => {
curData = data[++dataIndex]; curData = SOURCES_DATA[++dataIndex];
cur.render(curData); cur.render(curData);
}); });
document.getElementById('btn-prev').addEventListener('click', e => { document.getElementById('btn-prev').addEventListener('click', e => {
curData = data[--dataIndex]; curData = SOURCES_DATA[--dataIndex];
cur.render(curData); cur.render(curData);
}); });
@ -270,12 +144,10 @@
// ------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------
container.addEventListener('mousemove', e => { container.addEventListener('mousemove', e => {
let x = e.offsetX, y = e.offsetY; let x = e.offsetX,
y = e.offsetY;
pos.innerHTML = `${x},${y}`; pos.innerHTML = `${x},${y}`;
}); });
</script> </script>
</body> </body>

View File

@ -1,147 +0,0 @@
SV.registerLayout('BinaryTree', {
defineOptions() {
return {
element: {
default: {
type: 'binary-tree-node',
size: [60, 30],
label: '[data]',
style: {
fill: '#b83b5e',
stroke: "#333",
cursor: 'pointer'
}
}
},
link: {
child: {
type: 'line',
sourceAnchor: index => index === 0? 3: 1,
targetAnchor: 0,
style: {
stroke: '#333',
lineAppendWidth: 6,
cursor: 'pointer',
endArrow: 'default',
startArrow: {
path: G6.Arrow.circle(2, -1),
fill: '#333'
}
}
}
},
marker: {
external: {
type: 'pointer',
anchor: 0,
offset: 14,
style: {
fill: '#f08a5d'
},
labelOptions: {
style: {
fill: '#000099'
}
}
}
},
layout: {
xInterval: 40,
yInterval: 40
},
behavior: {
// dragNode: false
}
};
},
/**
* 对子树进行递归布局
*/
layoutItem(node, parent, index, layoutOptions) {
// 次双亲不进行布局
if(!node) {
return null;
}
let bound = node.getBound(),
width = bound.width,
height = bound.height,
group = new Group(node);
if(parent) {
node.set('y', parent.get('y') + layoutOptions.yInterval + height);
// 左节点
if(index === 0) {
node.set('x', parent.get('x') - layoutOptions.xInterval / 2 - width / 2);
}
// 右结点
if(index === 1) {
node.set('x', parent.get('x') + layoutOptions.xInterval / 2 + width / 2);
}
}
if(node.child && (node.child[0] || node.child[1])) {
let leftChild = node.child[0],
rightChild = node.child[1],
leftGroup = this.layoutItem(leftChild, node, 0, layoutOptions),
rightGroup = this.layoutItem(rightChild, node, 1, layoutOptions),
intersection = null,
move = 0;
// 处理左右子树相交问题
if(leftGroup && rightGroup) {
intersection = Bound.intersect(leftGroup.getBound(), rightGroup.getBound());
move = 0;
if(intersection && intersection.width > 0) {
move = (intersection.width + layoutOptions.xInterval) / 2;
leftGroup.translate(-move, 0);
rightGroup.translate(move, 0);
}
}
if(leftGroup) {
group.add(leftGroup);
}
if(rightGroup) {
group.add(rightGroup)
}
}
return group;
},
/**
* 布局函数
* @param {*} elements
* @param {*} layoutOptions
*/
layout(elements, layoutOptions) {
let root = elements[0];
this.layoutItem(root, null, -1, layoutOptions);
}
});
[
{
"id": 6385328,
"data": "",
"external": [
"L"
],
"root": true,
"after": null,
"next": null
}
]

14104
dist/sv.js vendored

File diff suppressed because one or more lines are too long

4080
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,20 @@
{ {
"dependencies": {
"@antv/g6": "^4.4.1",
"merge": "^2.1.1"
},
"devDependencies": {
"merge": "^2.1.1",
"ts-loader": "^5.2.1",
"typescript": "^3.2.2",
"webpack": "^4.46.0",
"webpack-cli": "^3.2.3"
},
"scripts": { "scripts": {
"build": "webpack --config webpack.config.product.js", "build": "webpack --config webpack.config.product.js",
"dep": "webpack --config webpack.config.product.js && node copyDist2Anyview.js", "dep": "webpack --config webpack.config.product.js && node copyDist2Anyview.js",
"dev": "webpack --w --config webpack.config.develop.js", "dev": "webpack --watch --config webpack.config.develop.js",
"copy": "node copyDist2Anyview.js" "copy": "node copyDist2Anyview.js"
},
"devDependencies": {
"acorn": "^8.7.0",
"merge": "^2.1.1",
"ts-loader": "^9.2.8",
"typescript": "^4.6.2",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2"
},
"dependencies": {
"@antv/g6": "^4.6.4",
"merge": "^2.1.1"
} }
} }

View File

@ -1,30 +1,28 @@
import { Graph, INode } from "@antv/g6"; import { G6Event, Graph, INode } from '@antv/g6';
import { EventBus } from "../Common/eventBus"; import { EventBus } from '../Common/eventBus';
import { LayoutGroupTable } from "../Model/modelConstructor"; import { LayoutGroupTable } from '../Model/modelConstructor';
import { SVModel } from "../Model/SVModel"; import { SVModel } from '../Model/SVModel';
import { SVNode } from "../Model/SVNode"; import { SVNode } from '../Model/SVNode';
import { ViewContainer } from "../View/viewContainer"; import { ViewContainer } from '../View/viewContainer';
/** /**
* *
* @param node * @param node
* @param dragNodeOption * @param dragNodeOption
*/ */
const checkNodeDragAlone = function (node: SVNode, dragNodeOption: boolean | string[]): boolean { const checkNodeDragAlone = function (node: SVNode, dragNodeOption: boolean | string[]): boolean {
const nodeSourceType = node.sourceType; const nodeSourceType = node.sourceType;
if (Array.isArray(dragNodeOption)) { if (Array.isArray(dragNodeOption)) {
return dragNodeOption.includes(nodeSourceType); return dragNodeOption.includes(nodeSourceType);
} }
if (dragNodeOption === undefined || dragNodeOption === true) { if (dragNodeOption === undefined || dragNodeOption === true) {
return true; return true;
} }
return false;
}
return false;
};
/** /**
* *
@ -32,181 +30,173 @@ const checkNodeDragAlone = function (node: SVNode, dragNodeOption: boolean | str
* 2. dragNodeOption type时 * 2. dragNodeOption type时
* 3. dragNodeOption false type的节点 * 3. dragNodeOption false type的节点
*/ */
export const DetermineNodeDrag = function (layoutGroupTable: LayoutGroupTable, node: SVNode, brushSelectedModels: SVModel[]) { export const DetermineNodeDrag = function (
const layoutGroup = layoutGroupTable.get(node.group), layoutGroupTable: LayoutGroupTable,
dragNodeOption = layoutGroup.options.behavior?.dragNode, node: SVNode,
canNodeDragAlone = checkNodeDragAlone(node, dragNodeOption); brushSelectedModels: SVModel[]
) {
const layoutGroup = layoutGroupTable.get(node.group),
dragNodeOption = layoutGroup.options.behavior?.dragNode,
canNodeDragAlone = checkNodeDragAlone(node, dragNodeOption);
if (canNodeDragAlone) { if (canNodeDragAlone) {
return true; return true;
} }
const nodeSourceType = node.sourceType, const nodeSourceType = node.sourceType,
nodeModelType = node.getModelType(), nodeModelType = node.getModelType(),
modelList = (<SVModel[]>layoutGroup[nodeModelType]).filter(item => item.sourceType === nodeSourceType), modelList = (<SVModel[]>layoutGroup[nodeModelType]).filter(item => item.sourceType === nodeSourceType),
brushSelectedSameTypeModels = brushSelectedModels.filter(item => { brushSelectedSameTypeModels = brushSelectedModels.filter(item => {
return item.group === node.group && return (
item.getModelType() === nodeModelType && item.group === node.group && item.getModelType() === nodeModelType && item.sourceType === nodeSourceType
item.sourceType === nodeSourceType );
}); });
return modelList.length === brushSelectedSameTypeModels.length;
}
return modelList.length === brushSelectedSameTypeModels.length;
};
/** /**
* appendage * appendage
* *
*/ */
export function SolveNodeAppendagesDrag(viewContainer: ViewContainer) { export function SolveNodeAppendagesDrag(viewContainer: ViewContainer) {
const g6Instance: Graph = viewContainer.getG6Instance(); const g6Instance: Graph = viewContainer.getG6Instance();
g6Instance.on('node:dragstart', event => { g6Instance.on('node:dragstart', event => {
let node: SVNode = event.item['SVModel']; let node: SVNode = event.item['SVModel'];
if (node instanceof SVNode === false) { if (node instanceof SVNode === false) {
return; return;
} }
const isNodeSelected = viewContainer.brushSelectedModels.find(item => item.id === node.id); const isNodeSelected = viewContainer.brushSelectedModels.find(item => item.id === node.id);
// 如果在框选完成之后,拖拽了被框选之外的其他节点,那么取消已框选的节点的选中状态 // 如果在框选完成之后,拖拽了被框选之外的其他节点,那么取消已框选的节点的选中状态
if (isNodeSelected === undefined) { if (isNodeSelected === undefined) {
viewContainer.brushSelectedModels.forEach(item => { viewContainer.brushSelectedModels.forEach(item => {
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;
} }
}); });
g6Instance.on('node:dragend', event => { g6Instance.on('node:dragend', event => {
let node: SVNode = event.item['SVModel']; let node: SVNode = event.item['SVModel'];
if (node instanceof SVNode === false) { if (node instanceof SVNode === false) {
return; return;
} }
const isNodeSelected = viewContainer.brushSelectedModels.find(item => item.id === node.id); const isNodeSelected = viewContainer.brushSelectedModels.find(item => item.id === node.id);
// 如果当前拖拽的节点是在已框选选中的节点之中,那么不需要取消选中的状态,否则需要取消 // 如果当前拖拽的节点是在已框选选中的节点之中,那么不需要取消选中的状态,否则需要取消
if (isNodeSelected === undefined) { if (isNodeSelected === undefined) {
node.setSelectedState(false); node.setSelectedState(false);
node.set({ node.set({
x: node.G6Item.getModel().x, x: node.G6Item.getModel().x,
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,
y: item.G6Item.getModel().y y: item.G6Item.getModel().y,
}); });
}); });
} }
viewContainer.brushSelectedModels.forEach(item => { viewContainer.brushSelectedModels.forEach(item => {
item.set({ item.set({
x: item.G6Item.getModel().x, x: item.G6Item.getModel().x,
y: item.G6Item.getModel().y y: item.G6Item.getModel().y,
}); });
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,
}); });
}); });
} }
}); });
}); });
} }
/** /**
* *
* @param viewContainer * @param viewContainer
*/ */
export function SolveBrushSelectDrag(viewContainer: ViewContainer) { export function SolveBrushSelectDrag(viewContainer: ViewContainer) {
const g6Instance: Graph = viewContainer.getG6Instance(); const g6Instance: Graph = viewContainer.getG6Instance();
// 当框选完成后,监听被框选节点的数量变化事件,将被框选的节点添加到 brushSelectedModels 数组里面 // 当框选完成后,监听被框选节点的数量变化事件,将被框选的节点添加到 brushSelectedModels 数组里面
g6Instance.on('nodeselectchange', event => { g6Instance.on('nodeselectchange' as G6Event, event => {
const selectedItems = event.selectedItems as { nodes: INode[]; }, const selectedItems = event.selectedItems as { nodes: INode[] },
tmpSelectedModelList = []; tmpSelectedModelList = [];
// 如果是点击选中,不理会 // 如果是点击选中,不理会
if (event.target) { if (event.target) {
return; return;
} }
// 先清空上一次框选保存的内容 // 先清空上一次框选保存的内容
viewContainer.brushSelectedModels.length = 0; viewContainer.brushSelectedModels.length = 0;
// 首先将已框选中的节点加到一个临时队列 // 首先将已框选中的节点加到一个临时队列
selectedItems.nodes.forEach(item => { selectedItems.nodes.forEach(item => {
tmpSelectedModelList.push(item['SVModel']); tmpSelectedModelList.push(item['SVModel']);
}); });
// 之后逐个检测被框选中的节点是否可以拖拽,可以拖拽的才加入到真正的框选队列 // 之后逐个检测被框选中的节点是否可以拖拽,可以拖拽的才加入到真正的框选队列
selectedItems.nodes.forEach(item => { selectedItems.nodes.forEach(item => {
const node: SVNode = item['SVModel']; const node: SVNode = item['SVModel'];
if (DetermineNodeDrag(viewContainer.getLayoutGroupTable(), node, tmpSelectedModelList)) { if (DetermineNodeDrag(viewContainer.getLayoutGroupTable(), node, tmpSelectedModelList)) {
viewContainer.brushSelectedModels.push(node); viewContainer.brushSelectedModels.push(node);
} } else {
else { node.setSelectedState(false);
node.setSelectedState(false); }
} });
}); });
});
} }
/** /**
* *
* @param g6Instance * @param g6Instance
* @param hasLeak * @param hasLeak
*/ */
export function SolveDragCanvasWithLeak(viewContainer: ViewContainer) { export function SolveDragCanvasWithLeak(viewContainer: ViewContainer) {
let g6Instance = viewContainer.getG6Instance(), let g6Instance = viewContainer.getG6Instance();
prevDy = 0;
g6Instance.on('viewportchange', event => { g6Instance.on('viewportchange', event => {
if (event.action !== 'translate') { if (event.action !== 'translate') {
return false; return false;
} }
let translateX = event.matrix[7], let translateY = event.matrix[7],
dy = translateX - prevDy; dy = translateY - viewContainer.lastLeakAreaTranslateY;
prevDy = translateX; viewContainer.lastLeakAreaTranslateY = translateY;
viewContainer.leakAreaY = viewContainer.leakAreaY + dy; viewContainer.leakAreaY = viewContainer.leakAreaY + dy;
if (viewContainer.hasLeak) { if (viewContainer.hasLeak) {
EventBus.emit('onLeakAreaUpdate', { EventBus.emit('onLeakAreaUpdate', {
leakAreaY: viewContainer.leakAreaY, leakAreaY: viewContainer.leakAreaY,
hasLeak: viewContainer.hasLeak hasLeak: viewContainer.hasLeak,
}); });
} }
}); });
} }
/** /**
* *
* @param g6Instance * @param g6Instance
* @param generalModelsGroup * @param generalModelsGroup
*/ */
export function SolveZoomCanvasWithLeak(viewContainer: ViewContainer) { export function SolveZoomCanvasWithLeak(viewContainer: ViewContainer) {}
}

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);
}); });
@ -53,8 +53,13 @@ export function InitG6Behaviors(engine: Engine, viewContainer: ViewContainer): M
}; };
const selectNodeFilter = event => { const selectNodeFilter = event => {
let g6Item = event.item, let g6Item = event.item;
node: SVNode = g6Item.SVModel;
if(g6Item === null) {
return false
}
let node: SVNode = g6Item.SVModel;
if (g6Item === null || node.isNode() === false) { if (g6Item === null || node.isNode() === false) {
return false; return false;

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 = 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;
}
if(!overlapsX || !overlapsY) return null; y = Math.max(b1y, b2y);
maxY = Math.min(b1my, b2my);
overlapsY = maxY - y;
return { if (!overlapsX || !overlapsY) return null;
x,
y,
width: overlapsX,
height: overlapsY
};
},
/** return {
* x,
* @param bound y,
* @param dx width: overlapsX,
* @param dy height: overlapsY,
*/ };
translate(bound: BoundingRect, dx: number, dy: number) { },
bound.x += dx;
bound.y += dy;
},
/** /**
* *
* @param bound * @param bound
* @param rot * @param dx
*/ * @param dy
rotation(bound: BoundingRect, rot: number): BoundingRect { */
let cx = bound.x + bound.width / 2, translate(bound: BoundingRect, dx: number, dy: number) {
cy = bound.y + bound.height / 2; 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;
/** return Bound.fromPoints(Bound.toPoints(bound).map(item => Vector.rotation(rot, item, [cx, cy])));
* },
* @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; *
} * @param b1
* @param b2
return false; */
} 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;
},
}; };

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

@ -1,52 +1,89 @@
import { EdgeConfig, IEdge } from "@antv/g6-core"; import { EdgeConfig, IEdge } from '@antv/g6-core';
import { Util } from "../Common/util"; import { Util } from '../Common/util';
import { LinkLabelOption, LinkOption, Style } from "../options"; import { LinkLabelOption, LinkOption, Style } from '../options';
import { SVModel } from "./SVModel"; import { SVModel } from './SVModel';
import { SVNode } from "./SVNode"; import { SVNode } from './SVNode';
export class SVLink extends SVModel { export class SVLink extends SVModel {
public node: SVNode; public node: SVNode;
public target: SVNode; public target: SVNode;
public linkIndex: number; public linkIndex: number;
public shadowG6Item: IEdge; public shadowG6Item: IEdge;
public G6Item: IEdge; public G6Item: IEdge;
constructor(id: string, type: string, group: string, layout: string, node: SVNode, target: SVNode, index: number, options: LinkOption) { public nodeId: string;
super(id, type, group, layout, 'link'); public targetId: string;
this.node = node; constructor(
this.target = target; id: string,
this.linkIndex = index; type: string,
group: string,
layout: string,
node: SVNode,
target: SVNode,
index: number,
options: LinkOption
) {
super(id, type, group, layout, 'link');
node.links.outDegree.push(this); this.node = node;
target.links.inDegree.push(this); this.target = target;
this.G6ModelProps = this.generateG6ModelProps(options); this.nodeId = node.id;
} this.targetId = target.id;
this.linkIndex = index;
generateG6ModelProps(options: LinkOption): EdgeConfig { node.links.outDegree.push(this);
let sourceAnchor = options.sourceAnchor, target.links.inDegree.push(this);
targetAnchor = options.targetAnchor; this.G6ModelProps = this.generateG6ModelProps(options);
}
if(options.sourceAnchor && typeof options.sourceAnchor === 'function' && this.linkIndex !== null) { generateG6ModelProps(options: LinkOption): EdgeConfig {
sourceAnchor = options.sourceAnchor(this.linkIndex); let sourceAnchor = options.sourceAnchor,
} targetAnchor = options.targetAnchor;
if(options.targetAnchor && typeof options.targetAnchor === 'function' && this.linkIndex !== null) { if (options.sourceAnchor && typeof options.sourceAnchor === 'function' && this.linkIndex !== null) {
targetAnchor = options.targetAnchor(this.linkIndex); sourceAnchor = options.sourceAnchor(this.linkIndex);
} }
return { if (options.targetAnchor && typeof options.targetAnchor === 'function' && this.linkIndex !== null) {
id: this.id, targetAnchor = options.targetAnchor(this.linkIndex);
type: options.type, }
source: this.node.id,
target: this.target.id, let label = this.target.sourceNode.freed ? 'freed' : '',
sourceAnchor, labelCfg = this.target.sourceNode.freed
targetAnchor, ? { position: 'start', autoRotate: true, refY: 7, style: { fontSize: 11, opacity: 0.8 } }
label: options.label, : Util.objectClone<LinkLabelOption>(options.labelOptions || ({} as LinkLabelOption));
style: Util.objectClone<Style>(options.style),
labelCfg: Util.objectClone<LinkLabelOption>(options.labelOptions), return {
curveOffset: options.curveOffset id: this.id,
}; type: options.type,
} source: this.node.id,
}; target: this.target.id,
sourceAnchor,
targetAnchor,
label,
style: Util.objectClone<Style>(options.style),
labelCfg,
curveOffset: options.curveOffset,
};
}
triggerHighlight(changeHighlightColor: string) {
this.originStyle = Util.objectClone(this.G6ModelProps.style);
this.set('style', {
stroke: changeHighlightColor,
});
}
isEqual(link: SVLink): boolean {
return link.targetId === this.targetId && link.nodeId === this.nodeId && link.linkIndex === this.linkIndex;
}
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,220 +1,223 @@
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"; import { Graph } from '@antv/g6-pc';
import merge from 'merge'; import merge from 'merge';
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 originStyle: 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;
}
triggerHighlight(changeHighlightColor: string) {
this.originStyle = Util.objectClone(this.G6ModelProps.style);
this.set('style', {
fill: changeHighlightColor,
});
}
restoreHighlight() {
if (this.originStyle) {
this.set('style', { ...this.originStyle });
}
}
/**
* modelSVNode
*/
isNode(): boolean {
return false;
}
isEqual(model: SVModel): boolean {
return this.id === model.id;
}
beforeRender () {}
afterRender() {}
beforeDestroy () {}
afterDestroy() {
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

@ -1,101 +1,119 @@
import { INode, NodeConfig } from "@antv/g6-core"; import { INode, NodeConfig } from '@antv/g6-core';
import { Util } from "../Common/util"; import { Util } from '../Common/util';
import { NodeLabelOption, NodeOption, Style } from "../options"; import { NodeLabelOption, NodeOption, Style } from '../options';
import { SourceNode } from "../sources"; import { SourceNode } from '../sources';
import { SVLink } from "./SVLink"; import { ModelConstructor } from './modelConstructor';
import { SVModel } from "./SVModel"; import { SVLink } from './SVLink';
import { SVAddressLabel, SVFreedLabel, SVIndexLabel, SVMarker, SVNodeAppendage } from "./SVNodeAppendage"; import { SVModel } from './SVModel';
import { SVNodeAppendage } from './SVNodeAppendage';
export class SVNode extends SVModel { export class SVNode extends SVModel {
public sourceId: string; public sourceId: string;
public sourceNode: SourceNode; public sourceNode: SourceNode;
public links: { public links: {
inDegree: SVLink[]; inDegree: SVLink[];
outDegree: SVLink[]; outDegree: SVLink[];
}; };
private label: string | string[]; private label: string | string[];
private disable: boolean; private disable: boolean;
public appendages: { [key: string]: SVNodeAppendage[] };
public shadowG6Item: INode; constructor(
public G6Item: INode; id: string,
type: string,
group: string,
layout: string,
sourceNode: SourceNode,
label: string | string[],
options: NodeOption
) {
super(id, type, group, layout, 'node');
public marker: SVMarker; this.group = group;
public freedLabel: SVFreedLabel; this.layout = layout;
public indexLabel: SVIndexLabel;
public addressLabel: SVAddressLabel;
public appendages: SVNodeAppendage[];
constructor(id: string, type: string, group: string, layout: string, sourceNode: SourceNode, label: string | string[], options: NodeOption) { Object.keys(sourceNode).map(prop => {
super(id, type, group, layout, 'node'); if (prop !== 'id') {
this[prop] = sourceNode[prop];
}
});
this.group = group; this.sourceNode = sourceNode;
this.layout = layout; this.sourceId = sourceNode.id.toString();
this.links = { inDegree: [], outDegree: [] };
this.appendages = {};
this.sourceNode = sourceNode;
this.label = label;
this.G6ModelProps = this.generateG6ModelProps(options);
}
Object.keys(sourceNode).map(prop => { generateG6ModelProps(options: NodeOption): NodeConfig {
if (prop !== 'id') { const style = Util.objectClone<Style>(options.style);
this[prop] = sourceNode[prop];
} return {
...this.sourceNode,
id: this.id,
x: 0,
y: 0,
rotation: options.rotation || 0,
type: options.type,
size: options.size || [60, 30],
anchorPoints: options.anchorPoints,
label: this.label as string,
style: {
...style,
},
labelCfg: Util.objectClone<NodeLabelOption>(options.labelOptions),
};
}
isNode(): boolean {
return true;
}
/**
*
* @param isSelected
*/
setSelectedState(isSelected: boolean) {
if (this.G6Item === null) {
return;
}
this.G6Item.setState('selected', isSelected);
this.getAppendagesList().forEach(item => {
item.setSelectedState(isSelected);
});
}
getSourceId(): string {
return this.sourceId;
}
getAppendagesList(): SVNodeAppendage[] {
const list = [];
Object.entries(this.appendages).forEach(item => {
list.push(...item[1]);
}); });
this.sourceNode = sourceNode; return list;
this.sourceId = sourceNode.id.toString();
this.links = { inDegree: [], outDegree: [] };
this.appendages = [];
this.sourceNode = sourceNode;
this.label = label;
this.G6ModelProps = this.generateG6ModelProps(options);
} }
generateG6ModelProps(options: NodeOption): NodeConfig { /**
const style = Util.objectClone<Style>(options.style); * group
* @param node
return { */
...this.sourceNode, isSameGroup(node: SVNode): boolean {
id: this.id, return ModelConstructor.isSameGroup(this, node);
x: 0,
y: 0,
rotation: options.rotation || 0,
type: options.type,
size: options.size || [60, 30],
anchorPoints: options.anchorPoints,
label: this.label as string,
style: {
...style,
fill: this.disable ? '#ccc' : style.fill
},
labelCfg: Util.objectClone<NodeLabelOption>(options.labelOptions)
};
} }
isNode(): boolean { beforeDestroy(): void {
return true; this.sourceNode = null;
this.links.inDegree.length = 0;
this.links.outDegree.length = 0;
this.appendages = {};
} }
}
/**
*
* @param isSelected
*/
setSelectedState(isSelected: boolean) {
if (this.G6Item === null) {
return;
}
this.G6Item.setState('selected', isSelected);
this.appendages.forEach(item => {
item.setSelectedState(isSelected);
});
}
getSourceId(): string {
return this.sourceId;
}
};

View File

@ -1,182 +1,206 @@
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 targetId: string;
public target: SVNode;
public label: string;
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, label: string | string[]) {
super(id, type, group, layout, modelType); super(id, type, group, layout, modelType);
this.target = target; this.target = target;
this.target.appendages.push(this); this.targetId = target.id;
} this.label = typeof label === 'string' ? label : label.join(', ');
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;
}
isEqual(appendage: SVNodeAppendage): boolean {
return appendage.targetId === this.targetId && appendage.label === this.label;
}
} }
/** /**
* *
*/ */
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: this.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; constructor(id: string, type: string, group: string, layout: string, target: SVNode, options: AddressLabelOption) {
super(id, type, group, layout, 'addressLabel', target, target.sourceId);
this.G6ModelProps = this.generateG6ModelProps(options);
}
constructor(id: string, type: string, group: string, layout: string, target: SVNode, options: AddressLabelOption) { getBound(): BoundingRect {
super(id, type, group, layout, 'addressLabel', target); const textBound = this.shadowG6Item.getContainer().getChildren()[1].getBBox(),
keyBound = this.shadowG6Item.getBBox();
return {
x: keyBound.x + textBound.x,
y: keyBound.y + textBound.y,
width: textBound.width,
height: textBound.height,
};
}
this.sourceId = target.sourceId; generateG6ModelProps(options: AddressLabelOption) {
this.target.addressLabel = this; return {
this.G6ModelProps = this.generateG6ModelProps(options); id: this.id,
} x: 0,
y: 0,
generateG6ModelProps(options: AddressLabelOption) { type: 'rect',
return { label: this.label,
id: this.id, labelCfg: {
x: 0, style: {
y: 0, fill: '#666',
type: 'rect', fontSize: 16,
label: this.sourceId, ...options.style,
labelCfg: { },
style: { },
fill: '#666', size: [0, 0],
fontSize: 16, style: {
...options.style stroke: null,
} fill: 'transparent',
}, },
size: [0, 0], };
style: { }
stroke: null,
fill: 'transparent'
}
};
}
} }
/** /**
* *
*/ */
export class SVIndexLabel extends SVNodeAppendage { export class SVIndexLabel extends SVNodeAppendage {
private value: string; constructor(
id: string,
indexName: string,
group: string,
layout: string,
value: string,
target: SVNode,
options: IndexLabelOption
) {
super(id, indexName, group, layout, 'indexLabel', target, value);
this.G6ModelProps = this.generateG6ModelProps(options) as NodeConfig;
}
constructor(id: string, indexName: string, group: string, layout: string, value: string, target: SVNode, options: IndexLabelOption) { generateG6ModelProps(options: IndexLabelOption): NodeConfig | EdgeConfig {
super(id, indexName, group, layout, 'indexLabel', target); return {
id: this.id,
this.target.indexLabel = this; x: 0,
this.value = value; y: 0,
this.G6ModelProps = this.generateG6ModelProps(options) as NodeConfig; type: 'rect',
} label: this.label,
labelCfg: {
generateG6ModelProps(options: IndexLabelOption): NodeConfig | EdgeConfig { style: {
return { fill: '#bbb',
id: this.id, textAlign: 'center',
x: 0, textBaseline: 'middle',
y: 0, fontSize: 14,
type: 'rect', fontStyle: 'italic',
label: this.value, ...options.style,
labelCfg: { },
style: { },
fill: '#bbb', size: [0, 0],
textAlign: 'center', style: {
textBaseline: 'middle', stroke: null,
fontSize: 14, fill: 'transparent',
fontStyle: 'italic', },
...options.style };
} }
},
size: [0, 0],
style: {
stroke: null,
fill: 'transparent'
}
};
}
} }
/** /**
* *
*/ */
export class SVMarker extends SVNodeAppendage { export class SVMarker extends SVNodeAppendage {
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, label);
this.G6ModelProps = this.generateG6ModelProps(options);
}
this.label = label; generateG6ModelProps(options: MarkerOption): NodeConfig {
this.anchor = options.anchor;
this.target.marker = this; const type = options.type,
this.G6ModelProps = this.generateG6ModelProps(options); defaultSize: [number, number] = type === 'pointer' ? [8, 30] : [12, 12];
}
generateG6ModelProps(options: MarkerOption): NodeConfig { return {
this.anchor = options.anchor; id: this.id,
x: 0,
y: 0,
rotation: 0,
type: options.type || 'marker',
size: options.size || defaultSize,
anchorPoints: null,
label: this.label,
style: Util.objectClone<Style>(options.style),
labelCfg: Util.objectClone<NodeLabelOption>(options.labelOptions),
};
}
const type = options.type, public getLabelSizeRadius(): number {
defaultSize: [number, number] = type === 'pointer' ? [8, 30] : [12, 12]; const { width, height } = this.shadowG6Item.getContainer().getChildren()[2].getBBox();
return Math.max(width, height);
return { }
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)
};
}
public getLabelSizeRadius(): number {
const { width, height } = this.shadowG6Item.getContainer().getChildren()[2].getBBox();
return Math.max(width, height);
}
};

View File

@ -1,440 +1,590 @@
import { Util } from "../Common/util"; import { Group } from '../Common/group';
import { Engine } from "../engine"; import { Util } from '../Common/util';
import { AddressLabelOption, IndexLabelOption, LayoutCreator, LayoutGroupOptions, LinkOption, MarkerOption, NodeOption } from "../options"; import { Engine } from '../engine';
import { sourceLinkData, LinkTarget, Sources, SourceNode } from "../sources"; import {
import { SV } from "../StructV"; AddressLabelOption,
import { SVLink } from "./SVLink"; IndexLabelOption,
import { SVModel } from "./SVModel"; LayoutCreator,
import { SVNode } from "./SVNode"; LayoutGroupOptions,
import { SVAddressLabel, SVFreedLabel, SVIndexLabel, SVMarker } from "./SVNodeAppendage"; LinkOption,
MarkerOption,
NodeOption,
} from '../options';
import { sourceLinkData, LinkTarget, Sources, SourceNode } from '../sources';
import { SV } from '../StructV';
import { SVLink } from './SVLink';
import { SVModel } from './SVModel';
import { SVNode } from './SVNode';
import { SVAddressLabel, SVFreedLabel, SVIndexLabel, SVMarker, SVNodeAppendage } from './SVNodeAppendage';
export type LayoutGroup = { export type LayoutGroup = {
name: string; name: string;
node: SVNode[]; node: SVNode[];
indexLabel: SVIndexLabel[]; appendage: SVNodeAppendage[];
freedLabel: SVFreedLabel[]; link: SVLink[];
addressLabel: SVAddressLabel[]; layoutCreator: LayoutCreator;
link: SVLink[]; layout: string;
marker: SVMarker[]; options: LayoutGroupOptions;
layoutCreator: LayoutCreator; modelList: SVModel[];
layout: string; isHide: boolean;
options: LayoutGroupOptions;
modelList: SVModel[];
isHide: boolean;
}; };
export type LayoutGroupTable = Map<string, LayoutGroup>; export type LayoutGroupTable = Map<string, LayoutGroup>;
export class ModelConstructor { export class ModelConstructor {
private engine: Engine; private engine: Engine;
private layoutGroupTable: LayoutGroupTable; private layoutGroupTable: LayoutGroupTable;
private prevSourcesStringMap: { [key: string]: string }; // 保存上一次源数据转换为字符串之后的值,用作比较该次源数据和上一次源数据是否有差异,若相同,则可跳过重复构建过程 private prevSourcesStringMap: { [key: string]: string }; // 保存上一次源数据转换为字符串之后的值,用作比较该次源数据和上一次源数据是否有差异,若相同,则可跳过重复构建过程
constructor(engine: Engine) { constructor(engine: Engine) {
this.engine = engine; this.engine = engine;
this.prevSourcesStringMap = {}; this.prevSourcesStringMap = {};
} }
/** /**
* SVNodeSVLink, SVMarker, SVAddressLabel, SVIndexLabel等 * SVNodeSVLink, SVMarker, SVAddressLabel, SVIndexLabel等
* @param sourceList * @param sourceList
*/ */
public construct(sources: Sources): LayoutGroupTable { public construct(sources: Sources): LayoutGroupTable {
const layoutGroupTable = new Map<string, LayoutGroup>(), const layoutGroupTable = new Map<string, LayoutGroup>(),
layoutMap: { [key: string]: LayoutCreator } = SV.registeredLayout; layoutMap: { [key: string]: LayoutCreator } = SV.registeredLayout;
Object.keys(sources).forEach(group => { Object.keys(sources).forEach(group => {
let sourceGroup = sources[group], let sourceGroup = sources[group],
layout = sourceGroup.layouter, layout = sourceGroup.layouter,
layoutCreator: LayoutCreator = layoutMap[layout]; layoutCreator: LayoutCreator = layoutMap[layout];
if (!layout || !layoutCreator) { if (!layout || !layoutCreator) {
return; return;
} }
let sourceDataString: string = JSON.stringify(sourceGroup.data), let sourceDataString: string = JSON.stringify(sourceGroup.data),
prevString: string = this.prevSourcesStringMap[group], prevString: string = this.prevSourcesStringMap[group],
nodeList: SVNode[] = [], nodeList: SVNode[] = [],
freedLabelList: SVFreedLabel[] = [], appendageList: SVNodeAppendage[] = [];
addressLabelList: SVAddressLabel[] = [],
indexLabelList: SVIndexLabel[] = [],
markerList: SVMarker[] = [];
if (prevString === sourceDataString) { if (prevString === sourceDataString) {
return; return;
} }
const options: LayoutGroupOptions = layoutCreator.defineOptions(sourceGroup.data), const options: LayoutGroupOptions = layoutCreator.defineOptions(sourceGroup.data),
sourceData = layoutCreator.sourcesPreprocess(sourceGroup.data, options), sourceData = layoutCreator.sourcesPreprocess(sourceGroup.data, options),
nodeOptions = options.node || options['element'] || {}, nodeOptions = options.node || options['element'] || {},
markerOptions = options.marker || {}, markerOptions = options.marker || {},
indexLabelOptions = options.indexLabel || {}, indexLabelOptions = options.indexLabel || {},
addressLabelOption = options.addressLabel || {}; addressLabelOption = options.addressLabel || {};
nodeList = this.constructNodes(group, layout, nodeOptions, sourceData); nodeList = this.constructNodes(group, layout, nodeOptions, sourceData);
markerList = this.constructMarkers(group, layout, markerOptions, nodeList); appendageList.push(...this.constructMarkers(group, layout, markerOptions, nodeList));
indexLabelList = this.constructIndexLabel(group, layout, indexLabelOptions, nodeList); appendageList.push(...this.constructIndexLabel(group, layout, indexLabelOptions, nodeList));
addressLabelList = this.constructAddressLabel(group, layout, addressLabelOption, nodeList); appendageList.push(...this.constructAddressLabel(group, layout, addressLabelOption, nodeList));
nodeList.forEach(item => { nodeList.forEach(item => {
if(item.freedLabel) { if (item.appendages.freedLabel) {
freedLabelList.push(item.freedLabel); appendageList.push(...item.appendages.freedLabel);
} }
}); });
layoutGroupTable.set(group, { layoutGroupTable.set(group, {
name: group, name: group,
node: nodeList, node: nodeList,
freedLabel: freedLabelList, appendage: appendageList,
addressLabel: addressLabelList, link: [],
indexLabel: indexLabelList, options,
link: [], layoutCreator,
marker: markerList, modelList: [...nodeList, ...appendageList],
options, layout,
layoutCreator, isHide: false,
modelList: [ });
...nodeList, });
...markerList,
...freedLabelList,
...addressLabelList,
...indexLabelList
],
layout,
isHide: false
});
});
layoutGroupTable.forEach((layoutGroup: LayoutGroup, group: string) => { layoutGroupTable.forEach((layoutGroup: LayoutGroup, group: string) => {
const linkOptions = layoutGroup.options.link || {}, const linkOptions = layoutGroup.options.link || {},
linkList: SVLink[] = this.constructLinks(group, layoutGroup.layout, linkOptions, layoutGroup.node, layoutGroupTable); linkList: SVLink[] = this.constructLinks(
group,
layoutGroup.layout,
linkOptions,
layoutGroup.node,
layoutGroupTable
);
layoutGroup.link = linkList; layoutGroup.link = linkList;
layoutGroup.modelList.push(...linkList); layoutGroup.modelList.push(...linkList);
}); });
this.layoutGroupTable = layoutGroupTable; this.layoutGroupTable = layoutGroupTable;
return this.layoutGroupTable; return this.layoutGroupTable;
} }
/**
* node
* @param nodeOptions
* @param group
* @param sourceList
* @param layout
* @returns
*/
private constructNodes(
group: string,
layout: string,
nodeOptions: { [key: string]: NodeOption },
sourceList: SourceNode[]
): SVNode[] {
let defaultSourceNodeType: string = 'default',
nodeList: SVNode[] = [];
/** sourceList.forEach(item => {
* node if (item === null) {
* @param nodeOptions return;
* @param group }
* @param sourceList
* @param layout
* @returns
*/
private constructNodes(group: string, layout: string, nodeOptions: { [key: string]: NodeOption }, sourceList: SourceNode[]): SVNode[] {
let defaultSourceNodeType: string = 'default',
nodeList: SVNode[] = [];
sourceList.forEach(item => { if (item.type === undefined || item.type === null) {
if (item === null) { item.type = defaultSourceNodeType;
return; }
}
if (item.type === undefined || item.type === null) { nodeList.push(this.createNode(item, item.type, group, layout, nodeOptions[item.type]));
item.type = defaultSourceNodeType; });
}
nodeList.push(this.createNode(item, item.type, group, layout, nodeOptions[item.type])); return nodeList;
}); }
return nodeList; /**
} * node link
* @param linkOptions
* @param nodes
* @param layoutGroupTable
* @returns
*/
private constructLinks(
group: string,
layout: string,
linkOptions: { [key: string]: LinkOption },
nodes: SVNode[],
layoutGroupTable: LayoutGroupTable
): SVLink[] {
let linkList: SVLink[] = [],
linkNames = Object.keys(linkOptions);
/** linkNames.forEach(name => {
* node link for (let i = 0; i < nodes.length; i++) {
* @param linkOptions let node: SVNode = nodes[i],
* @param nodes sourceLinkData: sourceLinkData = node.sourceNode[name],
* @param layoutGroupTable targetNode: SVNode | SVNode[] = null,
* @returns link: SVLink = null;
*/
private constructLinks(group: string, layout: string, linkOptions: { [key: string]: LinkOption }, nodes: SVNode[], layoutGroupTable: LayoutGroupTable): SVLink[] {
let linkList: SVLink[] = [],
linkNames = Object.keys(linkOptions);
linkNames.forEach(name => { if (sourceLinkData === undefined || sourceLinkData === null) {
for (let i = 0; i < nodes.length; i++) { node[name] = null;
let node: SVNode = nodes[i], continue;
sourceLinkData: sourceLinkData = node.sourceNode[name], }
targetNode: SVNode | SVNode[] = null,
link: SVLink = null;
if (sourceLinkData === undefined || sourceLinkData === null) { // ------------------- 将连接声明字段 sourceLinkData 从 id 变为 SVNode -------------------
node[name] = null; if (Array.isArray(sourceLinkData)) {
continue; node[name] = sourceLinkData.map((item, index) => {
} targetNode = this.fetchTargetNodes(layoutGroupTable, node, item);
let isGeneralLink = ModelConstructor.isGeneralLink(sourceLinkData.toString());
// ------------------- 将连接声明字段 sourceLinkData 从 id 变为 SVNode ------------------- if (targetNode) {
if (Array.isArray(sourceLinkData)) { link = this.createLink(name, group, layout, node, targetNode, index, linkOptions[name]);
node[name] = sourceLinkData.map((item, index) => { linkList.push(link);
targetNode = this.fetchTargetNodes(layoutGroupTable, node, item); }
let isGeneralLink = this.isGeneralLink(sourceLinkData.toString());
if (targetNode) { return isGeneralLink ? targetNode : null;
link = this.createLink(name, group, layout, node, targetNode, index, linkOptions[name]); });
linkList.push(link); } else {
} targetNode = this.fetchTargetNodes(layoutGroupTable, node, sourceLinkData);
let isGeneralLink = ModelConstructor.isGeneralLink(sourceLinkData.toString());
return isGeneralLink ? targetNode : null; if (targetNode) {
}); link = this.createLink(name, group, layout, node, targetNode, 0, linkOptions[name]);
} linkList.push(link);
else { }
targetNode = this.fetchTargetNodes(layoutGroupTable, node, sourceLinkData);
let isGeneralLink = this.isGeneralLink(sourceLinkData.toString());
if (targetNode) { node[name] = isGeneralLink ? targetNode : null;
link = this.createLink(name, group, layout, node, targetNode, null, linkOptions[name]); }
linkList.push(link); }
} });
node[name] = isGeneralLink ? targetNode : null; return linkList;
} }
}
});
return linkList; /**
} * indexLabel
* @param group
* @param layout
* @param indexLabelOptions
*/
private constructIndexLabel(
group: string,
layout: string,
indexLabelOptions: { [key: string]: IndexLabelOption },
nodes: SVNode[]
): SVIndexLabel[] {
let indexLabelList: SVIndexLabel[] = [],
indexNames = Object.keys(indexLabelOptions);
/** indexNames.forEach(name => {
* indexLabel for (let i = 0; i < nodes.length; i++) {
* @param group let node = nodes[i],
* @param layout value = node[name];
* @param indexLabelOptions
*/
private constructIndexLabel(group: string, layout: string, indexLabelOptions: { [key: string]: IndexLabelOption }, nodes: SVNode[]): SVIndexLabel[] {
let indexLabelList: SVIndexLabel[] = [],
indexNames = Object.keys(indexLabelOptions);
indexNames.forEach(name => { // 若没有指针字段的结点则跳过
for (let i = 0; i < nodes.length; i++) { if (value === undefined || value === null) continue;
let node = nodes[i],
value = node[name];
// 若没有指针字段的结点则跳过 let id = `${group}[${name}(${value})]`,
if (value === undefined || value === null) continue; indexLabel = new SVIndexLabel(
id,
name,
group,
layout,
value.toString(),
node,
indexLabelOptions[name]
);
let id = `${group}.${name}#${value}`, indexLabelList.push(indexLabel);
indexLabel = new SVIndexLabel(id, name, group, layout, value.toString(), node, indexLabelOptions[name]); }
});
indexLabelList.push(indexLabel); return indexLabelList;
} }
});
return indexLabelList; /**
} *
* @param group
* @param layout
* @param addressLabelOption
* @param nodes
*/
private constructAddressLabel(
group: string,
layout: string,
addressLabelOption: AddressLabelOption,
nodes: SVNode[]
): SVAddressLabel[] {
let addressLabelList: SVAddressLabel[] = [];
/** nodes.forEach(item => {
* const addressLabel = new SVAddressLabel(
* @param group `address-label(${item.id})`,
* @param layout item.sourceType,
* @param addressLabelOption group,
* @param nodes layout,
*/ item,
private constructAddressLabel(group: string, layout: string, addressLabelOption: AddressLabelOption, nodes: SVNode[]): SVAddressLabel[] { addressLabelOption
let addressLabelList: SVAddressLabel[] = []; );
addressLabelList.push(addressLabel);
});
nodes.forEach(item => { return addressLabelList;
const addressLabel = new SVAddressLabel(`${item.id}-address-label`, item.sourceType, group, layout, item, addressLabelOption); }
addressLabelList.push(addressLabel);
}); /**
* node marker
* @param markerOptions
* @param nodes
* @returns
*/
private constructMarkers(
group: string,
layout: string,
markerOptions: { [key: string]: MarkerOption },
nodes: SVNode[]
): SVMarker[] {
let markerList: SVMarker[] = [],
markerNames = Object.keys(markerOptions);
markerNames.forEach(name => {
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i],
markerData = node[name];
// 若没有指针字段的结点则跳过
if (!markerData) continue;
let id = `[${name}(${Array.isArray(markerData) ? markerData.join('-') : markerData})]`,
return addressLabelList; marker = new SVMarker(id, name, group, layout, markerData, node, markerOptions[name]);
markerList.push(marker);
}
});
return markerList;
}
/**
* label文本
* @param label
* @param sourceNode
*/
private resolveNodeLabel(label: string | string[], sourceNode: SourceNode): string {
let targetLabel: any = '';
if (Array.isArray(label)) {
targetLabel = label.map(item => this.parserNodeContent(sourceNode, item) ?? '');
} else {
targetLabel = this.parserNodeContent(sourceNode, label);
}
if (targetLabel === 'undefined') {
targetLabel = '';
}
return targetLabel ?? '';
}
/**
* Node
* @param sourceNode
* @param sourceNodeType
* @param group
* @param layout
* @param options
*/
private createNode(
sourceNode: SourceNode,
sourceNodeType: string,
group: string,
layout: string,
options: NodeOption
): SVNode {
let label: string | string[] = this.resolveNodeLabel(options.label, sourceNode),
id = `${sourceNodeType}(${sourceNode.id.toString()})`,
node = new SVNode(id, sourceNodeType, group, layout, sourceNode, label, options);
if (node.freed) {
new SVFreedLabel(`freed-label(${id})`, sourceNodeType, group, layout, node);
}
return node;
}
/**
* 线Link
* @param linkName
* @param group
* @param layout
* @param node
* @param target
* @param index
* @param options
* @returns
*/
private createLink(
linkName: string,
group: string,
layout: string,
node: SVNode,
target: SVNode,
index: number,
options: LinkOption
): SVLink {
let id = `${linkName}{${node.id}-${target.id}}#${index}`;
return new SVLink(id, linkName, group, layout, node, target, index, options);
}
/**
*
* @param sourceNode
* @param formatLabel
*/
private parserNodeContent(sourceNode: SourceNode, formatLabel: string): string {
let fields = Util.textParser(formatLabel);
if (Array.isArray(fields)) {
let values = fields.map(item => sourceNode[item]);
values.map((item, index) => {
formatLabel = formatLabel.replace('[' + fields[index] + ']', item);
});
}
return formatLabel;
}
/**
* source中的连接字段获取真实的连接目标元素
* @param nodeContainer
* @param node
* @param linkTarget
*/
private fetchTargetNodes(layoutGroupTable: LayoutGroupTable, node: SVNode, linkTarget: LinkTarget): SVNode {
let group: string = node.group,
sourceNodeType = node.sourceType,
nodeList: SVNode[],
targetId = linkTarget,
targetGroupName = group,
targetNode = null;
if (linkTarget === null || linkTarget === undefined) {
return null;
}
if (typeof linkTarget === 'number' || (typeof linkTarget === 'string' && !linkTarget.includes('#'))) {
linkTarget = 'default#' + linkTarget;
}
let info = linkTarget.split('#');
targetId = info.pop();
if (info.length > 1) {
sourceNodeType = info.pop();
targetGroupName = info.pop();
} else {
let field = info.pop();
if (layoutGroupTable.get(targetGroupName).node.find(item => item.sourceType === field)) {
sourceNodeType = field;
} else if (layoutGroupTable.has(field)) {
targetGroupName = field;
} else {
return null;
}
}
// 为了可以连接到不同group的结点
for (let layoutGroup of layoutGroupTable.values()) {
nodeList = layoutGroup.node.filter(item => item.sourceType === sourceNodeType);
if (nodeList === undefined) {
continue;
}
targetNode = nodeList.find(item => item.sourceId === targetId);
if (targetNode) {
break;
}
} }
// nodeList = layoutGroupTable.get(targetGroupName).node.filter(item => item.sourceType === sourceNodeType);
// // 若目标node不存在返回null
// if (nodeList === undefined) {
// return null;
// }
// targetNode = nodeList.find(item => item.sourceId === targetId);
return targetNode || null;
}
/**
*
* @returns
*/
public getLayoutGroupTable(): LayoutGroupTable {
return this.layoutGroupTable;
}
/**
*
*/
public destroy() {
this.layoutGroupTable = null;
this.prevSourcesStringMap = null;
}
// -------------------------------------------------------------------------------------------------
/**
* group
* @param linkId
*/
static isGeneralLink(linkId: string): boolean {
let counter = 0;
for (let i = 0; i < linkId.length; i++) {
if (linkId[i] === '#') {
counter++;
}
}
return counter <= 2;
}
/**
* group
* @param node1
* @param node2
*/
static isSameGroup(node1: SVNode, node2: SVNode): boolean {
return node1.group === node2.group;
}
/** /**
* node marker *
* @param markerOptions * - appendages
* @param nodes * @param models
* @returns * @returns
*/ */
private constructMarkers(group: string, layout: string, markerOptions: { [key: string]: MarkerOption }, nodes: SVNode[]): SVMarker[] { static getClusters(models: SVModel[]): Group[] {
let markerList: SVMarker[] = [], const clusterGroupList = [],
markerNames = Object.keys(markerOptions); idMap = {},
idName = '__clusterId';
markerNames.forEach(name => { models.forEach(item => {
for (let i = 0; i < nodes.length; i++) { idMap[item.id] = item;
let node = nodes[i], });
markerData = node[name];
// 若没有指针字段的结点则跳过 const DFS = (model: SVModel, clusterId: number, idMap): SVModel[] => {
if (!markerData) continue; if(model === null) {
return [];
let id = `${group}.${name}.${Array.isArray(markerData) ? markerData.join('-') : markerData}`,
marker = new SVMarker(id, name, group, layout, markerData, node, markerOptions[name]);
markerList.push(marker);
} }
});
return markerList; if (idMap[model.id] === undefined) {
} return [];
}
/** if (model[idName] !== undefined) {
* label文本 return [];
* @param label }
* @param sourceNode
*/
private resolveNodeLabel(label: string | string[], sourceNode: SourceNode): string {
let targetLabel: any = '';
if (Array.isArray(label)) { const list = [model];
targetLabel = label.map(item => this.parserNodeContent(sourceNode, item) ?? ''); model[idName] = clusterId;
}
else {
targetLabel = this.parserNodeContent(sourceNode, label);
}
if (targetLabel === 'undefined') { if (model instanceof SVNode) {
targetLabel = ''; model.getAppendagesList().forEach(item => {
} list.push(...DFS(item, clusterId, idMap));
});
return targetLabel ?? ''; model.links.inDegree.forEach(item => {
} list.push(...DFS(item, clusterId, idMap));
});
/** model.links.outDegree.forEach(item => {
* Node list.push(...DFS(item, clusterId, idMap));
* @param sourceNode });
* @param sourceNodeType }
* @param group
* @param layout
* @param options
*/
private createNode(sourceNode: SourceNode, sourceNodeType: string, group: string, layout: string, options: NodeOption): SVNode {
let label: string | string[] = this.resolveNodeLabel(options.label, sourceNode),
id = sourceNodeType + '.' + sourceNode.id.toString(),
node = new SVNode(id, sourceNodeType, group, layout, sourceNode, label, options);
if(node.freed) { if (model instanceof SVLink) {
node.freedLabel = new SVFreedLabel(`${id}-freed-label`, sourceNodeType, group, layout, node); list.push(...DFS(model.node, clusterId, idMap));
} list.push(...DFS(model.target, clusterId, idMap));
}
return node; if (model instanceof SVNodeAppendage) {
} list.push(...DFS(model.target, clusterId, idMap));
}
/** return list;
* 线Link };
* @param linkName
* @param group
* @param layout
* @param node
* @param target
* @param index
* @param options
* @returns
*/
private createLink(linkName: string, group: string, layout: string, node: SVNode, target: SVNode, index: number, options: LinkOption): SVLink {
let id = `${linkName}(${node.id}-${target.id})`;
return new SVLink(id, linkName, group, layout, node, target, index, options);
}
/** for (let i = 0; i < models.length; i++) {
* const model = models[i];
* @param sourceNode
* @param formatLabel
*/
private parserNodeContent(sourceNode: SourceNode, formatLabel: string): string {
let fields = Util.textParser(formatLabel);
if (Array.isArray(fields)) { if (model[idName] !== undefined) {
let values = fields.map(item => sourceNode[item]); delete model[idName];
continue;
}
values.map((item, index) => { const group = new Group(),
formatLabel = formatLabel.replace('[' + fields[index] + ']', item); clusterList = DFS(model, i, idMap);
});
}
return formatLabel; clusterList.forEach(item => {
} group.add(item);
});
clusterGroupList.push(group);
delete model[idName];
}
/** return clusterGroupList;
* source中的连接字段获取真实的连接目标元素 }
* @param nodeContainer }
* @param node
* @param linkTarget
*/
private fetchTargetNodes(layoutGroupTable: LayoutGroupTable, node: SVNode, linkTarget: LinkTarget): SVNode {
let group: string = node.group,
sourceNodeType = node.sourceType,
nodeList: SVNode[],
targetId = linkTarget,
targetGroupName = group,
targetNode = null;
if (linkTarget === null || linkTarget === undefined) {
return null;
}
if (typeof linkTarget === 'number' || (typeof linkTarget === 'string' && !linkTarget.includes('#'))) {
linkTarget = 'default#' + linkTarget;
}
let info = linkTarget.split('#');
targetId = info.pop();
if (info.length > 1) {
sourceNodeType = info.pop();
targetGroupName = info.pop();
}
else {
let field = info.pop();
if (layoutGroupTable.get(targetGroupName).node.find(item => item.sourceType === field)) {
sourceNodeType = field;
}
else if (layoutGroupTable.has(field)) {
targetGroupName = field;
}
else {
return null;
}
}
nodeList = layoutGroupTable.get(targetGroupName).node.filter(item => item.sourceType === sourceNodeType);
// 若目标node不存在返回null
if (nodeList === undefined) {
return null;
}
targetNode = nodeList.find(item => item.sourceId === targetId);
return targetNode || null;
}
/**
* group
* @param linkId
*/
private isGeneralLink(linkId: string): boolean {
let counter = 0;
for (let i = 0; i < linkId.length; i++) {
if (linkId[i] === '#') {
counter++;
}
}
return counter <= 2;
}
/**
*
* @returns
*/
public getLayoutGroupTable(): LayoutGroupTable {
return this.layoutGroupTable;
}
/**
*
*/
destroy() {
this.layoutGroupTable = null;
this.prevSourcesStringMap = null;
}
};

View File

@ -1,67 +1,111 @@
import { Util } from '../Common/util'; import { Util } from '../Common/util';
export default Util.registerShape(
'binary-tree-node',
{
draw(cfg, group) {
cfg.size = cfg.size;
export default Util.registerShape('binary-tree-node', { const width = cfg.size[0],
draw(cfg, group) { height = cfg.size[1];
cfg.size = cfg.size;
const width = cfg.size[0], const wrapperRect = group.addShape('rect', {
height = cfg.size[1]; attrs: {
x: width / 2,
y: height / 2,
width: width,
height: height,
stroke: cfg.style.stroke || '#333',
cursor: cfg.style.cursor,
fill: cfg.style.backgroundFill || '#eee',
},
name: 'wrapper',
});
const wrapperRect = group.addShape('rect', { group.addShape('rect', {
attrs: { attrs: {
x: width / 2, x: width / 4 + width / 2,
y: height / 2, y: height / 2,
width: width, width: width / 2,
height: height, height: height,
stroke: cfg.style.stroke || '#333', fill: cfg.style.fill,
cursor: cfg.style.cursor, stroke: cfg.style.stroke || '#333',
fill: cfg.style.backgroundFill || '#eee' cursor: cfg.style.cursor,
}, },
name: 'wrapper' name: 'mid',
}); draggable: true,
});
group.addShape('rect', { const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
attrs: {
x: width / 4 + width / 2,
y: height / 2,
width: width / 2,
height: height,
fill: cfg.style.fill,
stroke: cfg.style.stroke || '#333',
cursor: cfg.style.cursor
},
name: 'mid',
draggable: true
});
if (cfg.label) { if (cfg.label) {
const style = (cfg.labelCfg && cfg.labelCfg.style) || {}; group.addShape('text', {
group.addShape('text', { attrs: {
attrs: { x: width, // 居中
x: width, // 居中 y: height,
y: height, textAlign: 'center',
textAlign: 'center', textBaseline: 'middle',
textBaseline: 'middle', text: cfg.label,
text: cfg.label, fill: style.fill || '#000',
fill: style.fill || '#000', fontSize: style.fontSize || 16,
fontSize: style.fontSize || 16, cursor: cfg.style.cursor,
cursor: cfg.style.cursor },
}, name: 'label',
name: 'text', draggable: true,
draggable: true });
}); }
}
return wrapperRect; const isLeftEmpty =
}, !cfg.child || cfg.child[0] === undefined || cfg.child[0] === undefined || cfg.child[0] == '0x0',
isRightEmpty =
!cfg.child || cfg.child[1] === undefined || cfg.child[1] === undefined || cfg.child[1] == '0x0';
getAnchorPoints() { //节点没有左孩子节点时
return [ if (isLeftEmpty) {
[0.5, 0], group.addShape('text', {
[0.875, 0.5], attrs: {
[0.5, 1], x: width * (5 / 8),
[0.125, 0.5] y: height * (8 / 7),
]; textAlign: 'center',
}, textBaseline: 'middle',
}, 'rect'); text: '^',
fill: style.fill || '#000',
fontSize: 16,
cursor: cfg.style.cursor,
},
name: 'null-left',
draggable: true,
});
}
//节点没有右孩子节点时
if (isRightEmpty) {
group.addShape('text', {
attrs: {
x: width * (11 / 8),
y: height * (8 / 7),
textAlign: 'center',
textBaseline: 'middle',
text: '^',
fill: style.fill || '#000',
fontSize: 16,
cursor: cfg.style.cursor,
},
name: 'null-right',
draggable: true,
});
}
return wrapperRect;
},
getAnchorPoints() {
return [
[0.5, 0],
[0.875, 0.5],
[0.5, 1],
[0.125, 0.5],
];
},
},
'rect'
);

View File

@ -0,0 +1,74 @@
import { Util } from '../Common/util';
export default Util.registerShape(
'chain-hashtable-node',
{
draw(cfg, group) {
cfg.size = cfg.size || [30, 30];
const width = cfg.size[0],
height = cfg.size[1];
const wrapperRect = group.addShape('rect', {
attrs: {
x: width / 2,
y: height / 2,
width: width,
height: height,
stroke: cfg.style.stroke,
fill: cfg.style.backgroundFill || '#eee'
},
name: 'wrapper',
draggable: true
});
const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
if (cfg.label) {
group.addShape('text', {
attrs: {
x: width,
y: height,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.label,
fill: style.fill || '#000',
fontSize: style.fontSize || 16
},
name: 'text',
draggable: true
});
}
//节点没有后续指针时
if (!cfg.next && !cfg.loopNext) {
group.addShape('text', {
attrs: {
x: width,
y: height,
textAlign: 'center',
textBaseline: 'middle',
text: '^',
fill: style.fill || '#000',
fontSize: 16,
cursor: cfg.style.cursor,
},
name: 'text',
draggable: true,
});
}
return wrapperRect;
},
getAnchorPoints() {
return [
[1 / 6, 0],
[0.5, 0],
[0.5, 0.5],
[5 / 6, 0.5],
[0.5, 1],
[0, 0.5]
];
}
}
)

View File

@ -1,120 +1,106 @@
import G6 from "@antv/g6"; import G6 from '@antv/g6';
import { Util } from "../Common/util"; import { Util } from '../Common/util';
export default function rotate(shape, angle, transform) { export default function rotate(shape, angle, transform) {
const matrix1 = shape.getMatrix(); const matrix1 = shape.getMatrix();
const newMatrix1 = transform(matrix1, [ const newMatrix1 = transform(matrix1, [['r', angle]]);
['r', angle], shape.setMatrix(newMatrix1);
]);
shape.setMatrix(newMatrix1);
} }
function translate(shape, x, y, transform) { function translate(shape, x, y, transform) {
const matrix1 = shape.getMatrix(); const matrix1 = shape.getMatrix();
const newMatrix1 = transform(matrix1, [ const newMatrix1 = transform(matrix1, [['t', x, y]]);
['t', x, y], shape.setMatrix(newMatrix1);
]);
shape.setMatrix(newMatrix1);
} }
function culcuRotate(angle, R) { function culcuRotate(angle, R) {
let offsetX = Math.cos(angle) * R; let offsetX = Math.cos(angle) * R;
let offsetY = -Math.sin(angle) * R; let offsetY = -Math.sin(angle) * R;
console.log(offsetX, offsetY, R); console.log(offsetX, offsetY, R);
return { return {
offsetX, offsetX,
offsetY, offsetY,
} };
} }
Util.registerShape('clen-queue-pointer', { Util.registerShape('clen-queue-pointer', {
draw(cfg, group) { draw(cfg, group) {
let id = cfg.id as string; let id = cfg.id as string;
const index = parseInt(id.split('-')[1]); const index = parseInt(id.split('-')[1]);
const len = parseInt(id.split('-')[2]); const len = parseInt(id.split('-')[2]);
const keyShape = group.addShape('path', { const keyShape = group.addShape('path', {
attrs: { attrs: {
x: 0, x: 0,
y: 0, y: 0,
path: this.getPath(cfg), path: this.getPath(cfg),
fill: cfg.style.fill, fill: cfg.style.fill,
// matrix: cfg.style.matrix // matrix: cfg.style.matrix
}, },
name: 'pointer-path' name: 'pointer-path',
}); });
const angle = index * Math.PI * 2 / len; const angle = (index * Math.PI * 2) / len;
if (cfg.label) { if (cfg.label) {
const style = (cfg.labelCfg && cfg.labelCfg.style) || {}; const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
const bgRect = group.addShape('rect', {
attrs: {
x: 0,
y: 0,
width: 0,
height: 0,
text: cfg.label,
fill: null,
radius: 2
},
name: 'bgRect'
});
let label = cfg.label as string; const bgRect = group.addShape('rect', {
attrs: {
x: 0,
y: 0,
width: 0,
height: 0,
text: cfg.label,
fill: null,
radius: 2,
},
name: 'bgRect',
});
let pointerText = label.split('-')[0]; let label = cfg.label as string;
let y = pointerText=="front"?30:15;
const text = group.addShape('text', {
attrs: {
x: culcuRotate(Math.PI/2 - angle, y).offsetX,
y: culcuRotate(Math.PI/2 - angle, y).offsetY,
// x: 0,
// y: 0,
textAlign: 'center',
textBaseline: 'middle',
text: pointerText,
fill: style.fill || '#999',
fontSize: style.fontSize || 16
},
name: 'pointer-text-shape'
});
// rotate(text, angle, G6.Util.transform); let pointerText = label.split('-')[0];
translate(text, 0, -75, G6.Util.transform); let y = pointerText == 'front' ? 30 : 15;
} const text = group.addShape('text', {
rotate(keyShape, angle, G6.Util.transform); attrs: {
translate(keyShape, 0, -75, G6.Util.transform); x: culcuRotate(Math.PI / 2 - angle, y).offsetX,
y: culcuRotate(Math.PI / 2 - angle, y).offsetY,
// x: 0,
// y: 0,
textAlign: 'center',
textBaseline: 'middle',
text: pointerText,
fill: style.fill || '#999',
fontSize: style.fontSize || 16,
},
name: 'pointer-text-shape',
});
return keyShape; // rotate(text, angle, G6.Util.transform);
translate(text, 0, -75, G6.Util.transform);
}
rotate(keyShape, angle, G6.Util.transform);
translate(keyShape, 0, -75, G6.Util.transform);
return keyShape;
}, },
getPath(cfg) {
let width = 1,
height = 38,
arrowWidth = width + 4,
arrowHeight = height * 0.3;
const path = [ getPath(cfg) {
['M', 0, 0], let width = 1,
['L', -width / 2, 0], height = 38,
['L', -width / 2, -height], arrowWidth = width + 4,
['L', -width / 2 - (arrowWidth / 2), -height], arrowHeight = height * 0.3;
['L', 0, -height-arrowHeight],
['L', width / 2 + (arrowWidth / 2), -height],
['L', width / 2, -height],
['L', width / 2, 0],
['Z'],
];
return path; const path = [
}, ['M', 0, 0],
['L', -width / 2, 0],
['L', -width / 2, -height],
['L', -width / 2 - arrowWidth / 2, -height],
['L', 0, -height - arrowHeight],
['L', width / 2 + arrowWidth / 2, -height],
['L', width / 2, -height],
['L', width / 2, 0],
['Z'],
];
return path;
},
}); });

View File

@ -1,71 +1,100 @@
import { Util } from "../Common/util"; /*
* @Author: your name
* @Date: 2022-01-26 01:58:25
* @LastEditTime: 2022-02-18 18:51:28
* @LastEditors: Please set LastEditors
* @Description: koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
* @FilePath: \c:\Users\13127\Desktop\\0126\StructV2\src\RegisteredShape\linkListNode.ts
*/
import { Util } from '../Common/util';
export default Util.registerShape(
'link-list-node',
{
draw(cfg, group) {
cfg.size = cfg.size || [30, 10];
const width = cfg.size[0],
height = cfg.size[1];
export default Util.registerShape('link-list-node', { const wrapperRect = group.addShape('rect', {
draw(cfg, group) { attrs: {
cfg.size = cfg.size || [30, 10]; x: width / 2,
y: height / 2,
width: width,
height: height,
stroke: cfg.style.stroke || '#333',
fill: cfg.style.backgroundFill || '#eee',
cursor: cfg.style.cursor,
},
name: 'wrapper',
});
const width = cfg.size[0], group.addShape('rect', {
height = cfg.size[1]; attrs: {
x: width / 2,
const wrapperRect = group.addShape('rect', { y: height / 2,
attrs: { width: width * (2 / 3),
x: width / 2, height: height,
y: height / 2, fill: cfg.style.fill,
width: width, stroke: cfg.style.stroke || '#333',
height: height, cursor: cfg.style.cursor,
stroke: cfg.style.stroke || '#333', },
fill: cfg.style.backgroundFill || '#eee', name: 'main-rect',
cursor: cfg.style.cursor draggable: true,
}, });
name: 'wrapper'
});
group.addShape('rect', { const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
attrs: {
x: width / 2,
y: height / 2,
width: width * (2 / 3),
height: height,
fill: cfg.style.fill,
stroke: cfg.style.stroke || '#333',
cursor: cfg.style.cursor
},
name: 'main-rect',
draggable: true
});
if (cfg.label) { if (cfg.label) {
const style = (cfg.labelCfg && cfg.labelCfg.style) || {}; group.addShape('text', {
group.addShape('text', { attrs: {
attrs: { x: width * (5 / 6),
x: width * (5 / 6), y: height,
y: height, textAlign: 'center',
textAlign: 'center', textBaseline: 'middle',
textBaseline: 'middle', text: cfg.label,
text: cfg.label, fill: style.fill || '#000',
fill: style.fill || '#000', fontSize: style.fontSize || 16,
fontSize: style.fontSize || 16 },
}, name: 'label',
name: 'text', draggable: true,
draggable: true });
}); }
}
return wrapperRect; //节点没有后续指针时
}, if (!cfg.next && !cfg.loopNext) {
group.addShape('text', {
attrs: {
x: width * (4 / 3),
y: height * (6 / 5),
textAlign: 'center',
textBaseline: 'middle',
text: '^',
fill: style.fill || '#000',
fontSize: 16,
cursor: cfg.style.cursor,
},
name: 'null',
draggable: true,
});
}
getAnchorPoints() { return wrapperRect;
return [ },
[0.5, 0],
[5 / 6, 0], getAnchorPoints() {
[5 / 6, 0.5], return [
[1, 0.5], [0.5, 0],
[5 / 6, 1], [5 / 6, 0],
[0.5, 1], [5 / 6, 0.5],
[0, 0.5], [1, 0.5],
[1 / 3, 1] [5 / 6, 1],
]; [0.5, 1],
} [0, 0.5],
}, 'rect'); [1 / 3, 1],
];
},
},
'rect'
);

View File

@ -1,88 +1,89 @@
import { Util } from '../Common/util'; import { Util } from '../Common/util';
export default Util.registerShape('pointer', { export default Util.registerShape('pointer', {
draw(cfg, group) { draw(cfg, group) {
const keyShape = group.addShape('path', { const keyShape = group.addShape('path', {
attrs: { attrs: {
path: this.getPath(cfg), path: this.getPath(cfg),
fill: cfg.style.fill, fill: cfg.style.fill,
matrix: cfg.style.matrix matrix: cfg.style.matrix,
}, },
name: 'pointer-path' name: 'pointer-path',
}); });
if (cfg.label) { if (cfg.label) {
const labelStyle = (cfg.labelCfg && cfg.labelCfg.style) || {}; const labelStyle = (cfg.labelCfg && cfg.labelCfg.style) || {};
const bgRect = group.addShape('rect', { const bgRect = group.addShape('rect', {
attrs: { attrs: {
x: 0, x: 0,
y: 0, y: 0,
text: cfg.label, text: cfg.label,
fill: null, fill: null,
radius: 2 radius: 2,
}, },
name: 'bgRect' name: 'bgRect',
}); });
const text = group.addShape('text', { const text = group.addShape('text', {
attrs: { attrs: {
x: 0, x: 0,
y: 0, y: 0,
textAlign: 'center', textAlign: 'center',
textBaseline: 'middle', textBaseline: 'middle',
text: cfg.label, text: cfg.label,
fill: labelStyle.fill || '#999', fill: labelStyle.fill || '#999',
fontSize: labelStyle.fontSize || 16 fontSize: labelStyle.fontSize || 16,
}, },
name: 'pointer-text-shape' name: 'pointer-text-shape',
}); });
const { width: textWidth, height: textHeight } = text.getBBox(); const { width: textWidth, height: textHeight } = text.getBBox();
bgRect.attr({ const { width: pointerWidth, height: pointerHeight } = keyShape.getBBox();
width: textWidth + 6, bgRect.attr({
height: textHeight + 6 width: textWidth + pointerWidth + 6,
}); height: textHeight + pointerHeight + 6,
});
// 旋转文字 // 旋转文字
const markerEndPosition = cfg.markerEndPosition; const markerEndPosition = cfg.markerEndPosition;
if(markerEndPosition) { if (markerEndPosition) {
let textX = markerEndPosition[0], let textX = markerEndPosition[0],
textY = markerEndPosition[1]; textY = markerEndPosition[1];
text.attr({ text.attr({
x: textX, x: textX,
y: textY y: textY,
}); });
bgRect.attr({ bgRect.attr({
x: textX - textWidth / 2 - 3, x: textX - textWidth / 2 - 3,
y: textY - textHeight / 2 - 3 y: textY - textHeight / 2 - 3,
}); });
} }
} return bgRect;
}
return keyShape; return keyShape;
}, },
getPath(cfg) {
let width = cfg.size[0],
height = cfg.size[1],
arrowWidth = width + 4,
arrowHeight = height * 0.3;
const path = [ getPath(cfg) {
['M', 0, 0], let width = cfg.size[0],
['L', -width / 2 - (arrowWidth / 2), -arrowHeight], height = cfg.size[1],
['L', -width / 2, -arrowHeight], arrowWidth = width + 4,
['L', -width / 2, -height], arrowHeight = height * 0.3;
['L', width / 2, -height],
['L', width / 2, -arrowHeight],
['L', width / 2 + (arrowWidth / 2), -arrowHeight],
['Z'],
];
return path; const path = [
} ['M', 0, 0],
}); ['L', -width / 2 - arrowWidth / 2, -arrowHeight],
['L', -width / 2, -arrowHeight],
['L', -width / 2, -height],
['L', width / 2, -height],
['L', width / 2, -arrowHeight],
['L', width / 2 + arrowWidth / 2, -arrowHeight],
['Z'],
];
return path;
},
});

View File

@ -0,0 +1,189 @@
/*
* @Author: your name
* @Date: 2022-01-18 20:42:31
* @LastEditTime: 2022-02-17 21:57:59
* @LastEditors: Please set LastEditors
* @Description: koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
* @FilePath: \c:\Users\13127\Desktop\\-new\StructV2\src\RegisteredShape\treeCellNode.ts
*/
import { registerNode } from '@antv/g6';
export default registerNode('three-cell', {
draw(cfg, group) {
cfg.size = cfg.size || [30, 10];
const width = cfg.size[0],
height = cfg.size[1];
const wrapperRect = group.addShape('rect', {
attrs: {
x: width / 2,
y: height / 2,
width: width,
height: height,
stroke: cfg.style.stroke,
fill: cfg.style.backgroundFill || '#eee',
},
name: 'wrapper',
});
group.addShape('rect', {
attrs: {
x: width / 2,
y: height / 2,
width: width / 3,
height: height,
fill: cfg.style.fill,
stroke: cfg.style.stroke,
},
name: 'left-rect',
draggable: true,
});
group.addShape('rect', {
attrs: {
x: width * (5 / 6),
y: height / 2,
width: width / 3,
height: height,
fill: cfg.style.fill,
stroke: cfg.style.stroke,
},
name: 'middle-rect',
draggable: true,
});
const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
//节点上方文字
if (cfg.root && cfg.rootLabel) {
group.addShape('text', {
attrs: {
x: width * (2 / 3),
y: 0,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.rootLabel[0],
fill: style.fill || '#bbb',
fontSize: style.fontSize || 16,
fontStyle: 'italic',
cursor: cfg.style.cursor,
},
name: 'rootLabel0',
draggable: true,
});
group.addShape('text', {
attrs: {
x: width,
y: 0,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.rootLabel[1],
fill: style.fill || '#bbb',
fontSize: style.fontSize || 16,
fontStyle: 'italic',
cursor: cfg.style.cursor,
},
name: 'rootLabel1',
draggable: true,
});
group.addShape('text', {
attrs: {
x: width * (4 / 3),
y: 0,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.rootLabel[2],
fill: style.fill || '#bbb',
fontSize: style.fontSize || 16,
fontStyle: 'italic',
cursor: cfg.style.cursor,
},
name: 'rootLabel2',
draggable: true,
});
}
//节点左边文字
if (cfg.index !== null) {
group.addShape('text', {
attrs: {
x: width * (2 / 5),
y: height,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.index,
fill: style.fill || '#bbb',
fontSize: style.fontSize || 16,
fontStyle: 'italic',
cursor: cfg.style.cursor,
},
name: 'index',
draggable: true,
});
}
//节点文字(数组形式)
if (cfg.label) {
group.addShape('text', {
attrs: {
x: width * (2 / 3),
y: height,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.label[0],
fill: style.fill || '#000',
fontSize: style.fontSize || 16,
cursor: cfg.style.cursor,
},
name: 'label1',
draggable: true,
});
group.addShape('text', {
attrs: {
x: width,
y: height,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.label[1],
fill: style.fill || '#000',
fontSize: style.fontSize || 16,
cursor: cfg.style.cursor,
},
name: 'label2',
draggable: true,
});
}
//节点没有后续指针时
if (!cfg.headNext) {
group.addShape('text', {
attrs: {
x: width * (4 / 3),
y: height * (6 / 5),
textAlign: 'center',
textBaseline: 'middle',
text: '^',
fill: style.fill || '#000',
fontSize: 22,
cursor: cfg.style.cursor,
},
name: 'null',
draggable: true,
});
}
return wrapperRect;
},
getAnchorPoints() {
return [
[0.5, 0],
[5 / 6, 0.5],
[0.5, 1],
[0, 0.5],
];
},
});

View File

@ -1,100 +1,143 @@
import { Util } from '../Common/util'; import { Util } from '../Common/util';
export default Util.registerShape('tri-tree-node', { export default Util.registerShape('tri-tree-node', {
draw(cfg, group) { draw(cfg, group) {
cfg.size = cfg.size; cfg.size = cfg.size;
const width = cfg.size[0], const width = cfg.size[0],
height = cfg.size[1]; height = cfg.size[1];
const wrapperRect = group.addShape('rect', { const wrapperRect = group.addShape('rect', {
attrs: { attrs: {
x: width / 2, x: width / 2,
y: height / 2, y: height / 2,
width: width, width: width,
height: height, height: height,
stroke: cfg.style.stroke || '#333', stroke: cfg.style.stroke || '#333',
cursor: cfg.style.cursor, cursor: cfg.style.cursor,
fill: cfg.style.backgroundFill || '#eee' fill: '#eee',
}, },
name: 'wrapper' name: 'wrapper',
}); });
group.addShape('rect', { group.addShape('rect', {
attrs: { attrs: {
x: width / 4 + width / 2, x: width / 4 + width / 2,
y: height / 2, y: height / 2,
width: width / 2, width: width / 2,
height: height / 4, height: height / 4,
fill: '#eee', fill: '#eee',
stroke: cfg.style.stroke || '#333', stroke: cfg.style.stroke || '#333',
cursor: cfg.style.cursor cursor: cfg.style.cursor,
}, },
name: 'top', name: 'top',
draggable: true draggable: true,
}); });
group.addShape('rect', { group.addShape('rect', {
attrs: { attrs: {
x: width / 4 + width / 2, x: width / 4 + width / 2,
y: height / 2 + height / 4, y: height / 2 + height / 4,
width: width / 2, width: width / 2,
height: height / 4 * 3, height: (height / 4) * 3,
fill: cfg.color || cfg.style.fill, fill: cfg.color || cfg.style.fill,
stroke: cfg.style.stroke || '#333', stroke: cfg.style.stroke || '#333',
cursor: cfg.style.cursor cursor: cfg.style.cursor,
}, },
name: 'bottom', name: 'bottom',
draggable: true draggable: true,
}); });
const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
if (cfg.label) {
group.addShape('text', {
attrs: {
x: width, // 居中
y: height + height / 8,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.label,
fill: style.fill || '#000',
fontSize: style.fontSize || 16,
cursor: cfg.style.cursor,
},
name: 'label',
draggable: true,
});
}
let parent = cfg.parent || cfg.l_parent || cfg.r_parent;
const isLeftEmpty =
!cfg.child || cfg.child[0] === undefined || cfg.child[0] === undefined || cfg.child[0] == '0x0',
isRightEmpty =
!cfg.child || cfg.child[1] === undefined || cfg.child[1] === undefined || cfg.child[1] == '0x0',
isParentEmpty = parent[0] == '0x0';
if (cfg.label) {
const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
group.addShape('text', {
attrs: {
x: width, // 居中
y: height + height / 8,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.label,
fill: style.fill || '#000',
fontSize: style.fontSize || 16,
cursor: cfg.style.cursor
},
name: 'text',
draggable: true
});
}
if (cfg.root) { if (isParentEmpty) {
const style = (cfg.labelCfg && cfg.labelCfg.style) || {}; {
group.addShape('text', { group.addShape('text', {
attrs: { attrs: {
x: width, // 居中 x: width, // 居中
y: height / 4 * 3, y: (height / 4) * 3,
textAlign: 'center', textAlign: 'center',
textBaseline: 'middle', textBaseline: 'middle',
text: "^", text: '^',
fill: style.fill || '#000', fill: style.fill || '#000',
fontSize: style.fontSize || 14, fontSize: style.fontSize || 14,
cursor: cfg.style.cursor cursor: cfg.style.cursor,
}, },
name: 'parent', name: 'null-parent',
draggable: true draggable: true,
}); });
} }
}
return wrapperRect; //节点没有左孩子节点时
}, if (isLeftEmpty) {
group.addShape('text', {
attrs: {
x: width * (5 / 8),
y: height * (8 / 7),
textAlign: 'center',
textBaseline: 'middle',
text: '^',
fill: style.fill || '#000',
fontSize: 16,
cursor: cfg.style.cursor,
},
name: 'null-left',
draggable: true,
});
}
//节点没有右孩子节点时
if (isRightEmpty) {
group.addShape('text', {
attrs: {
x: width * (11 / 8),
y: height * (8 / 7),
textAlign: 'center',
textBaseline: 'middle',
text: '^',
fill: style.fill || '#000',
fontSize: 16,
cursor: cfg.style.cursor,
},
name: 'null-right',
draggable: true,
});
}
getAnchorPoints() { return wrapperRect;
return [ },
[0.125, 0.5],
[0.875, 0.5], getAnchorPoints() {
[0.5, 1], return [
[0.5, 0], [0.125, 0.5],
[0.5, 0.125] [0.875, 0.5],
]; [0.4, 1],
}, [0.5, 0],
}, 'rect'); [0.5, 0.125],
[0.6, 1],
[0.5, 1],
];
},
});

View File

@ -1,102 +1,157 @@
import { Util } from '../Common/util'; import { Util } from '../Common/util';
export default Util.registerShape(
'two-cell-node',
{
draw(cfg, group) {
cfg.size = cfg.size || [30, 10];
const width = cfg.size[0],
height = cfg.size[1];
export default Util.registerShape('two-cell-node', { const wrapperRect = group.addShape('rect', {
draw(cfg, group) { attrs: {
cfg.size = cfg.size || [30, 10]; x: width / 2,
y: height / 2,
width: width,
height: height,
stroke: cfg.style.stroke,
fill: cfg.style.backgroundFill || '#eee',
},
name: 'wrapper',
});
const width = cfg.size[0], group.addShape('rect', {
height = cfg.size[1]; attrs: {
x: width / 2,
y: height / 2,
width: width / 2,
height: height,
fill: cfg.style.fill,
stroke: cfg.style.stroke,
},
name: 'left-rect',
draggable: true,
});
const wrapperRect = group.addShape('rect', { const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
attrs: {
x: width / 2,
y: height / 2,
width: width,
height: height,
stroke: cfg.style.stroke,
fill: cfg.style.backgroundFill || '#eee'
},
name: 'wrapper'
});
group.addShape('rect', { if (cfg.label) {
attrs: { if (Array.isArray(cfg.label)) {
x: width / 2, let tag = cfg.label[0],
y: height / 2, data = cfg.label[1];
width: width / 2,
height: height,
fill: cfg.style.fill,
stroke: cfg.style.stroke
},
name: 'left-rect',
draggable: true
});
const style = (cfg.labelCfg && cfg.labelCfg.style) || {}; group.addShape('text', {
attrs: {
x: width * (3 / 4),
y: height,
textAlign: 'center',
textBaseline: 'middle',
text: tag,
fill: style.fill || '#000',
fontSize: style.fontSize || 16,
cursor: cfg.style.cursor,
},
name: 'tag',
draggable: true,
});
if (cfg.label) { group.addShape('text', {
if(Array.isArray(cfg.label)) { attrs: {
let tag = cfg.label[0], data = cfg.label[1]; x: width * (5 / 4),
y: height,
textAlign: 'center',
textBaseline: 'middle',
text: data,
fill: style.fill || '#000',
fontSize: style.fontSize || 16,
cursor: cfg.style.cursor,
},
name: 'data',
draggable: true,
});
} else {
group.addShape('text', {
attrs: {
x: width * (3 / 4),
y: height,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.label,
fill: style.fill || '#000',
fontSize: style.fontSize || 16,
cursor: cfg.style.cursor,
},
name: 'label',
draggable: true,
});
}
}
group.addShape('text', { //图 数据结构中没有后续指针
attrs: { if (cfg.id.includes('tableHeadNode') && !cfg.headNext) {
x: width * (3 / 4), group.addShape('text', {
y: height, attrs: {
textAlign: 'center', x: width * (5 / 4),
textBaseline: 'middle', y: height * (8 / 7),
text: tag, textAlign: 'center',
fill: style.fill || '#000', textBaseline: 'middle',
fontSize: style.fontSize || 16, text: '^',
cursor: cfg.style.cursor, fill: style.fill || '#000',
}, fontSize: 20,
name: 'text', cursor: cfg.style.cursor,
draggable: true },
}); name: 'null-headNext',
draggable: true,
});
}
group.addShape('text', { //哈希表 数据结构中没有后续指针
attrs: { if (cfg.id.includes('head') && !cfg.start) {
x: width * (5 / 4), group.addShape('text', {
y: height, attrs: {
textAlign: 'center', x: width * (5 / 4),
textBaseline: 'middle', y: height * (8 / 7),
text: data, textAlign: 'center',
fill: style.fill || '#000', textBaseline: 'middle',
fontSize: style.fontSize || 16, text: '^',
cursor: cfg.style.cursor, fill: style.fill || '#000',
}, fontSize: 20,
name: 'text', cursor: cfg.style.cursor,
draggable: true },
}); name: 'null-start',
} draggable: true,
else { });
group.addShape('text', { }
attrs: {
x: width * (3 / 4),
y: height,
textAlign: 'center',
textBaseline: 'middle',
text: cfg.label,
fill: style.fill || '#000',
fontSize: style.fontSize || 16,
cursor: cfg.style.cursor,
},
name: 'text',
draggable: true
});
}
}
return wrapperRect; //pctree 数据结构中没有后续指针
}, if (cfg.id.includes('PCTreeHead') && !cfg.headNext) {
group.addShape('text', {
attrs: {
x: width * (5 / 4),
y: height * (8 / 7),
textAlign: 'center',
textBaseline: 'middle',
text: '^',
fill: style.fill || '#000',
fontSize: 20,
cursor: cfg.style.cursor,
},
name: 'null-headNext2',
draggable: true,
});
}
return wrapperRect;
},
getAnchorPoints() { getAnchorPoints() {
return [ return [
[0.5, 0], [0.5, 0],
[3 / 4, 0.5], [3 / 4, 0.5],
[0.5, 1], [0.5, 1],
[0, 0.5] [0, 0.5],
]; ];
} },
}, 'rect'); },
'rect'
);

View File

@ -1,3 +1,11 @@
/*
* @Author: your name
* @Date: 2022-01-26 01:58:25
* @LastEditTime: 2022-01-26 02:06:16
* @LastEditors: your name
* @Description: koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
* @FilePath: \c:\Users\13127\Desktop\\0126\StructV2\src\StructV.ts
*/
import { Engine } from "./engine"; import { Engine } from "./engine";
import { Bound } from "./Common/boundingRect"; import { Bound } from "./Common/boundingRect";
import { Group } from "./Common/group"; import { Group } from "./Common/group";
@ -6,15 +14,16 @@ import Pointer from "./RegisteredShape/pointer";
import LinkListNode from "./RegisteredShape/linkListNode"; import LinkListNode from "./RegisteredShape/linkListNode";
import BinaryTreeNode from "./RegisteredShape/binaryTreeNode"; import BinaryTreeNode from "./RegisteredShape/binaryTreeNode";
import ForceNode from "./RegisteredShape/force"; import ForceNode from "./RegisteredShape/force";
import TriTreeNode from "./RegisteredShape/triTreeNode";
import CLenQueuePointer from "./RegisteredShape/clenQueuePointer"; import CLenQueuePointer from "./RegisteredShape/clenQueuePointer";
import TwoCellNode from "./RegisteredShape/twoCellNode"; import TwoCellNode from "./RegisteredShape/twoCellNode";
import ThreeCellNode from "./RegisteredShape/threeCellNode";
import ArrayNode from "./RegisteredShape/arrayNode"; import ArrayNode from "./RegisteredShape/arrayNode";
import Cursor from "./RegisteredShape/cursor"; import Cursor from "./RegisteredShape/cursor";
import { Vector } from "./Common/vector"; 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";
@ -58,7 +67,9 @@ SV.registeredShape = [
Pointer, Pointer,
LinkListNode, LinkListNode,
BinaryTreeNode, BinaryTreeNode,
TriTreeNode,
TwoCellNode, TwoCellNode,
ThreeCellNode,
Cursor, Cursor,
ArrayNode, ArrayNode,
CLenQueuePointer, CLenQueuePointer,
@ -85,5 +96,6 @@ SV.registerLayout = function(name: string, layoutCreator: LayoutCreator) {
} }
SV.registeredLayout[name] = layoutCreator; SV.registeredLayout[name] = layoutCreator;
}; };

View File

@ -1,345 +1,347 @@
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';
import { ViewContainer } from './viewContainer'; import { ViewContainer } from './viewContainer';
export class LayoutProvider { 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;
this.viewOptions = this.engine.viewOptions; this.viewOptions = this.engine.viewOptions;
this.viewContainer = viewContainer; this.viewContainer = viewContainer;
} }
/**
*
* @param layoutGroupTable
*/
private preLayoutProcess(layoutGroupTable: LayoutGroupTable) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
/** modelList.forEach(item => {
* item.preLayout = true;
* @param layoutGroupTable
*/
private preLayoutProcess(layoutGroupTable: LayoutGroupTable) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
modelList.forEach(item => { item.set('rotation', item.get('rotation'));
item.preLayout = true; item.set({ x: 0, y: 0 });
});
}
item.set('rotation', item.get('rotation')); /**
item.set({ x: 0, y: 0 }); *
}); * @param layoutGroupTable
} */
private postLayoutProcess(layoutGroupTable: LayoutGroupTable) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
/** modelList.forEach(item => {
* item.preLayout = false;
* @param layoutGroupTable
*/
private postLayoutProcess(layoutGroupTable: LayoutGroupTable) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
modelList.forEach(item => { // 用两个变量保存节点布局完成后的坐标因为拖拽节点会改变节点的xy坐标
item.preLayout = false; // 然后当节点移动到泄漏区的时候,不应该保持节点被拖拽后的状态,应该恢复到布局完成后的状态,不然就会很奇怪
item.layoutX = item.get('x');
item.layoutY = item.get('y');
});
}
// 用两个变量保存节点布局完成后的坐标因为拖拽节点会改变节点的xy坐标 /**
// 然后当节点移动到泄漏区的时候,不应该保持节点被拖拽后的状态,应该恢复到布局完成后的状态,不然就会很奇怪 *
item.layoutX = item.get('x'); * @param marker
item.layoutY = item.get('y'); * @param markerOptions
}); */
} private layoutMarker(markers: SVMarker[], markerOptions: { [key: string]: MarkerOption }) {
markers.forEach(item => {
const options: MarkerOption = markerOptions[item.sourceType],
offset = options.offset ?? 8,
anchor = item.anchor ?? 0,
labelOffset = options.labelOffset ?? 2;
/** let target = item.target,
* targetBound: BoundingRect = target.getBound(),
* @param marker g6AnchorPosition = (<INode>item.target.shadowG6Item).getAnchorPoints()[anchor] as IPoint,
* @param markerOptions center: [number, number] = [
*/ targetBound.x + targetBound.width / 2,
private layoutMarker(markers: SVMarker[], markerOptions: { [key: string]: MarkerOption }) { targetBound.y + targetBound.height / 2,
markers.forEach(item => { ],
const options: MarkerOption = markerOptions[item.sourceType], markerPosition: [number, number],
offset = options.offset ?? 8, markerEndPosition: [number, number];
anchor = item.anchor ?? 0,
labelOffset = options.labelOffset ?? 2;
let target = item.target, let anchorPosition: [number, number] = [g6AnchorPosition.x, g6AnchorPosition.y];
targetBound: BoundingRect = target.getBound(),
g6AnchorPosition = item.target.shadowG6Item.getAnchorPoints()[anchor] as IPoint,
center: [number, number] = [targetBound.x + targetBound.width / 2, targetBound.y + targetBound.height / 2],
markerPosition: [number, number],
markerEndPosition: [number, number];
let anchorPosition: [number, number] = [g6AnchorPosition.x, g6AnchorPosition.y]; let anchorVector = Vector.subtract(anchorPosition, center),
angle = 0,
len = Vector.length(anchorVector) + offset;
let anchorVector = Vector.subtract(anchorPosition, center), if (anchorVector[0] === 0) {
angle = 0, len = Vector.length(anchorVector) + offset; angle = anchorVector[1] > 0 ? -Math.PI : 0;
} else {
angle = Math.sign(anchorVector[0]) * (Math.PI / 2 - Math.atan(anchorVector[1] / anchorVector[0]));
}
if (anchorVector[0] === 0) { const markerHeight = item.get('size')[1],
angle = anchorVector[1] > 0 ? -Math.PI : 0; labelRadius = item.getLabelSizeRadius() / 2;
}
else {
angle = Math.sign(anchorVector[0]) * (Math.PI / 2 - Math.atan(anchorVector[1] / anchorVector[0]));
}
const markerHeight = item.get('size')[1], anchorVector = Vector.normalize(anchorVector);
labelRadius = item.getLabelSizeRadius() / 2; markerPosition = Vector.location(center, anchorVector, len);
markerEndPosition = Vector.location(center, anchorVector, markerHeight + len + labelRadius + labelOffset);
markerEndPosition = Vector.subtract(markerEndPosition, markerPosition);
anchorVector = Vector.normalize(anchorVector); item.set({
markerPosition = Vector.location(center, anchorVector, len); x: markerPosition[0],
markerEndPosition = Vector.location(center, anchorVector, markerHeight + len + labelRadius + labelOffset); y: markerPosition[1],
markerEndPosition = Vector.subtract(markerEndPosition, markerPosition); rotation: angle,
markerEndPosition,
});
});
}
item.set({ /**
x: markerPosition[0], *
y: markerPosition[1], * @param freedLabels
rotation: angle, */
markerEndPosition private layoutFreedLabel(freedLabels: SVFreedLabel[]) {
}); freedLabels.forEach(item => {
}); const freedNodeBound = item.target.getBound();
}
/** item.set({
* x: freedNodeBound.x + freedNodeBound.width / 2,
* @param freedLabels y: freedNodeBound.y + freedNodeBound.height * 1.5,
*/ size: [freedNodeBound.width, 0],
private layoutFreedLabel(freedLabels: SVFreedLabel[]) { });
freedLabels.forEach(item => { });
const freedNodeBound = item.target.getBound(); }
item.set({ /**
x: freedNodeBound.x + freedNodeBound.width / 2, *
y: freedNodeBound.y + freedNodeBound.height * 1.5, * @param indexLabels
size: [freedNodeBound.width, 0] * @param indexLabelOptions
}); */
}); private layoutIndexLabel(indexLabels: SVIndexLabel[], indexLabelOptions: { [key: string]: IndexLabelOption }) {
} const indexLabelPositionMap: {
[key: string]: (
nodeBound: BoundingRect,
labelBound: BoundingRect,
offset: number
) => { x: number; y: number };
} = {
top: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y - offset,
};
},
right: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width + offset,
y: nodeBound.y + nodeBound.height / 2,
};
},
bottom: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y + nodeBound.height + offset,
};
},
left: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x - labelBound.width / 2- offset,
y: nodeBound.y + nodeBound.height / 2,
};
},
};
/** indexLabels.forEach(item => {
* const options: IndexLabelOption = indexLabelOptions[item.sourceType],
* @param indexLabels nodeBound = item.target.getBound(),
* @param indexLabelOptions // labelBound = item.getBound(),
*/ labelBound = item.shadowG6Item.getContainer().getChildren()[1].getBBox(),
private layoutIndexLabel(indexLabels: SVIndexLabel[], indexLabelOptions: { [key: string]: IndexLabelOption }) { offset = options.offset ?? 20,
const indexLabelPositionMap: { [key: string]: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => { x: number, y: number } } = { position = options.position ?? 'bottom';
top: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y - offset
};
},
right: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width + offset,
y: nodeBound.y + nodeBound.height / 2
};
},
bottom: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y + nodeBound.height + offset
};
},
left: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x - labelBound.width - offset,
y: nodeBound.y + nodeBound.height / 2
};
}
};
indexLabels.forEach(item => { const pos = indexLabelPositionMap[position](nodeBound, labelBound, offset);
const options: IndexLabelOption = indexLabelOptions[item.sourceType], item.set(pos);
nodeBound = item.target.getBound(), });
labelBound = item.getBound(), }
offset = options.offset ?? 20,
position = options.position ?? 'bottom';
const pos = indexLabelPositionMap[position](nodeBound, labelBound, offset); /**
item.set(pos); * address label
}); * @param leakAddress
} */
private layoutAddressLabel(leakAddress: SVAddressLabel[], addressLabelOption: AddressLabelOption) {
const offset = addressLabelOption.offset || 16;
/** leakAddress.forEach(item => {
* address label const nodeBound = item.target.getBound();
* @param leakAddress
*/
private layoutAddressLabel(leakAddress: SVAddressLabel[], addressLabelOption: AddressLabelOption) {
const offset = addressLabelOption.offset || 16;
leakAddress.forEach(item => { item.set({
const nodeBound = item.target.getBound(); x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y - offset,
});
});
}
item.set({ /**
x: nodeBound.x + nodeBound.width / 2, * model进行布局
y: nodeBound.y - offset * @param layoutGroupTable
}); */
}); private layoutModels(layoutGroupTable: LayoutGroupTable): Group[] {
} const modelGroupList: Group[] = [];
layoutGroupTable.forEach(group => {
const modelList: SVModel[] = group.modelList,
modelGroup: Group = new Group();
/** const layoutOptions: LayoutOptions = group.options.layout;
* model进行布局
* @param layoutGroupTable
*/
private layoutModels(layoutGroupTable: LayoutGroupTable): Group[] {
const modelGroupList: Group[] = [];
layoutGroupTable.forEach(group => { modelList.forEach(item => {
const modelList: SVModel[] = group.modelList, modelGroup.add(item);
modelGroup: Group = new Group(); });
const layoutOptions: LayoutOptions = group.options.layout; group.layoutCreator.layout(group.node, layoutOptions); // 布局节点
modelGroupList.push(modelGroup);
});
modelList.forEach(item => { layoutGroupTable.forEach(group => {
modelGroup.add(item); const markerOptions = group.options.marker || {},
}); indexLabelOptions = group.options.indexLabel || {},
addressLabelOption = group.options.addressLabel || {};
group.layoutCreator.layout(group.node, layoutOptions); // 布局节点 const indexLabel = group.appendage.filter(item => item instanceof SVIndexLabel) as SVIndexLabel[],
modelGroupList.push(modelGroup); freedLabel = group.appendage.filter(item => item instanceof SVFreedLabel) as SVFreedLabel[],
}); addressLabel = group.appendage.filter(item => item instanceof SVAddressLabel) as SVAddressLabel[],
marker = group.appendage.filter(item => item instanceof SVMarker) as SVMarker[];
layoutGroupTable.forEach(group => { this.layoutIndexLabel(indexLabel, indexLabelOptions);
const markerOptions = group.options.marker || {}, this.layoutFreedLabel(freedLabel);
indexLabelOptions = group.options.indexLabel || {}, this.layoutAddressLabel(addressLabel, addressLabelOption);
addressLabelOption = group.options.addressLabel || {}; this.layoutMarker(marker, markerOptions); // 布局外部指针
});
this.layoutIndexLabel(group.indexLabel, indexLabelOptions); return modelGroupList;
this.layoutFreedLabel(group.freedLabel); }
this.layoutAddressLabel(group.addressLabel, addressLabelOption);
this.layoutMarker(group.marker, markerOptions); // 布局外部指针
});
return modelGroupList; /**
} *
* @param accumulateLeakModels
* // todo: 部分元素被抽离后的泄漏区
*/
private layoutLeakArea(accumulateLeakModels: SVModel[]) {
const containerHeight = this.viewContainer.getG6Instance().getHeight(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
leakAreaY = containerHeight - leakAreaHeight;
/** let prevBound: BoundingRect;
*
* @param leakModels
* @param accumulateLeakModels
*/
private layoutLeakModels(leakModels: SVModel[], accumulateLeakModels: SVModel[]) {
const group: Group = new Group(),
containerHeight = this.viewContainer.getG6Instance().getHeight(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
leakAreaY = containerHeight - leakAreaHeight,
xOffset = 60;
let prevBound: BoundingRect; // 避免在泄漏前拖拽节点导致的位置变化,先把节点位置重置为布局后的标准位置
accumulateLeakModels.forEach(item => {
item.set({
x: item.layoutX,
y: item.layoutY,
});
});
// 避免在泄漏前拖拽节点导致的位置变化,先把节点位置重置为布局后的标准位置 const clusters = ModelConstructor.getClusters(accumulateLeakModels);
leakModels.forEach(item => {
item.set({
x: item.layoutX,
y: item.layoutY
});
});
const globalLeakGroupBound: BoundingRect = accumulateLeakModels.length ? // 每一个簇从左往右布局就完事了,比之前的方法简单稳定很多
Bound.union(...accumulateLeakModels.map(item => item.getBound())) : clusters.forEach(item => {
{ x: 0, y: leakAreaY, width: 0, height: 0 }; const bound = item.getBound(),
x = prevBound ? prevBound.x + prevBound.width + this.leakClusterXInterval : this.leakAreaXoffset,
dx = x - bound.x,
dy = leakAreaY - bound.y;
const layoutGroups = Util.groupBy(leakModels, 'group'); item.translate(dx, dy);
Object.keys(layoutGroups).forEach(key => { Bound.translate(bound, dx, dy);
group.add(...layoutGroups[key]); prevBound = bound;
});
}
const currentBound: BoundingRect = group.getBound(), /**
prevBoundEnd = prevBound? prevBound.x + prevBound.width: 0, *
{ x: groupX, y: groupY } = currentBound, * @param modelGroupTable
dx = globalLeakGroupBound.x + globalLeakGroupBound.width + prevBoundEnd + xOffset - groupX, */
dy = globalLeakGroupBound.y - groupY; private layoutGroups(modelGroupList: Group[]): Group {
let wrapperGroup: Group = new Group(),
group: Group,
prevBound: BoundingRect,
bound: BoundingRect,
boundList: BoundingRect[] = [],
dx = 0;
group.translate(dx, dy); // 左往右布局
group.clear(); for (let i = 0; i < modelGroupList.length; i++) {
Bound.translate(currentBound, dx, dy); group = modelGroupList[i];
bound = group.getPaddingBound(this.viewOptions.groupPadding);
prevBound = currentBound; if (prevBound) {
}); dx = prevBound.x + prevBound.width - bound.x;
} } else {
dx = bound.x;
}
/** group.translate(dx, 0);
* Bound.translate(bound, dx, 0);
* @param modelGroupTable boundList.push(bound);
*/ wrapperGroup.add(group);
private layoutGroups(modelGroupList: Group[]): Group { prevBound = bound;
let wrapperGroup: Group = new Group(), }
group: Group,
prevBound: BoundingRect,
bound: BoundingRect,
boundList: BoundingRect[] = [],
dx = 0;
// 左往右布局 return wrapperGroup;
for (let i = 0; i < modelGroupList.length; i++) { }
group = modelGroupList[i];
bound = group.getPaddingBound(this.viewOptions.groupPadding);
if (prevBound) { /**
dx = prevBound.x + prevBound.width - bound.x; *
} * @param models
else { * @param leakAreaHeight
dx = bound.x; */
} private fitCenter(group: Group) {
let width = this.viewContainer.getG6Instance().getWidth(),
height = this.viewContainer.getG6Instance().getHeight(),
viewBound: BoundingRect = group.getBound(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight;
group.translate(dx, 0); const centerX = width / 2,
Bound.translate(bound, dx, 0); centerY = height / 2,
boundList.push(bound); boundCenterX = viewBound.x + viewBound.width / 2;
wrapperGroup.add(group);
prevBound = bound;
}
return wrapperGroup; let dx = centerX - boundCenterX,
} dy = 0;
if (this.viewContainer.hasLeak) {
const boundBottomY = viewBound.y + viewBound.height;
dy = height - leakAreaHeight - 130 - boundBottomY;
} else {
const boundCenterY = viewBound.y + viewBound.height / 2;
dy = centerY - boundCenterY;
}
/** group.translate(dx, dy);
* }
* @param models
* @param leakAreaHeight
*/
private fitCenter(group: Group) {
let width = this.viewContainer.getG6Instance().getWidth(),
height = this.viewContainer.getG6Instance().getHeight(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight;
if (this.viewContainer.hasLeak) { /**
height = height - leakAreaHeight; *
} * @param layoutGroupTable
* @param accumulateLeakModels
*/
public layoutAll(layoutGroupTable: LayoutGroupTable, accumulateLeakModels: SVModel[]) {
this.preLayoutProcess(layoutGroupTable);
const viewBound: BoundingRect = group.getBound(), const modelGroupList: Group[] = this.layoutModels(layoutGroupTable);
centerX = width / 2, centerY = height / 2, const generalGroup: Group = this.layoutGroups(modelGroupList);
boundCenterX = viewBound.x + viewBound.width / 2,
boundCenterY = viewBound.y + viewBound.height / 2,
dx = centerX - boundCenterX,
dy = centerY - boundCenterY;
group.translate(dx, dy); this.layoutLeakArea(accumulateLeakModels);
}
this.fitCenter(generalGroup);
this.postLayoutProcess(layoutGroupTable);
/** }
* }
* @param layoutGroupTable
* @param leakModels
* @param hasLeak
* @param needFitCenter
*/
public layoutAll(layoutGroupTable: LayoutGroupTable, accumulateLeakModels: SVModel[], leakModels: SVModel[]) {
this.preLayoutProcess(layoutGroupTable);
const modelGroupList: Group[] = this.layoutModels(layoutGroupTable);
const generalGroup: Group = this.layoutGroups(modelGroupList);
if (leakModels.length) {
this.layoutLeakModels(leakModels, accumulateLeakModels);
}
this.fitCenter(generalGroup);
this.postLayoutProcess(layoutGroupTable);
}
}

View File

@ -1,430 +1,442 @@
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 { handleUpdate } from '../sources';
import { Renderer } from "./renderer"; import { Animations } from './animation';
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[];
} }
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 => { ): SVModel[] {
let removeIndex = accumulateLeakModels.findIndex(leakModel => item.id === leakModel.id); 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),
svModel = accumulateLeakModels[removeIndex];
return appendModels;
} if (removeIndex > -1) {
svModel.leaked = false;
/** accumulateLeakModels.splice(removeIndex, 1);
* }
* @param layoutGroupTable });
* @param prevModelList
* @param modelList return appendModels;
* @returns }
*/
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
); * @param prevModelList
const leakModels: SVModel[] = []; * @param modelList
* @returns
// 先把节点拿出来 */
const potentialLeakNodes = potentialLeakModels.filter(item => item.isNode()) as SVNode[], private getLeakModels(
groups = Util.groupBy<SVNode>(potentialLeakNodes, 'group'); layoutGroupTable: LayoutGroupTable,
prevModelList: SVModel[],
// 再把非节点的model拿出来 modelList: SVModel[]
potentialLeakModels = potentialLeakModels.filter(item => item.isNode() === false); ): SVModel[] {
let potentialLeakModels: SVModel[] = prevModelList.filter(
Object.keys(groups).forEach(key => { item => !modelList.find(model => model.id === item.id) && !item.freed
const leakRule = layoutGroupTable.get(key).layoutCreator.defineLeakRule; );
if(leakRule && typeof leakRule === 'function') { const leakModels: SVModel[] = [];
potentialLeakModels.push(...leakRule(groups[key]));
} // 先把节点拿出来
}); const potentialLeakNodes = potentialLeakModels.filter(item => item.isNode()) as SVNode[],
groups = Util.groupBy<SVNode>(potentialLeakNodes, 'group');
potentialLeakModels.forEach(item => {
if (item instanceof SVNode) { // 再把非节点的model拿出来
item.leaked = true; potentialLeakModels = potentialLeakModels.filter(item => item.isNode() === false);
leakModels.push(item);
Object.keys(groups).forEach(key => {
item.appendages.forEach(appendage => { const leakRule = layoutGroupTable.get(key).layoutCreator.defineLeakRule;
appendage.leaked = true; if (leakRule && typeof leakRule === 'function') {
leakModels.push(appendage); potentialLeakModels.push(...leakRule(groups[key]));
}); }
} });
});
potentialLeakModels.forEach(item => {
potentialLeakModels.forEach(item => { if (item instanceof SVNode) {
if (item instanceof SVLink && item.node.leaked !== false && item.target.leaked !== false) { item.leaked = true;
item.leaked = true; leakModels.push(item);
leakModels.push(item);
} item.getAppendagesList().forEach(appendage => {
}); // 外部指针先不加入泄漏区(这个需要讨论一下,我觉得不应该)
if (appendage instanceof SVMarker) {
leakModels.forEach(item => { return;
// 不能用上次的G6item了不然布局的时候会没有动画 }
item.G6Item = null;
}); appendage.leaked = true;
leakModels.push(appendage);
return leakModels; });
} }
});
/**
* potentialLeakModels.forEach(item => {
* @param prevModelList if (item instanceof SVLink && item.node.leaked !== false && item.target.leaked !== false) {
* @param modelList item.leaked = true;
*/ leakModels.push(item);
private getRemoveModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] { }
let removedModels: SVModel[] = []; });
for (let i = 0; i < prevModelList.length; i++) { return leakModels;
let prevModel = prevModelList[i], }
target = modelList.find(item => item.id === prevModel.id);
/**
if (target === undefined && !prevModel.leaked) { *
removedModels.push(prevModel); * @param prevModelList
} * @param modelList
} */
private getRemoveModels(
return removedModels; prevModelList: SVModel[],
} modelList: SVModel[],
accumulateLeakModels: SVModel[]
): SVModel[] {
/** let removedModels: SVModel[] = [];
*
* @param prevModelList for (let i = 0; i < prevModelList.length; i++) {
* @param modelList let prevModel = prevModelList[i],
* @returns target = modelList.find(item => item.id === prevModel.id);
*/
private getReTargetMarkers(prevModelList: SVModel[], modelList: SVModel[]): SVMarker[] { if (target === undefined && !prevModel.leaked) {
const prevMarkers: SVMarker[] = prevModelList.filter(item => item instanceof SVMarker) as SVMarker[], removedModels.push(prevModel);
markers: SVMarker[] = modelList.filter(item => item instanceof SVMarker) as SVMarker[]; }
}
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) {
/** if (leakModel.node.leaked === false || leakModel.target.leaked === false) {
* label model accumulateLeakModels.splice(i, 1);
* @param prevModelList i--;
* @param modelList removedModels.push(leakModel);
* @returns }
*/ }
private getLabelChangeModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] { }
let labelChangeModels: SVModel[] = [];
removedModels.forEach(item => {
modelList.forEach(item => { item.beforeDestroy();
const prevItem = prevModelList.find(prevItem => prevItem.id === item.id); });
if (prevItem === undefined) { return removedModels;
return; }
}
/**
const prevLabel = prevItem.get('label'), *
label = item.get('label'); * @param prevModelList
* @param modelList
if (prevLabel !== label) { * @returns
labelChangeModels.push(item); */
} private getReTargetMarkers(prevModelList: SVModel[], modelList: SVModel[]): SVMarker[] {
}); const prevMarkers: SVMarker[] = prevModelList.filter(item => item instanceof SVMarker) as SVMarker[],
markers: SVMarker[] = modelList.filter(item => item instanceof SVMarker) as SVMarker[];
return labelChangeModels;
} return markers.filter(item =>
prevMarkers.find(prevItem => {
/** return prevItem.id === item.id && prevItem.target.id !== item.target.id;
* free })
* @param prevModelList );
* @param modelList }
* @returns
*/ /**
private getFreedModels(prevModelList: SVModel[], modelList: SVModel[]): SVNode[] { * label model
const freedNodes = modelList.filter(item => item instanceof SVNode && item.freed) as SVNode[]; * @param prevModelList
* @param modelList
freedNodes.forEach(item => { * @returns
const prev = prevModelList.find(prevModel => item.id === prevModel.id); */
private getLabelChangeModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] {
if (prev) { let labelChangeModels: SVModel[] = [];
item.set('label', prev.get('label'));
} modelList.forEach(item => {
}); const prevItem = prevModelList.find(prevItem => prevItem.id === item.id);
return freedNodes; if (prevItem === undefined) {
} return;
}
// ------------------------------------------------------------------------------------------------
const prevLabel = prevItem.get('label'),
/** label = item.get('label');
* models
* @param continuousModels if (String(prevLabel) !== String(label)) {
*/ labelChangeModels.push(item);
private handleContinuousModels(continuousModels: SVModel[]) { }
for (let i = 0; i < continuousModels.length; i++) { });
let model = continuousModels[i];
return labelChangeModels;
if (model instanceof SVNode) { }
const group = model.G6Item.getContainer();
group.attr({ opacity: 1 }); /**
} * free
} * @param prevModelList
} * @param modelList
* @returns
/** */
* models private getFreedModels(prevModelList: SVModel[], modelList: SVModel[]): SVNode[] {
* @param appendData const freedNodes = modelList.filter(item => item instanceof SVNode && item.freed) as SVNode[];
*/
private handleAppendModels(appendModels: SVModel[]) { freedNodes.forEach(item => {
let { duration, timingFunction } = this.engine.animationOptions; const prev = prevModelList.find(prevModel => item.id === prevModel.id);
appendModels.forEach(item => { if (prev) {
if (item instanceof SVNodeAppendage) { item.set('label', prev.get('label'));
// 先不显示泄漏区节点上面的地址文本 }
if (item instanceof SVAddressLabel) { });
// 先将透明度改为0隐藏掉
const AddressLabelG6Group = item.G6Item.getContainer(); return freedNodes;
AddressLabelG6Group.attr({ opacity: 0 }); }
}
else { // ------------------------------------------------------------------------------------------------
Animations.FADE_IN(item.G6Item, {
duration, /**
timingFunction * models
}); * @param continuousModels
} */
} private handleContinuousModels(continuousModels: SVModel[]) {
else { for (let i = 0; i < continuousModels.length; i++) {
Animations.APPEND(item.G6Item, { let model = continuousModels[i];
duration,
timingFunction if (model instanceof SVNode) {
}); const group = model.G6Item.getContainer();
} group.attr({ opacity: 1 });
}); }
} }
}
/**
* models /**
* @param removeData * models
*/ * @param appendData
private handleRemoveModels(removeModels: SVModel[]) { */
let { duration, timingFunction } = this.engine.animationOptions; private handleAppendModels(appendModels: SVModel[]) {
let { duration, timingFunction } = this.engine.animationOptions;
removeModels.forEach(item => {
Animations.REMOVE(item.G6Item, { appendModels.forEach(item => {
duration, if (item instanceof SVNodeAppendage) {
timingFunction, // 先不显示泄漏区节点上面的地址文本
callback: () => { if (item instanceof SVAddressLabel) {
this.renderer.removeModel(item); // 先将透明度改为0隐藏掉
} const AddressLabelG6Group = item.G6Item.getContainer();
}); AddressLabelG6Group.attr({ opacity: 0 });
}); } else {
} Animations.FADE_IN(item.G6Item, {
duration,
/** timingFunction,
* models });
* @param leakModels }
*/ } else {
private handleLeakModels(leakModels: SVModel[]) { Animations.APPEND(item.G6Item, {
let { duration, timingFunction } = this.engine.animationOptions; duration,
timingFunction,
leakModels.forEach(item => { callback: () => item.afterRender(),
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 => { item.afterDestroy();
if (item.generalStyle) { },
item.set('style', { ...item.generalStyle }); });
} });
}); }
}
/**
* models
/** * @param leakModels
* models */
* @param freedModes private handleLeakModels(leakModels: SVModel[]) {
*/ let { duration, timingFunction } = this.engine.animationOptions;
private handleFreedModels(freedModes: SVNode[]) {
const { duration, timingFunction } = this.engine.animationOptions, leakModels.forEach(item => {
alpha = 0.4; if (item instanceof SVAddressLabel) {
Animations.FADE_IN(item.G6Item, {
freedModes.forEach(item => { duration,
const nodeGroup = item.G6Item.getContainer(); timingFunction,
});
item.set('style', { fill: '#ccc' }); }
nodeGroup.attr({ opacity: alpha });
item.G6Item.enableCapture(false);
if (item.marker) { });
const markerGroup = item.marker.G6Item.getContainer();
item.marker.set('style', { fill: '#ccc' }); EventBus.emit('onLeak', leakModels);
markerGroup.attr({ opacity: alpha + 0.5 }); }
}
/**
item.freedLabel.G6Item.toFront(); * models
Animations.FADE_IN(item.freedLabel.G6Item, { duration, timingFunction }); * @param freedModes
}); */
private handleFreedModels(freedModes: SVNode[]) {
EventBus.emit('onFreed', freedModes); const { duration, timingFunction } = this.engine.animationOptions,
} alpha = 0.4;
/** freedModes.forEach(item => {
* models const nodeGroup = item.G6Item.getContainer();
* @param models
*/ item.set('style', { fill: '#ccc' });
private handleChangeModels(models: SVModel[]) { nodeGroup.attr({ opacity: alpha });
const changeHighlightColor: string = this.engine.viewOptions.updateHighlight;
if (item.appendages.marker) {
if (!changeHighlightColor || typeof changeHighlightColor !== 'string') { item.appendages.marker.forEach(marker => {
return; const markerGroup = marker.G6Item.getContainer();
} marker.set('style', { fill: '#ccc' });
markerGroup.attr({ opacity: alpha + 0.5 });
models.forEach(item => { });
if (item.generalStyle === undefined) { }
item.generalStyle = Util.objectClone(item.G6ModelProps.style);
} if (item.appendages.freedLabel) {
item.appendages.freedLabel.forEach(freedLabel => {
if (item instanceof SVLink) { freedLabel.G6Item.toFront();
item.set('style', { Animations.FADE_IN(freedLabel.G6Item, { duration, timingFunction });
stroke: changeHighlightColor });
}); }
} });
else {
item.set('style', { EventBus.emit('onFreed', freedModes);
fill: changeHighlightColor }
});
} /**
}); * models
} * @param models
*/
private handleChangeModels(models: SVModel[]) {
/** const changeHighlightColor: string = this.engine.viewOptions.updateHighlight;
* diff
* @param layoutGroupTable if (!changeHighlightColor || typeof changeHighlightColor !== 'string') {
* @param prevModelList return;
* @param modelList }
* @param accumulateLeakModels
* @returns models.forEach(item => {
*/ item.triggerHighlight(changeHighlightColor);
public diff(layoutGroupTable: LayoutGroupTable, prevModelList: SVModel[], modelList: SVModel[], accumulateLeakModels: SVModel[]): DiffResult { });
const continuousModels: SVModel[] = this.getContinuousModels(prevModelList, modelList); }
const leakModels: SVModel[] = this.getLeakModels(layoutGroupTable, prevModelList, modelList);
const appendModels: SVModel[] = this.getAppendModels(prevModelList, modelList, accumulateLeakModels); /**
const removeModels: SVModel[] = this.getRemoveModels(prevModelList, modelList); * model中那些不需要高亮的modeltarget和node都一样的linkmarker等
const updateModels: SVModel[] = [ * @param appendModels
...this.getReTargetMarkers(prevModelList, modelList), */
...this.getLabelChangeModels(prevModelList, modelList), private filterUnChangeModelsOfAppend(appendModels: SVModel[], prevModelList: SVModel[]): SVModel[] {
...appendModels, return appendModels.filter(item => !prevModelList.some(prev => item.isEqual(prev)));
...leakModels }
];
const freedModels: SVNode[] = this.getFreedModels(prevModelList, modelList); /**
* diff
return { * @param layoutGroupTable
CONTINUOUS: continuousModels, * @param prevModelList
APPEND: appendModels, * @param modelList
REMOVE: removeModels, * @param accumulateLeakModels
FREED: freedModels, * @returns
LEAKED: leakModels, */
UPDATE: updateModels, public diff(
ACCUMULATE_LEAK: [...accumulateLeakModels] layoutGroupTable: LayoutGroupTable,
}; prevModelList: SVModel[],
} modelList: SVModel[],
accumulateLeakModels: SVModel[],
isEnterFunction: boolean
/** ): DiffResult {
* const continuousModels: SVModel[] = this.getContinuousModels(prevModelList, modelList);
* @param diffResult const leakModels: SVModel[] = isEnterFunction
* @param isFirstRender ? []
*/ : this.getLeakModels(layoutGroupTable, prevModelList, modelList);
public patch(diffResult: DiffResult) { const appendModels: SVModel[] = this.getAppendModels(prevModelList, modelList, accumulateLeakModels);
const { const removeModels: SVModel[] = this.getRemoveModels(prevModelList, modelList, accumulateLeakModels);
APPEND, const updateModels: SVModel[] = [
REMOVE, ...this.getReTargetMarkers(prevModelList, modelList),
FREED, ...this.getLabelChangeModels(prevModelList, modelList),
LEAKED, ...this.filterUnChangeModelsOfAppend(appendModels, prevModelList),
UPDATE, ...leakModels,
CONTINUOUS, ];
ACCUMULATE_LEAK const freedModels: SVNode[] = this.getFreedModels(prevModelList, modelList);
} = diffResult;
return {
this.handleAccumulateLeakModels(ACCUMULATE_LEAK); CONTINUOUS: continuousModels,
APPEND: appendModels,
// 第一次渲染的时候不高亮变化的元素 REMOVE: removeModels,
if (this.isFirstPatch === false) { FREED: freedModels,
this.handleChangeModels(UPDATE); LEAKED: leakModels,
} UPDATE: updateModels,
};
this.handleContinuousModels(CONTINUOUS); }
this.handleFreedModels(FREED);
this.handleAppendModels(APPEND); /**
this.handleLeakModels(LEAKED); *
this.handleRemoveModels(REMOVE); * @param diffResult
* @param isFirstRender
if (this.isFirstPatch) { */
this.isFirstPatch = false; public patch(diffResult: DiffResult, handleUpdate: handleUpdate) {
} const { APPEND, REMOVE, FREED, LEAKED, UPDATE, CONTINUOUS } = diffResult;
}
// 第一次渲染和进入函数的时候不高亮变化的元素
public destroy() { } if (this.isFirstPatch === false && !handleUpdate?.isEnterFunction && !handleUpdate?.isFirstDebug) {
} 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,16 +4,14 @@ 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;
@ -194,4 +192,4 @@ export class Renderer {
this.shadowG6Instance.destroy(); this.shadowG6Instance.destroy();
this.g6Instance.destroy(); this.g6Instance.destroy();
} }
} }

View File

@ -1,20 +1,25 @@
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';
import { handleUpdate } from '../sources';
export class ViewContainer { export class ViewContainer {
<<<<<<< HEAD
private engine: Engine; private engine: Engine;
private layoutProvider: LayoutProvider; private layoutProvider: LayoutProvider;
private reconcile: Reconcile; private reconcile: Reconcile;
@ -129,98 +134,239 @@ export class ViewContainer {
this.renderer.refresh(); this.renderer.refresh();
this.leakAreaY += dy; this.leakAreaY += dy;
=======
private engine: Engine;
private layoutProvider: LayoutProvider;
private reconcile: Reconcile;
public renderer: Renderer;
private layoutGroupTable: LayoutGroupTable;
private prevModelList: SVModel[];
private accumulateLeakModels: SVModel[];
public hasLeak: boolean;
public leakAreaY: number;
public lastLeakAreaTranslateY: number;
public brushSelectedModels: SVModel[]; // 保存框选过程中被选中的节点
public clickSelectNode: SVNode; // 点击选中的节点
constructor(engine: Engine, DOMContainer: HTMLElement) {
const behaviorsModes: Modes = InitG6Behaviors(engine, this);
this.engine = engine;
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;
const g6Instance = this.renderer.getG6Instance(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
height = g6Instance.getHeight(),
{ drag, zoom } = this.engine.behaviorOptions;
this.leakAreaY = height - leakAreaHeight;
this.lastLeakAreaTranslateY = 0;
SolveNodeAppendagesDrag(this);
SolveBrushSelectDrag(this);
drag && SolveDragCanvasWithLeak(this);
zoom && SolveZoomCanvasWithLeak(this);
}
// ----------------------------------------------------------------------------------------------
/**
*
*/
reLayout() {
const g6Instance = this.getG6Instance(),
group = g6Instance.getGroup(),
matrix = group.getMatrix(),
bound = group.getCanvasBBox();
const { duration, enable, timingFunction } = this.engine.animationOptions;
if (matrix) {
let dx = matrix[6],
dy = matrix[7];
g6Instance.moveTo(bound.minX - dx, bound.minY - dy, enable, {
duration,
easing: timingFunction,
});
}
const leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
height = g6Instance.getHeight();
this.leakAreaY = height - leakAreaHeight;
this.lastLeakAreaTranslateY = 0;
this.layoutProvider.layoutAll(this.layoutGroupTable, this.accumulateLeakModels);
g6Instance.refresh();
>>>>>>> eac9d007bcc1b52fe573ddf6cb97030c9b2d3a6d
EventBus.emit('onLeakAreaUpdate', { EventBus.emit('onLeakAreaUpdate', {
leakAreaY: this.leakAreaY, leakAreaY: this.leakAreaY,
hasLeak: this.hasLeak hasLeak: this.hasLeak,
}); });
} }
/** /**
* * g6
* @param models */
* @param layoutFn getG6Instance(): Graph {
*/ return this.renderer.getG6Instance();
render(layoutGroupTable: LayoutGroupTable) { }
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable),
diffResult = this.reconcile.diff(this.layoutGroupTable, this.prevModelList, modelList, this.accumulateLeakModels),
renderModelList = [
...modelList,
...diffResult.REMOVE,
...diffResult.LEAKED,
...diffResult.ACCUMULATE_LEAK
];
if (this.hasLeak === true && this.accumulateLeakModels.length === 0) { /**
this.hasLeak = false; *
EventBus.emit('onLeakAreaUpdate', { * @returns
leakAreaY: this.leakAreaY, */
hasLeak: this.hasLeak getAccumulateLeakModels(): SVModel[] {
}); return this.accumulateLeakModels;
} }
if (diffResult.LEAKED.length) { /**
this.hasLeak = true; *
EventBus.emit('onLeakAreaUpdate', { */
leakAreaY: this.leakAreaY, getLayoutGroupTable(): LayoutGroupTable {
hasLeak: this.hasLeak return this.layoutGroupTable;
}); }
}
this.renderer.build(renderModelList); // 首先在离屏canvas渲染先 /**
this.layoutProvider.layoutAll(layoutGroupTable, this.accumulateLeakModels, diffResult.LEAKED); // 进行布局设置model的xy样式等 *
*/
refresh() {
this.renderer.getG6Instance().refresh();
}
this.beforeRender(); /**
this.renderer.render(renderModelList); // 渲染视图 *
this.reconcile.patch(diffResult); // 对视图上的某些变化进行对应的动作,比如:节点创建动画,节点消失动画等 * @param width
this.afterRender(); * @param height
*/
resize(width: number, height: number) {
const g6Instance = this.getG6Instance(),
prevContainerHeight = g6Instance.getHeight(),
globalGroup: Group = new Group();
this.accumulateLeakModels.push(...diffResult.LEAKED); // 对泄漏节点进行累积 globalGroup.add(...this.prevModelList, ...this.accumulateLeakModels);
this.renderer.changeSize(width, height);
this.layoutGroupTable = layoutGroupTable; const containerHeight = g6Instance.getHeight(),
this.prevModelList = modelList; dy = containerHeight - prevContainerHeight;
}
/** globalGroup.translate(0, dy);
* this.renderer.refresh();
*/
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.leakAreaY += dy;
EventBus.emit('onLeakAreaUpdate', {
leakAreaY: this.leakAreaY,
hasLeak: this.hasLeak,
});
}
// ------------------------------------------------------------------------------ /**
*
* @param models
*/
private restoreHighlight(models: SVModel[]) {
models.forEach(item => {
// 不是free节点才进行还原
if (!item.freed) {
item.restoreHighlight();
}
});
}
/**
*
* @param models
* @param layoutFn
*/
render(layoutGroupTable: LayoutGroupTable, isSameSources: boolean, handleUpdate: handleUpdate) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
/** this.restoreHighlight([...modelList, ...this.accumulateLeakModels]);
*
*/
private afterRender() {
this.prevModelList.forEach(item => {
if (item.leaked === false) {
item.discarded = true;
}
});
}
/** // 如果数据没变的话,直接退出
* if (isSameSources) {
*/ return;
private beforeRender() { } }
const diffResult = this.reconcile.diff(
this.layoutGroupTable,
this.prevModelList,
modelList,
this.accumulateLeakModels,
handleUpdate?.isEnterFunction
),
renderModelList = [...modelList, ...diffResult.REMOVE, ...diffResult.LEAKED, ...this.accumulateLeakModels];
// 从有泄漏区变成无泄漏区
if (this.hasLeak === true && this.accumulateLeakModels.length === 0) {
this.hasLeak = false;
EventBus.emit('onLeakAreaUpdate', {
leakAreaY: this.leakAreaY,
hasLeak: this.hasLeak,
});
}
// 从无泄漏区变成有泄漏区
if (diffResult.LEAKED.length) {
this.hasLeak = true;
EventBus.emit('onLeakAreaUpdate', {
leakAreaY: this.leakAreaY,
hasLeak: this.hasLeak,
});
}
this.accumulateLeakModels.push(...diffResult.LEAKED); // 对泄漏节点进行向后累积
this.renderer.build(renderModelList); // 首先在离屏canvas渲染先
this.layoutProvider.layoutAll(layoutGroupTable, this.accumulateLeakModels); // 进行布局设置model的xy样式等
this.beforeRender();
this.renderer.render(renderModelList); // 渲染视图
this.reconcile.patch(diffResult, handleUpdate); // 对视图上的某些变化进行对应的动作,比如:节点创建动画,节点消失动画等
this.afterRender();
this.layoutGroupTable = layoutGroupTable;
this.prevModelList = modelList;
}
/**
*
*/
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

@ -1,4 +1,4 @@
import { Sources } from "./sources"; import { handleUpdate, Sources } from "./sources";
import { LayoutGroupTable, ModelConstructor } from "./Model/modelConstructor"; import { LayoutGroupTable, ModelConstructor } from "./Model/modelConstructor";
import { AnimationOptions, BehaviorOptions, EngineOptions, LayoutGroupOptions, ViewOptions } from "./options"; import { AnimationOptions, BehaviorOptions, EngineOptions, LayoutGroupOptions, ViewOptions } from "./options";
import { EventBus } from "./Common/eventBus"; import { EventBus } from "./Common/eventBus";
@ -6,6 +6,7 @@ import { ViewContainer } from "./View/viewContainer";
import { SVNode } from "./Model/SVNode"; import { SVNode } from "./Model/SVNode";
import { Util } from "./Common/util"; import { Util } from "./Common/util";
import { SVModel } from "./Model/SVModel"; import { SVModel } from "./Model/SVModel";
import { G6Event } from "@antv/g6";
export class Engine { export class Engine {
@ -49,9 +50,10 @@ export class Engine {
/** /**
* *
* @param sources * @param sources
* @param force * @param prevStep
*/ */
public render(source: Sources) { public render(source: Sources) {
<<<<<<< HEAD
if (source === undefined || source === null) { if (source === undefined || source === null) {
return; return;
} }
@ -59,15 +61,36 @@ export class Engine {
let stringSource = JSON.stringify(source); let stringSource = JSON.stringify(source);
if (this.prevStringSource === stringSource) { if (this.prevStringSource === stringSource) {
return; return;
=======
let isSameSources: boolean = false,
layoutGroupTable: LayoutGroupTable;
if (source === undefined || source === null) {
return;
}
let handleUpdate: handleUpdate = source.handleUpdate,
stringSource = JSON.stringify(source);
if (this.prevStringSource === stringSource) {
isSameSources = true;
>>>>>>> eac9d007bcc1b52fe573ddf6cb97030c9b2d3a6d
} }
this.prevStringSource = stringSource; this.prevStringSource = stringSource;
// 1 转换模型data => model
const layoutGroupTable = this.modelConstructor.construct(source); if(isSameSources) {
// 若源数据两次一样的用回上一次的layoutGroupTable
layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
}
else {
// 1 转换模型data => model
layoutGroupTable = this.modelConstructor.construct(source);
}
// 2 渲染使用g6进行渲染 // 2 渲染使用g6进行渲染
this.viewContainer.render(layoutGroupTable); this.viewContainer.render(layoutGroupTable, isSameSources, handleUpdate);
} }
@ -192,7 +215,7 @@ export class Engine {
return; return;
} }
this.viewContainer.getG6Instance().on(eventName, event => { this.viewContainer.getG6Instance().on(eventName as G6Event, event => {
callback(event.item['SVModel']); callback(event.item['SVModel']);
}); });
} }

View File

@ -1,4 +1,3 @@
// 连接目标信息 // 连接目标信息
export type LinkTarget = number | string; export type LinkTarget = number | string;
@ -10,17 +9,19 @@ export type sourceMarkerData = string | string[];
// 源数据单元 // 源数据单元
export interface SourceNode { export interface SourceNode {
id: string | number; id: string | number;
[key: string]: any | sourceLinkData | sourceMarkerData; [key: string]: any | sourceLinkData | sourceMarkerData;
} }
export interface handleUpdate {
isEnterFunction: boolean;
isFirstDebug: boolean;
}
export type Sources = { export type Sources = {
[key: string]: { [key: string]: {
data: SourceNode[]; data: SourceNode[];
layouter: string; layouter: string;
} };
handleUpdate?: handleUpdate | any;
}; };

View File

@ -2,7 +2,8 @@
"compilerOptions": { "compilerOptions": {
"target": "ES2015", "target": "ES2015",
"module": "commonJS", "module": "commonJS",
"removeComments": true "removeComments": true,
"lib": ["DOM", "ES2015", "ES2016", "ES2017"]
}, },
"exclude": [ "exclude": [
"node_modules" "node_modules"

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
mode: 'production',
entry: './src/StructV.ts', entry: './src/StructV.ts',
output: { output: {
filename: './sv.js', filename: './sv.js',
@ -17,6 +17,5 @@ module.exports = {
loader: 'ts-loader' loader: 'ts-loader'
} }
] ]
}, }
// devtool: 'eval-source-map'
}; };

3498
yarn.lock

File diff suppressed because it is too large Load Diff