diff --git a/crates/prc/src/lib.rs b/crates/prc/src/lib.rs index d594e74065..513b28a683 100644 --- a/crates/prc/src/lib.rs +++ b/crates/prc/src/lib.rs @@ -34,4 +34,4 @@ pub use error::PrcError; pub use geometry::Matrix; pub use schema::Schema; pub use tess::{Tessellation, TriangleMesh, TriangleStyle}; -pub use tree::{EntityOverrides, MAX_PLACEMENTS, ModelNode, NameSource, Placement, Rgba, StyleAlpha}; +pub use tree::{EntityOverrides, MAX_PLACEMENTS, ModelNode, ModelUnits, NameSource, Placement, Rgba, StyleAlpha}; diff --git a/crates/prc/src/tree/mod.rs b/crates/prc/src/tree/mod.rs index 3ed9d8eb20..db2d5352c8 100644 --- a/crates/prc/src/tree/mod.rs +++ b/crates/prc/src/tree/mod.rs @@ -221,6 +221,20 @@ struct Tree { #[derive(Debug, Clone, PartialEq)] struct ModelFile { roots: Vec<(UniqueId, u32)>, + units_in_mm: f64, + units_from_cad_file: bool, +} + +/// The unit a PRC model's coordinates are in, from its model file [WD 7.3.3]. +#[derive(Debug, Clone, Copy, PartialEq)] +#[non_exhaustive] +pub struct ModelUnits { + /// Millimetres per model unit: 1 for millimetres, 25.4 for inches. + pub mm_per_unit: f64, + /// The file says the unit is the CAD system's own. When `false` it is the exporter's + /// default and may not be the model's real scale, so a measurement should say the unit + /// is assumed. + pub from_cad_file: bool, } impl PrcFile { @@ -270,6 +284,22 @@ impl PrcFile { self.model_file.iter().any(|&b| b != 0) } + /// The unit the model's coordinates are in. `None` when the file stores no tree + /// ([`Self::stores_tree`]) or the stored unit is not a positive finite number. + /// + /// # Errors + /// [`PrcError::Malformed`] or a read error when the model file is stored but does not read. + pub fn units(&self) -> Result, PrcError> { + if !self.stores_tree() { + return Ok(None); + } + let mut r = BitReader::with_budget(&self.model_file, Budget::FULL); + let schema = Schema::read(&mut r)?; + let model = Ctx::new(r, &schema, self.header.authoring_version).model_file()?; + let mm = model.units_in_mm; + Ok((mm.is_finite() && mm > 0.0).then_some(ModelUnits { mm_per_unit: mm, from_cad_file: model.units_from_cad_file })) + } + fn walk(&self, rule: StyleAlpha, scope: EntityOverrides) -> Result<(Vec, Vec), PrcError> { self.walk_within(rule, scope, Budget::FULL) } diff --git a/crates/prc/src/tree/read.rs b/crates/prc/src/tree/read.rs index ff3affaeee..f3361dfeef 100644 --- a/crates/prc/src/tree/read.rs +++ b/crates/prc/src/tree/read.rs @@ -437,8 +437,8 @@ impl Ctx<'_, '_> { pub(super) fn model_file(&mut self) -> Result { self.expect_type(MODEL_FILE)?; self.content_prc_base()?; - self.r.bit()?; // units_from_cad_file - self.r.double()?; // units_in_mm + let units_from_cad_file = self.r.bit()?; + let units_in_mm = self.r.double()?; let n = self.count(6, "root occurrences")?; let mut roots = Vec::with_capacity(n); for _ in 0..n { @@ -447,6 +447,6 @@ impl Ctx<'_, '_> { self.r.bit()?; // is_active roots.push((id, index)); } - Ok(ModelFile { roots }) + Ok(ModelFile { roots, units_in_mm, units_from_cad_file }) } } diff --git a/crates/prc/src/tree/tests.rs b/crates/prc/src/tree/tests.rs index a2264588bd..fe9c72aa55 100644 --- a/crates/prc/src/tree/tests.rs +++ b/crates/prc/src/tree/tests.rs @@ -9,7 +9,7 @@ use crate::bits::{BitReader, Budget}; use crate::entity::Ctx; use crate::tess::tests::two_face_square; use crate::testw::W; -use crate::{PrcError, PrcFile, Rgba, Schema, Tessellation, TriangleMesh, UniqueId}; +use crate::{ModelUnits, PrcError, PrcFile, Rgba, Schema, Tessellation, TriangleMesh, UniqueId}; const RED: Rgba = Rgba { r: 1., g: 0., b: 0., a: 1. }; const GREEN: Rgba = Rgba { r: 0., g: 1., b: 0., a: 1. }; @@ -274,10 +274,15 @@ fn ref_faces(brep: (u32, u32), faces: &[u32], g: (u32, u16)) -> W { /// A model file (schema included) whose roots are `(file structure, biased occurrence)`. fn model(roots: &[(usize, u32)]) -> W { + model_in(roots, false, 1.) +} + +/// [`model`] with its unit: whether it is the CAD file's, and millimetres per unit. +fn model_in(roots: &[(usize, u32)], from_cad_file: bool, units_in_mm: f64) -> W { let mut w = W::default(); w.uint(0).uint(301); base(&mut w, None); - w.bit(false).double(1.).uint(len(roots.len())); + w.bit(from_cad_file).double(units_in_mm).uint(len(roots.len())); for &(fs, index) in roots { for v in fs_id(fs).0 { w.uint(v); @@ -391,6 +396,22 @@ fn a_parsed_assembly_places_names_and_colours_each_occurrence() { ); } +#[test] +fn the_model_file_gives_the_unit_and_whether_it_is_assumed() { + let fs = || Fs { globals: palette(), tree: tree(&[plate(S_RED)], &[Occ::default()]), tess: two_face_square() }; + let units = |model: W| PrcFile::parse(&prc(&[fs()], &model)).unwrap().units(); + let unit = |mm_per_unit, from_cad_file| Some(ModelUnits { mm_per_unit, from_cad_file }); + assert_eq!(units(model(&[(0, 1)])).unwrap(), unit(1., false)); + assert_eq!(units(model_in(&[(0, 1)], true, 25.4)).unwrap(), unit(25.4, true)); + for bad in [0., -1., f64::NAN, f64::INFINITY] { + assert_eq!(units(model_in(&[(0, 1)], true, bad)).unwrap(), None, "{bad}"); + } + assert_eq!(units(W::default()).unwrap(), None); + let mut not_a_model = W::default(); + not_a_model.uint(0).uint(302); + assert!(units(not_a_model).is_err()); +} + #[test] fn a_file_without_a_model_file_entity_has_no_tree() { let fs = Fs { globals: palette(), tree: tree(&[], &[]), tess: two_face_square() };