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,
73 pub origin_y: f32,
74 pub perspective: [f32; 3],
83}
84
85impl Transform {
86 pub fn identity() -> Self {
87 Self {
88 translate_x: 0.0,
89 translate_y: 0.0,
90 scale_x: 1.0,
91 scale_y: 1.0,
92 rotate: 0.0,
93 shear_x: 0.0,
94 shear_y: 0.0,
95 origin_x: 0.5,
96 origin_y: 0.5,
97 perspective: [0.0, 0.0, 1.0],
98 }
99 }
100
101 pub fn translate(x: f32, y: f32) -> Self {
102 Self {
103 translate_x: x,
104 translate_y: y,
105 scale_x: 1.0,
106 scale_y: 1.0,
107 rotate: 0.0,
108 shear_x: 0.0,
109 shear_y: 0.0,
110 origin_x: 0.5,
111 origin_y: 0.5,
112 perspective: [0.0, 0.0, 1.0],
113 }
114 }
115
116 pub fn linear(&self) -> [f32; 4] {
119 let c = self.rotate.cos();
120 let s = self.rotate.sin();
121 let hsx = self.scale_x;
122 let hsy = self.scale_y;
123 let k00 = hsx;
124 let k01 = self.shear_x * hsy;
125 let k10 = self.shear_y * hsx;
126 let k11 = hsy;
127 [
128 c * k00 - s * k10,
129 c * k01 - s * k11,
130 s * k00 + c * k10,
131 s * k01 + c * k11,
132 ]
133 }
134
135 pub fn inverse_linear(&self) -> Option<[f32; 4]> {
137 let m = self.linear();
138 let det = m[0] * m[3] - m[1] * m[2];
139 if det.abs() < 1e-12 {
140 return None;
141 }
142 Some([m[3] / det, -m[1] / det, -m[2] / det, m[0] / det])
143 }
144
145 pub fn linear_is_identity(&self) -> bool {
147 self.scale_x == 1.0
148 && self.scale_y == 1.0
149 && self.rotate == 0.0
150 && self.shear_x == 0.0
151 && self.shear_y == 0.0
152 }
153
154 pub fn has_perspective(&self) -> bool {
156 self.perspective != [0.0, 0.0, 1.0]
157 }
158
159 pub fn projective_matrix(&self) -> [f32; 9] {
168 let m = self.linear();
169 [
170 m[0],
171 m[1],
172 self.translate_x,
173 m[2],
174 m[3],
175 self.translate_y,
176 self.perspective[0],
177 self.perspective[1],
178 self.perspective[2],
179 ]
180 }
181
182 pub fn from_projective_rows(row0: [f32; 3], row1: [f32; 3], perspective: [f32; 3]) -> Self {
190 let (sx, sy, rot, hx, hy) = decompose_linear([row0[0], row0[1], row1[0], row1[1]])
191 .unwrap_or((1.0, 1.0, 0.0, 0.0, 0.0));
192 Self {
193 translate_x: row0[2],
194 translate_y: row1[2],
195 scale_x: sx,
196 scale_y: sy,
197 rotate: rot,
198 shear_x: hx,
199 shear_y: hy,
200 origin_x: 0.5,
201 origin_y: 0.5,
202 perspective,
203 }
204 }
205
206 pub fn apply_projective(&self, p: Vec2) -> Vec2 {
211 let m = self.linear();
212 let x = m[0] * p.x + m[1] * p.y + self.translate_x;
213 let y = m[2] * p.x + m[3] * p.y + self.translate_y;
214 let w = self.projective_w(p);
215 Vec2 { x: x / w, y: y / w }
216 }
217
218 pub fn projective_w(&self, p: Vec2) -> f32 {
220 let w = self.perspective[0] * p.x + self.perspective[1] * p.y + self.perspective[2];
221 if w.abs() < 1e-6 {
222 if w < 0.0 { -1e-6 } else { 1e-6 }
223 } else {
224 w
225 }
226 }
227
228 pub fn compose_projective(outer: &[f32; 9], inner: &[f32; 9]) -> [f32; 9] {
234 let mut c = [0.0f32; 9];
235 for i in 0..3 {
236 for j in 0..3 {
237 c[i * 3 + j] = outer[i * 3] * inner[j]
238 + outer[i * 3 + 1] * inner[3 + j]
239 + outer[i * 3 + 2] * inner[6 + j];
240 }
241 }
242 c
243 }
244
245 pub fn project_rect(&self, r: &Rect) -> Rect {
248 let corners = [
249 Vec2 { x: r.x, y: r.y },
250 Vec2 {
251 x: r.x + r.w,
252 y: r.y,
253 },
254 Vec2 {
255 x: r.x + r.w,
256 y: r.y + r.h,
257 },
258 Vec2 {
259 x: r.x,
260 y: r.y + r.h,
261 },
262 ];
263 let mut min_x = f32::MAX;
264 let mut min_y = f32::MAX;
265 let mut max_x = f32::MIN;
266 let mut max_y = f32::MIN;
267 for c in corners {
268 let p = self.apply_projective(c);
269 min_x = min_x.min(p.x);
270 min_y = min_y.min(p.y);
271 max_x = max_x.max(p.x);
272 max_y = max_y.max(p.y);
273 }
274 Rect {
275 x: min_x,
276 y: min_y,
277 w: (max_x - min_x).max(0.0),
278 h: (max_y - min_y).max(0.0),
279 }
280 }
281
282 pub fn apply_to_point(&self, p: Vec2) -> Vec2 {
283 let ox = self.origin_x;
284 let oy = self.origin_y;
285 let m = self.linear();
286 let x = p.x - ox;
287 let y = p.y - oy;
288
289 Vec2 {
290 x: m[0] * x + m[1] * y + ox + self.translate_x,
291 y: m[2] * x + m[3] * y + oy + self.translate_y,
292 }
293 }
294
295 pub fn apply_to_rect(&self, r: Rect) -> Rect {
296 let ox = r.x + r.w * self.origin_x;
297 let oy = r.y + r.h * self.origin_y;
298 let m = self.linear();
299 let corners = [
300 Vec2 { x: r.x, y: r.y },
301 Vec2 {
302 x: r.x + r.w,
303 y: r.y,
304 },
305 Vec2 {
306 x: r.x,
307 y: r.y + r.h,
308 },
309 Vec2 {
310 x: r.x + r.w,
311 y: r.y + r.h,
312 },
313 ];
314 let mut min_x = f32::MAX;
315 let mut min_y = f32::MAX;
316 let mut max_x = f32::MIN;
317 let mut max_y = f32::MIN;
318 for c in corners {
319 let x = c.x - ox;
320 let y = c.y - oy;
321 let tx = m[0] * x + m[1] * y + ox + self.translate_x;
322 let ty = m[2] * x + m[3] * y + oy + self.translate_y;
323 min_x = min_x.min(tx);
324 min_y = min_y.min(ty);
325 max_x = max_x.max(tx);
326 max_y = max_y.max(ty);
327 }
328 Rect {
329 x: min_x,
330 y: min_y,
331 w: max_x - min_x,
332 h: max_y - min_y,
333 }
334 }
335
336 pub fn combine(&self, other: &Transform) -> Transform {
354 debug_assert!(
355 !self.has_perspective() && !other.has_perspective(),
356 "Transform::combine is affine-only; compose projective rows with compose_projective"
357 );
358 let a = self.linear();
359 let b = other.linear();
360 let m = [
361 a[0] * b[0] + a[1] * b[2],
362 a[0] * b[1] + a[1] * b[3],
363 a[2] * b[0] + a[3] * b[2],
364 a[2] * b[1] + a[3] * b[3],
365 ];
366 let translate_x = a[0] * other.translate_x + a[1] * other.translate_y + self.translate_x;
367 let translate_y = a[2] * other.translate_x + a[3] * other.translate_y + self.translate_y;
368
369 let (scale_x, scale_y, rotate, shear_x, shear_y) =
370 decompose_linear(m).unwrap_or((1.0, 1.0, 0.0, 0.0, 0.0));
371
372 Transform {
373 translate_x,
374 translate_y,
375 scale_x,
376 scale_y,
377 rotate,
378 shear_x,
379 shear_y,
380 origin_x: self.origin_x,
381 origin_y: self.origin_y,
382 perspective: [0.0, 0.0, 1.0],
384 }
385 }
386}
387
388fn decompose_linear(m: [f32; 4]) -> Option<(f32, f32, f32, f32, f32)> {
395 let (mut a, mut b, c, d) = (m[0], m[1], m[2], m[3]);
396 let mut angle_sign = 1.0;
397 if a * d - b * c < 0.0 {
398 a = -a;
399 b = -b;
400 angle_sign = -1.0;
401 }
402 let e = a * a + c * c;
403 let f = a * b + c * d;
404 let g = b * b + d * d;
405 let det_p = (e * g - f * f).max(0.0);
406 let s = (e + g + 2.0 * det_p.sqrt()).sqrt();
407 if !(s > 1e-12) {
408 return None;
409 }
410 let root_det = det_p.sqrt();
411 let k00 = (e + root_det) / s;
412 let k01 = f / s;
413 let k10 = f / s;
414 let k11 = (g + root_det) / s;
415 let det_k = (k00 * k11 - k01 * k10).max(1e-24);
416 let r00 = (a * k11 - b * k10) / det_k;
417 let r10 = (c * k11 - d * k10) / det_k;
418 let rotate = r10.atan2(r00);
419 let (sx, sy) = (k00, k11);
420 if sx.abs() < 1e-12 || sy.abs() < 1e-12 {
421 return None;
422 }
423 if angle_sign < 0.0 {
424 return Some((-sx, sy, -rotate, -(k01 / sy), -(k10 / sx)));
425 }
426 Some((sx, sy, rotate, k01 / sy, k10 / sx))
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 fn approx(a: f32, b: f32) -> bool {
434 (a - b).abs() < 1e-3
435 }
436
437 fn transform(tx: f32, ty: f32, sx: f32, sy: f32, rot: f32, hx: f32, hy: f32) -> Transform {
438 Transform {
439 translate_x: tx,
440 translate_y: ty,
441 scale_x: sx,
442 scale_y: sy,
443 rotate: rot,
444 shear_x: hx,
445 shear_y: hy,
446 origin_x: 0.0,
447 origin_y: 0.0,
448 perspective: [0.0, 0.0, 1.0],
449 }
450 }
451
452 #[test]
453 fn shear_free_matches_legacy_formula() {
454 let t = transform(5.0, -3.0, 2.0, 0.5, 0.7, 0.0, 0.0);
455 let p = Vec2 { x: 11.0, y: 7.0 };
456 let got = t.apply_to_point(p);
457 let (c, s) = (0.7f32.cos(), 0.7f32.sin());
458 let x = (p.x) * 2.0;
459 let y = (p.y) * 0.5;
460 assert!(approx(got.x, x * c - y * s + 5.0));
461 assert!(approx(got.y, x * s + y * c - 3.0));
462 }
463
464 #[test]
465 fn shear_order_is_scale_then_shear_then_rotate() {
466 let t = transform(0.0, 0.0, 2.0, 3.0, 0.0, 1.0, 0.0);
467 let got = t.apply_to_point(Vec2 { x: 1.0, y: 1.0 });
468 assert!(approx(got.x, 2.0 + 3.0));
469 assert!(approx(got.y, 3.0));
470 }
471
472 #[test]
473 fn combine_round_trips_through_apply() {
474 let cases = [
475 (
476 transform(3.0, 4.0, 1.0, 1.0, 0.0, 0.0, 0.0),
477 transform(0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 0.0),
478 ),
479 (
480 transform(1.0, 2.0, 1.5, 0.5, 0.6, 0.0, 0.0),
481 transform(-2.0, 1.0, 0.7, 1.3, -0.4, 0.0, 0.0),
482 ),
483 (
484 transform(0.0, 0.0, 2.0, 0.5, 0.9, 0.0, 0.0),
485 transform(4.0, -1.0, 1.0, 1.0, 0.3, 0.0, 0.0),
486 ),
487 (
488 transform(2.0, 0.0, 1.0, 1.0, 0.2, 0.8, -0.3),
489 transform(0.0, 5.0, 1.2, 0.9, -0.5, 0.4, 0.1),
490 ),
491 (
492 transform(0.0, 0.0, -1.0, 1.0, 0.0, 0.0, 0.0),
493 transform(7.0, 7.0, 1.0, 1.0, 1.1, 0.2, 0.0),
494 ),
495 ];
496 let points = [
497 Vec2 { x: 0.0, y: 0.0 },
498 Vec2 { x: 10.0, y: -4.0 },
499 Vec2 { x: -3.5, y: 8.25 },
500 Vec2 { x: 100.0, y: 200.0 },
501 ];
502 for (a, b) in cases {
503 let combined = a.combine(&b);
504 for p in points {
505 let expect = a.apply_to_point(b.apply_to_point(p));
506 let got = combined.apply_to_point(p);
507 assert!(
508 approx(got.x, expect.x) && approx(got.y, expect.y),
509 "combine mismatch: {a:?} then {b:?} at {p:?}: got {got:?}, want {expect:?}"
510 );
511 }
512 }
513 }
514
515 #[test]
516 fn inverse_linear_round_trips() {
517 let t = transform(3.0, -2.0, 1.5, 0.75, 0.8, 0.6, -0.2);
518 let m = t.linear();
519 let n = t.inverse_linear().expect("invertible");
520 let id = [
521 n[0] * m[0] + n[1] * m[2],
522 n[0] * m[1] + n[1] * m[3],
523 n[2] * m[0] + n[3] * m[2],
524 n[2] * m[1] + n[3] * m[3],
525 ];
526 assert!(approx(id[0], 1.0) && approx(id[3], 1.0));
527 assert!(approx(id[1], 0.0) && approx(id[2], 0.0));
528 }
529}
530
531#[cfg(test)]
532mod projective_tests {
533 use super::*;
534
535 fn approx(a: f32, b: f32) -> bool {
536 (a - b).abs() < 1e-3
537 }
538
539 #[test]
540 fn identity_row_is_affine_noop() {
541 let t = Transform::identity();
542 assert!(!t.has_perspective());
543 let p = Vec2 { x: 13.0, y: -7.0 };
544 let q = t.apply_projective(p);
545 assert!(approx(q.x, p.x) && approx(q.y, p.y));
546 assert!(approx(t.projective_w(p), 1.0));
547 }
548
549 #[test]
550 fn tilt_about_center_matches_hand_computation() {
551 let (cx, cy, f) = (100.0f32, 100.0f32, 1000.0f32);
554 let th = 0.5f32;
555 let (s, c) = (th.sin(), th.cos());
556 let t = Transform::from_projective_rows(
557 [1.0, 0.0, 0.0],
558 [0.0, c, cy * (1.0 - c)],
559 [0.0, s / f, 1.0 - cy * s / f],
560 );
561 assert!(t.has_perspective());
562 let q = t.apply_projective(Vec2 { x: cx, y: cy });
564 assert!(approx(q.x, cx) && approx(q.y, cy));
565 assert!(
566 t.projective_w(Vec2 {
567 x: cx,
568 y: cy - 50.0
569 }) < 1.0
570 );
571 assert!(
572 t.projective_w(Vec2 {
573 x: cx,
574 y: cy + 50.0
575 }) > 1.0
576 );
577 }
578
579 #[test]
580 fn from_projective_rows_round_trips_affine_part() {
581 let t = Transform::from_projective_rows(
582 [2.0, 0.5, 7.0],
583 [-0.5, 3.0, -2.0],
584 [0.001, -0.002, 1.0],
585 );
586 let m = t.projective_matrix();
588 assert!(approx(m[0], 2.0) && approx(m[1], 0.5) && approx(m[2], 7.0));
589 assert!(approx(m[3], -0.5) && approx(m[4], 3.0) && approx(m[5], -2.0));
590 assert!(approx(m[6], 0.001) && approx(m[7], -0.002) && approx(m[8], 1.0));
591 }
592
593 #[test]
594 fn compose_projective_matches_sequential_apply() {
595 let outer = Transform {
596 translate_x: 5.0,
597 ..Transform::identity()
598 };
599 let inner =
600 Transform::from_projective_rows([1.0, 0.0, 0.0], [0.0, 0.9, 10.0], [0.0, 0.001, 1.0]);
601 let c =
602 Transform::compose_projective(&outer.projective_matrix(), &inner.projective_matrix());
603 for p in [Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 40.0, y: -25.0 }] {
604 let q1 = inner.apply_projective(p);
606 let m = outer.projective_matrix();
607 let q1 = Vec2 {
608 x: m[0] * q1.x + m[1] * q1.y + m[2],
609 y: m[3] * q1.x + m[4] * q1.y + m[5],
610 };
611 let w = c[6] * p.x + c[7] * p.y + c[8];
612 let q2 = Vec2 {
613 x: (c[0] * p.x + c[1] * p.y + c[2]) / w,
614 y: (c[3] * p.x + c[4] * p.y + c[5]) / w,
615 };
616 assert!(
617 approx(q1.x, q2.x) && approx(q1.y, q2.y),
618 "mismatch at {p:?}: {q1:?} vs {q2:?}"
619 );
620 }
621 }
622
623 #[test]
624 fn project_rect_bounds_projected_corners() {
625 let t =
626 Transform::from_projective_rows([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.002, 0.0, 1.0]);
627 let r = Rect {
628 x: 0.0,
629 y: 0.0,
630 w: 100.0,
631 h: 100.0,
632 };
633 let b = t.project_rect(&r);
634 assert!(approx(b.x, 0.0));
637 assert!(approx(b.w, 100.0 / 1.2));
638 assert!(approx(b.y, 0.0) && approx(b.h, 100.0));
639 }
640}