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