STEP and IGES import: SMLib makes the pcurves that have no closed form

A use with no closed-form pcurve (an approximated intersection curve on a cone,
a spline edge on a spline surface) no longer refuses its solid. Its face goes to
SMLib alone, an open sheet with the trims it has and without the others, and
SMLib makes those as it makes every trim it needs (GetOrCreateUVTrimCurve on
export); they come back on the record's parameters and the face is closed and
checked as before. The STEP reader takes the trim maker from the kernel
(read_solids_with, read_solid_with); read_solids stays exact-only.

Kernel lib tests: 490 pass, 239 fail (were 468 / 261; pushed head 475 / 254),
none newly failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
This commit is contained in:
jyviko
2026-10-23 22:00:09 -04:00
parent 28e0eea80a
commit 1bf2062590
7 changed files with 250 additions and 40 deletions
+1 -1
View File
@@ -866,7 +866,7 @@ impl Reader {
/// the file does not parse or has no solid the reader can read.
pub fn step_orientation_errors(text: &str) -> std::result::Result<Vec<String>, String> {
let ex = p21::parse(text)?;
let read = solidcraft_step::topology::read_solids(&ex);
let read = solidcraft_step::topology::read_solids_with(&ex, &solidcraft_smlib::pcurves::SmlibTrims);
if read.solids.is_empty() {
return Err(read.failures.first().map_or_else(|| "the file has no solids".into(), |f| format!("solid #{}: {}", f.item, f.message)));
}
+1 -1
View File
@@ -85,7 +85,7 @@ fn placed(body: &Body, m: &M4) -> crate::Result<Body> {
/// 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.
pub(super) fn smlib_part(ex: &Exchange, item: u64, units: Units) -> std::result::Result<(Part, Vec<String>), Failure> {
let solid = solidcraft_step::topology::read_solid(ex, item, units).map_err(|msg| Failure { read: true, msg })?;
let solid = solidcraft_step::topology::read_solid_with(ex, item, units, &solidcraft_smlib::pcurves::SmlibTrims).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)?;
+1 -1
View File
@@ -1682,7 +1682,7 @@ fn step_conformance_parts_read_back_their_exact_shape() {
/// measured as the body of that record.
pub(super) fn schema_4_volume(text: &str) -> f64 {
let ex = solidcraft_step::p21::parse(text).unwrap();
let r = solidcraft_step::topology::read_solids(&ex);
let r = solidcraft_step::topology::read_solids_with(&ex, &solidcraft_smlib::pcurves::SmlibTrims);
assert!(r.failures.is_empty() && r.solids.len() == 1, "{:?}", r.failures);
measure(&Body::from_record(r.solids[0].brep.clone()).unwrap()).unwrap().volume
}
+16 -9
View File
@@ -12,7 +12,7 @@
//! - Every end value (edge intervals, face domains) is already in the flat arrays' units, radians
//! for angles; the C side converts them to SMLib's own pointwise.
use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, HashMap, HashSet};
use solidcraft_geom::brep as b;
@@ -85,7 +85,7 @@ fn z_axis(f: &b::Frame) -> [f64; 3] {
pub fn arrays(brep: &b::Brep, trusted: bool) -> R<Arrays> {
brep.validate().map_err(|e| Unsupported::Invalid(e.0))?;
let brep = analytic::analytic(brep)?;
laid_out(brep.as_ref(), trusted, true)
laid_out(brep.as_ref(), trusted, true, &HashSet::new())
}
/// [`arrays`] with every surface as the record holds it: a round face truck holds as a rational
@@ -94,12 +94,19 @@ pub fn arrays(brep: &b::Brep, trusted: bool) -> R<Arrays> {
/// surfaces. A revolution is still refused (SMLib has no such surface).
pub fn arrays_keeping_splines(brep: &b::Brep, trusted: bool) -> R<Arrays> {
brep.validate().map_err(|e| Unsupported::Invalid(e.0))?;
laid_out(brep, trusted, false)
laid_out(brep, trusted, false, &HashSet::new())
}
/// The flat arrays of a record whose uses in `bare` (face, loop, position) go without their
/// UV trims, for SMLib to make (their pcurves in the record are not read): what
/// [`crate::pcurves`] hands SMLib. Not validated, as its bare uses have no pcurves.
pub(crate) fn arrays_with_bare_uses(brep: &b::Brep, bare: &HashSet<(u32, u32, u32)>) -> R<Arrays> {
laid_out(brep, true, true, bare)
}
/// The flat arrays; `analytic`: a plane on any axes goes as SMLib's plane (else, off unit
/// orthogonal axes, as the bilinear patch it is).
fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R<Arrays> {
/// orthogonal axes, as the bilinear patch it is). The uses in `bare` go without UV trims.
fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool, bare: &HashSet<(u32, u32, u32)>) -> R<Arrays> {
let mut a = Arrays::default();
// Regions and shells. Each region gets its own copy of each shell it lists.
@@ -174,7 +181,7 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R<Arrays> {
let any_spline_face =
brep.faces.iter().any(|f| f.nurbs.is_some() || matches!(f.surface, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. }));
let any_form = brep.faces.iter().any(|f| f.nurbs.is_some());
for f in &brep.faces {
for (fi, f) in brep.faces.iter().enumerate() {
a.i(flat::FACE_LOOP_COUNT).push(int(f.loops.len())?);
a.i(flat::FACE_TRIM_TYPE).push(0);
// A face with a NURBS form: what it is (its analytic entry), then the NURBS it is
@@ -190,7 +197,7 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R<Arrays> {
if any_form {
a.i(flat::FACE_NURBS_FORM).push(i64::from(f.nurbs.is_some()));
}
for l in &f.loops {
for (li, l) in f.loops.iter().enumerate() {
match l {
b::Loop::Vertex { vertex } => {
a.i(flat::LOOP_EDGEUSE_COUNT).push(0);
@@ -199,11 +206,11 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R<Arrays> {
b::Loop::Edges { edges } => {
a.i(flat::LOOP_EDGEUSE_COUNT).push(int(edges.len())?);
a.i(flat::LOOP_VERTEX).push(-1);
for u in edges {
for (p, u) in edges.iter().enumerate() {
a.i(flat::EDGEUSE_EDGE).push(i64::from(u.edge));
a.i(flat::EDGEUSE_ORIENTATION).push(i64::from(u.forward));
if any_spline_face {
if spline {
if spline && !bare.contains(&(idx(fi)?, idx(li)?, idx(p)?)) {
let shift = intervals.get(u.edge as usize).map_or(0.0, |i| i.1);
trim(&mut a, &u.pcurve, u.forward, shift)?;
} else {
+1
View File
@@ -23,6 +23,7 @@ pub mod history;
pub mod import;
mod measure;
mod ops;
pub mod pcurves;
mod sheets;
mod split;
pub mod ssi;
+97
View File
@@ -0,0 +1,97 @@
//! SMLib's own pcurves for a face's uses that have no closed form (an approximated intersection
//! curve on a cone, a file's spline edge on a spline surface): the face goes to SMLib alone, an
//! open sheet, with the trims it has and without the others, and SMLib makes those as it makes
//! every trim it needs (`SmEdgeuse::GetOrCreateUVTrimCurve`, when the body is exported). They are
//! SMLib's, relayed as it holds them on the record's parameters; nothing here fits a curve.
use std::collections::HashSet;
use solidcraft_geom::brep::pcurve::{TrimFace, Trims};
use solidcraft_geom::brep::{self as b, Curve2, EdgeUseRef, Loop, OrientedEdge, OrientedFace, Region, Shell, Vertex};
/// [`Trims`] by SMLib.
#[derive(Clone, Copy, Debug, Default)]
pub struct SmlibTrims;
impl Trims for SmlibTrims {
fn trims(&self, face: &TrimFace<'_>) -> Result<Vec<Vec<Curve2>>, String> {
trims(face).map_err(|e| format!("SMLib pcurves: {e}"))
}
}
fn trims(face: &TrimFace<'_>) -> Result<Vec<Vec<Curve2>>, String> {
let (record, bare) = sheet(face)?;
let arrays = crate::import::arrays_with_bare_uses(&record, &bare).map_err(|e| e.to_string())?;
let body = crate::flat::Flat::from_arrays(&arrays).and_then(|f| f.import()).map_err(|e| e.to_string())?;
let (out, order) = body.to_native_ordered().map_err(|e| e.to_string())?;
let [made] = out.faces.as_slice() else { return Err(format!("SMLib made {} faces of one", out.faces.len())) };
let mut result = Vec::with_capacity(face.loops.len());
for l in &face.loops {
let mut pcs = Vec::with_capacity(l.len());
for u in l {
// SMLib keeps the edges in the order they came; the record renumbers them.
let e = order.edges.get(u.edge as usize).copied().ok_or("an edge SMLib lost")?;
let edge = out.edges.get(e).ok_or("an edge SMLib lost")?;
let ours = face.edges.get(u.edge as usize).ok_or("edge reference")?;
let slack = 1e-9 * ours.interval.iter().fold(1.0f64, |m, t| m.max(t.abs()));
if (0..2).any(|k| (edge.interval[k] - ours.interval[k]).abs() > slack) {
return Err(format!("SMLib moved an edge's interval from {:?} to {:?}", ours.interval, edge.interval));
}
let found = made.loops.iter().find_map(|ml| match ml {
Loop::Edges { edges } => edges.iter().find(|m| m.edge as usize == e && m.forward == u.forward),
Loop::Vertex { .. } => None,
});
pcs.push(found.ok_or("a use SMLib did not keep")?.pcurve.clone());
}
result.push(pcs);
}
Ok(result)
}
/// The face as a one-face open sheet, and its uses that go without trims.
fn sheet(face: &TrimFace<'_>) -> Result<(b::Brep, HashSet<(u32, u32, u32)>), String> {
let mut edges: Vec<b::Edge> = face
.edges
.iter()
.map(|e| b::Edge {
curve: e.curve.clone(),
nurbs: e.nurbs.cloned(),
interval: e.interval,
vertices: e.vertices,
tolerance: e.tolerance,
uses: Vec::new(),
})
.collect();
let mut bare = HashSet::new();
let mut loops = Vec::with_capacity(face.loops.len());
for (li, l) in face.loops.iter().enumerate() {
let li = u32::try_from(li).map_err(|_| "too many loops")?;
let mut uses = Vec::with_capacity(l.len());
for (p, u) in l.iter().enumerate() {
let p = u32::try_from(p).map_err(|_| "too many uses")?;
let edge = edges.get_mut(u.edge as usize).ok_or("edge reference")?;
edge.uses.push(EdgeUseRef { face: 0, r#loop: li, position: p, upper: !u.forward });
let pcurve = match &u.pcurve {
Some(pc) => pc.clone(),
None => {
bare.insert((0, li, p));
// Not read: the use goes without a trim (`arrays_with_bare_uses`).
Curve2::Spline { knots: vec![edge.interval[0], edge.interval[0], edge.interval[1], edge.interval[1]], points: vec![[0.0; 2]; 2] }
}
};
uses.push(OrientedEdge { edge: u.edge, forward: u.forward, pcurve });
}
loops.push(Loop::Edges { edges: uses });
}
let vertices = face.vertices.iter().map(|&point| Vertex { point, tolerance: face.tolerance }).collect();
let record = b::Brep {
version: b::VERSION,
tolerance: face.tolerance,
regions: vec![Region { shells: vec![0], void: false }],
shells: vec![Shell::Faces { faces: vec![OrientedFace { face: 0, forward: true }], wires: Vec::new() }],
faces: vec![b::Face { surface: face.surface.clone(), nurbs: face.nurbs.cloned(), domain: face.domain, loops }],
edges,
vertices,
};
Ok((record, bare))
}
+133 -28
View File
@@ -27,8 +27,9 @@
//! distance (with margin) as its tolerance; a face whose edges lie off its surface gets its
//! pcurves fitted at a looser tolerance, which its edges then carry.
//!
//! Pcurves are computed from the 3D edges and our surfaces ([`pcurve::pcurves`]); the file's
//! own PCURVE entities are not used.
//! Pcurves are the closed forms the 3D edges have on our surfaces ([`pcurve::exact_pcurves`]); a
//! use with none gets its pcurve from the kernel ([`read_solids_with`], SMLib's own trims). The
//! file's own PCURVE entities are not used.
//!
//! Input is hostile: every reference is checked, nesting and counts are capped, and the work of
//! a whole file is bounded (shared shells count each time a solid uses them).
@@ -38,7 +39,7 @@ use std::f64::consts::TAU;
use solidcraft_geom::Vec3;
use solidcraft_geom::brep::eval::{CurveEval, SurfaceEval};
use solidcraft_geom::brep::pcurve::{self, EdgeUse};
use solidcraft_geom::brep::pcurve::{self, EdgeUse, TrimEdge, TrimFace, TrimUse, Trims};
use solidcraft_geom::brep::{
self, Brep, Curve as BCurve, Curve2, Edge, EdgeUseRef, Face, Frame, Loop, Natural, OrientedEdge, OrientedFace, Region, Shell,
Surface as BSurface, Vertex,
@@ -227,8 +228,18 @@ impl Index {
}
/// Read every solid of the file. A solid that cannot be read is listed in
/// [`Solids::failures`] with its reason; the others are still read.
/// [`Solids::failures`] with its reason; the others are still read. A face's use with no
/// closed-form pcurve refuses its solid: [`read_solids_with`] has a kernel make those.
pub fn read_solids(ex: &Exchange) -> Solids {
read_solids_in(ex, None)
}
/// [`read_solids`], with `trims` making the pcurves of uses that have no closed form.
pub fn read_solids_with(ex: &Exchange, trims: &dyn Trims) -> Solids {
read_solids_in(ex, Some(trims))
}
fn read_solids_in(ex: &Exchange, trims: Option<&dyn Trims>) -> Solids {
let mut items: Vec<u64> = ex.entities.iter().filter(|(_, e)| SOLID_ITEMS.contains(&e.name())).map(|(id, _)| *id).collect();
items.sort_unstable();
let index = Index::new(ex);
@@ -247,7 +258,7 @@ pub fn read_solids(ex: &Exchange) -> Solids {
(Reader::new(ex, c.units), c)
});
let context = *context;
match read_with(ex, reader, item, context, &mut budget) {
match read_with(ex, reader, item, context, &mut budget, trims) {
Ok(mut s) => {
s.product = index.product.get(&item).cloned();
if s.name.is_empty()
@@ -267,7 +278,14 @@ pub fn read_solids(ex: &Exchange) -> Solids {
pub fn read_solid(ex: &Exchange, item: u64, context: units::Context) -> R<Solid> {
let mut reader = Reader::new(ex, context.units);
let mut budget = MAX_FILE_FACES;
read_with(ex, &mut reader, item, context, &mut budget)
read_with(ex, &mut reader, item, context, &mut budget, None)
}
/// [`read_solid`], with `trims` making the pcurves of uses that have no closed form.
pub fn read_solid_with(ex: &Exchange, item: u64, context: units::Context, trims: &dyn Trims) -> R<Solid> {
let mut reader = Reader::new(ex, context.units);
let mut budget = MAX_FILE_FACES;
read_with(ex, &mut reader, item, context, &mut budget, Some(trims))
}
/// The base tolerance of a context.
@@ -275,11 +293,18 @@ pub fn tolerance(context: &units::Context) -> f64 {
context.uncertainty.unwrap_or(DEFAULT_TOLERANCE).clamp(MIN_TOLERANCE, MAX_FIT_TOLERANCE)
}
fn read_with<'a>(ex: &'a Exchange, reader: &mut Reader<'a>, item: u64, context: units::Context, budget: &mut usize) -> R<Solid> {
fn read_with<'a>(
ex: &'a Exchange,
reader: &mut Reader<'a>,
item: u64,
context: units::Context,
budget: &mut usize,
trims: Option<&dyn Trims>,
) -> R<Solid> {
let e = entity(ex, item)?;
let p = e.params();
let name = p.first().and_then(Param::as_str).unwrap_or("").to_string();
let mut b = Builder::new(ex, reader, tolerance(&context), budget);
let mut b = Builder::new(ex, reader, tolerance(&context), budget, trims);
let (shells, kind) = match e.name() {
"MANIFOLD_SOLID_BREP" => (vec![b.shell(rid(p.get(1), "outer shell")?)?], Kind::Solid),
"BREP_WITH_VOIDS" => {
@@ -347,6 +372,8 @@ struct Builder<'b, 'a> {
reader: &'b mut Reader<'a>,
tol: f64,
budget: &'b mut usize,
/// Makes the pcurves of uses that have no closed form; without it such a face is refused.
trims: Option<&'b dyn Trims>,
vertex_ids: HashMap<u64, u32>,
vertices: Vec<Vertex>,
/// STEP edge → schema edge, and whether the schema edge runs with the STEP edge.
@@ -358,12 +385,13 @@ struct Builder<'b, 'a> {
}
impl<'b, 'a> Builder<'b, 'a> {
fn new(ex: &'a Exchange, reader: &'b mut Reader<'a>, tol: f64, budget: &'b mut usize) -> Self {
fn new(ex: &'a Exchange, reader: &'b mut Reader<'a>, tol: f64, budget: &'b mut usize, trims: Option<&'b dyn Trims>) -> Self {
Builder {
ex,
reader,
tol,
budget,
trims,
vertex_ids: HashMap::new(),
vertices: Vec::new(),
edge_ids: HashMap::new(),
@@ -965,23 +993,6 @@ impl<'b, 'a> Builder<'b, 'a> {
// Which loop index in `face.loops` each edge loop is.
let positions: Vec<usize> = face.loops.iter().enumerate().filter(|(_, l)| matches!(l, LoopB::Edges(_))).map(|(i, _)| i).collect();
let tol = self.tol;
// The face's pcurves on `on`, each edge on its NURBS form.
let pcurves_on = |on: &BSurface, domain: Option<[[f64; 2]; 2]>| -> R<Vec<Vec<Curve2>>> {
let mut uses = Vec::with_capacity(edge_loops.len());
for l in &edge_loops {
let mut u = Vec::with_capacity(l.len());
for &(e, forward) in l {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
let (curve, interval) = match &edge.form {
Some((c, i)) => (c, *i),
_ => (&edge.curve, edge.interval),
};
u.push(EdgeUse { edge: e, curve, interval, forward, tolerance: 0.0, given: None });
}
uses.push(u);
}
pcurve::pcurves_within(on, &uses, tol, domain).map_err(|e| e.to_string())
};
// A cylinder, cone, sphere, torus or revolution on its NURBS form, over the extent of its
// boundary on its label; then every pcurve on the forms.
let form = match brep::form::frame_of(&label) {
@@ -1004,8 +1015,21 @@ impl<'b, 'a> Builder<'b, 'a> {
Some((f, d)) => (Some(f), Some(d)),
None => (None, None),
};
let surface = form.clone().unwrap_or(label);
let pcs = pcurves_on(&surface, box_on_form)?;
let surface = form.clone().unwrap_or_else(|| label.clone());
// Closed forms where the geometry has them; the kernel makes the rest (`trims`).
let exact = pcurve::exact_pcurves(&surface, &uses_of(&self.edges, &edge_loops, None)?, tol, box_on_form).map_err(|e| e.to_string())?;
// Without a kernel, a use with neither is refused below with why.
let made = match self.trims {
Some(trims) if exact.iter().flatten().any(Option::is_none) => {
let domain = match box_on_form {
Some(d) => d,
None => natural_box(&surface).ok_or_else(|| unsupported("a face on an unbounded surface without closed-form pcurves"))?,
};
Some(trims.trims(&self.trim_face(&label, form.as_ref(), domain, &edge_loops, exact)?)?)
}
_ => None,
};
let pcs = pcurve::pcurves_within(&surface, &uses_of(&self.edges, &edge_loops, made.as_ref())?, tol, box_on_form).map_err(|e| e.to_string())?;
// 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
@@ -1071,6 +1095,87 @@ impl<'b, 'a> Builder<'b, 'a> {
}
}
impl Builder<'_, '_> {
/// The face as a kernel takes it to make its missing pcurves: its edges and vertices
/// numbered within it, each use with its closed-form pcurve where `exact` has one.
fn trim_face<'s>(
&'s self,
surface: &'s BSurface,
nurbs: Option<&'s BSurface>,
domain: [[f64; 2]; 2],
edge_loops: &[Vec<(u32, bool)>],
exact: Vec<Vec<Option<Curve2>>>,
) -> R<TrimFace<'s>> {
let (mut edge_at, mut vertex_at) = (HashMap::new(), HashMap::new());
let (mut edges, mut vertices) = (Vec::new(), Vec::new());
let mut loops = Vec::with_capacity(edge_loops.len());
for (l, pcs) in edge_loops.iter().zip(exact) {
let mut uses = Vec::with_capacity(l.len());
for (&(e, forward), pcurve) in l.iter().zip(pcs) {
let local = match edge_at.get(&e) {
Some(i) => *i,
None => {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
let mut ends = [0u32; 2];
for (end, v) in ends.iter_mut().zip(edge.vertices) {
*end = match vertex_at.get(&v) {
Some(i) => *i,
None => {
let i = index(vertices.len())?;
vertices.push(self.point(v)?);
vertex_at.insert(v, i);
i
}
};
}
let (curve, nurbs, interval) = match &edge.form {
Some((n, i)) => (&edge.curve, Some(n), *i),
None => (&edge.curve, None, edge.interval),
};
let i = index(edges.len())?;
edges.push(TrimEdge { curve, nurbs, interval, vertices: ends, tolerance: edge.tolerance });
edge_at.insert(e, i);
i
}
};
uses.push(TrimUse { edge: local, forward, pcurve });
}
loops.push(uses);
}
Ok(TrimFace { surface, nurbs, domain, edges, vertices, loops, tolerance: self.tol })
}
}
/// A face's uses on their edges' NURBS forms, with `given` pcurves where a kernel made them.
fn uses_of<'e>(edges: &'e [EdgeB], edge_loops: &[Vec<(u32, bool)>], given: Option<&'e Vec<Vec<Curve2>>>) -> R<Vec<Vec<EdgeUse<'e>>>> {
let mut uses = Vec::with_capacity(edge_loops.len());
for (li, l) in edge_loops.iter().enumerate() {
let mut u = Vec::with_capacity(l.len());
for (p, &(e, forward)) in l.iter().enumerate() {
let edge = edges.get(e as usize).ok_or("edge reference")?;
let (curve, interval) = match &edge.form {
Some((c, i)) => (c, *i),
_ => (&edge.curve, edge.interval),
};
let given = match given {
Some(g) => Some(g.get(li).and_then(|l| l.get(p)).ok_or("a pcurve the kernel did not make")?),
None => None,
};
u.push(EdgeUse { edge: e, curve, interval, forward, tolerance: 0.0, given });
}
uses.push(u);
}
Ok(uses)
}
/// A spline surface's parameter box: its knot ranges.
fn natural_box(s: &BSurface) -> Option<[[f64; 2]; 2]> {
match s.natural() {
[Natural::Range(a, b), Natural::Range(c, d)] if [a, b, c, d].iter().all(|x| x.is_finite()) => Some([[a, b], [c, d]]),
_ => None,
}
}
/// The loop run the other way.
fn reverse(uses: &mut [(u32, bool)]) {
uses.reverse();