1use std::collections::HashMap;
19
20use nalgebra::{Point2, RealField};
21
22use super::{Coord, LatticeKind};
23
24#[derive(Clone, Copy, Debug, PartialEq)]
26#[non_exhaustive]
27pub struct PredictedPosition<F: RealField + Copy> {
28 pub position: Point2<F>,
31 pub n_axis_pairs: usize,
35}
36
37pub fn predict_grid_position<F: RealField + Copy>(
67 labelled: &HashMap<Coord, Point2<F>>,
68 at: Coord,
69 kind: LatticeKind,
70) -> Option<PredictedPosition<F>> {
71 let half = F::from_subset(&0.5_f64);
72 let mut sum_x = F::zero();
73 let mut sum_y = F::zero();
74 let mut n_axis_pairs = 0usize;
75
76 for &off in kind.neighbour_offsets() {
77 if off.u < 0 || (off.u == 0 && off.v < 0) {
80 continue;
81 }
82 let fwd = Coord::new(at.u + off.u, at.v + off.v);
83 let bwd = Coord::new(at.u - off.u, at.v - off.v);
84 if let (Some(pf), Some(pb)) = (labelled.get(&fwd), labelled.get(&bwd)) {
85 sum_x += half * (pf.x + pb.x);
86 sum_y += half * (pf.y + pb.y);
87 n_axis_pairs += 1;
88 }
89 }
90
91 if n_axis_pairs == 0 {
92 return None;
93 }
94 let n = F::from_subset(&(n_axis_pairs as f64));
95 Some(PredictedPosition {
96 position: Point2::new(sum_x / n, sum_y / n),
97 n_axis_pairs,
98 })
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use nalgebra::Matrix3;
105
106 fn assert_close(actual: Point2<f64>, expected: Point2<f64>) {
107 let err = (actual - expected).norm();
108 assert!(
109 err < 1e-9,
110 "expected {expected:?}, got {actual:?} (err {err:e})"
111 );
112 }
113
114 fn square_grid(n: i32, spacing: f64) -> HashMap<Coord, Point2<f64>> {
115 let mut map = HashMap::new();
116 for v in 0..n {
117 for u in 0..n {
118 map.insert(
119 Coord::new(u, v),
120 Point2::new(u as f64 * spacing, v as f64 * spacing),
121 );
122 }
123 }
124 map
125 }
126
127 fn hex_grid(radius: i32, spacing: f64) -> HashMap<Coord, Point2<f64>> {
128 let sqrt3 = 3.0f64.sqrt();
129 let mut map = HashMap::new();
130 for q in -radius..=radius {
131 for r in -radius..=radius {
132 if (q + r).abs() > radius {
133 continue;
134 }
135 let x = spacing * (q as f64 + r as f64 * 0.5);
136 let y = spacing * (r as f64 * sqrt3 / 2.0);
137 map.insert(Coord::new(q, r), Point2::new(x, y));
138 }
139 }
140 map
141 }
142
143 fn warp(map: &mut HashMap<Coord, Point2<f64>>, h: &Matrix3<f64>) {
144 for p in map.values_mut() {
145 let w = h * nalgebra::Vector3::new(p.x, p.y, 1.0);
146 *p = Point2::new(w.x / w.z, w.y / w.z);
147 }
148 }
149
150 #[test]
151 fn square_interior_predicts_exactly() {
152 let grid = square_grid(5, 40.0);
153 let pred = predict_grid_position(&grid, Coord::new(2, 2), LatticeKind::Square).unwrap();
154 assert_close(pred.position, Point2::new(80.0, 80.0));
155 assert_eq!(pred.n_axis_pairs, 2);
156 }
157
158 #[test]
159 fn square_missing_cell_is_predicted_from_neighbours() {
160 let mut grid = square_grid(5, 40.0);
161 grid.remove(&Coord::new(2, 2));
162 let pred = predict_grid_position(&grid, Coord::new(2, 2), LatticeKind::Square).unwrap();
163 assert_close(pred.position, Point2::new(80.0, 80.0));
164 assert_eq!(pred.n_axis_pairs, 2);
165 }
166
167 #[test]
168 fn square_edge_uses_single_pair() {
169 let grid = square_grid(5, 40.0);
170 let pred = predict_grid_position(&grid, Coord::new(2, 0), LatticeKind::Square).unwrap();
172 assert_eq!(pred.n_axis_pairs, 1);
173 assert_close(pred.position, Point2::new(80.0, 0.0));
174 }
175
176 #[test]
177 fn square_corner_has_no_pair() {
178 let grid = square_grid(5, 40.0);
179 assert!(predict_grid_position(&grid, Coord::new(0, 0), LatticeKind::Square).is_none());
180 assert!(predict_grid_position(&grid, Coord::new(-1, 2), LatticeKind::Square).is_none());
181 }
182
183 #[test]
184 fn hex_interior_averages_three_pairs() {
185 let grid = hex_grid(3, 60.0);
186 let pred = predict_grid_position(&grid, Coord::new(0, 0), LatticeKind::Hex).unwrap();
187 assert_eq!(pred.n_axis_pairs, 3);
188 assert_close(pred.position, Point2::new(0.0, 0.0));
189 }
190
191 #[test]
192 fn hex_frontier_yields_none() {
193 let grid = hex_grid(2, 60.0);
194 assert!(predict_grid_position(&grid, Coord::new(3, 0), LatticeKind::Hex).is_none());
196 }
197
198 #[test]
199 fn perspective_warp_keeps_prediction_close() {
200 let h = Matrix3::new(
204 0.9, 0.05, 12.0, -0.04, 1.1, -7.0, 2e-4, 1e-4, 1.0,
207 );
208 for (kind, mut grid) in [
209 (LatticeKind::Square, square_grid(7, 40.0)),
210 (LatticeKind::Hex, hex_grid(3, 40.0)),
211 ] {
212 warp(&mut grid, &h);
213 for (&at, &truth) in &grid {
214 let Some(pred) = predict_grid_position(&grid, at, kind) else {
215 continue;
216 };
217 let err = (pred.position - truth).norm();
218 assert!(
219 err < 0.5,
220 "{kind:?} at {at:?}: prediction off by {err:.3} px"
221 );
222 }
223 }
224 }
225
226 #[test]
227 fn displaced_point_does_not_poison_its_own_prediction() {
228 let mut grid = square_grid(5, 40.0);
231 grid.insert(Coord::new(2, 2), Point2::new(95.0, 71.0));
232 let pred = predict_grid_position(&grid, Coord::new(2, 2), LatticeKind::Square).unwrap();
233 assert_close(pred.position, Point2::new(80.0, 80.0));
234 }
235}