1use crate::{Point, Rect};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Transform {
11 pub a: f64,
12 pub b: f64,
13 pub c: f64,
14 pub d: f64,
15 pub e: f64,
16 pub f: f64,
17}
18
19impl Transform {
20 pub const IDENTITY: Transform = Transform {
22 a: 1.0,
23 b: 0.0,
24 c: 0.0,
25 d: 1.0,
26 e: 0.0,
27 f: 0.0,
28 };
29
30 pub fn rotate_about(degrees: f64, cx: f64, cy: f64) -> Transform {
32 let (sin, cos) = degrees.to_radians().sin_cos();
33 Transform {
34 a: cos,
35 b: sin,
36 c: -sin,
37 d: cos,
38 e: cx - cos * cx + sin * cy,
39 f: cy - sin * cx - cos * cy,
40 }
41 }
42
43 pub fn then(self, next: Transform) -> Transform {
45 Transform {
46 a: next.a * self.a + next.c * self.b,
47 b: next.b * self.a + next.d * self.b,
48 c: next.a * self.c + next.c * self.d,
49 d: next.b * self.c + next.d * self.d,
50 e: next.a * self.e + next.c * self.f + next.e,
51 f: next.b * self.e + next.d * self.f + next.f,
52 }
53 }
54
55 pub fn apply(self, point: Point) -> Point {
57 Point {
58 x: self.a * point.x + self.c * point.y + self.e,
59 y: self.b * point.x + self.d * point.y + self.f,
60 }
61 }
62
63 pub fn is_identity(self) -> bool {
65 self == Self::IDENTITY
66 }
67
68 pub fn transform_rect_bbox(self, rect: Rect) -> Rect {
70 let corners = [
71 self.apply(Point {
72 x: rect.x,
73 y: rect.y,
74 }),
75 self.apply(Point {
76 x: rect.x + rect.width,
77 y: rect.y,
78 }),
79 self.apply(Point {
80 x: rect.x,
81 y: rect.y + rect.height,
82 }),
83 self.apply(Point {
84 x: rect.x + rect.width,
85 y: rect.y + rect.height,
86 }),
87 ];
88
89 let mut min_x = corners[0].x;
90 let mut min_y = corners[0].y;
91 let mut max_x = corners[0].x;
92 let mut max_y = corners[0].y;
93 for corner in &corners[1..] {
94 min_x = min_x.min(corner.x);
95 min_y = min_y.min(corner.y);
96 max_x = max_x.max(corner.x);
97 max_y = max_y.max(corner.y);
98 }
99
100 Rect {
101 x: min_x,
102 y: min_y,
103 width: max_x - min_x,
104 height: max_y - min_y,
105 }
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::Transform;
112 use crate::{Point, Rect};
113
114 const EPSILON: f64 = 1.0e-10;
115
116 fn assert_close(actual: f64, expected: f64) {
117 assert!(
118 (actual - expected).abs() < EPSILON,
119 "expected {expected}, got {actual}"
120 );
121 }
122
123 fn assert_point_close(actual: Point, expected: Point) {
124 assert_close(actual.x, expected.x);
125 assert_close(actual.y, expected.y);
126 }
127
128 #[test]
129 fn identity_is_neutral_for_points_and_composition() {
130 let transform = Transform {
131 a: 2.0,
132 b: 3.0,
133 c: 5.0,
134 d: 7.0,
135 e: 11.0,
136 f: 13.0,
137 };
138 let point = Point { x: 17.0, y: 19.0 };
139
140 assert_eq!(Transform::IDENTITY.apply(point), point);
141 assert_eq!(Transform::IDENTITY.then(transform), transform);
142 assert_eq!(transform.then(Transform::IDENTITY), transform);
143 }
144
145 #[test]
146 fn rotate_about_keeps_the_pivot_fixed() {
147 let fractional = Transform::rotate_about(33.5, 10.0, 20.0);
148 assert_point_close(
149 fractional.apply(Point { x: 10.0, y: 20.0 }),
150 Point { x: 10.0, y: 20.0 },
151 );
152
153 let quarter_turn = Transform::rotate_about(90.0, 10.0, 20.0);
154 assert_point_close(
155 quarter_turn.apply(Point { x: 11.0, y: 20.0 }),
156 Point { x: 10.0, y: 21.0 },
157 );
158 }
159
160 #[test]
161 fn then_matches_the_pdf_cm_composition_order() {
162 let first = Transform {
163 a: 2.0,
164 b: 3.0,
165 c: 5.0,
166 d: 7.0,
167 e: 11.0,
168 f: 13.0,
169 };
170 let next = Transform {
171 a: 17.0,
172 b: 19.0,
173 c: 23.0,
174 d: 29.0,
175 e: 31.0,
176 f: 37.0,
177 };
178
179 let combined = first.then(next);
182 assert_eq!(
183 combined,
184 Transform {
185 a: 103.0,
186 b: 125.0,
187 c: 246.0,
188 d: 298.0,
189 e: 517.0,
190 f: 623.0,
191 }
192 );
193 assert_eq!(
194 combined.apply(Point { x: 4.0, y: 3.0 }),
195 Point {
196 x: 1667.0,
197 y: 2017.0
198 }
199 );
200 assert_eq!(
201 combined.apply(Point { x: 4.0, y: 3.0 }),
202 next.apply(first.apply(Point { x: 4.0, y: 3.0 }))
203 );
204 }
205
206 #[test]
207 fn transform_rect_bbox_contains_all_four_transformed_corners() {
208 let transform = Transform::rotate_about(-30.0, 0.0, 0.0);
209 let rect = Rect {
210 x: 1.0,
211 y: 2.0,
212 width: 3.0,
213 height: 4.0,
214 };
215 let bounds = transform.transform_rect_bbox(rect);
216 let cos = 3.0_f64.sqrt() / 2.0;
217
218 assert_close(bounds.x, cos + 1.0);
221 assert_close(bounds.y, -2.0 + 2.0 * cos);
222 assert_close(bounds.width, 3.0 * cos + 2.0);
223 assert_close(bounds.height, 1.5 + 4.0 * cos);
224
225 for corner in [
226 Point {
227 x: rect.x,
228 y: rect.y,
229 },
230 Point {
231 x: rect.x + rect.width,
232 y: rect.y,
233 },
234 Point {
235 x: rect.x,
236 y: rect.y + rect.height,
237 },
238 Point {
239 x: rect.x + rect.width,
240 y: rect.y + rect.height,
241 },
242 ] {
243 let point = transform.apply(corner);
244 assert!(point.x >= bounds.x - EPSILON);
245 assert!(point.x <= bounds.x + bounds.width + EPSILON);
246 assert!(point.y >= bounds.y - EPSILON);
247 assert!(point.y <= bounds.y + bounds.height + EPSILON);
248 }
249 }
250
251 #[test]
252 fn is_identity_is_exact() {
253 assert!(Transform::IDENTITY.is_identity());
254 for near_identity in [
255 Transform {
256 a: 1.0 + f64::EPSILON,
257 ..Transform::IDENTITY
258 },
259 Transform {
260 b: f64::EPSILON,
261 ..Transform::IDENTITY
262 },
263 Transform {
264 c: f64::EPSILON,
265 ..Transform::IDENTITY
266 },
267 Transform {
268 d: 1.0 + f64::EPSILON,
269 ..Transform::IDENTITY
270 },
271 Transform {
272 e: f64::EPSILON,
273 ..Transform::IDENTITY
274 },
275 Transform {
276 f: f64::EPSILON,
277 ..Transform::IDENTITY
278 },
279 ] {
280 assert!(!near_identity.is_identity());
281 }
282 }
283}