use crate::math::Vec2;
const EPS: f64 = 1e-12;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Affine2 {
pub m: [[f64; 3]; 3],
}
impl Affine2 {
#[must_use]
pub fn identity() -> Self {
Self { m: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] }
}
#[must_use]
pub fn translation(t: Vec2) -> Self {
Self { m: [[1.0, 0.0, t.x], [0.0, 1.0, t.y], [0.0, 0.0, 1.0]] }
}
#[must_use]
pub fn rotation(angle: f64) -> Self {
let (s, c) = angle.sin_cos();
Self { m: [[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]] }
}
#[must_use]
pub fn rotation_about(angle: f64, center: Vec2) -> Self {
Self::translation(center)
.compose(&Self::rotation(angle))
.compose(&Self::translation(-center))
}
#[must_use]
pub fn scaling(sx: f64, sy: f64) -> Self {
Self { m: [[sx, 0.0, 0.0], [0.0, sy, 0.0], [0.0, 0.0, 1.0]] }
}
#[must_use]
pub fn shear(kx: f64, ky: f64) -> Self {
Self { m: [[1.0, kx, 0.0], [ky, 1.0, 0.0], [0.0, 0.0, 1.0]] }
}
#[must_use]
pub fn reflection(line_through_origin_dir: Vec2) -> Self {
let d = line_through_origin_dir;
assert!(d.magnitude_squared() > 0.0, "reflection requires a non-zero direction");
let n = d.normalized();
let (x, y) = (n.x, n.y);
Self {
m: [
[2.0 * x * x - 1.0, 2.0 * x * y, 0.0],
[2.0 * x * y, 2.0 * y * y - 1.0, 0.0],
[0.0, 0.0, 1.0],
],
}
}
#[must_use]
pub fn from_three_points(src: [Vec2; 3], dst: [Vec2; 3]) -> Option<Self> {
let det = src[0].x * (src[1].y - src[2].y) - src[1].x * (src[0].y - src[2].y)
+ src[2].x * (src[0].y - src[1].y);
if det.abs() < EPS {
return None;
}
let solve_row = |d0: f64, d1: f64, d2: f64| -> [f64; 3] {
let a = [
[src[0].x, src[0].y, 1.0],
[src[1].x, src[1].y, 1.0],
[src[2].x, src[2].y, 1.0],
];
let rhs = [d0, d1, d2];
let det3 = |c0: [f64; 3], c1: [f64; 3], c2: [f64; 3]| {
c0[0] * (c1[1] * c2[2] - c1[2] * c2[1])
- c1[0] * (c0[1] * c2[2] - c0[2] * c2[1])
+ c2[0] * (c0[1] * c1[2] - c0[2] * c1[1])
};
let col = |j: usize| [a[0][j], a[1][j], a[2][j]];
let d_full = det3(col(0), col(1), col(2));
[
det3(rhs, col(1), col(2)) / d_full,
det3(col(0), rhs, col(2)) / d_full,
det3(col(0), col(1), rhs) / d_full,
]
};
let r0 = solve_row(dst[0].x, dst[1].x, dst[2].x);
let r1 = solve_row(dst[0].y, dst[1].y, dst[2].y);
Some(Self { m: [r0, r1, [0.0, 0.0, 1.0]] })
}
#[must_use]
pub fn compose(&self, other: &Self) -> Self {
let mut out = [[0.0; 3]; 3];
for (i, row) in out.iter_mut().enumerate() {
for (j, v) in row.iter_mut().enumerate() {
*v = (0..3).map(|k| self.m[i][k] * other.m[k][j]).sum();
}
}
Self { m: out }
}
#[must_use]
pub fn apply(&self, p: Vec2) -> Vec2 {
Vec2::new(
self.m[0][0] * p.x + self.m[0][1] * p.y + self.m[0][2],
self.m[1][0] * p.x + self.m[1][1] * p.y + self.m[1][2],
)
}
#[must_use]
pub fn apply_vector(&self, v: Vec2) -> Vec2 {
Vec2::new(
self.m[0][0] * v.x + self.m[0][1] * v.y,
self.m[1][0] * v.x + self.m[1][1] * v.y,
)
}
#[must_use]
pub fn inverse(&self) -> Option<Self> {
let [a, b] = [self.m[0][0], self.m[0][1]];
let [c, d] = [self.m[1][0], self.m[1][1]];
let det = a * d - b * c;
if det.abs() < EPS {
return None;
}
let (tx, ty) = (self.m[0][2], self.m[1][2]);
let inv = 1.0 / det;
let (ia, ib, ic, id) = (d * inv, -b * inv, -c * inv, a * inv);
Some(Self {
m: [
[ia, ib, -(ia * tx + ib * ty)],
[ic, id, -(ic * tx + id * ty)],
[0.0, 0.0, 1.0],
],
})
}
#[must_use]
pub fn decompose(&self) -> (Vec2, f64, Vec2, f64) {
let t = Vec2::new(self.m[0][2], self.m[1][2]);
let col0 = Vec2::new(self.m[0][0], self.m[1][0]);
let col1 = Vec2::new(self.m[0][1], self.m[1][1]);
let sx = col0.magnitude();
let rot = col0.y.atan2(col0.x);
let r0 = col0.normalized();
let h = r0.dot(&col1); let perp = col1 - r0 * h;
let mut sy = perp.magnitude();
if col0.cross(&col1) < 0.0 {
sy = -sy;
}
let shear = if sy.abs() > EPS { h / sy } else { 0.0 };
(t, rot, Vec2::new(sx, sy), shear)
}
#[must_use]
pub fn is_rigid(&self, tol: f64) -> bool {
let (a, b) = (self.m[0][0], self.m[0][1]);
let (c, d) = (self.m[1][0], self.m[1][1]);
let det = a * d - b * c;
((a * a + c * c) - 1.0).abs() <= tol
&& ((b * b + d * d) - 1.0).abs() <= tol
&& (a * b + c * d).abs() <= tol
&& (det - 1.0).abs() <= tol
}
#[must_use]
pub fn is_similarity(&self, tol: f64) -> bool {
let (a, b) = (self.m[0][0], self.m[0][1]);
let (c, d) = (self.m[1][0], self.m[1][1]);
let n0 = a * a + c * c;
let n1 = b * b + d * d;
(n0 - n1).abs() <= tol * n0.max(n1).max(1.0) && (a * b + c * d).abs() <= tol * n0.max(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::constants::PI;
fn close(a: Vec2, b: Vec2, tol: f64) -> bool {
a.distance_to(&b) < tol
}
#[test]
fn test_basic_maps() {
let t = Affine2::translation(Vec2::new(1.0, 2.0));
assert!(close(t.apply(Vec2::ZERO), Vec2::new(1.0, 2.0), 1e-15));
assert!(close(t.apply_vector(Vec2::new(3.0, 4.0)), Vec2::new(3.0, 4.0), 1e-15));
let r = Affine2::rotation(PI / 2.0);
assert!(close(r.apply(Vec2::new(1.0, 0.0)), Vec2::new(0.0, 1.0), 1e-12));
let s = Affine2::scaling(2.0, 3.0);
assert!(close(s.apply(Vec2::new(1.0, 1.0)), Vec2::new(2.0, 3.0), 1e-15));
let sh = Affine2::shear(0.5, 0.0);
assert!(close(sh.apply(Vec2::new(0.0, 2.0)), Vec2::new(1.0, 2.0), 1e-15));
}
#[test]
fn test_rotation_about_center_fixes_center() {
let c = Vec2::new(3.0, -1.0);
let r = Affine2::rotation_about(1.2, c);
assert!(close(r.apply(c), c, 1e-12));
let p = Vec2::new(5.0, 2.0);
assert!((r.apply(p).distance_to(&c) - p.distance_to(&c)).abs() < 1e-12);
}
#[test]
fn test_reflection_is_involution() {
let m = Affine2::reflection(Vec2::new(1.0, 2.0));
let p = Vec2::new(-3.0, 0.7);
assert!(close(m.apply(m.apply(p)), p, 1e-12));
let on_line = Vec2::new(2.0, 4.0);
assert!(close(m.apply(on_line), on_line, 1e-12));
}
#[test]
fn test_from_three_points_exact() {
let src = [Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), Vec2::new(0.0, 1.0)];
let dst = [Vec2::new(2.0, 1.0), Vec2::new(3.0, 2.0), Vec2::new(1.0, 3.0)];
let m = Affine2::from_three_points(src, dst).unwrap();
for (s, d) in src.iter().zip(&dst) {
assert!(close(m.apply(*s), *d, 1e-10));
}
let bad = [Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0), Vec2::new(2.0, 2.0)];
assert!(Affine2::from_three_points(bad, dst).is_none());
}
#[test]
fn test_inverse_and_compose() {
let m = Affine2::translation(Vec2::new(1.0, -2.0))
.compose(&Affine2::rotation(0.7))
.compose(&Affine2::scaling(2.0, 0.5));
let inv = m.inverse().unwrap();
let p = Vec2::new(0.3, -1.1);
assert!(close(inv.apply(m.apply(p)), p, 1e-12));
assert!(Affine2::scaling(0.0, 1.0).inverse().is_none());
}
#[test]
fn test_decompose_recompose_roundtrip() {
let (t, rot, s, k) = (Vec2::new(1.0, 2.0), 0.6, Vec2::new(2.0, 0.7), 0.3);
let shear_m = Affine2 {
m: [[s.x, k * s.y, 0.0], [0.0, s.y, 0.0], [0.0, 0.0, 1.0]],
};
let m = Affine2::translation(t).compose(&Affine2::rotation(rot)).compose(&shear_m);
let (t2, rot2, s2, k2) = m.decompose();
assert!(close(t2, t, 1e-12));
assert!((rot2 - rot).abs() < 1e-12);
assert!(close(s2, s, 1e-12));
assert!((k2 - k).abs() < 1e-12);
}
#[test]
fn test_rigid_and_similarity_predicates() {
let rigid = Affine2::translation(Vec2::new(1.0, 1.0)).compose(&Affine2::rotation(0.4));
assert!(rigid.is_rigid(1e-12));
assert!(rigid.is_similarity(1e-12));
let sim = rigid.compose(&Affine2::scaling(2.0, 2.0));
assert!(!sim.is_rigid(1e-9));
assert!(sim.is_similarity(1e-12));
let general = Affine2::scaling(2.0, 1.0);
assert!(!general.is_similarity(1e-9));
let reflect = Affine2::reflection(Vec2::new(1.0, 0.0));
assert!(!reflect.is_rigid(1e-9), "reflection is not orientation-preserving");
}
}