use std::collections::HashMap;
use crate::target::TargetLayout;
pub(super) const HEX_NEIGHBORS: [(i32, i32); 6] =
[(1, 0), (-1, 0), (0, 1), (0, -1), (1, -1), (-1, 1)];
pub(super) struct BoardIndex {
pub(super) id_to_xy: HashMap<usize, [f32; 2]>,
pub(super) board_neighbors: HashMap<usize, Vec<usize>>,
pub(super) pitch_mm: f64,
pub(super) neighbor_spacing_mm: f64,
}
impl BoardIndex {
pub(super) fn build(target: &TargetLayout) -> Self {
let mut qr_to_id: HashMap<(i32, i32), usize> = HashMap::new();
let mut id_to_qr: HashMap<usize, (i32, i32)> = HashMap::new();
let mut id_to_xy: HashMap<usize, [f32; 2]> = HashMap::new();
for cell in target.cells() {
let Some(id) = cell.id else { continue };
qr_to_id.insert((cell.coord.u, cell.coord.v), id);
id_to_qr.insert(id, (cell.coord.u, cell.coord.v));
id_to_xy.insert(id, cell.xy_mm);
}
let mut board_neighbors: HashMap<usize, Vec<usize>> = HashMap::new();
for (&id, &(q, r)) in &id_to_qr {
let neighbors = HEX_NEIGHBORS
.iter()
.filter_map(|&(dq, dr)| qr_to_id.get(&(q + dq, r + dr)).copied())
.collect();
board_neighbors.insert(id, neighbors);
}
Self {
id_to_xy,
board_neighbors,
pitch_mm: target.pitch_mm() as f64,
neighbor_spacing_mm: f64::from(target.lattice().min_center_spacing_mm()),
}
}
pub(super) fn nearest_within(&self, xy_mm: [f64; 2], tolerance_mm: f64) -> Option<usize> {
let tol2 = tolerance_mm * tolerance_mm;
let mut best_id: Option<usize> = None;
let mut best_d2 = tol2;
for (&id, &bxy) in &self.id_to_xy {
let dx = xy_mm[0] - bxy[0] as f64;
let dy = xy_mm[1] - bxy[1] as f64;
let d2 = dx * dx + dy * dy;
if d2 < best_d2 || (d2 == best_d2 && best_id.is_some_and(|b| id < b)) {
best_d2 = d2;
best_id = Some(id);
}
}
best_id
}
pub(super) fn are_neighbors(&self, id_a: usize, id_b: usize) -> bool {
self.board_neighbors
.get(&id_a)
.is_some_and(|nbrs| nbrs.contains(&id_b))
}
pub(super) fn nearest_k_ids(&self, xy_mm: [f64; 2], k: usize) -> Vec<(usize, f64)> {
if k == 0 {
return Vec::new();
}
let mut ranked: Vec<(usize, f64)> = self
.id_to_xy
.iter()
.map(|(&id, &bxy)| {
let dx = xy_mm[0] - f64::from(bxy[0]);
let dy = xy_mm[1] - f64::from(bxy[1]);
(id, dx * dx + dy * dy)
})
.collect();
ranked.sort_by(|(ida, d2a), (idb, d2b)| d2a.total_cmp(d2b).then_with(|| ida.cmp(idb)));
ranked.truncate(k);
ranked
}
}
#[inline]
pub(super) fn dist2(a: [f64; 2], b: [f64; 2]) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
dx * dx + dy * dy
}
#[cfg(test)]
mod tests {
use super::*;
fn index_with(positions: &[(usize, [f32; 2])]) -> BoardIndex {
BoardIndex {
id_to_xy: positions.iter().copied().collect(),
board_neighbors: HashMap::new(),
pitch_mm: 1.0,
neighbor_spacing_mm: 1.0,
}
}
#[test]
fn nearest_within_breaks_ties_by_lower_id() {
for _ in 0..64 {
let idx = index_with(&[(7, [0.0, 0.0]), (3, [2.0, 0.0]), (42, [9.0, 9.0])]);
assert_eq!(idx.nearest_within([1.0, 0.0], 5.0), Some(3));
}
}
#[test]
fn nearest_within_respects_tolerance() {
let idx = index_with(&[(1, [0.0, 0.0])]);
assert_eq!(idx.nearest_within([0.5, 0.0], 1.0), Some(1));
assert_eq!(idx.nearest_within([5.0, 0.0], 1.0), None);
}
}