1use crate::graph::{GridGraphParams, NeighborCandidate};
4use crate::hex::direction::{HexDirection, HexNodeNeighbor};
5use crate::Float;
6use kiddo::{KdTree, SquaredEuclidean};
7use nalgebra::{Point2, Vector2};
8
9pub trait HexNeighborValidator<F: Float = f32> {
14 type PointData;
17
18 fn validate(
22 &self,
23 source_index: usize,
24 source_data: &Self::PointData,
25 candidate: &NeighborCandidate<F>,
26 candidate_data: &Self::PointData,
27 ) -> Option<(HexDirection, F)>;
28}
29
30pub struct HexGridGraph<F: Float = f32> {
35 pub neighbors: Vec<Vec<HexNodeNeighbor<F>>>,
37}
38
39impl<F: Float + kiddo::float::kdtree::Axis> HexGridGraph<F> {
40 pub fn build<V: HexNeighborValidator<F>>(
47 positions: &[Point2<F>],
48 point_data: &[V::PointData],
49 validator: &V,
50 params: &GridGraphParams<F>,
51 ) -> Self {
52 assert_eq!(
53 positions.len(),
54 point_data.len(),
55 "positions and point_data must have the same length"
56 );
57
58 let coords: Vec<[F; 2]> = positions.iter().map(|p| [p.x, p.y]).collect();
59 let tree: KdTree<F, 2> = (&coords).into();
60 let max_dist_sq = params.max_distance * params.max_distance;
61
62 let mut neighbors = Vec::with_capacity(positions.len());
63
64 for (i, pos) in positions.iter().enumerate() {
65 let query = [pos.x, pos.y];
66 let results = tree.nearest_n::<SquaredEuclidean>(&query, params.k_neighbors);
67
68 let mut candidates = Vec::new();
69
70 for nn in results {
71 let j = nn.item as usize;
72 if j == i {
73 continue;
74 }
75
76 let dist_sq = nn.distance;
77 if dist_sq > max_dist_sq {
78 continue;
79 }
80
81 let neighbor_pos = positions[j];
82 let offset = Vector2::new(neighbor_pos.x - pos.x, neighbor_pos.y - pos.y);
83 let distance = dist_sq.sqrt();
84
85 let candidate = NeighborCandidate {
86 index: j,
87 offset,
88 distance,
89 };
90
91 if let Some((direction, score)) =
92 validator.validate(i, &point_data[i], &candidate, &point_data[j])
93 {
94 candidates.push(HexNodeNeighbor {
95 direction,
96 index: j,
97 distance,
98 score,
99 });
100 }
101 }
102
103 neighbors.push(select_hex_neighbors(candidates));
104 }
105
106 Self { neighbors }
107 }
108}
109
110fn select_hex_neighbors<F: Float>(candidates: Vec<HexNodeNeighbor<F>>) -> Vec<HexNodeNeighbor<F>> {
112 let mut best: [Option<HexNodeNeighbor<F>>; 6] = [None, None, None, None, None, None];
113
114 for candidate in candidates {
115 let slot = &mut best[candidate.direction.slot_index()];
116
117 let replace = match slot {
118 None => true,
119 Some(current) => {
120 candidate.score < current.score
121 || (candidate.score == current.score && candidate.distance < current.distance)
122 }
123 };
124
125 if replace {
126 *slot = Some(candidate);
127 }
128 }
129
130 best.into_iter().flatten().collect()
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 struct AngleValidator;
139
140 impl HexNeighborValidator for AngleValidator {
141 type PointData = ();
142
143 fn validate(
144 &self,
145 _source_index: usize,
146 _source_data: &(),
147 candidate: &NeighborCandidate,
148 _candidate_data: &(),
149 ) -> Option<(HexDirection, f32)> {
150 let angle = candidate.offset.y.atan2(candidate.offset.x);
151 let deg = angle.to_degrees();
152
153 let dir = if (-30.0..30.0).contains(°) {
155 HexDirection::East
156 } else if (30.0..90.0).contains(°) {
157 HexDirection::SouthEast
158 } else if (90.0..150.0).contains(°) {
159 HexDirection::SouthWest
160 } else if !(-150.0..150.0).contains(°) {
161 HexDirection::West
162 } else if (-150.0..-90.0).contains(°) {
163 HexDirection::NorthWest
164 } else {
165 HexDirection::NorthEast
166 };
167
168 Some((dir, candidate.distance))
169 }
170 }
171
172 fn hex_lattice(radius: i32, spacing: f32) -> Vec<Point2<f32>> {
174 let mut points = Vec::new();
175 let sqrt3 = 3.0f32.sqrt();
176 for q in -radius..=radius {
177 for r in -radius..=radius {
178 if (q + r).abs() > radius {
179 continue;
180 }
181 let x = spacing * (q as f32 + r as f32 * 0.5);
182 let y = spacing * (r as f32 * sqrt3 / 2.0);
183 points.push(Point2::new(x, y));
184 }
185 }
186 points
187 }
188
189 #[test]
190 fn center_node_has_six_neighbors() {
191 let spacing = 50.0;
192 let points = hex_lattice(2, spacing);
193 let data = vec![(); points.len()];
194
195 let params = GridGraphParams {
196 k_neighbors: 12,
197 max_distance: spacing * 1.5,
198 };
199
200 let graph = HexGridGraph::build(&points, &data, &AngleValidator, ¶ms);
201
202 let center = points
204 .iter()
205 .position(|p| p.x.abs() < 0.01 && p.y.abs() < 0.01)
206 .unwrap();
207
208 assert_eq!(graph.neighbors[center].len(), 6);
209 }
210
211 #[test]
212 fn edge_nodes_have_fewer_neighbors() {
213 let spacing = 50.0;
214 let points = hex_lattice(1, spacing);
215 let data = vec![(); points.len()];
216
217 let params = GridGraphParams {
218 k_neighbors: 12,
219 max_distance: spacing * 1.5,
220 };
221
222 let graph = HexGridGraph::build(&points, &data, &AngleValidator, ¶ms);
223
224 for (i, p) in points.iter().enumerate() {
226 if p.x.abs() < 0.01 && p.y.abs() < 0.01 {
227 assert_eq!(graph.neighbors[i].len(), 6);
228 } else {
229 assert_eq!(
230 graph.neighbors[i].len(),
231 3,
232 "edge node {i} at ({}, {}) has {} neighbors",
233 p.x,
234 p.y,
235 graph.neighbors[i].len()
236 );
237 }
238 }
239 }
240
241 #[test]
242 fn select_keeps_best_per_direction() {
243 let candidates = vec![
244 HexNodeNeighbor {
245 direction: HexDirection::East,
246 index: 1,
247 distance: 50.0,
248 score: 0.9,
249 },
250 HexNodeNeighbor {
251 direction: HexDirection::East,
252 index: 2,
253 distance: 55.0,
254 score: 0.5,
255 },
256 HexNodeNeighbor {
257 direction: HexDirection::West,
258 index: 3,
259 distance: 50.0,
260 score: 0.3,
261 },
262 ];
263
264 let selected = select_hex_neighbors(candidates);
265 assert_eq!(selected.len(), 2);
266
267 let east = selected
268 .iter()
269 .find(|n| n.direction == HexDirection::East)
270 .unwrap();
271 assert_eq!(east.index, 2); }
273}