feat: update polyline

This commit is contained in:
yvonneyx 2023-10-16 19:54:03 +08:00
parent a082480db3
commit a1a1f8a35a
24 changed files with 172 additions and 163 deletions

View File

@ -7,6 +7,7 @@ import { updateShapes } from '../util/shape';
import { animateShapes } from '../util/animate';
import { EdgeStyleSet } from '../types/theme';
import { isSamePoint, getNearestPoint } from '../util/point';
import { isPolylineWithObstacleAvoidance } from '../util/polyline';
import Item from './item';
import Node from './node';
import Combo from './combo';
@ -134,8 +135,9 @@ export default class Edge extends Item {
* e.g. source and target nodes' position changed
* @param force bypass the nodes position change check and force to re-draw
*/
public forceUpdate(force = false) {
public forceUpdate() {
if (this.destroyed) return;
const force = isPolylineWithObstacleAvoidance(this.displayModel);
const { sourcePoint, targetPoint, changed } = this.getEndPoints(
this.displayModel,
);
@ -255,6 +257,7 @@ export default class Edge extends Item {
targetItem: Node | Combo,
shapeIds?: string[],
disableAnimate?: boolean,
visible?: boolean,
transientItemMap?: Map<ID, Node | Edge | Combo | Group>,
) {
if (shapeIds?.length) {
@ -302,6 +305,7 @@ export default class Edge extends Item {
lodStrategy: this.lodStrategy,
},
});
if (visible) return clonedEdge;
Object.keys(this.shapeMap).forEach((shapeId) => {
if (!this.shapeMap[shapeId].isVisible())
clonedEdge.shapeMap[shapeId]?.hide();

View File

@ -1,5 +1,6 @@
import { Graph as GraphLib, ID } from '@antv/graphlib';
import { clone, isArray, isObject } from '@antv/util';
import { clone, isArray, isObject, uniq } from '@antv/util';
import { AABB } from '@antv/g';
import { registery as registry } from '../../stdlib';
import { ComboModel, ComboUserModel, GraphData, IGraph } from '../../types';
import { ComboUserModelData } from '../../types/combo';
@ -36,6 +37,7 @@ import { getExtension } from '../../util/extension';
import { convertToNumber } from '../../util/type';
import { isTreeLayout } from '../../util/layout';
import { hasTreeBehaviors } from '../../util/behavior';
import { EdgeCollisionChecker, QuadTree } from '../../util/polyline';
/**
* Manages the data transform extensions;
@ -130,6 +132,50 @@ export class DataController {
) {
return this.graphCore.getRelatedEdges(nodeId, direction);
}
public findNearEdges(nodeId: ID, transientItem?: Node) {
const edges = this.graphCore.getAllEdges();
const canvasBBox = this.graph.getRenderBBox(undefined) as AABB;
const quadTree = new QuadTree(canvasBBox, 4);
edges.forEach((edge) => {
const {
data: { x: sourceX, y: sourceY },
} = this.graphCore.getNode(edge.source);
const {
data: { x: targetX, y: targetY },
} = this.graphCore.getNode(edge.target);
quadTree.insert({
id: edge.id,
p1: { x: sourceX, y: sourceY },
p2: { x: targetX, y: targetY },
bbox: this.graph.getRenderBBox(edge.id) as AABB,
});
});
const nodeBBox = this.graph.getRenderBBox(nodeId) as AABB;
if (transientItem) {
// @ts-ignore
const nodeData = transientItem.displayModel.data;
if (nodeData) {
nodeBBox.update(
[nodeData.x as number, nodeData.y as number, 0],
nodeBBox.halfExtents,
);
}
}
const checker = new EdgeCollisionChecker(quadTree);
const collisions = checker.getCollidingEdges(nodeBBox);
const collidingEdges = collisions.map((collision) =>
this.graphCore.getEdge(collision.id),
);
return collidingEdges;
}
public findNeighborNodes(
nodeId: ID,
direction: 'in' | 'out' | 'both' = 'both',

View File

@ -64,9 +64,9 @@ import {
import { getGroupedChanges } from '../../util/event';
import { BaseNode } from '../../stdlib/item/node/base';
import { BaseEdge } from '../../stdlib/item/edge/base';
import { EdgeCollisionChecker, QuadTree } from '../../util/polyline';
import { isBBoxInBBox, isPointInBBox } from '../../util/bbox';
import { convertToNumber } from '../../util/type';
import { isPointPreventPolylineOverlap } from '../../util/polyline';
/**
* Manages and stores the node / edge / combo items.
@ -136,6 +136,8 @@ export class ItemController {
ID,
Node | Edge | Combo | Group
>();
/** Caches */
private nearEdgesCache: Map<ID, EdgeModel[]> = new Map<ID, EdgeModel[]>();
constructor(graph: IGraph<any, any>) {
this.graph = graph;
@ -519,15 +521,20 @@ export class ItemController {
);
}
const preventPolylineEdgeOverlap =
innerModel?.data?.preventPolylineEdgeOverlap || false;
const relatedEdgeInnerModels = preventPolylineEdgeOverlap
? this.findNearEdgesByNode(id, graphCore).concat(
graphCore.getRelatedEdges(id),
const newNearEdges = this.graph.getNearEdgesData(id);
const relatedEdges = graphCore.getRelatedEdges(id);
const adjacentEdgeInnerModels = isPointPreventPolylineOverlap(
innerModel,
)
? uniq(
(this.nearEdgesCache.get(id) || [])
.concat(newNearEdges)
.concat(relatedEdges),
)
: graphCore.getRelatedEdges(id);
: relatedEdges;
this.nearEdgesCache.set(id, newNearEdges);
relatedEdgeInnerModels.forEach((edge) => {
adjacentEdgeInnerModels.forEach((edge) => {
edgeIdsToUpdate.add(edge.id);
nodeRelatedIdsToUpdate.add(edge.id);
});
@ -661,33 +668,6 @@ export class ItemController {
}
});
}
// === 10. redraw part of polyline which enables automatic obstacle avoidance
this.itemMap.forEach((item, id) => {
if (item.getType() === 'edge') {
const edgeModel = this.graph.getEdgeData(id);
const obstacleAvoidance =
// @ts-ignore
edgeModel.data?.keyShape?.routeCfg?.obstacleAvoidance || false;
// Only the edge enables automatic obstacle avoidance and the changed node is not the source/target, redrawing is forced.
if (obstacleAvoidance) {
const { NodeAdded, NodeDataUpdated, NodeRemoved } = groupedChanges;
const relatedNodeIds = new Set(
[...NodeAdded, ...NodeDataUpdated, ...NodeRemoved].map(
(node) =>
// @ts-ignore
node.id,
),
);
if (
relatedNodeIds.has(edgeModel.source) ||
relatedNodeIds.has(edgeModel.target)
)
return;
(item as Edge).forceUpdate(true);
}
}
});
}
/**
@ -726,7 +706,6 @@ export class ItemController {
graphCore: GraphCore;
animate?: boolean;
keepKeyShape?: boolean;
edgeForceUpdate?: boolean;
}) {
const {
ids,
@ -734,7 +713,6 @@ export class ItemController {
graphCore,
animate = true,
keepKeyShape = false,
edgeForceUpdate = false,
} = param;
ids.forEach((id) => {
const item = this.itemMap.get(id);
@ -748,9 +726,7 @@ export class ItemController {
if (value) {
if (type === 'edge') {
item.show(animate);
if (edgeForceUpdate) {
(item as Edge).forceUpdate(true);
}
(item as Edge).forceUpdate();
} else {
if (graphCore.hasTreeStructure('combo')) {
let anccestorCollapsed = false;
@ -992,7 +968,6 @@ export class ItemController {
{ shapeIds, drawSource, drawTarget, visible },
upsertAncestors,
);
if (shapeIds) {
// only update node positions to cloned node container(group)
if (
@ -1081,7 +1056,7 @@ export class ItemController {
}
public getTransientItem(id: ID) {
return this.transientItemMap[id];
return this.transientItemMap.get(id);
}
public findDisplayModel(id: ID) {
@ -1426,56 +1401,6 @@ export class ItemController {
return item.isVisible();
}
/**
* Identify edges that are intersected by a particular node
* @param nodeId node id
* @param graphCore
* @returns
*/
public findNearEdgesByNode(nodeId: ID, graphCore: GraphCore) {
const edges = graphCore.getAllEdges();
const canvasBBox = this.graph.getRenderBBox(undefined) as AABB;
const quadTree = new QuadTree(canvasBBox, 4);
each(edges, (edge) => {
const {
data: { x: sourceX, y: sourceY },
} = graphCore.getNode(edge.source);
const {
data: { x: targetX, y: targetY },
} = graphCore.getNode(edge.target);
quadTree.insert({
id: edge.id,
p1: { x: sourceX, y: sourceY },
p2: { x: targetX, y: targetY },
bbox: this.graph.getRenderBBox(edge.id) as AABB,
});
});
// update node position
const node = (this.getTransientItem(nodeId) ||
this.getItemById(nodeId)) as Node;
const nodeBBox = this.graph.getRenderBBox(nodeId) as AABB;
const nodeData = node?.model?.data;
if (nodeData) {
nodeBBox.update(
[nodeData.x as number, nodeData.y as number, 0],
nodeBBox.halfExtents,
);
}
const checker = new EdgeCollisionChecker(quadTree);
const collisions = checker.getCollidingEdges(nodeBBox);
const collidingEdges = map(collisions, (collision) =>
graphCore.getEdge(collision.id),
);
return collidingEdges;
}
public sortByComboTree(graphCore: GraphCore) {
if (!graphCore.hasTreeStructure('combo')) return;
graphCoreTreeDfs(graphCore, graphCore.getRoots('combo'), (node) => {

View File

@ -1026,6 +1026,17 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
): EdgeModel[] {
return this.dataController.findRelatedEdges(nodeId, direction);
}
/**
* Get nearby edges from a start node using quadtree collision detection.
* @param nodeId id of the start node
* @returns nearby edges' data array
*/
public getNearEdgesData(nodeId: ID): EdgeModel[] {
const transientItem = this.itemController.getTransientItem(
nodeId,
) as unknown as Node;
return this.dataController.findNearEdges(nodeId, transientItem);
}
/**
* Get one-hop node ids from a start node.
* @param nodeId id of the start node
@ -1060,16 +1071,6 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
return this.itemController.findDisplayModel(id);
}
/**
* Retrieve the nearby edges for a given node using quadtree collision detection.
* @param nodeId node id
* @group Data
*/
public getNearEdgesForNode(nodeId: ID): EdgeModel[] {
const { graphCore } = this.dataController;
return this.itemController.findNearEdgesByNode(nodeId, graphCore);
}
/**
* Find items which has the state.
* @param itemType item type
@ -1467,11 +1468,7 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Item
*/
public showItem(
ids: ID | ID[],
disableAnimate = false,
edgeForceUpdate = false,
) {
public showItem(ids: ID | ID[], disableAnimate = false) {
const idArr = isArray(ids) ? ids : [ids];
if (isEmpty(idArr)) return;
const changes = {
@ -1483,7 +1480,6 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
value: true,
graphCore: this.dataController.graphCore,
animate: !disableAnimate,
edgeForceUpdate,
});
this.emit('afteritemvisibilitychange', {
ids,

View File

@ -5,6 +5,7 @@ import { Behavior } from '../../types/behavior';
import { IG6GraphEvent } from '../../types/event';
import { Point } from '../../types/common';
import { graphComboTreeDfs } from '../../util/data';
import { isPointPreventPolylineOverlap } from '../../util/polyline';
const DELEGATE_SHAPE_ID = 'g6-drag-node-delegate-shape';
@ -108,6 +109,7 @@ export class DragNode extends Behavior {
private originPositions: Array<Position> = [];
private pointerDown: Point | undefined = undefined;
private dragging = false;
private hiddenNearEdgesCache: EdgeModel[] = [];
constructor(options: Partial<DragNodeOptions>) {
const finalOptions = Object.assign({}, DEFAULT_OPTIONS, options);
@ -171,7 +173,7 @@ export class DragNode extends Behavior {
/** Retrieve the nearby edges for a given node using quadtree collision detection. */
private getNearEdgesForNodes(nodeIds: ID[]) {
return uniq(
nodeIds.flatMap((nodeId) => this.graph.getNearEdgesForNode(nodeId)),
nodeIds.flatMap((nodeId) => this.graph.getNearEdgesData(nodeId)),
);
}
@ -323,28 +325,38 @@ export class DragNode extends Behavior {
}
/**
* When dragging nodes, if nodes are set to `preventPolylineEdgeOverlap`,
* use quadtree collision detection to identity nearby edges and dynamically update them
* When dragging nodes, if nodes are set to `preventPolylineEdgeOverlap`, identity nearby edges and dynamically update them
*/
if (this.dragging && enableTransient) {
const autoRoutedNodesIds = this.selectedNodeIds.filter((nodeId) => {
return (
this.graph.getNodeData(nodeId)?.data.preventPolylineEdgeOverlap ||
false
);
});
const preventPolylineOverlapNodeIds = this.selectedNodeIds.filter(
(nodeId) => {
const innerModel = this.graph.getNodeData(nodeId);
return isPointPreventPolylineOverlap(innerModel);
},
);
if (autoRoutedNodesIds) {
if (preventPolylineOverlapNodeIds.length) {
const hiddenEdgesIds = this.hiddenEdges.map((edge) => edge.id);
this.hiddenNearEdgesCache = this.hiddenNearEdges;
this.hiddenNearEdges = this.getNearEdgesForNodes(
autoRoutedNodesIds,
preventPolylineOverlapNodeIds,
).filter((edge) => !hiddenEdgesIds.includes(edge.id));
const hiddenNearEdgesIds = this.hiddenNearEdges.map((edge) => edge.id);
this.hiddenNearEdgesCache.forEach((edge) => {
if (!hiddenNearEdgesIds.includes(edge.id)) {
this.graph.drawTransient('edge', edge.id, { action: 'remove' });
this.graph.showItem(edge.id);
}
});
if (this.hiddenNearEdges.length) {
this.hiddenNearEdges.forEach((edge) => {
this.graph.drawTransient('edge', edge.id, {});
this.graph.drawTransient('edge', edge.id, {
visible: true,
});
});
this.graph.hideItem(
this.hiddenNearEdges.map((edge) => edge.id),
true,
@ -490,7 +502,6 @@ export class DragNode extends Behavior {
this.graph.showItem(
this.hiddenNearEdges.map((edge) => edge.id),
true,
true,
);
this.hiddenNearEdges = [];
}

View File

@ -463,7 +463,7 @@ export const genBubbleSet = (
// eslint-disable-next-line no-redeclare
const options = Object.assign(defaultOps, ops);
const centroid = getPointsCenter(
members.map((model) => ({ x: model.data.x, y: model.data.y } as Point)),
members.map((model) => ({ x: model.data.x, y: model.data.y }) as Point),
);
// 按照到中心距离远近排序
members = members.sort(

View File

@ -21,7 +21,7 @@ export const genConvexHull = (models: (NodeModel | ComboModel)[]) => {
({
x: model.data.x,
y: model.data.y,
} as Point),
}) as Point,
);
points.sort((a, b) => {
return a.x === b.x ? a.y - b.y : a.x - b.x;

View File

@ -113,6 +113,12 @@ export interface IGraph<
nodeId: ID,
direction?: 'in' | 'out' | 'both',
) => EdgeModel[];
/**
* Get nearby edges from a start node using quadtree collision detection.
* @param nodeId id of the start node
* @returns nearby edges' data array
*/
getNearEdgesData: (nodeId: ID) => EdgeModel[];
/**
* Get one-hop node ids from a start node.
* @param nodeId id of the start node
@ -123,12 +129,6 @@ export interface IGraph<
nodeId: ID,
direction?: 'in' | 'out' | 'both',
) => NodeModel[];
/**
* Retrieve the nearby edges for a given node using quadtree collision detection.
* @param nodeId target node's id
* @returns edges
*/
getNearEdgesForNode: (nodeId: ID) => EdgeModel[];
/*
* Get the children's data of a combo.
* @param comboId combo id
@ -472,11 +472,7 @@ export interface IGraph<
* @returns
* @group Data
*/
showItem: (
ids: ID | ID[],
disableAnimate?: boolean,
edgeForceUpdate?: boolean,
) => void;
showItem: (ids: ID | ID[], disableAnimate?: boolean) => void;
/**
* Hide the item(s).
* @param ids the item id(s) to be hidden

View File

@ -89,7 +89,6 @@ export interface Hooks {
enableStack?: boolean;
changes?: any;
keepKeyShape?: boolean;
edgeForceUpdate?: boolean;
}>;
itemzindexchange: IHook<{
ids: ID[];

View File

@ -92,6 +92,7 @@ export const upsertTransientItem = (
target,
shapeIds,
true,
visible,
transientItemMap,
) as Edge;
} else {

View File

@ -1,7 +1,10 @@
import { AABB } from '@antv/g';
import { each } from '@antv/util';
import { ID } from '@antv/graphlib';
import { EdgeDisplayModel } from '../types/edge';
import { NodeDisplayModel } from '../types';
import Node from '../item/node';
import Item from '../item/item';
import { Point, PolyPoint } from '../types/common';
import {
getBBoxFromPoint,
@ -559,7 +562,10 @@ export class QuadTree {
private southwest?: QuadTree;
private southeast?: QuadTree;
constructor(public boundary: AABB, capacity: number) {
constructor(
public boundary: AABB,
capacity: number,
) {
this.capacity = capacity;
}
@ -652,3 +658,28 @@ export class EdgeCollisionChecker {
return potentialCollisions;
}
}
/**
* Check if the edge is a polyline and obstacle avoidance is enabled
*/
export const isPolylineWithObstacleAvoidance = (
displayModel: EdgeDisplayModel,
) => {
const { type, keyShape } = displayModel.data;
const isPolyline = type === 'polyline-edge';
if (!isPolyline) return false;
// @ts-ignore
const isObstacleAvoidanceEnabled = (keyShape?.routeCfg as RouterCfg)
?.enableObstacleAvoidance;
return isObstacleAvoidanceEnabled;
};
/**
* Check if the node prevents polyline edges from overlapping
*/
export const isPointPreventPolylineOverlap = (
displayModel: NodeDisplayModel,
) => {
const { preventPolylineEdgeOverlap } = displayModel.data;
return preventPolylineEdgeOverlap || false;
};

View File

@ -44,12 +44,12 @@ export interface RouterCfg {
* Whether to automatically avoid other nodes (obstacles) on the path
* Defaults to false.
*/
obstacleAvoidance?: boolean;
enableObstacleAvoidance?: boolean;
}
const defaultCfg: RouterCfg = {
name: 'orth',
obstacleAvoidance: false,
enableObstacleAvoidance: false,
offset: 2,
maxAllowedDirectionChange: Math.PI / 2,
maximumLoops: 2000,
@ -393,7 +393,7 @@ export const pathFinder = (
const cfg: RouterCfg = deepMix(defaultCfgs, routerCfg);
if (!cfg.obstacleAvoidance) {
if (!cfg.enableObstacleAvoidance) {
return cfg.fallbackRoute(startPoint, endPoint, startNode, endNode, cfg);
}

View File

@ -63,7 +63,7 @@ export default () => {
routeCfg: {
gridSize: 10,
maxAllowedDirectionChange: Math.PI / 2,
obstacleAvoidance: true,
enableObstacleAvoidance: true,
},
},
// labelShape: {

View File

@ -76,7 +76,7 @@ export { default as timebar_chart } from './plugins/timebar-chart';
export {
minimap,
map,
// map,
mapper,
anchor,
animations_node_build_in,

View File

@ -234,7 +234,7 @@ const createControls = () => {
data: {
keyShape: {
routeCfg: {
obstacleAvoidance: true,
enableObstacleAvoidance: true,
},
},
},
@ -246,7 +246,7 @@ const createControls = () => {
data: {
keyShape: {
routeCfg: {
obstacleAvoidance: false,
enableObstacleAvoidance: false,
},
},
},

View File

@ -104,7 +104,7 @@ export default (context: TestCaseContext) => {
// lineWidth: 2,
// stroke: '#C2C8D5',
// routeCfg: {
// obstacleAvoidance: true,
// enableObstacleAvoidance: true,
// },
},
},

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View File

@ -85,7 +85,7 @@ describe('Items edge polyline', () => {
const $obstacle = document.querySelectorAll(
'input',
)[5] as HTMLInputElement;
const $obstacleAvoidance = document.querySelectorAll(
const $enableObstacleAvoidance = document.querySelectorAll(
'input',
)[6] as HTMLInputElement;
@ -99,7 +99,7 @@ describe('Items edge polyline', () => {
// 'input',
// )[8] as HTMLInputElement;
// $obstacle.click();
// $obstacleAvoidance.click();
// $enableObstacleAvoidance.click();
// $preventObstacleOverlapEdges.click();
// $moveObstacle.click();
// await expect(canvas).toMatchCanvasSnapshot(
@ -107,7 +107,7 @@ describe('Items edge polyline', () => {
// 'items-edge-polyline-prevent-overlap-edges',
// );
// $obstacle.click();
// $obstacleAvoidance.click();
// $enableObstacleAvoidance.click();
// $preventObstacleOverlapEdges.click();
// $moveObstacle.click();
@ -189,17 +189,17 @@ describe('Items edge polyline', () => {
const $obstacle = document.querySelectorAll(
'input',
)[5] as HTMLInputElement;
const $obstacleAvoidance = document.querySelectorAll(
const $enableObstacleAvoidance = document.querySelectorAll(
'input',
)[6] as HTMLInputElement;
// $obstacle.click();
// $obstacleAvoidance.click();
// $enableObstacleAvoidance.click();
// await expect(canvas).toMatchSVGSnapshot(
// dir,
// 'items-edge-polyline-obstacle-avoidance',
// );
// $obstacle.click();
// $obstacleAvoidance.click();
// $enableObstacleAvoidance.click();
/**
* Click the checkbox to prevent obstacle to overlap edges.
@ -211,7 +211,7 @@ describe('Items edge polyline', () => {
// 'input',
// )[8] as HTMLInputElement;
// $obstacle.click();
// $obstacleAvoidance.click();
// $enableObstacleAvoidance.click();
// $preventObstacleOverlapEdges.click();
// $moveObstacle.click();
// await expect(canvas).toMatchSVGSnapshot(
@ -219,7 +219,7 @@ describe('Items edge polyline', () => {
// 'items-edge-polyline-prevent-overlap-edges',
// );
// $obstacle.click();
// $obstacleAvoidance.click();
// $enableObstacleAvoidance.click();
// $preventObstacleOverlapEdges.click();
// $moveObstacle.click();

View File

@ -345,6 +345,6 @@ interface RouterCfg {
* Whether to automatically avoid other nodes (obstacles) on the path
* Defaults to false.
*/
obstacleAvoidance?: boolean;
enableObstacleAvoidance?: boolean;
}
```

View File

@ -338,6 +338,6 @@ interface RouterCfg {
* Whether to automatically avoid other nodes (obstacles) on the path
* Defaults to false.
*/
obstacleAvoidance?: boolean;
enableObstacleAvoidance?: boolean;
}
```

View File

@ -705,7 +705,7 @@ You only need to configure the labelShape for the Hull instance, and you can spe
- Polyline supports automatic obstacle avoidance:
Set `keyShape.routeCfg.obstacleAvoidance: true` for the edge to automatically avoid nodes.
Set `keyShape.routeCfg.enableObstacleAvoidance: true` for the edge to automatically avoid nodes.
[Polyline Obstacle Avoidance DEMO](https://g6-next.antv.antgroup.com/examples/item/defaultEdges/#polyline3)

View File

@ -752,7 +752,7 @@ v4 的坐标系统三套见文档https://g6.antv.antgroup.com/manual/ad
- 折线支持自动避障:
设置边的 `keyShape.routeCfg.obstacleAvoidance: true` 即可自动躲避节点。
设置边的 `keyShape.routeCfg.enableObstacleAvoidance: true` 即可自动躲避节点。
[Polyline 避障 DEMO](https://g6-next.antv.antgroup.com/examples/item/defaultEdges/#polyline3)

View File

@ -47,7 +47,7 @@ const data = {
},
keyShape: {
routeCfg: {
obstacleAvoidance: false
enableObstacleAvoidance: false
}
}
}
@ -73,7 +73,7 @@ const data = {
* 是否开启自动避障默认为 false
* Whether to enable automatic obstacle avoidance, default is false
*/
obstacleAvoidance: true,
enableObstacleAvoidance: true,
},
/**
* 拐弯处的圆角弧度默认为直角值为 0

View File

@ -231,7 +231,7 @@ const graph = new ExtGraph({
lineWidth: 2,
stroke: '#C2C8D5',
routeCfg: {
obstacleAvoidance: true,
enableObstacleAvoidance: true,
},
},
},