#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AffineMatrix {
pub data: [[f32; 3]; 3],
}
impl AffineMatrix {
#[must_use]
pub fn identity() -> Self {
Self {
data: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
}
}
#[must_use]
pub fn translation(tx: f32, ty: f32) -> Self {
Self {
data: [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]],
}
}
#[must_use]
pub fn rotation(angle_rad: f32) -> Self {
let c = angle_rad.cos();
let s = angle_rad.sin();
Self {
data: [[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]],
}
}
#[must_use]
pub fn scale(sx: f32, sy: f32) -> Self {
Self {
data: [[sx, 0.0, 0.0], [0.0, sy, 0.0], [0.0, 0.0, 1.0]],
}
}
#[must_use]
pub fn multiply(&self, rhs: &AffineMatrix) -> AffineMatrix {
let a = &self.data;
let b = &rhs.data;
let mut out = [[0.0_f32; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
out[i][j] += a[i][k] * b[k][j];
}
}
}
AffineMatrix { data: out }
}
#[must_use]
pub fn transform_point(&self, x: f32, y: f32) -> (f32, f32) {
let d = &self.data;
let xp = d[0][0] * x + d[0][1] * y + d[0][2];
let yp = d[1][0] * x + d[1][1] * y + d[1][2];
(xp, yp)
}
#[must_use]
pub fn is_identity(&self) -> bool {
let id = AffineMatrix::identity();
for i in 0..3 {
for j in 0..3 {
if (self.data[i][j] - id.data[i][j]).abs() > 1e-5 {
return false;
}
}
}
true
}
}
impl Default for AffineMatrix {
fn default() -> Self {
Self::identity()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AffineTransform {
pub matrix: AffineMatrix,
}
impl AffineTransform {
#[must_use]
pub fn new(matrix: AffineMatrix) -> Self {
Self { matrix }
}
#[must_use]
pub fn compose(&self, other: &AffineTransform) -> AffineTransform {
AffineTransform {
matrix: self.matrix.multiply(&other.matrix),
}
}
#[must_use]
pub fn inverse_translation(&self) -> AffineTransform {
let tx = -self.matrix.data[0][2];
let ty = -self.matrix.data[1][2];
AffineTransform {
matrix: AffineMatrix::translation(tx, ty),
}
}
}
impl Default for AffineTransform {
fn default() -> Self {
Self::new(AffineMatrix::identity())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::PI;
#[test]
fn test_identity_is_identity() {
let m = AffineMatrix::identity();
assert!(m.is_identity());
}
#[test]
fn test_translation_matrix_values() {
let m = AffineMatrix::translation(3.0, -5.0);
assert_eq!(m.data[0][2], 3.0);
assert_eq!(m.data[1][2], -5.0);
assert_eq!(m.data[2][2], 1.0);
assert!(!m.is_identity());
}
#[test]
fn test_transform_point_identity() {
let m = AffineMatrix::identity();
let (xp, yp) = m.transform_point(7.0, -3.0);
assert!((xp - 7.0).abs() < 1e-5);
assert!((yp + 3.0).abs() < 1e-5);
}
#[test]
fn test_transform_point_translation() {
let m = AffineMatrix::translation(10.0, 20.0);
let (xp, yp) = m.transform_point(1.0, 2.0);
assert!((xp - 11.0).abs() < 1e-5);
assert!((yp - 22.0).abs() < 1e-5);
}
#[test]
fn test_rotation_90_degrees() {
let m = AffineMatrix::rotation(PI / 2.0);
let (xp, yp) = m.transform_point(1.0, 0.0);
assert!((xp - 0.0).abs() < 1e-5);
assert!((yp - 1.0).abs() < 1e-5);
}
#[test]
fn test_rotation_zero_is_identity() {
let m = AffineMatrix::rotation(0.0);
assert!(m.is_identity());
}
#[test]
fn test_scale_matrix() {
let m = AffineMatrix::scale(2.0, 3.0);
let (xp, yp) = m.transform_point(4.0, 5.0);
assert!((xp - 8.0).abs() < 1e-5);
assert!((yp - 15.0).abs() < 1e-5);
}
#[test]
fn test_scale_identity_is_identity() {
let m = AffineMatrix::scale(1.0, 1.0);
assert!(m.is_identity());
}
#[test]
fn test_multiply_identity_with_translation() {
let id = AffineMatrix::identity();
let t = AffineMatrix::translation(5.0, -2.0);
let result = id.multiply(&t);
assert!((result.data[0][2] - 5.0).abs() < 1e-5);
assert!((result.data[1][2] + 2.0).abs() < 1e-5);
}
#[test]
fn test_multiply_two_translations() {
let t1 = AffineMatrix::translation(3.0, 4.0);
let t2 = AffineMatrix::translation(1.0, -1.0);
let result = t1.multiply(&t2);
assert!((result.data[0][2] - 4.0).abs() < 1e-5);
assert!((result.data[1][2] - 3.0).abs() < 1e-5);
}
#[test]
fn test_affine_transform_compose() {
let t1 = AffineTransform::new(AffineMatrix::translation(2.0, 0.0));
let t2 = AffineTransform::new(AffineMatrix::translation(0.0, 3.0));
let composed = t1.compose(&t2);
let (xp, yp) = composed.matrix.transform_point(0.0, 0.0);
assert!((xp - 2.0).abs() < 1e-5);
assert!((yp - 3.0).abs() < 1e-5);
}
#[test]
fn test_inverse_translation() {
let t = AffineTransform::new(AffineMatrix::translation(7.0, -4.0));
let inv = t.inverse_translation();
assert!((inv.matrix.data[0][2] + 7.0).abs() < 1e-5);
assert!((inv.matrix.data[1][2] - 4.0).abs() < 1e-5);
}
#[test]
fn test_translation_then_inverse_is_identity() {
let t = AffineTransform::new(AffineMatrix::translation(5.0, 3.0));
let inv = t.inverse_translation();
let composed = t.compose(&inv);
let (xp, yp) = composed.matrix.transform_point(1.0, 1.0);
assert!((xp - 1.0).abs() < 1e-4);
assert!((yp - 1.0).abs() < 1e-4);
}
#[test]
fn test_default_affine_matrix_is_identity() {
let m = AffineMatrix::default();
assert!(m.is_identity());
}
#[test]
fn test_default_affine_transform_is_identity() {
let t = AffineTransform::default();
assert!(t.matrix.is_identity());
}
}