1#[derive(Clone, Copy, Debug, Default, PartialEq)]
2pub struct Vec2 {
3 pub x: f32,
4 pub y: f32,
5}
6
7impl Vec2 {
8 pub const ZERO: Vec2 = Vec2 { x: 0.0, y: 0.0 };
9}
10
11impl std::ops::Add for Vec2 {
12 type Output = Vec2;
13 fn add(self, other: Vec2) -> Vec2 {
14 Vec2 {
15 x: self.x + other.x,
16 y: self.y + other.y,
17 }
18 }
19}
20
21impl std::ops::Sub for Vec2 {
22 type Output = Vec2;
23 fn sub(self, other: Vec2) -> Vec2 {
24 Vec2 {
25 x: self.x - other.x,
26 y: self.y - other.y,
27 }
28 }
29}
30
31impl std::ops::Neg for Vec2 {
32 type Output = Vec2;
33 fn neg(self) -> Vec2 {
34 Vec2 {
35 x: -self.x,
36 y: -self.y,
37 }
38 }
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq)]
42pub struct Size {
43 pub width: f32,
44 pub height: f32,
45}
46
47#[derive(Clone, Copy, Debug, Default, PartialEq)]
48pub struct Rect {
49 pub x: f32,
50 pub y: f32,
51 pub w: f32,
52 pub h: f32,
53}
54
55impl Rect {
56 pub fn contains(&self, p: Vec2) -> bool {
57 p.x >= self.x && p.x <= self.x + self.w && p.y >= self.y && p.y <= self.y + self.h
58 }
59}
60
61#[derive(Clone, Copy, Debug, Default, PartialEq)]
62pub struct Transform {
63 pub translate_x: f32,
64 pub translate_y: f32,
65 pub scale_x: f32,
66 pub scale_y: f32,
67 pub rotate: f32, pub shear_x: f32,
70 pub shear_y: f32,
72 pub origin_x: f32,
84 pub origin_y: f32,
85 pub perspective: [f32; 3],
94}
95
96impl Transform {
97 pub fn identity() -> Self {
98 Self {
99 translate_x: 0.0,
100 translate_y: 0.0,
101 scale_x: 1.0,
102 scale_y: 1.0,
103 rotate: 0.0,
104 shear_x: 0.0,
105 shear_y: 0.0,
106 origin_x: 0.5,
107 origin_y: 0.5,
108 perspective: [0.0, 0.0, 1.0],
109 }
110 }
111
112 pub fn translate(x: f32, y: f32) -> Self {
113 Self {
114 translate_x: x,
115 translate_y: y,
116 scale_x: 1.0,
117 scale_y: 1.0,
118 rotate: 0.0,
119 shear_x: 0.0,
120 shear_y: 0.0,
121 origin_x: 0.5,
122 origin_y: 0.5,
123 perspective: [0.0, 0.0, 1.0],
124 }
125 }
126
127 pub fn linear(&self) -> [f32; 4] {
130 let c = self.rotate.cos();
131 let s = self.rotate.sin();
132 let hsx = self.scale_x;
133 let hsy = self.scale_y;
134 let k00 = hsx;
135 let k01 = self.shear_x * hsy;
136 let k10 = self.shear_y * hsx;
137 let k11 = hsy;
138 [
139 c * k00 - s * k10,
140 c * k01 - s * k11,
141 s * k00 + c * k10,
142 s * k01 + c * k11,
143 ]
144 }
145
146 pub fn inverse_linear(&self) -> Option<[f32; 4]> {
148 let m = self.linear();
149 let det = m[0] * m[3] - m[1] * m[2];
150 if det.abs() < 1e-12 {
151 return None;
152 }
153 Some([m[3] / det, -m[1] / det, -m[2] / det, m[0] / det])
154 }
155
156 pub fn linear_is_identity(&self) -> bool {
158 self.scale_x == 1.0
159 && self.scale_y == 1.0
160 && self.rotate == 0.0
161 && self.shear_x == 0.0
162 && self.shear_y == 0.0
163 }
164
165 pub fn has_perspective(&self) -> bool {
167 self.perspective != [0.0, 0.0, 1.0]
168 }
169
170 pub fn projective_matrix(&self) -> [f32; 9] {
179 let m = self.linear();
180 [
181 m[0],
182 m[1],
183 self.translate_x,
184 m[2],
185 m[3],
186 self.translate_y,
187 self.perspective[0],
188 self.perspective[1],
189 self.perspective[2],
190 ]
191 }
192
193 pub fn from_projective_rows(row0: [f32; 3], row1: [f32; 3], perspective: [f32; 3]) -> Self {
201 let (sx, sy, rot, hx, hy) = decompose_linear([row0[0], row0[1], row1[0], row1[1]])
202 .unwrap_or((1.0, 1.0, 0.0, 0.0, 0.0));
203 Self {
204 translate_x: row0[2],
205 translate_y: row1[2],
206 scale_x: sx,
207 scale_y: sy,
208 rotate: rot,
209 shear_x: hx,
210 shear_y: hy,
211 origin_x: 0.5,
212 origin_y: 0.5,
213 perspective,
214 }
215 }
216
217 pub fn apply_projective(&self, p: Vec2) -> Vec2 {
222 let m = self.linear();
223 let x = m[0] * p.x + m[1] * p.y + self.translate_x;
224 let y = m[2] * p.x + m[3] * p.y + self.translate_y;
225 let w = self.projective_w(p);
226 Vec2 { x: x / w, y: y / w }
227 }
228
229 pub fn projective_w(&self, p: Vec2) -> f32 {
231 let w = self.perspective[0] * p.x + self.perspective[1] * p.y + self.perspective[2];
232 if w.abs() < 1e-6 {
233 if w < 0.0 { -1e-6 } else { 1e-6 }
234 } else {
235 w
236 }
237 }
238
239 pub fn compose_projective(outer: &[f32; 9], inner: &[f32; 9]) -> [f32; 9] {
245 let mut c = [0.0f32; 9];
246 for i in 0..3 {
247 for j in 0..3 {
248 c[i * 3 + j] = outer[i * 3] * inner[j]
249 + outer[i * 3 + 1] * inner[3 + j]
250 + outer[i * 3 + 2] * inner[6 + j];
251 }
252 }
253 c
254 }
255
256 pub fn project_rect(&self, r: &Rect) -> Rect {
259 let corners = [
260 Vec2 { x: r.x, y: r.y },
261 Vec2 {
262 x: r.x + r.w,
263 y: r.y,
264 },
265 Vec2 {
266 x: r.x + r.w,
267 y: r.y + r.h,
268 },
269 Vec2 {
270 x: r.x,
271 y: r.y + r.h,
272 },
273 ];
274 let mut min_x = f32::MAX;
275 let mut min_y = f32::MAX;
276 let mut max_x = f32::MIN;
277 let mut max_y = f32::MIN;
278 for c in corners {
279 let p = self.apply_projective(c);
280 min_x = min_x.min(p.x);
281 min_y = min_y.min(p.y);
282 max_x = max_x.max(p.x);
283 max_y = max_y.max(p.y);
284 }
285 Rect {
286 x: min_x,
287 y: min_y,
288 w: (max_x - min_x).max(0.0),
289 h: (max_y - min_y).max(0.0),
290 }
291 }
292
293 pub fn apply_to_point(&self, p: Vec2) -> Vec2 {
294 let ox = self.origin_x;
296 let oy = self.origin_y;
297 let m = self.linear();
298 let x = p.x - ox;
299 let y = p.y - oy;
300
301 Vec2 {
302 x: m[0] * x + m[1] * y + ox + self.translate_x,
303 y: m[2] * x + m[3] * y + oy + self.translate_y,
304 }
305 }
306
307 pub fn pivot_for_rect(&self, r: &Rect) -> Vec2 {
309 Vec2 {
310 x: r.x + r.w * self.origin_x,
311 y: r.y + r.h * self.origin_y,
312 }
313 }
314
315 pub fn apply_to_point_with_pivot(&self, p: Vec2, pivot: Vec2) -> Vec2 {
319 let m = self.linear();
320 let x = p.x - pivot.x;
321 let y = p.y - pivot.y;
322 Vec2 {
323 x: m[0] * x + m[1] * y + pivot.x + self.translate_x,
324 y: m[2] * x + m[3] * y + pivot.y + self.translate_y,
325 }
326 }
327
328 pub fn apply_to_point_in_rect(&self, p: Vec2, r: &Rect) -> Vec2 {
331 self.apply_to_point_with_pivot(p, self.pivot_for_rect(r))
332 }
333
334 pub fn apply_to_rect(&self, r: Rect) -> Rect {
335 let ox = r.x + r.w * self.origin_x;
336 let oy = r.y + r.h * self.origin_y;
337 let m = self.linear();
338 let corners = [
339 Vec2 { x: r.x, y: r.y },
340 Vec2 {
341 x: r.x + r.w,
342 y: r.y,
343 },
344 Vec2 {
345 x: r.x,
346 y: r.y + r.h,
347 },
348 Vec2 {
349 x: r.x + r.w,
350 y: r.y + r.h,
351 },
352 ];
353 let mut min_x = f32::MAX;
354 let mut min_y = f32::MAX;
355 let mut max_x = f32::MIN;
356 let mut max_y = f32::MIN;
357 for c in corners {
358 let x = c.x - ox;
359 let y = c.y - oy;
360 let tx = m[0] * x + m[1] * y + ox + self.translate_x;
361 let ty = m[2] * x + m[3] * y + oy + self.translate_y;
362 min_x = min_x.min(tx);
363 min_y = min_y.min(ty);
364 max_x = max_x.max(tx);
365 max_y = max_y.max(ty);
366 }
367 Rect {
368 x: min_x,
369 y: min_y,
370 w: max_x - min_x,
371 h: max_y - min_y,
372 }
373 }
374
375 pub fn combine(&self, other: &Transform) -> Transform {
401 debug_assert!(
402 !self.has_perspective() && !other.has_perspective(),
403 "Transform::combine is affine-only; compose projective rows with compose_projective"
404 );
405 let a = self.linear();
406 let b = other.linear();
407 let m = [
408 a[0] * b[0] + a[1] * b[2],
409 a[0] * b[1] + a[1] * b[3],
410 a[2] * b[0] + a[3] * b[2],
411 a[2] * b[1] + a[3] * b[3],
412 ];
413 let translate_x = a[0] * other.translate_x + a[1] * other.translate_y + self.translate_x;
414 let translate_y = a[2] * other.translate_x + a[3] * other.translate_y + self.translate_y;
415
416 let (scale_x, scale_y, rotate, shear_x, shear_y) =
417 decompose_linear(m).unwrap_or((1.0, 1.0, 0.0, 0.0, 0.0));
418
419 Transform {
420 translate_x,
421 translate_y,
422 scale_x,
423 scale_y,
424 rotate,
425 shear_x,
426 shear_y,
427 origin_x: self.origin_x,
428 origin_y: self.origin_y,
429 perspective: [0.0, 0.0, 1.0],
431 }
432 }
433}
434
435fn decompose_linear(m: [f32; 4]) -> Option<(f32, f32, f32, f32, f32)> {
442 let (mut a, mut b, c, d) = (m[0], m[1], m[2], m[3]);
443 let mut angle_sign = 1.0;
444 if a * d - b * c < 0.0 {
445 a = -a;
446 b = -b;
447 angle_sign = -1.0;
448 }
449 let e = a * a + c * c;
450 let f = a * b + c * d;
451 let g = b * b + d * d;
452 let det_p = (e * g - f * f).max(0.0);
453 let s = (e + g + 2.0 * det_p.sqrt()).sqrt();
454 if !(s > 1e-12) {
455 return None;
456 }
457 let root_det = det_p.sqrt();
458 let k00 = (e + root_det) / s;
459 let k01 = f / s;
460 let k10 = f / s;
461 let k11 = (g + root_det) / s;
462 let det_k = (k00 * k11 - k01 * k10).max(1e-24);
463 let r00 = (a * k11 - b * k10) / det_k;
464 let r10 = (c * k11 - d * k10) / det_k;
465 let rotate = r10.atan2(r00);
466 let (sx, sy) = (k00, k11);
467 if sx.abs() < 1e-12 || sy.abs() < 1e-12 {
468 return None;
469 }
470 if angle_sign < 0.0 {
471 return Some((-sx, sy, -rotate, -(k01 / sy), -(k10 / sx)));
472 }
473 Some((sx, sy, rotate, k01 / sy, k10 / sx))
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 fn approx(a: f32, b: f32) -> bool {
481 (a - b).abs() < 1e-3
482 }
483
484 fn transform(tx: f32, ty: f32, sx: f32, sy: f32, rot: f32, hx: f32, hy: f32) -> Transform {
485 Transform {
486 translate_x: tx,
487 translate_y: ty,
488 scale_x: sx,
489 scale_y: sy,
490 rotate: rot,
491 shear_x: hx,
492 shear_y: hy,
493 origin_x: 0.0,
494 origin_y: 0.0,
495 perspective: [0.0, 0.0, 1.0],
496 }
497 }
498
499 #[test]
500 fn shear_free_matches_legacy_formula() {
501 let t = transform(5.0, -3.0, 2.0, 0.5, 0.7, 0.0, 0.0);
502 let p = Vec2 { x: 11.0, y: 7.0 };
503 let got = t.apply_to_point(p);
504 let (c, s) = (0.7f32.cos(), 0.7f32.sin());
505 let x = (p.x) * 2.0;
506 let y = (p.y) * 0.5;
507 assert!(approx(got.x, x * c - y * s + 5.0));
508 assert!(approx(got.y, x * s + y * c - 3.0));
509 }
510
511 #[test]
512 fn shear_order_is_scale_then_shear_then_rotate() {
513 let t = transform(0.0, 0.0, 2.0, 3.0, 0.0, 1.0, 0.0);
514 let got = t.apply_to_point(Vec2 { x: 1.0, y: 1.0 });
515 assert!(approx(got.x, 2.0 + 3.0));
516 assert!(approx(got.y, 3.0));
517 }
518
519 #[test]
520 fn combine_round_trips_through_apply() {
521 let cases = [
522 (
523 transform(3.0, 4.0, 1.0, 1.0, 0.0, 0.0, 0.0),
524 transform(0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 0.0),
525 ),
526 (
527 transform(1.0, 2.0, 1.5, 0.5, 0.6, 0.0, 0.0),
528 transform(-2.0, 1.0, 0.7, 1.3, -0.4, 0.0, 0.0),
529 ),
530 (
531 transform(0.0, 0.0, 2.0, 0.5, 0.9, 0.0, 0.0),
532 transform(4.0, -1.0, 1.0, 1.0, 0.3, 0.0, 0.0),
533 ),
534 (
535 transform(2.0, 0.0, 1.0, 1.0, 0.2, 0.8, -0.3),
536 transform(0.0, 5.0, 1.2, 0.9, -0.5, 0.4, 0.1),
537 ),
538 (
539 transform(0.0, 0.0, -1.0, 1.0, 0.0, 0.0, 0.0),
540 transform(7.0, 7.0, 1.0, 1.0, 1.1, 0.2, 0.0),
541 ),
542 ];
543 let points = [
544 Vec2 { x: 0.0, y: 0.0 },
545 Vec2 { x: 10.0, y: -4.0 },
546 Vec2 { x: -3.5, y: 8.25 },
547 Vec2 { x: 100.0, y: 200.0 },
548 ];
549 for (a, b) in cases {
550 let combined = a.combine(&b);
551 for p in points {
552 let expect = a.apply_to_point(b.apply_to_point(p));
553 let got = combined.apply_to_point(p);
554 assert!(
555 approx(got.x, expect.x) && approx(got.y, expect.y),
556 "combine mismatch: {a:?} then {b:?} at {p:?}: got {got:?}, want {expect:?}"
557 );
558 }
559 }
560 }
561
562 #[test]
563 fn inverse_linear_round_trips() {
564 let t = transform(3.0, -2.0, 1.5, 0.75, 0.8, 0.6, -0.2);
565 let m = t.linear();
566 let n = t.inverse_linear().expect("invertible");
567 let id = [
568 n[0] * m[0] + n[1] * m[2],
569 n[0] * m[1] + n[1] * m[3],
570 n[2] * m[0] + n[3] * m[2],
571 n[2] * m[1] + n[3] * m[3],
572 ];
573 assert!(approx(id[0], 1.0) && approx(id[3], 1.0));
574 assert!(approx(id[1], 0.0) && approx(id[2], 0.0));
575 }
576}
577
578#[cfg(test)]
579mod projective_tests {
580 use super::*;
581
582 fn approx(a: f32, b: f32) -> bool {
583 (a - b).abs() < 1e-3
584 }
585
586 #[test]
587 fn identity_row_is_affine_noop() {
588 let t = Transform::identity();
589 assert!(!t.has_perspective());
590 let p = Vec2 { x: 13.0, y: -7.0 };
591 let q = t.apply_projective(p);
592 assert!(approx(q.x, p.x) && approx(q.y, p.y));
593 assert!(approx(t.projective_w(p), 1.0));
594 }
595
596 #[test]
597 fn tilt_about_center_matches_hand_computation() {
598 let (cx, cy, f) = (100.0f32, 100.0f32, 1000.0f32);
601 let th = 0.5f32;
602 let (s, c) = (th.sin(), th.cos());
603 let t = Transform::from_projective_rows(
604 [1.0, 0.0, 0.0],
605 [0.0, c, cy * (1.0 - c)],
606 [0.0, s / f, 1.0 - cy * s / f],
607 );
608 assert!(t.has_perspective());
609 let q = t.apply_projective(Vec2 { x: cx, y: cy });
611 assert!(approx(q.x, cx) && approx(q.y, cy));
612 assert!(
613 t.projective_w(Vec2 {
614 x: cx,
615 y: cy - 50.0
616 }) < 1.0
617 );
618 assert!(
619 t.projective_w(Vec2 {
620 x: cx,
621 y: cy + 50.0
622 }) > 1.0
623 );
624 }
625
626 #[test]
627 fn from_projective_rows_round_trips_affine_part() {
628 let t = Transform::from_projective_rows(
629 [2.0, 0.5, 7.0],
630 [-0.5, 3.0, -2.0],
631 [0.001, -0.002, 1.0],
632 );
633 let m = t.projective_matrix();
635 assert!(approx(m[0], 2.0) && approx(m[1], 0.5) && approx(m[2], 7.0));
636 assert!(approx(m[3], -0.5) && approx(m[4], 3.0) && approx(m[5], -2.0));
637 assert!(approx(m[6], 0.001) && approx(m[7], -0.002) && approx(m[8], 1.0));
638 }
639
640 #[test]
641 fn compose_projective_matches_sequential_apply() {
642 let outer = Transform {
643 translate_x: 5.0,
644 ..Transform::identity()
645 };
646 let inner =
647 Transform::from_projective_rows([1.0, 0.0, 0.0], [0.0, 0.9, 10.0], [0.0, 0.001, 1.0]);
648 let c =
649 Transform::compose_projective(&outer.projective_matrix(), &inner.projective_matrix());
650 for p in [Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 40.0, y: -25.0 }] {
651 let q1 = inner.apply_projective(p);
653 let m = outer.projective_matrix();
654 let q1 = Vec2 {
655 x: m[0] * q1.x + m[1] * q1.y + m[2],
656 y: m[3] * q1.x + m[4] * q1.y + m[5],
657 };
658 let w = c[6] * p.x + c[7] * p.y + c[8];
659 let q2 = Vec2 {
660 x: (c[0] * p.x + c[1] * p.y + c[2]) / w,
661 y: (c[3] * p.x + c[4] * p.y + c[5]) / w,
662 };
663 assert!(
664 approx(q1.x, q2.x) && approx(q1.y, q2.y),
665 "mismatch at {p:?}: {q1:?} vs {q2:?}"
666 );
667 }
668 }
669
670 #[test]
671 fn project_rect_bounds_projected_corners() {
672 let t =
673 Transform::from_projective_rows([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.002, 0.0, 1.0]);
674 let r = Rect {
675 x: 0.0,
676 y: 0.0,
677 w: 100.0,
678 h: 100.0,
679 };
680 let b = t.project_rect(&r);
681 assert!(approx(b.x, 0.0));
684 assert!(approx(b.w, 100.0 / 1.2));
685 assert!(approx(b.y, 0.0) && approx(b.h, 100.0));
686 }
687}