Skip to main content

dotloom_geometry/
tolerance.rs

1//! Tolerance kinds.
2//!
3//! Dotloom keeps four tolerance concepts strictly apart (ADR-0002):
4//!
5//! * [`ModelTolerance`] — when two model values are "the same" (mm, absolute + relative).
6//! * solver tolerance — lives in `dotloom-constraints` (residual acceptance).
7//! * [`FlattenTolerance`] — maximum chord deviation when curves are tessellated.
8//! * [`ScreenTolerance`] — pick/snap radius in CSS pixels, converted to model units
9//!   through the current view scale, never used directly as a model distance.
10
11use serde::{Deserialize, Serialize};
12
13/// Model comparison tolerance: `|a - b| <= max(abs, rel * max(|a|, |b|))`.
14#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
15pub struct ModelTolerance {
16    /// Absolute tolerance in model length units (millimetres).
17    pub abs: f64,
18    /// Relative tolerance, applied to the magnitude of the compared values.
19    pub rel: f64,
20}
21
22impl ModelTolerance {
23    /// Default model tolerance: 1 nm absolute, 1e-12 relative.
24    pub const DEFAULT: Self = Self { abs: 1e-6, rel: 1e-12 };
25
26    /// Whether two scalars are equal within this tolerance.
27    #[must_use]
28    pub fn eq(self, a: f64, b: f64) -> bool {
29        let scale = a.abs().max(b.abs());
30        (a - b).abs() <= self.abs.max(self.rel * scale)
31    }
32
33    /// Whether `v` is zero within the absolute tolerance.
34    #[must_use]
35    pub fn is_zero(self, v: f64) -> bool {
36        v.abs() <= self.abs
37    }
38
39    /// Effective absolute tolerance near magnitude `scale`.
40    #[must_use]
41    pub fn at_scale(self, scale: f64) -> f64 {
42        self.abs.max(self.rel * scale.abs())
43    }
44}
45
46impl Default for ModelTolerance {
47    fn default() -> Self {
48        Self::DEFAULT
49    }
50}
51
52/// Maximum distance (model units) between a curve and its flattened polyline.
53#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
54pub struct FlattenTolerance(pub f64);
55
56impl FlattenTolerance {
57    /// Smallest accepted flatten tolerance; prevents runaway subdivision.
58    pub const MIN: f64 = 1e-9;
59
60    /// Tolerance clamped to a sane positive value.
61    #[must_use]
62    pub fn value(self) -> f64 {
63        if self.0.is_finite() && self.0 > Self::MIN { self.0 } else { Self::MIN.max(1e-3) }
64    }
65
66    /// Tolerance for drawing at `pixels_per_unit` screen scale with `max_px` deviation.
67    #[must_use]
68    pub fn for_screen(pixels_per_unit: f64, max_px: f64) -> Self {
69        if pixels_per_unit.is_finite() && pixels_per_unit > 0.0 { Self(max_px / pixels_per_unit) } else { Self(1e-3) }
70    }
71}
72
73impl Default for FlattenTolerance {
74    fn default() -> Self {
75        Self(0.01)
76    }
77}
78
79/// A screen-space tolerance in CSS pixels (pick radius, snap radius).
80#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
81pub struct ScreenTolerance {
82    /// Radius in CSS pixels (device pixel ratio is already applied by the view).
83    pub css_px: f64,
84}
85
86impl ScreenTolerance {
87    /// Convert to model units given the view's model units per CSS pixel.
88    #[must_use]
89    pub fn to_model(self, model_units_per_css_px: f64) -> f64 {
90        if self.css_px.is_finite() && model_units_per_css_px.is_finite() {
91            (self.css_px * model_units_per_css_px).abs()
92        } else {
93            0.0
94        }
95    }
96}