#![allow(clippy::many_single_char_names)]
pub type Mat3 = [[f64; 3]; 3];
pub const IDENTITY: Mat3 = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
pub fn identity() -> Mat3 {
IDENTITY
}
pub fn mul(a: &Mat3, b: &Mat3) -> Mat3 {
let mut m = [[0.0; 3]; 3];
for (i, row) in m.iter_mut().enumerate() {
for (j, v) in row.iter_mut().enumerate() {
*v = (0..3).map(|k| a[i][k] * b[k][j]).sum();
}
}
m
}
pub fn inverse(m: &Mat3) -> Option<Mat3> {
let c = |i: usize, j: usize| {
let (r0, r1) = ((i + 1) % 3, (i + 2) % 3);
let (c0, c1) = ((j + 1) % 3, (j + 2) % 3);
m[r0][c0] * m[r1][c1] - m[r0][c1] * m[r1][c0]
};
let det = m[0][0] * c(0, 0) + m[0][1] * c(0, 1) + m[0][2] * c(0, 2);
let scale = m.iter().flatten().fold(0.0_f64, |a, v| a.max(v.abs()));
if !det.is_finite() || det.abs() <= 1e-12 * scale.powi(3) {
return None;
}
let mut inv = [[0.0; 3]; 3];
for (i, row) in inv.iter_mut().enumerate() {
for (j, v) in row.iter_mut().enumerate() {
*v = c(j, i) / det;
}
}
Some(inv)
}
pub fn apply(m: &Mat3, [x, y]: [f64; 2]) -> [f64; 2] {
let w = m[2][0] * x + m[2][1] * y + m[2][2];
[
(m[0][0] * x + m[0][1] * y + m[0][2]) / w,
(m[1][0] * x + m[1][1] * y + m[1][2]) / w,
]
}
pub fn translation(tx: f64, ty: f64) -> Mat3 {
[[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]]
}
pub fn similarity(centre: [f64; 2], tx: f64, ty: f64, angle: f64, scale: f64) -> Mat3 {
let (s, c) = angle.sin_cos();
let (a, b) = (scale * c, scale * s);
let [cx, cy] = centre;
[
[a, -b, cx + tx - a * cx + b * cy],
[b, a, cy + ty - b * cx - a * cy],
[0.0, 0.0, 1.0],
]
}
pub fn scale_about(centre: [f64; 2], scale: f64) -> Mat3 {
similarity(centre, 0.0, 0.0, 0.0, scale)
}
pub fn decompose_similarity(m: &Mat3, centre: [f64; 2]) -> (f64, f64, f64, f64) {
let a = f64::midpoint(m[0][0], m[1][1]);
let b = 0.5 * (m[1][0] - m[0][1]);
let [cx, cy] = centre;
let [x, y] = apply(m, centre);
(x - cx, y - cy, b.atan2(a), a.hypot(b))
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: &Mat3, b: &Mat3) -> bool {
a.iter()
.flatten()
.zip(b.iter().flatten())
.all(|(x, y)| (x - y).abs() < 1e-9)
}
#[test]
fn inverse_roundtrip() {
let m = mul(
&similarity([960.0, 540.0], 12.5, -3.0, 0.02, 1.07),
&[[1.0, 0.1, 3.0], [0.05, 0.9, -2.0], [1e-5, 2e-5, 1.0]],
);
let inv = inverse(&m).unwrap();
assert!(close(&mul(&m, &inv), &IDENTITY));
assert!(close(&mul(&inv, &m), &IDENTITY));
assert!(inverse(&[[1.0, 2.0, 0.0], [2.0, 4.0, 0.0], [0.0, 0.0, 1.0]]).is_none());
}
#[test]
fn similarity_about_centre() {
let c = [1920.0, 1080.0];
let m = similarity(c, 5.0, -7.0, 0.3, 1.2);
let [x, y] = apply(&m, c);
assert!((x - 1925.0).abs() < 1e-9 && (y - 1073.0).abs() < 1e-9);
let [x, y] = apply(&m, [c[0] + 100.0, c[1]]);
assert!((x - (1925.0 + 120.0 * 0.3_f64.cos())).abs() < 1e-9);
assert!((y - (1073.0 + 120.0 * 0.3_f64.sin())).abs() < 1e-9);
let (tx, ty, a, s) = decompose_similarity(&m, c);
assert!((tx - 5.0).abs() < 1e-9 && (ty + 7.0).abs() < 1e-9);
assert!((a - 0.3).abs() < 1e-12 && (s - 1.2).abs() < 1e-12);
assert!(close(
&mul(&translation(5.0, -7.0), &scale_about(c, 1.2)),
&similarity(c, 5.0, -7.0, 0.0, 1.2)
));
}
}