Merge pull request #2188 from brett19/fix/2129-trim-uv-spans-split

fix: bspline_trim_preserves_uv_spans cleans a UV end span that is a pole (#2129)
This commit is contained in:
bherbruck
2026-11-01 05:12:24 -05:00
committed by GitHub
4 changed files with 61 additions and 5 deletions
+4 -1
View File
@@ -685,7 +685,10 @@ pub struct UpstreamFixes {
pub uv_trim_joins_drop_pieces: bool,
/// `BSplineCurve::trim` removes short end spans only for model-space curves
/// (dimension 3), preserving exact UV trims in surface parameter units
/// (WR-441, #1114). Off by default for pinned SMLib compatibility.
/// (WR-441, #1114). A short UV end span whose control points all lie within check
/// test #15's pole tolerance (`SM_ZONE_TOL_3D / 10`) is still removed as upstream
/// removes it, so the trim passes the check (#2129). Off by default for pinned SMLib
/// compatibility.
pub bspline_trim_preserves_uv_spans: bool,
/// `BSplineCurve::trim` keeps a short end span of a rational curve (its control points carry weights, `N_IsCrvRat`: a conic's NURB) instead of removing it with `N_CrvRemoveDegenSegs`, which stretches the remaining spans over its length and so moves a conic off its circle or ellipse (a trimmed half circle of radius 16 by about 1e-6, enough for its edge and vertex gaps to pass the tolerance); non-rational curves are cleaned as upstream (WR-575, #1529). An end span whose control points all lie within check test #15's pole tolerance (`SM_ZONE_TOL_3D / 10`) is not kept: the trim ends at its interior knot instead, on the conic and within that tolerance of the requested end, so the curve passes the check (#2090). Off by default for pinned SMLib compatibility.
pub bspline_trim_keeps_rational_end_spans: bool,
@@ -95,7 +95,9 @@
//! [`crate::kernel::UpstreamFixes::bspline_trim_preserves_uv_spans`], degenerate
//! end-span removal applies only to dimension-3 model curves. UV spans are in
//! surface parameter units and must not be removed using a 3D zone tolerance.
//! The default retains pinned SMLib behavior.
//! A short UV end span that collapses to a pole (its control points within check
//! test #15's tolerance `SM_ZONE_TOL_3D / 10`) is still removed as upstream does
//! (#2129). The default retains pinned SMLib behavior.
//! - **Opt-in rational end spans (WR-575, #1529).** With
//! [`crate::kernel::UpstreamFixes::bspline_trim_keeps_rational_end_spans`], a
//! rational trimmed curve (a conic's NURB) keeps its short end spans: their removal
@@ -2225,7 +2227,7 @@ impl BSplineCurve {
/// zone tolerance mapped to parameter space) are removed. With
/// [`crate::kernel::UpstreamFixes::bspline_trim_preserves_uv_spans`] enabled,
/// that removal applies only to dimension-3 model curves, preserving UV spans
/// exactly. With
/// exactly, except a short UV end span that collapses to a pole (#2129). With
/// [`TrimMode::IntervalOnly`] only the interval is computed.
///
/// # Errors
@@ -2401,7 +2403,18 @@ impl BSplineCurve {
}
}
}
if (!k.fixes.bspline_trim_preserves_uv_spans || self.dim == 3)
// `fixes.bspline_trim_preserves_uv_spans` (WR-441): a UV curve keeps its
// short end spans, unless one collapses to a pole that check test #15 would
// report (`SM_ZONE_TOL_3D / 10`); that curve is cleaned as upstream (#2129)
let cleans_spans = !k.fixes.bspline_trim_preserves_uv_spans
|| self.dim == 3
|| short_end_span_is_pole(
cur1.cpts(),
degree,
knots.len() > 2 && head < zone_tol_1d_min,
knots.len() > 2 && tail < zone_tol_1d_max,
);
if cleans_spans
&& !keeps_rational
&& ((head < zone_tol_1d_min && knots.len() > 2)
|| (tail < zone_tol_1d_max && knots.len() > 2))
@@ -2436,6 +2449,18 @@ impl BSplineCurve {
}
}
/// Whether the short head span (`head`) or short tail span (`tail`) of a trimmed curve
/// collapses to a pole as check test #15 measures one ([`end_span_is_pole`] with its
/// tolerance `SM_ZONE_TOL_3D / 10`). Not upstream (#2129).
fn short_end_span_is_pole(cpts: &[CPoint], degree: usize, head: bool, tail: bool) -> bool {
let pole_tol = ZONE_TOL_3D / 10.0;
let n_cpts = cpts.len();
(head && end_span_is_pole(cpts, 0, degree, pole_tol))
|| (tail
&& n_cpts > degree
&& end_span_is_pole(cpts, n_cpts - 1 - degree, degree, pole_tol))
}
/// Whether the `degree + 1` control points of the span starting at control point
/// `first` all lie within `tol` of the first, as check test #15 measures a pole
/// (`SmBSplineCurve::HasInternalPole`, [`BSplineCurve::has_internal_pole`]); `false`
@@ -63,8 +63,14 @@ fn cylinder(k: &mut Kernel, radius: f64, seam: f64, axis: Vec3, angle: f64, to:
/// A's seam `seam_a` degrees round from its bottom line (180 is its top, through the
/// origin), B's `seam_b` round from its +x side (270 is its top).
fn union(split: bool, seam_a: f64, seam_b: f64) -> (Kernel, Result<Brep>) {
union_with(split, false, seam_a, seam_b)
}
/// [`union`], with `UpstreamFixes::bspline_trim_preserves_uv_spans` set to `uv_spans`.
fn union_with(split: bool, uv_spans: bool, seam_a: f64, seam_b: f64) -> (Kernel, Result<Brep>) {
let mut k = Kernel::default();
k.fixes.ssi_split_curves_at_singular_points = split;
k.fixes.bspline_trim_preserves_uv_spans = uv_spans;
let y = Vec3::new(0.0, 1.0, 0.0);
let x = Vec3::new(1.0, 0.0, 0.0);
let a = cylinder(&mut k, 10.0, seam_a, y, 90.0, Vec3::new(-20.0, 0.0, -10.0));
@@ -78,7 +84,11 @@ fn check_lines(k: &mut Kernel, b: &mut Brep) -> Vec<String> {
}
fn assert_sound(seam_a: f64, seam_b: f64) {
let (mut k, u) = union(true, seam_a, seam_b);
assert_sound_with(false, seam_a, seam_b);
}
fn assert_sound_with(uv_spans: bool, seam_a: f64, seam_b: f64) {
let (mut k, u) = union_with(true, uv_spans, seam_a, seam_b);
let mut u = u.unwrap_or_else(|e| panic!("seams {seam_a} {seam_b}: {e:?}"));
assert!(brep_is_manifold_solid(&u).unwrap());
let lines = check_lines(&mut k, &mut u);
@@ -118,6 +128,19 @@ fn split_seams_through_the_node() {
assert_sound(180.0, 270.0);
}
/// #2129: with `bspline_trim_preserves_uv_spans` also on, A's seam at 270 with B's at
/// 135 or 225 left a UV trim a head span 8e-8 long whose four control points lie within
/// check test #15's pole tolerance (`SmBSplineCurve` #15, "Bad Repeated `CPts`"); that trim
/// is now cleaned as upstream cleans it, and every placement checks clean.
#[test]
fn split_with_uv_spans_kept_checks_clean_at_every_seam() {
for ia in 0..8 {
for ib in 0..8 {
assert_sound_with(true, 45.0 * f64::from(ia), 45.0 * f64::from(ib));
}
}
}
#[test]
fn upstream_fails_the_check_at_the_issue_seams() {
let (mut k, u) = union(false, 135.0, 225.0);
@@ -0,0 +1,5 @@
Updates: WR-441
#### WR-441 (update, 2026-11-01). A short UV end span that collapses to a pole is still cleaned (#2129)
- **Trigger:** the #2110 tangent-cylinder union (`crates/solidcore/tests/issue_2110_tangent_cylinders_union.rs`) with `ssi_split_curves_at_singular_points` and `bspline_trim_preserves_uv_spans` both on, A's seam at 270 degrees and B's at 135 or 225. A UV trim is trimmed to `[3.928811601715155, 23.78128087946006]` (and `[13.326503945274428, 33.17897323660256]`), leaving a cubic head span `8.18e-8` (`9.54e-8`) long whose four control points lie within `8.2e-8` of each other in `(u, v)`. The flag kept that span, and `check_brep` Level2 reported `SmBSplineCurve` test #15 (Bad Repeated CPts, tolerance `SM_ZONE_TOL_3D / 10 = 1e-6`). With the flag off, upstream's `N_CrvRemoveDegenSegs` removes it and all 64 seam placements check clean.
- **Rust port:** under the same flag, a dimension-2 curve whose short end span (shorter than the zone tolerance mapped to parameter space, as before) has all its `degree + 1` control points within check test #15's pole tolerance is cleaned as upstream cleans it (`N_CrvRemoveDegenSegs`); other short UV end spans are still kept. Flag off is unchanged. Regression: `split_with_uv_spans_kept_checks_clean_at_every_seam` in `crates/solidcore/tests/issue_2110_tangent_cylinders_union.rs` (all 64 placements clean at Level2 and within `1e-6` relative of the closed-form volume).