use nalgebra::{Point2, Vector2};
pub mod hex;
pub mod predict;
pub mod square;
pub use hex::Hex;
pub use predict::{predict_grid_position, PredictedPosition};
pub use square::Square;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CellTopology {
TrianglePairToQuad,
TriangleIsCell,
}
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
serde::Serialize,
serde::Deserialize,
)]
#[non_exhaustive]
pub struct Coord {
pub u: i32,
pub v: i32,
}
impl Coord {
pub const fn new(u: i32, v: i32) -> Self {
Self { u, v }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct GridDimensions {
pub width: usize,
pub height: usize,
}
impl GridDimensions {
pub const fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum LatticeKind {
Square,
Hex,
}
impl LatticeKind {
pub fn model_point(self, coord: Coord) -> Point2<f32> {
match self {
Self::Square => Square.model_point(coord),
Self::Hex => Hex.model_point(coord),
}
}
pub fn neighbour_offsets(self) -> &'static [Coord] {
match self {
Self::Square => Square.neighbour_offsets(),
Self::Hex => Hex.neighbour_offsets(),
}
}
pub fn symmetry_transforms(self) -> &'static [GridTransform] {
match self {
Self::Square => Square.symmetry_transforms(),
Self::Hex => Hex.symmetry_transforms(),
}
}
pub fn axis_family_count(self) -> usize {
match self {
Self::Square => Square.axis_family_count(),
Self::Hex => Hex.axis_family_count(),
}
}
pub fn model_axis_directions(self) -> &'static [Vector2<f32>] {
match self {
Self::Square => Square.model_axis_directions(),
Self::Hex => Hex.model_axis_directions(),
}
}
pub fn cell_topology(self) -> CellTopology {
match self {
Self::Square => Square.cell_topology(),
Self::Hex => Hex.cell_topology(),
}
}
}
mod private {
pub trait Sealed {}
impl Sealed for super::Square {}
impl Sealed for super::Hex {}
}
pub trait Lattice: Copy + private::Sealed {
const KIND: LatticeKind;
fn model_point(self, coord: Coord) -> Point2<f32>;
fn neighbour_offsets(self) -> &'static [Coord];
fn symmetry_transforms(self) -> &'static [GridTransform];
fn axis_family_count(self) -> usize;
fn model_axis_directions(self) -> &'static [Vector2<f32>];
fn cell_topology(self) -> CellTopology;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct GridTransform {
pub source_kind: LatticeKind,
pub matrix: [[i32; 2]; 2],
pub offset: [i32; 2],
}
impl GridTransform {
pub const fn new(source_kind: LatticeKind, matrix: [[i32; 2]; 2], offset: [i32; 2]) -> Self {
Self {
source_kind,
matrix,
offset,
}
}
pub fn apply(self, coord: Coord) -> Coord {
Coord {
u: self.matrix[0][0] * coord.u + self.matrix[0][1] * coord.v + self.offset[0],
v: self.matrix[1][0] * coord.u + self.matrix[1][1] * coord.v + self.offset[1],
}
}
pub const fn determinant(self) -> i32 {
self.matrix[0][0] * self.matrix[1][1] - self.matrix[0][1] * self.matrix[1][0]
}
}
pub const SQUARE_CARDINAL_OFFSETS: [Coord; 4] = square::SQUARE_CARDINAL_OFFSETS;
pub const HEX_AXIAL_OFFSETS: [Coord; 6] = hex::HEX_AXIAL_OFFSETS;
pub const D4_TRANSFORMS: [GridTransform; 8] = square::D4_TRANSFORMS;
pub const D6_TRANSFORMS: [GridTransform; 12] = hex::D6_TRANSFORMS;
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
#[test]
fn square_model_mapping_is_cartesian() {
let p = LatticeKind::Square.model_point(Coord::new(2, -3));
assert_eq!(p, Point2::new(2.0, -3.0));
}
#[test]
fn hex_model_mapping_is_axial() {
let p = LatticeKind::Hex.model_point(Coord::new(1, 2));
assert!((p.x - 2.0).abs() < 1e-6);
assert!((p.y - 3.0_f32.sqrt()).abs() < 1e-6);
}
#[test]
fn kind_dispatch_matches_trait_impls() {
let c = Coord::new(3, -1);
assert_eq!(LatticeKind::Square.model_point(c), Square.model_point(c));
assert_eq!(LatticeKind::Hex.model_point(c), Hex.model_point(c));
assert_eq!(
LatticeKind::Square.neighbour_offsets(),
Square.neighbour_offsets()
);
assert_eq!(
LatticeKind::Square.symmetry_transforms().len(),
D4_TRANSFORMS.len()
);
assert_eq!(
LatticeKind::Hex.symmetry_transforms().len(),
D6_TRANSFORMS.len()
);
}
#[test]
fn d4_table_is_complete() {
let set: HashSet<_> = D4_TRANSFORMS.iter().map(|t| t.matrix).collect();
assert_eq!(set.len(), 8);
assert!(D4_TRANSFORMS
.iter()
.all(|t| t.source_kind == LatticeKind::Square && t.determinant().abs() == 1));
}
#[test]
fn axis_family_counts() {
assert_eq!(LatticeKind::Square.axis_family_count(), 2);
assert_eq!(LatticeKind::Hex.axis_family_count(), 3);
}
#[test]
fn cell_topology_by_family() {
assert_eq!(
LatticeKind::Square.cell_topology(),
CellTopology::TrianglePairToQuad
);
assert_eq!(
LatticeKind::Hex.cell_topology(),
CellTopology::TriangleIsCell
);
}
#[test]
fn model_axis_directions_are_unit_and_match_offsets() {
let sq = LatticeKind::Square.model_axis_directions();
assert_eq!(sq.len(), 2);
for v in sq {
assert!((v.norm() - 1.0).abs() < 1e-6);
}
let hx = LatticeKind::Hex.model_axis_directions();
assert_eq!(hx.len(), 3);
for v in hx {
assert!((v.norm() - 1.0).abs() < 1e-6);
}
let d_q = LatticeKind::Hex.model_point(Coord::new(1, 0))
- LatticeKind::Hex.model_point(Coord::new(0, 0));
let ang_offset = d_q.y.atan2(d_q.x);
let ang_dir = hx[0].y.atan2(hx[0].x);
let diff = (ang_offset - ang_dir).abs() % std::f32::consts::PI;
assert!(diff < 1e-5 || (std::f32::consts::PI - diff) < 1e-5);
}
#[test]
fn d6_table_is_complete() {
let set: HashSet<_> = D6_TRANSFORMS.iter().map(|t| t.matrix).collect();
assert_eq!(set.len(), 12);
assert!(D6_TRANSFORMS
.iter()
.all(|t| t.source_kind == LatticeKind::Hex && t.determinant().abs() == 1));
}
}