use crate::grid_index::GridIndex;
use nalgebra::Point2;
use std::collections::HashMap;
const HEX_AXIS_PAIRS: [((i32, i32), (i32, i32)); 3] = [
((-1, 0), (1, 0)),
((1, -1), (-1, 1)),
((0, -1), (0, 1)),
];
pub fn hex_predict_grid_position(
grid: &HashMap<GridIndex, Point2<f32>>,
idx: GridIndex,
) -> Option<Point2<f32>> {
let mut pred_sum = Point2::new(0.0f32, 0.0f32);
let mut pred_count = 0u32;
for &((dq_a, dr_a), (dq_b, dr_b)) in &HEX_AXIS_PAIRS {
let a = GridIndex {
i: idx.i + dq_a,
j: idx.j + dr_a,
};
let b = GridIndex {
i: idx.i + dq_b,
j: idx.j + dr_b,
};
if let (Some(&pa), Some(&pb)) = (grid.get(&a), grid.get(&b)) {
pred_sum.x += 0.5 * (pa.x + pb.x);
pred_sum.y += 0.5 * (pa.y + pb.y);
pred_count += 1;
}
}
if pred_count == 0 {
return None;
}
Some(Point2::new(
pred_sum.x / pred_count as f32,
pred_sum.y / pred_count as f32,
))
}
pub fn hex_find_inconsistent_corners(
grid: &HashMap<GridIndex, Point2<f32>>,
threshold: f32,
) -> Vec<(GridIndex, Point2<f32>)> {
let threshold_sq = threshold * threshold;
let mut flagged = Vec::new();
for (&idx, &pos) in grid {
if let Some(predicted) = hex_predict_grid_position(grid, idx) {
let dx = pos.x - predicted.x;
let dy = pos.y - predicted.y;
if dx * dx + dy * dy > threshold_sq {
flagged.push((idx, predicted));
}
}
}
flagged
}
#[cfg(test)]
mod tests {
use super::*;
fn make_hex_grid(radius: i32, spacing: f32) -> HashMap<GridIndex, Point2<f32>> {
let sqrt3 = 3.0f32.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 f32 + r as f32 * 0.5);
let y = spacing * (r as f32 * sqrt3 / 2.0);
map.insert(GridIndex { i: q, j: r }, Point2::new(x, y));
}
}
map
}
#[test]
fn clean_hex_grid_has_no_inconsistencies() {
let grid = make_hex_grid(3, 60.0);
let flagged = hex_find_inconsistent_corners(&grid, 3.0);
assert!(flagged.is_empty());
}
#[test]
fn displaced_corner_is_flagged() {
let mut grid = make_hex_grid(2, 60.0);
let center = GridIndex { i: 0, j: 0 };
grid.insert(center, Point2::new(9.0, 9.0));
let flagged = hex_find_inconsistent_corners(&grid, 3.0);
assert_eq!(1, flagged.len());
assert_eq!(center, flagged[0].0);
let pred = flagged[0].1;
assert!(pred.x.abs() < 0.01, "pred.x = {}", pred.x);
assert!(pred.y.abs() < 0.01, "pred.y = {}", pred.y);
}
#[test]
fn isolated_nodes_are_skipped() {
let mut grid = HashMap::new();
grid.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
grid.insert(GridIndex { i: 10, j: 10 }, Point2::new(500.0, 500.0));
let flagged = hex_find_inconsistent_corners(&grid, 3.0);
assert!(flagged.is_empty());
}
#[test]
fn prediction_from_single_axis_pair() {
let spacing = 60.0;
let sqrt3 = 3.0f32.sqrt();
let mut grid = HashMap::new();
grid.insert(GridIndex { i: -1, j: 0 }, Point2::new(-spacing, 0.0));
grid.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
grid.insert(GridIndex { i: 1, j: 0 }, Point2::new(spacing, 0.0));
let pred = hex_predict_grid_position(&grid, GridIndex { i: 0, j: 0 }).unwrap();
assert!((pred.x - 0.0).abs() < 0.01);
assert!((pred.y - 0.0).abs() < 0.01);
let mut grid2 = HashMap::new();
grid2.insert(
GridIndex { i: 0, j: -1 },
Point2::new(-0.5 * spacing, -sqrt3 / 2.0 * spacing),
);
grid2.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
grid2.insert(
GridIndex { i: 0, j: 1 },
Point2::new(0.5 * spacing, sqrt3 / 2.0 * spacing),
);
let pred2 = hex_predict_grid_position(&grid2, GridIndex { i: 0, j: 0 }).unwrap();
assert!((pred2.x - 0.0).abs() < 0.01);
assert!((pred2.y - 0.0).abs() < 0.01);
}
#[test]
fn perspective_distorted_hex_grid_passes() {
let spacing = 60.0;
let sqrt3 = 3.0f32.sqrt();
let mut grid = HashMap::new();
let radius: i32 = 3;
for q in -radius..=radius {
for r in -radius..=radius {
if (q + r).abs() > radius {
continue;
}
let x = spacing * (q as f32 + r as f32 * 0.5);
let y = spacing * (r as f32 * sqrt3 / 2.0);
let scale = 1.0 + 0.01 * y / spacing;
grid.insert(GridIndex { i: q, j: r }, Point2::new(x * scale, y * scale));
}
}
let flagged = hex_find_inconsistent_corners(&grid, 3.0);
assert!(flagged.is_empty());
}
}