Revert "Merge branch 'feat-stage' into 'main'"

This reverts merge request !1
This commit is contained in:
Phenom 2021-12-02 09:22:05 +00:00
parent 346b6c42fd
commit 2baed0b927
51 changed files with 1796 additions and 5280 deletions

View File

@ -19,7 +19,7 @@ const isNeighbor = function (itemA, itemB) {
}
SV.registerLayout('AdjoinMatrixGraph', {
SV.registerLayouter('AdjoinMatrixGraph', {
sourcesPreprocess(sources) {
let dataLength = sources.length;

View File

@ -1,7 +1,7 @@
SV.registerLayout('AdjoinTableGraph', {
SV.registerLayouter('AdjoinTableGraph', {
sourcesPreprocess(sources, options) {
let dataLength = sources.length;

View File

@ -1,7 +1,7 @@
SV.registerLayout('Array', {
SV.registerLayouter('Array', {
sourcesPreprocess(sources) {
const firstElement = sources[0];

View File

@ -1,6 +1,6 @@
SV.registerLayout('BinaryTree', {
SV.registerLayouter('BinaryTree', {
defineOptions() {
return {
element: {

View File

@ -6,7 +6,7 @@
/**
* 连地址哈希表
*/
SV.registerLayout('ChainHashTable', {
SV.registerLayouter('ChainHashTable', {
defineOptions() {
return {

View File

@ -80,7 +80,7 @@ SV.registerShape('three-cell-node', {
SV.registerLayout('GeneralizedList', {
SV.registerLayouter('GeneralizedList', {
defineOptions() {
return {

View File

@ -1,7 +1,7 @@
SV.registerLayout('HashTable', {
SV.registerLayouter('HashTable', {
sourcesPreprocess(sources) {
const firstElement = sources[0];

View File

@ -1,6 +1,6 @@
SV.registerLayout('LinkList', {
SV.registerLayouter('LinkList', {
sourcesPreprocess(sources) {
let root = sources[0];
@ -15,7 +15,7 @@ SV.registerLayout('LinkList', {
defineOptions() {
return {
node: {
element: {
default: {
type: 'link-list-node',
label: '[data]',
@ -107,6 +107,7 @@ SV.registerLayout('LinkList', {
layout(elements, layoutOptions) {
let root = elements[0];
this.layoutItem(root, null, layoutOptions);
}
});

View File

@ -2,7 +2,7 @@
SV.registerLayout('LinkQueue', {
SV.registerLayouter('LinkQueue', {
defineOptions() {
return {

View File

@ -3,7 +3,7 @@
/**
* 单链表
*/
SV.registerLayout('LinkStack', {
SV.registerLayouter('LinkStack', {
sourcesPreprocess(sources) {
const headNode = sources[0];

View File

@ -1,6 +1,6 @@
SV.registerLayout('Stack', {
SV.registerLayouter('Stack', {
sourcesPreprocess(sources, options) {
const stackBottomNode = sources[sources.length - 1];

View File

@ -1,7 +1,7 @@
/**
* 三叉树
*/
SV.registerLayout('TriTree', {
SV.registerLayouter('TriTree', {
defineOptions() {
return {
/**

View File

@ -25,43 +25,38 @@
#container {
width: 100%;
height: 500px;
position: relative;
overflow: hidden;
height: 400px;
}
#freed {
width: 200px;
height: 300px;
margin-right: 30px;
}
#leak {
position: absolute;
left: 0;
top: 100px;
opacity: 0;
width: 100%;
box-sizing: border-box;
padding: 4px;
border-top: 1px dashed #000;
pointer-events: none;
transition: opacity 0.75s ease-in-out;
}
#leak>span {
color: #000;
width: 400px;
height: 300px;
}
</style>
</head>
<body>
<div class="container" id="container">
<div id="leak">
<span>泄漏区</span>
</div>
</div>
<div class="container" id="container"></div>
<button id="btn-prev">prev</button>
<button id="btn-next">next</button>
<button id="resize">resize</button>
<button id="hide">隐藏</button>
<span id="pos"></span>
<div class="down">
<div id="freed" class="container"></div>
<div id="leak" class="container"></div>
</div>
<script src="./../dist/sv.js"></script>
<script>
@ -87,8 +82,9 @@
const curSelectData = { element: null, style: null };
let cur = SV(document.getElementById('container'), {
freedContainer: document.getElementById('freed'),
leakContainer: document.getElementById('leak'),
view: {
leakAreaHeight: 0.25,
groupPadding: 40,
},
});
@ -97,42 +93,54 @@
let data = [{
"LinkList0": {
"data": [
{ id: 0, data: 'A', next: 1 },
{ id: 1, data: 'B' }
{
"id": "0x617fb0",
"data": "N",
"next": "0x617ff0",
"rootExternal": [
"before"
],
"type": "default"
},
{
"id": "0x617ff0",
"data": "A",
"next": "0x618010",
"type": "default"
},
{
"id": "0x618010",
"data": "A",
"next": "0x618030",
"type": "default"
},
{
"id": "0x618030",
"data": "B",
"next": "0x618050",
"type": "default"
},
{
"id": "0x618050",
"data": "N",
"next": null,
"type": "default"
}
],
"layouter": "LinkList"
}
}, {
"LinkList0": {
},
"LinkList1": {
"data": [
{ id: 0, data: 'A', next: 1 },
{ id: 1, data: 'B', next: 2 },
{ id: 2, data: 'C' }
{
"freed": true,
"id": "0x617fd0",
"data": "",
"next": "0x605010",
"rootExternal": [
"tmpNode"
],
"layouter": "LinkList"
"type": "default"
}
}, {
"LinkList0": {
"data": [
{ id: 0, data: 'A', next: 1 },
{ id: 1, data: 'B', next: 2 },
{ id: 2, data: 'C', next: 3 },
{ id: 3, data: 'D' }
],
"layouter": "LinkList"
}
}, {
"LinkList0": {
"data": [
{ id: 0, data: 'A', next: 1 },
{ id: 1, data: 'B', next: 2, freed: true }
],
"layouter": "LinkList"
}
}, {
"LinkList0": {
"data": [
{ id: 0, data: 'A' }
],
"layouter": "LinkList"
}
@ -145,9 +153,17 @@
cur.render(curData);
let f = true;
document.getElementById('hide').addEventListener('click', e => {
f ? cur.hideGroups(['BinaryTree']) : cur.hideGroups(['LinkList']);
f = !f;
});
document.getElementById('btn-next').addEventListener('click', e => {
curData = data[++dataIndex];
cur.render(curData);
// curSelectData.element = null;
// curSelectData.style = null;
});
document.getElementById('btn-prev').addEventListener('click', e => {
@ -163,21 +179,49 @@
pos.innerHTML = `${x},${y}`;
});
document.getElementById('resize').addEventListener('click', e => {
container.style.height = 300 + 'px';
cur.resize(container.offsetWidth, container.offsetHeight);
document.getElementById('hide').addEventListener('click', () => {
cur.selectElement(1000, ele => {
ele.set('label', '6666');
});
const leak = document.getElementById('leak');
cur.on('onLeakAreaUpdate', payload => {
leak.style.opacity = payload.hasLeak ? 1 : 0;
leak.style.top = payload.leakAreaY - 40 + 'px';
});
// -------------------------------------------------------------------------------------------------------
/**
* 选中一个element
*/
function selectElement(element) {
if (element === curSelectData.element) {
return;
}
if (curSelectData.element) {
const style = curSelectData.style;
curSelectData.element.set("style", style);
}
curSelectData.element = element;
curSelectData.style = { ...element.get("style") };
this.setSelectElementStyle(element);
}
/**
* 设置选中的节点的颜色
*/
function setSelectElementStyle(element) {
element.set("style", {
fill: "#e23e57",
});
}
cur.on('node:click', ele => {
selectElement(ele);
});
</script>
</body>

2
dist/sv.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,10 @@
{
"dependencies": {
"@antv/g6": "^4.4.1",
"@antv/g6": "^4.2.1",
"awesome-typescript-loader": "^5.2.1",
"typescript": "^3.2.2",
"webpack": "^4.28.2"
"webpack": "^4.28.2",
"zrender": "^5.0.3"
},
"devDependencies": {
"webpack-cli": "^3.2.3"

View File

@ -1,54 +0,0 @@
import { EventBus } from "../Common/eventBus";
import { ViewContainer } from "../View/viewContainer";
/**
*
* @param g6Instance
* @param hasLeak
*/
export function InitDragCanvasWithLeak(viewContainer: ViewContainer) {
let g6Instance = viewContainer.getG6Instance(),
startPositionY = 0,
currentLeakAreaY = 0;
g6Instance.on('canvas:dragstart', event => {
startPositionY = event.canvasY;
currentLeakAreaY = viewContainer.leakAreaY;
});
g6Instance.on('canvas:drag', event => {
let zoom = g6Instance.getZoom(),
dy = (event.canvasY - startPositionY) / zoom,
leakAreaY = currentLeakAreaY + dy;
viewContainer.leakAreaY = leakAreaY;
if(viewContainer.hasLeak) {
EventBus.emit('onLeakAreaUpdate', {
leakAreaY: viewContainer.leakAreaY,
hasLeak: viewContainer.hasLeak
});
}
});
g6Instance.on('canvas:dragend', event => {
startPositionY = 0;
})
}

View File

@ -1,116 +0,0 @@
import { Graph } from "@antv/g6-pc";
import { SVNode } from "../Model/SVNode";
import { LayoutGroupOptions } from "../options";
/**
*
*
*/
export function FixNodeMarkerDrag(g6Instance: Graph, optionsTable: { [key: string]: LayoutGroupOptions }) {
let dragActive: boolean = false;
const nodeData = {
node: null,
startX: 0,
startY: 0
};
const markerData = {
marker: null,
startX: 0,
startY: 0
};
const freedLabelData = {
freedLabel: null,
startX: 0,
startY: 0
};
g6Instance.on('node:dragstart', event => {
nodeData.node = event.item['SVModel'];
let node: SVNode = nodeData.node;
if (node.isNode() === false || node.leaked) {
return false;
}
const dragNode = optionsTable[node.layout].behavior.dragNode;
if (dragNode === false) {
return;
}
if (Array.isArray(dragNode) && dragNode.find(item => item === node.sourceType) === undefined) {
return;
}
dragActive = true;
nodeData.startX = event.canvasX;
nodeData.startY = event.canvasY;
if (node.marker) {
markerData.marker = node.marker;
markerData.startX = markerData.marker.get('x');
markerData.startY = markerData.marker.get('y');
}
if(node.freedLabel) {
freedLabelData.freedLabel = node.freedLabel;
freedLabelData.startX = freedLabelData.freedLabel.get('x');
freedLabelData.startY = freedLabelData.freedLabel.get('y');
}
});
g6Instance.on('node:dragend', event => {
if(!dragActive) {
return false;
}
let distanceX = event.canvasX - nodeData.startX,
distanceY = event.canvasY - nodeData.startY,
nodeX = nodeData.node.get('x'),
nodeY = nodeData.node.get('y');
nodeData.node.set({
x: nodeX + distanceX,
y: nodeY + distanceY
});
nodeData.node = null;
nodeData.startX = 0;
nodeData.startY = 0;
markerData.marker = null;
markerData.startX = 0;
markerData.startY = 0;
freedLabelData.freedLabel = null;
freedLabelData.startX = 0;
freedLabelData.startY = 0;
dragActive = false;
});
g6Instance.on('node:drag', ev => {
if (!dragActive) {
return false;
}
let dx = ev.canvasX - nodeData.startX,
dy = ev.canvasY - nodeData.startY,
zoom = g6Instance.getZoom();
if(markerData.marker) {
markerData.marker.set({
x: markerData.startX + dx / zoom,
y: markerData.startY + dy / zoom
});
}
if(freedLabelData.freedLabel) {
freedLabelData.freedLabel.set({
x: freedLabelData.startX + dx / zoom,
y: freedLabelData.startY + dy / zoom
});
}
});
}

View File

@ -1,78 +0,0 @@
import { SVModel } from "../Model/SVModel";
import { LayoutGroupOptions } from "../options";
/**
* g6 options
* @param optionsTable
* @returns
*/
export function InitViewBehaviors(optionsTable: { [key: string]: LayoutGroupOptions }) {
const dragNodeTable: { [key: string]: boolean | string[] } = {},
selectNodeTable: { [key: string]: boolean | string[] } = {},
defaultModes = [];
Object.keys(optionsTable).forEach(item => {
dragNodeTable[item] = optionsTable[item].behavior.dragNode;
selectNodeTable[item] = optionsTable[item].behavior.selectNode;
});
const dragNodeFilter = event => {
let g6Item = event.item,
node: SVModel = g6Item.SVModel;
if (g6Item === null || node.isNode() === false || node.leaked) {
return false;
}
let dragNode = optionsTable[node.layout].behavior.dragNode;
if (typeof dragNode === 'boolean') {
return dragNode;
}
if (Array.isArray(dragNode) && dragNode.indexOf(node.sourceType) > -1) {
return true;
}
return false;
}
const selectNodeFilter = event => {
let g6Item = event.item,
node: SVModel = g6Item.SVModel;
if (g6Item === null || node.isNode() === false || node.leaked) {
return false;
}
let selectNode = optionsTable[node.layout].behavior.selectNode;
if (typeof selectNode === 'boolean') {
return selectNode;
}
if (Array.isArray(selectNode) && selectNode.indexOf(node.sourceType) > -1) {
return true;
}
return false;
}
defaultModes.push({
type: 'drag-node',
shouldBegin: dragNodeFilter
});
defaultModes.push({
type: 'drag-canvas'
});
defaultModes.push({
type: 'click-select',
shouldBegin: selectNodeFilter
});
return defaultModes;
}

View File

@ -1,59 +0,0 @@
import { Graph, IGroup } from "@antv/g6-pc";
import { ext } from '@antv/matrix-util';
const transform = ext.transform;
/**
*
* @param g6Instance
* @param generalModelsGroup
*/
export function InitZoomCanvas(g6Instance: Graph, g6GeneralGroup: IGroup) {
const minZoom = 0.2,
maxZoom = 2,
step = 0.15;
g6Instance.on('wheel', event => {
let delta = event.wheelDelta,
matrix = g6GeneralGroup.getMatrix(),
center = [event.x, event.y],
targetScale = 1;
if (delta > 0) {
targetScale += step;
}
if (delta < 0) {
targetScale -= step;
}
matrix = transform(matrix, [
['t', -center[0], -center[1]],
['s', targetScale, targetScale],
['t', center[0], center[1]],
]);
if ((minZoom && matrix[0] < minZoom) || (maxZoom && matrix[0] > maxZoom)) {
return false;
}
g6GeneralGroup.setMatrix(matrix);
g6Instance.paint();
});
}

View File

@ -1,7 +1,6 @@
import { Util } from "./util";
import { BoundingRect, Bound } from "./boundingRect";
import { SVModel } from "../Model/SVModel";
import { ext } from '@antv/matrix-util';
import { Element, Model } from "../Model/modelData";
@ -10,12 +9,12 @@ import { ext } from '@antv/matrix-util';
*/
export class Group {
id: string;
private models: Array<SVModel | Group> = [];
private models: Array<Model | Group> = [];
constructor(...arg: Array<SVModel | Group>) {
constructor(...arg: Array<Model | Group>) {
this.id = Util.generateId();
if (arg) {
if(arg) {
this.add(...arg);
}
}
@ -24,7 +23,7 @@ export class Group {
* element
* @param arg
*/
add(...arg: Array<SVModel | Group>) {
add(...arg: Array<Model | Group>) {
arg.map(ele => {
this.models.push(ele);
});
@ -34,7 +33,7 @@ export class Group {
* model
* @param element
*/
remove(model: SVModel | Group) {
remove(model: Model | Group) {
Util.removeFromList(this.models, item => item.id === model.id);
}
@ -42,8 +41,8 @@ export class Group {
* group的包围盒
*/
getBound(): BoundingRect {
return this.models.length ?
Bound.union(...this.models.map(item => item.getBound())) :
return this.models.length?
Bound.union(...this.models.map(item => item.getBound())):
{ x: 0, y: 0, width: 0, height: 0 };
}
@ -63,10 +62,6 @@ export class Group {
return bound;
}
getModels(): Array<SVModel | Group> {
return this.models;
}
/**
* group
* @param dx
@ -74,7 +69,7 @@ export class Group {
*/
translate(dx: number, dy: number) {
this.models.map(item => {
if (item instanceof Group) {
if(item instanceof Group) {
item.translate(dx, dy);
}
else {
@ -84,27 +79,6 @@ export class Group {
});
}
/**
* group
* @param center
* @param ratio
*/
scale(center: [number, number], ratio: number) {
this.models.map(item => {
if (item instanceof Group) {
item.scale(center, ratio);
}
else {
const matrix = ext.transform(item.getMatrix(), [
['t', -center[0], -center[1]],
['s', ratio, ratio],
['t', center[0], center[1]],
]);
item.setMatrix(matrix);
}
});
}
/**
* group
*/

View File

@ -1,8 +1,7 @@
import { EdgeConfig, GraphData, NodeConfig } from "@antv/g6-core";
import { LayoutGroup, LayoutGroupTable } from "../Model/modelConstructor";
import { SVLink } from "../Model/SVLink";
import { SVModel } from "../Model/SVModel";
import { G6EdgeModel, G6NodeModel, Link, Model } from "../Model/modelData";
import { SV } from "../StructV";
import { G6Data } from "../View/renderer";
/**
@ -90,8 +89,8 @@ export const Util = {
* @param groupTable
* @returns
*/
convertGroupTable2ModelList(groupTable: LayoutGroupTable): SVModel[] {
const list: SVModel[] = [];
convertGroupTable2ModelList(groupTable: LayoutGroupTable): Model[] {
const list: Model[] = [];
groupTable.forEach(item => {
list.push(...item.modelList);
@ -105,13 +104,13 @@ export const Util = {
* @param layoutGroup
* @returns
*/
convertG6Data(layoutGroup: LayoutGroup): GraphData {
let nodes = [...layoutGroup.node, ...layoutGroup.marker],
convertG6Data(layoutGroup: LayoutGroup): G6Data {
let nodes = [...layoutGroup.element, ...layoutGroup.marker],
edges = layoutGroup.link;
return {
nodes: nodes.map(item => item.getG6ModelProps()) as NodeConfig[],
edges: edges.map(item => item.getG6ModelProps()) as EdgeConfig[]
nodes: nodes.map(item => item.cloneProps()) as G6NodeModel[],
edges: edges.map(item => item.cloneProps()) as G6EdgeModel[]
};
},
@ -119,10 +118,10 @@ export const Util = {
* modelList G6Data
* @param modelList
*/
convertModelList2G6Data(modelList: SVModel[]): GraphData {
convertModelList2G6Data(modelList: Model[]): G6Data {
return {
nodes: <NodeConfig[]>(modelList.filter(item => !(item instanceof SVLink)).map(item => item.getG6ModelProps())),
edges: <EdgeConfig[]>(modelList.filter(item => item instanceof SVLink).map(item => item.getG6ModelProps()))
nodes: <G6NodeModel[]>(modelList.filter(item => !(item instanceof Link)).map(item => item.cloneProps())),
edges: <G6EdgeModel[]>(modelList.filter(item => item instanceof Link).map(item => item.cloneProps()))
}
},
@ -138,4 +137,3 @@ export const Util = {
}
};

31
src/Lib/g6.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,53 +0,0 @@
import { EdgeConfig, IEdge } from "@antv/g6-core";
import { Util } from "../Common/util";
import { LinkLabelOption, LinkOption, Style } from "../options";
import { SVModel } from "./SVModel";
import { SVNode } from "./SVNode";
export class SVLink extends SVModel {
public node: SVNode;
public target: SVNode;
public linkIndex: number;
public shadowG6Item: IEdge;
public G6Item: IEdge;
constructor(id: string, type: string, group: string, layout: string, node: SVNode, target: SVNode, index: number, options: LinkOption) {
super(id, type, group, layout);
this.node = node;
this.target = target;
this.linkIndex = index;
node.links.outDegree.push(this);
target.links.inDegree.push(this);
this.G6ModelProps = this.generateG6ModelProps(options);
}
protected generateG6ModelProps(options: LinkOption): EdgeConfig {
let sourceAnchor = options.sourceAnchor,
targetAnchor = options.targetAnchor;
if(options.sourceAnchor && typeof options.sourceAnchor === 'function' && this.linkIndex !== null) {
sourceAnchor = options.sourceAnchor(this.linkIndex);
}
if(options.targetAnchor && typeof options.targetAnchor === 'function' && this.linkIndex !== null) {
targetAnchor = options.targetAnchor(this.linkIndex);
}
return {
id: this.id,
type: options.type,
source: this.node.id,
target: this.target.id,
sourceAnchor,
targetAnchor,
label: options.label,
style: Util.objectClone<Style>(options.style),
labelCfg: Util.objectClone<LinkLabelOption>(options.labelOptions),
curveOffset: options.curveOffset
};
}
};

View File

@ -1,51 +0,0 @@
import { INode, NodeConfig } from "@antv/g6-core";
import { Util } from "../Common/util";
import { MarkerOption, NodeLabelOption, Style } from "../options";
import { SVModel } from "./SVModel";
import { SVNode } from "./SVNode";
export class SVMarker extends SVModel {
public target: SVNode;
public label: string | string[];
public anchor: number;
public shadowG6Item: INode;
public G6Item: INode;
constructor(id: string, type: string, group: string, layout: string, label: string | string[], target: SVNode, options: MarkerOption) {
super(id, type, group, layout);
this.target = target;
this.label = label;
this.target.marker = this;
this.G6ModelProps = this.generateG6ModelProps(options);
}
protected generateG6ModelProps(options: MarkerOption): NodeConfig {
this.anchor = options.anchor;
const type = options.type,
defaultSize: [number, number] = type === 'pointer'? [8, 30]: [12, 12];
return {
id: this.id,
x: 0,
y: 0,
rotation: 0,
type: options.type || 'marker',
size: options.size || defaultSize,
anchorPoints: null,
label: typeof this.label === 'string'? this.label: this.label.join(', '),
style: Util.objectClone<Style>(options.style),
labelCfg: Util.objectClone<NodeLabelOption>(options.labelOptions)
};
}
public getLabelSizeRadius(): number {
const { width, height } = this.shadowG6Item.getContainer().getChildren()[2].getBBox();
return width > height? width: height;
}
};

View File

@ -1,170 +0,0 @@
import { Util } from "../Common/util";
import { ModelOption, Style } from "../options";
import { BoundingRect } from "../Common/boundingRect";
import { EdgeConfig, Item, NodeConfig } from "@antv/g6-core";
import { Point } from "@antv/g-base";
export class SVModel {
public id: string;
public sourceType: string;
public group: string;
public layout: string;
public G6ModelProps: NodeConfig | EdgeConfig;
public shadowG6Item: Item;
public G6Item: Item;
public discarded: boolean;
public freed: boolean;
public leaked: boolean;
public generalStyle: Partial<Style>;
private transformMatrix: number[];
constructor(id: string, type: string, group: string, layout: string) {
this.id = id;
this.sourceType = type;
this.group = group;
this.layout = layout;
this.shadowG6Item = null;
this.G6Item = null;
this.discarded = false;
this.freed = false;
this.leaked = false;
this.transformMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
}
/**
* @override
* G6 model
* @param option
*/
protected generateG6ModelProps(options: ModelOption) {
return null;
}
/**
* G6 model
* @param attr
*/
get(attr: string): any {
return this.G6ModelProps[attr];
}
/**
* G6 model
* @param attr
* @param value
* @returns
*/
set(attr: string | object, value?: any) {
if(this.discarded) {
return;
}
if(typeof attr === 'object') {
Object.keys(attr).map(item => {
this.set(item, attr[item]);
});
return;
}
if(this.G6ModelProps[attr] === value) {
return;
}
if(attr === 'style' || attr === 'labelCfg') {
Object.assign(this.G6ModelProps[attr], value);
}
else {
this.G6ModelProps[attr] = value;
}
if(attr === 'rotation') {
const matrix = Util.calcRotateMatrix(this.getMatrix(), value);
this.setMatrix(matrix);
}
// 更新G6Item
if(this.G6Item) {
if(attr === 'x' || attr === 'y') {
this.G6Item.updatePosition({ [attr]: value } as Point);
this.G6Item.refresh();
}
else {
this.G6Item.update(this.G6ModelProps);
}
}
// 更新shadowG6Item
if(this.shadowG6Item) {
if(attr === 'x' || attr === 'y') {
this.shadowG6Item.updatePosition({ [attr]: value } as Point);
this.shadowG6Item.refresh();
}
else {
this.shadowG6Item.update(this.G6ModelProps);
}
}
}
/**
*
* @returns
*/
getBound(): BoundingRect {
return this.shadowG6Item.getBBox();
}
/**
*
*/
getMatrix(): number[] {
return [...this.transformMatrix];
}
/**
*
* @param matrix
*/
setMatrix(matrix: number[]) {
this.transformMatrix = matrix;
this.set('style', { matrix });
}
/**
*
* @returns
*/
getG6ModelProps(): NodeConfig | EdgeConfig {
return Util.objectClone(this.G6ModelProps);
}
/**
* modelSVNode
*/
isNode(): boolean {
return false;
}
}

View File

@ -1,151 +0,0 @@
import { INode, NodeConfig } from "@antv/g6-core";
import { Util } from "../Common/util";
import { NodeIndexOption, NodeLabelOption, NodeOption, Style } from "../options";
import { SourceNode } from "../sources";
import { SVLink } from "./SVLink";
import { SVMarker } from "./SVMarker";
import { SVModel } from "./SVModel";
export class SVFreedLabel extends SVModel {
public node: SVNode;
constructor(id: string, type: string, group: string, layout: string, node: SVNode) {
super(id, type, group, layout);
this.node = node;
this.node.freedLabel = this;
this.G6ModelProps = this.generateG6ModelProps();
}
generateG6ModelProps() {
return {
id: this.id,
x: 0,
y: 0,
type: 'rect',
label: '已释放',
labelCfg: {
style: {
fill: '#b83b5e',
opacity: 0.6
}
},
size: [0, 0],
style: {
stroke: null,
fill: 'transparent'
}
};
}
}
export class SVLeakAddress extends SVModel {
public node: SVNode;
private sourceId: string;
constructor(id: string, type: string, group: string, layout: string, node: SVNode) {
super(id, type, group, layout);
this.node = node;
this.sourceId = node.sourceId;
this.node.leakAddress = this;
this.G6ModelProps = this.generateG6ModelProps();
}
generateG6ModelProps() {
return {
id: this.id,
x: 0,
y: 0,
type: 'rect',
label: this.sourceId,
labelCfg: {
style: {
fill: '#666',
fontSize: 16
}
},
size: [0, 0],
style: {
stroke: null,
fill: 'transparent'
}
};
}
}
export class SVNode extends SVModel {
public sourceId: string;
public sourceNode: SourceNode;
public marker: SVMarker;
public freedLabel: SVFreedLabel;
public leakAddress: SVLeakAddress;
public links: {
inDegree: SVLink[];
outDegree: SVLink[];
};
private label: string | string[];
public shadowG6Item: INode;
public G6Item: INode;
constructor(id: string, type: string, group: string, layout: string, sourceNode: SourceNode, label: string | string[], options: NodeOption) {
super(id, type, group, layout);
this.group = group;
this.layout = layout;
Object.keys(sourceNode).map(prop => {
if (prop !== 'id') {
this[prop] = sourceNode[prop];
}
});
this.sourceNode = sourceNode;
this.sourceId = sourceNode.id.toString();
this.marker = null;
this.links = { inDegree: [], outDegree: [] };
this.sourceNode = sourceNode;
this.label = label;
this.G6ModelProps = this.generateG6ModelProps(options);
}
protected generateG6ModelProps(options: NodeOption): NodeConfig {
let indexOptions = Util.objectClone<NodeIndexOption>(options.indexOptions);
if (indexOptions) {
Object.keys(indexOptions).map(key => {
let indexOptionItem = indexOptions[key];
indexOptionItem.value = this.sourceNode[key] ?? '';
});
}
return {
...this.sourceNode,
id: this.id,
x: 0,
y: 0,
rotation: options.rotation || 0,
type: options.type,
size: options.size || [60, 30],
anchorPoints: options.anchorPoints,
label: this.label as string,
style: Util.objectClone<Style>(options.style),
labelCfg: Util.objectClone<NodeLabelOption>(options.labelOptions),
indexCfg: indexOptions
};
}
isNode(): boolean {
return true;
}
};

View File

@ -1,25 +1,20 @@
import { Util } from "../Common/util";
import { Engine } from "../engine";
import { LayoutCreator, LayoutGroupOptions, LinkOption, MarkerOption, NodeOption } from "../options";
import { sourceLinkData, LinkTarget, Sources, SourceNode } from "../sources";
import { ElementIndexOption, ElementOption, Layouter, LayoutGroupOptions, LinkOption, MarkerOption } from "../options";
import { sourceLinkData, SourceElement, LinkTarget, Sources } from "../sources";
import { SV } from "../StructV";
import { SVLink } from "./SVLink";
import { SVMarker } from "./SVMarker";
import { SVModel } from "./SVModel";
import { SVFreedLabel, SVLeakAddress, SVNode } from "./SVNode";
import { Element, Link, Marker, Model } from "./modelData";
export type LayoutGroup = {
name: string;
node: SVNode[];
freedLabel: SVFreedLabel[];
leakAddress: SVLeakAddress[];
link: SVLink[];
marker: SVMarker[];
layoutCreator: LayoutCreator;
layout: string;
element: Element[];
link: Link[];
marker: Marker[];
layouter: Layouter;
layouterName: string;
options: LayoutGroupOptions;
modelList: SVModel[];
modelList: Model[];
isHide: boolean;
};
@ -38,66 +33,56 @@ export class ModelConstructor {
}
/**
* svnodesvlink svmarker
* elementlink和marker
* @param sourceList
*/
public construct(sources: Sources): LayoutGroupTable {
const layoutGroupTable = new Map<string, LayoutGroup>(),
layoutMap: { [key: string]: LayoutCreator } = SV.registeredLayout,
layouterMap: { [key: string]: Layouter } = SV.registeredLayouter,
optionsTable = this.engine.optionsTable;
Object.keys(sources).forEach(group => {
let sourceGroup = sources[group],
layout = sourceGroup.layouter,
layoutCreator: LayoutCreator = layoutMap[layout];
Object.keys(sources).forEach(name => {
let sourceGroup = sources[name],
layouterName = sourceGroup.layouter,
layouter: Layouter = layouterMap[sourceGroup.layouter];
if (!layout || !layoutCreator) {
if (!layouterName || !layouter) {
return;
}
let sourceDataString: string = JSON.stringify(sourceGroup.data),
prevString: string = this.prevSourcesStringMap[group],
nodeList: SVNode[] = [],
freedLabelList: SVFreedLabel[] = [],
leakAddress: SVLeakAddress[] = [],
markerList: SVMarker[] = [];
prevString: string = this.prevSourcesStringMap[name],
elementList: Element[] = [],
markerList: Marker[] = [];
if (prevString === sourceDataString) {
return;
}
const options: LayoutGroupOptions = optionsTable[layout],
sourceData = layoutCreator.sourcesPreprocess(sourceGroup.data, options),
nodeOptions = options.node || options['element'] || {},
const options: LayoutGroupOptions = optionsTable[layouterName],
sourceData = layouter.sourcesPreprocess(sourceGroup.data, options),
elementOptions = options.element || {},
markerOptions = options.marker || {};
nodeList = this.constructNodes(nodeOptions, group, sourceData, layout);
leakAddress = nodeList.map(item => item.leakAddress);
markerList = this.constructMarkers(group, layout, markerOptions, nodeList);
nodeList.forEach(item => {
if(item.freedLabel) {
freedLabelList.push(item.freedLabel);
}
});
elementList = this.constructElements(elementOptions, name, sourceData, layouterName);
markerList = this.constructMarkers(name, markerOptions, elementList);
layoutGroupTable.set(group, {
name: group,
node: nodeList,
freedLabel: freedLabelList,
leakAddress: leakAddress,
layoutGroupTable.set(name, {
name,
element: elementList,
link: [],
marker: markerList,
options: options,
layoutCreator,
modelList: [...nodeList, ...markerList, ...freedLabelList, ...leakAddress],
layout,
layouter: layouter,
modelList: [...elementList, ...markerList],
layouterName,
isHide: false
});
});
layoutGroupTable.forEach((layoutGroup: LayoutGroup, group: string) => {
layoutGroupTable.forEach((layoutGroup: LayoutGroup) => {
const linkOptions = layoutGroup.options.link || {},
linkList: SVLink[] = this.constructLinks(linkOptions, layoutGroup.node, layoutGroupTable, group, layoutGroup.layout, );
linkList: Link[] = this.constructLinks(linkOptions, layoutGroup.element, layoutGroupTable);
layoutGroup.link = linkList;
layoutGroup.modelList.push(...linkList);
@ -117,16 +102,16 @@ export class ModelConstructor {
}
/**
* node
* @param nodeOptions
* @param group
* element
* @param elementOptions
* @param groupName
* @param sourceList
* @param layout
* @param layouterName
* @returns
*/
private constructNodes(nodeOptions: { [key: string]: NodeOption }, group: string, sourceList: SourceNode[], layout: string): SVNode[] {
let defaultSourceNodeType: string = 'default',
nodeList: SVNode[] = [];
private constructElements(elementOptions: { [key: string]: ElementOption }, groupName: string, sourceList: SourceElement[], layouterName: string): Element[] {
let defaultElementType: string = 'default',
elementList: Element[] = [];
sourceList.forEach(item => {
if (item === null) {
@ -134,62 +119,62 @@ export class ModelConstructor {
}
if (item.type === undefined || item.type === null) {
item.type = defaultSourceNodeType;
item.type = defaultElementType;
}
nodeList.push(this.createNode(item, item.type, group, layout, nodeOptions[item.type]));
elementList.push(this.createElement(item, item.type, groupName, layouterName, elementOptions[item.type]));
});
return nodeList;
return elementList;
}
/**
* node link
* element link
* @param linkOptions
* @param nodes
* @param elements
* @param layoutGroupTable
* @returns
*/
private constructLinks(linkOptions: { [key: string]: LinkOption }, nodes: SVNode[], layoutGroupTable: LayoutGroupTable, group: string, layout: string): SVLink[] {
let linkList: SVLink[] = [],
private constructLinks(linkOptions: { [key: string]: LinkOption }, elements: Element[], layoutGroupTable: LayoutGroupTable): Link[] {
let linkList: Link[] = [],
linkNames = Object.keys(linkOptions);
linkNames.forEach(name => {
for (let i = 0; i < nodes.length; i++) {
let node: SVNode = nodes[i],
sourceLinkData: sourceLinkData = node.sourceNode[name],
targetNode: SVNode | SVNode[] = null,
link: SVLink = null;
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;
if (sourceLinkData === undefined || sourceLinkData === null) {
node[name] = null;
element[name] = null;
continue;
}
// ------------------- 将连接声明字段 sourceLinkData 从 id 变为 SVNode -------------------
// ------------------- 将连接声明字段 sourceLinkData 从 id 变为 Element -------------------
if (Array.isArray(sourceLinkData)) {
node[name] = sourceLinkData.map((item, index) => {
targetNode = this.fetchTargetNodes(layoutGroupTable, node, item);
element[name] = sourceLinkData.map((item, index) => {
targetElement = this.fetchTargetElements(layoutGroupTable, element, item);
let isGeneralLink = this.isGeneralLink(sourceLinkData.toString());
if (targetNode) {
link = this.createLink(name, group, layout, node, targetNode, index, linkOptions[name]);
if (targetElement) {
link = this.createLink(name, element, targetElement, index, linkOptions[name]);
linkList.push(link);
}
return isGeneralLink ? targetNode : null;
return isGeneralLink ? targetElement : null;
});
}
else {
targetNode = this.fetchTargetNodes(layoutGroupTable, node, sourceLinkData);
targetElement = this.fetchTargetElements(layoutGroupTable, element, sourceLinkData);
let isGeneralLink = this.isGeneralLink(sourceLinkData.toString());
if (targetNode) {
link = this.createLink(name, group, layout, node, targetNode, null, linkOptions[name]);
if (targetElement) {
link = this.createLink(name, element, targetElement, null, linkOptions[name]);
linkList.push(link);
}
node[name] = isGeneralLink ? targetNode : null;
element[name] = isGeneralLink ? targetElement : null;
}
}
});
@ -198,25 +183,25 @@ export class ModelConstructor {
}
/**
* node marker
* element marker
* @param markerOptions
* @param nodes
* @param elements
* @returns
*/
private constructMarkers(group: string, layout: string, markerOptions: { [key: string]: MarkerOption }, nodes: SVNode[]): SVMarker[] {
let markerList: SVMarker[] = [],
private constructMarkers(groupName: string, markerOptions: { [key: string]: MarkerOption }, elements: Element[]): Marker[] {
let markerList: Marker[] = [],
markerNames = Object.keys(markerOptions);
markerNames.forEach(name => {
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i],
markerData = node[name];
for (let i = 0; i < elements.length; i++) {
let element = elements[i],
markerData = element[name];
// 若没有指针字段的结点则跳过
if (!markerData) continue;
let id = `${group}.${name}.${Array.isArray(markerData) ? markerData.join('-') : markerData}`,
marker = this.createMarker(id, name, markerData, group, layout, node, markerOptions[name]);
let id = `${groupName}.${name}.${Array.isArray(markerData) ? markerData.join('-') : markerData}`,
marker = this.createMarker(id, name, markerData, element, markerOptions[name]);
markerList.push(marker);
}
@ -228,19 +213,19 @@ export class ModelConstructor {
/**
* label文本
* @param label
* @param sourceNode
* @param sourceElement
*/
private resolveNodeLabel(label: string | string[], sourceNode: SourceNode): string {
private resolveElementLabel(label: string | string[], sourceElement: SourceElement): string {
let targetLabel: any = '';
if (Array.isArray(label)) {
targetLabel = label.map(item => this.parserNodeContent(sourceNode, item) ?? '');
targetLabel = label.map(item => this.parserElementContent(sourceElement, item) ?? '');
}
else {
targetLabel = this.parserNodeContent(sourceNode, label);
targetLabel = this.parserElementContent(sourceElement, label);
}
if (targetLabel === 'undefined') {
if(targetLabel === 'undefined') {
targetLabel = '';
}
@ -248,67 +233,90 @@ export class ModelConstructor {
}
/**
* Node
* @param sourceNode
* @param sourceNodeType
* @param group
* @param layout
* @param options
* index文本
* @param indexOptions
* @param sourceElement
*/
private createNode(sourceNode: SourceNode, sourceNodeType: string, group: string, layout: string, options: NodeOption): SVNode {
let label: string | string[] = this.resolveNodeLabel(options.label, sourceNode),
id = sourceNodeType + '.' + sourceNode.id.toString(),
node = new SVNode(id, sourceNodeType, group, layout, sourceNode, label, options);
node.leakAddress = new SVLeakAddress(`${id}-leak-adress`, sourceNodeType, group, layout, node);
if(node.freed) {
node.freedLabel = new SVFreedLabel(`${id}-freed-label`, sourceNodeType, group, layout, node);
private resolveElementIndex(indexOptions: ElementIndexOption, sourceElement: SourceElement) {
if(indexOptions === undefined || indexOptions === null) {
return;
}
return node;
Object.keys(indexOptions).map(key => {
let indexOptionItem = indexOptions[key];
indexOptionItem.value = sourceElement[key] ?? '';
});
}
/**
* Element
* @param sourceElement
* @param elementName
* @param groupName
* @param layouterName
* @param options
*/
private createElement(sourceElement: SourceElement, elementName: string, groupName: string, layouterName: string, options: ElementOption): Element {
let element: Element = undefined,
label: string | string[] = this.resolveElementLabel(options.label, sourceElement),
id = elementName + '.' + sourceElement.id.toString();
element = new Element(id, elementName, groupName, layouterName, sourceElement);
element.initProps(options);
element.set('label', label);
element.sourceElement = sourceElement;
// 处理element的index文本
this.resolveElementIndex(element.get('indexCfg'), sourceElement);
return element;
}
/**
* marker
* @param id
* @param markerName
* @param markerData
* @param group
* @param layout
* @param label
* @param target
* @param options
* @returns
*/
private createMarker(id: string, markerName: string, markerData: string | string[], group: string, layout: string, target: SVNode, options: MarkerOption): SVMarker {
return new SVMarker(id, markerName, group, layout, markerData, target, options);;
private createMarker(id: string, markerName: string, markerData: string | string[], target: Element, options: MarkerOption): Marker {
let marker = undefined;
marker = new Marker(id, markerName, markerData, target);
marker.initProps(options);
return marker;
};
/**
* 线Link
* @param linkName
* @param group
* @param layout
* @param node
* @param element
* @param target
* @param index
* @param options
* @returns
*/
private createLink(linkName: string, group: string, layout: string, node: SVNode, target: SVNode, index: number, options: LinkOption): SVLink {
let id = `${linkName}(${node.id}-${target.id})`;
return new SVLink(id, linkName, group, layout, node, target, index, options);
private createLink(linkName: string, element: Element, target: Element, index: number, options: LinkOption): Link {
let link = undefined,
id = `${linkName}(${element.id}-${target.id})`;
link = new Link(id, linkName, element, target, index);
link.initProps(options);
return link;
}
/**
*
* @param sourceNode
* @param sourceElement
* @param formatLabel
*/
private parserNodeContent(sourceNode: SourceNode, formatLabel: string): string {
private parserElementContent(sourceElement: SourceElement, formatLabel: string): string {
let fields = Util.textParser(formatLabel);
if (Array.isArray(fields)) {
let values = fields.map(item => sourceNode[item]);
let values = fields.map(item => sourceElement[item]);
values.map((item, index) => {
formatLabel = formatLabel.replace('[' + fields[index] + ']', item);
@ -320,17 +328,17 @@ export class ModelConstructor {
/**
* source中的连接字段获取真实的连接目标元素
* @param nodeContainer
* @param node
* @param elementContainer
* @param element
* @param linkTarget
*/
private fetchTargetNodes(layoutGroupTable: LayoutGroupTable, node: SVNode, linkTarget: LinkTarget): SVNode {
let group: string = node.group,
sourceNodeType = node.sourceType,
nodeList: SVNode[],
private fetchTargetElements(layoutGroupTable: LayoutGroupTable, element: Element, linkTarget: LinkTarget): Element {
let groupName: string = element.groupName,
elementName = element.type,
elementList: Element[],
targetId = linkTarget,
targetGroupName = group,
targetNode = null;
targetGroupName = groupName,
targetElement = null;
if (linkTarget === null || linkTarget === undefined) {
return null;
@ -345,13 +353,13 @@ export class ModelConstructor {
targetId = info.pop();
if (info.length > 1) {
sourceNodeType = info.pop();
elementName = info.pop();
targetGroupName = info.pop();
}
else {
let field = info.pop();
if (layoutGroupTable.get(targetGroupName).node.find(item => item.sourceType === field)) {
sourceNodeType = field;
if (layoutGroupTable.get(targetGroupName).element.find(item => item.type === field)) {
elementName = field;
}
else if (layoutGroupTable.has(field)) {
targetGroupName = field;
@ -361,15 +369,15 @@ export class ModelConstructor {
}
}
nodeList = layoutGroupTable.get(targetGroupName).node.filter(item => item.sourceType === sourceNodeType);
elementList = layoutGroupTable.get(targetGroupName).element.filter(item => item.type === elementName);
// 若目标node不存在返回null
if (nodeList === undefined) {
// 若目标element不存在返回null
if (elementList === undefined) {
return null;
}
targetNode = nodeList.find(item => item.sourceId === targetId);
return targetNode || null;
targetElement = elementList.find(item => item.sourceId === targetId);
return targetElement || null;
}
/**

337
src/Model/modelData.ts Normal file
View File

@ -0,0 +1,337 @@
import { Util } from "../Common/util";
import { ElementIndexOption, ElementLabelOption, ElementOption, LinkLabelOption, LinkOption, MarkerOption, Style } from "../options";
import { SourceElement } from "../sources";
import { BoundingRect } from "../Common/boundingRect";
import { SV } from './../StructV';
export interface G6NodeModel {
id: string;
x: number;
y: number;
rotation: number;
type: string;
size: number | [number, number];
anchorPoints: [number, number];
label: string | string[];
style: Style;
labelCfg: ElementLabelOption;
indexCfg?: ElementIndexOption;
markerId: string;
SVLayouter: string;
SVModelType: string;
SVModelName: string;
};
export interface G6EdgeModel {
id: string;
source: string | number;
target: string | number;
type: string;
curveOffset: number;
sourceAnchor: number | ((index: number) => number);
targetAnchor: number | ((index: number) => number);
label: string;
style: Style;
labelCfg: LinkLabelOption;
SVModelType: string;
SVModelName: string;
};
export class Model {
id: string;
type: string;
isLeak: boolean;
isDestroy: boolean;
modelType: string;
props: G6NodeModel | G6EdgeModel;
shadowG6Item;
renderG6Item;
G6Item;
constructor(id: string, type: string) {
this.id = id;
this.type = type;
this.shadowG6Item = null;
this.renderG6Item = null;
this.G6Item = null;
this.props = <G6NodeModel | G6EdgeModel>{ };
this.isLeak = false;
this.isDestroy = false;
}
/**
* @override
* G6 model
* @param option
*/
protected defineProps(option: ElementOption | LinkOption | MarkerOption) {
return null;
}
/**
* G6 model
* @param option
*/
initProps(option: ElementOption | LinkOption | MarkerOption) {
this.props = this.defineProps(option);
}
/**
* G6 model
* @returns
*/
cloneProps() {
return Util.objectClone(this.props);
}
/**
* G6 model
* @param attr
*/
get(attr: string): any {
return this.props[attr];
}
/**
* G6 model
* @param attr
* @param value
* @returns
*/
set(attr: string | object, value?: any) {
if(this.isDestroy) {
return;
}
if(typeof attr === 'object') {
Object.keys(attr).map(item => {
this.set(item, attr[item]);
});
return;
}
if(this.props[attr] === value) {
return;
}
if(attr === 'style' || attr === 'labelCfg') {
Object.assign(this.props[attr], value);
}
else {
this.props[attr] = value;
}
if(this.G6Item === null) {
return;
}
if(attr === 'rotation') {
const matrix = Util.calcRotateMatrix(this.getMatrix(), value);
this.set('style', { matrix });
}
if(attr === 'x' || attr === 'y') {
this.G6Item.updatePosition({
[attr]: value
});
}
else {
this.G6Item.update(this.props);
}
}
/**
*
* @returns
*/
getBound(): BoundingRect {
return this.G6Item.getBBox();
}
/**
*
*/
getMatrix(): number[] {
if(this.G6Item === null) return null;
const Mat3 = SV.G6.Util.mat3;
return Mat3.create();
}
getType(): string {
return this.type;
}
getModelType(): string {
return this.modelType;
}
getId(): string {
return this.id;
}
}
export class Element extends Model {
sourceElement: SourceElement;
sourceId: string;
groupName: string;
layouterName: string;
freed: boolean;
markers: { [key: string]: Marker };
links: {
inDegree: Link[];
outDegree: Link[];
};
constructor(id: string, type: string, group: string, layouter: string, sourceElement: SourceElement) {
super(id, type);
this.modelType = 'element';
if(type === null) {
return;
}
this.groupName = group;
this.layouterName = layouter;
this.freed = false;
Object.keys(sourceElement).map(prop => {
if(prop !== 'id') {
this[prop] = sourceElement[prop];
}
});
this.sourceId = this.id.split('.')[1];
this.sourceElement = sourceElement;
this.markers = { };
this.links = { inDegree: [], outDegree: [] };
}
protected defineProps(option: ElementOption): G6NodeModel {
return {
...this.sourceElement,
id: this.id,
x: 0,
y: 0,
rotation: option.rotation || 0,
type: option.type,
size: option.size || [60, 30],
anchorPoints: option.anchorPoints,
label: option.label,
style: Util.objectClone<Style>(option.style),
labelCfg: Util.objectClone<ElementLabelOption>(option.labelOptions),
indexCfg: Util.objectClone<ElementIndexOption>(option.indexOptions),
markerId: null,
SVLayouter: this.layouterName,
SVModelType: 'element',
SVModelName: this.type
};
}
getSourceId(): string {
return this.sourceId;
}
};
export class Link extends Model {
element: Element;
target: Element;
index: number;
constructor(id: string, type: string, element: Element, target: Element, index: number) {
super(id, type);
this.modelType = 'link';
this.element = element;
this.target = target;
this.index = index;
element.links.outDegree.push(this);
target.links.inDegree.push(this);
}
protected defineProps(option: LinkOption): G6EdgeModel {
let sourceAnchor = option.sourceAnchor,
targetAnchor = option.targetAnchor;
if(option.sourceAnchor && typeof option.sourceAnchor === 'function' && this.index !== null) {
sourceAnchor = option.sourceAnchor(this.index);
}
if(option.targetAnchor && typeof option.targetAnchor === 'function' && this.index !== null) {
targetAnchor = option.targetAnchor(this.index);
}
return {
id: this.id,
type: option.type,
source: this.element.id,
target: this.target.id,
sourceAnchor,
targetAnchor,
label: option.label,
style: Util.objectClone<Style>(option.style),
labelCfg: Util.objectClone<LinkLabelOption>(option.labelOptions),
curveOffset: option.curveOffset,
SVModelType: 'link',
SVModelName: this.type
};
}
};
export class Marker extends Model {
target: Element;
label: string | string[];
anchor: number;
constructor(id: string, type: string, label: string | string[], target: Element) {
super(id, type);
this.modelType = 'marker';
this.target = target;
this.label = label;
this.target.set('markerId', id);
this.target.markers[type] = this;
}
getLabelSizeRadius(): number {
const { width, height } = this.G6Item.getContainer().getChildren()[2].getBBox();
return width > height? width: height;
}
protected defineProps(option: MarkerOption): G6NodeModel {
this.anchor = option.anchor;
const type = option.type,
defaultSize: [number, number] = type === 'pointer'? [8, 30]: [12, 12];
return {
id: this.id,
x: 0,
y: 0,
rotation: 0,
type: option.type || 'marker',
size: option.size || defaultSize,
anchorPoints: null,
label: typeof this.label === 'string'? this.label: this.label.join(', '),
style: Util.objectClone<Style>(option.style),
labelCfg: Util.objectClone<ElementLabelOption>(option.labelOptions),
markerId: null,
SVLayouter: null,
SVModelType: 'marker',
SVModelName: this.type
};
}
};

View File

@ -1,4 +1,4 @@
import G6 from '@antv/g6';
import * as G6 from "./../Lib/g6.js";
export default G6.registerNode('binary-tree-node', {

View File

@ -1,12 +1,11 @@
import G6 from '@antv/g6';
import * as G6 from "../Lib/g6.js";
export default G6.registerNode('clen-queue-pointer', {
draw(cfg, group) {
let id = cfg.id as string;
const index = parseInt(id.split('-')[1]);
const len = parseInt(id.split('-')[2]);
// console.log(cfg);
const index = cfg.id.split('-')[1];
const len = cfg.id.split('-')[2];
const keyShape = group.addShape('path', {
attrs: {
x: 0,
@ -17,7 +16,6 @@ export default G6.registerNode('clen-queue-pointer', {
},
name: 'pointer-path'
});
const angle = index * Math.PI * 2 / len;
if (cfg.label) {
const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
@ -34,10 +32,7 @@ export default G6.registerNode('clen-queue-pointer', {
},
name: 'bgRect'
});
let label = cfg.label as string;
let pointerText = label.split('-')[0];
let pointerText = cfg.label.split('-')[0];
let y = pointerText=="front"?30:15;
const text = group.addShape('text', {
attrs: {

View File

@ -1,4 +1,6 @@
import G6 from '@antv/g6';
import * as G6 from "./../Lib/g6.js";
export default G6.registerNode('cursor', {

View File

@ -1,4 +1,4 @@
import G6 from '@antv/g6';
import * as G6 from "./../Lib/g6.js";
export default G6.registerNode('indexed-node', {

View File

@ -1,4 +1,4 @@
import G6 from '@antv/g6';
import * as G6 from "./../Lib/g6.js";
export default G6.registerNode('link-list-node', {

View File

@ -1,4 +1,4 @@
import G6 from '@antv/g6';
import * as G6 from "../Lib/g6.js";
export default G6.registerNode('pointer', {

View File

@ -1,4 +1,4 @@
import G6 from '@antv/g6';
import * as G6 from "./../Lib/g6.js";
export default G6.registerNode('tri-tree-node', {

View File

@ -1,4 +1,4 @@
import G6 from '@antv/g6';
import * as G6 from "./../Lib/g6.js";

View File

@ -2,7 +2,7 @@ import { Engine } from "./engine";
import { Bound } from "./Common/boundingRect";
import { Group } from "./Common/group";
import pointer from "./RegisteredShape/pointer";
import G6, { Util } from '@antv/g6';
import * as G6 from "./Lib/g6.js";
import linkListNode from "./RegisteredShape/linkListNode";
import binaryTreeNode from "./RegisteredShape/binaryTreeNode";
import CLenQueuePointer from "./RegisteredShape/clenQueuePointer";
@ -10,10 +10,9 @@ import twoCellNode from "./RegisteredShape/twoCellNode";
import Cursor from "./RegisteredShape/cursor";
import { Vector } from "./Common/vector";
import indexedNode from "./RegisteredShape/indexedNode";
import { EngineOptions, LayoutCreator } from "./options";
import { SVNode } from "./Model/SVNode";
import { SourceNode } from "./sources";
import { EngineOptions, Layouter } from "./options";
import { SourceElement } from "./sources";
import { Element } from "./Model/modelData";
export interface StructV {
@ -26,16 +25,16 @@ export interface StructV {
registeredShape: any[];
registeredLayout: { [key: string]: LayoutCreator },
registeredLayouter: { [key: string]: Layouter },
registerShape: Function,
/**
*
* @param name
* @param layout
* @param layouter
*/
registerLayout(name: string, layoutCreator: LayoutCreator);
registerLayouter(name: string, layouter);
}
@ -46,10 +45,10 @@ export const SV: StructV = function(DOMContainer: HTMLElement, engineOptions: En
SV.Group = Group;
SV.Bound = Bound;
SV.Vector = Vector;
SV.Mat3 = Util.mat3;
SV.Mat3 = G6.Util.mat3;
SV.G6 = G6;
SV.registeredLayout = {};
SV.registeredLayouter = {};
SV.registeredShape = [
pointer,
linkListNode,
@ -61,25 +60,25 @@ SV.registeredShape = [
];
SV.registerShape = G6.registerNode;
SV.registerLayout = function(name: string, layoutCreator: LayoutCreator) {
SV.registerLayouter = function(name: string, layouter: Layouter) {
if(typeof layoutCreator.sourcesPreprocess !== 'function') {
layoutCreator.sourcesPreprocess = function(data: SourceNode[]): SourceNode[] {
if(typeof layouter.sourcesPreprocess !== 'function') {
layouter.sourcesPreprocess = function(data: SourceElement[]): SourceElement[] {
return data;
}
}
if(typeof layoutCreator.defineLeakRule !== 'function') {
layoutCreator.defineLeakRule = function(nodes: SVNode[]): SVNode[] {
return nodes;
if(typeof layouter.defineLeakRule !== 'function') {
layouter.defineLeakRule = function(elements: Element[]): Element[] {
return elements;
}
}
if(typeof layoutCreator.defineOptions !== 'function' || typeof layoutCreator.layout !== 'function') {
if(typeof layouter.defineOptions !== 'function' || typeof layouter.layout !== 'function') {
return;
}
SV.registeredLayout[name] = layoutCreator;
SV.registeredLayouter[name] = layouter;
};

View File

@ -1,11 +1,12 @@
import { Util } from '@antv/g6';
import { Model } from "../Model/modelData";
import { SV } from "../StructV";
export type animationConfig = {
duration: number;
timingFunction: string;
payload?: any;
callback?: Function;
[key: string]: any;
}
@ -16,31 +17,34 @@ export const Animations = {
/**
* /
* @param G6Item
* @param model
* @param animationConfig
*/
APPEND(G6Item: any, animationConfig: animationConfig) {
const type = G6Item.getType(),
animate_append(model: Model, animationConfig: animationConfig) {
model.G6Item === null && console.log(model);
const G6Item = model.G6Item,
type = G6Item.getType(),
group = G6Item.getContainer(),
Mat3 = Util.mat3,
Mat3 = SV.Mat3,
animateCfg = {
duration: animationConfig.duration,
easing: animationConfig.timingFunction,
callback: animationConfig.callback
};
if (type === 'node') {
if(type === 'node') {
let matrix = group.getMatrix(),
targetMatrix = Mat3.clone(matrix);
Mat3.scale(matrix, matrix, [0, 0]);
Mat3.scale(targetMatrix, targetMatrix, [1, 1]);
group.attr({ matrix, opacity: 0 });
group.animate({ matrix: targetMatrix, opacity: 1 }, animateCfg);
group.attr({ opacity: 0, matrix });
group.animate({ opacity: 1, matrix: targetMatrix }, animateCfg);
}
if (type === 'edge') {
if(type === 'edge') {
const line = group.get('children')[0],
length = line.getTotalLength();
@ -51,27 +55,28 @@ export const Animations = {
/**
* /
* @param G6Item
* @param model
* @param animationConfig
*/
REMOVE(G6Item: any, animationConfig: animationConfig) {
const type = G6Item.getType(),
animate_remove(model: Model, animationConfig: animationConfig) {
const G6Item = model.G6Item,
type = G6Item.getType(),
group = G6Item.getContainer(),
Mat3 = Util.mat3,
Mat3 = SV.Mat3,
animateCfg = {
duration: animationConfig.duration,
easing: animationConfig.timingFunction,
callback: animationConfig.callback
};
if (type === 'node') {
if(type === 'node') {
let matrix = Mat3.clone(group.getMatrix());
Mat3.scale(matrix, matrix, [0, 0]);
group.animate({ opacity: 0, matrix }, animateCfg);
}
if (type === 'edge') {
if(type === 'edge') {
const line = group.get('children')[0],
length = line.getTotalLength();
@ -80,21 +85,22 @@ export const Animations = {
},
/**
*
* @param G6Item
* /
* @param model
* @param animationConfig
*/
FADE_IN(G6Item: any, animationConfig: animationConfig) {
const group = G6Item.getContainer(),
animate_fadeOut(model: Model, animationConfig: animationConfig) {
const G6Item = model.G6Item,
group = G6Item.getContainer(),
animateCfg = {
duration: animationConfig.duration,
duration: 1200,
easing: animationConfig.timingFunction,
callback: animationConfig.callback
};
group.attr({ opacity: 0 });
group.animate({ opacity: 1 }, animateCfg);
group.animate({ opacity: 0 }, animateCfg);
}
};

View File

@ -0,0 +1,268 @@
import { Engine } from "../../engine";
import { Model, Marker } from "../../Model/modelData";
import { AnimationOptions, InteractionOptions, LayoutGroupOptions } from "../../options";
import { SV } from "../../StructV";
import { Animations } from "../animation";
import { g6Behavior, Renderer } from "../renderer";
export class Container {
protected engine: Engine;
protected DOMContainer: HTMLElement; // 可视化视图容器
protected renderer: Renderer; // 渲染器
protected prevModelList: Model[]; // 上一次渲染的模型列表
protected animationsOptions: AnimationOptions;
protected interactionOptions: InteractionOptions;
protected afterAppendModelsCallbacks: ((models: Model[]) => void)[] = [];
protected afterRemoveModelsCallbacks: ((models: Model[]) => void)[] = [];
constructor(engine: Engine, DOMContainer: HTMLElement, g6Options: { [key: string]: any } = {}) {
this.engine = engine;
this.DOMContainer = DOMContainer;
this.animationsOptions = engine.animationOptions;
this.interactionOptions = engine.interactionOptions;
this.prevModelList = [];
const g6Plugins = [];
if (g6Options.tooltip) {
const tooltip = new SV.G6.Tooltip({
offsetX: 10,
offsetY: 20,
shouldBegin(event) {
return event.item.getModel().SVModelType === 'element';
},
getContent: event => this.getTooltipContent(event.item.SVModel, { address: 'sourceId', data: 'data' }),
itemTypes: ['node']
});
g6Plugins.push(tooltip);
}
this.renderer = new Renderer(engine, DOMContainer, {
fitCenter: g6Options.fitCenter,
modes: {
default: this.initBehaviors(this.engine.optionsTable)
},
plugins: g6Plugins
});
this.afterInitRenderer();
}
private getTooltipContent(model: Model, items: { [key: string]: string }): HTMLElement {
const wrapper = document.createElement('div');
Object.keys(items).map(key => {
let value = model[items[key]];
if (value !== undefined && value !== null) {
let item = document.createElement('div');
item.innerHTML = `${key}${value}`;
wrapper.appendChild(item);
}
});
return wrapper;
}
/**
*
* @param optionsTable
* @returns
*/
protected initBehaviors(optionsTable: { [key: string]: LayoutGroupOptions }): g6Behavior[] {
return ['drag-canvas', 'zoom-canvas'];
}
/**
* modelList
* @param prevList
* @param list
*/
protected getAppendModels(prevList: Model[], list: Model[]): Model[] {
return list.filter(item => !prevList.find(n => n.id === item.id));
}
/**
* modelList
* @param prevList
* @param list
*/
protected getRemoveModels(prevList: Model[], list: Model[]): Model[] {
return prevList.filter(item => !list.find(n => n.id === item.id));
}
/**
*
* @param list
* @returns
*/
protected findReTargetMarkers(list: Model[]): Marker[] {
let prevMarkers = this.prevModelList.filter(item => item instanceof Marker),
markers = list.filter(item => item instanceof Marker);
return <Marker[]>markers.filter(item => prevMarkers.find(prevItem => {
return prevItem.id === item.id && (<Marker>prevItem).target.id !== (<Marker>item).target.id
}));
}
/**
* label model
* @param list
*/
protected findLabelChangeModels(list: Model[]): Model[] {
let labelChangeModels: Model[] = [];
list.forEach(item => {
const prevItem = this.prevModelList.find(prevItem => prevItem.id === item.id);
if (prevItem === undefined) {
return;
}
const prevLabel = prevItem.get('label'),
label = item.get('label');
if (prevLabel !== label) {
labelChangeModels.push(item);
}
});
return labelChangeModels;
}
/**
* G6Item
* @param appendData
*/
protected handleAppendModels(appendModels: Model[]) {
let counter = 0;
appendModels.forEach(item => {
Animations.animate_append(item, {
duration: this.animationsOptions.duration,
timingFunction: this.animationsOptions.timingFunction,
callback: () => {
counter++;
if (counter === appendModels.length) {
this.afterAppendModelsCallbacks.map(item => item(appendModels));
}
}
});
});
}
/**
* G6Item
* @param removeData
*/
protected handleRemoveModels(removeModels: Model[]) {
let counter = 0;
removeModels.forEach(item => {
Animations.animate_remove(item, {
duration: this.animationsOptions.duration,
timingFunction: this.animationsOptions.timingFunction,
callback: () => {
if (item.isLeak === false) {
this.renderer.removeModel(item);
item.renderG6Item = item.G6Item = null;
}
counter++;
if (counter === removeModels.length) {
this.afterRemoveModelsCallbacks.map(item => item(removeModels));
}
}
});
});
}
/**
* models
* @param models
*/
protected handleChangeModels(models: Model[]) { }
/**
*
*/
protected afterInitRenderer() { }
// ------------------------------------------ hook ---------------------------------------------
afterAppendModels(callback: (models: Model[]) => void) {
this.afterAppendModelsCallbacks.push(callback);
}
afterRemoveModels(callback: (models: Model[]) => void) {
this.afterRemoveModelsCallbacks.push(callback);
}
// ----------------------------------------------------------------------------------------
/**
*
* @param modelList
*/
public render(modelList: Model[]) {
const appendModels: Model[] = this.getAppendModels(this.prevModelList, modelList),
removeModels: Model[] = this.getRemoveModels(this.prevModelList, modelList),
changeModels: Model[] = [
...appendModels,
...this.findLabelChangeModels(modelList),
...this.findReTargetMarkers(modelList)
];
// 渲染视图
this.renderer.render(modelList, removeModels);
// 处理副作用
this.handleAppendModels(appendModels);
this.handleRemoveModels(removeModels);
this.handleChangeModels(changeModels);
if (this.renderer.getIsFirstRender()) {
this.renderer.setIsFirstRender(false);
}
this.prevModelList = modelList;
}
/**
* g6
*/
public getG6Instance() {
return this.renderer.getG6Instance();
}
/**
*
*/
public destroy() {
this.renderer.destroy();
this.DOMContainer = null;
this.prevModelList = [];
this.animationsOptions = this.interactionOptions = null;
}
}
// -----------------------------------------------------------------------------------------------------------

View File

@ -0,0 +1,43 @@
import { BoundingRect } from "../../Common/boundingRect";
import { Group } from "../../Common/group";
import { Model } from "../../Model/modelData";
import { Container } from "./container";
/**
*
*/
export class FreedContainer extends Container {
/**
*
* @param freedModels
*/
fitCenter(freedModels: Model[]) {
freedModels.forEach(item => {
item.G6Item = item.shadowG6Item;
});
let width = this.getG6Instance().getWidth(),
height = this.getG6Instance().getHeight(),
group = new Group();
group.add(...freedModels);
let 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;
group.translate(dx, dy);
freedModels.forEach(item => {
item.G6Item = item.renderG6Item;
});
}
protected initBehaviors(): string[] {
return [];
}
};

View File

@ -0,0 +1,8 @@
import { Container } from "./container";
/**
*
*/
export class LeakContainer extends Container {
};

203
src/View/container/main.ts Normal file
View File

@ -0,0 +1,203 @@
import { Engine } from "../../engine";
import { Link, Model } from "../../Model/modelData";
import { InteractionOptions, LayoutGroupOptions } from "../../options";
import { Container } from "./container";
/**
*
*/
export class MainContainer extends Container {
constructor(engine: Engine, DOMContainer: HTMLElement, g6Options: { [key: string]: any } = { }) {
super(engine, DOMContainer, g6Options);
}
/**
*
* @param optionsTable
* @returns
*/
protected initBehaviors(optionsTable: { [key: string]: LayoutGroupOptions }) {
const dragNodeTable: { [key: string]: boolean | string[] } = { },
selectNodeTable: { [key: string]: boolean | string[] } = { },
interactionOptions: InteractionOptions = this.engine.interactionOptions,
defaultModes = [];
Object.keys(optionsTable).forEach(item => {
dragNodeTable[item] = optionsTable[item].behavior.dragNode;
selectNodeTable[item] = optionsTable[item].behavior.selectNode;
});
if(interactionOptions.drag) {
defaultModes.push('drag-canvas');
}
if(interactionOptions.zoom) {
defaultModes.push('zoom-canvas');
}
const dragNodeFilter = node => {
let model = node.item.getModel();
if(node.item === null) {
return false;
}
if(model.SVModelType === 'marker') {
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 === 'marker') {
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(),
marker = null,
markerX = 0,
markerY = 0,
dragStartX = 0,
dragStartY = 0,
element = null;
g6Instance.on('node:dragstart', ev => {
const model = ev.item.getModel();
element = ev.item.SVModel;
if(model.SVModelType === 'marker') {
return;
}
const dragNode = this.engine.optionsTable[model.SVLayouter].behavior.dragNode;
if(dragNode === false) {
return;
}
if(Array.isArray(dragNode) && dragNode.find(item => item === model.SVModelName) === undefined) {
return;
}
dragStartX = ev.canvasX;
dragStartY = ev.canvasY;
marker = g6Instance.findById(model.markerId);
if(marker) {
markerX = marker.getModel().x,
markerY = marker.getModel().y;
}
});
g6Instance.on('node:dragend', ev => {
let distanceX = ev.canvasX - dragStartX,
distanceY = ev.canvasY - dragStartY,
elementX = element.get('x'),
elementY = element.get('y');
element.set({
x: elementX + distanceX,
y: elementY + distanceY
});
marker = null;
markerX = 0,
markerY = 0,
dragStartX = 0,
dragStartY = 0;
element = null;
});
g6Instance.on('node:drag', ev => {
if(!marker) {
return;
}
let dx = ev.canvasX - dragStartX,
dy = ev.canvasY - dragStartY,
zoom = g6Instance.getZoom();
marker.updatePosition({
x: markerX + dx / zoom,
y: markerY + dy / zoom
});
});
}
protected handleChangeModels(models: Model[]) {
const changeHighlightColor: string = this.interactionOptions.changeHighlight;
// 第一次渲染的时候不高亮变化的元素
if(this.renderer.getIsFirstRender()) {
return;
}
if(!changeHighlightColor || typeof changeHighlightColor !== 'string') {
return;
}
models.forEach(item => {
if(item instanceof Link) {
item.set('style', {
stroke: changeHighlightColor
});
}
else {
item.set('style', {
fill: changeHighlightColor
});
}
});
}
};

View File

@ -1,35 +1,30 @@
import { IPoint } from '@antv/g6-core';
import { Bound, BoundingRect } from '../Common/boundingRect';
import { Group } from '../Common/group';
import { Vector } from '../Common/vector';
import { Engine } from '../engine';
import { LayoutGroupTable } from '../Model/modelConstructor';
import { SVMarker } from '../Model/SVMarker';
import { SVModel } from '../Model/SVModel';
import { SVFreedLabel, SVLeakAddress, SVNode } from '../Model/SVNode';
import { Element, Model, Marker } from '../Model/modelData';
import { LayoutOptions, MarkerOption, ViewOptions } from '../options';
import { ViewContainer } from './viewContainer';
import { Container } from './container/container';
export class LayoutProvider {
export class Layouter {
private engine: Engine;
private viewOptions: ViewOptions;
private viewContainer: ViewContainer;
constructor(engine: Engine, viewContainer: ViewContainer) {
constructor(engine: Engine) {
this.engine = engine;
this.viewOptions = this.engine.viewOptions;
this.viewContainer = viewContainer;
}
/**
*
* @param nodes
* @param elements
* @param markers
*/
private initLayoutValue(nodes: SVNode[], markers: SVMarker[]) {
[...nodes, ...markers].forEach(item => {
private initLayoutValue(elements: Element[], markers: Marker[]) {
[...elements, ...markers].forEach(item => {
item.set('rotation', item.get('rotation'));
item.set({ x: 0, y: 0 });
});
@ -40,21 +35,21 @@ export class LayoutProvider {
* @param marker
* @param markerOptions
*/
private layoutMarker(markers: SVMarker[], markerOptions: { [key: string]: MarkerOption }) {
private layoutMarker(markers: Marker[], markerOptions: { [key: string]: MarkerOption }) {
markers.forEach(item => {
const options: MarkerOption = markerOptions[item.sourceType],
const options: MarkerOption = markerOptions[item.getType()],
offset = options.offset ?? 8,
anchor = item.anchor ?? 0,
labelOffset = options.labelOffset ?? 2;
let target = item.target,
targetBound: BoundingRect = target.getBound(),
g6AnchorPosition = item.target.shadowG6Item.getAnchorPoints()[anchor] as IPoint,
anchorPosition = item.target.G6Item.getAnchorPoints()[anchor],
center: [number, number] = [targetBound.x + targetBound.width / 2, targetBound.y + targetBound.height / 2],
markerPosition: [number, number],
markerEndPosition: [number, number];
let anchorPosition: [number, number] = [g6AnchorPosition.x, g6AnchorPosition.y];
anchorPosition = [anchorPosition.x, anchorPosition.y];
let anchorVector = Vector.subtract(anchorPosition, center),
angle = 0, len = Vector.length(anchorVector) + offset;
@ -84,38 +79,23 @@ export class LayoutProvider {
}
/**
*
* @param freedLabels
*
* @param container
* @param models
*/
private layoutFreedLabel(freedLabels: SVFreedLabel[]) {
freedLabels.forEach(item => {
const freedNodeBound = item.node.getBound();
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;
item.set({
x: freedNodeBound.x + freedNodeBound.width / 2,
y: freedNodeBound.y + freedNodeBound.height * 1.5,
size: [freedNodeBound.width, 0]
});
});
group.translate(dx, dy);
}
/**
* address label
* @param leakAddress
*/
private layoutLeakAddress(leakAddress: SVLeakAddress[]) {
leakAddress.forEach(item => {
const nodeBound = item.node.getBound();
item.set({
x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y - 16,
size: [nodeBound.width, 0]
});
});
}
/**
* model进行布局
* @param layoutGroupTable
@ -125,58 +105,31 @@ export class LayoutProvider {
layoutGroupTable.forEach(group => {
const options: LayoutOptions = group.options.layout,
modelList: SVModel[] = group.modelList,
modelList: Model[] = group.modelList,
modelGroup: Group = new Group();
modelList.forEach(item => {
modelGroup.add(item);
});
this.initLayoutValue(group.node, group.marker); // 初始化布局参数
group.layoutCreator.layout(group.node, options); // 布局节点
this.initLayoutValue(group.element, group.marker); // 初始化布局参数
group.layouter.layout(group.element, options); // 布局节点
modelGroupList.push(modelGroup);
});
layoutGroupTable.forEach(group => {
this.layoutFreedLabel(group.freedLabel);
this.layoutLeakAddress(group.leakAddress);
this.layoutMarker(group.marker, group.options.marker); // 布局外部指针
});
return modelGroupList;
}
/**
*
* @param leakModels
* @param accumulateLeakModels
*/
private layoutLeakModels(leakModels: SVModel[], accumulateLeakModels: SVModel[]) {
const group: Group = new Group(),
containerHeight = this.viewContainer.getG6Instance().getHeight(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
xOffset = 50;
group.add(...leakModels);
const currentLeakGroupBound: BoundingRect = group.getBound(),
globalLeakGroupBound: BoundingRect = accumulateLeakModels.length ?
Bound.union(...accumulateLeakModels.map(item => item.getBound())) :
{ x: 0, y: containerHeight * (1 - leakAreaHeight), width: 0, height: 0 };
const { x: groupX, y: groupY } = currentLeakGroupBound,
dx = globalLeakGroupBound.x + globalLeakGroupBound.width + xOffset - groupX,
dy = globalLeakGroupBound.y - groupY;
group.translate(dx, dy);
return globalLeakGroupBound;
}
/**
*
* @param container
* @param modelGroupTable
*/
private layoutGroups(modelGroupList: Group[]): Group {
private layoutGroups(container: Container, modelGroupList: Group[]): Group {
let wrapperGroup: Group = new Group(),
group: Group,
prevBound: BoundingRect,
@ -221,42 +174,69 @@ export class LayoutProvider {
return wrapperGroup;
}
public layoutSingleMarker(marker: Marker, markerOptions: { [key: string]: MarkerOption }) {
const options: MarkerOption = markerOptions[marker.getType()],
offset = options.offset ?? 8,
anchor = marker.anchor ?? 0,
labelOffset = options.labelOffset ?? 2;
/**
*
* @param models
* @param leakBound
*/
private fitCenter(group: Group, leakBound: BoundingRect) {
let width = this.viewContainer.getG6Instance().getWidth(),
height = this.viewContainer.getG6Instance().getHeight() - leakBound.height;
let target = marker.target,
targetBound: BoundingRect = target.getBound(),
anchorPosition = marker.target.G6Item.getAnchorPoints()[anchor],
center: [number, number] = [targetBound.x + targetBound.width / 2, targetBound.y + targetBound.height / 2],
markerPosition: [number, number],
markerEndPosition: [number, number];
const 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;
anchorPosition = [anchorPosition.x, anchorPosition.y];
group.translate(dx, dy);
let anchorVector = Vector.subtract(anchorPosition, center),
angle = 0, len = Vector.length(anchorVector) + offset;
if (anchorVector[0] === 0) {
angle = anchorVector[1] > 0 ? -Math.PI : 0;
}
else {
angle = Math.sign(anchorVector[0]) * (Math.PI / 2 - Math.atan(anchorVector[1] / anchorVector[0]));
}
const markerHeight = marker.get('size')[1],
labelRadius = marker.getLabelSizeRadius() / 2;
anchorVector = Vector.normalize(anchorVector);
markerPosition = Vector.location(center, anchorVector, len);
markerEndPosition = Vector.location(center, anchorVector, markerHeight + len + labelRadius + labelOffset);
markerEndPosition = Vector.subtract(markerEndPosition, markerPosition);
marker.set({
x: markerPosition[0],
y: markerPosition[1],
rotation: angle,
markerEndPosition
});
}
/**
*
* @param container
* @param layoutGroupTable
* @param leakModels
* @param hasLeak
*/
public layoutAll(layoutGroupTable: LayoutGroupTable, accumulateLeakModels: SVModel[], leakModels: SVModel[]) {
public layoutAll(container: Container, layoutGroupTable: LayoutGroupTable) {
layoutGroupTable.forEach(item => {
item.modelList.forEach(model => {
model.G6Item = model.shadowG6Item;
});
});
const modelGroupList: Group[] = this.layoutModels(layoutGroupTable);
const globalGroup: Group = this.layoutGroups(modelGroupList);
const containerHeight = this.viewContainer.getG6Instance().getHeight();
let leakBound: BoundingRect = { x: 0, y: containerHeight, width: 0, height: 0 }
if (leakModels.length) {
leakBound = this.layoutLeakModels(leakModels, accumulateLeakModels);
const wrapperGroup: Group = this.layoutGroups(container, modelGroupList);
this.fitCenter(container, wrapperGroup);
layoutGroupTable.forEach(item => {
item.modelList.forEach(model => {
model.G6Item = model.renderG6Item;
});
});
}
this.fitCenter(globalGroup, leakBound);
}
}

View File

@ -1,406 +0,0 @@
import { EventBus } from "../Common/eventBus";
import { Util } from "../Common/util";
import { Engine } from "../engine";
import { SVLink } from "../Model/SVLink";
import { SVMarker } from "../Model/SVMarker";
import { SVModel } from "../Model/SVModel";
import { SVLeakAddress, SVNode } from "../Model/SVNode";
import { Animations } from "./animation";
import { Renderer } from "./renderer";
export interface DiffResult {
CONTINUOUS: SVModel[];
APPEND: SVModel[];
REMOVE: SVModel[];
FREED: SVNode[];
LEAKED: SVModel[];
UPDATE: SVModel[];
ACCUMULATE_LEAK: SVModel[];
}
export class Reconcile {
private engine: Engine;
private renderer: Renderer;
private isFirstPatch: boolean;
constructor(engine: Engine, renderer: Renderer) {
this.engine = engine;
this.renderer = renderer;
this.isFirstPatch = true;
}
/**
* models
* @param prevModelList
* @param modelList
*/
private getContinuousModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] {
const continuousModels = modelList.filter(item => prevModelList.find(prevModel => item.id === prevModel.id));
return continuousModels;
}
/**
*
* @param prevModelList
* @param modelList
* @param accumulateLeakModels
* @returns
*/
private getAppendModels(prevModelList: SVModel[], modelList: SVModel[], accumulateLeakModels: SVModel[]): SVModel[] {
const appendModels = modelList.filter(item => !prevModelList.find(model => model.id === item.id));
appendModels.forEach(item => {
let removeIndex = accumulateLeakModels.findIndex(leakModel => item.id === leakModel.id);
if(removeIndex > -1) {
accumulateLeakModels.splice(removeIndex, 1);
}
});
return appendModels;
}
/**
*
* @param prevModelList
* @param modelList
* @returns
*/
private getLeakModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] {
const potentialLeakModels: SVModel[] = prevModelList.filter(item =>
!modelList.find(model => model.id === item.id) && !item.freed
);
const leakModels: SVModel[] = [];
potentialLeakModels.forEach(item => {
if (item instanceof SVNode) {
item.leaked = true;
leakModels.push(item);
if (item.marker) {
item.marker.leaked = true;
leakModels.push(item.marker);
}
if(item.freedLabel) {
item.marker.leaked = true;
leakModels.push(item.freedLabel);
}
item.leakAddress.leaked = true;
leakModels.push(item.leakAddress);
}
});
potentialLeakModels.forEach(item => {
if (item instanceof SVLink && item.node.leaked !== false && item.target.leaked !== false) {
item.leaked = true;
leakModels.push(item);
}
});
leakModels.forEach(item => {
// 不能用上次的G6item了不然布局的时候会没有动画
item.G6Item = null;
});
return leakModels;
}
/**
*
* @param prevModelList
* @param modelList
*/
private getRemoveModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] {
let removedModels: SVModel[] = [];
for (let i = 0; i < prevModelList.length; i++) {
let prevModel = prevModelList[i],
target = modelList.find(item => item.id === prevModel.id);
if (target === undefined && !prevModel.leaked) {
removedModels.push(prevModel);
}
}
return removedModels;
}
/**
*
* @param prevModelList
* @param modelList
* @returns
*/
private getReTargetMarkers(prevModelList: SVModel[], modelList: SVModel[]): SVMarker[] {
const prevMarkers: SVMarker[] = prevModelList.filter(item => item instanceof SVMarker) as SVMarker[],
markers: SVMarker[] = modelList.filter(item => item instanceof SVMarker) as SVMarker[];
return markers.filter(item => prevMarkers.find(prevItem => {
return prevItem.id === item.id && prevItem.target.id !== item.target.id
}));
}
/**
* label model
* @param prevModelList
* @param modelList
* @returns
*/
private getLabelChangeModels(prevModelList: SVModel[], modelList: SVModel[]): SVModel[] {
let labelChangeModels: SVModel[] = [];
modelList.forEach(item => {
const prevItem = prevModelList.find(prevItem => prevItem.id === item.id);
if (prevItem === undefined) {
return;
}
const prevLabel = prevItem.get('label'),
label = item.get('label');
if (prevLabel !== label) {
labelChangeModels.push(item);
}
});
return labelChangeModels;
}
/**
* free
* @param prevModelList
* @param modelList
* @returns
*/
private getFreedModels(prevModelList: SVModel[], modelList: SVModel[]): SVNode[] {
const freedNodes = modelList.filter(item => item instanceof SVNode && item.freed) as SVNode[];
freedNodes.forEach(item => {
const prev = prevModelList.find(prevModel => item.id === prevModel.id);
if(prev) {
item.set('label', prev.get('label'));
}
});
return freedNodes;
}
// ------------------------------------------------------------------------------------------------
/**
* models
* @param continuousModels
*/
private handleContinuousModels(continuousModels: SVModel[]) {
for(let i = 0; i < continuousModels.length; i++) {
let model = continuousModels[i];
if(model instanceof SVNode) {
const group = model.G6Item.getContainer();
group.attr({ opacity: 1 });
}
}
}
/**
* models
* @param appendData
*/
private handleAppendModels(appendModels: SVModel[]) {
let { duration, timingFunction } = this.engine.animationOptions;
appendModels.forEach(item => {
if(item instanceof SVLeakAddress) {
const leakAddressG6Group = item.G6Item.getContainer();
leakAddressG6Group.attr({ opacity: 0 });
}
else {
Animations.APPEND(item.G6Item, {
duration,
timingFunction
});
}
});
}
/**
* models
* @param removeData
*/
private handleRemoveModels(removeModels: SVModel[]) {
let { duration, timingFunction } = this.engine.animationOptions;
removeModels.forEach(item => {
Animations.REMOVE(item.G6Item, {
duration,
timingFunction,
callback: () => {
this.renderer.removeModel(item);
}
});
});
}
/**
* models
* @param leakModels
*/
private handleLeakModels(leakModels: SVModel[]) {
let { duration, timingFunction } = this.engine.animationOptions;
leakModels.forEach(item => {
if(item instanceof SVLeakAddress) {
Animations.FADE_IN(item.G6Item, {
duration,
timingFunction
});
}
});
EventBus.emit('onLeak', leakModels);
}
/**
* models
* @param accumulateModels
*/
private handleAccumulateLeakModels(accumulateModels: SVModel[]) {
accumulateModels.forEach(item => {
if(item.generalStyle) {
item.set('style', { ...item.generalStyle });
}
});
}
/**
* models
* @param freedModes
*/
private handleFreedModels(freedModes: SVNode[]) {
const { duration, timingFunction } = this.engine.animationOptions,
alpha = 0.4;
freedModes.forEach(item => {
const nodeGroup = item.G6Item.getContainer();
item.set('style', { fill: '#ccc' });
nodeGroup.attr({ opacity: alpha });
if (item.marker) {
const markerGroup = item.marker.G6Item.getContainer();
item.marker.set('style', { fill: '#ccc' });
markerGroup.attr({ opacity: alpha + 0.5 });
}
item.freedLabel.G6Item.toFront();
Animations.FADE_IN(item.freedLabel.G6Item, { duration, timingFunction });
});
EventBus.emit('onFreed', freedModes);
}
/**
* models
* @param models
*/
private handleChangeModels(models: SVModel[]) {
const changeHighlightColor: string = this.engine.viewOptions.updateHighlight;
if (!changeHighlightColor || typeof changeHighlightColor !== 'string') {
return;
}
models.forEach(item => {
if(item.generalStyle === undefined) {
item.generalStyle = Util.objectClone(item.G6ModelProps.style);
}
if (item instanceof SVLink) {
item.set('style', {
stroke: changeHighlightColor
});
}
else {
item.set('style', {
fill: changeHighlightColor
});
}
});
}
/**
* diff
* @param prevLayoutGroupTable
* @param layoutGroupTable
* @param accumulateLeakModels
* @returns
*/
public diff(prevModelList: SVModel[], modelList: SVModel[], accumulateLeakModels: SVModel[]): DiffResult {
const continuousModels: SVModel[] = this.getContinuousModels(prevModelList, modelList);
const leakModels: SVModel[] = this.getLeakModels(prevModelList, modelList);
const appendModels: SVModel[] = this.getAppendModels(prevModelList, modelList, accumulateLeakModels);
const removeModels: SVModel[] = this.getRemoveModels(prevModelList, modelList);
const updateModels: SVModel[] = [
...this.getReTargetMarkers(prevModelList, modelList),
...this.getLabelChangeModels(prevModelList, modelList),
...appendModels,
...leakModels
];
const freedModels: SVNode[] = this.getFreedModels(prevModelList, modelList);
return {
CONTINUOUS: continuousModels,
APPEND: appendModels,
REMOVE: removeModels,
FREED: freedModels,
LEAKED: leakModels,
UPDATE: updateModels,
ACCUMULATE_LEAK: [...accumulateLeakModels]
};
}
/**
*
* @param diffResult
* @param isFirstRender
*/
public patch(diffResult: DiffResult) {
const {
APPEND,
REMOVE,
FREED,
LEAKED,
UPDATE,
CONTINUOUS,
ACCUMULATE_LEAK
} = diffResult;
// 第一次渲染的时候不高亮变化的元素
if (this.isFirstPatch === false) {
this.handleChangeModels(UPDATE);
}
this.handleContinuousModels(CONTINUOUS);
this.handleFreedModels(FREED);
this.handleAppendModels(APPEND);
this.handleLeakModels(LEAKED);
this.handleRemoveModels(REMOVE);
this.handleAccumulateLeakModels(ACCUMULATE_LEAK);
if(this.isFirstPatch) {
this.isFirstPatch = false;
}
}
}

View File

@ -1,16 +1,15 @@
import { Engine } from '../engine';
import { SVModel } from '../Model/SVModel';
import { G6EdgeModel, G6NodeModel } from '../Model/modelData';
import { Util } from '../Common/util';
import G6 from '@antv/g6';
import { InitViewBehaviors } from '../BehaviorHelper/initViewBehaviors';
import { Graph, GraphData, IGroup } from '@antv/g6-pc';
import { SV } from '../StructV';
import { Model } from './../Model/modelData';
export interface RenderModelPack {
leaKModels: SVModel[];
generalModel: SVModel[];
}
export interface G6Data {
nodes: G6NodeModel[];
edges: G6EdgeModel[];
};
export type g6Behavior = string | { type: string; shouldBegin?: Function; shouldUpdate?: Function; shouldEnd?: Function; };
@ -18,32 +17,24 @@ export type g6Behavior = string | { type: string; shouldBegin?: Function; should
export class Renderer {
private engine: Engine;
private g6Instance: Graph; // g6 实例
private shadowG6Instance: Graph;
constructor(engine: Engine, DOMContainer: HTMLElement) {
private DOMContainer: HTMLElement; // 主可视化视图容器
private g6Instance; // g6 实例
private prevRenderModelList: Model[] = null;
private isFirstRender: boolean; // 是否为第一次渲染
constructor(engine: Engine, DOMContainer: HTMLElement, g6Options: { [key: string]: any }) {
this.engine = engine;
this.DOMContainer = DOMContainer;
this.isFirstRender = true;
const enable: boolean = this.engine.animationOptions.enable,
duration: number = this.engine.animationOptions.duration,
timingFunction: string = this.engine.animationOptions.timingFunction;
const tooltip = new G6.Tooltip({
offsetX: 10,
offsetY: 20,
shouldBegin(event) {
return event.item['SVModel'].isNode();
},
getContent: event => this.getTooltipContent(event.item['SVModel'], { address: 'sourceId', data: 'data' }),
itemTypes: ['node']
});
this.shadowG6Instance = new G6.Graph({
container: DOMContainer.cloneNode() as HTMLElement
});
// 初始化g6实例
this.g6Instance = new G6.Graph({
this.g6Instance = new SV.G6.Graph({
container: DOMContainer,
width: DOMContainer.offsetWidth,
height: DOMContainer.offsetHeight,
@ -55,74 +46,68 @@ export class Renderer {
},
fitView: false,
modes: {
default: InitViewBehaviors(this.engine.optionsTable)
default: []
},
plugins: [tooltip]
...g6Options
});
}
/**
* tooltip元素
* @param model
* @param items
* @returns
*/
private getTooltipContent(model: SVModel, items: { [key: string]: string }): HTMLDivElement {
const wrapper = document.createElement('div');
if(model === null || model === undefined) {
return wrapper;
public getIsFirstRender(): boolean {
return this.isFirstRender;
}
Object.keys(items).map(key => {
let value = model[items[key]];
if (value !== undefined && value !== null) {
let item = document.createElement('div');
item.innerHTML = `${key}${value}`;
wrapper.appendChild(item);
}
});
return wrapper;
}
/**
* model Canvas G6 item
* @param renderModelList
*/
public build(renderModelList: SVModel[]) {
const g6Data: GraphData = Util.convertModelList2G6Data(renderModelList);
this.shadowG6Instance.clear();
this.shadowG6Instance.read(g6Data);
renderModelList.forEach(item => {
item.shadowG6Item = this.shadowG6Instance.findById(item.id);
});
}
/**
*
* @param renderModelList
* @param isFirstRender
*/
public render(renderModelList: SVModel[]) {
const renderData: GraphData = Util.convertModelList2G6Data(renderModelList);
this.g6Instance.changeData(renderData);
renderModelList.forEach(item => {
item.G6Item = this.g6Instance.findById(item.id);
item.G6Item['SVModel'] = item;
});
public setIsFirstRender(value: boolean) {
this.isFirstRender = value;
}
/**
* Model
* @param model
*/
public removeModel(model: SVModel) {
this.g6Instance.removeItem(model.G6Item);
this.shadowG6Instance.removeItem(model.shadowG6Item);
public removeModel(model: Model) {
this.g6Instance.removeItem(model.renderG6Item);
}
/**
*
* @param modelList
*/
public render(modelList: Model[], removeModels: Model[]) {
const renderModelList = [...modelList, ...removeModels],
renderData: G6Data = Util.convertModelList2G6Data(renderModelList);
if(this.isFirstRender) {
this.g6Instance.read(renderData);
}
else {
this.g6Instance.changeData(renderData);
}
if(this.engine.viewOptions.fitView) {
this.g6Instance.fitView();
}
modelList.forEach(item => {
item.renderG6Item = this.g6Instance.findById(item.id);
if(item.renderG6Item) {
item.G6Item = item.renderG6Item;
item.renderG6Item.SVModel = item;
}
});
// 将上一次的model全部标记为已销毁
if(this.prevRenderModelList) {
this.prevRenderModelList.forEach(item => { item.isDestroy = true; });
}
this.prevRenderModelList = renderModelList;
// 把所有连线置顶
if(this.isFirstRender) {
this.g6Instance.getEdges().forEach(item => item.toFront());
this.g6Instance.paint();
}
}
/**
@ -136,7 +121,7 @@ export class Renderer {
*
*/
public destroy() {
this.shadowG6Instance.destroy();
this.g6Instance.destroy();
this.DOMContainer = null;
}
}

View File

@ -1,184 +0,0 @@
import { Engine } from "../engine";
import { LayoutProvider } from "./layoutProvider";
import { LayoutGroupTable } from "../Model/modelConstructor";
import { Util } from "../Common/util";
import { SVModel } from "../Model/SVModel";
import { Renderer } from "./renderer";
import { Reconcile } from "./reconcile";
import { FixNodeMarkerDrag } from "../BehaviorHelper/fixNodeMarkerDrag";
import { InitDragCanvasWithLeak } from "../BehaviorHelper/dragCanavsWithLeak";
import { EventBus } from "../Common/eventBus";
export class ViewContainer {
private engine: Engine;
private layoutProvider: LayoutProvider;
private reconcile: Reconcile;
public renderer: Renderer;
private modelList: SVModel[];
private prevLayoutGroupTable: LayoutGroupTable;
private prevModelList: SVModel[];
private accumulateLeakModels: SVModel[];
public hasLeak: boolean;
public leakAreaY: number;
constructor(engine: Engine, DOMContainer: HTMLElement) {
this.engine = engine;
this.layoutProvider = new LayoutProvider(engine, this);
this.renderer = new Renderer(engine, DOMContainer);
this.reconcile = new Reconcile(engine, this.renderer);
this.prevLayoutGroupTable = new Map();
this.modelList = [];
this.prevModelList = [];
this.accumulateLeakModels = [];
this.hasLeak = false; // 判断是否已经发生过泄漏
const g6Instance = this.renderer.getG6Instance(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
height = this.getG6Instance().getHeight(),
{ drag, zoom } = this.engine.interactionOptions;
this.leakAreaY = height * (1- leakAreaHeight);
if(drag) {
InitDragCanvasWithLeak(this);
}
if(zoom) {
// InitZoomCanvas(g6Instance, g6GeneralGroup);
}
FixNodeMarkerDrag(g6Instance, this.engine.optionsTable);
}
// ----------------------------------------------------------------------------------------------
/**
*
*/
reLayout() {
this.layoutProvider.layoutAll(this.prevLayoutGroupTable, [], this.accumulateLeakModels);
}
/**
* g6
*/
getG6Instance() {
return this.renderer.getG6Instance();
}
/**
*
*/
refresh() {
this.renderer.getG6Instance().refresh();
}
/**
*
* @param width
* @param height
*/
resize(width: number, height: number) {
this.renderer.getG6Instance().changeSize(width, height);
}
/**
*
* @param models
* @param layoutFn
*/
render(layoutGroupTable: LayoutGroupTable) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable),
diffResult = this.reconcile.diff(this.prevModelList, modelList, this.accumulateLeakModels),
renderModelList = [
...modelList,
...diffResult.REMOVE,
...diffResult.LEAKED,
...diffResult.ACCUMULATE_LEAK
];
if(this.hasLeak === true && this.accumulateLeakModels.length === 0) {
this.hasLeak = false;
EventBus.emit('onLeakAreaUpdate', {
leakAreaY: this.leakAreaY,
hasLeak: this.hasLeak
});
}
if (diffResult.LEAKED.length) {
this.hasLeak = true;
EventBus.emit('onLeakAreaUpdate', {
leakAreaY: this.leakAreaY,
hasLeak: this.hasLeak
});
}
this.modelList = modelList;
this.renderer.build(renderModelList);
// 进行布局设置model的xy样式等
this.layoutProvider.layoutAll(layoutGroupTable, this.accumulateLeakModels, diffResult.LEAKED);
this.beforeRender();
this.renderer.render(renderModelList); // 渲染视图
this.reconcile.patch(diffResult); // 对视图上的某些变化进行对应的动作,比如:节点创建动画,节点消失动画等
this.afterRender();
this.accumulateLeakModels.push(...diffResult.LEAKED); // 对泄漏节点进行累积
this.prevLayoutGroupTable = layoutGroupTable;
this.prevModelList = modelList;
}
/**
*
*/
destroy() {
this.renderer.destroy();
}
// ------------------------------------------------------------------------------
/**
*
*/
private afterRender() {
const g6Instance = this.renderer.getG6Instance();
// 把所有连线置顶
g6Instance.getEdges().forEach(item => item.toFront());
g6Instance.paint();
this.prevModelList.forEach(item => {
if(item.leaked === false) {
item.discarded = true;
}
});
}
/**
*
*/
private beforeRender() {
}
}

291
src/View/viewManager.ts Normal file
View File

@ -0,0 +1,291 @@
import { Engine } from "../engine";
import { Element, Link, Marker, Model } from "../Model/modelData";
import { EngineOptions } from "../options";
import { Container } from "./container/container";
import { SV } from '../StructV';
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";
import { EventBus } from "../Common/eventBus";
export class ViewManager {
private engine: Engine;
private layouter: Layouter;
private mainContainer: Container;
private freedContainer: FreedContainer;
private leakContainer: LeakContainer;
private prevLayoutGroupTable: LayoutGroupTable;
private prevModelList: Model[];
private prevFreedElements: Element[];
private shadowG6Instance;
constructor(engine: Engine, DOMContainer: HTMLElement) {
this.engine = engine;
this.layouter = new Layouter(engine);
this.mainContainer = new MainContainer(engine, DOMContainer, { tooltip: true });
this.prevLayoutGroupTable = new Map();
this.prevFreedElements = [];
this.prevModelList = [];
const options: EngineOptions = this.engine.engineOptions;
if (options.freedContainer) {
this.freedContainer = new FreedContainer(engine, options.freedContainer, { fitCenter: true, tooltip: true });
}
if (options.leakContainer) {
this.leakContainer = new LeakContainer(engine, options.leakContainer, { fitCenter: true, tooltip: false });
}
this.shadowG6Instance = new SV.G6.Graph({
container: DOMContainer.cloneNode()
});
}
/**
* model Canvas G6 item
* @param constructList
*/
private build(modelList: Model[]) {
modelList.forEach(item => {
const type = item instanceof Link ? 'edge' : 'node';
this.shadowG6Instance.addItem(type, item.cloneProps());
item.shadowG6Item = this.shadowG6Instance.findById(item.id);
});
}
/**
*
* @param freedElement
* @param prevLayoutGroup
*/
private handleFreedLabel(freedElement: Element[], prevLayoutGroup: LayoutGroup) {
if (prevLayoutGroup === undefined) {
return;
}
const prevElementList: Element[] = prevLayoutGroup.element;
freedElement.map(item => {
let prevElement = prevElementList.find(el => el.id === item.id),
prevLabel = prevElement.get('label') ?? '';
item.set('label', prevLabel);
});
}
/**
* free
* @param layoutGroupTable
* @returns
*/
private getFreedModelList(prevLayoutGroupTable: LayoutGroupTable, layoutGroupTable: LayoutGroupTable): Model[] {
let freedElements: Element[] = [],
freedMarkers: Marker[] = [],
removeModels: Model[] = [],
freedGroupName: string;
layoutGroupTable.forEach((group, key) => {
let targetElements: Element[] = group.element.filter(item => item.freed);
targetElements.forEach(fItem => {
removeModels.push(
...Util.removeFromList(group.element, item => item.id === fItem.id),
...Util.removeFromList(group.link, item => item.element.id === fItem.id || item.target.id === fItem.id),
...Util.removeFromList(group.marker, item => item.target.id === fItem.id),
);
});
removeModels.map(model => {
Util.removeFromList(group.modelList, item => item.id === model.id);
});
// 找出最新的freed节点防止上一次的freed节点被遗留在该次渲染此时会出现大于一个freed节点
targetElements.forEach(item => {
if (this.prevFreedElements.find(prevEle => prevEle.id === item.id) === undefined) {
freedGroupName = key;
freedElements.push(item);
}
});
});
freedElements.map(item => {
const markers = Object.keys(item.markers).map(name => item.markers[name]);
freedMarkers.push(...markers);
});
this.handleFreedLabel(freedElements, prevLayoutGroupTable.get(freedGroupName));
this.prevFreedElements = freedElements;
const freedItems = [...freedElements, ...freedMarkers];
freedItems.forEach(item => {
item.set('style', {
fill: '#ccc'
});
});
return freedItems;
}
/**
*
* @param prevModelList
* @param modelList
* @returns
*/
private getLeakModelList(prevModelList: Model[], modelList: Model[]): Model[] {
const leakModelList: Model[] = prevModelList.filter(item => !modelList.find(n => n.id === item.id)),
elements: Element[] = <Element[]>leakModelList.filter(item => item instanceof Element && item.freed === false),
links: Link[] = <Link[]>leakModelList.filter(item => item instanceof Link),
elementIds: string[] = [],
res: Model[] = [];
elements.forEach(item => {
elementIds.push(item.id);
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--;
}
}
res.push(...elements, ...links);
res.map(item => {
item.isLeak = true;
});
return res;
}
// ----------------------------------------------------------------------------------------------
/**
*
* @param layoutGroupTable
*/
reLayout(layoutGroupTable: LayoutGroupTable) {
this.layouter.layoutAll(this.mainContainer, layoutGroupTable);
}
/**
* g6
*/
getG6Instance() {
return this.mainContainer.getG6Instance();
}
/**
*
*/
refresh() {
this.mainContainer.getG6Instance().refresh();
}
/**
*
* @param containerName
* @param width
* @param 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);
}
}
/**
*
* @param models
* @param layoutFn
*/
renderAll(layoutGroupTable: LayoutGroupTable) {
this.shadowG6Instance.clear();
let modelList = Util.convertGroupTable2ModelList(layoutGroupTable),
leakModelList = [],
freedModelList = [];
this.build(modelList);
if (this.leakContainer) {
leakModelList = this.getLeakModelList(this.prevModelList, modelList);
this.build(leakModelList);
}
// 进行布局设置model的xy
this.layouter.layoutAll(this.mainContainer, layoutGroupTable);
freedModelList = this.getFreedModelList(this.prevLayoutGroupTable, layoutGroupTable);
if (this.freedContainer && freedModelList.length) {
this.freedContainer.fitCenter(freedModelList);
this.freedContainer.render(freedModelList);
EventBus.emit('onFreed', freedModelList);
}
// 从新获取一次因为第一次获取没有把freed节点筛选出去
modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
this.mainContainer.render(modelList);
if (this.leakContainer && leakModelList.length) {
this.leakContainer.render(leakModelList);
EventBus.emit('onLeak', leakModelList);
}
this.prevLayoutGroupTable = layoutGroupTable;
this.prevModelList = modelList;
}
/**
*
*/
destroy() {
this.shadowG6Instance.destroy();
this.mainContainer.destroy();
this.freedContainer && this.freedContainer.destroy();
this.leakContainer && this.leakContainer.destroy();
}
}

View File

@ -1,18 +1,17 @@
import { Element, Link, Marker } from "./Model/modelData";
import { Sources } from "./sources";
import { ModelConstructor } from "./Model/modelConstructor";
import { LayoutGroupTable, ModelConstructor } from "./Model/modelConstructor";
import { AnimationOptions, EngineOptions, InteractionOptions, LayoutGroupOptions, ViewOptions } from "./options";
import { ViewManager } from "./View/viewManager";
import { SV } from "./StructV";
import { EventBus } from "./Common/eventBus";
import { ViewContainer } from "./View/viewContainer";
import { SVLink } from "./Model/SVLink";
import { SVNode } from "./Model/SVNode";
import { SVMarker } from "./Model/SVMarker";
export class Engine {
private modelConstructor: ModelConstructor = null;
private viewContainer: ViewContainer
private viewManager: ViewManager
private prevStringSourceData: string;
private layoutGroupTable: LayoutGroupTable;
public engineOptions: EngineOptions;
public viewOptions: ViewOptions;
@ -23,19 +22,21 @@ export class Engine {
constructor(DOMContainer: HTMLElement, engineOptions: EngineOptions) {
this.optionsTable = {};
this.engineOptions = Object.assign({}, engineOptions);
this.engineOptions = Object.assign({
freedContainer: null,
leakContainer: null
}, engineOptions);
this.viewOptions = Object.assign({
fitCenter: true,
fitView: false,
groupPadding: 20,
leakAreaHeight: 0.3,
updateHighlight: '#fc5185'
groupPadding: 20
}, engineOptions.view);
this.animationOptions = Object.assign({
enable: true,
duration: 750,
duration: 900,
timingFunction: 'easePolyOut'
}, engineOptions.animation);
@ -43,25 +44,26 @@ export class Engine {
drag: true,
zoom: true,
dragNode: true,
selectNode: true
selectNode: true,
changeHighlight: '#fc5185'
}, engineOptions.interaction);
// 初始化布局器配置项
Object.keys(SV.registeredLayout).forEach(layout => {
if(this.optionsTable[layout] === undefined) {
const options: LayoutGroupOptions = SV.registeredLayout[layout].defineOptions();
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[layout] = options;
this.optionsTable[layouter] = options;
}
});
this.modelConstructor = new ModelConstructor(this);
this.viewContainer = new ViewContainer(this, DOMContainer);
this.viewManager = new ViewManager(this, DOMContainer);
}
/**
@ -73,10 +75,6 @@ export class Engine {
return;
}
if(this.viewContainer.getG6Instance().isAnimating()) {
return;
}
let stringSourceData = JSON.stringify(sourceData);
if(this.prevStringSourceData === stringSourceData) {
return;
@ -84,70 +82,74 @@ export class Engine {
this.prevStringSourceData = stringSourceData;
// 1 转换模型data => model
const layoutGroupTable = this.modelConstructor.construct(sourceData);
this.layoutGroupTable = this.modelConstructor.construct(sourceData);
// 2 渲染使用g6进行渲染
this.viewContainer.render(layoutGroupTable);
this.viewManager.renderAll(this.layoutGroupTable);
}
/**
*
*/
public reLayout() {
this.viewContainer.reLayout();
const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
// layoutGroupTable.forEach(group => {
// group.modelList.forEach(item => {
// if(item instanceof SVLink) return;
this.viewManager.reLayout(layoutGroupTable);
// 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();
}
/**
* G6
*/
public getGraphInstance() {
return this.viewContainer.getG6Instance();
return this.viewManager.getG6Instance();
}
/**
* element
* @param group
*/
public getNodes(group?: string): SVNode[] {
public getElements(group?: string): Element[] {
const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
if(group && layoutGroupTable.has('group')) {
return layoutGroupTable.get('group').node;
return layoutGroupTable.get('group').element;
}
const nodes: SVNode[] = [];
const elements: Element[] = [];
layoutGroupTable.forEach(item => {
nodes.push(...item.node);
elements.push(...item.element);
})
return nodes;
return elements;
}
/**
* marker
* @param group
*/
public getMarkers(group?: string): SVMarker[] {
public getMarkers(group?: string): Marker[] {
const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
if(group && layoutGroupTable.has('group')) {
return layoutGroupTable.get('group').marker;
}
const markers: SVMarker[] = [];
const markers: Marker[] = [];
layoutGroupTable.forEach(item => {
markers.push(...item.marker);
})
@ -159,14 +161,14 @@ export class Engine {
* link
* @param group
*/
public getLinks(group?: string): SVLink[] {
public getLinks(group?: string): Link[] {
const layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
if(group && layoutGroupTable.has('group')) {
return layoutGroupTable.get('group').link;
}
const links: SVLink[] = [];
const links: Link[] = [];
layoutGroupTable.forEach(item => {
links.push(...item.link);
})
@ -180,11 +182,10 @@ export class Engine {
*/
public hideGroups(groupNames: string | string[]) {
const names = Array.isArray(groupNames)? groupNames: [groupNames],
instance = this.viewContainer.getG6Instance(),
layoutGroupTable = this.modelConstructor.getLayoutGroupTable();
instance = this.viewManager.getG6Instance();
layoutGroupTable.forEach(item => {
const hasName = names.find(name => name === item.layout);
this.layoutGroupTable.forEach(item => {
const hasName = names.find(name => name === item.layouterName);
if(hasName && !item.isHide) {
item.modelList.forEach(model => instance.hideItem(model.G6Item));
@ -203,7 +204,7 @@ export class Engine {
* @param id
*/
public findElement(id: string) {
const elements = this.getNodes();
const elements = this.getElements();
const stringId = id.toString();
const targetElement = elements.find(item => item.sourceId === stringId);
@ -212,11 +213,12 @@ export class Engine {
/**
*
* @param containerName
* @param width
* @param height
*/
public resize(width: number, height: number) {
this.viewContainer.resize(width, height);
public resize(containerName: string, width: number, height: number) {
this.viewManager.resize(containerName, width, height);
}
/**
@ -234,13 +236,8 @@ export class Engine {
return;
}
if(eventName === 'onLeakAreaUpdate') {
EventBus.on(eventName, callback);
return;
}
this.viewContainer.getG6Instance().on(eventName, event => {
callback(event.item['SVModel']);
this.viewManager.getG6Instance().on(eventName, event => {
callback(event.item.SVModel);
});
}
@ -249,6 +246,6 @@ export class Engine {
*/
public destroy() {
this.modelConstructor.destroy();
this.viewContainer.destroy();
this.viewManager.destroy();
}
};

View File

@ -1,5 +1,5 @@
import { SVNode } from "./Model/SVNode";
import { SourceNode } from "./sources";
import { Element } from "./Model/modelData";
import { SourceElement } from "./sources";
export interface Style {
@ -15,14 +15,14 @@ export interface Style {
};
export interface NodeLabelOption {
export interface ElementLabelOption {
position: string;
offset: number;
style: Style;
};
export interface NodeIndexOption extends NodeLabelOption {
export interface ElementIndexOption extends ElementLabelOption {
position: 'top' | 'right' | 'bottom' | 'left';
value: string;
style: Style;
@ -38,32 +38,31 @@ export interface LinkLabelOption {
};
export interface ModelOption {
export interface ElementOption {
type: string;
size: number | [number, number];
rotation: number;
anchorPoints: [number, number];
label: string | string[];
labelOptions: ElementLabelOption;
indexOptions: ElementIndexOption;
style: Style;
}
export interface NodeOption extends ModelOption {
size: number | [number, number];
rotation: number;
label: string | string[];
anchorPoints: number[][];
indexOptions: NodeIndexOption;
labelOptions: NodeLabelOption;
}
export interface LinkOption extends ModelOption {
export interface LinkOption {
type: string;
sourceAnchor: number | ((index: number) => number);
targetAnchor: number | ((index: number) => number);
label: string;
curveOffset: number;
labelOptions: LinkLabelOption;
style: Style;
}
export interface MarkerOption extends NodeOption {
export interface MarkerOption extends ElementOption {
type: 'pointer' | 'cursor' | 'clen-queue-pointer';
anchor: number;
offset: number;
@ -84,7 +83,7 @@ export interface BehaviorOptions {
export interface LayoutGroupOptions {
node: { [key: string]: NodeOption };
element: { [key: string]: ElementOption };
link?: { [key: string]: LinkOption }
marker?: { [key: string]: MarkerOption }
layout?: LayoutOptions;
@ -100,9 +99,8 @@ export interface LayoutGroupOptions {
export interface ViewOptions {
fitCenter: boolean;
fitView: boolean;
groupPadding: number;
updateHighlight: string;
leakAreaHeight: number;
}
@ -114,22 +112,25 @@ export interface AnimationOptions {
export interface InteractionOptions {
changeHighlight: string;
drag: boolean;
zoom: boolean;
}
export interface EngineOptions {
freedContainer?: HTMLElement;
leakContainer?: HTMLElement;
view?: ViewOptions;
animation?: AnimationOptions;
interaction?: InteractionOptions;
};
export interface LayoutCreator {
export interface Layouter {
defineOptions(): LayoutGroupOptions;
sourcesPreprocess?(sources: SourceNode[], options: LayoutGroupOptions): SourceNode[];
defineLeakRule?(nodes: SVNode[]): SVNode[];
layout(nodes: SVNode[], layoutOptions: LayoutOptions);
sourcesPreprocess?(sources: SourceElement[], options: LayoutGroupOptions): SourceElement[];
defineLeakRule?(elements: Element[]): Element[];
layout(elements: Element[], layoutOptions: LayoutOptions);
[key: string]: any;
}

View File

@ -6,17 +6,17 @@ export type LinkTarget = number | string;
export type sourceLinkData = LinkTarget | LinkTarget[];
// 结点指针声明
export type sourceMarkerData = string | string[];
export type sourcePointerData = string | string[];
// 源数据单元
export interface SourceNode {
export interface SourceElement {
id: string | number;
[key: string]: any | sourceLinkData | sourceMarkerData;
[key: string]: any | sourceLinkData | sourcePointerData;
}
export type Sources = {
[key: string]: { data: SourceNode[]; layouter: string; }
[key: string]: { data: SourceElement[]; layouter: string; }
};

3334
yarn.lock

File diff suppressed because it is too large Load Diff