Skip to content

Commit

Permalink
feat: use astar alogorithm to draw polyline
Browse files Browse the repository at this point in the history
  • Loading branch information
yvonneyx committed Aug 14, 2024
1 parent 7078585 commit 269f55e
Show file tree
Hide file tree
Showing 10 changed files with 730 additions and 75 deletions.
43 changes: 43 additions & 0 deletions packages/g6/__tests__/demos/element-edge-polyline-astar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Graph } from '@antv/g6';

export const elementEdgePolylineAstar: TestCase = async (context) => {
const graph = new Graph({
...context,
data: {
nodes: [
{ id: 'node-1', style: { x: 100, y: 100 } },
{ id: 'node-2', style: { x: 200, y: 200 } },
{ id: 'node-3', style: { x: 150, y: 150 } },
],
edges: [{ source: 'node-1', target: 'node-2' }],
},
node: {
type: 'rect',
style: {
size: 40,
fill: 'transparent',
stroke: '#1783FF',
lineWidth: 1,
// ports: [{ id: 'port-1', placement: [1, 0.2], r: 2, fill: '#31d0c6' }],
},
},
edge: {
type: 'polyline',
style: {
lineWidth: 1,
router: {
type: 'shortest-path',
offset: 0,
enableObstacleAvoidance: false,
startDirections: ['top', 'right', 'bottom', 'left'],
endDirections: ['top', 'right', 'bottom', 'left'],
},
},
},
behaviors: ['drag-element'],
});

await graph.render();

return graph;
};
8 changes: 4 additions & 4 deletions packages/g6/__tests__/demos/element-edge-polyline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const elementEdgePolyline: TestCase = async (context) => {
{
source: 'node-1',
target: 'node-2',
style: { router: true },
style: { router: { type: 'orth' } },
},
{
source: 'node-3',
Expand All @@ -126,17 +126,17 @@ export const elementEdgePolyline: TestCase = async (context) => {
{
source: 'node-5',
target: 'node-6',
style: { router: true, controlPoints: [[100, 300]] },
style: { router: { type: 'orth' }, controlPoints: [[100, 300]] },
},
{
source: 'node-7',
target: 'node-8',
style: { router: true },
style: { router: { type: 'orth' } },
},
{
source: 'node-9',
target: 'node-10',
style: { router: true, controlPoints: [[340, 390]] },
style: { router: { type: 'orth' }, controlPoints: [[340, 390]] },
},
],
},
Expand Down
1 change: 1 addition & 0 deletions packages/g6/__tests__/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export { elementEdgeLoopCurve } from './element-edge-loop-curve';
export { elementEdgeLoopPolyline } from './element-edge-loop-polyline';
export { elementEdgePolyline } from './element-edge-polyline';
export { elementEdgePolylineAnimation } from './element-edge-polyline-animation';
export { elementEdgePolylineAstar } from './element-edge-polyline-astar';
export { elementEdgePort } from './element-edge-port';
export { elementEdgeQuadratic } from './element-edge-quadratic';
export { elementEdgeSize } from './element-edge-size';
Expand Down
33 changes: 33 additions & 0 deletions packages/g6/__tests__/unit/utils/router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
pointToNode,
pointToPoint,
} from '@/src/utils/router/orth';
import { estimateCost, getNearestPoint } from '@/src/utils/router/shortest-path';
import { manhattanDistance } from '@/src/utils/vector';
import { AABB } from '@antv/g';

describe('router', () => {
Expand Down Expand Up @@ -181,4 +183,35 @@ describe('router', () => {
expect(freeJoin(sourcePoint, targetPoint, sourceBBox)).toEqual([20, 10]);
});
});

describe('shortestPath', () => {
it('estimateCost', () => {
expect(
estimateCost(
[0, 0],
[
[1, 0],
[0, 1],
[1, 1],
],
manhattanDistance,
),
).toEqual(1);
});

it('getNearestPoint', () => {
expect(
getNearestPoint(
[
[0, 0],
[1, 0],
[0, 1],
[1, 1],
],
[2, 0],
manhattanDistance,
),
).toEqual([1, 0]);
});
});
});
89 changes: 28 additions & 61 deletions packages/g6/src/elements/edges/polyline.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { DisplayObjectConfig } from '@antv/g';
import type { PathArray } from '@antv/util';
import type { LoopStyleProps, Padding, Point, Port } from '../../types';
import type { LoopStyleProps, Point, PolylineRouter } from '../../types';
import { getBBoxHeight, getBBoxWidth, getNodeBBox } from '../../utils/bbox';
import { getPolylineLoopPath, getPolylinePath } from '../../utils/edge';
import { findPorts, getConnectionPoint, getPortPosition } from '../../utils/element';
import { subStyleProps } from '../../utils/prefix';
import { orth } from '../../utils/router/orth';
import { aStarSearch } from '../../utils/router/shortest-path';
import { mergeOptions } from '../../utils/style';
import type { BaseEdgeStyleProps } from './base-edge';
import { BaseEdge } from './base-edge';
Expand Down Expand Up @@ -35,24 +36,9 @@ export interface PolylineStyleProps extends BaseEdgeStyleProps {
* <en/> Whether to enable routing, it is enabled by default and controlPoints will be automatically included
* @defaultValue false
*/
router?: boolean;
/**
* <zh/> 路由名称,目前支持 'orth'
*
* <en/> Routing name, currently supports 'orth'
* @defaultValue 'orth'
* @remarks
* <img width="300" src="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*8Ia2RJFFQSoAAAAAAAAAAAAADmJ7AQ/original" />
*/
routerName?: 'orth';
/**
* <zh/> 节点边距
*
* <en/> Padding for routing calculation
* @defaultValue 10
*/
routerPadding?: Padding;
router?: PolylineRouter;
}

