solidcraft-step: representation context units and uncertainty, shared with the kernel's STEP reader

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brennen Herbruck
2026-10-21 01:23:02 -04:00
committed by jyviko
parent 4983b88722
commit acff340a04
3 changed files with 128 additions and 150 deletions
+12 -150
View File
@@ -88,10 +88,6 @@ pub struct StepImport {
pub schema: String,
/// FILE_NAME originating system.
pub system: String,
/// FILE_NAME preprocessor version.
pub preprocessor: String,
/// FILE_DESCRIPTION text.
pub description: String,
}
/// A solid the walk found, converted later.
@@ -118,9 +114,6 @@ pub(crate) struct Ctx<'a> {
pub ang: f64,
/// Vertex-on-curve tolerance (mm).
pub tol: f64,
/// Farthest an edge's vertex may sit off its curve and still be moved onto it (mm, at least
/// `tol`): see [`snap_limit`].
pub snap: f64,
}
#[derive(Clone, Copy)]
@@ -128,115 +121,21 @@ struct Units {
len: f64,
ang: f64,
tol: f64,
snap: f64,
}
const DEFAULT_UNITS: Units = Units { len: 1.0, ang: 1.0, tol: 0.01, snap: 0.01 };
const DEFAULT_UNITS: Units = Units { len: 1.0, ang: 1.0, tol: 0.01 };
/// Largest tolerance a file can ask for (mm), however coarse the accuracy it declares.
const MAX_TOL: f64 = 1.0;
/// Gap between an edge's vertex and its curve that is still read as the same point, as a share
/// of the model's size: the chord error the kernel's own coarse meshes allow at that size
/// (`Body::size() * 1e-3`), so moving the vertex onto the curve changes the shape by less than
/// those meshes already do.
const SNAP_SHARE: f64 = 1e-3;
/// Farthest a vertex may sit off its edge's curve and be moved onto it (mm): the file's own
/// tolerance `tol`, or up to [`SNAP_SHARE`] of the model's size `extent` (mm) when that is
/// larger. Some exporters write vertices further off than the accuracy they declare (a surface
/// export may declare 0.01 mm and join two curves 0.38 mm apart at a vertex between them).
/// Never more than [`MAX_TOL`], so a far stray point cannot turn the check off.
fn snap_limit(tol: f64, extent: f64) -> f64 {
let share = if extent.is_finite() { extent * SNAP_SHARE } else { 0.0 };
tol.max(share.min(MAX_TOL))
}
fn si_prefix(p: Option<&Param>) -> f64 {
match p.and_then(Param::as_enum) {
Some("EXA") => 1e18,
Some("PETA") => 1e15,
Some("TERA") => 1e12,
Some("GIGA") => 1e9,
Some("MEGA") => 1e6,
Some("KILO") => 1e3,
Some("HECTO") => 1e2,
Some("DECA") => 1e1,
Some("DECI") => 1e-1,
Some("CENTI") => 1e-2,
Some("MILLI") => 1e-3,
Some("MICRO") => 1e-6,
Some("NANO") => 1e-9,
Some("PICO") => 1e-12,
Some("FEMTO") => 1e-15,
Some("ATTO") => 1e-18,
_ => 1.0,
}
}
/// A unit's size: millimetres for lengths, radians for plane angles.
fn unit_factor(ex: &Exchange, id: u64, depth: usize) -> Option<(&'static str, f64)> {
if depth > 4 {
return None;
}
let e = ex.get(id)?;
let kind = if e.has("LENGTH_UNIT") {
"length"
} else if e.has("PLANE_ANGLE_UNIT") {
"angle"
} else {
"other"
};
if let Some(si) = e.record("SI_UNIT") {
let base = match si.get(1).and_then(Param::as_enum) {
Some("METRE") => 1000.0,
Some("RADIAN") => 1.0,
_ => return Some((kind, 1.0)),
};
return Some((kind, si_prefix(si.first()) * base));
}
if let Some(cb) = e.record("CONVERSION_BASED_UNIT") {
let m = ex.get(cb.get(1)?.as_ref_id()?)?;
let mp = m.params();
let v = mp.first()?.as_f64()?;
let (_, f) = unit_factor(ex, mp.get(1)?.as_ref_id()?, depth + 1)?;
let x = v * f;
return (x.is_finite() && x > 0.0).then_some((kind, x));
}
Some((kind, 1.0))
}
/// Units of a representation context (GLOBAL_UNIT_ASSIGNED_CONTEXT, uncertainty).
/// Units of a representation context (GLOBAL_UNIT_ASSIGNED_CONTEXT, uncertainty), read by
/// [`solidcraft_step::units::context`]; the vertex-on-curve tolerance is ten times the
/// uncertainty (at least 1e-3 mm), else a hundredth of the length unit, within [1e-4, 1] mm.
fn context_units(ex: &Exchange, ctx: Option<u64>, cache: &mut HashMap<u64, Units>) -> Units {
let Some(id) = ctx else { return DEFAULT_UNITS };
if let Some(u) = cache.get(&id) {
return *u;
}
let mut u = DEFAULT_UNITS;
let mut tol_file = None;
if let Some(e) = ex.get(id) {
if let Some(units) = e.record("GLOBAL_UNIT_ASSIGNED_CONTEXT").and_then(|r| r.first()).and_then(Param::as_list) {
for uid in units.iter().filter_map(Param::as_ref_id) {
match unit_factor(ex, uid, 0) {
Some(("length", f)) if f.is_finite() && f > 0.0 => u.len = f,
Some(("angle", f)) if f.is_finite() && f > 0.0 => u.ang = f,
_ => {}
}
}
}
if let Some(unc) = e.record("GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT").and_then(|r| r.first()).and_then(Param::as_list) {
for uid in unc.iter().filter_map(Param::as_ref_id) {
if let Some(m) = ex.get(uid)
&& let Some(v) = m.params().first().and_then(Param::as_f64)
{
let f = m.params().get(1).and_then(Param::as_ref_id).and_then(|x| unit_factor(ex, x, 0)).map(|x| x.1);
tol_file = Some(v * f.unwrap_or(u.len));
}
}
}
}
u.tol = tol_file.filter(|t| t.is_finite() && *t > 0.0).map(|t| (t * 10.0).max(1e-3)).unwrap_or(u.len * 1e-2).clamp(1e-4, MAX_TOL);
u.snap = snap_limit(u.tol, ex.vertex_extent() * u.len);
let c = solidcraft_step::units::context(ex, id);
let tol = c.uncertainty.map(|t| (t * 10.0).max(1e-3)).unwrap_or(c.units.len * 1e-2).clamp(1e-4, 1.0);
let u = Units { len: c.units.len, ang: c.units.ang, tol };
cache.insert(id, u);
u
}
@@ -588,41 +487,12 @@ impl Reader {
}
fn placement_matrix(&mut self, item: u64, u: Units) -> R<mt::Matrix4> {
let cx = Ctx { ex: &self.ex, len: u.len, ang: u.ang, tol: u.tol, snap: u.snap };
let cx = Ctx { ex: &self.ex, len: u.len, ang: u.ang, tol: u.tol };
let e = geom::entity(&cx, item)?;
if matches!(e.name(), "CARTESIAN_TRANSFORMATION_OPERATOR_3D" | "CARTESIAN_TRANSFORMATION_OPERATOR_3D_NON_UNIFORM") {
if e.name() == "CARTESIAN_TRANSFORMATION_OPERATOR_3D" {
let p = e.params();
let o = geom::point(&cx, p.get(4).and_then(Param::as_ref_id).ok_or("transformation without origin")?)?;
let direction = |i: usize, fallback: mt::Vector3| -> R<mt::Vector3> {
match p.get(i) {
None | Some(Param::Null) => Ok(fallback),
Some(Param::Ref(id)) => geom::direction(&cx, *id),
_ => Err("invalid transformation direction".into()),
}
};
let scale = |i: usize, fallback: f64| -> R<f64> {
match p.get(i) {
None | Some(Param::Null) => Ok(fallback),
Some(v) => v.as_f64().filter(|x| x.is_finite() && *x != 0.0).ok_or_else(|| "invalid transformation scale".into()),
}
};
let sx = scale(5, 1.0)?;
let (sy, sz) = (scale(7, sx)?, scale(8, sx)?);
let m = mt::Matrix4::from_cols(
(direction(2, mt::Vector3::unit_x())? * sx).extend(0.0),
(direction(3, mt::Vector3::unit_y())? * sy).extend(0.0),
(direction(6, mt::Vector3::unit_z())? * sz).extend(0.0),
o.to_homogeneous(),
);
if !to_array(&m).iter().flatten().all(|x| x.is_finite()) {
return Err("invalid transformation scale".into());
}
use mt::SquareMatrix as _;
let det = m.determinant();
if !det.is_finite() || det == 0.0 {
return Err("singular transformation".into());
}
return Ok(m);
return Ok(mt::Matrix4::from_translation(o.to_homogeneous().truncate()));
}
Ok(geom::frame(&cx, item)?.matrix())
}
@@ -789,7 +659,7 @@ impl Reader {
};
let placement = self.batches.get(p.batch).map_or(mt::Matrix4::from_scale(1.0), |b| b.m);
sources.push((i, item));
jobs.push(parallel::Job { source, placement, check_mesh: !self.unmeshed_checked.contains(&item), tol: units.tol });
jobs.push(parallel::Job { source, placement, check_mesh: !self.unmeshed_checked.contains(&item) });
}
// Reading and orienting new parts is shared out between the workers.
let prepared = parallel::bodies(&ex, &jobs, self.workers);
@@ -1322,15 +1192,7 @@ impl StepReading {
return Err(KernelError::Failed(format!("STEP import: no bodies ({why})")));
}
let bodies = std::mem::take(&mut rd.bodies);
Ok(StepImport {
bodies,
tree,
warnings,
schema: ex.schemas.join(", "),
system: ex.originating_system.clone(),
preprocessor: ex.preprocessor.clone(),
description: ex.description.clone(),
})
Ok(StepImport { bodies, tree, warnings, schema: ex.schemas.join(", "), system: ex.originating_system.clone() })
}
/// [`Self::finish`], and the import is cached for the next read of the same text.
+1
View File
@@ -10,6 +10,7 @@ mod eval;
pub mod geom;
pub mod p21;
pub mod read;
pub mod units;
pub mod write;
#[cfg(test)]
+115
View File
@@ -0,0 +1,115 @@
//! The units of a representation context (ISO 10303-41 measures and units, ISO 10303-43
//! representation contexts): GLOBAL_UNIT_ASSIGNED_CONTEXT's length and plane angle units and
//! GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT's distance uncertainty. Missing or malformed units read
//! as millimetres and radians; nothing here fails.
use crate::p21::{Exchange, Param};
use crate::read::Units;
/// What a representation context says about its numbers.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Context {
pub units: Units,
/// The context's distance uncertainty in millimetres (the last UNCERTAINTY_MEASURE_WITH_UNIT
/// it lists), if any.
pub uncertainty: Option<f64>,
}
impl Default for Context {
fn default() -> Self {
Context { units: Units::default(), uncertainty: None }
}
}
fn si_prefix(p: Option<&Param>) -> f64 {
match p.and_then(Param::as_enum) {
Some("EXA") => 1e18,
Some("PETA") => 1e15,
Some("TERA") => 1e12,
Some("GIGA") => 1e9,
Some("MEGA") => 1e6,
Some("KILO") => 1e3,
Some("HECTO") => 1e2,
Some("DECA") => 1e1,
Some("DECI") => 1e-1,
Some("CENTI") => 1e-2,
Some("MILLI") => 1e-3,
Some("MICRO") => 1e-6,
Some("NANO") => 1e-9,
Some("PICO") => 1e-12,
Some("FEMTO") => 1e-15,
Some("ATTO") => 1e-18,
_ => 1.0,
}
}
/// What a unit measures.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnitKind {
Length,
Angle,
Other,
}
/// A unit's kind and size: millimetres for lengths, radians for plane angles (1 for anything
/// else, or a unit that cannot be read).
pub fn unit_factor(ex: &Exchange, id: u64) -> Option<(UnitKind, f64)> {
factor(ex, id, 0)
}
fn factor(ex: &Exchange, id: u64, depth: usize) -> Option<(UnitKind, f64)> {
if depth > 4 {
return None;
}
let e = ex.get(id)?;
let kind = if e.has("LENGTH_UNIT") {
UnitKind::Length
} else if e.has("PLANE_ANGLE_UNIT") {
UnitKind::Angle
} else {
UnitKind::Other
};
if let Some(si) = e.record("SI_UNIT") {
let base = match si.get(1).and_then(Param::as_enum) {
Some("METRE") => 1000.0,
Some("RADIAN") => 1.0,
_ => return Some((kind, 1.0)),
};
return Some((kind, si_prefix(si.first()) * base));
}
if let Some(cb) = e.record("CONVERSION_BASED_UNIT") {
let m = ex.get(cb.get(1)?.as_ref_id()?)?;
let mp = m.params();
let v = mp.first()?.as_f64()?;
let (_, f) = factor(ex, mp.get(1)?.as_ref_id()?, depth + 1)?;
let x = v * f;
return (x.is_finite() && x > 0.0).then_some((kind, x));
}
Some((kind, 1.0))
}
/// The units and uncertainty of the representation context `id`.
pub fn context(ex: &Exchange, id: u64) -> Context {
let mut c = Context::default();
let Some(e) = ex.get(id) else { return c };
if let Some(units) = e.record("GLOBAL_UNIT_ASSIGNED_CONTEXT").and_then(|r| r.first()).and_then(Param::as_list) {
for uid in units.iter().filter_map(Param::as_ref_id) {
match unit_factor(ex, uid) {
Some((UnitKind::Length, f)) if f.is_finite() && f > 0.0 => c.units.len = f,
Some((UnitKind::Angle, f)) if f.is_finite() && f > 0.0 => c.units.ang = f,
_ => {}
}
}
}
if let Some(unc) = e.record("GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT").and_then(|r| r.first()).and_then(Param::as_list) {
for uid in unc.iter().filter_map(Param::as_ref_id) {
if let Some(m) = ex.get(uid)
&& let Some(v) = m.params().first().and_then(Param::as_f64)
{
let f = m.params().get(1).and_then(Param::as_ref_id).and_then(|x| unit_factor(ex, x)).map(|x| x.1);
c.uncertainty = Some(v * f.unwrap_or(c.units.len)).filter(|t| t.is_finite() && *t > 0.0);
}
}
}
c
}