基本重构完成
底层渲染库舍弃 zrender,换为 antvG6
This commit is contained in:
parent
b886f33d9e
commit
72fcf5394a
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
node_modules
|
||||
test
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"cSpell.language": "en"
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "commonJS",
|
||||
"removeComments": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
190
demo/dataStruct/BinaryTree.js
Normal file
190
demo/dataStruct/BinaryTree.js
Normal file
@ -0,0 +1,190 @@
|
||||
const Engine = SV.Engine,
|
||||
Group = SV.Group,
|
||||
Bound = SV.Bound,
|
||||
G6 = SV.G6;
|
||||
|
||||
|
||||
class BinaryTree extends Engine {
|
||||
defineOptions() {
|
||||
return {
|
||||
element: {
|
||||
default: {
|
||||
type: 'binary-tree-node',
|
||||
size: [60, 30],
|
||||
label: '[id]',
|
||||
style: {
|
||||
fill: '#b83b5e'
|
||||
}
|
||||
}
|
||||
},
|
||||
link: {
|
||||
child: {
|
||||
type: 'line',
|
||||
sourceAnchor: index => index + 1,
|
||||
targetAnchor: 0,
|
||||
style: {
|
||||
stroke: '#333',
|
||||
endArrow: {
|
||||
path: G6.Arrow.triangle(8, 6, 0),
|
||||
fill: '#333'
|
||||
},
|
||||
startArrow: {
|
||||
path: G6.Arrow.circle(2, -1),
|
||||
fill: '#333'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pointer: {
|
||||
external: {
|
||||
offset: 14,
|
||||
style: {
|
||||
fill: '#f08a5d'
|
||||
}
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
xInterval: 40,
|
||||
yInterval: 40,
|
||||
fitCenter: true
|
||||
},
|
||||
animation: {
|
||||
enable: true,
|
||||
duration: 750,
|
||||
timingFunction: 'easePolyOut'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 对子树进行递归布局
|
||||
*/
|
||||
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 nodes = elements.default,
|
||||
rootNodes = [],
|
||||
node,
|
||||
root,
|
||||
lastRoot,
|
||||
treeGroup = new Group(),
|
||||
i;
|
||||
|
||||
for(i = 0; i < nodes.length; i++) {
|
||||
node = nodes[i];
|
||||
|
||||
if(node.root) {
|
||||
rootNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
for(i = 0; i < rootNodes.length; i++) {
|
||||
root = rootNodes[i];
|
||||
|
||||
root.subTreeGroup = this.layoutItem(root, null, i, layoutOptions);
|
||||
|
||||
if(lastRoot) {
|
||||
let curBound = root.subTreeGroup.getBound(),
|
||||
lastBound = lastRoot.subTreeGroup.getBound();
|
||||
|
||||
let move = lastBound.x + lastBound.width + layoutOptions.xInterval - curBound.x;
|
||||
root.subTreeGroup.translate(move, 0);
|
||||
}
|
||||
|
||||
lastRoot = root;
|
||||
treeGroup.add(root);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const BTree = function(container) {
|
||||
return{
|
||||
engine: new BinaryTree(container),
|
||||
data: [[
|
||||
{ id: 1, child: [2, 3], root: true, external: ['treeA', 'gear'] },
|
||||
{ id: 2, child: [null, 6] },
|
||||
{ id: 3, child: [5, 4] },
|
||||
{ id: 4, external: 'foo' },
|
||||
{ id: 5 },
|
||||
{ id: 6, external: 'bar', child: [null, 7] },
|
||||
{ id: 7 },
|
||||
{ id: 8, child: [9, 10], root: true },
|
||||
{ id: 9, child: [11, null] },
|
||||
{ id: 10 },
|
||||
{ id: 11 }
|
||||
],
|
||||
[
|
||||
{ id: 1, child: [2, 3], root: true, external: 'treeA' },
|
||||
{ id: 2, external: 'gear' },
|
||||
{ id: 3, child: [5, 4] },
|
||||
{ id: 4, external: 'foo' },
|
||||
{ id: 5, child: [12, 13] },
|
||||
{ id: 12 }, { id: 13 }
|
||||
]]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
45
demo/demo.html
Normal file
45
demo/demo.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DEMO</title>
|
||||
<style>
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" id="container"></div>
|
||||
<button id="btn">change</button>
|
||||
<span id="pos"></span>
|
||||
|
||||
|
||||
<script src="./../dist/sv.js"></script>
|
||||
<script src="./dataStruct/BinaryTree.js"></script>
|
||||
<script src="./demo.js"></script>
|
||||
<script>
|
||||
|
||||
const container = document.getElementById('container'),
|
||||
pos = document.getElementById('pos');
|
||||
|
||||
container.addEventListener('mousemove', e => {
|
||||
let x = e.offsetX, y = e.offsetY;
|
||||
pos.innerHTML = `${x},${y}`;
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
10
demo/demo.js
Normal file
10
demo/demo.js
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
|
||||
|
||||
let cur = BTree(document.getElementById('container'));
|
||||
|
||||
cur.engine.render(cur.data[0]);
|
||||
|
||||
document.getElementById('btn').addEventListener('click', e => {
|
||||
cur.engine.render(cur.data[1]);
|
||||
});
|
2
dist/sv.js
vendored
2
dist/sv.js
vendored
File diff suppressed because one or more lines are too long
@ -1,5 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@antv/g6": "^4.2.1",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"typescript": "^3.2.2",
|
||||
"webpack": "^4.28.2",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as zrender from "zrender";
|
||||
|
||||
|
||||
import { Engine } from "../engine";
|
||||
import { ConstructedData } from "../Model/modelConstructor";
|
||||
import { SV } from "../StructV";
|
||||
import { G6Data, Renderer } from "../View/renderer";
|
||||
|
||||
|
||||
/**
|
||||
@ -8,6 +9,7 @@ import * as zrender from "zrender";
|
||||
*/
|
||||
export const Util = {
|
||||
|
||||
|
||||
/**
|
||||
* 生成唯一id
|
||||
*/
|
||||
@ -19,29 +21,11 @@ export const Util = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 扩展对象
|
||||
* @param origin 原对象
|
||||
* @param ext 扩展的对象
|
||||
* 乞丐版对象克隆
|
||||
* @param obj
|
||||
*/
|
||||
extends(origin, ext) {
|
||||
zrender.util.extend(origin, ext);
|
||||
},
|
||||
|
||||
/**
|
||||
* 合并对象
|
||||
* @param origin
|
||||
* @param dest
|
||||
*/
|
||||
merge(origin, dest) {
|
||||
zrender.util.merge(origin, dest, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* 拷贝对象
|
||||
* @param object
|
||||
*/
|
||||
clone(object) {
|
||||
return zrender.util.clone(object);
|
||||
objectClone<T extends Object>(obj: T): T {
|
||||
return obj? JSON.parse(JSON.stringify(obj)): { };
|
||||
},
|
||||
|
||||
/**
|
||||
@ -55,26 +39,6 @@ export const Util = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 从一个由数组组成的路径中获取几何中心
|
||||
* @param path
|
||||
*/
|
||||
getPathCenter(path: Array<[number, number]>): [number, number] {
|
||||
let maxX = -Infinity,
|
||||
minX = Infinity,
|
||||
maxY = -Infinity,
|
||||
minY = Infinity;
|
||||
|
||||
path.map(item => {
|
||||
if(item[0] > maxX) maxX = item[0];
|
||||
if(item[0] < minX) minX = item[0];
|
||||
if(item[1] > maxY) maxY = item[1];
|
||||
if(item[1] < minY) minY = item[1];
|
||||
});
|
||||
|
||||
return [(maxX + minX) / 2, (maxY + minY) / 2];
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言函数
|
||||
* @param assertFn
|
||||
@ -86,14 +50,6 @@ export const Util = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取类的名称
|
||||
* @param classConstructor
|
||||
*/
|
||||
getClassName(classConstructor): string {
|
||||
return classConstructor.prototype.constructor.toString().split(' ')[1];
|
||||
},
|
||||
|
||||
/**
|
||||
* 文本解析
|
||||
* @param text
|
||||
@ -119,6 +75,41 @@ export const Util = {
|
||||
if(value <= max && value >= min) return value;
|
||||
if(value > max) return max;
|
||||
if(value < min) return min;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param constructedDataType
|
||||
* @returns
|
||||
*/
|
||||
converterList(constructedDataType: ConstructedData[keyof ConstructedData]) {
|
||||
return [].concat(...Object.keys(constructedDataType).map(item => constructedDataType[item]));
|
||||
},
|
||||
|
||||
/**
|
||||
* G6 data 转换器
|
||||
* @param constructedData
|
||||
* @returns
|
||||
*/
|
||||
convertG6Data(constructedData: ConstructedData): G6Data {
|
||||
let nodes = [...Util.converterList(constructedData.element), ...Util.converterList(constructedData.pointer)],
|
||||
edges = Util.converterList(constructedData.link);
|
||||
|
||||
return {
|
||||
nodes: nodes.map(item => item.props),
|
||||
edges: edges.map(item => item.props)
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算旋转矩阵
|
||||
* @param matrix
|
||||
* @param rotation
|
||||
*/
|
||||
calcRotateMatrix(matrix: number[], rotation: number): number[] {
|
||||
const Mat3 = SV.G6.Util.mat3;
|
||||
Mat3.rotate(matrix, matrix, rotation);
|
||||
return matrix;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,219 +1,117 @@
|
||||
|
||||
|
||||
// 二维向量 {x, y}
|
||||
export class Vector {
|
||||
public x: number;
|
||||
public y: number;
|
||||
|
||||
constructor(x?: number, y?: number) {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
|
||||
if(x !== undefined && y !== undefined) {
|
||||
this.set(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
//-------------操作----------------
|
||||
export const Vector = {
|
||||
|
||||
/**
|
||||
* 设置值
|
||||
* @param x
|
||||
* @param y
|
||||
* 向量相加
|
||||
* @param v1
|
||||
* @param v2
|
||||
*/
|
||||
set(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
add(v1: [number, number], v2: [number, number]): [number, number] {
|
||||
return [v1[0] + v2[0], v1[1] + v2[1]];
|
||||
},
|
||||
|
||||
/**
|
||||
* 相加
|
||||
* 向量相减
|
||||
* @param v1
|
||||
* @param v2
|
||||
*/
|
||||
subtract(v1: [number, number], v2: [number, number]): [number, number] {
|
||||
return [v1[0] - v2[0], v1[1] - v2[1]];
|
||||
},
|
||||
|
||||
/**
|
||||
* 向量点积
|
||||
* @param v1
|
||||
* @param v2
|
||||
*/
|
||||
dot(v1: [number, number], v2: [number, number]): number {
|
||||
return v1[0] * v2[0] + v1[1] * v2[1];
|
||||
},
|
||||
|
||||
/**
|
||||
* 向量缩放
|
||||
* @param v
|
||||
* @param n
|
||||
*/
|
||||
scale(v: [number, number], n: number): [number, number] {
|
||||
return [v[0] * n, v[1] * n];
|
||||
},
|
||||
|
||||
/**
|
||||
* 向量求模长
|
||||
* @param v
|
||||
*/
|
||||
add(v: Vector, out?: Vector): Vector {
|
||||
out = out || new Vector();
|
||||
|
||||
out.x = this.x + v.x;
|
||||
out.y = this.y + v.y;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 相减
|
||||
* @param v
|
||||
*/
|
||||
sub(v: Vector, out?: Vector): Vector {
|
||||
out = out || new Vector();
|
||||
|
||||
out.x = this.x - v.x;
|
||||
out.y = this.y - v.y;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点积
|
||||
* @param v
|
||||
*/
|
||||
dot(v: Vector): number {
|
||||
return this.x * v.x + this.y * v.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 叉积
|
||||
* @param v
|
||||
*/
|
||||
cro(v: Vector): number {
|
||||
return this.x * v.y - v.x * this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 与标量进行叉积
|
||||
* @param n
|
||||
*/
|
||||
croNum(n: number, out?: Vector): Vector {
|
||||
out = out || new Vector();
|
||||
|
||||
out.x = -n * this.y;
|
||||
out.y = n * this.x;
|
||||
|
||||
return out;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 投影
|
||||
* @param v
|
||||
*/
|
||||
pro(v: Vector): number {
|
||||
return this.dot(v) / v.len();
|
||||
}
|
||||
|
||||
/**
|
||||
* 法向
|
||||
*/
|
||||
nor(out?: Vector): Vector {
|
||||
out = out || new Vector();
|
||||
|
||||
out.x = this.y;
|
||||
out.y = -this.x;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 求模
|
||||
*/
|
||||
len(): number {
|
||||
return Math.hypot(this.x, this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平方模(节省求平方根操作)
|
||||
*/
|
||||
len_s(): number {
|
||||
return this.x * this.x + this.y * this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位化
|
||||
*/
|
||||
nol(): Vector {
|
||||
let len = this.len();
|
||||
|
||||
if(len === 0) {
|
||||
return new Vector();
|
||||
}
|
||||
|
||||
this.x = this.x / len;
|
||||
this.y = this.y / len;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放
|
||||
* @param n
|
||||
*/
|
||||
scl(n: number, out?: Vector): Vector {
|
||||
out = out || new Vector();
|
||||
out.x = n * this.x;
|
||||
out.y = n * this.y;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反向
|
||||
*/
|
||||
inv(out?: Vector): Vector {
|
||||
out = out || new Vector();
|
||||
out.x = -this.x;
|
||||
out.y = -this.y;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两向量是否相等
|
||||
* @param v
|
||||
*/
|
||||
eql(v: Vector): boolean {
|
||||
return this.x === v.x && this.y === v.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 求两向量夹角(弧度制)
|
||||
* @param v
|
||||
*/
|
||||
ang(v: Vector): number {
|
||||
return Math.acos(this.dot(v) / (this.len() * v.len()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆向量
|
||||
*/
|
||||
col(): Vector {
|
||||
return new Vector(this.x, this.y);
|
||||
}
|
||||
length(v: [number, number]): number {
|
||||
return Math.sqrt(v[0]**2 + v[1]**2);
|
||||
},
|
||||
|
||||
/**
|
||||
* 绕某点旋转向量
|
||||
* @param radian 角度(弧度制)
|
||||
* @param point 绕的点
|
||||
* @param point 旋转的点
|
||||
* @param center 绕的点
|
||||
*/
|
||||
rot(radian: number, point: Vector, out?: Vector): Vector {
|
||||
out = out || new Vector();
|
||||
rotation(radian: number, point: [number, number], center: [number, number] = [0, 0]): [number, number] {
|
||||
if(radian === 0) return point;
|
||||
|
||||
radian = -radian;
|
||||
|
||||
let cos = Math.cos(radian),
|
||||
sin = Math.sin(radian),
|
||||
dx = this.x - point.x,
|
||||
dy = this.y - point.y;
|
||||
|
||||
out.x = point.x + (dx * cos - dy * sin);
|
||||
out.y = point.y + (dx * sin + dy * cos);
|
||||
dv = [point[0] - center[0], point[1] - center[1]],
|
||||
v = [0, 0];
|
||||
|
||||
v[0] = center[0] + (dv[0] * cos - dv[1] * sin);
|
||||
v[1] = center[1] + (dv[0] * sin + dv[1] * cos);
|
||||
|
||||
return out;
|
||||
}
|
||||
return v as [number, number];
|
||||
},
|
||||
|
||||
/**
|
||||
* 求向量法向
|
||||
*/
|
||||
tangent(v: [number, number]): [number, number] {
|
||||
return [-v[1], v[0]];
|
||||
},
|
||||
|
||||
/**
|
||||
* 向量单位化
|
||||
*/
|
||||
normalize(v: [number, number]): [number, number] {
|
||||
let len = Vector.length(v);
|
||||
|
||||
if(len === 0) {
|
||||
return [0, 0];
|
||||
}
|
||||
else if(len === 1) {
|
||||
return v;
|
||||
}
|
||||
else {
|
||||
return [v[0] / len, v[1] / len];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 求一个向量(点)按照direction方向,延长len长度后的坐标
|
||||
* @param v
|
||||
* @param direction
|
||||
* @param len
|
||||
*/
|
||||
loc(direction: Vector, len: number, out?: Vector): Vector {
|
||||
out = out || new Vector();
|
||||
location(v: [number, number], direction: [number, number], len: number): [number, number] {
|
||||
return Vector.add(v, Vector.scale(Vector.normalize(direction), len));
|
||||
},
|
||||
|
||||
direction = direction.nol();
|
||||
out.x = this.x + direction.x * len;
|
||||
out.y = this.y + direction.y * len;
|
||||
|
||||
return out;
|
||||
/**
|
||||
* 向量取反
|
||||
*/
|
||||
negative(v: [number, number]): [number, number] {
|
||||
return Vector.scale(v, -1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const _tempVector1 = new Vector();
|
||||
export const _tempVector2 = new Vector();
|
||||
export const _tempVector3 = new Vector();
|
||||
export const _tempVector4 = new Vector();
|
||||
|
||||
|
||||
|
||||
|
31
src/Lib/g6.js
Normal file
31
src/Lib/g6.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,163 +0,0 @@
|
||||
import { Util } from "../Common/util";
|
||||
import { SourceElement } from "../sources";
|
||||
import { Shape, ShapeStatus } from "../View/shape";
|
||||
import { zrShape } from "../View/shapeScheduler";
|
||||
import { Link } from "./link";
|
||||
import { Pointer } from "./pointer";
|
||||
|
||||
|
||||
export interface Style {
|
||||
// 填充颜色
|
||||
fill: string;
|
||||
// 图形文本
|
||||
text: string;
|
||||
// 文本颜色
|
||||
textFill: string;
|
||||
// 字体大小
|
||||
fontSize: number;
|
||||
// 字重
|
||||
fontWeight: number;
|
||||
// 描边样式
|
||||
stroke: string;
|
||||
// 透明度
|
||||
opacity: number;
|
||||
// 线宽
|
||||
lineWidth: number;
|
||||
// 其余属性(免得每次新增属性都要声明一个新的子interface)
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface ElementStatus {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
width: number;
|
||||
height: number;
|
||||
zIndex: number;
|
||||
content: string;
|
||||
style: Style;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export class Element {
|
||||
id: number;
|
||||
elementId: string = null;
|
||||
elementLabel: string = null;
|
||||
|
||||
elementStatus: ElementStatus = null;
|
||||
shapes: Shape[] | Shape = null;
|
||||
relativeLinks: Link[] = [];
|
||||
relativePointers: Pointer[] = [];
|
||||
|
||||
isDirty: boolean = false;
|
||||
|
||||
// 给sourceElement的部分
|
||||
[key: string]: any;
|
||||
|
||||
constructor(elementLabel: string, sourceElement: SourceElement) {
|
||||
this.elementLabel = elementLabel;
|
||||
|
||||
Object.keys(sourceElement).map(prop => {
|
||||
this[prop] = sourceElement[prop];
|
||||
});
|
||||
|
||||
this.elementStatus = {
|
||||
x: 0, y: 0,
|
||||
rotation: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
zIndex: 1,
|
||||
content: '',
|
||||
style: {
|
||||
fill: '#000',
|
||||
text: '',
|
||||
textFill: '#000',
|
||||
fontSize: 15,
|
||||
fontWeight: null,
|
||||
stroke: null,
|
||||
opacity: 1,
|
||||
transformText: true,
|
||||
lineWidth: 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* set方法,设置elementStatus的值
|
||||
* @param propName
|
||||
* @param value
|
||||
*/
|
||||
set(propName: string, value: any) {
|
||||
if(this.elementStatus[propName] !== undefined) {
|
||||
this.elementStatus[propName] = value;
|
||||
}
|
||||
|
||||
this.setDirty(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 元素是否为脏
|
||||
* @param isDirty
|
||||
*/
|
||||
setDirty(isDirty: boolean) {
|
||||
this.isDirty = isDirty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义该element映射的图形
|
||||
* @param shapes
|
||||
* @param elementStatus
|
||||
* @override
|
||||
*/
|
||||
renderShape(shapes: Shape[] | Shape, elementStatus: ElementStatus) { }
|
||||
|
||||
/**
|
||||
* 应用shapeOptions
|
||||
* @param shapeOptions
|
||||
*/
|
||||
applyShapeOptions(shapeOptions: Partial<ShapeStatus>) {
|
||||
Util.extends(this.elementStatus, shapeOptions);
|
||||
}
|
||||
|
||||
// ------------------------------- 钩子方法 ------------------------------
|
||||
|
||||
onClick(event: any) { }
|
||||
|
||||
/**
|
||||
* 当结点连接其他结点触发
|
||||
* @param targetEle
|
||||
*/
|
||||
onLinkTo(targetEle: Element) {};
|
||||
|
||||
/**
|
||||
* 当结点被其他结点连接时触发
|
||||
* @param emitEle
|
||||
*/
|
||||
onLinkFrom(emitEle: Element) {};
|
||||
|
||||
/**
|
||||
* 当结点断开与其他结点触发
|
||||
* @param targetEle
|
||||
*/
|
||||
onUnLinkTo(targetEle: Element) {}
|
||||
|
||||
/**
|
||||
* 当结点被其他结点断开连接时触发
|
||||
* @param emitEle
|
||||
*/
|
||||
onUnLinkFrom(emitEle: Element) {}
|
||||
|
||||
/**
|
||||
* 当指向结点时触发
|
||||
*/
|
||||
onRefer() {}
|
||||
|
||||
/**
|
||||
* 当指针离开该结点触发
|
||||
*/
|
||||
onUnRefer() {}
|
||||
};
|
@ -1,165 +0,0 @@
|
||||
import { Engine } from "../engine";
|
||||
import { SourceElement, Sources } from "../sources";
|
||||
import { Shape, ShapeStatus } from "../View/shape";
|
||||
import { ShapeScheduler, ZrShapeConstructor } from "../View/shapeScheduler";
|
||||
import { Element } from "./element";
|
||||
|
||||
|
||||
// 元素集类型
|
||||
export type ElementContainer = { [key: string]: Element[] };
|
||||
export type ElementConstructor = { new(elementLabel: string, sourceElement: SourceElement): Element };
|
||||
|
||||
|
||||
export class ElementScheduler {
|
||||
private engine: Engine;
|
||||
|
||||
private shapeScheduler: ShapeScheduler;
|
||||
// 元素队列
|
||||
private elementList: Element[] = [];
|
||||
// 元素容器,即源数据经element包装后的结构
|
||||
private elementContainer: ElementContainer = {};
|
||||
|
||||
private elementMap: {
|
||||
[key: string]: {
|
||||
elementConstructor: ElementConstructor,
|
||||
zrShapeConstructors: ZrShapeConstructor[] | ZrShapeConstructor,
|
||||
shapeOptions: Partial<ShapeStatus>
|
||||
}
|
||||
};
|
||||
|
||||
constructor(engine: Engine, shapeScheduler: ShapeScheduler) {
|
||||
this.engine = engine;
|
||||
this.shapeScheduler = shapeScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个元素的信息
|
||||
* @param elementLabel
|
||||
* @param elementConstructor
|
||||
* @param zrShapeConstructors
|
||||
* @param shapeOptions
|
||||
*/
|
||||
setElementMap(
|
||||
elementLabel: string,
|
||||
elementConstructor: ElementConstructor,
|
||||
zrShapeConstructors: ZrShapeConstructor[] | ZrShapeConstructor,
|
||||
shapeOptions: Partial<ShapeStatus>
|
||||
) {
|
||||
this.elementMap[elementLabel] = {
|
||||
elementConstructor,
|
||||
zrShapeConstructors,
|
||||
shapeOptions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从源数据构建 element 集
|
||||
* 主要工作:
|
||||
* - 遍历源数据,将每个 SourceElement 转化为 Element
|
||||
* - 处理连接
|
||||
* - 处理指针
|
||||
* @param sourceData
|
||||
*/
|
||||
constructElements(sourceData: Sources) {
|
||||
if(Array.isArray(sourceData)) {
|
||||
this.elementContainer['element'] = [];
|
||||
sourceData.forEach(item => {
|
||||
if(item) {
|
||||
let ele = this.createElement(item, 'element');
|
||||
this.elementContainer['element'].push(ele);
|
||||
this.elementList.push(ele);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
Object.keys(sourceData).forEach(prop => {
|
||||
this.elementContainer[prop] = [];
|
||||
sourceData[prop].forEach(item => {
|
||||
if(item) {
|
||||
let ele = this.createElement(item, prop);
|
||||
this.elementContainer[prop].push(ele);
|
||||
this.elementList.push(ele);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 元素工厂,创建Element
|
||||
* @param sourceElement
|
||||
* @param elementLabel
|
||||
*/
|
||||
private createElement(sourceElement: SourceElement, elementLabel: string): Element {
|
||||
let elementInfo = this.elementMap[elementLabel],
|
||||
shapes: Shape[] | Shape;
|
||||
|
||||
if(elementInfo === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { elementConstructor, zrShapeConstructors, shapeOptions } = elementInfo,
|
||||
element: Element = null,
|
||||
elementId = `${elementLabel}#${sourceElement.id}`;
|
||||
|
||||
element = new elementConstructor(elementLabel, sourceElement);
|
||||
element.applyShapeOptions(shapeOptions);
|
||||
|
||||
if(Array.isArray(zrShapeConstructors)) {
|
||||
shapes = zrShapeConstructors.map(function(item, index) {
|
||||
return this.shapeScheduler.createShape(`${elementId}(${index})`, item, element)
|
||||
});
|
||||
|
||||
this.shapeScheduler.packShapes(shapes);
|
||||
}
|
||||
else {
|
||||
shapes = this.shapeScheduler.createShape(`elementId`, zrShapeConstructors, element);
|
||||
}
|
||||
|
||||
element.shapes = shapes;
|
||||
element.renderShape(shapes, element.elementStatus);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新element对应的图形
|
||||
*/
|
||||
public updateShapes() {
|
||||
for(let i = 0; i < this.elementList.length; i++) {
|
||||
let ele = this.elementList[i];
|
||||
|
||||
if(ele.isDirty) {
|
||||
ele.renderShape(ele.zrShapes, ele.elementStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有element元素
|
||||
*/
|
||||
public getElementContainer(): ElementContainer | Element[] {
|
||||
let keys = Object.keys(this.elementContainer);
|
||||
|
||||
if(keys.length === 1 && keys[0] === 'element') {
|
||||
return this.elementContainer['element'];
|
||||
}
|
||||
|
||||
return this.elementContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取element列表
|
||||
*/
|
||||
public getElementList(): Element[] {
|
||||
return this.elementList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
public reset() {
|
||||
this.elementList.length = 0;
|
||||
this.elementContainer = { };
|
||||
}
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
import { LinkTarget } from "../sources";
|
||||
import { Shape } from "../View/shape";
|
||||
import { zrShape } from "../View/shapeScheduler";
|
||||
import { Element, ElementStatus, Style } from "./element";
|
||||
import { LabelStyle } from "./pointer";
|
||||
|
||||
|
||||
|
||||
export interface LinkOptions {
|
||||
style: Style;
|
||||
labelStyle: LabelStyle;
|
||||
};
|
||||
|
||||
|
||||
export interface LinkStatus extends ElementStatus {
|
||||
points: [number, number][];
|
||||
};
|
||||
|
||||
|
||||
export class Link {
|
||||
id: string;
|
||||
element: Element = null;
|
||||
target: Element = null;
|
||||
linkLabel: string = null;
|
||||
linkStatus: LinkStatus;
|
||||
|
||||
shapes: Shape[] | Shape = [];
|
||||
index: number = -1;
|
||||
sourceLinkTarget: LinkTarget = null;
|
||||
|
||||
isDirty: boolean = false;
|
||||
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* 元素是否为脏
|
||||
* @param isDirty
|
||||
*/
|
||||
setDirty(isDirty: boolean) {
|
||||
this.isDirty = isDirty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义该element映射的图形
|
||||
* @param shapes
|
||||
* @param elementStatus
|
||||
* @override
|
||||
*/
|
||||
renderShape(shapes: Shape[] | Shape, elementStatus: ElementStatus) { }
|
||||
};
|
@ -1,69 +0,0 @@
|
||||
import { ShapeStatus } from "../View/shape";
|
||||
import { ZrShapeConstructor } from "../View/shapeScheduler";
|
||||
import { Element } from "./element";
|
||||
import { Link } from "./link";
|
||||
|
||||
|
||||
|
||||
|
||||
export class LinkScheduler {
|
||||
private links: Link[] = [];
|
||||
private prevLinks: Link[] = [];
|
||||
|
||||
private linkMap: {
|
||||
[key: string]: {
|
||||
linkConstructor: { new(): Link },
|
||||
zrShapeConstructors: ZrShapeConstructor[] | ZrShapeConstructor,
|
||||
shapeOptions: Partial<ShapeStatus>
|
||||
}
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建链接模型
|
||||
* @param elementList
|
||||
*/
|
||||
public constructLinks(elementList: Element[]) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param linkLabel
|
||||
* @param linkConstructor
|
||||
* @param zrShapeConstructors
|
||||
* @param shapeOptions
|
||||
*/
|
||||
public setLinkMap(
|
||||
linkLabel: string,
|
||||
linkConstructor: { new(): Link },
|
||||
zrShapeConstructors: ZrShapeConstructor[] | ZrShapeConstructor,
|
||||
shapeOptions: Partial<ShapeStatus>
|
||||
) {
|
||||
this.linkMap[linkLabel] = {
|
||||
linkConstructor,
|
||||
zrShapeConstructors,
|
||||
shapeOptions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新element对应的图形
|
||||
*/
|
||||
public updateShapes() {
|
||||
for(let i = 0; i < this.links.length; i++) {
|
||||
let link = this.links[i];
|
||||
|
||||
if(link.isDirty) {
|
||||
link.renderShape(link.shapes, link.linkStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.links.length = 0;
|
||||
}
|
||||
}
|
243
src/Model/modelConstructor.ts
Normal file
243
src/Model/modelConstructor.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import { Util } from "../Common/util";
|
||||
import { Engine } from "../engine";
|
||||
import { LinkOption, PointerOption } from "../options";
|
||||
import { sourceLinkData, SourceElement, Sources, LinkTarget } from "../sources";
|
||||
import { Element, Link, Pointer } from "./modelData";
|
||||
|
||||
|
||||
export interface ConstructedData {
|
||||
element: { [key: string]: Element[] };
|
||||
link: { [key: string]: Link[] };
|
||||
pointer: { [key: string]: Pointer[] };
|
||||
};
|
||||
|
||||
|
||||
export class ModelConstructor {
|
||||
private engine: Engine;
|
||||
|
||||
constructor(engine: Engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建element,link和pointer
|
||||
* @param sourceData
|
||||
*/
|
||||
public construct(sourceData: Sources): ConstructedData {
|
||||
let elementContainer = this.constructElements(sourceData),
|
||||
linkContainer = this.constructLinks(this.engine.linkOptions, elementContainer),
|
||||
pointerContainer = this.constructPointers(this.engine.pointerOptions, elementContainer);
|
||||
|
||||
return {
|
||||
element: elementContainer,
|
||||
link: linkContainer,
|
||||
pointer: pointerContainer
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从源数据构建 element 集
|
||||
* @param sourceData
|
||||
*/
|
||||
private constructElements(sourceData: Sources): { [key: string]: Element[] } {
|
||||
let defaultElementName: string = 'default',
|
||||
elementContainer: { [key: string]: Element[] } = { };
|
||||
|
||||
if(Array.isArray(sourceData)) {
|
||||
elementContainer[defaultElementName] = [];
|
||||
sourceData.forEach(item => {
|
||||
if(item) {
|
||||
let ele = this.createElement(item, defaultElementName);
|
||||
elementContainer[defaultElementName].push(ele);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
Object.keys(sourceData).forEach(prop => {
|
||||
elementContainer[prop] = [];
|
||||
sourceData[prop].forEach(item => {
|
||||
if(item) {
|
||||
let element = this.createElement(item, prop);
|
||||
elementContainer[prop].push(element);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return elementContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从配置和 element 集构建 link 集
|
||||
* @param linkOptions
|
||||
* @param elementContainer
|
||||
* @returns
|
||||
*/
|
||||
private constructLinks(linkOptions: { [key: string]: LinkOption }, elementContainer: { [key: string]: Element[] }): { [key: string]: Link[] } {
|
||||
let linkContainer: { [key: string]: Link[] } = { },
|
||||
elementList: Element[] = Object
|
||||
.keys(elementContainer)
|
||||
.map(item => elementContainer[item])
|
||||
.reduce((prev, cur) => [...prev, ...cur]),
|
||||
linkNames = Object.keys(linkOptions);
|
||||
|
||||
linkNames.forEach(name => {
|
||||
linkContainer[name] = [];
|
||||
});
|
||||
|
||||
linkNames.forEach(name => {
|
||||
for(let i = 0; i < elementList.length; i++) {
|
||||
let element: Element = elementList[i],
|
||||
sourceLinkData: sourceLinkData = element[name],
|
||||
options: LinkOption = linkOptions[name],
|
||||
targetElement: Element | Element[] = null,
|
||||
link: Link = null;
|
||||
|
||||
if(sourceLinkData === undefined || sourceLinkData === null) continue;
|
||||
|
||||
// ------------------- 将连接声明字段 sourceLinkData 从 id 变为 Element -------------------
|
||||
if(Array.isArray(sourceLinkData)) {
|
||||
element[name] = sourceLinkData.map((item, index) => {
|
||||
targetElement = this.fetchTargetElements(elementContainer, element, item);
|
||||
|
||||
if(targetElement) {
|
||||
link = new Link(name, element, targetElement, index);
|
||||
linkContainer[name].push(link);
|
||||
link.initProps(options);
|
||||
}
|
||||
|
||||
return targetElement;
|
||||
});
|
||||
}
|
||||
else {
|
||||
targetElement = this.fetchTargetElements(elementContainer, element, sourceLinkData);
|
||||
|
||||
if(targetElement) {
|
||||
link = new Link(name, element, targetElement, null);
|
||||
linkContainer[name].push(link);
|
||||
link.initProps(options);
|
||||
}
|
||||
|
||||
element[name] = targetElement;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return linkContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从配置和 element 集构建 pointer 集
|
||||
* @param pointerOptions
|
||||
* @param elementContainer
|
||||
* @returns
|
||||
*/
|
||||
private constructPointers(pointerOptions: { [key: string]: PointerOption }, elementContainer: { [key: string]: Element[] }): { [key: string]: Pointer[] } {
|
||||
let pointerContainer: { [key: string]: Pointer[] } = { },
|
||||
elementList: Element[] = Object
|
||||
.keys(elementContainer)
|
||||
.map(item => elementContainer[item])
|
||||
.reduce((prev, cur) => [...prev, ...cur]),
|
||||
pointerNames = Object.keys(pointerOptions);
|
||||
|
||||
pointerNames.forEach(name => {
|
||||
pointerContainer[name] = [];
|
||||
});
|
||||
|
||||
pointerNames.forEach(name => {
|
||||
let options = pointerOptions[name];
|
||||
|
||||
for(let i = 0; i < elementList.length; i++) {
|
||||
let element = elementList[i],
|
||||
pointerData = element[name];
|
||||
|
||||
// 若没有指针字段的结点则跳过
|
||||
if(!pointerData) continue;
|
||||
|
||||
let id = name + '#' + (Array.isArray(pointerData)? pointerData.join('-'): pointerData),
|
||||
pointer = new Pointer(id, name, pointerData, element);
|
||||
|
||||
pointer.initProps(options);
|
||||
pointerContainer[name].push(pointer);
|
||||
}
|
||||
});
|
||||
|
||||
return pointerContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 元素工厂,创建Element
|
||||
* @param sourceElement
|
||||
* @param elementName
|
||||
*/
|
||||
private createElement(sourceElement: SourceElement, elementName: string): Element {
|
||||
let elementOption = this.engine.elementOptions[elementName],
|
||||
element = new Element(elementName, sourceElement),
|
||||
label = elementOption.label? this.parserElementContent(sourceElement, elementOption.label): '';
|
||||
|
||||
element.initProps(elementOption);
|
||||
element.set('label', label);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析元素文本内容
|
||||
* @param sourceElement
|
||||
* @param formatLabel
|
||||
*/
|
||||
private parserElementContent(sourceElement: SourceElement, formatLabel: string): string {
|
||||
let fields = Util.textParser(formatLabel);
|
||||
|
||||
if(Array.isArray(fields)) {
|
||||
let values = fields.map(item => sourceElement[item]);
|
||||
|
||||
values.map((item, index) => {
|
||||
formatLabel = formatLabel.replace('[' + fields[index] + ']', item);
|
||||
});
|
||||
}
|
||||
|
||||
return formatLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 由source中的连接字段获取真实的连接目标元素
|
||||
* @param elementContainer
|
||||
* @param element
|
||||
* @param linkTarget
|
||||
*/
|
||||
private fetchTargetElements(
|
||||
elementContainer: { [key: string]: Element[] } ,
|
||||
element: Element,
|
||||
linkTarget: LinkTarget
|
||||
): Element {
|
||||
let elementName = element.name,
|
||||
elementList: Element[],
|
||||
targetId = linkTarget,
|
||||
targetElement = null;
|
||||
|
||||
if(linkTarget === null || linkTarget === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(typeof linkTarget === 'string' && linkTarget.includes('#')) {
|
||||
let info = linkTarget.split('#');
|
||||
elementName = info[0];
|
||||
targetId = info[1];
|
||||
}
|
||||
|
||||
if(typeof targetId === 'number') {
|
||||
targetId = targetId.toString();
|
||||
}
|
||||
|
||||
elementList = elementContainer[elementName];
|
||||
|
||||
// 若目标element不存在,返回null
|
||||
if(elementList === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
targetElement = elementList.find(item => item.id === targetId);
|
||||
return targetElement || null;
|
||||
}
|
||||
};
|
232
src/Model/modelData.ts
Normal file
232
src/Model/modelData.ts
Normal file
@ -0,0 +1,232 @@
|
||||
import { Util } from "../Common/util";
|
||||
import { ElementLabelOption, ElementOption, LinkLabelOption, LinkOption, PointerOption, Style } from "../options";
|
||||
import { SourceElement } from "../sources";
|
||||
import { BoundingRect } from "../View/boundingRect";
|
||||
|
||||
|
||||
export interface G6NodeModel {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
type: string;
|
||||
size: number | [number, number];
|
||||
anchorPoints: [number, number];
|
||||
label: string;
|
||||
style: Style;
|
||||
labelCfg: ElementLabelOption;
|
||||
externalPointerId: string;
|
||||
};
|
||||
|
||||
|
||||
export interface G6EdgeModel {
|
||||
id: string;
|
||||
source: string | number;
|
||||
target: string | number;
|
||||
type: string;
|
||||
sourceAnchor: number | ((index: number) => number);
|
||||
targetAnchor: number | ((index: number) => number);
|
||||
label: string;
|
||||
style: Style;
|
||||
labelCfg: LinkLabelOption;
|
||||
};
|
||||
|
||||
|
||||
class Model {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
props: G6NodeModel | G6EdgeModel;
|
||||
G6Item;
|
||||
|
||||
constructor(id: string, name: string) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.G6Item = null;
|
||||
this.props = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 G6 model 的属性
|
||||
* @param option
|
||||
*/
|
||||
initProps(option: ElementOption | LinkOption | PointerOption) { }
|
||||
|
||||
/**
|
||||
* 获取 G6 model 的属性
|
||||
* @param attr
|
||||
*/
|
||||
get(attr: string): any {
|
||||
return this.props[attr];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 G6 model 的属性
|
||||
* @param attr
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
set(attr: string, value: any) {
|
||||
if(this.props[attr] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(attr === 'style' || attr === 'labelCfg') {
|
||||
Object.assign(this.props[attr], value);
|
||||
}
|
||||
else {
|
||||
this.props[attr] = value;
|
||||
}
|
||||
|
||||
if(this.G6Item === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(attr === 'rotation') {
|
||||
const matrix = Util.calcRotateMatrix(this.getMatrix(), value);
|
||||
this.set('style', { matrix });
|
||||
}
|
||||
else if(attr === 'x' || attr === 'y') {
|
||||
this.G6Item.updatePosition({
|
||||
[attr]: value
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.G6Item.update(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取包围盒
|
||||
* @returns
|
||||
*/
|
||||
getBound(): BoundingRect {
|
||||
return this.G6Item.getBBox();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变换矩阵
|
||||
*/
|
||||
getMatrix(): number[] {
|
||||
if(this.G6Item === null) return null;
|
||||
return this.G6Item.getContainer().getMatrix();
|
||||
}
|
||||
|
||||
/**
|
||||
* 钩子函数:在获取 G6Item 后执行
|
||||
*/
|
||||
afterInitG6Item() {
|
||||
this.set('rotation', this.get('rotation'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Element extends Model {
|
||||
constructor(type: string, sourceElement: SourceElement) {
|
||||
super(sourceElement.id.toString(), type);
|
||||
|
||||
Object.keys(sourceElement).map(prop => {
|
||||
if(prop !== 'id') {
|
||||
this[prop] = sourceElement[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 G6 model 的属性
|
||||
* @param option
|
||||
*/
|
||||
initProps(option: ElementOption) {
|
||||
this.props = {
|
||||
id: this.id,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: option.rotation || 0,
|
||||
type: option.type,
|
||||
size: option.size,
|
||||
anchorPoints: option.anchorPoint,
|
||||
label: null,
|
||||
style: Util.objectClone<Style>(option.style),
|
||||
labelCfg: Util.objectClone<ElementLabelOption>(option.labelOptions),
|
||||
externalPointerId: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export class Link extends Model {
|
||||
element: Element;
|
||||
target: Element;
|
||||
index: number;
|
||||
|
||||
constructor(type: string, element: Element, target: Element, index: number) {
|
||||
super(`${element.id}-${target.id}`, type);
|
||||
this.element = element;
|
||||
this.target = target;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 G6 model 的属性
|
||||
* @param option
|
||||
*/
|
||||
initProps(option: LinkOption) {
|
||||
let sourceAnchor = option.sourceAnchor,
|
||||
targetAnchor = option.targetAnchor;
|
||||
|
||||
if(option.sourceAnchor && typeof option.sourceAnchor === 'function' && this.index !== null) {
|
||||
sourceAnchor = option.sourceAnchor(this.index);
|
||||
}
|
||||
|
||||
if(option.targetAnchor && typeof option.targetAnchor === 'function' && this.index !== null) {
|
||||
targetAnchor = option.targetAnchor(this.index);
|
||||
}
|
||||
|
||||
this.props = {
|
||||
id: this.id,
|
||||
type: option.type,
|
||||
source: this.element.id,
|
||||
target: this.target.id,
|
||||
sourceAnchor,
|
||||
targetAnchor,
|
||||
label: option.label,
|
||||
style: Util.objectClone<Style>(option.style),
|
||||
labelCfg: Util.objectClone<LinkLabelOption>(option.labelOptions)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export class Pointer extends Model {
|
||||
target: Element;
|
||||
label: string | string[];
|
||||
|
||||
constructor(id: string, type: string, label: string | string[], target: Element) {
|
||||
super(id, type);
|
||||
this.target = target;
|
||||
this.label = label;
|
||||
|
||||
this.target.set('externalPointerId', id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 G6 model 的属性
|
||||
* @param option
|
||||
*/
|
||||
initProps(option: ElementOption) {
|
||||
this.props = {
|
||||
id: this.id,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
type: option.type || 'external-pointer',
|
||||
size: option.size,
|
||||
anchorPoints: option.anchorPoint,
|
||||
label: typeof this.label === 'string'? this.label: this.label.join(', '),
|
||||
style: Util.objectClone<Style>(option.style),
|
||||
labelCfg: Util.objectClone<ElementLabelOption>(option.labelOptions),
|
||||
externalPointerId: null
|
||||
};
|
||||
}
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
import { Shape } from "../View/shape";
|
||||
import { zrShape } from "../View/shapeScheduler";
|
||||
import { ElementStatus, Style } from "./element";
|
||||
|
||||
|
||||
export interface LabelStyle extends Style {
|
||||
textBackgroundColor: 'rgba(0, 0, 0, 1)',
|
||||
textFill: '#fff',
|
||||
textPadding: [4, 4, 4, 4]
|
||||
};
|
||||
|
||||
|
||||
export interface PointerOptions {
|
||||
labelStyle: LabelStyle;
|
||||
style: Style;
|
||||
};
|
||||
|
||||
|
||||
export class Pointer {
|
||||
// 指针 id
|
||||
id: string;
|
||||
// 指针图形实例
|
||||
shape: Shape;
|
||||
// 指针类型名称
|
||||
pointerLabel: string;
|
||||
// 被该指针合并的其他指针
|
||||
branchPointer: Pointer[];
|
||||
// 若该指针是一个被合并的指针,保存合并这个指针的主指针
|
||||
masterPointer: Pointer;
|
||||
|
||||
// 指针标签内容
|
||||
text: string;
|
||||
// 指针标签图形实例
|
||||
textZrShapes: Shape[];
|
||||
// 逗号图形实例
|
||||
commaShapes: Shape[];
|
||||
// 目标 element
|
||||
target: Element;
|
||||
|
||||
isDirty: boolean = false;
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 元素是否为脏
|
||||
* @param isDirty
|
||||
*/
|
||||
setDirty(isDirty: boolean) {
|
||||
this.isDirty = isDirty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义该element映射的图形
|
||||
* @param shapes
|
||||
* @param elementStatus
|
||||
* @override
|
||||
*/
|
||||
renderShape(shapes: Shape[] | Shape, elementStatus: ElementStatus) { }
|
||||
};
|
@ -1,55 +0,0 @@
|
||||
import { ShapeStatus } from "../View/shape";
|
||||
import { ZrShapeConstructor } from "../View/shapeScheduler";
|
||||
import { Element, Style } from "./element";
|
||||
import { Pointer } from "./pointer";
|
||||
|
||||
|
||||
export interface PointerOptions {
|
||||
style: Style;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export class PointerScheduler {
|
||||
private pointers: Pointer[] = [];
|
||||
private prevPointers: Pointer[] = [];
|
||||
private pointerMap: {
|
||||
[key: string]: {
|
||||
pointerConstructor: { new(): Pointer },
|
||||
zrShapeConstructors: ZrShapeConstructor[] | ZrShapeConstructor,
|
||||
shapeOptions: Partial<ShapeStatus>
|
||||
}
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建指针模型
|
||||
* @param elementList
|
||||
*/
|
||||
constructPointers(elementList: Element[]) {
|
||||
|
||||
}
|
||||
|
||||
setPointerMap(
|
||||
pointerLabel: string,
|
||||
pointerConstructor: { new(): Pointer },
|
||||
zrShapeConstructors: ZrShapeConstructor[] | ZrShapeConstructor,
|
||||
shapeOptions: Partial<ShapeStatus>
|
||||
) {
|
||||
this.pointerMap[pointerLabel] = {
|
||||
pointerConstructor,
|
||||
zrShapeConstructors,
|
||||
shapeOptions
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
reset() {
|
||||
this.pointers.length = 0;
|
||||
}
|
||||
};
|
62
src/RegisteredShape/binaryTreeNode.ts
Normal file
62
src/RegisteredShape/binaryTreeNode.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import * as G6 from "./../Lib/g6.js";
|
||||
|
||||
|
||||
export default G6.registerNode('binary-tree-node', {
|
||||
draw(cfg, group) {
|
||||
cfg.size = cfg.size || [60, 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: '#333',
|
||||
fill: 'transparent'
|
||||
},
|
||||
name: 'wrapper'
|
||||
});
|
||||
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: width / 4 + width / 2,
|
||||
y: height / 2,
|
||||
width: width / 2,
|
||||
height: height,
|
||||
fill: cfg.style.fill
|
||||
},
|
||||
name: 'mid',
|
||||
draggable: true
|
||||
});
|
||||
|
||||
if (cfg.label) {
|
||||
const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
return wrapperRect;
|
||||
},
|
||||
|
||||
getAnchorPoints() {
|
||||
return [
|
||||
[0.5, 0],
|
||||
[0.125, 0.5],
|
||||
[0.875, 0.5],
|
||||
];
|
||||
},
|
||||
});
|
54
src/RegisteredShape/externalPointer.ts
Normal file
54
src/RegisteredShape/externalPointer.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import * as G6 from "./../Lib/g6.js";
|
||||
|
||||
|
||||
export default G6.registerNode('external-pointer', {
|
||||
draw(cfg, group) {
|
||||
cfg.size = cfg.size || [8, 35];
|
||||
|
||||
const keyShape = group.addShape('path', {
|
||||
attrs: {
|
||||
path: this.getPath(cfg),
|
||||
fill: cfg.style.fill
|
||||
},
|
||||
name: 'pointer-path'
|
||||
});
|
||||
|
||||
if (cfg.label) {
|
||||
const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
x: cfg.size[0] + 2, // 居中
|
||||
y: -cfg.size[1],
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
text: cfg.label,
|
||||
fill: style.fill || '#000',
|
||||
fontSize: style.fontSize || 16
|
||||
},
|
||||
name: 'pointer-text-shape'
|
||||
});
|
||||
}
|
||||
|
||||
return keyShape;
|
||||
},
|
||||
|
||||
getPath(cfg) {
|
||||
let width = cfg.size[0],
|
||||
height = cfg.size[1],
|
||||
arrowWidth = width + 4,
|
||||
arrowHeight = height * 0.3;
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
63
src/RegisteredShape/linkListNode.ts
Normal file
63
src/RegisteredShape/linkListNode.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import * as G6 from "./../Lib/g6.js";
|
||||
|
||||
|
||||
export default G6.registerNode('link-list-node', {
|
||||
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: '#ddd'
|
||||
},
|
||||
name: 'wrapper'
|
||||
});
|
||||
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: width / 3,
|
||||
y: height / 2,
|
||||
width: width * (2 / 3),
|
||||
height: height,
|
||||
fill: cfg.style.fill
|
||||
},
|
||||
name: 'main-rect'
|
||||
});
|
||||
|
||||
if (cfg.label) {
|
||||
const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
x: cfg.size[0] + 2, // 居中
|
||||
y: -cfg.size[1],
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle',
|
||||
text: cfg.label,
|
||||
fill: style.fill || '#000',
|
||||
fontSize: style.fontSize || 16
|
||||
},
|
||||
name: 'pointer-text-shape',
|
||||
draggable: false,
|
||||
});
|
||||
}
|
||||
|
||||
return wrapperRect;
|
||||
},
|
||||
|
||||
update(cfg, item) {
|
||||
console.log(88);
|
||||
},
|
||||
|
||||
getAnchorPoints() {
|
||||
return [
|
||||
[0, 0.5],
|
||||
[2 / 3, 0.5]
|
||||
];
|
||||
}
|
||||
});
|
@ -1,18 +1,19 @@
|
||||
import { Engine } from "./engine";
|
||||
|
||||
|
||||
|
||||
|
||||
import { Bound } from "./View/boundingRect";
|
||||
import { Group } from "./View/group";
|
||||
import externalPointer from "./RegisteredShape/externalPointer";
|
||||
import * as G6 from "./Lib/g6.js";
|
||||
import linkListNode from "./RegisteredShape/linkListNode";
|
||||
import binaryTreeNode from "./RegisteredShape/binaryTreeNode";
|
||||
|
||||
|
||||
export const SV = {
|
||||
|
||||
|
||||
createEngine(engineName: string): Engine {
|
||||
return new Engine(engineName);
|
||||
},
|
||||
|
||||
|
||||
|
||||
Engine: Engine,
|
||||
Group: Group,
|
||||
Bound: Bound,
|
||||
G6,
|
||||
registeredShape: [
|
||||
externalPointer, linkListNode, binaryTreeNode
|
||||
]
|
||||
};
|
||||
|
||||
|
101
src/View/animation.ts
Normal file
101
src/View/animation.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { SV } from "../StructV";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 动画表
|
||||
*/
|
||||
export class Animations {
|
||||
private duration: number;
|
||||
private timingFunction: string;
|
||||
private mat3 = SV.G6.Util.mat3;
|
||||
|
||||
constructor(duration: number, timingFunction: string) {
|
||||
this.duration = duration;
|
||||
this.timingFunction = timingFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加节点 / 边时的动画效果
|
||||
* @param G6Item
|
||||
* @param callback
|
||||
*/
|
||||
append(G6Item, callback: Function = null) {
|
||||
const type = G6Item.getType(),
|
||||
group = G6Item.getContainer(),
|
||||
animateCfg = {
|
||||
duration: this.duration,
|
||||
easing: this.timingFunction,
|
||||
callback
|
||||
};
|
||||
|
||||
if(type === 'node') {
|
||||
let mat3 = this.mat3,
|
||||
matrix = group.getMatrix(),
|
||||
targetMatrix = mat3.clone(matrix);
|
||||
|
||||
mat3.scale(matrix, matrix, [0, 0]);
|
||||
mat3.scale(targetMatrix, targetMatrix, [1, 1]);
|
||||
|
||||
group.attr({ opacity: 0, matrix });
|
||||
group.animate({ opacity: 1, matrix: targetMatrix }, animateCfg);
|
||||
}
|
||||
|
||||
if(type === 'edge') {
|
||||
const line = group.get('children')[0],
|
||||
length = line.getTotalLength();
|
||||
|
||||
line.attr({ lineDash: [0, length], opacity: 0 });
|
||||
line.animate({ lineDash: [length, 0], opacity: 1 }, animateCfg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除节点 / 边时的动画效果
|
||||
* @param G6Item
|
||||
* @param callback
|
||||
*/
|
||||
remove(G6Item, callback: Function = null) {
|
||||
const type = G6Item.getType(),
|
||||
group = G6Item.getContainer(),
|
||||
animateCfg = {
|
||||
duration: this.duration,
|
||||
easing: this.timingFunction,
|
||||
callback
|
||||
};
|
||||
|
||||
if(type === 'node') {
|
||||
let mat3 = this.mat3,
|
||||
matrix = mat3.clone(group.getMatrix());
|
||||
|
||||
mat3.scale(matrix, matrix, [0, 0]);
|
||||
group.animate({ opacity: 0, matrix }, animateCfg);
|
||||
}
|
||||
|
||||
if(type === 'edge') {
|
||||
const line = group.get('children')[0],
|
||||
length = line.getTotalLength();
|
||||
|
||||
line.animate({ lineDash: [0, length], opacity: 0 }, animateCfg);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
140
src/View/boundingRect.ts
Normal file
140
src/View/boundingRect.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { Vector } from "../Common/vector";
|
||||
|
||||
|
||||
|
||||
// 包围盒类型
|
||||
export type BoundingRect = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
|
||||
// 包围盒操作
|
||||
export const Bound = {
|
||||
|
||||
/**
|
||||
* 从点集生成包围盒
|
||||
* @param points
|
||||
*/
|
||||
fromPoints(points: Array<[number, number]>): BoundingRect {
|
||||
let maxX = -Infinity,
|
||||
minX = Infinity,
|
||||
maxY = -Infinity,
|
||||
minY = Infinity;
|
||||
|
||||
points.map(item => {
|
||||
if(item[0] > maxX) maxX = item[0];
|
||||
if(item[0] < minX) minX = item[0];
|
||||
if(item[1] > maxY) maxY = item[1];
|
||||
if(item[1] < minY) minY = item[1];
|
||||
});
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 由包围盒转化为四个顶点(顺时针)
|
||||
* @param bound
|
||||
*/
|
||||
toPoints(bound: BoundingRect): Array<[number, number]> {
|
||||
return [
|
||||
[bound.x, bound.y],
|
||||
[bound.x + bound.width, bound.y],
|
||||
[bound.x + bound.width, bound.y + bound.height],
|
||||
[bound.x, bound.y + bound.height]
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* 求包围盒并集
|
||||
* @param arg
|
||||
*/
|
||||
union(...arg: BoundingRect[]): BoundingRect {
|
||||
return arg.length > 1?
|
||||
arg.reduce((total, cur) => {
|
||||
let minX = total.x < cur.x? total.x: cur.x,
|
||||
maxX = total.x + total.width < cur.x + cur.width? cur.x + cur.width: total.x + total.width,
|
||||
minY = total.y < cur.y? total.y: cur.y,
|
||||
maxY = total.y + total.height < cur.y + cur.height? cur.y + cur.height: total.y + total.height;
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY
|
||||
};
|
||||
}): arg[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* 包围盒求交集
|
||||
* @param b1
|
||||
* @param b2
|
||||
*/
|
||||
intersect(b1: BoundingRect, b2: BoundingRect): BoundingRect {
|
||||
let x, y,
|
||||
maxX, maxY,
|
||||
overlapsX,
|
||||
overlapsY;
|
||||
|
||||
if(b1.x < b2.x + b2.width && b1.x + b1.width > b2.x) {
|
||||
x = b1.x < b2.x? b2.x: b1.x;
|
||||
maxX = b1.x + b1.width < b2.x + b2.width? b1.x + b1.width: b2.x + b2.width;
|
||||
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;
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width: overlapsX,
|
||||
height: overlapsY
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 求包围盒旋转后新形成的包围盒
|
||||
* @param bound
|
||||
* @param rot
|
||||
*/
|
||||
rotation(bound: BoundingRect, rot: number): BoundingRect {
|
||||
let cx = bound.x + bound.width / 2,
|
||||
cy = bound.y + bound.height / 2;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
101
src/View/group.ts
Normal file
101
src/View/group.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { Util } from "../Common/util";
|
||||
import { BoundingRect, Bound } from "../View/boundingRect";
|
||||
import { Vector } from "../Common/vector";
|
||||
import { Element } from "../Model/modelData";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* element组
|
||||
*/
|
||||
export class Group {
|
||||
id: string;
|
||||
private elements: Array<Element | Group> = [];
|
||||
|
||||
constructor(...arg: Array<Element | Group>) {
|
||||
this.id = Util.generateId();
|
||||
|
||||
if(arg) {
|
||||
this.add(...arg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加element
|
||||
* @param arg
|
||||
*/
|
||||
add(...arg: Array<Element | Group>) {
|
||||
arg.map(ele => {
|
||||
this.elements.push(ele);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除element
|
||||
* @param element
|
||||
*/
|
||||
remove(element: Element | Group) {
|
||||
Util.removeFromList(this.elements, item => item.id === element.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取group的包围盒
|
||||
*/
|
||||
getBound(): BoundingRect {
|
||||
return Bound.union(...this.elements.map(item => item.getBound()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 位移group
|
||||
* @param dx
|
||||
* @param dy
|
||||
*/
|
||||
translate(dx: number, dy: number) {
|
||||
this.elements.map(item => {
|
||||
if(item instanceof Group) {
|
||||
item.translate(dx, dy);
|
||||
}
|
||||
else {
|
||||
item.set('x', item.get('x') + dx);
|
||||
item.set('y', item.get('y') + dy);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转group
|
||||
* @param rotation
|
||||
* @param center
|
||||
*/
|
||||
rotate(rotation: number, center?: [number, number]) {
|
||||
// if(rotation === 0) return;
|
||||
|
||||
// let {x, y, width, height} = this.getBound(),
|
||||
// cx = x + width / 2,
|
||||
// cy = y + height / 2;
|
||||
|
||||
// if(center) {
|
||||
// cx = center[0];
|
||||
// cy = center[1];
|
||||
// }
|
||||
|
||||
// this.elements.map(item => {
|
||||
// if(item instanceof Group) {
|
||||
// item.rotate(rotation, [cx, cy]);
|
||||
// }
|
||||
// else {
|
||||
// let d = Vector.rotation(rotation, [item.x, item.y], [cx, cy]);
|
||||
// item.x = d[0];
|
||||
// item.y = d[1];
|
||||
// item.set('rotation', rotation);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空group
|
||||
*/
|
||||
clear() {
|
||||
this.elements.length = 0;
|
||||
}
|
||||
}
|
77
src/View/layouter.ts
Normal file
77
src/View/layouter.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { Util } from "../Common/util";
|
||||
import { Engine } from "../engine";
|
||||
import { ConstructedData } from "../Model/modelConstructor";
|
||||
import { Element, Pointer } from "../Model/modelData";
|
||||
import { LayoutOptions, PointerOption } from "../options";
|
||||
import { Bound, BoundingRect } from "./boundingRect";
|
||||
|
||||
|
||||
|
||||
|
||||
export class Layouter {
|
||||
private engine: Engine;
|
||||
private containerWidth: number;
|
||||
private containerHeight: number;
|
||||
|
||||
constructor(engine: Engine, containerWidth: number, containerHeight: number) {
|
||||
this.engine = engine;
|
||||
this.containerWidth = containerWidth;
|
||||
this.containerHeight = containerHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将视图调整至画布中心
|
||||
* @param nodes
|
||||
*/
|
||||
private fiTCenter(nodes: (Element | Pointer)[]) {
|
||||
const viewBound: BoundingRect = nodes.map(item => item.getBound()).reduce((prev, cur) => Bound.union(prev, cur));
|
||||
|
||||
let centerX = this.containerWidth / 2, centerY = this.containerHeight / 2,
|
||||
boundCenterX = viewBound.x + viewBound.width / 2,
|
||||
boundCenterY = viewBound.y + viewBound.height / 2,
|
||||
dx = centerX - boundCenterX,
|
||||
dy = centerY - boundCenterY;
|
||||
|
||||
nodes.forEach(item => {
|
||||
item.set('x', item.get('x') + dx);
|
||||
item.set('y', item.get('y') + dy)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 布局外部指针
|
||||
* @param pointer
|
||||
*/
|
||||
private layoutPointer(pointer: { [key: string]: Pointer[] }) {
|
||||
Object.keys(pointer).map(name => {
|
||||
const options: PointerOption = this.engine.pointerOptions[name],
|
||||
pointerList: Pointer[] = pointer[name],
|
||||
offset = options.offset || 8;
|
||||
|
||||
pointerList.forEach(item => {
|
||||
let targetBound: BoundingRect = item.target.getBound();
|
||||
item.set('x', targetBound.x + targetBound.width / 2);
|
||||
item.set('y', targetBound.y - offset);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 主布局函数
|
||||
* @param constructedData
|
||||
* @param layoutFn
|
||||
*/
|
||||
public layout(constructedData: ConstructedData, layoutFn: (element: { [ket: string]: Element[] }, layoutOptions: LayoutOptions) => void) {
|
||||
const options: LayoutOptions = this.engine.layoutOptions,
|
||||
nodes: (Element | Pointer)[] = [...Util.converterList(constructedData.element), ...Util.converterList(constructedData.pointer)]
|
||||
|
||||
// 布局节点
|
||||
layoutFn.call(this.engine, constructedData.element, options);
|
||||
|
||||
// 布局外部指针
|
||||
this.layoutPointer(constructedData.pointer);
|
||||
|
||||
// 将视图调整到画布中心
|
||||
options.fitCenter && this.fiTCenter(nodes);
|
||||
}
|
||||
}
|
@ -1,225 +0,0 @@
|
||||
import { Util } from "../Common/util";
|
||||
import { Style } from "../Model/element";
|
||||
import { Shape } from "./shape";
|
||||
import { ShapeScheduler } from "./shapeScheduler";
|
||||
|
||||
|
||||
|
||||
export enum patchType {
|
||||
ADD,
|
||||
REMOVE,
|
||||
POSITION,
|
||||
PATH,
|
||||
ROTATION,
|
||||
SIZE,
|
||||
STYLE
|
||||
}
|
||||
|
||||
|
||||
export interface patchInfo {
|
||||
type: number;
|
||||
shape: Shape;
|
||||
}
|
||||
|
||||
|
||||
export class Reconciler {
|
||||
private shapeScheduler: ShapeScheduler;
|
||||
|
||||
constructor(shapeScheduler: ShapeScheduler) {
|
||||
this.shapeScheduler = shapeScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行图形样式对象的比较
|
||||
* @param oldStyle
|
||||
* @param newStyle
|
||||
*/
|
||||
reconcileStyle(oldStyle: Style, newStyle: Style): {name: string, old: any, new: any }[] {
|
||||
let styleName: {name: string, old: any, new: any }[] = [];
|
||||
|
||||
Object.keys(newStyle).map(prop => {
|
||||
if(newStyle[prop] !== oldStyle[prop]) {
|
||||
styleName.push({
|
||||
name: prop,
|
||||
old: oldStyle[prop],
|
||||
new: newStyle[prop]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return styleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图形间的 differ
|
||||
* @param shape
|
||||
*/
|
||||
reconcileShape(shape: Shape) {
|
||||
let patchList: patchInfo[] = [];
|
||||
|
||||
if(shape.isDirty === false) return;
|
||||
|
||||
// 比较图形路径
|
||||
if(JSON.stringify(shape.prevShapeStatus) !== JSON.stringify(shape.shapeStatus.points)) {
|
||||
patchList.push({
|
||||
type: patchType.PATH,
|
||||
shape
|
||||
});
|
||||
}
|
||||
|
||||
// 比较图形坐标位置
|
||||
if(shape.prevShapeStatus.x !== shape.shapeStatus.x || shape.prevShapeStatus.y !== shape.shapeStatus.y) {
|
||||
patchList.push({
|
||||
type: patchType.POSITION,
|
||||
shape
|
||||
});
|
||||
}
|
||||
|
||||
// 比较旋转角度
|
||||
if(shape.prevShapeStatus.rotation !== shape.shapeStatus.rotation) {
|
||||
patchList.push({
|
||||
type: patchType.ROTATION,
|
||||
shape
|
||||
});
|
||||
}
|
||||
|
||||
// 比较尺寸
|
||||
if(shape.prevShapeStatus.width !== shape.shapeStatus.width || shape.prevShapeStatus.height !== shape.shapeStatus.height) {
|
||||
patchList.push({
|
||||
type: patchType.SIZE,
|
||||
shape
|
||||
});
|
||||
}
|
||||
|
||||
// 比较样式
|
||||
let style = this.reconcileStyle(shape.prevShapeStatus.style, shape.shapeStatus.style);
|
||||
if(style.length) {
|
||||
patchList.push({
|
||||
type: patchType.STYLE,
|
||||
shape
|
||||
});
|
||||
}
|
||||
|
||||
// 对变化进行更新
|
||||
this.patch(patchList);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param container
|
||||
* @param shapeList
|
||||
*/
|
||||
reconcileShapeList(container: { [key: string]: Shape[] }, shapeList: Shape[]) {
|
||||
let patchList: patchInfo[] = [];
|
||||
|
||||
for(let i = 0; i < shapeList.length; i++) {
|
||||
let shape = shapeList[i],
|
||||
name = shape.type;
|
||||
|
||||
// 若发现存在于新视图模型而不存在于旧视图模型的图形,则该图形都标记为 ADD
|
||||
if(container[name] === undefined) {
|
||||
patchList.push({
|
||||
type: patchType.ADD,
|
||||
shape
|
||||
});
|
||||
}
|
||||
else {
|
||||
let oldShape = container[name].find(item => item.id === shape.id);
|
||||
|
||||
// 若旧图形列表存在对应的图形,进行 shape 间 differ
|
||||
if(oldShape) {
|
||||
oldShape.isReconcilerVisited = true;
|
||||
this.reconcileShape(shape);
|
||||
}
|
||||
// 若发现存在于新视图模型而不存在于旧视图模型的图形,则该图形都标记为 ADD
|
||||
else {
|
||||
patchList.push({
|
||||
type: patchType.ADD,
|
||||
shape
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在旧视图容器中寻找未访问过的图形,表明该图形该图形需要移除
|
||||
Object.keys(container).forEach(key => {
|
||||
container[key].forEach(shape => {
|
||||
if(shape.isReconcilerVisited === false) {
|
||||
patchList.push({
|
||||
type: patchType.REMOVE,
|
||||
shape,
|
||||
});
|
||||
}
|
||||
|
||||
shape.isReconcilerVisited = false;
|
||||
});
|
||||
});
|
||||
|
||||
this.patch(patchList);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 对修改的视图进行补丁更新
|
||||
* @param patchList
|
||||
*/
|
||||
patch(patchList: patchInfo[]) {
|
||||
let patch: patchInfo,
|
||||
shape: Shape,
|
||||
i;
|
||||
|
||||
for(i = 0; i < patchList.length; i++) {
|
||||
patch = patchList[i];
|
||||
shape = patch.shape;
|
||||
|
||||
switch(patch.type) {
|
||||
case patchType.ADD: {
|
||||
this.shapeScheduler.appendShape(shape);
|
||||
this.shapeScheduler.emitAnimation(shape, 'append');
|
||||
break;
|
||||
}
|
||||
|
||||
case patchType.REMOVE: {
|
||||
this.shapeScheduler.removeShape(shape);
|
||||
this.shapeScheduler.emitAnimation(shape, 'remove');
|
||||
break;
|
||||
}
|
||||
|
||||
case patchType.PATH: {
|
||||
shape.prevShapeStatus.points = shape.shapeStatus.points;
|
||||
this.shapeScheduler.emitAnimation(shape, 'path');
|
||||
}
|
||||
|
||||
case patchType.POSITION: {
|
||||
shape.prevShapeStatus.x = shape.shapeStatus.x;
|
||||
shape.prevShapeStatus.y = shape.shapeStatus.y;
|
||||
this.shapeScheduler.emitAnimation(shape, 'position');
|
||||
break;
|
||||
}
|
||||
|
||||
case patchType.ROTATION: {
|
||||
shape.prevShapeStatus.rotation = shape.shapeStatus.rotation;
|
||||
this.shapeScheduler.emitAnimation(shape, 'rotation');
|
||||
break;
|
||||
}
|
||||
|
||||
case patchType.SIZE: {
|
||||
shape.prevShapeStatus.width = shape.shapeStatus.width;
|
||||
shape.prevShapeStatus.height = shape.shapeStatus.height;
|
||||
this.shapeScheduler.emitAnimation(shape, 'size');
|
||||
break;
|
||||
}
|
||||
|
||||
case patchType.STYLE: {
|
||||
shape.prevShapeStatus.style = Util.clone(shape.shapeStatus.style);
|
||||
this.shapeScheduler.emitAnimation(shape, 'style');
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,213 @@
|
||||
import { Engine } from '../engine';
|
||||
import { Element, G6EdgeModel, G6NodeModel, Link, Pointer } from '../Model/modelData';
|
||||
import { ConstructedData } from '../Model/modelConstructor';
|
||||
import { Util } from '../Common/util';
|
||||
import { Animations } from './animation';
|
||||
import { SV } from '../StructV';
|
||||
|
||||
|
||||
|
||||
export interface G6Data {
|
||||
nodes: G6NodeModel[];
|
||||
edges: G6EdgeModel[];
|
||||
};
|
||||
|
||||
|
||||
|
||||
export class Renderer {
|
||||
constructor() {
|
||||
|
||||
private engine: Engine;
|
||||
private DOMContainer: HTMLElement;
|
||||
private animations: Animations;
|
||||
private isFirstRender: boolean;
|
||||
private prevRenderData: G6Data;
|
||||
private graphInstance;
|
||||
private helpGraphInstance;
|
||||
|
||||
constructor(engine: Engine, DOMContainer: HTMLElement, containerWidth: number, containerHeight: number) {
|
||||
this.engine = engine;
|
||||
this.DOMContainer = DOMContainer;
|
||||
this.isFirstRender = true;
|
||||
this.prevRenderData = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
};
|
||||
|
||||
const enable: boolean = this.engine.animationOptions.enable === undefined? true: this.engine.animationOptions.enable,
|
||||
duration: number = this.engine.animationOptions.duration,
|
||||
timingFunction: string = this.engine.animationOptions.timingFunction;
|
||||
|
||||
this.graphInstance = new SV.G6.Graph({
|
||||
container: DOMContainer,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
animate: enable,
|
||||
animateCfg: {
|
||||
duration: duration,
|
||||
easing: timingFunction
|
||||
},
|
||||
fitView: this.engine.layoutOptions.fitView,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'zoom-canvas', 'drag-node']
|
||||
}
|
||||
});
|
||||
|
||||
this.helpGraphInstance = new SV.G6.Graph({
|
||||
container: DOMContainer.cloneNode()
|
||||
});
|
||||
|
||||
this.animations = new Animations(duration, timingFunction);
|
||||
this.initBehavior();
|
||||
}
|
||||
|
||||
applyAnimation() {
|
||||
/**
|
||||
* 初始化交互
|
||||
*/
|
||||
private initBehavior() {
|
||||
this.graphInstance.on('node:drag', (() => {
|
||||
let pointer = null;
|
||||
|
||||
return ev => {
|
||||
if(pointer === null) {
|
||||
pointer = this.graphInstance.findById(ev.item.getModel().externalPointerId);
|
||||
}
|
||||
|
||||
if(pointer) {
|
||||
pointer.updatePosition({
|
||||
x: ev.canvasX,
|
||||
y: ev.canvasY
|
||||
});
|
||||
}
|
||||
|
||||
console.log(ev);
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比上一次和该次 G6Data 找出新添加的节点和边
|
||||
* @param prevData
|
||||
* @param data
|
||||
*/
|
||||
private diffAppendItems(prevData: G6Data, data: G6Data): G6Data {
|
||||
return {
|
||||
nodes: data.nodes.filter(item => !prevData.nodes.find(n => n.id === item.id)),
|
||||
edges: data.edges.filter(item => !prevData.edges.find(e => e.id === item.id))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比上一次和该次 G6Data 找出被删除的节点和边
|
||||
* @param prevData
|
||||
* @param data
|
||||
*/
|
||||
private diffRemoveItems(prevData: G6Data, data: G6Data): G6Data {
|
||||
return {
|
||||
nodes: prevData.nodes.filter(item => !data.nodes.find(n => n.id === item.id)),
|
||||
edges: prevData.edges.filter(item => !data.edges.find(e => e.id === item.id))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新增的 G6Item(主要是动画)
|
||||
* @param appendData
|
||||
*/
|
||||
private handleAppendItems(appendData: G6Data) {
|
||||
const appendItems = [
|
||||
...appendData.nodes.map(item => this.graphInstance.findById(item.id)),
|
||||
...appendData.edges.map(item => this.graphInstance.findById(item.id))
|
||||
];
|
||||
|
||||
appendItems.forEach(item => {
|
||||
this.animations.append(item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理被移除的 G6Item(主要是动画)
|
||||
* @param removeData
|
||||
*/
|
||||
private handleRemoveItems(removeData: G6Data) {
|
||||
const removeItems = [
|
||||
...removeData.nodes.map(item => this.graphInstance.findById(item.id)),
|
||||
...removeData.edges.map(item => this.graphInstance.findById(item.id))
|
||||
];
|
||||
|
||||
removeItems.forEach(item => {
|
||||
this.animations.remove(item, () => {
|
||||
this.graphInstance.removeItem(item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 G6 元素
|
||||
* @param constructedData
|
||||
*/
|
||||
public build(constructedData: ConstructedData) {
|
||||
let elementList: Element[] = Util.converterList(constructedData.element),
|
||||
linkList: Link[] = Util.converterList(constructedData.link),
|
||||
pointerList: Pointer[] = Util.converterList(constructedData.pointer),
|
||||
nodeList = [...elementList.map(item => item.props), ...pointerList.map(item => item.props)],
|
||||
edgeList = linkList.map(item => item.props),
|
||||
list = [...elementList, ...linkList, ...pointerList];
|
||||
|
||||
const data: G6Data = {
|
||||
nodes: <G6NodeModel[]>nodeList,
|
||||
edges: <G6EdgeModel[]>edgeList
|
||||
};
|
||||
|
||||
this.helpGraphInstance.clear();
|
||||
this.helpGraphInstance.read(data);
|
||||
|
||||
list.forEach(item => {
|
||||
item.G6Item = this.helpGraphInstance.findById(item.id);
|
||||
item.afterInitG6Item();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染函数
|
||||
* @param constructedData
|
||||
*/
|
||||
public render(constructedData: ConstructedData) {
|
||||
let data: G6Data = Util.convertG6Data(constructedData),
|
||||
renderData: G6Data = null,
|
||||
appendData: G6Data = null,
|
||||
removeData: G6Data = null;
|
||||
|
||||
appendData = this.diffAppendItems(this.prevRenderData, data);
|
||||
removeData = this.diffRemoveItems(this.prevRenderData, data);
|
||||
renderData = {
|
||||
nodes: [...data.nodes, ...removeData.nodes],
|
||||
edges: [...data.edges, ...removeData.edges]
|
||||
};
|
||||
|
||||
this.prevRenderData = data;
|
||||
|
||||
if(this.isFirstRender) {
|
||||
this.graphInstance.read(renderData);
|
||||
this.isFirstRender = false;
|
||||
}
|
||||
else {
|
||||
this.graphInstance.changeData(renderData);
|
||||
}
|
||||
|
||||
this.handleAppendItems(appendData);
|
||||
this.handleRemoveItems(removeData);
|
||||
|
||||
if(this.engine.layoutOptions.fitView) {
|
||||
this.graphInstance.fitView();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定 G6 事件
|
||||
* @param eventName
|
||||
* @param callback
|
||||
*/
|
||||
public on(eventName: string, callback: Function) {
|
||||
if(this.graphInstance) {
|
||||
this.graphInstance.on(eventName, callback)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import { Util } from "../Common/util";
|
||||
import { Element, Style } from "../Model/element";
|
||||
import { Group, zrShape, ZrShapeConstructor } from "./shapeScheduler";
|
||||
|
||||
|
||||
export interface ShapeStatus {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
zIndex: number;
|
||||
width: number;
|
||||
height: number;
|
||||
content: string;
|
||||
points: [number, number][];
|
||||
style: Style;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export class Shape {
|
||||
id: string = '';
|
||||
type: string = '';
|
||||
zrConstructor: ZrShapeConstructor = null;
|
||||
zrShape: zrShape = null;
|
||||
targetElement: Element = null;
|
||||
parentGroup: Group = null;
|
||||
|
||||
shapeStatus: ShapeStatus = {
|
||||
x: 0, y: 0,
|
||||
rotation: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
zIndex: 1,
|
||||
content: '',
|
||||
points: [],
|
||||
style: {
|
||||
fill: '#000',
|
||||
text: '',
|
||||
textFill: '#000',
|
||||
fontSize: 15,
|
||||
fontWeight: null,
|
||||
stroke: null,
|
||||
opacity: 1,
|
||||
transformText: true,
|
||||
lineWidth: 1
|
||||
}
|
||||
};
|
||||
|
||||
prevShapeStatus: ShapeStatus = null;
|
||||
isDirty: boolean = false;
|
||||
isReconcilerVisited: boolean = false;
|
||||
|
||||
constructor(id: string, zrConstructor: ZrShapeConstructor, element: Element) {
|
||||
this.id = id;
|
||||
this.type = Util.getClassName(zrConstructor);
|
||||
this.targetElement = element;
|
||||
this.zrConstructor = zrConstructor;
|
||||
this.zrShape = new zrConstructor();
|
||||
this.prevShapeStatus = Util.clone(this.shapeStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性
|
||||
* @param propName
|
||||
* @param props
|
||||
* @param sync
|
||||
*/
|
||||
attr(propName: string, props: any, sync: boolean = false) {
|
||||
if(this.shapeStatus[propName] === undefined) return;
|
||||
|
||||
if(propName === 'style') {
|
||||
Util.merge(this.shapeStatus.style, props);
|
||||
}
|
||||
else {
|
||||
this.shapeStatus[propName] = props;
|
||||
}
|
||||
|
||||
if(sync) {
|
||||
this.zrShape.attr(propName, props);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateShape() {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,124 +0,0 @@
|
||||
import { Util } from "../Common/util";
|
||||
import { Engine } from "../engine";
|
||||
import { Shape } from "./shape";
|
||||
import * as zrender from "zrender";
|
||||
import { Element } from "../Model/element";
|
||||
|
||||
|
||||
export type zrShape = any;
|
||||
export type Group = any;
|
||||
export type ZrShapeConstructor = { new(): zrShape };
|
||||
|
||||
|
||||
export class ShapeScheduler {
|
||||
private engine: Engine;
|
||||
private shapeList: Shape[] = [];
|
||||
private shapeTable: { [key: string]: Shape[] } = {};
|
||||
|
||||
private parentGroupList: Group[] = [];
|
||||
private appendList: Shape[] = [];
|
||||
private removeList: Shape[] = [];
|
||||
|
||||
constructor(engine: Engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图形元素
|
||||
* @param id
|
||||
* @param zrShapeConstructors
|
||||
* @param element
|
||||
*/
|
||||
public createShape(id: string, zrShapeConstructors: ZrShapeConstructor, element: Element): Shape {
|
||||
let shapeType = Util.getClassName(zrShapeConstructors),
|
||||
shape = this.getReuseShape(id, shapeType);
|
||||
|
||||
if(shape === null) {
|
||||
shape = new Shape(id, zrShapeConstructors, element);
|
||||
}
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param shapes
|
||||
*/
|
||||
public packShapes(shapes: Shape[]){
|
||||
let group: Group = new zrender.Group(),
|
||||
shape: Shape;
|
||||
|
||||
for(let i = 0; i < shapes.length; i++) {
|
||||
shape = shapes[i];
|
||||
group.add(shape.zrShape);
|
||||
shape.parentGroup = group;
|
||||
}
|
||||
|
||||
this.parentGroupList.push(group);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个图形
|
||||
* @param shape
|
||||
*/
|
||||
public appendShape(shape: Shape) {
|
||||
let shapeType = shape.type;
|
||||
|
||||
if(this.shapeTable[shapeType] === undefined) {
|
||||
this.shapeTable[shapeType] = [];
|
||||
}
|
||||
|
||||
this.shapeTable[shapeType].push(shape);
|
||||
this.shapeList.push(shape);
|
||||
this.appendList.push(shape);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个图形
|
||||
* @param shape
|
||||
*/
|
||||
public removeShape(shape: Shape) {
|
||||
let shapeType = shape.type;
|
||||
|
||||
Util.removeFromList(this.shapeTable[shapeType], item => item.id === shape.id);
|
||||
|
||||
if(this.shapeTable[shapeType].length === 0) {
|
||||
delete this.shapeTable[shapeType];
|
||||
}
|
||||
|
||||
Util.removeFromList(this.shapeList, item => item.id === shape.id);
|
||||
this.removeList.push(shape);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起一个动画请求
|
||||
* @param shape
|
||||
* @param animationType
|
||||
*/
|
||||
public emitAnimation(shape: Shape, animationType: string) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找可复用的图形
|
||||
* @param id
|
||||
* @param shapeType
|
||||
*/
|
||||
private getReuseShape(id: string, shapeType: string): Shape {
|
||||
if(this.shapeTable[shapeType] !== undefined) {
|
||||
let reuseShape = this.shapeTable[shapeType].find(item => item.id === id);
|
||||
|
||||
if(reuseShape) return reuseShape;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
public reset() {
|
||||
this.appendList.length = 0;
|
||||
this.removeList.length = 0;
|
||||
}
|
||||
};
|
172
src/engine.ts
172
src/engine.ts
@ -1,57 +1,57 @@
|
||||
import { Element } from "./Model/element";
|
||||
import { Element } from "./Model/modelData";
|
||||
import { Sources } from "./sources";
|
||||
import { Pointer } from "./Model/pointer";
|
||||
import { ShapeStatus } from "./View/shape";
|
||||
import { ShapeScheduler, ZrShapeConstructor } from "./View/shapeScheduler";
|
||||
import { ElementConstructor, ElementContainer, ElementScheduler } from "./Model/elementScheduler";
|
||||
import { Link } from "./Model/link";
|
||||
import { LinkScheduler } from "./Model/linkScheduler";
|
||||
import { PointerScheduler } from "./Model/pointerScheduler";
|
||||
|
||||
|
||||
export type LayoutFunction = (elements: ElementContainer | Element[], containerWidth: number, containerHeight: number) => void;
|
||||
import { ConstructedData, ModelConstructor } from "./Model/modelConstructor";
|
||||
import { Renderer } from "./View/renderer";
|
||||
import { AnimationOptions, ElementOption, LayoutOptions, LinkOption, Options, PointerOption } from "./options";
|
||||
import { Layouter } from "./View/layouter";
|
||||
|
||||
|
||||
export class Engine {
|
||||
// 引擎id
|
||||
private id: string;
|
||||
// 引擎名称
|
||||
private engineName: string;
|
||||
// HTML容器
|
||||
private DOMContainer: HTMLElement;
|
||||
// 当前保存的源数据
|
||||
private sources: Sources = null;
|
||||
// 序列化的源数据
|
||||
private stringifySources: string = null;
|
||||
|
||||
private elementScheduler: ElementScheduler = null;
|
||||
private linkScheduler: LinkScheduler = null;
|
||||
private pointerScheduler: PointerScheduler = null;
|
||||
private shapeScheduler: ShapeScheduler = null;
|
||||
private stringifySources: string = null; // 序列化的源数据
|
||||
|
||||
private modelConstructor: ModelConstructor = null;
|
||||
private layouter: Layouter = null;
|
||||
private renderer: Renderer = null;
|
||||
private containerWidth: number;
|
||||
private containerHeight: number;
|
||||
|
||||
private layoutFunction: LayoutFunction;
|
||||
public elementOptions: { [key: string]: ElementOption } = { };
|
||||
public linkOptions: { [key: string]: LinkOption } = { };
|
||||
public pointerOptions: { [key: string]: PointerOption } = { };
|
||||
public layoutOptions: LayoutOptions = null;
|
||||
public animationOptions: AnimationOptions = null;
|
||||
|
||||
constructor(DOMContainer: HTMLElement, engineName: string) {
|
||||
this.engineName = engineName;
|
||||
this.DOMContainer = DOMContainer;
|
||||
this.shapeScheduler = new ShapeScheduler(this);
|
||||
this.elementScheduler = new ElementScheduler(this, this.shapeScheduler);
|
||||
this.linkScheduler = new LinkScheduler();
|
||||
this.pointerScheduler = new PointerScheduler();
|
||||
constructor(DOMContainer: HTMLElement) {
|
||||
const options: Options = this.defineOptions();
|
||||
|
||||
this.containerWidth = this.DOMContainer.offsetWidth;
|
||||
this.containerHeight = this.DOMContainer.offsetHeight;
|
||||
this.elementOptions = options.element;
|
||||
this.linkOptions = options.link || { };
|
||||
this.pointerOptions = options.pointer || { };
|
||||
|
||||
this.layoutOptions = Object.assign({
|
||||
fitCenter: true,
|
||||
fitView: false
|
||||
}, options.layout);
|
||||
|
||||
this.animationOptions = Object.assign({
|
||||
enable: true,
|
||||
duration: 750,
|
||||
timingFunction: 'linearEasing'
|
||||
}, options.animation);
|
||||
|
||||
this.containerWidth = DOMContainer.offsetWidth,
|
||||
this.containerHeight = DOMContainer.offsetHeight;
|
||||
|
||||
this.modelConstructor = new ModelConstructor(this);
|
||||
this.layouter = new Layouter(this, this.containerWidth, this.containerHeight);
|
||||
this.renderer = new Renderer(this, DOMContainer, this.containerWidth, this.containerHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 输入数据进行渲染
|
||||
* @param sourceData
|
||||
*/
|
||||
public render(sourceData: Sources) {
|
||||
|
||||
if(sourceData === undefined || sourceData === null) {
|
||||
return;
|
||||
}
|
||||
@ -59,88 +59,48 @@ export class Engine {
|
||||
// 若前后数据没有发生变化,什么也不干(将json字符串化后比较)
|
||||
let stringifySources = JSON.stringify(sourceData);
|
||||
if(stringifySources === this.stringifySources) return;
|
||||
this.sources = sourceData;
|
||||
this.stringifySources = stringifySources;
|
||||
|
||||
this.constructModel(sourceData);
|
||||
this.layoutFunction(this.elementScheduler.getElementContainer(), this.containerWidth, this.containerHeight);
|
||||
const constructedData: ConstructedData = this.modelConstructor.construct(sourceData);
|
||||
|
||||
this.renderer.build(constructedData);
|
||||
|
||||
this.layouter.layout(constructedData, this.layout);
|
||||
|
||||
this.renderer.render(constructedData);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elementLabel
|
||||
* @param element
|
||||
* @param zrShapeConstructors
|
||||
* @param shapeOptions
|
||||
* 定义配置项
|
||||
* @returns
|
||||
*/
|
||||
public applyElement(
|
||||
elementLabel: string,
|
||||
elementConstructor: ElementConstructor,
|
||||
zrShapeConstructors: ZrShapeConstructor[] | ZrShapeConstructor,
|
||||
shapeOptions: Partial<ShapeStatus>
|
||||
) {
|
||||
this.elementScheduler.setElementMap(elementLabel, elementConstructor, zrShapeConstructors, shapeOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用一个Link模型
|
||||
* @param linkLabel
|
||||
* @param linkConstructor
|
||||
* @param zrShapeConstructors
|
||||
* @param shapeOptions
|
||||
*/
|
||||
public applyLink(
|
||||
linkLabel: string, linkConstructor: { new(): Link },
|
||||
zrShapeConstructors: ZrShapeConstructor[] | ZrShapeConstructor,
|
||||
shapeOptions: Partial<ShapeStatus>
|
||||
) {
|
||||
this.linkScheduler.setLinkMap(linkLabel, linkConstructor, zrShapeConstructors, shapeOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用一个Pointer模型
|
||||
* @param pointerLabel
|
||||
* @param pointerConstructor
|
||||
* @param zrShapeConstructors
|
||||
* @param shapeOptions
|
||||
*/
|
||||
public applyPointer(
|
||||
pointerLabel: string, pointerConstructor: { new(): Pointer },
|
||||
zrShapeConstructors: ZrShapeConstructor[] | ZrShapeConstructor,
|
||||
shapeOptions: Partial<ShapeStatus>
|
||||
) {
|
||||
this.pointerScheduler.setPointerMap(pointerLabel, pointerConstructor, zrShapeConstructors, shapeOptions);
|
||||
public defineOptions(): Options {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置布局函数
|
||||
* @param layoutFunction
|
||||
* @overwrite
|
||||
*/
|
||||
public applyLayout(layoutFunction: LayoutFunction) {
|
||||
this.layoutFunction = layoutFunction;
|
||||
public layout(elementContainer: { [ket: string]: Element[] }, layoutOptions: LayoutOptions) { }
|
||||
|
||||
/**
|
||||
* 获取容器尺寸
|
||||
* @returns
|
||||
*/
|
||||
public getContainerSize(): { width: number, height: number } {
|
||||
return {
|
||||
width: this.containerWidth,
|
||||
height: this.containerHeight
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建模型
|
||||
* @param sourceData
|
||||
* 绑定 G6 事件
|
||||
* @param eventName
|
||||
* @param callback
|
||||
*/
|
||||
private constructModel(sourceData: Sources) {
|
||||
this.elementScheduler.constructElements(sourceData);
|
||||
this.linkScheduler.constructLinks([]);
|
||||
this.pointerScheduler.constructPointers([]);
|
||||
}
|
||||
|
||||
private updateShapes() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置引擎数据
|
||||
*/
|
||||
private resetData() {
|
||||
this.elementScheduler.reset();
|
||||
this.linkScheduler.reset();
|
||||
this.pointerScheduler.reset();
|
||||
this.shapeScheduler.reset();
|
||||
public on(eventName: string, callback: Function) {
|
||||
this.renderer.on(eventName, callback);
|
||||
}
|
||||
};
|
84
src/options.ts
Normal file
84
src/options.ts
Normal file
@ -0,0 +1,84 @@
|
||||
|
||||
|
||||
export interface Style {
|
||||
fill: string; // 填充颜色
|
||||
text: string; // 图形文本
|
||||
textFill: string; // 文本颜色
|
||||
fontSize: number; // 字体大小
|
||||
fontWeight: number; // 字重
|
||||
stroke: string; // 描边样式
|
||||
opacity: number; // 透明度
|
||||
lineWidth: number; // 线宽
|
||||
matrix: number[]; // 变换矩阵
|
||||
};
|
||||
|
||||
|
||||
export interface ElementLabelOption {
|
||||
position: string;
|
||||
offset: number;
|
||||
style: Style;
|
||||
};
|
||||
|
||||
|
||||
export interface LinkLabelOption {
|
||||
refX: number;
|
||||
refY: number;
|
||||
position: string;
|
||||
autoRotate: boolean;
|
||||
style: Style;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export interface ElementOption {
|
||||
type: string;
|
||||
size: number | [number, number];
|
||||
rotation: number;
|
||||
anchorPoint: [number, number];
|
||||
label: string;
|
||||
labelOptions: ElementLabelOption;
|
||||
style: Style;
|
||||
}
|
||||
|
||||
|
||||
export interface LinkOption {
|
||||
type: string;
|
||||
sourceAnchor: number | ((index: number) => number);
|
||||
targetAnchor: number | ((index: number) => number);
|
||||
label: string;
|
||||
labelOptions: LinkLabelOption;
|
||||
style: Style;
|
||||
}
|
||||
|
||||
|
||||
export interface PointerOption extends ElementOption {
|
||||
position: 'top' | 'left' | 'bottom' | 'right';
|
||||
offset: number;
|
||||
};
|
||||
|
||||
|
||||
export interface LayoutOptions {
|
||||
fitCenter: boolean;
|
||||
fitView: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
|
||||
export interface AnimationOptions {
|
||||
enable: boolean;
|
||||
duration: number;
|
||||
timingFunction: string;
|
||||
};
|
||||
|
||||
|
||||
export interface Options {
|
||||
element: { [key: string]: ElementOption };
|
||||
link?: { [key: string]: LinkOption }
|
||||
pointer?: { [key: string]: PointerOption };
|
||||
layout?: LayoutOptions;
|
||||
animation?: AnimationOptions;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
@ -1,26 +1,20 @@
|
||||
|
||||
// 连接目标信息
|
||||
export type LinkTarget = {
|
||||
element: string;
|
||||
target: number | string;
|
||||
[key: string]: any;
|
||||
} | number | string;
|
||||
export type LinkTarget = number | string;
|
||||
|
||||
// 结点连接声明
|
||||
export type LinkData = LinkTarget | LinkTarget[];
|
||||
export type sourceLinkData = LinkTarget | LinkTarget[];
|
||||
|
||||
// 结点指针声明
|
||||
export type PointerData = string | string[];
|
||||
export type sourcePointerData = string | string[];
|
||||
|
||||
// 源数据单元
|
||||
export interface SourceElement {
|
||||
id: string | number;
|
||||
[key: string]: any | LinkData | PointerData;
|
||||
[key: string]: any | sourceLinkData | sourcePointerData;
|
||||
}
|
||||
|
||||
// 源数据格式
|
||||
export type Sources = { } | SourceElement[];
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,17 +1,14 @@
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "commonJS",
|
||||
"experimentalDecorators": true,
|
||||
//"outDir": "./../Demos/src/StructV",
|
||||
"outDir": "./../Visualizer/src/StructV",
|
||||
"declaration": true
|
||||
"removeComments": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules", "examples"
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -17,12 +17,9 @@ module.exports = {
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'awesome-typescript-loader',
|
||||
options: {
|
||||
configFileName: './atlconfig.json'
|
||||
}
|
||||
loader: 'awesome-typescript-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
//devtool: 'eval-source-map'
|
||||
devtool: 'eval-source-map'
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user