|
| 1 | +/* |
| 2 | + * Copyright © 2025 Dustin Collins (Strega's Gate) |
| 3 | + * All Rights Reserved. |
| 4 | + * |
| 5 | + * http://stregasgate.com |
| 6 | + */ |
| 7 | + |
| 8 | +public import Collections |
| 9 | + |
| 10 | +public typealias PathFinding3f = PathFinding3n<Float32> |
| 11 | +public typealias PathFinding3d = PathFinding3n<Float64> |
| 12 | + |
| 13 | +/// A type that can trace a path through a graph of known positions. Based on the A-Star algorithm. |
| 14 | +public struct PathFinding3n<Scalar: Vector3n.ScalarType & FloatingPoint & Comparable> { |
| 15 | + @usableFromInline |
| 16 | + internal var graph: OrderedSet<Node> |
| 17 | + |
| 18 | + public var linkDistance: Scalar |
| 19 | + |
| 20 | + /** |
| 21 | + Build an empty PathFinding3 graph, to be populated later. |
| 22 | + - parameter manhatanDistance: A distance value checked against each axis (x,y,z) of each node. Nodes within the distance are linked together in the graph |
| 23 | + */ |
| 24 | + public init(linkDistance manhatanDistance: Scalar) { |
| 25 | + self.linkDistance = manhatanDistance |
| 26 | + self.graph = [] |
| 27 | + } |
| 28 | + |
| 29 | + /** |
| 30 | + Build a graph from provided positions linking any withing the provided distance |
| 31 | + - parameter positions: The unique unordered positions of the nodes |
| 32 | + - parameter manhatanDistance: A distance value checked against each axis (x,y,z) of each node. Nodes within the distance are linked together in the graph |
| 33 | + */ |
| 34 | + public init(positions: [Position3n<Scalar>], linkedWithin manhatanDistance: Scalar) { |
| 35 | + self.init(linkDistance: manhatanDistance) |
| 36 | + |
| 37 | + for position in positions { |
| 38 | + self.insert(position) |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + @discardableResult |
| 43 | + public mutating func insert(_ position: Position3n<Scalar>) -> Index { |
| 44 | + var node1 = Node(position: position) |
| 45 | + let result = graph.append(node1) |
| 46 | + let node1Index = result.index |
| 47 | + if result.inserted { |
| 48 | + for node2Index in self.indices { |
| 49 | + guard node2Index != node1Index else {continue} |
| 50 | + |
| 51 | + var node2 = self[node2Index] |
| 52 | + guard Swift.abs(node1.position.x - node2.position.x) <= linkDistance else {continue} |
| 53 | + guard Swift.abs(node1.position.y - node2.position.y) <= linkDistance else {continue} |
| 54 | + guard Swift.abs(node1.position.z - node2.position.z) <= linkDistance else {continue} |
| 55 | + |
| 56 | + let distance = node1.position.distance(from: node2.position) |
| 57 | + node1.children.insert(.init(index: node2Index, distance: distance)) |
| 58 | + node2.children.insert(.init(index: node1Index, distance: distance)) |
| 59 | + self.graph.update(node2, at: node2Index) |
| 60 | + } |
| 61 | + self.graph.update(node1, at: node1Index) |
| 62 | + } |
| 63 | + return node1Index |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +extension PathFinding3n { |
| 68 | + public func nearestNodes(to position: Position3n<Scalar>, withinRadius: Scalar? = nil) -> [Index] { |
| 69 | + let sortNodes: [(index: Index, distance: Scalar)] = self.indices.map({ index in |
| 70 | + let node = self[index] |
| 71 | + return (index: index, distance: node.position.distance(from: position)) |
| 72 | + }).sorted(by: {$0.distance < $1.distance}) |
| 73 | + return sortNodes.compactMap({ |
| 74 | + if let radius = withinRadius { |
| 75 | + if $0.distance > radius { |
| 76 | + return nil |
| 77 | + } |
| 78 | + } |
| 79 | + return $0.index |
| 80 | + }) |
| 81 | + } |
| 82 | + |
| 83 | + public func path(from startIndex: Index, to goalIndex: Index) -> [Index]? { |
| 84 | + precondition(self.indices.contains(startIndex), "Index out of range.") |
| 85 | + precondition(self.indices.contains(goalIndex), "Index out of range.") |
| 86 | + |
| 87 | + // The node we want to reach |
| 88 | + let goalNode = self[goalIndex] |
| 89 | + |
| 90 | + // A collection of pairs where the key is the desired node, |
| 91 | + // and the value is the previous node traveled (its parent in the path) |
| 92 | + var parents: Dictionary<Index, Index> = [:] |
| 93 | + |
| 94 | + // A list of nodes that could be valid for the path |
| 95 | + var openList: Deque<PathNode> = [ |
| 96 | + PathNode(index: startIndex, hCost: 0) // Start Node |
| 97 | + ] |
| 98 | + // A list of nodes that have already been checked |
| 99 | + var visited: Set<Index> = [] |
| 100 | + |
| 101 | + // A list of gScores for checked nodes |
| 102 | + var gScores: Dictionary<Index, Scalar> = [ |
| 103 | + startIndex: 0 // Give the start node a zero gScore |
| 104 | + ] |
| 105 | + |
| 106 | + while openList.isEmpty == false { |
| 107 | + let currentPathNode = openList.removeFirst() |
| 108 | + |
| 109 | + // Build the path if we reached the end |
| 110 | + if currentPathNode.index == goalIndex { |
| 111 | + var path: [Index] = [] |
| 112 | + var currentIndex: Array<Node>.Index? = currentPathNode.index |
| 113 | + while let _currentIndex = currentIndex { |
| 114 | + path.append(_currentIndex) |
| 115 | + currentIndex = parents[_currentIndex] |
| 116 | + } |
| 117 | + assert(path.last == startIndex && path.first == goalIndex) |
| 118 | + return path.reversed() |
| 119 | + } |
| 120 | + |
| 121 | + // mark current node as visited |
| 122 | + visited.insert(currentPathNode.index) |
| 123 | + |
| 124 | + let currentGraphNode = self[currentPathNode.index] |
| 125 | + |
| 126 | + for child in currentGraphNode.children { |
| 127 | + // Don't check already visited nodes |
| 128 | + guard visited.contains(child.index) == false else { continue } |
| 129 | + |
| 130 | + let currentGraphNodeChild = self[child.index] |
| 131 | + |
| 132 | + let gCost = gScores[currentPathNode.index, default: .infinity] + child.distance |
| 133 | + let gCostChild = gScores[child.index, default: .infinity] |
| 134 | + |
| 135 | + if gCost < gCostChild { |
| 136 | + gScores[child.index] = gCost |
| 137 | + |
| 138 | + let gCost: Scalar = gCost |
| 139 | + let fCost: Scalar = // Manhattan distance |
| 140 | + Swift.abs(currentGraphNodeChild.position.x - goalNode.position.x) + |
| 141 | + Swift.abs(currentGraphNodeChild.position.y - goalNode.position.y) + |
| 142 | + Swift.abs(currentGraphNodeChild.position.z - goalNode.position.z) |
| 143 | + let hCost = gCost + fCost |
| 144 | + |
| 145 | + let pathNode = PathNode( |
| 146 | + index: child.index, |
| 147 | + hCost: hCost |
| 148 | + ) |
| 149 | + |
| 150 | + // Keep openList sorted by hCost |
| 151 | + if let index = openList.firstIndex(where: {hCost < $0.hCost}) { |
| 152 | + openList.insert(pathNode, at: index) |
| 153 | + }else{ |
| 154 | + openList.append(pathNode) |
| 155 | + } |
| 156 | + |
| 157 | + parents[child.index] = currentPathNode.index |
| 158 | + } |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + // No path found |
| 163 | + return nil |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +extension PathFinding3n { |
| 168 | + public struct Node: Equatable, Hashable { |
| 169 | + public let position: Position3n<Scalar> |
| 170 | + public var children: Set<Child> |
| 171 | + public struct Child: Equatable, Hashable { |
| 172 | + public let index: Index |
| 173 | + public let distance: Scalar |
| 174 | + |
| 175 | + public init(index: Index, distance: Scalar) { |
| 176 | + self.index = index |
| 177 | + self.distance = distance |
| 178 | + } |
| 179 | + |
| 180 | + public static func == (lhs: Self, rhs: Self) -> Bool { |
| 181 | + return lhs.index == rhs.index |
| 182 | + } |
| 183 | + public func hash(into hasher: inout Hasher) { |
| 184 | + hasher.combine(index) |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + public init(position: Position3n<Scalar>, children: Set<Child> = []) { |
| 189 | + self.position = position |
| 190 | + self.children = children |
| 191 | + } |
| 192 | + |
| 193 | + public static func == (lhs: Self, rhs: Self) -> Bool { |
| 194 | + return lhs.position == rhs.position |
| 195 | + } |
| 196 | + } |
| 197 | + |
| 198 | + struct PathNode { |
| 199 | + let index: Index |
| 200 | + var hCost: Scalar |
| 201 | + |
| 202 | + init(index: Index, hCost: Scalar) { |
| 203 | + self.index = index |
| 204 | + self.hCost = hCost |
| 205 | + } |
| 206 | + } |
| 207 | +} |
| 208 | + |
| 209 | +extension PathFinding3n: RandomAccessCollection { |
| 210 | + public typealias Index = Int |
| 211 | + public typealias Element = Node |
| 212 | + |
| 213 | + @inlinable |
| 214 | + public var startIndex: Index { |
| 215 | + return 0 |
| 216 | + } |
| 217 | + |
| 218 | + @inlinable |
| 219 | + public var endIndex: Index { |
| 220 | + if graph.isEmpty { |
| 221 | + return startIndex |
| 222 | + } |
| 223 | + return graph.count |
| 224 | + } |
| 225 | + |
| 226 | + @inlinable |
| 227 | + public subscript(index: Index) -> Node { |
| 228 | + nonmutating get { |
| 229 | + return graph[index] |
| 230 | + } |
| 231 | + } |
| 232 | +} |
0 commit comments