Merge pull request #1922 from aapolipponen/fix/wr96

fix: topology.trimming_tools — opt-in: a later curve loop starts after the previous loop ends (WR-96)
This commit is contained in:
bherbruck
2026-10-31 02:06:06 -04:00
committed by GitHub
5 changed files with 124 additions and 19 deletions
@@ -1384,14 +1384,14 @@ fn run_rust_geom(k: &mut Kernel, op: &OpIn) -> Result<Vec<f64>, Fail> {
}
/// The shim's `tt_sample_safe`.
fn sample_safe(l: &CurveLoops, store: &[Curve], key: CurveKey, t: f64) -> Result<bool, Fail> {
fn sample_safe(k: &Kernel, l: &CurveLoops, store: &[Curve], key: CurveKey, t: f64) -> Result<bool, Fail> {
let c = store.get(key.0).ok_or_else(|| harness("key"))?;
let d = c.natural_interval();
let tol = d.length() / 1000.0;
if d.contains_value(t, -tol) {
return Ok(true);
}
let adj = match l.find_adjacent_loop_curve(store, key, t) {
let adj = match l.find_adjacent_loop_curve(k, store, key, t) {
Ok(a) => a,
Err(e) if e.is_unported() => return Err(fail(&e)),
Err(_) => 0,
@@ -1433,7 +1433,7 @@ fn run_rust_loops(
}
LoopOp::StartEnd { i } => {
o.push(2.0);
let r = l.loop_start_end(u(*i));
let r = l.loop_start_end(k, u(*i));
o.push(status(&r)?);
if let Ok((s, e)) = r {
o.extend([s as f64, e as f64]);
@@ -1441,7 +1441,7 @@ fn run_rust_loops(
}
LoopOp::Adjacent { key, t } => {
o.push(3.0);
let r = l.find_adjacent_loop_curve(&store, CurveKey(*key), *t);
let r = l.find_adjacent_loop_curve(k, &store, CurveKey(*key), *t);
o.push(status(&r)?);
if let Ok(a) = r {
o.push(a as f64);
@@ -1457,7 +1457,7 @@ fn run_rust_loops(
}
LoopOp::Sample { key, t } => {
o.push(5.0);
if !sample_safe(&l, &store, CurveKey(*key), *t)? {
if !sample_safe(k, &l, &store, CurveKey(*key), *t)? {
o.push(NULL_DEREF);
continue;
}
@@ -1481,7 +1481,7 @@ fn run_rust_loops(
o.push(NULL_DEREF);
continue;
};
if !sample_safe(&l, &store, CurveKey(*key), *t)? {
if !sample_safe(k, &l, &store, CurveKey(*key), *t)? {
o.push(NULL_DEREF);
continue;
}
+3
View File
@@ -145,6 +145,9 @@ impl Caches {
#[allow(clippy::struct_excessive_bools)]
#[non_exhaustive]
pub struct UpstreamFixes {
/// `SmCurveLoops::GetLoopStartEnd` starts a later loop after the previous loop
/// ends, keeping adjacency within that loop (WR-96). Off by default for C++ parity.
pub curve_loop_start_after_previous_end: bool,
/// The generic temporary-radius constructor reports setup failures before filleting (#458, UB-252 follow-up).
pub temporary_radius_setup_errors: bool,
/// `SmFilletGeom::Trim` returns the first curve/surface trim or interval-intersection error instead of dropping it and continuing with an unset interval (WR-604, #483). Earlier successful edits remain; off by default for pinned C++ parity.
@@ -36,7 +36,8 @@
//! `m_apObjects[1]`).
//! - **Indices** are `usize` with upstream's `ULONG` wrap-around arithmetic.
//! `GetLoopStartEnd` sets the start of a later loop to the previous loop's last index,
//! as upstream does (`plan/upstream-issues.md` WR-96). An index past the curve list, which
//! as upstream does (WR-96), unless `Kernel::fixes.curve_loop_start_after_previous_end`
//! is enabled. An index past the curve list, which
//! upstream reads out of bounds, is [`SolidError::InvalidInput`].
//! - **Ignored statuses.** `SampleNearbyPoints` ignores `FindAdjacentLoopCurve`'s status
//! (index `0` when the curve is not in the loops) and `FindAdjacentLoopCurve` ignores
@@ -281,7 +282,7 @@ impl CurveLoops {
/// `GetLoopStartEnd` with its outputs as left on failure: the status, `rlStart` and
/// `rlEnd`.
fn loop_start_end_raw(&self, curve_index: usize) -> (bool, usize, usize) {
fn loop_start_end_raw(&self, k: &Kernel, curve_index: usize) -> (bool, usize, usize) {
let mut start = 0usize;
let mut end = 0usize;
for (ii, &c) in self.loop_counts.iter().enumerate() {
@@ -292,20 +293,24 @@ impl CurveLoops {
if curve_index <= end {
return (true, start, end);
}
start = end;
start = if k.fixes.curve_loop_start_after_previous_end {
end.wrapping_add(1)
} else {
end
};
}
(false, start, end)
}
/// `GetLoopStartEnd`: the first and last curve index of the loop holding
/// `curve_index` (a later loop's start is the previous loop's last index, as
/// upstream).
/// `curve_index`. With `k.fixes.curve_loop_start_after_previous_end`, a later
/// loop starts after the previous loop's end; the default reproduces WR-96.
///
/// # Errors
///
/// `SM_ERR` when the index is past every loop.
pub fn loop_start_end(&self, curve_index: usize) -> Result<(usize, usize)> {
match self.loop_start_end_raw(curve_index) {
pub fn loop_start_end(&self, k: &Kernel, curve_index: usize) -> Result<(usize, usize)> {
match self.loop_start_end_raw(k, curve_index) {
(true, s, e) => Ok((s, e)),
_ => Err(SolidError::sm_err(OP)),
}
@@ -313,7 +318,7 @@ impl CurveLoops {
/// `FindAdjacentLoopCurve`: the index of the loop curve before `key` (when `param`
/// lies in the first half of its natural interval) or after it, cyclically in its
/// loop.
/// loop. Honors `k.fixes.curve_loop_start_after_previous_end` (WR-96).
///
/// # Errors
///
@@ -321,6 +326,7 @@ impl CurveLoops {
/// in `store`.
pub fn find_adjacent_loop_curve(
&self,
k: &Kernel,
store: &[Curve],
key: CurveKey,
param: f64,
@@ -329,7 +335,7 @@ impl CurveLoops {
return Err(SolidError::sm_err(OP));
};
// status ignored upstream
let (_, start, end) = self.loop_start_end_raw(idx);
let (_, start, end) = self.loop_start_end_raw(k, idx);
let mid = curve_of(store, key)?.natural_interval().mid();
Ok(if param < mid {
if idx == start {
@@ -425,7 +431,7 @@ impl CurveLoops {
}
// find adjacent loop curve (status ignored, index 0 on failure)
let adj = match self.find_adjacent_loop_curve(store, key, param) {
let adj = match self.find_adjacent_loop_curve(k, store, key, param) {
Ok(a) => a,
Err(e) if e.is_unported() => return Err(e),
Err(_) => 0,
@@ -770,10 +776,10 @@ mod tests {
let l = loops(&[3, 4]);
assert_eq!(l.loop_start_index(1).unwrap(), 3);
assert!(l.loop_start_index(2).is_err());
assert_eq!(l.loop_start_end(1).unwrap(), (0, 2));
assert_eq!(l.loop_start_end(&Kernel::default(), 1).unwrap(), (0, 2));
// upstream's start of a later loop is the previous loop's last index
assert_eq!(l.loop_start_end(4).unwrap(), (2, 6));
assert!(l.loop_start_end(7).is_err());
assert_eq!(l.loop_start_end(&Kernel::default(), 4).unwrap(), (2, 6));
assert!(l.loop_start_end(&Kernel::default(), 7).is_err());
}
#[test]
@@ -0,0 +1,88 @@
//! WR-96: later loops start after the previous loop's inclusive end.
use solidcore::topology::trimming_tools::curve_loops::CurveLoops;
use solidcore::Kernel;
#[test]
fn later_loop_ranges_are_disjoint_with_the_fix() {
let mut k = Kernel::default();
k.fixes.curve_loop_start_after_previous_end = true;
let keys: Vec<_> = (0..8)
.map(solidcore::topology::trimming_tools::curve_loops::CurveKey)
.collect();
let loops = CurveLoops::from_loops(&[3, 4, 1], None, Some(&keys), &[]);
for (index, range) in [
(0, (0, 2)),
(2, (0, 2)),
(3, (3, 6)),
(4, (3, 6)),
(6, (3, 6)),
(7, (7, 7)),
] {
assert_eq!(loops.loop_start_end(&k, index).unwrap(), range);
}
assert!(loops.loop_start_end(&k, 8).is_err());
}
#[test]
fn default_ranges_keep_pinned_cpp_results() {
let k = Kernel::default();
assert!(!k.fixes.curve_loop_start_after_previous_end);
let keys: Vec<_> = (0..8)
.map(solidcore::topology::trimming_tools::curve_loops::CurveKey)
.collect();
let loops = CurveLoops::from_loops(&[3, 4, 1], None, Some(&keys), &[]);
assert_eq!(loops.loop_start_end(&k, 0).unwrap(), (0, 2));
assert_eq!(loops.loop_start_end(&k, 4).unwrap(), (2, 6));
assert_eq!(loops.loop_start_end(&k, 7).unwrap(), (6, 7));
assert!(loops.loop_start_end(&k, 8).is_err());
assert!(CurveLoops::new().loop_start_end(&k, 0).is_err());
}
#[test]
fn adjacency_and_sampling_wrap_within_the_second_loop() {
use solidcore::curve::curve::Curve;
use solidcore::curve::line::Line;
use solidcore::math::Point3;
use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType;
use solidcore::topology::trimming_tools::curve_loops::CurveKey;
let store: Vec<Curve> = (0..7)
.map(|i| {
let x = f64::from(i);
Curve::from(Line::from_points(
Point3::new(x, 0.0, 0.0),
Point3::new(x, 1.0, 0.0),
3,
))
})
.collect();
let keys: Vec<_> = (0..7).map(CurveKey).collect();
let loops = CurveLoops::from_loops(&[3, 4], None, Some(&keys), &[OrientType::Same; 7]);
for fix in [false, true] {
let mut k = Kernel::default();
k.fixes.curve_loop_start_after_previous_end = fix;
let previous = if fix { 6 } else { 2 };
let next = if fix { 3 } else { 2 };
assert_eq!(
loops
.find_adjacent_loop_curve(&k, &store, CurveKey(3), 0.0)
.unwrap(),
previous
);
assert_eq!(
loops
.find_adjacent_loop_curve(&k, &store, CurveKey(6), 1.0)
.unwrap(),
next
);
let (_, before) = loops
.sample_nearby_points(&k, &store, CurveKey(3), 0.0)
.unwrap();
let (_, after) = loops
.sample_nearby_points(&k, &store, CurveKey(6), 1.0)
.unwrap();
assert_eq!(before.x, previous as f64);
assert_eq!(after.x, next as f64);
}
}
@@ -0,0 +1,8 @@
Updates: WR-96
#### WR-96 (update). Opt-in correction of later loop starts
- **Location:** `source/SMLib/src/SmTrimmingTools.cpp:4134`, `SmCurveLoops::GetLoopStartEnd`: `rlStart = rlEnd` copies an inclusive end instead of advancing past it. The pinned source retains this defect; the original record's `loops_start_end_second_loop` fixture confirms the C++ result.
- **Rust port:** `Kernel::fixes.curve_loop_start_after_previous_end` (off by default) advances the start to the previous end plus one. `CurveLoops::loop_start_end` and `find_adjacent_loop_curve` now take `&Kernel`; `sample_nearby_points` forwards its kernel so adjacency and sampling use the same opt-in.
- **Regression:** `crates/solidcore/tests/wr96_curve_loop_start.rs` checks first, second and singleton third loops, out-of-range indices, default C++ ranges, and both adjacency directions and endpoint sampling on the second loop. Before the formula change, the enabled range regression failed with `(2, 6)` instead of `(3, 6)` for counts `[3, 4, 1]`.
- **Scope:** only the later-loop start assignment changes when enabled. Empty-loop count underflow and upstream's ignored-status behavior remain as before. Default difftest inputs and tolerances are unchanged.