1use crate::geometry::{Point, Rect};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct AffineTransform {
19 inner: glam::Affine2,
20}
21
22impl AffineTransform {
23 pub const IDENTITY: Self = Self {
24 inner: glam::Affine2::IDENTITY,
25 };
26
27 #[must_use]
29 pub fn new(a: f32, b: f32, c: f32, d: f32, tx: f32, ty: f32) -> Self {
30 Self {
31 inner: glam::Affine2::from_cols(
32 glam::Vec2::new(a, b),
33 glam::Vec2::new(c, d),
34 glam::Vec2::new(tx, ty),
35 ),
36 }
37 }
38
39 #[must_use]
40 pub fn translate(tx: f32, ty: f32) -> Self {
41 Self {
42 inner: glam::Affine2::from_translation(glam::Vec2::new(tx, ty)),
43 }
44 }
45
46 #[must_use]
47 pub fn scale(sx: f32, sy: f32) -> Self {
48 Self {
49 inner: glam::Affine2::from_scale(glam::Vec2::new(sx, sy)),
50 }
51 }
52
53 #[must_use]
54 pub fn uniform_scale(s: f32) -> Self {
55 Self::scale(s, s)
56 }
57
58 #[must_use]
60 pub fn rotate(angle: f32) -> Self {
61 Self {
62 inner: glam::Affine2::from_angle(angle),
63 }
64 }
65
66 #[must_use]
67 pub fn is_identity(&self) -> bool {
68 self.inner == glam::Affine2::IDENTITY
69 }
70
71 #[must_use]
73 pub fn is_translation_only(&self) -> bool {
74 self.inner.matrix2 == glam::Mat2::IDENTITY
75 }
76
77 #[must_use]
80 pub fn preserves_axis_alignment(&self) -> bool {
81 let m = self.inner.matrix2;
82 (m.x_axis.y == 0.0 && m.y_axis.x == 0.0) || (m.x_axis.x == 0.0 && m.y_axis.y == 0.0)
83 }
84
85 #[must_use]
87 pub fn then(&self, other: &Self) -> Self {
88 Self {
89 inner: other.inner * self.inner,
90 }
91 }
92
93 #[must_use]
95 pub fn pre_translate(&self, tx: f32, ty: f32) -> Self {
96 let t = glam::Affine2::from_translation(glam::Vec2::new(tx, ty));
97 Self {
98 inner: self.inner * t,
99 }
100 }
101
102 #[must_use]
104 pub fn pre_scale(&self, sx: f32, sy: f32) -> Self {
105 let s = glam::Affine2::from_scale(glam::Vec2::new(sx, sy));
106 Self {
107 inner: self.inner * s,
108 }
109 }
110
111 #[must_use]
114 pub fn inverse(&self) -> Option<Self> {
115 let det = self.determinant();
116 if det.abs() < f32::EPSILON {
117 return None;
118 }
119 Some(Self {
120 inner: self.inner.inverse(),
121 })
122 }
123
124 #[must_use]
125 pub fn determinant(&self) -> f32 {
126 self.inner.matrix2.determinant()
127 }
128
129 #[must_use]
130 pub fn transform_point(&self, p: Point) -> Point {
131 let result = self.inner.transform_point2(glam::Vec2::new(p.x, p.y));
132 Point::new(result.x, result.y)
133 }
134
135 pub fn transform_rect(&self, r: &Rect) -> Rect {
138 if self.preserves_axis_alignment() {
139 let p0 = self.transform_point(r.origin);
140 let p1 = self.transform_point(Point::new(r.right(), r.bottom()));
141 let min_x = p0.x.min(p1.x);
142 let min_y = p0.y.min(p1.y);
143 let max_x = p0.x.max(p1.x);
144 let max_y = p0.y.max(p1.y);
145 return Rect::from_ltrb(min_x, min_y, max_x, max_y);
146 }
147
148 let corners = [
149 self.transform_point(Point::new(r.left(), r.top())),
150 self.transform_point(Point::new(r.right(), r.top())),
151 self.transform_point(Point::new(r.right(), r.bottom())),
152 self.transform_point(Point::new(r.left(), r.bottom())),
153 ];
154
155 let min_x = corners.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
156 let min_y = corners.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
157 let max_x = corners
158 .iter()
159 .map(|p| p.x)
160 .fold(f32::NEG_INFINITY, f32::max);
161 let max_y = corners
162 .iter()
163 .map(|p| p.y)
164 .fold(f32::NEG_INFINITY, f32::max);
165
166 Rect::from_ltrb(min_x, min_y, max_x, max_y)
167 }
168
169 #[must_use]
172 pub fn as_raw(&self) -> &glam::Affine2 {
173 &self.inner
174 }
175}
176
177impl Default for AffineTransform {
178 fn default() -> Self {
179 Self::IDENTITY
180 }
181}
182
183impl std::ops::Mul for AffineTransform {
184 type Output = Self;
185 fn mul(self, rhs: Self) -> Self {
187 rhs.then(&self)
188 }
189}
190
191#[derive(Clone, Copy, Debug, PartialEq)]
200pub struct Transform3D {
201 inner: glam::Mat4,
202}
203
204impl Transform3D {
205 pub const IDENTITY: Self = Self {
206 inner: glam::Mat4::IDENTITY,
207 };
208
209 #[must_use]
210 pub fn translate(tx: f32, ty: f32, tz: f32) -> Self {
211 Self {
212 inner: glam::Mat4::from_translation(glam::Vec3::new(tx, ty, tz)),
213 }
214 }
215
216 #[must_use]
217 pub fn scale(sx: f32, sy: f32, sz: f32) -> Self {
218 Self {
219 inner: glam::Mat4::from_scale(glam::Vec3::new(sx, sy, sz)),
220 }
221 }
222
223 #[must_use]
224 pub fn rotate_x(angle: f32) -> Self {
225 Self {
226 inner: glam::Mat4::from_rotation_x(angle),
227 }
228 }
229
230 #[must_use]
231 pub fn rotate_y(angle: f32) -> Self {
232 Self {
233 inner: glam::Mat4::from_rotation_y(angle),
234 }
235 }
236
237 #[must_use]
239 pub fn rotate_z(angle: f32) -> Self {
240 Self {
241 inner: glam::Mat4::from_rotation_z(angle),
242 }
243 }
244
245 #[must_use]
248 pub fn perspective(depth: f32) -> Self {
249 if depth == 0.0 {
250 return Self::IDENTITY;
251 }
252 let mut m = glam::Mat4::IDENTITY;
253 *m.col_mut(2) = glam::Vec4::new(0.0, 0.0, 1.0, -1.0 / depth);
255 Self { inner: m }
256 }
257
258 #[must_use]
259 pub fn is_identity(&self) -> bool {
260 self.inner == glam::Mat4::IDENTITY
261 }
262
263 #[must_use]
267 pub fn is_2d(&self) -> bool {
268 let m = self.inner;
269 let c = |col: usize, row: usize| m.col(col)[row];
270 c(0, 2) == 0.0
271 && c(1, 2) == 0.0
272 && c(2, 0) == 0.0
273 && c(2, 1) == 0.0
274 && c(2, 2) == 1.0
275 && c(2, 3) == 0.0
276 && c(3, 2) == 0.0
277 && c(0, 3) == 0.0
278 && c(1, 3) == 0.0
279 && c(3, 3) == 1.0
280 }
281
282 #[must_use]
285 pub fn to_affine(&self) -> AffineTransform {
286 let m = self.inner;
287 AffineTransform::new(
288 m.col(0)[0],
289 m.col(0)[1],
290 m.col(1)[0],
291 m.col(1)[1],
292 m.col(3)[0],
293 m.col(3)[1],
294 )
295 }
296
297 #[must_use]
299 pub fn from_affine(t: &AffineTransform) -> Self {
300 let a = t.inner;
301 let m2 = a.matrix2;
302 let tr = a.translation;
303 Self {
304 inner: glam::Mat4::from_cols(
305 glam::Vec4::new(m2.x_axis.x, m2.x_axis.y, 0.0, 0.0),
306 glam::Vec4::new(m2.y_axis.x, m2.y_axis.y, 0.0, 0.0),
307 glam::Vec4::new(0.0, 0.0, 1.0, 0.0),
308 glam::Vec4::new(tr.x, tr.y, 0.0, 1.0),
309 ),
310 }
311 }
312
313 #[must_use]
314 pub fn then(&self, other: &Self) -> Self {
315 Self {
316 inner: other.inner * self.inner,
317 }
318 }
319
320 #[must_use]
322 pub fn inverse(&self) -> Option<Self> {
323 let det = self.inner.determinant();
324 if det.abs() < f32::EPSILON {
325 return None;
326 }
327 Some(Self {
328 inner: self.inner.inverse(),
329 })
330 }
331
332 #[must_use]
333 pub fn determinant(&self) -> f32 {
334 self.inner.determinant()
335 }
336
337 #[must_use]
341 pub fn transform_point(&self, p: Point) -> Point {
342 let v = self.inner * glam::Vec4::new(p.x, p.y, 0.0, 1.0);
343 if v.w.abs() < f32::EPSILON {
344 return Point::new(v.x, v.y);
345 }
346 Point::new(v.x / v.w, v.y / v.w)
347 }
348
349 #[must_use]
352 pub fn is_back_face_visible(&self) -> bool {
353 let m = self.inner;
357 let col0 = glam::Vec3::new(m.col(0)[0], m.col(0)[1], m.col(0)[2]);
358 let col1 = glam::Vec3::new(m.col(1)[0], m.col(1)[1], m.col(1)[2]);
359 let col2 = glam::Vec3::new(m.col(2)[0], m.col(2)[1], m.col(2)[2]);
360 col0.cross(col1).dot(col2) < 0.0
361 }
362
363 #[must_use]
366 pub fn as_raw(&self) -> &glam::Mat4 {
367 &self.inner
368 }
369}
370
371impl Default for Transform3D {
372 fn default() -> Self {
373 Self::IDENTITY
374 }
375}
376
377impl std::ops::Mul for Transform3D {
378 type Output = Self;
379 fn mul(self, rhs: Self) -> Self {
380 rhs.then(&self)
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use std::f32::consts::FRAC_PI_2;
388
389 fn approx_eq(a: f32, b: f32) -> bool {
390 (a - b).abs() < 1e-4
391 }
392
393 fn point_approx_eq(a: Point, b: Point) -> bool {
394 approx_eq(a.x, b.x) && approx_eq(a.y, b.y)
395 }
396
397 #[test]
398 fn identity_does_nothing() {
399 let p = Point::new(42.0, 17.0);
400 assert_eq!(AffineTransform::IDENTITY.transform_point(p), p);
401 }
402
403 #[test]
404 fn translate_moves_point() {
405 let t = AffineTransform::translate(10.0, 20.0);
406 assert_eq!(t.transform_point(Point::ZERO), Point::new(10.0, 20.0));
407 }
408
409 #[test]
410 fn scale_multiplies() {
411 let t = AffineTransform::scale(2.0, 3.0);
412 assert_eq!(
413 t.transform_point(Point::new(5.0, 10.0)),
414 Point::new(10.0, 30.0)
415 );
416 }
417
418 #[test]
419 fn rotate_90_degrees() {
420 let t = AffineTransform::rotate(FRAC_PI_2);
421 let result = t.transform_point(Point::new(1.0, 0.0));
422 assert!(point_approx_eq(result, Point::new(0.0, 1.0)));
423 }
424
425 #[test]
426 fn compose_scale_then_translate() {
427 let s = AffineTransform::scale(2.0, 2.0);
428 let t = AffineTransform::translate(10.0, 0.0);
429 let combined = s.then(&t);
430 let p = combined.transform_point(Point::new(5.0, 0.0));
431 assert_eq!(p, Point::new(20.0, 0.0));
432 }
433
434 #[test]
435 fn inverse_roundtrip() {
436 let t = AffineTransform::translate(10.0, 20.0)
437 .then(&AffineTransform::scale(2.0, 3.0))
438 .then(&AffineTransform::rotate(0.5));
439
440 let inv = t.inverse().unwrap();
441 let p = Point::new(42.0, 17.0);
442 let roundtrip = inv.transform_point(t.transform_point(p));
443 assert!(point_approx_eq(roundtrip, p));
444 }
445
446 #[test]
447 fn singular_matrix_no_inverse() {
448 let t = AffineTransform::scale(0.0, 1.0);
449 assert!(t.inverse().is_none());
450 }
451
452 #[test]
453 fn transform_rect_with_translation() {
454 let t = AffineTransform::translate(10.0, 10.0);
455 let r = Rect::new(0.0, 0.0, 50.0, 50.0);
456 assert_eq!(t.transform_rect(&r), Rect::new(10.0, 10.0, 50.0, 50.0));
457 }
458
459 #[test]
460 fn transform_rect_with_scale() {
461 let t = AffineTransform::scale(2.0, 0.5);
462 let r = Rect::new(10.0, 10.0, 20.0, 40.0);
463 let result = t.transform_rect(&r);
464 assert_eq!(result, Rect::new(20.0, 5.0, 40.0, 20.0));
465 }
466
467 #[test]
468 fn mul_operator() {
469 let a = AffineTransform::translate(5.0, 0.0);
470 let b = AffineTransform::scale(2.0, 2.0);
471 let p = (a * b).transform_point(Point::new(10.0, 0.0));
472 assert_eq!(p, Point::new(25.0, 0.0));
473 }
474
475 #[test]
476 fn is_translation_only() {
477 assert!(AffineTransform::translate(1.0, 2.0).is_translation_only());
478 assert!(!AffineTransform::scale(2.0, 1.0).is_translation_only());
479 assert!(!AffineTransform::rotate(0.1).is_translation_only());
480 }
481
482 #[test]
483 fn pre_translate() {
484 let t = AffineTransform::scale(2.0, 2.0).pre_translate(5.0, 0.0);
485 let p = t.transform_point(Point::ZERO);
486 assert_eq!(p, Point::new(10.0, 0.0));
487 }
488
489 #[test]
492 fn identity_3d() {
493 let p = Point::new(42.0, 17.0);
494 assert_eq!(Transform3D::IDENTITY.transform_point(p), p);
495 }
496
497 #[test]
498 fn translate_3d() {
499 let t = Transform3D::translate(10.0, 20.0, 0.0);
500 assert_eq!(t.transform_point(Point::ZERO), Point::new(10.0, 20.0));
501 }
502
503 #[test]
504 fn rotate_z_matches_2d() {
505 let t2d = AffineTransform::rotate(FRAC_PI_2);
506 let t3d = Transform3D::rotate_z(FRAC_PI_2);
507 let p = Point::new(1.0, 0.0);
508 assert!(point_approx_eq(
509 t2d.transform_point(p),
510 t3d.transform_point(p)
511 ));
512 }
513
514 #[test]
515 fn affine_roundtrip() {
516 let t2d = AffineTransform::new(2.0, 0.5, -0.3, 1.5, 10.0, 20.0);
517 let t3d = Transform3D::from_affine(&t2d);
518 assert!(t3d.is_2d());
519
520 let back = t3d.to_affine();
521 assert_eq!(back, t2d);
522 }
523
524 #[test]
525 fn perspective_leaves_z0_unchanged() {
526 let t = Transform3D::perspective(100.0);
527 let p = Point::new(50.0, 50.0);
528 assert_eq!(t.transform_point(p), p);
529 }
530
531 #[test]
532 fn is_2d_checks() {
533 assert!(Transform3D::IDENTITY.is_2d());
534 assert!(Transform3D::translate(1.0, 2.0, 0.0).is_2d());
535 assert!(!Transform3D::translate(0.0, 0.0, 5.0).is_2d());
536 assert!(!Transform3D::rotate_x(0.1).is_2d());
537 assert!(!Transform3D::perspective(100.0).is_2d());
538 }
539
540 #[test]
541 fn back_face_detection() {
542 assert!(!Transform3D::IDENTITY.is_back_face_visible());
543 assert!(Transform3D::scale(-1.0, 1.0, 1.0).is_back_face_visible());
546 assert!(!Transform3D::scale(1.0, 1.0, 1.0).is_back_face_visible());
547 }
548
549 #[test]
550 fn inverse_3d_roundtrip() {
551 let t = Transform3D::translate(5.0, 10.0, 0.0)
552 .then(&Transform3D::rotate_z(0.7))
553 .then(&Transform3D::scale(2.0, 3.0, 1.0));
554
555 let inv = t.inverse().unwrap();
556 let p = Point::new(42.0, 17.0);
557 let roundtrip = inv.transform_point(t.transform_point(p));
558 assert!(point_approx_eq(roundtrip, p));
559 }
560}