#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Transform {
pub sx: f64,
pub ky: f64,
pub kx: f64,
pub sy: f64,
pub tx: f64,
pub ty: f64,
}
impl Transform {
pub fn identity() -> Self {
Self { sx: 1.0, ky: 0.0, kx: 0.0, sy: 1.0, tx: 0.0, ty: 0.0 }
}
pub fn apply(&self, x: f64, y: f64) -> (f64, f64) {
(self.sx * x + self.kx * y + self.tx, self.ky * x + self.sy * y + self.ty)
}
pub fn pre_translate(&self, dx: f64, dy: f64) -> Self {
Self {
sx: self.sx,
ky: self.ky,
kx: self.kx,
sy: self.sy,
tx: self.sx * dx + self.kx * dy + self.tx,
ty: self.ky * dx + self.sy * dy + self.ty,
}
}
pub fn pre_scale(&self, sx: f64, sy: f64) -> Self {
Self {
sx: self.sx * sx,
ky: self.ky * sx,
kx: self.kx * sy,
sy: self.sy * sy,
tx: self.tx,
ty: self.ty,
}
}
pub fn pre_rotate(&self, angle_rad: f64) -> Self {
let (s, c) = angle_rad.sin_cos();
Self {
sx: self.sx * c + self.kx * s,
ky: self.ky * c + self.sy * s,
kx: -self.sx * s + self.kx * c,
sy: -self.ky * s + self.sy * c,
tx: self.tx,
ty: self.ty,
}
}
pub fn scale_factor(&self) -> f64 {
let x_len = (self.sx * self.sx + self.ky * self.ky).sqrt();
let y_len = (self.kx * self.kx + self.sy * self.sy).sqrt();
((x_len + y_len) / 2.0).max(1e-9)
}
pub fn is_axis_aligned(&self) -> bool {
self.kx.abs() < 1e-9 && self.ky.abs() < 1e-9
}
}
#[cfg(test)]
mod tests {
use super::Transform;
#[test]
fn identity_is_a_no_op() {
let t = Transform::identity();
assert_eq!(t.apply(3.0, 4.0), (3.0, 4.0));
}
#[test]
fn translate_then_scale_matches_canvas_semantics() {
let t = Transform::identity().pre_translate(10.0, 20.0).pre_scale(2.0, 3.0);
assert_eq!(t.apply(0.0, 0.0), (10.0, 20.0));
assert_eq!(t.apply(5.0, 5.0), (20.0, 35.0));
}
#[test]
fn rotate_90_degrees_maps_x_axis_onto_y_axis() {
let t = Transform::identity().pre_rotate(std::f64::consts::FRAC_PI_2);
let (x, y) = t.apply(1.0, 0.0);
assert!((x - 0.0).abs() < 1e-9);
assert!((y - 1.0).abs() < 1e-9);
}
#[test]
fn axis_aligned_detects_rotation() {
assert!(Transform::identity().pre_translate(5.0, 5.0).pre_scale(2.0, 2.0).is_axis_aligned());
assert!(!Transform::identity().pre_rotate(0.3).is_axis_aligned());
}
#[test]
fn scale_factor_matches_uniform_scale_exactly() {
let t = Transform::identity().pre_scale(2.5, 2.5);
assert!((t.scale_factor() - 2.5).abs() < 1e-9);
}
}