teamcraft/mesh/creo/
mod.rs1pub 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
28pub const VISIBLE_GEOMETRY: &str = "VisibGeom";
30pub const UNIT_SYSTEM: &str = "ND:0:UnitSystemDef_L03:1";
32
33pub 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(§ion) 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
74fn 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
87pub(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(§ion)?;
104 let mut brep = brep::extract(&tree);
105 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 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}