Skip to main content

hayro_interpret/
util.rs

1//! A number of utility methods.
2
3use kurbo::{BezPath, PathEl, Rect};
4use siphasher::sip128::{Hasher128, SipHasher13};
5use std::hash::Hash;
6use std::ops::Sub;
7
8pub(crate) trait OptionLog {
9    fn warn_none(self, f: &str) -> Self;
10}
11
12impl<T> OptionLog for Option<T> {
13    #[inline]
14    fn warn_none(self, _f: &str) -> Self {
15        self.or_else(|| {
16            warn!("{_f}");
17
18            None
19        })
20    }
21}
22
23const SCALAR_NEARLY_ZERO: f32 = 1.0 / (1 << 8) as f32;
24
25/// A number of useful methods for f32 numbers.
26pub trait Float32Ext: Sized + Sub<f32, Output = f32> + Copy + PartialOrd<f32> {
27    /// Whether the number is approximately 0.
28    fn is_nearly_zero(&self) -> bool {
29        self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO)
30    }
31
32    /// Whether the number is nearly equal to another number.
33    fn is_nearly_equal(&self, other: f32) -> bool {
34        (*self - other).is_nearly_zero()
35    }
36
37    /// Whether the number is nearly equal to another number.
38    fn is_nearly_less_or_equal(&self, other: f32) -> bool {
39        (*self - other).is_nearly_zero() || *self < other
40    }
41
42    /// Whether the number is nearly equal to another number.
43    fn is_nearly_greater_or_equal(&self, other: f32) -> bool {
44        (*self - other).is_nearly_zero() || *self > other
45    }
46
47    /// Whether the number is approximately 0, with a given tolerance.
48    fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool;
49}
50
51impl Float32Ext for f32 {
52    fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool {
53        debug_assert!(tolerance >= 0.0, "tolerance must be non-negative");
54
55        self.abs() <= tolerance
56    }
57}
58
59/// A number of useful methods for f64 numbers.
60pub trait Float64Ext: Sized + Sub<f64, Output = f64> + Copy + PartialOrd<f64> {
61    /// Whether the number is approximately 0.
62    fn is_nearly_zero(&self) -> bool {
63        self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO as f64)
64    }
65
66    /// Whether the number is nearly equal to another number.
67    fn is_nearly_equal(&self, other: f64) -> bool {
68        (*self - other).is_nearly_zero()
69    }
70
71    /// Whether the number is nearly equal to another number.
72    fn is_nearly_less_or_equal(&self, other: f64) -> bool {
73        (*self - other).is_nearly_zero() || *self < other
74    }
75
76    /// Whether the number is nearly equal to another number.
77    fn is_nearly_greater_or_equal(&self, other: f64) -> bool {
78        (*self - other).is_nearly_zero() || *self > other
79    }
80
81    /// Whether the number is approximately 0, with a given tolerance.
82    fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool;
83}
84
85impl Float64Ext for f64 {
86    fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool {
87        debug_assert!(tolerance >= 0.0, "tolerance must be non-negative");
88
89        self.abs() <= tolerance
90    }
91}
92
93pub(crate) trait PointExt: Sized {
94    fn x(&self) -> f32;
95    fn y(&self) -> f32;
96
97    fn nearly_same(&self, other: Self) -> bool {
98        self.x().is_nearly_equal(other.x()) && self.y().is_nearly_equal(other.y())
99    }
100}
101
102impl PointExt for kurbo::Point {
103    fn x(&self) -> f32 {
104        self.x as f32
105    }
106
107    fn y(&self) -> f32 {
108        self.y as f32
109    }
110}
111
112/// Calculate a 128-bit siphash of a value.
113pub(crate) fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
114    let mut state = SipHasher13::new();
115    value.hash(&mut state);
116    state.finish128().as_u128()
117}
118
119pub(crate) trait BezPathExt {
120    fn fast_bounding_box(&self) -> Rect;
121}
122
123impl BezPathExt for BezPath {
124    fn fast_bounding_box(&self) -> Rect {
125        let mut min_x = f64::INFINITY;
126        let mut min_y = f64::INFINITY;
127        let mut max_x = f64::NEG_INFINITY;
128        let mut max_y = f64::NEG_INFINITY;
129
130        let mut include = |x: f64, y: f64| {
131            min_x = min_x.min(x);
132            min_y = min_y.min(y);
133            max_x = max_x.max(x);
134            max_y = max_y.max(y);
135        };
136
137        for el in self.elements() {
138            match *el {
139                PathEl::MoveTo(p) | PathEl::LineTo(p) => include(p.x, p.y),
140                PathEl::QuadTo(p1, p2) => {
141                    include(p1.x, p1.y);
142                    include(p2.x, p2.y);
143                }
144                PathEl::CurveTo(p1, p2, p3) => {
145                    include(p1.x, p1.y);
146                    include(p2.x, p2.y);
147                    include(p3.x, p3.y);
148                }
149                PathEl::ClosePath => {}
150            }
151        }
152
153        if min_x > max_x {
154            Rect::ZERO
155        } else {
156            Rect::new(min_x, min_y, max_x, max_y)
157        }
158    }
159}
160
161/// Extension methods for converting a [`hayro_syntax::transform::Transform`] to a [`kurbo::Affine`].
162pub trait TransformExt {
163    /// Convert to a `kurbo::Affine`.
164    fn to_kurbo(&self) -> kurbo::Affine;
165}
166
167impl TransformExt for hayro_syntax::transform::Transform {
168    fn to_kurbo(&self) -> kurbo::Affine {
169        kurbo::Affine::new(self.as_coeffs())
170    }
171}
172
173/// Extension methods for rectangles.
174pub trait RectExt {
175    /// Convert the rectangle to a `kurbo` rectangle.
176    fn to_kurbo(&self) -> Rect;
177}
178
179impl RectExt for hayro_syntax::object::Rect {
180    fn to_kurbo(&self) -> Rect {
181        Rect::new(self.x0, self.y0, self.x1, self.y1)
182    }
183}