Skip to main content

teamcraft/mesh/creo/
mod.rs

1//! PTC Creo parts (`.prt.N`): a preview mesh tessellated from the exact
2//! B-rep the file stores – Creo keeps no display tessellation of the body.
3//! See `docs/src/research/creo-brep-preview.md`. Assemblies (`.asm.N`) are
4//! put together from their parts (`asm`, `services::assemblies`).
5//!
6//! * `asm`: the components of an assembly and their placements;
7//! * `container`: the text table of contents and the LZW-compressed sections;
8//! * `tagged`: the self-describing binary format of a section and its
9//!   compact numbers;
10//! * `brep`: faces (plane, cylinder, cone, torus) with their contours and the
11//!   edges sampled in both faces' parameter spaces; the per-face
12//!   parametrisation is resolved across shared edges;
13//! * `tess`: regions per face, ear clipping in the parameter plane, welding,
14//!   PN refinement along the analytic normals.
15
16pub mod asm;
17pub mod brep;
18pub mod container;
19pub mod freeform;
20pub mod tagged;
21pub mod tess;
22
23use anyhow::{anyhow, bail, Result};
24
25use crate::mesh::Mesh;
26use tagged::Node;
27
28/// The section with the visible B-rep.
29pub const VISIBLE_GEOMETRY: &str = "VisibGeom";
30/// The section with the unit system (parts and assemblies).
31pub const UNIT_SYSTEM: &str = "ND:0:UnitSystemDef_L03:1";
32
33/// Millimetres per length unit of the file (1 when the file names none or
34/// one not known): the unit's name follows `Offset` in the names
35/// (`string_xar`) of the `UnitSystemDef` section – `mm`, `in`, …; other
36/// systems give the metres per unit in `double_xar[0]`.
37pub fn unit_mm(data: &[u8], toc: &[container::Entry]) -> f64 {
38    let prefix = UNIT_SYSTEM.split('_').next().unwrap_or(UNIT_SYSTEM);
39    let Some(entry) = toc.iter().find(|e| e.name.starts_with(prefix)) else {
40        return 1.0;
41    };
42    let Ok(Some(section)) = container::section(data, toc, &entry.name) else {
43        return 1.0;
44    };
45    let Ok(tree) = tagged::parse(&section) else {
46        return 1.0;
47    };
48    let root = tree.root();
49    let names: Vec<&str> = tree
50        .child(root, "string_xar")
51        .map(|l| {
52            tree.list(l)
53                .iter()
54                .filter_map(|&i| match tree.node(i) {
55                    Node::Str(s) => Some(s.as_str()),
56                    _ => None,
57                })
58                .collect()
59        })
60        .unwrap_or_default();
61    let by_name = names
62        .iter()
63        .position(|&s| s == "Offset")
64        .and_then(|i| names.get(i + 1))
65        .and_then(|u| mm_per_unit(u));
66    let by_scale = tree
67        .child(root, "double_xar")
68        .and_then(|l| tree.dbls(l).first().copied())
69        .filter(|m| m.is_finite() && *m > 1e-9 && *m < 1e6)
70        .map(|m| m * 1000.0);
71    by_name.or(by_scale).unwrap_or(1.0)
72}
73
74/// Millimetres per unit of a Creo length unit name.
75fn mm_per_unit(name: &str) -> Option<f64> {
76    Some(match name.to_ascii_lowercase().as_str() {
77        "mm" => 1.0,
78        "cm" => 10.0,
79        "m" => 1000.0,
80        "in" | "inch" => 25.4,
81        "ft" | "foot" => 304.8,
82        "um" | "micron" => 0.001,
83        _ => return None,
84    })
85}
86
87/// The preview mesh of a Creo part (bytes of the whole file), in millimetres
88/// (the NIST parts are stored in millimetres or inches).
89pub(crate) fn read_part(data: &[u8]) -> Result<Mesh> {
90    if !container::is_creo(data) {
91        bail!("not a Creo file (the file does not start with '#UGC:')");
92    }
93    if data
94        .get(..24)
95        .is_some_and(|h| h.windows(8).any(|w| w == b"ASSEMBLY"))
96    {
97        bail!("A Creo assembly is shown with the previews of its parts (found by file name).");
98    }
99    let toc = container::toc(data)?;
100    let section = container::section(data, &toc, VISIBLE_GEOMETRY)?.ok_or_else(|| {
101        anyhow!("This Creo file stores no visible geometry – the preview image is shown instead.")
102    })?;
103    let tree = tagged::parse(&section)?;
104    let mut brep = brep::extract(&tree);
105    // faces of solid bodies only (loose and cosmetic surfaces have list_id -1)
106    brep.surfaces
107        .retain(|s| s.list_type.is_none_or(|t| t == 98));
108    if brep.surfaces.is_empty() {
109        bail!("The Creo part contains no solid body.");
110    }
111    tess::add_twins(&mut brep);
112    let mut fixed = brep::resolve(&brep);
113    if tess::repair_twins(&mut brep, &fixed) > 0 {
114        fixed = brep::resolve(&brep);
115    }
116    let coarse = tess::tessellate(&brep, &fixed)?;
117    let mut mesh = tess::build(&coarse)?;
118    let mm = unit_mm(data, &toc);
119    if mm != 1.0 {
120        for p in &mut mesh.positions {
121            *p = (f64::from(*p) * mm) as f32;
122        }
123    }
124    Ok(mesh)
125}
126
127#[cfg(test)]
128#[allow(clippy::indexing_slicing)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn ctc_01_becomes_a_closed_mesh_with_the_step_box() {
134        let data = std::fs::read(concat!(
135            env!("CARGO_MANIFEST_DIR"),
136            "/tests/fixtures/cad/nist/ctc-01/creo-10/nist_ctc_01_asme1_rd_cr10030.prt.5"
137        ))
138        .unwrap();
139        let mesh = read_part(&data).unwrap();
140        mesh.check().unwrap();
141        let b = mesh.bounds();
142        for (got, want) in b.iter().zip([-400.0, -225.0, -100.0, 400.0, 225.0, 50.0]) {
143            assert!((got - want).abs() < 0.05, "{b:?}");
144        }
145        assert!(mesh.triangle_count() > 10_000, "{}", mesh.triangle_count());
146        assert_eq!(open_edges(&mesh), 0);
147        let v = volume(&mesh);
148        assert!(v > 1.4e7 && v < 1.5e7, "{v}");
149    }
150
151    /// Directed edges without their reverse.
152    fn open_edges(mesh: &Mesh) -> usize {
153        let mut edges = std::collections::HashMap::<(u32, u32), i32>::new();
154        for t in mesh.indices.chunks(3) {
155            for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
156                *edges.entry((a.min(b), a.max(b))).or_default() += if a < b { 1 } else { -1 };
157            }
158        }
159        edges.values().filter(|&&n| n != 0).count()
160    }
161
162    fn volume(mesh: &Mesh) -> f64 {
163        let p = |i: u32| {
164            let k = i as usize * 3;
165            [
166                f64::from(mesh.positions[k]),
167                f64::from(mesh.positions[k + 1]),
168                f64::from(mesh.positions[k + 2]),
169            ]
170        };
171        mesh.indices
172            .chunks(3)
173            .map(|t| brep::dot(p(t[0]), brep::cross(p(t[1]), p(t[2]))))
174            .sum::<f64>()
175            / 6.0
176    }
177
178    #[test]
179    fn assemblies_and_other_files_are_refused() {
180        assert!(read_part(b"#UGC:2 ASSEMBLY 1 2 3 \\\n").is_err());
181        assert!(read_part(b"solid stl").is_err());
182        assert!(read_part(&[]).is_err());
183    }
184}