1use crate::model::Point3;
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct Tolerance {
15 pub point_eps: f64,
17 pub distance_eps: f64,
19 pub area_eps: f64,
21 pub angle_eps: f64,
23 pub sample_eps: f64,
25}
26
27impl Default for Tolerance {
28 fn default() -> Self {
29 Self {
30 point_eps: 1e-6,
31 distance_eps: 1e-6,
32 area_eps: 1e-10,
33 angle_eps: 1e-9,
34 sample_eps: 1e-4,
35 }
36 }
37}
38
39impl Tolerance {
40 pub const REFERENCE_DIAGONAL: f64 = 1000.0;
42 pub const MAX_SCALE: f64 = 1e3;
44
45 pub fn scaled_for_diagonal(diagonal: f64) -> Self {
50 let base = Self::default();
51 if !(diagonal > 0.0) || !diagonal.is_finite() {
52 return base;
53 }
54 let s = (diagonal / Self::REFERENCE_DIAGONAL).clamp(1.0 / Self::MAX_SCALE, Self::MAX_SCALE);
55 Self {
56 point_eps: base.point_eps * s,
57 distance_eps: base.distance_eps * s,
58 area_eps: base.area_eps * s * s,
59 angle_eps: base.angle_eps,
60 sample_eps: base.sample_eps * s,
61 }
62 }
63
64 pub fn validate(&self) -> Result<(), NumericError> {
66 let fields = [
67 ("pointEps", self.point_eps),
68 ("distanceEps", self.distance_eps),
69 ("areaEps", self.area_eps),
70 ("angleEps", self.angle_eps),
71 ("sampleEps", self.sample_eps),
72 ];
73 for (name, value) in fields {
74 if !value.is_finite() {
75 return Err(NumericError::NonFinite { field: name });
76 }
77 if value <= 0.0 {
78 return Err(NumericError::NotPositive { field: name });
79 }
80 }
81 Ok(())
82 }
83
84 pub fn points_equal(&self, a: Point3, b: Point3) -> bool {
85 a.distance(b) <= self.point_eps
86 }
87
88 pub fn distances_equal(&self, a: f64, b: f64) -> bool {
89 (a - b).abs() <= self.distance_eps
90 }
91
92 pub fn distance_less(&self, a: f64, b: f64) -> bool {
94 a < b - self.distance_eps
95 }
96
97 pub fn is_zero_area(&self, area: f64) -> bool {
98 area.abs() <= self.area_eps
99 }
100
101 pub fn parallel(&self, a: Point3, b: Point3) -> bool {
103 a.cross(b).length() <= self.angle_eps
104 }
105
106 pub fn perpendicular(&self, a: Point3, b: Point3) -> bool {
108 a.dot(b).abs() <= self.angle_eps
109 }
110
111 pub fn is_vertical(&self, dir: Point3) -> bool {
113 self.parallel(dir, Point3::Z)
114 }
115
116 pub fn is_horizontal(&self, dir: Point3) -> bool {
118 self.perpendicular(dir, Point3::Z)
119 }
120
121 pub fn span_too_short(&self, length: f64) -> bool {
123 length < self.sample_eps
124 }
125}
126
127#[derive(Clone, Debug, PartialEq, Eq)]
129pub enum NumericError {
130 NonFinite { field: &'static str },
131 NotPositive { field: &'static str },
132}
133
134impl std::fmt::Display for NumericError {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 match self {
137 NumericError::NonFinite { field } => write!(f, "tolerance {field} is not finite"),
138 NumericError::NotPositive { field } => write!(f, "tolerance {field} must be positive"),
139 }
140 }
141}
142
143impl std::error::Error for NumericError {}
144
145pub fn finite_point(p: Point3) -> bool {
147 p.is_finite()
148}
149