ifc_alignment/curve/
tolerance.rs1use ifc_model::{EntityId, Model};
36
37use crate::error::{AlignmentError, AlignmentResult};
38use crate::horizontal::AlignmentUnits;
39
40#[derive(Debug, Clone, Copy, PartialEq)]
46#[non_exhaustive]
47pub struct SeamTolerance {
48 length: f64,
50}
51
52impl Default for SeamTolerance {
53 fn default() -> Self {
54 Self::strict()
55 }
56}
57
58impl SeamTolerance {
59 pub const MAX_PRECISION: f64 = 1e-3;
64
65 #[must_use]
67 pub fn strict() -> Self {
68 Self { length: 0.0 }
69 }
70
71 pub fn from_precision(precision_metres: f64) -> AlignmentResult<Self> {
79 if !precision_metres.is_finite() || precision_metres < 0.0 {
80 return Err(AlignmentError::InvalidUnits {
81 detail: "seam precision must be finite and non-negative",
82 });
83 }
84 Ok(Self {
85 length: precision_metres.min(Self::MAX_PRECISION),
86 })
87 }
88
89 pub fn for_model(model: &Model, units: AlignmentUnits) -> AlignmentResult<Self> {
102 if !units.length_to_metres.is_finite() || units.length_to_metres <= 0.0 {
103 return Err(AlignmentError::InvalidUnits {
104 detail: "length factor must be finite and positive",
105 });
106 }
107 let mut coarsest: Option<f64> = None;
108 for (id, entity) in model.iter() {
109 if !entity
112 .type_name
113 .eq_ignore_ascii_case("IFCGEOMETRICREPRESENTATIONCONTEXT")
114 {
115 continue;
116 }
117 if let Some(precision) = declared_3d_precision(id, &entity.attributes)? {
118 let metres = precision * units.length_to_metres;
119 coarsest = Some(coarsest.map_or(metres, |c: f64| c.max(metres)));
120 }
121 }
122 coarsest.map_or(Ok(Self::strict()), Self::from_precision)
123 }
124
125 #[must_use]
127 pub fn length(&self) -> f64 {
128 self.length
129 }
130
131 pub(crate) fn same_length(&self, left: f64, right: f64) -> bool {
133 (left - right).abs() <= rounding(left, right).max(self.length)
134 }
135
136 pub(crate) fn same_gradient(&self, left: f64, right: f64) -> bool {
139 (left - right).abs() <= rounding(left, right)
140 }
141}
142
143fn rounding(left: f64, right: f64) -> f64 {
145 1e-9 * left.abs().max(right.abs()).max(1.0)
146}
147
148fn declared_3d_precision(
154 id: EntityId,
155 attributes: &[ifc_model::Value],
156) -> AlignmentResult<Option<f64>> {
157 const DIMENSION: usize = 2;
158 const PRECISION: usize = 3;
159 let dimension = attributes
160 .get(DIMENSION)
161 .and_then(|value| value.unwrap_typed().as_f64());
162 if dimension != Some(3.0) {
163 return Ok(None);
164 }
165 match attributes.get(PRECISION) {
166 None | Some(ifc_model::Value::Null | ifc_model::Value::Derived) => Ok(None),
167 Some(value) => match value.unwrap_typed().as_f64() {
168 Some(precision) if precision.is_finite() && precision > 0.0 => Ok(Some(precision)),
169 _ => Err(AlignmentError::InvalidAttribute {
170 entity: id,
171 index: PRECISION,
172 name: "Precision",
173 }),
174 },
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use ifc_model::{Entity, Value};
182 use std::sync::Arc;
183
184 fn metres() -> AlignmentUnits {
185 AlignmentUnits {
186 length_to_metres: 1.0,
187 angle_to_radians: 1.0,
188 }
189 }
190
191 fn context(model: &mut Model, id: u64, dimension: i64, precision: Value) {
192 model.insert(
193 EntityId(id),
194 Entity::new(
195 "IFCGEOMETRICREPRESENTATIONCONTEXT",
196 vec![
197 Value::Null,
198 Value::Text(Arc::from("Model")),
199 Value::Integer(dimension),
200 precision,
201 Value::Null,
202 Value::Null,
203 ],
204 ),
205 );
206 }
207
208 #[test]
209 fn no_declared_precision_is_strict() {
210 let mut model = Model::new();
211 context(&mut model, 1, 3, Value::Null);
212 assert_eq!(
213 SeamTolerance::for_model(&model, metres()),
214 Ok(SeamTolerance::strict())
215 );
216 }
217
218 #[test]
219 fn the_coarsest_3d_precision_applies_and_2d_contexts_are_ignored() {
220 let mut model = Model::new();
221 context(&mut model, 1, 3, Value::Real(1e-6));
222 context(&mut model, 2, 3, Value::Real(1e-4));
223 context(&mut model, 3, 2, Value::Real(1e-2));
224 let tolerance = SeamTolerance::for_model(&model, metres()).expect("tolerance");
225 assert_eq!(tolerance.length(), 1e-4);
226 }
227
228 #[test]
229 fn precision_is_converted_from_the_project_length_unit() {
230 let mut model = Model::new();
231 context(&mut model, 1, 3, Value::Real(0.1));
233 let millimetres = AlignmentUnits {
234 length_to_metres: 0.001,
235 angle_to_radians: 1.0,
236 };
237 let tolerance = SeamTolerance::for_model(&model, millimetres).expect("tolerance");
238 assert!((tolerance.length() - 1e-4).abs() < 1e-18);
239 }
240
241 #[test]
242 fn a_coarse_precision_is_capped_at_one_millimetre() {
243 let mut model = Model::new();
244 context(&mut model, 1, 3, Value::Real(0.5));
245 let tolerance = SeamTolerance::for_model(&model, metres()).expect("tolerance");
246 assert_eq!(tolerance.length(), SeamTolerance::MAX_PRECISION);
247 assert!(!tolerance.same_length(52.0, 52.002));
248 }
249
250 #[test]
251 fn a_non_positive_precision_is_refused_by_name() {
252 let mut model = Model::new();
253 context(&mut model, 7, 3, Value::Real(-1e-5));
254 assert_eq!(
255 SeamTolerance::for_model(&model, metres()),
256 Err(AlignmentError::InvalidAttribute {
257 entity: EntityId(7),
258 index: 3,
259 name: "Precision",
260 })
261 );
262 }
263
264 #[test]
265 fn gradients_ignore_the_length_precision() {
266 let tolerance = SeamTolerance::from_precision(1e-4).expect("tolerance");
267 assert!(tolerance.same_length(100.0, 100.00009));
268 assert!(!tolerance.same_gradient(0.02, 0.02009));
269 }
270}