use projective_grid::lattice::LatticeKind;
use projective_grid::Coord;
use projective_grid::GridTransform as NextGridTransform;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GridTransform {
pub a: i32,
pub b: i32,
pub c: i32,
pub d: i32,
}
impl GridTransform {
pub const IDENTITY: GridTransform = GridTransform {
a: 1,
b: 0,
c: 0,
d: 1,
};
#[inline]
pub fn apply(&self, u: i32, v: i32) -> Coord {
Coord::new(self.a * u + self.b * v, self.c * u + self.d * v)
}
pub fn inverse(&self) -> Option<GridTransform> {
let det = self.a * self.d - self.b * self.c;
if det != 1 && det != -1 {
return None;
}
Some(GridTransform {
a: self.d / det,
b: -self.b / det,
c: -self.c / det,
d: self.a / det,
})
}
}
#[inline]
pub fn cell_rect_corners_at(gc: Coord, px_per_cell: f32) -> [nalgebra::Point2<f32>; 4] {
let x0 = gc.u as f32 * px_per_cell;
let y0 = gc.v as f32 * px_per_cell;
let s = px_per_cell;
[
nalgebra::Point2::new(x0, y0),
nalgebra::Point2::new(x0 + s, y0),
nalgebra::Point2::new(x0 + s, y0 + s),
nalgebra::Point2::new(x0, y0 + s),
]
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GridAlignment {
pub transform: GridTransform,
pub translation: [i32; 2],
}
impl GridAlignment {
pub const IDENTITY: GridAlignment = GridAlignment {
transform: GridTransform::IDENTITY,
translation: [0, 0],
};
#[inline]
pub fn map(&self, u: i32, v: i32) -> Coord {
let g = self.transform.apply(u, v);
Coord::new(g.u + self.translation[0], g.v + self.translation[1])
}
pub fn inverse(&self) -> Option<GridAlignment> {
let inv = self.transform.inverse()?;
let [tx, ty] = self.translation;
let g = inv.apply(-tx, -ty);
Some(GridAlignment {
transform: inv,
translation: [g.u, g.v],
})
}
}
pub const GRID_TRANSFORMS_D4: [GridTransform; 8] = [
GridTransform {
a: 1,
b: 0,
c: 0,
d: 1,
},
GridTransform {
a: 0,
b: 1,
c: -1,
d: 0,
},
GridTransform {
a: -1,
b: 0,
c: 0,
d: -1,
},
GridTransform {
a: 0,
b: -1,
c: 1,
d: 0,
},
GridTransform {
a: -1,
b: 0,
c: 0,
d: 1,
},
GridTransform {
a: 1,
b: 0,
c: 0,
d: -1,
},
GridTransform {
a: 0,
b: 1,
c: 1,
d: 0,
},
GridTransform {
a: 0,
b: -1,
c: -1,
d: 0,
},
];
#[inline]
pub fn grid_transform_to_next(t: GridTransform) -> NextGridTransform {
NextGridTransform::new(LatticeKind::Square, [[t.a, t.b], [t.c, t.d]], [0, 0])
}
#[inline]
pub fn grid_transform_from_next(t: NextGridTransform) -> GridTransform {
let m = t.matrix;
GridTransform {
a: m[0][0],
b: m[0][1],
c: m[1][0],
d: m[1][1],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn d4_index_1_is_v_negative_u() {
let t = GRID_TRANSFORMS_D4[1];
assert_eq!(t.apply(1, 0), Coord::new(0, -1));
assert_eq!(t.apply(0, 1), Coord::new(1, 0));
}
#[test]
fn transform_identity_mapping_and_inverse() {
let identity = GridTransform::IDENTITY;
assert_eq!(identity.apply(7, -3), Coord::new(7, -3));
assert_eq!(identity.inverse(), Some(identity));
for t in GRID_TRANSFORMS_D4 {
let inv = t.inverse().expect("D4 transform is unimodular");
let p = Coord::new(4, -9);
let q = t.apply(p.u, p.v);
assert_eq!(inv.apply(q.u, q.v), p);
}
}
#[test]
fn alignment_mapping_and_inverse() {
let align = GridAlignment {
transform: GRID_TRANSFORMS_D4[1],
translation: [3, -4],
};
let p = Coord::new(2, 5);
let q = align.map(p.u, p.v);
assert_eq!(q, Coord::new(8, -6));
let inv = align.inverse().expect("D4 alignment is invertible");
assert_eq!(inv.map(q.u, q.v), p);
}
#[test]
fn transform_round_trips_through_next() {
for (idx, t) in GRID_TRANSFORMS_D4.iter().enumerate() {
let next = grid_transform_to_next(*t);
assert_eq!(next.source_kind, LatticeKind::Square);
assert_eq!(next.offset, [0, 0]);
assert_eq!(grid_transform_from_next(next), *t, "D4[{idx}] round-trip");
}
}
}