use crate::topology::NeighborhoodKind;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Boundary {
Torus,
Bounded,
}
#[inline]
pub fn wrap_index(v: i32, m: i32) -> i32 {
v.rem_euclid(m)
}
#[inline]
pub fn wrap_coord(v: f32, world: f32) -> f32 {
let wrapped = v.rem_euclid(world);
if wrapped == world { 0.0 } else { wrapped }
}
#[inline]
pub fn cell_index(x: u32, y: u32, w: u32) -> u32 {
y * w + x
}
#[inline]
pub fn offset_cell(x: u32, y: u32, dx: i32, dy: i32, w: u32, h: u32, boundary: Boundary) -> Option<(u32, u32)> {
let nx = x as i32 + dx;
let ny = y as i32 + dy;
match boundary {
Boundary::Torus => Some((wrap_index(nx, w as i32) as u32, wrap_index(ny, h as i32) as u32)),
Boundary::Bounded => {
if nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 {
None
} else {
Some((nx as u32, ny as u32))
}
}
}
}
#[inline]
pub fn axis_delta(a: f32, b: f32, world: f32, boundary: Boundary) -> f32 {
let d = b - a;
match boundary {
Boundary::Bounded => d,
Boundary::Torus => {
let half = world * 0.5;
if d >= half {
let c = d - world;
if c < half {
return c;
}
} else if d < -half {
let c = d + world;
if c >= -half {
return c;
}
} else {
return d;
}
(d + half).rem_euclid(world) - half
}
}
}
#[inline]
pub fn dist_sq(ax: f32, ay: f32, bx: f32, by: f32, world_w: f32, world_h: f32, boundary: Boundary) -> f32 {
let dx = axis_delta(ax, bx, world_w, boundary);
let dy = axis_delta(ay, by, world_h, boundary);
dx * dx + dy * dy
}
pub const MOORE_ROW_MAJOR: [(i32, i32); 8] = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)];
pub const MOORE_COLUMN_MAJOR: [(i32, i32); 8] = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)];
pub const VON_NEUMANN: [(i32, i32); 4] = [(0, -1), (-1, 0), (1, 0), (0, 1)];
#[inline]
pub fn offsets(kind: NeighborhoodKind) -> &'static [(i32, i32)] {
match kind {
NeighborhoodKind::Moore => &MOORE_ROW_MAJOR,
NeighborhoodKind::VonNeumann => &VON_NEUMANN,
}
}
#[inline]
pub fn for_each_neighbor(
x: u32,
y: u32,
w: u32,
h: u32,
offsets: &[(i32, i32)],
boundary: Boundary,
mut f: impl FnMut(u32, u32),
) {
for &(dx, dy) in offsets {
if let Some((nx, ny)) = offset_cell(x, y, dx, dy, w, h, boundary) {
f(nx, ny);
}
}
}
#[inline]
pub fn heading_octant(vx: f32, vy: f32) -> u8 {
let east = vx >= 0.0;
let south = vy >= 0.0;
let steep = vy.abs() > vx.abs();
match (east, south, steep) {
(true, true, false) => 0,
(true, true, true) => 1,
(false, true, true) => 2,
(false, true, false) => 3,
(false, false, false) => 4,
(false, false, true) => 5,
(true, false, true) => 6,
(true, false, false) => 7,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_wrapped_coordinate_never_reaches_the_world_size() {
for world in [1.0f32, 7.5, 200.0, 4096.0] {
for v in [-1e-8f32, -1e-12, -f32::MIN_POSITIVE, -0.0, world, 2.0 * world] {
let wrapped = wrap_coord(v, world);
assert!((0.0..world).contains(&wrapped), "wrap_coord({v}, {world}) = {wrapped}");
}
}
assert!(wrap_coord(f32::NAN, 10.0).is_nan(), "a NaN stays NaN");
}
#[test]
fn the_two_moore_orders_are_permutations_of_each_other() {
let mut row = MOORE_ROW_MAJOR;
let mut col = MOORE_COLUMN_MAJOR;
row.sort_unstable();
col.sort_unstable();
assert_eq!(row, col, "the two Moore tables cover different cells");
}
#[test]
fn no_table_contains_the_origin() {
for (name, table) in [
("row major", &MOORE_ROW_MAJOR[..]),
("column major", &MOORE_COLUMN_MAJOR[..]),
("von Neumann", &VON_NEUMANN[..]),
] {
assert!(!table.contains(&(0, 0)), "{name} includes the origin");
}
}
#[test]
fn row_major_matches_the_step_cell_slice_order() {
assert_eq!(
MOORE_ROW_MAJOR,
[(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)],
"step_cell reads its neighbours in this order"
);
}
#[test]
fn column_major_runs_dx_outer() {
let mut expected = Vec::new();
for dx in -1..=1 {
for dy in -1..=1 {
if (dx, dy) != (0, 0) {
expected.push((dx, dy));
}
}
}
assert_eq!(MOORE_COLUMN_MAJOR.to_vec(), expected, "dx must be the outer loop");
}
#[test]
fn a_torus_offset_wraps_on_both_axes() {
assert_eq!(offset_cell(0, 0, -1, -1, 4, 4, Boundary::Torus), Some((3, 3)));
assert_eq!(offset_cell(3, 3, 1, 1, 4, 4, Boundary::Torus), Some((0, 0)));
}
#[test]
fn a_single_cell_torus_is_its_own_neighbour() {
for &(dx, dy) in &MOORE_ROW_MAJOR {
assert_eq!(
offset_cell(0, 0, dx, dy, 1, 1, Boundary::Torus),
Some((0, 0)),
"offset ({dx}, {dy}) left the one cell there is"
);
}
}
#[test]
fn boundaries_agree_away_from_the_edges() {
for &(dx, dy) in &MOORE_ROW_MAJOR {
let torus = offset_cell(4, 4, dx, dy, 9, 9, Boundary::Torus);
let bounded = offset_cell(4, 4, dx, dy, 9, 9, Boundary::Bounded);
assert_eq!(torus, bounded, "offset ({dx}, {dy}) disagrees in the interior");
}
}
#[test]
fn a_bounded_corner_has_three_neighbours() {
let mut count = 0;
for_each_neighbor(0, 0, 4, 4, &MOORE_ROW_MAJOR, Boundary::Bounded, |_, _| count += 1);
assert_eq!(count, 3, "a corner of a bounded grid has 3 Moore neighbours");
}
#[test]
fn a_torus_corner_has_eight_neighbours() {
let mut count = 0;
for_each_neighbor(0, 0, 4, 4, &MOORE_ROW_MAJOR, Boundary::Torus, |_, _| count += 1);
assert_eq!(count, 8, "a torus has no corners");
}
#[test]
fn a_torus_delta_is_antisymmetric_in_magnitude() {
let world = 10.0;
for i in 0..20 {
let a = i as f32 * 0.5;
let b = 7.25;
let ab = axis_delta(a, b, world, Boundary::Torus);
let ba = axis_delta(b, a, world, Boundary::Torus);
assert!(
(ab.abs() - ba.abs()).abs() < 1e-5 || (ab.abs() - world * 0.5).abs() < 1e-5,
"delta {a} -> {b} was {ab} but {b} -> {a} was {ba}"
);
}
}
#[test]
fn a_torus_delta_never_exceeds_half_the_world() {
let world = 10.0;
for i in 0..40 {
for j in 0..40 {
let (a, b) = (i as f32 * 0.25, j as f32 * 0.25);
let d = axis_delta(a, b, world, Boundary::Torus);
assert!(
d >= -world * 0.5 && d < world * 0.5,
"delta {a} -> {b} was {d}, outside [-5, 5)"
);
}
}
}
#[test]
fn a_torus_delta_handles_positions_outside_the_world() {
let world = 10.0;
for k in -3..=3 {
let shifted = 2.0 + k as f32 * world;
assert!(
(axis_delta(shifted, 8.0, world, Boundary::Torus) - axis_delta(2.0, 8.0, world, Boundary::Torus)).abs()
< 1e-4,
"a position {k} worlds away gave a different delta"
);
}
}
#[test]
fn octants_run_clockwise_from_east_with_y_pointing_down() {
for expected in 0..8u8 {
let center = (f32::from(expected) + 0.5) * std::f32::consts::TAU / 8.0;
let (vx, vy) = (center.cos(), center.sin());
assert_eq!(
heading_octant(vx, vy),
expected,
"octant {expected} center ({vx}, {vy})"
);
}
}
#[test]
fn every_heading_lands_in_range() {
for step in 0..64u8 {
let angle = f32::from(step) * std::f32::consts::TAU / 64.0;
let octant = heading_octant(angle.cos(), angle.sin());
assert!(octant < 8, "angle {angle} gave octant {octant}");
}
}
}