From f1845b2abff366fe9de0e5f3f17ed4924fa044de Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 17:15:13 -0700 Subject: [PATCH 1/5] claim: region nesting disagreement after shell-ray face preparation Co-Authored-By: Claude Opus 5.5 From fd54ae43a8aabe6dcecd8b0a70b0882ea24e4dbb Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 17:08:38 -0700 Subject: [PATCH 2/5] fix: keep IsInnerShell's UV trim curves on the B-rep IsInnerShell ran its rays and PointsInFace on a copy of the B-rep, and PointsInFace's UV-domain and curve classification on further copies, so the UV trim curves upstream's const face queries leave on the B-rep were dropped. Run them on the caller's B-rep and copy the classification snapshot's trims and tolerance growth back. Co-Authored-By: Claude Opus 5.5 --- .../src/units/topology_topology_traverser.rs | 13 ++- crates/solidcore/src/query/face/p2.rs | 27 ++++++ crates/solidcore/src/topology/brep/p7.rs | 2 +- crates/solidcore/src/topology/face/p2.rs | 82 +++++++++++++------ crates/solidcore/src/topology/shell.rs | 13 +-- .../src/topology/topology_traverser.rs | 15 ++-- 6 files changed, 106 insertions(+), 46 deletions(-) diff --git a/crates/difftest/src/units/topology_topology_traverser.rs b/crates/difftest/src/units/topology_topology_traverser.rs index 327487fbb..9b9bafa13 100644 --- a/crates/difftest/src/units/topology_topology_traverser.rs +++ b/crates/difftest/src/units/topology_topology_traverser.rs @@ -232,15 +232,14 @@ impl UnitDiff for Runner { &mut k, &mut b, &mut marks, start_fu, target, &mut fus, &lock, ), 6 => tr::remove_shell(&mut k, &mut b, ShellId::from_index(r.start as u32), target), - _ => tr::is_inner_shell( - &mut k, - &b, - &b.faceuses() + _ => { + let faceuses = b + .faceuses() .iter() .filter_map(|(id, fu)| (fu.shell == Some(target)).then_some(id)) - .collect::>(), - ) - .map(|v| closed = i64::from(v)), + .collect::>(); + tr::is_inner_shell(&mut k, &mut b, &faceuses).map(|v| closed = i64::from(v)) + } }; if let Err(e) = result { if let Some(out) = Output::unported(&e) { diff --git a/crates/solidcore/src/query/face/p2.rs b/crates/solidcore/src/query/face/p2.rs index 0d61f9455..d4df12e4b 100644 --- a/crates/solidcore/src/query/face/p2.rs +++ b/crates/solidcore/src/query/face/p2.rs @@ -956,6 +956,33 @@ pub(crate) struct CurveOnClassifyCache { } impl CurveOnClassifyCache { + /// Copy back what the classification's lazy queries left on its snapshot that + /// upstream keeps on the face's B-rep: the gap caches ([`Self::apply_gap_updates`]), + /// the edge uses' UV trim curves and the edge and vertex tolerances + /// `SetUVTrimCurve` raised to fit them. + pub(crate) fn apply_uv_trim_updates(self, b: &mut Brep) -> Result<()> { + if let Some(snapshot) = &self.snapshot { + for (id, eu) in snapshot.edgeuses().iter() { + if b.edgeuse(id)?.uv_trim_curve.is_none() && eu.uv_trim_curve.is_some() { + b.edgeuse_mut(id)? + .uv_trim_curve + .clone_from(&eu.uv_trim_curve); + } + } + for (id, e) in snapshot.edges().iter() { + if e.tolerance > b.edge(id)?.tolerance { + b.edge_mut(id)?.tolerance = e.tolerance; + } + } + for (id, v) in snapshot.vertices().iter() { + if v.tolerance > b.vertex(id)?.tolerance { + b.vertex_mut(id)?.tolerance = v.tolerance; + } + } + } + self.apply_gap_updates(b) + } + pub(crate) fn apply_gap_updates(self, b: &mut Brep) -> Result<()> { if let Some(snapshot) = self.snapshot { for (id, vu) in snapshot.vertexuses().iter() { diff --git a/crates/solidcore/src/topology/brep/p7.rs b/crates/solidcore/src/topology/brep/p7.rs index fa98e1a94..5018b6fa9 100644 --- a/crates/solidcore/src/topology/brep/p7.rs +++ b/crates/solidcore/src/topology/brep/p7.rs @@ -512,7 +512,7 @@ fn merge_shells(k: &mut Kernel, b: &mut Brep, mut del: ShellId, mut keep: ShellI } else if del_region == inf // pKeepShell->IsInnerShell(bIsInner): swap when it succeeds with TRUE (asked // only when neither region is the infinite one) - || ignored(b.shell(keep)?.is_inner_shell(k, b))? == Some(true) + || ignored(crate::topology::Shell::is_inner_shell(k, b, keep))? == Some(true) { core::mem::swap(&mut del, &mut keep); } diff --git a/crates/solidcore/src/topology/face/p2.rs b/crates/solidcore/src/topology/face/p2.rs index db19607a4..3b208eeaf 100644 --- a/crates/solidcore/src/topology/face/p2.rs +++ b/crates/solidcore/src/topology/face/p2.rs @@ -113,7 +113,9 @@ use crate::nurbs::b_spline_curve_n_lib::TrimMode; use crate::nurbs::b_spline_surface::p1::SurfParamType; use crate::nurbs::b_spline_surface_n_lib::p2::{EvaluateFlags, OrientType}; use crate::nurbs::knots::Side; -use crate::query::face::p2::{curve_on_classify, CurveOnClassifyObjects}; +use crate::query::face::p2::{ + curve_on_classify_cached, CurveOnClassifyCache, CurveOnClassifyObjects, +}; use crate::query::face::p3 as qf3; use crate::surface::offset_surface::{OffsetSurface, SS_UNKNOWN}; use crate::surface::surface::p1::map_to_2d; @@ -681,6 +683,39 @@ fn classify_curve_on_face( with_uv_curve: bool, tol: f64, data_size: usize, +) -> Result>> { + let mut cache = CurveOnClassifyCache::default(); + classify_curve_on_face_cached(k, b, f, curve, with_uv_curve, tol, data_size, &mut cache) +} + +/// [`classify_curve_on_face`] on the caller's B-rep: the UV trim curves (with their +/// edge tolerance growth) and gap caches the classification makes stay on `b`, as +/// upstream's `const` `CurveOnClassify` leaves them. +fn classify_curve_on_face_in_place( + k: &Kernel, + b: &mut Brep, + f: FaceId, + curve: &Curve, + with_uv_curve: bool, + tol: f64, +) -> Result>> { + let mut cache = CurveOnClassifyCache::default(); + let intervals = + classify_curve_on_face_cached(k, b, f, curve, with_uv_curve, tol, 0, &mut cache); + cache.apply_uv_trim_updates(b)?; + intervals +} + +#[allow(clippy::too_many_arguments)] // upstream's constructor arguments +fn classify_curve_on_face_cached( + k: &Kernel, + b: &Brep, + f: FaceId, + curve: &Curve, + with_uv_curve: bool, + tol: f64, + data_size: usize, + cache: &mut CurveOnClassifyCache, ) -> Result>> { let objects = FaceClassifyObjects { brep: b, curve }; let init = ClassificationInit { @@ -699,7 +734,7 @@ fn classify_curve_on_face( }; let mut classification = CurveClassification::with_curve(k, &init)?; // bHasBigGaps, bDoCheck3d: FALSE; bUVSpaceOkay: TRUE (the defaults) - curve_on_classify( + curve_on_classify_cached( k, b, f, @@ -709,21 +744,21 @@ fn classify_curve_on_face( false, false, true, + cache, )?; Ok(classification.intervals().to_vec()) } /// `SmFace::CalculateUVDomainFromUVTrimCurves(rTrimUVDomain)` (unit `topology.face_p1`), -/// on a copy of the B-rep: the caller holds it shared, so the UV trim curves the call -/// makes (a cache upstream fills through a `const` method) are not kept. The copy costs -/// a whole-B-rep clone per call (O(faces * B-rep size) over a sweep of every face); pass -/// the B-rep mutably once [`points_in_face`]'s callers can. -fn calculate_uv_domain_from_uv_trim_curves(k: &Kernel, b: &Brep, f: FaceId) -> Result { - let mut copy = b.clone(); - crate::topology::face::p1::calculate_uv_domain_from_uv_trim_curves( - k, &mut copy, f, true, false, false, - ) - .map(|r| r.domain) +/// on the caller's B-rep: the UV trim curves the call makes (a cache upstream fills +/// through a `const` method) stay on it. +fn calculate_uv_domain_from_uv_trim_curves( + k: &Kernel, + b: &mut Brep, + f: FaceId, +) -> Result { + crate::topology::face::p1::calculate_uv_domain_from_uv_trim_curves(k, b, f, true, false, false) + .map(|r| r.domain) } /// `pEU->NormalizedEvaluate(dT, TRUE, rUV)` (UV evaluation). @@ -1207,7 +1242,7 @@ fn rand_dither(k: &mut Kernel) -> f64 { /// without surface; [`SolidError::Unported`] for the trimmed branch. pub fn points_in_face( k: &mut Kernel, - b: &Brep, + b: &mut Brep, f: FaceId, point_count: u64, ) -> Result { @@ -1231,14 +1266,15 @@ pub fn points_in_face( return Err(null("GetPointsInFace: point count too large")); } - // locals - let surf = surface_deref(b, f)?; + // locals (the surface is cloned: the face classifications below edit `b`) + let rectangular_trim = face.rectangular_trim; + let surf = surface_deref(b, f)?.clone(); let count = point_count as usize; let mut trim_curve_cnt = (2 * point_count).min(30); u_inc /= (trim_curve_cnt + 1) as f64; let mut u = u_inc; - if face.rectangular_trim == 1 { + if rectangular_trim == 1 { // when faced is rectangularly trimmed trim_curve_cnt = point_count.min(30); let j_cnt = if (point_count / trim_curve_cnt) * trim_curve_cnt < point_count { @@ -1318,9 +1354,9 @@ pub fn points_in_face( } // classify the isoParameter UVTrimCurve against the face - let tol = face.tolerance; + let tol = b.face(f)?.tolerance; let crv = Curve::from(uv_curve.clone()); - let intervals = classify_curve_on_face(k, b, f, &crv, true, tol, 0)?; + let intervals = classify_curve_on_face_in_place(k, b, f, &crv, true, tol)?; if intervals .iter() .any(|ivl| ivl.mid.point_class() == PointClassType::Face) @@ -2816,16 +2852,16 @@ mod tests { .unwrap(); let _ = s; let mut k = Kernel::default(); - let r = points_in_face(&mut k, &b, f, 1).unwrap(); + let r = points_in_face(&mut k, &mut b, f, 1).unwrap(); assert_eq!(r.uv, vec![Point2::new(1.0 / 3.0, 1.0)]); assert_eq!(r.pv.len(), 2); assert_eq!(r.pv[1], Vec3::Z); - let r = points_in_face(&mut k, &b, f, 7).unwrap(); + let r = points_in_face(&mut k, &mut b, f, 7).unwrap(); assert_eq!(r.uv.len(), 7); - assert!(points_in_face(&mut k, &b, f, MAX_POINTS_IN_FACE + 1).is_err()); - assert!(points_in_face(&mut k, &b, f, 0).unwrap().uv.is_empty()); + assert!(points_in_face(&mut k, &mut b, f, MAX_POINTS_IN_FACE + 1).is_err()); + assert!(points_in_face(&mut k, &mut b, f, 0).unwrap().uv.is_empty()); set_uv_domain(&mut b, f, &Extent2d::from_bounds(0.0, 0.0, 0.0, 1.0)).unwrap(); - assert!(points_in_face(&mut k, &b, f, 1).is_err()); + assert!(points_in_face(&mut k, &mut b, f, 1).is_err()); } #[test] diff --git a/crates/solidcore/src/topology/shell.rs b/crates/solidcore/src/topology/shell.rs index 58d2398bf..a756544ef 100644 --- a/crates/solidcore/src/topology/shell.rs +++ b/crates/solidcore/src/topology/shell.rs @@ -295,15 +295,16 @@ impl Shell { })??; Ok(output) } - /// Classify whether this is an inner shell: `SmTopologyTraverser::IsInnerShell` - /// on this shell's face uses (`GetFaceuses`). + /// Classify whether shell `id` of `brep` is an inner shell: + /// `SmTopologyTraverser::IsInnerShell` on its face uses (`GetFaceuses`), on `brep` + /// itself (the UV trim curves its face queries make stay, as upstream's). /// /// # Errors - /// Invalid member classes and the errors of + /// A dangling id, invalid member classes and the errors of /// [`crate::topology::topology_traverser::is_inner_shell`] (its `SM_ERR` for an /// empty or inconclusive set, `Unported` from its ray intersections). - pub fn is_inner_shell(&self, kernel: &mut Kernel, brep: &Brep) -> Result { - let faceuses = self.get_faceuses()?; + pub fn is_inner_shell(kernel: &mut Kernel, brep: &mut Brep, id: ShellId) -> Result { + let faceuses = brep.shell(id)?.get_faceuses()?; crate::topology::topology_traverser::is_inner_shell(kernel, brep, &faceuses) } } @@ -427,7 +428,7 @@ pub fn order_region_shells(kernel: &mut Kernel, brep: &mut Brep, id: RegionId) - if faceuse_shells.len() > 1 { for &shell in &faceuse_shells { if brep.shell(shell)?.is_closed(brep, shell)? { - match brep.shell(shell)?.is_inner_shell(kernel, brep) { + match Shell::is_inner_shell(kernel, brep, shell) { Ok(true) => { chosen = Some(shell); break; diff --git a/crates/solidcore/src/topology/topology_traverser.rs b/crates/solidcore/src/topology/topology_traverser.rs index 6f811b7da..8ea867f30 100644 --- a/crates/solidcore/src/topology/topology_traverser.rs +++ b/crates/solidcore/src/topology/topology_traverser.rs @@ -546,15 +546,12 @@ fn line_parameters( /// failures skip the face, as upstream). /// An empty or inconclusive set returns the upstream `SM_ERR`. /// -/// The rays run on one copy of `b`, so the lazy cache updates of the face -/// classifications carry over from ray to ray as upstream's do, and are dropped at the -/// end (`b` is the caller's shared B-rep). -pub fn is_inner_shell(k: &mut Kernel, b: &Brep, faceuses: &[FaceuseId]) -> Result { +/// The rays and face samples run on `b` itself, so the UV trim curves their face +/// classifications create stay on the B-rep, as upstream's `const` queries leave them. +pub fn is_inner_shell(k: &mut Kernel, b: &mut Brep, faceuses: &[FaceuseId]) -> Result { if faceuses.is_empty() { return Err(SolidError::sm_err(OP)); } - let mut scratch = b.clone(); - let b = &mut scratch; let mut samples = 3; if faceuses.len() < 6 { samples *= 2; @@ -569,9 +566,9 @@ pub fn is_inner_shell(k: &mut Kernel, b: &Brep, faceuses: &[FaceuseId]) -> Resul let mut caches: BTreeMap> = BTreeMap::new(); for &source in faceuses { let source = b.faceuse(source)?; - let f = required(source.face)?; - let mut points = face::points_in_face(k, &*b, f, samples)?.pv; - if source.orientation == OrientType::Opposite { + let (f, orientation) = (required(source.face)?, source.orientation); + let mut points = face::points_in_face(k, b, f, samples)?.pv; + if orientation == OrientType::Opposite { for pair in points.chunks_exact_mut(2) { if let Some(normal) = pair.get_mut(1) { *normal *= -1.0; From 165745d640776270a2b2aac3fe276feab3000b21 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 17:32:12 -0700 Subject: [PATCH 3/5] docs: record the IsInnerShell root cause and update the parked cases Co-Authored-By: Claude Opus 5.5 --- crates/difftest/known_mismatches.toml | 24 +++------- crates/solidcore/src/topology/face/p2.rs | 6 ++- .../src/topology/topology_traverser.rs | 7 +-- plan/findings.md | 45 +++++++++++++++++++ .../primitive.primitive_creation_p2.toml | 2 +- plan/units/sweep.api_surfaces.toml | 2 +- plan/units/topology.brep_p6.toml | 2 +- plan/units/topology.face_p2.toml | 2 +- plan/units/topology.topology_traverser.toml | 2 +- 9 files changed, 66 insertions(+), 26 deletions(-) diff --git a/crates/difftest/known_mismatches.toml b/crates/difftest/known_mismatches.toml index 6f05b8131..8d6f0c7e9 100644 --- a/crates/difftest/known_mismatches.toml +++ b/crates/difftest/known_mismatches.toml @@ -46,47 +46,37 @@ unit = "topology.brep_p3" case = "gen0241" why = "TurnToNURBS tracker: upstream curves/surfaces notify their own SM_NO_PRE_EDIT/SM_NO_POST_EDIT edits, the port's geometry sends none (plan/findings.md: topology.face_p1 SetSurface SM_NO_CHANGE_GEOMETRY wired (PR #637), 'Still deferred: geometry edit notifications')" -[[mismatch]] -unit = "primitive.primitive_creation_p2" -case = "skin_concentric_loops" -why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)" - -[[mismatch]] -unit = "sweep.api_surfaces" -case = "skin_primitive_annular" -why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)" - [[mismatch]] unit = "topology.brep_p6" case = "two_cubes_at_a_vertex" -why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)" +why = "geometry matches; only the Mark2 counter differs (one per IsInnerShell sample face): upstream IntersectTSBound takes the face's trimmed-surface-cache branch with SmNewMarkAndLock, the port walks the loop uses (plan/findings.md: IsInnerShell classifies on the B-rep itself)" [[mismatch]] unit = "topology.brep_p6" case = "two_disjoint_cubes" -why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)" +why = "geometry matches; only the Mark2 counter differs (one per IsInnerShell sample face): upstream IntersectTSBound takes the face's trimmed-surface-cache branch with SmNewMarkAndLock, the port walks the loop uses (plan/findings.md: IsInnerShell classifies on the B-rep itself)" [[mismatch]] unit = "topology.brep_p6" case = "cube_in_cube" -why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)" +why = "geometry matches; only the Mark2 counter differs (one per IsInnerShell sample face): upstream IntersectTSBound takes the face's trimmed-surface-cache branch with SmNewMarkAndLock, the port walks the loop uses (plan/findings.md: IsInnerShell classifies on the B-rep itself)" [[mismatch]] unit = "topology.brep_p6" case = "cube_with_wire" -why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)" +why = "geometry matches; only the Mark2 counter differs (one per IsInnerShell sample face): upstream IntersectTSBound takes the face's trimmed-surface-cache branch with SmNewMarkAndLock, the port walks the loop uses (plan/findings.md: IsInnerShell classifies on the B-rep itself)" [[mismatch]] unit = "topology.brep_p6" case = "cube_with_inner_wire" -why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)" +why = "geometry matches; only the Mark2 counter differs (one per IsInnerShell sample face): upstream IntersectTSBound takes the face's trimmed-surface-cache branch with SmNewMarkAndLock, the port walks the loop uses (plan/findings.md: IsInnerShell classifies on the B-rep itself)" [[mismatch]] unit = "topology.brep_p6" case = "cube_with_inner_vertex" -why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)" +why = "geometry matches; only the Mark2 counter differs (one per IsInnerShell sample face): upstream IntersectTSBound takes the face's trimmed-surface-cache branch with SmNewMarkAndLock, the port walks the loop uses (plan/findings.md: IsInnerShell classifies on the B-rep itself)" [[mismatch]] unit = "topology.brep_p6" case = "cube_with_outer_vertex" -why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)" +why = "geometry matches; only the Mark2 counter differs (one per IsInnerShell sample face): upstream IntersectTSBound takes the face's trimmed-surface-cache branch with SmNewMarkAndLock, the port walks the loop uses (plan/findings.md: IsInnerShell classifies on the B-rep itself)" diff --git a/crates/solidcore/src/topology/face/p2.rs b/crates/solidcore/src/topology/face/p2.rs index 3b208eeaf..82020dfd3 100644 --- a/crates/solidcore/src/topology/face/p2.rs +++ b/crates/solidcore/src/topology/face/p2.rs @@ -82,7 +82,11 @@ //! against upstream (`plan/findings.md`). Point counts above //! [`MAX_POINTS_IN_FACE`] are [`SolidError::InvalidInput`] (upstream allocates //! them). The rectangular branch samples from `dUInc` / `dVInc`, not from the -//! domain minimum, as upstream does (`plan/upstream-issues.md`). +//! domain minimum, as upstream does (`plan/upstream-issues.md`). It takes the +//! B-rep mutably: its UV-domain and iso-line classifications run on it (the +//! classification's snapshot hands back the UV trim curves, the tolerance growth +//! that fits them and the gap caches), so what upstream's `const` queries leave on +//! the B-rep stays there. //! - **`CreateCurvesByTrimming`** drops its unused `bForceUVClassify` argument; the //! output arrays are asked for with `want_3d` / `want_uv`. //! - **Unported callees.** `SmCurveClassification::ReSet` in the constructor of a curve diff --git a/crates/solidcore/src/topology/topology_traverser.rs b/crates/solidcore/src/topology/topology_traverser.rs index 8ea867f30..da976d311 100644 --- a/crates/solidcore/src/topology/topology_traverser.rs +++ b/crates/solidcore/src/topology/topology_traverser.rs @@ -22,9 +22,10 @@ //! arrays where early returns preserve optional arrays upstream. Shell ownership //! moves through `topology::shell` (`transfer_children`, `move_faceuse`: the //! `Remove` / `PostInsert` statuses that `FaceuseShellReplacement` ignores cannot -//! fail on a consistent B-rep, so an inconsistent one is an error). The ray/face -//! intersections (`SmFace::GlobalLineIntersect`) classify on a copy of the B-rep. -//! `IsInnerShell` keeps each target face surface's solve cache for the whole call +//! fail on a consistent B-rep, so an inconsistent one is an error). `IsInnerShell`'s +//! face samples and ray/face intersections (`SmFace::GetPointsInFace`, +//! `SmFace::GlobalLineIntersect`) run on the caller's B-rep, which keeps the UV trim +//! curves they make, as upstream's `const` queries leave them. `IsInnerShell` keeps each target face surface's solve cache for the whole call //! (upstream's surface keeps its cache across the rays); the port otherwise builds one //! per intersection, and a failed build is kept too instead of being retried on every //! ray (the build is deterministic, so a retry fails the same way). diff --git a/plan/findings.md b/plan/findings.md index 487f9d79e..ba160ee49 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -17972,6 +17972,51 @@ claimed. No fixture, comparison gate or tolerance was changed. - **Status.** All 9 cases are listed in `crates/difftest/known_mismatches.toml`. They were deferred on both parents, so no case that previously passed now fails. +## IsInnerShell classifies on the B-rep itself (follow-up to #685, 2026-10-24) + +- **Root cause of the region-nesting mismatches parked by #685.** The disagreements in + topology.brep_p6 `make_face.json#89..95` were missing UV trim curves. + - `topology_traverser::is_inner_shell` ran its rays and `GetPointsInFace` on a + `b.clone()` scratch copy. + - `topology::face::p2::points_in_face` ran `CalculateUVDomainFromUVTrimCurves` and + its iso-line `CurveOnClassify` on further copies. + - Upstream's `const` face queries leave the UV trim curves they create + (`GetOrCreateUVTrimCurve`) on the B-rep. The C++ export had a stored trim on + edge uses where Rust had none (two_disjoint_cubes geom.len cpp 1385, rust 1232). + - Found by logging every `set_uv_trim_curve` with its B-rep address: 15 trims landed + on the final B-rep and 18 on copies. All 18 came from `is_inner_shell`, through + `global_line_intersect_held` (10), `points_in_face`'s `classify_curve_on_face` (6) + and `calculate_uv_domain_from_uv_trim_curves` (2). +- **Fix.** + - `is_inner_shell`, `points_in_face` and `Shell::is_inner_shell` take + `&mut Brep` and run on the caller's B-rep. + - The iso-line classification copies the UV trim curves, raised edge/vertex + tolerances and gap caches back from its `CurveOnClassifyCache` snapshot + (`apply_uv_trim_updates`). + - The brep_p6 geometry and topology now match C++ exactly. +- **Still differs (kept in `known_mismatches.toml`): the Mark2 counter.** Upstream + takes one more mark per sampled face (two_disjoint_cubes cpp 28, rust 16; one + cube 14 vs 8). + - It comes from `SmCurveClassification::IntersectTSBound`'s trimmed-surface-cache + branch (`SmCurveClass.cpp:16242`, `SmNewMarkAndLock` while walking the cache's + UV nodes). + - The port's `query::face::p2::intersect_ts_bound` walks every loop use when the + caller supplies no trimmed cache. This is the gap recorded under the #639 entry + above ("IntersectTSBound without the face's trimmed cache"). + - Porting the cached branch needs the face-owned trimmed cache that + `smsurf_GetSurfaceCache` builds (surface.trim_srf_cache), plus `&mut Kernel` + through the classification for the mark. That is a separate change. +- **The skin cases.** sweep.api_surfaces `skin_primitive_annular`, + primitive.primitive_creation_p2 `skin_concentric_loops`, and creation_p2 gen0201 + and gen0214 (which passed before) now go further and defer at + `TrimSrfCache::build_tree_with_face for a face-owned surface` + (surface.trim_srf_cache): with the trims kept, a later face query needs the + face-owned trimmed cache. Their known-mismatch entries are removed. +- **#614's copy-based `classify_face_interval` path is not involved.** Switching the + `query::face::p2` curve-on-classify between the read-only and mutable interval + classification changed nothing for these cases (probe recorded in the #685 entry). + This fix does not change that path. + ## Status index (2026-10-23, main CI `e33985411`) This file is append-only, so older headings keep the status they had when written. This diff --git a/plan/units/primitive.primitive_creation_p2.toml b/plan/units/primitive.primitive_creation_p2.toml index f6aff2492..0984693ed 100644 --- a/plan/units/primitive.primitive_creation_p2.toml +++ b/plan/units/primitive.primitive_creation_p2.toml @@ -9,4 +9,4 @@ tests = ["SM_API_test:TestSmCreateSkinPrimitive", "SM_API_test:TestSmPipeSweep", status = "partial" claimed_by = "" claimed_at = "" -notes = "main CI e33985411 (2026-10-23): PARTIAL primitive.primitive_creation_p2 cases=328 passed=195 failed=0 deferred=113 rejected=20 blocked_by=boolean.merge_p2(34),surface.trim_srf_cache(57),topology.face_p2(8),topology.face_p4(14). CO-PORTED in sweep.api_brep (PR #110): SmSweepOptions -> solidcore::primitive::primitive_creation::SweepOptions (crates/solidcore/src/primitive/primitive_creation.rs, parent module of p1..p3); 12 fns; largest: SmPrimitiveCreation::CreateTaperExtrude, SmPrimitiveCreation::CreateSweepAlongPlanarPath, sm_CreateSkinPrimitiveInternal, SmPrimitiveCreation::CreateCone; 45 upstream test entry points reach this unit; PR #250: all twelve owned functions ported through PrimitiveCreationAccess (extends p3's PrimitiveAccess; Unported defaults for B-rep services of unported units), with full tails after unavailable calls. Difftest 328 cases: 129 bit-identical passes (entry-point early exits: cone height/turn/side/axis with the editing flag left on, degenerate sphere/torus radii, full turns through p1, skins of fewer than two profiles with their cleared lists and ownership report, curve sweeps of nothing), 179 deferrals (topology.brep_p3 81, topology.brep_p8 78, topology.brep_p1 20), 20 guarded rejections (UB-215, HANG-35, UB-50, UB-247), 0 failures; no absolute floors; perturb 129/129; fuzz 1000 clean. PR #250 review (2026-10-20): the taper's FFI guard replays FixChainCurvesList (both input passes; the reversed pass only where upstream may take it; with caps 2/3, where upstream may reverse, a closed chain whose short curve meets one over 100 times longer at a corner sharper than 3e-5 rad is refused for the offset end cap's double free, UB-50; with caps 1/3 curves that close a loop while an end meets no other end or more than one, UB-247); review_duplicate_curves.json keeps 31 tapers that run natively; ComputeCrvsNormal failures zero the normal as upstream; api_primitives' sphere/cone/torus stubs wired to their next unported call (topology.brep_p3) and the blend retagged to topology.brep_p8. Construction tails (partial sweeps, taper, pipe, planar path, skin faces, blend) are not certified. PR #250 second review (2026-10-21): SmBrep::SetTolerance, IsManifoldSolid and SmMerge::BooleanTrees wired (topology.brep_p2, boolean.merge_p3); merged #233 f810c60a and main ca3bbd2b. New upstream issues UB-215, HANG-35, WR-191, UB-247. Caller must supply a concrete PrimitiveCreationAccess. See fixtures/primitive.primitive_creation_p2/README.md and plan/findings.md. PrimitiveAccess forwards caller notifications into the shared SweepStore constructor/merge services. Wire (2026-10-23, with primitive.primitive_creation_p3): PrimitiveAccess's B-rep face services and axis classification are real defaults; the runner adapter keeps a tag store and untagged curves. Difftest 328 cases: 196 bit-identical passes (was 146), 0 failed, 112 deferred (was 162; topology.brep_p8 71 -> 0, now topology.brep_p3 81, topology.face_p4 14, surface.trim_srf_cache 6, topology.face_p2 5, curve.curve_p2 3, query.face_p1 3), 20 rejected. Runner on the production adapter (2026-10-23): entry points run through primitive::primitive_creation::model::PrimitiveModel (scratch B-reps, face copies, production face keys) instead of a one-B-rep fixture. Difftest 328 cases: 194 bit-identical passes, 0 failed, 114 deferred (surface.trim_srf_cache 51, boolean.merge_p2 34 via PrimitiveModel::new_merge, topology.face_p4 14, curve.curve_p2 7, topology.face_p2 5, query.face_p1 3), 20 rejected; 10 cases that waited for SmBrep::SmBrep now pass, 12 earlier passes relied on fixture face keys the B-rep caches cannot resolve and now defer (plan/findings.md). KNOWN MISMATCH (stacked merge of #613 with main): skin_concentric_loops runs once #613's shell ray face preparation and #639's forwarded face hooks combine, and its region nesting differs from C++; listed in known_mismatches.toml (findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)." +notes = "main CI e33985411 (2026-10-23): PARTIAL primitive.primitive_creation_p2 cases=328 passed=195 failed=0 deferred=113 rejected=20 blocked_by=boolean.merge_p2(34),surface.trim_srf_cache(57),topology.face_p2(8),topology.face_p4(14). CO-PORTED in sweep.api_brep (PR #110): SmSweepOptions -> solidcore::primitive::primitive_creation::SweepOptions (crates/solidcore/src/primitive/primitive_creation.rs, parent module of p1..p3); 12 fns; largest: SmPrimitiveCreation::CreateTaperExtrude, SmPrimitiveCreation::CreateSweepAlongPlanarPath, sm_CreateSkinPrimitiveInternal, SmPrimitiveCreation::CreateCone; 45 upstream test entry points reach this unit; PR #250: all twelve owned functions ported through PrimitiveCreationAccess (extends p3's PrimitiveAccess; Unported defaults for B-rep services of unported units), with full tails after unavailable calls. Difftest 328 cases: 129 bit-identical passes (entry-point early exits: cone height/turn/side/axis with the editing flag left on, degenerate sphere/torus radii, full turns through p1, skins of fewer than two profiles with their cleared lists and ownership report, curve sweeps of nothing), 179 deferrals (topology.brep_p3 81, topology.brep_p8 78, topology.brep_p1 20), 20 guarded rejections (UB-215, HANG-35, UB-50, UB-247), 0 failures; no absolute floors; perturb 129/129; fuzz 1000 clean. PR #250 review (2026-10-20): the taper's FFI guard replays FixChainCurvesList (both input passes; the reversed pass only where upstream may take it; with caps 2/3, where upstream may reverse, a closed chain whose short curve meets one over 100 times longer at a corner sharper than 3e-5 rad is refused for the offset end cap's double free, UB-50; with caps 1/3 curves that close a loop while an end meets no other end or more than one, UB-247); review_duplicate_curves.json keeps 31 tapers that run natively; ComputeCrvsNormal failures zero the normal as upstream; api_primitives' sphere/cone/torus stubs wired to their next unported call (topology.brep_p3) and the blend retagged to topology.brep_p8. Construction tails (partial sweeps, taper, pipe, planar path, skin faces, blend) are not certified. PR #250 second review (2026-10-21): SmBrep::SetTolerance, IsManifoldSolid and SmMerge::BooleanTrees wired (topology.brep_p2, boolean.merge_p3); merged #233 f810c60a and main ca3bbd2b. New upstream issues UB-215, HANG-35, WR-191, UB-247. Caller must supply a concrete PrimitiveCreationAccess. See fixtures/primitive.primitive_creation_p2/README.md and plan/findings.md. PrimitiveAccess forwards caller notifications into the shared SweepStore constructor/merge services. Wire (2026-10-23, with primitive.primitive_creation_p3): PrimitiveAccess's B-rep face services and axis classification are real defaults; the runner adapter keeps a tag store and untagged curves. Difftest 328 cases: 196 bit-identical passes (was 146), 0 failed, 112 deferred (was 162; topology.brep_p8 71 -> 0, now topology.brep_p3 81, topology.face_p4 14, surface.trim_srf_cache 6, topology.face_p2 5, curve.curve_p2 3, query.face_p1 3), 20 rejected. Runner on the production adapter (2026-10-23): entry points run through primitive::primitive_creation::model::PrimitiveModel (scratch B-reps, face copies, production face keys) instead of a one-B-rep fixture. Difftest 328 cases: 194 bit-identical passes, 0 failed, 114 deferred (surface.trim_srf_cache 51, boolean.merge_p2 34 via PrimitiveModel::new_merge, topology.face_p4 14, curve.curve_p2 7, topology.face_p2 5, query.face_p1 3), 20 rejected; 10 cases that waited for SmBrep::SmBrep now pass, 12 earlier passes relied on fixture face keys the B-rep caches cannot resolve and now defer (plan/findings.md). KNOWN MISMATCH (stacked merge of #613 with main): skin_concentric_loops runs once #613's shell ray face preparation and #639's forwarded face hooks combine, and its region nesting differs from C++; listed in known_mismatches.toml (findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks). UPDATE (IsInnerShell on the B-rep itself): skin_concentric_loops, gen0201 and gen0214 now defer at surface.trim_srf_cache (TrimSrfCache::build_tree_with_face for a face-owned surface) once IsInnerShell keeps its UV trims; known-mismatch entry removed." diff --git a/plan/units/sweep.api_surfaces.toml b/plan/units/sweep.api_surfaces.toml index ae7d17c48..55364f8a9 100644 --- a/plan/units/sweep.api_surfaces.toml +++ b/plan/units/sweep.api_surfaces.toml @@ -9,4 +9,4 @@ tests = ["SM_API_test:TestSmCreateRuledSurface", "SM_API_test:TestSmCreateSkinSu status = "partial" claimed_by = "" claimed_at = "" -notes = "main CI e33985411 (2026-10-23): PARTIAL sweep.api_surfaces cases=882 passed=879 failed=0 deferred=3 blocked_by=surface.trim_srf_cache(3). partial (PR #37): difftest cases=882 passed=831 deferred=51 (blocked_by primitive.primitive_creation_p2(11), primitive.primitive_creation_p3(40): SmApiCreateSkinPrimitive / SmApiCreateSkinFromFaces / SmApiCreateSurfaceRevolution validate and copy inputs, then stub SmPrimitiveCreation::CreateSkinPrimitive / CreateSkinFromFaces / CreateRotationalSweep); extrude, sweep, ruled and skin bit-identical; 9 fns; largest: SmApiCreateSkinPrimitive, SmApiCreateSurfaceRevolution, SmApiCreateSkin, SmApiCreateSkinFromFaces; entry points: SmApiCreateExtrude, SmApiCreateRuledSurface, SmApiCreateSkin, SmApiCreateSkinFromFaces, SmApiCreateSkinPrimitive, SmApiCreateSurfaceRevolution, SmApiCreateSweep; 15 upstream test entry points reach this unit UPDATE after topology.brep_p3 (PR #204): SmBrep::SmBrep wired to topology::brep::p3::new_brep (create_skin_primitive/create_skin_from_faces take k: &mut Kernel, create_surface_revolution gained the kernel, upstream API context); 882 cases, 831 pass, 0 failed, 51 deferred (topology.shell 51). UPDATE (PR #204, after topology.shell was wired into topology.brep_p3): new_brep completes, so the 51 topology.shell deferrals now stop at SmPrimitiveCreation: 882 cases, 831 pass, 0 failed, 51 deferred (primitive.primitive_creation_p2 11, primitive.primitive_creation_p3 40), case-by-case only the deferral target changed. partial (PR #37): difftest cases=882 passed=831 deferred=51 (blocked_by primitive.primitive_creation_p2(11), topology.brep_p3(40): SmApiCreateSkinPrimitive / SmApiCreateSkinFromFaces / SmApiCreateSurfaceRevolution validate and copy inputs; p3 construction calls await the earlier SmBrep constructor and a concrete PrimitiveAccess adapter (PR #233)); UPDATE 2026-10-23 (claude/face-p1-hooks): the query.face_p1 (SmFace::Point3DClassify) deferral of upstream.json#14 now runs past it and defers at boolean.topology_intersector_p2; cases=882 passed=862 failed=0 deferred=20 (boolean.topology_intersector_p2 17, surface.trim_srf_cache 3) (Mac Studio run; Linux CI is the gate).; Historical E validation before integration with query.face_p1 hooks: Primitive p3 cleanup/owner bridge follow-up (main 54f5865ee): the API now extracts and detaches the actual first surface using PrimitiveModel's notify_ref owner identities, matching native GetSurfaces/SetOwner(NULL). The runner no longer rejects a genuine successful revolution: retains original status/has and compares owner-clear state, natural UV domain and 25 virtual point evaluations/statuses. 882 cases: 879 exact comparable passes, 0 failures, 3 unchanged downstream deferrals (2 face-owned TrimSrfCache, 1 Point3DClassify), down from 20; all 862 old passing case records and native digests unchanged. All 879 comparable native/Rust output digests agree exactly. Original guards, tolerances and negative cases remain unchanged. KNOWN MISMATCH (stacked merge of #613 with main): skin_primitive_annular runs once #613's shell ray face preparation and #639's forwarded face hooks combine, and its region nesting differs from C++; listed in known_mismatches.toml (findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)." +notes = "main CI e33985411 (2026-10-23): PARTIAL sweep.api_surfaces cases=882 passed=879 failed=0 deferred=3 blocked_by=surface.trim_srf_cache(3). partial (PR #37): difftest cases=882 passed=831 deferred=51 (blocked_by primitive.primitive_creation_p2(11), primitive.primitive_creation_p3(40): SmApiCreateSkinPrimitive / SmApiCreateSkinFromFaces / SmApiCreateSurfaceRevolution validate and copy inputs, then stub SmPrimitiveCreation::CreateSkinPrimitive / CreateSkinFromFaces / CreateRotationalSweep); extrude, sweep, ruled and skin bit-identical; 9 fns; largest: SmApiCreateSkinPrimitive, SmApiCreateSurfaceRevolution, SmApiCreateSkin, SmApiCreateSkinFromFaces; entry points: SmApiCreateExtrude, SmApiCreateRuledSurface, SmApiCreateSkin, SmApiCreateSkinFromFaces, SmApiCreateSkinPrimitive, SmApiCreateSurfaceRevolution, SmApiCreateSweep; 15 upstream test entry points reach this unit UPDATE after topology.brep_p3 (PR #204): SmBrep::SmBrep wired to topology::brep::p3::new_brep (create_skin_primitive/create_skin_from_faces take k: &mut Kernel, create_surface_revolution gained the kernel, upstream API context); 882 cases, 831 pass, 0 failed, 51 deferred (topology.shell 51). UPDATE (PR #204, after topology.shell was wired into topology.brep_p3): new_brep completes, so the 51 topology.shell deferrals now stop at SmPrimitiveCreation: 882 cases, 831 pass, 0 failed, 51 deferred (primitive.primitive_creation_p2 11, primitive.primitive_creation_p3 40), case-by-case only the deferral target changed. partial (PR #37): difftest cases=882 passed=831 deferred=51 (blocked_by primitive.primitive_creation_p2(11), topology.brep_p3(40): SmApiCreateSkinPrimitive / SmApiCreateSkinFromFaces / SmApiCreateSurfaceRevolution validate and copy inputs; p3 construction calls await the earlier SmBrep constructor and a concrete PrimitiveAccess adapter (PR #233)); UPDATE 2026-10-23 (claude/face-p1-hooks): the query.face_p1 (SmFace::Point3DClassify) deferral of upstream.json#14 now runs past it and defers at boolean.topology_intersector_p2; cases=882 passed=862 failed=0 deferred=20 (boolean.topology_intersector_p2 17, surface.trim_srf_cache 3) (Mac Studio run; Linux CI is the gate).; Historical E validation before integration with query.face_p1 hooks: Primitive p3 cleanup/owner bridge follow-up (main 54f5865ee): the API now extracts and detaches the actual first surface using PrimitiveModel's notify_ref owner identities, matching native GetSurfaces/SetOwner(NULL). The runner no longer rejects a genuine successful revolution: retains original status/has and compares owner-clear state, natural UV domain and 25 virtual point evaluations/statuses. 882 cases: 879 exact comparable passes, 0 failures, 3 unchanged downstream deferrals (2 face-owned TrimSrfCache, 1 Point3DClassify), down from 20; all 862 old passing case records and native digests unchanged. All 879 comparable native/Rust output digests agree exactly. Original guards, tolerances and negative cases remain unchanged. KNOWN MISMATCH (stacked merge of #613 with main): skin_primitive_annular runs once #613's shell ray face preparation and #639's forwarded face hooks combine, and its region nesting differs from C++; listed in known_mismatches.toml (findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks). UPDATE (IsInnerShell on the B-rep itself): skin_primitive_annular now defers at surface.trim_srf_cache (TrimSrfCache::build_tree_with_face for a face-owned surface); known-mismatch entry removed." diff --git a/plan/units/topology.brep_p6.toml b/plan/units/topology.brep_p6.toml index af766fbb5..a7b25ead5 100644 --- a/plan/units/topology.brep_p6.toml +++ b/plan/units/topology.brep_p6.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_tsurf_cyl_creation_2d", "prog_test:my_test_tsurf_cyl status = "partial" claimed_by = "" claimed_at = "" -notes = "main CI e33985411 (2026-10-23): PARTIAL topology.brep_p6 cases=595 passed=586 failed=0 deferred=9 blocked_by=topology.edgeuse_p3(9). PARTIAL, MakeFaceTopology remainder (PR #363): SmBrep::MakeFaceTopology is ported in full, and the mislabelled UNPORTED(topology.face_p3) SetTolerance stop is gone. difftest 587 cases, 521 pass, 0 fail, 66 deferred (intersect.surface_drop_lib_p3 37: DropCurve in CreateUVTrimCurve(s) for trims and radial-sector recovery; topology.brep_p3 18; query.brep 9: CalculateBoundingBox; curve.crv_on_surf 2), max_rel_err 0, no absolute floors. Op 3 (fixtures make_face.json: 89 cases, 50 pass and 39 defer; plus generated cases) runs MakeFaceTopology sequences on kind-5 wire networks. It compares statuses, outputs, mark counters and the whole post-op graph (edge marks excluded, see findings). Downstream: brep_p7 238/78 (was 237/79), boolean.merge_p2 32/13 (was 30/15; MergeContext::SplitMut). With PR #72 merged locally, brep_p8: see the PR description. EARLIER: PARTIAL (PR #169, merged after dependency PR #154): 498 difftest cases, 450 passed, 0 failed, 48 deferred (topology.brep_p2 6: SmBrep::Notify; topology.brep_p1 23: SmBrep::GetInfiniteRegion inside CreateWireEdgeFromCurve, wired to topology.brep_p5 in PR #194, whose copy notifications follow behind a topology.brep_p2 key-routing barrier; topology.face_p3 19: SmFace::SetTolerance), max_rel_err 0, no absolute floors. Of the explicit op-0 cases, 369 return before acquiring a mark (separated mates or lamina); six tiny planes are edgeless and defer after closed traversal at Notify. 56 new open_box sheet cases reach ClosureTraversal and the open return: seeds 0/5 have no lamina edges and mates in the same shell. They compare actual nonzero context increments (MARK +4 per call without a caller lock), lock release, all faceuse marks and shell links over held locks and repeats. Other passing inputs are empty CopyEdges and null-surface MakeFaceTopology. MakeFaceTopology shim always has empty loops/edgeuses/loop-vertices and no explicit region: loop validation and existing-shell branches are not compared; every non-null surface defers at SetTolerance. No nonempty copy, successful face construction or completed split is certified; dependent mutation/classification/cleanup remains deferred. New shell/region identities and shell links use native -3, distinct from null -1. Wired heal.heal_data_p1 using RegionSplitAttempt: only ordinary native status may be ignored, preserving partial outputs; Unported and Rust representation errors propagate. Reused traverser HasLaminaEdge and the latest shell mark-lock API. Sensitivity detects 450/450 perturbations, with zero empty outputs. Four kernel and four native FFI regressions cover status/outputs and prove the open-sheet traversal advances marks. 3 fns; largest: SmBrep::TestClosureSplitRegion, SmBrep::MakeFaceTopology, SmBrep::CopyEdges; cycle back-edges (call into later units; stub or co-port): topology.brep_p1(3), topology.brep_p2(2), topology.brep_p5(2), query.brep(1); unit-SCC #22 (220 units); 196 upstream test entry points reach this unit UPDATE after topology.brep_p2 (PR #149): TestClosureSplitRegion's UNPORTED(topology.brep_p2) SmBrep::Notify is wired: the split API (test_closure_split_region / _attempt) takes the caller's NotifyEnv (signature only) and calls brep::p2::notify with PRE_EDIT and SPLIT_IN_BREP(old, old, new), statuses ignored (SmBrep.cpp:27954-5); the new region gets its default SmTopology record when it is made; the split then stops at UNPORTED(query.brep) SmBrep::CalculateBoundingBox. The runner passes a harness RegionNotifyState. Re-run: 498 cases, 450 pass, 0 fail, 48 deferred (topology.brep_p5 23, topology.face_p3 19, query.brep 6: the 6 former SmBrep::Notify deferrals, relabelled only), --perturb SENSITIVE 450/450. Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 498 cases, 450 pass, 0 fail, 48 deferred; the CopyEdges Notify / SetTolerance sites are wired (the 6 SmBrep::Notify deferrals relabel to query.brep CalculateBoundingBox). UPDATE (PR #535): TestClosureSplitRegion remainder (SmBrep.cpp:27966-28583) ported; the OMITTED stop is gone. difftest 587 cases, 545 pass, 0 fail, 42 deferred (intersect.surface_drop_lib_p3 37, curve.crv_on_surf 2, topology.brep_p1 2, geom.tol 1), max_rel_err 0, no absolute floors. Ray fires read the geometry under the caller's key_of (brep::p2::caller_cache_key). Downstream: brep_p10 1075 pass, 312 deferred (+4 pass); the former split stops in fillet_geom_p4, merge_p2, brep_p7, brep_p8 and heal_data_p3 now defer at geom.tol GetZoneTol3d(const SmObject*), topology.face_p2 or DropCurve. See plan/findings.md. KNOWN MISMATCH (stacked merge of #613 with main): make_face.json#89..95 (7 cases) runs once #613's shell ray face preparation and #639's forwarded face hooks combine, and its region nesting differs from C++; listed in known_mismatches.toml (findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)." +notes = "main CI e33985411 (2026-10-23): PARTIAL topology.brep_p6 cases=595 passed=586 failed=0 deferred=9 blocked_by=topology.edgeuse_p3(9). PARTIAL, MakeFaceTopology remainder (PR #363): SmBrep::MakeFaceTopology is ported in full, and the mislabelled UNPORTED(topology.face_p3) SetTolerance stop is gone. difftest 587 cases, 521 pass, 0 fail, 66 deferred (intersect.surface_drop_lib_p3 37: DropCurve in CreateUVTrimCurve(s) for trims and radial-sector recovery; topology.brep_p3 18; query.brep 9: CalculateBoundingBox; curve.crv_on_surf 2), max_rel_err 0, no absolute floors. Op 3 (fixtures make_face.json: 89 cases, 50 pass and 39 defer; plus generated cases) runs MakeFaceTopology sequences on kind-5 wire networks. It compares statuses, outputs, mark counters and the whole post-op graph (edge marks excluded, see findings). Downstream: brep_p7 238/78 (was 237/79), boolean.merge_p2 32/13 (was 30/15; MergeContext::SplitMut). With PR #72 merged locally, brep_p8: see the PR description. EARLIER: PARTIAL (PR #169, merged after dependency PR #154): 498 difftest cases, 450 passed, 0 failed, 48 deferred (topology.brep_p2 6: SmBrep::Notify; topology.brep_p1 23: SmBrep::GetInfiniteRegion inside CreateWireEdgeFromCurve, wired to topology.brep_p5 in PR #194, whose copy notifications follow behind a topology.brep_p2 key-routing barrier; topology.face_p3 19: SmFace::SetTolerance), max_rel_err 0, no absolute floors. Of the explicit op-0 cases, 369 return before acquiring a mark (separated mates or lamina); six tiny planes are edgeless and defer after closed traversal at Notify. 56 new open_box sheet cases reach ClosureTraversal and the open return: seeds 0/5 have no lamina edges and mates in the same shell. They compare actual nonzero context increments (MARK +4 per call without a caller lock), lock release, all faceuse marks and shell links over held locks and repeats. Other passing inputs are empty CopyEdges and null-surface MakeFaceTopology. MakeFaceTopology shim always has empty loops/edgeuses/loop-vertices and no explicit region: loop validation and existing-shell branches are not compared; every non-null surface defers at SetTolerance. No nonempty copy, successful face construction or completed split is certified; dependent mutation/classification/cleanup remains deferred. New shell/region identities and shell links use native -3, distinct from null -1. Wired heal.heal_data_p1 using RegionSplitAttempt: only ordinary native status may be ignored, preserving partial outputs; Unported and Rust representation errors propagate. Reused traverser HasLaminaEdge and the latest shell mark-lock API. Sensitivity detects 450/450 perturbations, with zero empty outputs. Four kernel and four native FFI regressions cover status/outputs and prove the open-sheet traversal advances marks. 3 fns; largest: SmBrep::TestClosureSplitRegion, SmBrep::MakeFaceTopology, SmBrep::CopyEdges; cycle back-edges (call into later units; stub or co-port): topology.brep_p1(3), topology.brep_p2(2), topology.brep_p5(2), query.brep(1); unit-SCC #22 (220 units); 196 upstream test entry points reach this unit UPDATE after topology.brep_p2 (PR #149): TestClosureSplitRegion's UNPORTED(topology.brep_p2) SmBrep::Notify is wired: the split API (test_closure_split_region / _attempt) takes the caller's NotifyEnv (signature only) and calls brep::p2::notify with PRE_EDIT and SPLIT_IN_BREP(old, old, new), statuses ignored (SmBrep.cpp:27954-5); the new region gets its default SmTopology record when it is made; the split then stops at UNPORTED(query.brep) SmBrep::CalculateBoundingBox. The runner passes a harness RegionNotifyState. Re-run: 498 cases, 450 pass, 0 fail, 48 deferred (topology.brep_p5 23, topology.face_p3 19, query.brep 6: the 6 former SmBrep::Notify deferrals, relabelled only), --perturb SENSITIVE 450/450. Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 498 cases, 450 pass, 0 fail, 48 deferred; the CopyEdges Notify / SetTolerance sites are wired (the 6 SmBrep::Notify deferrals relabel to query.brep CalculateBoundingBox). UPDATE (PR #535): TestClosureSplitRegion remainder (SmBrep.cpp:27966-28583) ported; the OMITTED stop is gone. difftest 587 cases, 545 pass, 0 fail, 42 deferred (intersect.surface_drop_lib_p3 37, curve.crv_on_surf 2, topology.brep_p1 2, geom.tol 1), max_rel_err 0, no absolute floors. Ray fires read the geometry under the caller's key_of (brep::p2::caller_cache_key). Downstream: brep_p10 1075 pass, 312 deferred (+4 pass); the former split stops in fillet_geom_p4, merge_p2, brep_p7, brep_p8 and heal_data_p3 now defer at geom.tol GetZoneTol3d(const SmObject*), topology.face_p2 or DropCurve. See plan/findings.md. KNOWN MISMATCH (stacked merge of #613 with main): make_face.json#89..95 (7 cases) runs once #613's shell ray face preparation and #639's forwarded face hooks combine, and its region nesting differs from C++; listed in known_mismatches.toml (findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks). UPDATE (IsInnerShell on the B-rep itself): make_face.json#89..95 now match C++ in geometry and topology; only the Mark2 counter differs (IntersectTSBound's trimmed-cache branch takes a mark the port does not), so they stay known mismatches (findings.md: IsInnerShell classifies on the B-rep itself)." diff --git a/plan/units/topology.face_p2.toml b/plan/units/topology.face_p2.toml index 7bfcf2da1..ea7d3f95e 100644 --- a/plan/units/topology.face_p2.toml +++ b/plan/units/topology.face_p2.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test: status = "partial" claimed_by = "" claimed_at = "" -notes = "PR #639 (2026-10-23, WithMadeTrimCurves pass-through + mutable face queries): difftest 2692 cases, 2596 pass, 0 failed, 96 deferred (was 2532 / 160): topology.edge 47, query.face_p2 PointClassify 33, topology.face_p4 FindVertexSector 16. PARTIAL: difftest 2692 cases (1176 fixture: upstream-derived (smMergeTest GetPointsInFace(1) on plane and B-spline faces, TestSmBrep ComputePreciseProperties layouts for all 63 flag sets and the iso-curve/area integrands at offset origins, thicknesses 0/0.1, accuracies 1e-3/1e-5, smTopoTest CreateCurvesByTrimming, TestSmTrimmedSurfaces GetOuterLoops) plus edge cases (degenerate and EFF_ZERO-limit UV domains, sm_FixUVTrimCurves gap limits EFF_ZERO/10 and min-size/20, tiny 1e-4 / huge 1e4 / pinched / sub-domain B-spline faces, real SmPlanes, sm_CheckSeamCrossing 90% jumps); 16 generated + 1500 more generated in code), 2322 pass, 0 fail, 370 deferred (topology.edgeuse_p3 156, curve.crv_on_surf 74, topology.face_p3 63 (CalculateUVDomainFromUVTrimCurves wired by topology.face_p1, PR #148; now CreateUVTrimCurves), query.face_p3 47, intersect.curve_class_p4 30; the 112 thick-face cases deferred on surface.offset_surface pass since PR #62 wired SmOffsetSurface::EvaluateSimple); mass.integrator wired in PR #119: 29 previously deferred cases pass and 4 now reach offset evaluation, max_rel_err 0; --perturb SENSITIVE. Runner imports the shim's exported graph into topology::Brep (as topology.vertexuse) with each face's m_pFU / m_vUVDomain / m_bRectangularTrim / m_sZoneTol3d / surface owner, SmPlanes as planes and vertex/face gap UVs; ops on faces or edges whose surface (revolution/extrusion/cone/sphere/torus subclasses) or edge curve (ellipse) the export cannot carry exactly report inexact_geometry on both sides. GetPointsInFace with more than one point compares only the draw-invariant count, U values and V grid slots (upstream srand(time)). Never compared (always deferred): sms_ReviewStartAndStopOnSeamCrvClassifies (GetIvlBound_EdgeuseObj), CalculateAnInternalPoint past its early returns (sm_FaceClassifyUVLine), CreateCurvesByTrimming of non-degenerate curves (SmCurveClassification), CreateCurvesFromFace edge loops (NormalizedEvaluate), sm_AreConnected (ComputeUVPoint stubs). Face record gains faceuse, uv_domain, rectangular_trim, tolerance (Face::new = SmFace()). Wired UNPORTED(topology.face_p2) GetUVDomain/GetTolerance in topology::vertexuse (its shim/runner now export/import the face members and SmPlanes). Remaining UNPORTED(topology.face_p2) sites (geom::object, geom::solution_array, geom::tol, intersect::point_class, intersect::curve_class::p1, intersect::topology_solver::p1, surface::plane::p2, surface::surface::p1/p7, surface::surface_cache::p1) hold opaque face handles without a Brep (plan/findings.md). Plan notes: 32 fns; largest: sm_FixUVTrimCurves, SmFace::GetPointsInFace, SmODEIsoCurveLengthIFEO::Evaluate, SmFace::CreateCurvesByTrimming; cycle back-edges (call into later units; stub or co-port): topology.edge(9), topology.vertexuse(5), topology.vertex(4), topology.loopuse(3), mass.integrator(2), query.face_p2(2), topology.edgeuse_p3(2), heal.gap(1), query.face_p3(1), topology.face_p1(1); unit-SCC #22 (220 units); hazards: clock,rng; 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmFace::m_pSurface -> topology::Face::surface; CO-PORTED in topology.edgeuse_p2 (PR #87): SmFace::m_vUVDomain, GetUVDomain, m_sZoneTol3d, GetTolerance -> topology::Face::uv_domain, topology::Face::tolerance [topology.edgeuse_p3 wired (PR #117): 2692 cases, 2346 pass, 0 failed, 346 deferred (topology.face_p2 93, curve.crv_on_surf 74, topology.face_p3 63, query.face_p3 47, intersect.surface_drop_lib_p3 39, intersect.curve_class_p4 30); the 156 topology.edgeuse_p3 deferrals run: 24 pass, 93 defer further on topology.face_p2 and 39 on SmSurface::DropCurve]; CO-PORTED in intersect.curve_class_p4 (PR #137): FaceClassifyObjects implements ClassGeometry::topology_entity / topology_handle; ClassHandle gained Loop, Edge, Faceuse, Edgeuse, Vertexuse handles (CurveOnClassifyObjects::loop_id resolves Loop); review_start_and_stop_on_seam_crv_classifies(geometry, ...) takes a ClassGeometry for ivl_bound_edgeuse_obj; classify_curve_on_face lost its unused cross-hatch flag. [topology.face_p3 wired (PR #139, CreateUVTrimCurves): 2692 cases, 2346 pass, 0 failed, 346 deferred (topology.face_p2 93, curve.crv_on_surf 74, topology.face_p4 63, query.face_p3 47, intersect.surface_drop_lib_p3 39, intersect.curve_class_p4 30); the 63 topology.face_p3 deferrals now defer inside CreateUVTrimCurves at SmFace::GetUpwardLoopuses] [generated cases draw a B-rep with the face or edgeuse the op selects (PR #340, #294 row 6): 2692 cases, 2303 pass (1950 comparing values, no no_face left), 0 failed, 389 deferred (topology.face_p2 120, curve.crv_on_surf 84, intersect.surface_drop_lib_p3 74, topology.face_p4 63, intersect.curve_class_p2 45, query.face_p2 3)] Owner zone-tol cache (surface/owner-zone-tol): 2692 cases, 2405 pass, 0 fail, 287 deferred." +notes = "PR #639 (2026-10-23, WithMadeTrimCurves pass-through + mutable face queries): difftest 2692 cases, 2596 pass, 0 failed, 96 deferred (was 2532 / 160): topology.edge 47, query.face_p2 PointClassify 33, topology.face_p4 FindVertexSector 16. PARTIAL: difftest 2692 cases (1176 fixture: upstream-derived (smMergeTest GetPointsInFace(1) on plane and B-spline faces, TestSmBrep ComputePreciseProperties layouts for all 63 flag sets and the iso-curve/area integrands at offset origins, thicknesses 0/0.1, accuracies 1e-3/1e-5, smTopoTest CreateCurvesByTrimming, TestSmTrimmedSurfaces GetOuterLoops) plus edge cases (degenerate and EFF_ZERO-limit UV domains, sm_FixUVTrimCurves gap limits EFF_ZERO/10 and min-size/20, tiny 1e-4 / huge 1e4 / pinched / sub-domain B-spline faces, real SmPlanes, sm_CheckSeamCrossing 90% jumps); 16 generated + 1500 more generated in code), 2322 pass, 0 fail, 370 deferred (topology.edgeuse_p3 156, curve.crv_on_surf 74, topology.face_p3 63 (CalculateUVDomainFromUVTrimCurves wired by topology.face_p1, PR #148; now CreateUVTrimCurves), query.face_p3 47, intersect.curve_class_p4 30; the 112 thick-face cases deferred on surface.offset_surface pass since PR #62 wired SmOffsetSurface::EvaluateSimple); mass.integrator wired in PR #119: 29 previously deferred cases pass and 4 now reach offset evaluation, max_rel_err 0; --perturb SENSITIVE. Runner imports the shim's exported graph into topology::Brep (as topology.vertexuse) with each face's m_pFU / m_vUVDomain / m_bRectangularTrim / m_sZoneTol3d / surface owner, SmPlanes as planes and vertex/face gap UVs; ops on faces or edges whose surface (revolution/extrusion/cone/sphere/torus subclasses) or edge curve (ellipse) the export cannot carry exactly report inexact_geometry on both sides. GetPointsInFace with more than one point compares only the draw-invariant count, U values and V grid slots (upstream srand(time)). Never compared (always deferred): sms_ReviewStartAndStopOnSeamCrvClassifies (GetIvlBound_EdgeuseObj), CalculateAnInternalPoint past its early returns (sm_FaceClassifyUVLine), CreateCurvesByTrimming of non-degenerate curves (SmCurveClassification), CreateCurvesFromFace edge loops (NormalizedEvaluate), sm_AreConnected (ComputeUVPoint stubs). Face record gains faceuse, uv_domain, rectangular_trim, tolerance (Face::new = SmFace()). Wired UNPORTED(topology.face_p2) GetUVDomain/GetTolerance in topology::vertexuse (its shim/runner now export/import the face members and SmPlanes). Remaining UNPORTED(topology.face_p2) sites (geom::object, geom::solution_array, geom::tol, intersect::point_class, intersect::curve_class::p1, intersect::topology_solver::p1, surface::plane::p2, surface::surface::p1/p7, surface::surface_cache::p1) hold opaque face handles without a Brep (plan/findings.md). Plan notes: 32 fns; largest: sm_FixUVTrimCurves, SmFace::GetPointsInFace, SmODEIsoCurveLengthIFEO::Evaluate, SmFace::CreateCurvesByTrimming; cycle back-edges (call into later units; stub or co-port): topology.edge(9), topology.vertexuse(5), topology.vertex(4), topology.loopuse(3), mass.integrator(2), query.face_p2(2), topology.edgeuse_p3(2), heal.gap(1), query.face_p3(1), topology.face_p1(1); unit-SCC #22 (220 units); hazards: clock,rng; 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmFace::m_pSurface -> topology::Face::surface; CO-PORTED in topology.edgeuse_p2 (PR #87): SmFace::m_vUVDomain, GetUVDomain, m_sZoneTol3d, GetTolerance -> topology::Face::uv_domain, topology::Face::tolerance [topology.edgeuse_p3 wired (PR #117): 2692 cases, 2346 pass, 0 failed, 346 deferred (topology.face_p2 93, curve.crv_on_surf 74, topology.face_p3 63, query.face_p3 47, intersect.surface_drop_lib_p3 39, intersect.curve_class_p4 30); the 156 topology.edgeuse_p3 deferrals run: 24 pass, 93 defer further on topology.face_p2 and 39 on SmSurface::DropCurve]; CO-PORTED in intersect.curve_class_p4 (PR #137): FaceClassifyObjects implements ClassGeometry::topology_entity / topology_handle; ClassHandle gained Loop, Edge, Faceuse, Edgeuse, Vertexuse handles (CurveOnClassifyObjects::loop_id resolves Loop); review_start_and_stop_on_seam_crv_classifies(geometry, ...) takes a ClassGeometry for ivl_bound_edgeuse_obj; classify_curve_on_face lost its unused cross-hatch flag. [topology.face_p3 wired (PR #139, CreateUVTrimCurves): 2692 cases, 2346 pass, 0 failed, 346 deferred (topology.face_p2 93, curve.crv_on_surf 74, topology.face_p4 63, query.face_p3 47, intersect.surface_drop_lib_p3 39, intersect.curve_class_p4 30); the 63 topology.face_p3 deferrals now defer inside CreateUVTrimCurves at SmFace::GetUpwardLoopuses] [generated cases draw a B-rep with the face or edgeuse the op selects (PR #340, #294 row 6): 2692 cases, 2303 pass (1950 comparing values, no no_face left), 0 failed, 389 deferred (topology.face_p2 120, curve.crv_on_surf 84, intersect.surface_drop_lib_p3 74, topology.face_p4 63, intersect.curve_class_p2 45, query.face_p2 3)] Owner zone-tol cache (surface/owner-zone-tol): 2692 cases, 2405 pass, 0 fail, 287 deferred. UPDATE (IsInnerShell on the B-rep itself): GetPointsInFace takes the B-rep mutably; its UV-domain and iso-line classifications keep their UV trims, tolerance growth and gap caches on it." diff --git a/plan/units/topology.topology_traverser.toml b/plan/units/topology.topology_traverser.toml index 5fb7550c3..1f14eefd7 100644 --- a/plan/units/topology.topology_traverser.toml +++ b/plan/units/topology.topology_traverser.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_tsurf_cyl_creation_2d", "prog_test:my_test_tsurf_cyl status = "green" claimed_by = "" claimed_at = "" -notes = "GREEN (2026-10-23): difftest 909 cases, 909 passed, 0 failed, 0 deferred, max_rel_err=0e0 (UvLineObjects in topology.face_p2 implements topology_entity and topology_handle gated on resolve_topology: true, resolving SmEdge::GetCurve on face UV line classifications during seam check; resolves the sole remaining adversarial_02_op7_00 deferral; was 908/1). PR #639 (2026-10-23, WithMadeTrimCurves pass-through): difftest 909 cases, 908 pass, 0 failed, 1 deferred (topology.edge GetCurve via the UV-line adapter; was 903 / 6). PARTIAL (PR #113): difftest 909 cases, 889 passed, 0 failed, 20 deferred: topology.face_p3(5) (were topology.face_p1 before PR #148 wired FlipFaceOrientation), topology.face_p4(15) (re-run in PR #154: the 7 topology.shell deferrals pass, RemoveShell / FaceuseShellReplacement wired to topology::shell); exact traversal order and all face/edge/vertex/faceuse/vertexuse marks compared, 0 absolute floors; wire recursion replaced by an explicit continuation stack in the same order (6000-edge regression). No existing UNPORTED(topology.topology_traverser) back-edges. 14 fns; largest: sm_ShellConnectivity, SmTopologyTraverser::IsInnerShell, sm_CollectFaces, SmTopologyTraverser::FaceuseTraversal; cycle back-edges (call into later units; stub or co-port): topology.face_p3(1); unit-SCC #22 (220 units); 200 upstream test entry points reach this unit PR #163 exposes the existing co-ported face_edges helper for merge_p1 reuse; it keeps duplicates between loops and deduplicates within each loop (regression added). PR #169 exposes its existing co-ported has_lamina_edge for brep_p6 reuse; no duplicated face algorithm. Re-run: 909 cases, 889 pass, 20 deferred, 0 failures, max_rel_err 0. [topology.face_p3 wired (PR #139): 909 cases, 889 pass, 0 failed, 20 deferred (topology.face_p4 20; the 5 topology.face_p3 deferrals now defer inside it)] After topology.face_p4 (PR #239) merged with the batch: 909 cases, 895 pass, 0 failed, 14 deferred (was 889/20). CO-PORTED in sweep.topology_sweep: FaceTraversalMarks bridges traversal to persistent caller Topology notification records without changing visit order." +notes = "GREEN (2026-10-23): difftest 909 cases, 909 passed, 0 failed, 0 deferred, max_rel_err=0e0 (UvLineObjects in topology.face_p2 implements topology_entity and topology_handle gated on resolve_topology: true, resolving SmEdge::GetCurve on face UV line classifications during seam check; resolves the sole remaining adversarial_02_op7_00 deferral; was 908/1). PR #639 (2026-10-23, WithMadeTrimCurves pass-through): difftest 909 cases, 908 pass, 0 failed, 1 deferred (topology.edge GetCurve via the UV-line adapter; was 903 / 6). PARTIAL (PR #113): difftest 909 cases, 889 passed, 0 failed, 20 deferred: topology.face_p3(5) (were topology.face_p1 before PR #148 wired FlipFaceOrientation), topology.face_p4(15) (re-run in PR #154: the 7 topology.shell deferrals pass, RemoveShell / FaceuseShellReplacement wired to topology::shell); exact traversal order and all face/edge/vertex/faceuse/vertexuse marks compared, 0 absolute floors; wire recursion replaced by an explicit continuation stack in the same order (6000-edge regression). No existing UNPORTED(topology.topology_traverser) back-edges. 14 fns; largest: sm_ShellConnectivity, SmTopologyTraverser::IsInnerShell, sm_CollectFaces, SmTopologyTraverser::FaceuseTraversal; cycle back-edges (call into later units; stub or co-port): topology.face_p3(1); unit-SCC #22 (220 units); 200 upstream test entry points reach this unit PR #163 exposes the existing co-ported face_edges helper for merge_p1 reuse; it keeps duplicates between loops and deduplicates within each loop (regression added). PR #169 exposes its existing co-ported has_lamina_edge for brep_p6 reuse; no duplicated face algorithm. Re-run: 909 cases, 889 pass, 20 deferred, 0 failures, max_rel_err 0. [topology.face_p3 wired (PR #139): 909 cases, 889 pass, 0 failed, 20 deferred (topology.face_p4 20; the 5 topology.face_p3 deferrals now defer inside it)] After topology.face_p4 (PR #239) merged with the batch: 909 cases, 895 pass, 0 failed, 14 deferred (was 889/20). CO-PORTED in sweep.topology_sweep: FaceTraversalMarks bridges traversal to persistent caller Topology notification records without changing visit order. UPDATE (IsInnerShell on the B-rep itself): IsInnerShell runs its face samples and rays on the caller's B-rep (no scratch copy), keeping the UV trim curves upstream leaves." From 782412884e2e8f3d07b1268786b36da8c9f1a505 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 17:38:17 -0700 Subject: [PATCH 4/5] findings: IsInnerShell on the B-rep needs a face-bound trimmed cache in its rays Running IsInnerShell in place fixes the brep_p6 geometry but turns 14 topology.topology_traverser passes into deferrals at the face-owned trimmed surface cache: the scratch copy used to drop surface owners. Record the blocker and the proposed follow-up. Co-Authored-By: Claude Opus 5.5 --- plan/findings.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plan/findings.md b/plan/findings.md index ba160ee49..4cf7ab277 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -18016,6 +18016,28 @@ claimed. No fixture, comparison gate or tolerance was changed. `query::face::p2` curve-on-classify between the read-only and mutable interval classification changed nothing for these cases (probe recorded in the #685 entry). This fix does not change that path. +- **Blocker: topology.topology_traverser (green on main) loses 14 passes.** Running + `IsInnerShell` on the caller's B-rep turns its op-7 cases (constructed_05..07, + adversarial_02..06, upstream_files_00/01, gen0001/0003/0012/0013) from pass to + deferred at `SmFace handle of a surface's face` (topology.face_p2). The backtrace runs + `is_inner_shell` -> `face::p4::global_line_intersect_held` -> + `surface::p1::get_surface_cache` -> `make_trim_srf_cache`. + - On main the scratch copy came from `Brep::clone`, which drops geometry owners + (open PR #681). The ray's surface looked free, so the port built an untrimmed + cache. On the caller's B-rep the surface keeps its face owner, and a face-owned + trimmed cache needs a face handle bound to it (`bind_trimmed_face`), which the + ray path (`RaySurface` handles) does not supply. + - The values matched C++ before only because both sides' answers agree for + these inputs. Upstream builds the face's `SmTrimSrfCache`. + - Neither copy-back is the cause: putting `CalculateUVDomainFromUVTrimCurves` + back on a copy, or dropping `apply_uv_trim_updates`, leaves the 14 deferrals. +- **Proposed fix (larger than this change).** Give `global_line_intersect_held` (and + the ray world of `IsInnerShell`) a face-bound trimmed surface cache, as + `smsurf_GetSurfaceCache` builds for a face-owned surface. Then port + `IntersectTSBound`'s cached branch with its `SmNewMarkAndLock`, taking + `&mut Kernel`. Together these unblock the traverser cases, the skin cases' + `build_tree_with_face` deferral, and the brep_p6 Mark2 counter. Until then the + in-place `IsInnerShell` cannot land without turning a green unit partial. ## Status index (2026-10-23, main CI `e33985411`) From a1207b900694a5e863a53b18c9f1144371fe9752 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 20:27:35 -0700 Subject: [PATCH 5/5] topology.brep_p6 to green: the region-nesting cases match with #697's kept trimmed caches Merged #697 (face surfaces keep their trimmed caches; IntersectTSBound's cached branch and its mark). The 7 brep_p6 region-nesting known mismatches now pass and are removed; the unit is green (595/595). The two skin annulus cases still differ (the cap comes out as two faces) and stay listed with a findings entry. Co-Authored-By: Claude Opus 5.5 --- crates/difftest/known_mismatches.toml | 10 ++++++++++ plan/findings.md | 17 +++++++++++++++++ plan/units/topology.brep_p6.toml | 4 ++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/difftest/known_mismatches.toml b/crates/difftest/known_mismatches.toml index 4d058376a..f73b5fedc 100644 --- a/crates/difftest/known_mismatches.toml +++ b/crates/difftest/known_mismatches.toml @@ -50,3 +50,13 @@ why = "MoveSeam with split: the runner's null classification handles cannot name unit = "topology.face_p1" case = "torus.smb_1_f5_op9" why = "MoveSeam with split: the runner's null classification handles cannot name topology, so the port's post-move HasSeamProblem fails (BrepSeamObjects InvalidInput, ignored as 'no problem') where upstream finds one and splits; reachable once the UB-137 fork trap runs the call (plan/findings.md: topology.face_p1 fork trap merged with main)" + +[[mismatch]] +unit = "primitive.primitive_creation_p2" +case = "skin_concentric_loops" +why = "the annular cap between two concentric loops comes out as two faces (record[7] cpp 1, rust 2): region nesting still differs once IsInnerShell runs on the B-rep with the face surfaces' kept trimmed caches (plan/findings.md: IsInnerShell on the B-rep with the kept trimmed caches)" + +[[mismatch]] +unit = "sweep.api_surfaces" +case = "skin_primitive_annular" +why = "the annular cap between two concentric loops comes out as two faces (faces[1] cpp 1, rust 2): region nesting still differs once IsInnerShell runs on the B-rep with the face surfaces' kept trimmed caches (plan/findings.md: IsInnerShell on the B-rep with the kept trimmed caches)" diff --git a/plan/findings.md b/plan/findings.md index 8643ab7a0..ee04d1797 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -18388,3 +18388,20 @@ semantics are reverted: deviation; the runner has no op that moves a trim's end, so the only coverage is the unit test `fix_uv_trim_curves_keeps_interior_parameters`, which fails on the upstream code path (interior knots 3.3333 -> 3.3324 for a 1e-3 overshoot) and passes here. + +## IsInnerShell on the B-rep with the kept trimmed caches (PR #694 merged with #697, 2026-10-24) + +- **Result.** With `IsInnerShell` and `GetPointsInFace` on the caller's B-rep (#694) and + the face surfaces' kept trimmed caches with `IntersectTSBound`'s cached branch (#697), + topology.brep_p6's 7 region-nesting cases (`make_face.json#89..95`) match C++ bit for + bit, the Mark2 counter included; their known-mismatch entries are removed and the unit + is green (595/595). topology.topology_traverser stays green (909/909: the 14 + `IsInnerShell` rays solve on the face's kept cache, the caller-held cache now only + stands for a surface without face), topology.shell 911/911. +- **Still differing (kept as known mismatches).** primitive.primitive_creation_p2 + `skin_concentric_loops` (record[7] cpp 1, rust 2) and sweep.api_surfaces + `skin_primitive_annular` (faces[1] cpp 1, rust 2): the annular cap between the two + concentric loops comes out as two faces in Rust, one face with a hole in C++. They no + longer defer at `TrimSrfCache::build_tree_with_face` (#697's GlobalLineIntersect fix); + the remaining difference is in how the inner loop is nested in the outer one and has + not been narrowed to a call. diff --git a/plan/units/topology.brep_p6.toml b/plan/units/topology.brep_p6.toml index a7b25ead5..5886f3776 100644 --- a/plan/units/topology.brep_p6.toml +++ b/plan/units/topology.brep_p6.toml @@ -6,7 +6,7 @@ soft_depends_on = ["geom.extent1d", "geom.extent2d", "geom.extent3d", "intersect est_lines = 1479 layer = 45 tests = ["prog_test:my_test_tsurf_cyl_creation_2d", "prog_test:my_test_tsurf_cyl_creation_3d", "prog_test:my_test_tsurf_cyl_creation_fast", "prog_test:my_test_tsurf_creation", "SM_API_test:TestSmCreatePlanarCircle", "SM_API_test:TestIntersectionOutputStatus"] -status = "partial" +status = "green" claimed_by = "" claimed_at = "" -notes = "main CI e33985411 (2026-10-23): PARTIAL topology.brep_p6 cases=595 passed=586 failed=0 deferred=9 blocked_by=topology.edgeuse_p3(9). PARTIAL, MakeFaceTopology remainder (PR #363): SmBrep::MakeFaceTopology is ported in full, and the mislabelled UNPORTED(topology.face_p3) SetTolerance stop is gone. difftest 587 cases, 521 pass, 0 fail, 66 deferred (intersect.surface_drop_lib_p3 37: DropCurve in CreateUVTrimCurve(s) for trims and radial-sector recovery; topology.brep_p3 18; query.brep 9: CalculateBoundingBox; curve.crv_on_surf 2), max_rel_err 0, no absolute floors. Op 3 (fixtures make_face.json: 89 cases, 50 pass and 39 defer; plus generated cases) runs MakeFaceTopology sequences on kind-5 wire networks. It compares statuses, outputs, mark counters and the whole post-op graph (edge marks excluded, see findings). Downstream: brep_p7 238/78 (was 237/79), boolean.merge_p2 32/13 (was 30/15; MergeContext::SplitMut). With PR #72 merged locally, brep_p8: see the PR description. EARLIER: PARTIAL (PR #169, merged after dependency PR #154): 498 difftest cases, 450 passed, 0 failed, 48 deferred (topology.brep_p2 6: SmBrep::Notify; topology.brep_p1 23: SmBrep::GetInfiniteRegion inside CreateWireEdgeFromCurve, wired to topology.brep_p5 in PR #194, whose copy notifications follow behind a topology.brep_p2 key-routing barrier; topology.face_p3 19: SmFace::SetTolerance), max_rel_err 0, no absolute floors. Of the explicit op-0 cases, 369 return before acquiring a mark (separated mates or lamina); six tiny planes are edgeless and defer after closed traversal at Notify. 56 new open_box sheet cases reach ClosureTraversal and the open return: seeds 0/5 have no lamina edges and mates in the same shell. They compare actual nonzero context increments (MARK +4 per call without a caller lock), lock release, all faceuse marks and shell links over held locks and repeats. Other passing inputs are empty CopyEdges and null-surface MakeFaceTopology. MakeFaceTopology shim always has empty loops/edgeuses/loop-vertices and no explicit region: loop validation and existing-shell branches are not compared; every non-null surface defers at SetTolerance. No nonempty copy, successful face construction or completed split is certified; dependent mutation/classification/cleanup remains deferred. New shell/region identities and shell links use native -3, distinct from null -1. Wired heal.heal_data_p1 using RegionSplitAttempt: only ordinary native status may be ignored, preserving partial outputs; Unported and Rust representation errors propagate. Reused traverser HasLaminaEdge and the latest shell mark-lock API. Sensitivity detects 450/450 perturbations, with zero empty outputs. Four kernel and four native FFI regressions cover status/outputs and prove the open-sheet traversal advances marks. 3 fns; largest: SmBrep::TestClosureSplitRegion, SmBrep::MakeFaceTopology, SmBrep::CopyEdges; cycle back-edges (call into later units; stub or co-port): topology.brep_p1(3), topology.brep_p2(2), topology.brep_p5(2), query.brep(1); unit-SCC #22 (220 units); 196 upstream test entry points reach this unit UPDATE after topology.brep_p2 (PR #149): TestClosureSplitRegion's UNPORTED(topology.brep_p2) SmBrep::Notify is wired: the split API (test_closure_split_region / _attempt) takes the caller's NotifyEnv (signature only) and calls brep::p2::notify with PRE_EDIT and SPLIT_IN_BREP(old, old, new), statuses ignored (SmBrep.cpp:27954-5); the new region gets its default SmTopology record when it is made; the split then stops at UNPORTED(query.brep) SmBrep::CalculateBoundingBox. The runner passes a harness RegionNotifyState. Re-run: 498 cases, 450 pass, 0 fail, 48 deferred (topology.brep_p5 23, topology.face_p3 19, query.brep 6: the 6 former SmBrep::Notify deferrals, relabelled only), --perturb SENSITIVE 450/450. Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 498 cases, 450 pass, 0 fail, 48 deferred; the CopyEdges Notify / SetTolerance sites are wired (the 6 SmBrep::Notify deferrals relabel to query.brep CalculateBoundingBox). UPDATE (PR #535): TestClosureSplitRegion remainder (SmBrep.cpp:27966-28583) ported; the OMITTED stop is gone. difftest 587 cases, 545 pass, 0 fail, 42 deferred (intersect.surface_drop_lib_p3 37, curve.crv_on_surf 2, topology.brep_p1 2, geom.tol 1), max_rel_err 0, no absolute floors. Ray fires read the geometry under the caller's key_of (brep::p2::caller_cache_key). Downstream: brep_p10 1075 pass, 312 deferred (+4 pass); the former split stops in fillet_geom_p4, merge_p2, brep_p7, brep_p8 and heal_data_p3 now defer at geom.tol GetZoneTol3d(const SmObject*), topology.face_p2 or DropCurve. See plan/findings.md. KNOWN MISMATCH (stacked merge of #613 with main): make_face.json#89..95 (7 cases) runs once #613's shell ray face preparation and #639's forwarded face hooks combine, and its region nesting differs from C++; listed in known_mismatches.toml (findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks). UPDATE (IsInnerShell on the B-rep itself): make_face.json#89..95 now match C++ in geometry and topology; only the Mark2 counter differs (IntersectTSBound's trimmed-cache branch takes a mark the port does not), so they stay known mismatches (findings.md: IsInnerShell classifies on the B-rep itself)." +notes = "GREEN (PR #694 on #697, 2026-10-24): difftest 595 cases, 595 passed, 0 failed, 0 deferred, max_rel_err=0e0; the 7 region-nesting cases (make_face.json#89..95) match C++ bit for bit, Mark2 counter included: IsInnerShell runs on the B-rep, and IntersectTSBound takes the kept trimmed cache branch with its mark (#697); their known_mismatches entries are removed. main CI e33985411 (2026-10-23): PARTIAL topology.brep_p6 cases=595 passed=586 failed=0 deferred=9 blocked_by=topology.edgeuse_p3(9). PARTIAL, MakeFaceTopology remainder (PR #363): SmBrep::MakeFaceTopology is ported in full, and the mislabelled UNPORTED(topology.face_p3) SetTolerance stop is gone. difftest 587 cases, 521 pass, 0 fail, 66 deferred (intersect.surface_drop_lib_p3 37: DropCurve in CreateUVTrimCurve(s) for trims and radial-sector recovery; topology.brep_p3 18; query.brep 9: CalculateBoundingBox; curve.crv_on_surf 2), max_rel_err 0, no absolute floors. Op 3 (fixtures make_face.json: 89 cases, 50 pass and 39 defer; plus generated cases) runs MakeFaceTopology sequences on kind-5 wire networks. It compares statuses, outputs, mark counters and the whole post-op graph (edge marks excluded, see findings). Downstream: brep_p7 238/78 (was 237/79), boolean.merge_p2 32/13 (was 30/15; MergeContext::SplitMut). With PR #72 merged locally, brep_p8: see the PR description. EARLIER: PARTIAL (PR #169, merged after dependency PR #154): 498 difftest cases, 450 passed, 0 failed, 48 deferred (topology.brep_p2 6: SmBrep::Notify; topology.brep_p1 23: SmBrep::GetInfiniteRegion inside CreateWireEdgeFromCurve, wired to topology.brep_p5 in PR #194, whose copy notifications follow behind a topology.brep_p2 key-routing barrier; topology.face_p3 19: SmFace::SetTolerance), max_rel_err 0, no absolute floors. Of the explicit op-0 cases, 369 return before acquiring a mark (separated mates or lamina); six tiny planes are edgeless and defer after closed traversal at Notify. 56 new open_box sheet cases reach ClosureTraversal and the open return: seeds 0/5 have no lamina edges and mates in the same shell. They compare actual nonzero context increments (MARK +4 per call without a caller lock), lock release, all faceuse marks and shell links over held locks and repeats. Other passing inputs are empty CopyEdges and null-surface MakeFaceTopology. MakeFaceTopology shim always has empty loops/edgeuses/loop-vertices and no explicit region: loop validation and existing-shell branches are not compared; every non-null surface defers at SetTolerance. No nonempty copy, successful face construction or completed split is certified; dependent mutation/classification/cleanup remains deferred. New shell/region identities and shell links use native -3, distinct from null -1. Wired heal.heal_data_p1 using RegionSplitAttempt: only ordinary native status may be ignored, preserving partial outputs; Unported and Rust representation errors propagate. Reused traverser HasLaminaEdge and the latest shell mark-lock API. Sensitivity detects 450/450 perturbations, with zero empty outputs. Four kernel and four native FFI regressions cover status/outputs and prove the open-sheet traversal advances marks. 3 fns; largest: SmBrep::TestClosureSplitRegion, SmBrep::MakeFaceTopology, SmBrep::CopyEdges; cycle back-edges (call into later units; stub or co-port): topology.brep_p1(3), topology.brep_p2(2), topology.brep_p5(2), query.brep(1); unit-SCC #22 (220 units); 196 upstream test entry points reach this unit UPDATE after topology.brep_p2 (PR #149): TestClosureSplitRegion's UNPORTED(topology.brep_p2) SmBrep::Notify is wired: the split API (test_closure_split_region / _attempt) takes the caller's NotifyEnv (signature only) and calls brep::p2::notify with PRE_EDIT and SPLIT_IN_BREP(old, old, new), statuses ignored (SmBrep.cpp:27954-5); the new region gets its default SmTopology record when it is made; the split then stops at UNPORTED(query.brep) SmBrep::CalculateBoundingBox. The runner passes a harness RegionNotifyState. Re-run: 498 cases, 450 pass, 0 fail, 48 deferred (topology.brep_p5 23, topology.face_p3 19, query.brep 6: the 6 former SmBrep::Notify deferrals, relabelled only), --perturb SENSITIVE 450/450. Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 498 cases, 450 pass, 0 fail, 48 deferred; the CopyEdges Notify / SetTolerance sites are wired (the 6 SmBrep::Notify deferrals relabel to query.brep CalculateBoundingBox). UPDATE (PR #535): TestClosureSplitRegion remainder (SmBrep.cpp:27966-28583) ported; the OMITTED stop is gone. difftest 587 cases, 545 pass, 0 fail, 42 deferred (intersect.surface_drop_lib_p3 37, curve.crv_on_surf 2, topology.brep_p1 2, geom.tol 1), max_rel_err 0, no absolute floors. Ray fires read the geometry under the caller's key_of (brep::p2::caller_cache_key). Downstream: brep_p10 1075 pass, 312 deferred (+4 pass); the former split stops in fillet_geom_p4, merge_p2, brep_p7, brep_p8 and heal_data_p3 now defer at geom.tol GetZoneTol3d(const SmObject*), topology.face_p2 or DropCurve. See plan/findings.md. KNOWN MISMATCH (stacked merge of #613 with main): make_face.json#89..95 (7 cases) runs once #613's shell ray face preparation and #639's forwarded face hooks combine, and its region nesting differs from C++; listed in known_mismatches.toml (findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks). UPDATE (IsInnerShell on the B-rep itself): make_face.json#89..95 now match C++ in geometry and topology; only the Mark2 counter differs (IntersectTSBound's trimmed-cache branch takes a mark the port does not), so they stay known mismatches (findings.md: IsInnerShell classifies on the B-rep itself)."