mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
solidcraft-step: refuse loops across a closed spline surface's seam; conformance parts and truck volumes
A face winding around a closed (non-periodic) spline surface without a seam edge has loops that jump across the surface's domain; it is refused for now instead of making a record whose loops do not close. The step-conformance parts are read in a test, and the kernel's ignored corpus comparison builds each record back into a truck solid to compare volumes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,13 @@ pub fn smlib_body(body: &Body) -> Result<solidcraft_smlib::Brep> {
|
||||
solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| KernelError::Failed(format!("SMLib: {e}")))
|
||||
}
|
||||
|
||||
/// The truck solid of a schema 4 record, as a saved body's is built: tests compare records
|
||||
/// made elsewhere (the STEP reader's) with truck's own import.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn solid_of_record(record: solidcraft_geom::brep::Brep) -> Result<truck_modeling::Solid> {
|
||||
guard("decode a B-rep record", || adapter::decode(record))
|
||||
}
|
||||
|
||||
/// Decode bounded untrusted data. Validate indices before topology extraction.
|
||||
/// Only a caller that has checked who produced the data may treat it as evaluated geometry.
|
||||
pub fn decode_native_body(bytes: &[u8]) -> Result<Body> {
|
||||
|
||||
@@ -1821,16 +1821,19 @@ fn step_conformance_parts_read_back_their_exact_shape() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Each STEP file in `SOLIDCRAFT_STEP_CORPUS`: bodies, faces and edges as truck imports it and
|
||||
/// as `solidcraft_step::topology` reads it into schema 4 records. Counts may differ where truck
|
||||
/// splits faces (bands, balls) or edges; this reports, it does not judge. Run with
|
||||
/// `--ignored --nocapture`.
|
||||
/// Each STEP file in `SOLIDCRAFT_STEP_CORPUS`: bodies, faces, edges and volume as truck
|
||||
/// imports it and as `solidcraft_step::topology` reads it into schema 4 records (built back into
|
||||
/// truck solids for their volume, signed: negative means faces turned into the material).
|
||||
/// Counts may differ where truck splits faces (bands, balls) or edges and where schema 4 adds
|
||||
/// seams; volumes are coarse meshes' (1% of the size). This reports, it does not judge. Run
|
||||
/// with `--ignored --nocapture`.
|
||||
#[test]
|
||||
#[ignore = "needs SOLIDCRAFT_STEP_CORPUS"]
|
||||
fn corpus_truck_and_schema_4_counts() {
|
||||
let Ok(dir) = std::env::var("SOLIDCRAFT_STEP_CORPUS") else { return };
|
||||
let mut paths: Vec<_> = std::fs::read_dir(&dir).into_iter().flatten().flatten().map(|e| e.path()).collect();
|
||||
paths.sort();
|
||||
let volume = |s: &mt::Solid| s.boundaries().iter().map(crate::body::shell_volume).sum::<Option<f64>>();
|
||||
for path in paths.iter().filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("step") || e.eq_ignore_ascii_case("stp"))) {
|
||||
let text = std::fs::read_to_string(path).unwrap_or_default();
|
||||
let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
@@ -1840,7 +1843,8 @@ fn corpus_truck_and_schema_4_counts() {
|
||||
let mut seen = HashSet::new();
|
||||
let once: Vec<&ImportedBody> = imp.bodies.iter().filter(|b| seen.insert(b.source_item)).collect();
|
||||
let (f, e) = once.iter().fold((0, 0), |(f, e), b| (f + b.body.face_count(), e + b.body.edge_count()));
|
||||
format!("{} bodies, {f} faces, {e} edges", once.len())
|
||||
let v: f64 = once.iter().filter_map(|b| volume(&b.body.solid)).sum();
|
||||
format!("{} bodies, {f} faces, {e} edges, volume {v:.4}", once.len())
|
||||
}
|
||||
Err(e) => format!("failed: {e}"),
|
||||
};
|
||||
@@ -1853,6 +1857,26 @@ fn corpus_truck_and_schema_4_counts() {
|
||||
};
|
||||
let r = solidcraft_step::topology::read_solids(&ex);
|
||||
let (f, e) = r.solids.iter().fold((0, 0), |(f, e), s| (f + s.brep.faces.len(), e + s.brep.edges.len()));
|
||||
println!("{name}: truck {truck}; schema 4 {} solids ({} refused), {f} faces, {e} edges", r.solids.len(), r.failures.len());
|
||||
let (mut v, mut unbuilt) = (0.0, Vec::new());
|
||||
for s in &r.solids {
|
||||
// Truck holds a plane by three points: the same plane, within a rounding.
|
||||
let mut record = s.brep.clone();
|
||||
for f in &mut record.faces {
|
||||
if let solidcraft_geom::brep::Surface::Plane { origin, u_axis, v_axis } = f.surface {
|
||||
let at = |d: [f64; 3]| [origin[0] + d[0], origin[1] + d[1], origin[2] + d[2]];
|
||||
f.surface = solidcraft_geom::brep::Surface::PlaneThrough { origin, u_point: at(u_axis), v_point: at(v_axis) };
|
||||
}
|
||||
}
|
||||
match crate::native::solid_of_record(record) {
|
||||
Ok(solid) => v += volume(&solid).unwrap_or(f64::NAN),
|
||||
Err(err) => unbuilt.push(err.to_string()),
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{name}: truck {truck}; schema 4 {} solids ({} refused), {f} faces, {e} edges, volume {v:.4}{}",
|
||||
r.solids.len(),
|
||||
r.failures.len(),
|
||||
if unbuilt.is_empty() { String::new() } else { format!(" ({} not built by truck: {})", unbuilt.len(), unbuilt[0]) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -913,6 +913,31 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
}
|
||||
// Signed areas of the loops that close in (u, v).
|
||||
let periods = natural_periods(&surface);
|
||||
// Each use starts near where the one before it ends, along a bounded direction that
|
||||
// does not wrap: a loop around a closed spline surface without a seam edge jumps across
|
||||
// its domain.
|
||||
let natural = surface.natural();
|
||||
for (l, pc) in edge_loops.iter().zip(&pcs) {
|
||||
let mut ends = Vec::with_capacity(l.len());
|
||||
for (&(_, forward), c) in l.iter().zip(pc) {
|
||||
let [a, b] = c.interval().ok_or("pcurve interval")?;
|
||||
let (s, e) = (c.eval(a).ok_or("pcurve start")?, c.eval(b).ok_or("pcurve end")?);
|
||||
ends.push(if forward { (s, e) } else { (e, s) });
|
||||
}
|
||||
for k in 0..ends.len() {
|
||||
let (Some((start, _)), Some((_, end))) = (ends.get(k), ends.get(if k == 0 { ends.len() - 1 } else { k - 1 })) else { continue };
|
||||
for d in 0..2 {
|
||||
// Only a bounded direction has a seam to jump; half its width is no gap.
|
||||
let Some(Natural::Range(a, b)) = natural.get(d) else { continue };
|
||||
if !(b - a).is_finite() {
|
||||
continue;
|
||||
}
|
||||
if !((start[d] - end[d]).abs() <= 0.5 * (b - a)) {
|
||||
return Err(unsupported("a loop across a closed spline surface's seam without a seam edge"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let shapes: Vec<Option<f64>> = edge_loops.iter().zip(&pcs).map(|(l, pc)| loop_area(l, pc, periods)).collect();
|
||||
let face = self.faces.get_mut(fi).ok_or("face reference")?;
|
||||
let marked = face.outer.and_then(|o| positions.iter().position(|p| *p == o));
|
||||
|
||||
@@ -792,6 +792,31 @@ fn hostile_topology_is_an_error_never_a_panic() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The step-conformance parts (tests/step-conformance, CC0): every one reads, its faces facing
|
||||
/// out of its material, except the NURBS cylinder, dome and torus whose faces wind around a
|
||||
/// closed spline surface without a seam edge (refused for now).
|
||||
#[test]
|
||||
fn step_conformance_parts() {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/step-conformance");
|
||||
let mut paths: Vec<_> =
|
||||
std::fs::read_dir(&dir).unwrap().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|e| e == "step")).collect();
|
||||
paths.sort();
|
||||
assert_eq!(paths.len(), 17);
|
||||
for path in paths {
|
||||
let name = path.file_stem().unwrap().to_string_lossy().into_owned();
|
||||
let r = read_solids(&p21::parse(&std::fs::read_to_string(&path).unwrap()).unwrap());
|
||||
if name.ends_with("_nurbs") {
|
||||
assert!(r.solids.is_empty() && r.failures[0].message.contains("closed spline surface's seam"), "{name}: {:?}", r.failures);
|
||||
continue;
|
||||
}
|
||||
assert!(r.failures.is_empty() && r.solids.len() == 1, "{name}: {:?}", r.failures);
|
||||
let b = &r.solids[0].brep;
|
||||
b.validate().unwrap();
|
||||
b.check_geometry().unwrap();
|
||||
assert_eq!(b.regions.len(), 2, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every solid of every STEP file in `SOLIDCRAFT_STEP_CORPUS` (the tangency-labs/models files)
|
||||
/// read into schema 4: solids read and refused (by reason), faces, edges, time. Run with
|
||||
/// `--ignored --nocapture`.
|
||||
|
||||
Reference in New Issue
Block a user