Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/src/transform.rs

4.8 KiB, 36 runs

created by r1870400018:13954, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Affine transforms in two dimensions.
2//!
3//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
4//! Anthropic Claude
5
6use crate::path::Pt;
7
8/// A 2D affine transform.
9///
10/// The six coefficients are those of the matrix
11///
12/// ```text
13/// | a c e |
14/// | b d f |
15/// | 0 0 1 |
16/// ```
17///
18/// which maps a point `(x, y)` to `(a·x + c·y + e, b·x + d·y + f)`.
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct Transform {
21 pub a: f32, // horizontal scale
22 pub b: f32, // vertical shear
23 pub c: f32, // horizontal shear
24 pub d: f32, // vertical scale
25 pub e: f32, // horizontal translation
26 pub f: f32, // vertical translation
27}
28
29impl Default for Transform {
30 fn default() -> Self {
31 Self::IDENTITY
32 }
33}
34
35impl Transform {
36
37 pub const IDENTITY: Self = Self { a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: 0.0, f: 0.0 };
38
39 pub const fn translate(tx: f32, ty: f32) -> Self {
40 Self { a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: tx, f: ty }
41 }
42
43 /// Scales about the origin.
44 pub const fn scale(sx: f32, sy: f32) -> Self {
45 Self { a: sx, b: 0.0, c: 0.0, d: sy, e: 0.0, f: 0.0 }
46 }
47
48 /// Rotates about the origin, anticlockwise in a y-up frame, by an angle in radians.
49 pub fn rotate(radians: f32) -> Self {
50 let (s, c) = radians.sin_cos();
51 Self { a: c, b: s, c: -s, d: c, e: 0.0, f: 0.0 }
52 }
53
54 /// Applies `self` first and then `next`. That is the order a caller means when they say "scale
55 /// it, then move it", which is the reverse of the order the matrices multiply in.
56 pub fn then(&self, next: &Self) -> Self {
57 Self {
58 a: next.a * self.a + next.c * self.b,
59 b: next.b * self.a + next.d * self.b,
60 c: next.a * self.c + next.c * self.d,
61 d: next.b * self.c + next.d * self.d,
62 e: next.a * self.e + next.c * self.f + next.e,
63 f: next.b * self.e + next.d * self.f + next.f,
64 }
65 }
66
67 pub fn apply(&self, p: Pt) -> Pt {
68 Pt {
69 x: self.a * p.x + self.c * p.y + self.e,
70 y: self.b * p.x + self.d * p.y + self.f,
71 }
72 }
73
74 /// The square root of the absolute determinant, which is the factor by which lengths stretch.
75 ///
76 /// A curve is flattened in the space it is defined in, but the tolerance that matters is the
77 /// one measured in pixels, so the tolerance is divided by this before flattening.
78 pub fn scale_factor(&self) -> f32 {
79 (self.a * self.d - self.b * self.c).abs().sqrt()
80 }
81
82 /// Is this the identity, and so skippable?
83 pub fn is_identity(&self) -> bool {
84 *self == Self::IDENTITY
85 }
86
87 /// A transform with a zero determinant has collapsed the plane onto a line or a point and
88 /// cannot be undone, since everything on that line came from somewhere different. A caller
89 /// carrying a pixel back into the coordinates a shape was defined in -- to read a gradient, a
90 /// pattern or a texture there -- is what this is for.
91 pub fn invert(&self) -> Option<Self> {
92 let det = self.a * self.d - self.b * self.c;
93 if det == 0.0 || !det.is_finite() {
94 return None;
95 }
96 let k = 1.0 / det;
97 Some(Self {
98 a: self.d * k,
99 b: -self.b * k,
100 c: -self.c * k,
101 d: self.a * k,
102 e: (self.c * self.f - self.d * self.e) * k,
103 f: (self.b * self.e - self.a * self.f) * k,
104 })
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_identity_leaves_a_point_00() {
114 let p = Pt::new(3.0, 4.0);
115 assert_eq!(Transform::IDENTITY.apply(p), p);
116 }
117
118 #[test]
119 fn test_scale_then_translate_01() {
120 // Scale by two, then move right by ten: the point (1, 1) lands at (12, 2).
121 let t = Transform::scale(2.0, 2.0).then(&Transform::translate(10.0, 0.0));
122 assert_eq!(t.apply(Pt::new(1.0, 1.0)), Pt::new(12.0, 2.0));
123 }
124
125 #[test]
126 fn test_translate_then_scale_differs_02() {
127 // The other order: move right by ten, then scale by two, landing at (22, 2).
128 let t = Transform::translate(10.0, 0.0).then(&Transform::scale(2.0, 2.0));
129 assert_eq!(t.apply(Pt::new(1.0, 1.0)), Pt::new(22.0, 2.0));
130 }
131
132 #[test]
133 fn test_scale_factor_03() {
134 assert_eq!(Transform::scale(3.0, 3.0).scale_factor(), 3.0);
135 assert_eq!(Transform::IDENTITY.scale_factor(), 1.0);
136 }
137 #[test]
138 fn test_a_transform_and_its_inverse_return_a_point_04() {
139 let t = Transform::scale(3.0, -2.0)
140 .then(&Transform::rotate(0.7))
141 .then(&Transform::translate(11.0, -4.0));
142 let inv = match t.invert() {
143 Some(inv) => inv,
144 None => panic!("a transform of non-zero determinant must invert"),
145 };
146 for p in [Pt::new(0.0, 0.0), Pt::new(1.0, 0.0), Pt::new(-13.5, 7.25)] {
147 let back = inv.apply(t.apply(p));
148 assert!((back.x - p.x).abs() < 1e-3 && (back.y - p.y).abs() < 1e-3,
149 "({}, {}) came back as ({}, {})", p.x, p.y, back.x, back.y);
150 }
151 }
152
153 #[test]
154 fn test_a_collapsed_transform_has_no_inverse_05() {
155 // A scale of zero on one axis folds the plane onto a line, and everything on that line
156 // came from somewhere different.
157 assert!(Transform::scale(1.0, 0.0).invert().is_none());
158 assert!(Transform::scale(0.0, 0.0).invert().is_none());
159 assert!(Transform::IDENTITY.invert().is_some());
160 }
161
162}