1use crate::grid_index::GridIndex;
8use nalgebra::Point2;
9use std::collections::HashMap;
10
11const HEX_AXIS_PAIRS: [((i32, i32), (i32, i32)); 3] = [
15 ((-1, 0), (1, 0)),
17 ((1, -1), (-1, 1)),
19 ((0, -1), (0, 1)),
21];
22
23pub fn hex_predict_grid_position(
33 grid: &HashMap<GridIndex, Point2<f32>>,
34 idx: GridIndex,
35) -> Option<Point2<f32>> {
36 let mut pred_sum = Point2::new(0.0f32, 0.0f32);
37 let mut pred_count = 0u32;
38
39 for &((dq_a, dr_a), (dq_b, dr_b)) in &HEX_AXIS_PAIRS {
40 let a = GridIndex {
41 i: idx.i + dq_a,
42 j: idx.j + dr_a,
43 };
44 let b = GridIndex {
45 i: idx.i + dq_b,
46 j: idx.j + dr_b,
47 };
48 if let (Some(&pa), Some(&pb)) = (grid.get(&a), grid.get(&b)) {
49 pred_sum.x += 0.5 * (pa.x + pb.x);
50 pred_sum.y += 0.5 * (pa.y + pb.y);
51 pred_count += 1;
52 }
53 }
54
55 if pred_count == 0 {
56 return None;
57 }
58
59 Some(Point2::new(
60 pred_sum.x / pred_count as f32,
61 pred_sum.y / pred_count as f32,
62 ))
63}
64
65pub fn hex_find_inconsistent_corners(
70 grid: &HashMap<GridIndex, Point2<f32>>,
71 threshold: f32,
72) -> Vec<(GridIndex, Point2<f32>)> {
73 let threshold_sq = threshold * threshold;
74 let mut flagged = Vec::new();
75
76 for (&idx, &pos) in grid {
77 if let Some(predicted) = hex_predict_grid_position(grid, idx) {
78 let dx = pos.x - predicted.x;
79 let dy = pos.y - predicted.y;
80 if dx * dx + dy * dy > threshold_sq {
81 flagged.push((idx, predicted));
82 }
83 }
84 }
85
86 flagged
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 fn make_hex_grid(radius: i32, spacing: f32) -> HashMap<GridIndex, Point2<f32>> {
94 let sqrt3 = 3.0f32.sqrt();
95 let mut map = HashMap::new();
96 for q in -radius..=radius {
97 for r in -radius..=radius {
98 if (q + r).abs() > radius {
99 continue;
100 }
101 let x = spacing * (q as f32 + r as f32 * 0.5);
102 let y = spacing * (r as f32 * sqrt3 / 2.0);
103 map.insert(GridIndex { i: q, j: r }, Point2::new(x, y));
104 }
105 }
106 map
107 }
108
109 #[test]
110 fn clean_hex_grid_has_no_inconsistencies() {
111 let grid = make_hex_grid(3, 60.0);
112 let flagged = hex_find_inconsistent_corners(&grid, 3.0);
113 assert!(flagged.is_empty());
114 }
115
116 #[test]
117 fn displaced_corner_is_flagged() {
118 let mut grid = make_hex_grid(2, 60.0);
119 let center = GridIndex { i: 0, j: 0 };
120 grid.insert(center, Point2::new(9.0, 9.0));
122
123 let flagged = hex_find_inconsistent_corners(&grid, 3.0);
124 assert_eq!(1, flagged.len());
125 assert_eq!(center, flagged[0].0);
126
127 let pred = flagged[0].1;
129 assert!(pred.x.abs() < 0.01, "pred.x = {}", pred.x);
130 assert!(pred.y.abs() < 0.01, "pred.y = {}", pred.y);
131 }
132
133 #[test]
134 fn isolated_nodes_are_skipped() {
135 let mut grid = HashMap::new();
136 grid.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
137 grid.insert(GridIndex { i: 10, j: 10 }, Point2::new(500.0, 500.0));
138
139 let flagged = hex_find_inconsistent_corners(&grid, 3.0);
140 assert!(flagged.is_empty());
141 }
142
143 #[test]
144 fn prediction_from_single_axis_pair() {
145 let spacing = 60.0;
146 let sqrt3 = 3.0f32.sqrt();
147 let mut grid = HashMap::new();
148 grid.insert(GridIndex { i: -1, j: 0 }, Point2::new(-spacing, 0.0));
150 grid.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
151 grid.insert(GridIndex { i: 1, j: 0 }, Point2::new(spacing, 0.0));
152
153 let pred = hex_predict_grid_position(&grid, GridIndex { i: 0, j: 0 }).unwrap();
154 assert!((pred.x - 0.0).abs() < 0.01);
155 assert!((pred.y - 0.0).abs() < 0.01);
156
157 let mut grid2 = HashMap::new();
159 grid2.insert(
160 GridIndex { i: 0, j: -1 },
161 Point2::new(-0.5 * spacing, -sqrt3 / 2.0 * spacing),
162 );
163 grid2.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
164 grid2.insert(
165 GridIndex { i: 0, j: 1 },
166 Point2::new(0.5 * spacing, sqrt3 / 2.0 * spacing),
167 );
168
169 let pred2 = hex_predict_grid_position(&grid2, GridIndex { i: 0, j: 0 }).unwrap();
170 assert!((pred2.x - 0.0).abs() < 0.01);
171 assert!((pred2.y - 0.0).abs() < 0.01);
172 }
173
174 #[test]
175 fn perspective_distorted_hex_grid_passes() {
176 let spacing = 60.0;
177 let sqrt3 = 3.0f32.sqrt();
178 let mut grid = HashMap::new();
179 let radius: i32 = 3;
180 for q in -radius..=radius {
181 for r in -radius..=radius {
182 if (q + r).abs() > radius {
183 continue;
184 }
185 let x = spacing * (q as f32 + r as f32 * 0.5);
186 let y = spacing * (r as f32 * sqrt3 / 2.0);
187 let scale = 1.0 + 0.01 * y / spacing;
189 grid.insert(GridIndex { i: q, j: r }, Point2::new(x * scale, y * scale));
190 }
191 }
192
193 let flagged = hex_find_inconsistent_corners(&grid, 3.0);
194 assert!(flagged.is_empty());
195 }
196}