1use std::collections::HashMap;
25use std::io::Write;
26use std::panic::{catch_unwind, AssertUnwindSafe};
27
28use anyhow::{anyhow, bail, Context, Result};
29
30use super::p21::{self, Instance};
31use super::{check_triangles, fan, tessellated, Mesh, MAX_TRIANGLES};
32
33pub const MAX_STEP_BYTES: usize = 256 * 1024 * 1024;
36
37const BREP_ENTITIES: &[&str] = &[
39 "CARTESIAN_POINT",
40 "DIRECTION",
41 "VECTOR",
42 "PLACEMENT",
43 "AXIS1_PLACEMENT",
44 "AXIS2_PLACEMENT_2D",
45 "AXIS2_PLACEMENT_3D",
46 "LINE",
47 "POLYLINE",
48 "B_SPLINE_CURVE_WITH_KNOTS",
49 "BEZIER_CURVE",
50 "QUASI_UNIFORM_CURVE",
51 "UNIFORM_CURVE",
52 "CIRCLE",
53 "ELLIPSE",
54 "HYPERBOLA",
55 "PARABOLA",
56 "PCURVE",
57 "SURFACE_CURVE",
58 "SEAM_CURVE",
59 "PLANE",
60 "SPHERICAL_SURFACE",
61 "CYLINDRICAL_SURFACE",
62 "TOROIDAL_SURFACE",
63 "CONICAL_SURFACE",
64 "B_SPLINE_SURFACE_WITH_KNOTS",
65 "UNIFORM_SURFACE",
66 "QUASI_UNIFORM_SURFACE",
67 "BEZIER_SURFACE",
68 "SURFACE_OF_LINEAR_EXTRUSION",
69 "SURFACE_OF_REVOLUTION",
70 "VERTEX_POINT",
71 "EDGE_CURVE",
72 "ORIENTED_EDGE",
73 "EDGE_LOOP",
74 "FACE_BOUND",
75 "FACE_OUTER_BOUND",
76 "FACE_SURFACE",
77 "ADVANCED_FACE",
78 "ORIENTED_FACE",
79 "OPEN_SHELL",
80 "CLOSED_SHELL",
81 "ORIENTED_OPEN_SHELL",
82 "ORIENTED_CLOSED_SHELL",
83 "DEFINITIONAL_REPRESENTATION",
84];
85
86const BREP_COMPLEX: [&str; 2] = ["B_SPLINE_CURVE", "B_SPLINE_SURFACE"];
88
89const SHELLS: [&str; 4] = [
90 "OPEN_SHELL",
91 "CLOSED_SHELL",
92 "ORIENTED_OPEN_SHELL",
93 "ORIENTED_CLOSED_SHELL",
94];
95
96const HEADER: &[u8] = b"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\n\
98FILE_NAME('','',(''),(''),'','','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\nENDSEC;\nDATA;\n";
99
100struct Found {
102 mesh: Mesh,
103 shown: usize,
104 total: usize,
105}
106
107impl Found {
108 fn into_mesh(mut self) -> Mesh {
109 if self.shown < self.total {
110 self.mesh.faces = Some((self.shown as u32, self.total as u32));
111 }
112 self.mesh
113 }
114}
115
116pub fn read(data: &[u8]) -> Result<Mesh> {
117 if data.len() > MAX_STEP_BYTES {
118 bail!(
119 "The STEP file is too large to preview ({} MB, at most {} MB) – download it instead.",
120 data.len() / (1024 * 1024),
121 MAX_STEP_BYTES / (1024 * 1024)
122 );
123 }
124 let start = data.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(data);
125 let start = start.trim_ascii_start();
126 if !start.starts_with(b"ISO-10303-21") {
127 bail!("Not a STEP file (ISO-10303-21 header missing).");
128 }
129 let insts = p21::scan(data)?;
130 let by_id: HashMap<u64, usize> = insts.iter().enumerate().map(|(i, x)| (x.id, i)).collect();
131
132 let mut partial = None;
133 let mut brep_error = None;
134 if insts.iter().any(|i| SHELLS.iter().any(|s| i.is(s))) {
135 match brep(data, &insts) {
136 Ok(found) if found.shown >= found.total => return Ok(found.into_mesh()),
137 Ok(found) => partial = Some(found),
138 Err(e) => brep_error = Some(e),
139 }
140 }
141 let faces = tessellated::shape_faces(data, &insts, &by_id)?;
142 if !faces.is_empty() {
143 let (mesh, shown) = match tessellated::read(data, &insts, &by_id, &faces) {
144 Ok(read) => read,
145 Err(_) if partial.is_some() => (Mesh::default(), 0),
147 Err(e) => return Err(e),
148 };
149 let found = Found {
150 mesh,
151 shown,
152 total: faces.len(),
153 };
154 let better = partial
155 .as_ref()
156 .is_none_or(|b| found.shown * b.total >= b.shown * found.total);
157 if !found.mesh.indices.is_empty() && better {
158 return Ok(found.into_mesh());
159 }
160 }
161 if let Some(found) = partial {
162 return Ok(found.into_mesh());
163 }
164 Err(brep_error.unwrap_or_else(|| {
165 anyhow!("The STEP file contains no solids, surfaces or tessellated geometry the preview can show.")
166 }))
167}
168
169fn for_brep(data: &[u8], inst: &Instance<'_>) -> Result<bool> {
171 if !inst.complex {
172 return Ok(BREP_ENTITIES.iter().any(|e| inst.is(e)));
173 }
174 Ok(p21::record_names(data, inst)?.iter().any(|n| {
175 BREP_COMPLEX
176 .iter()
177 .any(|e| n.eq_ignore_ascii_case(e.as_bytes()))
178 }))
179}
180
181fn brep_text(data: &[u8], insts: &[Instance<'_>]) -> Result<String> {
183 let mut text = Vec::with_capacity(data.len() / 2 + HEADER.len() + 32);
184 text.extend_from_slice(HEADER);
185 for inst in insts {
186 if for_brep(data, inst)? {
187 p21::write_normalized(data, inst, &mut text)?;
188 } else {
189 writeln!(text, "#{}=X();", inst.id)?;
190 }
191 }
192 text.extend_from_slice(b"ENDSEC;\nEND-ISO-10303-21;\n");
193 String::from_utf8(text).context("Internal error: STEP text not ASCII")
194}
195
196fn brep(data: &[u8], insts: &[Instance<'_>]) -> Result<Found> {
197 use truck_stepio::r#in::Table;
198
199 let text = brep_text(data, insts)?;
200 let table = catch_unwind(|| Table::from_step(&text))
201 .ok()
202 .flatten()
203 .context("The STEP file could not be parsed by the B-rep reader.")?;
204 drop(text);
205 let mut shells: Vec<_> = table.shell.iter().collect();
206 shells.sort_by_key(|(id, _)| **id);
207 let mut out = Mesh::default();
208 let (mut shown, mut total) = (0usize, 0usize);
209 for (_, holder) in shells {
210 let faces = holder.cfs_faces.len();
211 total += faces;
212 match catch_unwind(AssertUnwindSafe(|| shell_mesh(&table, holder))) {
214 Ok(Ok((part, n))) if !part.indices.is_empty() => {
215 shown += n.min(faces);
216 out.append(part);
217 check_triangles(out.triangle_count())?;
218 }
219 Ok(Err(e)) if e.is::<TooManyTriangles>() => return Err(e),
220 _ => {}
221 }
222 }
223 if out.indices.is_empty() {
224 bail!("The B-rep reader could not tessellate the {total} face(s) of this STEP file (unsupported surface types?).");
225 }
226 Ok(Found {
227 mesh: out,
228 shown,
229 total,
230 })
231}
232
233fn shell_mesh(
235 table: &truck_stepio::r#in::Table,
236 holder: &truck_stepio::r#in::ShellHolder,
237) -> Result<(Mesh, usize)> {
238 use truck_meshalgo::prelude::*;
239
240 let whole = catch_unwind(AssertUnwindSafe(|| -> Result<Vec<PolygonMesh>> {
242 let shell = table
243 .to_compressed_shell(holder)
244 .map_err(|e| anyhow!("{e}"))?;
245 let tolerance = tolerance([shell.vertices.as_slice()])?;
246 let meshed = shell.robust_triangulation(tolerance);
247 Ok(meshed
248 .faces
249 .into_iter()
250 .filter_map(|f| oriented(f.surface, f.orientation))
251 .collect())
252 }));
253 let polygons = match whole {
254 Ok(Ok(polygons)) => polygons,
255 _ => {
256 #[cfg(test)]
259 tests::FALLBACKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
260 let mut single = holder.clone();
261 let faces = std::mem::take(&mut single.cfs_faces);
262 let shells: Vec<_> = faces
263 .into_iter()
264 .filter_map(|face| {
265 single.cfs_faces = vec![face];
266 catch_unwind(AssertUnwindSafe(|| table.to_compressed_shell(&single)))
267 .ok()?
268 .ok()
269 })
270 .collect();
271 let tolerance = tolerance(shells.iter().map(|s| s.vertices.as_slice()))?;
272 let mut polygons = Vec::new();
273 for shell in &shells {
274 if let Ok(meshed) =
275 catch_unwind(AssertUnwindSafe(|| shell.robust_triangulation(tolerance)))
276 {
277 polygons.extend(
278 meshed
279 .faces
280 .into_iter()
281 .filter_map(|f| oriented(f.surface, f.orientation)),
282 );
283 }
284 }
285 polygons
286 }
287 };
288 let shown = polygons.len();
289 let triangles = polygons
290 .iter()
291 .flat_map(|p| p.face_iter())
292 .map(|f| f.len().saturating_sub(2))
293 .sum();
294 if triangles > MAX_TRIANGLES {
295 return Err(TooManyTriangles(triangles).into());
296 }
297 let mut polygon = PolygonMesh::default();
298 for p in polygons {
299 polygon.merge(p);
300 }
301 polygon
302 .put_together_same_attrs(TOLERANCE * 50.0)
303 .remove_degenerate_faces()
304 .remove_unused_attrs();
305 let mut part = Mesh {
306 parts: 1,
307 ..Mesh::default()
308 };
309 for p in polygon.positions() {
310 part.vertex([p[0], p[1], p[2]]);
311 }
312 let n = part.vertex_count();
313 let mut corners = Vec::new();
314 for face in polygon.face_iter() {
315 corners.clear();
316 for v in face {
317 if v.pos >= n {
318 bail!("Internal error: tessellation index out of range.");
319 }
320 corners.push(v.pos as u32);
321 }
322 fan(&mut part, &corners);
323 }
324 Ok((part, shown))
325}
326
327fn tolerance<'a>(
329 vertices: impl IntoIterator<Item = &'a [truck_meshalgo::prelude::Point3]>,
330) -> Result<f64> {
331 let mut min = [f64::MAX; 3];
332 let mut max = [f64::MIN; 3];
333 let mut any = false;
334 for v in vertices.into_iter().flatten() {
335 any = true;
336 for ((lo, hi), c) in min.iter_mut().zip(max.iter_mut()).zip([v.x, v.y, v.z]) {
337 *lo = lo.min(c);
338 *hi = hi.max(c);
339 }
340 }
341 let diameter = if any {
342 ((max[0] - min[0]).powi(2) + (max[1] - min[1]).powi(2) + (max[2] - min[2]).powi(2)).sqrt()
343 } else {
344 1.0
345 };
346 let tolerance = (diameter * 0.002).max(truck_meshalgo::prelude::TOLERANCE * 10.0);
347 if !tolerance.is_finite() {
348 bail!("invalid coordinates");
349 }
350 Ok(tolerance)
351}
352
353fn oriented(
356 surface: Option<truck_meshalgo::prelude::PolygonMesh>,
357 orientation: bool,
358) -> Option<truck_meshalgo::prelude::PolygonMesh> {
359 use truck_meshalgo::prelude::*;
360 let p = surface?;
361 let pos = p.positions();
362 let area = p.face_iter().any(|f| {
363 (2..f.len()).any(|k| {
364 let at = |i: usize| f.get(i).and_then(|v| <[Point3]>::get(pos, v.pos)).copied();
365 let (Some(a), Some(b), Some(c)) = (at(0), at(k - 1), at(k)) else {
366 return false;
367 };
368 let n = (b - a).cross(c - a).magnitude2();
369 n.is_finite() && n > 0.0
370 })
371 });
372 area.then(|| if orientation { p } else { p.inverse() })
373}
374
375#[derive(Debug)]
377struct TooManyTriangles(usize);
378
379impl std::fmt::Display for TooManyTriangles {
380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381 write!(
382 f,
383 "The model has {} triangles – more than the preview shows ({MAX_TRIANGLES}).",
384 self.0
385 )
386 }
387}
388
389impl std::error::Error for TooManyTriangles {}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 const CUBE: &[u8] = include_bytes!("../../tests/fixtures/cube.step");
396
397 pub(super) static FALLBACKS: std::sync::atomic::AtomicUsize =
399 std::sync::atomic::AtomicUsize::new(0);
400
401 fn fallbacks() -> usize {
402 FALLBACKS.load(std::sync::atomic::Ordering::Relaxed)
403 }
404
405 #[test]
406 fn noise_around_the_brep_does_not_matter() {
407 let text = String::from_utf8_lossy(CUBE).replace(
411 "DATA;\n",
412 "DATA;\n/* exported by a CAD program */\n\
413 #9001=DRAUGHTING_MODEL('it''s',(),#9002);\n\
414 #9002=(\n GEOMETRIC_REPRESENTATION_CONTEXT(3)\n GLOBAL_UNIT_ASSIGNED_CONTEXT((#9003))\n REPRESENTATION_CONTEXT('','')\n);\n\
415 #9003=(LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.));\n\
416 #9004=PROPERTY('Geh\u{e4}use',\"0F\",@1);\n",
417 );
418 let mesh = read(text.as_bytes()).unwrap();
419 assert_eq!(mesh.faces, None, "complete");
420 assert!(mesh.triangle_count() >= 12);
421 let b = mesh.bounds();
422 assert!((b[3] - b[0] - 10.0).abs() < 1e-3, "{b:?}");
423 }
424
425 #[test]
426 fn a_bad_face_costs_that_face_not_the_model() {
427 let cube = String::from_utf8_lossy(CUBE).into_owned();
430 let plane = "#108=PLANE('',#107);";
431 let knots = |degree: u8, mult: &str, knots: &str| {
432 format!("#108=B_SPLINE_SURFACE_WITH_KNOTS('',{degree},{degree},((#1,#2),(#3,#4)),.UNSPECIFIED.,.F.,.F.,.F.,{mult},{mult},{knots},{knots},.UNSPECIFIED.);")
433 };
434 for (from, to) in [
435 (plane, "#108=UNKNOWN_SURFACE('',#107);".to_string()),
436 (plane, "#108=CYLINDRICAL_SURFACE('',#107,0.);".into()),
437 (plane, "#108=SPHERICAL_SURFACE('',#107,-5.);".into()),
438 (plane, "#108=CONICAL_SURFACE('',#107,0.,0.);".into()),
439 (
440 plane,
441 "#108=SURFACE_OF_LINEAR_EXTRUSION('',#41,#31);".into(),
442 ),
443 (plane, knots(3, "(2,2)", "(0.,1.)")),
444 (plane, knots(1, "(1,1)", "(0.,0.)")),
445 (
446 "#107=AXIS2_PLACEMENT_3D('',#1,#26,#21);",
447 "#107=AXIS2_PLACEMENT_3D('',#1,#999,#21);\n#999=DIRECTION('',(0.,0.,0.));".into(),
448 ),
449 (
450 "#109=ADVANCED_FACE('bottom',(#106),#108,.T.);",
451 "#109=ADVANCED_FACE('bottom',(),#108,.T.);".into(),
452 ),
453 ] {
454 let mesh =
455 read(cube.replace(from, &to).as_bytes()).unwrap_or_else(|e| panic!("{to}: {e}"));
456 assert_eq!(mesh.faces, Some((5, 6)), "{to}");
457 assert_eq!(mesh.triangle_count(), 10, "{to}");
458 mesh.check().unwrap();
459 }
460 let before = fallbacks();
463 let mesh = read(
464 cube.replace(plane, &knots(1, "(2,2)", "(1.,0.)"))
465 .as_bytes(),
466 )
467 .unwrap();
468 assert!(fallbacks() > before, "face by face");
469 assert_eq!(mesh.faces, Some((5, 6)));
470 assert_eq!(mesh.triangle_count(), 10);
471 }
472
473 #[test]
474 fn tessellated_geometry_without_brep() {
475 let text = b"ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\n\
476 #1=COORDINATES_LIST('',4,((0.,0.,0.),(1.,0.,0.),(1.,1.,0.),(0.,1.,0.)));\n\
477 #2=TRIANGULATED_FACE('',#1,4,(),$,(),((1,2,3),(1,3,4)));\n\
478 #3=TESSELLATED_SHELL('',(#2),$);\n\
479 ENDSEC;\nEND-ISO-10303-21;\n";
480 let mesh = read(text).unwrap();
481 assert_eq!(mesh.triangle_count(), 2);
482 assert_eq!(mesh.faces, None);
483 }
484
485 #[test]
486 fn hostile_files_are_errors_not_panics() {
487 let cube = String::from_utf8_lossy(CUBE).into_owned();
488 let inputs: Vec<Vec<u8>> = vec![
489 b"not a step file".to_vec(),
490 b"ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\nENDSEC;\nEND-ISO-10303-21;\n".to_vec(),
491 cube.replacen("(0.,0.,0.)", "(99999999999999999999,0.,0.)", 1).into_bytes(),
493 cube.replacen(
495 "(0.,0.,0.)",
496 &format!("{}0.{}", "(".repeat(10_000), ")".repeat(10_000)),
497 1,
498 )
499 .into_bytes(),
500 cube.replace("VERTEX_POINT('',#", "VERTEX_POINT('',#7").into_bytes(),
502 cube.replace("#1=CARTESIAN_POINT", "#1=EDGE_LOOP").into_bytes(),
503 CUBE[..CUBE.len() / 2].to_vec(),
505 b"ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\n#1=CLOSED_SHELL('',());\n#2=CLOSED_SHELL('',#1);\nENDSEC;\nEND-ISO-10303-21;\n".to_vec(),
507 cube.replacen("(10.,0.,0.)", "(1.E308,1.E308,-1.E308)", 1).into_bytes(),
509 cube.replace("#41=LINE('',#1,#31);", "#41=CIRCLE('',#107,0.);").into_bytes(),
510 cube.replace("#41=LINE('',#1,#31);", "#41=ELLIPSE('',#107,0.,0.);").into_bytes(),
511 cube.replace("#31=VECTOR('',#21,1.);", "#31=VECTOR('',#21,0.);").into_bytes(),
512 cube.replace("(#101,#102,#103,#104)", "(#101,#101,#113,#104)").into_bytes(),
513 cube.replace("(#101,#102,#103,#104)", "(#101)").into_bytes(),
514 cube.replace("('bottom',(#106)", "('bottom',(#106,#106,#116)").into_bytes(),
515 cube.replace("#1=CARTESIAN_POINT('',(0.,0.,0.))", "#1=CARTESIAN_POINT('',(0.,0.))").into_bytes(),
516 ];
517 for input in inputs {
518 let result = std::panic::catch_unwind(|| read(&input));
519 assert!(
520 result.is_ok(),
521 "panicked: {}",
522 String::from_utf8_lossy(&input[..input.len().min(200)])
523 );
524 if let Ok(Ok(mesh)) = result {
525 mesh.check().unwrap();
526 }
527 }
528 }
529}