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#[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 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 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 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}