pub const SNAP: f32 = 0.5;
pub const TILE: f32 = 1.0;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum SnapLevel {
#[default]
Tile,
Fine,
Finer,
}
impl SnapLevel {
pub fn depth(self) -> u32 {
match self {
SnapLevel::Tile => 0,
SnapLevel::Fine => 1,
SnapLevel::Finer => 2,
}
}
pub fn finer(self) -> SnapLevel {
match self {
SnapLevel::Tile => SnapLevel::Fine,
SnapLevel::Fine | SnapLevel::Finer => SnapLevel::Finer,
}
}
pub fn next(self) -> SnapLevel {
match self {
SnapLevel::Tile => SnapLevel::Fine,
SnapLevel::Fine => SnapLevel::Finer,
SnapLevel::Finer => SnapLevel::Tile,
}
}
pub fn pitch(self, divisor: u32) -> f32 {
let d = divisor.max(2);
TILE / (d.pow(self.depth()) as f32)
}
}
pub fn snap_corner(centre: f32, span: f32, pitch: f32) -> f32 {
if !(pitch.is_finite() && pitch > 0.0) || !centre.is_finite() || !span.is_finite() {
return centre;
}
let half = span * 0.5;
let min = centre - half;
(min / pitch).round() * pitch + half
}
pub fn snap_centre(centre: f32, pitch: f32) -> f32 {
if !(pitch.is_finite() && pitch > 0.0) || !centre.is_finite() {
return centre;
}
(centre / pitch).round() * pitch
}
pub fn snap_span(span: f32) -> f32 {
let cells = (span / SNAP).round().max(1.0);
cells * SNAP
}
pub fn cells(span: f32) -> (u32, f32) {
let size = snap_span(span);
((size / SNAP).round() as u32, size - span)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_span_snaps_to_the_nearest_whole_cell() {
assert_eq!(snap_span(0.5), 0.5);
assert_eq!(snap_span(1.0), 1.0);
assert_eq!(snap_span(0.55), 0.5);
assert_eq!(snap_span(0.8), 1.0);
assert_eq!(snap_span(1.45), 1.5);
}
#[test]
fn a_tiny_or_zero_span_still_occupies_a_cell() {
assert_eq!(snap_span(0.0), SNAP);
assert_eq!(snap_span(0.01), SNAP);
assert_eq!(cells(0.0), (1, SNAP));
}
#[test]
fn slack_is_signed_so_a_gap_and_an_overlap_read_differently() {
let (n, slack) = cells(1.45);
assert_eq!(n, 3);
assert!((slack - 0.05).abs() < 1e-5, "{slack}");
let (n, slack) = cells(0.55);
assert_eq!(n, 1);
assert!((slack + 0.05).abs() < 1e-5, "{slack}");
}
#[test]
fn a_tile_sized_piece_can_only_land_filling_one_cell() {
for aim in [0.0, 0.1, 0.4, 0.49, 0.5, 0.51, 0.9, 1.0, 1.4, 1.6, 2.3, -0.3, -1.2] {
let got = snap_corner(aim, TILE, SnapLevel::Tile.pitch(3));
let min = got - TILE * 0.5;
assert!(
(min - min.round()).abs() < 1e-5,
"aiming at {aim} put the near edge at {min}, which is not a cell boundary"
);
assert!(
(got.fract().abs() - 0.5).abs() < 1e-5,
"aiming at {aim} gave centre {got}, which straddles two cells"
);
}
}
#[test]
fn the_minimum_corner_always_lands_on_the_lattice() {
for divisor in [2u32, 3, 4, 5] {
for level in [SnapLevel::Tile, SnapLevel::Fine, SnapLevel::Finer] {
let pitch = level.pitch(divisor);
for span in [0.1f32, 0.25, 0.5, 0.55, 1.0, 1.5, 2.0, 3.0] {
for aim in [-2.7f32, -0.4, 0.0, 0.13, 0.5, 0.97, 1.0, 4.62] {
let got = snap_corner(aim, span, pitch);
let min = got - span * 0.5;
let k = min / pitch;
assert!(
(k - k.round()).abs() < 1e-3,
"divisor {divisor} {level:?} span {span} aim {aim}: near edge {min} is \
{k} pitches, not a whole number"
);
assert!(
(got - aim).abs() <= pitch * 0.5 + 1e-3,
"moved {} m, more than half a pitch of {pitch}",
(got - aim).abs()
);
}
}
}
}
}
#[test]
fn the_ladder_divides_the_tile_once_per_rung() {
assert_eq!(SnapLevel::Tile.pitch(3), 1.0);
assert!((SnapLevel::Fine.pitch(3) - 1.0 / 3.0).abs() < 1e-6);
assert!((SnapLevel::Finer.pitch(3) - 1.0 / 9.0).abs() < 1e-6);
assert_eq!(SnapLevel::Fine.pitch(2), SNAP);
assert_eq!(SnapLevel::Finer.pitch(2), 0.25);
assert_eq!(SnapLevel::Finer.pitch(1), SnapLevel::Finer.pitch(2));
assert_eq!(SnapLevel::Finer.pitch(0), SnapLevel::Finer.pitch(2));
}
#[test]
fn each_rung_is_finer_than_the_one_above() {
for divisor in [2u32, 3, 7] {
let (t, f, ff) = (
SnapLevel::Tile.pitch(divisor),
SnapLevel::Fine.pitch(divisor),
SnapLevel::Finer.pitch(divisor),
);
assert!(t > f && f > ff, "divisor {divisor}: {t} {f} {ff}");
}
}
#[test]
fn a_degenerate_pitch_or_position_returns_the_input() {
assert_eq!(snap_corner(1.7, 1.0, 0.0), 1.7);
assert_eq!(snap_corner(1.7, 1.0, -1.0), 1.7);
assert_eq!(snap_corner(1.7, 1.0, f32::NAN), 1.7);
assert!(snap_corner(f32::NAN, 1.0, 0.5).is_nan(), "a NaN aim is the caller's problem");
assert_eq!(snap_corner(1.7, f32::INFINITY, 0.5), 1.7);
}
#[test]
fn the_cell_count_and_the_cell_size_cannot_disagree() {
for span in [0.1, 0.24, 0.26, 0.5, 0.74, 0.76, 1.0, 1.45, 2.29, 7.5] {
let (n, _) = cells(span);
assert_eq!(
n as f32 * SNAP,
snap_span(span),
"{span} m: reported {n} cells but lays down {}",
snap_span(span)
);
}
}
#[test]
fn the_ladder_steps_one_way_for_a_modifier_and_another_for_a_cycle() {
assert_eq!(SnapLevel::Tile.finer(), SnapLevel::Fine);
assert_eq!(SnapLevel::Fine.finer(), SnapLevel::Finer);
assert_eq!(
SnapLevel::Finer.finer(),
SnapLevel::Finer,
"the bottom rung stays put — a modifier must never be the biggest jump on offer"
);
assert_eq!(SnapLevel::Tile.next(), SnapLevel::Fine);
assert_eq!(SnapLevel::Fine.next(), SnapLevel::Finer);
assert_eq!(
SnapLevel::Finer.next(),
SnapLevel::Tile,
"a key that cycles must come back round, or the third press does nothing"
);
let mut seen = vec![SnapLevel::Tile];
let mut at = SnapLevel::Tile;
for _ in 0..2 {
at = at.next();
seen.push(at);
}
assert_eq!(seen, vec![SnapLevel::Tile, SnapLevel::Fine, SnapLevel::Finer]);
}
}