Keep fine meshes whole at collapsed taper boundaries (#906)

The exact surface search could return an arbitrary parameter at a collapsed NURBS boundary, which corrupted the trimmed triangulation domain and lost volume on fine meshes of a fillet tapering to zero. Search results in the boundary projection are now validated, preferring a bounded parameter at a pole and snapping to a finite endpoint only when its transverse tangent has collapsed and the residual improves. Adds four regressions for decreasing chord tolerances, pole parameters, unbounded planar support and provisional preview.

Fixes #796.
This commit is contained in:
selune authored and GitHub committed 2026-10-13 20:45:02 -04:00
1 parent 7da7bbb335
commit 3f1c023563
5 files changed
+235 -15

No files matched your search

+46 -14
View File
@@ -46,17 +46,47 @@ fn clamped<S: ParametricSurface>(surface: &S, (u, v): (f64, f64)) -> (f64, f64)
(c(u, ur, surface.u_period()), c(v, vr, surface.v_period()))
}
/// SolidCraft: a nearest-parameter search can run off a B-spline's domain and extrapolate to
/// thousands of parameter units (meshing that grid never finishes). Outside the nominal domain
/// keep the result only if it is really closer to the point than the clamped one (planes report
/// a nominal unit range but extend for ever).
fn sane<S: RobustMeshableSurface>(surface: &S, point: Point3, uv: (f64, f64)) -> (f64, f64) {
if in_domain(surface, uv) {
return uv;
}
/// SolidCraft: exact and nearest searches can run off a B-spline's domain, especially at a
/// collapsed edge. Prefer a bounded equivalent within the existing spatial search tolerance;
/// otherwise retain extrapolation only when it is closer (planes have a nominal unit range
/// but extend for ever). At most four boundary candidates are evaluated, with no new search.
fn sane<S: ParametricSurface3D>(surface: &S, point: Point3, uv: (f64, f64)) -> (f64, f64) {
let c = clamped(surface, uv);
let d = |(u, v): (f64, f64)| surface.subs(u, v).distance2(point);
if d(uv) <= d(c) { uv } else { c }
let (mut best, mut error) = (uv, d(uv));
let dc = d(c);
let collapsed = |(u, v): (f64, f64), axis| {
let tangent = if axis == 0 { surface.vder(u, v) } else { surface.uder(u, v) };
tangent.magnitude2() <= TOLERANCE2
};
if !in_domain(surface, uv)
&& (dc < error || (dc <= TOLERANCE2 && (collapsed(c, 0) || collapsed(c, 1)))) {
(best, error) = (c, dc);
}
// SolidCraft: at a collapsed boundary an exact search can return arbitrary,
// enormous parameters, or stop just inside the pole. Prefer an equivalent
// in-domain point and the exact boundary when it improves the spatial fit.
// A plane's nominal unit box must not clip genuinely unbounded geometry.
let (ur, vr) = surface.try_range_tuple();
for (axis, range, period) in [(0, ur, surface.u_period()), (1, vr, surface.v_period())] {
if period.is_some() { continue; }
if let Some((a, b)) = range {
for end in [a, b] {
if !end.is_finite() { continue; }
let coordinate = if axis == 0 { best.0 } else { best.1 };
let other = if end == a { b } else { a };
if (coordinate - end).abs() > (coordinate - other).abs() { continue; }
let candidate = if axis == 0 { (end, best.1) } else { (best.0, end) };
let distance = d(candidate);
// A closed spline seam has equivalent endpoints too, but moving
// between them changes the trim loop. Only canonicalize poles.
if distance <= TOLERANCE2 && distance < error && collapsed(candidate, axis) {
(best, error) = (candidate, distance);
}
}
}
}
best
}
pub(super) fn by_search_parameter<S>(
@@ -83,8 +113,8 @@ where
surface
.search_parameter(point, hint, 100)
.or_else(|| surface.search_parameter(point, None, 100))
.or_else(|| surface.search_nearest_parameter(point, hint, 100).map(|uv| sane(surface, point, uv)))
.or_else(|| surface.search_nearest_parameter(point, None, 100).map(|uv| sane(surface, point, uv)))
.or_else(|| surface.search_nearest_parameter(point, hint, 100))
.or_else(|| surface.search_nearest_parameter(point, None, 100))
}
/// Tessellates faces
@@ -393,7 +423,9 @@ impl PolyBoundaryPiece {
.into_iter()
.flat_map(|pt| {
let (mut u, mut v) = match sp(surface, pt, previous) {
Some(hint) => hint,
// SolidCraft: validate exact and nearest results alike, once per
// boundary point, including the bounded provisional preview.
Some(hint) => sane(surface, pt, hint),
None => return vec![None],
};
if let (Some(up), Some((u0, _))) = (up, previous) {
@@ -704,8 +736,8 @@ pub(super) fn preview_face<S: RobustMeshableSurface>(surface: &S, loops: &[Vec<P
let search = |surface: &S, point, hint| {
surface.search_parameter(point, hint, 16)
.or_else(|| surface.search_parameter(point, None, 16))
.or_else(|| surface.search_nearest_parameter(point, hint, 16).map(|uv| sane(surface, point, uv)))
.or_else(|| surface.search_nearest_parameter(point, None, 16).map(|uv| sane(surface, point, uv)))
.or_else(|| surface.search_nearest_parameter(point, hint, 16))
.or_else(|| surface.search_nearest_parameter(point, None, 16))
};
let pieces = loops.iter().map(|w| {
PolyBoundaryPiece::try_new(surface, std::iter::once(PolylineCurve(w.clone())), search)