Skip to main content

fret_core/
geometry.rs

1use serde::{Deserialize, Serialize};
2use std::ops::{Add, Div, Mul, Sub};
3
4#[repr(transparent)]
5#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
6#[serde(transparent)]
7pub struct Px(pub f32);
8
9impl From<f32> for Px {
10    fn from(value: f32) -> Self {
11        Self(value)
12    }
13}
14
15impl Px {
16    pub fn min(self, other: Self) -> Self {
17        Self(self.0.min(other.0))
18    }
19
20    pub fn max(self, other: Self) -> Self {
21        Self(self.0.max(other.0))
22    }
23
24    pub fn clamp(self, min: Self, max: Self) -> Self {
25        Self(self.0.clamp(min.0, max.0))
26    }
27}
28
29impl Add for Px {
30    type Output = Self;
31
32    fn add(self, rhs: Self) -> Self::Output {
33        Self(self.0 + rhs.0)
34    }
35}
36
37impl Sub for Px {
38    type Output = Self;
39
40    fn sub(self, rhs: Self) -> Self::Output {
41        Self(self.0 - rhs.0)
42    }
43}
44
45impl Mul<f32> for Px {
46    type Output = Self;
47
48    fn mul(self, rhs: f32) -> Self::Output {
49        Self(self.0 * rhs)
50    }
51}
52
53impl Div<f32> for Px {
54    type Output = Self;
55
56    fn div(self, rhs: f32) -> Self::Output {
57        Self(self.0 / rhs)
58    }
59}
60
61#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
62pub struct Point {
63    pub x: Px,
64    pub y: Px,
65}
66
67impl Point {
68    pub const fn new(x: Px, y: Px) -> Self {
69        Self { x, y }
70    }
71}
72
73#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
74pub struct Size {
75    pub width: Px,
76    pub height: Px,
77}
78
79impl Size {
80    pub const fn new(width: Px, height: Px) -> Self {
81        Self { width, height }
82    }
83}
84
85#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
86pub struct Rect {
87    pub origin: Point,
88    pub size: Size,
89}
90
91impl Rect {
92    pub const fn new(origin: Point, size: Size) -> Self {
93        Self { origin, size }
94    }
95
96    pub fn contains(&self, point: Point) -> bool {
97        let x0 = self.origin.x.0;
98        let y0 = self.origin.y.0;
99        let x1 = x0 + self.size.width.0;
100        let y1 = y0 + self.size.height.0;
101
102        point.x.0 >= x0 && point.x.0 < x1 && point.y.0 >= y0 && point.y.0 < y1
103    }
104}
105
106#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
107pub struct RectPx {
108    pub x: u32,
109    pub y: u32,
110    pub w: u32,
111    pub h: u32,
112}
113
114impl RectPx {
115    pub const fn new(x: u32, y: u32, w: u32, h: u32) -> Self {
116        Self { x, y, w, h }
117    }
118
119    pub const fn full(w: u32, h: u32) -> Self {
120        Self { x: 0, y: 0, w, h }
121    }
122
123    pub fn is_empty(&self) -> bool {
124        self.w == 0 || self.h == 0
125    }
126}
127
128#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
129pub struct Corners {
130    pub top_left: Px,
131    pub top_right: Px,
132    pub bottom_right: Px,
133    pub bottom_left: Px,
134}
135
136impl Corners {
137    pub const fn all(radius: Px) -> Self {
138        Self {
139            top_left: radius,
140            top_right: radius,
141            bottom_right: radius,
142            bottom_left: radius,
143        }
144    }
145}
146
147#[derive(Debug, Default, Clone, Copy, PartialEq)]
148pub struct Edges {
149    pub top: Px,
150    pub right: Px,
151    pub bottom: Px,
152    pub left: Px,
153}
154
155impl Edges {
156    pub const fn all(value: Px) -> Self {
157        Self {
158            top: value,
159            right: value,
160            bottom: value,
161            left: value,
162        }
163    }
164
165    pub const fn symmetric(horizontal: Px, vertical: Px) -> Self {
166        Self {
167            top: vertical,
168            right: horizontal,
169            bottom: vertical,
170            left: horizontal,
171        }
172    }
173}
174
175/// A 2D affine transform in logical pixels.
176///
177/// Matrix form (applied to column vectors):
178///
179/// ```text
180/// | a  c  tx |
181/// | b  d  ty |
182/// | 0  0  1  |
183/// ```
184///
185/// So:
186/// - `x' = a*x + c*y + tx`
187/// - `y' = b*x + d*y + ty`
188#[derive(Debug, Clone, Copy, PartialEq)]
189pub struct Transform2D {
190    pub a: f32,
191    pub b: f32,
192    pub c: f32,
193    pub d: f32,
194    pub tx: f32,
195    pub ty: f32,
196}
197
198impl Default for Transform2D {
199    fn default() -> Self {
200        Self::IDENTITY
201    }
202}
203
204impl Transform2D {
205    pub const IDENTITY: Self = Self {
206        a: 1.0,
207        b: 0.0,
208        c: 0.0,
209        d: 1.0,
210        tx: 0.0,
211        ty: 0.0,
212    };
213
214    pub const fn translation(delta: Point) -> Self {
215        Self {
216            tx: delta.x.0,
217            ty: delta.y.0,
218            ..Self::IDENTITY
219        }
220    }
221
222    pub const fn scale_uniform(s: f32) -> Self {
223        Self {
224            a: s,
225            d: s,
226            ..Self::IDENTITY
227        }
228    }
229
230    pub fn rotation_radians(theta: f32) -> Self {
231        let (sin, cos) = theta.sin_cos();
232        Self {
233            a: cos,
234            b: sin,
235            c: -sin,
236            d: cos,
237            ..Self::IDENTITY
238        }
239    }
240
241    pub fn rotation_degrees(degrees: f32) -> Self {
242        Self::rotation_radians(degrees.to_radians())
243    }
244
245    pub fn rotation_about_radians(theta: f32, center: Point) -> Self {
246        let to_center = Self::translation(center);
247        let from_center = Self::translation(Point::new(Px(-center.x.0), Px(-center.y.0)));
248        to_center * Self::rotation_radians(theta) * from_center
249    }
250
251    pub fn rotation_about_degrees(degrees: f32, center: Point) -> Self {
252        Self::rotation_about_radians(degrees.to_radians(), center)
253    }
254
255    /// Matrix composition: `self * rhs`.
256    ///
257    /// This means: apply `rhs` first, then `self`.
258    pub fn compose(self, rhs: Self) -> Self {
259        Self {
260            a: self.a * rhs.a + self.c * rhs.b,
261            b: self.b * rhs.a + self.d * rhs.b,
262            c: self.a * rhs.c + self.c * rhs.d,
263            d: self.b * rhs.c + self.d * rhs.d,
264            tx: self.a * rhs.tx + self.c * rhs.ty + self.tx,
265            ty: self.b * rhs.tx + self.d * rhs.ty + self.ty,
266        }
267    }
268
269    pub fn apply_point(self, p: Point) -> Point {
270        Point::new(
271            Px(self.a * p.x.0 + self.c * p.y.0 + self.tx),
272            Px(self.b * p.x.0 + self.d * p.y.0 + self.ty),
273        )
274    }
275
276    pub fn inverse(self) -> Option<Self> {
277        let det = self.a * self.d - self.b * self.c;
278        if !det.is_finite() || det == 0.0 {
279            return None;
280        }
281        let inv_det = 1.0 / det;
282        let ia = self.d * inv_det;
283        let ib = -self.b * inv_det;
284        let ic = -self.c * inv_det;
285        let id = self.a * inv_det;
286
287        let itx = -(ia * self.tx + ic * self.ty);
288        let ity = -(ib * self.tx + id * self.ty);
289
290        Some(Self {
291            a: ia,
292            b: ib,
293            c: ic,
294            d: id,
295            tx: itx,
296            ty: ity,
297        })
298    }
299
300    /// Converts a logical-px transform to a physical-px transform.
301    ///
302    /// If you already have coordinates multiplied by `scale_factor`, apply the returned transform
303    /// directly in physical pixels.
304    pub fn to_physical_px(self, scale_factor: f32) -> Self {
305        Self {
306            tx: self.tx * scale_factor,
307            ty: self.ty * scale_factor,
308            ..self
309        }
310    }
311
312    /// Returns `(scale, translation)` if this is a translation + uniform scale transform.
313    pub fn as_translation_uniform_scale(self) -> Option<(f32, Point)> {
314        if !self.a.is_finite()
315            || !self.b.is_finite()
316            || !self.c.is_finite()
317            || !self.d.is_finite()
318            || !self.tx.is_finite()
319            || !self.ty.is_finite()
320        {
321            return None;
322        }
323
324        if self.b != 0.0 || self.c != 0.0 || self.a != self.d {
325            return None;
326        }
327        Some((self.a, Point::new(Px(self.tx), Px(self.ty))))
328    }
329}
330
331impl std::ops::Mul for Transform2D {
332    type Output = Self;
333
334    fn mul(self, rhs: Self) -> Self::Output {
335        self.compose(rhs)
336    }
337}
338
339impl std::ops::MulAssign for Transform2D {
340    fn mul_assign(&mut self, rhs: Self) {
341        *self = self.compose(rhs);
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn transform_inverse_roundtrips_point() {
351        let t = Transform2D {
352            a: 2.0,
353            b: 1.0,
354            c: -0.5,
355            d: 1.5,
356            tx: 10.0,
357            ty: -7.0,
358        };
359        let inv = t.inverse().expect("invertible");
360        let p = Point::new(Px(3.0), Px(4.0));
361        let p2 = inv.apply_point(t.apply_point(p));
362        assert!((p2.x.0 - p.x.0).abs() < 1e-4);
363        assert!((p2.y.0 - p.y.0).abs() < 1e-4);
364    }
365}