type ParsedPolylineStyleProps = Required<PolylineStyleProps>;

/**
Expand All @@ -65,71 +51,52 @@ export class Polyline extends BaseEdge {
radius: 0,
controlPoints: [],
router: false,
routerName: 'orth',
routerPadding: 10,
};

constructor(options: DisplayObjectConfig<PolylineStyleProps>) {
super(mergeOptions({ style: Polyline.defaultStyleProps }, options));
}

protected getKeyPath(attributes: ParsedPolylineStyleProps): PathArray {
const { radius } = attributes;
protected getPoints(attributes: ParsedPolylineStyleProps): Point[] {
const { controlPoints, router } = attributes;
const { sourceNode, targetNode } = this;
// 1. 获取连接点(若有连接桩,取连接桩中心;反之,取节点中心)和连接桩 | Get connection points (if port, take port center; otherwise, take node center) and ports
const { sourcePoint, targetPoint, sourcePort, targetPort } = this.getEndpointsAndPorts(attributes);

// 2. 计算控制点 | Calculate control points
const controlPoints = this.getControlPoints(attributes, sourcePoint, targetPoint);

// 3. 计算实际的连接点 | Calculate the actual connection points
const newSourcePoint = getConnectionPoint(sourcePort || sourceNode, controlPoints[0] || targetPort || targetNode);
const newTargetPoint = getConnectionPoint(
targetPort || targetNode,
controlPoints[controlPoints.length - 1] || sourcePort || sourceNode,
);

// 4. 获取路径 | Get the path
return getPolylinePath([newSourcePoint, ...controlPoints, newTargetPoint], radius);
}

private getEndpointsAndPorts(attributes: ParsedPolylineStyleProps): {
sourcePoint: Point;
targetPoint: Point;
sourcePort: Port | undefined;
targetPort: Port | undefined;
} {
const { sourcePort: sourcePortKey, targetPort: targetPortKey } = attributes;
const { sourceNode, targetNode } = this;

const [sourcePort, targetPort] = findPorts(sourceNode, targetNode, sourcePortKey, targetPortKey);

return {
sourcePoint: sourcePort ? getPortPosition(sourcePort) : sourceNode.getCenter(),
targetPoint: targetPort ? getPortPosition(targetPort) : targetNode.getCenter(),
sourcePort,
targetPort,
};
}
const sourcePoint = sourcePort ? getPortPosition(sourcePort) : sourceNode.getCenter();
const targetPoint = targetPort ? getPortPosition(targetPort) : targetNode.getCenter();

protected getControlPoints(attributes: ParsedPolylineStyleProps, sourcePoint: Point, targetPoint: Point): Point[] {
const { controlPoints, router, routerPadding } = attributes;
const { sourceNode, targetNode } = this;
if (!router) return [sourcePoint, ...controlPoints, targetPoint];

if (router.type === 'orth') {
const vertices = orth(sourcePoint, targetPoint, sourceNode, targetNode, controlPoints, router);

if (!router) return [...controlPoints];
const newSourcePoint = getConnectionPoint(sourcePort || sourceNode, vertices[0] || targetPort || targetNode);
const newTargetPoint = getConnectionPoint(
targetPort || targetNode,
vertices[vertices.length - 1] || sourcePort || sourceNode,
);

const sourceBBox = getNodeBBox(sourceNode, routerPadding);
const targetBBox = getNodeBBox(targetNode, routerPadding);
return [newSourcePoint, ...vertices, newTargetPoint];
}

return orth(sourcePoint, targetPoint, sourceBBox, targetBBox, controlPoints, routerPadding);
const nodes = this.context.element!.getNodes();
return aStarSearch(sourceNode, targetNode, nodes, router);
}

protected getKeyPath(attributes: ParsedPolylineStyleProps): PathArray {
const points = this.getPoints(attributes);
return getPolylinePath(points, attributes.radius);
}

protected getLoopPath(attributes: ParsedPolylineStyleProps): PathArray {
const { sourcePort: sourcePortKey, targetPort: targetPortKey, radius } = attributes;
const node = this.sourceNode;

const bbox = getNodeBBox(node);
// 默认转折点距离为 bbox 的最大宽高的 1/4 | Default distance of the turning point is 1/4 of the maximum width and height of the bbox
// 默认转折点距离为 bbox 的最大宽高的 1/4
// Default distance of the turning point is 1/4 of the maximum width and height of the bbox
const defaultDist = Math.max(getBBoxWidth(bbox), getBBoxHeight(bbox)) / 4;

const {
Expand Down
1 change: 1 addition & 0 deletions packages/g6/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type * from './padding';
export type * from './placement';
export type * from './point';
export type * from './prefix';
export type * from './router';
export type * from './size';
export type * from './state';
export type * from './style';
Expand Down
107 changes: 107 additions & 0 deletions packages/g6/src/types/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Padding } from './padding';
import type { CardinalPlacement } from './placement';
import { Point } from './point';

export type Direction = CardinalPlacement;

export type PolylineRouter = false | OrthRouter | ShortestPathRouter;

export interface OrthRouter extends OrthRouterOptions {
/**
* <zh/> 正交路由,通过在路径上添加额外的控制点,使得边的每一段都保持水平或垂直
*
* <en/> Orthogonal routing that adds additional control points on the path to ensure each segment of the edge horizontal or vertical
* @remarks
* <zh/> 采用基于节点的相对位置和专家经验得出的寻径算法来模糊计算控制点,非最优解但计算速度快。该路由支持 `controlPoints` 来作为额外的控制点,但不支持自动避障。
*
* <en/> It uses a pathfinding algorithm based on the relative position of the nodes and expert experience to calculate the control points, which is not the optimal solution but is fast to calculate. This routing supports `controlPoints` as additional control points, but does not support automatic obstacle avoidance.
*/
type: 'orth';
}

