STEP import: an edge's distance from an analytic face is measured before its closed-form pcurve

A closed-form pcurve (a B-spline edge's control points projected onto its plane) is
accepted within half its edge's tolerance, and the edge carried the file's: Poppy
Humanoid has planes whose B-spline edges stand 9 µm off them in a file that states
10 µm. The closed form was refused, and a plane has no parameter box for SMLib to make
pcurves in: "a face on an unbounded surface without closed-form pcurves" on 11 of its
solids. On a plane, cylinder, cone, sphere or torus (nearest points in closed form)
each edge's distance from the surface is now measured along it first, and its
tolerance covers three times that; a spline face's gaps stay measured on its kernel's
pcurves, with the 1.5 × margin of a vertex's. The pcurve code reads each use's own
tolerance.

Poppy: 194 → 195 bodies, the unbounded-surface refusals 11 → 3 (the solids they let
through now stop at later steps); the corpus survey and the kernel tests are as before.

Test: a plane face whose B-spline edges stand 0.8e-6 mm above it in a file stating
1e-6 mm imports with its closed forms; without the measure it fails as Poppy's did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
jyviko
2026-10-27 16:00:37 -04:00
committed by Irakli Kourtis
parent 7ba82d7291
commit 3a7b51f723
2 changed files with 84 additions and 12 deletions
+28 -5
View File
@@ -1954,7 +1954,13 @@ fn step_conformance_parts_read_into_smlib() {
/// looser than its context. The patch has no closed-form pcurves for them, so SMLib makes them,
/// on the surface, and they lie `dz` from their edges.
fn dome_with_edges_raised(dz: f64) -> String {
let z = |i: usize, j: usize| if (1..=2).contains(&i) && (1..=2).contains(&j) { 6.0 } else { 0.0 };
patch_with_edges_raised(dz, false)
}
/// [`dome_with_edges_raised`], or with `flat` the plane z = 0 in its place: its edges' pcurves
/// are then their control points' projections onto it, in closed form.
fn patch_with_edges_raised(dz: f64, flat: bool) -> String {
let z = |i: usize, j: usize| if !flat && (1..=2).contains(&i) && (1..=2).contains(&j) { 6.0 } else { 0.0 };
let mut t = String::from(HEAD);
let mut rows = Vec::new();
for i in 0..4 {
@@ -1964,10 +1970,14 @@ fn dome_with_edges_raised(dz: f64) -> String {
}
rows.push(format!("({})", row.join(",")));
}
t += &format!(
"#10=B_SPLINE_SURFACE_WITH_KNOTS('',3,3,({}),.UNSPECIFIED.,.F.,.F.,.F.,(4,4),(4,4),(0.,1.),(0.,1.),.UNSPECIFIED.);\n",
rows.join(",")
);
if flat {
t += "#10=PLANE('',#13);\n#11=CARTESIAN_POINT('',(0.,0.,0.));\n#12=DIRECTION('',(0.,0.,1.));\n#14=DIRECTION('',(1.,0.,0.));\n#13=AXIS2_PLACEMENT_3D('',#11,#12,#14);\n";
} else {
t += &format!(
"#10=B_SPLINE_SURFACE_WITH_KNOTS('',3,3,({}),.UNSPECIFIED.,.F.,.F.,.F.,(4,4),(4,4),(0.,1.),(0.,1.),.UNSPECIFIED.);\n",
rows.join(",")
);
}
// The boundary, counter-clockwise from above: (i, j) runs i = 0 → 3 at j = 0, j = 0 → 3 at
// i = 3, then back; each side's control points are the patch's, raised.
let sides: [[(usize, usize); 4]; 4] =
@@ -2016,3 +2026,16 @@ fn edges_written_off_their_spline_face_carry_their_gap_as_their_tolerance() {
};
assert!(refused, "{far:?}");
}
#[test]
fn spline_edges_off_their_plane_by_less_than_the_file_states_get_their_closed_form() {
// 0.8e-6 mm off, inside the file's 1e-6 mm but past the half of it a closed-form pcurve is
// checked within: each edge's distance from the plane, measured first, is its tolerance
// (with margin), and the face reads with its pcurves the edges' projections. A plane has no
// parameter box for SMLib to make pcurves in, so without them the face is refused.
let imp = step_import(&patch_with_edges_raised(0.8e-6, true)).unwrap();
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
assert!(imp.warnings.iter().any(|w| w.contains("4 edges lie off their faces' surfaces")), "{:?}", imp.warnings);
let m = measure(&imp.bodies[0].body).unwrap();
assert!(rel(m.area, 900.0) < 1e-6, "{}", m.area);
}
+56 -7
View File
@@ -83,8 +83,13 @@ const MAX_VERTEX_GAP: f64 = 1.0;
const MAX_EDGE_GAP: f64 = 1.0;
/// Intervals an edge's curve is measured over against each face's surface it bounds.
const EDGE_GAP_SAMPLES: usize = 16;
/// An edge's tolerance over the largest gap measured on its samples, as a vertex's is.
/// An edge's tolerance over the largest gap measured between its pcurves and its curve, as the
/// record check measures it: with margin, as a vertex's.
const EDGE_GAP_MARGIN: f64 = 1.5;
/// An edge's tolerance over its largest distance from a face's surface, measured before its
/// pcurve is made: a closed-form pcurve is accepted within half its edge's tolerance
/// (`pcurve::pcurves_within`), so the tolerance goes past twice the distance.
const EDGE_SURFACE_MARGIN: f64 = 3.0;
/// A solid (or sheet) read from the file.
#[derive(Clone, Debug)]
@@ -445,16 +450,58 @@ impl<'b, 'a> Builder<'b, 'a> {
Ok(())
}
/// Edge `e`'s tolerance covers `gap` (with margin): refused if far off.
fn cover_edge(&mut self, e: u32, gap: f64) -> R<()> {
/// An edge's curve as the file has it may lie off a face's surface by more than the file's
/// uncertainty states (a B-spline edge whose control points stand 9 µm off its plane, in a
/// file that states 10 µm, gets no closed-form pcurve, which is checked within half that).
/// On a surface whose nearest point is in closed form (a plane, cylinder, cone, sphere or
/// torus) the edge's distance from it is measured along the edge before its pcurve is made,
/// and the edge's tolerance covers it. A spline face's gaps are measured on the pcurves its
/// kernel makes (`raise_edges`): an iterative nearest point may settle elsewhere on it.
fn measure_edges(&mut self, surface: &BSurface, edge_loops: &[Vec<(u32, bool)>]) -> R<()> {
if !matches!(
surface,
BSurface::Plane { .. }
| BSurface::PlaneThrough { .. }
| BSurface::Cylinder { .. }
| BSurface::Cone { .. }
| BSurface::Sphere { .. }
| BSurface::Torus { .. }
) {
return Ok(());
}
let on = SurfaceEval::new(surface).map_err(|m| m.to_string())?;
for l in edge_loops {
for &(e, _) in l {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
let (curve, [t0, t1]) = match &edge.form {
Some((c, i)) => (c, *i),
None => (&edge.curve, edge.interval),
};
let eval = CurveEval::new(curve).map_err(|m| m.to_string())?;
let mut gap = 0.0f64;
for k in 0..=EDGE_GAP_SAMPLES {
let t = t0 + (t1 - t0) * (k as f64 / EDGE_GAP_SAMPLES as f64);
let Some(p) = eval.point(t) else { continue };
// A point with no nearest point is the pcurves' to refuse, with why.
let Some(s) = on.project(p, None, f64::INFINITY).and_then(|q| on.point(q)) else { continue };
gap = gap.max(s.dist(p));
}
self.cover_edge(e, gap, EDGE_SURFACE_MARGIN)?;
}
}
Ok(())
}
/// Edge `e`'s tolerance covers `gap` with `margin`: refused if far off.
fn cover_edge(&mut self, e: u32, gap: f64, margin: f64) -> R<()> {
let Some(x) = self.edges.get_mut(e as usize) else { return Err("edge reference".into()) };
if gap <= x.tolerance {
if margin * gap <= x.tolerance {
return Ok(());
}
if !(gap <= MAX_EDGE_GAP) {
return Err(format!("an edge lies {gap} mm off the face's surface"));
}
x.tolerance = EDGE_GAP_MARGIN * gap;
x.tolerance = margin * gap;
self.raised_edges.insert(e);
Ok(())
}
@@ -483,7 +530,7 @@ impl<'b, 'a> Builder<'b, 'a> {
gap = gap.max(s.dist(p));
}
}
self.cover_edge(e, gap)?;
self.cover_edge(e, gap, EDGE_GAP_MARGIN)?;
}
}
Ok(())
@@ -1082,6 +1129,8 @@ impl<'b, 'a> Builder<'b, 'a> {
None => (None, None),
};
let surface = form.clone().unwrap_or_else(|| label.clone());
// How far each edge lies off this surface, so its closed form is checked at that.
self.measure_edges(&label, &edge_loops)?;
// 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.
@@ -1231,7 +1280,7 @@ fn uses_of<'e>(edges: &'e [EdgeB], edge_loops: &[Vec<(u32, bool)>], given: Optio
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 });
u.push(EdgeUse { edge: e, curve, interval, forward, tolerance: edge.tolerance, given });
}
uses.push(u);
}