diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000000..6c0d612ea6 --- /dev/null +++ b/jest.config.js @@ -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/(.*)': '/src/$1', + '@g6/types': '/types' + } +}; diff --git a/package.json b/package.json index 08dcd9b1cd..299f46384b 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/behavior/behavior.ts b/src/behavior/behavior.ts index 8747634cc2..b2d99a606f 100644 --- a/src/behavior/behavior.ts +++ b/src/behavior/behavior.ts @@ -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(type: string, behavior: IBehaviorOpation) { + public static registerBehavior(type: string, behavior: BehaviorOpation) { if(!behavior) { throw new Error(`please specify handler for this behavior: ${type}`) } diff --git a/src/behavior/behaviorOption.ts b/src/behavior/behaviorOption.ts index 259680d87d..4522810a76 100644 --- a/src/behavior/behaviorOption.ts +++ b/src/behavior/behaviorOption.ts @@ -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 diff --git a/src/behavior/drag-canvas.ts b/src/behavior/drag-canvas.ts index 19085386f4..25eb3a0554 100644 --- a/src/behavior/drag-canvas.ts +++ b/src/behavior/drag-canvas.ts @@ -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 ] diff --git a/src/graph/controller/index.ts b/src/graph/controller/index.ts new file mode 100644 index 0000000000..4c836e5ac6 --- /dev/null +++ b/src/graph/controller/index.ts @@ -0,0 +1 @@ +export { default as Mode } from './mode' \ No newline at end of file diff --git a/src/graph/controller/mode.ts b/src/graph/controller/mode.ts new file mode 100644 index 0000000000..e085394c66 --- /dev/null +++ b/src/graph/controller/mode.ts @@ -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 + } + +} \ No newline at end of file diff --git a/src/graph/graph.ts b/src/graph/graph.ts new file mode 100644 index 0000000000..b8bd225ed6 --- /dev/null +++ b/src/graph/graph.ts @@ -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] + } +} \ No newline at end of file diff --git a/src/graph/tree-graph.ts b/src/graph/tree-graph.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/interface/behavior.ts b/src/interface/behavior.ts index 95ce59692b..fa0cb36715 100644 --- a/src/interface/behavior.ts +++ b/src/interface/behavior.ts @@ -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 diff --git a/src/interface/graph.ts b/src/interface/graph.ts index 6305c1c39e..0000b41833 100644 --- a/src/interface/graph.ts +++ b/src/interface/graph.ts @@ -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(key: string): T; } diff --git a/src/interface/item.ts b/src/interface/item.ts index 0aa1fcdab5..86f1e813d0 100644 --- a/src/interface/item.ts +++ b/src/interface/item.ts @@ -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(key: string): T; set(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; } \ No newline at end of file diff --git a/src/item/edge.ts b/src/item/edge.ts new file mode 100644 index 0000000000..92508c5e0d --- /dev/null +++ b/src/item/edge.ts @@ -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(); + } + +} \ No newline at end of file diff --git a/src/item/item.ts b/src/item/item.ts new file mode 100644 index 0000000000..ce874685df --- /dev/null +++ b/src/item/item.ts @@ -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; + } + } + +} \ No newline at end of file diff --git a/src/item/node.ts b/src/item/node.ts new file mode 100644 index 0000000000..773f8afe5a --- /dev/null +++ b/src/item/node.ts @@ -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) + } +} \ No newline at end of file diff --git a/src/util/base.ts b/src/util/base.ts index 6fdea87640..f52517f3be 100644 --- a/src/util/base.ts +++ b/src/util/base.ts @@ -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; diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 4b33e8b2d5..583ce3925d 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -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 = { diff --git a/src/util/group.ts b/src/util/group.ts index be9f48da2a..63d6151587 100644 --- a/src/util/group.ts +++ b/src/util/group.ts @@ -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 = groupBy(data.groups, 'id'); - const groupByParentId: ObjectType = groupBy(data.groups, 'parentId'); +export const getAllNodeInGroups = (data: GraphData): GroupNodeIds => { + const groupById: ObjectType = groupBy(data.groups, 'id'); + const groupByParentId: ObjectType = 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; diff --git a/src/util/math.ts b/src/util/math.ts index 352951f0bb..3ddad9e738 100644 --- a/src/util/math.ts +++ b/src/util/math.ts @@ -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 ] + ]) } \ No newline at end of file diff --git a/src/util/path.ts b/src/util/path.ts index 90c5fe91c8..4fb00c576f 100644 --- a/src/util/path.ts +++ b/src/util/path.ts @@ -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' /** * 替换字符串中的字段 diff --git a/tests/unit/behavior/index-spec.ts b/tests/unit/behavior/index-spec.ts index 430176489c..5081c69f9a 100644 --- a/tests/unit/behavior/index-spec.ts +++ b/tests/unit/behavior/index-spec.ts @@ -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', () => { diff --git a/tests/unit/graph/controller/mode-spec.ts b/tests/unit/graph/controller/mode-spec.ts new file mode 100644 index 0000000000..7cb5729370 --- /dev/null +++ b/tests/unit/graph/controller/mode-spec.ts @@ -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'); + }) +}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2928f61ef9..8820b438c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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": { diff --git a/types/index.ts b/types/index.ts index 82b8c84723..747d18a717 100644 --- a/types/index.ts +++ b/types/index.ts @@ -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 = { +export type BehaviorOpation = { [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 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; -} \ No newline at end of file