mirror of
https://gitee.com/antv/g6.git
synced 2024-11-29 18:28:19 +08:00
使用相机实现视口相关功能 (#4348)
* feat: support translate & zoom with camera #4344 * feat: support fitCenter & focusItem with Camera API #4344 * feat: support fitView #4344 * feat: support focusing multi-items & stop transition of current transform #4348
This commit is contained in:
parent
8f68192dd3
commit
918584d6b1
@ -2,5 +2,6 @@ export * from './data';
|
|||||||
export * from './interaction';
|
export * from './interaction';
|
||||||
export * from './item';
|
export * from './item';
|
||||||
export * from './layout';
|
export * from './layout';
|
||||||
|
export * from './viewport';
|
||||||
export * from './theme';
|
export * from './theme';
|
||||||
export * from './extension';
|
export * from './extension';
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
|
import { AABB, Canvas, DisplayObject, Group } from '@antv/g';
|
||||||
import { GraphChange, ID } from '@antv/graphlib';
|
import { GraphChange, ID } from '@antv/graphlib';
|
||||||
import { Group, AABB, DisplayObject, Canvas } from '@antv/g';
|
|
||||||
import { ComboModel, IGraph } from '../../types';
|
|
||||||
import registry from '../../stdlib';
|
|
||||||
import { getExtension } from '../../util/extension';
|
|
||||||
import { GraphCore } from '../../types/data';
|
|
||||||
import { NodeDisplayModel, NodeEncode, NodeModel, NodeModelData } from '../../types/node';
|
|
||||||
import { EdgeDisplayModel, EdgeEncode, EdgeModel, EdgeModelData } from '../../types/edge';
|
|
||||||
import Node from '../../item/node';
|
|
||||||
import Edge from '../../item/edge';
|
|
||||||
import Combo from '../../item/combo';
|
|
||||||
import { ThemeSpecification, ItemThemeSpecifications, ItemStyleSet } from '../../types/theme';
|
|
||||||
import { isArray, isObject } from '@antv/util';
|
import { isArray, isObject } from '@antv/util';
|
||||||
import { ITEM_TYPE, SHAPE_TYPE, ShapeStyle } from '../../types/item';
|
import Combo from '../../item/combo';
|
||||||
|
import Edge from '../../item/edge';
|
||||||
|
import Node from '../../item/node';
|
||||||
|
import registry from '../../stdlib';
|
||||||
|
import { ComboModel, IGraph } from '../../types';
|
||||||
import { ComboDisplayModel, ComboEncode } from '../../types/combo';
|
import { ComboDisplayModel, ComboEncode } from '../../types/combo';
|
||||||
|
import { GraphCore } from '../../types/data';
|
||||||
|
import { EdgeDisplayModel, EdgeEncode, EdgeModel, EdgeModelData } from '../../types/edge';
|
||||||
|
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../../types/item';
|
||||||
|
import { ItemStyleSet, ItemThemeSpecifications, ThemeSpecification } from '../../types/theme';
|
||||||
|
import { getExtension } from '../../util/extension';
|
||||||
import { upsertShape } from '../../util/shape';
|
import { upsertShape } from '../../util/shape';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,7 +60,7 @@ export class ItemController {
|
|||||||
constructor(graph: IGraph<any, any>) {
|
constructor(graph: IGraph<any, any>) {
|
||||||
this.graph = graph;
|
this.graph = graph;
|
||||||
// get mapper for node / edge / combo
|
// get mapper for node / edge / combo
|
||||||
const { node, edge, combo, nodeState, edgeState, comboState } = graph.getSpecification();
|
const { node, edge, combo, nodeState, edgeState, comboState } = graph.getSpecification();
|
||||||
this.nodeMapper = node;
|
this.nodeMapper = node;
|
||||||
this.edgeMapper = edge;
|
this.edgeMapper = edge;
|
||||||
this.comboMapper = combo;
|
this.comboMapper = combo;
|
||||||
@ -114,7 +113,7 @@ export class ItemController {
|
|||||||
* Listener of runtime's render hook.
|
* Listener of runtime's render hook.
|
||||||
* @param param contains inner data stored in graphCore structure
|
* @param param contains inner data stored in graphCore structure
|
||||||
*/
|
*/
|
||||||
private onRender(param: { graphCore: GraphCore, theme: ThemeSpecification }) {
|
private onRender(param: { graphCore: GraphCore; theme: ThemeSpecification }) {
|
||||||
const { graphCore, theme = {} } = param;
|
const { graphCore, theme = {} } = param;
|
||||||
const { graph } = this;
|
const { graph } = this;
|
||||||
// TODO: 0. clear groups on canvas, and create new groups
|
// TODO: 0. clear groups on canvas, and create new groups
|
||||||
@ -177,11 +176,17 @@ export class ItemController {
|
|||||||
|
|
||||||
// === 3. add nodes ===
|
// === 3. add nodes ===
|
||||||
if (groupedChanges.NodeAdded.length) {
|
if (groupedChanges.NodeAdded.length) {
|
||||||
this.renderNodes(groupedChanges.NodeAdded.map((change) => change.value), nodeTheme);
|
this.renderNodes(
|
||||||
|
groupedChanges.NodeAdded.map((change) => change.value),
|
||||||
|
nodeTheme,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// === 4. add edges ===
|
// === 4. add edges ===
|
||||||
if (groupedChanges.EdgeAdded.length) {
|
if (groupedChanges.EdgeAdded.length) {
|
||||||
this.renderEdges(groupedChanges.EdgeAdded.map((change) => change.value), edgeTheme);
|
this.renderEdges(
|
||||||
|
groupedChanges.EdgeAdded.map((change) => change.value),
|
||||||
|
edgeTheme,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 5. update nodes's data ===
|
// === 5. update nodes's data ===
|
||||||
@ -209,7 +214,12 @@ export class ItemController {
|
|||||||
// update the theme if the dataType value is changed
|
// update the theme if the dataType value is changed
|
||||||
let themeStyles;
|
let themeStyles;
|
||||||
if (previous[nodeDataTypeField] !== current[nodeDataTypeField]) {
|
if (previous[nodeDataTypeField] !== current[nodeDataTypeField]) {
|
||||||
themeStyles = getThemeStyles(this.nodeDataTypeSet, nodeDataTypeField, current[nodeDataTypeField], nodeTheme);
|
themeStyles = getThemeStyles(
|
||||||
|
this.nodeDataTypeSet,
|
||||||
|
nodeDataTypeField,
|
||||||
|
current[nodeDataTypeField],
|
||||||
|
nodeTheme,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const item = itemMap[id];
|
const item = itemMap[id];
|
||||||
const innerModel = graphCore.getNode(id);
|
const innerModel = graphCore.getNode(id);
|
||||||
@ -247,7 +257,12 @@ export class ItemController {
|
|||||||
// update the theme if the dataType value is changed
|
// update the theme if the dataType value is changed
|
||||||
let themeStyles;
|
let themeStyles;
|
||||||
if (previous[edgeDataTypeField] !== current[edgeDataTypeField]) {
|
if (previous[edgeDataTypeField] !== current[edgeDataTypeField]) {
|
||||||
themeStyles = getThemeStyles(this.edgeDataTypeSet, edgeDataTypeField, current[edgeDataTypeField], edgeTheme);
|
themeStyles = getThemeStyles(
|
||||||
|
this.edgeDataTypeSet,
|
||||||
|
edgeDataTypeField,
|
||||||
|
current[edgeDataTypeField],
|
||||||
|
edgeTheme,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const item = itemMap[id];
|
const item = itemMap[id];
|
||||||
const innerModel = graphCore.getEdge(id);
|
const innerModel = graphCore.getEdge(id);
|
||||||
@ -283,9 +298,9 @@ export class ItemController {
|
|||||||
* value: state value
|
* value: state value
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
private onItemStateChange(param: { ids: ID[], states: string[], value: boolean }) {
|
private onItemStateChange(param: { ids: ID[]; states: string[]; value: boolean }) {
|
||||||
const { ids, states, value } = param;
|
const { ids, states, value } = param;
|
||||||
ids.forEach(id => {
|
ids.forEach((id) => {
|
||||||
const item = this.itemMap[id];
|
const item = this.itemMap[id];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
console.warn(`Fail to set state for item ${id}, which is not exist.`);
|
console.warn(`Fail to set state for item ${id}, which is not exist.`);
|
||||||
@ -295,20 +310,20 @@ export class ItemController {
|
|||||||
// clear all the states
|
// clear all the states
|
||||||
item.clearStates(states);
|
item.clearStates(states);
|
||||||
} else {
|
} else {
|
||||||
states.forEach(state => item.setState(state, value));
|
states.forEach((state) => item.setState(state, value));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onTransientUpdate(param: {
|
private onTransientUpdate(param: {
|
||||||
type: ITEM_TYPE | SHAPE_TYPE,
|
type: ITEM_TYPE | SHAPE_TYPE;
|
||||||
id: ID,
|
id: ID;
|
||||||
config: {
|
config: {
|
||||||
style: ShapeStyle,
|
style: ShapeStyle;
|
||||||
action: 'remove' | 'add' | 'update' | undefined,
|
action: 'remove' | 'add' | 'update' | undefined;
|
||||||
[shapeConfig: string]: unknown,
|
[shapeConfig: string]: unknown;
|
||||||
},
|
};
|
||||||
canvas: Canvas
|
canvas: Canvas;
|
||||||
}) {
|
}) {
|
||||||
const { transientMap } = this;
|
const { transientMap } = this;
|
||||||
const { type, id, config = {}, canvas } = param;
|
const { type, id, config = {}, canvas } = param;
|
||||||
@ -405,7 +420,7 @@ export class ItemController {
|
|||||||
*/
|
*/
|
||||||
public findIdByState(itemType: ITEM_TYPE, state: string, value: string | boolean = true) {
|
public findIdByState(itemType: ITEM_TYPE, state: string, value: string | boolean = true) {
|
||||||
const ids = [];
|
const ids = [];
|
||||||
Object.values(this.itemMap).forEach(item => {
|
Object.values(this.itemMap).forEach((item) => {
|
||||||
if (item.getType() !== itemType) return;
|
if (item.getType() !== itemType) return;
|
||||||
if (item.hasState(state) === value) ids.push(item.getID());
|
if (item.hasState(state) === value) ids.push(item.getID());
|
||||||
});
|
});
|
||||||
@ -427,6 +442,10 @@ export class ItemController {
|
|||||||
return item.hasState(state);
|
return item.hasState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getItemById(id: ID) {
|
||||||
|
return this.itemMap[id];
|
||||||
|
}
|
||||||
|
|
||||||
public getItemBBox(id: ID, isKeyShape: boolean = false): AABB | false {
|
public getItemBBox(id: ID, isKeyShape: boolean = false): AABB | false {
|
||||||
const item = this.itemMap[id];
|
const item = this.itemMap[id];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
@ -446,7 +465,12 @@ export class ItemController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getThemeStyles = (dataTypeSet: Set<string>, dataTypeField: string, dataType: string, itemTheme: ItemThemeSpecifications): ItemStyleSet => {
|
const getThemeStyles = (
|
||||||
|
dataTypeSet: Set<string>,
|
||||||
|
dataTypeField: string,
|
||||||
|
dataType: string,
|
||||||
|
itemTheme: ItemThemeSpecifications,
|
||||||
|
): ItemStyleSet => {
|
||||||
const { styles: themeStyles } = itemTheme;
|
const { styles: themeStyles } = itemTheme;
|
||||||
if (!dataTypeField) {
|
if (!dataTypeField) {
|
||||||
// dataType field is not assigned
|
// dataType field is not assigned
|
||||||
@ -462,4 +486,4 @@ const getThemeStyles = (dataTypeSet: Set<string>, dataTypeField: string, dataTyp
|
|||||||
themeStyle = themeStyles[dataType] || themeStyles.others;
|
themeStyle = themeStyles[dataType] || themeStyles.others;
|
||||||
}
|
}
|
||||||
return themeStyle;
|
return themeStyle;
|
||||||
}
|
};
|
||||||
|
92
packages/g6/src/runtime/controller/viewport.ts
Normal file
92
packages/g6/src/runtime/controller/viewport.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { ICamera, PointLike } from '@antv/g';
|
||||||
|
import { IGraph } from '../../types';
|
||||||
|
import { CameraAnimationOptions } from '../../types/animate';
|
||||||
|
import { ViewportChangeHookParams } from '../../types/hook';
|
||||||
|
|
||||||
|
let landmarkCounter = 0;
|
||||||
|
|
||||||
|
export class ViewportController {
|
||||||
|
public graph: IGraph;
|
||||||
|
|
||||||
|
constructor(graph: IGraph<any>) {
|
||||||
|
this.graph = graph;
|
||||||
|
this.tap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe the lifecycle of graph.
|
||||||
|
*/
|
||||||
|
private tap() {
|
||||||
|
this.graph.hooks.viewportchange.tap(this.onViewportChange.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onViewportChange({ transform, effectTiming }: ViewportChangeHookParams) {
|
||||||
|
const camera = this.graph.canvas.getCamera();
|
||||||
|
const { translate, rotate, zoom, origin = this.graph.getViewportCenter() } = transform;
|
||||||
|
const currentZoom = camera.getZoom();
|
||||||
|
|
||||||
|
if (effectTiming) {
|
||||||
|
const { duration = 1000, easing = 'linear', easingFunction } = effectTiming;
|
||||||
|
const landmarkOptions: Partial<{
|
||||||
|
position: [number, number] | [number, number, number] | Float32Array;
|
||||||
|
focalPoint: [number, number] | [number, number, number] | Float32Array;
|
||||||
|
zoom: number;
|
||||||
|
roll: number;
|
||||||
|
}> = {};
|
||||||
|
|
||||||
|
if (translate) {
|
||||||
|
const { dx = 0, dy = 0 } = translate;
|
||||||
|
const [px, py] = camera.getPosition();
|
||||||
|
const [fx, fy] = camera.getFocalPoint();
|
||||||
|
landmarkOptions.position = [px - dx, py - dy];
|
||||||
|
landmarkOptions.focalPoint = [fx - dx, fy - dy];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoom) {
|
||||||
|
const { ratio } = zoom;
|
||||||
|
landmarkOptions.zoom = currentZoom * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotate) {
|
||||||
|
const { angle } = rotate;
|
||||||
|
landmarkOptions.roll = camera.getRoll() + angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const landmark = camera.createLandmark(`mark${landmarkCounter++}`, landmarkOptions);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
camera.gotoLandmark(landmark, {
|
||||||
|
duration: Number(duration),
|
||||||
|
easing,
|
||||||
|
easingFunction,
|
||||||
|
onfinish: () => {
|
||||||
|
resolve(undefined);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (translate) {
|
||||||
|
const { dx = 0, dy = 0 } = translate;
|
||||||
|
camera.pan(-dx / currentZoom, -dy / currentZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotate) {
|
||||||
|
const { angle } = rotate;
|
||||||
|
const [x, y] = camera.getPosition();
|
||||||
|
if (origin) {
|
||||||
|
camera.setPosition(origin.x, origin.y);
|
||||||
|
}
|
||||||
|
camera.rotate(0, 0, angle);
|
||||||
|
if (origin) {
|
||||||
|
camera.pan(x - origin.x, y - origin.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoom) {
|
||||||
|
const { ratio } = zoom;
|
||||||
|
camera.setZoomByViewportPoint(currentZoom * ratio, [origin.x, origin.y]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import EventEmitter from '@antv/event-emitter';
|
import EventEmitter from '@antv/event-emitter';
|
||||||
import { Canvas, runtime, AABB, DisplayObject } from '@antv/g';
|
import { AABB, Canvas, DisplayObject, PointLike, runtime } from '@antv/g';
|
||||||
import { GraphChange, ID } from '@antv/graphlib';
|
import { GraphChange, ID } from '@antv/graphlib';
|
||||||
import { isArray, isNil, isNumber, isObject, isString } from '@antv/util';
|
import { isArray, isNil, isNumber, isObject, isString } from '@antv/util';
|
||||||
import {
|
import {
|
||||||
@ -10,13 +10,13 @@ import {
|
|||||||
NodeUserModel,
|
NodeUserModel,
|
||||||
Specification,
|
Specification,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { AnimateCfg } from '../types/animate';
|
import { CameraAnimationOptions } from '../types/animate';
|
||||||
import { BehaviorObjectOptionsOf, BehaviorOptionsOf, BehaviorRegistry } from '../types/behavior';
|
import { BehaviorObjectOptionsOf, BehaviorOptionsOf, BehaviorRegistry } from '../types/behavior';
|
||||||
import { ComboModel } from '../types/combo';
|
import { ComboModel } from '../types/combo';
|
||||||
import { Padding, Point } from '../types/common';
|
import { Padding } from '../types/common';
|
||||||
import { GraphCore } from '../types/data';
|
import { GraphCore } from '../types/data';
|
||||||
import { EdgeModel, EdgeModelData } from '../types/edge';
|
import { EdgeModel, EdgeModelData } from '../types/edge';
|
||||||
import { Hooks } from '../types/hook';
|
import { Hooks, ViewportChangeHookParams } from '../types/hook';
|
||||||
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../types/item';
|
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../types/item';
|
||||||
import {
|
import {
|
||||||
ImmediatelyInvokedLayoutOptions,
|
ImmediatelyInvokedLayoutOptions,
|
||||||
@ -25,8 +25,9 @@ import {
|
|||||||
} from '../types/layout';
|
} from '../types/layout';
|
||||||
import { NodeModel, NodeModelData } from '../types/node';
|
import { NodeModel, NodeModelData } from '../types/node';
|
||||||
import { ThemeRegistry, ThemeSpecification } from '../types/theme';
|
import { ThemeRegistry, ThemeSpecification } from '../types/theme';
|
||||||
import { FitViewRules, GraphAlignment } from '../types/view';
|
import { FitViewRules, GraphTransformOptions } from '../types/view';
|
||||||
import { createCanvas } from '../util/canvas';
|
import { createCanvas } from '../util/canvas';
|
||||||
|
import { formatPadding } from '../util/shape';
|
||||||
import {
|
import {
|
||||||
DataController,
|
DataController,
|
||||||
ExtensionController,
|
ExtensionController,
|
||||||
@ -34,6 +35,7 @@ import {
|
|||||||
ItemController,
|
ItemController,
|
||||||
LayoutController,
|
LayoutController,
|
||||||
ThemeController,
|
ThemeController,
|
||||||
|
ViewportController,
|
||||||
} from './controller';
|
} from './controller';
|
||||||
import Hook from './hooks';
|
import Hook from './hooks';
|
||||||
|
|
||||||
@ -42,7 +44,10 @@ import Hook from './hooks';
|
|||||||
*/
|
*/
|
||||||
runtime.enableCSSParsing = false;
|
runtime.enableCSSParsing = false;
|
||||||
|
|
||||||
export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry> extends EventEmitter implements IGraph<B, T> {
|
export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||||
|
extends EventEmitter
|
||||||
|
implements IGraph<B, T>
|
||||||
|
{
|
||||||
public hooks: Hooks;
|
public hooks: Hooks;
|
||||||
// for nodes and edges, which will be separate into groups
|
// for nodes and edges, which will be separate into groups
|
||||||
public canvas: Canvas;
|
public canvas: Canvas;
|
||||||
@ -58,6 +63,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
private dataController: DataController;
|
private dataController: DataController;
|
||||||
private interactionController: InteractionController;
|
private interactionController: InteractionController;
|
||||||
private layoutController: LayoutController;
|
private layoutController: LayoutController;
|
||||||
|
private viewportController: ViewportController;
|
||||||
private itemController: ItemController;
|
private itemController: ItemController;
|
||||||
private extensionController: ExtensionController;
|
private extensionController: ExtensionController;
|
||||||
private themeController: ThemeController;
|
private themeController: ThemeController;
|
||||||
@ -65,9 +71,9 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
private defaultSpecification = {
|
private defaultSpecification = {
|
||||||
theme: {
|
theme: {
|
||||||
type: 'spec',
|
type: 'spec',
|
||||||
base: 'light'
|
base: 'light',
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(spec: Specification<B, T>) {
|
constructor(spec: Specification<B, T>) {
|
||||||
super();
|
super();
|
||||||
@ -82,8 +88,8 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
canvases: {
|
canvases: {
|
||||||
background: this.backgroundCanvas,
|
background: this.backgroundCanvas,
|
||||||
main: this.canvas,
|
main: this.canvas,
|
||||||
transient: this.transientCanvas
|
transient: this.transientCanvas,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data } = spec;
|
const { data } = spec;
|
||||||
@ -102,6 +108,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
this.layoutController = new LayoutController(this);
|
this.layoutController = new LayoutController(this);
|
||||||
this.themeController = new ThemeController(this);
|
this.themeController = new ThemeController(this);
|
||||||
this.itemController = new ItemController(this);
|
this.itemController = new ItemController(this);
|
||||||
|
this.viewportController = new ViewportController(this);
|
||||||
this.extensionController = new ExtensionController(this);
|
this.extensionController = new ExtensionController(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,10 +139,10 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
this.hooks = {
|
this.hooks = {
|
||||||
init: new Hook<{
|
init: new Hook<{
|
||||||
canvases: {
|
canvases: {
|
||||||
background: Canvas,
|
background: Canvas;
|
||||||
main: Canvas,
|
main: Canvas;
|
||||||
transient: Canvas
|
transient: Canvas;
|
||||||
}
|
};
|
||||||
}>({ name: 'init' }),
|
}>({ name: 'init' }),
|
||||||
datachange: new Hook<{ data: GraphData; type: 'replace' }>({ name: 'datachange' }),
|
datachange: new Hook<{ data: GraphData; type: 'replace' }>({ name: 'datachange' }),
|
||||||
itemchange: new Hook<{
|
itemchange: new Hook<{
|
||||||
@ -144,16 +151,24 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
graphCore: GraphCore;
|
graphCore: GraphCore;
|
||||||
theme: ThemeSpecification;
|
theme: ThemeSpecification;
|
||||||
}>({ name: 'itemchange' }),
|
}>({ name: 'itemchange' }),
|
||||||
render: new Hook<{ graphCore: GraphCore, theme: ThemeSpecification; }>({ name: 'render' }),
|
render: new Hook<{ graphCore: GraphCore; theme: ThemeSpecification }>({ name: 'render' }),
|
||||||
layout: new Hook<{ graphCore: GraphCore }>({ name: 'layout' }),
|
layout: new Hook<{ graphCore: GraphCore }>({ name: 'layout' }),
|
||||||
|
viewportchange: new Hook<ViewportChangeHookParams>({ name: 'viewport' }),
|
||||||
modechange: new Hook<{ mode: string }>({ name: 'modechange' }),
|
modechange: new Hook<{ mode: string }>({ name: 'modechange' }),
|
||||||
behaviorchange: new Hook<{
|
behaviorchange: new Hook<{
|
||||||
action: 'update' | 'add' | 'remove';
|
action: 'update' | 'add' | 'remove';
|
||||||
modes: string[];
|
modes: string[];
|
||||||
behaviors: BehaviorOptionsOf<{}>[];
|
behaviors: BehaviorOptionsOf<{}>[];
|
||||||
}>({ name: 'behaviorchange' }),
|
}>({ name: 'behaviorchange' }),
|
||||||
itemstatechange: new Hook<{ ids: ID[], state: string, value: boolean }>({ name: 'itemstatechange' }),
|
itemstatechange: new Hook<{ ids: ID[]; state: string; value: boolean }>({
|
||||||
transientupdate: new Hook<{ type: ITEM_TYPE | SHAPE_TYPE, id: ID, config: { style: ShapeStyle, action: 'remove' | 'add' | 'update' | undefined }, canvas: Canvas }>({ name: 'transientupdate'}), // TODO
|
name: 'itemstatechange',
|
||||||
|
}),
|
||||||
|
transientupdate: new Hook<{
|
||||||
|
type: ITEM_TYPE | SHAPE_TYPE;
|
||||||
|
id: ID;
|
||||||
|
config: { style: ShapeStyle; action: 'remove' | 'add' | 'update' | undefined };
|
||||||
|
canvas: Canvas;
|
||||||
|
}>({ name: 'transientupdate' }), // TODO
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +199,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
const emitRender = async () => {
|
const emitRender = async () => {
|
||||||
this.hooks.render.emit({
|
this.hooks.render.emit({
|
||||||
graphCore: this.dataController.graphCore,
|
graphCore: this.dataController.graphCore,
|
||||||
theme: this.themeController.specification
|
theme: this.themeController.specification,
|
||||||
});
|
});
|
||||||
this.emit('afterrender');
|
this.emit('afterrender');
|
||||||
|
|
||||||
@ -211,7 +226,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
this.hooks.datachange.emit({ data, type });
|
this.hooks.datachange.emit({ data, type });
|
||||||
this.hooks.render.emit({
|
this.hooks.render.emit({
|
||||||
graphCore: this.dataController.graphCore,
|
graphCore: this.dataController.graphCore,
|
||||||
theme: this.themeController.specification
|
theme: this.themeController.specification,
|
||||||
});
|
});
|
||||||
this.emit('afterrender');
|
this.emit('afterrender');
|
||||||
|
|
||||||
@ -226,84 +241,223 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public getViewportCenter(): PointLike {
|
||||||
* Move the graph with a relative vector.
|
const { width, height } = this.canvas.getConfig();
|
||||||
* @param dx x of the relative vector
|
return { x: width! / 2, y: height! / 2 };
|
||||||
* @param dy y of the relative vector
|
}
|
||||||
* @param animateCfg animation configurations
|
public async transform(
|
||||||
* @returns
|
options: GraphTransformOptions,
|
||||||
* @group View
|
effectTiming?: CameraAnimationOptions,
|
||||||
*/
|
): Promise<void> {
|
||||||
public move(dx: number, dy: number, animateCfg?: AnimateCfg) {
|
await this.hooks.viewportchange.emitLinearAsync({
|
||||||
// TODO
|
transform: options,
|
||||||
|
effectTiming,
|
||||||
|
});
|
||||||
|
this.emit('viewportchange', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the graph and align to a point.
|
* Stop the current transition of transform immediately.
|
||||||
* @param x position on the canvas to align
|
|
||||||
* @param y position on the canvas to align
|
|
||||||
* @param alignment alignment of the graph content
|
|
||||||
* @param animateCfg animation configurations
|
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
public moveTo(x: number, y: number, alignment: GraphAlignment, animateCfg?: AnimateCfg) {
|
public stopTransformTransition() {
|
||||||
// TODO
|
this.canvas.getCamera().cancelLandmarkAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the graph with a relative distance under viewport coordinates.
|
||||||
|
* @param dx x of the relative distance
|
||||||
|
* @param dy y of the relative distance
|
||||||
|
* @param effectTiming animation configurations
|
||||||
|
*/
|
||||||
|
public async translate(dx: number, dy: number, effectTiming?: CameraAnimationOptions) {
|
||||||
|
await this.transform(
|
||||||
|
{
|
||||||
|
translate: {
|
||||||
|
dx,
|
||||||
|
dy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
effectTiming,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the graph to destination under viewport coordinates.
|
||||||
|
* @param destination destination under viewport coordinates.
|
||||||
|
* @param effectTiming animation configurations
|
||||||
|
*/
|
||||||
|
public async translateTo({ x, y }: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||||
|
const { x: cx, y: cy } = this.getViewportCenter();
|
||||||
|
await this.translate(cx - x, cy - y, effectTiming);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zoom the graph with a relative ratio.
|
* Zoom the graph with a relative ratio.
|
||||||
* @param ratio relative ratio to zoom
|
* @param ratio relative ratio to zoom
|
||||||
* @param center zoom center
|
* @param origin origin under viewport coordinates.
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
public zoom(ratio: number, center?: Point, animateCfg?: AnimateCfg) {
|
public async zoom(ratio: number, origin?: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||||
// TODO
|
await this.transform(
|
||||||
|
{
|
||||||
|
zoom: {
|
||||||
|
ratio,
|
||||||
|
},
|
||||||
|
origin,
|
||||||
|
},
|
||||||
|
effectTiming,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zoom the graph to a specified ratio.
|
* Zoom the graph to a specified ratio.
|
||||||
* @param toRatio specified ratio
|
* @param zoom specified ratio
|
||||||
* @param center zoom center
|
* @param origin zoom center
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
public zoomTo(toRatio: number, center?: Point, animateCfg?: AnimateCfg) {
|
public async zoomTo(zoom: number, origin?: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||||
// TODO
|
await this.zoom(zoom / this.canvas.getCamera().getZoom(), origin, effectTiming);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current zoom level of camera.
|
||||||
|
* @returns current zoom
|
||||||
|
*/
|
||||||
|
public getZoom() {
|
||||||
|
return this.canvas.getCamera().getZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate the graph with a relative angle.
|
||||||
|
* @param angle
|
||||||
|
* @param origin
|
||||||
|
* @param effectTiming
|
||||||
|
*/
|
||||||
|
public async rotate(angle: number, origin?: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||||
|
await this.transform(
|
||||||
|
{
|
||||||
|
rotate: {
|
||||||
|
angle,
|
||||||
|
},
|
||||||
|
origin,
|
||||||
|
},
|
||||||
|
effectTiming,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate the graph to an absolute angle.
|
||||||
|
* @param angle
|
||||||
|
* @param origin
|
||||||
|
* @param effectTiming
|
||||||
|
*/
|
||||||
|
public async rotateTo(angle: number, origin?: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||||
|
await this.rotate(angle - this.canvas.getCamera().getRoll(), origin, effectTiming);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fit the graph content to the view.
|
* Fit the graph content to the view.
|
||||||
* @param padding padding while fitting
|
* @param options.padding padding while fitting
|
||||||
* @param rules rules for fitting
|
* @param options.rules rules for fitting
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
public fitView(padding?: Padding, rules?: FitViewRules, animateCfg?: AnimateCfg) {
|
public async fitView(
|
||||||
// TODO
|
options?: {
|
||||||
|
padding: Padding;
|
||||||
|
rules: FitViewRules;
|
||||||
|
},
|
||||||
|
effectTiming?: CameraAnimationOptions,
|
||||||
|
) {
|
||||||
|
const { padding, rules } = options || {};
|
||||||
|
const [top, right, bottom, left] = padding ? formatPadding(padding) : [0, 0, 0, 0];
|
||||||
|
const { direction = 'both', ratioRule = 'min' } = rules || {};
|
||||||
|
|
||||||
|
// Get the bounds of the whole graph.
|
||||||
|
const {
|
||||||
|
center: [graphCenterX, graphCenterY],
|
||||||
|
halfExtents,
|
||||||
|
} = this.canvas.document.documentElement.getBounds();
|
||||||
|
const origin = this.canvas.canvas2Viewport({ x: graphCenterX, y: graphCenterY });
|
||||||
|
const { width: viewportWidth, height: viewportHeight } = this.canvas.getConfig();
|
||||||
|
|
||||||
|
const graphWidth = halfExtents[0] * 2;
|
||||||
|
const graphHeight = halfExtents[1] * 2;
|
||||||
|
const tlInCanvas = this.canvas.viewport2Canvas({ x: left, y: top });
|
||||||
|
const brInCanvas = this.canvas.viewport2Canvas({
|
||||||
|
x: viewportWidth! - right,
|
||||||
|
y: viewportHeight! - bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetViewWidth = brInCanvas.x - tlInCanvas.x;
|
||||||
|
const targetViewHeight = brInCanvas.y - tlInCanvas.y;
|
||||||
|
|
||||||
|
const wRatio = targetViewWidth / graphWidth;
|
||||||
|
const hRatio = targetViewHeight / graphHeight;
|
||||||
|
|
||||||
|
let ratio: number;
|
||||||
|
if (direction === 'x') {
|
||||||
|
ratio = wRatio;
|
||||||
|
} else if (direction === 'y') {
|
||||||
|
ratio = hRatio;
|
||||||
|
} else {
|
||||||
|
ratio = ratioRule === 'max' ? Math.max(wRatio, hRatio) : Math.min(wRatio, hRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.transform(
|
||||||
|
{
|
||||||
|
translate: {
|
||||||
|
dx: viewportWidth! / 2 - origin.x,
|
||||||
|
dy: viewportHeight! / 2 - origin.y,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
ratio,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
effectTiming,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Fit the graph center to the view center.
|
* Fit the graph center to the view center.
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
public fitCenter(animateCfg?: AnimateCfg) {
|
public async fitCenter(effectTiming?: CameraAnimationOptions) {
|
||||||
// TODO
|
// Get the bounds of the whole graph.
|
||||||
|
const {
|
||||||
|
center: [graphCenterX, graphCenterY],
|
||||||
|
} = this.canvas.document.documentElement.getBounds();
|
||||||
|
await this.translateTo(
|
||||||
|
this.canvas.canvas2Viewport({ x: graphCenterX, y: graphCenterY }),
|
||||||
|
effectTiming,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Move the graph to make the item align the view center.
|
* Move the graph to make the item align the view center.
|
||||||
* @param item node/edge/combo item or its id
|
* @param item node/edge/combo item or its id
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
public focusItem(ids: ID | ID[], animateCfg?: AnimateCfg) {
|
public async focusItem(id: ID | ID[], effectTiming?: CameraAnimationOptions) {
|
||||||
// TODO
|
let bounds: AABB | null = null;
|
||||||
|
for (const itemId of !isArray(id) ? [id] : id) {
|
||||||
|
const item = this.itemController.getItemById(itemId);
|
||||||
|
if (item) {
|
||||||
|
const itemBounds = item.group.getBounds();
|
||||||
|
if (!bounds) {
|
||||||
|
bounds = itemBounds;
|
||||||
|
} else {
|
||||||
|
bounds.add(itemBounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bounds) {
|
||||||
|
const {
|
||||||
|
center: [itemCenterX, itemCenterY],
|
||||||
|
} = bounds;
|
||||||
|
await this.translateTo(
|
||||||
|
this.canvas.canvas2Viewport({ x: itemCenterX, y: itemCenterY }),
|
||||||
|
effectTiming,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== item operations =====
|
// ===== item operations =====
|
||||||
@ -371,7 +525,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
public getRelatedEdgesData(nodeId: ID, direction: 'in' | 'out' | 'both' = 'both'): EdgeModel[] {
|
public getRelatedEdgesData(nodeId: ID, direction: 'in' | 'out' | 'both' = 'both'): EdgeModel[] {
|
||||||
return this.dataController.findRelatedEdgeIds(nodeId, direction);
|
return this.dataController.findRelatedEdgeIds(nodeId, direction);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Get one-hop node ids from a start node.
|
* Get one-hop node ids from a start node.
|
||||||
* @param nodeId id of the start node
|
* @param nodeId id of the start node
|
||||||
* @returns one-hop node ids
|
* @returns one-hop node ids
|
||||||
@ -430,7 +584,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
type: itemType,
|
type: itemType,
|
||||||
changes: graphCore.reduceChanges(event.changes),
|
changes: graphCore.reduceChanges(event.changes),
|
||||||
graphCore,
|
graphCore,
|
||||||
theme: specification
|
theme: specification,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -466,7 +620,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
type: itemType,
|
type: itemType,
|
||||||
changes: event.changes,
|
changes: event.changes,
|
||||||
graphCore,
|
graphCore,
|
||||||
theme: specification
|
theme: specification,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.hooks.datachange.emit({
|
this.hooks.datachange.emit({
|
||||||
@ -505,7 +659,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
type: itemType,
|
type: itemType,
|
||||||
changes: event.changes,
|
changes: event.changes,
|
||||||
graphCore,
|
graphCore,
|
||||||
theme: specification
|
theme: specification,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -587,7 +741,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
* @returns rendering bounding box. returns false if the item is not exist
|
* @returns rendering bounding box. returns false if the item is not exist
|
||||||
* @group Item
|
* @group Item
|
||||||
*/
|
*/
|
||||||
public getRenderBBox(id: ID | undefined): AABB | false{
|
public getRenderBBox(id: ID | undefined): AABB | false {
|
||||||
if (!id) return this.canvas.getRoot().getRenderBounds();
|
if (!id) return this.canvas.getRoot().getRenderBounds();
|
||||||
return this.itemController.getItemBBox(id);
|
return this.itemController.getItemBBox(id);
|
||||||
}
|
}
|
||||||
@ -769,7 +923,11 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
|||||||
* @returns upserted shape or group
|
* @returns upserted shape or group
|
||||||
* @group Interaction
|
* @group Interaction
|
||||||
*/
|
*/
|
||||||
public drawTransient(type: ITEM_TYPE | SHAPE_TYPE, id: ID, config: { action: 'remove' | 'add' | 'update' | undefined, style: ShapeStyle}): DisplayObject {
|
public drawTransient(
|
||||||
|
type: ITEM_TYPE | SHAPE_TYPE,
|
||||||
|
id: ID,
|
||||||
|
config: { action: 'remove' | 'add' | 'update' | undefined; style: ShapeStyle },
|
||||||
|
): DisplayObject {
|
||||||
this.hooks.transientupdate.emit({ type, id, config, canvas: this.transientCanvas });
|
this.hooks.transientupdate.emit({ type, id, config, canvas: this.transientCanvas });
|
||||||
return this.itemController.getTransient(String(id));
|
return this.itemController.getTransient(String(id));
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IAnimationEffectTiming } from '@antv/g';
|
||||||
|
|
||||||
export interface AnimateCfg {
|
export interface AnimateCfg {
|
||||||
/**
|
/**
|
||||||
* Whether enable animation.
|
* Whether enable animation.
|
||||||
@ -39,7 +41,7 @@ export interface AnimateCfg {
|
|||||||
* @type {function}
|
* @type {function}
|
||||||
*/
|
*/
|
||||||
resumeCallback?: () => void;
|
resumeCallback?: () => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type AnimateWhen = 'show' | 'exit' | 'update' | 'last';
|
export type AnimateWhen = 'show' | 'exit' | 'update' | 'last';
|
||||||
|
|
||||||
@ -48,3 +50,6 @@ export interface AnimateAttr {
|
|||||||
type: string;
|
type: string;
|
||||||
[param: string]: unknown;
|
[param: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CameraAnimationOptions
|
||||||
|
extends Pick<IAnimationEffectTiming, 'duration' | 'easing' | 'easingFunction'> {}
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
import EventEmitter from '@antv/event-emitter';
|
import EventEmitter from '@antv/event-emitter';
|
||||||
import { Canvas, AABB, DisplayObject } from '@antv/g';
|
import { AABB, Canvas, DisplayObject, PointLike } from '@antv/g';
|
||||||
import { ID } from '@antv/graphlib';
|
import { ID } from '@antv/graphlib';
|
||||||
import { Hooks } from '../types/hook';
|
import { Hooks } from '../types/hook';
|
||||||
import { AnimateCfg } from './animate';
|
import { CameraAnimationOptions } from './animate';
|
||||||
import { BehaviorObjectOptionsOf, BehaviorOptionsOf, BehaviorRegistry } from './behavior';
|
import { BehaviorObjectOptionsOf, BehaviorOptionsOf, BehaviorRegistry } from './behavior';
|
||||||
import { ComboModel, ComboUserModel } from './combo';
|
import { ComboModel, ComboUserModel } from './combo';
|
||||||
import { Padding, Point } from './common';
|
import { Padding, Point } from './common';
|
||||||
import { DataChangeType, GraphData } from './data';
|
import { GraphData } from './data';
|
||||||
import { EdgeModel, EdgeUserModel } from './edge';
|
import { EdgeModel, EdgeUserModel } from './edge';
|
||||||
import { ITEM_TYPE, SHAPE_TYPE } from './item';
|
import { ITEM_TYPE, SHAPE_TYPE } from './item';
|
||||||
import { LayoutOptions } from './layout';
|
import { LayoutOptions } from './layout';
|
||||||
import { NodeModel, NodeUserModel } from './node';
|
import { NodeModel, NodeUserModel } from './node';
|
||||||
import { Specification } from './spec';
|
import { Specification } from './spec';
|
||||||
import { ThemeRegistry } from './theme';
|
import { ThemeRegistry } from './theme';
|
||||||
import { FitViewRules, GraphAlignment } from './view';
|
import { FitViewRules, GraphTransformOptions } from './view';
|
||||||
|
|
||||||
export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry, T extends ThemeRegistry = ThemeRegistry> extends EventEmitter {
|
export interface IGraph<
|
||||||
|
B extends BehaviorRegistry = BehaviorRegistry,
|
||||||
|
T extends ThemeRegistry = ThemeRegistry,
|
||||||
|
> extends EventEmitter {
|
||||||
hooks: Hooks;
|
hooks: Hooks;
|
||||||
canvas: Canvas;
|
canvas: Canvas;
|
||||||
destroyed: boolean;
|
destroyed: boolean;
|
||||||
@ -182,63 +185,98 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry, T extends
|
|||||||
* Move the graph with a relative vector.
|
* Move the graph with a relative vector.
|
||||||
* @param dx x of the relative vector
|
* @param dx x of the relative vector
|
||||||
* @param dy y of the relative vector
|
* @param dy y of the relative vector
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
move: (dx: number, dy: number, animateCfg?: AnimateCfg) => void;
|
translate: (dx: number, dy: number, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Move the graph and align to a point.
|
* Move the graph and align to a point.
|
||||||
* @param x position on the canvas to align
|
* @param point position on the canvas to align
|
||||||
* @param y position on the canvas to align
|
* @param effectTiming animation configurations
|
||||||
* @param alignment alignment of the graph content
|
|
||||||
* @param animateCfg animation configurations
|
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
moveTo: (x: number, y: number, alignment: GraphAlignment, animateCfg?: AnimateCfg) => void;
|
translateTo: (point: PointLike, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Return the current zoom level of camera.
|
||||||
|
* @returns current zoom
|
||||||
|
*/
|
||||||
|
getZoom: () => number;
|
||||||
/**
|
/**
|
||||||
* Zoom the graph with a relative ratio.
|
* Zoom the graph with a relative ratio.
|
||||||
* @param ratio relative ratio to zoom
|
* @param ratio relative ratio to zoom
|
||||||
* @param center zoom center
|
* @param center zoom center
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
zoom: (ratio: number, center?: Point, animateCfg?: AnimateCfg) => void;
|
zoom: (ratio: number, center?: Point, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Zoom the graph to a specified ratio.
|
* Zoom the graph to a specified ratio.
|
||||||
* @param toRatio specified ratio
|
* @param toRatio specified ratio
|
||||||
* @param center zoom center
|
* @param center zoom center
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
zoomTo: (toRatio: number, center?: Point, animateCfg?: AnimateCfg) => void;
|
zoomTo: (toRatio: number, center?: Point, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Rotate the graph with a relative angle in clockwise.
|
||||||
|
* @param angle
|
||||||
|
* @param center
|
||||||
|
* @param effectTiming
|
||||||
|
*/
|
||||||
|
rotate: (angle: number, center?: Point, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Rotate the graph to an absolute angle in clockwise.
|
||||||
|
* @param toAngle
|
||||||
|
* @param center
|
||||||
|
* @param effectTiming
|
||||||
|
*/
|
||||||
|
rotateTo: (
|
||||||
|
toAngle: number,
|
||||||
|
center?: Point,
|
||||||
|
effectTiming?: CameraAnimationOptions,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the graph with a CSS-Transform-like syntax.
|
||||||
|
* @param options
|
||||||
|
* @param effectTiming
|
||||||
|
*/
|
||||||
|
transform: (
|
||||||
|
options: GraphTransformOptions,
|
||||||
|
effectTiming?: CameraAnimationOptions,
|
||||||
|
) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Stop the current transition of transform immediately.
|
||||||
|
*/
|
||||||
|
stopTransformTransition: () => void;
|
||||||
|
/**
|
||||||
|
* Return the center of viewport, e.g. for a 500 * 500 canvas, its center is [250, 250].
|
||||||
|
*/
|
||||||
|
getViewportCenter: () => PointLike;
|
||||||
/**
|
/**
|
||||||
* Fit the graph content to the view.
|
* Fit the graph content to the view.
|
||||||
* @param padding padding while fitting
|
* @param options.padding padding while fitting
|
||||||
* @param rules rules for fitting
|
* @param options.rules rules for fitting
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
* @returns
|
||||||
* @group View
|
* @group View
|
||||||
*/
|
*/
|
||||||
fitView: (padding?: Padding, rules?: FitViewRules, animateCfg?: AnimateCfg) => void;
|
fitView: (
|
||||||
|
options?: {
|
||||||
|
padding: Padding;
|
||||||
|
rules: FitViewRules;
|
||||||
|
},
|
||||||
|
effectTiming?: CameraAnimationOptions,
|
||||||
|
) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Fit the graph center to the view center.
|
* Fit the graph center to the view center.
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
* @returns
|
||||||
* @group View
|
* @group View
|
||||||
*/
|
*/
|
||||||
fitCenter: (animateCfg?: AnimateCfg) => void;
|
fitCenter: (effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Move the graph to make the item align the view center.
|
* Move the graph to make the item align the view center.
|
||||||
* @param item node/edge/combo item or its id
|
* @param item node/edge/combo item or its id
|
||||||
* @param animateCfg animation configurations
|
* @param effectTiming animation configurations
|
||||||
* @returns
|
|
||||||
* @group View
|
|
||||||
*/
|
*/
|
||||||
focusItem: (ids: ID | ID[], animateCfg?: AnimateCfg) => void;
|
focusItem: (id: ID | ID[], effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||||
|
|
||||||
// ===== item operations =====
|
// ===== item operations =====
|
||||||
/**
|
/**
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { Canvas } from '@antv/g';
|
import { Canvas } from '@antv/g';
|
||||||
import { DataChangeType, GraphCore, GraphData } from "./data";
|
import { GraphChange, ID } from '@antv/graphlib';
|
||||||
import { NodeModelData } from "./node";
|
import { CameraAnimationOptions } from './animate';
|
||||||
import { EdgeModelData } from "./edge";
|
import { DataChangeType, GraphCore, GraphData } from './data';
|
||||||
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from "./item";
|
import { EdgeModelData } from './edge';
|
||||||
import { GraphChange, ID } from "@antv/graphlib";
|
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from './item';
|
||||||
import { LayoutOptions } from "./layout";
|
import { LayoutOptions } from './layout';
|
||||||
import { ThemeSpecification } from "./theme";
|
import { NodeModelData } from './node';
|
||||||
|
import { ThemeSpecification } from './theme';
|
||||||
|
import { GraphTransformOptions } from './view';
|
||||||
|
|
||||||
export interface IHook<T> {
|
export interface IHook<T> {
|
||||||
name: string;
|
name: string;
|
||||||
@ -16,18 +18,23 @@ export interface IHook<T> {
|
|||||||
emitLinearAsync: (param: T) => Promise<void>;
|
emitLinearAsync: (param: T) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ViewportChangeHookParams = {
|
||||||
|
transform: GraphTransformOptions;
|
||||||
|
effectTiming?: CameraAnimationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
export interface Hooks {
|
export interface Hooks {
|
||||||
init: IHook<{
|
init: IHook<{
|
||||||
canvases: {
|
canvases: {
|
||||||
background: Canvas,
|
background: Canvas;
|
||||||
main: Canvas,
|
main: Canvas;
|
||||||
transient: Canvas
|
transient: Canvas;
|
||||||
}
|
};
|
||||||
}>;
|
}>;
|
||||||
// data
|
// data
|
||||||
datachange: IHook<{
|
datachange: IHook<{
|
||||||
type: DataChangeType;
|
type: DataChangeType;
|
||||||
data: GraphData
|
data: GraphData;
|
||||||
}>;
|
}>;
|
||||||
itemchange: IHook<{
|
itemchange: IHook<{
|
||||||
type: ITEM_TYPE;
|
type: ITEM_TYPE;
|
||||||
@ -35,7 +42,7 @@ export interface Hooks {
|
|||||||
graphCore: GraphCore;
|
graphCore: GraphCore;
|
||||||
theme: ThemeSpecification;
|
theme: ThemeSpecification;
|
||||||
}>;
|
}>;
|
||||||
render: IHook<{ graphCore: GraphCore, theme: ThemeSpecification }>; // TODO: define param template
|
render: IHook<{ graphCore: GraphCore; theme: ThemeSpecification }>; // TODO: define param template
|
||||||
layout: IHook<{ graphCore: GraphCore; options?: LayoutOptions }>; // TODO: define param template
|
layout: IHook<{ graphCore: GraphCore; options?: LayoutOptions }>; // TODO: define param template
|
||||||
// 'updatelayout': IHook<any>; // TODO: define param template
|
// 'updatelayout': IHook<any>; // TODO: define param template
|
||||||
modechange: IHook<{ mode: string }>;
|
modechange: IHook<{ mode: string }>;
|
||||||
@ -54,12 +61,12 @@ export interface Hooks {
|
|||||||
id: ID;
|
id: ID;
|
||||||
canvas: Canvas;
|
canvas: Canvas;
|
||||||
config: {
|
config: {
|
||||||
style: ShapeStyle,
|
style: ShapeStyle;
|
||||||
action: 'remove' | 'add' | 'update' | undefined
|
action: 'remove' | 'add' | 'update' | undefined;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
// TODO: define param template
|
// TODO: define param template
|
||||||
// 'viewportchange': IHook<any>; // TODO: define param template
|
viewportchange: IHook<ViewportChangeHookParams>;
|
||||||
// 'destroy': IHook<any>; // TODO: define param template
|
// 'destroy': IHook<any>; // TODO: define param template
|
||||||
// TODO: more timecycles here
|
// TODO: more timecycles here
|
||||||
}
|
}
|
||||||
|
@ -4,4 +4,27 @@ export interface FitViewRules {
|
|||||||
ratioRule?: 'max' | 'min'; // Ratio rule to fit.
|
ratioRule?: 'max' | 'min'; // Ratio rule to fit.
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GraphAlignment = 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' | 'center' | [number, number];
|
export type GraphAlignment =
|
||||||
|
| 'left-top'
|
||||||
|
| 'right-top'
|
||||||
|
| 'left-bottom'
|
||||||
|
| 'right-bottom'
|
||||||
|
| 'center'
|
||||||
|
| [number, number];
|
||||||
|
|
||||||
|
export type GraphTransformOptions = {
|
||||||
|
translate?: {
|
||||||
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
};
|
||||||
|
rotate?: {
|
||||||
|
angle: number;
|
||||||
|
};
|
||||||
|
zoom?: {
|
||||||
|
ratio: number;
|
||||||
|
};
|
||||||
|
origin?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -148,7 +148,7 @@ export const mergeStyles = (styleMaps: ItemShapeStyles[]) => {
|
|||||||
if (i > 0) currentResult = merge2Styles(currentResult, styleMap);
|
if (i > 0) currentResult = merge2Styles(currentResult, styleMap);
|
||||||
});
|
});
|
||||||
return currentResult;
|
return currentResult;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge two shape style map including undefined value in incoming map.
|
* Merge two shape style map including undefined value in incoming map.
|
||||||
@ -160,17 +160,17 @@ const merge2Styles = (styleMap1: ItemShapeStyles, styleMap2: ItemShapeStyles) =>
|
|||||||
if (!styleMap1) return clone(styleMap2);
|
if (!styleMap1) return clone(styleMap2);
|
||||||
else if (!styleMap2) return clone(styleMap1);
|
else if (!styleMap2) return clone(styleMap1);
|
||||||
const mergedStyle = clone(styleMap1);
|
const mergedStyle = clone(styleMap1);
|
||||||
Object.keys(styleMap2).forEach(shapeId => {
|
Object.keys(styleMap2).forEach((shapeId) => {
|
||||||
const style = styleMap2[shapeId];
|
const style = styleMap2[shapeId];
|
||||||
mergedStyle[shapeId] = mergedStyle[shapeId] || {};
|
mergedStyle[shapeId] = mergedStyle[shapeId] || {};
|
||||||
if (!style) return;
|
if (!style) return;
|
||||||
Object.keys(style).forEach(styleName => {
|
Object.keys(style).forEach((styleName) => {
|
||||||
const value = style[styleName];
|
const value = style[styleName];
|
||||||
mergedStyle[shapeId][styleName] = value;
|
mergedStyle[shapeId][styleName] = value;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return mergedStyle;
|
return mergedStyle;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether two polygons are intersected.
|
* Whether two polygons are intersected.
|
||||||
@ -179,10 +179,10 @@ const merge2Styles = (styleMap1: ItemShapeStyles, styleMap2: ItemShapeStyles) =>
|
|||||||
*/
|
*/
|
||||||
export const isPolygonsIntersect = (points1: number[][], points2: number[][]): boolean => {
|
export const isPolygonsIntersect = (points1: number[][], points2: number[][]): boolean => {
|
||||||
const getBBox = (points): Partial<AABB> => {
|
const getBBox = (points): Partial<AABB> => {
|
||||||
const xArr = points.map(p => p[0]);
|
const xArr = points.map((p) => p[0]);
|
||||||
const yArr = points.map(p => p[1]);
|
const yArr = points.map((p) => p[1]);
|
||||||
return {
|
return {
|
||||||
min: [Math.min.apply(null, xArr), Math.min.apply(null, yArr), 0],
|
min: [Math.min.apply(null, xArr), Math.min.apply(null, yArr), 0],
|
||||||
max: [Math.max.apply(null, xArr), Math.max.apply(null, yArr), 0],
|
max: [Math.max.apply(null, xArr), Math.max.apply(null, yArr), 0],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -235,7 +235,7 @@ export const isPolygonsIntersect = (points1: number[][], points2: number[][]): b
|
|||||||
|
|
||||||
let isIn = false;
|
let isIn = false;
|
||||||
// 判定点是否在多边形内部,一旦有一个点在另一个多边形内,则返回
|
// 判定点是否在多边形内部,一旦有一个点在另一个多边形内,则返回
|
||||||
points2.forEach(point => {
|
points2.forEach((point) => {
|
||||||
if (isPointInPolygon(points1, point[0], point[1])) {
|
if (isPointInPolygon(points1, point[0], point[1])) {
|
||||||
isIn = true;
|
isIn = true;
|
||||||
return false;
|
return false;
|
||||||
@ -244,7 +244,7 @@ export const isPolygonsIntersect = (points1: number[][], points2: number[][]): b
|
|||||||
if (isIn) {
|
if (isIn) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
points1.forEach(point => {
|
points1.forEach((point) => {
|
||||||
if (isPointInPolygon(points2, point[0], point[1])) {
|
if (isPointInPolygon(points2, point[0], point[1])) {
|
||||||
isIn = true;
|
isIn = true;
|
||||||
return false;
|
return false;
|
||||||
@ -257,7 +257,7 @@ export const isPolygonsIntersect = (points1: number[][], points2: number[][]): b
|
|||||||
const lines1 = parseToLines(points1);
|
const lines1 = parseToLines(points1);
|
||||||
const lines2 = parseToLines(points2);
|
const lines2 = parseToLines(points2);
|
||||||
let isIntersect = false;
|
let isIntersect = false;
|
||||||
lines2.forEach(line => {
|
lines2.forEach((line) => {
|
||||||
if (lineIntersectPolygon(lines1, line)) {
|
if (lineIntersectPolygon(lines1, line)) {
|
||||||
isIntersect = true;
|
isIntersect = true;
|
||||||
return false;
|
return false;
|
||||||
@ -333,11 +333,11 @@ const onSegment = (p1, p2, q) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
const lineIntersectPolygon = (lines, line) => {
|
const lineIntersectPolygon = (lines, line) => {
|
||||||
let isIntersect = false;
|
let isIntersect = false;
|
||||||
lines.forEach(l => {
|
lines.forEach((l) => {
|
||||||
if (getLineIntersect(l.from, l.to, line.from, line.to)) {
|
if (getLineIntersect(l.from, l.to, line.from, line.to)) {
|
||||||
isIntersect = true;
|
isIntersect = true;
|
||||||
return false;
|
return false;
|
||||||
|
608
packages/g6/tests/unit/view-spec.ts
Normal file
608
packages/g6/tests/unit/view-spec.ts
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
import { Graph, Layout, LayoutMapping } from '@antv/layout';
|
||||||
|
import { Circle } from '@antv/g';
|
||||||
|
import G6, { IGraph, stdLib } from '../../src/index';
|
||||||
|
import { data } from '../datasets/dataset1';
|
||||||
|
const container = document.createElement('div');
|
||||||
|
|
||||||
|
// document.getElementById('__jest-electron-test-results__')?.style.position = 'absolute';
|
||||||
|
document.querySelector('body')!.appendChild(container);
|
||||||
|
|
||||||
|
describe('viewport', () => {
|
||||||
|
let graph: any;
|
||||||
|
it('should translate viewport without animation correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', () => {
|
||||||
|
graph.translate(250, 250);
|
||||||
|
let [px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBe(0);
|
||||||
|
expect(py).toBe(0);
|
||||||
|
|
||||||
|
graph.translate(-250, -250);
|
||||||
|
[px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBe(250);
|
||||||
|
expect(py).toBe(250);
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should translate viewport with animation correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', async () => {
|
||||||
|
await graph.translate(249, 249, {
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('viewportchange', ({ translate }) => {
|
||||||
|
expect(translate.dx).toBe(-250);
|
||||||
|
expect(translate.dy).toBe(-250);
|
||||||
|
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBe(251);
|
||||||
|
expect(py).toBe(251);
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.translateTo(
|
||||||
|
{ x: 500, y: 500 },
|
||||||
|
{
|
||||||
|
duration: 2000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should zoom viewport without animation correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', async () => {
|
||||||
|
graph.once('viewportchange', ({ zoom }) => {
|
||||||
|
expect(zoom.ratio).toBe(0.5);
|
||||||
|
expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||||
|
});
|
||||||
|
await graph.zoom(0.5, { x: 250, y: 250 });
|
||||||
|
expect(graph.getZoom()).toBe(0.5);
|
||||||
|
|
||||||
|
graph.once('viewportchange', ({ zoom }) => {
|
||||||
|
expect(zoom.ratio).toBe(2);
|
||||||
|
expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||||
|
});
|
||||||
|
await graph.zoom(2, { x: 250, y: 250 });
|
||||||
|
|
||||||
|
const op = graph.canvas.canvas2Viewport({ x: 450, y: 250 });
|
||||||
|
graph.once('viewportchange', ({ zoom }) => {
|
||||||
|
expect(zoom.ratio).toBe(1.2);
|
||||||
|
expect(graph.canvas.getCamera().getZoom()).toBe(1.2);
|
||||||
|
|
||||||
|
// Zoom origin should be fixed.
|
||||||
|
const { x, y } = graph.canvas.canvas2Viewport({ x: 450, y: 250 });
|
||||||
|
expect(x).toBeCloseTo(op.x);
|
||||||
|
expect(y).toBeCloseTo(op.y);
|
||||||
|
});
|
||||||
|
await graph.zoomTo(1.2, { x: 450, y: 250 });
|
||||||
|
expect(graph.getZoom()).toBe(1.2);
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should zoom viewport with animation correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', async () => {
|
||||||
|
graph.once('viewportchange', ({ zoom }) => {
|
||||||
|
expect(zoom.ratio).toBe(0.5);
|
||||||
|
expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||||
|
});
|
||||||
|
await graph.zoom(
|
||||||
|
0.5,
|
||||||
|
{ x: 250, y: 250 },
|
||||||
|
{
|
||||||
|
duration: 2000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
graph.once('viewportchange', ({ zoom }) => {
|
||||||
|
expect(zoom.ratio).toBe(2);
|
||||||
|
expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||||
|
expect(graph.canvas.getCamera().getPosition()[0]).toBe(250);
|
||||||
|
expect(graph.canvas.getCamera().getPosition()[1]).toBe(250);
|
||||||
|
});
|
||||||
|
await graph.zoom(
|
||||||
|
2,
|
||||||
|
{ x: 250, y: 250 },
|
||||||
|
{
|
||||||
|
duration: 2000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// const op = graph.canvas.canvas2Viewport({ x: 450, y: 250 });
|
||||||
|
// graph.once('viewportchange', ({ zoom }) => {
|
||||||
|
// expect(zoom.ratio).toBe(3);
|
||||||
|
// expect(graph.canvas.getCamera().getZoom()).toBe(3);
|
||||||
|
// // Zoom origin should be fixed.
|
||||||
|
// const { x, y } = graph.canvas.canvas2Viewport({ x: 450, y: 250 });
|
||||||
|
// expect(x).toBeCloseTo(op.x);
|
||||||
|
// expect(y).toBeCloseTo(op.y);
|
||||||
|
// });
|
||||||
|
// await graph.zoomTo(
|
||||||
|
// 3,
|
||||||
|
// { x: 450, y: 250 },
|
||||||
|
// {
|
||||||
|
// duration: 1000,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// graph.once('viewportchange', ({ zoom }) => {
|
||||||
|
// expect(zoom.ratio).toBe(1 / 3);
|
||||||
|
// expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||||
|
// expect(graph.canvas.getCamera().getPosition()[0]).toBe(250);
|
||||||
|
// expect(graph.canvas.getCamera().getPosition()[1]).toBe(250);
|
||||||
|
// });
|
||||||
|
// await graph.zoomTo(
|
||||||
|
// 1,
|
||||||
|
// { x: 250, y: 250 },
|
||||||
|
// {
|
||||||
|
// duration: 1000,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rotate viewport correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', async () => {
|
||||||
|
graph.once('viewportchange', ({ rotate }) => {
|
||||||
|
expect(rotate.angle).toBe(30);
|
||||||
|
expect(graph.canvas.getCamera().getRoll()).toBe(30);
|
||||||
|
});
|
||||||
|
await graph.rotateTo(30, undefined, {
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('viewportchange', ({ rotate }) => {
|
||||||
|
expect(rotate.angle).toBe(30);
|
||||||
|
expect(graph.canvas.getCamera().getRoll()).toBe(60);
|
||||||
|
});
|
||||||
|
await graph.rotateTo(60, undefined, {
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('viewportchange', ({ rotate }) => {
|
||||||
|
expect(rotate.angle).toBe(-30);
|
||||||
|
expect(graph.canvas.getCamera().getRoll()).toBe(30);
|
||||||
|
});
|
||||||
|
await graph.rotateTo(30, undefined, {
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('viewportchange', ({ rotate }) => {
|
||||||
|
expect(rotate.angle).toBe(-29);
|
||||||
|
expect(graph.canvas.getCamera().getRoll()).toBe(1);
|
||||||
|
});
|
||||||
|
// without animation
|
||||||
|
await graph.rotate(-29);
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform viewport without animation correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', async () => {
|
||||||
|
const origin = new Circle({
|
||||||
|
style: {
|
||||||
|
cx: 450,
|
||||||
|
cy: 250,
|
||||||
|
r: 30,
|
||||||
|
fill: 'green',
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
graph.canvas.appendChild(origin);
|
||||||
|
|
||||||
|
// graph.once('viewportchange', ({ zoom }) => {
|
||||||
|
// expect(zoom.ratio).toBe(0.5);
|
||||||
|
// expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||||
|
// });
|
||||||
|
// await graph.transform({
|
||||||
|
// zoom: {
|
||||||
|
// ratio: 0.5,
|
||||||
|
// },
|
||||||
|
// origin: { x: 250, y: 250 },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// graph.once('viewportchange', ({ zoom, rotate }) => {
|
||||||
|
// expect(zoom.ratio).toBe(2);
|
||||||
|
// expect(rotate.angle).toBe(30);
|
||||||
|
// expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||||
|
// expect(graph.canvas.getCamera().getRoll()).toBe(30);
|
||||||
|
// });
|
||||||
|
// await graph.transform({
|
||||||
|
// zoom: {
|
||||||
|
// ratio: 2,
|
||||||
|
// },
|
||||||
|
// rotate: {
|
||||||
|
// angle: 30,
|
||||||
|
// },
|
||||||
|
// origin: { x: 250, y: 250 },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// graph.once('viewportchange', ({ rotate }) => {
|
||||||
|
// expect(rotate.angle).toBe(-30);
|
||||||
|
// expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||||
|
// expect(graph.canvas.getCamera().getRoll()).toBe(0);
|
||||||
|
// });
|
||||||
|
// await graph.transform({
|
||||||
|
// rotate: {
|
||||||
|
// angle: -30,
|
||||||
|
// },
|
||||||
|
// origin: { x: 250, y: 250 },
|
||||||
|
// });
|
||||||
|
|
||||||
|
graph.once('viewportchange', ({ translate }) => {
|
||||||
|
expect(translate.dx).toBe(-250);
|
||||||
|
expect(translate.dy).toBe(-250);
|
||||||
|
// expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||||
|
// expect(graph.canvas.getCamera().getRoll()).toBe(0);
|
||||||
|
});
|
||||||
|
await graph.transform({
|
||||||
|
translate: {
|
||||||
|
dx: -250,
|
||||||
|
dy: -250,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('viewportchange', ({ zoom }) => {
|
||||||
|
expect(zoom.ratio).toBe(0.5);
|
||||||
|
expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||||
|
});
|
||||||
|
await graph.transform({
|
||||||
|
zoom: {
|
||||||
|
ratio: 0.5,
|
||||||
|
},
|
||||||
|
origin: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.transform({
|
||||||
|
translate: {
|
||||||
|
dx: 250,
|
||||||
|
dy: 250,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
ratio: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await graph.transform({
|
||||||
|
translate: {
|
||||||
|
dx: -250,
|
||||||
|
dy: -250,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// graph.once('viewportchange', ({ rotate }) => {
|
||||||
|
// // expect(rotate.angle).toBe(0);
|
||||||
|
// expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||||
|
// // expect(graph.canvas.getCamera().getRoll()).toBe(0);
|
||||||
|
// });
|
||||||
|
// await graph.transform({
|
||||||
|
// // rotate: {
|
||||||
|
// // angle: 0,
|
||||||
|
// // },
|
||||||
|
// zoom: {
|
||||||
|
// ratio: 1,
|
||||||
|
// },
|
||||||
|
// origin: { x: 450, y: 250 },
|
||||||
|
// });
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
origin.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop the current transition of transform correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', async () => {
|
||||||
|
graph.once('viewportchange', ({ translate }) => {
|
||||||
|
expect(translate.dx).toBe(-250);
|
||||||
|
expect(translate.dy).toBe(-250);
|
||||||
|
});
|
||||||
|
await graph.transform(
|
||||||
|
{
|
||||||
|
translate: {
|
||||||
|
dx: -250,
|
||||||
|
dy: -250,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: 2000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
graph.stopTransformTransition();
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fitCenter with transition correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', async () => {
|
||||||
|
await graph.translate(249, 249, {
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('viewportchange', ({ translate }) => {
|
||||||
|
expect(translate.dx).toBeCloseTo(-249);
|
||||||
|
expect(translate.dy).toBeCloseTo(-249);
|
||||||
|
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBeCloseTo(250);
|
||||||
|
expect(py).toBeCloseTo(250);
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.fitCenter({
|
||||||
|
duration: 2000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.zoom(0.5);
|
||||||
|
await graph.translate(249, 249);
|
||||||
|
graph.once('viewportchange', ({ translate }) => {
|
||||||
|
expect(translate.dx).toBeCloseTo(-249);
|
||||||
|
expect(translate.dy).toBeCloseTo(-249);
|
||||||
|
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBeCloseTo(250);
|
||||||
|
expect(py).toBeCloseTo(250);
|
||||||
|
});
|
||||||
|
await graph.fitCenter();
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should focusItem with transition correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', async () => {
|
||||||
|
const nodesData = graph.getAllNodesData();
|
||||||
|
expect(nodesData[0].id).toBe('Argentina');
|
||||||
|
expect(nodesData[0].data.x).toBe(450);
|
||||||
|
expect(nodesData[0].data.y).toBe(250);
|
||||||
|
|
||||||
|
expect(nodesData[4].id).toBe('Colombia');
|
||||||
|
expect(nodesData[4].data.x).toBeCloseTo(391.421356237309);
|
||||||
|
expect(nodesData[4].data.y).toBeCloseTo(391.4213562373095);
|
||||||
|
|
||||||
|
graph.once('viewportchange', () => {
|
||||||
|
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBeCloseTo(450);
|
||||||
|
expect(py).toBeCloseTo(250);
|
||||||
|
});
|
||||||
|
await graph.focusItem('Argentina', {
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('viewportchange', () => {
|
||||||
|
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBeCloseTo(391.421356237309);
|
||||||
|
expect(py).toBeCloseTo(391.4213562373095);
|
||||||
|
});
|
||||||
|
await graph.focusItem('Colombia', {
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.focusItem('Spain', {
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('viewportchange', () => {
|
||||||
|
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBe(450);
|
||||||
|
expect(py).toBe(250);
|
||||||
|
});
|
||||||
|
await graph.focusItem('Argentina');
|
||||||
|
|
||||||
|
graph.once('viewportchange', () => {
|
||||||
|
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBeCloseTo(250);
|
||||||
|
expect(py).toBeCloseTo(250);
|
||||||
|
});
|
||||||
|
await graph.focusItem(nodesData.map((node) => node.id));
|
||||||
|
|
||||||
|
graph.focusItem('Non existed node');
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fitView with transition correctly.', (done) => {
|
||||||
|
graph = new G6.Graph({
|
||||||
|
container,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
type: 'graph',
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
center: [250, 250],
|
||||||
|
radius: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.once('afterlayout', async () => {
|
||||||
|
const nodesData = graph.getAllNodesData();
|
||||||
|
expect(nodesData[0].id).toBe('Argentina');
|
||||||
|
expect(nodesData[0].data.x).toBe(450);
|
||||||
|
expect(nodesData[0].data.y).toBe(250);
|
||||||
|
|
||||||
|
expect(nodesData[4].id).toBe('Colombia');
|
||||||
|
expect(nodesData[4].data.x).toBeCloseTo(391.421356237309);
|
||||||
|
expect(nodesData[4].data.y).toBeCloseTo(391.4213562373095);
|
||||||
|
|
||||||
|
graph.once('viewportchange', () => {
|
||||||
|
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||||
|
expect(px).toBeCloseTo(450);
|
||||||
|
expect(py).toBeCloseTo(250);
|
||||||
|
});
|
||||||
|
await graph.focusItem('Argentina', {
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
});
|
||||||
|
await graph.zoom(0.5, undefined, {
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
});
|
||||||
|
await graph.fitView(
|
||||||
|
{
|
||||||
|
padding: [50, 50, 50, 50],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await graph.translate(100, 100, {
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
});
|
||||||
|
await graph.fitView(
|
||||||
|
{
|
||||||
|
padding: [150, 100],
|
||||||
|
rules: {
|
||||||
|
direction: 'both',
|
||||||
|
ratioRule: 'min',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await graph.fitView(undefined, {
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'ease-in',
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user