Skip to main content

step_io/scene/
units.rs

1//! The model's units — the length and angle units and precision a kernel
2//! needs to interpret coordinates.
3//!
4//! [`Scene::units`] reports them together. Each [`Unit`] carries a
5//! [`to_si`](Unit::to_si) factor — to metres or radians — so a consumer
6//! multiplies the file's raw coordinates by it; a field is `None` where the
7//! model gives no such unit.
8
9use crate::StepModel;
10use crate::generated::model as m;
11use crate::scene::Scene;
12
13/// The model's unit context. Fields are `None` when the model carries no such
14/// unit (or no unit context at all).
15#[derive(Clone, Debug, Default)]
16pub struct Units {
17    pub length: Option<Unit>,
18    pub angle: Option<Unit>,
19    /// Modelling precision (uncertainty), in the length unit.
20    pub precision: Option<f64>,
21}
22
23/// A single unit: a display name and the factor to multiply a value by to reach
24/// the SI base unit (metre for length, radian for angle).
25#[derive(Clone, Debug)]
26pub struct Unit {
27    pub name: String,
28    pub to_si: f64,
29}
30
31#[derive(Clone, Copy, PartialEq, Eq)]
32enum UnitKind {
33    Length,
34    PlaneAngle,
35    Other,
36}
37
38impl Scene<'_> {
39    /// The model's length / angle units and precision.
40    ///
41    /// Picks the first unit context found; a model normally has one global
42    /// context. Units it cannot classify (or a missing context) come back as
43    /// `None` fields rather than an error.
44    #[must_use]
45    pub fn units(&self) -> Units {
46        units_of(self.model())
47    }
48}
49
50/// Resolve a model's units (delegated to by [`Scene::units`]; also reused by the
51/// NURBS layer to read the plane-angle unit).
52pub(crate) fn units_of(model: &StepModel) -> Units {
53    let ctx = find_unit_context(model);
54
55    let unit_refs: &[m::UnitRef] = ctx
56        .and_then(|cu| {
57            cu.parts.iter().find_map(|p| match p {
58                m::UnitPart::GlobalUnitAssignedContext { units } => Some(units.as_slice()),
59                _ => None,
60            })
61        })
62        .or_else(|| {
63            model
64                .global_unit_assigned_context_arena
65                .items
66                .first()
67                .map(|g| g.units.as_slice())
68        })
69        .unwrap_or(&[]);
70
71    let mut out = Units::default();
72    for ur in unit_refs {
73        if let Some((kind, unit)) = classify_unit(model, ur) {
74            match kind {
75                UnitKind::Length if out.length.is_none() => out.length = Some(unit),
76                UnitKind::PlaneAngle if out.angle.is_none() => out.angle = Some(unit),
77                _ => {}
78            }
79        }
80    }
81    out.precision = ctx
82        .and_then(|cu| {
83            cu.parts.iter().find_map(|p| match p {
84                m::UnitPart::GlobalUncertaintyAssignedContext { uncertainty } => {
85                    Some(uncertainty.as_slice())
86                }
87                _ => None,
88            })
89        })
90        .and_then(|u| u.first())
91        .map(
92            |m::UncertaintyMeasureWithUnitRef::UncertaintyMeasureWithUnit(id)| {
93                model
94                    .uncertainty_measure_with_unit_arena
95                    .get(id.0)
96                    .value_component
97                    .value
98                    .as_f64()
99            },
100        );
101    out
102}
103
104/// The first complex instance carrying a `GLOBAL_UNIT_ASSIGNED_CONTEXT` facet —
105/// the model's unit context (it also carries the uncertainty facet).
106fn find_unit_context(model: &StepModel) -> Option<&m::ComplexUnit> {
107    model.complex_unit_arena.items.iter().find(|cu| {
108        cu.parts
109            .iter()
110            .any(|p| matches!(p, m::UnitPart::GlobalUnitAssignedContext { .. }))
111    })
112}
113
114/// Classify a unit reference (SI, conversion-based, or complex-wrapped) into its
115/// kind and SI factor.
116fn classify_unit(model: &StepModel, ur: &m::UnitRef) -> Option<(UnitKind, Unit)> {
117    match ur {
118        m::UnitRef::SiUnit(id) => {
119            let si = model.si_unit_arena.get(id.0);
120            Some(classify_si(si.prefix, si.name))
121        }
122        m::UnitRef::ConversionBasedUnit(id) => {
123            let cbu = model.conversion_based_unit_arena.get(id.0);
124            classify_cbu(model, &cbu.name, &cbu.conversion_factor)
125        }
126        m::UnitRef::Complex(cuid) => {
127            let cu = model.complex_unit_arena.get(cuid.0);
128            cu.parts.iter().find_map(|p| match p {
129                m::UnitPart::SiUnit { prefix, name } => Some(classify_si(*prefix, *name)),
130                m::UnitPart::ConversionBasedUnit {
131                    name,
132                    conversion_factor,
133                } => classify_cbu(model, name, conversion_factor),
134                _ => None,
135            })
136        }
137        _ => None,
138    }
139}
140
141/// An SI unit's kind and factor: the kind from the base name, the factor from
142/// the decimal prefix (base SI is 1.0).
143fn classify_si(prefix: Option<m::SiPrefix>, name: m::SiUnitName) -> (UnitKind, Unit) {
144    let kind = match name {
145        m::SiUnitName::Metre => UnitKind::Length,
146        m::SiUnitName::Radian => UnitKind::PlaneAngle,
147        _ => UnitKind::Other,
148    };
149    let display = format!(
150        "{}{:?}",
151        prefix.map(|p| format!("{p:?}")).unwrap_or_default(),
152        name
153    )
154    .to_uppercase();
155    (
156        kind,
157        Unit {
158            name: display,
159            to_si: si_prefix_factor(prefix),
160        },
161    )
162}
163
164/// A conversion-based unit (e.g. inch, degree): its factor is the conversion
165/// value times the base unit's own factor to SI; its kind comes from the base.
166fn classify_cbu(
167    model: &StepModel,
168    name: &str,
169    factor: &m::MeasureWithUnitRef,
170) -> Option<(UnitKind, Unit)> {
171    let (value, base) = measure_with_unit(model, factor)?;
172    let (kind, base_unit) = classify_unit(model, base)?;
173    Some((
174        kind,
175        Unit {
176            name: name.to_uppercase(),
177            to_si: value * base_unit.to_si,
178        },
179    ))
180}
181
182/// Resolve a `MEASURE_WITH_UNIT` (or a length/angle subtype) to its value and
183/// the unit it is expressed in.
184fn measure_with_unit<'m>(
185    model: &'m StepModel,
186    r: &'m m::MeasureWithUnitRef,
187) -> Option<(f64, &'m m::UnitRef)> {
188    let (value, unit) = match r {
189        m::MeasureWithUnitRef::MeasureWithUnit(i) => {
190            let e = model.measure_with_unit_arena.get(i.0);
191            (&e.value_component, &e.unit_component)
192        }
193        m::MeasureWithUnitRef::LengthMeasureWithUnit(i) => {
194            let e = model.length_measure_with_unit_arena.get(i.0);
195            (&e.value_component, &e.unit_component)
196        }
197        m::MeasureWithUnitRef::PlaneAngleMeasureWithUnit(i) => {
198            let e = model.plane_angle_measure_with_unit_arena.get(i.0);
199            (&e.value_component, &e.unit_component)
200        }
201        _ => return None,
202    };
203    Some((value.value.as_f64(), unit))
204}
205
206/// Decimal SI prefix as a multiplier (no prefix = 1.0).
207fn si_prefix_factor(prefix: Option<m::SiPrefix>) -> f64 {
208    use m::SiPrefix::{
209        Atto, Centi, Deca, Deci, Exa, Femto, Giga, Hecto, Kilo, Mega, Micro, Milli, Nano, Peta,
210        Pico, Tera,
211    };
212    match prefix {
213        None => 1.0,
214        Some(Exa) => 1e18,
215        Some(Peta) => 1e15,
216        Some(Tera) => 1e12,
217        Some(Giga) => 1e9,
218        Some(Mega) => 1e6,
219        Some(Kilo) => 1e3,
220        Some(Hecto) => 1e2,
221        Some(Deca) => 1e1,
222        Some(Deci) => 1e-1,
223        Some(Centi) => 1e-2,
224        Some(Milli) => 1e-3,
225        Some(Micro) => 1e-6,
226        Some(Nano) => 1e-9,
227        Some(Pico) => 1e-12,
228        Some(Femto) => 1e-15,
229        Some(Atto) => 1e-18,
230    }
231}