diff --git a/crates/kernel/src/step_in/mod.rs b/crates/kernel/src/step_in/mod.rs index 7e1143e7f..ec7828015 100644 --- a/crates/kernel/src/step_in/mod.rs +++ b/crates/kernel/src/step_in/mod.rs @@ -121,9 +121,12 @@ struct Units { len: f64, ang: f64, tol: f64, + /// The context's own distance uncertainty (mm), as the file states it (SMLib's reads take it). + #[cfg_attr(not(feature = "smlib"), allow(dead_code))] + uncertainty: Option, } -const DEFAULT_UNITS: Units = Units { len: 1.0, ang: 1.0, tol: 0.01 }; +const DEFAULT_UNITS: Units = Units { len: 1.0, ang: 1.0, tol: 0.01, uncertainty: None }; /// Units of a representation context (GLOBAL_UNIT_ASSIGNED_CONTEXT, uncertainty), read by /// [`solidcraft_step::units::context`]; the vertex-on-curve tolerance is ten times the @@ -135,7 +138,7 @@ fn context_units(ex: &Exchange, ctx: Option, cache: &mut HashMap usize { fn prepare(ex: &Exchange, job: &Job) -> Prepared { let (part, read) = match &job.source { Source::Oriented(part) => (part.clone(), None), + Source::Item { item, units } if smlib_backend() => { + let (part, read) = smlib_part(ex, *item, *units)?; + (Arc::new(part), Some(read)) + } Source::Item { item, units } => { let cx = Ctx { ex, len: units.len, ang: units.ang, tol: units.tol, snap: units.snap }; let out = guard("step brep", || brep::read_item(&cx, *item).map_err(KernelError::Failed)) @@ -91,6 +95,49 @@ fn prepare(ex: &Exchange, job: &Job) -> Prepared { .map_err(|e| Failure { read: false, msg: e.to_string() }) } +/// Whether solids are read into SMLib ([`crate::backend`]). A setting this build can't honour +/// reads them on neither kernel: [`smlib_part`] then says why. +fn smlib_backend() -> bool { + !matches!(crate::backend(), Ok(crate::Backend::Truck)) +} + +/// A solid item read with the STEP reader into its schema 4 record, then into SMLib (healed +/// where SMLib's check refuses it as given), and brought back as a body: the part SMLib made of +/// the file. A solid the reader or SMLib refuses is not imported, with the reason; there is no +/// other kernel to fall back on. +#[cfg(feature = "smlib")] +pub(super) fn smlib_part(ex: &Exchange, item: u64, units: Units) -> std::result::Result<(Part, Vec), Failure> { + crate::backend().map_err(|e| Failure { read: true, msg: e.to_string() })?; + let context = + solidcraft_step::units::Context { units: solidcraft_step::read::Units { len: units.len, ang: units.ang }, uncertainty: units.uncertainty }; + let solid = solidcraft_step::topology::read_solid(ex, item, context).map_err(|msg| Failure { read: true, msg })?; + let failed = |e: KernelError| Failure { read: false, msg: e.to_string().trim_start_matches("the operation failed: ").to_string() }; + let (brep, smlib_order) = crate::native::smlib_record_ordered(&solid.brep).map_err(failed)?; + let (body, body_order) = crate::native::body_from_smlib_ordered(&brep).map_err(failed)?; + // The file face of each body face: SMLib's face i is the record's `smlib_order` face and the + // body's `body_order` face (an empty order is the same one). + let (Some(smlib_order), Some(body_order)) = (smlib_order, body_order) else { + return Err(Failure { read: false, msg: "not supported yet: SMLib renumbered the solid's faces".into() }); + }; + let at = |order: &[usize], i: usize| if order.is_empty() { Some(i) } else { order.get(i).copied() }; + let faces = brep.counts().map_err(|e| failed(KernelError::Failed(format!("SMLib: {e}"))))?[0]; + let mut face_origin = vec![0u64; body.face_count()]; + for i in 0..faces { + let file_face = at(&smlib_order.faces, i).and_then(|r| solid.face_origin.get(r)).copied(); + match (at(&body_order.faces, i).and_then(|b| face_origin.get_mut(b)), file_face) { + (Some(slot), Some(f)) => *slot = f, + _ => return Err(Failure { read: false, msg: "not supported yet: SMLib renumbered the solid's faces".into() }), + } + } + let part = Part { body, closed: !solid.sheet, file_faces: solid.face_origin.len(), face_origin }; + Ok((part, solid.notes)) +} + +#[cfg(not(feature = "smlib"))] +pub(super) fn smlib_part(_: &Exchange, _: u64, _: Units) -> std::result::Result<(Part, Vec), Failure> { + Err(Failure { read: true, msg: crate::backend().err().map_or_else(|| "this build has no SMLib".into(), |e| e.to_string()) }) +} + /// Prepare `jobs`, the results in the jobs' order. Solids still to read and orient (the slow /// part) are shared out between up to `workers` threads, each taking the next job when it is /// done with one, so one large part does not hold up the rest. diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index ad0a4f30c..cf250b564 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -1899,3 +1899,54 @@ fn corpus_truck_and_schema_4_counts() { ); } } + +/// The step-conformance parts read into SMLib (`SOLIDCRAFT_KERNEL=smlib`): the STEP reader's +/// schema 4 record, SMLib's body of it, back as a kernel body. Each has its exact volume and +/// face count, and every face is traced to a face of the file. The NURBS cylinder, dome and +/// torus wind around a closed spline surface without a seam edge, which the reader refuses for +/// now, and stay refused on this path with that reason. +#[cfg(feature = "smlib")] +#[test] +fn step_conformance_parts_read_into_smlib() { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/step-conformance"); + // (file, exact volume (mm³) and faces of its solid items: cylinder_twice places its one + // cylinder twice) + #[rustfmt::skip] + let parts: [(&str, f64, usize); 17] = [ + ("cone_point", 1000.0 * std::f64::consts::PI, 2), ("cone_point_vertex", 1000.0 * std::f64::consts::PI, 2), + ("cylinder", 9424.778, 3), ("cylinder_inch", 9424.778, 3), ("cylinder_nurbs", 9424.778, 3), + ("cylinder_placed", 9424.778, 3), ("cylinder_seam", 9424.778, 3), ("cylinder_twice", 9424.778, 3), + ("dome", 16755.161, 2), ("dome_nurbs", 16755.161, 2), ("far_cylinder", 9424.778, 3), + ("grooved_shaft", 153532.251, 753), ("plate_100_holes", 71588.321, 110), ("sheet_disc", 193396443.755, 3), + ("tiny_pin", std::f64::consts::FRAC_PI_8, 3), ("torus_ring_nurbs", 4934.802, 2), ("washer", 3534.292, 4), + ]; + for (name, volume, faces) in parts { + let text = std::fs::read_to_string(dir.join(format!("{name}.step"))).unwrap(); + let ex = solidcraft_step::p21::parse(&text).unwrap(); + // The file's one representation context, as the import reads it. + let ctx = ex.entities.iter().find(|(_, e)| e.record("GLOBAL_UNIT_ASSIGNED_CONTEXT").is_some()).map(|(id, _)| *id); + let units = context_units(&ex, ctx, &mut HashMap::new()); + let mut items: Vec = ex.entities.iter().filter(|(_, e)| e.name() == "MANIFOLD_SOLID_BREP").map(|(id, _)| *id).collect(); + items.sort_unstable(); + let (mut v, mut n) = (0.0, 0); + for item in items { + let made = parallel::smlib_part(&ex, item, units); + if name.ends_with("_nurbs") { + let msg = made.err().map(|f| f.msg).unwrap_or_default(); + assert!(msg.contains("closed spline surface's seam"), "{name}: {msg}"); + continue; + } + let (part, _) = made.unwrap_or_else(|f| panic!("{name}: {}", f.msg)); + assert_eq!(part.face_origin.len(), part.body.face_count(), "{name}"); + assert!(part.face_origin.iter().all(|f| ex.get(*f).is_some_and(|e| e.name() == "ADVANCED_FACE")), "{name}: {:?}", part.face_origin); + let m = measure(&part.body).unwrap_or_else(|e| panic!("{name}: {e}")); + v += m.volume; + n += m.faces; + } + if name.ends_with("_nurbs") { + continue; + } + assert!(rel(v, volume) < 1e-3, "{name}: volume {v} vs {volume}"); + assert_eq!(n, faces, "{name}: faces"); + } +}