diff --git a/crates/difftest/src/units/blend_hcr.rs b/crates/difftest/src/units/blend_hcr.rs index 119593987f..ba0c56701d 100644 --- a/crates/difftest/src/units/blend_hcr.rs +++ b/crates/difftest/src/units/blend_hcr.rs @@ -341,6 +341,10 @@ impl UnitDiff for Runner { { let _ = surface.set_owner(Some(arena_identity(CacheObject::Face(id)))); } + // the face's tolerance, as the face upstream answers GetTolerance with + scene + .sync_face_surface_tolerance(id) + .map_err(|e| harness(e.to_string()))?; } let [x0, y0, z0, x1, y1, z1] = s.scene_box; let (lo, hi) = (Vec3::new(x0, y0, z0), Vec3::new(x1, y1, z1)); diff --git a/crates/difftest/src/units/boolean_merge_p3.rs b/crates/difftest/src/units/boolean_merge_p3.rs index cd6b7b66f1..a7b93e5de9 100644 --- a/crates/difftest/src/units/boolean_merge_p3.rs +++ b/crates/difftest/src/units/boolean_merge_p3.rs @@ -426,6 +426,9 @@ impl Pool { ))); } } + // a new owner key drops the cached face tolerance: record it again + brep.sync_face_surface_tolerance(face) + .map_err(|error| harness(&error))?; } breps.push(brep); } diff --git a/crates/difftest/src/units/heal_face_props.rs b/crates/difftest/src/units/heal_face_props.rs index f633cae538..832c53e201 100644 --- a/crates/difftest/src/units/heal_face_props.rs +++ b/crates/difftest/src/units/heal_face_props.rs @@ -463,7 +463,11 @@ pub(crate) fn rust_brep(k: &Kernel, f: &FaceIn) -> Result<(Brep, FaceId), Harnes .map_err(e)?; b.shell_mut(shell).map_err(e)?.faces.push(face); let up = next_id!(b.faceuses(), FaceuseId); - let down = next_id!(b.faceuses(), FaceuseId); + // the down face use is the record after the up one (both ids are taken before either + // record is added, so they cannot both come from the arena's length) + let down = FaceuseId::from_index( + u32::try_from(b.faceuses().len() + 1).map_err(|_| harness("too many records"))?, + ); for (mate, orientation) in [(down, OrientType::Same), (up, OrientType::Opposite)] { b.add_faceuse_record(Faceuse { shell: Some(shell), diff --git a/crates/difftest/src/units/intersect_curve_class_p4.rs b/crates/difftest/src/units/intersect_curve_class_p4.rs index 2adba83a6e..11a82e45c1 100644 --- a/crates/difftest/src/units/intersect_curve_class_p4.rs +++ b/crates/difftest/src/units/intersect_curve_class_p4.rs @@ -10,7 +10,8 @@ //! optional `face` (a one-face B-rep built with `SmBrepConstructor` as in unit //! `heal.face_props`: a plane or `SmSurfOfRevolution::CreateCanonical` surface, cylinder or //! sphere among them, with a vertex loop or edge loops of line edges; rebuilt record by record -//! on the Rust side by `heal_face_props::rust_brep`) whose face, loops, edges, vertices and edge +//! on the Rust side by `heal_face_props::rust_brep` plus its down face use's side, +//! `heal_face_props::add_down_side`) whose face, loops, edges, vertices and edge //! uses join the pool after the objects, and `ops` on two default classifications `cc[0]`, //! `cc[1]`: the setters of unit `intersect.curve_class_p1` build them (intervals, boundary //! classifications on the face's edges and vertices, the classification curve, face and @@ -775,7 +776,10 @@ fn build_pool(k: &Kernel, s: &Script) -> Result { pool.handles.push(H::Obj(i, class_kind_of(obj_kind(o)))); } if let Some(face) = &s.face { - let (b, f) = face_brep(k, face)?; + let (mut b, f) = face_brep(k, face)?; + // the down face use's loops, so that edge mates (an edge's end vertex, the end edge + // use of a vertex use's UV sector) exist as upstream + hfp::add_down_side(&mut b, f)?; for (e, _) in topology_entities(&b, f)? { pool.handles.push(H::Topo(e)); } diff --git a/crates/difftest/src/units/nurbs_b_spline_surface_p1.rs b/crates/difftest/src/units/nurbs_b_spline_surface_p1.rs index 8da1f7bd94..b9339dcb6c 100644 --- a/crates/difftest/src/units/nurbs_b_spline_surface_p1.rs +++ b/crates/difftest/src/units/nurbs_b_spline_surface_p1.rs @@ -1116,6 +1116,7 @@ fn rust_surface(k: &Kernel, op: &OpIn, s: &mut BSplineSurface, o: &mut Vec) pseudo_guess: pg.as_ref(), polar_guess: plg.as_ref(), expand_by_zone_tol: *expand, + expand_tolerance: None, }; let b = s.calculate_bounding_box(k, &domain_of(*domain), &req)?; if let Some(nb) = b.normal { diff --git a/crates/difftest/src/units/query_api_queries.rs b/crates/difftest/src/units/query_api_queries.rs index 152239eed2..77a1420991 100644 --- a/crates/difftest/src/units/query_api_queries.rs +++ b/crates/difftest/src/units/query_api_queries.rs @@ -403,6 +403,9 @@ fn imported(k: &Kernel, a: &[f64; 22], export_op: i32) -> std::result::Result
Result { key: ObjectKey(i as u64), })); } + // the face's tolerance, as the face upstream answers GetTolerance with + b.sync_face_surface_tolerance(f) + .map_err(|e| harness(e.to_string()))?; } Ok(Model { path, diff --git a/crates/difftest/src/units/topology_brep_p1.rs b/crates/difftest/src/units/topology_brep_p1.rs index 6057ba73db..322f6c8e76 100644 --- a/crates/difftest/src/units/topology_brep_p1.rs +++ b/crates/difftest/src/units/topology_brep_p1.rs @@ -861,6 +861,9 @@ pub(crate) fn import(ex: &[f64], graph_at: usize) -> Result<(Brep, ImportTypes), { let _ = surface.set_owner(Some(arena_identity(CacheObject::Face(id)))); } + // the face's tolerance, as the face upstream answers GetTolerance with + b.sync_face_surface_tolerance(id) + .map_err(|e| harness(e.to_string()))?; } // the shared import leaves `m_pFU` unset: the face's upward (SAME) face use let ups: Vec<(FaceId, FaceuseId)> = b @@ -1960,6 +1963,21 @@ fn run_rust_op( dir, } => { let id = |i: i64| (i >= 0).then(|| EdgeuseId::from_index(i as u32)); + // a blend (flag != 0) samples both edge uses' face surfaces (DropPoint, + // normals, directional derivatives): an analytic surface the export hands + // over as its B-spline part takes other paths in C++ (e.g. + // SmSurfOfRevolution::DropPointFast), so those cases are not comparable + if *f != 0 { + if let (Some(e1), Some(e2)) = (id(*eu1), id(*eu2)) { + let face = solidcore::topology::edgeuse::p1::face; + let has_edges = b.edgeuse(e1)?.edge.is_some() && b.edgeuse(e2)?.edge.is_some(); + if let (true, Some(f1), Some(f2)) = (has_edges, face(b, e1)?, face(b, e2)?) { + if let Some(d) = inexact(types, Some(&[f1, f2]), Some(&[])) { + return Ok(Err(d)); + } + } + } + } match bp1::create_edge_edge_blend( k, u64::from(*f), diff --git a/crates/difftest/src/units/topology_trimming_tools.rs b/crates/difftest/src/units/topology_trimming_tools.rs index cae4f60ef5..0bc123c09f 100644 --- a/crates/difftest/src/units/topology_trimming_tools.rs +++ b/crates/difftest/src/units/topology_trimming_tools.rs @@ -25,11 +25,13 @@ //! Rust side reports [`SolidError::InvalidInput`] or fails the same guard), `-3` a call //! the shim skips because the Rust side defers it to an unported unit. //! -//! Deferred: `SmFace::ComputeLoopOrientation` (`CheckFace` after its first loop check, -//! `FixTrimLoopOrientation`, `FixTrimLoopOrder`) at its `sm_AreConnected` / -//! `GetOrCreateUVTrimCurve` / `NormalizedEvaluate` calls (`topology.edgeuse_p3`), -//! `SplitAtSeams` (`topology.face_p1` / `topology.face_p4`), `CreateUVTrimCurves` at the -//! face-owned surface's `SmFace::GetTolerance` stub of `GetSingularities` +//! `CheckFace`, `FixTrimLoopOrientation` and `FixTrimLoopOrder` run in full on both sides +//! (`SmFace::ComputeLoopOrientation` is ported; it was skipped with `-3` while the Rust +//! side still deferred it), guarded (`-2`) where a loop lacks its loop use, the face its +//! surface or UV trim curves, or `CheckLoop` a loop's UV extremes. +//! +//! Deferred: `SplitAtSeams` (`topology.face_p1` / `topology.face_p4`), `CreateUVTrimCurves` +//! at the face-owned surface's `SmFace::GetTolerance` stub of `GetSingularities` //! (`topology.face_p2`; it and `ClearUVTrimCurves` are compared, with every edge use's //! UV trim curve after the call), `SmEdgeuse::GetOrCreateUVTrimCurve` //! (`topology.edgeuse_p3`), and the vertex-use UV points some B-reps need @@ -1573,6 +1575,20 @@ fn ok_loop_extremes(b: &Brep, l: LoopId) -> Result { Ok(true) } +/// The shim's `tt_ok_face_loops`: every loop has its loop use, and a face with loops +/// has a surface and UV trim curves everywhere. +fn ok_face_loops(b: &Brep, f: FaceId) -> Result { + let loops = face_loops(b, f)?; + for &l in &loops { + if !ok_loop(b, l)? { + return Ok(false); + } + } + Ok(loops.is_empty() + || (b.face(f).map_err(|e| fail(&e))?.surface.is_some() + && tt::face_uv_curves_present(b, f).map_err(|e| fail(&e))?)) +} + /// The surface of a loop's face (a copy), `None` without face or surface. fn loop_surface(b: &Brep, l: LoopId) -> Result, Fail> { let f = lu1::loop_face(b, l).map_err(|e| fail(&e))?; @@ -1754,6 +1770,21 @@ fn run_rust_brep(k: &Kernel, b: &mut Brep, op: &OpIn) -> Result, Fail> return Ok(o); } } + // the shim's guards of the CheckLoop / ComputeLoopOrientation walk + let loops = face_loops(b, fc)?; + if !loops.is_empty() && tt::face_uv_curves_present(b, fc).map_err(|e| fail(&e))? { + let mut ok = b.face(fc).map_err(|e| fail(&e))?.surface.is_some(); + for &l in &loops { + if !ok || *skip { + break; + } + ok = ok_loop_extremes(b, l)?; + } + if !ok { + o.push(NULL_DEREF); + return Ok(o); + } + } let mut a = AssertArray::new(); let r = tt::check_face(k, b, fc, *tol, list.then_some(&mut a), *skip); o.push(status(&r)?); @@ -1886,11 +1917,19 @@ fn run_rust_brep(k: &Kernel, b: &mut Brep, op: &OpIn) -> Result, Fail> put_uv_curves(&mut o, b); } OpIn::BOrient { f: face, .. } => { + if !ok_face_loops(b, fid(*face))? { + o.push(NULL_DEREF); + return Ok(o); + } let r = tt::fix_face_loop_orientations(k, b, fid(*face)); o.push(status(&r)?); o.push(r.map_or(0.0, |n| n as f64)); } OpIn::BOrder { f: face, .. } => { + if !ok_face_loops(b, fid(*face))? { + o.push(NULL_DEREF); + return Ok(o); + } let r = tt::fix_trim_loop_order(k, b, fid(*face)); o.push(status(&r)?); } diff --git a/crates/solidcore/src/intersect/point_class.rs b/crates/solidcore/src/intersect/point_class.rs index 09aa735cce..1aca6f0479 100644 --- a/crates/solidcore/src/intersect/point_class.rs +++ b/crates/solidcore/src/intersect/point_class.rs @@ -106,7 +106,9 @@ //! surface the tolerance of its edge or face (`GetEdge` / `GetFace`), and every other //! non-topology object the system zone tolerance. A curve's edge is found with //! [`Curve::edge`] (intermediate owners through [`ClassGeometry::owner_of`]); a -//! surface's face is [`Surface::face`](crate::surface::surface::Surface::face); +//! surface's face is [`Surface::face`](crate::surface::surface::Surface::face), +//! whose tolerance is the one its B-rep cached on the surface +//! ([`Surface::owner_zone_tol_3d`](crate::surface::surface::Surface::owner_zone_tol_3d)); //! an edge's or face's tolerance is that of the edge or face. Topology //! kinds without a `GetTolerance` override (region, faceuse, loop, loopuse, //! vertexuse) get `SmTopology::GetTolerance`, `SM_ZONE_TOL_3D`, and a faceuse @@ -1591,14 +1593,9 @@ pub fn object_zone_tol_3d( | ClassObjectKind::Region => Ok(ZONE_TOL_3D), ClassObjectKind::Surface => { let surface = geometry.surface(o).ok_or_else(|| unresolved(OP_GEOMETRY))?; - if surface.face()?.is_some() { - // UNPORTED(topology.face_p2): SmFace::GetTolerance - return Err(SolidError::unported( - "SmFace::GetTolerance", - "topology.face_p2", - )); - } - Ok(system) + // GetFace() ? its tolerance (cached on the surface by its B-rep; + // UNPORTED(topology.face_p2): SmFace::GetTolerance for a bare face handle) + Ok(surface.face_zone_tol_3d()?.unwrap_or(system)) } ClassObjectKind::Curve => { let curve = geometry.curve(o).ok_or_else(|| unresolved(OP_GEOMETRY))?; diff --git a/crates/solidcore/src/nurbs/b_spline_surface/p1.rs b/crates/solidcore/src/nurbs/b_spline_surface/p1.rs index d56fd496ae..3df758fe4f 100644 --- a/crates/solidcore/src/nurbs/b_spline_surface/p1.rs +++ b/crates/solidcore/src/nurbs/b_spline_surface/p1.rs @@ -115,7 +115,8 @@ //! `SmSurface::EvaluateNormal` (`surface::surface::p7`); its zone tolerance expansion //! reads `GetFace()`, and a bare B-spline has no owner (the owner is held by the //! [`Surface`](crate::surface::surface::Surface) wrapper), so it uses -//! `SM_ZONE_TOL_3D`; `CreateOffsetSurface` uses the base `SmSurface` body and +//! `SM_ZONE_TOL_3D` unless the wrapper passes its face's cached tolerance as +//! [`BoundingBoxRequest::expand_tolerance`]; `CreateOffsetSurface` uses the base `SmSurface` body and //! constructs an `OffsetSurface`, asking the copy for its singularities. //! `STEPInversion` runs `GlobalPointSolve` and `IsClosed` on the owning surface //! (including plane overrides) and calls the base `LocalPointSolve` and @@ -379,6 +380,11 @@ pub struct BoundingBoxRequest<'a> { /// Expand the normal and pseudo boxes by the zone tolerance /// (`bExpandPosBoxesByZoneTol3d`, upstream default `TRUE`). pub expand_by_zone_tol: bool, + /// The zone tolerance of the owning face to expand by (`GetFace()->GetTolerance()`): + /// a bare B-spline has no owner, so the [`Surface`](crate::surface::surface::Surface) + /// wrapper holding it passes its face's cached tolerance here; `None` expands by + /// the ownerless `SM_ZONE_TOL_3D`. + pub expand_tolerance: Option, } /// The boxes of [`BSplineSurface::calculate_bounding_box`] (each present when asked). @@ -499,11 +505,13 @@ fn evaluate_normal(k: &Kernel, srf: &BSplineSurface, uv: Point2) -> Result /// bare B-spline has no owner (the owner is held by the /// [`Surface`](crate::surface::surface::Surface) wrapper, whose /// [`calculate_bounding_box`](crate::surface::surface::Surface::calculate_bounding_box) -/// handles a face owner before calling here): `SM_ZONE_TOL_3D`. Callers that hold a -/// `Surface` with a face must check it first (as `Surface::calculate_bounding_box` and -/// `Surface::is_planar` do); the patch boxes of surface caches are ownerless. -const fn owner_zone_tolerance(_srf: &BSplineSurface) -> f64 { - crate::geom::tol::ZONE_TOL_3D +/// passes a face owner's tolerance as [`BoundingBoxRequest::expand_tolerance`]): +/// that tolerance when given, else `SM_ZONE_TOL_3D`. Callers that hold a `Surface` with +/// a face must pass it (as `Surface::calculate_bounding_box` and `Surface::is_planar` +/// do); the patch boxes of surface caches are ownerless. +fn owner_zone_tolerance(req: &BoundingBoxRequest<'_>) -> f64 { + req.expand_tolerance + .unwrap_or(crate::geom::tol::ZONE_TOL_3D) } /// `SmSurface::IsDegenerateCurve` (the base body, on a copy of the B-spline as an @@ -1415,11 +1423,11 @@ impl BSplineSurface { // When asked, expand the normal box by the zone tolerance if req.expand_by_zone_tol { if let Some(nb) = normal_box.as_mut() { - nb.expand_absolute(owner_zone_tolerance(self)); + nb.expand_absolute(owner_zone_tolerance(req)); } // When asked, expand the pPseudoBox box by the zone tolerance if let Some(pb) = pseudo_box.as_mut() { - pb.expand_absolute(owner_zone_tolerance(self)); + pb.expand_absolute(owner_zone_tolerance(req)); } } diff --git a/crates/solidcore/src/query/face/p1.rs b/crates/solidcore/src/query/face/p1.rs index ab8a71a601..864407588c 100644 --- a/crates/solidcore/src/query/face/p1.rs +++ b/crates/solidcore/src/query/face/p1.rs @@ -26,16 +26,18 @@ //! face's own tolerance (what `SmBSplineSurface::CalculateBoundingBox` reads through //! `GetFace()`): arena surfaces hold no owning face, so the B-spline/plane/revolution //! box is computed unexpanded and expanded here, as the override's last step does. -//! `Point3DClassify` builds no surface cache (the Rust point solvers take none, as in -//! `topology::vertexuse` and `intersect::curve_class::p1`), so upstream's temporary -//! switch-off of the cache's point test (SmFace.cpp:12801) has nothing to act on yet: -//! the face-owned surface's solve stops at `surface::surface::p4`'s -//! `UNPORTED(query.face_p2)` `SmFace::PointClassify` stub, a call the C++ never makes -//! here, and that stub's NOTE lists this caller so the switch is added for it when it is -//! wired. `PointClassify` gets -//! no trimmed surface cache (a fresh one is never face-containment-complete, so its -//! cache branch cannot run). `EdgeCurveOnClassify` lifts a 2D curve to a curve on the -//! face's surface only when an upward edge use of the face is found (upstream lifts it +//! `Point3DClassify` builds the surface's trimmed surface cache first +//! (`smsurf_GetSurfaceCache`, SmFace.cpp:12792) and hands it to `PointClassify`, which +//! upstream finds again through `SmCacheMgr::GetObjectCache`: built for a face with +//! loops, its face containment is done, so `PointClassify` classifies through the +//! leaf node containing the point before the vertex and loop tests (`query.face_p1` +//! `gen0160`: C++ FACE from an inside leaf, the vertex test alone gives VERTEX). Its +//! face walk is [`SolidError::Unported`] today (`SmFace handle of a surface's face`, +//! `topology.face_p2`), so every face-owned surface defers there. The Rust point solvers +//! take no cache (as in `topology::vertexuse` and `intersect::curve_class::p1`), so +//! upstream's temporary switch-off of the cache's point test (SmFace.cpp:12801) and the +//! cache check-out have nothing to act on. `EdgeCurveOnClassify` lifts a 2D curve to a +//! curve on the face's surface only when an upward edge use of the face is found (upstream lifts it //! first and ignores `SetAll`'s status); an `SmCrvOnSurf` over a surface other than a //! B-spline is a dispatch gap (`curve.crv_on_surf`). No recursion. @@ -53,6 +55,7 @@ use crate::kernel::Kernel; use crate::nurbs::b_spline_curve::BoundingBoxRequest; use crate::nurbs::b_spline_surface::SurfParamType; use crate::nurbs::b_spline_surface_n_lib::p2::OrientType; +use crate::surface::surface::p1::{make_surface_cache, make_trim_srf_cache}; use crate::surface::surface::p4::NoObject; use crate::surface::surface::{Surface, SurfaceKind}; use crate::surface::volume::Volume; @@ -433,6 +436,23 @@ pub fn point_3d_classify( pc.set_face(Some(EntityRef::Face(f))); pc.set_src_zone_tol_3d(src_zone_tol_3d); + // SmSurfaceCache *pTSC = smsurf_GetSurfaceCache(pSurface): the surface's trimmed + // surface cache (built with its face containment when the face has loops), else + // its surface cache. PointClassify below finds the trimmed one again + // (SmCacheMgr::GetObjectCache) and classifies through its leaf nodes first. + let mut trim_cache = { + let surface = fp2::surface(b, f)? + .ok_or_else(|| SolidError::invalid_input(OP_POINT, "face without surface"))?; + match make_trim_srf_cache(k, surface, None::) { + Ok(c) => Some(c), + Err(e) if e.is_unported() => return Err(e), + Err(_) => { + ignored(make_surface_cache(k, surface, None::))?; + None + } + } + }; + let uv = { let surface = fp2::surface(b, f)? .ok_or_else(|| SolidError::invalid_input(OP_POINT, "face without surface"))?; @@ -496,7 +516,7 @@ pub fn point_3d_classify( tgt_loops_only: opts.tgt_loops_only, tgt_loops: opts.tgt_loops, uv_space_okay: opts.uv_space_okay, - trim_cache: None, + trim_cache: trim_cache.as_mut(), }, )?; diff --git a/crates/solidcore/src/query/face/p2.rs b/crates/solidcore/src/query/face/p2.rs index 708720790b..c2a6948e5d 100644 --- a/crates/solidcore/src/query/face/p2.rs +++ b/crates/solidcore/src/query/face/p2.rs @@ -71,6 +71,14 @@ //! - Boundary views use required handle mappings and the same [`ClassGeometry`] //! implementation as part 5. Lazy UV and gap calculations run on a private B-rep //! snapshot; numerical results are preserved while cache-only updates are discarded. +//! - **Vertex/face gaps.** With `bHasBigGaps`, `IntersectVertices` reads each vertex's +//! `GetVertexuseOfFace(pFace)->GetVertexFaceGap()` length. [`curve_on_classify`] +//! computes them from the B-rep (on one private snapshot per call, which keeps the +//! gap caches across both passes) for the face's vertices before the vertex +//! intersections, and answers [`ClassGeometry::vertex_face_gap`] for them around the +//! caller's [`ClassGeometry`]; a gap error therefore surfaces before the earlier +//! vertices are intersected (upstream: a null dereference or the same unported +//! callee). A vertex without a face use is unresolved (upstream dereferences null). //! - No recursion. use crate::curve::curve::Curve; @@ -626,6 +634,70 @@ pub trait CurveOnClassifyObjects: ClassGeometry { } } +/// `pVertex->GetVertexuseOfFace(pFace)->GetVertexFaceGap()->GetLength()`, 0 when the +/// vertex use has no vertex/face gap (`IntersectVertices`' `dMaxGap3d`), on the B-rep +/// snapshot `b` (which keeps the gap cache `GetVertexFaceGap` fills). `None` for a +/// vertex without a use on a face (upstream dereferences the null vertex use; part 5 +/// reports the unresolved gap as invalid input). +fn vertex_face_gap_length(k: &Kernel, b: &mut Brep, v: VertexId, f: FaceId) -> Result> { + let Some(vu) = crate::topology::vertex::vertexuse_of_face(b, v, Some(f))? else { + return Ok(None); + }; + Ok(Some( + crate::topology::vertexuse::vertex_face_gap(k, b, vu, false)?.map_or(0.0, |g| g.gap_3d), + )) +} + +/// The caller's [`ClassGeometry`] answering [`ClassGeometry::vertex_face_gap`] for the +/// vertices `IntersectVertices` intersects (their gaps computed from the B-rep, as +/// upstream's vertex pointers reach them); everything else is the caller's. +/// +/// Every [`ClassGeometry`] method must be forwarded to `inner` here, including any added +/// later: a trait default would otherwise silently replace the inner adapter's override. +struct WithVertexFaceGaps<'g, G, O> { + inner: &'g G, + gaps: Vec<(O, Option)>, +} + +impl, O: ClassObject> ClassGeometry for WithVertexFaceGaps<'_, G, O> { + fn curve(&self, o: O) -> Option<&Curve> { + self.inner.curve(o) + } + fn surface(&self, o: O) -> Option<&Surface> { + self.inner.surface(o) + } + fn volume(&self, o: O) -> Option<&crate::surface::volume::Volume> { + self.inner.volume(o) + } + fn point(&self, o: O) -> Option { + self.inner.point(o) + } + fn classification_vertex_point(&self, vertex: O) -> Option { + self.inner.classification_vertex_point(vertex) + } + fn topology_zone_tol_3d(&self, object: O) -> Option { + self.inner.topology_zone_tol_3d(object) + } + fn vertex_face_gap(&self, vertex: O, surface: &Surface, face: Option) -> Option { + match self.gaps.iter().find(|(h, _)| *h == vertex) { + Some((_, gap)) => *gap, + None => self.inner.vertex_face_gap(vertex, surface, face), + } + } + fn topology_entity(&self, object: O) -> Option<(&Brep, EntityRef)> { + self.inner.topology_entity(object) + } + fn topology_handle(&self, entity: EntityRef) -> Option { + self.inner.topology_handle(entity) + } + fn owner_of( + &self, + owner: crate::geom::object::ObjectRef, + ) -> Option { + self.inner.owner_of(owner) + } +} + /// Resolve stored edge data and its optional gap on this face. The snapshot /// retains lazy gap-cache updates without mutating the caller's borrowed B-rep. fn boundary_edge( @@ -754,6 +826,8 @@ pub fn curve_on_classify( reset_classification(k, cc)?; cc.set_face(Some(objects.face(f))); let mut modified = false; + // (the vertex/face gap caches GetVertexFaceGap fills: a private B-rep snapshot) + let mut gap_snapshot: Option = None; // for two iterations to allow the surf/surf/surf intersection heuristic a chance to // update curve and edge shapes. @@ -794,11 +868,31 @@ pub fn curve_on_classify( .surface .as_ref() .ok_or_else(|| null(OP_CURVE, "face without surface"))?; + // bHasBigGaps: each vertex's GetVertexuseOfFace(pFace)->GetVertexFaceGap() + // (pFace is this face, set on the classification above) + let gaps = if has_big_gaps { + let snapshot = gap_snapshot.get_or_insert_with(|| b.clone()); + vertices + .iter() + .map(|&v| { + Ok(( + objects.vertex(v), + vertex_face_gap_length(k, snapshot, v, f)?, + )) + }) + .collect::>>()? + } else { + Vec::new() + }; let vertices = vertices .iter() .map(|&v| objects.vertex(v)) .collect::>(); - cc.intersect_vertices(k, objects, &vertices, surface, has_big_gaps, true)?; + let geometry = WithVertexFaceGaps { + inner: objects, + gaps, + }; + cc.intersect_vertices(k, &geometry, &vertices, surface, has_big_gaps, true)?; // 2nd intersect face edges with the curve let edges = face_edges(b, f, cc.tgt_loops_only(), Some(&tgt_loops))?; diff --git a/crates/solidcore/src/query/face/p3.rs b/crates/solidcore/src/query/face/p3.rs index 1e6a60540a..8aa34b1f46 100644 --- a/crates/solidcore/src/query/face/p3.rs +++ b/crates/solidcore/src/query/face/p3.rs @@ -279,6 +279,7 @@ fn surface_is_planar(k: &Kernel, b: &Brep, f: FaceId, s: &Surface, tol_3d: f64) pseudo_guess: None, polar_guess: None, expand_by_zone_tol: false, + expand_tolerance: None, }; let pseudo = ignored(bs.calculate_bounding_box(k, &s.natural_uv_domain(), &req))? .and_then(|r| r.pseudo) diff --git a/crates/solidcore/src/surface/cone/p3.rs b/crates/solidcore/src/surface/cone/p3.rs index c242cb038b..7fd191fd1a 100644 --- a/crates/solidcore/src/surface/cone/p3.rs +++ b/crates/solidcore/src/surface/cone/p3.rs @@ -46,6 +46,7 @@ use crate::kernel::Kernel; use crate::math::{max2, min2, EFF_ZERO, EFF_ZERO_SQRT, PI, UNDEF_DOUBLE}; use crate::nurbs::b_spline_curve::BSplineCurve; use crate::nurbs::b_spline_surface::{BoundingBoxRequest, LocationType}; +use crate::surface::plane::p2::PlaneOwner; use crate::surface::plane::Plane; use crate::surface::surf_of_extrusion::SurfOfExtrusion; use crate::surface::surf_of_revolution::p3::Revolved; @@ -287,23 +288,40 @@ impl Cone { plane: &Plane, other_domain: &Extent2d, approx_tol: Option, + ) -> Result<(bool, SurfaceIntersection)> { + self.intersect_with_plane_owned( + k, + domain, + plane, + PlaneOwner::NoFace, + other_domain, + approx_tol, + ) + } + + /// [`Cone::intersect_with_plane`] with the plane's owner: its box is expanded by the + /// tolerance of the plane's face (`GetFace()->GetTolerance()`), else by + /// `SM_ZONE_TOL_3D`. + /// + /// # Errors + /// + /// Those of [`Cone::intersect_with_plane`]; [`SolidError::Unported`] + /// (`SmFace::GetTolerance`) for a plane with a face whose tolerance is not cached. + pub(crate) fn intersect_with_plane_owned( + &self, + k: &Kernel, + domain: &Extent2d, + plane: &Plane, + plane_owner: PlaneOwner, + other_domain: &Extent2d, + approx_tol: Option, ) -> Result<(bool, SurfaceIntersection)> { let mut out = SurfaceIntersection::default(); let tol = approx_tol.map_or(EFF_ZERO_SQRT, |t| approx_tol3d(k, t)); let box1 = bbox(k, self, domain)?; - let mut box2 = plane - .base_with_nurb()? - .calculate_bounding_box( - k, - other_domain, - &BoundingBoxRequest { - normal: true, - expand_by_zone_tol: true, - ..BoundingBoxRequest::default() - }, - )? - .normal - .ok_or_else(|| SolidError::sm_err(OP))?; + // crPlane.CalculateBoundingBox: expanded by its face's tolerance (no face: + // SM_ZONE_TOL_3D) + let mut box2 = plane.bounding_boxes(k, other_domain, plane_owner)?.0; box2.expand_absolute(tol); if box1.are_disjoint(&box2, 0.0) { return Ok((false, out)); diff --git a/crates/solidcore/src/surface/offset_surface.rs b/crates/solidcore/src/surface/offset_surface.rs index 17a1ac61ca..18ceb79429 100644 --- a/crates/solidcore/src/surface/offset_surface.rs +++ b/crates/solidcore/src/surface/offset_surface.rs @@ -713,8 +713,9 @@ impl OffsetSurface { /// /// Those of the base surface's [`Surface::calculate_bounding_box`] and of /// [`Surface::base_calculate_bounding_box`] (an owned base reaches this offset's face - /// through its owner, so a face-owned B-spline-kind base's expanded box returns - /// `SmFace::GetTolerance`'s [`SolidError::Unported`], as upstream asks that face). + /// through its owner, so a face-owned B-spline-kind base's box is expanded by the + /// face tolerance passed down to it, [`Surface::owner_zone_tol_3d`], as upstream asks + /// that face; without it, `SmFace::GetTolerance`'s [`SolidError::Unported`]). pub fn calculate_bounding_box( &self, k: &Kernel, diff --git a/crates/solidcore/src/surface/plane/p2.rs b/crates/solidcore/src/surface/plane/p2.rs index 85664c2bce..387a1c803f 100644 --- a/crates/solidcore/src/surface/plane/p2.rs +++ b/crates/solidcore/src/surface/plane/p2.rs @@ -29,8 +29,9 @@ //! tolerance of the plane's face ([`Surface::face`], `SmSurface::GetFace`), else //! `SM_ZONE_TOL_3D`. A bare [`Plane`] has no owner (the owner lives on [`Surface`]), so //! it uses `SM_ZONE_TOL_3D`; through [`Surface::global_surface_intersect`] a plane with -//! a face returns [`SolidError::Unported`] there (`SmFace::GetTolerance`, unit -//! `topology.face_p2`). +//! a face uses the face tolerance its B-rep cached on the surface +//! ([`Surface::owner_zone_tol_3d`]), and returns [`SolidError::Unported`] there +//! without it (`SmFace::GetTolerance`, unit `topology.face_p2`). //! - **NURB.** `IntersectWithPlane`'s parallel case tests whether both planes hold a //! NURB (`m_pNurb`), as upstream; the boxes are computed on a NURB made on a copy //! when the plane has none (upstream makes it in place, a `const_cast`). Because the @@ -112,18 +113,45 @@ impl SolutionObject for CurveHandle { } } -/// The tolerance of a plane's face (`GetFace()->GetTolerance()`, when `owned`: the -/// plane has a face), else `SM_ZONE_TOL_3D` (the `bExpandPosBoxesByZoneTol3d` -/// expansion of `CalculateBoundingBox`). -fn zone_tolerance(owned: bool) -> Result { - if owned { - // UNPORTED(topology.face_p2): SmFace::GetTolerance - return Err(SolidError::unported( +/// Whether a plane has a face (`SmSurface::GetFace`), and that face's tolerance as its +/// B-rep cached it on the [`Surface`] ([`Surface::owner_zone_tol_3d`]). +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum PlaneOwner { + /// No face: the boxes expand by `SM_ZONE_TOL_3D`. + NoFace, + /// A face, with its cached tolerance (`None`: a bare face handle). + Face(Option), +} + +impl PlaneOwner { + /// The owner state of a plane held by `surface`. + /// + /// # Errors + /// + /// Those of [`Surface::face`]. + pub(crate) fn of(surface: &Surface) -> Result { + Ok(if surface.face()?.is_some() { + Self::Face(surface.owner_zone_tol_3d()) + } else { + Self::NoFace + }) + } +} + +/// The tolerance of a plane's face (`GetFace()->GetTolerance()`, when the plane has a +/// face), else `SM_ZONE_TOL_3D` (the `bExpandPosBoxesByZoneTol3d` expansion of +/// `CalculateBoundingBox`). +fn zone_tolerance(owner: PlaneOwner) -> Result { + match owner { + PlaneOwner::NoFace => Ok(ZONE_TOL_3D), + PlaneOwner::Face(Some(tol)) => Ok(tol), + // UNPORTED(topology.face_p2): SmFace::GetTolerance (a bare face handle without + // the tolerance its B-rep caches on the surface) + PlaneOwner::Face(None) => Err(SolidError::unported( "SmFace::GetTolerance", "topology.face_p2", - )); + )), } - Ok(ZONE_TOL_3D) } impl Plane { @@ -480,7 +508,7 @@ impl Plane { &self, k: &Kernel, uv_domain: &Extent2d, - owned: bool, + owner: PlaneOwner, ) -> Result<(Extent3d, PseudoBox)> { let req = BoundingBoxRequest { normal: true, @@ -494,8 +522,8 @@ impl Plane { let mut bbox = b.normal.ok_or(SolidError::null_pointer(OP_PP))?; let mut pbox = b.pseudo.ok_or(SolidError::null_pointer(OP_PP))?; // When asked, expand the normal and pseudo boxes by the zone tolerance - bbox.expand_absolute(zone_tolerance(owned)?); - pbox.expand_absolute(zone_tolerance(owned)?); + bbox.expand_absolute(zone_tolerance(owner)?); + pbox.expand_absolute(zone_tolerance(owner)?); Ok((bbox, pbox)) } @@ -528,11 +556,11 @@ impl Plane { other, other_uv_domain, approx_tol_3d, - [false, false], + [PlaneOwner::NoFace, PlaneOwner::NoFace], ) } - /// [`Plane::intersect_with_plane`] with the planes' ownership (for the zone + /// [`Plane::intersect_with_plane`] with the planes' owners (for the zone /// tolerance). #[allow(clippy::too_many_lines)] // one upstream function pub(crate) fn intersect_with_plane_owned( @@ -542,7 +570,7 @@ impl Plane { other: &Self, other_uv_domain: &Extent2d, approx_tol_3d: Option, - owned: [bool; 2], + owners: [PlaneOwner; 2], ) -> Result<(bool, SurfaceIntersection)> { // init output let mut out = SurfaceIntersection::default(); @@ -582,8 +610,8 @@ impl Plane { } // get plane1 and plane2 bounding and pseudo boxes - let (bbox1, pbox1) = self.bounding_boxes(k, uv_domain, owned[0])?; - let (bbox2, pbox2) = other.bounding_boxes(k, other_uv_domain, owned[1])?; + let (bbox1, pbox1) = self.bounding_boxes(k, uv_domain, owners[0])?; + let (bbox2, pbox2) = other.bounding_boxes(k, other_uv_domain, owners[1])?; // no work - when bounding or pseudo boxes are disjoint if bbox1.are_disjoint(&bbox2, 0.0) || pbox1.are_disjoint(&pbox2, None) { @@ -667,7 +695,7 @@ impl Plane { // get size of bounded ThisPlane if this_bounded { - (bbox1, pbox1) = self.bounding_boxes(k, uv_domain, owned[0])?; + (bbox1, pbox1) = self.bounding_boxes(k, uv_domain, owners[0])?; let radius1 = bbox1.sphere_bound().radius; bbox1.expand_absolute(tol / 2.0); pbox1.expand_absolute(tol / 2.0); @@ -676,7 +704,7 @@ impl Plane { // get size of bounded OtherPlane if other_bounded { - (bbox2, pbox2) = other.bounding_boxes(k, other_uv_domain, owned[1])?; + (bbox2, pbox2) = other.bounding_boxes(k, other_uv_domain, owners[1])?; let radius2 = bbox2.sphere_bound().radius; bbox2.expand_absolute(tol / 2.0); pbox2.expand_absolute(tol / 2.0); @@ -1374,28 +1402,30 @@ pub(crate) fn global_surface_intersect( match other.kind() { // plane/plane special case intersection SurfaceKind::Plane(other_plane) => { - // (whether each plane has a face: SmSurface::GetFace) - let owned = [this.face()?.is_some(), other.face()?.is_some()]; + // (whether each plane has a face: SmSurface::GetFace, and its tolerance) + let owners = [PlaneOwner::of(this)?, PlaneOwner::of(other)?]; (need_nurb_intersection, out) = plane.intersect_with_plane_owned( k, uv_domain, other_plane, other_uv_domain, approx_tol_3d, - owned, + owners, )?; } // cone/plane special case intersection (SmCone_TYPE and SmCylinder_TYPE): // SmCone::IntersectWithPlane, which hides the surface of revolution's; the // orders and the UV curves are swapped SurfaceKind::Cone(cone) => { - // the plane's boxes take its face's tolerance (taken as without owner - // there; see the module header) - if this.face()?.is_some() { - zone_tolerance(true)?; - } - let (need, mut x) = - cone.intersect_with_plane(k, other_uv_domain, plane, uv_domain, approx_tol_3d)?; + // the plane's boxes take its face's tolerance + let (need, mut x) = cone.intersect_with_plane_owned( + k, + other_uv_domain, + plane, + PlaneOwner::of(this)?, + uv_domain, + approx_tol_3d, + )?; core::mem::swap(&mut x.surface1_uv_curves, &mut x.surface2_uv_curves); (need_nurb_intersection, out) = (need, x); } @@ -1403,12 +1433,14 @@ pub(crate) fn global_surface_intersect( // curves are swapped (the surface of revolution's edge flags and angle // tolerance are not used by it) SurfaceKind::SurfOfRevolution(rev) => { - // the plane's boxes take its face's tolerance (taken as without owner - // there; see the module header) - if this.face()?.is_some() { - zone_tolerance(true)?; - } - let (need, mut x) = rev.intersect_with_plane(k, plane, uv_domain, approx_tol_3d)?; + // the plane's boxes take its face's tolerance + let (need, mut x) = rev.intersect_with_plane_owned( + k, + plane, + PlaneOwner::of(this)?, + uv_domain, + approx_tol_3d, + )?; core::mem::swap(&mut x.surface1_uv_curves, &mut x.surface2_uv_curves); (need_nurb_intersection, out) = (need, x); } diff --git a/crates/solidcore/src/surface/sphere.rs b/crates/solidcore/src/surface/sphere.rs index f329adebe1..10354e1b84 100644 --- a/crates/solidcore/src/surface/sphere.rs +++ b/crates/solidcore/src/surface/sphere.rs @@ -952,7 +952,11 @@ impl Sphere { // Do a little bounding box computation to see if things really intersect. if plane.is_bounded() { // get min distance from sphereOrigin to planeBoundingBox - let (bbox, _) = plane.bounding_boxes(k, other_uv_domain, false)?; + let (bbox, _) = plane.bounding_boxes( + k, + other_uv_domain, + crate::surface::plane::p2::PlaneOwner::NoFace, + )?; let sph_point = Extent3d::from_points(sph_origin, sph_origin); let dist = bbox.min_distance(&sph_point); diff --git a/crates/solidcore/src/surface/surf_of_revolution/p1.rs b/crates/solidcore/src/surface/surf_of_revolution/p1.rs index 6c65d7b34b..64f0f6b25e 100644 --- a/crates/solidcore/src/surface/surf_of_revolution/p1.rs +++ b/crates/solidcore/src/surface/surf_of_revolution/p1.rs @@ -78,8 +78,12 @@ //! - **`IntersectWithPlane`** (its body below the plane box ported by //! `surface.plane_p2`) takes a bare [`Plane`], which has no owner: its pseudo box is //! expanded by `SM_ZONE_TOL_3D` (upstream: the zone tolerance of the plane's face or -//! B-rep, `SmSurface::GetFace`, `NULL` for a plane without one). It takes a -//! [`Kernel`] for the curve operations. +//! B-rep, `SmSurface::GetFace`, `NULL` for a plane without one). Through +//! `Surface::global_surface_intersect` the plane's owner is passed +//! (`intersect_with_plane_owned`): a face-owned plane expands by the face tolerance +//! its B-rep cached on the surface, and returns [`SolidError::Unported`] +//! (`SmFace::GetTolerance`) without it. It takes a [`Kernel`] for the curve +//! operations. //! - **`sm_TestForIsoCircles`** returns its outputs in [`IsoCircles`]; the ones //! upstream leaves unwritten (no non-degenerate iso curve, no second plane) are //! `None`. With parallel iso planes `smgu_IntersectTwoPlanes` writes no line, so the @@ -108,6 +112,7 @@ use crate::nurbs::b_spline_curve::{BSplineCurve, BoundingBoxRequest as CurveBoxR use crate::nurbs::b_spline_surface::{ BSplineSurface, BSplineSurfaceForm, IsoCurveOptions, SurfParamType, }; +use crate::surface::plane::p2::PlaneOwner; use crate::surface::plane::Plane; use crate::surface::surface::p2::{ scale_step_angular_derivatives, SurfaceIntersection, TsectCurveType, SURF_OF_REVOLUTION_TYPE, @@ -1430,13 +1435,37 @@ impl SurfOfRevolution { /// Those of the angle measures, the generator transform, property analysis and /// evaluation and of the iso curves (upstream `SER`); `SM_ERR_NULL_POINTER` without /// generator. - #[allow(clippy::too_many_lines)] // one upstream function pub fn intersect_with_plane( &self, k: &Kernel, plane: &Plane, other_uv_domain: &Extent2d, approx_tol_3d: Option, + ) -> Result<(bool, SurfaceIntersection)> { + self.intersect_with_plane_owned( + k, + plane, + PlaneOwner::NoFace, + other_uv_domain, + approx_tol_3d, + ) + } + + /// [`SurfOfRevolution::intersect_with_plane`] with the plane's owner: its box is + /// expanded by the tolerance of the plane's face (`GetFace()->GetTolerance()`). + /// + /// # Errors + /// + /// Those of [`SurfOfRevolution::intersect_with_plane`]; [`SolidError::Unported`] + /// (`SmFace::GetTolerance`) for a plane with a face whose tolerance is not cached. + #[allow(clippy::too_many_lines)] // one upstream function + pub(crate) fn intersect_with_plane_owned( + &self, + k: &Kernel, + plane: &Plane, + plane_owner: PlaneOwner, + other_uv_domain: &Extent2d, + approx_tol_3d: Option, ) -> Result<(bool, SurfaceIntersection)> { let mut out = SurfaceIntersection::default(); @@ -1460,9 +1489,10 @@ impl SurfOfRevolution { } // get plane bounding box increased by tolerance (status ignored) - let mut plane_pbox = ignored(plane.bounding_boxes(k, &plane.natural_uv_domain(), false))? - .map(|(_, p)| p) - .unwrap_or_default(); + let mut plane_pbox = + ignored(plane.bounding_boxes(k, &plane.natural_uv_domain(), plane_owner))? + .map(|(_, p)| p) + .unwrap_or_default(); plane_pbox.expand_absolute(tol); let mut types: Vec = Vec::new(); diff --git a/crates/solidcore/src/surface/surface/p1.rs b/crates/solidcore/src/surface/surface/p1.rs index 1238a9ce9d..608d2b8b99 100644 --- a/crates/solidcore/src/surface/surface/p1.rs +++ b/crates/solidcore/src/surface/surface/p1.rs @@ -91,8 +91,10 @@ //! `Unitize` returns `TRUE`. //! - **Unported callees.** `ApproximateSize` of kinds other than B-splines needs //! `SmIsoCurve` over any surface (the ported iso curve holds a B-spline), the zone -//! tolerance of a surface with a face needs `SmFace::GetTolerance` -//! (`topology.face_p2`): those calls return [`SolidError::Unported`]. Every tree build +//! tolerance of a surface with a face is the one its B-rep cached on it +//! ([`Surface::owner_zone_tol_3d`]) and needs `SmFace::GetTolerance` +//! (`topology.face_p2`) for a bare face handle without it: those calls return +//! [`SolidError::Unported`]. Every tree build //! needs `SmSurface::CalculateContinuities` (`surface.surface_p6`), whose iso curves //! exist for the B-spline, plane and revolution kinds. //! [`map_to_2d_on_curve`] ports the UV-curve and the general 3D branch of @@ -608,19 +610,17 @@ impl StepSurface { // ---- tolerances of surfaces (SmTol co-ports) ---------------------------------------------- /// `SmTol::GetZoneTol3d(const SmObject*)` of a surface: its face's tolerance when it -/// has a face, else the system zone tolerance. +/// has a face (the tolerance the B-rep cached on the surface, +/// [`Surface::owner_zone_tol_3d`]), else the system zone tolerance. /// /// # Errors /// -/// Those of [`Surface::face`]; [`SolidError::Unported`]: `SmFace::GetTolerance` -/// (`topology.face_p2`) for a surface with a face. +/// Those of [`Surface::face_zone_tol_3d`]: [`SolidError::Unported`] +/// (`SmFace::GetTolerance`, `topology.face_p2`) for a surface with a face whose +/// tolerance was not cached on it. pub fn zone_tol_3d(k: &Kernel, surface: &Surface) -> Result { - match surface.face()? { - // UNPORTED(topology.face_p2): SmFace::GetTolerance - Some(_) => Err(SolidError::unported( - "SmFace::GetTolerance", - "topology.face_p2", - )), + match surface.face_zone_tol_3d()? { + Some(tol) => Ok(tol), None => Ok(kernel_zone_tol_3d(k, SmallTopoType::NormalSize)), } } @@ -1048,12 +1048,19 @@ impl Surface { } } - /// Sets the owner and returns the previous one (`SetOwner`). + /// Sets the owner and returns the previous one (`SetOwner`). A different owner + /// drops the cached face tolerance ([`Surface::owner_zone_tol_3d`]): the B-rep + /// records the new face's ([`Surface::set_owner_zone_tol_3d`]). pub fn set_owner(&mut self, new_owner: Option) -> Option { // (notify when pNewOwner is NULL or a Face: the object layer's notify) let old = core::mem::replace(self.owner_mut(), new_owner); - // the bases this surface owns reach the face through it - self.update_owned_bases(); + if old == new_owner { + // the bases this surface owns reach the face through it + self.update_owned_bases(); + } else { + // (also passes the face down to the bases this surface owns) + self.set_owner_zone_tol_3d(None); + } old } @@ -1541,7 +1548,8 @@ impl Surface { /// /// Those of the surface cache and of the polar box; [`SolidError::Unported`]: /// `SmFace::GetTolerance` (`topology.face_p2`) when a B-spline or plane with a - /// face expands its boxes. + /// face expands its boxes and the face's tolerance is not cached on the surface + /// ([`Surface::owner_zone_tol_3d`]). pub fn calculate_bounding_box( &self, k: &Kernel, @@ -1556,15 +1564,14 @@ impl Surface { | SurfaceKind::Plane(_) | SurfaceKind::SurfOfRevolution(_) => { // the B-spline override expands by its face's tolerance (GetFace()): - // the owner is held here, not by the B-spline - if expand_pos_boxes_by_zone_tol_3d && (want[0] || want[1]) && self.face()?.is_some() - { - // UNPORTED(topology.face_p2): SmFace::GetTolerance - return Err(SolidError::unported( - "SmFace::GetTolerance", - "topology.face_p2", - )); - } + // the owner is held here, not by the B-spline, so the face's cached + // tolerance is passed down (the GetBrep() arm is reached only without + // a face, where a surface's GetBrep is null: SM_ZONE_TOL_3D) + let expand_tolerance = if expand_pos_boxes_by_zone_tol_3d && (want[0] || want[1]) { + self.face_zone_tol_3d()? + } else { + None + }; let req = crate::nurbs::b_spline_surface::BoundingBoxRequest { normal: want[0], pseudo: want[1], @@ -1572,6 +1579,7 @@ impl Surface { pseudo_guess: None, polar_guess: None, expand_by_zone_tol: expand_pos_boxes_by_zone_tol_3d, + expand_tolerance, }; let r = match self.kind() { SurfaceKind::BSpline(s) => s.calculate_bounding_box(k, uv_domain, &req)?, diff --git a/crates/solidcore/src/surface/surface/p2.rs b/crates/solidcore/src/surface/surface/p2.rs index cb17698332..c35d774e64 100644 --- a/crates/solidcore/src/surface/surface/p2.rs +++ b/crates/solidcore/src/surface/surface/p2.rs @@ -63,6 +63,12 @@ //! | [`IsoCurveLengthIntegrand`] | `SmIsoCurveLengthIFEO` (constructor) | //! //! Deviations: +//! - **Owner tolerance.** A surface holds only an owner handle, never its B-rep, so the +//! face tolerance upstream reads through `GetFace()->GetTolerance()` is cached on the +//! surface by the B-rep ([`Surface::owner_zone_tol_3d`], kept in step where a face +//! record's surface or tolerance changes) together with the face it belongs to; an +//! owner change drops it, the copy constructor resets it and a bare face handle +//! without it returns [`SolidError::Unported`] (`SmFace::GetTolerance`). //! - Evaluation dispatch and copies check all owned surface, curve and volume branches against a //! 64-level depth bound before invoking recursive copies. The surface evaluator //! also bounds its wrapper traversal to 64 levels and retains B-spline output @@ -292,6 +298,12 @@ pub struct Surface { /// surface's own owner chain. The owning surface holds this surface by value, so it /// cannot walk up; the owner keeps this in step ([`Surface::set_owner`]). owner_chain_face: Option, + /// The stored zone tolerance (`SmFace::m_sZoneTol3d`, [`crate::topology::Face`]'s + /// `tolerance`) of the face this surface reaches ([`Surface::reached_face`]), with + /// that face: the B-rep keeps it in step when it attaches a surface to a face or sets + /// a face tolerance. Valid only while the recorded face is still the one reached, so + /// an owner change can never leave another face's tolerance behind. + owner_zone_tol_3d: Option<(ObjectRef, f64)>, } /// The copy constructor: copies the geometry, resets the owner (an owning offset in it @@ -302,6 +314,7 @@ impl Clone for Surface { kind: self.kind.clone(), owner: None, owner_chain_face: None, + owner_zone_tol_3d: None, }; s.update_owned_bases(); s @@ -433,6 +446,7 @@ impl Surface { kind, owner: self.owner, owner_chain_face: self.owner_chain_face, + owner_zone_tol_3d: self.owner_zone_tol_3d, }) } } @@ -545,6 +559,7 @@ impl Surface { kind, owner: None, owner_chain_face: None, + owner_zone_tol_3d: None, } } @@ -588,6 +603,8 @@ impl Surface { pub(crate) fn set_surface_owner(&mut self, owner: ObjectRef, chain_face: Option) { self.owner = Some(owner); self.owner_chain_face = chain_face; + // a new owner chain: the cached face tolerance belonged to the old one + self.owner_zone_tol_3d = None; self.update_owned_bases(); } @@ -597,6 +614,7 @@ impl Surface { pub(crate) fn restore_owner_state(&mut self, from: &Self) { self.owner = from.owner; self.owner_chain_face = from.owner_chain_face; + self.owner_zone_tol_3d = from.owner_zone_tol_3d; self.update_owned_bases(); } @@ -614,6 +632,8 @@ impl Surface { /// `MAX_SURFACE_NESTING` at construction but is walked without recursion. pub(crate) fn update_owned_bases(&mut self) { let face = self.reached_face(); + // the bases reach the same face, so they carry its cached tolerance too + let tol = face.zip(self.owner_zone_tol_3d()); let mut cur = self; while let SurfaceKind::OffsetSurface(off) = &mut cur.kind { let Some(base) = off.owned_base_mut() else { @@ -621,10 +641,56 @@ impl Surface { }; base.owner = Some(crate::surface::offset_surface::BASE_OWNER); base.owner_chain_face = face; + base.owner_zone_tol_3d = tol; cur = base; } } + /// The zone tolerance of the owning face (`GetFace()->GetTolerance()`, + /// `SmFace::m_sZoneTol3d`), when the B-rep has recorded it on this surface + /// ([`Surface::set_owner_zone_tol_3d`]); `None` for a surface without face, or for a + /// bare owner handle whose B-rep never recorded the face's tolerance. + #[must_use] + pub fn owner_zone_tol_3d(&self) -> Option { + match (self.reached_face(), self.owner_zone_tol_3d) { + (Some(face), Some((cached, tol))) if face == cached => Some(tol), + _ => None, + } + } + + /// Records the zone tolerance of the face this surface reaches (`None` clears it), + /// and passes it down the bases this surface owns. Called by the B-rep wherever a + /// face record's surface or tolerance changes; a surface reaching no face records + /// nothing. + pub fn set_owner_zone_tol_3d(&mut self, tol: Option) { + self.owner_zone_tol_3d = tol.and_then(|t| self.reached_face().map(|f| (f, t))); + self.update_owned_bases(); + } + + /// `GetFace() ? GetFace()->GetTolerance() : none`: `Ok(None)` for a surface without + /// face, the cached face tolerance ([`Surface::owner_zone_tol_3d`]) for one with a + /// face. + /// + /// # Errors + /// + /// Those of [`Surface::face`]; [`SolidError::Unported`] (`SmFace::GetTolerance`, + /// `topology.face_p2`) for a surface with a face whose B-rep has not recorded the + /// face's tolerance on it (a bare owner handle: the surface cannot reach the B-rep). + pub fn face_zone_tol_3d(&self) -> Result> { + if self.face()?.is_none() { + return Ok(None); + } + match self.owner_zone_tol_3d() { + Some(tol) => Ok(Some(tol)), + // UNPORTED(topology.face_p2): SmFace::GetTolerance (a bare face handle + // without the tolerance its B-rep records here) + None => Err(SolidError::unported( + "SmFace::GetTolerance", + "topology.face_p2", + )), + } + } + /// `GetFace()`'s result: the owner when it is a face, the face reached through a /// surface owner, else none. pub(crate) fn reached_face(&self) -> Option { @@ -1184,7 +1250,10 @@ impl Surface { plane, other_uv_domain, approx_tol_3d, - [self.face()?.is_some(), false], + [ + crate::surface::plane::p2::PlaneOwner::of(self)?, + crate::surface::plane::p2::PlaneOwner::NoFace, + ], ), SurfaceKind::Cone(s) => { s.intersect_with_plane(k, uv_domain, plane, other_uv_domain, approx_tol_3d) @@ -5258,6 +5327,187 @@ mod curve_on_classify_tests { } } +#[cfg(test)] +mod owner_zone_tol_tests { + use super::*; + use crate::geom::assert_array::labels::FACE_TYPE; + use crate::geom::attribute::ObjectKey; + use crate::surface::offset_surface::OffsetSurface; + use crate::topology::{Brep, Face, FaceId}; + + fn face_ref(key: u64) -> ObjectRef { + ObjectRef { + type_code: FACE_TYPE, + key: ObjectKey(key), + } + } + + fn plane() -> Surface { + Surface::from(Plane::from_normal(Point3::ZERO, Vec3::Z)) + } + + /// A B-rep with one face of tolerance `tol` holding `surface`. + fn brep_with(surface: Surface, tol: f64) -> (Brep, FaceId) { + let mut b = Brep::new(); + let shell = b.add_shell().unwrap(); + let f = b + .add_face_record(Face { + surface: Some(surface), + tolerance: tol, + ..Face::new(shell) + }) + .unwrap(); + (b, f) + } + + fn surface_of(b: &Brep, f: FaceId) -> &Surface { + b.face(f).unwrap().surface.as_ref().unwrap() + } + + #[test] + fn add_face_record_caches_the_face_tolerance() { + let mut s = plane(); + s.set_owner(Some(face_ref(1))); + assert!(s.owner_zone_tol_3d().is_none()); + assert!(s.face_zone_tol_3d().unwrap_err().is_unported()); + let (b, f) = brep_with(s, 0.25); + let s = surface_of(&b, f); + assert_eq!(s.owner_zone_tol_3d(), Some(0.25)); + assert_eq!(s.face_zone_tol_3d().unwrap(), Some(0.25)); + let k = Kernel::default(); + assert_eq!( + crate::surface::surface::p1::zone_tol_3d(&k, s).unwrap(), + 0.25 + ); + } + + #[test] + fn unowned_surface_records_nothing() { + let (b, f) = brep_with(plane(), 0.25); + let s = surface_of(&b, f); + assert_eq!(s.owner_zone_tol_3d(), None); + assert_eq!(s.face_zone_tol_3d().unwrap(), None); + } + + #[test] + fn owner_changes_drop_the_cache() { + let mut s = plane(); + s.set_owner(Some(face_ref(1))); + let (mut b, f) = brep_with(s, 0.25); + let s = b.face_mut(f).unwrap().surface.as_mut().unwrap(); + // the same owner keeps it + s.set_owner(Some(face_ref(1))); + assert_eq!(s.owner_zone_tol_3d(), Some(0.25)); + // another face drops it + s.set_owner(Some(face_ref(2))); + assert_eq!(s.owner_zone_tol_3d(), None); + assert!(s.face_zone_tol_3d().unwrap_err().is_unported()); + // and so does no owner, also when the face comes back + b.sync_face_surface_tolerance(f).unwrap(); + let s = b.face_mut(f).unwrap().surface.as_mut().unwrap(); + assert_eq!(s.owner_zone_tol_3d(), Some(0.25)); + s.set_owner(None); + assert_eq!(s.owner_zone_tol_3d(), None); + s.set_owner(Some(face_ref(2))); + assert_eq!(s.owner_zone_tol_3d(), None); + } + + #[test] + fn direct_owner_writes_cannot_leak_the_cache() { + let mut s = plane(); + s.set_owner(Some(face_ref(1))); + s.set_owner_zone_tol_3d(Some(0.5)); + *s.owner_mut() = Some(face_ref(2)); + assert_eq!(s.owner_zone_tol_3d(), None); + *s.owner_mut() = None; + assert_eq!(s.owner_zone_tol_3d(), None); + } + + #[test] + fn evaluation_copy_keeps_and_clone_resets() { + let mut s = plane(); + s.set_owner(Some(face_ref(1))); + s.set_owner_zone_tol_3d(Some(0.5)); + let e = s.clone_for_evaluation().unwrap(); + assert_eq!(e.owner_zone_tol_3d(), Some(0.5)); + let c = s.clone(); + assert_eq!(c.owner(), None); + assert_eq!(c.owner_zone_tol_3d(), None); + let kept = s.clone_keeping_owner(); + assert_eq!(kept.owner_zone_tol_3d(), Some(0.5)); + } + + #[test] + fn owned_offset_base_reaches_the_cache() { + let k = Kernel::default(); + let mut off = Surface::from(OffsetSurface::new(&k, 1.0, plane(), true, 0).unwrap()); + off.set_owner(Some(face_ref(1))); + let base = |s: &Surface| match s.kind() { + SurfaceKind::OffsetSurface(o) => o.base_surface().owner_zone_tol_3d(), + _ => panic!("not an offset"), + }; + assert_eq!(base(&off), None); + off.set_owner_zone_tol_3d(Some(0.125)); + assert_eq!(off.owner_zone_tol_3d(), Some(0.125)); + assert_eq!(base(&off), Some(0.125)); + off.set_owner(None); + assert_eq!(base(&off), None); + } + + /// The offset paths #307 rewires through the exact snapshot helpers: an owned base + /// keeps the cached face tolerance in an evaluation copy, and a shared base (the same + /// object upstream) keeps its own face's tolerance in `OffsetSurface::copy`. + #[test] + fn offset_snapshots_keep_the_cache() { + let k = Kernel::default(); + let base = |s: &Surface| match s.kind() { + SurfaceKind::OffsetSurface(o) => o.base_surface().owner_zone_tol_3d(), + _ => panic!("not an offset"), + }; + let mut owned = Surface::from(OffsetSurface::new(&k, 1.0, plane(), true, 0).unwrap()); + owned.set_owner(Some(face_ref(1))); + owned.set_owner_zone_tol_3d(Some(0.125)); + let e = owned.clone_for_evaluation().unwrap(); + assert_eq!(e.owner_zone_tol_3d(), Some(0.125)); + assert_eq!(base(&e), Some(0.125)); + + let mut shared_base = plane(); + shared_base.set_owner(Some(face_ref(2))); + shared_base.set_owner_zone_tol_3d(Some(0.75)); + let shared = OffsetSurface::new(&k, 1.0, shared_base, false, 0).unwrap(); + let c = shared.copy(&k).unwrap(); + assert_eq!(c.base_surface().owner_zone_tol_3d(), Some(0.75)); + let e = Surface::from(shared).clone_for_evaluation().unwrap(); + assert_eq!(base(&e), Some(0.75)); + } + + #[test] + fn face_set_tolerance_updates_the_cache() { + let mut s = plane(); + s.set_owner(Some(face_ref(1))); + let (mut b, f) = brep_with(s, 0.25); + crate::topology::face::p3::set_tolerance(&mut b, f, 0.5, true, false).unwrap(); + assert_eq!(surface_of(&b, f).owner_zone_tol_3d(), Some(0.5)); + // only if larger: unchanged + crate::topology::face::p3::set_tolerance(&mut b, f, 0.1, true, false).unwrap(); + assert_eq!(surface_of(&b, f).owner_zone_tol_3d(), Some(0.5)); + crate::topology::face::p3::set_tolerance(&mut b, f, 0.1, false, false).unwrap(); + assert_eq!(surface_of(&b, f).owner_zone_tol_3d(), Some(0.1)); + } + + #[test] + fn rescue_surface_owner_restores_the_cache() { + let mut s = plane(); + s.set_owner(Some(face_ref(1))); + s.set_owner_zone_tol_3d(Some(0.5)); + let rescue = crate::topology::face::p2::RescueSurfaceOwner::new(&s); + s.set_owner(None); + assert_eq!(s.owner_zone_tol_3d(), None); + rescue.restore(&mut s); + assert_eq!(s.owner_zone_tol_3d(), Some(0.5)); + } +} + #[cfg(test)] mod kernel_dispatch_tests { use super::*; diff --git a/crates/solidcore/src/surface/surface/p3.rs b/crates/solidcore/src/surface/surface/p3.rs index 6af77e2f85..df994571db 100644 --- a/crates/solidcore/src/surface/surface/p3.rs +++ b/crates/solidcore/src/surface/surface/p3.rs @@ -34,10 +34,11 @@ //! outputs keep their previous values (initially undefined); the port does the same //! and still propagates [`SolidError::Unported`]. //! - **Tolerance of a surface.** `SmTol::GetApproxTol3d(this)` is the system value for a -//! surface without an owner; for an owned surface it needs the face tolerance -//! (`topology.face_p2`) and returns [`SolidError::Unported`], unless the caller that -//! has the face passes it ([`Surface::get_singularities_of_face`], used by the face -//! props). +//! surface without an owner; for an owned surface it is half the face tolerance the +//! caller that has the face passes ([`Surface::get_singularities_of_face`], used by +//! the face props), else the one the B-rep cached on the surface +//! ([`Surface::owner_zone_tol_3d`]); a bare face handle without it returns +//! [`SolidError::Unported`] (`topology.face_p2`). //! - **Unported callees** (return [`SolidError::Unported`]): `SmFace::RemoveUVTrimCurves` //! (`topology.face_p1`) when the surface is owned by a face. @@ -99,7 +100,9 @@ const fn side(from_left: bool) -> Side { /// A [`Surface`] holds only an owner handle, not the B-rep, so the face's tolerance /// (`SmFace::GetTolerance`, `topology.face_p2`) is passed by a caller that has the face: /// `face_tolerance` is `Some` for a surface known to be that face's own surface, and the -/// zone tolerance of the face is then used whether or not the handle is set. +/// zone tolerance of the face is then used whether or not the handle is set. Otherwise +/// a surface with a face uses the tolerance its B-rep cached on it +/// ([`Surface::owner_zone_tol_3d`]). fn surface_approx_tol_3d( k: &Kernel, surface: &Surface, @@ -111,14 +114,12 @@ fn surface_approx_tol_3d( } // SmTol::GetApproxTol3d(this) = .5 * GetZoneTol3d(this): GetFace() ? its tolerance // : the system's (a surface owned by an offset with no face uses the latter) - if surface.face()?.is_some() { - // UNPORTED(topology.face_p2): SmFace::GetTolerance of the owner, for the callers - // that cannot pass it (a surface does not reach its B-rep; justified: plan/findings.md, - // heal.face_props, "Why UNPORTED(topology.face_p2) stays"); the face props pass it through `get_singularities_of_face` - return Err(SolidError::unported( - "SmFace::GetTolerance", - "topology.face_p2", - )); + // the face's tolerance cached on the surface by its B-rep; a bare face handle + // without it stays UNPORTED(topology.face_p2): SmFace::GetTolerance + // (`Surface::face_zone_tol_3d`); the face props pass it through + // `get_singularities_of_face` + if let Some(zone) = surface.face_zone_tol_3d()? { + return Ok(approx_tol3d(k, 0.5 * zone)); } Ok(approx_tol_3d(k, SmallTopoType::NormalSize)) } diff --git a/crates/solidcore/src/surface/surface/p4.rs b/crates/solidcore/src/surface/surface/p4.rs index 4e2cb944ae..7a0480cb71 100644 --- a/crates/solidcore/src/surface/surface/p4.rs +++ b/crates/solidcore/src/surface/surface/p4.rs @@ -61,7 +61,8 @@ //! - **Kernel.** `is_on_seam` and `global_point_solve` (and `global_point_solve_starts`) //! take the [`Kernel`] that the tolerance, drop and cache callees now need. //! - **Unported callees** (return [`SolidError::Unported`]): the zone tolerance of a face's -//! surface (`SmFace::GetTolerance`, `topology.face_p2`), `GlobalPointSolve_1` +//! surface whose B-rep has not cached it on the surface ([`Surface::owner_zone_tol_3d`]; +//! `SmFace::GetTolerance`, `topology.face_p2`), `GlobalPointSolve_1` //! (`surface.surface_point_solve_p3`), the trim containment of a face's surface //! (`SmFace::PointClassify`, `query.face_p2`) and the edge and face tolerances of owned //! curves and surfaces (`topology.edge`, `topology.face_p2`). @@ -1741,12 +1742,12 @@ pub fn classify_curve_surface_xsect_point( curve_tol = curve_radius / 1000.0; } if surface.owner().is_some_and(|o| o.type_code == FACE_TYPE) { - // UNPORTED(topology.face_p2): SmFace::GetTolerance - let _ = surface_tol; - return Err(SolidError::unported( - "SmFace::GetTolerance", - "topology.face_p2", - )); + // pSurfaceOwner->GetTolerance(): the direct owner is the face, so the face the + // surface reaches; its tolerance as cached by the B-rep + surface_tol = surface.owner_zone_tol_3d().ok_or( + // UNPORTED(topology.face_p2): SmFace::GetTolerance (a bare face handle) + SolidError::unported("SmFace::GetTolerance", "topology.face_p2"), + )?; } else if !surface_line { surface_tol = surface_radius / 1000.0; } diff --git a/crates/solidcore/src/surface/surface/p5.rs b/crates/solidcore/src/surface/surface/p5.rs index afe1d7778c..141b11df4a 100644 --- a/crates/solidcore/src/surface/surface/p5.rs +++ b/crates/solidcore/src/surface/surface/p5.rs @@ -51,7 +51,8 @@ //! `CoverCoincidentSurface` leaves `rdMaxDist` at `SM_BIG_DOUBLE` (its first //! statement). //! - **Unported callees** (return [`SolidError::Unported`]): `SmSurface::GetFace`, -//! `SmFace::GetTolerance`, `LocalPointSolve_1`, `sm_GetSurfaceToEvaluate`, the STEP +//! `SmFace::GetTolerance` (for a surface whose B-rep has not cached its face's +//! tolerance on it, [`Surface::owner_zone_tol_3d`]), `LocalPointSolve_1`, `sm_GetSurfaceToEvaluate`, the STEP //! domain conversions and the surface cache. use crate::curve::curve::p1::ApproximateCurveOptions; @@ -1000,7 +1001,8 @@ impl Surface { /// # Errors /// /// Those of the global solve; [`SolidError::Unported`]: `GetFace` and - /// `SmFace::GetTolerance` for an owned surface, `LocalPointSolve_1`. + /// `SmFace::GetTolerance` for an owned surface without the face tolerance cached + /// ([`Surface::owner_zone_tol_3d`]), `LocalPointSolve_1`. #[allow(clippy::too_many_lines)] // one upstream routine pub fn drop_point( &self, @@ -1024,15 +1026,9 @@ impl Surface { let mut answer_in_array = false; // We need a tolerance value. - let dist_tol = if self.face()?.is_some() { - // UNPORTED(topology.face_p2): SmFace::GetTolerance - return Err(SolidError::unported( - "SmFace::GetTolerance", - "topology.face_p2", - )); - } else { - XSECT_TOL_3D - }; + // pFace ? pFace->GetTolerance() : SM_XSECT_TOL_3D (the face's tolerance cached + // on the surface; UNPORTED(topology.face_p2) for a bare face handle) + let dist_tol = self.face_zone_tol_3d()?.unwrap_or(XSECT_TOL_3D); // first try DropPointFast(), to catch special cases. if let Some(fast) = ignored(self.drop_point_fast::( @@ -1250,7 +1246,8 @@ impl Surface { /// # Errors /// /// [`SolidError::Unported`]: `GetFace`/`SmFace::GetTolerance` for an owned surface - /// and the surface caches (`smsurf_GetSurfaceCache`). The status of the UV-guess + /// without the face tolerance cached ([`Surface::owner_zone_tol_3d`]) and the + /// surface caches (`smsurf_GetSurfaceCache`). The status of the UV-guess /// `LocalSurfaceSurfaceIntersect` is ignored, as upstream (no answer). #[allow(clippy::too_many_arguments)] // upstream's argument list pub fn local_surface_surface_intersect_xyz( @@ -1265,17 +1262,9 @@ impl Surface { opt_xyz_guess2: Option, ) -> Result<(bool, Solution)> { // GWC:TODO - upstream's magic numbers for the tolerance - let face_tol = |s: &Self| -> Result { - if s.face()?.is_some() { - // UNPORTED(topology.face_p2): SmFace::GetTolerance - Err(SolidError::unported( - "SmFace::GetTolerance", - "topology.face_p2", - )) - } else { - Ok(1e-05) - } - }; + // GetFace() ? its tolerance (cached on the surface; UNPORTED(topology.face_p2) + // for a bare face handle) : 1e-05 + let face_tol = |s: &Self| -> Result { Ok(s.face_zone_tol_3d()?.unwrap_or(1e-05)) }; let tol1 = face_tol(self)?; let tol2 = face_tol(surface2)?; let tol3 = face_tol(surface3)?; diff --git a/crates/solidcore/src/surface/surface/p7.rs b/crates/solidcore/src/surface/surface/p7.rs index 1ed9d98339..6520b37178 100644 --- a/crates/solidcore/src/surface/surface/p7.rs +++ b/crates/solidcore/src/surface/surface/p7.rs @@ -37,6 +37,10 @@ //! | [`good_drop_result`] | `sm_GoodDropResult` (static) | //! //! Deviations: +//! - **Face tolerance of `IsPlanar`.** The B-spline, revolution and cone boxes are +//! expanded by the face tolerance the B-rep cached on the surface +//! ([`Surface::owner_zone_tol_3d`]). A face-owned surface without it returns +//! [`SolidError::Unported`] (`SmFace::GetTolerance`). //! - **Generic bodies.** The `SmSurface` bodies that only call the evaluation virtuals //! (`Evaluate`, `EvaluatePoint`, `GetNaturalUVDomain`) are generic over //! [`SurfaceEvaluate`], so a caller holding a bare [`BSplineSurface`] (which does not @@ -839,22 +843,25 @@ impl Surface { }) } SurfaceKind::BSpline(s) => { - // the box expansion uses the owning face's tolerance - if self.face()?.is_some() { - // UNPORTED(topology.face_p2): SmFace::GetTolerance - return Err(SolidError::unported( - "SmFace::GetTolerance", - "topology.face_p2", - )); - } - Ok(planar_of(k, s, &self.natural_uv_domain())?.is_planar(tol_3d)) + // the box expansion uses the owning face's tolerance, as cached on the + // surface by its B-rep (UNPORTED(topology.face_p2): SmFace::GetTolerance + // for a bare face handle without it) + let face_tol = self.face_zone_tol_3d()?; + Ok(planar_of(k, s, &self.natural_uv_domain(), face_tol)?.is_planar(tol_3d)) } - // SmSurfOfRevolution inherits SmBSplineSurface::CalculateBoundingBox + // SmSurfOfRevolution (and SmCone) inherit SmBSplineSurface::CalculateBoundingBox: + // the face's cached tolerance, as for the B-spline (UNPORTED(topology.face_p2): + // SmFace::GetTolerance for a bare face handle without it) SurfaceKind::Cone(s) => { - Ok(planar_of(k, s.rev().base(), &self.natural_uv_domain())?.is_planar(tol_3d)) + let face_tol = self.face_zone_tol_3d()?; + Ok( + planar_of(k, s.rev().base(), &self.natural_uv_domain(), face_tol)? + .is_planar(tol_3d), + ) } SurfaceKind::SurfOfRevolution(s) => { - Ok(planar_of(k, s.base(), &self.natural_uv_domain())?.is_planar(tol_3d)) + let face_tol = self.face_zone_tol_3d()?; + Ok(planar_of(k, s.base(), &self.natural_uv_domain(), face_tol)?.is_planar(tol_3d)) } // SmSurface body: use the virtual box and ignore ordinary failures, // which leave the default pseudo box. The kernel is available here. @@ -1525,12 +1532,18 @@ impl Surface { /// /// [`SolidError::Unported`] from the bounding box. pub fn b_spline_is_planar(k: &Kernel, s: &BSplineSurface, tol_3d: f64) -> Result { - Ok(planar_of(k, s, &s.natural_uv_domain())?.is_planar(tol_3d)) + Ok(planar_of(k, s, &s.natural_uv_domain(), None)?.is_planar(tol_3d)) } -/// The pseudo box of a B-spline surface over `uv_domain`; a failed box is the default +/// The pseudo box of a B-spline surface over `uv_domain`, expanded by `face_tol` (the +/// owning face's tolerance; `None` for `SM_ZONE_TOL_3D`); a failed box is the default /// box, as upstream ignores the status. -fn planar_of(k: &Kernel, s: &BSplineSurface, uv_domain: &Extent2d) -> Result { +fn planar_of( + k: &Kernel, + s: &BSplineSurface, + uv_domain: &Extent2d, + face_tol: Option, +) -> Result { let req = BoundingBoxRequest { normal: false, pseudo: true, @@ -1538,6 +1551,7 @@ fn planar_of(k: &Kernel, s: &BSplineSurface, uv_domain: &Extent2d) -> Result SurfaceCache { pseudo_guess: None, polar_guess: guess, expand_by_zone_tol: true, + expand_tolerance: None, }; let boxes = ignored(calculate_bounding_box(k, surface, &domain, &req))?; Ok(boxes.and_then(|b| b.polar)) diff --git a/crates/solidcore/src/surface/surface_cache/p2.rs b/crates/solidcore/src/surface/surface_cache/p2.rs index 21f40132e5..027757af4a 100644 --- a/crates/solidcore/src/surface/surface_cache/p2.rs +++ b/crates/solidcore/src/surface/surface_cache/p2.rs @@ -54,7 +54,6 @@ use crate::error::{Result, SolidError}; use crate::geom::extent2d::Extent2d; use crate::geom::extent3d::Extent3d; use crate::geom::pseudo_box::PseudoBox; -use crate::geom::tol::{zone_tol_3d, SmallTopoType}; use crate::geom::tree::{ AuxDataType, NodeClass, NodeData, NodeGeomType, NodeId, SplitDir, SurfaceAux, TreeNode, }; @@ -433,7 +432,8 @@ impl SurfaceCache { /// those of `CalculateBoundingBox`, `sm_ComputeNetConstants`, /// `sm_ComputeSurfConstants`, `GetCorners` and `ComputeSizeTolerances`; /// `InvalidInput` where upstream reads through null or mistyped data; - /// [`SolidError::Unported`] from `SmTol::GetZoneTol3d` of an owned surface and the + /// [`SolidError::Unported`] from `SmTol::GetZoneTol3d` of a face-owned surface whose + /// face tolerance is not cached on it ([`Surface::owner_zone_tol_3d`]) and from the /// surface calls. #[allow(clippy::too_many_lines)] // one upstream routine pub fn test_against_tolerances(&mut self, k: &Kernel, node: NodeId) -> Result { @@ -469,18 +469,9 @@ impl SurfaceCache { let chord_height_squared = chord_tol * chord_tol; let ang_tol_deg = opts.ang_tol_rad * 180.0 / PI; // SmTol::GetZoneTol3d(m_cpSurface): GetFace() ? its tolerance : the system's - let zone_tol = if self.surface().face()?.is_some() { - // UNPORTED(geom.tol): SmTol::GetZoneTol3d(const SmObject*) on an opaque handle. Every - // arm it dispatches to is ported (topology::{vertex, edge, face::p2, edgeuse::p1}::tolerance, - // Brep::tolerance) but each takes `&Brep` and none is in scope here; wire once this code - // receives the owning B-rep - return Err(SolidError::unported( - "SmTol::GetZoneTol3d(const SmObject*)", - "geom.tol", - )); - } else { - zone_tol_3d(k, SmallTopoType::NormalSize) - }; + // (the face's tolerance as its B-rep cached it on the surface; + // UNPORTED(topology.face_p2): SmFace::GetTolerance for a bare face handle) + let zone_tol = crate::surface::surface::p1::zone_tol_3d(k, self.surface())?; // local tolerance state - a zero ChordHeight or Angle Tol value means to ignore the test { @@ -524,6 +515,7 @@ impl SurfaceCache { pseudo_guess: None, polar_guess: None, expand_by_zone_tol: true, + expand_tolerance: None, }; let boxes = calculate_bounding_box(k, node_surface, &node_domain, &req)?; let bbox = boxes @@ -955,6 +947,7 @@ impl SurfaceCache { pseudo_guess: None, polar_guess: None, expand_by_zone_tol: true, + expand_tolerance: None, }; if let Some(boxes) = ignore(patch.calculate_bounding_box(k, &patch_domain, &req))? { if let Some(b) = boxes.normal { diff --git a/crates/solidcore/src/surface/surface_cache/p3.rs b/crates/solidcore/src/surface/surface_cache/p3.rs index 8627544058..527663a0b6 100644 --- a/crates/solidcore/src/surface/surface_cache/p3.rs +++ b/crates/solidcore/src/surface/surface_cache/p3.rs @@ -169,6 +169,7 @@ fn patch_boxes( pseudo_guess, polar_guess: None, expand_by_zone_tol: false, + expand_tolerance: None, }; let boxes = bss.calculate_bounding_box(k, &bss.natural_uv_domain(), &req)?; let mut bbox = boxes.normal.ok_or_else(|| invalid(op, "no bounding box"))?; diff --git a/crates/solidcore/src/topology.rs b/crates/solidcore/src/topology.rs index 96ef0a84e2..9dbcc0af79 100644 --- a/crates/solidcore/src/topology.rs +++ b/crates/solidcore/src/topology.rs @@ -937,13 +937,33 @@ impl Brep { } /// Appends a face record as is (no validation; the shell's face list is not - /// touched); the face joins the end of the B-rep's face list. + /// touched); the face joins the end of the B-rep's face list, and its surface + /// records the face's tolerance ([`Brep::sync_face_surface_tolerance`]). pub fn add_face_record(&mut self, face: Face) -> Result { let id = self.faces.insert(face)?; self.face_list.push(id); + self.sync_face_surface_tolerance(id)?; Ok(id) } + /// Records the face's tolerance on its surface + /// ([`Surface::set_owner_zone_tol_3d`](crate::surface::surface::Surface::set_owner_zone_tol_3d)), + /// so the surface's `GetFace()->GetTolerance()` reads it: the B-rep invariant that a + /// face record's surface carries that record's `tolerance`. A surface reaching no + /// face records nothing. Called wherever a face's surface or tolerance changes. + /// + /// # Errors + /// + /// [`SolidError::NotFound`] for a dangling id. + pub fn sync_face_surface_tolerance(&mut self, f: FaceId) -> Result<()> { + let face = self.face_mut(f)?; + let tol = face.tolerance; + if let Some(surface) = face.surface.as_mut() { + surface.set_owner_zone_tol_3d(Some(tol)); + } + Ok(()) + } + /// Appends a loop record as is (no validation; the face's loop list is not /// touched). pub fn add_loop_record(&mut self, loop_: Loop) -> Result { diff --git a/crates/solidcore/src/topology/brep/p10.rs b/crates/solidcore/src/topology/brep/p10.rs index dd02631082..c521731661 100644 --- a/crates/solidcore/src/topology/brep/p10.rs +++ b/crates/solidcore/src/topology/brep/p10.rs @@ -734,6 +734,9 @@ fn build_faces( // Connect up face and faceuses and set FU fields rec.faceuse = Some(fu1); // FU1 is primary faceuse + + // the surface reads the face's tolerance through GetFace() + b.sync_face_surface_tolerance(f)?; *b.faceuse_mut(fu1)? = Faceuse { face: Some(f), mate: Some(fu2), diff --git a/crates/solidcore/src/topology/brep/p6.rs b/crates/solidcore/src/topology/brep/p6.rs index a0e7080cbf..38ca41df3c 100644 --- a/crates/solidcore/src/topology/brep/p6.rs +++ b/crates/solidcore/src/topology/brep/p6.rs @@ -261,7 +261,8 @@ fn set_surface_owner( if let Some(surface) = brep.face_mut(face)?.surface.as_mut() { surface.set_owner(owner); } - Ok(()) + // the surface reads the face's tolerance through GetFace() once it is owned again + brep.sync_face_surface_tolerance(face) } // `delete pShell` of an emptied shell: `~SmShell` notifies `SM_NO_DESTRUCTION` diff --git a/crates/solidcore/src/topology/brep/p7.rs b/crates/solidcore/src/topology/brep/p7.rs index 19fc7d75ea..c74fa75daf 100644 --- a/crates/solidcore/src/topology/brep/p7.rs +++ b/crates/solidcore/src/topology/brep/p7.rs @@ -695,6 +695,7 @@ fn make_face_with_state( // make the face->surface connections surface.set_owner(Some(this_face(new_face))); b.face_mut(new_face)?.surface = Some(surface); + b.sync_face_surface_tolerance(new_face)?; Ok(NewFace { region: new_region, diff --git a/crates/solidcore/src/topology/face/p1.rs b/crates/solidcore/src/topology/face/p1.rs index b4d5a10d00..5bc5f7fdb7 100644 --- a/crates/solidcore/src/topology/face/p1.rs +++ b/crates/solidcore/src/topology/face/p1.rs @@ -915,6 +915,8 @@ pub fn set_surface( face.uv_domain = surface.natural_uv_domain(); surface.set_owner(Some(this)); face.surface = Some(surface); + // the surface reads the face's tolerance through GetFace() + b.sync_face_surface_tolerance(f)?; Ok(old) } @@ -1393,6 +1395,9 @@ pub fn move_seam( let zone_tol = crate::surface::surface::p1::zone_tol_3d(k, surface0)?; let mut surface = surface0.clone(); *surface.owner_mut() = surface0.owner(); + // the copy reaches the same face, so it carries the face tolerance cached on the + // original (a B-spline kind: no owned bases) + surface.set_owner_zone_tol_3d(surface0.owner_zone_tol_3d()); let crosses_u = match props { Some(p) => names(p.bad_crossed_seam_dir, Sp::U), diff --git a/crates/solidcore/src/topology/face/p2.rs b/crates/solidcore/src/topology/face/p2.rs index 52333d306a..f5a2fbc4f2 100644 --- a/crates/solidcore/src/topology/face/p2.rs +++ b/crates/solidcore/src/topology/face/p2.rs @@ -2491,17 +2491,22 @@ pub fn find_best_uv_curve( /// Saves a surface's owner and puts it back (`SmRescueSurfaceOwner`: the constructor /// saves, the destructor restores). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct RescueSurfaceOwner { owner: Option, + /// The face tolerance cached on the surface ([`Surface::owner_zone_tol_3d`]), put + /// back with the owner (a caller that may change the face's tolerance meanwhile + /// re-syncs it from the B-rep). + zone_tol_3d: Option, } impl RescueSurfaceOwner { /// Saves `surface`'s owner. #[must_use] - pub const fn new(surface: &Surface) -> Self { + pub fn new(surface: &Surface) -> Self { Self { owner: surface.owner(), + zone_tol_3d: surface.owner_zone_tol_3d(), } } @@ -2514,6 +2519,9 @@ impl RescueSurfaceOwner { /// Restores the saved owner on `surface` (the destructor). pub fn restore(self, surface: &mut Surface) { surface.set_owner(self.owner); + if self.zone_tol_3d.is_some() { + surface.set_owner_zone_tol_3d(self.zone_tol_3d); + } } } diff --git a/crates/solidcore/src/topology/face/p3.rs b/crates/solidcore/src/topology/face/p3.rs index e8894f2ec2..aa3993bcc5 100644 --- a/crates/solidcore/src/topology/face/p3.rs +++ b/crates/solidcore/src/topology/face/p3.rs @@ -339,6 +339,8 @@ pub fn set_tolerance( if !update_only_if_larger || new_zone_tol_3d > face.tolerance { face.tolerance = new_zone_tol_3d; } + // the face's surface reads it through GetFace() + b.sync_face_surface_tolerance(f)?; // when asked - make sure Edge->VertexTols at least as big as thie EdgeTol if cascade_to_boundaries { @@ -791,6 +793,8 @@ pub fn create_uv_trim_curves( if let Some(s) = b.face_mut(f)?.surface.as_mut() { rescue.restore(s); } + // the face's tolerance may have changed while the surface had no owner + b.sync_face_surface_tolerance(f)?; result } diff --git a/harness/cpp/shim/topology_trimming_tools.cpp b/harness/cpp/shim/topology_trimming_tools.cpp index d48bd02714..0de0fe5178 100644 --- a/harness/cpp/shim/topology_trimming_tools.cpp +++ b/harness/cpp/shim/topology_trimming_tools.cpp @@ -845,6 +845,20 @@ bool tt_ok_loop_extremes(SmLoop* l) return true; } +// Whether the loop walks of FixTrimLoopOrientation / FixTrimLoopOrder dereference +// nothing null: every loop has its loop use, and a face with loops has a surface and UV +// trim curves on every edge use (SmFace::ComputeLoopOrientation and the UV boxes of +// FixTrimLoopOrder read them). +bool tt_ok_face_loops(SmFace* f) +{ + SmTArray ls; + f->GetLoops(ls); + for (ULONG i = 0; i < ls.GetSize(); i++) + if (!tt_ok_loop(ls[i])) + return false; + return ls.GetSize() == 0 || (f->GetSurface() != nullptr && SmTrimmingTools::AreUVCurvesPresent(f)); +} + // The pre-step of the repair and check ops: `k side` (k = -1: none) mirrors the UV trim // curve of the k-th edge use (modulo their count) of `l`'s first loop use across its face // surface's natural domain with SmTrimmingTools::MirrorCurve (only a plain B-spline curve; @@ -999,30 +1013,25 @@ void tt_brep_op(int32_t op, Rd& rd, const char* path, std::vector& o) o.push_back(TT_NULL_DEREF); break; } - // with UV curves on every loop, the Rust side reaches ComputeLoopOrientation - // (unported) unless CheckLoop fails first: run only CheckLoop's part then + // With UV curves on every loop CheckFace reaches CheckLoop of every loop (which + // reads each loop's UV extremes) and SmFace::ComputeLoopOrientation: guarded + // (TT_NULL_DEREF) where upstream would dereference a missing surface or extreme. bool all_uv = ls.GetSize() > 0 && SmTrimmingTools::AreUVCurvesPresent(f); SmAssertArray a; - if (ls.GetSize() == 0 || !all_uv) + if (all_uv) { - o.push_back(st(SmTrimmingTools::CheckFace(f, tol, want_list ? &a : NULL, SM_LEVEL_0, NULL, - skip ? TRUE : FALSE))); - tt_put_reports(o, g, a); - break; + if (f->GetSurface() == nullptr) + ok = false; + for (ULONG i = 0; ok && !skip && i < ls.GetSize(); i++) + ok = tt_ok_loop_extremes(ls[i]); + if (!ok) + { + o.push_back(TT_NULL_DEREF); + break; + } } - if (skip || f->GetSurface() == nullptr || !tt_ok_loop_extremes(ls[0])) - { - o.push_back(TT_NOT_RUN); - break; - } - ULONG closure = 99; - SmStatus r = SmTrimmingTools::CheckLoop(f->GetSurface(), ls[0], tol, closure, want_list ? &a : NULL); - if (r == SM_SUCCESS) - { - o.push_back(TT_NOT_RUN); - break; - } - o.push_back(st(r)); + o.push_back(st(SmTrimmingTools::CheckFace(f, tol, want_list ? &a : NULL, SM_LEVEL_0, NULL, + skip ? TRUE : FALSE))); tt_put_reports(o, g, a); break; } @@ -1154,14 +1163,12 @@ void tt_brep_op(int32_t op, Rd& rd, const char* path, std::vector& o) } case OP_B_ORIENT: { - // FixTrimLoopOrientation of the face: SmFace::ComputeLoopOrientation is - // unported, so only a face without loops is run + // FixTrimLoopOrientation of the face (SmFace::ComputeLoopOrientation of every + // loop): guarded like CheckFace's ComputeLoopOrientation SmFace* f = g.faces.at((size_t)rd.idx()); - SmTArray ls; - f->GetLoops(ls); - if (ls.GetSize() != 0) + if (!tt_ok_face_loops(f)) { - o.push_back(TT_NOT_RUN); + o.push_back(TT_NULL_DEREF); break; } ULONG n = 99; @@ -1172,14 +1179,12 @@ void tt_brep_op(int32_t op, Rd& rd, const char* path, std::vector& o) } case OP_B_ORDER: { - // FixTrimLoopOrder: only its error for a face without loops is reached before - // the unported SmFace::ComputeLoopOrientation + // FixTrimLoopOrder (UV boxes of every loop's trim curves, then + // SmFace::ComputeLoopOrientation): guarded like FixTrimLoopOrientation SmFace* f = g.faces.at((size_t)rd.idx()); - SmTArray ls; - f->GetLoops(ls); - if (ls.GetSize() != 0) + if (!tt_ok_face_loops(f)) { - o.push_back(TT_NOT_RUN); + o.push_back(TT_NULL_DEREF); break; } SmBoolean made = UNSURE; diff --git a/plan/STATUS.md b/plan/STATUS.md index ce9f37aeb6..1d45ffd35c 100644 --- a/plan/STATUS.md +++ b/plan/STATUS.md @@ -22,6 +22,8 @@ Latest port: `sweep.topology_sweep` ([PR #222](https://github.com/tangency-labs/ Review follow-up merges canonical main `bb137d7d`, preserving both findings sections and main's full part 7 notes plus the sweep API extension. The merge p3/p4 integration boundary remains coordinated with the unavailable `merge_p2` constructor; the two `SM_DEG2RAD` sites now match upstream grouping. +Latest cross-unit change: owner zone-tolerance cache on `Surface` (branch `surface/owner-zone-tol`, PR #301): `topology.face_p2` deferrals 3495 -> 766, `surface.surface_cache_p2` 223 -> 0, +1751 pass over 15 units, 0 FAIL; the 76 cases it exposed (`query.face_p3` 75: missing vertex/face gaps in `CurveOnClassify`; `query.face_p1` 1: `Point3DClassify` skipped the trimmed surface cache) are fixed and now defer on unported callees (see `plan/findings.md`). + Latest harness: PR #295 (B-rep records for the classification shims' bare topology stand-ins, on top of main's real one-face B-rep adapter #347): curve_class_p1 2054->2070, p2 1495->1627, p7 1327->1474, point_class 2411->2466 passes, 0 failed. Latest unit: `topology.face_p4` partial (port: topology.face_p4, taking over the #134 claim): 5506 cases, 3957 bit-identical passes, 0 failed, 1549 deferred (mostly the tolerance of face-owned surfaces: `SmTol::GetZoneTol3d` in the surface cache, tagged `geom.tol`, and `SmFace::GetTolerance`, `topology.face_p2`); 26 of its 44 back-edge stubs wired, including those of `heal.stitch_p1`, `topology.brep_p5`/`p7`/`p9`/`p10` and `topology.face_p3` (169 more `face_p3` passes); the rest take generic handles without a B-rep, or (one in `brep_p5`) no kernel; its own calls into `query.face_p3` (`sm_FaceClassifyUVLine`, `SectorClassifyCurveOn`) are wired since #132; `HasSeamProblem` returns `SM_SUCCESS` as TRUE upstream (`plan/upstream-issues.md` WR-176); 238 units (main after #229), 116 green and 122 partial. diff --git a/plan/findings.md b/plan/findings.md index 4ded337b1e..887da56a53 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -12671,6 +12671,100 @@ boundary. No Rust/C++ value mismatch was observed in the completed fuzz cases. The ordinary unit run remains 2686 cases, 2250 passed, 436 deferred, 0 failed, with maximum relative error 0. +## surface: owning face's zone tolerance cached on `Surface` (branch `surface/owner-zone-tol`, 2026-10-20) + +Upstream reads `GetFace()->GetTolerance()` from a surface in ten places (`SmTol::GetZoneTol3d(SmObject*)` +SmTol.cpp:775, `SmBSplineSurface::CalculateBoundingBox` SmBSplineSurface.cpp:797-806, `SmSurface::DropPoint` +SmSurface.cpp:10432, `LocalSurfaceSurfaceIntersectXYZ` SmSurface.cpp:29051, `ClassifyCurveSurfaceXsectPoint` +SmSurface.cpp:13373, `SmPlane` zone tolerance, `IsPlanar`, approx tol, point_class, surface_cache_p2). The port's +surface owner is an opaque handle, so every site returned `Unported(topology.face_p2)`. `Surface` now caches the +owning face record's `tolerance`, keyed by the reached face handle (a different or cleared owner never reads it); +`Brep::sync_face_surface_tolerance` keeps it current at every place a face record's surface or tolerance changes. +A face owner without cache still returns `Unported`. `surface.surface_cache_p1` `mark_face_containment` (an opaque +node face handle) is not covered. On main 862746f4 (`--green`, all units): `topology.face_p2` deferral label 3495 -> +766, `surface.surface_cache_p2` 223 -> 0, total deferred 24839 -> 22724, +2039 pass over 15 units, +76 FAIL in +`query.face_p3` (75) and `query.face_p1` (1), both below (exposed, not caused: each case read the face tolerance +correctly; the next divergent step is elsewhere). After both fixes below: 0 FAIL, +1751 pass over 15 units, total +deferred 24839 -> 23088 (`query.face_p3` 75 and `query.face_p1` 289 now defer on unported callees). + +### topology.trimming_tools: stale `TT_NOT_RUN` shim gate + +`harness/cpp/shim/topology_trimming_tools.cpp` returned `-3` (`TT_NOT_RUN`) for `b_check_face`, `b_orient` and +`b_order` on every face with loops, written while `SmFace::ComputeLoopOrientation` was unported. `topology.face_p3` +ported it; the Rust side kept deferring only through `SmFace::GetTolerance`. With the cache the Rust side +completes, so 305 cases (104 check_face, 102 orient, 99 order) compared a real result against `-3`. The shim now +runs `CheckFace` / `FixTrimLoopOrientation` / `FixTrimLoopOrder` in full, guarded with `-2` (mirrored in the +runner) where a loop lacks its loop use, a face with loops lacks surface or UV trim curves, or `CheckLoop` would +dereference a null UV extreme (17 cases). The fixtures never reorient a loop (orient always 0, order always +`false`): those two ops are compared on the no-change path only. Follow-up: misordered / misoriented loop fixtures. + +### topology.brep_p1 `edge_edge_blend` on analytic face surfaces: harness input mismatch, not a port bug + +Inputs: `surf_rev.smb` (`crates/difftest/fixtures/topology.brep_p1/upstream_files.json#1518` flag=1 eu1=0 eu2=1 +dir=0, `#1520` flag=3 eu1=1 eu2=2 dir=1). C++ `out[979]` = 3.4935082719274374e-1 vs Rust 3.4935082670870377e-1 +(1.4e-9), growing to 7.3e-4 (`3_1_2_1`: 5.3e-4). Previously deferred on `SmFace::GetTolerance`. Cause: the +shared `topology.edgeuse_p1` export hands Rust the faces' `SmSurfOfRevolution` as its B-spline part. In +`GetSampleValues` -> `DropPoint`, C++ succeeds in `SmSurfOfRevolution::DropPointFast` (SmSurfOfRevolution.cpp:2356), +while Rust's B-spline `drop_point_fast` is the base `SM_ERR` and it uses `LocalPointSolve` (bit-identical to C++ +`LocalPointSolve`). First drop difference: u = 0x3fcdfd6b2cbee1af vs 0x3fcdfd6b2cbee37e (5.5e-14), amplified by +the G2 blend fit. With the C++ faces flattened to the same B-spline part (temporary shim switch), all 159 compared +blend cases match at 0. surf_extrude (`SmSurfOfExtrusion::DropPointFast`, up to 9.4e-10) and surf_rev 2_0_2_0 +(8.9e-11) show the same drift below 1e-9. Resolution: the runner defers blends (flag != 0) whose faces are +non-exact surfaces (`harness:analytic-geometry-export`, +27 cases: 2 FAIL, 25 pass). Exporting revolution / +extrusion surfaces exactly in `sc_edgeuse_graph.h` would let them be compared. + +### query.face_p3 `bisector`: `LoopObjectCurves` has no `vertex_face_gap` (75 FAIL, previously deferred on `SmFace::GetTolerance`) + +`sms_FindAndClassifyLoopBisector` calls `SmFace::CurveOnClassify(TRUE, cc, TRUE /* bHasBigGaps */, TRUE, ..)` +(SmFace.cpp:2884-2892). With big gaps and a face, `IntersectVertices` reads +`pVertex->GetVertexuseOfFace(pFace)->GetVertexFaceGap()` (SmCurveClass.cpp:14716-14744). The port's +`vertex_big_gap_tolerance` (`crates/solidcore/src/intersect/curve_class/p5.rs:163`) asks +`ClassGeometry::vertex_face_gap`, which `query::face::p3::LoopObjectCurves` (`crates/solidcore/src/query/face/p3.rs:549`) +does not implement (trait default `None`), so every call returns `InvalidInput("unresolved classification +vertex/face gap")`. The bisector ignores that status as upstream does (`ignored(curve_on_classify(..))`), so the Rust +side records no intervals: e.g. `bisector_Mouse_f2_t0` `values.interval_bounds.len` C++ 6 vs Rust 2, +`bisector_holed_box_top_flat` `ints.interval_classes[0]` C++ 103 vs Rust 100. A temporary probe showed this error on +all 75 classification calls of the failing cases and on no other path. Fix: implement `vertex_face_gap` on +`LoopObjectCurves` (vertex use of the face, `has_vertex_face_gap`, cached or computed `SmVertexuse::GetVertexFaceGap` +length, 0 when absent); it needs a kernel and a non-caching gap computation, since the adapter holds `&Brep`. +More generally, an adapter gap reported as `InvalidInput` is indistinguishable from an upstream status under +`ignored`: no `ClassGeometry` implementation provides `vertex_face_gap` today. + +Resolved: the gap is a B-rep property, so `query::face::p2::curve_on_classify` (which has the B-rep and kernel) +now computes it for every caller, not only `LoopObjectCurves`: with `has_big_gaps` it reads +`vertex::vertexuse_of_face` -> `vertexuse::vertex_face_gap(.., false)` length (0 without a gap) of each face +vertex on one private B-rep snapshot per call (it keeps the gap caches across both passes, as upstream's +`GetVertexFaceGap` caches into the B-rep), and answers `ClassGeometry::vertex_face_gap` for those vertex handles +through a wrapper around the caller's geometry. With the gap tolerance in place, all 75 cases reach +`SmCrvGeometry::GlobalPointSolve` on the bisector's `SmCrvOnSurf`, whose `Curve::knots` stops at the known +`UNPORTED(intersect.curve_class_p2): SmCurveClassification::ReSet` (`crates/solidcore/src/curve/crv_on_surf.rs:218`, +no `Kernel` under `Curve::knots`): 75 FAIL -> 75 deferred. `query.face_p3`: 543 pass / 0 fail / 656 deferred. + +### query.face_p1 `gen0160` (1 FAIL, previously deferred on `SmFace::GetTolerance`) + +Generated `Point3DClassify` on a 1e-3 x 3e-3 bilinear-in-u B-spline sliver whose u=0 edge collapses onto the z axis +(`generated:seed=4634842130399439208`), point offset ~5e-4, `src_tol` 0.01, `pc_tol` 1e-8, face tolerance 1e-5. +`values.pc[0]` C++ 102 (FACE, uv 0.720517876766358, 0.823164791473039) vs Rust 104 (VERTEX, no uv). The global point +solve's UV is bit-identical on both sides (probe), and the three face-tolerance reads (`is_on_boundary`, +`local_point_solve_1`) all return 1e-5 as in C++; the divergence is inside `query::face::p2::point_classify`, which +reads no surface face tolerance. Not localised further. + +Resolved (C++ probe, hunks in the worktree's `GEN0160-PROBES.diff`, reverted): after `Point3DClassify` the face +surface has an `SmTrimSrfCache` with `m_bHaveTSurfaceCache` 1, `m_bFaceContainmentDone` 1, `m_bFaceWasModified` 0, +`GetSurface()->GetFace() == this`; the one leaf node at the UV is `SM_NC_INSIDE`. The face's 4 vertices (tolerance +1e-5) are 1.083e-3 from the surface point, inside the vertex test's `max(1e-5, 1e-5, GetTolerance()=src 0.01)`. +C++ `PointClassify` at the same UV with the cache dropped returns VERTEX (104), as Rust. Cause: upstream +`Point3DClassify` calls `smsurf_GetSurfaceCache(pSurface)` first (SmFace.cpp:12792), which builds the trimmed +surface cache with its face containment; `PointClassify` then classifies through the inside leaf and returns FACE +before the vertex test. The port skipped that call (its header claimed a fresh cache is never +face-containment-complete, which is wrong for a face with loops: SmTrimSrfCache.cpp:330-347). Fix: +`point_3d_classify` now builds it (`make_trim_srf_cache`, else `make_surface_cache`) and passes it to +`point_classify`. The trimmed cache's face walk is unported (`Unported("SmFace handle of a surface's face", +topology.face_p2)`), so every `Point3DClassify` case on a face-owned surface now defers there: the 288 point cases +that passed with the skipped call (the cache path and the vertex/loop path agree outside such tolerance edges) +plus `gen0160`. `query.face_p1`: 1580 pass / 0 fail / 644 deferred (main 862746f4: 1570 / 0 / 654). They become +comparable with `surface.surface_cache_p1` `mark_face_containment` and the `TrimSrfCache` face walk. + ## Kernel-aware Surface integration: PR #133 review follow-up (2026-10-16) diff --git a/plan/modules.toml b/plan/modules.toml index 26402ff2dd..7fdb5c471a 100644 --- a/plan/modules.toml +++ b/plan/modules.toml @@ -2072,7 +2072,7 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin status = "partial" claimed_by = "" claimed_at = "" -notes = "PR #295 (stand-in vertex/edge/face objects as B-rep records, BareTopology, used when the case has no real one-face B-rep; merged with main 215f690e1 / #347): 1516 cases, 1474 pass, 0 fail, 42 deferred (topology.edge 34, topology.face_p2 4, topology.vertex 4); main 1327/189; --perturb 1474/1474. Topology hooks wired (branch wire/point-class-topology): difftest 1516 cases, 1327 passed, 0 failed, 189 deferred (topology.edge 106, topology.vertex 52, topology.face_p2 31: only stand-in objects whose adapter has no topology_entity); SmEdge/SmVertex members (GetCurve, GetInterval, IsConnectedTo, GetPoint, zone tol) now resolve through ClassGeometry::topology_entity; point_classifies_connected_to/find_vertex_objects/find_edge_objects/is_crossed_by_edge take the geometry; harness scripts may carry a one-face B-rep (sc_one_face_brep on the C++ side, rust_brep + add_down_side on the Rust side) whose face/loop/edge/vertex/edgeuse handles join the object pool; fix: TestForPartialCoincidentEdges accepted only BSpline edge curves, now Line/conics via their B-spline base (upstream slices to SmBSplineCurve); still UNPORTED: SetPoint/SetCurve, SmFace queries (topology.face_p4, query.face_p1). Before: PR #65: partial, difftest 1516 cases (1077 passed, 0 failed, 439 deferred: topology.edge 226, topology.vertex 160, topology.face_p2 53), --perturb SENSITIVE 1077/1077; topology members (SmEdge/SmVertex) and SmFace queries (topology.face_p4, query.face_p1, query.face_p2) stay UNPORTED stubs because the opaque handle O cannot reach a Brep; SmCrvOnSurf over a non-B-spline surface is UNPORTED(curve.crv_on_surf); wired back-edges: intersect.curve_class_p3 (GetPointClassByIndex, GetPointClassMateByIndex, AreCoincidentMaybe; its private IsDegenerate co-port is reused here) and query.face_p2 (ClassifyFaceInterval, CurveOnClassify now takes ClassGeometry); 14 fns; largest: SmCurveClassification::TestForPartialCoincidentEdges, SmCurveClassification::ClassifyFaceInterval, sm_SetDoFilletingGeometry, SmCurveClassification::AreCoincidentMaybe; cycle back-edges (call into later units; stub or co-port): topology.edge(9), topology.vertex(3), query.face_p1(2), query.face_p2(1), topology.face_p4(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit" +notes = "PR #295 (stand-in vertex/edge/face objects as B-rep records, BareTopology, used when the case has no real one-face B-rep; merged with main 215f690e1 / #347): 1516 cases, 1474 pass, 0 fail, 42 deferred (topology.edge 34, topology.face_p2 4, topology.vertex 4); main 1327/189; --perturb 1474/1474. Topology hooks wired (branch wire/point-class-topology): difftest 1516 cases, 1327 passed, 0 failed, 189 deferred (topology.edge 106, topology.vertex 52, topology.face_p2 31: only stand-in objects whose adapter has no topology_entity); SmEdge/SmVertex members (GetCurve, GetInterval, IsConnectedTo, GetPoint, zone tol) now resolve through ClassGeometry::topology_entity; point_classifies_connected_to/find_vertex_objects/find_edge_objects/is_crossed_by_edge take the geometry; harness scripts may carry a one-face B-rep (sc_one_face_brep on the C++ side, rust_brep + add_down_side on the Rust side) whose face/loop/edge/vertex/edgeuse handles join the object pool; fix: TestForPartialCoincidentEdges accepted only BSpline edge curves, now Line/conics via their B-spline base (upstream slices to SmBSplineCurve); still UNPORTED: SetPoint/SetCurve, SmFace queries (topology.face_p4, query.face_p1). Before: PR #65: partial, difftest 1516 cases (1077 passed, 0 failed, 439 deferred: topology.edge 226, topology.vertex 160, topology.face_p2 53), --perturb SENSITIVE 1077/1077; topology members (SmEdge/SmVertex) and SmFace queries (topology.face_p4, query.face_p1, query.face_p2) stay UNPORTED stubs because the opaque handle O cannot reach a Brep; SmCrvOnSurf over a non-B-spline surface is UNPORTED(curve.crv_on_surf); wired back-edges: intersect.curve_class_p3 (GetPointClassByIndex, GetPointClassMateByIndex, AreCoincidentMaybe; its private IsDegenerate co-port is reused here) and query.face_p2 (ClassifyFaceInterval, CurveOnClassify now takes ClassGeometry); 14 fns; largest: SmCurveClassification::TestForPartialCoincidentEdges, SmCurveClassification::ClassifyFaceInterval, sm_SetDoFilletingGeometry, SmCurveClassification::AreCoincidentMaybe; cycle back-edges (call into later units; stub or co-port): topology.edge(9), topology.vertex(3), query.face_p1(2), query.face_p2(1), topology.face_p4(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit Owner zone-tol cache (surface/owner-zone-tol, merged main db47090f): 1671 cases, 1654 pass, 0 fail, 17 deferred (topology.face_p2 17)." [[unit]] id = "intersect.ray_tracer" @@ -2296,7 +2296,7 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin status = "partial" claimed_by = "" claimed_at = "" -notes = "PARTIAL (PR #137): difftest 3201 cases (201 hand-written edge scripts + 3000 generated), 1318 pass bit-identical (max_rel_err 0), 0 failed, 1883 deferred (intersect.curve_class_p2 1014: ReSet in the constructor, ConnectIntervals in every squeeze, GetMinUV/GetMaxUV at an edge end; topology.face_p4 360: SmFace::HasSeamProblem in SetProps_Stage2 of a closed face; topology.face_p2 376: SmFace::GetTolerance in the zone tolerance of the face-owned surface, a surface.surface_p1/_p4 stub reached by the seam sampling; topology.edge 82 and topology.vertex 51: SmEdge::GetCurve / SmEdge::IsConnectedTo / SmVertex::IsConnectedTo in PointClassification::curve_object and is_connected_to, reached by the touch lists); --perturb SENSITIVE 1318/1318. Review round 2 (PR #137): GetPointClassifiesConnectedTo wired to intersect.curve_class_p7 (PR #65 merged: 1076 -> 1318 pass, 2125 -> 1883 deferred). Harness: the shim builds a real one-face B-rep with SmBrepConstructor (shared harness/cpp/shim/sc_one_face_brep.h with heal.face_props: plane, cylinder and sphere faces from SmSurfOfRevolution::CreateCanonical, closed or open, plus the heal.face_props face generator; vertex loops and edge loops of line edges, NURBs made on both sides), rebuilt record by record in Rust (heal_face_props::rust_brep); the topology stand-ins are gone. Compared: 374 passing cases run GetLoopCrossings over real loops up to the touch list (loop -> face, SetProps_Stage2 through the hook or face props preset at stage >= 2a by seeds, the props snapshot, surface, IsClosed/IsDegenerate and the no-crossings return of a degenerate classification curve); 400 passing cases run GetIvlBound_EdgeuseObj on the real face (upward face use, surface, the NULL return of a non-edge end); the squeeze and the constructor compare 0 cases beyond the empty-classification rejection (both defer on intersect.curve_class_p2); every edge end and the touch lists that reach an unported IsConnectedTo defer. Not reachable: a face without surface. Topology members go through the defaulted ClassGeometry::topology_entity / topology_handle hooks (CO-PORTED into intersect.point_class; implemented by topology::face::p2 FaceClassifyObjects and the difftest pool); unresolved handles are InvalidInput. FaceProps::set_props_stage2 InvalidInput (UB-131 null m_pFace) now propagates from GetLoopCrossings, and the FFI guard rejects seeded props that reach it (stage GAPSa/GAPS without field 24). Stubs: UNPORTED(intersect.curve_class_p2) (PR #85, still open); none left for intersect.curve_class_p7 (wired); SmEdgeuse::EvaluateBinormal wired to topology.edgeuse_p3 (PR #117) after merging main: it runs on a copy of the B-rep (bUse3DOnly TRUE creates no UV trim curve); counts unchanged, 1318 pass / 1883 deferred, --perturb SENSITIVE 1318/1318; the status of both EvaluateBinormal call sites is ignored as upstream (a failure leaves the out-vectors as C++ does: point (0,0,0) after the output init, untouched for a non-loop edge use; only Unported and a null-dereference InvalidInput propagate), and the first site keeps its out-vectors across the 10 samples as upstream declares them outside the loop (no difftest case reaches a failing EvaluateBinormal yet: counts unchanged). Wired: topology::face::p2 (constructor x3, CurveOnClassify of query.face_p2; GetPointsInFace site passes dTol, no borrowed data), intersect::topology_solver::p2 (constructor, zone_tol3d of the solver tolerance; its CurveOnClassify stays justified: the solver adapter has no face -> Brep resolution), intersect::curve_class::p6 (sm_SqueezeOutSmallestInterval), topology::proto_topology::p3 (constructor); crv_in_volume / crv_on_surf constructor stubs retagged UNPORTED(intersect.curve_class_p2): SmCurveClassification::ReSet (no Kernel under Curve::knots; plan/findings.md). CO-PORTED in blend.hcr (PR #191): SmCurveClassification::SmCurveClassification(cpCurve, crInterval, NULL, tol, n, pData) with its ReSet (new curve) -> blend::hcr::new_curve_classification (on CurveClassification::with_curve_interval, SM_UNINIT_TOL for a non-positive tolerance) (kept beside the PR #137 constructor: blend.hcr builds it on CurveClassification::with_curve_interval and does not defer on ReSet). Wired to intersect.curve_class_p2 (wire/curve-class-p4): ReSet in the constructor, ConnectIntervals in sm_SqueezeOutSmallestInterval and SmCurveInterval::GetMinUV / GetMaxUV in GetIvlBound_EdgeuseObj (which now takes the Kernel, as does topology::face::p2::review_start_and_stop_on_seam_crv_classifies); no UNPORTED(intersect.curve_class_p2) stub is left here. difftest 3201 cases: 2224 pass (was 1319), 0 failed, 977 deferred (topology.face_p2 566, topology.face_p4 411); max_rel_err 0. New shim guard: GetLoopCrossings over loops on an empty classification (left by squeezing its only interval) is rejected, since GetPointClassifiesConnectedTo reads m_pData[0] (Rust: InvalidInput). Callers that now get past the constructor: heal.heal_data_p2 469 -> 483 pass, heal.heal_data_p3 329 -> 344, heal.stitch_p2 332 -> 337, topology.face_p3 823 -> 888; topology.brep_p2's merge_plane10_line now reaches CurveOnClassify, whose topology its MergeObjects adapter cannot resolve (merge_curves_on_surface holds the B-rep mutably), and defers as topology.brep_p2. The shared .smb import of the difftest runners (topology_edgeuse_p1::import) now sets each face's m_pFU (its SM_OT_SAME face use; the export does not carry it): without it topology.shell's region-order cases on Mouse.smb / box_with_large_fillet.smb reached CurveOnClassify through IsInnerShell with no upward face use and failed; they now defer deeper (crv_on_surf, topology.edge). Full sweep after merging main (7eb93aa4): 238 units, 0 failed." +notes = "PARTIAL (PR #137): difftest 3201 cases (201 hand-written edge scripts + 3000 generated), 1318 pass bit-identical (max_rel_err 0), 0 failed, 1883 deferred (intersect.curve_class_p2 1014: ReSet in the constructor, ConnectIntervals in every squeeze, GetMinUV/GetMaxUV at an edge end; topology.face_p4 360: SmFace::HasSeamProblem in SetProps_Stage2 of a closed face; topology.face_p2 376: SmFace::GetTolerance in the zone tolerance of the face-owned surface, a surface.surface_p1/_p4 stub reached by the seam sampling; topology.edge 82 and topology.vertex 51: SmEdge::GetCurve / SmEdge::IsConnectedTo / SmVertex::IsConnectedTo in PointClassification::curve_object and is_connected_to, reached by the touch lists); --perturb SENSITIVE 1318/1318. Review round 2 (PR #137): GetPointClassifiesConnectedTo wired to intersect.curve_class_p7 (PR #65 merged: 1076 -> 1318 pass, 2125 -> 1883 deferred). Harness: the shim builds a real one-face B-rep with SmBrepConstructor (shared harness/cpp/shim/sc_one_face_brep.h with heal.face_props: plane, cylinder and sphere faces from SmSurfOfRevolution::CreateCanonical, closed or open, plus the heal.face_props face generator; vertex loops and edge loops of line edges, NURBs made on both sides), rebuilt record by record in Rust (heal_face_props::rust_brep); the topology stand-ins are gone. Compared: 374 passing cases run GetLoopCrossings over real loops up to the touch list (loop -> face, SetProps_Stage2 through the hook or face props preset at stage >= 2a by seeds, the props snapshot, surface, IsClosed/IsDegenerate and the no-crossings return of a degenerate classification curve); 400 passing cases run GetIvlBound_EdgeuseObj on the real face (upward face use, surface, the NULL return of a non-edge end); the squeeze and the constructor compare 0 cases beyond the empty-classification rejection (both defer on intersect.curve_class_p2); every edge end and the touch lists that reach an unported IsConnectedTo defer. Not reachable: a face without surface. Topology members go through the defaulted ClassGeometry::topology_entity / topology_handle hooks (CO-PORTED into intersect.point_class; implemented by topology::face::p2 FaceClassifyObjects and the difftest pool); unresolved handles are InvalidInput. FaceProps::set_props_stage2 InvalidInput (UB-131 null m_pFace) now propagates from GetLoopCrossings, and the FFI guard rejects seeded props that reach it (stage GAPSa/GAPS without field 24). Stubs: UNPORTED(intersect.curve_class_p2) (PR #85, still open); none left for intersect.curve_class_p7 (wired); SmEdgeuse::EvaluateBinormal wired to topology.edgeuse_p3 (PR #117) after merging main: it runs on a copy of the B-rep (bUse3DOnly TRUE creates no UV trim curve); counts unchanged, 1318 pass / 1883 deferred, --perturb SENSITIVE 1318/1318; the status of both EvaluateBinormal call sites is ignored as upstream (a failure leaves the out-vectors as C++ does: point (0,0,0) after the output init, untouched for a non-loop edge use; only Unported and a null-dereference InvalidInput propagate), and the first site keeps its out-vectors across the 10 samples as upstream declares them outside the loop (no difftest case reaches a failing EvaluateBinormal yet: counts unchanged). Wired: topology::face::p2 (constructor x3, CurveOnClassify of query.face_p2; GetPointsInFace site passes dTol, no borrowed data), intersect::topology_solver::p2 (constructor, zone_tol3d of the solver tolerance; its CurveOnClassify stays justified: the solver adapter has no face -> Brep resolution), intersect::curve_class::p6 (sm_SqueezeOutSmallestInterval), topology::proto_topology::p3 (constructor); crv_in_volume / crv_on_surf constructor stubs retagged UNPORTED(intersect.curve_class_p2): SmCurveClassification::ReSet (no Kernel under Curve::knots; plan/findings.md). CO-PORTED in blend.hcr (PR #191): SmCurveClassification::SmCurveClassification(cpCurve, crInterval, NULL, tol, n, pData) with its ReSet (new curve) -> blend::hcr::new_curve_classification (on CurveClassification::with_curve_interval, SM_UNINIT_TOL for a non-positive tolerance) (kept beside the PR #137 constructor: blend.hcr builds it on CurveClassification::with_curve_interval and does not defer on ReSet). Wired to intersect.curve_class_p2 (wire/curve-class-p4): ReSet in the constructor, ConnectIntervals in sm_SqueezeOutSmallestInterval and SmCurveInterval::GetMinUV / GetMaxUV in GetIvlBound_EdgeuseObj (which now takes the Kernel, as does topology::face::p2::review_start_and_stop_on_seam_crv_classifies); no UNPORTED(intersect.curve_class_p2) stub is left here. difftest 3201 cases: 2224 pass (was 1319), 0 failed, 977 deferred (topology.face_p2 566, topology.face_p4 411); max_rel_err 0. New shim guard: GetLoopCrossings over loops on an empty classification (left by squeezing its only interval) is rejected, since GetPointClassifiesConnectedTo reads m_pData[0] (Rust: InvalidInput). Callers that now get past the constructor: heal.heal_data_p2 469 -> 483 pass, heal.heal_data_p3 329 -> 344, heal.stitch_p2 332 -> 337, topology.face_p3 823 -> 888; topology.brep_p2's merge_plane10_line now reaches CurveOnClassify, whose topology its MergeObjects adapter cannot resolve (merge_curves_on_surface holds the B-rep mutably), and defers as topology.brep_p2. The shared .smb import of the difftest runners (topology_edgeuse_p1::import) now sets each face's m_pFU (its SM_OT_SAME face use; the export does not carry it): without it topology.shell's region-order cases on Mouse.smb / box_with_large_fillet.smb reached CurveOnClassify through IsInnerShell with no upward face use and failed; they now defer deeper (crv_on_surf, topology.edge). Full sweep after merging main (7eb93aa4): 238 units, 0 failed. Owner zone-tol cache (surface/owner-zone-tol): 3201 cases, 1740 pass, 0 fail, 1461 deferred." [[unit]] id = "intersect.surface_intersector_p3" @@ -2380,7 +2380,7 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test: status = "partial" claimed_by = "" claimed_at = "" -notes = "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)]" +notes = "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." [[unit]] id = "intersect.curve_class_p5" @@ -2478,7 +2478,7 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test: status = "partial" claimed_by = "" claimed_at = "" -notes = "PARTIAL: SmSurface::GetSingularities wired to surface.surface_p3 (has_singularities takes &Kernel): difftest 2686 cases, 2250 pass, 436 deferred (topology.edgeuse_p3 346, topology.face_p2 90), max_rel_err 0. History: (after heal.gap wired CalcVertexEdgeGap/CalcVertexFaceGap: 2250 pass, 436 deferred (topology.edgeuse_p3 346, surface.surface_p3 90); after topology.face_p2 wired GetUVDomain/GetTolerance: 1928 pass, 758 deferred (topology.edgeuse_p3 346, heal.gap 322, surface.surface_p3 90); originally 2670 fixture: 870 on 15 upstream .smb B-reps read by SmBrep::ReadFromFile, with and without RebuildUVTrimCurves; 972 on prog_test my_test_topology's wire network, wire edges and faces from B-spline surfaces and SmPlanes; 828 adversarial: huge/tiny planes, tiny and huge (1e5, out-of-bounds) B-spline faces, pinched (pole) surface, sub-domain and sliver faces, tiny and huge wire edges; 16 generated, 2000 more checked once), 1621 pass, 1065 deferred (topology.face_p2 536, heal.gap 403, surface.surface_p3 126), max_rel_err 0; --perturb SENSITIVE. Runner imports the shim's exported graph into topology::Brep (as topology.edgeuse_p1). ComputeUVPoint/ComputeUVSector on faces defer at SmFace::GetUVDomain; their remaining bodies are ported behind stubs (NormalizedEvaluate, Local/GlobalPointSolve, IsSingularity, IsOnSeam, SmLoopuse::GetOrientation). Co-ports: SmVertexEdgeGap record (heal.gap), SmVertex::GetVertexuses/GetVertexusesOfEdge/GetVertexusesOfFace by arena scan (topology.vertex), SmEdge::IsWire (topology.edge); Vertexuse gains vertex_edge_gap and uv_point. Back-edge stubs UNPORTED(topology.vertexuse) in surface::trim_srf_cache and intersect::point_class not wired (opaque handles without a Brep; plan/findings.md). Plan notes: 29 fns; largest: SmVertexuse::ComputeUVSector, SmVertexuse::ComputeUVPoint, SmVertexuse::CalcVertexFaceGap, SmVertexuse::Notify; cycle back-edges (call into later units; stub or co-port): heal.gap(11), topology.edge(6), topology.vertex(5), topology.loopuse(4), blend.fillet_geom_p1(1), topology.edgeuse_p3(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmVertexuse members (owner vertex, m_tVertexuseType/m_pSorLUorEU, m_sVertexFaceGap3d) -> topology::{Vertexuse, VertexuseOwner}; SmVertexuse::GetEdgeuse -> topology::edgeuse::p1::vertexuse_edgeuse [topology.edgeuse_p3 wired (PR #117): 2686 cases, 2250 pass, 0 failed, 436 deferred (intersect.surface_drop_lib_p3 92, topology.face_p2 344)] UPDATE (PR #149, lesson 30): the FFI Reader is the shared crate::reader::unit_reader!(MAX_COORD) (#186 left this unit for #149); its finite/index/below/flag/finites/vec3/nonzero3 stay unit methods (different messages or bounds), num/done come from the shared Cursor; the reader is private again and topology_brep_p2 reads blocks through surface_at / curve_at / brep_block_at. Report unchanged (2686 cases, 2250 pass, 436 deferred)." +notes = "PARTIAL: SmSurface::GetSingularities wired to surface.surface_p3 (has_singularities takes &Kernel): difftest 2686 cases, 2250 pass, 436 deferred (topology.edgeuse_p3 346, topology.face_p2 90), max_rel_err 0. History: (after heal.gap wired CalcVertexEdgeGap/CalcVertexFaceGap: 2250 pass, 436 deferred (topology.edgeuse_p3 346, surface.surface_p3 90); after topology.face_p2 wired GetUVDomain/GetTolerance: 1928 pass, 758 deferred (topology.edgeuse_p3 346, heal.gap 322, surface.surface_p3 90); originally 2670 fixture: 870 on 15 upstream .smb B-reps read by SmBrep::ReadFromFile, with and without RebuildUVTrimCurves; 972 on prog_test my_test_topology's wire network, wire edges and faces from B-spline surfaces and SmPlanes; 828 adversarial: huge/tiny planes, tiny and huge (1e5, out-of-bounds) B-spline faces, pinched (pole) surface, sub-domain and sliver faces, tiny and huge wire edges; 16 generated, 2000 more checked once), 1621 pass, 1065 deferred (topology.face_p2 536, heal.gap 403, surface.surface_p3 126), max_rel_err 0; --perturb SENSITIVE. Runner imports the shim's exported graph into topology::Brep (as topology.edgeuse_p1). ComputeUVPoint/ComputeUVSector on faces defer at SmFace::GetUVDomain; their remaining bodies are ported behind stubs (NormalizedEvaluate, Local/GlobalPointSolve, IsSingularity, IsOnSeam, SmLoopuse::GetOrientation). Co-ports: SmVertexEdgeGap record (heal.gap), SmVertex::GetVertexuses/GetVertexusesOfEdge/GetVertexusesOfFace by arena scan (topology.vertex), SmEdge::IsWire (topology.edge); Vertexuse gains vertex_edge_gap and uv_point. Back-edge stubs UNPORTED(topology.vertexuse) in surface::trim_srf_cache and intersect::point_class not wired (opaque handles without a Brep; plan/findings.md). Plan notes: 29 fns; largest: SmVertexuse::ComputeUVSector, SmVertexuse::ComputeUVPoint, SmVertexuse::CalcVertexFaceGap, SmVertexuse::Notify; cycle back-edges (call into later units; stub or co-port): heal.gap(11), topology.edge(6), topology.vertex(5), topology.loopuse(4), blend.fillet_geom_p1(1), topology.edgeuse_p3(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmVertexuse members (owner vertex, m_tVertexuseType/m_pSorLUorEU, m_sVertexFaceGap3d) -> topology::{Vertexuse, VertexuseOwner}; SmVertexuse::GetEdgeuse -> topology::edgeuse::p1::vertexuse_edgeuse [topology.edgeuse_p3 wired (PR #117): 2686 cases, 2250 pass, 0 failed, 436 deferred (intersect.surface_drop_lib_p3 92, topology.face_p2 344)] UPDATE (PR #149, lesson 30): the FFI Reader is the shared crate::reader::unit_reader!(MAX_COORD) (#186 left this unit for #149); its finite/index/below/flag/finites/vec3/nonzero3 stay unit methods (different messages or bounds), num/done come from the shared Cursor; the reader is private again and topology_brep_p2 reads blocks through surface_at / curve_at / brep_block_at. Report unchanged (2686 cases, 2250 pass, 436 deferred). Owner zone-tol cache (surface/owner-zone-tol): 2686 cases, 2594 pass, 0 fail, 92 deferred." [[unit]] id = "topology.vertex" @@ -2590,7 +2590,7 @@ tests = ["SM_API_test:TestSmOrderCurves", "SM_API_test:TestSmCurves", "SM_API_te status = "partial" claimed_by = "" claimed_at = "" -notes = "PARTIAL: difftest 3263 cases (2063 fixture: upstream.json re-expresses SM_API_test TestSmOrderCurves, TestSmCreatePlanarFaces, TestSmCreatePlanarCircle; edge_cases.json: empty/open/off-plane/degenerate loops, tolerance-limit gaps, 1e-6..1e6 scales, nested and side-by-side loops, zero-area loops (UB-119), iso/mirror tolerance limits, curve-loop scripts; seams.json; rv_ellipse.json: review cases, SmEllipse (inside-out, tiny radii) and SmLine UV curves in MirrorCurve / IsIsoCurve / IsIsoCurveOnSeam; breps.json: AreUVCurvesPresent, GetEdgeUVExtremes, CheckLoop, CheckFace, FixTrimLoopTinyParameterSpaceGaps, EnsureLastEdgeuseUVCorrect, FixBackwardsOuterLoopOnSeam (with a MirrorCurve pre-step making seam jumps), FixTrimLoopOrientation/Order on the 15 topology.vertexuse .smb files; 1200 generated over real SmBSplineCurve/SmLine/SmEllipse/circle arcs and SmBSplineSurface (open, closed in u or v, pinched, C0, rational)/SmPlane/SmCurveBoundedSurface), 2842 pass bit-identical, 421 deferred (topology.face_p3 318: SmFace::ComputeLoopOrientation, CreateUVTrimCurves; topology.face_p2 87; intersect.surface_drop_lib_p3 7: SmSurface::DropCurve in GetOrCreateUVTrimCurve (topology.edgeuse_p3 wired in PR #117: it no longer defers, FixTrimLoopOrder's call takes the kernel); topology.face_p4 9: GetEdges, HasSeamProblem), max_rel_err 0; --perturb SENSITIVE (2842/2842). After merging main (PR #52 surface.surface_p3) the 265 CheckLoop cases that deferred at GetSingularities pass bit-identical. UV trim curves that are SmEllipse/SmHyperbola/SmParabola run the inherited SmBSplineCurve GetCanonical/SetCanonical/EditEndPoint (review fix). B-rep ops normalise analytic geometry to B-spline copies as heal.gap. Back-edges wired: SmSurface::CreatePlaneWith3DCurves (surface.surface_p2) now calls OrderCurvesIntoLoops/FindOuterLoops (takes the Kernel). Upstream issues WR-96, WR-97, WR-98 (ported as is), UB-119, UB-120. Plan notes: 49 fns; largest: SmTrimmingTools::OrderCurvesIntoLoops, SmTrimmingTools::FindOuterLoops, SmTrimmingTools::CheckLoop, SmTrimmingTools::CheckFace; cycle back-edges (call into later units; stub or co-port): topology.face_p3(4), topology.edge(2), topology.face_p1(2), topology.face_p4(2), topology.brep_p2(1), topology.brep_p7(1); unit-SCC #22 (220 units); 168 upstream test entry points reach this unit [topology.face_p3 wired (PR #139, CreateUVTrimCurves, ComputeLoopOrientation): 3263 cases, 2842 pass, 0 failed, 421 deferred (topology.face_p2 392, intersect.surface_drop_lib_p3 17, topology.face_p4 12); the 318 topology.face_p3 deferrals now defer inside them] PR #183: trim_surface_with_model_space_curves takes a NotifyEnv for topology.brep_p7's MakeFaceWithCurves, which now calls topology.brep_p9's MakeShellVertex. topology.face_p4 (PR #239): with face_p3 and the face_p4 getters wired, CreateUVTrimCurves runs in Rust, so the shim now runs it too and both sides compare its status and every edge use's UV trim curve: 3263 cases, 2850 pass, 0 failed, 413 deferred (was 2842/421)." +notes = "PARTIAL: difftest 3263 cases (2063 fixture: upstream.json re-expresses SM_API_test TestSmOrderCurves, TestSmCreatePlanarFaces, TestSmCreatePlanarCircle; edge_cases.json: empty/open/off-plane/degenerate loops, tolerance-limit gaps, 1e-6..1e6 scales, nested and side-by-side loops, zero-area loops (UB-119), iso/mirror tolerance limits, curve-loop scripts; seams.json; rv_ellipse.json: review cases, SmEllipse (inside-out, tiny radii) and SmLine UV curves in MirrorCurve / IsIsoCurve / IsIsoCurveOnSeam; breps.json: AreUVCurvesPresent, GetEdgeUVExtremes, CheckLoop, CheckFace, FixTrimLoopTinyParameterSpaceGaps, EnsureLastEdgeuseUVCorrect, FixBackwardsOuterLoopOnSeam (with a MirrorCurve pre-step making seam jumps), FixTrimLoopOrientation/Order on the 15 topology.vertexuse .smb files; 1200 generated over real SmBSplineCurve/SmLine/SmEllipse/circle arcs and SmBSplineSurface (open, closed in u or v, pinched, C0, rational)/SmPlane/SmCurveBoundedSurface), 2842 pass bit-identical, 421 deferred (topology.face_p3 318: SmFace::ComputeLoopOrientation, CreateUVTrimCurves; topology.face_p2 87; intersect.surface_drop_lib_p3 7: SmSurface::DropCurve in GetOrCreateUVTrimCurve (topology.edgeuse_p3 wired in PR #117: it no longer defers, FixTrimLoopOrder's call takes the kernel); topology.face_p4 9: GetEdges, HasSeamProblem), max_rel_err 0; --perturb SENSITIVE (2842/2842). After merging main (PR #52 surface.surface_p3) the 265 CheckLoop cases that deferred at GetSingularities pass bit-identical. UV trim curves that are SmEllipse/SmHyperbola/SmParabola run the inherited SmBSplineCurve GetCanonical/SetCanonical/EditEndPoint (review fix). B-rep ops normalise analytic geometry to B-spline copies as heal.gap. Back-edges wired: SmSurface::CreatePlaneWith3DCurves (surface.surface_p2) now calls OrderCurvesIntoLoops/FindOuterLoops (takes the Kernel). Upstream issues WR-96, WR-97, WR-98 (ported as is), UB-119, UB-120. Plan notes: 49 fns; largest: SmTrimmingTools::OrderCurvesIntoLoops, SmTrimmingTools::FindOuterLoops, SmTrimmingTools::CheckLoop, SmTrimmingTools::CheckFace; cycle back-edges (call into later units; stub or co-port): topology.face_p3(4), topology.edge(2), topology.face_p1(2), topology.face_p4(2), topology.brep_p2(1), topology.brep_p7(1); unit-SCC #22 (220 units); 168 upstream test entry points reach this unit [topology.face_p3 wired (PR #139, CreateUVTrimCurves, ComputeLoopOrientation): 3263 cases, 2842 pass, 0 failed, 421 deferred (topology.face_p2 392, intersect.surface_drop_lib_p3 17, topology.face_p4 12); the 318 topology.face_p3 deferrals now defer inside them] PR #183: trim_surface_with_model_space_curves takes a NotifyEnv for topology.brep_p7's MakeFaceWithCurves, which now calls topology.brep_p9's MakeShellVertex. topology.face_p4 (PR #239): with face_p3 and the face_p4 getters wired, CreateUVTrimCurves runs in Rust, so the shim now runs it too and both sides compare its status and every edge use's UV trim curve: 3263 cases, 2850 pass, 0 failed, 413 deferred (was 2842/421). Owner zone-tol cache (surface/owner-zone-tol): 3263 cases, 3259 pass, 0 fail, 4 deferred. Shim runs CheckFace/FixTrimLoopOrientation/FixTrimLoopOrder (stale TT_NOT_RUN lifted)." [[unit]] id = "heal.gap" @@ -2632,7 +2632,7 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test: status = "partial" claimed_by = "" claimed_at = "" -notes = "CO-PORTED in topology.topology_traverser (PR #113): SmFace::GetEdges (default arguments) -> topology::topology_traverser::face_edges; 10 fns; largest: SmFace::HasSeamProblem, SmFace::FindVertexSector, SmFace::GlobalLineIntersect, SmFace::IsConnectedTo; cycle back-edges (call into later units; stub or co-port): heal.face_props(2), query.face_p2(2), query.face_p3(2); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in query.face_p2 (PR #104): SmFace::GetVertices(SmTArray&, ULONG*, SmBoolean, const SmTArray*) without attribute filter -> solidcore::query::face::p2::face_vertices PR #163: existing SmFace::GetEdges(defaults) co-port is public topology::topology_traverser::face_edges and reused by boolean.merge_p1; preserve repeats between loops. PARTIAL (port: topology.face_p4): difftest 5506 cases (810 zone-tolerance line cases from the review of PR #239, plus 4680 systematic over the 15 pt_TestFiles B-reps with and without rebuilt UV trim curves + 16 generated), 3957 pass bit-identical, 0 failed, 1549 deferred (geom.tol 1118: SmTol::GetZoneTol3d(const SmObject*) of the face-owned surface in the surface cache's subdivision test (the stub sits in surface.surface_cache_p2's code; main tags it geom.tol), deferred since #147's cache copy keeps the surface's owner (4136 passed before, matching the system tolerance by coincidence); topology.face_p2 292: SmFace::GetTolerance of a face-owned surface in IsClosed / the surface line intersection; intersect.curve_class_p2 50: periodic HasSeamProblem, past part 5's vertex/face gap (the runner's classification objects answer it from a copy of the B-rep) and GetLoopCrossings (intersect.curve_class_p4, wired), at curve_class_p4's own curve_class_p2 stubs; curve.crv_on_surf 80: inside query.face_p3's SectorClassifyCurveOn (sm_FaceClassifyUVLine and SectorClassifyCurveOn are wired since #132 merged; ClassifyProblem is query.face_p1's co-port, re-exported); intersect.surface_drop_lib_p3 9: SmSurface::DropCurve), max_rel_err 0, perturb SENSITIVE. Wired: GetEdges (brep_cache, trimming_tools, face_p1, heal_data_p3, geom.tol, query.face_p2; topology_traverser::face_edges and query::face::p2::face_vertices now delegate to face::p4), GetVertices (heal_data_p3), GetBrep / GetVertices / GetEdges (heal.stitch_p1, 2 cases now pass), GetBrep / GetEdges (topology.brep_p7) and GetVertices / GetEdges (topology.brep_p10, both latent: no case reaches them yet), GetUpwardLoopuses / GetUpwardEdgeuses / GetUpwardVertexuses / GetEdges(outer loop) / GetBrep (topology.face_p3: 823 pass, 654 without), GetEdges / GetVertices (topology.brep_p5) and FindVertexSector (topology.brep_p9, both latent), GetBrep / GetUpwardLoopuses / GetUpwardVertexuses (face_p1), GlobalLineIntersect (topology_traverser::is_inner_shell, on a B-rep copy). Kept UNPORTED(topology.face_p4) markers where the caller has no B-rep or classification-object mapper (generic handles): brep_p5 (FindVertexSector: its FaceEdgeBackend has no kernel), boolean.topology_intersector_p1 (FindVertexSector), point_class (GetBrep, IsConnectedTo), curve_class_p7 (FindVertexSector), trim_srf_cache / surface_p7 / boolean.topology_intersector_p1 (GetBrep, GetVertices, GetEdges), proto_topology_p1 (GetEdges of a ProtoTopology face), and HasSeamProblem in face_p1 (MoveSeam/SplitAtSeam), trimming_tools and heal.face_props::set_props_stage2 (callers with a mapper use FaceProps::set_props_stage2_with). upstream-issues UB-180, WR-176 (HasSeamProblem returns SM_SUCCESS as TRUE), WR-177." +notes = "CO-PORTED in topology.topology_traverser (PR #113): SmFace::GetEdges (default arguments) -> topology::topology_traverser::face_edges; 10 fns; largest: SmFace::HasSeamProblem, SmFace::FindVertexSector, SmFace::GlobalLineIntersect, SmFace::IsConnectedTo; cycle back-edges (call into later units; stub or co-port): heal.face_props(2), query.face_p2(2), query.face_p3(2); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in query.face_p2 (PR #104): SmFace::GetVertices(SmTArray&, ULONG*, SmBoolean, const SmTArray*) without attribute filter -> solidcore::query::face::p2::face_vertices PR #163: existing SmFace::GetEdges(defaults) co-port is public topology::topology_traverser::face_edges and reused by boolean.merge_p1; preserve repeats between loops. PARTIAL (port: topology.face_p4): difftest 5506 cases (810 zone-tolerance line cases from the review of PR #239, plus 4680 systematic over the 15 pt_TestFiles B-reps with and without rebuilt UV trim curves + 16 generated), 3957 pass bit-identical, 0 failed, 1549 deferred (geom.tol 1118: SmTol::GetZoneTol3d(const SmObject*) of the face-owned surface in the surface cache's subdivision test (the stub sits in surface.surface_cache_p2's code; main tags it geom.tol), deferred since #147's cache copy keeps the surface's owner (4136 passed before, matching the system tolerance by coincidence); topology.face_p2 292: SmFace::GetTolerance of a face-owned surface in IsClosed / the surface line intersection; intersect.curve_class_p2 50: periodic HasSeamProblem, past part 5's vertex/face gap (the runner's classification objects answer it from a copy of the B-rep) and GetLoopCrossings (intersect.curve_class_p4, wired), at curve_class_p4's own curve_class_p2 stubs; curve.crv_on_surf 80: inside query.face_p3's SectorClassifyCurveOn (sm_FaceClassifyUVLine and SectorClassifyCurveOn are wired since #132 merged; ClassifyProblem is query.face_p1's co-port, re-exported); intersect.surface_drop_lib_p3 9: SmSurface::DropCurve), max_rel_err 0, perturb SENSITIVE. Wired: GetEdges (brep_cache, trimming_tools, face_p1, heal_data_p3, geom.tol, query.face_p2; topology_traverser::face_edges and query::face::p2::face_vertices now delegate to face::p4), GetVertices (heal_data_p3), GetBrep / GetVertices / GetEdges (heal.stitch_p1, 2 cases now pass), GetBrep / GetEdges (topology.brep_p7) and GetVertices / GetEdges (topology.brep_p10, both latent: no case reaches them yet), GetUpwardLoopuses / GetUpwardEdgeuses / GetUpwardVertexuses / GetEdges(outer loop) / GetBrep (topology.face_p3: 823 pass, 654 without), GetEdges / GetVertices (topology.brep_p5) and FindVertexSector (topology.brep_p9, both latent), GetBrep / GetUpwardLoopuses / GetUpwardVertexuses (face_p1), GlobalLineIntersect (topology_traverser::is_inner_shell, on a B-rep copy). Kept UNPORTED(topology.face_p4) markers where the caller has no B-rep or classification-object mapper (generic handles): brep_p5 (FindVertexSector: its FaceEdgeBackend has no kernel), boolean.topology_intersector_p1 (FindVertexSector), point_class (GetBrep, IsConnectedTo), curve_class_p7 (FindVertexSector), trim_srf_cache / surface_p7 / boolean.topology_intersector_p1 (GetBrep, GetVertices, GetEdges), proto_topology_p1 (GetEdges of a ProtoTopology face), and HasSeamProblem in face_p1 (MoveSeam/SplitAtSeam), trimming_tools and heal.face_props::set_props_stage2 (callers with a mapper use FaceProps::set_props_stage2_with). upstream-issues UB-180, WR-176 (HasSeamProblem returns SM_SUCCESS as TRUE), WR-177. Owner zone-tol cache (surface/owner-zone-tol): 5506 cases, 4222 pass, 0 fail, 1284 deferred." [[unit]] id = "query.face_p2" @@ -2674,7 +2674,7 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin status = "partial" claimed_by = "" claimed_at = "" -notes = "PARTIAL (PR #139): difftest 1758 cases (1742 fixture in fixtures/topology.face_p3/constructed.json: every op (IsLamina, HasLaminaEdge, HasConsecutiveLaminaEdges, SetTolerance with both flags, Notify PRE_EDIT/POST_EDIT/CHANGE_GEOMETRY/ADD_TO_BREP/RM_FROM_BREP with and without cached gaps, ComputeLoopOrientation with/without UV bounds and 3D-loop flag, IsDegenerate with/without quick check and zone tolerances incl. ones above every edge (all-degenerate TRUE), CreateComposites, CreateUVTrimCurves with/without tolerance adjustment, FaceIntersect with coincidence flag unset/FALSE/TRUE and points) on 7 upstream .smb files (plain and RebuildUVTrimCurves), plane / tilted plane / bilinear / pole / biquadratic faces, the my_test_topology wire network every Endcap face pair, 112 faces with an inner vertex loop (MakeVertexLoop) and 80 CreateUVTrimCurves cases passing the face's own UV curves; 16 generated over the 15 topology.vertexuse files, wire edges, faces and the network),530 pass bit-identical (max_rel_err 0), 0 failed, 1228 deferred (intersect.curve_class_p4 206, topology.edgeuse_p3 420, topology.face_p2 122, topology.face_p4 144; plus 336 import/dispatch-gap deferrals, surface.cone_p1 224 and surface.surf_of_extrusion_p2 112, owned by the SurfaceKind variant work, not by a pending port: the runner's inexact_geometry maps the C++ type codes 15012/15013/15015/15030 to those units). Values compare for the lamina queries, SetTolerance, Notify, ComputeLoopOrientation of an inner vertex loop, and the empty-intersection and all-degenerate FaceIntersect/IsDegenerate cases; CreateComposites, ComputeLoopOrientation of edge loops and CreateUVTrimCurves (also with its own UV curves) compare nothing yet (all deferred, see plan/findings.md); --perturb SENSITIVE; probe 3000 seeds clean. Harness: the topology.vertexuse B-rep blocks and graph import (SmPlane as a plane, SmLine as a line); geometric ops on B-reps holding an analytic surface/curve kind the import keeps only as its B-spline part defer to the kind's unit (as topology.edgeuse_p2). Unported callees stubbed: SmEdgeuse::NormalizedEvaluate, GetOrCreateUVTrimCurve (topology.edgeuse_p3, PR #117), SmFace::GetEdges(outer loop), GetUpwardLoopuses/Edgeuses/Vertexuses, GetBrep (topology.face_p4, PR #134), SmCurveClassification constructor (intersect.curve_class_p4), SmFace::Point3DClassify (query.face_p1). Wired back-edges: heal.heal_data_p1 (CreateUVTrimCurves), boolean.merge_p1 (SetTolerance), and in this unit InsertTopologyIntersections / ExtractGeometry (intersect.curve_class_p5) and SmSurface::CoincidenceCheck (surface.surface_p8) after merging main; topology.vertex (SmFace::SetTolerance), topology.trimming_tools (CreateUVTrimCurves, ComputeLoopOrientation), topology.loopuse (SmFace::Notify around FlipLoopOrientation), heal.heal_data_p2 (SetTolerance, IsDegenerate); wired by merging main (PR #139 round): topology.face_p1 (CreateUVTrimCurves and ten of eleven Notify calls, through topology.loopuse's face_notify_edit environment), geom.tol (SmFace::SetTolerance) and boolean.topology_intersector_p1 (SmFace::FaceIntersect through IntersectorBrep::face_intersect); stubs left with justification in plan/findings.md (query.poly SetOriginalFace Notify, surface.trim_srf_cache CreateUVTrimCurves: opaque face handles; topology.face_p1 SetSurface's CHANGE_GEOMETRY Notify: surfaces have no identity). Upstream defects UB-189, UB-193. CO-PORTED in topology.topology_traverser (PR #113): SmFace::HasLaminaEdge -> topology::topology_traverser::has_lamina_edge (re-exported here); SmFace::GetEdges with default arguments -> topology_traverser::face_edges. [Review round 2026-10-18 (merged main e1ae2640: edgeuse_p3 #117, shell #154, brep_p6 #169): 1774 cases (1758 fixture: + a rational closed B-spline cylinder face (seam; ComputeLoopOrientation 3D/UV with bounds, vertex loop, IsDegenerate, CreateComposites, CreateUVTrimCurves), ComputeLoopOrientation of another face's loop on plane_square.smb, and negative SetTolerance / IsDegenerate tolerances (SmZoneTol3d fabs) from the probe), 700 pass (697 compare values, 3 none; ComputeLoopOrientation of edge loops in 3D now compares orientation, bounds and use states, 43-51 values per case), 0 failed, 1074 deferred (topology.face_p4 268, surface.surface_p2 224, intersect.curve_class_p4 207, topology.face_p2 148, intersect.surface_drop_lib_p3 115, surface.surf_of_extrusion_p2 112), max_rel_err 0, --perturb SENSITIVE. Wired SmEdgeuse::NormalizedEvaluate / GetOrCreateUVTrimCurve (topology.edgeuse_p3); CreateUVTrimCurves reads poles with get_singularities_of_face (no face_p2 stub). The import/dispatch-gap deferrals name surface.surface_p2 (the SurfaceKind dispatch, issue #76) for SmCone/SmCylinder/SmSphere/SmTorus (their own units are green) and surface.surf_of_extrusion_p2 for the extrusion. The shim's -2 null check for ComputeLoopOrientation follows the selected loop (vertex loops never -2). Justified stubs added: topology.brep_p6 MakeFaceTopology SetTolerance (that unit's own unported remainder follows) and topology.edgeuse_p3 EvaluateBinormal's surface cache CreateUVTrimCurves (opaque face handle, as surface.trim_srf_cache).] Batch integration (batch/clean-2026-10-19): the SmCurveClassification constructor is wired to CurveClassification::with_curve (intersect.curve_class_p4); those cases now defer on its ReSet (intersect.curve_class_p2). topology.face_p4 (PR #239) wired GetUpwardLoopuses / GetUpwardEdgeuses / GetUpwardVertexuses (default arguments), GetEdges(outer loop only) and GetBrep (Brep tolerance): after main through cfa9d068: 1781 cases, 823 pass, 0 failed, 958 deferred (main without it: 654/1127)." +notes = "PARTIAL (PR #139): difftest 1758 cases (1742 fixture in fixtures/topology.face_p3/constructed.json: every op (IsLamina, HasLaminaEdge, HasConsecutiveLaminaEdges, SetTolerance with both flags, Notify PRE_EDIT/POST_EDIT/CHANGE_GEOMETRY/ADD_TO_BREP/RM_FROM_BREP with and without cached gaps, ComputeLoopOrientation with/without UV bounds and 3D-loop flag, IsDegenerate with/without quick check and zone tolerances incl. ones above every edge (all-degenerate TRUE), CreateComposites, CreateUVTrimCurves with/without tolerance adjustment, FaceIntersect with coincidence flag unset/FALSE/TRUE and points) on 7 upstream .smb files (plain and RebuildUVTrimCurves), plane / tilted plane / bilinear / pole / biquadratic faces, the my_test_topology wire network every Endcap face pair, 112 faces with an inner vertex loop (MakeVertexLoop) and 80 CreateUVTrimCurves cases passing the face's own UV curves; 16 generated over the 15 topology.vertexuse files, wire edges, faces and the network),530 pass bit-identical (max_rel_err 0), 0 failed, 1228 deferred (intersect.curve_class_p4 206, topology.edgeuse_p3 420, topology.face_p2 122, topology.face_p4 144; plus 336 import/dispatch-gap deferrals, surface.cone_p1 224 and surface.surf_of_extrusion_p2 112, owned by the SurfaceKind variant work, not by a pending port: the runner's inexact_geometry maps the C++ type codes 15012/15013/15015/15030 to those units). Values compare for the lamina queries, SetTolerance, Notify, ComputeLoopOrientation of an inner vertex loop, and the empty-intersection and all-degenerate FaceIntersect/IsDegenerate cases; CreateComposites, ComputeLoopOrientation of edge loops and CreateUVTrimCurves (also with its own UV curves) compare nothing yet (all deferred, see plan/findings.md); --perturb SENSITIVE; probe 3000 seeds clean. Harness: the topology.vertexuse B-rep blocks and graph import (SmPlane as a plane, SmLine as a line); geometric ops on B-reps holding an analytic surface/curve kind the import keeps only as its B-spline part defer to the kind's unit (as topology.edgeuse_p2). Unported callees stubbed: SmEdgeuse::NormalizedEvaluate, GetOrCreateUVTrimCurve (topology.edgeuse_p3, PR #117), SmFace::GetEdges(outer loop), GetUpwardLoopuses/Edgeuses/Vertexuses, GetBrep (topology.face_p4, PR #134), SmCurveClassification constructor (intersect.curve_class_p4), SmFace::Point3DClassify (query.face_p1). Wired back-edges: heal.heal_data_p1 (CreateUVTrimCurves), boolean.merge_p1 (SetTolerance), and in this unit InsertTopologyIntersections / ExtractGeometry (intersect.curve_class_p5) and SmSurface::CoincidenceCheck (surface.surface_p8) after merging main; topology.vertex (SmFace::SetTolerance), topology.trimming_tools (CreateUVTrimCurves, ComputeLoopOrientation), topology.loopuse (SmFace::Notify around FlipLoopOrientation), heal.heal_data_p2 (SetTolerance, IsDegenerate); wired by merging main (PR #139 round): topology.face_p1 (CreateUVTrimCurves and ten of eleven Notify calls, through topology.loopuse's face_notify_edit environment), geom.tol (SmFace::SetTolerance) and boolean.topology_intersector_p1 (SmFace::FaceIntersect through IntersectorBrep::face_intersect); stubs left with justification in plan/findings.md (query.poly SetOriginalFace Notify, surface.trim_srf_cache CreateUVTrimCurves: opaque face handles; topology.face_p1 SetSurface's CHANGE_GEOMETRY Notify: surfaces have no identity). Upstream defects UB-189, UB-193. CO-PORTED in topology.topology_traverser (PR #113): SmFace::HasLaminaEdge -> topology::topology_traverser::has_lamina_edge (re-exported here); SmFace::GetEdges with default arguments -> topology_traverser::face_edges. [Review round 2026-10-18 (merged main e1ae2640: edgeuse_p3 #117, shell #154, brep_p6 #169): 1774 cases (1758 fixture: + a rational closed B-spline cylinder face (seam; ComputeLoopOrientation 3D/UV with bounds, vertex loop, IsDegenerate, CreateComposites, CreateUVTrimCurves), ComputeLoopOrientation of another face's loop on plane_square.smb, and negative SetTolerance / IsDegenerate tolerances (SmZoneTol3d fabs) from the probe), 700 pass (697 compare values, 3 none; ComputeLoopOrientation of edge loops in 3D now compares orientation, bounds and use states, 43-51 values per case), 0 failed, 1074 deferred (topology.face_p4 268, surface.surface_p2 224, intersect.curve_class_p4 207, topology.face_p2 148, intersect.surface_drop_lib_p3 115, surface.surf_of_extrusion_p2 112), max_rel_err 0, --perturb SENSITIVE. Wired SmEdgeuse::NormalizedEvaluate / GetOrCreateUVTrimCurve (topology.edgeuse_p3); CreateUVTrimCurves reads poles with get_singularities_of_face (no face_p2 stub). The import/dispatch-gap deferrals name surface.surface_p2 (the SurfaceKind dispatch, issue #76) for SmCone/SmCylinder/SmSphere/SmTorus (their own units are green) and surface.surf_of_extrusion_p2 for the extrusion. The shim's -2 null check for ComputeLoopOrientation follows the selected loop (vertex loops never -2). Justified stubs added: topology.brep_p6 MakeFaceTopology SetTolerance (that unit's own unported remainder follows) and topology.edgeuse_p3 EvaluateBinormal's surface cache CreateUVTrimCurves (opaque face handle, as surface.trim_srf_cache).] Batch integration (batch/clean-2026-10-19): the SmCurveClassification constructor is wired to CurveClassification::with_curve (intersect.curve_class_p4); those cases now defer on its ReSet (intersect.curve_class_p2). topology.face_p4 (PR #239) wired GetUpwardLoopuses / GetUpwardEdgeuses / GetUpwardVertexuses (default arguments), GetEdges(outer loop only) and GetBrep (Brep tolerance): after main through cfa9d068: 1781 cases, 823 pass, 0 failed, 958 deferred (main without it: 654/1127). Owner zone-tol cache (surface/owner-zone-tol): 1781 cases, 971 pass, 0 fail, 810 deferred." [[unit]] id = "topology.topology_traverser" @@ -2702,7 +2702,7 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin status = "partial" claimed_by = "" claimed_at = "" -notes = "PARTIAL (PR #132; review fixes 2026-10-19): difftest 1199 cases (201 fixture: CalculateTightBoundingBox / CalculateAnInternalPoint / the loop bisector over every face of the 13 upstream TestFiles .smb files of unit topology.edgeuse_p1 (with and without RebuildUVTrimCurves), SmApiFaceBoundingBox(TRUE) planar faces (TestSmBrep.cpp:3023) and the holed-box top face of TestSmIntersectors.cpp:405 flat and tilted, SectorClassifyCurveOn on plane_square.smb at every edge use (edge interior, vertex with the CW neighbour, radial; 3D lines in, out and reversed, UV lines, singular); 398 review adversarial fixtures (fixtures/query.face_p3/review_adversarial.json: plane and flat B-spline faces, sector sweeps over the .smb files); 600 generated: SmPlane faces at scales 1e-3..1e3, B-spline faces (flat and curved, degrees 1..3) with rectangle / circle trims and holes, CreateFaceFromSurface faces, .smb files, real SmCylinder/SmSphere/SmTorus/SmCone faces; sector edge fractions 0/1/1e-6, tolerances 0..1e-3, uv_space_okay on/off), 523 pass (288 compare values, 235 both inexact_geometry, rejected or the same status), 0 failed, 676 deferred (topology.face_p2 305: SmFace::GetTolerance of a face-owned surface in the global solves of CalculateTightBoundingBox and DropPoint of the bisector; intersect.surface_drop_lib_p3 215 and topology.face_p3 55: SmSurface::DropCurve / SmFace::CreateUVTrimCurves reached through SmEdgeuse::EvaluateBinormal and, for CalculateAnInternalPoint, CurveOnClassify; curve.crv_on_surf 99: SmCrvOnSurf over an SmPlane; query.face_p2 2); max_rel_err 0; --perturb 523/523 detected. Per function, values are compared only for SmFace::CalculateTightBoundingBox (192 cases) and SmFace::SectorClassifyCurveOn (96); sm_FaceClassifyUVLine (through CalculateAnInternalPoint, 97 cases) and sms_FindAndClassifyLoopBisector (154 cases) never compare values: every case defers or both sides return the same status. sm_FaceClassifyUVLine is file-static: compared through SmFace::CalculateAnInternalPoint. Builds on PR #129 (query.face_p4, with #122 heal.face_props and #85 intersect.curve_class_p2) merged into the branch; the shim compiles the query.face_p4 shim's B-rep builders into its translation unit. Wired UNPORTED(query.face_p3): sms_FindAndClassifyLoopBisector in query::face::p4 (classify_loops returns InvalidInput for a handle type that cannot name the bisector's curves; the library's own healer handle topology::brep::p1::HealLoopHandle is now an enum that names them, as are the query.face_p4 and heal.face_props runner handles), sm_FaceClassifyUVLine in topology::face::p2::calculate_an_internal_point, SmFace::CalculateTightBoundingBox in query::api_queries::face_bounding_box. The bisector's seam flags are written through find_and_classify_loop_bisector_into (upstream's reference out-params: initialised after the domain check, kept when it fails later, read by ClassifyLoops). Also wired: SmEdgeuse::EvaluateBinormal (topology.edgeuse_p3); SmCurveClassification::ReSet in query::face::p2 and heal::face_props came from main (PR #85). LoopObject gained from_face/from_face_surface/from_classify_curve (default None). FFI guard: the tight op rejects B-rep values beyond 1e6 (TIGHT_MAX_COORD; upstream's global solves take minutes at 1e10..1e300, not undefined). Not compared: the shim's sector orientations are always eu1 SAME / eu2 OPPOSITE (the OPPOSITE tangent flip and PART 2's eu2/mate vertex use choices are not reached), and the tight op's has_trim_cache output is not asserted. Upstream SmTArray::FindElementInMaskedArray tests `mask & (1