export interface ShortestPathRouter extends ShortestPathRouterOptions {
/**
* <zh/> 最短路径路由,是正交路由 `'orth'` 的智能版本。该路由由水平或垂直的正交线段组成。采用 A* 算法计算最短路径,并支持自动避开路径上的其他节点(障碍)
*
* <en/> The shortest path routing is an intelligent version of the orthogonal routing `'orth'`. The routing consists of horizontal or vertical orthogonal line segments. It uses the A* algorithm to calculate the shortest path and supports automatic avoidance of other nodes (obstacles) on the path.
*/
type: 'shortest-path';
}

export type RouterOptions = OrthRouterOptions | ShortestPathRouterOptions;

export interface OrthRouterOptions {
/**
* <zh/> 指定节点连接点与转角的最小距离
*
* <en/> The minimum distance between the node connection point and the corner
*/
padding?: Padding;
}

export interface ShortestPathRouterOptions {
/**
* <zh/> 节点锚点与转角的最小距离
*
* <en/> The minimum distance between the node anchor point and the corner
*/
offset?: Padding;
/**
* <zh/> grid 格子大小
*
* <en/> grid size
*/
gridSize?: number;
/**
* <zh/> 支持的最大旋转角度(弧度)
*
* <en/> Maximum allowable rotation angle (radian)
*/
maxAllowedDirectionChange?: number;
/**
* <zh/> 节点的可能起始方向
*
* <en/> Possible starting directions from a node
*/
startDirections?: Direction[];
/**
* <zh/> 节点的可能结束方向
*
* <en/> Possible ending directions from a node
*/
endDirections?: Direction[];
/**
* <zh/> 指定可移动的方向
*
* <en/> Allowed edge directions
*/
directionMap?: {
[key in Direction]: { stepX: number; stepY: number };
};
/**
* <zh/> 表示在路径搜索过程中某些路径的额外代价。key 为弧度值,value 为代价
*
* <en/> Penalties for direction changes. Key is the radian value, value is the penalty
*/
penalties?: {
[key: string]: number;
};
/**
* <zh/> 指定计算两点之间距离的函数
*
* <en/> Function to calculate the distance between two points
*/
distFunc?: (p1: Point, p2: Point) => number;
/**
* <zh/> 最大迭代次数
*
* <en/> Maximum loops
*/
maximumLoops?: number;
/**
* <zh/> 是否开启避障
*
* <en/> Whether to enable obstacle avoidance while computing the path
*/
enableObstacleAvoidance?: boolean;
}
Loading

0 comments on commit 269f55e

Please sign in to comment.