kernel: with SOLIDCRAFT_KERNEL=smlib, STEP solids are SMLib's bodies

Each solid item of a STEP file goes through the schema 4 reader
(solidcraft_step::topology, at the file's units and uncertainty) into
SMLib through the flat converter, healed where SMLib's check refuses it,
and comes back as a kernel body; its faces keep the file faces they came
from. A solid the reader or SMLib refuses is not imported, with the
reason: there is no truck fallback on this path. truck's own reader is
unchanged and stays the default.

Test: the 17 step-conformance parts read into SMLib with their exact
volumes and face counts, every face traced to an ADVANCED_FACE; the three
NURBS parts stay refused by the reader (a loop across a closed spline
surface's seam), as on schema 4 today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brennen Herbruck
2026-10-21 11:25:40 -04:00
committed by jyviko
parent 8fde178fe4
commit 5014c4f17f
3 changed files with 103 additions and 2 deletions
+5 -2
View File
@@ -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<f64>,
}
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<u64>, cache: &mut HashMap<u64, Units
}
let c = solidcraft_step::units::context(ex, id);
let tol = c.uncertainty.map(|t| (t * 10.0).max(1e-3)).unwrap_or(c.units.len * 1e-2).clamp(1e-4, 1.0);
let u = Units { len: c.units.len, ang: c.units.ang, tol };
let u = Units { len: c.units.len, ang: c.units.ang, tol, uncertainty: c.uncertainty };
cache.insert(id, u);
u
}
+47
View File
@@ -66,6 +66,10 @@ pub(super) fn workers() -> 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<String>), 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<String>), 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.
+51
View File
@@ -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<u64> = 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");
}
}