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

4
.gitignore vendored
View File

@ -1,2 +1,4 @@
node_modules
test
test
demoV2
demo

View File

@ -238,6 +238,15 @@ const data = {
node: []
}
let d = [
{ id: 10, type: 'head', name: 'QPtr', label: 'front', external: ['lq'], front: 0 },
{ id: 11, type: 'head', name: 'QPtr', label: 'rear', external: null, rear: 2 },
{ id: 0, next: 1 },
{ id: 1, next: 2 },
{ id: 2 }
];
const LQueue = function(container) {
return{

View File

@ -82,6 +82,8 @@ const Engine = SV.Engine,
<script src="./dataStruct/GeneralizedList.js"></script>
<script>
const engines = {
0: BTree,
1: LList,
@ -98,7 +100,7 @@ const engines = {
let dataCounter = 0;
let cur = engines[3](document.getElementById('container'), {
let cur = engines[1](document.getElementById('container'), {
freedContainer: document.getElementById('freed'),
leakContainer: document.getElementById('leak')
});
@ -113,12 +115,9 @@ document.getElementById('btn-next').addEventListener('click', e => {
cur.engine.reLayout();
});
document.getElementById('btn-set').addEventListener('click', e => {
let els = cur.engine.getElements();
els.map(item => {
item.set('style', { fill: 'red' });
});
cur.engine.on('node:mouseover', evt => {
console.log(evt);
});

2
dist/sv.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,154 +0,0 @@
import { Engine } from "../engine";
import { Util } from "../Common/util";
export class Behavior {
private engine: Engine;
private graphInstance;
constructor(engine: Engine, graphInstance) {
this.engine = engine;
this.graphInstance = graphInstance;
const interactionOptions = this.engine.interactionOptions,
selectNode: boolean | string[] = interactionOptions.selectNode,
dragNode: boolean | string[] = interactionOptions.dragNode;
if(interactionOptions.dragNode) {
this.initDragNode(dragNode);
}
if(interactionOptions.selectNode) {
this.initSelectNode(selectNode);
}
}
/**
*
*/
private initDragNode(dragNode: boolean | string[]) {
let pointer = null,
pointerX = null,
pointerY = null,
dragStartX = null,
dragStartY = null;
this.graphInstance.on('node:dragstart', ev => {
if(dragNode === false) {
return;
}
let model = ev.item.getModel();
if(Array.isArray(dragNode) && dragNode.find(item => item === model.modelName) === undefined) {
return;
}
pointer = this.graphInstance.findById(model.externalPointerId);
if(pointer) {
pointerX = pointer.getModel().x,
pointerY = pointer.getModel().y;
dragStartX = ev.canvasX;
dragStartY = ev.canvasY;
}
});
this.graphInstance.on('node:dragend', ev => {
pointer = null;
pointerX = null,
pointerY = null,
dragStartX = null,
dragStartY = null;
});
this.graphInstance.on('node:drag', ev => {
if(!pointer) {
return;
}
let dx = ev.canvasX - dragStartX,
dy = ev.canvasY - dragStartY,
zoom = this.graphInstance.getZoom();
pointer.updatePosition({
x: pointerX + dx / zoom,
y: pointerY + dy / zoom
});
});
}
/**
* /
* @param selectNode
*/
private initSelectNode(selectNode: boolean | string[]) {
let defaultHighlightColor = '#f08a5d',
curSelectItem = null,
curSelectItemStyle = null;
if(selectNode === false) {
return;
}
const selectCallback = ev => {
const item = ev.item,
model = item.getModel(),
type = item.getType(),
name = model.modelName,
highlightColor = model.style.selectedColor;
if(Array.isArray(selectNode) && selectNode.find(item => item === name) === undefined) {
return;
}
if(model.isDynamic) {
return;
}
if(curSelectItem && curSelectItem !== item) {
curSelectItem.update({
style: curSelectItemStyle
});
}
curSelectItem = item;
curSelectItemStyle = Util.objectClone(curSelectItem.getModel().style);
curSelectItem.update({
style: {
...curSelectItemStyle,
[type === 'node'? 'fill': 'stroke']: highlightColor || defaultHighlightColor
}
});
};
this.graphInstance.on('node:click', selectCallback);
this.graphInstance.on('edge:click', selectCallback);
this.graphInstance.on('click', ev => {
if(curSelectItem === null) {
return;
}
curSelectItem.update({
style: curSelectItemStyle
});
curSelectItem = null;
curSelectItemStyle = null;
});
}
/**
* G6
* @param eventName
* @param callback
*/
public on(eventName: string, callback: Function) {
if(this.graphInstance === null) {
return;
}
this.graphInstance.on(eventName, evt => {
callback(evt.item);
});
}
};

View File

@ -106,6 +106,17 @@ export const Bound = {
};
},
/**
*
* @param bound
* @param dx
* @param dy
*/
translate(bound: BoundingRect, dx: number, dy: number) {
bound.x += dx;
bound.y += dy;
},
/**
*
* @param bound

View File

@ -1,18 +1,17 @@
import { Util } from "./util";
import { BoundingRect, Bound } from "./boundingRect";
import { Vector } from "./vector";
import { Element } from "../Model/modelData";
import { Element, Model } from "../Model/modelData";
/**
* element
* model
*/
export class Group {
id: string;
private elements: Array<Element | Group> = [];
private models: Array<Model | Group> = [];
constructor(...arg: Array<Element | Group>) {
constructor(...arg: Array<Model | Group>) {
this.id = Util.generateId();
if(arg) {
@ -24,25 +23,43 @@ export class Group {
* element
* @param arg
*/
add(...arg: Array<Element | Group>) {
add(...arg: Array<Model | Group>) {
arg.map(ele => {
this.elements.push(ele);
this.models.push(ele);
});
}
/**
* element
* model
* @param element
*/
remove(element: Element | Group) {
Util.removeFromList(this.elements, item => item.id === element.id);
remove(model: Model | Group) {
Util.removeFromList(this.models, item => item.id === model.id);
}
/**
* group的包围盒
*/
getBound(): BoundingRect {
return Bound.union(...this.elements.map(item => item.getBound()));
return this.models.length?
Bound.union(...this.models.map(item => item.getBound())):
{ x: 0, y: 0, width: 0, height: 0 };
}
/**
*
* @param padding
* @returns
*/
getPaddingBound(padding: number = 0): BoundingRect {
const bound = this.getBound();
bound.x -= padding;
bound.y -= padding;
bound.width += padding * 2;
bound.height += padding * 2;
return bound;
}
/**
@ -51,7 +68,7 @@ export class Group {
* @param dy
*/
translate(dx: number, dy: number) {
this.elements.map(item => {
this.models.map(item => {
if(item instanceof Group) {
item.translate(dx, dy);
}
@ -62,40 +79,10 @@ export class Group {
});
}
/**
* 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;
this.models.length = 0;
}
}

View File

@ -1,4 +1,4 @@
import { ConstructList } from "../Model/modelConstructor";
import { LayoutGroup, LayoutGroupTable } from "../Model/modelConstructor";
import { G6EdgeModel, G6NodeModel, Link, Model } from "../Model/modelData";
import { SV } from "../StructV";
import { G6Data } from "../View/renderer";
@ -78,21 +78,27 @@ export const Util = {
/**
*
* @param constructListType
* @param groupTable
* @returns
*/
converterList(modelContainer: { [key: string]: ConstructList[keyof ConstructList]}) {
return [].concat(...Object.keys(modelContainer).map(item => modelContainer[item]));
convertGroupTable2ModelList(groupTable: LayoutGroupTable): Model[] {
const list: Model[] = [];
groupTable.forEach(item => {
list.push(...item.modelList);
});
return list;
},
/**
* G6 data
* @param constructList
* @param layoutGroup
* @returns
*/
convertG6Data(constructList: ConstructList): G6Data {
let nodes = [...constructList.element, ...constructList.pointer],
edges = constructList.link;
convertG6Data(layoutGroup: LayoutGroup): G6Data {
let nodes = [...layoutGroup.element, ...layoutGroup.pointer],
edges = layoutGroup.link;
return {
nodes: nodes.map(item => item.cloneProps()) as G6NodeModel[],

View File

@ -1,58 +1,120 @@
import { Util } from "../Common/util";
import { Engine } from "../engine";
import { LinkOption, PointerOption } from "../options";
import { sourceLinkData, SourceElement, LinkTarget } from "../sources";
import { Element, Link, Pointer } from "./modelData";
import { ElementOption, Layouter, LayoutGroupOptions, LinkOption, PointerOption } from "../options";
import { sourceLinkData, SourceElement, LinkTarget, Sources } from "../sources";
import { SV } from "../StructV";
import { Element, Link, Model, Pointer } from "./modelData";
export interface ConstructList {
export type LayoutGroup = {
element: Element[];
link: Link[];
pointer: Pointer[];
layouter: Layouter;
options: LayoutGroupOptions;
modelList: Model[];
};
export type LayoutGroupTable = Map<string, LayoutGroup>;
export class ModelConstructor {
private engine: Engine;
private constructList: ConstructList;
private layoutGroupTable: LayoutGroupTable;
private prevSourcesStringMap: { [key: string]: string }; // 保存上一次源数据转换为字符串之后的值,用作比较该次源数据和上一次源数据是否有差异,若相同,则可跳过重复构建过程
constructor(engine: Engine) {
this.engine = engine;
this.prevSourcesStringMap = { };
}
/**
* elementlink和pointer
* @param sourceList
*/
public construct(sourceList: SourceElement[]): ConstructList {
let elementContainer = this.constructElements(sourceList),
linkContainer = this.constructLinks(this.engine.linkOptions, elementContainer),
pointerContainer = this.constructPointers(this.engine.pointerOptions, elementContainer);
this.constructList = {
element: Util.converterList(elementContainer),
link: Util.converterList(linkContainer),
pointer: Util.converterList(pointerContainer)
};
public construct(sources: Sources): LayoutGroupTable {
const layoutGroupTable = new Map<string, LayoutGroup>(),
layouterMap: { [key: string]: Layouter } = SV.registeredLayouter,
optionsTable = this.engine.optionsTable;
return this.constructList;
Object.keys(sources).forEach(name => {
let sourceGroup = sources[name],
layouterName = sourceGroup.layouter;
if(!layouterName) {
layoutGroupTable.set(name, {
element: [],
link: [],
pointer: [],
options: null,
layouter: null,
modelList: []
});
return;
}
let sourceDataString: string = JSON.stringify(sourceGroup.data),
prevString: string = this.prevSourcesStringMap[name],
layouter: Layouter = null,
options: LayoutGroupOptions = null,
elementList: Element[] = [],
pointerList: Pointer[] = [];
if(prevString === sourceDataString) {
return;
}
layouter = layouterMap[sourceGroup.layouter];
options = optionsTable[layouterName];
const sourceData = layouter.sourcesPreprocess? layouter.sourcesPreprocess(sourceGroup.data): sourceGroup.data;
elementList = this.constructElements(options.element, name, sourceData, layouterName);
pointerList = this.constructPointers(options.pointer, elementList);
layoutGroupTable.set(name, {
element: elementList,
link: [],
pointer: pointerList,
options: options,
layouter: layouter,
modelList: [...elementList, ...pointerList]
});
});
layoutGroupTable.forEach((layoutGroup: LayoutGroup) => {
const linkList: Link[] = this.constructLinks(layoutGroup.options.link, layoutGroup.element, layoutGroupTable);
layoutGroup.link = linkList;
layoutGroup.modelList.push(...linkList);
});
this.layoutGroupTable = layoutGroupTable;
return this.layoutGroupTable;
}
/**
*
* @returns
*/
public getConstructList(): ConstructList {
return this.constructList;
public getLayoutGroupTable(): LayoutGroupTable {
return this.layoutGroupTable;
}
/**
* element
* @param sourceList
* @param elementOptions
* @param groupName
* @param sourceList
* @param layouterName
* @returns
*/
private constructElements(sourceList: SourceElement[]): { [key: string]: Element[] } {
private constructElements(elementOptions: { [key: string]: ElementOption }, groupName: string, sourceList: SourceElement[], layouterName: string): Element[] {
let defaultElementType: string = 'default',
elementContainer: { [key: string]: Element[] } = { };
elementList: Element[] = [];
sourceList.forEach(item => {
if(item === null) {
@ -63,37 +125,26 @@ export class ModelConstructor {
item.type = defaultElementType;
}
if(elementContainer[item.type] === undefined) {
elementContainer[item.type] = [];
}
elementContainer[item.type].push(this.createElement(item, item.type));
elementList.push(this.createElement(item, item.type, groupName, layouterName, elementOptions[item.type]));
});
return elementContainer;
return elementList;
}
/**
* element link
* @param linkOptions
* @param elementContainer
* @param elements
* @param layoutGroupTable
* @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]),
private constructLinks(linkOptions: { [key: string]: LinkOption }, elements: Element[], layoutGroupTable: LayoutGroupTable): Link[] {
let linkList: Link[] = [],
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],
for(let i = 0; i < elements.length; i++) {
let element: Element = elements[i],
sourceLinkData: sourceLinkData = element.sourceElement[name],
targetElement: Element | Element[] = null,
link: Link = null;
@ -106,22 +157,22 @@ export class ModelConstructor {
// ------------------- 将连接声明字段 sourceLinkData 从 id 变为 Element -------------------
if(Array.isArray(sourceLinkData)) {
element[name] = sourceLinkData.map((item, index) => {
targetElement = this.fetchTargetElements(elementContainer, element, item);
targetElement = this.fetchTargetElements(layoutGroupTable, element, item);
if(targetElement) {
link = this.createLink(name, element, targetElement, index);
linkContainer[name].push(link);
link = this.createLink(name, element, targetElement, index, linkOptions[name]);
linkList.push(link);
}
return targetElement;
});
}
else {
targetElement = this.fetchTargetElements(elementContainer, element, sourceLinkData);
targetElement = this.fetchTargetElements(layoutGroupTable, element, sourceLinkData);
if(targetElement) {
link = this.createLink(name, element, targetElement, null);
linkContainer[name].push(link);
link = this.createLink(name, element, targetElement, null, linkOptions[name]);
linkList.push(link);
}
element[name] = targetElement;
@ -129,64 +180,57 @@ export class ModelConstructor {
}
});
return linkContainer;
return linkList;
}
/**
* element pointer
* @param pointerOptions
* @param elementContainer
* @param elements
* @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]),
private constructPointers(pointerOptions: { [key: string]: PointerOption }, elements: Element[]): Pointer[] {
let pointerList: Pointer[] = [],
pointerNames = Object.keys(pointerOptions);
pointerNames.forEach(name => {
pointerContainer[name] = [];
});
pointerNames.forEach(name => {
for(let i = 0; i < elementList.length; i++) {
let element = elementList[i],
for(let i = 0; i < elements.length; i++) {
let element = elements[i],
pointerData = element[name];
// 若没有指针字段的结点则跳过
if(!pointerData) continue;
let id = name + '.' + (Array.isArray(pointerData)? pointerData.join('-'): pointerData),
pointer = this.createPointer(id, name, pointerData, element);
pointer = this.createPointer(id, name, pointerData, element, pointerOptions[name]);
pointerContainer[name].push(pointer);
pointerList.push(pointer);
}
});
return pointerContainer;
return pointerList;
}
/**
* Element
* @param sourceElement
* @param elementName
* @param groupName
* @param layouterName
* @param options
*/
private createElement(sourceElement: SourceElement, elementName: string): Element {
let elementOption = this.engine.elementOptions[elementName],
element: Element = undefined,
label = elementOption.label? this.parserElementContent(sourceElement, elementOption.label): '',
private createElement(sourceElement: SourceElement, elementName: string, groupName: string, layouterName: string, options: ElementOption): Element {
let element: Element = undefined,
label = options.label? this.parserElementContent(sourceElement, options.label): '',
id = elementName + '.' + sourceElement.id.toString();
if(label === null || label === 'undefined') {
label = '';
}
element = new Element(id, elementName, sourceElement);
element.initProps(elementOption);
element = new Element(id, elementName, groupName, layouterName, sourceElement);
element.initProps(options);
element.set('label', label);
element.sourceElement = sourceElement;
@ -199,10 +243,10 @@ export class ModelConstructor {
* @param pointerName
* @param label
* @param target
* @param options
*/
private createPointer(id: string, pointerName: string, pointerData: string | string[], target: Element): Pointer {
let options = this.engine.pointerOptions[pointerName],
pointer = undefined;
private createPointer(id: string, pointerName: string, pointerData: string | string[], target: Element, options: PointerOption): Pointer {
let pointer = undefined;
pointer = new Pointer(id, pointerName, pointerData, target);
pointer.initProps(options);
@ -216,10 +260,10 @@ export class ModelConstructor {
* @param element
* @param target
* @param index
* @param options
*/
private createLink(linkName: string, element: Element, target: Element, index: number): Link {
let options: LinkOption = this.engine.linkOptions[linkName],
link = undefined,
private createLink(linkName: string, element: Element, target: Element, index: number, options: LinkOption): Link {
let link = undefined,
id = `${element.id}-${target.id}`;
link = new Link(id, linkName, element, target, index);
@ -233,7 +277,7 @@ export class ModelConstructor {
* @param sourceElement
* @param formatLabel
*/
private parserElementContent(sourceElement: SourceElement, formatLabel: string): string {
private parserElementContent(sourceElement: SourceElement, formatLabel: string): string {
let fields = Util.textParser(formatLabel);
if(Array.isArray(fields)) {
@ -253,31 +297,44 @@ export class ModelConstructor {
* @param element
* @param linkTarget
*/
private fetchTargetElements(
elementContainer: { [key: string]: Element[] } ,
element: Element,
linkTarget: LinkTarget
): Element {
let elementName = element.getType(),
private fetchTargetElements(layoutGroupTable: LayoutGroupTable, element: Element, linkTarget: LinkTarget): Element {
let groupName: string = element.groupName,
elementName = element.type,
elementList: Element[],
targetId = linkTarget,
targetGroupName = groupName,
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 linkTarget === 'number' || (typeof linkTarget === 'string' && !linkTarget.includes('#'))) {
linkTarget = 'default#' + linkTarget;
}
if(typeof targetId === 'number') {
targetId = targetId.toString();
let info = linkTarget.split('#');
targetId = info.pop();
if(info.length > 1) {
elementName = info.pop();
targetGroupName = info.pop();
}
else {
let field = info.pop();
if(layoutGroupTable.get(targetGroupName).element.find(item => item.type === field)) {
elementName = field;
}
else if(layoutGroupTable.has(field)) {
targetGroupName = field;
}
else {
return null;
}
}
elementList = elementContainer[elementName];
elementList = layoutGroupTable.get(targetGroupName).element.filter(item => item.type === elementName);
// 若目标element不存在返回null
if(elementList === undefined) {
@ -292,6 +349,6 @@ export class ModelConstructor {
*
*/
destroy() {
this.constructList = null;
this.layoutGroupTable = null;
}
};

View File

@ -17,6 +17,7 @@ export interface G6NodeModel {
style: Style;
labelCfg: ElementLabelOption;
externalPointerId: string;
SVLayouter: string;
SVModelType: string;
SVModelName: string;
};
@ -156,22 +157,30 @@ export class Model {
getType(): string {
return this.type;
}
getId(): string {
return this.id;
}
}
export class Element extends Model {
sourceElement: SourceElement;
sourceId: string;
free: boolean;
groupName: string;
layouterName: string;
freed: boolean;
constructor(id: string, type: string, sourceElement: SourceElement) {
constructor(id: string, type: string, group: string, layouter: string, sourceElement: SourceElement) {
super(id, type);
if(type === null) {
return;
}
this.free = false;
this.groupName = group;
this.layouterName = layouter;
this.freed = false;
Object.keys(sourceElement).map(prop => {
if(prop !== 'id') {
@ -197,6 +206,7 @@ export class Element extends Model {
style: Util.objectClone<Style>(option.style),
labelCfg: Util.objectClone<ElementLabelOption>(option.labelOptions),
externalPointerId: null,
SVLayouter: this.layouterName,
SVModelType: 'element',
SVModelName: this.type
};
@ -274,6 +284,7 @@ export class Pointer extends Model {
style: Util.objectClone<Style>(option.style),
labelCfg: Util.objectClone<ElementLabelOption>(option.labelOptions),
externalPointerId: null,
SVLayouter: null,
SVModelType: 'pointer',
SVModelName: this.type
};

View File

@ -27,7 +27,7 @@ export default G6.registerNode('binary-tree-node', {
y: height / 2,
width: width / 2,
height: height,
fill: cfg.style.fill,
fill: cfg.color || cfg.style.fill,
stroke: cfg.style.stroke || '#333',
cursor: cfg.style.cursor
},

View File

@ -16,7 +16,8 @@ export default G6.registerNode('indexed-node', {
width: width,
height: height,
stroke: cfg.style.stroke || '#333',
fill: disable? '#ccc': cfg.style.fill
fill: disable? '#ccc': cfg.style.fill,
cursor: cfg.style.cursor,
},
name: 'wrapper'
});

View File

@ -15,7 +15,8 @@ export default G6.registerNode('link-list-node', {
width: width,
height: height,
stroke: cfg.style.stroke || '#333',
fill: '#eee'
fill: '#eee',
cursor: cfg.style.cursor
},
name: 'wrapper'
});
@ -27,7 +28,8 @@ export default G6.registerNode('link-list-node', {
width: width * (2 / 3),
height: height,
fill: cfg.style.fill,
stroke: cfg.style.stroke || '#333'
stroke: cfg.style.stroke || '#333',
cursor: cfg.style.cursor
},
name: 'main-rect',
draggable: true

View File

@ -44,7 +44,8 @@ export default G6.registerNode('two-cell-node', {
textBaseline: 'middle',
text: cfg.label,
fill: style.fill || '#000',
fontSize: style.fontSize || 16
fontSize: style.fontSize || 16,
cursor: cfg.style.cursor,
},
name: 'text',
draggable: true

View File

@ -10,6 +10,10 @@ import { Vector } from "./Common/vector";
import indexedNode from "./RegisteredShape/indexedNode";
export const SV = {
Engine: Engine,
Group: Group,
@ -17,6 +21,7 @@ export const SV = {
Vector: Vector,
Mat3: G6.Util.mat3,
G6,
registeredShape: [
externalPointer,
linkListNode,
@ -24,6 +29,18 @@ export const SV = {
twoCellNode,
indexedNode
],
registerShape: G6.registerNode
registeredLayouter: { },
registerShape: G6.registerNode,
/**
*
* @param name
* @param layouter
*/
registerLayouter(name: string, layouter) {
SV.registeredLayouter[name] = layouter;
}
};

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)];

View File

@ -3,4 +3,9 @@ import { Container } from "./container";
/**
*
*/
export class FreedContainer extends Container { };
export class FreedContainer extends Container {
protected initBehaviors(): string[] {
return [];
}
};

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;

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;
});
}
}

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;
});
// 把所有连线置顶

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的xy
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;
}
/**

View File

@ -1,44 +1,42 @@
import { Element, Pointer } from "./Model/modelData";
import { SourceElement } from "./sources";
import { ModelConstructor, ConstructList } from "./Model/modelConstructor";
import { AnimationOptions, ElementOption, EngineInitOptions, InteractionOptions, LayoutOptions, LinkOption, Options, PointerOption } from "./options";
import { Behavior } from "./Behavior.ts/behavior";
import { Element, Link, Pointer } from "./Model/modelData";
import { Sources } from "./sources";
import { ModelConstructor } from "./Model/modelConstructor";
import { AnimationOptions, EngineOptions, InteractionOptions, LayoutGroupOptions, ViewOptions } from "./options";
import { ViewManager } from "./View/viewManager";
import { SV } from "./StructV";
export class Engine {
private stringifySources: string = null; // 序列化的源数据
private modelConstructor: ModelConstructor = null;
private viewManager: ViewManager
private behavior: Behavior;
private prevStringSourceData: string;
public initOptions: EngineInitOptions;
public elementOptions: { [key: string]: ElementOption } = { };
public linkOptions: { [key: string]: LinkOption } = { };
public pointerOptions: { [key: string]: PointerOption } = { };
public layoutOptions: LayoutOptions = null;
public animationOptions: AnimationOptions = null;
public interactionOptions: InteractionOptions = null;
public engineOptions: EngineOptions;
public viewOptions: ViewOptions;
public animationOptions: AnimationOptions;
public interactionOptions: InteractionOptions;
constructor(DOMContainer: HTMLElement, initOptions: EngineInitOptions = { }) {
const options: Options = this.defineOptions();
this.initOptions = initOptions;
this.elementOptions = options.element;
this.linkOptions = options.link || { };
this.pointerOptions = options.pointer || { };
public optionsTable: { [key: string]: LayoutGroupOptions };
this.layoutOptions = Object.assign({
constructor(DOMContainer: HTMLElement, engineOptions: EngineOptions = { }) {
this.optionsTable = {};
this.engineOptions = Object.assign({
freedContainer: null,
leakContainer: null
}, engineOptions);
this.viewOptions = Object.assign({
fitCenter: true,
fitView: false
}, options.layout);
fitView: false,
groupPadding: 20
}, engineOptions.view);
this.animationOptions = Object.assign({
enable: true,
duration: 750,
timingFunction: 'easePolyOut'
}, options.animation);
}, engineOptions.animation);
this.interactionOptions = Object.assign({
drag: true,
@ -46,106 +44,67 @@ export class Engine {
dragNode: true,
selectNode: true,
changeHighlight: '#fc5185'
}, options.interaction);
}, engineOptions.interaction);
this.initOptions = Object.assign({
freedContainer: null,
leakContainer: null
}, initOptions);
// 初始化布局器配置项
Object.keys(SV.registeredLayouter).forEach(layouter => {
if(this.optionsTable[layouter] === undefined) {
const options: LayoutGroupOptions = SV.registeredLayouter[layouter].defineOptions();
options.behavior = Object.assign({
dragNode: true,
selectNode: true
}, options.behavior);
this.optionsTable[layouter] = options;
}
});
this.modelConstructor = new ModelConstructor(this);
this.viewManager = new ViewManager(this, DOMContainer);
this.behavior = new Behavior(this, this.viewManager.getG6Instance());
}
/**
*
* @param sourceData
* @param sourcesData
*/
public render(sourceData: SourceElement[] | { [key: string]: SourceElement[] }) {
public render(sourceData: Sources) {
if(sourceData === undefined || sourceData === null) {
return;
}
// 若前后数据没有发生变化什么也不干将json字符串化后比较
let stringifySources = JSON.stringify(sourceData);
if(stringifySources === this.stringifySources) return;
this.stringifySources = stringifySources;
let processedSourcesData = this.sourcesPreprocess(sourceData);
if(processedSourcesData) {
sourceData = processedSourcesData;
let stringSourceData = JSON.stringify(sourceData);
if(this.prevStringSourceData === stringSourceData) {
return;
}
const sourceList: SourceElement[] = this.sourcesProcess(sourceData);
this.prevStringSourceData = stringSourceData;
// 1 转换模型data => model
const constructList: ConstructList = this.modelConstructor.construct(sourceList);
const layoutGroupTable = this.modelConstructor.construct(sourceData);
// 2 渲染使用g6进行渲染
this.viewManager.renderAll(constructList, this.layout.bind(this));
this.viewManager.renderAll(layoutGroupTable);
}
/**
*
* @param sourceData
*/
private sourcesProcess(sourceData: SourceElement[] | { [key: string]: SourceElement[] }): SourceElement[] {
if(Array.isArray(sourceData)) {
return sourceData;
}
const sourceList: SourceElement[] = [];
Object.keys(sourceData).forEach(name => {
sourceData[name].forEach(item => {
item.type = name;
});
sourceList.push(...sourceData[name]);
});
return sourceList;
}
/**
*
* @returns
*/
protected defineOptions(): Options {
return null;
}
/**
*
* @param sourceData
*/
protected sourcesPreprocess(sourceData: SourceElement[] | { [key: string]: SourceElement[] }): SourceElement[] | { [key: string]: SourceElement[] } | void {
return sourceData;
}
/**
*
* @overwrite
*/
protected layout(elements: Element[], layoutOptions: LayoutOptions) { }
/**
*
*/
public reLayout() {
const constructList: ConstructList = this.modelConstructor.getConstructList();
const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
this.viewManager.reLayout(constructList, this.layout.bind(this));
this.viewManager.reLayout(layoutGroupTable);
[...constructList.element, ...constructList.pointer].forEach(item => {
let model = item.G6Item.getModel(),
x = item.get('x'),
y = item.get('y');
layoutGroupTable.forEach(group => {
group.modelList.forEach(item => {
if(item instanceof Link) return;
model.x = x;
model.y = y;
let model = item.G6Item.getModel(),
x = item.get('x'),
y = item.get('y');
model.x = x;
model.y = y;
});
});
this.viewManager.refresh();
@ -160,35 +119,69 @@ export class Engine {
/**
* element
* @param group
*/
public getElements(): Element[] {
const constructList = this.modelConstructor.getConstructList();
return constructList.element;
public getElements(group?: string): Element[] {
const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
if(group && layoutGroupTable.has('group')) {
return layoutGroupTable.get('group').element;
}
const elements: Element[] = [];
layoutGroupTable.forEach(item => {
elements.push(...item.element);
})
return elements;
}
/**
* pointer
* @param group
*/
public getPointers(): Pointer[] {
const constructList = this.modelConstructor.getConstructList();
return constructList.pointer;
public getPointers(group?: string): Pointer[] {
const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
if(group && layoutGroupTable.has('group')) {
return layoutGroupTable.get('group').pointer;
}
const pointers: Pointer[] = [];
layoutGroupTable.forEach(item => {
pointers.push(...item.pointer);
})
return pointers;
}
/**
* link
* @param group
*/
public getLinks() {
const constructList = this.modelConstructor.getConstructList();
return constructList.link;
public getLinks(group?: string): Link[] {
const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
if(group && layoutGroupTable.has('group')) {
return layoutGroupTable.get('group').link;
}
const links: Link[] = [];
layoutGroupTable.forEach(item => {
links.push(...item.link);
})
return links;
}
/**
*
* @param containerName
* @param width
* @param height
*/
public resize(width: number, height: number) {
this.viewManager.resize(width, height);
public resize(containerName: string, width: number, height: number) {
this.viewManager.resize(containerName, width, height);
}
/**
@ -197,7 +190,7 @@ export class Engine {
* @param callback
*/
public on(eventName: string, callback: Function) {
this.behavior.on(eventName, callback);
this.viewManager.getG6Instance().on(eventName, callback);
}
/**
@ -206,6 +199,5 @@ export class Engine {
public destroy() {
this.modelConstructor.destroy();
this.viewManager.destroy();
this.behavior = null;
}
};

View File

@ -1,3 +1,5 @@
import { Element } from "./Model/modelData";
import { SourceElement } from "./sources";
export interface Style {
@ -59,12 +61,38 @@ export interface PointerOption extends ElementOption {
export interface LayoutOptions {
fitCenter: boolean;
fitView: boolean;
[key: string]: any;
};
export interface BehaviorOptions {
dragNode: boolean | string[];
selectNode: boolean | string[];
};
export interface LayoutGroupOptions {
element: { [key: string]: ElementOption };
link?: { [key: string]: LinkOption }
pointer?: { [key: string]: PointerOption };
layout?: LayoutOptions;
behavior?: BehaviorOptions;
};
/**
* ---------------------------------------------------------------------------------------------------------------------------------------------
* -------------------------------------------------------------------------------------------------------------------------------------------
* ------------------------------------------------------------------------------------------------------------------------
*/
export interface ViewOptions {
fitCenter: boolean;
fitView: boolean;
groupPadding: number;
}
export interface AnimationOptions {
enable: boolean;
duration: number;
@ -73,29 +101,24 @@ export interface AnimationOptions {
export interface InteractionOptions {
changeHighlight: string;
drag: boolean;
zoom: boolean;
dragNode: boolean | string[];
selectNode: boolean | string[];
changeHighlight: string;
};
}
export interface Options {
element: { [key: string]: ElementOption };
link?: { [key: string]: LinkOption }
pointer?: { [key: string]: PointerOption };
layout?: LayoutOptions;
export interface EngineOptions {
freedContainer?: HTMLElement;
leakContainer?: HTMLElement;
view?: ViewOptions;
animation?: AnimationOptions;
interaction?: InteractionOptions;
};
export interface EngineInitOptions {
freedContainer?: HTMLElement;
leakContainer?: HTMLElement;
};
export interface Layouter {
defineOptions(): LayoutGroupOptions;
sourcesPreprocess?(sources: SourceElement[]): SourceElement[];
layout(elements: Element[], layoutOptions: LayoutOptions);
[key: string]: Function;
}

View File

@ -15,4 +15,9 @@ export interface SourceElement {
}
export type Sources = {
[key: string]: { data: SourceElement[]; layouter: string; }
};