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