use lieui_geom::{Point, Rect};
use crate::track::Transform;
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct Affine {
pub m: [f32; 6],
}
impl Default for Affine {
fn default() -> Self {
Self::IDENTITY
}
}
impl Affine {
pub const IDENTITY: Affine = Affine {
m: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
};
pub const fn new(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self {
Self { m: [a, b, c, d, e, f] }
}
pub const fn translate(tx: f32, ty: f32) -> Self {
Self::new(1.0, 0.0, 0.0, 1.0, tx, ty)
}
pub const fn scale(sx: f32, sy: f32) -> Self {
Self::new(sx, 0.0, 0.0, sy, 0.0, 0.0)
}
pub fn rotate_deg(deg: f32) -> Self {
let r = deg.to_radians();
let (s, c) = r.sin_cos();
Self::new(c, s, -s, c, 0.0, 0.0)
}
pub fn is_identity(&self) -> bool {
self.m == Self::IDENTITY.m
}
pub fn then(&self, rhs: Affine) -> Affine {
let [a1, b1, c1, d1, e1, f1] = self.m;
let [a2, b2, c2, d2, e2, f2] = rhs.m;
Affine {
m: [
a1 * a2 + c1 * b2,
b1 * a2 + d1 * b2,
a1 * c2 + c1 * d2,
b1 * c2 + d1 * d2,
a1 * e2 + c1 * f2 + e1,
b1 * e2 + d1 * f2 + f1,
],
}
}
pub fn apply(&self, p: Point) -> Point {
let [a, b, c, d, e, f] = self.m;
Point::new(a * p.x + c * p.y + e, b * p.x + d * p.y + f)
}
pub fn apply_vector(&self, v: (f32, f32)) -> (f32, f32) {
let [a, b, c, d, _, _] = self.m;
(a * v.0 + c * v.1, b * v.0 + d * v.1)
}
pub fn inverse(&self) -> Option<Affine> {
let [a, b, c, d, e, f] = self.m;
let det = a * d - b * c;
if det.abs() < 1e-12 {
return None;
}
let inv = 1.0 / det;
Some(Affine {
m: [
d * inv,
-b * inv,
-c * inv,
a * inv,
(c * f - d * e) * inv,
(b * e - a * f) * inv,
],
})
}
pub fn bounding_box(&self, r: Rect) -> Rect {
let pts = [
self.apply(Point::new(r.x, r.y)),
self.apply(Point::new(r.x + r.width, r.y)),
self.apply(Point::new(r.x, r.y + r.height)),
self.apply(Point::new(r.x + r.width, r.y + r.height)),
];
let (mut min_x, mut min_y) = (f32::MAX, f32::MAX);
let (mut max_x, mut max_y) = (f32::MIN, f32::MIN);
for p in pts {
min_x = min_x.min(p.x);
min_y = min_y.min(p.y);
max_x = max_x.max(p.x);
max_y = max_y.max(p.y);
}
Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
}
}
impl Transform {
pub fn matrix(&self, rect: Rect) -> Affine {
if self.is_identity() {
return Affine::IDENTITY;
}
let ox = rect.x + self.origin.0 * rect.width;
let oy = rect.y + self.origin.1 * rect.height;
Affine::translate(ox + self.translate.0, oy + self.translate.1)
.then(Affine::rotate_deg(self.rotation_deg))
.then(Affine::scale(self.scale.0, self.scale.1))
.then(Affine::translate(-ox, -oy))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: Point, b: Point) {
assert!(
(a.x - b.x).abs() < 1e-3 && (a.y - b.y).abs() < 1e-3,
"期望 {b:?},实际 {a:?}"
);
}
#[test]
fn identity_is_a_no_op() {
let m = Affine::IDENTITY;
approx(m.apply(Point::new(3.0, 4.0)), Point::new(3.0, 4.0));
assert!(m.is_identity());
assert!(m.inverse().unwrap().is_identity());
}
#[test]
fn composition_applies_rhs_first() {
let m = Affine::scale(2.0, 2.0).then(Affine::translate(10.0, 0.0));
approx(m.apply(Point::new(1.0, 1.0)), Point::new(22.0, 2.0));
}
#[test]
fn inverse_round_trips() {
let m = Affine::translate(5.0, -3.0)
.then(Affine::rotate_deg(30.0))
.then(Affine::scale(2.0, 0.5));
let inv = m.inverse().unwrap();
let p = Point::new(7.0, 11.0);
approx(inv.apply(m.apply(p)), p);
}
#[test]
fn degenerate_matrix_has_no_inverse() {
assert!(Affine::scale(0.0, 1.0).inverse().is_none());
}
#[test]
fn rotate_90_maps_axes() {
let m = Affine::rotate_deg(90.0);
approx(m.apply(Point::new(1.0, 0.0)), Point::new(0.0, 1.0));
}
#[test]
fn transform_matrix_about_center() {
let rect = Rect::new(0.0, 0.0, 100.0, 50.0);
let t = Transform {
scale: (2.0, 2.0),
..Transform::default()
};
let m = t.matrix(rect);
approx(m.apply(Point::new(50.0, 25.0)), Point::new(50.0, 25.0));
approx(m.apply(Point::new(0.0, 25.0)), Point::new(-50.0, 25.0));
}
#[test]
fn identity_transform_short_circuits() {
let rect = Rect::new(10.0, 10.0, 20.0, 20.0);
assert!(Transform::default().matrix(rect).is_identity());
}
#[test]
fn translate_only() {
let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
let t = Transform {
translate: (5.0, -5.0),
..Transform::default()
};
let m = t.matrix(rect);
approx(m.apply(Point::new(0.0, 0.0)), Point::new(5.0, -5.0));
}
#[test]
fn bounding_box_covers_rotated_rect() {
let r = Rect::new(0.0, 0.0, 10.0, 10.0);
let m = Affine::rotate_deg(45.0);
let bb = m.bounding_box(r);
let s = 10.0 * std::f32::consts::SQRT_2;
assert!((bb.width - s).abs() < 1e-2 && (bb.height - s).abs() < 1e-2, "{bb:?}");
}
#[test]
fn apply_vector_ignores_translation() {
let m = Affine::translate(100.0, 100.0);
assert_eq!(m.apply_vector((1.0, 2.0)), (1.0, 2.0));
}
}