use ifc_model::{EntityId, Model};
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::AlignmentUnits;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct SeamTolerance {
length: f64,
}
impl Default for SeamTolerance {
fn default() -> Self {
Self::strict()
}
}
impl SeamTolerance {
pub const MAX_PRECISION: f64 = 1e-3;
#[must_use]
pub fn strict() -> Self {
Self { length: 0.0 }
}
pub fn from_precision(precision_metres: f64) -> AlignmentResult<Self> {
if !precision_metres.is_finite() || precision_metres < 0.0 {
return Err(AlignmentError::InvalidUnits {
detail: "seam precision must be finite and non-negative",
});
}
Ok(Self {
length: precision_metres.min(Self::MAX_PRECISION),
})
}
pub fn for_model(model: &Model, units: AlignmentUnits) -> AlignmentResult<Self> {
if !units.length_to_metres.is_finite() || units.length_to_metres <= 0.0 {
return Err(AlignmentError::InvalidUnits {
detail: "length factor must be finite and positive",
});
}
let mut coarsest: Option<f64> = None;
for (id, entity) in model.iter() {
if !entity
.type_name
.eq_ignore_ascii_case("IFCGEOMETRICREPRESENTATIONCONTEXT")
{
continue;
}
if let Some(precision) = declared_3d_precision(id, &entity.attributes)? {
let metres = precision * units.length_to_metres;
coarsest = Some(coarsest.map_or(metres, |c: f64| c.max(metres)));
}
}
coarsest.map_or(Ok(Self::strict()), Self::from_precision)
}
#[must_use]
pub fn length(&self) -> f64 {
self.length
}
pub(crate) fn same_length(&self, left: f64, right: f64) -> bool {
(left - right).abs() <= rounding(left, right).max(self.length)
}
pub(crate) fn same_gradient(&self, left: f64, right: f64) -> bool {
(left - right).abs() <= rounding(left, right)
}
}
fn rounding(left: f64, right: f64) -> f64 {
1e-9 * left.abs().max(right.abs()).max(1.0)
}
fn declared_3d_precision(
id: EntityId,
attributes: &[ifc_model::Value],
) -> AlignmentResult<Option<f64>> {
const DIMENSION: usize = 2;
const PRECISION: usize = 3;
let dimension = attributes
.get(DIMENSION)
.and_then(|value| value.unwrap_typed().as_f64());
if dimension != Some(3.0) {
return Ok(None);
}
match attributes.get(PRECISION) {
None | Some(ifc_model::Value::Null | ifc_model::Value::Derived) => Ok(None),
Some(value) => match value.unwrap_typed().as_f64() {
Some(precision) if precision.is_finite() && precision > 0.0 => Ok(Some(precision)),
_ => Err(AlignmentError::InvalidAttribute {
entity: id,
index: PRECISION,
name: "Precision",
}),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::{Entity, Value};
use std::sync::Arc;
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
}
}
fn context(model: &mut Model, id: u64, dimension: i64, precision: Value) {
model.insert(
EntityId(id),
Entity::new(
"IFCGEOMETRICREPRESENTATIONCONTEXT",
vec![
Value::Null,
Value::Text(Arc::from("Model")),
Value::Integer(dimension),
precision,
Value::Null,
Value::Null,
],
),
);
}
#[test]
fn no_declared_precision_is_strict() {
let mut model = Model::new();
context(&mut model, 1, 3, Value::Null);
assert_eq!(
SeamTolerance::for_model(&model, metres()),
Ok(SeamTolerance::strict())
);
}
#[test]
fn the_coarsest_3d_precision_applies_and_2d_contexts_are_ignored() {
let mut model = Model::new();
context(&mut model, 1, 3, Value::Real(1e-6));
context(&mut model, 2, 3, Value::Real(1e-4));
context(&mut model, 3, 2, Value::Real(1e-2));
let tolerance = SeamTolerance::for_model(&model, metres()).expect("tolerance");
assert_eq!(tolerance.length(), 1e-4);
}
#[test]
fn precision_is_converted_from_the_project_length_unit() {
let mut model = Model::new();
context(&mut model, 1, 3, Value::Real(0.1));
let millimetres = AlignmentUnits {
length_to_metres: 0.001,
angle_to_radians: 1.0,
};
let tolerance = SeamTolerance::for_model(&model, millimetres).expect("tolerance");
assert!((tolerance.length() - 1e-4).abs() < 1e-18);
}
#[test]
fn a_coarse_precision_is_capped_at_one_millimetre() {
let mut model = Model::new();
context(&mut model, 1, 3, Value::Real(0.5));
let tolerance = SeamTolerance::for_model(&model, metres()).expect("tolerance");
assert_eq!(tolerance.length(), SeamTolerance::MAX_PRECISION);
assert!(!tolerance.same_length(52.0, 52.002));
}
#[test]
fn a_non_positive_precision_is_refused_by_name() {
let mut model = Model::new();
context(&mut model, 7, 3, Value::Real(-1e-5));
assert_eq!(
SeamTolerance::for_model(&model, metres()),
Err(AlignmentError::InvalidAttribute {
entity: EntityId(7),
index: 3,
name: "Precision",
})
);
}
#[test]
fn gradients_ignore_the_length_precision() {
let tolerance = SeamTolerance::from_precision(1e-4).expect("tolerance");
assert!(tolerance.same_length(100.0, 100.00009));
assert!(!tolerance.same_gradient(0.02, 0.02009));
}
}