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

2
.gitignore vendored
View File

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

View File

@ -238,6 +238,15 @@ const data = {
node: [] 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) { const LQueue = function(container) {
return{ return{

View File

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

View File

@ -1,18 +1,17 @@
import { Util } from "./util"; import { Util } from "./util";
import { BoundingRect, Bound } from "./boundingRect"; import { BoundingRect, Bound } from "./boundingRect";
import { Vector } from "./vector"; import { Element, Model } from "../Model/modelData";
import { Element } from "../Model/modelData";
/** /**
* element * model
*/ */
export class Group { export class Group {
id: string; 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(); this.id = Util.generateId();
if(arg) { if(arg) {
@ -24,25 +23,43 @@ export class Group {
* element * element
* @param arg * @param arg
*/ */
add(...arg: Array<Element | Group>) { add(...arg: Array<Model | Group>) {
arg.map(ele => { arg.map(ele => {
this.elements.push(ele); this.models.push(ele);
}); });
} }
/** /**
* element * model
* @param element * @param element
*/ */
remove(element: Element | Group) { remove(model: Model | Group) {
Util.removeFromList(this.elements, item => item.id === element.id); Util.removeFromList(this.models, item => item.id === model.id);
} }
/** /**
* group的包围盒 * group的包围盒
*/ */
getBound(): BoundingRect { 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 * @param dy
*/ */
translate(dx: number, dy: number) { translate(dx: number, dy: number) {
this.elements.map(item => { this.models.map(item => {
if(item instanceof Group) { if(item instanceof Group) {
item.translate(dx, dy); 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 * group
*/ */
clear() { 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 { G6EdgeModel, G6NodeModel, Link, Model } from "../Model/modelData";
import { SV } from "../StructV"; import { SV } from "../StructV";
import { G6Data } from "../View/renderer"; import { G6Data } from "../View/renderer";
@ -78,21 +78,27 @@ export const Util = {
/** /**
* *
* @param constructListType * @param groupTable
* @returns * @returns
*/ */
converterList(modelContainer: { [key: string]: ConstructList[keyof ConstructList]}) { convertGroupTable2ModelList(groupTable: LayoutGroupTable): Model[] {
return [].concat(...Object.keys(modelContainer).map(item => modelContainer[item])); const list: Model[] = [];
groupTable.forEach(item => {
list.push(...item.modelList);
});
return list;
}, },
/** /**
* G6 data * G6 data
* @param constructList * @param layoutGroup
* @returns * @returns
*/ */
convertG6Data(constructList: ConstructList): G6Data { convertG6Data(layoutGroup: LayoutGroup): G6Data {
let nodes = [...constructList.element, ...constructList.pointer], let nodes = [...layoutGroup.element, ...layoutGroup.pointer],
edges = constructList.link; edges = layoutGroup.link;
return { return {
nodes: nodes.map(item => item.cloneProps()) as G6NodeModel[], nodes: nodes.map(item => item.cloneProps()) as G6NodeModel[],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,10 @@ import { Vector } from "./Common/vector";
import indexedNode from "./RegisteredShape/indexedNode"; import indexedNode from "./RegisteredShape/indexedNode";
export const SV = { export const SV = {
Engine: Engine, Engine: Engine,
Group: Group, Group: Group,
@ -17,6 +21,7 @@ export const SV = {
Vector: Vector, Vector: Vector,
Mat3: G6.Util.mat3, Mat3: G6.Util.mat3,
G6, G6,
registeredShape: [ registeredShape: [
externalPointer, externalPointer,
linkListNode, linkListNode,
@ -24,6 +29,18 @@ export const SV = {
twoCellNode, twoCellNode,
indexedNode 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 { Engine } from "../../engine";
import { ConstructList } from "../../Model/modelConstructor"; import { Model, Pointer } from "../../Model/modelData";
import { Element, Model, Pointer } from "../../Model/modelData"; import { AnimationOptions, InteractionOptions, LayoutGroupOptions, LayoutOptions } from "../../options";
import { AnimationOptions, InteractionOptions, LayoutOptions } from "../../options"; import { SV } from "../../StructV";
import { Animations } from "../animation"; import { Animations } from "../animation";
import { g6Behavior, Renderer } from "../renderer"; import { g6Behavior, Renderer } from "../renderer";
@ -25,20 +24,45 @@ export class Container {
this.DOMContainer = DOMContainer; this.DOMContainer = DOMContainer;
this.animationsOptions = engine.animationOptions; this.animationsOptions = engine.animationOptions;
this.interactionOptions = engine.interactionOptions; 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, { this.renderer = new Renderer(engine, DOMContainer, {
...g6Options, ...g6Options,
modes: { modes: {
default: this.initBehaviors() default: this.initBehaviors(this.engine.optionsTable)
} },
plugins: [tooltip]
}); });
this.prevModelList = [];
this.afterInitRenderer();
} }
/** /**
* *
* @param optionsTable
* @returns * @returns
*/ */
protected initBehaviors(): g6Behavior[] { protected initBehaviors(optionsTable: { [key: string]: LayoutGroupOptions }): g6Behavior[] {
return ['drag-canvas', 'zoom-canvas']; return ['drag-canvas', 'zoom-canvas'];
} }
@ -119,6 +143,11 @@ export class Container {
*/ */
protected handleChangeModels(models: Model[]) { } protected handleChangeModels(models: Model[]) { }
/**
*
*/
protected afterInitRenderer() { }
// ------------------------------------------ hook --------------------------------------------- // ------------------------------------------ hook ---------------------------------------------
afterAppendModels(callback: (models: Model[]) => void) { afterAppendModels(callback: (models: Model[]) => void) {
@ -135,11 +164,9 @@ export class Container {
/** /**
* *
* @param modelList * @param modelList
* @param layoutFn
*/ */
public render(constructList: ConstructList, layoutFn: (elements: Element[], layoutOptions: LayoutOptions) => void) { public render(modelList: Model[]) {
const modelList: Model[] = [...constructList.element, ...constructList.link, ...constructList.pointer], const appendModels: Model[] = this.getAppendModels(this.prevModelList, modelList),
appendModels: Model[] = this.getAppendModels(this.prevModelList, modelList),
removeModels: Model[] = this.getRemoveModels(this.prevModelList, modelList), removeModels: Model[] = this.getRemoveModels(this.prevModelList, modelList),
changeModels: Model[] = [...appendModels, ...this.findReTargetPointer(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 { Link, Model } from "../../Model/modelData";
import { InteractionOptions, LayoutGroupOptions } from "../../options";
import { Container } from "./container"; import { Container } from "./container";
@ -8,50 +9,142 @@ import { Container } from "./container";
*/ */
export class MainContainer extends Container { export class MainContainer extends Container {
protected initBehaviors() { protected initBehaviors(optionsTable: { [key: string]: LayoutGroupOptions }) {
const interactionOptions = this.interactionOptions, const dragNodeTable: { [key: string]: boolean | string[] } = { },
dragNode: boolean | string[] = interactionOptions.dragNode, selectNodeTable: { [key: string]: boolean | string[] } = { },
dragNodeFilter = node => { interactionOptions: InteractionOptions = this.engine.interactionOptions,
let model = node.item.getModel(); defaultModes = [];
if(node.item === null) { Object.keys(optionsTable).forEach(item => {
return false; dragNodeTable[item] = optionsTable[item].behavior.dragNode;
} selectNodeTable[item] = optionsTable[item].behavior.selectNode;
});
if(model.modelType === 'pointer') { if(interactionOptions.drag) {
return false; defaultModes.push('drag-canvas');
} }
if(typeof dragNode === 'boolean') { if(interactionOptions.zoom) {
return dragNode; defaultModes.push('zoom-canvas');
} }
if(Array.isArray(dragNode) && dragNode.indexOf(model.modelName) > -1) { const dragNodeFilter = node => {
return true; let model = node.item.getModel();
}
return false; if(node.item === null) {
} return false;
const modeMap = {
drag: 'drag-canvas',
zoom: 'zoom-canvas',
dragNode: {
type: 'drag-node',
shouldBegin: node => dragNodeFilter(node)
} }
},
defaultModes = [];
Object.keys(interactionOptions).forEach(item => { if(model.SVModelType === 'pointer') {
if(interactionOptions[item] && modeMap[item] !== undefined) { return false;
defaultModes.push(modeMap[item]);
} }
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; 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[]) { protected handleChangeModels(models: Model[]) {
const changeHighlightColor: string = this.interactionOptions.changeHighlight; const changeHighlightColor: string = this.interactionOptions.changeHighlight;

View File

@ -1,16 +1,19 @@
import { Bound, BoundingRect } from '../Common/boundingRect'; import { Bound, BoundingRect } from '../Common/boundingRect';
import { Group } from '../Common/group';
import { Engine } from '../engine'; import { Engine } from '../engine';
import { ConstructList } from '../Model/modelConstructor'; import { LayoutGroupTable } from '../Model/modelConstructor';
import { Element, Model, Pointer } from '../Model/modelData'; import { Element, Model, Pointer } from '../Model/modelData';
import { LayoutOptions, PointerOption } from '../options'; import { LayoutOptions, PointerOption, ViewOptions } from '../options';
import { Container } from './container/container'; import { Container } from './container/container';
export class Layouter { export class Layouter {
private engine: Engine; private engine: Engine;
private viewOptions: ViewOptions;
constructor(engine: Engine) { constructor(engine: Engine) {
this.engine = engine; this.engine = engine;
this.viewOptions = this.engine.viewOptions;
} }
@ -19,7 +22,7 @@ export class Layouter {
* @param elements * @param elements
* @param pointers * @param pointers
*/ */
private initLayoutValue(elements: Element[], pointers: Pointer[]) { private initLayoutValue(elements: Element[], pointers: Pointer[]) {
[...elements, ...pointers].forEach(item => { [...elements, ...pointers].forEach(item => {
item.set('rotation', item.get('rotation')); item.set('rotation', item.get('rotation'));
item.set({ x: 0, y: 0 }); item.set({ x: 0, y: 0 });
@ -29,10 +32,11 @@ export class Layouter {
/** /**
* *
* @param pointer * @param pointer
* @param pointerOptions
*/ */
private layoutPointer(pointers: Pointer[]) { private layoutPointer(pointers: Pointer[], pointerOptions: { [key: string]: PointerOption }) {
pointers.forEach(item => { pointers.forEach(item => {
const options: PointerOption = this.engine.pointerOptions[item.getType()], const options: PointerOption = pointerOptions[item.getType()],
offset = options.offset || 8, offset = options.offset || 8,
anchor = options.anchor || 0; anchor = options.anchor || 0;
@ -52,57 +56,118 @@ export class Layouter {
* @param container * @param container
* @param models * @param models
*/ */
private fitCenter(container: Container, models: Model[]) { private fitCenter(container: Container, group: Group) {
if(models.length === 0) {
return;
}
const viewBound: BoundingRect = models.map(item => item.getBound()).reduce((prev, cur) => Bound.union(prev, cur));
let width = container.getG6Instance().getWidth(), let width = container.getG6Instance().getWidth(),
height = container.getG6Instance().getHeight(), height = container.getG6Instance().getHeight(),
viewBound: BoundingRect = group.getBound(),
centerX = width / 2, centerY = height / 2, centerX = width / 2, centerY = height / 2,
boundCenterX = viewBound.x + viewBound.width / 2, boundCenterX = viewBound.x + viewBound.width / 2,
boundCenterY = viewBound.y + viewBound.height / 2, boundCenterY = viewBound.y + viewBound.height / 2,
dx = centerX - boundCenterX, dx = centerX - boundCenterX,
dy = centerY - boundCenterY; dy = centerY - boundCenterY;
models.forEach(item => { group.translate(dx, dy);
item.set({ }
x: item.get('x') + dx,
y: item.get('y') + 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); this.g6Instance.changeData(renderData);
} }
if(this.engine.layoutOptions.fitView) { if(this.engine.viewOptions.fitView) {
this.g6Instance.fitView(); this.g6Instance.fitView();
} }
modelList.forEach(item => { modelList.forEach(item => {
item.renderG6Item = this.g6Instance.findById(item.id); item.renderG6Item = this.g6Instance.findById(item.id);
item.G6Item = item.renderG6Item; item.G6Item = item.renderG6Item;
item.renderG6Item.SVModel = item;
}); });
// 把所有连线置顶 // 把所有连线置顶

View File

@ -1,13 +1,14 @@
import { Engine } from "../engine"; import { Engine } from "../engine";
import { Element, Link } from "../Model/modelData"; import { Element, Link, Model } from "../Model/modelData";
import { EngineInitOptions, LayoutOptions } from "../options"; import { EngineOptions } from "../options";
import { Container } from "./container/container"; import { Container } from "./container/container";
import { SV } from '../StructV'; import { SV } from '../StructV';
import { ConstructList } from "../Model/modelConstructor";
import { MainContainer } from "./container/main"; import { MainContainer } from "./container/main";
import { FreedContainer } from "./container/freed"; import { FreedContainer } from "./container/freed";
import { LeakContainer } from "./container/leak"; import { LeakContainer } from "./container/leak";
import { Layouter } from "./layouter"; import { Layouter } from "./layouter";
import { LayoutGroup, LayoutGroupTable } from "../Model/modelConstructor";
import { Util } from "../Common/util";
export class ViewManager { export class ViewManager {
@ -17,9 +18,7 @@ export class ViewManager {
private freedContainer: Container; private freedContainer: Container;
private leakContainer: Container; private leakContainer: Container;
private prevConstructList: ConstructList = { element:[], pointer: [], link: [] }; private prevLayoutGroupTable: LayoutGroupTable;
private freedConstructList: ConstructList = { element:[], pointer: [], link: [] };
private leakConstructList: ConstructList = { element:[], pointer: [], link: [] };
private shadowG6Instance; private shadowG6Instance;
@ -27,8 +26,9 @@ export class ViewManager {
this.engine = engine; this.engine = engine;
this.layouter = new Layouter(engine); this.layouter = new Layouter(engine);
this.mainContainer = new MainContainer(engine, DOMContainer); this.mainContainer = new MainContainer(engine, DOMContainer);
this.prevLayoutGroupTable = null;
const options: EngineInitOptions = this.engine.initOptions; const options: EngineOptions = this.engine.engineOptions;
if(options.freedContainer) { if(options.freedContainer) {
this.freedContainer = new FreedContainer(engine, options.freedContainer, { fitCenter: true }); this.freedContainer = new FreedContainer(engine, options.freedContainer, { fitCenter: true });
@ -47,38 +47,49 @@ export class ViewManager {
* model Canvas G6 item * model Canvas G6 item
* @param constructList * @param constructList
*/ */
private build(constructList: ConstructList) { private build(layoutGroupTable: LayoutGroupTable) {
constructList.element.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('node', item)); layoutGroupTable.forEach(group => {
constructList.pointer.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('node', item)); group.element.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('node', item));
constructList.link.map(item => item.cloneProps()).forEach(item => this.shadowG6Instance.addItem('edge', 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));
constructList.element.forEach(item => { group.element.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id); item.shadowG6Item = this.shadowG6Instance.findById(item.id);
}); });
constructList.pointer.forEach(item => { group.pointer.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id); item.shadowG6Item = this.shadowG6Instance.findById(item.id);
}); });
constructList.link.forEach(item => { group.link.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id); item.shadowG6Item = this.shadowG6Instance.findById(item.id);
});
}); });
} }
/** /**
* free * free
* @param constructList * @param layoutGroupTable
* @returns * @returns
*/ */
private getFreedConstructList(constructList: ConstructList): ConstructList { private getFreedConstructList(layoutGroupTable: LayoutGroupTable): Model[] {
const freedList: ConstructList = { let freedList: Model[] = [],
element: constructList.element.filter(item => item.free), freedGroup = null,
pointer: [], freedGroupName = null;
link: []
};
freedList.element.forEach(fItem => { for(let group in layoutGroupTable) {
constructList.element.splice(constructList.element.findIndex(item => item.id === fItem.id), 1); let freedElements: Model[] = layoutGroupTable[group].element.filter(item => item.freed);
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)); 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; return freedList;
@ -90,47 +101,63 @@ export class ViewManager {
* @param prevConstructList * @param prevConstructList
* @returns * @returns
*/ */
private getLeakConstructList(prevConstructList: ConstructList, constructList: ConstructList): ConstructList { private getLeakConstructList(prevLayoutGroupTable: LayoutGroupTable, layoutGroupTable: LayoutGroupTable): LayoutGroupTable {
const elements: Element[] = prevConstructList.element.filter(item => !constructList.element.find(n => n.id === item.id)), const leakLayoutGroupTable = new Map<string, LayoutGroup>();
links: Link[] = prevConstructList.link.filter(item => !constructList.link.find(n => n.id === item.id)),
elementIds: string[] = elements.map(item => item.id);
elements.forEach(item => { prevLayoutGroupTable.forEach((item, groupName) => {
item.set('style', { let prevGroup = item,
fill: '#ccc' 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++) { return leakLayoutGroupTable;
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: []
};
} }
// ---------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------
/** /**
* *
* @param constructList * @param layoutGroupTable
* @param layoutFn
*/ */
reLayout(constructList: ConstructList, layoutFn: (elements: Element[], layoutOptions: LayoutOptions) => void) { reLayout(layoutGroupTable: LayoutGroupTable) {
this.layouter.layout(this.mainContainer, constructList, layoutFn); this.layouter.layoutAll(this.mainContainer, layoutGroupTable);
} }
@ -150,11 +177,23 @@ export class ViewManager {
/** /**
* *
* @param containerName
* @param width * @param width
* @param height * @param height
*/ */
resize(width: number, height: number) { resize(containerName: string, width: number, height: number) {
this.mainContainer.getG6Instance().changeSize(width, height); 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 models
* @param layoutFn * @param layoutFn
*/ */
renderAll(constructList: ConstructList, layoutFn: (elements: Element[], layoutOptions: LayoutOptions) => void) { renderAll(layoutGroupTable: LayoutGroupTable) {
this.shadowG6Instance.clear(); this.shadowG6Instance.clear();
this.build(constructList); this.build(layoutGroupTable);
this.freedConstructList = this.getFreedConstructList(constructList); let freedList = this.getFreedConstructList(layoutGroupTable),
this.leakConstructList = this.getLeakConstructList(this.prevConstructList, constructList); leakLayoutGroupTable = null;
this.build(this.leakConstructList); if(this.leakContainer && this.prevLayoutGroupTable) {
leakLayoutGroupTable = this.getLeakConstructList(this.prevLayoutGroupTable, layoutGroupTable);
this.build(leakLayoutGroupTable);
}
if(this.freedContainer) { if(this.freedContainer) {
this.freedContainer.render(this.freedConstructList, layoutFn); this.freedContainer.render(freedList);
} }
// 进行布局设置model的xy // 进行布局设置model的xy
this.layouter.layout(this.mainContainer, constructList, layoutFn); this.layouter.layoutAll(this.mainContainer, layoutGroupTable);
this.mainContainer.render(constructList, layoutFn);
if(this.leakContainer) { const modelList: Model[] = Util.convertGroupTable2ModelList(layoutGroupTable);
this.mainContainer.render(modelList);
if(this.leakContainer && this.prevLayoutGroupTable) {
this.mainContainer.afterRemoveModels(() => { 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 { Element, Link, Pointer } from "./Model/modelData";
import { SourceElement } from "./sources"; import { Sources } from "./sources";
import { ModelConstructor, ConstructList } from "./Model/modelConstructor"; import { ModelConstructor } from "./Model/modelConstructor";
import { AnimationOptions, ElementOption, EngineInitOptions, InteractionOptions, LayoutOptions, LinkOption, Options, PointerOption } from "./options"; import { AnimationOptions, EngineOptions, InteractionOptions, LayoutGroupOptions, ViewOptions } from "./options";
import { Behavior } from "./Behavior.ts/behavior";
import { ViewManager } from "./View/viewManager"; import { ViewManager } from "./View/viewManager";
import { SV } from "./StructV";
export class Engine { export class Engine {
private stringifySources: string = null; // 序列化的源数据
private modelConstructor: ModelConstructor = null; private modelConstructor: ModelConstructor = null;
private viewManager: ViewManager private viewManager: ViewManager
private behavior: Behavior; private prevStringSourceData: string;
public initOptions: EngineInitOptions; public engineOptions: EngineOptions;
public elementOptions: { [key: string]: ElementOption } = { }; public viewOptions: ViewOptions;
public linkOptions: { [key: string]: LinkOption } = { }; public animationOptions: AnimationOptions;
public pointerOptions: { [key: string]: PointerOption } = { }; public interactionOptions: InteractionOptions;
public layoutOptions: LayoutOptions = null;
public animationOptions: AnimationOptions = null;
public interactionOptions: InteractionOptions = null;
constructor(DOMContainer: HTMLElement, initOptions: EngineInitOptions = { }) { public optionsTable: { [key: string]: LayoutGroupOptions };
const options: Options = this.defineOptions();
this.initOptions = initOptions; constructor(DOMContainer: HTMLElement, engineOptions: EngineOptions = { }) {
this.elementOptions = options.element; this.optionsTable = {};
this.linkOptions = options.link || { };
this.pointerOptions = options.pointer || { };
this.layoutOptions = Object.assign({ this.engineOptions = Object.assign({
freedContainer: null,
leakContainer: null
}, engineOptions);
this.viewOptions = Object.assign({
fitCenter: true, fitCenter: true,
fitView: false fitView: false,
}, options.layout); groupPadding: 20
}, engineOptions.view);
this.animationOptions = Object.assign({ this.animationOptions = Object.assign({
enable: true, enable: true,
duration: 750, duration: 750,
timingFunction: 'easePolyOut' timingFunction: 'easePolyOut'
}, options.animation); }, engineOptions.animation);
this.interactionOptions = Object.assign({ this.interactionOptions = Object.assign({
drag: true, drag: true,
@ -46,106 +44,67 @@ export class Engine {
dragNode: true, dragNode: true,
selectNode: true, selectNode: true,
changeHighlight: '#fc5185' changeHighlight: '#fc5185'
}, options.interaction); }, engineOptions.interaction);
this.initOptions = Object.assign({ // 初始化布局器配置项
freedContainer: null, Object.keys(SV.registeredLayouter).forEach(layouter => {
leakContainer: null if(this.optionsTable[layouter] === undefined) {
}, initOptions); 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.modelConstructor = new ModelConstructor(this);
this.viewManager = new ViewManager(this, DOMContainer); 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) { if(sourceData === undefined || sourceData === null) {
return; return;
} }
// 若前后数据没有发生变化什么也不干将json字符串化后比较 let stringSourceData = JSON.stringify(sourceData);
let stringifySources = JSON.stringify(sourceData); if(this.prevStringSourceData === stringSourceData) {
if(stringifySources === this.stringifySources) return; return;
this.stringifySources = stringifySources;
let processedSourcesData = this.sourcesPreprocess(sourceData);
if(processedSourcesData) {
sourceData = processedSourcesData;
} }
this.prevStringSourceData = stringSourceData;
const sourceList: SourceElement[] = this.sourcesProcess(sourceData);
// 1 转换模型data => model // 1 转换模型data => model
const constructList: ConstructList = this.modelConstructor.construct(sourceList); const layoutGroupTable = this.modelConstructor.construct(sourceData);
// 2 渲染使用g6进行渲染 // 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() { 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 => { layoutGroupTable.forEach(group => {
let model = item.G6Item.getModel(), group.modelList.forEach(item => {
x = item.get('x'), if(item instanceof Link) return;
y = item.get('y');
model.x = x; let model = item.G6Item.getModel(),
model.y = y; x = item.get('x'),
y = item.get('y');
model.x = x;
model.y = y;
});
}); });
this.viewManager.refresh(); this.viewManager.refresh();
@ -160,35 +119,69 @@ export class Engine {
/** /**
* element * element
* @param group
*/ */
public getElements(): Element[] { public getElements(group?: string): Element[] {
const constructList = this.modelConstructor.getConstructList(); const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
return constructList.element;
if(group && layoutGroupTable.has('group')) {
return layoutGroupTable.get('group').element;
}
const elements: Element[] = [];
layoutGroupTable.forEach(item => {
elements.push(...item.element);
})
return elements;
} }
/** /**
* pointer * pointer
* @param group
*/ */
public getPointers(): Pointer[] { public getPointers(group?: string): Pointer[] {
const constructList = this.modelConstructor.getConstructList(); const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
return constructList.pointer;
if(group && layoutGroupTable.has('group')) {
return layoutGroupTable.get('group').pointer;
}
const pointers: Pointer[] = [];
layoutGroupTable.forEach(item => {
pointers.push(...item.pointer);
})
return pointers;
} }
/** /**
* link * link
* @param group
*/ */
public getLinks() { public getLinks(group?: string): Link[] {
const constructList = this.modelConstructor.getConstructList(); const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
return constructList.link;
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 width
* @param height * @param height
*/ */
public resize(width: number, height: number) { public resize(containerName: string, width: number, height: number) {
this.viewManager.resize(width, height); this.viewManager.resize(containerName, width, height);
} }
/** /**
@ -197,7 +190,7 @@ export class Engine {
* @param callback * @param callback
*/ */
public on(eventName: string, callback: Function) { 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() { public destroy() {
this.modelConstructor.destroy(); this.modelConstructor.destroy();
this.viewManager.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 { export interface Style {
@ -59,12 +61,38 @@ export interface PointerOption extends ElementOption {
export interface LayoutOptions { export interface LayoutOptions {
fitCenter: boolean;
fitView: boolean;
[key: string]: any; [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 { export interface AnimationOptions {
enable: boolean; enable: boolean;
duration: number; duration: number;
@ -73,29 +101,24 @@ export interface AnimationOptions {
export interface InteractionOptions { export interface InteractionOptions {
changeHighlight: string;
drag: boolean; drag: boolean;
zoom: boolean; zoom: boolean;
dragNode: boolean | string[]; }
selectNode: boolean | string[];
changeHighlight: string;
};
export interface EngineOptions {
export interface Options { freedContainer?: HTMLElement;
element: { [key: string]: ElementOption }; leakContainer?: HTMLElement;
link?: { [key: string]: LinkOption } view?: ViewOptions;
pointer?: { [key: string]: PointerOption };
layout?: LayoutOptions;
animation?: AnimationOptions; animation?: AnimationOptions;
interaction?: InteractionOptions; interaction?: InteractionOptions;
}; };
export interface EngineInitOptions { export interface Layouter {
freedContainer?: HTMLElement; defineOptions(): LayoutGroupOptions;
leakContainer?: HTMLElement; 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; }
};