Skip to content

Commit

Permalink
使用相机实现视口相关功能 (#4348)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
xiaoiver authored Mar 20, 2023
1 parent 8f68192 commit 918584d
Show file tree
Hide file tree
Showing 10 changed files with 1,134 additions and 178 deletions.
1 change: 1 addition & 0 deletions packages/g6/src/runtime/controller/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './data';
export * from './interaction';
export * from './item';
export * from './layout';
export * from './viewport';
export * from './theme';
export * from './extension';
86 changes: 55 additions & 31 deletions packages/g6/src/runtime/controller/item.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { AABB, Canvas, DisplayObject, Group } from '@antv/g';
import { GraphChange, ID } from '@antv/graphlib';
import { Group, AABB, DisplayObject, Canvas } from '@antv/g';
import { ComboModel, IGraph } from '../../types';
import { isArray, isObject } from '@antv/util';
import Combo from '../../item/combo';
import Edge from '../../item/edge';
import Node from '../../item/node';
import registry from '../../stdlib';
import { getExtension } from '../../util/extension';
import { ComboModel, IGraph } from '../../types';
import { ComboDisplayModel, ComboEncode } from '../../types/combo';
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 { ITEM_TYPE, SHAPE_TYPE, ShapeStyle } from '../../types/item';
import { ComboDisplayModel, ComboEncode } from '../../types/combo';
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';

/**
Expand Down Expand Up @@ -61,7 +60,7 @@ export class ItemController {
constructor(graph: IGraph<any, any>) {
this.graph = graph;
// 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.edgeMapper = edge;
this.comboMapper = combo;
Expand Down Expand Up @@ -114,7 +113,7 @@ export class ItemController {
* Listener of runtime's render hook.
* @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 { graph } = this;
// TODO: 0. clear groups on canvas, and create new groups
Expand Down Expand Up @@ -177,11 +176,17 @@ export class ItemController {

// === 3. add nodes ===
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 ===
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 ===
Expand Down Expand Up @@ -209,7 +214,12 @@ export class ItemController {
// update the theme if the dataType value is changed
let themeStyles;
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 innerModel = graphCore.getNode(id);
Expand Down Expand Up @@ -247,7 +257,12 @@ export class ItemController {
// update the theme if the dataType value is changed
let themeStyles;
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 innerModel = graphCore.getEdge(id);
Expand Down Expand Up @@ -283,9 +298,9 @@ export class ItemController {
* 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;
ids.forEach(id => {
ids.forEach((id) => {
const item = this.itemMap[id];
if (!item) {
console.warn(`Fail to set state for item ${id}, which is not exist.`);
Expand All @@ -295,20 +310,20 @@ export class ItemController {
// clear all the states
item.clearStates(states);
} else {
states.forEach(state => item.setState(state, value));
states.forEach((state) => item.setState(state, value));
}
});
}

private onTransientUpdate(param: {
type: ITEM_TYPE | SHAPE_TYPE,
id: ID,
type: ITEM_TYPE | SHAPE_TYPE;
id: ID;
config: {
style: ShapeStyle,
action: 'remove' | 'add' | 'update' | undefined,
[shapeConfig: string]: unknown,
},
canvas: Canvas
style: ShapeStyle;
action: 'remove' | 'add' | 'update' | undefined;
[shapeConfig: string]: unknown;
};
canvas: Canvas;
}) {
const { transientMap } = this;
const { type, id, config = {}, canvas } = param;
Expand Down Expand Up @@ -345,7 +360,7 @@ export class ItemController {
let dataType;
if (dataTypeField) dataType = node.data[dataTypeField] as string;
const themeStyle = getThemeStyles(nodeDataTypeSet, dataTypeField, dataType, nodeTheme);

this.itemMap[node.id] = new Node({
model: node,
renderExtensions: nodeExtensions,
Expand Down Expand Up @@ -405,7 +420,7 @@ export class ItemController {
*/
public findIdByState(itemType: ITEM_TYPE, state: string, value: string | boolean = true) {
const ids = [];
Object.values(this.itemMap).forEach(item => {
Object.values(this.itemMap).forEach((item) => {
if (item.getType() !== itemType) return;
if (item.hasState(state) === value) ids.push(item.getID());
});
Expand All @@ -427,6 +442,10 @@ export class ItemController {
return item.hasState(state);
}

public getItemById(id: ID) {
return this.itemMap[id];
}

public getItemBBox(id: ID, isKeyShape: boolean = false): AABB | false {
const item = this.itemMap[id];
if (!item) {
Expand All @@ -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;
if (!dataTypeField) {
// dataType field is not assigned
Expand All @@ -462,4 +486,4 @@ const getThemeStyles = (dataTypeSet: Set<string>, dataTypeField: string, dataTyp
themeStyle = themeStyles[dataType] || themeStyles.others;
}
return themeStyle;
}
};
92 changes: 92 additions & 0 deletions packages/g6/src/runtime/controller/viewport.ts
Original file line number Diff line number Diff line change
@@ -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() {}
}
Loading

0 comments on commit 918584d

Please sign in to comment.