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}