If you have used flowchart drawing tools, you may be wondering how the connection lines between nodes are calculated:
Don't go away, follow along with this article to explore it.
Final effect preview: https://wanglin2.github.io/AssociationLineDemo/
basic structure
First use Vue3
to build the basic structure of the page. In order to simplify the canvas
operation, we use the konvajs library to draw graphics.
In the page template section, provide a container:
<div class="container" ref="container"></div>
js
part, mainly use konvajs
to create two draggable rectangular elements and a connecting line element, of course, there is no vertex data for the connecting line:
import { onMounted, ref } from "vue";
import Konva from "konva";
const container = ref(null);
// 创建两个矩形、一个折线元素
let layer, rect1, rect2, line;
// 矩形移动事件
const onDragMove = () => {
// 获取矩形实时位置
console.log(rect1.x(), rect1.y(), rect2.x(), rect2.y());
};
// 初始化图形
const init = () => {
const { width, height } = container.value.getBoundingClientRect();
// 创建舞台
let stage = new Konva.Stage({
container: container.value,
width,
height,
});
// 创建图层
layer = new Konva.Layer();
// 创建两个矩形
rect1 = new Konva.Rect({
x: 400,
y: 200,
width: 100,
height: 100,
fill: "#fbfbfb",
stroke: "black",
strokeWidth: 4,
draggable: true,// 图形允许拖拽
});
rect2 = new Konva.Rect({
x: 800,
y: 600,
width: 100,
height: 100,
fill: "#fbfbfb",
stroke: "black",
strokeWidth: 4,
draggable: true,
});
// 监听进行拖拽事件
rect1.on("dragmove", onDragMove);
rect2.on("dragmove", onDragMove);
// 矩形添加到图层
layer.add(rect1);
layer.add(rect2);
// 创建折线元素
line = new Konva.Line({
points: [],// 当前它的顶点数据是空的,所以你还看不见这个元素
stroke: "green",
strokeWidth: 2,
lineJoin: "round",
});
// 折线添加到图层
layer.add(line);
// 图层添加到舞台
stage.add(layer);
// 绘制
layer.draw();
};
onMounted(() => {
init();
});
The effect is as follows:
Next, we only need to calculate the vertices of the associated line in real time when the graph is dragged and then update it to the polyline element to draw the connection line.
Calculate the point where the associated line is most likely to pass
All the points on the entire canvas are actually possible points, but our connecting line is [horizontal, horizontal and vertical], and it should be the shortest route as possible, so it is not necessary to consider all the points, we can narrow the scope according to certain rules , and then calculate the optimal route from it.
First of all, the starting point and the end point are definitely necessary. Take the following figure as an example. Suppose we want to connect from the top middle position of the rectangle in the upper left corner to the top middle position of the rectangle in the lower right corner:
Next we set two principles:
1. The connecting line should not overlap with the edge of the graph as much as possible
2. The connecting line should not pass through the element as much as possible
Why try as much as possible, because when two elements are too close or overlap, these are unavoidable.
Combining the above two principles, we can stipulate that no line is allowed to pass within a certain distance around the element (except for the line segment connecting the starting point), which is equivalent to putting a rectangular bounding box outside the element:
The intersection of the line that passes through the starting and ending points and is perpendicular to the side of the starting and ending points and the bounding box must pass through, and these two points are the only points that can be directly connected to the starting and ending points, so we can regard these two points as " Starting point" and "end point", so that two points can be calculated less when calculating:
To calculate the point in the rectangle movement event, first cache the position and size information of the rectangle, then define the coordinates of the start and end points, and finally define an array to store all possible points:
// 矩形移动事件
const onDragMove = () => {
computedProbablyPoints();
};
// 计算所有可能经过的点
let rect1X, rect1Y, rect1W, rect1H, rect2X, rect2Y, rect2W, rect2H;
let startPoint = null, endPoint = null;
const computedProbablyPoints = () => {
// 保存矩形的尺寸、位置信息
rect1X = rect1.x();
rect1Y = rect1.y();
rect1W = rect1.width();
rect1H = rect1.height();
rect2X = rect2.x();
rect2Y = rect2.y();
rect2W = rect2.width();
rect2H = rect2.height();
// 起终点
startPoint = [rect1X + rect1W / 2, rect1Y];
endPoint = [rect2X + rect2W / 2, rect2Y];
// 保存所有可能经过的点
let points = [];
}
Since the start and end points can be in either direction of the rectangle, let's write a method to get the pseudo start point and pseudo end point and add them to the array:
const computedProbablyPoints = () => {
// ...
// 伪起点:经过起点且垂直于起点所在边的线与包围框线的交点
let fakeStartPoint = findStartNextOrEndPrePoint(rect1, startPoint);
points.push(fakeStartPoint);
// 伪终点:经过终点且垂直于终点所在边的线与包围框线的交点
let fakeEndPoint = findStartNextOrEndPrePoint(rect2, endPoint);
points.push(fakeEndPoint);
}
// 找出起点的下一个点或终点的前一个点
const MIN_DISTANCE = 30;
const findStartNextOrEndPrePoint = (rect, point) => {
// 起点或终点在左边
if (point[0] === rect.x()) {
return [rect.x() - MIN_DISTANCE, rect.y() + rect.height() / 2];
} else if (point[1] === rect.y()) {
// 起点或终点在上边
return [rect.x() + rect.width() / 2, rect.y() - MIN_DISTANCE];
} else if (point[0] === rect.x() + rect.width()) {
// 起点或终点在右边
return [
rect.x() + rect.width() + MIN_DISTANCE,
rect.y() + rect.height() / 2,
];
} else if (point[1] === rect.y() + rect.height()) {
// 起点或终点在下边
return [
rect.x() + rect.width() / 2,
rect.y() + rect.height() + MIN_DISTANCE,
];
}
};
The pseudo starting point and the pseudo ending point will form a rectangle. This rectangle and the bounding box of the starting point element can form a larger rectangle. The four corners of this rectangle are the points where the connection line may pass:
Add these points to the array, and one point is repeated with the pseudo-end point, but it doesn't matter, we can go to the repeat at the end:
const computedProbablyPoints = () => {
// ...
// 伪起点和伪终点形成的矩形 和 起点元素包围框 组成一个大矩形 的四个顶点
points.push(
...getBoundingBox([
// 伪起点终点
fakeStartPoint,
fakeEndPoint,
// 起点元素包围框
[rect1X - MIN_DISTANCE, rect1Y - MIN_DISTANCE], // 左上顶点
[rect1X + rect1W + MIN_DISTANCE, rect1Y + rect1H + MIN_DISTANCE], // 右下顶点
])
);
}
// 计算出给定点可以形成的最大的矩形的四个顶点
const getBoundingBox = (points) => {
let boundingBoxXList = [];
let boundingBoxYList = [];
points.forEach((item) => {
boundingBoxXList.push(item[0]);
boundingBoxYList.push(item[1]);
});
let minBoundingBoxX = Math.min(...boundingBoxXList);
let minBoundingBoxY = Math.min(...boundingBoxYList);
let maxBoundingBoxX = Math.max(...boundingBoxXList);
let maxBoundingBoxY = Math.max(...boundingBoxYList);
return [
[minBoundingBoxX, minBoundingBoxY],
[maxBoundingBoxX, minBoundingBoxY],
[minBoundingBoxX, maxBoundingBoxY],
[maxBoundingBoxX, maxBoundingBoxY],
];
};
As can be seen from the figure, the correlation lines are connected either from the right or the left.
Similarly, the rectangle formed by the pseudo starting point and the pseudo ending point will also form a larger rectangle with the bounding box of the ending element. The four vertices of this rectangle may also pass through, which will pass when the ending element is above the starting element:
// 伪起点和伪终点形成的矩形 和 终点元素包围框 组成一个大矩形 的四个顶点
points.push(
...getBoundingBox([
// 伪起点终点
fakeStartPoint,
fakeEndPoint,
// 终点元素包围框
[rect2X - MIN_DISTANCE, rect2Y - MIN_DISTANCE], // 左上顶点
[rect2X + rect2W + MIN_DISTANCE, rect2Y + rect2H + MIN_DISTANCE], // 右下顶点
])
);
The above points can basically satisfy the situation where the start and end points are above the element, but for the following situation where the start point is on the top and the end point is on the left:
It is obvious to see that if the following points exist:
This is actually the intersection of the two lines that pass through the starting and ending points and are perpendicular to the side where the starting and ending points are located. To find the intersection point, you can first calculate the straight line equation based on the two points, and then calculate the intersection point by combining the two equations, but our line Both are horizontal and vertical, so there is no need to be so troublesome. The two lines are either parallel, or one is horizontal and the other is vertical. It is easy to list all the cases:
// 计算两条线段的交点
const getIntersection = (seg1, seg2) => {
// 两条垂直线不会相交
if (seg1[0][0] === seg1[1][0] && seg2[0][0] === seg2[1][0]) {
return null;
}
// 两条水平线不会相交
if (seg1[0][1] === seg1[1][1] && seg2[0][1] === seg2[1][1]) {
return null;
}
// seg1是水平线、seg2是垂直线
if (seg1[0][1] === seg1[1][1] && seg2[0][0] === seg2[1][0]) {
return [seg2[0][0], seg1[0][1]];
}
// seg1是垂直线、seg2是水平线
if (seg1[0][0] === seg1[1][0] && seg2[0][1] === seg2[1][1]) {
return [seg1[0][0], seg2[0][1]];
}
};
With this method we can add this intersection to the array:
const computedProbablyPoints = () => {
// ...
// 经过起点且垂直于起点所在边的线 与 经过终点且垂直于终点所在边的线 的交点
let startEndPointVerticalLineIntersection = getIntersection([startPoint, fakeStartPoint], [endPoint, fakeEndPoint]);
startEndPointVerticalLineIntersection && points.push(startEndPointVerticalLineIntersection);
}
The points calculated here can satisfy most of the situations, but there is another situation that cannot be satisfied, when the starting and ending points are relative:
So when the previously calculated startEndPointVerticalLineIntersection
point does not exist, we calculate the intersection of a vertical line and a horizontal line passing through the pseudo-start and pseudo-end points (two yellow points):
const computedProbablyPoints = () => {
// ...
// 当 经过起点且垂直于起点所在边的线 与 经过终点且垂直于终点所在边的线 平行时,计算一条垂直线与经过另一个点的伪点的水平线 的节点
if (!startEndPointVerticalLineIntersection) {
let p1 = getIntersection(
[startPoint, fakeStartPoint],// 假设经过起点的垂直线是垂直的
[fakeEndPoint, [fakeEndPoint[0] + 10, fakeEndPoint[1]]]// 那么就要计算经过伪终点的水平线。水平线上的点y坐标相同,所以x坐标随便加减多少数值都可以
);
p1 && points.push(p1);
let p2 = getIntersection(
[startPoint, fakeStartPoint],// 假设经过起点的垂直线是水平的
[fakeEndPoint, [fakeEndPoint[0], fakeEndPoint[1] + 10]]// 那么就要计算经过伪终点的垂直线。
);
p2 && points.push(p2);
// 下面同上
let p3 = getIntersection(
[endPoint, fakeEndPoint],
[fakeStartPoint, [fakeStartPoint[0] + 10, fakeStartPoint[1]]]
);
p3 && points.push(p3);
let p4 = getIntersection(
[endPoint, fakeEndPoint],
[fakeStartPoint, [fakeStartPoint[0], fakeStartPoint[1] + 10]]
);
p4 && points.push(p4);
}
}
The points that you may pass through here are almost the same. There are a total of:
Next, deduplicate and export related data:
const computedProbablyPoints = () => {
// ...
// 去重
points = removeDuplicatePoint(points);
return {
startPoint,
endPoint,
fakeStartPoint,
fakeEndPoint,
points,
};
}
// 去重
const removeDuplicatePoint = (points) => {
let res = [];
let cache = {};
points.forEach(([x, y]) => {
if (cache[x + "-" + y]) {
return;
} else {
cache[x + "-" + y] = true;
res.push([x, y]);
}
});
return res;
};
Brute-force Solving: Backtracking Algorithms
If the efficiency and the shortest distance are not considered, we can directly use the breadth-first search or backtracking algorithm, that is, starting from the starting point, try the points around the starting point one by one, and try all the points around the next point one by one to the next point. When the end point is encountered, then the end, connecting the points passed through is a path, let's try it next.
Before starting the algorithm, you need to realize how to find the points around a point. If it is in the grid, it is very simple. The points around a point are x、y
coordinates plus 1
or Minus 1
, but the distance between our points is uncertain, so we can only search according to the coordinates, for example, to find the closest point to the right of a point, then according to the point y
Search for the coordinates to see if there are any points with the same coordinates y
, if there are any, find the closest one, of course, also check whether the connection between the found point and the target point Pass through the start and end elements, if yes, this point will also be skipped:
// 找出一个点周边的点
const getNextPoints = (point, points) => {
let [x, y] = point;
let xSamePoints = [];
let ySamePoints = [];
// 找出x或y坐标相同的点
points.forEach((item) => {
// 跳过目标点
if (checkIsSamePoint(point, item)) {
return;
}
if (item[0] === x) {
xSamePoints.push(item);
}
if (item[1] === y) {
ySamePoints.push(item);
}
});
// 找出x方向最近的点
let xNextPoints = getNextPoint(x, y, ySamePoints, "x");
// 找出y方向最近的点
let yNextPoints = getNextPoint(x, y, xSamePoints, "y");
return [...xNextPoints, ...yNextPoints];
};
// 检测是否为同一个点
const checkIsSamePoint = (a, b) => {
if (!a || !b) {
return false;
}
return a[0] === b[0] && a[1] === b[1];
};
Next is the implementation of the getNextPoint
method:
// 找出水平或垂直方向上最近的点
const getNextPoint = (x, y, list, dir) => {
let index = dir === "x" ? 0 : 1;// 求水平方向上最近的点,那么它们y坐标都是相同的,要比较x坐标,反之亦然
let value = dir === "x" ? x : y;
let nextLeftTopPoint = null;
let nextRIghtBottomPoint = null;
for (let i = 0; i < list.length; i++) {
let cur = list[i];
// 检查当前点和目标点的连线是否穿过起终点元素
if (checkLineThroughElements([x, y], cur)) {
continue;
}
// 左侧或上方最近的点
if (cur[index] < value) {
if (nextLeftTopPoint) {
if (cur[index] > nextLeftTopPoint[index]) {
nextLeftTopPoint = cur;
}
} else {
nextLeftTopPoint = cur;
}
}
// 右侧或下方最近的点
if (cur[index] > value) {
if (nextRIghtBottomPoint) {
if (cur[index] < nextRIghtBottomPoint[index]) {
nextRIghtBottomPoint = cur;
}
} else {
nextRIghtBottomPoint = cur;
}
}
}
return [nextLeftTopPoint, nextRIghtBottomPoint].filter((point) => {
return !!point;
});
};
checkLineThroughElements
method is used to judge whether a line segment passes through or overlaps with the start and end elements. It is also a simple comparison logic:
// 检查两个点组成的线段是否穿过起终点元素
const checkLineThroughElements = (a, b) => {
let rects = [rect1, rect2];
let minX = Math.min(a[0], b[0]);
let maxX = Math.max(a[0], b[0]);
let minY = Math.min(a[1], b[1]);
let maxY = Math.max(a[1], b[1]);
// 水平线
if (a[1] === b[1]) {
for (let i = 0; i < rects.length; i++) {
let rect = rects[i];
if (
minY >= rect.y() &&
minY <= rect.y() + rect.height() &&
minX <= rect.x() + rect.width() &&
maxX >= rect.x()
) {
return true;
}
}
} else if (a[0] === b[0]) {
// 垂直线
for (let i = 0; i < rects.length; i++) {
let rect = rects[i];
if (
minX >= rect.x() &&
minX <= rect.x() + rect.width() &&
minY <= rect.y() + rect.height() &&
maxY >= rect.y()
) {
return true;
}
}
}
return false;
};
Next, you can use the backtracking algorithm to find one of the paths. The backtracking algorithm is very simple. Because it is not the focus of this article, it will not be introduced in detail. If you are interested, you can read the backtracking (DFS) algorithm problem-solving routine framework .
After calculating the coordinate point, update the connection element, remember to add our real starting and ending coordinates:
// 矩形移动事件
const onDragMove = () => {
// 计算出所有可能的点
let { startPoint, endPoint, fakeStartPoint, fakeEndPoint, points } =
computedProbablyPoints();
// 使用回溯算法找出其中一条路径
const routes = useDFS(fakeStartPoint, fakeEndPoint, points);
// 更新连线元素
line.points(
// 加上真正的起点和终点
(routes.length > 0 ? [startPoint, ...routes, endPoint] : []).reduce(
(path, cur) => {
path.push(cur[0], cur[1]);
return path;
},
[]
)
);
};
// 使用回溯算法寻找路径
const useDFS = (startPoint, endPoint, points) => {
let res = [];
let used = {};
let track = (path, selects) => {
for (let i = 0; i < selects.length; i++) {
let cur = selects[i];
// 到达终点了
if (checkIsSamePoint(cur, endPoint)) {
res = [...path, cur];
break;
}
let key = cur[0] + "-" + cur[1];
// 该点已经被选择过了
if (used[key]) {
continue;
}
used[key] = true;
track([...path, cur], getNextPoints(cur, points));
used[key] = false;
}
};
track([], [startPoint]);
return res;
};
The effect is as follows:
It can be seen that a connecting line path is indeed calculated, but it is obviously not the shortest path, and the backtracking algorithm is a brute force algorithm, and there may be performance problems if there are too many points.
Use A* algorithm combined with Manhattan path to calculate the shortest path
Earlier, we used the backtracking algorithm to find one of the associated line paths, but in many cases the calculated path is not the shortest. Next, we use the A*
algorithm to find the shortest path.
A*
algorithm is somewhat similar to the backtracking algorithm, but instead of blindly traversing the points around a point one by one, it will find the most likely points to try first. The complete algorithm process is described as follows:
1. Create two arrays,
openList
to store the points to be traversed,closeList
to store the points that have been traversed;2. Put the starting point in
openList
;3. If
openList
is not empty, then select the point with the highest priority, assumingn
:
- 3.1. If
n
is the end point, then end the loop, start fromn
, and find the parent node forward in turn, which is the shortest path;3.2. If
n
is not the end point, then:
- 3.2.1. Delete
n
fromopenList
and add it tocloseList
;3.2.2. Traverse the points around
n
:
- 3.2.2.1. If the point is in
closeList
, then skip the point;3.2.2.2. If the point is also not in
openList
, then:
- 3.2.2.2.1. Set
n
as the parent node of this point;- 3.2.2.2.2. Calculate the cost of this point, the higher the cost, the lower the priority, and vice versa;
- 3.3.3.3.3. Add this point to
openList
;- 3.2.3. Continue the loop process of 3 until the end point is found, or
openList
is empty, no result;
According to the above process, we create a A*
class:
// A*算法类
class AStar {
constructor() {
this.startPoint = null;
this.endPoint = null;
this.pointList = [];
// 存放待遍历的点
this.openList = [];
// 存放已经遍历的点
this.closeList = [];
}
// 算法主流程
start(startPoint, endPoint, pointList) {
this.startPoint = startPoint;
this.endPoint = endPoint;
this.pointList = pointList;
this.openList = [
{
point: this.startPoint, // 起点加入openList
cost: 0, // 代价
parent: null, // 父节点
},
];
this.closeList = [];
while (this.openList.length) {
// 在openList中找出优先级最高的点
let point = this.getBestPoint();
// point为终点,那么算法结束,输出最短路径
if (checkIsSamePoint(point.point, this.endPoint)) {
return this.getRoutes(point);
} else {
// 将point从openList中删除
this.removeFromOpenList(point);
// 将point添加到closeList中
this.closeList.push(point);
// 遍历point周围的点
let nextPoints = getNextPoints(point.point, this.pointList);
for (let i = 0; i < nextPoints.length; i++) {
let cur = nextPoints[i];
// 如果该点在closeList中,那么跳过该点
if (this.checkIsInList(cur, this.closeList)) {
continue;
} else if (!this.checkIsInList(cur, this.openList)) {
// 如果该点也不在openList中
let pointObj = {
point: cur,
parent: point,// 设置point为当前点的父节点
cost: 0,
};
// 计算当前点的代价
this.computeCost(pointObj);
// 添加到openList中
this.openList.push(pointObj);
}
}
}
}
return []
}
// 获取openList中优先级最高的点,也就是代价最小的点
getBestPoint() {
let min = Infinity;
let point = null;
this.openList.forEach((item) => {
if (item.cost < min) {
point = item;
min = item.cost;
}
});
return point;
}
// 从point出发,找出其所有祖宗节点,也就是最短路径
getRoutes(point) {
let res = [point];
let par = point.parent;
while (par) {
res.unshift(par);
par = par.parent;
}
return res.map((item) => {
return item.point;
});
}
// 将点从openList中删除
removeFromOpenList(point) {
let index = this.openList.findIndex((item) => {
return checkIsSamePoint(point.point, item.point);
});
this.openList.splice(index, 1);
}
// 检查点是否在列表中
checkIsInList(point, list) {
return list.find((item) => {
return checkIsSamePoint(item.point, point);
});
}
// 计算一个点的代价
computeCost(point) {
// TODO
}
}
The code is a bit long, but the logic is very simple, start
The method is basically to restore the previous algorithm process, the others are some auxiliary tools, only one computeCost
The method has not been implemented for the time being, this method That is A*
the core of the algorithm.
A*
The node priority of the algorithm is determined by two parts:
f(n) = g(n) + h(n)
g(n)
represents the node n
the cost of distance from the starting point.
f(n)
represents the cost of the node n
to the end point, of course, this cost is only an estimate.
f(n)
is g(n)
plus h(n)
, which means the node n
the overall cost, the lower the priority. The higher the level, modify the computeCost
method and disassemble it into two methods:
// 计算一个点的优先级
computePriority(point) {
point.cost = this.computedGCost(point) + this.computedHCost(point);
}
The calculation of g(n)
is very simple, just add up the cost of all its ancestor nodes:
// 计算代价g(n)
computedGCost(point) {
let res = 0;
let par = point.parent;
while (par) {
res += par.cost;
par = par.parent;
}
return res;
}
And the calculation of h(n)
will use the Manhattan distance. The Manhattan distance of two points refers to the total distance between the two points in the horizontal and vertical directions:
For our calculation, that is the Manhattan distance from the current node to the end point:
// 计算代价h(n)
computedHCost(point) {
return (
Math.abs(this.endPoint[0] - point.point[0]) +
Math.abs(this.endPoint[1] - point.point[1])
);
}
Next instantiate a AStar
class and use it to calculate the shortest path:
const aStar = new AStar();
const onDragMove = () => {
let { startPoint, endPoint, fakeStartPoint, fakeEndPoint, points } =
computedProbablyPoints(startPos.value, endPos.value);
const routes = aStar.start(fakeStartPoint, fakeEndPoint, points);
// 更新连线元素
// ...
}
It can be seen that there is no super long path calculated by the backtracking algorithm.
optimization
By the previous section, the shortest path can be basically found, but there will be several problems. In this section, we will try to optimize it.
1. The connecting line breaks through the bounding box
As shown in the figure above, the connection line of the vertical part is obviously too close to the element. Although it has not overlapped with the element, it has broken through the bounding box. The better connection points should be the two on the right. The situation in the following figure is similar:
The solution is also very simple. Earlier, we implemented a method to determine whether a line segment passes through or overlaps with the start and end elements. We modify the comparison conditions and change the comparison object from the element itself to the element's bounding box:
export const checkLineThroughElements = (a, b) => {
// ...
// 水平线
if (a[1] === b[1]) {
for (let i = 0; i < rects.length; i++) {
let rect = rects[i];
if (
minY > rect.y() - MIN_DISTANCE &&// 增加或减去MIN_DISTANCE来将比较目标由元素改成元素的包围框
minY < rect.y() + rect.height() + MIN_DISTANCE &&
minX < rect.x() + rect.width() + MIN_DISTANCE &&
maxX > rect.x() - MIN_DISTANCE
) {
return true;
}
}
} else if (a[0] === b[0]) {
// 垂直线
for (let i = 0; i < rects.length; i++) {
let rect = rects[i];
if (
minX > rect.x() - MIN_DISTANCE &&
minX < rect.x() + rect.width() + MIN_DISTANCE &&
minY < rect.y() + rect.height() + MIN_DISTANCE &&
maxY > rect.y() - MIN_DISTANCE
) {
return true;
}
}
}
return false;
};
The effect is as follows:
2. The distance is too close and there is no connecting line
At present, if the two elements are too close in our logic, then the points that meet the requirements cannot be calculated, and naturally there is no line:
The solution is also very simple. When there is no result in the first path calculation, we assume that the distance is very close, and then we calculate it again in the loose mode. The so-called loose mode is to remove the judgment of whether it crosses or intersects with the element. That is to say, skip checkLineThroughElements
this method, in addition, the real starting point and end point should be added to the point list to participate in the calculation, and the starting point and end point of the calculation will no longer use the pseudo starting point and the pseudo end point, but use the real The starting point and the ending point, otherwise the following situation will occur:
First, modify the onDragMove
method to separate the path calculation into a method for easy reuse:
const onDragMove = () => {
// 计算点和路径提取成一个方法
let { startPoint, endPoint, routes } = computeRoutes();
// 如果没有计算出来路径,那么就以宽松模式再计算一次可能的点,也就是允许和元素交叉
if (routes.length <= 0) {
let res = computeRoutes(true);
routes = res.routes;
}
// 更新连线元素
updateLine(
(routes.length > 0 ? [startPoint, ...routes, endPoint] : []).reduce(
(path, cur) => {
path.push(cur[0], cur[1]);
return path;
},
[]
)
);
};
// 计算路径
const computeRoutes = (easy) => {
// 计算出所有可能的点
let { startPoint, endPoint, fakeStartPoint, fakeEndPoint, points } =
computedProbablyPoints(startPos.value, endPos.value, easy);
// 使用A*算法
let routes = = aStar.start(
easy ? startPoint : fakeStartPoint,// 如果是宽松模式则使用真正的起点和终点
easy ? endPoint : fakeEndPoint,
points
);
return {
startPoint,
endPoint,
routes,
};
};
Then modify the computedProbablyPoints
method and add a easy
parameter. When the parameter is true
, add the real starting point and end point to the list of points:
const computedProbablyPoints = (startPos, endPos, easy) => {
// ...
// 是否是宽松模式
easyMode = easy;
// 保存所有可能经过的点
let points = [];
// 宽松模式则把真正的起点和终点加入点列表中
if (easy) {
points.push(startPoint, endPoint);
}
// ...
}
Finally, modify the method of calculating the points around a point, and remove the detection of whether it crosses or intersects with elements:
const getNextPoint = (x, y, list, dir) => {
// ...
for (let i = 0; i < list.length; i++) {
let cur = list[i];
// 检查当前点和目标点的连线是否穿过起终点元素,宽松模式下直接跳过该检测
if (!easyMode && checkLineThroughElements([x, y], cur)) {
continue;
}
}
}
The final effect is as follows:
Summarize
This article tries to find the associated line path of the node through the A*
algorithm. I originally thought that the difficulty lies in the algorithm, but I did not expect to find the most difficult point in the implementation process. If there is a better way to find the point, welcome Leave a message in the comment area.
Source address: https://github.com/wanglin2/AssociationLineDemo .
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。