From ad56404238bbe6a94aaf0510991c72db18c896ec Mon Sep 17 00:00:00 2001 From: wanglin2 <1013335014@qq.com> Date: Wed, 8 Jun 2022 10:44:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AStar.js | 126 ++++++++++++ src/App.vue | 545 ++------------------------------------------------- src/dfs.js | 27 +++ src/draw.js | 99 ++++++++++ src/utils.js | 322 ++++++++++++++++++++++++++++++ 5 files changed, 591 insertions(+), 528 deletions(-) create mode 100644 src/AStar.js create mode 100644 src/dfs.js create mode 100644 src/draw.js create mode 100644 src/utils.js diff --git a/src/AStar.js b/src/AStar.js new file mode 100644 index 0000000..2cdeb86 --- /dev/null +++ b/src/AStar.js @@ -0,0 +1,126 @@ +import { checkIsSamePoint, getNextPoints } from "./utils"; + +// A*算法类 +export default 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) { + let point = this.getBestPoint(); + if (!point) { + return []; + } + // 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, + cost: 0, + }; + this.computeCost(pointObj); + 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) { + point.cost = this.computedGCost(point) + this.computedHCost(point); + } + + // 计算代价g(n) + computedGCost(point) { + let res = 0; + let par = point.parent; + while (par) { + res += par.cost; + par = par.parent; + } + return res; + } + + // 计算代价h(n) + computedHCost(point) { + return ( + Math.abs(this.endPoint[0] - point.point[0]) + + Math.abs(this.endPoint[1] - point.point[1]) + ); + } +} diff --git a/src/App.vue b/src/App.vue index 1a81b4e..8d6a62c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,551 +4,40 @@ diff --git a/src/dfs.js b/src/dfs.js new file mode 100644 index 0000000..15eb34a --- /dev/null +++ b/src/dfs.js @@ -0,0 +1,27 @@ +import { checkIsSamePoint, getNextPoints } from "./utils"; + +// 使用回溯算法寻找路径 +export 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; +}; diff --git a/src/draw.js b/src/draw.js new file mode 100644 index 0000000..501025a --- /dev/null +++ b/src/draw.js @@ -0,0 +1,99 @@ +import Konva from "konva"; + +// 初始化图形 +let layer = null, + line; +export const init = (container, onDragMove) => { + const { width, height } = container.value.getBoundingClientRect(); + + // 创建舞台 + let stage = new Konva.Stage({ + container: container.value, + width, + height, + }); + + // 创建图层 + layer = new Konva.Layer(); + + // 创建两个矩形 + let rect1 = new Konva.Rect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: "#fbfbfb", + stroke: "black", + strokeWidth: 4, + draggable: true, + }); + + let 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(); + + return { + rect1, + rect2, + line, + }; +}; + +// 绘制所有可能的顶点,测试用 +let testDotElementList = []; +export const drawTestDots = (points) => { + testDotElementList.forEach((testDotElement) => { + testDotElement.remove(); + }); + testDotElementList = points.map((point) => { + const circle = new Konva.Circle({ + x: point[0], + y: point[1], + radius: 2, + fill: "red", + stroke: "black", + strokeWidth: 4, + }); + + layer.add(circle); + + return circle; + }); +}; + +// 更新连线 +export const updateLine = (points) => { + line.points(points); +}; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..b265dda --- /dev/null +++ b/src/utils.js @@ -0,0 +1,322 @@ +let rect1, + rect2, + startPoint = null, + endPoint = null; + +let rect1X, rect1Y, rect1W, rect1H, rect2X, rect2Y, rect2W, rect2H; + +const MIN_DISTANCE = 30; + +// 保存矩形元素 +export const setRect = (r1, r2) => { + rect1 = r1; + rect2 = r2; +}; + +// 计算所有可能经过的点 +export 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];// 上 + startPoint = [rect1X, rect1Y + rect1H / 2]; // 左 + + // endPoint = [rect2X + rect2W / 2, rect2Y];// 上 + endPoint = [rect2X, rect2Y + rect2H / 2]; // 左 + // endPoint = [rect2X + rect2W, rect2Y + rect2H / 2]; // 右 + // endPoint = [rect2X + rect2W / 2, rect2Y + rect2H];// 下 + + // 保存所有可能经过的点 + let points = [startPoint, endPoint]; + + // 起点元素包围框上的四个顶点 + let startBoundingBox = [ + [rect1X - MIN_DISTANCE, rect1Y - MIN_DISTANCE], // 左上 + [rect1X + rect1W + MIN_DISTANCE, rect1Y - MIN_DISTANCE], // 右上 + [rect1X - MIN_DISTANCE, rect1Y + rect1H + MIN_DISTANCE], // 左下 + [rect1X + rect1W + MIN_DISTANCE, rect1Y + rect1H + MIN_DISTANCE], // 右下 + ]; + points.push(...startBoundingBox); + + // 经过起点且垂直于起点元素包围框的线与包围框线的交点 + let fakeStartPoint = findStartNextOrEndPrePoint(rect1, startPoint); + points.push(fakeStartPoint); + + // 终点元素保包围框上的四个顶点 + let endBoundingBox = [ + [rect2X - MIN_DISTANCE, rect2Y - MIN_DISTANCE], // 左上 + [rect2X + rect2W + MIN_DISTANCE, rect2Y - MIN_DISTANCE], // 右上 + [rect2X - MIN_DISTANCE, rect2Y + rect2H + MIN_DISTANCE], // 左下 + [rect2X + rect2W + MIN_DISTANCE, rect2Y + rect2H + MIN_DISTANCE], // 右下 + ]; + points.push(...endBoundingBox); + + // 经过终点且垂直于终点元素包围框的线与包围框线的交点 + let fakeEndPoint = findStartNextOrEndPrePoint(rect2, endPoint); + points.push(fakeEndPoint); + + // 两个包围框组成的更大的包围框的四个顶点 + let boundingBoxXList = []; + let boundingBoxYList = []; + [...startBoundingBox, ...endBoundingBox].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); + points.push( + [minBoundingBoxX, minBoundingBoxY], + [maxBoundingBoxX, minBoundingBoxY], + [minBoundingBoxX, maxBoundingBoxY], + [maxBoundingBoxX, maxBoundingBoxY] + ); + + // 起点元素包围框两条水平边的延长线与大包围框的交点 + points.push( + [minBoundingBoxX, startBoundingBox[0][1]], + [maxBoundingBoxX, startBoundingBox[0][1]], + [minBoundingBoxX, startBoundingBox[2][1]], + [maxBoundingBoxX, startBoundingBox[2][1]] + ); + + // 起点元素包围框两条垂直边的延长线与大包围框的交点 + points.push( + [startBoundingBox[0][0], minBoundingBoxY], + [startBoundingBox[0][0], maxBoundingBoxY], + [startBoundingBox[1][0], minBoundingBoxY], + [startBoundingBox[1][0], maxBoundingBoxY] + ); + + // 终点元素包围框两条水平边的延长线与大包围框的交点 + points.push( + [minBoundingBoxX, endBoundingBox[0][1]], + [maxBoundingBoxX, endBoundingBox[0][1]], + [minBoundingBoxX, endBoundingBox[2][1]], + [maxBoundingBoxX, endBoundingBox[2][1]] + ); + + // 终点元素包围框两条垂直边的延长线与大包围框的交点 + points.push( + [endBoundingBox[0][0], minBoundingBoxY], + [endBoundingBox[0][0], maxBoundingBoxY], + [endBoundingBox[1][0], minBoundingBoxY], + [endBoundingBox[1][0], maxBoundingBoxY] + ); + + // 起点包围框的水平边的延长线与终点包围框的垂直边的延长线的交点 + points.push( + [startBoundingBox[0][0], endBoundingBox[0][1]], + [startBoundingBox[1][0], endBoundingBox[0][1]], + [startBoundingBox[0][0], endBoundingBox[2][1]], + [startBoundingBox[1][0], endBoundingBox[2][1]] + ); + + // 起点包围框的垂直边的延长线与终点包围框的水平边的延长线的交点 + points.push( + [endBoundingBox[0][0], startBoundingBox[0][1]], + [endBoundingBox[0][0], startBoundingBox[2][1]], + [endBoundingBox[1][0], startBoundingBox[0][1]], + [endBoundingBox[1][0], startBoundingBox[2][1]] + ); + + // 经过起点的垂直线与包围框所有水平边延长线的交点 + points.push( + [startPoint[0], startBoundingBox[0][1]], + [startPoint[0], startBoundingBox[2][1]], + [startPoint[0], endBoundingBox[0][1]], + [startPoint[0], endBoundingBox[2][1]] + ); + + // 经过终点的垂直线与包围框所有水平边延长线的交点 + points.push( + [endPoint[0], startBoundingBox[0][1]], + [endPoint[0], startBoundingBox[2][1]], + [endPoint[0], endBoundingBox[0][1]], + [endPoint[0], endBoundingBox[2][1]] + ); + + // 去重 + points = removeDuplicatePoint(points); + + return { + startPoint, + endPoint, + fakeStartPoint, + fakeEndPoint, + points, + }; +}; + +// 找出起点的下一个点或终点的前一个点 +export 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, + ]; + } +}; + +// 找出一个点周边的点 +export 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, xSamePoints, "y"); + + // 找出y方向最近的点 + let yNextPoints = getNextPoint(x, y, ySamePoints, "x"); + + return [...yNextPoints, ...xNextPoints]; +}; + +// 找出水平或垂直方向上最近的点 +export const getNextPoint = (x, y, list, dir) => { + let index = dir === "x" ? 0 : 1; + 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 (checkLineThroughOrClose([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; + } + } + } + // 如果下一个点是起点或终点,那么直接忽略掉 + if ( + checkIsSamePoint(nextLeftTopPoint, startPoint) || + checkIsSamePoint(nextLeftTopPoint, endPoint) + ) { + nextLeftTopPoint = null; + } + if ( + checkIsSamePoint(nextRIghtBottomPoint, startPoint) || + checkIsSamePoint(nextRIghtBottomPoint, endPoint) + ) { + nextRIghtBottomPoint = null; + } + return [nextLeftTopPoint, nextRIghtBottomPoint].filter((point) => { + return !!point; + }); +}; + +// 检查直线是否穿过元素或离元素太近 +export const checkLineThroughOrClose = (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; +}; + +// 检测是否为同一个点 +export const checkIsSamePoint = (a, b) => { + if (!a || !b) { + return false; + } + return a[0] === b[0] && a[1] === b[1]; +}; + +// 去重 +export 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; +}; \ No newline at end of file