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

+7
View File
@@ -52,6 +52,13 @@ Changes:
It does not run adaptive subdivision or grid balancing. The kernel caller bounds
projection complexity and total allocation before using it; ordinary modelling
tessellation keeps its existing accuracy policy.
- `tessellation/triangulation.rs`: exact, robust and provisional-preview projections prefer an
equivalent in-domain parameter at collapsed surface edges, and snap to exact bounded
endpoints only at collapsed edges when that improves the spatial fit within the existing
search tolerance. Closed spline seams retain their distinct parameter endpoints.
This prevents huge arbitrary pole parameters from corrupting trimmed meshes (#796),
while retaining genuine extrapolation for unbounded planes. Work is bounded to four
endpoint evaluations per projected point; no search or tessellation tolerance is relaxed.
- `tessellation/triangulation.rs`: the inside test of a face's boundary skips loops whose
parameter box is clear of the point (it tested every segment of every loop, so a face with
hundreds of holes meshed in quadratic time).
+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)