use crate::point::Point;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transform {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub e: f32,
pub f: f32,
}
impl Default for Transform {
fn default() -> Self {
Self::IDENTITY
}
}
impl Transform {
pub const IDENTITY: Transform = Transform {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
};
pub fn translate(tx: f32, ty: f32) -> Self {
Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: tx,
f: ty,
}
}
pub fn scale_around(sx: f32, sy: f32, cx: f32, cy: f32) -> Self {
Self {
a: sx,
b: 0.0,
c: 0.0,
d: sy,
e: cx - sx * cx,
f: cy - sy * cy,
}
}
pub fn rotate_around(angle_deg: f32, cx: f32, cy: f32) -> Self {
let a = angle_deg.to_radians();
let cos = a.cos();
let sin = a.sin();
Self {
a: cos,
b: sin,
c: -sin,
d: cos,
e: cx - cx * cos + cy * sin,
f: cy - cx * sin - cy * cos,
}
}
pub fn then(self, next: Transform) -> Transform {
Transform {
a: next.a * self.a + next.c * self.b,
b: next.b * self.a + next.d * self.b,
c: next.a * self.c + next.c * self.d,
d: next.b * self.c + next.d * self.d,
e: next.a * self.e + next.c * self.f + next.e,
f: next.b * self.e + next.d * self.f + next.f,
}
}
pub fn apply(&self, p: Point) -> Point {
Point::new(
self.a * p.x + self.c * p.y + self.e,
self.b * p.x + self.d * p.y + self.f,
)
}
pub fn to_array(&self) -> [f32; 6] {
[self.a, self.b, self.c, self.d, self.e, self.f]
}
pub fn from_array(m: [f32; 6]) -> Transform {
Transform {
a: m[0],
b: m[1],
c: m[2],
d: m[3],
e: m[4],
f: m[5],
}
}
pub fn invert(&self) -> Option<Transform> {
let det = self.a * self.d - self.b * self.c;
if det.abs() < 1e-6 {
return None;
}
Some(Transform {
a: self.d / det,
b: -self.b / det,
c: -self.c / det,
d: self.a / det,
e: (self.c * self.f - self.d * self.e) / det,
f: (self.b * self.e - self.a * self.f) / det,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_is_noop() {
let p = Point::new(3.0, 4.0);
assert_eq!(Transform::IDENTITY.apply(p), p);
}
#[test]
fn then_composes_in_application_order() {
let translate = Transform {
e: 10.0,
..Transform::IDENTITY
};
let scale = Transform {
a: 2.0,
d: 2.0,
..Transform::IDENTITY
};
let t = translate.then(scale);
assert_eq!(t.apply(Point::new(0.0, 0.0)), Point::new(20.0, 0.0));
}
#[test]
fn rotate_around_keeps_center_fixed() {
let c = Point::new(5.0, 5.0);
let r = Transform::rotate_around(90.0, c.x, c.y).apply(c);
assert!((r.x - c.x).abs() < 1e-4 && (r.y - c.y).abs() < 1e-4);
}
#[test]
fn scale_around_keeps_center_fixed() {
let c = Point::new(7.0, 2.0);
let s = Transform::scale_around(3.0, 3.0, c.x, c.y).apply(c);
assert!((s.x - c.x).abs() < 1e-4 && (s.y - c.y).abs() < 1e-4);
}
#[test]
fn from_array_round_trips_to_array() {
let t = Transform {
a: 1.5,
b: 0.5,
c: -0.25,
d: 2.0,
e: 3.0,
f: -4.0,
};
assert_eq!(Transform::from_array(t.to_array()), t);
}
#[test]
fn then_invert_is_identity() {
let t = Transform {
a: 1.5,
b: 0.5,
c: -0.25,
d: 2.0,
e: 3.0,
f: -4.0,
};
let id = t.then(t.invert().unwrap());
let approx = |x: f32, y: f32| (x - y).abs() < 1e-4;
assert!(approx(id.a, 1.0));
assert!(approx(id.b, 0.0));
assert!(approx(id.c, 0.0));
assert!(approx(id.d, 1.0));
assert!(approx(id.e, 0.0));
assert!(approx(id.f, 0.0));
}
#[test]
fn invert_singular_returns_none() {
let singular = Transform {
a: 0.0,
b: 0.0,
c: 0.0,
d: 0.0,
e: 5.0,
f: 5.0,
};
assert!(singular.invert().is_none());
}
}