Skip to content

Commit f25d443

Browse files
committed
Add PathFinding3n
1 parent 22a9713 commit f25d443

File tree

1 file changed

+232
-0
lines changed

1 file changed

+232
-0
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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

Comments
 (0)