Merge pull request #1029 from antvis/g6-dev-20191209

feat: update types file
This commit is contained in:
Moyee 2019-12-11 10:09:19 +08:00 committed by GitHub
commit ccdeb3ae84
24 changed files with 1504 additions and 128 deletions

20
jest.config.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
runner: 'jest-electron/runner',
testEnvironment: 'jest-electron/environment',
preset: 'ts-jest',
collectCoverage: false,
collectCoverageFrom: [
'src/**/*.{ts,js}',
'!**/node_modules/**',
'!**/vendor/**'
],
testRegex: '/tests/.*-spec\\.ts?$',
moduleDirectories: [ 'node_modules', 'src' ],
moduleFileExtensions: [ 'js', 'ts', 'json' ],
moduleNameMapper: {
'@g6/(.*)': '<rootDir>/src/$1',
'@g6/types': '<rootDir>/types'
}
};

View File

@ -26,7 +26,7 @@
"clean": "rimraf esm lib dist",
"lint": "lint-staged",
"test": "jest",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/behavior/index-spec.ts",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/graph/controller/mode-spec.ts",
"coverage": "jest --coverage",
"ci": "run-s build coverage",
"doc": "rimraf apis && typedoc",
@ -81,18 +81,6 @@
"git add"
]
},
"jest": {
"runner": "jest-electron/runner",
"testEnvironment": "jest-electron/environment",
"preset": "ts-jest",
"collectCoverage": false,
"collectCoverageFrom": [
"src/**/*.{ts,js}",
"!**/node_modules/**",
"!**/vendor/**"
],
"testRegex": "/tests/.*-spec\\.ts?$"
},
"repository": {
"type": "git",
"url": "https://github.com/antvis/g6"

View File

@ -1,10 +1,10 @@
import clone from '@antv/util/lib/clone'
import { IBehaviorOpation } from '../../types';
import { BehaviorOpation } from '@g6/types';
import BehaviorOption from './behaviorOption'
export default class Behavior {
private static types = {}
public static registerBehavior<T, U>(type: string, behavior: IBehaviorOpation<U>) {
public static registerBehavior<T, U>(type: string, behavior: BehaviorOpation<U>) {
if(!behavior) {
throw new Error(`please specify handler for this behavior: ${type}`)
}

View File

@ -1,7 +1,7 @@
import each from '@antv/util/lib/each'
import wrapBehavior from '@antv/util/lib/wrap-behavior'
import { G6Event } from '../../types';
import { IGraph } from '../interface/graph'
import { IGraph } from '@g6/interface/graph'
import { G6Event } from '@g6/types';
export default class BehaviorOption {
private _events = null

View File

@ -1,5 +1,5 @@
import { G6Event, IG6GraphEvent } from "../../types";
import { cloneEvent } from '../util/base'
import { G6Event, IG6GraphEvent } from "@g6/types";
import { cloneEvent } from '@g6/util/base'
const abs = Math.abs
const DRAG_OFFSET = 10
const ALLOW_EVENTS = [ 16, 17, 18 ]

View File

@ -0,0 +1 @@
export { default as Mode } from './mode'

View File

@ -0,0 +1,175 @@
import each from '@antv/util/lib/each'
import isArray from '@antv/util/lib/is-array'
import isString from '@antv/util/lib/is-string'
import Behavior from '@g6/behavior/behavior'
import { IBehavior } from '@g6/interface/behavior';
import { IGraph, IMode, IModeType } from '@g6/interface/graph';
export default class Mode {
private graph: IGraph
/**
* modes = {
* default: [ 'drag-node', 'zoom-canvas' ],
* edit: [ 'drag-canvas', {
* type: 'brush-select',
* trigger: 'ctrl'
* }]
* }
*
* @private
* @type {IMode}
* @memberof Mode
*/
public modes: IMode
/**
* mode = 'drag-node'
*
* @private
* @type {string}
* @memberof Mode
*/
public mode: string
private currentBehaves: IBehavior[]
constructor(graph: IGraph) {
this.graph = graph
this.modes = graph.get('modes') || {
default: []
}
this.formatModes()
this.mode = graph.get('defaultMode') || 'default'
this.currentBehaves = []
this.setMode(this.mode)
}
private formatModes() {
const modes = this.modes;
each(modes, mode => {
each(mode, (behavior, i) => {
if (isString(behavior)) {
mode[i] = { type: behavior };
}
});
});
}
private setBehaviors(mode: string) {
const graph = this.graph;
const behaviors = this.modes[mode];
const behaves: IBehavior[] = [];
let behave: IBehavior;
each(behaviors, behavior => {
const BehaviorInstance = Behavior.getBehavior(behavior.type)
if (!BehaviorInstance) {
return;
}
behave = new BehaviorInstance(behavior);
if(behave) {
behave.bind(graph)
behaves.push(behave);
}
});
this.currentBehaves = behaves;
}
private mergeBehaviors(modeBehaviors: IModeType[], behaviors: IModeType[]): IModeType[] {
each(behaviors, behavior => {
if (modeBehaviors.indexOf(behavior) < 0) {
if (isString(behavior)) {
behavior = { type: behavior };
}
modeBehaviors.push(behavior);
}
});
return modeBehaviors;
}
private filterBehaviors(modeBehaviors: IModeType[], behaviors: IModeType[]): IModeType[] {
const result: IModeType[] = [];
modeBehaviors.forEach(behavior => {
let type: string = ''
if(isString(behavior)) {
type = behavior
} else {
type = behavior.type
}
if (behaviors.indexOf(type) < 0) {
result.push(behavior);
}
});
return result;
}
public setMode(mode: string): Mode {
const modes = this.modes;
const graph = this.graph;
const behaviors = modes[mode];
if (!behaviors) {
return;
}
graph.emit('beforemodechange', { mode });
each(this.currentBehaves, behave => {
behave.unbind(graph);
});
this.setBehaviors(mode);
graph.emit('aftermodechange', { mode });
this.mode = mode;
return this;
}
/**
* Behavior
*
* @param {IModeType[]} behaviors
* @param {(IModeType[] | IModeType)} modes
* @param {boolean} isAdd
* @returns {Mode}
* @memberof Mode
*/
public manipulateBehaviors(behaviors: IModeType[], modes: IModeType[] | IModeType, isAdd: boolean): Mode {
const self = this
if(!isArray(behaviors)) {
behaviors = [ behaviors ]
}
if(isArray(modes)) {
each(modes, mode => {
if (!self.modes[mode]) {
if (isAdd) {
self.modes[mode] = [].concat(behaviors);
}
} else {
if (isAdd) {
self.modes[mode] = this.mergeBehaviors(self.modes[mode], behaviors);
} else {
self.modes[mode] = this.filterBehaviors(self.modes[mode], behaviors);
}
}
})
return this
}
let currentMode: string = ''
if(!modes) {
currentMode = this.mode
}
if (isAdd) {
self.modes[currentMode] = this.mergeBehaviors(self.modes[currentMode], behaviors);
} else {
self.modes[currentMode] = this.filterBehaviors(self.modes[currentMode], behaviors);
}
self.setMode(this.mode)
return this
}
}

13
src/graph/graph.ts Normal file
View File

@ -0,0 +1,13 @@
import EventEmitter from '@antv/event-emitter'
import { GraphOptions, IGraph } from '@g6/interface/graph';
export default class Graph extends EventEmitter implements IGraph {
private _cfg: GraphOptions
constructor(cfg: GraphOptions) {
super()
this._cfg = cfg
}
public get(key: string) {
return this._cfg[key]
}
}

0
src/graph/tree-graph.ts Normal file
View File

View File

@ -1,9 +1,19 @@
import GraphEvent from '@antv/g-base/lib/event/graph-event';
import { IG6GraphEvent } from "../../types";
import { DefaultBehaviorType, G6Event, IG6GraphEvent } from '@g6/types';
import { IGraph } from './graph';
import { IItem } from './item';
export interface IBehavior {
constructor: (cfg?: object) => void;
getEvents: () => { [key in G6Event]?: string };
shouldBegin: () => boolean;
shouldUpdate: () => boolean;
shouldEnd: () => boolean;
bind: (graph: IGraph) => void;
unbind: (graph: IGraph) => void;
[key: string]: (...args: DefaultBehaviorType[]) => unknown;
}
export class G6GraphEvent extends GraphEvent implements IG6GraphEvent {
public item: IItem

View File

@ -1,7 +1,147 @@
import { G6Event } from '../../types'
import EventEmitter from '@antv/event-emitter';
import { Easeing, ModelStyle, ShapeStyle } from '@g6/types'
export interface IGraph {
on: (event: G6Event, handler: () => void) => void;
off: (event: G6Event, handler: () => void) => void;
export interface IModeOption {
type: string;
}
export type IModeType = string | IModeOption
export interface IMode {
default: IModeType[]
[key: string]: IModeType[]
}
export interface ILayoutOptions {
type: string;
}
export interface GraphOptions {
/**
* DOM DOM id HTML
*/
container: string | HTMLElement;
/**
* 'px'
*/
width: number;
/**
* 'px'
*/
height: number;
/**
* canvas和svg
*/
renderer?: 'canvas' | 'svg';
fitView?: boolean;
layout?: ILayoutOptions;
/**
*
* , fitViewPadding: 20
* fitViewPadding: [20, 40, 50,20]
*
*/
fitViewPadding?: number[] | number;
/**
* false
* true
*/
groupByTypes?: boolean;
groupStyle?: {
style: {
[key: string]: ShapeStyle
};
};
/**
* setAutoPaint()
* true
*/
autoPaint?: boolean;
/**
* G6中的Mode文档
*/
modes?: IMode;
/**
* shape, size, color data
*/
defaultNode?: {
shape?: string,
size?: string,
color?: string,
} & ModelStyle;
/**
* shape, size, color data
*/
defaultEdge?: {
shape?: string,
size?: string,
color?: string,
} & ModelStyle;
nodeStateStyles?: ModelStyle;
edgeStateStyles?: ModelStyle;
/**
* graph plugin
*/
plugins?: any[];
/**
*
*/
animate?: boolean;
/**
* animate为true时有效
*/
animateCfg?: {
/**
*
*/
onFrame?: () => unknown;
/**
*
*/
duration?: number;
/**
*
* easeLinear
*/
easing?: Easeing;
};
/**
*
* 0.2
*/
minZoom?: number;
/**
*
* 10
*/
maxZoom?: number;
/**
*
* 1.0
*/
pixelRatio?: number;
groupType?: string;
/**
* Edge
*/
linkCenter?: boolean;
}
export interface IGraph extends EventEmitter {
get<T>(key: string): T;
}

View File

@ -1,10 +1,8 @@
import Group from "@antv/g-canvas/lib/group";
import ShapeBase from "@antv/g-canvas/lib/shape/base";
import { BBox } from "@antv/g-canvas/lib/types";
import { IModelConfig, INodeConfig, IPoint, IShapeStyle } from '../../types'
import { IBBox, IPoint, IShapeBase, ModelConfig, NodeConfig, ShapeStyle } from '@g6/types'
// item 的配置项
interface IItemConfig {
export type IItemConfig = Partial<{
/**
* id
*/
@ -18,7 +16,7 @@ interface IItemConfig {
/**
* data model
*/
model: IModelConfig;
model: ModelConfig;
/**
* G Group
@ -46,23 +44,23 @@ interface IItemConfig {
/**
* key shape to calculate item's bbox
*/
keyShape: ShapeBase,
keyShape: IShapeBase,
/**
* item's states, such as selected or active
* @type Array
*/
states: string[];
}
}>
export interface IItem {
_cfg: IItemConfig;
destroyed: boolean;
isItem(): boolean;
getDefaultCfg(): IItemConfig;
getKeyShapeStyle(): IShapeStyle;
getKeyShapeStyle(): ShapeStyle;
/**
*
@ -77,11 +75,11 @@ export interface IItem {
*/
hasState(state: string): boolean;
getStateStyle(state: string): IShapeStyle;
getStateStyle(state: string): ShapeStyle;
getOriginStyle(): IShapeStyle;
getOriginStyle(): ShapeStyle;
getCurrentStatesStyle(): IShapeStyle;
getCurrentStatesStyle(): ShapeStyle;
/**
* visible
@ -103,13 +101,13 @@ export interface IItem {
* 线
* @return {G.Shape}
*/
getKeyShape(): ShapeBase;
getKeyShape(): IShapeBase;
/**
*
* @return {Object}
*/
getModel(): IModelConfig;
getModel(): ModelConfig;
/**
*
@ -117,7 +115,7 @@ export interface IItem {
*/
getType(): string;
getShapeCfg(model: IModelConfig): IModelConfig;
getShapeCfg(model: ModelConfig): ModelConfig;
/**
*
@ -133,7 +131,7 @@ export interface IItem {
* @internal Graph 使 graph.update
* @param {Object} cfg
*/
update(cfg: IModelConfig): void;
update(cfg: ModelConfig): void;
/**
*
@ -144,12 +142,7 @@ export interface IItem {
*
* @param {object} cfg
*/
updatePosition(cfg: INodeConfig): void;
/**
* / cache
*/
clearCache(): void;
updatePosition(cfg: NodeConfig): void;
/**
*
@ -159,7 +152,7 @@ export interface IItem {
/**
*
*/
getBBox(): BBox;
getBBox(): IBBox;
/**
*
@ -195,6 +188,8 @@ export interface IItem {
isVisible(): boolean;
isOnlyMove(cfg: ModelConfig): boolean;
get<T>(key: string): T;
set<T>(key: string, value: T): void;
}
@ -252,11 +247,15 @@ export interface INode extends IItem {
*/
removeEdge(edge: IEdge): void;
clearCache(): void;
/**
*
* @return {array} anchorPoints {x,y,...cfg}
*/
getAnchorPoints(): IPoint[];
hasLocked(): boolean;
lock(): void;
unlock(): void;
}

217
src/item/edge.ts Normal file
View File

@ -0,0 +1,217 @@
import isNil from '@antv/util/lib/is-nil';
import isPlainObject from '@antv/util/lib/is-plain-object'
import { IEdge, INode } from "@g6/interface/item";
import { EdgeConfig, IPoint, NodeConfig, SourceTarget } from '@g6/types';
import Item from "./item";
const END_MAP = { source: 'start', target: 'end' };
const ITEM_NAME_SUFFIX = 'Node'; // 端点的后缀,如 sourceNode, targetNode
const POINT_NAME_SUFFIX = 'Point'; // 起点或者结束点的后缀,如 startPoint, endPoint
const ANCHOR_NAME_SUFFIX = 'Anchor';
export default class Edge extends Item implements IEdge {
protected getDefaultCfg() {
return {
type: 'edge',
sourceNode: null,
targetNode: null,
startPoint: null,
endPoint: null,
linkCenter: false
}
}
private setEnd(name: string, value: INode) {
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
const itemName = name + ITEM_NAME_SUFFIX;
const preItem = this.get(itemName);
if(preItem) {
// 如果之前存在节点,则移除掉边
preItem.removeEdge(this)
}
if (isPlainObject(value)) { // 如果设置成具体的点,则清理节点
this.set(pointName, value);
this.set(itemName, null);
} else {
value!.addEdge(this);
this.set(itemName, value);
this.set(pointName, null);
}
}
/**
*
* @param name source | target
* @param model
* @param controlPoints
*/
private getLinkPoint(name: SourceTarget, model: EdgeConfig, controlPoints: IPoint[]): IPoint {
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
const itemName = name + ITEM_NAME_SUFFIX;
let point = this.get(pointName);
if (!point) {
const item = this.get(itemName);
const anchorName = name + ANCHOR_NAME_SUFFIX;
const prePoint = this.getPrePoint(name, controlPoints);
const anchorIndex = model[anchorName];
if (isNil(anchorIndex)) { // 如果有锚点,则使用锚点索引获取连接点
point = item.getLinkPointByAnchor(anchorIndex);
}
// 如果锚点没有对应的点或者没有锚点,则直接计算连接点
point = point || item.getLinkPoint(prePoint);
if (isNil(point.index)) {
this.set(name + 'AnchorIndex', point.index);
}
}
return point;
}
/**
*
* @param name
* @param controlPoints
*/
private getPrePoint(name: SourceTarget, controlPoints: IPoint[]): NodeConfig | IPoint {
if (controlPoints && controlPoints.length) {
const index = name === 'source' ? 0 : controlPoints.length - 1;
return controlPoints[index];
}
const oppositeName = name === 'source' ? 'target' : 'source'; // 取另一个节点的位置
return this.getEndPoint(oppositeName);
}
/**
*
* @param name
*/
private getEndPoint(name: string): NodeConfig | IPoint {
const itemName = name + ITEM_NAME_SUFFIX;
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
const item = this.get(itemName);
// 如果有端点,直接使用 model
if (item) {
return item.get('model');
} // 否则直接使用点
return this.get(pointName);
}
/**
*
* @param model
*/
private getControlPointsByCenter(model) {
const sourcePoint = this.getEndPoint('source');
const targetPoint = this.getEndPoint('target');
const shapeFactory = this.get('shapeFactory');
return shapeFactory.getControlPoints(model.shape, {
startPoint: sourcePoint,
endPoint: targetPoint
});
}
private getEndCenter(name: string): IPoint {
const itemName = name + ITEM_NAME_SUFFIX;
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
const item = this.get(itemName);
// 如果有端点,直接使用 model
if (item) {
const bbox = item.getBBox();
return {
x: bbox.centerX,
y: bbox.centerY
};
} // 否则直接使用点
return this.get(pointName);
}
protected init() {
super.init()
}
public getShapeCfg(model: EdgeConfig): EdgeConfig {
const self = this;
const linkCenter: boolean = self.get('linkCenter'); // 如果连接到中心,忽视锚点、忽视控制点
const cfg: any = super.getShapeCfg(model);
if (linkCenter) {
cfg.startPoint = self.getEndCenter('source');
cfg.endPoint = self.getEndCenter('target');
} else {
const controlPoints = cfg.controlPoints || self.getControlPointsByCenter(cfg);
cfg.startPoint = self.getLinkPoint('source', model, controlPoints);
cfg.endPoint = self.getLinkPoint('target', model, controlPoints);
}
cfg.sourceNode = self.get('sourceNode');
cfg.targetNode = self.get('targetNode');
return cfg;
}
/**
*
*/
public getModel(): EdgeConfig {
const model: EdgeConfig = this.get('model');
const out = Object.assign({}, model);
const sourceItem = this.get('source' + ITEM_NAME_SUFFIX);
const targetItem = this.get('target' + ITEM_NAME_SUFFIX);
if (sourceItem) {
out.source = sourceItem.get('id');
delete out['source' + ITEM_NAME_SUFFIX];
} else {
out.source = this.get('start' + POINT_NAME_SUFFIX);
}
if (targetItem) {
out.target = targetItem.get('id');
delete out['target' + ITEM_NAME_SUFFIX];
} else {
out.target = this.get('end' + POINT_NAME_SUFFIX);
}
return out;
}
public setSource(source: INode) {
this.setEnd('source', source)
this.set('source', source)
}
public setTarget(target: INode) {
this.setEnd('target', target)
this.set('target', target)
}
public getSource(): INode {
return this.get('source')
}
public getTarget(): INode {
return this.get('target')
}
public updatePosition() {}
/**
* path
* @param {object} cfg
*/
public update(cfg: EdgeConfig) {
const model: EdgeConfig = this.get('model');
Object.assign(model, cfg);
this.updateShape();
this.afterUpdate();
this.clearCache();
}
public destroy() {
const sourceItem: INode = this.get('source' + ITEM_NAME_SUFFIX);
const targetItem: INode = this.get('target' + ITEM_NAME_SUFFIX);
if(sourceItem && !sourceItem.destroyed) {
sourceItem.removeEdge(this)
}
if(targetItem && !targetItem.destroyed) {
targetItem.removeEdge(this);
}
super.destroy();
}
}

568
src/item/item.ts Normal file
View File

@ -0,0 +1,568 @@
import Group from '@antv/g-canvas/lib/group';
import each from '@antv/util/lib/each'
import isNil from '@antv/util/lib/is-nil';
import isPlainObject from '@antv/util/lib/is-plain-object'
import isString from '@antv/util/lib/is-string'
import uniqueId from '@antv/util/lib/unique-id'
import { IItem, IItemConfig } from "@g6/interface/item";
import { IBBox, IPoint, IShapeBase, ModelConfig, ModelStyle } from '@g6/types';
import { getBBox } from '@g6/util/graphic';
import { translate } from '@g6/util/math';
const CACHE_BBOX = 'bboxCache';
const RESERVED_STYLES = [ 'fillStyle', 'strokeStyle',
'path', 'points', 'img', 'symbol' ];
export default class Item implements IItem {
public _cfg: IItemConfig = {}
private defaultCfg: IItemConfig = {
/**
* id
* @type {string}
*/
id: null,
/**
*
* @type {string}
*/
type: 'item',
/**
* data model
* @type {object}
*/
model: {} as ModelConfig,
/**
* g group
* @type {G.Group}
*/
group: null,
/**
* is open animate
* @type {boolean}
*/
animate: false,
/**
* visible - not group visible
* @type {boolean}
*/
visible: true,
/**
* locked - lock node
* @type {boolean}
*/
locked: false,
/**
* capture event
* @type {boolean}
*/
event: true,
/**
* key shape to calculate item's bbox
* @type object
*/
keyShape: null,
/**
* item's states, such as selected or active
* @type Array
*/
states: []
};
public destroyed: boolean = false
constructor(cfg: IItemConfig) {
this._cfg = Object.assign(this.defaultCfg, this.getDefaultCfg(), cfg)
const group = cfg.group
group.set('item', this)
let id = this.get('model').id
if(!id) {
id = uniqueId(this.get('type'))
}
this.set('id', id)
group.set('id', id)
this.init()
this.draw()
}
/**
* keyshape
*/
private calculateBBox(): IBBox {
const keyShape: IShapeBase = this.get('keyShape');
const group: Group = this.get('group');
// 因为 group 可能会移动,所以必须通过父元素计算才能计算出正确的包围盒
const bbox = getBBox(keyShape, group);
bbox.x = bbox.minX;
bbox.y = bbox.minY;
bbox.width = bbox.maxX - bbox.minX;
bbox.height = bbox.maxY - bbox.minY;
bbox.centerX = (bbox.minX + bbox.maxX) / 2;
bbox.centerY = (bbox.minY + bbox.maxY) / 2;
return bbox;
}
/**
* draw shape
*/
private drawInner() {
const self = this;
const shapeFactory = self.get('shapeFactory');
const group: Group = self.get('group');
const model: ModelConfig = self.get('model');
group.clear();
if (!shapeFactory) {
return;
}
self.updatePosition(model);
const cfg = self.getShapeCfg(model); // 可能会附加额外信息
const shapeType: string = cfg.shape;
const keyShape: IShapeBase = shapeFactory.draw(shapeType, cfg, group);
if (keyShape) {
keyShape.isKeyShape = true;
self.set('keyShape', keyShape);
self.set('originStyle', this.getKeyShapeStyle());
}
// 防止由于用户外部修改 model 中的 shape 导致 shape 不更新
this.set('currentShape', shapeType);
this.resetStates(shapeFactory, shapeType);
}
/**
* reset shape states
* @param shapeFactory
* @param shapeType
*/
private resetStates(shapeFactory, shapeType: string) {
const self = this;
const states: string[] = self.get('states');
each(states, state => {
shapeFactory.setState(shapeType, state, true, self);
});
}
protected init() {
// TODO 实例化工厂方法,需要等 Shape 重构完成
// const shapeFactory = Shape.getFactory(this.get('type'));
// this.set('shapeFactory', shapeFactory);
}
/**
*
* @internal 使
* @param {String} key
* @return {object | string | number}
*/
public get(key: string) {
return this._cfg[key]
}
/**
*
* @internal 使
* @param {String|Object} key
* @param {object | string | number} val
*/
public set(key: string, val): void {
if(isPlainObject(key)) {
this._cfg = Object.assign({}, this._cfg, key)
} else {
this._cfg[key] = val
}
}
protected getDefaultCfg() {
return {}
}
/**
* / cache
*/
protected clearCache() {
this.set(CACHE_BBOX, null);
}
/**
*
*/
protected beforeDraw() {
}
/**
*
*/
protected afterDraw() {
}
/**
*
*/
protected afterUpdate() {
}
/**
* draw shape
*/
public draw() {
this.beforeDraw()
this.drawInner()
this.afterDraw()
}
public getKeyShapeStyle(): ModelStyle {
const keyShape = this.getKeyShape();
if (keyShape) {
const styles: ModelStyle = {};
each(keyShape.attr(), (val, key) => {
if (RESERVED_STYLES.indexOf(key) < 0) {
styles[key] = val;
}
});
return styles;
}
}
// TODO 确定还是否需要该方法
public getShapeCfg(model: ModelConfig): ModelConfig {
const styles = this.get('styles');
if (styles && styles.default) {
// merge graph的item样式与数据模型中的样式
const newModel = Object.assign({}, model);
newModel.style = Object.assign({}, styles.default, model.style);
return newModel;
}
return model;
}
/**
*
* @param state
*/
public getStateStyle(state: string) {
const type: string = this.getType()
const styles = this.get(`${type}StateStyles`);
const stateStyle = styles && styles[state];
return stateStyle;
}
/**
* get keyshape style
*/
public getOriginStyle(): ModelStyle {
return this.get('originStyle');
}
public getCurrentStatesStyle(): ModelStyle {
const self = this;
const originStyle = self.getOriginStyle();
each(self.getStates(), state => {
Object.assign(originStyle, self.getStateStyle(state));
});
return originStyle;
}
/**
* visible
* @internal graph 使
* @param {String} state
* @param {Boolean} enable
*/
public setState(state: string, enable: boolean) {
const states: string[] = this.get('states');
const shapeFactory = this.get('shapeFactory');
const index = states.indexOf(state);
if (enable) {
if (index > -1) {
return;
}
states.push(state);
} else if (index > -1) {
states.splice(index, 1);
}
if (shapeFactory) {
const model: ModelConfig = this.get('model');
shapeFactory.setState(model.shape, state, enable, this);
}
}
// TODO
public clearStates(states: string[]) {
const self = this;
const originStates = self.getStates();
const shapeFactory = self.get('shapeFactory');
const shape: string = self.get('model').shape;
if (!states) {
self.set('states', []);
shapeFactory.setState(shape, originStates[0], false, self);
return;
}
if (isString(states)) {
states = [ states ];
}
const newStates = originStates.filter(state => {
shapeFactory.setState(shape, state, false, self);
if (states.indexOf(state) >= 0) {
return false;
}
return true;
});
self.set('states', newStates);
}
/**
*
* @return {G.Group}
*/
public getContainer(): Group {
return this.get('group');
}
/**
* 线
* @return {G.Shape}
*/
public getKeyShape(): IShapeBase {
return this.get('keyShape');
}
/**
*
* @return {Object}
*/
public getModel(): ModelConfig {
return this.get('model');
}
/**
*
* @return {string}
*/
public getType(): string {
return this.get('type');
}
/**
* Item
*/
public isItem(): boolean {
return true
}
/**
*
* @return {Array}
*/
public getStates(): string[] {
return this.get('states');
}
/**
*
* @param {String} state
* @return {Boolean}
*/
public hasState(state: string): boolean {
const states = this.getStates()
return states.indexOf(state) >= 0;
}
/**
*
* 1. item model
* 2.
*
* shape
*/
public refresh() {
const model: ModelConfig = this.get('model');
// 更新元素位置
this.updatePosition(model);
// 更新元素内容,样式
this.updateShape();
// 做一些更新之后的操作
this.afterUpdate();
// 清除缓存
this.clearCache();
}
public isOnlyMove(cfg?: ModelConfig): boolean {
return false
}
/**
* model
* @internal Graph 使 graph.update
* @param {Object} cfg
*/
public update(cfg: ModelConfig) {
const model: ModelConfig = this.get('model');
const originPosition: IPoint = { x: model.x, y: model.y };
// 直接将更新合到原数据模型上,可以保证用户在外部修改源数据然后刷新时的样式符合期待。
Object.assign(model, cfg);
// isOnlyMove 仅用于node
const onlyMove = this.isOnlyMove(cfg);
// 仅仅移动位置时,既不更新,也不重绘
if (onlyMove) {
this.updatePosition(model);
} else {
// 如果 x,y 有变化,先重置位置
if (originPosition.x !== model.x || originPosition.y !== model.y) {
this.updatePosition(model);
}
this.updateShape();
}
this.afterUpdate();
this.clearCache();
}
/**
*
*/
public updateShape() {
const shapeFactory = this.get('shapeFactory');
const model = this.get('model');
const shape = model.shape;
// 判定是否允许更新
// 1. 注册的节点允许更新
// 2. 更新后的 shape 等于原先的 shape
if (shapeFactory.shouldUpdate(shape) && shape === this.get('currentShape')) {
const updateCfg = this.getShapeCfg(model);
shapeFactory.update(shape, updateCfg, this);
} else {
// 如果不满足上面两种状态,重新绘制
this.draw();
}
this.set('originStyle', this.getKeyShapeStyle());
// 更新后重置节点状态
this.resetStates(shapeFactory, shape);
}
/**
*
* @param {object} cfg
*/
public updatePosition(cfg: ModelConfig) {
const model: ModelConfig = this.get('model');
const x = isNil(cfg.x) ? model.x : cfg.x;
const y = isNil(cfg.y) ? model.y : cfg.y;
const group: Group = this.get('group');
if (isNil(x) || isNil(y)) {
return;
}
group.resetMatrix();
// G 4.0 element 中移除了矩阵相关方法详见https://www.yuque.com/antv/blog/kxzk9g#4rMMV
translate(group, { x, y });
model.x = x;
model.y = y;
this.clearCache(); // 位置更新后需要清除缓存
}
/**
*
* @return {Object} x,y,width,height, centerX, centerY
*/
public getBBox(): IBBox {
// 计算 bbox 开销有些大,缓存
let bbox: IBBox = this.get(CACHE_BBOX);
if (!bbox) {
bbox = this.calculateBBox();
this.set(CACHE_BBOX, bbox);
}
return bbox;
}
/**
*
*/
public toFront() {
this.get('group').toFront();
}
/**
*
*/
public toBack() {
this.get('group').toBack();
}
/**
*
*/
public show() {
this.changeVisibility(true);
}
/**
*
*/
public hide() {
this.changeVisibility(false);
}
/**
*
* @param {Boolean} visible
*/
public changeVisibility(visible: boolean) {
const group: Group = this.get('group');
if (visible) {
group.show();
} else {
group.hide();
}
this.set('visible', visible);
}
/**
*
*/
public isVisible(): boolean {
return this.get('visible');
}
/**
*
* @param {Boolean} enable
*/
public enableCapture(enable: boolean) {
const group: Group = this.get('group');
if(group) {
group.attr('capture', enable)
}
}
public destroy() {
if(!this.destroyed) {
const animate = this.get('animate');
const group: Group = this.get('group');
if (animate) {
group.stopAnimate();
}
group.remove();
this._cfg = null;
this.destroyed = true;
}
}
}

205
src/item/node.ts Normal file
View File

@ -0,0 +1,205 @@
import each from '@antv/util/lib/each'
import isNil from '@antv/util/lib/is-nil';
import mix from '@antv/util/lib/mix'
import { IEdge, INode } from '@g6/interface/item';
import { IPoint, IShapeBase, NodeConfig } from '@g6/types';
import { distance, getCircleIntersectByPoint, getEllispeIntersectByPoint, getRectIntersectByPoint } from '@g6/util/math';
import Item from './item'
const CACHE_ANCHOR_POINTS = 'anchorPointsCache'
const CACHE_BBOX = 'bboxCache'
export default class Node extends Item implements INode {
private getNearestPoint(points: IPoint[], curPoint: IPoint): IPoint {
let index = 0;
let nearestPoint = points[0];
let minDistance = distance(points[0], curPoint);
for (let i = 0; i < points.length; i++) {
const point = points[i];
const dis = distance(point, curPoint);
if (dis < minDistance) {
nearestPoint = point;
minDistance = dis;
index = i;
}
}
nearestPoint.anchorIndex = index;
return nearestPoint;
}
public getDefaultCfg() {
return {
type: 'node',
edges: []
}
}
/**
*
*/
public getEdges(): IEdge[] {
return this.get('edges')
}
/**
*
*/
public getInEdges(): IEdge[] {
const self = this;
return this.get('edges').filter((edge: IEdge) => {
return edge.get('target') === self;
});
}
/**
*
*/
public getOutEdges(): IEdge[] {
const self = this;
return this.get('edges').filter((edge: IEdge) => {
return edge.get('source') === self;
});
}
/**
*
* @param {Number} index
*/
public getLinkPointByAnchor(index): IPoint {
const anchorPoints = this.getAnchorPoints();
return anchorPoints[index];
}
/**
*
* @param point
*/
public getLinkPoint(point: IPoint): IPoint {
const keyShape: IShapeBase = this.get('keyShape');
const type: string = keyShape.get('type');
const bbox = this.getBBox();
const { centerX, centerY } = bbox;
const anchorPoints = this.getAnchorPoints();
let intersectPoint: IPoint;
switch (type) {
case 'circle':
intersectPoint = getCircleIntersectByPoint({
x: centerX,
y: centerY,
r: bbox.width / 2
}, point);
break;
case 'ellipse':
intersectPoint = getEllispeIntersectByPoint({
x: centerX,
y: centerY,
rx: bbox.width / 2,
ry: bbox.height / 2
}, point);
break;
default:
intersectPoint = getRectIntersectByPoint(bbox, point);
}
let linkPoint = intersectPoint;
// 如果存在锚点,则使用交点计算最近的锚点
if (anchorPoints.length) {
if (!linkPoint) { // 如果计算不出交点
linkPoint = point;
}
linkPoint = this.getNearestPoint(anchorPoints, linkPoint);
}
if (!linkPoint) { // 如果最终依然没法找到锚点和连接点,直接返回中心点
linkPoint = { x: centerX, y: centerY };
}
return linkPoint;
}
/**
*
* @return {array} anchorPoints
*/
public getAnchorPoints(): IPoint[] {
let anchorPoints: IPoint[] = this.get(CACHE_ANCHOR_POINTS);
if (!anchorPoints) {
anchorPoints = [];
const shapeFactory = this.get('shapeFactory');
const bbox = this.getBBox();
const model: NodeConfig = this.get('model');
const shapeCfg = this.getShapeCfg(model);
const points = shapeFactory.getAnchorPoints(model.shape, shapeCfg) || [];
each(points, (pointArr, index) => {
const point = mix({
x: bbox.minX + pointArr[0] * bbox.width,
y: bbox.minY + pointArr[1] * bbox.height
}, pointArr[2], {
index
});
anchorPoints.push(point);
});
this.set(CACHE_ANCHOR_POINTS, anchorPoints);
}
return anchorPoints;
}
/**
* add edge
* @param edge Edge instance
*/
public addEdge(edge: IEdge) {
this.get('edges').push(edge)
}
/**
*
*/
public lock() {
this.set('locked', true);
}
/**
*
*/
public unlock() {
this.set('locked', false);
}
public hasLocked(): boolean {
return this.get('locked');
}
/**
*
* @param {Edge} edge
*/
public removeEdge(edge: IEdge) {
const edges = this.getEdges();
const index = edges.indexOf(edge);
if (index > -1) {
edges.splice(index, 1);
}
}
public clearCache() {
this.set(CACHE_BBOX, null); // 清理缓存的 bbox
this.set(CACHE_ANCHOR_POINTS, null);
}
/**
*
* @param cfg
*/
public isOnlyMove(cfg: NodeConfig): boolean {
if(!cfg) {
return false
}
const existX = isNil(cfg.x)
const existY = isNil(cfg.y)
const keys = Object.keys(cfg)
// 仅有一个字段,包含 x 或者 包含 y
// 两个字段,同时有 x同时有 y
return (keys.length === 1 && (existX || existY))
|| (keys.length === 2 && existX && existY)
}
}

View File

@ -1,17 +1,16 @@
import GraphEvent from '@antv/g-base/lib/event/graph-event'
import isArray from '@antv/util/lib/is-array'
import isNil from '@antv/util/lib/is-nil'
import isNumber from "@antv/util/lib/is-number";
import isString from '@antv/util/lib/is-string'
import { IG6GraphEvent, IPadding } from "../../types";
import { G6GraphEvent } from '../interface/behavior';
import { G6GraphEvent } from '@g6/interface/behavior';
import { IG6GraphEvent, Padding } from '@g6/types';
/**
* turn padding into [top, right, bottom, right]
* @param {Number|Array} padding input padding
* @return {array} output
*/
export const formatPadding = (padding: IPadding): number[] => {
export const formatPadding = (padding: Padding): number[] => {
let top = 0;
let left = 0;
let right = 0;

View File

@ -1,11 +1,9 @@
import Group from "@antv/g-canvas/lib/group";
import ShapeBase from "@antv/g-canvas/lib/shape/base";
import { BBox } from "@antv/g-canvas/lib/types";
import { vec2 } from "@antv/matrix-util";
import each from '@antv/util/lib/each'
import { IEdgeConfig, IPoint, ITreeGraphData } from "../../types";
import Global from '../global'
import { INode } from "../interface/item";
import Global from '@g6/global'
import { INode } from "@g6/interface/item";
import { EdgeConfig, IBBox, IPoint, IShapeBase, TreeGraphData } from '@g6/types';
import { applyMatrix } from "./math";
const PI: number = Math.PI
@ -16,7 +14,7 @@ const cos: (x: number) => number = Math.cos
const SELF_LINK_SIN: number = sin(PI / 8);
const SELF_LINK_COS: number = cos(PI / 8);
export const getBBox = (element: ShapeBase, group: Group): BBox => {
export const getBBox = (element: IShapeBase, group: Group): IBBox => {
const bbox = element.getBBox();
let leftTop: IPoint = {
x: bbox.minX,
@ -52,13 +50,13 @@ export const getBBox = (element: ShapeBase, group: Group): BBox => {
* get loop edge config
* @param cfg edge config
*/
export const getLoopCfgs = (cfg: IEdgeConfig): IEdgeConfig => {
export const getLoopCfgs = (cfg: EdgeConfig): EdgeConfig => {
const item: INode = cfg.sourceNode || cfg.targetNode
const container: Group = item.get('group')
const containerMatrix = container.getMatrix()
const keyShape: ShapeBase = item.getKeyShape()
const bbox: BBox = keyShape.getBBox()
const keyShape: IShapeBase = item.getKeyShape()
const bbox: IBBox = keyShape.getBBox()
const loopCfg = cfg.loopCfg || {}
// 距离keyShape边的最高距离
@ -269,7 +267,7 @@ export const getLoopCfgs = (cfg: IEdgeConfig): IEdgeConfig => {
// return result;
// }
const traverse = (data: ITreeGraphData, fn: (param: ITreeGraphData) => boolean) => {
const traverse = (data: TreeGraphData, fn: (param: TreeGraphData) => boolean) => {
if(!fn(data)) {
return
}
@ -278,7 +276,7 @@ const traverse = (data: ITreeGraphData, fn: (param: ITreeGraphData) => boolean)
})
}
export const traverseTree = (data: ITreeGraphData, fn: (param: ITreeGraphData) => boolean) => {
export const traverseTree = (data: TreeGraphData, fn: (param: TreeGraphData) => boolean) => {
if(typeof fn !== 'function') {
return
}
@ -290,7 +288,7 @@ export const traverseTree = (data: ITreeGraphData, fn: (param: ITreeGraphData) =
* @param data Tree graph data
* @param layout
*/
export const radialLayout = (data: ITreeGraphData, layout?: string): ITreeGraphData => {
export const radialLayout = (data: TreeGraphData, layout?: string): TreeGraphData => {
// 布局方式有 H / V / LR / RL / TB / BT
const VERTICAL_LAYOUTS: string[] = [ 'V', 'TB', 'BT' ];
const min: IPoint = {

View File

@ -1,9 +1,9 @@
import groupBy, { ObjectType } from '@antv/util/lib/group-by'
import { IGraphData, IGroupConfig, IGroupNodeIds } from '../../types';
import { GraphData, GroupConfig, GroupNodeIds } from '@g6/types';
export const getAllNodeInGroups = (data: IGraphData): IGroupNodeIds => {
const groupById: ObjectType<IGroupConfig> = groupBy(data.groups, 'id');
const groupByParentId: ObjectType<IGroupConfig> = groupBy(data.groups, 'parentId');
export const getAllNodeInGroups = (data: GraphData): GroupNodeIds => {
const groupById: ObjectType<GroupConfig> = groupBy(data.groups, 'id');
const groupByParentId: ObjectType<GroupConfig> = groupBy(data.groups, 'parentId');
const result = {};
for (const parentId in groupByParentId) {
@ -39,7 +39,7 @@ export const getAllNodeInGroups = (data: IGraphData): IGroupNodeIds => {
}
// 缓存所有groupID对应的Node
const groupNodes: IGroupNodeIds = {} as IGroupNodeIds;
const groupNodes: GroupNodeIds = {} as GroupNodeIds;
for (const groupId in groupIds) {
if (!groupId || groupId === 'undefined') {
continue;

View File

@ -1,5 +1,7 @@
import Group from '@antv/g-canvas/lib/group';
import { mat3, vec3 } from '@antv/matrix-util'
import { ICircle, IEllipse, IGraphData, IMatrix, IPoint, IRect } from '../../types'
import { transform } from '@antv/matrix-util'
import { GraphData, ICircle, IEllipse, IMatrix, IPoint, IRect } from '@g6/types'
/**
*
@ -271,7 +273,7 @@ export const floydWarshall = (adjMatrix: IMatrix[]): IMatrix[] => {
* @param data graph data
* @param directed whether it's a directed graph
*/
export const getAdjMatrix = (data: IGraphData, directed: boolean): IMatrix[] => {
export const getAdjMatrix = (data: GraphData, directed: boolean): IMatrix[] => {
const nodes = data.nodes;
const edges = data.edges;
const matrix: IMatrix[] = [];
@ -294,4 +296,16 @@ export const getAdjMatrix = (data: IGraphData, directed: boolean): IMatrix[] =>
}
});
return matrix;
}
/**
* group
* @param group Group
* @param point
*/
export const translate = (group: Group, point: IPoint) => {
const matrix: IMatrix = group.getMatrix()
transform(matrix, [
[ 't', point.x, point.y ]
])
}

View File

@ -1,6 +1,6 @@
import { vec2 } from '@antv/matrix-util'
import { catmullRom2Bezier } from '@antv/path-util'
import { IPoint } from '../../types'
import { IPoint } from '@g6/types'
/**
*

View File

@ -1,6 +1,6 @@
import '../../../src/behavior'
import Behavior from '../../../src/behavior/behavior'
import { IBehavior } from '../../../types';
import { IBehavior } from '../../../src/interface/behavior';
describe('Behavior', () => {
it('register signle behavior', () => {

View File

@ -0,0 +1,19 @@
import { Mode } from '../../../../src/graph/controller'
import Graph from '../../../../src/graph/graph'
import { GraphOptions, IGraph } from '../../../../src/interface/graph';
describe('Mode Controller', () => {
it('signle mode', () => {
const cfg: GraphOptions = {
container: 'x',
width: 200,
height: 100
}
const graph: IGraph = new Graph(cfg)
const modeController = new Mode(graph)
expect(Object.keys(modeController.modes).length).toBe(1);
expect(modeController.modes.default).not.toBe(undefined);
expect(modeController.modes.default.length).toBe(0);
expect(modeController.mode).toBe('default');
})
})

View File

@ -11,7 +11,12 @@
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["jest"]
"types": ["jest"],
"baseUrl": ".",
"paths": {
"@g6/*": ["./src/*"],
"@g6/types": ["./types"]
}
},
"include": ["src"],
"typedocOptions": {

View File

@ -1,17 +1,28 @@
import GraphEvent from '@antv/g-base/lib/event/graph-event';
import { BBox } from '@antv/g-base/lib/types';
import ShapeBase from '@antv/g-canvas/lib/shape/base';
import { IGraph } from '../src/interface/graph';
import { IItem, INode } from '../src/interface/item'
// Math types
export interface IPoint {
x: number;
y: number;
// 获取连接点时使用
anchorIndex?: number;
}
export type IMatrix = number[];
export type IPadding = number | string | number[];
export interface IBBox extends BBox {
centerX?: number;
centerY?: number;
}
export type IShapeStyle = Partial<{
export type Padding = number | string | number[];
// Shape types
export type ShapeStyle = Partial<{
x: number;
y: number;
r: number;
@ -27,6 +38,10 @@ export type IShapeStyle = Partial<{
[key: string]: string | number | object | object[]
}>
export interface IShapeBase extends ShapeBase {
isKeyShape: boolean;
}
export interface IRect extends IPoint {
width: number;
height: number;
@ -41,12 +56,15 @@ export interface IEllipse extends IPoint {
ry: number;
}
type IModelStyle = Partial<{
export type SourceTarget = 'source' | 'target'
// model types (node edge group)
export type ModelStyle = Partial<{
style: {
[key: string]: IShapeStyle
[key: string]: ShapeStyle
};
stateStyles: {
[key: string]: IShapeStyle;
[key: string]: ShapeStyle;
};
// loop edge config
loopCfg: {
@ -56,37 +74,35 @@ type IModelStyle = Partial<{
clockwise?: boolean;
};
}>
interface IModelStyle1 {
style?: {
[key: string]: IShapeStyle
};
stateStyles?: {
[key: string]: IShapeStyle;
};
// loop edge config
loopCfg?: {
dist?: number;
position?: string;
// 如果逆时针画,交换起点和终点
clockwise?: boolean;
}
}
export type IModelConfig = INodeConfig | IEdgeConfig
export type Easeing =
'easeLinear'
| 'easePolyIn'
| 'easePolyOut'
| 'easePolyInOut'
| 'easeQuad'
| 'easeQuadIn'
| 'easeQuadOut'
| 'easeQuadInOut'
| string
export interface INodeConfig extends IModelStyle {
id: string;
export interface ModelConfig extends ModelStyle {
shape?: string;
label?: string;
groupId?: string;
description?: string;
x?: number;
y?: number;
}
export interface NodeConfig extends ModelConfig {
id: string;
groupId?: string;
description?: string;
}
export interface IEdgeConfig extends IModelStyle {
export interface EdgeConfig extends ModelConfig {
id?: string;
source: string;
target: string;
label?: string;
sourceNode?: INode;
targetNode?: INode;
startPoint?: IPoint;
@ -94,28 +110,28 @@ export interface IEdgeConfig extends IModelStyle {
controlPoints?: IPoint[];
}
export interface IGroupConfig {
export interface GroupConfig {
id: string;
parentId?: string;
[key: string]: string | IModelStyle;
[key: string]: string | ModelStyle;
}
export interface IGroupNodeIds {
export interface GroupNodeIds {
[key: string]: string[];
}
export interface IGraphData {
nodes?: INodeConfig[];
edges?: IEdgeConfig[];
groups?: IGroupConfig[];
export interface GraphData {
nodes?: NodeConfig[];
edges?: EdgeConfig[];
groups?: GroupConfig[];
}
export interface ITreeGraphData {
export interface TreeGraphData {
id: string;
label?: string;
x?: number;
y?: number;
children?: ITreeGraphData[];
children?: TreeGraphData[];
}
// Behavior type file
@ -160,12 +176,12 @@ type Unbind = 'unbind'
export type DefaultBehaviorType = IG6GraphEvent | string | number | object
export type IBehaviorOpation<U> = {
export type BehaviorOpation<U> = {
[T in keyof U]:
T extends GetEvents ? () => { [key in G6Event]?: string } :
T extends ShouldBegin ? (cfg?: IModelConfig) => boolean :
T extends ShouldEnd ? (cfg?: IModelConfig) => boolean :
T extends ShouldUpdate ? (cfg?: IModelConfig) => boolean :
T extends ShouldBegin ? (cfg?: ModelConfig) => boolean :
T extends ShouldEnd ? (cfg?: ModelConfig) => boolean :
T extends ShouldUpdate ? (cfg?: ModelConfig) => boolean :
T extends Bind ? (graph: IGraph) => void :
T extends Unbind ? (graph: IGraph) => void :
(...args: DefaultBehaviorType[]) => unknown;
@ -176,14 +192,3 @@ export type IEvent = Record<G6Event, string>
export interface IG6GraphEvent extends GraphEvent {
item: IItem;
}
export interface IBehavior {
constructor: (cfg?: object) => void;
getEvents: () => { [key in G6Event]?: string };
shouldBegin: () => boolean;
shouldUpdate: () => boolean;
shouldEnd: () => boolean;
bind: (graph: IGraph) => void;
unbind: (graph: IGraph) => void;
[key: string]: (...args: DefaultBehaviorType[]) => unknown;
}