use std::collections::HashMap;
use nalgebra::Point2;
use crate::float::{lit, Float};
use super::{Coord, LatticeKind};
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct PredictedPosition<F: Float> {
pub position: Point2<F>,
pub n_axis_pairs: usize,
}
pub fn predict_grid_position<F: Float>(
labelled: &HashMap<Coord, Point2<F>>,
at: Coord,
kind: LatticeKind,
) -> Option<PredictedPosition<F>> {
let half: F = lit(0.5);
let mut sum_x = F::zero();
let mut sum_y = F::zero();
let mut n_axis_pairs = 0usize;
for &off in kind.neighbour_offsets() {
if off.u < 0 || (off.u == 0 && off.v < 0) {
continue;
}
let fwd = Coord::new(at.u + off.u, at.v + off.v);
let bwd = Coord::new(at.u - off.u, at.v - off.v);
if let (Some(pf), Some(pb)) = (labelled.get(&fwd), labelled.get(&bwd)) {
sum_x += half * (pf.x + pb.x);
sum_y += half * (pf.y + pb.y);
n_axis_pairs += 1;
}
}
if n_axis_pairs == 0 {
return None;
}
let n: F = lit(n_axis_pairs as f32);
Some(PredictedPosition {
position: Point2::new(sum_x / n, sum_y / n),
n_axis_pairs,
})
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::Matrix3;
fn assert_close(actual: Point2<f64>, expected: Point2<f64>) {
let err = (actual - expected).norm();
assert!(
err < 1e-9,
"expected {expected:?}, got {actual:?} (err {err:e})"
);
}
fn square_grid(n: i32, spacing: f64) -> HashMap<Coord, Point2<f64>> {
let mut map = HashMap::new();
for v in 0..n {
for u in 0..n {
map.insert(
Coord::new(u, v),
Point2::new(u as f64 * spacing, v as f64 * spacing),
);
}
}
map
}
fn hex_grid(radius: i32, spacing: f64) -> HashMap<Coord, Point2<f64>> {
let sqrt3 = 3.0f64.sqrt();
let mut map = HashMap::new();
for q in -radius..=radius {
for r in -radius..=radius {
if (q + r).abs() > radius {
continue;
}
let x = spacing * (q as f64 + r as f64 * 0.5);
let y = spacing * (r as f64 * sqrt3 / 2.0);
map.insert(Coord::new(q, r), Point2::new(x, y));
}
}
map
}
fn warp(map: &mut HashMap<Coord, Point2<f64>>, h: &Matrix3<f64>) {
for p in map.values_mut() {
let w = h * nalgebra::Vector3::new(p.x, p.y, 1.0);
*p = Point2::new(w.x / w.z, w.y / w.z);
}
}
#[test]
fn square_interior_predicts_exactly() {
let grid = square_grid(5, 40.0);
let pred = predict_grid_position(&grid, Coord::new(2, 2), LatticeKind::Square).unwrap();
assert_close(pred.position, Point2::new(80.0, 80.0));
assert_eq!(pred.n_axis_pairs, 2);
}
#[test]
fn square_missing_cell_is_predicted_from_neighbours() {
let mut grid = square_grid(5, 40.0);
grid.remove(&Coord::new(2, 2));
let pred = predict_grid_position(&grid, Coord::new(2, 2), LatticeKind::Square).unwrap();
assert_close(pred.position, Point2::new(80.0, 80.0));
assert_eq!(pred.n_axis_pairs, 2);
}
#[test]
fn square_edge_uses_single_pair() {
let grid = square_grid(5, 40.0);
let pred = predict_grid_position(&grid, Coord::new(2, 0), LatticeKind::Square).unwrap();
assert_eq!(pred.n_axis_pairs, 1);
assert_close(pred.position, Point2::new(80.0, 0.0));
}
#[test]
fn square_corner_has_no_pair() {
let grid = square_grid(5, 40.0);
assert!(predict_grid_position(&grid, Coord::new(0, 0), LatticeKind::Square).is_none());
assert!(predict_grid_position(&grid, Coord::new(-1, 2), LatticeKind::Square).is_none());
}
#[test]
fn hex_interior_averages_three_pairs() {
let grid = hex_grid(3, 60.0);
let pred = predict_grid_position(&grid, Coord::new(0, 0), LatticeKind::Hex).unwrap();
assert_eq!(pred.n_axis_pairs, 3);
assert_close(pred.position, Point2::new(0.0, 0.0));
}
#[test]
fn hex_frontier_yields_none() {
let grid = hex_grid(2, 60.0);
assert!(predict_grid_position(&grid, Coord::new(3, 0), LatticeKind::Hex).is_none());
}
#[test]
fn perspective_warp_keeps_prediction_close() {
let h = Matrix3::new(
0.9, 0.05, 12.0, -0.04, 1.1, -7.0, 2e-4, 1e-4, 1.0,
);
for (kind, mut grid) in [
(LatticeKind::Square, square_grid(7, 40.0)),
(LatticeKind::Hex, hex_grid(3, 40.0)),
] {
warp(&mut grid, &h);
for (&at, &truth) in &grid {
let Some(pred) = predict_grid_position(&grid, at, kind) else {
continue;
};
let err = (pred.position - truth).norm();
assert!(
err < 0.5,
"{kind:?} at {at:?}: prediction off by {err:.3} px"
);
}
}
}
#[test]
fn displaced_point_does_not_poison_its_own_prediction() {
let mut grid = square_grid(5, 40.0);
grid.insert(Coord::new(2, 2), Point2::new(95.0, 71.0));
let pred = predict_grid_position(&grid, Coord::new(2, 2), LatticeKind::Square).unwrap();
assert_close(pred.position, Point2::new(80.0, 80.0));
}
}