use std::iter::Iterator;
use smallvec::smallvec;
use crate::{error, point::Point, spacecurve::SpaceCurve, spec::GridSpec};
#[derive(Debug)]
pub struct Scan {
dimension: u32,
size: u32,
length: u32,
}
impl Scan {
pub fn from_dimensions(dimension: u32, size: u32) -> error::Result<Self> {
let spec = GridSpec::new(dimension, size)?;
Ok(Self {
dimension: spec.dimension(),
size: spec.size(),
length: spec.length(),
})
}
}
impl SpaceCurve for Scan {
fn name(&self) -> &'static str {
"Scan"
}
fn info(&self) -> &'static str {
"Serpentine raster scan (boustrophedon) across rows/columns.\n\
Continuous with minimal turning, but locality drops at row boundaries.\n\
Useful as a simple, predictable baseline traversal."
}
fn length(&self) -> u32 {
self.length
}
fn dimensions(&self) -> u32 {
self.dimension
}
fn point(&self, index: u32) -> Point {
debug_assert!(index < self.length, "index out of bounds");
let mut should_reverse_direction = false;
let mut coordinates = smallvec![0; self.dimension as usize];
let mut remaining_index = index;
for dim_idx in (0..self.dimension).rev() {
let stride = self.size.pow(dim_idx);
let raw_coordinate = remaining_index / stride;
coordinates[dim_idx as usize] = if should_reverse_direction {
self.size - raw_coordinate - 1
} else {
raw_coordinate
};
if coordinates[dim_idx as usize] % 2 != 0 {
should_reverse_direction = !should_reverse_direction;
}
remaining_index -= raw_coordinate * stride;
}
Point::new_with_dimension(self.dimension, coordinates)
}
fn index(&self, point: &Point) -> u32 {
debug_assert_eq!(
point.len(),
self.dimension as usize,
"point dimension mismatch"
);
debug_assert!(
point.iter().all(|&c| c < self.size),
"point coordinate out of bounds"
);
let mut should_reverse_direction = false;
let mut index_accumulator = 0;
for (dim_idx, &coordinate) in point.iter().enumerate().rev() {
let stride = self.size.pow(dim_idx as u32);
let actual_value = if should_reverse_direction {
self.size - coordinate - 1
} else {
coordinate
};
index_accumulator += actual_value * stride;
if coordinate % 2 != 0 {
should_reverse_direction = !should_reverse_direction;
}
}
index_accumulator
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scan_point_simple() {
let s = Scan::from_dimensions(2, 3).unwrap();
assert_eq!(s.point(0), Point::new(vec![0, 0]));
assert_eq!(s.point(1), Point::new(vec![1, 0]));
assert_eq!(s.point(2), Point::new(vec![2, 0]));
assert_eq!(s.point(3), Point::new(vec![2, 1]));
assert_eq!(s.point(8), Point::new(vec![2, 2]));
}
#[test]
fn test_scan_index_simple() {
let s = Scan::from_dimensions(2, 3).unwrap();
assert_eq!(s.index(&Point::new(vec![0, 0])), 0);
assert_eq!(s.index(&Point::new(vec![1, 0])), 1);
assert_eq!(s.index(&Point::new(vec![2, 0])), 2);
assert_eq!(s.index(&Point::new(vec![2, 1])), 3);
assert_eq!(s.index(&Point::new(vec![2, 2])), 8);
}
#[test]
fn guard_matches_registry() {
assert!(Scan::from_dimensions(0, 3).is_err());
assert!(Scan::from_dimensions(2, 0).is_err());
}
#[test]
fn full_sequence_2d_snake() {
let s = Scan::from_dimensions(2, 3).unwrap();
let expected = vec![
vec![0, 0],
vec![1, 0],
vec![2, 0],
vec![2, 1],
vec![1, 1],
vec![0, 1],
vec![0, 2],
vec![1, 2],
vec![2, 2],
];
for (idx, coords) in expected.iter().enumerate() {
assert_eq!(Vec::<u32>::from(s.point(idx as u32)), *coords);
assert_eq!(s.index(&Point::new(coords.clone())), idx as u32);
}
}
#[test]
fn roundtrip_three_dimensions() {
let s = Scan::from_dimensions(3, 3).unwrap();
for idx in 0..s.length() {
let p = s.point(idx);
assert_eq!(s.index(&p), idx, "roundtrip failed at {idx}");
}
}
}