feat(prc): PrcFile::units, the model's unit from its model file (#6919)

The model file's units_in_mm and units_from_cad_file were read and
dropped. Keep them on ModelFile and expose PrcFile::units() ->
Result<Option<ModelUnits>, PrcError>: millimetres per unit and whether
the unit is the CAD system's own or the exporter's default. None when no
tree is stored or the unit is not a positive finite number.

Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ken Mantle
2026-11-11 08:54:32 +01:00
committed by GitHub
co-authored by KenM76 Claude Opus 5.5
parent f6c59c0b93
commit 532bdc8fa6
4 changed files with 57 additions and 6 deletions
+1 -1
View File
@@ -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};
+30
View File
@@ -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<Option<ModelUnits>, 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<Placement>, Vec<ModelNode>), PrcError> {
self.walk_within(rule, scope, Budget::FULL)
}
+3 -3
View File
@@ -437,8 +437,8 @@ impl Ctx<'_, '_> {
pub(super) fn model_file(&mut self) -> Result<ModelFile, PrcError> {
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 })
}
}
+23 -2
View File
@@ -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() };