|
| 1 | +--- A pathfinder based on A* to find the shortest path in a directed graph. |
| 2 | +--- The graph is defined by its edges only, there are no nodes needed. |
| 3 | +--- |
| 4 | +--- Edges are represented by polylines, and they can unidirectional or bidirectional. |
| 5 | +--- Unidirectional edges can only be entered at one end (at the first vertex of the |
| 6 | +--- polyline) and exited at the other (last vertex of the polyline) |
| 7 | +--- |
| 8 | +--- Bidirectional edges can be entered at either end, and exited at the other. |
| 9 | +--- |
| 10 | +--- Edges don't have to be connected, as long as the entry of another edge is close |
| 11 | +--- enough to the exit of another, the entry is a valid successor node (of the exit, |
| 12 | +--- which is the predecessor) |
| 13 | + |
| 14 | +---@class GraphPathfinder : HybridAStar |
| 15 | +GraphPathfinder = CpObject(HybridAStar) |
| 16 | + |
| 17 | +--- An edge of a directed graph |
| 18 | +---@class GraphPathfinder.GraphEdge : Polyline |
| 19 | +GraphPathfinder.GraphEdge = CpObject(Polyline) |
| 20 | + |
| 21 | +GraphPathfinder.GraphEdge.UNIDIRECTIONAL = {} |
| 22 | +GraphPathfinder.GraphEdge.BIDIRECTIONAL = {} |
| 23 | + |
| 24 | +---@param direction table GraphEdge.UNIDIRECTIONAL or GraphEdge.BIDIRECTIONAL |
| 25 | +---@param vertices table[] array of tables with x, y (Vector, Vertex, State3D or just plain {x, y} |
| 26 | +function GraphPathfinder.GraphEdge:init(direction, vertices) |
| 27 | + Polyline.init(self, vertices) |
| 28 | + self.direction = direction |
| 29 | +end |
| 30 | + |
| 31 | +---@return boolean is this a bidirectional edge? |
| 32 | +function GraphPathfinder.GraphEdge:isBidirectional() |
| 33 | + return self.direction == GraphPathfinder.GraphEdge.BIDIRECTIONAL |
| 34 | +end |
| 35 | + |
| 36 | +---@return Vertex[] array of vertices that can be used to enter this edge (one for |
| 37 | +--- unidirectional, two for bidirectional edges) |
| 38 | +function GraphPathfinder.GraphEdge:getEntries() |
| 39 | + if self:isBidirectional() then |
| 40 | + return { self[1], self[#self] } |
| 41 | + else |
| 42 | + return { self[1] } |
| 43 | + end |
| 44 | +end |
| 45 | + |
| 46 | +---@param entry Vector |
| 47 | +---@return Vector the exit when entered through the given entry |
| 48 | +function GraphPathfinder.GraphEdge:getExit(entry) |
| 49 | + if entry == self[1] then |
| 50 | + return self[#self] |
| 51 | + else |
| 52 | + return self[1] |
| 53 | + end |
| 54 | +end |
| 55 | + |
| 56 | +---@param yieldAfter number coroutine yield after so many iterations (number of iterations in one update loop) |
| 57 | +---@param maxIterations number maximum iterations before failing |
| 58 | +---@param range number when an edge's exit is closer than range to another edge's entry, the |
| 59 | +--- two edges are considered as connected (and thus can traverse from one to the other) |
| 60 | +---@param graph Vector[] the graph as described in the file header |
| 61 | +function GraphPathfinder:init(yieldAfter, maxIterations, range, graph) |
| 62 | + HybridAStar.init(self, { }, yieldAfter, maxIterations) |
| 63 | + self.range = range |
| 64 | + self.graph = graph |
| 65 | + self.deltaPosGoal = self.range |
| 66 | + self.deltaThetaDeg = 181 |
| 67 | + self.deltaThetaGoal = math.rad(self.deltaThetaDeg) |
| 68 | + self.maxDeltaTheta = math.pi |
| 69 | + self.originalDeltaThetaGoal = self.deltaThetaGoal |
| 70 | + self.analyticSolverEnabled = false |
| 71 | + self.ignoreValidityAtStart = false |
| 72 | +end |
| 73 | + |
| 74 | +function GraphPathfinder:getMotionPrimitives(turnRadius, allowReverse) |
| 75 | + return GraphPathfinder.GraphMotionPrimitives(self.range, self.graph) |
| 76 | +end |
| 77 | + |
| 78 | +--- Motion primitives to use with the graph pathfinder, providing the entries |
| 79 | +--- to the next edges. |
| 80 | +---@class GraphPathfinder.GraphMotionPrimitives : HybridAStar.MotionPrimitives |
| 81 | +GraphPathfinder.GraphMotionPrimitives = CpObject(HybridAStar.MotionPrimitives) |
| 82 | + |
| 83 | +---@param range number when an edge's exit is closer than range to another edge's entry, the |
| 84 | +--- two edges are considered as connected (and thus can traverse from one to the other) |
| 85 | +---@param graph Vector[] the graph as described in the file header |
| 86 | +function GraphPathfinder.GraphMotionPrimitives:init(range, graph) |
| 87 | + self.range = range |
| 88 | + self.graph = graph |
| 89 | +end |
| 90 | + |
| 91 | +---@return table [{x, y, d}] array of the next possible entries, their coordinates and |
| 92 | +--- the distance to the entry + the length of the edge |
| 93 | +function GraphPathfinder.GraphMotionPrimitives:getPrimitives(node) |
| 94 | + local primitives = {} |
| 95 | + for _, edge in ipairs(self.graph) do |
| 96 | + local entries = edge:getEntries() |
| 97 | + for _, entry in ipairs(entries) do |
| 98 | + local distanceToEntry = (node - entry):length() |
| 99 | + if distanceToEntry <= self.range then |
| 100 | + local exit = edge:getExit(entry) |
| 101 | + table.insert(primitives, {x = exit.x, y = exit.y, d = edge:getLength() + distanceToEntry} ) |
| 102 | + end |
| 103 | + end |
| 104 | + end |
| 105 | + return primitives |
| 106 | +end |
| 107 | + |
| 108 | +---@return State3D successor for the given primitive |
| 109 | +function GraphPathfinder.GraphMotionPrimitives:createSuccessor(node, primitive, hitchLength) |
| 110 | + return State3D(primitive.x, primitive.y, 0, node.g, node, node.gear, node.steer, |
| 111 | + 0, node.d + primitive.d) |
| 112 | +end |
| 113 | + |
0 commit comments