feat:添加多种数据结构同时渲染的支持

This commit is contained in:
黎智洲
2021-05-25 16:05:10 +08:00
parent 4c1510da1f
commit efdb325adc
24 changed files with 815 additions and 611 deletions
+39 -12
View File
@@ -1,8 +1,7 @@
import { Bound, BoundingRect } from "../../Common/boundingRect";
import { Engine } from "../../engine";
import { ConstructList } from "../../Model/modelConstructor";
import { Element, Model, Pointer } from "../../Model/modelData";
import { AnimationOptions, InteractionOptions, LayoutOptions } from "../../options";
import { Model, Pointer } from "../../Model/modelData";
import { AnimationOptions, InteractionOptions, LayoutGroupOptions, LayoutOptions } from "../../options";
import { SV } from "../../StructV";
import { Animations } from "../animation";
import { g6Behavior, Renderer } from "../renderer";
@@ -25,20 +24,45 @@ export class Container {
this.DOMContainer = DOMContainer;
this.animationsOptions = engine.animationOptions;
this.interactionOptions = engine.interactionOptions;
this.prevModelList = [];
const tooltip = new SV.G6.Tooltip({
offsetX: 10,
offsetY: 20,
shouldBegin(event) {
return event.item.getModel().SVModelType === 'element';
},
getContent(event) {
const data = event.item.SVModel.data,
wrapper = document.createElement('div');
wrapper.style.padding = '0 4px 0 4px';
wrapper.innerHTML = `
<h5>id: ${ event.item.SVModel.sourceId }</h5>
<h5>data: ${ data? data: '' }</h5>
`
return wrapper;
},
itemTypes: ['node']
});
this.renderer = new Renderer(engine, DOMContainer, {
...g6Options,
modes: {
default: this.initBehaviors()
}
default: this.initBehaviors(this.engine.optionsTable)
},
plugins: [tooltip]
});
this.prevModelList = [];
this.afterInitRenderer();
}
/**
* 初始化交互行为
* @param optionsTable
* @returns
*/
protected initBehaviors(): g6Behavior[] {
protected initBehaviors(optionsTable: { [key: string]: LayoutGroupOptions }): g6Behavior[] {
return ['drag-canvas', 'zoom-canvas'];
}
@@ -119,6 +143,11 @@ export class Container {
*/
protected handleChangeModels(models: Model[]) { }
/**
* 初始化渲染器之后的回调
*/
protected afterInitRenderer() { }
// ------------------------------------------ hook ---------------------------------------------
afterAppendModels(callback: (models: Model[]) => void) {
@@ -135,11 +164,9 @@ export class Container {
/**
* 渲染函数
* @param modelList
* @param layoutFn
*/
public render(constructList: ConstructList, layoutFn: (elements: Element[], layoutOptions: LayoutOptions) => void) {
const modelList: Model[] = [...constructList.element, ...constructList.link, ...constructList.pointer],
appendModels: Model[] = this.getAppendModels(this.prevModelList, modelList),
public render(modelList: Model[]) {
const appendModels: Model[] = this.getAppendModels(this.prevModelList, modelList),
removeModels: Model[] = this.getRemoveModels(this.prevModelList, modelList),
changeModels: Model[] = [...appendModels, ...this.findReTargetPointer(modelList)];
+6 -1
View File
@@ -3,4 +3,9 @@ import { Container } from "./container";
/**
* 释放区可视化视图
*/
export class FreedContainer extends Container { };
export class FreedContainer extends Container {
protected initBehaviors(): string[] {
return [];
}
};
+124 -31
View File
@@ -1,4 +1,5 @@
import { Link, Model } from "../../Model/modelData";
import { InteractionOptions, LayoutGroupOptions } from "../../options";
import { Container } from "./container";
@@ -8,50 +9,142 @@ import { Container } from "./container";
*/
export class MainContainer extends Container {
protected initBehaviors() {
const interactionOptions = this.interactionOptions,
dragNode: boolean | string[] = interactionOptions.dragNode,
dragNodeFilter = node => {
let model = node.item.getModel();
protected initBehaviors(optionsTable: { [key: string]: LayoutGroupOptions }) {
const dragNodeTable: { [key: string]: boolean | string[] } = { },
selectNodeTable: { [key: string]: boolean | string[] } = { },
interactionOptions: InteractionOptions = this.engine.interactionOptions,
defaultModes = [];
if(node.item === null) {
return false;
}
Object.keys(optionsTable).forEach(item => {
dragNodeTable[item] = optionsTable[item].behavior.dragNode;
selectNodeTable[item] = optionsTable[item].behavior.selectNode;
});
if(model.modelType === 'pointer') {
return false;
}
if(interactionOptions.drag) {
defaultModes.push('drag-canvas');
}
if(typeof dragNode === 'boolean') {
return dragNode;
}
if(interactionOptions.zoom) {
defaultModes.push('zoom-canvas');
}
if(Array.isArray(dragNode) && dragNode.indexOf(model.modelName) > -1) {
return true;
}
const dragNodeFilter = node => {
let model = node.item.getModel();
return false;
}
const modeMap = {
drag: 'drag-canvas',
zoom: 'zoom-canvas',
dragNode: {
type: 'drag-node',
shouldBegin: node => dragNodeFilter(node)
if(node.item === null) {
return false;
}
},
defaultModes = [];
Object.keys(interactionOptions).forEach(item => {
if(interactionOptions[item] && modeMap[item] !== undefined) {
defaultModes.push(modeMap[item]);
if(model.SVModelType === 'pointer') {
return false;
}
let dragNode = optionsTable[model.SVLayouter].behavior.dragNode;
if(typeof dragNode === 'boolean') {
return dragNode;
}
if(Array.isArray(dragNode) && dragNode.indexOf(model.SVModelName) > -1) {
return true;
}
return false;
}
const selectNodeFilter = node => {
let model = node.item.getModel();
if(node.item === null) {
return false;
}
if(model.SVModelType === 'pointer') {
return false;
}
let selectNode = optionsTable[model.SVLayouter].behavior.selectNode;
if(typeof selectNode === 'boolean') {
return selectNode;
}
if(Array.isArray(selectNode) && selectNode.indexOf(model.SVModelName) > -1) {
return true;
}
return false;
}
defaultModes.push({
type: 'drag-node',
shouldBegin: dragNodeFilter
});
defaultModes.push({
type: 'click-select',
shouldBegin: selectNodeFilter
});
return defaultModes;
}
/**
* 在初始化渲染器之后,修正节点拖拽时,外部指针没有跟着动的问题
* @param dragNodeTable
*/
protected afterInitRenderer() {
let g6Instance = this.getG6Instance(),
pointerY = null,
dragStartX = null,
dragStartY = null;
g6Instance.on('node:dragstart', ev => {
const model = ev.item.getModel(),
dragNode = this.engine.optionsTable[model.SVLayouter].behavior.dragNode;
if(dragNode === false) {
return;
}
if(Array.isArray(dragNode) && dragNode.find(item => item === model.SVModelName) === undefined) {
return;
}
pointer = g6Instance.findById(model.externalPointerId);
if(pointer) {
pointerX = pointer.getModel().x,
pointerY = pointer.getModel().y;
dragStartX = ev.canvasX;
dragStartY = ev.canvasY;
}
});
g6Instance.on('node:dragend', ev => {
pointer = null;
pointerX = null,
pointerY = null,
dragStartX = null,
dragStartY = null;
});
g6Instance.on('node:drag', ev => {
if(!pointer) {
return;
}
let dx = ev.canvasX - dragStartX,
dy = ev.canvasY - dragStartY,
zoom = g6Instance.getZoom();
pointer.updatePosition({
x: pointerX + dx / zoom,
y: pointerY + dy / zoom
});
});
}
protected handleChangeModels(models: Model[]) {
const changeHighlightColor: string = this.interactionOptions.changeHighlight;
+112 -47
View File
@@ -1,16 +1,19 @@
import { Bound, BoundingRect } from '../Common/boundingRect';
import { Group } from '../Common/group';
import { Engine } from '../engine';
import { ConstructList } from '../Model/modelConstructor';
import { LayoutGroupTable } from '../Model/modelConstructor';
import { Element, Model, Pointer } from '../Model/modelData';
import { LayoutOptions, PointerOption } from '../options';
import { LayoutOptions, PointerOption, ViewOptions } from '../options';
import { Container } from './container/container';
export class Layouter {
private engine: Engine;
private engine: Engine;
private viewOptions: ViewOptions;
constructor(engine: Engine) {
this.engine = engine;
this.viewOptions = this.engine.viewOptions;
}
@@ -19,7 +22,7 @@ export class Layouter {
* @param elements
* @param pointers
*/
private initLayoutValue(elements: Element[], pointers: Pointer[]) {
private initLayoutValue(elements: Element[], pointers: Pointer[]) {
[...elements, ...pointers].forEach(item => {
item.set('rotation', item.get('rotation'));
item.set({ x: 0, y: 0 });
@@ -29,10 +32,11 @@ export class Layouter {
/**
* 布局外部指针
* @param pointer
* @param pointerOptions
*/
private layoutPointer(pointers: Pointer[]) {
private layoutPointer(pointers: Pointer[], pointerOptions: { [key: string]: PointerOption }) {
pointers.forEach(item => {
const options: PointerOption = this.engine.pointerOptions[item.getType()],
const options: PointerOption = pointerOptions[item.getType()],
offset = options.offset || 8,
anchor = options.anchor || 0;
@@ -52,57 +56,118 @@ export class Layouter {
* @param container
* @param models
*/
private fitCenter(container: Container, models: Model[]) {
if(models.length === 0) {
return;
}
const viewBound: BoundingRect = models.map(item => item.getBound()).reduce((prev, cur) => Bound.union(prev, cur));
private fitCenter(container: Container, group: Group) {
let width = container.getG6Instance().getWidth(),
height = container.getG6Instance().getHeight(),
viewBound: BoundingRect = group.getBound(),
centerX = width / 2, centerY = height / 2,
boundCenterX = viewBound.x + viewBound.width / 2,
boundCenterY = viewBound.y + viewBound.height / 2,
dx = centerX - boundCenterX,
dy = centerY - boundCenterY;
models.forEach(item => {
item.set({
x: item.get('x') + dx,
y: item.get('y') + dy
group.translate(dx, dy);
}
/**
* 对每个组内部的model进行布局
* @param layoutGroupTable
*/
private layoutModels(layoutGroupTable: LayoutGroupTable): Group[] {
const modelGroupList: Group[] = [];
layoutGroupTable.forEach((group, groupName) => {
const options: LayoutOptions = group.options.layout,
modelList: Model[] = group.modelList,
modelGroup: Group = new Group();
modelList.forEach(item => {
modelGroup.add(item);
});
this.initLayoutValue(group.element, group.pointer); // 初始化布局参数
group.layouter.layout(group.element, options); // 布局节点
this.layoutPointer(group.pointer, group.options.pointer); // 布局外部指针
modelGroupList.push(modelGroup);
});
return modelGroupList;
}
/**
* 对所有组进行相互布局
* @param container
* @param modelGroupTable
*/
private layoutGroups(container: Container, modelGroupList: Group[]): Group {
let wrapperGroup: Group = new Group(),
group: Group,
prevBound: BoundingRect,
bound: BoundingRect,
boundList: BoundingRect[] = [],
maxHeight: number = -Infinity,
dx = 0, dy = 0;
// 左往右布局
for(let i = 0; i < modelGroupList.length; i++) {
group = modelGroupList[i],
bound = group.getPaddingBound(this.viewOptions.groupPadding);
if(prevBound) {
dx = prevBound.x + prevBound.width - bound.x;
}
else {
dx = bound.x;
}
if(bound.height > maxHeight) {
maxHeight = bound.height;
}
group.translate(dx, 0);
Bound.translate(bound, dx, 0);
boundList.push(bound);
wrapperGroup.add(group);
prevBound = bound;
}
// 居中对齐布局
for(let i = 0; i < modelGroupList.length; i++) {
group = modelGroupList[i];
bound = boundList[i];
dy = maxHeight / 2 - bound.height / 2;
group.translate(0, dy);
Bound.translate(bound, 0, dy);
}
return wrapperGroup;
}
/**
* 布局
* @param container
* @param layoutGroupTable
*/
public layoutAll(container: Container, layoutGroupTable: LayoutGroupTable) {
layoutGroupTable.forEach(item => {
item.modelList.forEach(model => {
model.G6Item = model.shadowG6Item;
});
});
const modelGroupList: Group[] = this.layoutModels(layoutGroupTable),
wrapperGroup: Group = this.layoutGroups(container, modelGroupList);
this.fitCenter(container, wrapperGroup);
layoutGroupTable.forEach(item => {
item.modelList.forEach(model => {
model.G6Item = model.renderG6Item;
});
});
}
/**
* 进行布局
* @param container
* @param constructList
* @param layoutFn
*/
public layout(container: Container, constructList: ConstructList, layoutFn: (elements: Element[], layoutOptions: LayoutOptions) => void) {
const options: LayoutOptions = this.engine.layoutOptions,
modelList: Model[] = [...constructList.element, ...constructList.pointer, ...constructList.link];
// 首先初始化所有节点的坐标为0,且设定旋转
modelList.forEach(item => {
item.G6Item = item.shadowG6Item;
});
// 初始化布局参数
this.initLayoutValue(constructList.element, constructList.pointer);
// 布局节点
layoutFn(constructList.element, options);
// 布局外部指针
this.layoutPointer(constructList.pointer);
// 将视图调整到画布中心
options.fitCenter && this.fitCenter(container, modelList);
modelList.forEach(item => {
item.G6Item = item.renderG6Item;
});
}
}
+2 -1
View File
@@ -86,13 +86,14 @@ export class Renderer {
this.g6Instance.changeData(renderData);
}
if(this.engine.layoutOptions.fitView) {
if(this.engine.viewOptions.fitView) {
this.g6Instance.fitView();
}
modelList.forEach(item => {
item.renderG6Item = this.g6Instance.findById(item.id);
item.G6Item = item.renderG6Item;
item.renderG6Item.SVModel = item;
});
// 把所有连线置顶
+119 -75
View File
@@ -1,13 +1,14 @@
import { Engine } from "../engine";
import { Element, Link } from "../Model/modelData";
import { EngineInitOptions, LayoutOptions } from "../options";
import { Element, Link, Model } from "../Model/modelData";
import { EngineOptions } from "../options";
import { Container } from "./container/container";
import { SV } from '../StructV';
import { ConstructList } from "../Model/modelConstructor";
import { MainContainer } from "./container/main";
import { FreedContainer } from "./container/freed";
import { LeakContainer } from "./container/leak";
import { Layouter } from "./layouter";
import { LayoutGroup, LayoutGroupTable } from "../Model/modelConstructor";
import { Util } from "../Common/util";
export class ViewManager {
@@ -17,9 +18,7 @@ export class ViewManager {
private freedContainer: Container;
private leakContainer: Container;
private prevConstructList: ConstructList = { element:[], pointer: [], link: [] };
private freedConstructList: ConstructList = { element:[], pointer: [], link: [] };
private leakConstructList: ConstructList = { element:[], pointer: [], link: [] };
private prevLayoutGroupTable: LayoutGroupTable;
private shadowG6Instance;
@@ -27,8 +26,9 @@ export class ViewManager {
this.engine = engine;
this.layouter = new Layouter(engine);
this.mainContainer = new MainContainer(engine, DOMContainer);
this.prevLayoutGroupTable = null;
const options: EngineInitOptions = this.engine.initOptions;
const options: EngineOptions = this.engine.engineOptions;
if(options.freedContainer) {
this.freedContainer = new FreedContainer(engine, options.freedContainer, { fitCenter: true });
@@ -47,38 +47,49 @@ export class ViewManager {
* 对每一个 model 在离屏 Canvas 上构建 G6 item,用作布局
* @param constructList
*/
private build(constructList: ConstructList) {
constructList.element.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('node', item));
constructList.pointer.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('node', item));
constructList.link.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('edge', item));
constructList.element.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id);
});
constructList.pointer.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id);
});
constructList.link.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id);
private build(layoutGroupTable: LayoutGroupTable) {
layoutGroupTable.forEach(group => {
group.element.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('node', item));
group.pointer.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('node', item));
group.link.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('edge', item));
group.element.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id);
});
group.pointer.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id);
});
group.link.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id);
});
});
}
/**
* 获取被 free 的节点
* @param constructList
* @param layoutGroupTable
* @returns
*/
private getFreedConstructList(constructList: ConstructList): ConstructList {
const freedList: ConstructList = {
element: constructList.element.filter(item => item.free),
pointer: [],
link: []
};
private getFreedConstructList(layoutGroupTable: LayoutGroupTable): Model[] {
let freedList: Model[] = [],
freedGroup = null,
freedGroupName = null;
freedList.element.forEach(fItem => {
constructList.element.splice(constructList.element.findIndex(item => item.id === fItem.id), 1);
constructList.link.splice(constructList.link.findIndex(item => item.element.id === fItem.id || item.target.id === fItem.id));
constructList.pointer.splice(constructList.pointer.findIndex(item => item.target.id === fItem.id));
for(let group in layoutGroupTable) {
let freedElements: Model[] = layoutGroupTable[group].element.filter(item => item.freed);
if(freedElements.length) {
freedGroupName = group;
break;
}
}
freedGroup = layoutGroupTable[freedGroupName];
freedList.forEach(fItem => {
freedGroup.element.splice(freedGroup.element.findIndex(item => item.id === fItem.id), 1);
freedGroup.link.splice(freedGroup.link.findIndex(item => item.element.id === fItem.id || item.target.id === fItem.id));
freedGroup.pointer.splice(freedGroup.pointer.findIndex(item => item.target.id === fItem.id));
});
return freedList;
@@ -90,47 +101,63 @@ export class ViewManager {
* @param prevConstructList
* @returns
*/
private getLeakConstructList(prevConstructList: ConstructList, constructList: ConstructList): ConstructList {
const elements: Element[] = prevConstructList.element.filter(item => !constructList.element.find(n => n.id === item.id)),
links: Link[] = prevConstructList.link.filter(item => !constructList.link.find(n => n.id === item.id)),
elementIds: string[] = elements.map(item => item.id);
private getLeakConstructList(prevLayoutGroupTable: LayoutGroupTable, layoutGroupTable: LayoutGroupTable): LayoutGroupTable {
const leakLayoutGroupTable = new Map<string, LayoutGroup>();
elements.forEach(item => {
item.set('style', {
fill: '#ccc'
prevLayoutGroupTable.forEach((item, groupName) => {
let prevGroup = item,
curGroup = layoutGroupTable.get(groupName),
elements: Element[] = [],
links: Link[] = [],
elementIds: string[] = [];
if(curGroup) {
elements = prevGroup.element.filter(item => !curGroup.element.find(n => n.id === item.id)).filter(item => item.freed === false),
links = prevGroup.link.filter(item => !curGroup.link.find(n => n.id === item.id)),
elementIds = elements.map(item => item.id);
}
elements.forEach(item => {
item.set('style', {
fill: '#ccc'
});
});
for(let i = 0; i < links.length; i++) {
let sourceId = links[i].element.id,
targetId = links[i].target.id;
links[i].set('style', {
stroke: '#333'
});
if(elementIds.find(item => item === sourceId) === undefined || elementIds.find(item => item === targetId) === undefined) {
links.splice(i, 1);
i--;
}
}
leakLayoutGroupTable.set(groupName, {
element: elements,
link: links,
pointer: [],
layouter: prevGroup.layouter,
options: prevGroup.options,
modelList: [...elements, ...links]
});
});
for(let i = 0; i < links.length; i++) {
let sourceId = links[i].element.id,
targetId = links[i].target.id;
links[i].set('style', {
stroke: '#333'
});
if(elementIds.find(item => item === sourceId) === undefined || elementIds.find(item => item === targetId) === undefined) {
links.splice(i, 1);
i--;
}
}
return {
element: elements,
link: links,
pointer: []
};
return leakLayoutGroupTable;
}
// ----------------------------------------------------------------------------------------------
/**
* 对主视图进行重新布局
* @param constructList
* @param layoutFn
* @param layoutGroupTable
*/
reLayout(constructList: ConstructList, layoutFn: (elements: Element[], layoutOptions: LayoutOptions) => void) {
this.layouter.layout(this.mainContainer, constructList, layoutFn);
reLayout(layoutGroupTable: LayoutGroupTable) {
this.layouter.layoutAll(this.mainContainer, layoutGroupTable);
}
@@ -150,11 +177,23 @@ export class ViewManager {
/**
* 重新调整容器尺寸
* @param containerName
* @param width
* @param height
*/
resize(width: number, height: number) {
this.mainContainer.getG6Instance().changeSize(width, height);
resize(containerName: string, width: number, height: number) {
if(containerName === 'main') {
this.mainContainer.getG6Instance().changeSize(width, height);
}
if(containerName === 'freed') {
this.freedContainer.getG6Instance().changeSize(width, height);
}
if(containerName === 'leak') {
this.leakContainer.getG6Instance().changeSize(width, height);
}
}
/**
@@ -162,31 +201,36 @@ export class ViewManager {
* @param models
* @param layoutFn
*/
renderAll(constructList: ConstructList, layoutFn: (elements: Element[], layoutOptions: LayoutOptions) => void) {
renderAll(layoutGroupTable: LayoutGroupTable) {
this.shadowG6Instance.clear();
this.build(constructList);
this.build(layoutGroupTable);
this.freedConstructList = this.getFreedConstructList(constructList);
this.leakConstructList = this.getLeakConstructList(this.prevConstructList, constructList);
this.build(this.leakConstructList);
let freedList = this.getFreedConstructList(layoutGroupTable),
leakLayoutGroupTable = null;
if(this.leakContainer && this.prevLayoutGroupTable) {
leakLayoutGroupTable = this.getLeakConstructList(this.prevLayoutGroupTable, layoutGroupTable);
this.build(leakLayoutGroupTable);
}
if(this.freedContainer) {
this.freedContainer.render(this.freedConstructList, layoutFn);
this.freedContainer.render(freedList);
}
// 进行布局(设置model的x,y)
this.layouter.layout(this.mainContainer, constructList, layoutFn);
this.mainContainer.render(constructList, layoutFn);
this.layouter.layoutAll(this.mainContainer, layoutGroupTable);
if(this.leakContainer) {
const modelList: Model[] = Util.convertGroupTable2ModelList(layoutGroupTable);
this.mainContainer.render(modelList);
if(this.leakContainer && this.prevLayoutGroupTable) {
this.mainContainer.afterRemoveModels(() => {
this.leakContainer.render(this.leakConstructList, layoutFn);
this.leakContainer.render(Util.convertGroupTable2ModelList(leakLayoutGroupTable));
});
}
this.prevConstructList = constructList;
this.prevLayoutGroupTable = layoutGroupTable;
}
/**