Skip to main content

repose_core/
geometry.rs

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, // radians
68    /// Horizontal shear factor (`x += shear_x * y`), e.g. ASS `\fax`.
69    pub shear_x: f32,
70    /// Vertical shear factor (`y += shear_y * x`), e.g. ASS `\fay`.
71    pub shear_y: f32,
72    pub origin_x: f32,
73    pub origin_y: f32,
74}
75
76impl Transform {
77    pub fn identity() -> Self {
78        Self {
79            translate_x: 0.0,
80            translate_y: 0.0,
81            scale_x: 1.0,
82            scale_y: 1.0,
83            rotate: 0.0,
84            shear_x: 0.0,
85            shear_y: 0.0,
86            origin_x: 0.5,
87            origin_y: 0.5,
88        }
89    }
90
91    pub fn translate(x: f32, y: f32) -> Self {
92        Self {
93            translate_x: x,
94            translate_y: y,
95            scale_x: 1.0,
96            scale_y: 1.0,
97            rotate: 0.0,
98            shear_x: 0.0,
99            shear_y: 0.0,
100            origin_x: 0.5,
101            origin_y: 0.5,
102        }
103    }
104
105    /// Forward 2x2 linear part (row-major `[m00, m01, m10, m11]`):
106    /// `M = R(rotate) * H(shear) * S(scale)` applied before translation.
107    pub fn linear(&self) -> [f32; 4] {
108        let c = self.rotate.cos();
109        let s = self.rotate.sin();
110        let hsx = self.scale_x;
111        let hsy = self.scale_y;
112        let k00 = hsx;
113        let k01 = self.shear_x * hsy;
114        let k10 = self.shear_y * hsx;
115        let k11 = hsy;
116        [
117            c * k00 - s * k10,
118            c * k01 - s * k11,
119            s * k00 + c * k10,
120            s * k01 + c * k11,
121        ]
122    }
123
124    /// Inverse of [`linear`](Self::linear), or `None` when singular.
125    pub fn inverse_linear(&self) -> Option<[f32; 4]> {
126        let m = self.linear();
127        let det = m[0] * m[3] - m[1] * m[2];
128        if det.abs() < 1e-12 {
129            return None;
130        }
131        Some([m[3] / det, -m[1] / det, -m[2] / det, m[0] / det])
132    }
133
134    /// Whether scale/rotation/shear are all identity (translation may apply).
135    pub fn linear_is_identity(&self) -> bool {
136        self.scale_x == 1.0
137            && self.scale_y == 1.0
138            && self.rotate == 0.0
139            && self.shear_x == 0.0
140            && self.shear_y == 0.0
141    }
142
143    pub fn apply_to_point(&self, p: Vec2) -> Vec2 {
144        let ox = self.origin_x;
145        let oy = self.origin_y;
146        let m = self.linear();
147        let x = p.x - ox;
148        let y = p.y - oy;
149
150        Vec2 {
151            x: m[0] * x + m[1] * y + ox + self.translate_x,
152            y: m[2] * x + m[3] * y + oy + self.translate_y,
153        }
154    }
155
156    pub fn apply_to_rect(&self, r: Rect) -> Rect {
157        let ox = r.x + r.w * self.origin_x;
158        let oy = r.y + r.h * self.origin_y;
159        let m = self.linear();
160        let corners = [
161            Vec2 { x: r.x, y: r.y },
162            Vec2 {
163                x: r.x + r.w,
164                y: r.y,
165            },
166            Vec2 {
167                x: r.x,
168                y: r.y + r.h,
169            },
170            Vec2 {
171                x: r.x + r.w,
172                y: r.y + r.h,
173            },
174        ];
175        let mut min_x = f32::MAX;
176        let mut min_y = f32::MAX;
177        let mut max_x = f32::MIN;
178        let mut max_y = f32::MIN;
179        for c in corners {
180            let x = c.x - ox;
181            let y = c.y - oy;
182            let tx = m[0] * x + m[1] * y + ox + self.translate_x;
183            let ty = m[2] * x + m[3] * y + oy + self.translate_y;
184            min_x = min_x.min(tx);
185            min_y = min_y.min(ty);
186            max_x = max_x.max(tx);
187            max_y = max_y.max(ty);
188        }
189        Rect {
190            x: min_x,
191            y: min_y,
192            w: max_x - min_x,
193            h: max_y - min_y,
194        }
195    }
196
197    /// Compose two transforms (`self` outer, `other` inner) for a
198    /// transform stack: `current.combine(pushed)` where `pushed` is the
199    /// newly pushed (inner) node.
200    ///
201    /// Returns a transform such that `combined.apply_to_point(p) ==
202    /// self.apply_to_point(other.apply_to_point(p))`.
203    ///
204    /// The linear part is composed exactly (via polar decomposition of the
205    /// 2x2 product); origins are inherited from `self`, matching the
206    /// previous behaviour for the shear-free cases.
207    pub fn combine(&self, other: &Transform) -> Transform {
208        let a = self.linear();
209        let b = other.linear();
210        let m = [
211            a[0] * b[0] + a[1] * b[2],
212            a[0] * b[1] + a[1] * b[3],
213            a[2] * b[0] + a[3] * b[2],
214            a[2] * b[1] + a[3] * b[3],
215        ];
216        let translate_x = a[0] * other.translate_x + a[1] * other.translate_y + self.translate_x;
217        let translate_y = a[2] * other.translate_x + a[3] * other.translate_y + self.translate_y;
218
219        let (scale_x, scale_y, rotate, shear_x, shear_y) =
220            decompose_linear(m).unwrap_or((1.0, 1.0, 0.0, 0.0, 0.0));
221
222        Transform {
223            translate_x,
224            translate_y,
225            scale_x,
226            scale_y,
227            rotate,
228            shear_x,
229            shear_y,
230            origin_x: self.origin_x,
231            origin_y: self.origin_y,
232        }
233    }
234}
235
236/// Split a 2x2 matrix `[m00, m01, m10, m11]` into
237/// `(scale_x, scale_y, rotate, shear_x, shear_y)` such that
238/// `M = R(rotate) * H(shear) * S(scale)`, or `None` when degenerate.
239///
240/// Uses polar decomposition (`M = R * K` with symmetric `K`), then reads
241/// scale/shear off `K`. Reflections fold their sign into `scale_x`.
242fn decompose_linear(m: [f32; 4]) -> Option<(f32, f32, f32, f32, f32)> {
243    let (mut a, mut b, c, d) = (m[0], m[1], m[2], m[3]);
244    let mut angle_sign = 1.0;
245    if a * d - b * c < 0.0 {
246        a = -a;
247        b = -b;
248        angle_sign = -1.0;
249    }
250    let e = a * a + c * c;
251    let f = a * b + c * d;
252    let g = b * b + d * d;
253    let det_p = (e * g - f * f).max(0.0);
254    let s = (e + g + 2.0 * det_p.sqrt()).sqrt();
255    if !(s > 1e-12) {
256        return None;
257    }
258    let root_det = det_p.sqrt();
259    let k00 = (e + root_det) / s;
260    let k01 = f / s;
261    let k10 = f / s;
262    let k11 = (g + root_det) / s;
263    let det_k = (k00 * k11 - k01 * k10).max(1e-24);
264    let r00 = (a * k11 - b * k10) / det_k;
265    let r10 = (c * k11 - d * k10) / det_k;
266    let rotate = r10.atan2(r00);
267    let (sx, sy) = (k00, k11);
268    if sx.abs() < 1e-12 || sy.abs() < 1e-12 {
269        return None;
270    }
271    if angle_sign < 0.0 {
272        return Some((-sx, sy, -rotate, -(k01 / sy), -(k10 / sx)));
273    }
274    Some((sx, sy, rotate, k01 / sy, k10 / sx))
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    fn approx(a: f32, b: f32) -> bool {
282        (a - b).abs() < 1e-3
283    }
284
285    fn transform(tx: f32, ty: f32, sx: f32, sy: f32, rot: f32, hx: f32, hy: f32) -> Transform {
286        Transform {
287            translate_x: tx,
288            translate_y: ty,
289            scale_x: sx,
290            scale_y: sy,
291            rotate: rot,
292            shear_x: hx,
293            shear_y: hy,
294            origin_x: 0.0,
295            origin_y: 0.0,
296        }
297    }
298
299    #[test]
300    fn shear_free_matches_legacy_formula() {
301        let t = transform(5.0, -3.0, 2.0, 0.5, 0.7, 0.0, 0.0);
302        let p = Vec2 { x: 11.0, y: 7.0 };
303        let got = t.apply_to_point(p);
304        let (c, s) = (0.7f32.cos(), 0.7f32.sin());
305        let x = (p.x) * 2.0;
306        let y = (p.y) * 0.5;
307        assert!(approx(got.x, x * c - y * s + 5.0));
308        assert!(approx(got.y, x * s + y * c - 3.0));
309    }
310
311    #[test]
312    fn shear_order_is_scale_then_shear_then_rotate() {
313        let t = transform(0.0, 0.0, 2.0, 3.0, 0.0, 1.0, 0.0);
314        let got = t.apply_to_point(Vec2 { x: 1.0, y: 1.0 });
315        assert!(approx(got.x, 2.0 + 3.0));
316        assert!(approx(got.y, 3.0));
317    }
318
319    #[test]
320    fn combine_round_trips_through_apply() {
321        let cases = [
322            (
323                transform(3.0, 4.0, 1.0, 1.0, 0.0, 0.0, 0.0),
324                transform(0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 0.0),
325            ),
326            (
327                transform(1.0, 2.0, 1.5, 0.5, 0.6, 0.0, 0.0),
328                transform(-2.0, 1.0, 0.7, 1.3, -0.4, 0.0, 0.0),
329            ),
330            (
331                transform(0.0, 0.0, 2.0, 0.5, 0.9, 0.0, 0.0),
332                transform(4.0, -1.0, 1.0, 1.0, 0.3, 0.0, 0.0),
333            ),
334            (
335                transform(2.0, 0.0, 1.0, 1.0, 0.2, 0.8, -0.3),
336                transform(0.0, 5.0, 1.2, 0.9, -0.5, 0.4, 0.1),
337            ),
338            (
339                transform(0.0, 0.0, -1.0, 1.0, 0.0, 0.0, 0.0),
340                transform(7.0, 7.0, 1.0, 1.0, 1.1, 0.2, 0.0),
341            ),
342        ];
343        let points = [
344            Vec2 { x: 0.0, y: 0.0 },
345            Vec2 { x: 10.0, y: -4.0 },
346            Vec2 { x: -3.5, y: 8.25 },
347            Vec2 { x: 100.0, y: 200.0 },
348        ];
349        for (a, b) in cases {
350            let combined = a.combine(&b);
351            for p in points {
352                let expect = a.apply_to_point(b.apply_to_point(p));
353                let got = combined.apply_to_point(p);
354                assert!(
355                    approx(got.x, expect.x) && approx(got.y, expect.y),
356                    "combine mismatch: {a:?} then {b:?} at {p:?}: got {got:?}, want {expect:?}"
357                );
358            }
359        }
360    }
361
362    #[test]
363    fn inverse_linear_round_trips() {
364        let t = transform(3.0, -2.0, 1.5, 0.75, 0.8, 0.6, -0.2);
365        let m = t.linear();
366        let n = t.inverse_linear().expect("invertible");
367        let id = [
368            n[0] * m[0] + n[1] * m[2],
369            n[0] * m[1] + n[1] * m[3],
370            n[2] * m[0] + n[3] * m[2],
371            n[2] * m[1] + n[3] * m[3],
372        ];
373        assert!(approx(id[0], 1.0) && approx(id[3], 1.0));
374        assert!(approx(id[1], 0.0) && approx(id[2], 0.0));
375    }
376}