From 1a2df8324cf2c000273cb3c25aec577a5b40dcd6 Mon Sep 17 00:00:00 2001 From: Kirill Frolov Date: Sat, 24 Oct 2026 00:34:55 +0300 Subject: [PATCH] fix: propagate fillet geometry trim failures (#483) Preserve successful prefix edits and snapping, but stop on failed trims or intersections instead of assigning unset intervals. Track the pinned C++ correctness differences explicitly and retain expanded native coverage. Co-Authored-By: Codex --- .../src/units/blend_fillet_geom_p1.rs | 4 +- crates/solidcore/src/blend/fillet_geom/p1.rs | 254 ++- crates/solidcore/src/kernel.rs | 2 + fixtures/blend.fillet_geom_p1/cases.json | 1788 ++++++++++++++++- ....fillet_geom_p1--fillet-trim-status-483.md | 81 + plan/units/blend.fillet_geom_p1.toml | 4 +- 6 files changed, 2115 insertions(+), 18 deletions(-) create mode 100644 plan/findings/blend.fillet_geom_p1--fillet-trim-status-483.md diff --git a/crates/difftest/src/units/blend_fillet_geom_p1.rs b/crates/difftest/src/units/blend_fillet_geom_p1.rs index 7e450067d5..ac7699b40d 100644 --- a/crates/difftest/src/units/blend_fillet_geom_p1.rs +++ b/crates/difftest/src/units/blend_fillet_geom_p1.rs @@ -80,7 +80,9 @@ use crate::runner::default_cases; use crate::{Case, HarnessError, Output, Rng, UnitDiff}; /// Generated cases per run, whatever `--seeds` says (it can only raise this). -pub const MIN_GENERATED: usize = 300; +// Keep the larger corpus canonical: it covers additional failed center trims, +// disjoint rail intervals and unset edge intervals from #483. +pub const MIN_GENERATED: usize = 2000; /// `blend.fillet_geom_p1` runner. pub struct Runner; diff --git a/crates/solidcore/src/blend/fillet_geom/p1.rs b/crates/solidcore/src/blend/fillet_geom/p1.rs index 5e6ef71e2b..f918009338 100644 --- a/crates/solidcore/src/blend/fillet_geom/p1.rs +++ b/crates/solidcore/src/blend/fillet_geom/p1.rs @@ -108,6 +108,11 @@ //! that B-rep ([`FilletSurfaceOwner`]) and resolve the face's surface in it; callers //! that run before any face is made pass `None`, and a transferred surface without //! (or with another) owner is refused with `InvalidInput`. +//! - **Trim failures (#483).** With `Kernel::fixes.fillet_trim_status` enabled, +//! `trim_geom` propagates curve/surface trim and interval-intersection errors. +//! Default behaviour drops ordinary statuses and installs unset intersections, +//! as pinned C++ does. Earlier successful edits and parameter snapping remain; +//! the operation is not transactional. //! - **Arguments.** Out-params become return values; `SmContext` arguments are the //! [`Kernel`]; `int iDebugLevel` arguments and debug drawing are dropped. //! `sm_TwoPntsIntersection` takes the start and end *points* of its `SmVertex` @@ -2434,6 +2439,9 @@ impl FilletBrep { /// /// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for a missing rail; /// [`SolidError::Unported`] from a trim; [`SolidError::NotFound`] for dangling ids. + /// With [`crate::kernel::UpstreamFixes::fillet_trim_status`] enabled, every + /// curve/surface trim and interval-intersection error is propagated. Earlier + /// successful edits and parameter snapping remain when a later step fails. pub fn trim_geom( &mut self, k: &Kernel, @@ -2453,11 +2461,18 @@ impl FilletBrep { let edge = self.brep.edge_mut(rail)?; if let Some(curve) = edge.curve.as_mut() { // may snap sIvl by tol to existing knots - ignore_non_unported(curve.trim(k, trim_interval, TrimMode::Notify))?; - let temp = edge - .interval - .intersect(trim_interval) - .unwrap_or_else(|_| Extent1d::new()); + let trimmed = curve.trim(k, trim_interval, TrimMode::Notify); + if k.fixes.fillet_trim_status { + trimmed?; + } else { + ignore_non_unported(trimmed)?; + } + let intersection = edge.interval.intersect(trim_interval); + let temp = if k.fixes.fillet_trim_status { + intersection? + } else { + intersection.unwrap_or_else(|_| Extent1d::new()) + }; set_interval(&mut self.brep, rail, &temp)?; } } @@ -2466,7 +2481,12 @@ impl FilletBrep { // Spine curve: if let Some(center) = geom.center_line_curve.as_mut() { - ignore_non_unported(center.trim(k, trim_interval, TrimMode::Notify))?; + let trimmed = center.trim(k, trim_interval, TrimMode::Notify); + if k.fixes.fillet_trim_status { + trimmed?; + } else { + ignore_non_unported(trimmed)?; + } } // Fillet Surface: @@ -2474,23 +2494,31 @@ impl FilletBrep { let mut domain = surface.natural_uv_domain_with_nurb(k)?; // Trimming is in the u-direction. let u_domain = domain.u_interval(); - let temp = u_domain - .intersect(trim_interval) - .unwrap_or_else(|_| Extent1d::new()); + let intersection = u_domain.intersect(trim_interval); + let temp = if k.fixes.fillet_trim_status { + intersection? + } else { + intersection.unwrap_or_else(|_| Extent1d::new()) + }; domain.set_u_interval(&temp); // trim to domain - ignore_non_unported(surface.trim_with_domain(k, &mut domain))?; + let trimmed = surface.trim_with_domain(k, &mut domain); + if k.fixes.fillet_trim_status { + trimmed?; + } else { + ignore_non_unported(trimmed)?; + } } Ok(()) } } -/// Ignores a status upstream does not check, but never an [`SolidError::Unported`]. -fn ignore_non_unported(r: Result) -> Result<()> { - match r { - Err(e) if e.is_unported() => Err(e), +/// Ignores a status upstream does not check, but never a missing dependency. +fn ignore_non_unported(result: Result) -> Result<()> { + match result { + Err(error) if error.is_unported() => Err(error), _ => Ok(()), } } @@ -4140,6 +4168,204 @@ mod tests { Surface::from(BSplineSurface::from_nurb(nurb, false)) } + fn trim_test_curve(end: f64) -> BSplineCurve { + let mut curve = BSplineCurve::create_line_segment( + 3, + Point3::new(0.0, 0.0, 0.0), + Point3::new(1.0, 0.0, 0.0), + ) + .unwrap(); + curve + .edit_parameterization(&Extent1d::from_min_max(0.0, end)) + .unwrap(); + curve + } + + fn trim_test_geom() -> (FilletBrep, FilletGeomId, [EdgeId; 4]) { + let k = Kernel::default(); + let mut fb = FilletBrep::new(); + let start = fb + .brep_mut() + .add_vertex(Point3::new(0.0, 0.0, 0.0)) + .unwrap(); + let end = fb + .brep_mut() + .add_vertex(Point3::new(1.0, 0.0, 0.0)) + .unwrap(); + let rails = std::array::from_fn(|_| { + let edge = fb.new_fillet_edge(start, end, None).unwrap(); + let record = fb.brep_mut().edge_mut(edge).unwrap(); + record.curve = Some(Curve::from(trim_test_curve(1.0))); + record.interval = Extent1d::from_min_max(0.0, 1.0); + edge + }); + let mut geom = FilletGeom::new(&k, None, FilletGeomType::Blends).unwrap(); + geom.rails = [Some(rails[0]), Some(rails[1])]; + geom.other_rails1.push(rails[2]); + geom.other_rails2.push(rails[3]); + let g = fb.add_geom(geom); + (fb, g, rails) + } + + #[test] + fn trim_geom_propagates_each_rail_curve_error_and_stops() { + let mut k = Kernel::default(); + k.fixes.fillet_trim_status = true; + for failed in 0..4 { + let (mut fb, g, rails) = trim_test_geom(); + fb.brep_mut().edge_mut(rails[failed]).unwrap().curve = + Some(Curve::from(trim_test_curve(0.5))); + let mut interval = Extent1d::from_min_max(0.25, 0.75); + let mut failing_curve = trim_test_curve(0.5); + let expected = failing_curve + .trim(&k, &mut interval, TrimMode::Notify) + .unwrap_err(); + assert_eq!(fb.trim_geom(&k, g, &mut interval).unwrap_err(), expected); + for (i, rail) in rails.iter().enumerate() { + let expected_interval = if i < failed { + interval + } else { + Extent1d::from_min_max(0.0, 1.0) + }; + assert_eq!(fb.brep().edge(*rail).unwrap().interval, expected_interval); + } + } + } + + #[test] + fn trim_geom_does_not_install_failed_rail_intersection() { + let mut k = Kernel::default(); + k.fixes.fillet_trim_status = true; + let (mut fb, g, rails) = trim_test_geom(); + let old = Extent1d::from_min_max(2.0, 3.0); + fb.brep_mut().edge_mut(rails[0]).unwrap().interval = old; + let mut interval = Extent1d::from_min_max(0.25, 0.75); + let expected = old.intersect(&interval).unwrap_err(); + assert_eq!( + fb.trim_geom(&k, g, &mut interval) + .unwrap_err(), + expected + ); + assert_eq!(fb.brep().edge(rails[0]).unwrap().interval, old); + // Successful curve edits before the failure remain, but later rails are untouched. + assert_eq!( + fb.brep() + .edge(rails[0]) + .unwrap() + .curve + .as_ref() + .unwrap() + .natural_interval(), + interval + ); + assert_eq!( + fb.brep().edge(rails[1]).unwrap().interval, + Extent1d::from_min_max(0.0, 1.0) + ); + } + + #[test] + fn trim_geom_propagates_center_line_error_before_surface_edit() { + let mut k = Kernel::default(); + k.fixes.fillet_trim_status = true; + let (mut fb, g, _) = trim_test_geom(); + let geom = fb.geom_mut(g).unwrap(); + geom.center_line_curve = Some(trim_test_curve(0.5)); + geom.fillet_surface = Some(bilinear_patch()); + let mut interval = Extent1d::from_min_max(0.25, 0.75); + let expected = trim_test_curve(0.5) + .trim(&k, &mut interval, TrimMode::Notify) + .unwrap_err(); + assert_eq!(fb.trim_geom(&k, g, &mut interval).unwrap_err(), expected); + assert_eq!( + fb.geom(g) + .unwrap() + .fillet_surface + .as_ref() + .unwrap() + .natural_uv_domain() + .u_interval(), + Extent1d::from_min_max(0.0, 1.0) + ); + } + + #[test] + fn trim_geom_rejects_disjoint_surface_domain() { + let mut k = Kernel::default(); + k.fixes.fillet_trim_status = true; + let (mut fb, g, rails) = trim_test_geom(); + for rail in rails { + fb.brep_mut().edge_mut(rail).unwrap().curve = None; + } + fb.geom_mut(g).unwrap().fillet_surface = Some(bilinear_patch()); + let mut interval = Extent1d::from_min_max(2.0, 3.0); + let expected = Extent1d::from_min_max(0.0, 1.0) + .intersect(&interval) + .unwrap_err(); + assert_eq!( + fb.trim_geom(&k, g, &mut interval) + .unwrap_err(), + expected + ); + assert_eq!( + fb.geom(g) + .unwrap() + .fillet_surface + .as_ref() + .unwrap() + .natural_uv_domain() + .u_interval(), + Extent1d::from_min_max(0.0, 1.0) + ); + } + + #[test] + fn trim_geom_propagates_surface_trim_failure() { + let mut k = Kernel::default(); + k.fixes.fillet_trim_status = true; + let (mut fb, g, rails) = trim_test_geom(); + for rail in rails { + fb.brep_mut().edge_mut(rail).unwrap().curve = None; + } + let mut surface = bilinear_patch(); + fb.geom_mut(g).unwrap().fillet_surface = Some(bilinear_patch()); + let mut interval = Extent1d::from_min_max(0.5, 0.5); + let mut domain = surface.natural_uv_domain(); + domain.set_u_interval(&interval); + let expected = surface.trim_with_domain(&k, &mut domain).unwrap_err(); + assert_eq!(fb.trim_geom(&k, g, &mut interval).unwrap_err(), expected); + } + + #[test] + fn trim_geom_keeps_successful_snapping_and_trims_all_geometry() { + let mut k = Kernel::default(); + k.fixes.fillet_trim_status = true; + let (mut fb, g, rails) = trim_test_geom(); + fb.geom_mut(g).unwrap().center_line_curve = Some(trim_test_curve(1.0)); + fb.geom_mut(g).unwrap().fillet_surface = Some(bilinear_patch()); + let mut interval = Extent1d::from_min_max(-1e-14, 0.75); + fb.trim_geom(&k, g, &mut interval).unwrap(); + assert_eq!(interval, Extent1d::from_min_max(0.0, 0.75)); + for rail in rails { + let edge = fb.brep().edge(rail).unwrap(); + assert_eq!(edge.interval, interval); + assert_eq!(edge.curve.as_ref().unwrap().natural_interval(), interval); + } + let geom = fb.geom(g).unwrap(); + assert_eq!( + geom.center_line_curve.as_ref().unwrap().natural_interval(), + interval + ); + assert_eq!( + geom.fillet_surface + .as_ref() + .unwrap() + .natural_uv_domain() + .u_interval(), + interval + ); + } + #[test] fn noise_cleanup_rounds_near_decimals() { assert_eq!(clean_up_noise(1.0 + 1e-13), 1.0); diff --git a/crates/solidcore/src/kernel.rs b/crates/solidcore/src/kernel.rs index f8adcfcdf8..792c14c3be 100644 --- a/crates/solidcore/src/kernel.rs +++ b/crates/solidcore/src/kernel.rs @@ -145,6 +145,8 @@ impl Caches { #[allow(clippy::struct_excessive_bools)] #[non_exhaustive] pub struct UpstreamFixes { + /// `SmFilletGeom::Trim` returns the first curve/surface trim or interval-intersection error instead of dropping it and continuing with an unset interval (#483). Earlier successful edits remain; off by default for pinned C++ parity. + pub fillet_trim_status: bool, /// `CalcRailIntEdgeuse`'s retry rail search holds a variable round's radius only where its first search does (at a corner calculated as constant radius), so the rail misses the arc it would meet only at that radius and the corner cuts the round off against the body; needs `fillet_nx1_end_rib_global_merge` there (WR-300, #1023). pub fillet_retry_radius_as_first: bool, /// `TestRollOver` joins a rail's neighbouring intervals on its face or the face's boundary before the rollover, so a rail along its face's rim does not fail "Unable to Process Rollover" (WR-301, #485). diff --git a/fixtures/blend.fillet_geom_p1/cases.json b/fixtures/blend.fillet_geom_p1/cases.json index eceecfff0b..c563599d3f 100644 --- a/fixtures/blend.fillet_geom_p1/cases.json +++ b/fixtures/blend.fillet_geom_p1/cases.json @@ -69616,6 +69616,1792 @@ {"name":"classify_face_point_corner_in_orig","upstream_ref":"source/SMLib/src/SmFilletGeom.cpp:480-595 sm_ClassifyFacePoint (PR #268 corner model: face 0 the y = 0 square [0, 0.1]^2 in x/z, face 1 the x = 0 square in y/z; plan/findings.md 2026-10-22)","inputs":{"op":"classify_face_point","surface1":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0.1,0,0,-1.7976931348623157e+308],[0.1,0,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"surface2":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0,0.1,0,-1.7976931348623157e+308],[0,0.1,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"pos":[0.05,0,0.05]}}, {"name":"classify_face_point_corner_near_edge","upstream_ref":"source/SMLib/src/SmFilletGeom.cpp:480-595 sm_ClassifyFacePoint (PR #268 corner model: face 0 the y = 0 square [0, 0.1]^2 in x/z, face 1 the x = 0 square in y/z; plan/findings.md 2026-10-22)","inputs":{"op":"classify_face_point","surface1":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0.1,0,0,-1.7976931348623157e+308],[0.1,0,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"surface2":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0,0.1,0,-1.7976931348623157e+308],[0,0.1,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"pos":[0,0.0179,0.001]}}, {"name":"classify_face_point_corner_off_both","upstream_ref":"source/SMLib/src/SmFilletGeom.cpp:480-595 sm_ClassifyFacePoint (PR #268 corner model: face 0 the y = 0 square [0, 0.1]^2 in x/z, face 1 the x = 0 square in y/z; plan/findings.md 2026-10-22)","inputs":{"op":"classify_face_point","surface1":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0.1,0,0,-1.7976931348623157e+308],[0.1,0,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"surface2":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0,0.1,0,-1.7976931348623157e+308],[0,0.1,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"pos":[0.05,0.05,0.05]}}, - {"name":"classify_face_point_corner_shared_edge","upstream_ref":"source/SMLib/src/SmFilletGeom.cpp:480-595 sm_ClassifyFacePoint (PR #268 corner model: face 0 the y = 0 square [0, 0.1]^2 in x/z, face 1 the x = 0 square in y/z; plan/findings.md 2026-10-22)","inputs":{"op":"classify_face_point","surface1":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0.1,0,0,-1.7976931348623157e+308],[0.1,0,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"surface2":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0,0.1,0,-1.7976931348623157e+308],[0,0.1,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"pos":[0,0,0.05]}} + {"name":"classify_face_point_corner_shared_edge","upstream_ref":"source/SMLib/src/SmFilletGeom.cpp:480-595 sm_ClassifyFacePoint (PR #268 corner model: face 0 the y = 0 square [0, 0.1]^2 in x/z, face 1 the x = 0 square in y/z; plan/findings.md 2026-10-22)","inputs":{"op":"classify_face_point","surface1":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0.1,0,0,-1.7976931348623157e+308],[0.1,0,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"surface2":{"net":[[[0,0,0,-1.7976931348623157e+308],[0,0,0.1,-1.7976931348623157e+308]],[[0,0.1,0,-1.7976931348623157e+308],[0,0.1,0.1,-1.7976931348623157e+308]]],"p":1,"q":1,"u":[0,0,1,1],"v":[0,0,1,1]},"pos":[0,0,0.05]}}, + { + "name": "trim_failed_rail0", + "upstream_ref": "source/SMLib/src/SmFilletGeom.cpp:SmFilletGeom::Trim (#483)", + "inputs": { + "op": "fb", + "model": { + "vertices": [ + { + "point": [ + 0, + 0, + 0 + ] + }, + { + "point": [ + 1, + 0, + 0 + ] + } + ], + "geoms": [ + { + "gtype": 1, + "rails": [ + 0, + 1 + ], + "other1": [ + 2 + ], + "other2": [ + 3 + ] + } + ], + "edges": [ + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 0.5, + 0.5 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + } + ] + }, + "query": { + "q": "trim", + "g": 0, + "interval": [ + 0.25, + 0.75 + ] + } + } + }, + { + "name": "trim_failed_rail1", + "upstream_ref": "source/SMLib/src/SmFilletGeom.cpp:SmFilletGeom::Trim (#483)", + "inputs": { + "op": "fb", + "model": { + "vertices": [ + { + "point": [ + 0, + 0, + 0 + ] + }, + { + "point": [ + 1, + 0, + 0 + ] + } + ], + "geoms": [ + { + "gtype": 1, + "rails": [ + 0, + 1 + ], + "other1": [ + 2 + ], + "other2": [ + 3 + ] + } + ], + "edges": [ + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 0.5, + 0.5 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + } + ] + }, + "query": { + "q": "trim", + "g": 0, + "interval": [ + 0.25, + 0.75 + ] + } + } + }, + { + "name": "trim_failed_other1", + "upstream_ref": "source/SMLib/src/SmFilletGeom.cpp:SmFilletGeom::Trim (#483)", + "inputs": { + "op": "fb", + "model": { + "vertices": [ + { + "point": [ + 0, + 0, + 0 + ] + }, + { + "point": [ + 1, + 0, + 0 + ] + } + ], + "geoms": [ + { + "gtype": 1, + "rails": [ + 0, + 1 + ], + "other1": [ + 2 + ], + "other2": [ + 3 + ] + } + ], + "edges": [ + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 0.5, + 0.5 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + } + ] + }, + "query": { + "q": "trim", + "g": 0, + "interval": [ + 0.25, + 0.75 + ] + } + } + }, + { + "name": "trim_failed_other2", + "upstream_ref": "source/SMLib/src/SmFilletGeom.cpp:SmFilletGeom::Trim (#483)", + "inputs": { + "op": "fb", + "model": { + "vertices": [ + { + "point": [ + 0, + 0, + 0 + ] + }, + { + "point": [ + 1, + 0, + 0 + ] + } + ], + "geoms": [ + { + "gtype": 1, + "rails": [ + 0, + 1 + ], + "other1": [ + 2 + ], + "other2": [ + 3 + ] + } + ], + "edges": [ + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 0.5, + 0.5 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + } + ] + }, + "query": { + "q": "trim", + "g": 0, + "interval": [ + 0.25, + 0.75 + ] + } + } + }, + { + "name": "trim_disjoint_rail_interval", + "upstream_ref": "source/SMLib/src/SmFilletGeom.cpp:SmFilletGeom::Trim (#483)", + "inputs": { + "op": "fb", + "model": { + "vertices": [ + { + "point": [ + 0, + 0, + 0 + ] + }, + { + "point": [ + 1, + 0, + 0 + ] + } + ], + "geoms": [ + { + "gtype": 1, + "rails": [ + 0, + 1 + ], + "other1": [ + 2 + ], + "other2": [ + 3 + ] + } + ], + "edges": [ + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 2, + 3 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + } + ] + }, + "query": { + "q": "trim", + "g": 0, + "interval": [ + 0.25, + 0.75 + ] + } + } + }, + { + "name": "trim_failed_center_line", + "upstream_ref": "source/SMLib/src/SmFilletGeom.cpp:SmFilletGeom::Trim (#483)", + "inputs": { + "op": "fb", + "model": { + "vertices": [ + { + "point": [ + 0, + 0, + 0 + ] + }, + { + "point": [ + 1, + 0, + 0 + ] + } + ], + "geoms": [ + { + "gtype": 1, + "rails": [ + 0, + 1 + ], + "other1": [ + 2 + ], + "other2": [ + 3 + ], + "center": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 0.5, + 0.5 + ] + }, + "surface": { + "net": [ + [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 0, + 2, + 0, + -1.7976931348623157e+308 + ] + ], + [ + [ + 3, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 3, + 2, + 0.5, + -1.7976931348623157e+308 + ] + ] + ], + "p": 1, + "q": 1, + "u": [ + 0, + 0, + 1, + 1 + ], + "v": [ + 0, + 0, + 1, + 1 + ] + } + } + ], + "edges": [ + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + } + ] + }, + "query": { + "q": "trim", + "g": 0, + "interval": [ + 0.25, + 0.75 + ] + } + } + }, + { + "name": "trim_disjoint_surface_domain", + "upstream_ref": "source/SMLib/src/SmFilletGeom.cpp:SmFilletGeom::Trim (#483)", + "inputs": { + "op": "fb", + "model": { + "vertices": [ + { + "point": [ + 0, + 0, + 0 + ] + }, + { + "point": [ + 1, + 0, + 0 + ] + } + ], + "geoms": [ + { + "gtype": 1, + "rails": [ + 0, + 1 + ], + "other1": [ + 2 + ], + "other2": [ + 3 + ], + "surface": { + "net": [ + [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 0, + 2, + 0, + -1.7976931348623157e+308 + ] + ], + [ + [ + 3, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 3, + 2, + 0.5, + -1.7976931348623157e+308 + ] + ] + ], + "p": 1, + "q": 1, + "u": [ + 0, + 0, + 1, + 1 + ], + "v": [ + 0, + 0, + 1, + 1 + ] + } + } + ], + "edges": [ + { + "vs": 0, + "ve": 1, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + } + ] + }, + "query": { + "q": "trim", + "g": 0, + "interval": [ + 2, + 3 + ] + } + } + }, + { + "name": "trim_failed_surface_point", + "upstream_ref": "source/SMLib/src/SmFilletGeom.cpp:SmFilletGeom::Trim (#483)", + "inputs": { + "op": "fb", + "model": { + "vertices": [ + { + "point": [ + 0, + 0, + 0 + ] + }, + { + "point": [ + 1, + 0, + 0 + ] + } + ], + "geoms": [ + { + "gtype": 1, + "rails": [ + 0, + 1 + ], + "other1": [ + 2 + ], + "other2": [ + 3 + ], + "surface": { + "net": [ + [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 0, + 2, + 0, + -1.7976931348623157e+308 + ] + ], + [ + [ + 3, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 3, + 2, + 0.5, + -1.7976931348623157e+308 + ] + ] + ], + "p": 1, + "q": 1, + "u": [ + 0, + 0, + 1, + 1 + ], + "v": [ + 0, + 0, + 1, + 1 + ] + } + } + ], + "edges": [ + { + "vs": 0, + "ve": 1, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + } + ] + }, + "query": { + "q": "trim", + "g": 0, + "interval": [ + 0.5, + 0.5 + ] + } + } + }, + { + "name": "trim_success_snaps_and_visits_all_rails", + "upstream_ref": "source/SMLib/src/SmFilletGeom.cpp:SmFilletGeom::Trim (#483)", + "inputs": { + "op": "fb", + "model": { + "vertices": [ + { + "point": [ + 0, + 0, + 0 + ] + }, + { + "point": [ + 1, + 0, + 0 + ] + } + ], + "geoms": [ + { + "gtype": 1, + "rails": [ + 0, + 1 + ], + "other1": [ + 2 + ], + "other2": [ + 3 + ], + "center": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "surface": { + "net": [ + [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 0, + 2, + 0, + -1.7976931348623157e+308 + ] + ], + [ + [ + 3, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 3, + 2, + 0.5, + -1.7976931348623157e+308 + ] + ] + ], + "p": 1, + "q": 1, + "u": [ + 0, + 0, + 1, + 1 + ], + "v": [ + 0, + 0, + 1, + 1 + ] + } + } + ], + "edges": [ + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + }, + { + "vs": 0, + "ve": 1, + "curve": { + "dim": 3, + "cpts": [ + [ + 0, + 0, + 0, + -1.7976931348623157e+308 + ], + [ + 1, + 0, + 0, + -1.7976931348623157e+308 + ] + ], + "p": 1, + "u": [ + 0, + 0, + 1.0, + 1.0 + ] + }, + "interval": [ + 0, + 1 + ], + "eus": [ + {}, + {} + ] + } + ] + }, + "query": { + "q": "trim", + "g": 0, + "interval": [ + -1e-14, + 0.75 + ] + } + } + } ] } diff --git a/plan/findings/blend.fillet_geom_p1--fillet-trim-status-483.md b/plan/findings/blend.fillet_geom_p1--fillet-trim-status-483.md new file mode 100644 index 0000000000..f751c93f43 --- /dev/null +++ b/plan/findings/blend.fillet_geom_p1--fillet-trim-status-483.md @@ -0,0 +1,81 @@ +# blend.fillet_geom_p1: FilletGeom Trim status propagation (#483, PR #674, 2026-10-24) + +`blend.fillet_geom_p1` copied the pinned C++ `SmFilletGeom::Trim`'s dropped +statuses. `FilletBrep::trim_geom` now returns the actual error from each primary +rail, both other-rail lists, the center line, either interval intersection and +the surface trim. A failed intersection has no usable result: it must not become +`Extent1d::new()` assigned to an edge or a surface domain. Successful edits and +parameter snapping before an error remain, in the original visit order. This +is error propagation, not transaction rollback. The wider boolean/offset/sweep +sites in #483 remain separate work; this PR does not close that broad issue. + +A small native reproduction uses a degree-one rail over `[0,0.5]` and asks to +trim it to `[0.25,0.75]`. Its trim returns `SM_ERR` (1001), but the pinned C++ +outer operation returns success, sets the edge interval to `[0.25,0.75]` although +the curve still ends at 0.5, and edits subsequent rails. Rust now returns 1001 +and stops at that rail. With a valid curve but edge interval `[2,3]`, the same +request makes C++ install its unset intersection `[1e20,-1e20]`; Rust preserves +`[2,3]` and returns the intersection error. Earlier successful curve edits remain. +No CAD application exists in this kernel repository to capture this status-only +API defect; the fixtures contain the complete small pseudo-B-reps and export +both statuses and geometry/interval state. + +Six Rust regressions exercise all four rail positions (one parameterized test), +a disjoint edge interval, center-line failure before surface edits, disjoint +surface U domains, a collapsed surface trim and successful snapping/trimming of +all geometry. Five tests fail against the original implementation; all six pass +with error propagation. The successful case snaps -1e-14 to 0 and trims every +rail, center line and surface to `[0,0.75]`. + +The native corpus retains all original cases and adds nine small fixtures. Its +minimum generated corpus increases from 300 to 2000 to keep the additional error +paths in the canonical check. The unclassified native run has 2199 cases: 2162 +bit-identical passes, 13 existing explicit reference guard rejections and 24 +C++ correctness mismatches. All 24 are recorded individually in +the former shared `crates/difftest/known_mismatches.toml`; they are not passes and this unit is +**partial**. No tolerance, reference implementation or refusal guard changes. +The mismatches are: + +- `fb_trim_outside`, `fb_trim_point`: out-of-domain/collapsed rail trim failures + were ignored; C++ changes rail intervals anyway (1001/1007 become success). +- `trim_failed_rail0`, `trim_failed_rail1`, `trim_failed_other1`, + `trim_failed_other2`: the short rail reproduction above at each visit position. +- `trim_disjoint_rail_interval`: failed intersection installs the unset sentinel. +- `trim_failed_center_line`: C++ continues to trim the surface after the center + curve trim failed; Rust propagates 1001. +- `trim_disjoint_surface_domain`, `trim_failed_surface_point`: C++ returns success + after the surface intersection/trim failed; Rust propagates 1001/1007. +- `gen0139`, `gen0185`, `gen0476`, `gen1351`, `gen1477`: requested first-rail trim + exceeds its natural domain; C++ drops the error and continues editing. +- `gen0438`: curve-less rails are skipped, then the center trim's lower bound is + outside its natural domain; C++ drops the error. +- `gen0245`, `gen1452`, `gen1626`: first visited rail has an unset edge interval; + C++ ignores the failed intersection and edits later geometry. +- `gen0829`: the reversed request is normalized to `[0.2866718515818742, + 0.4458908358559929]`; the second rail has an unset edge interval. The reversal + itself is not an error. C++ ignores the failed intersection. +- `gen1609`: an other rail has an unset edge interval; C++ proceeds into the center. +- `gen1440`, `gen1647`, `gen1709`: a visited rail's edge interval is disjoint from + the request; C++ installs an unset result and returns success. + +Local Rust validation on c44a2e1e3 plus this fix: 2077 kernel tests pass (two +pre-existing ignored tests), default workspace Clippy with -D warnings and +wasm32-unknown-unknown check pass. The full macOS --green suite remains subject +to the already reported C++ reference defects #448/#449/#450; it has not been +re-certified by this change. Canonical CI still needs maintainer approval. + +Default and native-feature workspace Clippy with -D warnings pass, as do the +46 default difftest unit tests and one floating-point roundtrip integration test. +The classified native run is PARTIAL: 2199 / 2162 pass / 24 known mismatches / +13 existing guard rejections / 0 unexpected failures. Whole-unit perturbation +reports INSENSITIVE because its 24 known-mismatch outcomes are not counted as +FAIL; all 2162 originally passing comparisons are detected, and the new +successful-snapping fixture independently reports SENSITIVE (1/1). This is not +claimed as a clean whole-unit perturbation gate. + +Final rebase onto main 73e557331 preserves all implementation/fixture/mismatch +bytes. Repeated fmt, all 2077 kernel tests, default/native workspace Clippy, +wasm and the canonical native run pass their stated gates on that base: the +native unit remains partial, with 2162 exact passes, 24 explicit reference bugs, +13 existing guard rejections and zero unexpected failures. Successful-snapping +perturbation remains SENSITIVE (1/1). No conflicts required resolution. diff --git a/plan/units/blend.fillet_geom_p1.toml b/plan/units/blend.fillet_geom_p1.toml index 0479065851..99607e751e 100644 --- a/plan/units/blend.fillet_geom_p1.toml +++ b/plan/units/blend.fillet_geom_p1.toml @@ -6,7 +6,7 @@ soft_depends_on = ["curve.curve_p1", "curve.curve_p4", "curve.hermite_curve", "g est_lines = 2113 layer = 48 tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_point", "prog_test:my_test_matrix", "prog_test:my_test_reversed_line_join"] -status = "green" +status = "partial" claimed_by = "" claimed_at = "" -notes = "Runner faces built as the shim builds them (fix/classify-face-point-268, 2026-10-22): CreateFaceFromSurface on a new_brep (was a bare loopless face of tolerance 0 on the Rust side only, which made sm_ClassifyFacePoint disagree on PR #268); 6 corner-model classify_face_point fixtures added. PARTIAL, 490 cases, 457 pass, 20 deferred (intersect.surface_drop_lib_p3 20: SmSurface::DropCurve making the faces' UV trim curves in CalculateUVDomainFromUVTrimCurves; 14 that compared a bare face before and the 6 new fixtures), 0 failed, 13 rejected, max_rel_err 0. PointBasedSurfaceIntersect wired to DoPointIntersection (area 6b, 2026-10-22): GREEN, 484 cases, 471 pass, 0 deferred (was intersect.surface_intersector_p4 7), 0 failed, 13 rejected, max_rel_err 0. PARTIAL (PR #147; edited by PR #264 blend.fillet_geom_p5: sm_TwoPntsIntersection's drops and the runner's SurfaceDropPoint name their surface (SurfaceHandle), CurveObjectHandle added, the model's pc_kind 4 (an SmEdge), four 1e-3 / 1e-6 seam fixtures; 480 -> 484 cases, 2000-seed sweep and fuzz counts below are from before that edit): difftest 484 cases (184 fixture in fixtures/blend.fillet_geom_p1/cases.json: noise cleanup, seam checks on a closed quadratic cylinder, closures, order checks, seam adjustment, the line/arc/parabola/Hermite interpolation branches, surface point drops, two-point intersections, mate evaluation inside and outside the domain, capping, a pseudo-B-rep model with every query, the HANG-29 / UB-166 / HANG-20 regression inputs and two near-zero curve-knot probes; 300 generated: random open/closed surfaces and curves at 1e-6..1e6 scales, tolerance-limit seam points, degenerate tangents, every SmSurfParamType closure, orientation and point class, random pseudo-B-rep models with random queries), 462 pass bit-identical (max_rel_err 0, both-errored 0, with_values 462, no_values 0), 0 failed, 13 rejected (fixtures flagged expected_rejection, the only ones a refusal may be recorded for: the FbExamine adjusting path, which the shim refuses, three HANG-29 inputs above the 1e150 bound, five UB-166 surfaces whose v knots the constructor's fixup moves onto the start knot (the shim reads the constructed vector back and refuses them for the six surface-cache ops only; the u over-multiplicity, non-monotone vectors and the other ops are compared) and four HANG-20 inputs above the 1e307 solver bound the FFI refuses), 9 deferred (intersect.surface_intersector_p4 7: sm_TwoPntsIntersection past its point drops (surface.surface_p2 stops at SmSurfaceIntersector::DoPointIntersection pending the intersector parts' virtual dispatch); intersect.surface_drop_lib_p1 2 (printed as intersect.surface_drop_curve, the label of that unit's stale stub): SmFilletVertex::CalcCliffRailIntEdge via SmSurface::GlobalCurveIntersect -> SmSurfaceDropCurve::DoTrace, ported but unwired in surface_drop_lib_p1 until its open PR #140); 2000-seed sweep 2051 pass / 116 deferred / 0 failed / 13 rejected (with_values 2051); --perturb SENSITIVE 462/462; --fuzz 2000 FUZZ-PASS (479 ran, 0 differs, crashes, panics, timeouts); fuzz_guards (per-call 30 s timeout, 180 s for the two ops reaching sm_ClassifyFacePoint; boundary specials incl. the HANG-29 slow range, the UB-166 knot gaps and the HANG-20 bound; a near-coincident-value mutation) found no crash or hang for the default seed at 60k calls and seeds 777 and 4242 at 40k (the earlier sweeps missed UB-166 and the slow HANG-29 inputs, and the widened specials then found HANG-20, so a clean sweep is evidence, not proof). Object model: FilletBrep = a topology::Brep plus side tables of fillet members keyed by VertexId/EdgeId/EdgeuseId/VertexuseId and the referenced FilletGeoms (FilletGeomId); SmFilletSolver*/SmFilletCorner* are opaque ids. Stubs replace only their call; the code after each is ported: SmFilletEdgeuse ctor -> SmBrep::GetInfiniteRegion (wired, topology.brep_p1 PR #145; the region itself comes from SmBrep(), so FilletBrep::new has none until UNPORTED(topology.brep_p3) SmBrep::SmBrep), SmRegion::GetShells (wired, topology.shell PR #154, no attribute filter as upstream's default), UNPORTED(topology.brep_p2) MakeShellVertexTopology, then the shell-owned edge use record; sm_CreateCubicRailRailInterpolant -> UNPORTED(blend.fillet_solver_p1) FindIndexOfRailOnFace, (blend.fillet_solver_p2) RailRailIntersect, then the degree-2 interpolant; sm_ExamineNFixFilletGeoms adjust path -> SmOffsetSurface::GetNaturalUVDomain (wired, surface.offset_surface PR #62), UNPORTED(intersect.surface_drop_lib_p3) SmSurface::DropCurve, then the mate edge use's UV curve (the solvers are passed in as co-ported ConstantRadiusAssistedFs; the shim refuses that path, SC_ERR_ARG, because its geometries have no SmFilletSolver and upstream dereferences it); SmFilletGeom ctor with a solver offset surface -> SmOffsetSurface::Copy (wired, surface.offset_surface PR #62; the kernel stands for the solver's creation context; no case reaches it, the shim's geometries have no solver). CO-PORTED in blend.fillet_geom_p1 (PR #147): SmFilletVertex(SmFilletCorner*), SmFilletEdge(SmFilletBrep*, SmFilletCorner*) (blend.fillet_geom_p2) -> blend::fillet_geom::p1::{FilletVertex::new, FilletBrep::new_fillet_edge}; SmFilletGeom(SmFilletSolver*, SmFilletGeomType) (blend.fillet_geom_p5) -> FilletGeom::new; SmFilletBrep() (blend.fillet_geom_p6) -> FilletBrep::new; smos_CleanUpNoise (SmMath.cpp, no owning unit) -> clean_up_noise; the one-line SmVertexuse/SmVertex/SmEdge::IsKindOf type tests (topology.vertexuse/vertex/edge) inlined privately. SmBoundaryTrimmingType is also co-ported, identically, by blend.fillet_standard_solver_p1 (PR #168): the second to land re-exports the other's. Upstream issues UB-159 (SmFilletGeom ctor leaves m_dOrientations/m_bCapped uninitialised), HANG-29 (SmCurve::GlobalCoincidenceChecker never returns for a surface control point >= 1e154 and is slow, shape-dependently, from about 1e15; for the two ops reaching sm_ClassifyFacePoint the FFI rejects Euclidean magnitudes above 1e150, the overflow regime, and accepts the slow inputs) and UB-166 (the SmBSplineSurface constructor's sm_FixupKnotVector snaps a first interior knot within the scaled zero of the start knot onto it, and sm_DecomposeSrf then writes one v-patch past the surface cache's patch array: SIGSEGV from sm_SurfaceDropPoint and sm_ClassifyFacePoint; the FFI refuses every surface whose snapped knot vector has start multiplicity above degree + 1; curves are safe, sm_CreateBezSegments skips such a start) and HANG-20 (SmGlobalSolver::SolveBranch with the point/surface, curve/surface and surface/surface local solvers never returns once a surface coordinate is in the top decade of the double range: cliff -7e307 returns / -8e307 never, point drop -3e307 / -7e307, two-point homogeneous -2e307 / -3e307, all return at -1e300; the FFI bounds SurfaceDropPoint, TwoPnts, FbVertOnVert and FbCliff at 1e307). The examine adjusting path (net-row copy, rail SetCurve resets, offset-distance save/restore, Unported at SmSurface::DropCurve) is covered by a Rust unit test with a solver only. Shim compiles SmFilletGeom.cpp into its translation unit for the file-static helpers. No UNPORTED(blend.fillet_geom_p1) stubs existed to wire. Plan notes: 111 fns; largest: sm_ExamineNFixFilletGeoms, sm_TwoPntsIntersection, SmFilletEdge::FindFilletBrepEdge, sm_InterpolateEndptsAndTangents; cycle back-edges (call into later units; stub or co-port): blend.fillet_solver_p1(6), blend.fillet_solver_p2(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in blend.fillet_intersector (PR #179): SmFilStatus -> solidcore::blend::fillet_intersector::FilStatus; the SmFilletGeom stores FlushCurve makes (SetFilletSurface, m_pCenterLineCurve, GetRail(i)->SetCurve, mate SetUVCurve) are FilletSolverOps methods (UNPORTED stubs) over the handle FilletSolverOps::FilletGeom Re-run in PR #238 (query.face_p1): sm_ClassifyFacePoint's SmFace::Point3DClassify is wired to query::face::p1::point_3d_classify (status compared to SM_SUCCESS, not propagated, as upstream); no case reaches it yet (they defer earlier), so the report is unchanged: 480 cases, 446 pass, 0 failed, 21 deferred, 13 rejected. UPDATE (PR #204, topology.brep_p3): FilletBrep::new makes SmBrep()'s infinite void region (topology::Brep::new_with_infinite_region, topology.brep_p10's co-port of the SmBrep() topology part), so the UNPORTED(topology.brep_p3) SmBrep::SmBrep stop in new_fillet_edgeuse is gone (a missing region is now SM_ERR_NULL_POINTER, upstream's unchecked dereference). FilletBrep::new keeps its kernel-free signature (about ten open fillet PRs call it), so the rest of SmBrep() is not applied to the pseudo B-rep: no context zone tolerance or size settings, no SM_NO_CONSTRUCTION notification (Constructors deviation). Report unchanged (480 cases, 446 pass, 0 fail, 21 deferred, 13 rejected). GREEN (promoted by intersect.surface_drop_lib_p3, PR #463): CI difftest 490 cases, 477 pass, 0 deferred, 0 failed, 13 rejected as on main (the 20 cases deferred on SmSurface::DropCurve now compare)." +notes = "Issue #483 / PR #674: trim_geom propagates all curve/surface trim and interval-intersection errors instead of reporting success or assigning unset intervals; earlier successful edits/snapping remain, no rollback promised. Six Rust regressions (all four rail positions, edge/surface intersections, center/surface failures, success snapping); five failed before the fix. Native corpus expanded to 2000 generated + 199 fixtures: 2199 total, 2162 bit-identical passes, 13 existing guard rejections, 24 explicitly tracked C++ correctness mismatches, 0 dependency deferrals; partial, never green. See findings.md: FilletGeom Trim status propagation. Runner faces built as the shim builds them (fix/classify-face-point-268, 2026-10-22): CreateFaceFromSurface on a new_brep (was a bare loopless face of tolerance 0 on the Rust side only, which made sm_ClassifyFacePoint disagree on PR #268); 6 corner-model classify_face_point fixtures added. PARTIAL, 490 cases, 457 pass, 20 deferred (intersect.surface_drop_lib_p3 20: SmSurface::DropCurve making the faces' UV trim curves in CalculateUVDomainFromUVTrimCurves; 14 that compared a bare face before and the 6 new fixtures), 0 failed, 13 rejected, max_rel_err 0. PointBasedSurfaceIntersect wired to DoPointIntersection (area 6b, 2026-10-22): GREEN, 484 cases, 471 pass, 0 deferred (was intersect.surface_intersector_p4 7), 0 failed, 13 rejected, max_rel_err 0. PARTIAL (PR #147; edited by PR #264 blend.fillet_geom_p5: sm_TwoPntsIntersection's drops and the runner's SurfaceDropPoint name their surface (SurfaceHandle), CurveObjectHandle added, the model's pc_kind 4 (an SmEdge), four 1e-3 / 1e-6 seam fixtures; 480 -> 484 cases, 2000-seed sweep and fuzz counts below are from before that edit): difftest 484 cases (184 fixture in fixtures/blend.fillet_geom_p1/cases.json: noise cleanup, seam checks on a closed quadratic cylinder, closures, order checks, seam adjustment, the line/arc/parabola/Hermite interpolation branches, surface point drops, two-point intersections, mate evaluation inside and outside the domain, capping, a pseudo-B-rep model with every query, the HANG-29 / UB-166 / HANG-20 regression inputs and two near-zero curve-knot probes; 300 generated: random open/closed surfaces and curves at 1e-6..1e6 scales, tolerance-limit seam points, degenerate tangents, every SmSurfParamType closure, orientation and point class, random pseudo-B-rep models with random queries), 462 pass bit-identical (max_rel_err 0, both-errored 0, with_values 462, no_values 0), 0 failed, 13 rejected (fixtures flagged expected_rejection, the only ones a refusal may be recorded for: the FbExamine adjusting path, which the shim refuses, three HANG-29 inputs above the 1e150 bound, five UB-166 surfaces whose v knots the constructor's fixup moves onto the start knot (the shim reads the constructed vector back and refuses them for the six surface-cache ops only; the u over-multiplicity, non-monotone vectors and the other ops are compared) and four HANG-20 inputs above the 1e307 solver bound the FFI refuses), 9 deferred (intersect.surface_intersector_p4 7: sm_TwoPntsIntersection past its point drops (surface.surface_p2 stops at SmSurfaceIntersector::DoPointIntersection pending the intersector parts' virtual dispatch); intersect.surface_drop_lib_p1 2 (printed as intersect.surface_drop_curve, the label of that unit's stale stub): SmFilletVertex::CalcCliffRailIntEdge via SmSurface::GlobalCurveIntersect -> SmSurfaceDropCurve::DoTrace, ported but unwired in surface_drop_lib_p1 until its open PR #140); 2000-seed sweep 2051 pass / 116 deferred / 0 failed / 13 rejected (with_values 2051); --perturb SENSITIVE 462/462; --fuzz 2000 FUZZ-PASS (479 ran, 0 differs, crashes, panics, timeouts); fuzz_guards (per-call 30 s timeout, 180 s for the two ops reaching sm_ClassifyFacePoint; boundary specials incl. the HANG-29 slow range, the UB-166 knot gaps and the HANG-20 bound; a near-coincident-value mutation) found no crash or hang for the default seed at 60k calls and seeds 777 and 4242 at 40k (the earlier sweeps missed UB-166 and the slow HANG-29 inputs, and the widened specials then found HANG-20, so a clean sweep is evidence, not proof). Object model: FilletBrep = a topology::Brep plus side tables of fillet members keyed by VertexId/EdgeId/EdgeuseId/VertexuseId and the referenced FilletGeoms (FilletGeomId); SmFilletSolver*/SmFilletCorner* are opaque ids. Stubs replace only their call; the code after each is ported: SmFilletEdgeuse ctor -> SmBrep::GetInfiniteRegion (wired, topology.brep_p1 PR #145; the region itself comes from SmBrep(), so FilletBrep::new has none until UNPORTED(topology.brep_p3) SmBrep::SmBrep), SmRegion::GetShells (wired, topology.shell PR #154, no attribute filter as upstream's default), UNPORTED(topology.brep_p2) MakeShellVertexTopology, then the shell-owned edge use record; sm_CreateCubicRailRailInterpolant -> UNPORTED(blend.fillet_solver_p1) FindIndexOfRailOnFace, (blend.fillet_solver_p2) RailRailIntersect, then the degree-2 interpolant; sm_ExamineNFixFilletGeoms adjust path -> SmOffsetSurface::GetNaturalUVDomain (wired, surface.offset_surface PR #62), UNPORTED(intersect.surface_drop_lib_p3) SmSurface::DropCurve, then the mate edge use's UV curve (the solvers are passed in as co-ported ConstantRadiusAssistedFs; the shim refuses that path, SC_ERR_ARG, because its geometries have no SmFilletSolver and upstream dereferences it); SmFilletGeom ctor with a solver offset surface -> SmOffsetSurface::Copy (wired, surface.offset_surface PR #62; the kernel stands for the solver's creation context; no case reaches it, the shim's geometries have no solver). CO-PORTED in blend.fillet_geom_p1 (PR #147): SmFilletVertex(SmFilletCorner*), SmFilletEdge(SmFilletBrep*, SmFilletCorner*) (blend.fillet_geom_p2) -> blend::fillet_geom::p1::{FilletVertex::new, FilletBrep::new_fillet_edge}; SmFilletGeom(SmFilletSolver*, SmFilletGeomType) (blend.fillet_geom_p5) -> FilletGeom::new; SmFilletBrep() (blend.fillet_geom_p6) -> FilletBrep::new; smos_CleanUpNoise (SmMath.cpp, no owning unit) -> clean_up_noise; the one-line SmVertexuse/SmVertex/SmEdge::IsKindOf type tests (topology.vertexuse/vertex/edge) inlined privately. SmBoundaryTrimmingType is also co-ported, identically, by blend.fillet_standard_solver_p1 (PR #168): the second to land re-exports the other's. Upstream issues UB-159 (SmFilletGeom ctor leaves m_dOrientations/m_bCapped uninitialised), HANG-29 (SmCurve::GlobalCoincidenceChecker never returns for a surface control point >= 1e154 and is slow, shape-dependently, from about 1e15; for the two ops reaching sm_ClassifyFacePoint the FFI rejects Euclidean magnitudes above 1e150, the overflow regime, and accepts the slow inputs) and UB-166 (the SmBSplineSurface constructor's sm_FixupKnotVector snaps a first interior knot within the scaled zero of the start knot onto it, and sm_DecomposeSrf then writes one v-patch past the surface cache's patch array: SIGSEGV from sm_SurfaceDropPoint and sm_ClassifyFacePoint; the FFI refuses every surface whose snapped knot vector has start multiplicity above degree + 1; curves are safe, sm_CreateBezSegments skips such a start) and HANG-20 (SmGlobalSolver::SolveBranch with the point/surface, curve/surface and surface/surface local solvers never returns once a surface coordinate is in the top decade of the double range: cliff -7e307 returns / -8e307 never, point drop -3e307 / -7e307, two-point homogeneous -2e307 / -3e307, all return at -1e300; the FFI bounds SurfaceDropPoint, TwoPnts, FbVertOnVert and FbCliff at 1e307). The examine adjusting path (net-row copy, rail SetCurve resets, offset-distance save/restore, Unported at SmSurface::DropCurve) is covered by a Rust unit test with a solver only. Shim compiles SmFilletGeom.cpp into its translation unit for the file-static helpers. No UNPORTED(blend.fillet_geom_p1) stubs existed to wire. Plan notes: 111 fns; largest: sm_ExamineNFixFilletGeoms, sm_TwoPntsIntersection, SmFilletEdge::FindFilletBrepEdge, sm_InterpolateEndptsAndTangents; cycle back-edges (call into later units; stub or co-port): blend.fillet_solver_p1(6), blend.fillet_solver_p2(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in blend.fillet_intersector (PR #179): SmFilStatus -> solidcore::blend::fillet_intersector::FilStatus; the SmFilletGeom stores FlushCurve makes (SetFilletSurface, m_pCenterLineCurve, GetRail(i)->SetCurve, mate SetUVCurve) are FilletSolverOps methods (UNPORTED stubs) over the handle FilletSolverOps::FilletGeom Re-run in PR #238 (query.face_p1): sm_ClassifyFacePoint's SmFace::Point3DClassify is wired to query::face::p1::point_3d_classify (status compared to SM_SUCCESS, not propagated, as upstream); no case reaches it yet (they defer earlier), so the report is unchanged: 480 cases, 446 pass, 0 failed, 21 deferred, 13 rejected. UPDATE (PR #204, topology.brep_p3): FilletBrep::new makes SmBrep()'s infinite void region (topology::Brep::new_with_infinite_region, topology.brep_p10's co-port of the SmBrep() topology part), so the UNPORTED(topology.brep_p3) SmBrep::SmBrep stop in new_fillet_edgeuse is gone (a missing region is now SM_ERR_NULL_POINTER, upstream's unchecked dereference). FilletBrep::new keeps its kernel-free signature (about ten open fillet PRs call it), so the rest of SmBrep() is not applied to the pseudo B-rep: no context zone tolerance or size settings, no SM_NO_CONSTRUCTION notification (Constructors deviation). Report unchanged (480 cases, 446 pass, 0 fail, 21 deferred, 13 rejected). GREEN (promoted by intersect.surface_drop_lib_p3, PR #463): CI difftest 490 cases, 477 pass, 0 deferred, 0 failed, 13 rejected as on main (the 20 cases deferred on SmSurface::DropCurve now compare)."