mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user