SMLib pcurves: an end SMLib moves within the tolerance it held the edge at is mapped

An edge that lies off its face's surface has its tolerance raised to cover the
gap before SMLib makes its trims (cover_gaps). Where such an edge runs out of
its patch, SMLib ends it where its drop onto the patch ends when that is within
the edge's cross-section tolerance of its vertex (SmEdgeuse::CreateUVTrimCurve),
and moves the edge's interval there. The moved end was checked against the
edge's tolerance before it was raised (1e-5 mm), so two faces of Poppy
Humanoid's left thigh, whose edges SMLib ended 0.08-0.1 um short with a held
tolerance of 0.77 um, were refused ("SMLib moved an edge's interval"). The end
is now checked against the tolerance SMLib held the edge at.

Poppy Humanoid: thigh_left comes in as a closed solid (refused 4 -> 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 18:27:48 -04:00
committed by Irakli Kourtis
parent d2858cb3d9
commit 8b4b4a9d6a
2 changed files with 32 additions and 15 deletions
+20 -10
View File
@@ -46,6 +46,8 @@ fn trims(face: &TrimFace<'_>) -> Result<Vec<Vec<Curve2>>, String> {
let once = ours.clone().find(|u| ours.clone().filter(|v| v.edge == u.edge).count() == 1).ok_or("a face of seam edges only")?;
let at = edge_of(once.edge)?;
let reversed = made_uses.iter().find(|m| m.edge as usize == at).ok_or("a use SMLib did not keep")?.forward != once.forward;
// Each edge's tolerance as SMLib held it (raised where it lies off the surface).
let held = arrays.reals.get(&crate::flat::EDGE_TOLERANCE).ok_or("edge tolerances")?;
let mut result = Vec::with_capacity(face.loops.len());
for l in &face.loops {
let mut pcs = Vec::with_capacity(l.len());
@@ -55,7 +57,8 @@ fn trims(face: &TrimFace<'_>) -> Result<Vec<Vec<Curve2>>, String> {
let ours = face.edges.get(u.edge as usize).ok_or("edge reference")?;
let found = made_uses.iter().find(|m| m.edge as usize == e && m.forward == (u.forward != reversed));
let pc = found.ok_or("a use SMLib did not keep")?.pcurve.clone();
pcs.push(on_our_interval(pc, ours, edge.interval)?);
let tolerance = held.get(u.edge as usize).copied().unwrap_or(ours.tolerance).max(ours.tolerance);
pcs.push(on_our_interval(pc, ours, edge.interval, tolerance)?);
}
result.push(pcs);
}
@@ -97,12 +100,19 @@ fn cover_gaps(body: &crate::Brep, face: &TrimFace<'_>, bare: &HashSet<(u32, u32,
}
/// A pcurve SMLib made on its edge's interval `theirs`, on ours. SMLib may set an edge's end to
/// a parameter of its own (a knot its vertex lies at, the end of the part on the face): where
/// each end it chose is within the edge's tolerance of ours on the edge's curve, the pcurve is
/// mapped onto our interval by the affine change of parameter (exact for a B-spline: its knots
/// move), and the record's check measures it against the edge as any pcurve. Farther is SMLib
/// trimming the edge, refused with how far.
pub(crate) fn on_our_interval(pc: Curve2, ours: &solidcraft_geom::brep::pcurve::TrimEdge<'_>, theirs: [f64; 2]) -> Result<Curve2, String> {
/// a parameter of its own (a knot its vertex lies at, the end of the part on the face: where an
/// edge runs past its face's patch, SMLib ends it where its drop onto the patch ends, when that
/// is within the edge's cross-section tolerance of its vertex): where each end it chose is
/// within `tolerance` of ours on the edge's curve (the edge's tolerance as SMLib held it,
/// [`cover_gaps`]), the pcurve is mapped onto our interval by the affine change of parameter
/// (exact for a B-spline: its knots move), and the record's check measures it against the edge
/// as any pcurve. Farther is SMLib trimming the edge, refused with how far.
pub(crate) fn on_our_interval(
pc: Curve2,
ours: &solidcraft_geom::brep::pcurve::TrimEdge<'_>,
theirs: [f64; 2],
tolerance: f64,
) -> Result<Curve2, String> {
let [a, b] = ours.interval;
let [c, d] = theirs;
if a == c && b == d {
@@ -111,10 +121,10 @@ pub(crate) fn on_our_interval(pc: Curve2, ours: &solidcraft_geom::brep::pcurve::
let curve = solidcraft_geom::brep::eval::CurveEval::new(ours.nurbs.unwrap_or(ours.curve)).map_err(|e| e.to_string())?;
let at = |t: f64| curve.point(t).ok_or_else(|| format!("the edge's curve has no point at {t}"));
let shift = at(a)?.dist(at(c)?).max(at(b)?.dist(at(d)?));
if !(shift <= ours.tolerance) || !(d > c) {
if !(shift <= tolerance) || !(d > c) {
return Err(format!(
"SMLib moved an edge's interval from {:?} to {:?}, its ends {shift} mm along its curve (tolerance {} mm)",
ours.interval, theirs, ours.tolerance
"SMLib moved an edge's interval from {:?} to {:?}, its ends {shift} mm along its curve (tolerance {tolerance} mm)",
ours.interval, theirs
));
}
let map = |k: f64| {
+12 -5
View File
@@ -91,9 +91,11 @@ fn every_call_is_a_profiler_span_timed_by_the_c_api() {
assert!(names.contains(&"smlib_make_box") && names.contains(&"smlib_brep_volume"), "{r:?}");
}
/// SMLib may end an edge at a parameter of its own (a knot its vertex lies at): within the
/// edge's tolerance on its curve the pcurve is mapped onto our interval exactly; farther is SMLib
/// trimming the edge, refused with how far.
/// SMLib may end an edge at a parameter of its own (a knot its vertex lies at, where its drop onto
/// the patch ends): within the edge's tolerance as SMLib held it, on its curve, the pcurve is
/// mapped onto our interval exactly; farther is SMLib trimming the edge, refused with how far. An
/// edge whose gap from its surface raised its tolerance (two of Poppy Humanoid's left thigh, SMLib
/// ending them 0.1 um short) is held at that tolerance.
#[test]
fn a_pcurve_smlib_ends_within_the_edges_tolerance_is_mapped_onto_our_interval() {
use solidcraft_geom::brep::pcurve::TrimEdge;
@@ -102,12 +104,17 @@ fn a_pcurve_smlib_ends_within_the_edges_tolerance_is_mapped_onto_our_interval()
let edge = TrimEdge { curve: &line, nurbs: None, interval: [0.0, 1.0], vertices: [0, 1], tolerance: 1e-5 };
let theirs = [0.0, 1.0 - 2e-8];
let pc = Curve2::Spline { knots: vec![0.0, 0.0, theirs[1], theirs[1]], points: vec![[0.0, 0.0], [1.0, 0.0]] };
let mapped = crate::pcurves::on_our_interval(pc.clone(), &edge, theirs).unwrap();
let mapped = crate::pcurves::on_our_interval(pc.clone(), &edge, theirs, edge.tolerance).unwrap();
assert_eq!(mapped.interval(), Some([0.0, 1.0]));
assert_eq!(mapped.eval(1.0), Some([1.0, 0.0]));
// 0.02 mm along the edge is past its 1e-5 mm.
let err = crate::pcurves::on_our_interval(pc, &edge, [0.0, 0.99]).unwrap_err();
let err = crate::pcurves::on_our_interval(pc, &edge, [0.0, 0.99], edge.tolerance).unwrap_err();
assert!(err.contains("SMLib moved an edge's interval") && err.contains("tolerance"), "{err}");
// Held at 0.03 mm, as an edge 0.01 mm off its surface is, the same end is within it.
let short = Curve2::Spline { knots: vec![0.0, 0.0, 0.99, 0.99], points: vec![[0.0, 0.0], [1.0, 0.0]] };
let held = crate::pcurves::on_our_interval(short, &edge, [0.0, 0.99], 0.03).unwrap();
assert_eq!(held.interval(), Some([0.0, 1.0]));
assert_eq!(held.eval(1.0), Some([1.0, 0.0]));
}
/// An edge whose curve runs 2.5 um past its vertex and its face's patch (as Poppy Humanoid's