Skip to main content

glifo/
util.rs

1// Copyright 2025 the Vello Authors and the Parley Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Utility helper functions.
5
6use core::ops::Sub;
7use peniko::kurbo::Affine;
8
9// From <https://github.com/linebender/tiny-skia/blob/68b198a7210a6bbf752b43d6bc4db62445730313/path/src/scalar.rs#L12>
10const SCALAR_NEARLY_ZERO_F32: f32 = 1.0 / (1 << 12) as f32;
11const SCALAR_NEARLY_ZERO_F64: f64 = 1.0 / (1 << 12) as f64;
12
13/// A number of useful methods for f32 numbers.
14pub(crate) trait FloatExt: Sized + Sub<f32, Output = f32> {
15    /// Whether the number is approximately 0.
16    fn is_nearly_zero(&self) -> bool {
17        self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO_F32)
18    }
19
20    /// Whether the number is approximately 0, with a given tolerance.
21    fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool;
22}
23
24impl FloatExt for f32 {
25    #[inline(always)]
26    fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool {
27        debug_assert!(tolerance >= 0.0, "tolerance must be positive");
28
29        self.abs() <= tolerance
30    }
31}
32
33pub(crate) trait AffineExt {
34    /// Whether the transform has any skewing coefficient.
35    fn has_skew(&self) -> bool;
36
37    /// Whether the transform has a vertical skew.
38    fn has_vertical_skew(&self) -> bool;
39
40    /// Whether the transform has positive, uniform scaling factors and no skew.
41    fn is_positive_uniform_scale_without_skew(&self) -> bool;
42
43    /// Whether the transform has positive, uniform scaling factors and no vertical skew.
44    fn is_positive_uniform_scale_without_vertical_skew(&self) -> bool;
45
46    /// Whether the transform has non-unit scale or skew.
47    ///
48    /// Note that negative scales (i.e. -1.0) are explicitly allowed.
49    fn has_non_unit_skew_or_scale(&self) -> bool;
50}
51
52impl AffineExt for Affine {
53    #[inline]
54    fn has_skew(&self) -> bool {
55        let [_, b, c, _, _, _] = self.as_coeffs();
56        b.abs() > SCALAR_NEARLY_ZERO_F64 || c.abs() > SCALAR_NEARLY_ZERO_F64
57    }
58
59    #[inline]
60    fn is_positive_uniform_scale_without_skew(&self) -> bool {
61        let [a, _, _, d, _, _] = self.as_coeffs();
62        (a - d).abs() <= SCALAR_NEARLY_ZERO_F64 && a > 0.0 && d > 0.0 && !self.has_skew()
63    }
64
65    #[inline]
66    fn has_non_unit_skew_or_scale(&self) -> bool {
67        let [a, _, _, d, _, _] = self.as_coeffs();
68        self.has_skew()
69            || (1.0 - a.abs()).abs() > SCALAR_NEARLY_ZERO_F64
70            || (1.0 - d.abs()).abs() > SCALAR_NEARLY_ZERO_F64
71    }
72
73    /// Whether the transform has a vertical skew.
74    #[inline]
75    fn has_vertical_skew(&self) -> bool {
76        let [_, b, _, _, _, _] = self.as_coeffs();
77        b.abs() > SCALAR_NEARLY_ZERO_F64
78    }
79
80    #[inline]
81    fn is_positive_uniform_scale_without_vertical_skew(&self) -> bool {
82        let [a, _, _, d, _, _] = self.as_coeffs();
83        (a - d).abs() <= SCALAR_NEARLY_ZERO_F64 && a > 0.0 && d > 0.0 && !self.has_vertical_skew()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::AffineExt;
90    use peniko::kurbo::Affine;
91
92    #[test]
93    fn detects_positive_uniform_scale_without_skew() {
94        let transform = Affine::scale(2.0);
95
96        assert!(!transform.has_skew());
97        assert!(!transform.has_vertical_skew());
98        assert!(transform.is_positive_uniform_scale_without_skew());
99        assert!(transform.is_positive_uniform_scale_without_vertical_skew());
100    }
101
102    #[test]
103    fn rejects_positive_uniform_scale_without_skew_when_horizontally_skewed() {
104        let transform = Affine::new([2.0, 0.0, 0.25, 2.0, 0.0, 0.0]);
105
106        assert!(transform.has_skew());
107        assert!(!transform.has_vertical_skew());
108        assert!(!transform.is_positive_uniform_scale_without_skew());
109        assert!(transform.is_positive_uniform_scale_without_vertical_skew());
110    }
111
112    #[test]
113    fn rejects_positive_uniform_scale_without_vertical_skew_when_vertically_skewed() {
114        let transform = Affine::new([2.0, 0.25, 0.0, 2.0, 0.0, 0.0]);
115
116        assert!(transform.has_skew());
117        assert!(transform.has_vertical_skew());
118        assert!(!transform.is_positive_uniform_scale_without_skew());
119        assert!(!transform.is_positive_uniform_scale_without_vertical_skew());
120    }
121
122    #[test]
123    fn rejects_non_uniform_or_non_positive_scale() {
124        let non_uniform = Affine::new([2.0, 0.0, 0.0, 3.0, 0.0, 0.0]);
125        let flipped = Affine::new([-2.0, 0.0, 0.0, -2.0, 0.0, 0.0]);
126
127        assert!(non_uniform.has_non_unit_skew_or_scale());
128        assert!(!non_uniform.is_positive_uniform_scale_without_skew());
129        assert!(!non_uniform.is_positive_uniform_scale_without_vertical_skew());
130        assert!(flipped.has_non_unit_skew_or_scale());
131        assert!(!flipped.is_positive_uniform_scale_without_skew());
132        assert!(!flipped.is_positive_uniform_scale_without_vertical_skew());
133    }
134
135    #[test]
136    fn allows_unit_axis_flips() {
137        let flip_x = Affine::new([-1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
138        let flip_y = Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
139        let flip_xy = Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
140
141        assert!(!flip_x.has_non_unit_skew_or_scale());
142        assert!(!flip_y.has_non_unit_skew_or_scale());
143        assert!(!flip_xy.has_non_unit_skew_or_scale());
144    }
145}