基本重构完成
底层渲染库舍弃 zrender,换为 antvG6
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+202
-3
@@ -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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user