Skip to main content

brepkit_math/
tolerance.rs

1//! Tolerance model for geometric comparisons.
2//!
3//! CAD kernels need well-defined tolerances for classifying geometric
4//! relationships. [`Tolerance`] bundles linear, angular, and relative
5//! thresholds.
6//!
7//! The [`approx_eq`](Tolerance::approx_eq) comparison is *scale-aware*:
8//! two values are equal when `|a - b| <= max(linear, relative * max(|a|, |b|))`.
9//! This prevents false negatives when comparing large coordinates.
10
11/// Tolerance thresholds for geometric comparisons.
12#[derive(Debug, Clone, Copy, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct Tolerance {
15    /// Absolute tolerance for linear (distance) comparisons.
16    pub linear: f64,
17    /// Absolute tolerance for angular (radian) comparisons.
18    pub angular: f64,
19    /// Relative tolerance as a fraction of the larger operand.
20    ///
21    /// Used by [`approx_eq`](Self::approx_eq) to scale comparisons:
22    /// `|a - b| <= max(linear, relative * max(|a|, |b|))`.
23    pub relative: f64,
24}
25
26impl Tolerance {
27    /// Sensible defaults for CAD geometry.
28    ///
29    /// Linear: 1e-7, angular: 1e-12, relative: 1e-10.
30    #[must_use]
31    pub const fn new() -> Self {
32        Self {
33            linear: 1e-7,
34            angular: 1e-12,
35            relative: 1e-10,
36        }
37    }
38
39    /// A looser tolerance for visualization or rough checks.
40    ///
41    /// Linear: 1e-4, angular: 1e-8, relative: 1e-6.
42    #[must_use]
43    pub const fn loose() -> Self {
44        Self {
45            linear: 1e-4,
46            angular: 1e-8,
47            relative: 1e-6,
48        }
49    }
50
51    /// A tighter tolerance for high-precision operations.
52    ///
53    /// Linear: 1e-10, angular: 1e-15, relative: 1e-14.
54    #[must_use]
55    pub const fn tight() -> Self {
56        Self {
57            linear: 1e-10,
58            angular: 1e-15,
59            relative: 1e-14,
60        }
61    }
62
63    /// Scale-aware approximate equality.
64    ///
65    /// Returns `true` when `|a - b| <= max(linear, relative * max(|a|, |b|))`.
66    /// This ensures comparisons remain meaningful at any coordinate magnitude.
67    #[must_use]
68    pub fn approx_eq(self, a: f64, b: f64) -> bool {
69        let diff = (a - b).abs();
70        let scale = a.abs().max(b.abs());
71        diff <= self.linear.max(self.relative * scale)
72    }
73
74    /// Purely absolute approximate equality (ignores `relative`).
75    ///
76    /// Use this when comparing values that are *not* coordinates, e.g.
77    /// parameter-space values in `[0, 1]` where relative scaling is wrong.
78    #[must_use]
79    pub fn approx_eq_abs(self, a: f64, b: f64) -> bool {
80        (a - b).abs() <= self.linear
81    }
82
83    /// Convert the linear tolerance to parameter space given the magnitude
84    /// of a surface derivative (or curve tangent).
85    ///
86    /// The parametric tolerance is `linear / derivative_magnitude`, so a
87    /// surface with `||∂S/∂u|| = 1000` and linear tolerance 1e-7 gives
88    /// parametric tolerance 1e-10.
89    ///
90    /// Clamps to `[1e-15, 0.1]` to prevent degeneracy.
91    #[must_use]
92    pub fn parametric(self, derivative_mag: f64) -> f64 {
93        if derivative_mag < 1e-30 {
94            return self.linear;
95        }
96        (self.linear / derivative_mag).clamp(1e-15, 0.1)
97    }
98
99    /// A squared linear tolerance, useful for distance² comparisons
100    /// that avoid the `sqrt` call.
101    #[must_use]
102    pub fn linear_sq(self) -> f64 {
103        self.linear * self.linear
104    }
105}
106
107impl Default for Tolerance {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    #![allow(clippy::unwrap_used, clippy::expect_used)]
116    use super::*;
117
118    #[test]
119    fn default_tolerance() {
120        let tol = Tolerance::new();
121        assert!((tol.linear - 1e-7).abs() < 1e-20);
122        assert!((tol.angular - 1e-12).abs() < 1e-20);
123        assert!((tol.relative - 1e-10).abs() < 1e-20);
124    }
125
126    #[test]
127    fn loose_tolerance() {
128        let tol = Tolerance::loose();
129        assert!(tol.linear > Tolerance::new().linear);
130        assert!(tol.relative > Tolerance::new().relative);
131    }
132
133    #[test]
134    fn tight_tolerance() {
135        let tol = Tolerance::tight();
136        assert!(tol.linear < Tolerance::new().linear);
137        assert!(tol.relative < Tolerance::new().relative);
138    }
139
140    #[test]
141    fn approx_eq_within_tolerance() {
142        let tol = Tolerance::new();
143        assert!(tol.approx_eq(1.0, 1.0 + 1e-8));
144        assert!(tol.approx_eq(0.0, 1e-8));
145    }
146
147    #[test]
148    fn approx_eq_outside_tolerance() {
149        let tol = Tolerance::new();
150        assert!(!tol.approx_eq(1.0, 1.001));
151        assert!(!tol.approx_eq(0.0, 0.001));
152    }
153
154    #[test]
155    fn approx_eq_exact() {
156        let tol = Tolerance::new();
157        assert!(tol.approx_eq(42.0, 42.0));
158        assert!(tol.approx_eq(0.0, 0.0));
159    }
160
161    #[test]
162    fn approx_eq_scales_with_magnitude() {
163        let tol = Tolerance {
164            linear: 1e-7,
165            angular: 1e-12,
166            relative: 1e-4, // 0.01%
167        };
168        // Near zero: absolute tolerance dominates
169        assert!(tol.approx_eq(0.0, 1e-8));
170        assert!(!tol.approx_eq(0.0, 1e-3));
171
172        // Large values: relative tolerance dominates
173        // 1e6 * 1e-4 = 100 → differences up to 100 are within tolerance
174        assert!(tol.approx_eq(1e6, 1e6 + 50.0));
175        assert!(!tol.approx_eq(1e6, 1e6 + 200.0));
176    }
177
178    #[test]
179    fn approx_eq_abs_ignores_relative() {
180        let tol = Tolerance {
181            linear: 1e-7,
182            angular: 1e-12,
183            relative: 1e-4,
184        };
185        // Even at large scale, abs only uses linear
186        assert!(!tol.approx_eq_abs(1e6, 1e6 + 1.0));
187        assert!(tol.approx_eq_abs(1e6, 1e6 + 1e-8));
188    }
189
190    #[test]
191    fn default_matches_new() {
192        assert_eq!(Tolerance::default(), Tolerance::new());
193    }
194}