From bac283053cad5a247b7368330c65ecb919dabb63 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 17:41:44 -0700 Subject: [PATCH 1/6] claim: face-bound trimmed surface cache and IntersectTSBound's cached branch Co-Authored-By: Claude Opus 5.5 From 2aa5ea77295bfa63e02b4386e9df5df0b95bddc1 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 18:03:59 -0700 Subject: [PATCH 2/6] surface caches: keep a face surface's trimmed cache on the surface; IntersectTSBound's cached branch The surface's cache slot now holds its SmTrimSrfCache: smsurf_GetSurfaceCache of a face surface reuses it (CacheMakeOrValidate), PointClassify reads it (GetObjectCache), Notify(PRE_EDIT) marks it modified. IntersectTSBound fetches it and takes the cached leaf-node branch with its context mark (SmNewMarkAndLock, claimed through atomic context marks). Co-Authored-By: Claude Opus 5.5 --- .../src/units/intersect_curve_class_p9.rs | 1 + crates/solidcore/src/geom/context.rs | 99 +++-- .../solidcore/src/intersect/curve_class/p9.rs | 37 +- crates/solidcore/src/query/face/p2.rs | 402 +++++++++++++----- crates/solidcore/src/surface/surface.rs | 2 +- crates/solidcore/src/surface/surface/p2.rs | 183 ++++++-- .../solidcore/src/surface/surface_cache/p1.rs | 6 + crates/solidcore/src/topology/edgeuse/p3.rs | 12 +- crates/solidcore/src/topology/face/p3.rs | 5 +- crates/solidcore/src/topology/face/p4.rs | 65 +-- 10 files changed, 609 insertions(+), 203 deletions(-) diff --git a/crates/difftest/src/units/intersect_curve_class_p9.rs b/crates/difftest/src/units/intersect_curve_class_p9.rs index cad60b1ff..6e0413f3c 100644 --- a/crates/difftest/src/units/intersect_curve_class_p9.rs +++ b/crates/difftest/src/units/intersect_curve_class_p9.rs @@ -546,6 +546,7 @@ fn rust(i: &Input) -> ScResult { surface: &pool.surface, loops: &loops, cache: &cache, + cache_handle: &Some, }, i.check3d, ) diff --git a/crates/solidcore/src/geom/context.rs b/crates/solidcore/src/geom/context.rs index 0a27fe611..c79072f3b 100644 --- a/crates/solidcore/src/geom/context.rs +++ b/crates/solidcore/src/geom/context.rs @@ -45,6 +45,7 @@ //! | `lock_mark`, `unlock_mark` (private) | `SmNewMarkAndLock::LockMark`, `UnlockMark` | //! | `unlocked_mark` (private) | `SmNewMarkAndLock::GetUnlockedMark` | //! | [`with_mark_lock`] | the scoped `{ SmNewMarkAndLock sMarkLock(pContext, mask); ... }` idiom | +//! | [`Context::claim_mark`], [`SharedMarkLock`] | the same idiom in a `const` query (`SmNewMarkAndLock(const SmContext*, mask)`) | //! | [`MassPropertyBehavior`] | `SmMassPropertyBehavior` (and its constructor) | //! | [`create_context`] | `SmApiCreateContext` | //! | [`get_or_create_context`] | `SmApiGetOrCreateContext` | @@ -58,6 +59,10 @@ //! [`Kernel`]'s own context. A method that upstream would run through a null //! context pointer (a crash) returns [`SolidError::InvalidInput`], as does an index //! outside the slice. +//! - The marks and their locks are atomics, so that a `const` query that only holds the +//! context shared (the port's `&Kernel`) can still claim a mark as upstream's do +//! ([`Context::claim_mark`], whose guard unlocks on drop). The kernel is +//! single-threaded: the accesses happen in program order. //! - The upstream two-context constructor locks the *first* context's mark type in the //! second context and increments the first context's mark twice; this is ported //! as is (see `plan/findings.md`). @@ -76,7 +81,7 @@ //! `Dump`, `GetTypeString` and `IsLockedMark` are unused upstream and not ported. use std::sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, Arc, }; @@ -187,11 +192,14 @@ pub trait NotifyCallback: std::fmt::Debug + Send { #[derive(Debug)] pub struct Context { /// `m_lCurrentMark`, `m_lCurrentMark2`, `m_lCurrentMark3`, `m_lCurrentMarkIO`, - /// `m_lCurrentMarkAssert`. - current_marks: [u64; 5], + /// `m_lCurrentMarkAssert`. Atomic so that a query holding the context shared can + /// claim a mark ([`Context::claim_mark`]), as upstream's `const` queries do + /// through `SmNewMarkAndLock(const SmContext*, ..)`; single-threaded use orders the + /// accesses. + current_marks: [AtomicU64; 5], /// `m_bMarkLock`, `m_bMark2Lock`, `m_bMark3Lock`, `m_bMarkIOLock`, /// `m_bMarkAssertLock`. - mark_locks: [bool; 5], + mark_locks: [AtomicBool; 5], /// `m_bDoingBoolean`. doing_boolean: bool, /// `m_dThisModelSizeEstimate`. @@ -460,8 +468,14 @@ impl Context { /// An immutable metadata snapshot, without copying callback ownership. pub(crate) fn metadata_copy(&self) -> Self { Self { - current_marks: self.current_marks, - mark_locks: self.mark_locks, + current_marks: self + .current_marks + .each_ref() + .map(|m| AtomicU64::new(m.load(Ordering::Relaxed))), + mark_locks: self + .mark_locks + .each_ref() + .map(|l| AtomicBool::new(l.load(Ordering::Relaxed))), doing_boolean: self.doing_boolean, model_size_estimate: self.model_size_estimate, large_small_size_ratio: self.large_small_size_ratio, @@ -479,8 +493,8 @@ impl Context { #[must_use] pub fn new(model_size_estimate: f64, large_small_size_ratio: f64) -> Self { Self { - current_marks: [1; 5], - mark_locks: [false; 5], + current_marks: [1; 5].map(AtomicU64::new), + mark_locks: [false; 5].map(AtomicBool::new), doing_boolean: false, model_size_estimate, large_small_size_ratio, @@ -676,34 +690,27 @@ impl Context { pub fn current_mark(&self, mark_type: MarkType) -> u64 { match mark_type { MarkType::NoMark => { - let [m1, m2, m3, mio, massert] = self.current_marks; + let [m1, m2, m3, mio, massert] = + self.current_marks.each_ref().map(|m| m.load(Ordering::Relaxed)); 1 + max5(m1, m2, m3, mio, massert) } _ => self .current_marks .get(mark_type.slot().unwrap_or(MARK_IO)) - .copied() - .unwrap_or(0), + .map_or(0, |m| m.load(Ordering::Relaxed)), } } /// Advances a mark by two and returns the new value (`SmContext::NewMark`; the /// lock check is a debug-only assertion upstream). [`MarkType::NoMark`] changes /// nothing and returns [`Context::current_mark`] of it. - fn new_mark(&mut self, mark_type: MarkType) -> u64 { + fn new_mark(&self, mark_type: MarkType) -> u64 { if mark_type == MarkType::NoMark { return self.current_mark(MarkType::NoMark); } - match self - .current_marks - .get_mut(mark_type.slot().unwrap_or(MARK_IO)) - { - Some(m) => { - *m = m.wrapping_add(2); - *m - } - None => 0, - } + self.current_marks + .get(mark_type.slot().unwrap_or(MARK_IO)) + .map_or(0, |m| m.fetch_add(2, Ordering::Relaxed).wrapping_add(2)) } /// Whether a mark is locked (`IsMarkLocked`); never for [`MarkType::NoMark`] or @@ -713,14 +720,52 @@ impl Context { mark_type .slot() .and_then(|s| self.mark_locks.get(s)) - .copied() - .unwrap_or(false) + .is_some_and(|l| l.load(Ordering::Relaxed)) } /// Sets a mark's lock flag (no-op for marks without a slot). - fn set_lock(&mut self, mark_type: MarkType, locked: bool) { - if let Some(l) = mark_type.slot().and_then(|s| self.mark_locks.get_mut(s)) { - *l = locked; + fn set_lock(&self, mark_type: MarkType, locked: bool) { + if let Some(l) = mark_type.slot().and_then(|s| self.mark_locks.get(s)) { + l.store(locked, Ordering::Relaxed); + } + } + + /// `SmNewMarkAndLock(const SmContext*, mask)` for a holder of the context that + /// cannot change it otherwise (upstream's `const` queries): picks the first + /// unlocked mark allowed by `mask` as [`MarkLock::new`] does, advances and locks + /// it until the returned guard is dropped (`~SmNewMarkAndLock`). + #[must_use] + pub fn claim_mark(&self, mask: i32) -> SharedMarkLock<'_> { + let mark_type = unlocked_mark(self, mask); + self.new_mark(mark_type); + self.set_lock(mark_type, true); + SharedMarkLock { + context: self, + mark_type, + } + } +} + +/// A mark claimed through a shared context ([`Context::claim_mark`]); dropping it +/// unlocks the mark if it is still locked (`~SmNewMarkAndLock`). +#[derive(Debug)] +pub struct SharedMarkLock<'a> { + context: &'a Context, + mark_type: MarkType, +} + +impl SharedMarkLock<'_> { + /// The claimed mark (`GetMarkType`). + #[must_use] + pub const fn mark_type(&self) -> MarkType { + self.mark_type + } +} + +impl Drop for SharedMarkLock<'_> { + fn drop(&mut self) { + if self.context.is_mark_locked(self.mark_type) { + self.context.set_lock(self.mark_type, false); } } } diff --git a/crates/solidcore/src/intersect/curve_class/p9.rs b/crates/solidcore/src/intersect/curve_class/p9.rs index 81cc4cc21..01146c7c1 100644 --- a/crates/solidcore/src/intersect/curve_class/p9.rs +++ b/crates/solidcore/src/intersect/curve_class/p9.rs @@ -8,8 +8,11 @@ //! | [`CurveClassification::intersect_ts_bound`] | `SmCurveClassification::IntersectTSBound` | //! //! Deviations: topology is supplied as borrowed views resolved from arena ids, and -//! the caller supplies the face's trim cache. Cached marks are local ordered lists -//! of visited handles. No input-driven recursion is added. Temporary surface-owner +//! the caller supplies the face's trim cache (in its own handle space, mapped to the +//! caller's by [`BoundaryFace::cache_handle`]). The cached branch claims and locks a +//! context mark as upstream's `SmNewMarkAndLock` does ([`crate::geom::context::Context::claim_mark`]); +//! the vertex uses and edge uses it marks are a local ordered list of visited +//! handles. No input-driven recursion is added. Temporary surface-owner //! clearing is represented by a private copy for a lifted 2D source. Concrete //! curve-on-surface dispatch is preserved; mutable edge insertion retains its dependency //! deferral. Ignored native evaluation @@ -27,6 +30,7 @@ use crate::curve::curve::p6::DroppedPoint; use crate::curve::curve::p8::CurveIntersectOptions; use crate::curve::curve::Curve; use crate::error::{ignored, Result, SolidError}; +use crate::geom::context::ALL_MARKS; use crate::geom::extent1d::Extent1d; use crate::geom::extent2d::Extent2d; use crate::geom::global_solver::SolverOperation; @@ -96,13 +100,17 @@ pub struct BoundaryLoop { /// Resolved upward faceuse traversal and the face's actual surface and trim cache. /// These views borrow geometry; they do not replace the B-rep arenas. -pub struct BoundaryFace<'a, O> { +pub struct BoundaryFace<'a, O, C = O> { /// Face surface. pub surface: &'a Surface, /// Upward loopuses, in native order. pub loops: &'a [BoundaryLoop], - /// Caller-owned trimmed surface cache (`GetSurfaceCache`). - pub cache: &'a TrimSrfCache, + /// The face surface's trimmed surface cache (`smsurf_GetSurfaceCache`), whose + /// vertex use and edge use handles are `C`. + pub cache: &'a TrimSrfCache, + /// The caller's handle of a vertex use or edge use the cache's leaf nodes name + /// (`None`: not one of the face's). + pub cache_handle: &'a dyn Fn(C) -> Option, } /// `IntersectEdges` flags. `has_big_gaps` is required upstream; this Rust options @@ -291,11 +299,11 @@ impl CurveClassification { /// # Errors /// Missing geometry/cache records fail; generic surface lifting and mutable /// edge insertion retain their existing dependency deferrals. - pub fn intersect_ts_bound( + pub fn intersect_ts_bound( &mut self, k: &Kernel, geometry: &impl ClassGeometry, - face: &BoundaryFace<'_, O>, + face: &BoundaryFace<'_, O, C>, do_check_3d: bool, ) -> Result<()> { let source_handle = self.curve().ok_or(SolidError::null_pointer(OP))?; @@ -337,6 +345,16 @@ impl CurveClassification { let mut domain = Extent2d::from_extent3d(&tree.node(top)?.bbox); domain.expand_absolute(EFF_ZERO_SQRT); let nodes = base.find_uv_nodes_built(k, &domain, false)?; + + // no work - curve is outside of the domain of the surface + if nodes.is_empty() { + return Ok(()); + } + + // increment the mark on the context by 1 (the vertex uses and edge uses it + // marks are the ordered `visited` list) + let _mark_lock = k.context.claim_mark(ALL_MARKS); + let tree = base.tree().ok_or(SolidError::null_pointer(OP))?; let mut visited = Vec::new(); for &id in &nodes { @@ -345,6 +363,7 @@ impl CurveClassification { } for item in &base.aux(id)?.vertex_list { let vu = item.vertexuse.ok_or(SolidError::null_pointer(OP))?; + let vu = (face.cache_handle)(vu).ok_or(SolidError::null_pointer(OP))?; if visited.contains(&vu) { continue; } @@ -365,6 +384,7 @@ impl CurveClassification { continue; } for &eu in &base.aux(id)?.edgeuse_list { + let eu = (face.cache_handle)(eu).ok_or(SolidError::null_pointer(OP))?; if visited.contains(&eu) { continue; } @@ -673,7 +693,8 @@ mod tests { &BoundaryFace { surface: &surface, loops: &loops, - cache: &cache + cache: &cache, + cache_handle: &Some, }, false ), diff --git a/crates/solidcore/src/query/face/p2.rs b/crates/solidcore/src/query/face/p2.rs index 0d61f9455..a20e550f0 100644 --- a/crates/solidcore/src/query/face/p2.rs +++ b/crates/solidcore/src/query/face/p2.rs @@ -118,9 +118,12 @@ use crate::kernel::Kernel; use crate::math::{max2, Point2, Point3, Vec3, BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQRT}; use crate::nurbs::b_spline_curve::BSplineCurve; use crate::query::r#loop::{contains_uv_point_with_topology, UvContainment}; -use crate::surface::surface::p1::{get_surface_cache_with_face, get_trim_surface_cache}; -use crate::surface::surface::{Surface, SurfaceKind}; -use crate::surface::surface_cache::p1::{FaceContainment, SurfaceCache, SurfaceCacheOptions}; +use crate::surface::surface::p1::{ + get_surface_cache_with_face, get_trim_surface_cache, make_surface_cache, make_trim_srf_cache, + make_trim_srf_cache_with_face, +}; +use crate::surface::surface::{Surface, SurfaceKind, TrimCacheSlot}; +use crate::surface::surface_cache::p1::{FaceContainment, SurfaceCache}; use crate::surface::trim_srf_cache::{CacheFace, TrimSrfCache}; use crate::topology::edgeuse::p1 as eu1; use crate::topology::face::p2 as fp2; @@ -303,24 +306,64 @@ impl CacheFace for BrepCacheFace<'_> { } } -/// `smsurf_GetSurfaceCache(pFace->GetSurface())`: the trimmed surface cache of face -/// `f`'s surface built with the face ([`get_surface_cache_with_face`]; this creates -/// the face's UV trim curves), else its plain surface cache wrapped as one. `handle` -/// is the surface's handle in the cache's object space. While the surface's owner is -/// cleared ([`Brep::is_surface_owner_cleared`], inside `CreateUVTrimCurve(s)`) the -/// surface has no face upstream, so the cache is built standalone -/// ([`get_trim_surface_cache`]) and makes no UV trim curves. +/// A face surface's cache as `smsurf_GetSurfaceCache` hands it out +/// ([`face_surface_cache`]). +#[derive(Debug)] +pub struct FaceSurfaceCache { + /// The cache. Its handles are the face's topology ([`EntityRef`]); the surface's + /// own handle is the face. + pub cache: TrimSrfCache, + /// Whether [`FaceSurfaceCache::cache`] is the surface's trimmed surface cache + /// (`SM_OC_TRIMSRF`), else the surface cache (`SM_OC_SURFACE`) the lookup falls back + /// to when no trimmed cache could be made, wrapped as one + /// ([`TrimSrfCache::from_base`]). + pub trimmed: bool, + /// Whether the cache was taken out of the surface's cache slot (the surface's own + /// trimmed cache, put back by [`FaceSurfaceCache::release`]). + kept: bool, +} + +impl FaceSurfaceCache { + /// Ends the use of the cache: the surface's own trimmed cache goes back into its + /// cache slot (shared with every copy of the surface), with what the use did to it + /// (a tree built on demand, a face containment computed); any other cache is + /// dropped. + pub fn release(self) { + if self.kept { + Surface::store_trim_cache(self.cache); + } + } +} + + +/// `smsurf_GetSurfaceCache(pFace->GetSurface())` (through +/// `SmCacheMgrTSrf::CacheMakeOrValidate`): the trimmed surface cache of face `f`'s +/// surface, else its plain surface cache wrapped as one. Like upstream's, the trimmed +/// cache is an object of the surface ([`Surface::take_trim_cache`]), made by the first +/// lookup and reused by every later one: +/// - a cache the surface already has is reused as it is (whether or not it was built +/// with the face), unless its face was modified (`m_bFaceWasModified`) and the +/// face's B-rep is not being edited, when it is deleted and made again; +/// - a new cache is built with the face ([`make_trim_srf_cache_with_face`]; this creates +/// the face's UV trim curves) unless the surface's owner is cleared +/// ([`Brep::is_surface_owner_cleared`], inside `CreateUVTrimCurve(s)`): the surface +/// then has no face upstream, so the cache is built standalone +/// ([`make_trim_srf_cache`]) and makes no UV trim curves; either way it is kept; +/// - a lookup while the cache is in use (taken out of the slot by an enclosing lookup, +/// upstream the same object, e.g. during its construction) gets a cache built the +/// same way that is not kept. +/// +/// When no trimmed cache can be made, the surface cache is built fresh and not kept +/// ([`FaceSurfaceCache::trimmed`] is false). The caller ends the use with +/// [`FaceSurfaceCache::release`]. /// /// # Errors /// -/// `SM_ERR_NULL_POINTER` for a face without surface; those of -/// [`get_surface_cache_with_face`]. -pub fn face_surface_cache( - k: &Kernel, - b: &mut Brep, - f: FaceId, - handle: Option, -) -> Result> { +/// `SM_ERR_NULL_POINTER` for a face without surface; those of the surface cache build +/// when neither cache can be made; [`SolidError::Unported`], +/// [`SolidError::InvalidInput`] and [`SolidError::NotFound`] from the trimmed cache +/// build. +pub fn face_surface_cache(k: &Kernel, b: &mut Brep, f: FaceId) -> Result { // the cache copies the surface; the face's B-rep is changed while it is built let surface = b .face(f)? @@ -328,13 +371,118 @@ pub fn face_surface_cache( .as_ref() .ok_or(SolidError::null_pointer(OP_CACHE_FACE))? .clone_for_evaluation()?; - if b.is_surface_owner_cleared(f) { - // `GetFace()` is NULL while the owner is cleared - let mut surface = surface; - let _ = surface.set_owner(None::); - return get_trim_surface_cache(k, &surface, handle); + // `GetFace()` is NULL while the owner is cleared + let cleared = b.is_surface_owner_cleared(f); + let mut standalone = surface.clone_for_evaluation()?; + if cleared { + let _ = standalone.set_owner(None::); + } + let handle = Some(EntityRef::Face(f)); + match surface.take_trim_cache() { + TrimCacheSlot::Stored(mut cache) => { + // the cache's surface is the face's surface: its owner as it is now + cache + .base_mut() + .surface_mut() + .adopt_owner_of(if cleared { &standalone } else { &surface }); + // If the face was modified and Brep editing is no longer enabled + // then delete the old cache and create a new one. + if !(cache.base().face_was_modified && !cleared && !b.editing_enabled()) { + // the Face was not modified or m_bEditingEnabled == TRUE, + // then use the old cache + return Ok(FaceSurfaceCache { + cache: *cache, + trimmed: true, + kept: true, + }); + } + surface.delete_trim_cache(); + } + TrimCacheSlot::CheckedOut => { + // the enclosing lookup's cache: build one the same way, not kept + let built = if cleared { + get_trim_surface_cache(k, &standalone, handle) + } else { + get_surface_cache_with_face(k, &surface, handle, &mut BrepCacheFace { b, f }) + }?; + let trimmed = built.base().trimmed_dispatch; + return Ok(FaceSurfaceCache { + cache: built, + trimmed, + kept: false, + }); + } + TrimCacheSlot::Empty => {} + } + + // next create a new SmTrimSrfCache and store it with the surface (before its tree + // is built: lookups during the build find it in use) + surface.check_out_trim_cache(); + let built = if cleared { + make_trim_srf_cache(k, &standalone, handle) + } else { + make_trim_srf_cache_with_face(k, &surface, handle, &mut BrepCacheFace { b, f }) + }; + match built { + Ok(cache) => Ok(FaceSurfaceCache { + cache, + trimmed: true, + kept: true, + }), + Err(e) => { + // when construction failed - the cache is deleted + surface.delete_trim_cache(); + match e { + SolidError::Unported { .. } + | SolidError::InvalidInput { .. } + | SolidError::NotFound { .. } => Err(e), + // else - get or make SurfaceCache + _ => Ok(FaceSurfaceCache { + cache: TrimSrfCache::from_base(make_surface_cache( + k, + if cleared { &standalone } else { &surface }, + handle, + )?), + trimmed: false, + kept: false, + }), + } + } } - get_surface_cache_with_face(k, &surface, handle, &mut BrepCacheFace { b, f }) +} + +/// [`face_surface_cache`] for a caller that only needs its side effects on the face +/// (the UV trim curves a new cache creates) and the cache kept on the surface. +/// +/// # Errors +/// +/// Those of [`face_surface_cache`]. +pub fn note_face_surface_cache(k: &Kernel, b: &mut Brep, f: FaceId) -> Result<()> { + face_surface_cache(k, b, f).map(FaceSurfaceCache::release) +} + +/// `SmSurfaceCache::GetTree` of a face surface's trimmed cache: builds the tree with the +/// face when there is none, or when the surface belongs to a face whose boundary is +/// not in the tree yet and no build is under way. +/// +/// # Errors +/// +/// Those of [`TrimSrfCache::build_tree_with_face`]. +pub fn face_cache_tree( + k: &Kernel, + b: &mut Brep, + f: FaceId, + cache: &mut TrimSrfCache, +) -> Result<()> { + let base = cache.base(); + if base.tree().is_none() + || (!base.have_t_surface_cache + && base.surface().face()?.is_some() + && base.face_containment_done != FaceContainment::UnderConstruction) + { + cache.build_tree_with_face(k, &mut BrepCacheFace { b, f })?; + } + Ok(()) } // ---- co-ported topology access ---------------------------------------------------------- @@ -567,6 +715,43 @@ fn classify_trim_cache( Ok(false) } +/// `PointClassify`'s fast classification by the face surface's trimmed cache `tsc` +/// when it is up to date (`own`: the surface's own cache, whose tree is built with the +/// face when it has none): `true` when the cache decided the point. +#[allow(clippy::too_many_arguments)] +fn classify_by_trim_cache( + k: &Kernel, + b: &mut QueryTopology<'_>, + f: FaceId, + tsc: &mut TrimSrfCache, + own: bool, + face_owned: bool, + uv: Point2, + pc: &mut PointClassification, +) -> Result { + if !(tsc.base().have_t_surface_cache + && tsc.base().face_containment_done == FaceContainment::Done + && !tsc.base().face_was_modified + && face_owned) + { + return Ok(false); + } + // get every TrimSrfCache leaf node intersecting the UVPoint + if tsc.base().tree().is_none() { + match b { + QueryTopology::Prepared(_) | QueryTopology::Requested(..) => { + return Err(SolidError::Unported { + upstream: "SmSurfaceCache::BuildTree (prepared query requires a built tree)", + unit: "surface.surface_cache_p1", + }); + } + QueryTopology::Mutable(b) if own => face_cache_tree(k, b, f, tsc)?, + QueryTopology::Mutable(_) => tsc.build_tree(k)?, + } + } + classify_trim_cache(k, tsc.base(), uv, pc, Ok) +} + fn point_classify_with_topology( k: &Kernel, b: &mut QueryTopology<'_>, @@ -626,30 +811,29 @@ fn point_classify_with_topology( // when doing all loops if !tgt_loops_only { + let face_owned = surface.map_or(Ok(false), |s| s.face().map(|o| o.is_some()))?; // 1. try fast point TrimSrfCache classification. - // First try to get the cache - will be NULL when object has no cache + // First try to get the cache - will be NULL when object has no cache: the + // caller's, else the surface's own (taken out of its slot while it is used) let tsc = if surface.is_some() { trim_cache } else { None }; - if let Some(tsc) = tsc { - if tsc.base().have_t_surface_cache - && tsc.base().face_containment_done == FaceContainment::Done - && !tsc.base().face_was_modified - && surface.map_or(Ok(false), |s| s.face().map(|o| o.is_some()))? - { - // get every TrimSrfCache leaf node intersecting the UVPoint - if tsc.base().tree().is_none() { - if matches!(b, QueryTopology::Prepared(_) | QueryTopology::Requested(..)) { - return Err(SolidError::Unported { - upstream: - "SmSurfaceCache::BuildTree (prepared query requires a built tree)", - unit: "surface.surface_cache_p1", - }); - } - tsc.build_tree(k)?; - } - if classify_trim_cache(k, tsc.base(), uv, pc, Ok)? { - return Ok(()); - } - } // end up-to-date TrimSrfCache check + let own = match (&tsc, surface) { + (None, Some(s)) => match s.take_trim_cache() { + TrimCacheSlot::Stored(cache) => Some(*cache), + TrimCacheSlot::Empty | TrimCacheSlot::CheckedOut => None, + }, + _ => None, + }; + let classified = if let Some(tsc) = tsc { + classify_by_trim_cache(k, b, f, tsc, false, face_owned, uv, pc) + } else if let Some(mut own) = own { + let classified = classify_by_trim_cache(k, b, f, &mut own, true, face_owned, uv, pc); + Surface::store_trim_cache(own); + classified + } else { + Ok(false) + }; + if classified? { + return Ok(()); } } // end when doing all loops @@ -660,7 +844,11 @@ fn point_classify_with_topology( // We must enable out-of-bounds evaluation, otherwise a point outside the surface // domain will be classified on a boundary. - let surface = surface.ok_or(SolidError::null_pointer(OP_CLASSIFY))?; + let surface = b + .face(f)? + .surface + .as_ref() + .ok_or(SolidError::null_pointer(OP_CLASSIFY))?; // (an owned surface: the loop classification edits the B-rep while it walks) let surface: Surface = if let Some(copy) = with_out_of_bounds_enabled(surface)? { copy @@ -940,10 +1128,6 @@ pub trait CurveOnClassifyObjects: ClassGeometry { fn face_surface(&self, f: FaceId) -> Option; /// The loop behind a target loop handle. fn loop_id(&self, tgt_loop: O) -> Option; - /// Existing face trim cache, if supplied by the caller's cache storage. - fn trim_cache(&self, _f: FaceId) -> Option<&TrimSrfCache> { - None - } } /// The lazy B-rep query state shared by classifications of the same face. Only gap @@ -955,6 +1139,21 @@ pub(crate) struct CurveOnClassifyCache { snapshot: Option, } +/// A private snapshot of `b` standing for `b` itself: a copy whose face surfaces share +/// the cache slots of `b`'s ([`Surface::share_cache_slot_of`]), so that the surface +/// caches the classification makes or uses on it are the B-rep's surfaces' own. +fn snapshot_of(b: &Brep) -> Brep { + let mut snapshot = b.clone(); + for (id, face) in b.faces().iter() { + if let (Some(original), Ok(copy)) = (face.surface.as_ref(), snapshot.face_mut(id)) { + if let Some(copy) = copy.surface.as_mut() { + copy.share_cache_slot_of(original); + } + } + } + snapshot +} + impl CurveOnClassifyCache { pub(crate) fn apply_gap_updates(self, b: &mut Brep) -> Result<()> { if let Some(snapshot) = self.snapshot { @@ -1191,14 +1390,9 @@ fn boundary_loops( objects: &impl CurveOnClassifyObjects, made_trims: &mut Vec<(O, Curve)>, face: FaceId, + cached: bool, ) -> Result>> { let fu = fp2::upward_faceuse(b, face)?.ok_or(SolidError::null_pointer(OP_CURVE))?; - let cached = objects.trim_cache(face).is_some_and(|cache| { - let base = cache.base(); - !base.face_was_modified - && base.have_t_surface_cache - && base.face_containment_done != FaceContainment::NotDone - }); let mut loops = Vec::new(); for &lu in &b.faceuse(fu)?.loopuses { let mut vertices = Vec::new(); @@ -1336,7 +1530,7 @@ pub(crate) fn curve_on_classify_cached( if curve.dim() == 2 { // 2D Curve branch - work in surface's uv space directly on the Face's UVcurves cc.set_surface1(objects.face_surface(f)); - let snapshot = snapshot.get_or_insert_with(|| b.clone()); + let snapshot = snapshot.get_or_insert_with(|| snapshot_of(b)); match intersect_ts_bound(k, b, snapshot, objects, &mut made_trims, cc, f, do_check_3d) { Ok(()) => {} Err(e) if e.is_unported() => return Err(e), @@ -1363,7 +1557,7 @@ pub(crate) fn curve_on_classify_cached( // bHasBigGaps: each vertex's GetVertexuseOfFace(pFace)->GetVertexFaceGap() // (pFace is this face, set on the classification above) let gaps = if has_big_gaps { - let snapshot = snapshot.get_or_insert_with(|| b.clone()); + let snapshot = snapshot.get_or_insert_with(|| snapshot_of(b)); vertices .iter() .map(|&v| { @@ -1391,7 +1585,7 @@ pub(crate) fn curve_on_classify_cached( intersect_edges( k, b, - snapshot.get_or_insert_with(|| b.clone()), + snapshot.get_or_insert_with(|| snapshot_of(b)), objects, cc, &edges, @@ -1436,7 +1630,7 @@ pub(crate) fn curve_on_classify_cached( inner: objects, trims: made_trims.as_slice(), brep: b, - snapshot: snapshot.get_or_insert_with(|| b.clone()), + snapshot: snapshot.get_or_insert_with(|| snapshot_of(b)), }, ii, do_point_classify, @@ -1483,29 +1677,44 @@ fn intersect_ts_bound( .surface .as_ref() .ok_or(SolidError::null_pointer(OP_CURVE))?; - let loops = boundary_loops(k, b, snapshot, objects, made_trims, face)?; - let fresh; - let cache = if let Some(cache) = objects.trim_cache(face) { - cache - } else { - fresh = TrimSrfCache::new(surface.clone(), SurfaceCacheOptions::default()); - &fresh - }; - cc.intersect_ts_bound( - k, - &WithMadeTrimCurves { - inner: objects, - trims: made_trims.as_slice(), - brep: b, - snapshot, - }, - &BoundaryFace { - surface, - loops: &loops, - cache, - }, - do_check_3d, - ) + // Face->surface and TrimCache: smsurf_GetSurfaceCache(pSurface) (made on the + // snapshot, whose surfaces share the B-rep's cache slots); NER of a surface cache + // that is not a trimmed one + let lease = face_surface_cache(k, snapshot, face)?; + if !lease.trimmed { + lease.release(); + return Err(SolidError::null_pointer(OP_CURVE)); + } + // the cached branch reads the vertex uses' UV points from the cache + let base = lease.cache.base(); + let cached = !base.face_was_modified + && base.have_t_surface_cache + && base.face_containment_done != FaceContainment::NotDone; + let result = boundary_loops(k, b, snapshot, objects, made_trims, face, cached).and_then(|loops| { + let cache_handle = |h: EntityRef| match h { + EntityRef::Vertexuse(vu) => Some(objects.vertexuse(vu)), + EntityRef::Edgeuse(eu) => Some(objects.edgeuse(eu)), + _ => None, + }; + cc.intersect_ts_bound( + k, + &WithMadeTrimCurves { + inner: objects, + trims: made_trims.as_slice(), + brep: b, + snapshot, + }, + &BoundaryFace { + surface, + loops: &loops, + cache: &lease.cache, + cache_handle: &cache_handle, + }, + do_check_3d, + ) + }); + lease.release(); + result } /// `SmFace::GetEdges(SmTArray&, NULL, bTgtLoopsOnly, pOptTgtLoops)`. @@ -1932,7 +2141,7 @@ pub(crate) mod tests { struct Handles<'a> { b: &'a Brep, source: Curve, - cache: Option>, + } impl ClassGeometry for Handles<'_> { fn curve(&self, h: Handle) -> Option<&Curve> { @@ -1994,9 +2203,6 @@ pub(crate) mod tests { fn loop_id(&self, _: Handle) -> Option { None } - fn trim_cache(&self, _: FaceId) -> Option<&TrimSrfCache> { - self.cache.as_ref() - } } fn with_same_face_sibling(b: &mut Brep) -> (FaceId, VertexuseId) { @@ -2034,7 +2240,7 @@ pub(crate) mod tests { let objects = Handles { b: &b, source: Curve::from(Line::from_points(Vec3::ZERO, Vec3::X, 3)), - cache: None, + }; let mut cc = CurveClassification::with_curve_interval( &k, @@ -2127,14 +2333,15 @@ pub(crate) mod tests { LoopuseStart::Vertexuse(Some(VertexuseId::from_index(0))); let objects = Handles { b: &b, - cache: None, + source: Curve::from(Line::from_points( Point3::new(0.2, 1.5, 0.), Point3::new(0.8, 1.5, 0.), 2, )), }; - let loops = boundary_loops(&k, &b, &mut b.clone(), &objects, &mut Vec::new(), f).unwrap(); + let loops = + boundary_loops(&k, &b, &mut b.clone(), &objects, &mut Vec::new(), f, false).unwrap(); assert_eq!(loops.len(), 1); assert_eq!(loops[0].vertices.len(), 1); assert!(loops[0].edges.is_empty()); @@ -2164,7 +2371,7 @@ pub(crate) mod tests { assert_eq!(cc.size(), 1); let objects = Handles { b: &b, - cache: None, + source: Curve::from(Line::from_points( Point3::new(0.2, 0.5, 2.), Point3::new(0.8, 0.5, 2.), @@ -2189,19 +2396,12 @@ pub(crate) mod tests { let k = Kernel::default(); let mut b = Brep::new(); let f = square(&mut b); - let mut cache = TrimSrfCache::new( - b.face(f).unwrap().surface.clone().unwrap(), - SurfaceCacheOptions::default(), - ); - cache.base_mut().have_t_surface_cache = true; - cache.base_mut().face_was_modified = false; - cache.base_mut().face_containment_done = FaceContainment::Done; let objects = Handles { b: &b, - cache: Some(cache), source: Curve::from(Line::from_points(Vec3::ZERO, Vec3::X, 2)), }; - let loops = boundary_loops(&k, &b, &mut b.clone(), &objects, &mut Vec::new(), f).unwrap(); + let loops = + boundary_loops(&k, &b, &mut b.clone(), &objects, &mut Vec::new(), f, true).unwrap(); assert_eq!(loops[0].vertices.len(), 4); assert_eq!(loops[0].edges.len(), 4); for eu in &loops[0].edges { diff --git a/crates/solidcore/src/surface/surface.rs b/crates/solidcore/src/surface/surface.rs index 99eea2b97..f708b89b3 100644 --- a/crates/solidcore/src/surface/surface.rs +++ b/crates/solidcore/src/surface/surface.rs @@ -20,4 +20,4 @@ pub mod p8; pub mod p9; pub use p1::StepSurface; -pub use p2::{CacheTreeState, Surface, SurfaceKind}; +pub use p2::{CacheTreeState, Surface, SurfaceKind, TrimCacheSlot}; diff --git a/crates/solidcore/src/surface/surface/p2.rs b/crates/solidcore/src/surface/surface/p2.rs index 61234a471..80a990598 100644 --- a/crates/solidcore/src/surface/surface/p2.rs +++ b/crates/solidcore/src/surface/surface/p2.rs @@ -170,15 +170,14 @@ //! constructor leaves `m_pCacheObj` null. The cache builds of `surface.surface_p1` //! and `surface.surface_cache_p3` keep it in step. -use std::sync::atomic::{AtomicU8, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Mutex, PoisonError}; use crate::curve::curve::p3::PointSolveOptions; use crate::curve::curve::p7::PropertyAnalysisOptions; use crate::curve::curve::p8::CurveIntersectOptions; use crate::curve::curve::{Curve, CurvePropertyType}; use crate::curve::line::Line; -use crate::error::{ignore_status, Result, SolidError, SM_ERR}; +use crate::error::{ignore_status, EntityRef, Result, SolidError, SM_ERR}; use crate::geom::a_object::{self, AObject, CacheStore}; use crate::geom::assert_array::labels::DERIV_SURF_DEFINITION_TYPE; use crate::geom::assert_array::{AssertArray, AssertReport, ObjectKey, ReportSpec}; @@ -228,6 +227,7 @@ use crate::surface::surf_of_revolution::{Revolved, SurfOfRevolution}; use crate::surface::surface::p1::{StepSurface, SurfaceIntersectContext, SurfaceSolveContext}; use crate::surface::surface::p8::FaceSolveCache; use crate::surface::surface_cache::p1::SurfaceCache; +use crate::surface::trim_srf_cache::TrimSrfCache; use crate::surface::torus::Torus; use crate::topology::database_io::DatabaseReader; use crate::topology::trimming_tools; @@ -363,35 +363,44 @@ pub enum CacheTreeState { Built, } -impl CacheTreeState { - const fn to_u8(self) -> u8 { - match self { - Self::NoCache => 0, - Self::Unbuilt => 1, - Self::Built => 2, - } - } +/// The trimmed surface cache a surface's cache slot holds: the `SM_OC_TRIMSRF` entry of +/// upstream's global cache queue for the surface (`SmCacheMgr::GetObjectCache`), in the +/// handle space of the surface's face ([`EntityRef`]). +#[derive(Debug, Default)] +pub enum TrimCacheSlot { + /// No trimmed cache. + #[default] + Empty, + /// The cache, not in use. Its own surface copy does not share the slot (that would + /// make the slot own itself). + Stored(Box>), + /// The cache is in use: a caller took it out ([`Surface::take_trim_cache`]) and puts + /// it back when done. Upstream's lookups in the meantime find that same object + /// (it is in the queue before its tree is built). + CheckedOut, +} - const fn from_u8(v: u8) -> Self { - match v { - 1 => Self::Unbuilt, - 2 => Self::Built, - _ => Self::NoCache, - } - } +/// What a [`CacheSlot`] holds. +#[derive(Debug, Default)] +struct SlotState { + tree: CacheTreeState, + trim: TrimCacheSlot, + /// The face was modified while the trimmed cache was in use. + face_modified_while_out: bool, } /// The cache slot of a surface. Upstream keeps a surface's cache on the surface object /// and everything that works on the surface reaches it through a pointer; the port -/// builds its caches afresh and passes evaluation copies -/// ([`Surface::clone_for_evaluation`]) where upstream passes that pointer. The slot -/// records the one part of the cache's history a later reader can observe (whether -/// its tree was built), shared between a surface and its evaluation copies so that a -/// tree built through any of them is seen by all; `Clone` (the copy constructor, -/// which leaves `m_pCacheObj` null) starts an empty slot. Single-threaded use only -/// orders the accesses, so the result is deterministic. +/// passes evaluation copies ([`Surface::clone_for_evaluation`]) where upstream passes +/// that pointer. The slot records what a later reader of the cache can observe, shared +/// between a surface and its evaluation copies so that what is done through any of +/// them is seen by all: whether the cache's tree was built ([`CacheTreeState`]) and, +/// once one has been made, the surface's trimmed surface cache itself +/// ([`TrimCacheSlot`]). `Clone` (the copy constructor, which leaves `m_pCacheObj` null) +/// starts an empty slot. Single-threaded use only orders the accesses, so the result +/// is deterministic. #[derive(Debug, Default)] -struct CacheSlot(Arc); +struct CacheSlot(Arc>); impl CacheSlot { /// The same slot (an evaluation copy stands for the same surface object). @@ -399,12 +408,25 @@ impl CacheSlot { Self(Arc::clone(&self.0)) } + /// Whether `self` and `other` are the same slot (stand for the same surface + /// object): an identity test, never an ordering. + fn same(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } + + fn with(&self, f: impl FnOnce(&mut SlotState) -> R) -> R { + // a poisoned lock only means a panic unwound through `with`; every write + // leaves the state consistent, so it stays usable + let mut guard = self.0.lock().unwrap_or_else(PoisonError::into_inner); + f(&mut guard) + } + fn get(&self) -> CacheTreeState { - CacheTreeState::from_u8(self.0.load(Ordering::Relaxed)) + self.with(|s| s.tree) } fn set(&self, state: CacheTreeState) { - self.0.store(state.to_u8(), Ordering::Relaxed); + self.with(|s| s.tree = state); } } @@ -872,6 +894,111 @@ impl Surface { self.cache_slot.set(state); } + /// Takes this surface's trimmed surface cache out of its cache slot (shared with its + /// evaluation copies) for use, leaving [`TrimCacheSlot::CheckedOut`] until + /// [`Surface::put_trim_cache`] returns it: [`TrimCacheSlot::Stored`] with the cache, + /// whose own surface copy shares this slot again while it is out; otherwise what the + /// slot holds ([`TrimCacheSlot::Empty`] or [`TrimCacheSlot::CheckedOut`]), unchanged. + #[must_use] + pub fn take_trim_cache(&self) -> TrimCacheSlot { + let taken = self.cache_slot.with(|s| match &s.trim { + TrimCacheSlot::Stored(_) => core::mem::replace(&mut s.trim, TrimCacheSlot::CheckedOut), + TrimCacheSlot::Empty => TrimCacheSlot::Empty, + TrimCacheSlot::CheckedOut => TrimCacheSlot::CheckedOut, + }); + match taken { + TrimCacheSlot::Stored(mut cache) => { + cache.base_mut().surface_mut().cache_slot = self.cache_slot.share(); + TrimCacheSlot::Stored(cache) + } + other => other, + } + } + + /// Stores `cache` as the trimmed surface cache of the surface it caches (whose cache + /// slot its own surface copy shares: upstream's + /// `SmCacheMgr::AddToObjectCache(SM_OC_TRIMSRF, ..)`, or the return of a cache taken + /// with [`Surface::take_trim_cache`]), and records its tree state. A face + /// modification noted while the cache was out ([`Surface::mark_trim_cache_face_modified`]) + /// is applied to it. The cache's own surface copy then gets a slot of its own, so + /// that the slot does not own itself. + pub fn store_trim_cache(mut cache: TrimSrfCache) { + let slot = cache.base().surface().cache_slot.share(); + cache.base_mut().surface_mut().cache_slot = CacheSlot::default(); + let tree = if cache.base().tree().is_some() { + CacheTreeState::Built + } else { + CacheTreeState::Unbuilt + }; + slot.with(|s| { + if core::mem::take(&mut s.face_modified_while_out) { + cache.base_mut().face_was_modified = true; + } + s.tree = tree; + s.trim = TrimCacheSlot::Stored(Box::new(cache)); + }); + } + + /// `SmFace::Notify(SM_NO_PRE_EDIT)` on this face surface's trimmed surface cache + /// (`GetObjectCache(SM_OC_TRIMSRF, ..)`): sets its `m_bFaceWasModified`; a cache in + /// use gets it when it is stored again. + pub fn mark_trim_cache_face_modified(&self) { + self.cache_slot.with(|s| match &mut s.trim { + TrimCacheSlot::Stored(cache) => cache.base_mut().face_was_modified = true, + TrimCacheSlot::CheckedOut => s.face_modified_while_out = true, + TrimCacheSlot::Empty => {} + }); + } + + /// Takes the owner of `other` (the same surface object as it is now): a cache's + /// copy of the surface it caches follows that surface's owner. + pub(crate) fn adopt_owner_of(&mut self, other: &Self) { + self.owner = other.owner; + self.owner_chain_face = other.owner_chain_face; + self.owner_zone_tol_3d = other.owner_zone_tol_3d; + self.update_owned_bases(); + } + + /// Marks this surface's trimmed surface cache as in use without one having been + /// stored (a cache under construction: upstream adds it to the queue before its + /// tree is built). + pub fn check_out_trim_cache(&self) { + self.cache_slot.with(|s| s.trim = TrimCacheSlot::CheckedOut); + } + + /// Deletes this surface's trimmed surface cache (`SmCacheMgr::DeleteObjectsCache( + /// SM_OC_TRIMSRF, ..)`), or ends a checkout without returning a cache (a build that + /// failed). The tree state is left as it is (the surface cache of `SM_OC_SURFACE` + /// keeps its own). + pub fn delete_trim_cache(&self) { + self.cache_slot.with(|s| { + s.trim = TrimCacheSlot::Empty; + s.face_modified_while_out = false; + }); + } + + /// Whether this surface's cache slot holds a trimmed surface cache (stored or in + /// use). + #[must_use] + pub fn has_trim_cache(&self) -> bool { + self.cache_slot + .with(|s| !matches!(s.trim, TrimCacheSlot::Empty)) + } + + /// Makes this surface share `other`'s cache slot: `self` is a copy that stands for + /// the same surface object (a private snapshot of a B-rep standing for the B-rep + /// itself). + pub fn share_cache_slot_of(&mut self, other: &Self) { + self.cache_slot = other.cache_slot.share(); + } + + /// Whether `self` and `other` share one cache slot (stand for the same surface + /// object). + #[must_use] + pub fn shares_cache_slot_with(&self, other: &Self) -> bool { + self.cache_slot.same(&other.cache_slot) + } + /// The concrete kind. #[must_use] pub const fn kind(&self) -> &SurfaceKind { diff --git a/crates/solidcore/src/surface/surface_cache/p1.rs b/crates/solidcore/src/surface/surface_cache/p1.rs index e2808c50e..cbf015581 100644 --- a/crates/solidcore/src/surface/surface_cache/p1.rs +++ b/crates/solidcore/src/surface/surface_cache/p1.rs @@ -1853,6 +1853,12 @@ impl SurfaceCache { &self.surface } + /// The cached surface, mutable: only for the bookkeeping of the surface object it + /// stands for (its cache slot), never its geometry. + pub(crate) fn surface_mut(&mut self) -> &mut Surface { + &mut self.surface + } + /// The cached surface and the singularity memo of the running /// [`SurfaceCache::subdivide_to_tolerances_with`] (`None` outside it). pub(crate) fn surface_and_pole_memo(&mut self) -> (&Surface, Option<&mut PoleMemo>) { diff --git a/crates/solidcore/src/topology/edgeuse/p3.rs b/crates/solidcore/src/topology/edgeuse/p3.rs index 4b7b27f4d..6c2d13371 100644 --- a/crates/solidcore/src/topology/edgeuse/p3.rs +++ b/crates/solidcore/src/topology/edgeuse/p3.rs @@ -281,13 +281,13 @@ fn status(r: Result) -> Result> { /// `smsurf_GetSurfaceCache(pSurface)` of the surface of face `f` (`NER`), as /// `EvaluateBinormal` calls it before clearing the surface's owner: the face's trimmed -/// surface cache, whose construction attaches a UV trim curve to every edge use of the -/// face (`SmFace::CreateUVTrimCurves`, `SmTrimSrfCache.cpp:209`), built over the B-rep -/// ([`crate::query::face::p2::face_surface_cache`]). The cache itself is not kept (the -/// Rust surface solvers build their own); only its side effect on the face is. +/// surface cache, kept on the surface, whose construction (when the surface has none +/// yet) attaches a UV trim curve to every edge use of the face +/// (`SmFace::CreateUVTrimCurves`, `SmTrimSrfCache.cpp:209`), built over the B-rep +/// ([`crate::query::face::p2::face_surface_cache`]). The Rust surface solve that +/// follows builds its own cache; only the face's and the surface's state is kept. fn face_surface_cache(k: &Kernel, b: &mut Brep, f: FaceId) -> Result<()> { - crate::query::face::p2::face_surface_cache(k, b, f, Some(crate::error::EntityRef::Face(f))) - .map(|_| ()) + crate::query::face::p2::note_face_surface_cache(k, b, f) } /// `pEdge->SetTolerance(tolerance)` (default arguments). diff --git a/crates/solidcore/src/topology/face/p3.rs b/crates/solidcore/src/topology/face/p3.rs index 4a33171cf..1c938e32b 100644 --- a/crates/solidcore/src/topology/face/p3.rs +++ b/crates/solidcore/src/topology/face/p3.rs @@ -391,7 +391,7 @@ pub fn destroy(k: &mut Kernel, b: &mut Brep, f: FaceId, env: &mut NotifyEnv<'_>) } /// `Notify`: `PreEdit` resets the rectangular-trim flag to `UNSURE` and marks the -/// surface's trimmed surface cache (`trim_cache`) as modified; `PostEdit`, +/// surface's trimmed surface cache (`trim_cache`, else the one the surface keeps) as modified; `PostEdit`, /// `SplitInBrep`, `ChangeGeometry` and `Split` are passed to every edge use and vertex /// use of the upward face use (clearing their cached UV trim curves and gaps); then the /// notification goes to `SmTopology::Notify`. @@ -415,8 +415,11 @@ pub fn notify( N::PreEdit => { // m_bRectangularTrim: UNSURE = Not yet set b.face_mut(f)?.rectangular_trim = face2::UNSURE; + // the cache given, else the surface's own (GetObjectCache) if let Some(sc) = trim_cache { sc.base_mut().face_was_modified = true; + } else if let Some(s) = b.face(f)?.surface.as_ref() { + s.mark_trim_cache_face_modified(); } } N::PostEdit | N::SplitInBrep | N::ChangeGeometry | N::Split => { diff --git a/crates/solidcore/src/topology/face/p4.rs b/crates/solidcore/src/topology/face/p4.rs index 7062e6368..6c22164dd 100644 --- a/crates/solidcore/src/topology/face/p4.rs +++ b/crates/solidcore/src/topology/face/p4.rs @@ -122,7 +122,8 @@ use crate::math::{BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQRT}; use crate::nurbs::b_spline_surface::p1::SurfParamType; use crate::nurbs::b_spline_surface_n_lib::p2::OrientType; use crate::query::face::p2::{ - curve_on_classify_cached, face_surface_cache, point_classify, BrepCacheFace, + curve_on_classify_cached, face_cache_tree, face_surface_cache, point_classify, BrepCacheFace, + FaceSurfaceCache, CurveOnClassifyCache, CurveOnClassifyObjects, PointClassifyOptions, }; use crate::surface::surface::p5::{SS_NONE, SS_UMAX, SS_UMIN, SS_VMAX, SS_VMIN}; @@ -530,37 +531,35 @@ struct FaceLineCache<'b, O> { b: &'b mut Brep, f: FaceId, surface: O, - /// The trimmed cache, once built. - trim: Option>, + /// The face surface's cache, once fetched; given back to the surface after the + /// solve. + trim: Option, } impl FaceSolveCache for FaceLineCache<'_, O> { fn build(&mut self, k: &Kernel) -> Result> { - // the face's trimmed cache, its tree built (GetTree); the surface needs a name - // in the cache's object space: after the build every node's `mBA_pSurface` is - // the cache surface (the polar boxes of the leaf solves are computed on it) - let mut c = face_surface_cache(k, self.b, self.f, Some(EntityRef::Face(self.f)))?; - if c.base().tree().is_none() { - c.build_tree_with_face( - k, - &mut BrepCacheFace { - b: &mut *self.b, - f: self.f, - }, - )?; - } + // the face's trimmed cache (kept on the surface), its tree built (GetTree); the + // surface needs a name in the cache's object space: after the build every + // node's `mBA_pSurface` is the cache surface (the polar boxes of the leaf solves + // are computed on it) + let mut lease = face_surface_cache(k, self.b, self.f)?; + let built = face_cache_tree(k, self.b, self.f, &mut lease.cache); let surface = self.surface; - let mut solve_cache = c.base().map_handles(|_| Ok(surface))?; - solve_cache.set_surface_handle(Some(surface)); - self.trim = Some(c); - Ok(solve_cache) + let solve_cache = built.and_then(|()| { + let mut c = lease.cache.base().map_handles(|_| Ok(surface))?; + c.set_surface_handle(Some(surface)); + Ok(c) + }); + self.trim = Some(lease); + solve_cache } fn point_test(&mut self, k: &Kernel, uv: Point2) -> Result { - let trim = self + let trim = &self .trim .as_ref() - .ok_or_else(|| null("GlobalLineIntersect: point test without cache"))?; + .ok_or_else(|| null("GlobalLineIntersect: point test without cache"))? + .cache; // PointTest(uv, ok, NULL, NULL): the face's zone tolerance let (ok, _) = trim.point_test_in( k, @@ -603,8 +602,12 @@ fn global_line_intersect_with( .as_ref() .ok_or_else(|| null("GlobalLineIntersect: face without surface"))? .clone_for_evaluation()?; - // the face's trimmed surface cache, when the surface solve fetched it - let mut trim: Option> = None; + // A surface with a face solves on the face's trimmed cache, kept on the surface: + // a cache held by the caller stands only for a surface without face. + let held = match held { + Some(held) if face_surface.face()?.is_none() => Some(held), + _ => None, + }; let mut solutions = if let Some(held) = held { face_surface.global_line_intersect_held( k, @@ -634,9 +637,12 @@ fn global_line_intersect_with( line_interval, false, src_zone_tol_3d, - )?; - trim = face_cache.trim; - solutions + ); + // the cache goes back to the surface (PointClassify below finds it there) + if let Some(lease) = face_cache.trim.take() { + lease.release(); + } + solutions? }; // If there are no solutions then return whatever the surface method returns. @@ -660,7 +666,7 @@ fn global_line_intersect_with( if sol.solution_type == SolutionType::SingleValue { // Get uv on surface let uv = Point2::new(sol.start.parameters[1], sol.start.parameters[2]); - if classify_uv_point(k, b, f, uv, src_zone_tol_3d, trim.as_mut())? + if classify_uv_point(k, b, f, uv, src_zone_tol_3d, None)? == PointClassType::Unknown { removal_indices.push(ii); @@ -1456,9 +1462,6 @@ impl, O: ClassObject> CurveOnClassifyObjects fn loop_id(&self, tgt_loop: O) -> Option { self.inner.loop_id(tgt_loop) } - fn trim_cache(&self, f: FaceId) -> Option<&TrimSrfCache> { - self.inner.trim_cache(f) - } } /// The inputs of [`has_seam_problem`] after the face (`HasSeamProblem`'s optional From 1cbe7ad83de8d5eba80261376b3823d93d2665f3 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 18:12:59 -0700 Subject: [PATCH 3/6] ComputeUVPoint solves on the face surface's kept cache; IntersectTSBound makes missing trims in its cached branch ComputeUVPoint fetches the face surface's cache (smsurf_GetSurfaceCache) and keeps it checked out around GlobalPointSolve, whose fast drop classifies on the B-rep when the cache is trimmed with point testing on. IntersectTSBound's cached branch runs its traversals itself, so GetOrCreateUVTrimCurve makes a missing trim on the classification snapshot when the edge use is visited. Co-Authored-By: Claude Opus 5.5 --- crates/solidcore/src/geom/context.rs | 24 +- .../solidcore/src/intersect/curve_class/p9.rs | 127 +++++++-- crates/solidcore/src/query/face/p2.rs | 251 ++++++++++++++++-- crates/solidcore/src/surface/surface/p2.rs | 10 +- crates/solidcore/src/surface/surface/p4.rs | 72 ++++- crates/solidcore/src/topology/face/p4.rs | 7 +- crates/solidcore/src/topology/vertexuse.rs | 104 ++++++-- 7 files changed, 512 insertions(+), 83 deletions(-) diff --git a/crates/solidcore/src/geom/context.rs b/crates/solidcore/src/geom/context.rs index c79072f3b..80b2e44b6 100644 --- a/crates/solidcore/src/geom/context.rs +++ b/crates/solidcore/src/geom/context.rs @@ -690,8 +690,10 @@ impl Context { pub fn current_mark(&self, mark_type: MarkType) -> u64 { match mark_type { MarkType::NoMark => { - let [m1, m2, m3, mio, massert] = - self.current_marks.each_ref().map(|m| m.load(Ordering::Relaxed)); + let [m1, m2, m3, mio, massert] = self + .current_marks + .each_ref() + .map(|m| m.load(Ordering::Relaxed)); 1 + max5(m1, m2, m3, mio, massert) } _ => self @@ -1117,6 +1119,24 @@ pub fn get_or_create_context(slot: &mut Option) -> &mut Context { #[cfg(test)] mod tests { + #[test] + fn a_shared_context_claims_and_unlocks_a_mark() { + let mut k = Kernel::default(); + let before = k.context.current_mark(MarkType::Mark2); + let m = with_mark_lock(&mut k, ALL_MARKS, |k, outer| { + assert_eq!(outer, MarkType::Mark); + let lock = k.context.claim_mark(ALL_MARKS); + // the first mark is locked by the enclosing lock: the second is claimed + assert_eq!(lock.mark_type(), MarkType::Mark2); + assert!(k.context.is_mark_locked(MarkType::Mark2)); + lock.mark_type() + }) + .unwrap(); + assert_eq!(m, MarkType::Mark2); + assert!(!k.context.is_mark_locked(MarkType::Mark2)); + assert_eq!(k.context.current_mark(MarkType::Mark2), before + 2); + } + use super::*; const ALL: [MarkType; 7] = [ diff --git a/crates/solidcore/src/intersect/curve_class/p9.rs b/crates/solidcore/src/intersect/curve_class/p9.rs index 01146c7c1..51eb64118 100644 --- a/crates/solidcore/src/intersect/curve_class/p9.rs +++ b/crates/solidcore/src/intersect/curve_class/p9.rs @@ -113,6 +113,19 @@ pub struct BoundaryFace<'a, O, C = O> { pub cache_handle: &'a dyn Fn(C) -> Option, } +/// What the cached branch of `IntersectTSBound` visits +/// ([`CurveClassification::ts_bound_cached_visits`]). +#[derive(Debug, Clone)] +pub struct CachedVisits { + /// No cache node meets the source curve's UV box: nothing to do, and no mark is + /// claimed. + pub no_nodes: bool, + /// The vertex uses of the leaf nodes, each once, with the cache's UV points. + pub vertices: Vec>, + /// The edge uses of the leaf nodes, each once. + pub edgeuses: Vec>, +} + /// `IntersectEdges` flags. `has_big_gaps` is required upstream; this Rust options /// struct defaults it to false. `uv_space_okay` has the native default true. #[derive(Debug, Clone, Copy)] @@ -314,11 +327,7 @@ impl CurveClassification { "trim boundary source must be 2D", )); } - let base = face.cache.base(); - if base.face_was_modified - || !base.have_t_surface_cache - || base.face_containment_done == FaceContainment::NotDone - { + let Some(visits) = self.ts_bound_cached_visits(k, geometry, face)? else { for lp in face.loops { for vertex in &lp.vertices { self.p9_vertex(k, geometry, source, face.surface, *vertex, do_check_3d)?; @@ -328,6 +337,48 @@ impl CurveClassification { } } return Ok(()); + }; + // no work - curve is outside of the domain of the surface + if visits.no_nodes { + return Ok(()); + } + // increment the mark on the context by 1 (the vertex uses and edge uses it + // marks are the ordered visit lists) + let _mark_lock = k.context.claim_mark(ALL_MARKS); + for vertex in &visits.vertices { + self.ts_bound_vertex(k, geometry, face.surface, *vertex, do_check_3d)?; + } + for edgeuse in &visits.edgeuses { + self.ts_bound_edgeuse(k, geometry, face.surface, *edgeuse)?; + } + Ok(()) + } + + /// The cached branch of `IntersectTSBound`, up to its traversals: `None` when the + /// face's trimmed cache is not up to date (the loop-use branch runs instead), else + /// the vertex uses (with the cache's UV points) and then the edge uses of the + /// cache's leaf nodes that meet the source curve's UV box, each once, in the order + /// upstream visits (marks) them. The caller claims the context mark when there are + /// nodes ([`CachedVisits::no_nodes`] is false), then intersects them with + /// [`Self::ts_bound_vertex`] and [`Self::ts_bound_edgeuse`]. + /// + /// # Errors + /// Missing geometry or cache records; a source curve whose curve cache cannot be + /// built (`SM_ERR_NULL_POINTER`). + pub fn ts_bound_cached_visits( + &self, + k: &Kernel, + geometry: &impl ClassGeometry, + face: &BoundaryFace<'_, O, C>, + ) -> Result>> { + let source_handle = self.curve().ok_or(SolidError::null_pointer(OP))?; + let source = curve(geometry, source_handle)?; + let base = face.cache.base(); + if base.face_was_modified + || !base.have_t_surface_cache + || base.face_containment_done == FaceContainment::NotDone + { + return Ok(None); } // `GetOrCreateObjectCache(SM_OC_CURVE, ..)` then `NER(pCC)`: a failed cache // build (e.g. a degenerate source) is a NULL cache, SM_ERR_NULL_POINTER. @@ -345,29 +396,28 @@ impl CurveClassification { let mut domain = Extent2d::from_extent3d(&tree.node(top)?.bbox); domain.expand_absolute(EFF_ZERO_SQRT); let nodes = base.find_uv_nodes_built(k, &domain, false)?; - - // no work - curve is outside of the domain of the surface - if nodes.is_empty() { - return Ok(()); + let mut visits = CachedVisits { + no_nodes: nodes.is_empty(), + vertices: Vec::new(), + edgeuses: Vec::new(), + }; + if visits.no_nodes { + return Ok(Some(visits)); } - // increment the mark on the context by 1 (the vertex uses and edge uses it - // marks are the ordered `visited` list) - let _mark_lock = k.context.claim_mark(ALL_MARKS); - let tree = base.tree().ok_or(SolidError::null_pointer(OP))?; - let mut visited = Vec::new(); for &id in &nodes { + // skip all but the leaf nodes if tree.node(id)?.child1.is_some() { continue; } for item in &base.aux(id)?.vertex_list { let vu = item.vertexuse.ok_or(SolidError::null_pointer(OP))?; let vu = (face.cache_handle)(vu).ok_or(SolidError::null_pointer(OP))?; - if visited.contains(&vu) { + // skip vertices that have already been processed + if visits.vertices.iter().any(|v| v.vertexuse == vu) { continue; } - visited.push(vu); let mut vertex = face .loops .iter() @@ -376,7 +426,7 @@ impl CurveClassification { .copied() .ok_or(SolidError::null_pointer(OP))?; vertex.uv = item.uv_point; - self.p9_vertex(k, geometry, source, face.surface, vertex, do_check_3d)?; + visits.vertices.push(vertex); } } for &id in &nodes { @@ -385,10 +435,10 @@ impl CurveClassification { } for &eu in &base.aux(id)?.edgeuse_list { let eu = (face.cache_handle)(eu).ok_or(SolidError::null_pointer(OP))?; - if visited.contains(&eu) { + // skip edgeuses already processed + if visits.edgeuses.iter().any(|e| e.edgeuse == eu) { continue; } - visited.push(eu); let edgeuse = face .loops .iter() @@ -396,10 +446,45 @@ impl CurveClassification { .find(|e| e.edgeuse == eu) .copied() .ok_or(SolidError::null_pointer(OP))?; - self.p9_edgeuse(k, geometry, source_handle, face.surface, edgeuse, true)?; + visits.edgeuses.push(edgeuse); } } - Ok(()) + Ok(Some(visits)) + } + + /// One vertex use of the cached branch of `IntersectTSBound` (see + /// [`Self::ts_bound_cached_visits`]). + /// + /// # Errors + /// As [`Self::intersect_ts_bound`]. + pub fn ts_bound_vertex( + &mut self, + k: &Kernel, + geometry: &impl ClassGeometry, + surface: &Surface, + vertex: BoundaryVertex, + do_check_3d: bool, + ) -> Result<()> { + let source_handle = self.curve().ok_or(SolidError::null_pointer(OP))?; + let source = curve(geometry, source_handle)?; + self.p9_vertex(k, geometry, source, surface, vertex, do_check_3d) + } + + /// One edge use of the cached branch of `IntersectTSBound` (see + /// [`Self::ts_bound_cached_visits`]); its UV trim curve (`GetOrCreateUVTrimCurve`) + /// must be in `edgeuse`. + /// + /// # Errors + /// As [`Self::intersect_ts_bound`]. + pub fn ts_bound_edgeuse( + &mut self, + k: &Kernel, + geometry: &impl ClassGeometry, + surface: &Surface, + edgeuse: BoundaryEdgeuse, + ) -> Result<()> { + let source_handle = self.curve().ok_or(SolidError::null_pointer(OP))?; + self.p9_edgeuse(k, geometry, source_handle, surface, edgeuse, true) } fn p9_vertex( diff --git a/crates/solidcore/src/query/face/p2.rs b/crates/solidcore/src/query/face/p2.rs index a20e550f0..717adbc40 100644 --- a/crates/solidcore/src/query/face/p2.rs +++ b/crates/solidcore/src/query/face/p2.rs @@ -335,7 +335,6 @@ impl FaceSurfaceCache { } } - /// `smsurf_GetSurfaceCache(pFace->GetSurface())` (through /// `SmCacheMgrTSrf::CacheMakeOrValidate`): the trimmed surface cache of face `f`'s /// surface, else its plain surface cache wrapped as one. Like upstream's, the trimmed @@ -381,10 +380,11 @@ pub fn face_surface_cache(k: &Kernel, b: &mut Brep, f: FaceId) -> Result { // the cache's surface is the face's surface: its owner as it is now - cache - .base_mut() - .surface_mut() - .adopt_owner_of(if cleared { &standalone } else { &surface }); + cache.base_mut().surface_mut().adopt_owner_of(if cleared { + &standalone + } else { + &surface + }); // If the face was modified and Brep editing is no longer enabled // then delete the old cache and create a new one. if !(cache.base().face_was_modified && !cleared && !b.editing_enabled()) { @@ -1690,31 +1690,143 @@ fn intersect_ts_bound( let cached = !base.face_was_modified && base.have_t_surface_cache && base.face_containment_done != FaceContainment::NotDone; - let result = boundary_loops(k, b, snapshot, objects, made_trims, face, cached).and_then(|loops| { - let cache_handle = |h: EntityRef| match h { - EntityRef::Vertexuse(vu) => Some(objects.vertexuse(vu)), - EntityRef::Edgeuse(eu) => Some(objects.edgeuse(eu)), - _ => None, - }; - cc.intersect_ts_bound( + let result = + boundary_loops(k, b, snapshot, objects, made_trims, face, cached).and_then(|loops| { + let cache_handle = |h: EntityRef| match h { + EntityRef::Vertexuse(vu) => Some(objects.vertexuse(vu)), + EntityRef::Edgeuse(eu) => Some(objects.edgeuse(eu)), + _ => None, + }; + let boundary = BoundaryFace { + surface, + loops: &loops, + cache: &lease.cache, + cache_handle: &cache_handle, + }; + let visits = cc.ts_bound_cached_visits( + k, + &WithMadeTrimCurves { + inner: objects, + trims: made_trims.as_slice(), + brep: b, + snapshot, + }, + &boundary, + )?; + match visits { + // no up-to-date trimmed cache: every vertex use and edge use of the face + None => cc.intersect_ts_bound( + k, + &WithMadeTrimCurves { + inner: objects, + trims: made_trims.as_slice(), + brep: b, + snapshot, + }, + &boundary, + do_check_3d, + ), + // no work - curve is outside of the domain of the surface + Some(visits) if visits.no_nodes => Ok(()), + Some(visits) => cached_ts_bound( + k, + b, + snapshot, + objects, + made_trims, + cc, + face, + surface, + visits, + do_check_3d, + ), + } + }); + lease.release(); + result +} + +/// The traversals of `IntersectTSBound`'s up-to-date trimmed cache branch, under its +/// context mark (`SmNewMarkAndLock`): the vertex uses, then the edge uses, whose +/// `GetOrCreateUVTrimCurve` makes a missing UV trim curve on the snapshot (status +/// ignored, `SE`) and whose edge tolerance is read when it is visited. +#[allow(clippy::too_many_arguments)] +fn cached_ts_bound( + k: &Kernel, + b: &Brep, + snapshot: &mut Brep, + objects: &impl CurveOnClassifyObjects, + made_trims: &mut Vec<(O, Curve)>, + cc: &mut CurveClassification, + face: FaceId, + surface: &Surface, + visits: crate::intersect::curve_class::p9::CachedVisits, + do_check_3d: bool, +) -> Result<()> { + // the face's edge uses behind the handles + let fu = fp2::upward_faceuse(b, face)?.ok_or(SolidError::null_pointer(OP_CURVE))?; + let mut ids = Vec::new(); + for &lu in &b.faceuse(fu)?.loopuses { + for eu in loopuse::edgeuses(b, lu, None, None)? { + let edge = b + .edgeuse(eu)? + .edge + .ok_or(SolidError::null_pointer(OP_CURVE))?; + ids.push((objects.edgeuse(eu), eu, edge)); + } + } + + // increment the mark on the context by 1 + let _mark_lock = k.context.claim_mark(crate::geom::context::ALL_MARKS); + for vertex in visits.vertices { + cc.ts_bound_vertex( k, &WithMadeTrimCurves { inner: objects, trims: made_trims.as_slice(), brep: b, - snapshot, - }, - &BoundaryFace { - surface, - loops: &loops, - cache: &lease.cache, - cache_handle: &cache_handle, + snapshot: &mut *snapshot, }, + surface, + vertex, do_check_3d, - ) - }); - lease.release(); - result + )?; + } + for mut edgeuse in visits.edgeuses { + let &(_, eu, edge) = ids + .iter() + .find(|(h, _, _)| *h == edgeuse.edgeuse) + .ok_or(SolidError::null_pointer(OP_CURVE))?; + // get/create the UVCurve for this Edgeuse + if edgeuse.trim_curve.is_none() { + let fetch = crate::topology::edgeuse::p3::get_or_create_uv_trim_curve( + k, snapshot, eu, false, false, true, + )?; + // SE: a failed status is only reported + let _ = fetch.status; + if let (Some(trim), Some(owner)) = eu1::uv_trim_curve(snapshot, eu)? { + let handle = objects.trim_curve(owner); + if !made_trims.iter().any(|(h, _)| *h == handle) { + made_trims.push((handle, trim.clone())); + } + edgeuse.trim_curve = Some(handle); + } + } + // SmTol::GetZoneTol3d(pE) as it is now + edgeuse.edge = boundary_edge(k, snapshot, objects, face, edge, false)?; + cc.ts_bound_edgeuse( + k, + &WithMadeTrimCurves { + inner: objects, + trims: made_trims.as_slice(), + brep: b, + snapshot: &mut *snapshot, + }, + surface, + edgeuse, + )?; + } + Ok(()) } /// `SmFace::GetEdges(SmTArray&, NULL, bTgtLoopsOnly, pOptTgtLoops)`. @@ -1923,6 +2035,97 @@ pub(crate) mod tests { assert_eq!(pc.src_zone_tol_3d(), 1e-6); } + #[test] + fn face_surface_cache_is_kept_on_the_surface_and_reused() { + let k = Kernel::default(); + let mut b = Brep::new(); + let f = square(&mut b); + let surface = || b.face(f).unwrap().surface.as_ref().unwrap(); + assert!(!surface().has_trim_cache()); + + // the first lookup makes the cache and keeps it on the surface + let mut lease = face_surface_cache(&k, &mut b, f).unwrap(); + assert!(lease.trimmed); + let surface = b.face(f).unwrap().surface.as_ref().unwrap(); + assert!(surface.trim_cache_in_use()); + // a lookup while it is in use gets a cache that is not kept + let nested = face_surface_cache(&k, &mut b, f).unwrap(); + assert!(!nested.kept); + nested.release(); + // a later lookup finds the same object, with what its use did to it + lease.cache.base_mut().process_boundary_curves = true; + let flag_before = lease.cache.base().point_test_enabled; + lease.cache.base_mut().point_test_enabled = !flag_before; + lease.release(); + let surface = b.face(f).unwrap().surface.as_ref().unwrap(); + assert!(surface.has_trim_cache() && !surface.trim_cache_in_use()); + let mut lease = face_surface_cache(&k, &mut b, f).unwrap(); + assert!(lease.kept); + assert_eq!(lease.cache.base().point_test_enabled, !flag_before); + + // a modified face while its B-rep is edited: still reused + lease.cache.base_mut().face_was_modified = true; + lease.release(); + b.set_editing_enabled(true); + let lease = face_surface_cache(&k, &mut b, f).unwrap(); + assert!(lease.cache.base().face_was_modified); + lease.release(); + // editing over: deleted and made again + b.set_editing_enabled(false); + let lease = face_surface_cache(&k, &mut b, f).unwrap(); + assert!(!lease.cache.base().face_was_modified); + assert_eq!(lease.cache.base().point_test_enabled, flag_before); + lease.release(); + + // a pre-edit notification marks the kept cache modified, also while in use + let surface = b.face(f).unwrap().surface.as_ref().unwrap(); + surface.mark_trim_cache_face_modified(); + let lease = face_surface_cache(&k, &mut b, f).unwrap(); + // (editing is off: the modified cache was made again) + assert!(!lease.cache.base().face_was_modified); + b.face(f) + .unwrap() + .surface + .as_ref() + .unwrap() + .mark_trim_cache_face_modified(); + lease.release(); + b.set_editing_enabled(true); + let lease = face_surface_cache(&k, &mut b, f).unwrap(); + assert!(lease.cache.base().face_was_modified); + lease.release(); + } + + #[test] + fn a_cache_made_while_the_owner_is_cleared_is_standalone_and_kept() { + let k = Kernel::default(); + let mut b = Brep::new(); + let f = square(&mut b); + b.begin_surface_owner_cleared(f); + let lease = face_surface_cache(&k, &mut b, f).unwrap(); + assert!(lease.kept && !lease.cache.base().have_t_surface_cache); + lease.release(); + b.end_surface_owner_cleared(f); + // reused as it is: no face data, so IntersectTSBound walks the loop uses + let lease = face_surface_cache(&k, &mut b, f).unwrap(); + assert!(lease.kept && !lease.cache.base().have_t_surface_cache); + lease.release(); + } + + #[test] + fn a_classification_snapshot_shares_the_surface_caches() { + let mut b = Brep::new(); + let f = square(&mut b); + let snapshot = snapshot_of(&b); + let original = b.face(f).unwrap().surface.as_ref().unwrap(); + let copy = snapshot.face(f).unwrap().surface.as_ref().unwrap(); + assert!(copy.shares_cache_slot_with(original)); + // a plain copy (the copy constructor) does not + let plain = b.clone(); + let copy = plain.face(f).unwrap().surface.as_ref().unwrap(); + assert!(!copy.shares_cache_slot_with(original)); + } + #[test] fn prepared_classification_matches_mutable_state() { let k = Kernel::default(); @@ -2141,7 +2344,6 @@ pub(crate) mod tests { struct Handles<'a> { b: &'a Brep, source: Curve, - } impl ClassGeometry for Handles<'_> { fn curve(&self, h: Handle) -> Option<&Curve> { @@ -2240,7 +2442,6 @@ pub(crate) mod tests { let objects = Handles { b: &b, source: Curve::from(Line::from_points(Vec3::ZERO, Vec3::X, 3)), - }; let mut cc = CurveClassification::with_curve_interval( &k, diff --git a/crates/solidcore/src/surface/surface/p2.rs b/crates/solidcore/src/surface/surface/p2.rs index 80a990598..d99f615a6 100644 --- a/crates/solidcore/src/surface/surface/p2.rs +++ b/crates/solidcore/src/surface/surface/p2.rs @@ -227,8 +227,8 @@ use crate::surface::surf_of_revolution::{Revolved, SurfOfRevolution}; use crate::surface::surface::p1::{StepSurface, SurfaceIntersectContext, SurfaceSolveContext}; use crate::surface::surface::p8::FaceSolveCache; use crate::surface::surface_cache::p1::SurfaceCache; -use crate::surface::trim_srf_cache::TrimSrfCache; use crate::surface::torus::Torus; +use crate::surface::trim_srf_cache::TrimSrfCache; use crate::topology::database_io::DatabaseReader; use crate::topology::trimming_tools; @@ -985,6 +985,14 @@ impl Surface { .with(|s| !matches!(s.trim, TrimCacheSlot::Empty)) } + /// Whether this surface's trimmed surface cache is in use (taken out of its slot + /// by a caller that has not given it back yet). + #[must_use] + pub fn trim_cache_in_use(&self) -> bool { + self.cache_slot + .with(|s| matches!(s.trim, TrimCacheSlot::CheckedOut)) + } + /// Makes this surface share `other`'s cache slot: `self` is a copy that stands for /// the same surface object (a private snapshot of a B-rep standing for the B-rep /// itself). diff --git a/crates/solidcore/src/surface/surface/p4.rs b/crates/solidcore/src/surface/surface/p4.rs index 0d470ed45..a6aa1745a 100644 --- a/crates/solidcore/src/surface/surface/p4.rs +++ b/crates/solidcore/src/surface/surface/p4.rs @@ -15,7 +15,7 @@ //! | [`pass_sp_convergence_test`] | `sm_PassSPConvergenceTest` (static) | //! | [`Surface::create_seam_curve`] | `SmSurface::CreateSeamCurve` | //! | [`Surface::is_on_seam`], [`SeamInfo`] | `SmSurface::IsOnSeam` | -//! | [`Surface::global_point_solve`], [`Surface::global_point_solve_point_test_off`], [`global_point_solve_starts`], [`NoObject`] | `SmSurface::GlobalPointSolve` | +//! | [`Surface::global_point_solve`], [`Surface::global_point_solve_point_test_off`], [`Surface::global_point_solve_face_tested`], [`FaceTest`], [`global_point_solve_starts`], [`NoObject`] | `SmSurface::GlobalPointSolve` | //! | [`classify_curve_surface_xsect_point`], [`XsectClassification`] | `sm_ClassifyCurveSurfaceXSectPoint` (static) | //! | [`CsIntersectEnfo`] (`EvalNFunctions`) | `SmCSIntersectENFO::Evaluate` (struct: `surface.surface_p1`) | //! | [`CsNormalizeEnfo`] (`EvalNFunctions`) | `SmCSNormalizeENFO::Evaluate` (struct: `surface.surface_p1`) | @@ -117,6 +117,11 @@ const OP_SS_NORMALIZE: &str = "surface::surface::p4::SsNormalizeEnfo::evaluate"; const OP_EVAL_NORMAL: &str = "surface::surface::p4::SurfaceEval::eval_normal"; const OP_CLASSIFY: &str = "surface::surface::p4::classify_curve_surface_xsect_point"; +/// The trim boundary test a caller holding a face gives +/// [`Surface::global_point_solve_face_tested`]: whether the solution at `uv` (classified +/// with the tolerance given) lies in or on the face; `None` for no test. +pub type FaceTest<'a> = Option<&'a mut dyn FnMut(&Kernel, Point2, f64) -> Result>; + /// Entry `i` of an evaluation result. fn nth(v: &[Vec3], i: usize, op: &'static str) -> Result { v.get(i) @@ -1339,6 +1344,7 @@ impl Surface { opt_target_distance, requested, &mut solutions, + None, true, )?; Ok(solutions) @@ -1375,6 +1381,7 @@ impl Surface { opt_target_distance, requested, solutions, + None, true, ) } @@ -1410,6 +1417,7 @@ impl Surface { opt_target_distance, requested, solutions, + None, false, ) } @@ -1447,10 +1455,53 @@ impl Surface { opt_target_distance, requested, solutions, + None, false, ) } + /// [`Surface::global_point_solve`] of a face's surface by a caller that holds the + /// face and the surface's cache: the trim boundary test of the fast drop + /// (`pFace->PointClassify(uv, tol, TRUE, FALSE, ..)` of each solution, keeping + /// face, edge and vertex points) is `classify` (`uv`, `src_zone_tol_3d`), or none + /// when `classify` is `None` (the cache is not a trimmed one, has point testing off + /// or its face was modified). + /// + /// # Errors + /// + /// As [`Surface::global_point_solve`], and those of `classify`. + #[allow(clippy::too_many_arguments)] + pub fn global_point_solve_face_tested( + &self, + k: &Kernel, + surface: Option, + uv_domain: &Extent2d, + op: SolverOperation, + test_point: Point3, + src_zone_tol_3d: f64, + opt_target_distance: Option, + requested: SolutionRequested, + classify: FaceTest<'_>, + ) -> Result> { + let mut solutions = SolutionArray::new(); + self.global_point_solve_in( + k, + surface, + None, + None, + uv_domain, + op, + test_point, + src_zone_tol_3d, + opt_target_distance, + requested, + &mut solutions, + Some(classify), + false, + )?; + Ok(solutions) + } + #[allow(clippy::too_many_arguments, clippy::too_many_lines)] fn global_point_solve_in( &self, @@ -1465,6 +1516,7 @@ impl Surface { opt_target_distance: Option, requested: SolutionRequested, solutions: &mut SolutionArray, + face_test: Option>, point_test_off: bool, ) -> Result<()> { // init output @@ -1510,7 +1562,23 @@ impl Surface { // values are within the trim boundaries of the face, unless flags in its // surface cache indicate otherwise. if !point_test_off && self.owner().is_some_and(|o| o.type_code == FACE_TYPE) { - if let Some(context) = context { + if let Some(test) = face_test { + // the caller holds the face and its surface's cache: it tests + // only when that cache is trimmed, point testing is on and its + // face was not modified (`None`: no test) + if let Some(classify) = test { + let mut i = 0; + while i < solutions.len() { + let point = solutions.get_at(i)?; + let uv = Point2::new(point.start.param(0)?, point.start.param(1)?); + if classify(k, uv, src_zone_tol_3d)? { + i += 1; + } else { + solutions.remove_at(i, 1); + } + } + } + } else if let Some(context) = context { let built; let cache = if let Some(cache) = cache { cache diff --git a/crates/solidcore/src/topology/face/p4.rs b/crates/solidcore/src/topology/face/p4.rs index 6c22164dd..3447a8507 100644 --- a/crates/solidcore/src/topology/face/p4.rs +++ b/crates/solidcore/src/topology/face/p4.rs @@ -123,8 +123,7 @@ use crate::nurbs::b_spline_surface::p1::SurfParamType; use crate::nurbs::b_spline_surface_n_lib::p2::OrientType; use crate::query::face::p2::{ curve_on_classify_cached, face_cache_tree, face_surface_cache, point_classify, BrepCacheFace, - FaceSurfaceCache, - CurveOnClassifyCache, CurveOnClassifyObjects, PointClassifyOptions, + CurveOnClassifyCache, CurveOnClassifyObjects, FaceSurfaceCache, PointClassifyOptions, }; use crate::surface::surface::p5::{SS_NONE, SS_UMAX, SS_UMIN, SS_VMAX, SS_VMIN}; use crate::surface::surface::p8::{FaceSolveCache, HeldSolveCache}; @@ -666,9 +665,7 @@ fn global_line_intersect_with( if sol.solution_type == SolutionType::SingleValue { // Get uv on surface let uv = Point2::new(sol.start.parameters[1], sol.start.parameters[2]); - if classify_uv_point(k, b, f, uv, src_zone_tol_3d, None)? - == PointClassType::Unknown - { + if classify_uv_point(k, b, f, uv, src_zone_tol_3d, None)? == PointClassType::Unknown { removal_indices.push(ii); ii += 1; continue; diff --git a/crates/solidcore/src/topology/vertexuse.rs b/crates/solidcore/src/topology/vertexuse.rs index ffc6e9128..f3bde401f 100644 --- a/crates/solidcore/src/topology/vertexuse.rs +++ b/crates/solidcore/src/topology/vertexuse.rs @@ -768,37 +768,84 @@ fn local_point_solve( )) } -/// `pSurface->GlobalPointSolve(domain, op, point, tol, NULL, request, solutions)` on -/// the surface's cache made to look standalone (`m_bHaveTSurfaceCache = FALSE`) with -/// boundary curve processing on and, when `point_test` is false, point testing off -/// (all restored afterwards); `NER` of a missing surface cache. +/// `pSurface->GlobalPointSolve(domain, op, point, tol, NULL, request, solutions)` of +/// face `f`'s surface, on the surface's cache (`smsurf_GetSurfaceCache`, `NER`) checked +/// out and made to look standalone (`m_bHaveTSurfaceCache = FALSE`) with boundary +/// curve processing on and, when `point_test` is false, point testing off (all restored +/// afterwards). The cache stays out of the surface's slot during the solve, so the +/// face classifications in it walk the loops as upstream's do with +/// `m_bHaveTSurfaceCache` off. The fast drop's trim test classifies on the B-rep when +/// the cache is a trimmed one with point testing on and its face not modified. A cache +/// already in use by an enclosing caller (upstream the same object, e.g. while its +/// tree is built with point testing off) is not fetched again, and the solve tests no +/// point. +#[allow(clippy::too_many_arguments)] fn global_point_solve( k: &Kernel, - surface: &Surface, + b: &mut Brep, + f: FaceId, domain: &Extent2d, op: SolverOperation, point: Point3, tol: f64, request: SolutionRequested, - _point_test: bool, + point_test: bool, ) -> Result> { - // smsurf_GetSurfaceCache(pSurface): the surface's cache slot (its tree is not built - // during a Boolean operation, which later continuity readers can observe) - crate::surface::surface::p1::note_surface_cache(k, surface); - // The Rust surface solver builds no surface cache, so the cache flags upstream - // switches around the call (point testing, boundary curves, `m_bHaveTSurfaceCache`) - // have nothing to act on; a face-owned surface is Unported (query.face_p2) inside. - let solutions = surface.global_point_solve::( - k, - Some(crate::surface::surface::p4::NoObject), - domain, - op, - point, - tol, - None, - request, - )?; - solutions + use crate::intersect::point_class::{PointClassType, PointClassification}; + use crate::query::face::p2::{face_surface_cache, point_classify, PointClassifyOptions}; + + let surface = face_surface(b, f)?.clone_for_evaluation()?; + let lease = if surface.trim_cache_in_use() { + None + } else { + Some(face_surface_cache(k, b, f)?) + }; + let test_on = point_test + && lease.as_ref().is_some_and(|l| { + l.trimmed && l.cache.base().point_test_enabled && !l.cache.base().face_was_modified + }); + let mut classify = |k: &Kernel, uv: Point2, src_tol: f64| -> Result { + // pFace->PointClassify(uv, tol, TRUE, FALSE, sPtClass): good on SM_SUCCESS and + // a face, edge or vertex class + let mut pc = PointClassification::new(src_tol); + let status = point_classify( + k, + b, + f, + uv, + &mut pc, + PointClassifyOptions { + src_zone_tol_3d: src_tol, + do_boundary_intersections: true, + use_uv_classification: false, + ..PointClassifyOptions::default() + }, + ); + match status { + Err(e) if e.is_unported() => Err(e), + Err(_) => Ok(false), + Ok(()) => Ok(matches!( + pc.point_class(), + PointClassType::Face | PointClassType::Edge | PointClassType::Vertex + )), + } + }; + let solutions = surface + .global_point_solve_face_tested::( + k, + Some(crate::surface::surface::p4::NoObject), + domain, + op, + point, + tol, + None, + request, + if test_on { Some(&mut classify) } else { None }, + ); + if let Some(lease) = lease { + lease.release(); + } + solutions? .as_slice() .iter() .map(|s| { @@ -1048,7 +1095,8 @@ pub fn compute_uv_point( let face_tol = face_tolerance(b, f)?; let sols = global_point_solve( k, - face_surface(b, f)?, + b, + f, &uv_domain, SolverOperation::Minimize, vertex_pnt, @@ -1081,10 +1129,11 @@ pub fn compute_uv_point( } else if is_loop_vertexuse(b, vu)? { // project point down to face's surface let face_tol = face_tolerance(b, f)?; - let surface = face_surface(b, f)?; + let mut sols = global_point_solve( k, - surface, + b, + f, &uv_domain, SolverOperation::Intersect, vertex_pnt, @@ -1096,7 +1145,8 @@ pub fn compute_uv_point( if sols.is_empty() { sols = global_point_solve( k, - surface, + b, + f, &uv_domain, SolverOperation::Minimize, vertex_pnt, From 99d0a23c6cff4943ce3e1a112e12a10af620f05d Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 19:09:11 -0700 Subject: [PATCH 4/6] surface caches: standalone caches made while a face surface's owner is cleared are kept; scratch copies share the cache slots IntersectVertices and IntersectEdges clear the face surface's owner while they read the vertex/face and edge/face gaps, so a trimmed cache that ComputeUVPoint makes then is a standalone one, and upstream keeps it on the surface (SmCacheMgrTSrf). The face-unaware smsurf_GetSurfaceCache keeps such a cache too when it is made in an owner-cleared scope. The classification snapshots, HasSeamProblem's seam objects and the curve classification's binormal and sector scratch copies stand for the B-rep itself, so their face surfaces share the B-rep's cache slots (Brep::clone_sharing_surface_caches). difftest: intersect.curve_class_p9's ComputeUVPoint fixture attaches the shim's cache first, as the shim does; topology.brep_p4 seeds the face surfaces' cache states a file read leaves. Co-Authored-By: Claude Opus 5.5 --- .../src/units/intersect_curve_class_p9.rs | 6 ++ crates/difftest/src/units/topology_brep_p4.rs | 13 ++++ .../solidcore/src/intersect/curve_class/p4.rs | 4 +- crates/solidcore/src/query/face/p2.rs | 42 +++++------ crates/solidcore/src/surface/surface/p1.rs | 2 + crates/solidcore/src/surface/surface/p2.rs | 72 +++++++++++++++++++ .../solidcore/src/surface/trim_srf_cache.rs | 21 ++++++ crates/solidcore/src/topology.rs | 25 +++++++ crates/solidcore/src/topology/face/p4.rs | 2 +- 9 files changed, 161 insertions(+), 26 deletions(-) diff --git a/crates/difftest/src/units/intersect_curve_class_p9.rs b/crates/difftest/src/units/intersect_curve_class_p9.rs index 6e0413f3c..8e6eb75a2 100644 --- a/crates/difftest/src/units/intersect_curve_class_p9.rs +++ b/crates/difftest/src/units/intersect_curve_class_p9.rs @@ -417,6 +417,12 @@ fn loop_vertex_uv(k: &Kernel, surface: &Surface, point: Vec3, zone: f64) -> ScRe ..Vertexuse::default() })?; b.loopuse_mut(lu)?.start = LoopuseStart::Vertexuse(Some(vu)); + // The shim attaches its cache to the surface first, with point testing off, so + // ComputeUVPoint's smsurf_GetSurfaceCache reuses it instead of building a trimmed + // cache of the face: give the Rust surface the same kind of cache. + if let Some(s) = b.face(f)?.surface.as_ref() { + s.attach_trim_cache(|c| c.base_mut().point_test_enabled = false)?; + } solidcore::topology::vertexuse::compute_uv_point(k, &mut b, vu, true) } fn rust(i: &Input) -> ScResult { diff --git a/crates/difftest/src/units/topology_brep_p4.rs b/crates/difftest/src/units/topology_brep_p4.rs index ac8f6f99c..ff35f76bf 100644 --- a/crates/difftest/src/units/topology_brep_p4.rs +++ b/crates/difftest/src/units/topology_brep_p4.rs @@ -842,6 +842,19 @@ impl UnitDiff for Runner { } let mut b = import(&f)?; own_surfaces(&mut b)?; + // reading a file heals the B-rep during a Boolean operation, which leaves its + // face surfaces with trimmed caches whose trees are not built (and that later + // smsurf_GetSurfaceCache calls reuse): the face surfaces' cache states as the + // C++ read left them + if let BrepIn::File { .. } = &i.brep { + use super::topology_edgeuse_p1 as eu1; + let file = eu1::BrepIn::File { + file: String::new(), + rebuild: false, + }; + let blk = eu1::brep_block_of(Some(&file), false, false)?; + eu1::seed_cache_tree_states(&b, &path, &blk)?; + } damage(&mut b, i.damage)?; b.set_editing_enabled(i.editing); let pre = record_counts(&b); diff --git a/crates/solidcore/src/intersect/curve_class/p4.rs b/crates/solidcore/src/intersect/curve_class/p4.rs index 53cd4ecf2..af964123b 100644 --- a/crates/solidcore/src/intersect/curve_class/p4.rs +++ b/crates/solidcore/src/intersect/curve_class/p4.rs @@ -440,7 +440,7 @@ fn edgeuse_evaluate_binormal>( if !eu1::is_loop_edgeuse(b, eu)? { return Ok(()); } - let mut scratch = b.clone(); + let mut scratch = b.clone_sharing_surface_caches(); match topo_eu3::evaluate_binormal( k, &mut scratch, @@ -500,7 +500,7 @@ fn vertexuse_compute_uv_sector>( vertexuse: O, ) -> Result> { let (b, vu) = vertexuse_id(geometry, vertexuse, OP_CROSS)?; - let mut scratch = b.clone(); + let mut scratch = b.clone_sharing_surface_caches(); let io = topo_vertexuse::compute_uv_sector(k, &mut scratch, vu, false)?.io; let eu_handle = |eu: Option| -> Result> { eu.map(|eu| handle_of(geometry, b, EntityRef::Edgeuse(eu), OP_CROSS)) diff --git a/crates/solidcore/src/query/face/p2.rs b/crates/solidcore/src/query/face/p2.rs index 717adbc40..c79c423dc 100644 --- a/crates/solidcore/src/query/face/p2.rs +++ b/crates/solidcore/src/query/face/p2.rs @@ -1139,21 +1139,6 @@ pub(crate) struct CurveOnClassifyCache { snapshot: Option, } -/// A private snapshot of `b` standing for `b` itself: a copy whose face surfaces share -/// the cache slots of `b`'s ([`Surface::share_cache_slot_of`]), so that the surface -/// caches the classification makes or uses on it are the B-rep's surfaces' own. -fn snapshot_of(b: &Brep) -> Brep { - let mut snapshot = b.clone(); - for (id, face) in b.faces().iter() { - if let (Some(original), Ok(copy)) = (face.surface.as_ref(), snapshot.face_mut(id)) { - if let Some(copy) = copy.surface.as_mut() { - copy.share_cache_slot_of(original); - } - } - } - snapshot -} - impl CurveOnClassifyCache { pub(crate) fn apply_gap_updates(self, b: &mut Brep) -> Result<()> { if let Some(snapshot) = self.snapshot { @@ -1530,7 +1515,7 @@ pub(crate) fn curve_on_classify_cached( if curve.dim() == 2 { // 2D Curve branch - work in surface's uv space directly on the Face's UVcurves cc.set_surface1(objects.face_surface(f)); - let snapshot = snapshot.get_or_insert_with(|| snapshot_of(b)); + let snapshot = snapshot.get_or_insert_with(|| b.clone_sharing_surface_caches()); match intersect_ts_bound(k, b, snapshot, objects, &mut made_trims, cc, f, do_check_3d) { Ok(()) => {} Err(e) if e.is_unported() => return Err(e), @@ -1557,8 +1542,12 @@ pub(crate) fn curve_on_classify_cached( // bHasBigGaps: each vertex's GetVertexuseOfFace(pFace)->GetVertexFaceGap() // (pFace is this face, set on the classification above) let gaps = if has_big_gaps { - let snapshot = snapshot.get_or_insert_with(|| snapshot_of(b)); - vertices + let snapshot = snapshot.get_or_insert_with(|| b.clone_sharing_surface_caches()); + // IntersectVertices reads them with the surface's owner cleared + // (SmTemporaryChangeValue on m_pOwner), so a surface cache made for + // them is a standalone one + snapshot.begin_surface_owner_cleared(f); + let gaps = vertices .iter() .map(|&v| { Ok(( @@ -1566,7 +1555,9 @@ pub(crate) fn curve_on_classify_cached( vertex_face_gap_length(k, snapshot, v, f)?, )) }) - .collect::>>()? + .collect::>>(); + snapshot.end_surface_owner_cleared(f); + gaps? } else { Vec::new() }; @@ -1585,7 +1576,7 @@ pub(crate) fn curve_on_classify_cached( intersect_edges( k, b, - snapshot.get_or_insert_with(|| snapshot_of(b)), + snapshot.get_or_insert_with(|| b.clone_sharing_surface_caches()), objects, cc, &edges, @@ -1630,7 +1621,7 @@ pub(crate) fn curve_on_classify_cached( inner: objects, trims: made_trims.as_slice(), brep: b, - snapshot: snapshot.get_or_insert_with(|| snapshot_of(b)), + snapshot: snapshot.get_or_insert_with(|| b.clone_sharing_surface_caches()), }, ii, do_point_classify, @@ -1855,10 +1846,15 @@ fn intersect_edges( .surface .as_ref() .ok_or(SolidError::null_pointer(OP_CURVE))?; + // IntersectEdges reads the edge/face gaps with the surface's owner cleared + // (SmTemporaryChangeValue on m_pOwner) + snapshot.begin_surface_owner_cleared(face); let views = edges .iter() .map(|&edge| boundary_edge(k, snapshot, objects, face, edge, has_big_gaps)) - .collect::>>()?; + .collect::>>(); + snapshot.end_surface_owner_cleared(face); + let views = views?; cc.intersect_edges( k, objects, @@ -2116,7 +2112,7 @@ pub(crate) mod tests { fn a_classification_snapshot_shares_the_surface_caches() { let mut b = Brep::new(); let f = square(&mut b); - let snapshot = snapshot_of(&b); + let snapshot = b.clone_sharing_surface_caches(); let original = b.face(f).unwrap().surface.as_ref().unwrap(); let copy = snapshot.face(f).unwrap().surface.as_ref().unwrap(); assert!(copy.shares_cache_slot_with(original)); diff --git a/crates/solidcore/src/surface/surface/p1.rs b/crates/solidcore/src/surface/surface/p1.rs index 1286912b9..e33b8453d 100644 --- a/crates/solidcore/src/surface/surface/p1.rs +++ b/crates/solidcore/src/surface/surface/p1.rs @@ -1024,6 +1024,8 @@ pub fn make_trim_srf_cache( let mut cache = TrimSrfCache::new(surface.clone_for_evaluation()?, cache_options(tol)); cache.base_mut().set_surface_handle(handle); trim_srf_cache_tree(k, surface, state, &mut cache, |c| c.build_tree(k))?; + // a face surface whose owner is cleared keeps it (SmCacheMgr::AddToObjectCache) + surface.keep_standalone_trim_cache(&cache)?; Ok(cache) } diff --git a/crates/solidcore/src/surface/surface/p2.rs b/crates/solidcore/src/surface/surface/p2.rs index d99f615a6..8d57afb77 100644 --- a/crates/solidcore/src/surface/surface/p2.rs +++ b/crates/solidcore/src/surface/surface/p2.rs @@ -231,6 +231,7 @@ use crate::surface::torus::Torus; use crate::surface::trim_srf_cache::TrimSrfCache; use crate::topology::database_io::DatabaseReader; use crate::topology::trimming_tools; +use crate::topology::FaceId; // ---- type codes --------------------------------------------------------------------- @@ -387,6 +388,10 @@ struct SlotState { trim: TrimCacheSlot, /// The face was modified while the trimmed cache was in use. face_modified_while_out: bool, + /// The faces whose `CreateUVTrimCurve(s)` has cleared this surface's owner (one + /// entry per active clearing, innermost last; see + /// [`Surface::begin_owner_cleared`]). + cleared_for: Vec, } /// The cache slot of a surface. Upstream keeps a surface's cache on the surface object @@ -939,6 +944,26 @@ impl Surface { }); } + /// `SetCacheObj(new SmTrimSrfCache(*this))`: attaches a new trimmed surface cache + /// with the default tolerances and no tree, set up by `configure`, as this + /// surface's (a caller that makes the cache itself). + /// + /// # Errors + /// + /// Those of the surface copy. + pub fn attach_trim_cache( + &self, + configure: impl FnOnce(&mut TrimSrfCache), + ) -> Result<()> { + let mut cache = TrimSrfCache::new( + self.clone_for_evaluation()?, + crate::surface::surface_cache::p1::SurfaceCacheOptions::default(), + ); + configure(&mut cache); + Self::store_trim_cache(cache); + Ok(()) + } + /// `SmFace::Notify(SM_NO_PRE_EDIT)` on this face surface's trimmed surface cache /// (`GetObjectCache(SM_OC_TRIMSRF, ..)`): sets its `m_bFaceWasModified`; a cache in /// use gets it when it is stored again. @@ -985,6 +1010,53 @@ impl Surface { .with(|s| !matches!(s.trim, TrimCacheSlot::Empty)) } + /// Records that face `f`'s `CreateUVTrimCurve(s)` cleared this face surface's owner + /// (the start of its `SmTemporaryChangeValue(m_pOwner, NULL)` scope), for + /// this surface and every copy that shares its cache slot: a face-unaware + /// `smsurf_GetSurfaceCache` of it in the meantime makes the standalone trimmed cache + /// upstream keeps on the surface ([`Surface::keep_standalone_trim_cache`]). + /// [`Surface::end_owner_cleared`] ends it. + pub fn begin_owner_cleared(&self, f: FaceId) { + self.cache_slot.with(|s| s.cleared_for.push(f)); + } + + /// Ends the innermost [`Surface::begin_owner_cleared`] scope of face `f`. + pub fn end_owner_cleared(&self, f: FaceId) { + self.cache_slot.with(|s| { + if let Some(i) = s.cleared_for.iter().rposition(|&g| g == f) { + s.cleared_for.remove(i); + } + }); + } + + /// `SmCacheMgrTSrf::CacheMakeOrValidate` keeping the trimmed cache it made with + /// the surface: for a face surface whose owner is cleared + /// ([`Surface::begin_owner_cleared`]) and that has no trimmed cache yet, a copy of + /// the standalone `cache` just built for it (its handles all name the surface, + /// which becomes the face's) is stored as the surface's trimmed cache, so that the + /// face's later lookups reuse it as upstream's do. Any other surface keeps nothing + /// (the port builds its caches afresh). + /// + /// # Errors + /// + /// Those of the handle mapping (a cache of this surface has only the surface's + /// handle, so none in practice). + pub fn keep_standalone_trim_cache( + &self, + cache: &TrimSrfCache, + ) -> Result<()> { + let face = self.cache_slot.with(|s| match s.trim { + TrimCacheSlot::Empty => s.cleared_for.last().copied(), + TrimCacheSlot::Stored(_) | TrimCacheSlot::CheckedOut => None, + }); + if let Some(f) = face { + if cache.base().surface().shares_cache_slot_with(self) { + Self::store_trim_cache(cache.map_handles(|_| Ok(EntityRef::Face(f)))?); + } + } + Ok(()) + } + /// Whether this surface's trimmed surface cache is in use (taken out of its slot /// by a caller that has not given it back yet). #[must_use] diff --git a/crates/solidcore/src/surface/trim_srf_cache.rs b/crates/solidcore/src/surface/trim_srf_cache.rs index d4f21a1d2..d384efb16 100644 --- a/crates/solidcore/src/surface/trim_srf_cache.rs +++ b/crates/solidcore/src/surface/trim_srf_cache.rs @@ -366,6 +366,27 @@ impl TrimSrfCache { &mut self.base } + /// The same cache with every handle mapped by `map` (the base's + /// [`SurfaceCache::map_handles`] and the implanted edge uses' curves). + /// + /// # Errors + /// + /// Those of `map` and of the base's mapping. + pub fn map_handles( + &self, + map: impl Fn(O) -> Result

, + ) -> Result> { + let edgeuse_curves = self + .edgeuse_curves + .iter() + .map(|(o, c)| Ok((map(*o)?, c.clone_exact()))) + .collect::>()?; + Ok(TrimSrfCache { + base: self.base.map_handles(&map)?, + edgeuse_curves, + }) + } + /// Unwraps the `SmSurfaceCache` base. #[must_use] pub fn into_base(self) -> SurfaceCache { diff --git a/crates/solidcore/src/topology.rs b/crates/solidcore/src/topology.rs index 32743bcd9..f97eaf158 100644 --- a/crates/solidcore/src/topology.rs +++ b/crates/solidcore/src/topology.rs @@ -760,6 +760,28 @@ impl Brep { /// [`Brep::end_surface_owner_cleared`] ends it. pub fn begin_surface_owner_cleared(&mut self, f: FaceId) { self.surface_owner_cleared.push(f); + // the surface object (and its copies) learns it too + if let Some(s) = self.faces.get(f).and_then(|face| face.surface.as_ref()) { + s.begin_owner_cleared(f); + } + } + + /// A private copy of this B-rep that stands for the B-rep itself (a query's scratch + /// copy where upstream works on the B-rep): its face surfaces share the cache slots + /// of this B-rep's ([`Surface::share_cache_slot_of`]), so that the surface caches the + /// query makes or reuses on the copy are the B-rep's surfaces' own, as upstream's + /// are. A plain `clone` (the copy constructor) starts every surface without cache. + #[must_use] + pub fn clone_sharing_surface_caches(&self) -> Self { + let mut copy = self.clone(); + for (id, face) in self.faces.iter() { + if let (Some(original), Ok(face_copy)) = (face.surface.as_ref(), copy.face_mut(id)) { + if let Some(surface) = face_copy.surface.as_mut() { + surface.share_cache_slot_of(original); + } + } + } + copy } /// Ends the innermost [`Brep::begin_surface_owner_cleared`] scope of face `f`. @@ -767,6 +789,9 @@ impl Brep { if let Some(i) = self.surface_owner_cleared.iter().rposition(|&g| g == f) { self.surface_owner_cleared.remove(i); } + if let Some(s) = self.faces.get(f).and_then(|face| face.surface.as_ref()) { + s.end_owner_cleared(f); + } } /// `m_pLockedBoundingBox` (unit `query.brep`): the locked bounding box, if any. diff --git a/crates/solidcore/src/topology/face/p4.rs b/crates/solidcore/src/topology/face/p4.rs index 3447a8507..728875be9 100644 --- a/crates/solidcore/src/topology/face/p4.rs +++ b/crates/solidcore/src/topology/face/p4.rs @@ -2067,7 +2067,7 @@ impl ClassGeometry for BrepSeamObjects<'_, O> { .map(|(f, _)| f)?, }; let vu = vertex::vertexuse_of_face(self.b, v, Some(f)).ok()??; - let mut snapshot = self.b.clone(); + let mut snapshot = self.b.clone_sharing_surface_caches(); let gap = vertexuse::vertex_face_gap(self.k, &mut snapshot, vu, false).ok()?; Some(gap.map_or(0.0, |g| g.gap_3d)) } From 3761ba48a2cb9b5e17630f59bb7b4aa734feaeda Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 19:28:18 -0700 Subject: [PATCH 5/6] difftest: the import's gap refill leaves no surface cache history; docs for the kept trimmed caches The shared import recomputes the cached gaps the C++ export reported. Its ComputeUVPoint calls now fetch the face surface's trimmed cache, which would build it with the face (making UV trim curves the C++ B-rep does not have). The refill now runs as during a Boolean operation and drops the trimmed caches it kept, leaving each surface's slot as before (topology.brep_p4: 241 -> 302 passes). Co-Authored-By: Claude Opus 5.5 --- .../difftest/src/units/topology_vertexuse.rs | 35 ++++++++++++++++++- crates/solidcore/src/query/face/p2.rs | 28 ++++++++++++--- crates/solidcore/src/surface/surface/p2.rs | 14 ++++++-- crates/solidcore/src/topology/vertexuse.rs | 12 +++++-- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/crates/difftest/src/units/topology_vertexuse.rs b/crates/difftest/src/units/topology_vertexuse.rs index 8ca65cee1..c5313676a 100644 --- a/crates/difftest/src/units/topology_vertexuse.rs +++ b/crates/difftest/src/units/topology_vertexuse.rs @@ -1407,8 +1407,41 @@ pub(crate) fn recalc_gaps(b: &mut Brep) -> Result<(), SolidError> { /// Each record is stored only after it is fully computed. `strict`: stop at the first /// failure; else skip the failed record. fn fill_gaps(b: &mut Brep, strict: bool) -> Result<(), SolidError> { + // The refill reconstructs values the C++ export reported, and must not leave cache + // history the C++ side did not have: its smsurf_GetSurfaceCache calls build no + // tree (as during a Boolean operation: no UV trim curves are made), and the trimmed + // caches they keep are dropped again, leaving each surface's cache slot as a + // face-unaware lookup outside a Boolean operation leaves it (a built tree). + use solidcore::surface::surface::CacheTreeState; + let before: Vec> = b + .faces() + .iter() + .map(|(_, f)| { + f.surface + .as_ref() + .map(|s| (s.has_trim_cache(), s.cache_tree_state())) + }) + .collect(); + let result = fill_gaps_body(b, strict); + for ((_, f), before) in b.faces().iter().zip(before) { + if let (Some(s), Some((had, state))) = (f.surface.as_ref(), before) { + if !had && s.has_trim_cache() && !s.trim_cache_in_use() { + s.delete_trim_cache(); + } + if state == CacheTreeState::NoCache && s.cache_tree_state() == CacheTreeState::Unbuilt + { + s.set_cache_tree_state(CacheTreeState::Built); + } + } + } + result +} + +fn fill_gaps_body(b: &mut Brep, strict: bool) -> Result<(), SolidError> { use std::collections::BTreeSet; - let k = Kernel::default(); + let mut k = Kernel::default(); + k.context.set_doing_boolean(true); + let k = k; let keep = |r: Result<(), SolidError>| match r { Err(e) if strict => Err(e), _ => Ok(()), diff --git a/crates/solidcore/src/query/face/p2.rs b/crates/solidcore/src/query/face/p2.rs index c79c423dc..878bd4436 100644 --- a/crates/solidcore/src/query/face/p2.rs +++ b/crates/solidcore/src/query/face/p2.rs @@ -17,7 +17,9 @@ //! | [`ProtoTopologyManager::capture_distance`] | `SmProtoTopologyManager::GetCaptureDistance` (already in `topology::proto_topology`) | //! | [`face_vertices`] | `SmFace::GetVertices(SmTArray&, ..)` (CO-PORTED, `topology.face_p4`) | //! | `ClassObject for EntityRef` | the `SmObject*` of a classification of B-rep topology | -//! | [`BrepCacheFace`], [`face_surface_cache`] | `GetFace()` of a face surface's trimmed cache (`SmTrimSrfCache::BuildTree`, `PointClassify`), `smsurf_GetSurfaceCache(GetSurface())` | +//! | [`BrepCacheFace`] | `GetFace()` of a face surface's trimmed cache (`SmTrimSrfCache::BuildTree`, `PointClassify`) | +//! | [`face_surface_cache`], [`FaceSurfaceCache`], [`note_face_surface_cache`] | `smsurf_GetSurfaceCache(GetSurface())` through `SmCacheMgrTSrf::CacheMakeOrValidate` | +//! | [`face_cache_tree`] | `SmSurfaceCache::GetTree` of a face surface's trimmed cache | //! //! Deviations: //! - **Object model.** The face is a record of a [`Brep`]; the functions take the @@ -29,10 +31,16 @@ //! `bDoBoundaryIntersections` and `bUVSpaceOkay` are `NotUsed` upstream; they are //! kept in [`PointClassifyOptions`] and ignored. `bUseUVClassification` only chooses //! between two identical `SM_ERR` returns. -//! - **Trimmed surface cache.** `SmCacheMgr::GetObjectCache(SM_OC_TRIMSRF, surface)` -//! has no counterpart: B-rep surfaces carry no object cache in the port, so the -//! caller passes the face surface's [`TrimSrfCache`] (if any) in -//! [`PointClassifyOptions::trim_cache`]. The test +//! - **Trimmed surface cache.** A face surface keeps its trimmed cache in its cache +//! slot ([`Surface::take_trim_cache`]; upstream's `SM_OC_TRIMSRF` queue entry), made +//! by the first [`face_surface_cache`] and reused by later ones as +//! `CacheMakeOrValidate` reuses it (whether it was built with the face or +//! standalone, while the owner was cleared). A lookup made while the cache is in use +//! (taken out of the slot) gets a cache built the same way that is not kept. The +//! global cache queue's size limit (200 trimmed caches, least recently used out) is +//! not modelled. `SmCacheMgr::GetObjectCache(SM_OC_TRIMSRF, surface)` of +//! `PointClassify` is the caller's [`PointClassifyOptions::trim_cache`], else the +//! surface's kept cache (none while it is in use). The test //! `GetSurface()->GetFace() == this` is "the face's surface has a face owner" //! (the owner of a face's surface is that face). //! - **Out-of-bounds evaluation.** `SmTemporaryChangeValue` on the surface's @@ -85,6 +93,16 @@ //! 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). +//! - **`IntersectTSBound`** fetches the face surface's trimmed cache itself +//! ([`face_surface_cache`] on the classification snapshot, whose face surfaces share +//! the B-rep's cache slots: [`Brep::clone_sharing_surface_caches`]); when it is up to +//! date the cached branch visits only the vertex uses and edge uses of the leaf nodes +//! the curve's box meets, under a context mark +//! ([`crate::geom::context::Context::claim_mark`]), and makes a visited edge use's +//! missing UV trim curve on the snapshot (`GetOrCreateUVTrimCurve`). +//! `IntersectVertices` / `IntersectEdges` clear the surface's owner while they read +//! the vertex/face and edge/face gaps, so the gap reads run in an owner-cleared scope +//! ([`Brep::begin_surface_owner_cleared`]) and a cache they make is standalone. //! - The UV trim curves `IntersectTSBound`'s `ComputeUVPoint` calls make there (upstream //! on the face's own B-rep) stay visible for the rest of the classification through //! the private `WithMadeTrimCurves` geometry. The gap caches and these trim curves diff --git a/crates/solidcore/src/surface/surface/p2.rs b/crates/solidcore/src/surface/surface/p2.rs index 8d57afb77..1b4035a35 100644 --- a/crates/solidcore/src/surface/surface/p2.rs +++ b/crates/solidcore/src/surface/surface/p2.rs @@ -160,9 +160,17 @@ //! `SmSurfaceEval`, `SmDerivSurfDefinition`, `SmDropCurveFail` and //! `SmLSIGlobalSolver` are implicit `Drop`; the classes themselves belong to other //! units. -//! - **Cache slot.** The port builds surface caches afresh instead of keeping one on -//! the surface (`m_pCacheObj`), but whether that cache's tree has been built is -//! observable: `GetDomainWithContinuity`, `LiftCurve`, `GetSurfaceContinuity` and +//! - **Cache slot.** A face surface keeps its trimmed surface cache in its cache slot +//! ([`Surface::take_trim_cache`], [`Surface::store_trim_cache`], [`TrimCacheSlot`]): +//! the face lookups (`query::face::p2::face_surface_cache`) make, validate and reuse +//! it as `SmCacheMgrTSrf::CacheMakeOrValidate` does, and a face-unaware lookup made +//! while the face's `CreateUVTrimCurve(s)` / `IntersectVertices` has cleared the +//! owner ([`Surface::begin_owner_cleared`]) keeps the standalone cache it made +//! ([`Surface::keep_standalone_trim_cache`]). The stored cache's own surface copy +//! gets a slot of its own (no self-reference); a cache taken out shares the slot +//! again until it is stored. Other surface caches are built afresh instead of being +//! kept on the surface (`m_pCacheObj`), but whether that cache's tree has been built +//! is observable: `GetDomainWithContinuity`, `LiftCurve`, `GetSurfaceContinuity` and //! the drop-curve boundary search read the continuity arrays without `GetTree`. //! [`Surface::cache_tree_state`] keeps that bit of history ([`CacheTreeState`]) in a //! slot shared by the surface and its evaluation copies (which stand for upstream's diff --git a/crates/solidcore/src/topology/vertexuse.rs b/crates/solidcore/src/topology/vertexuse.rs index f3bde401f..fec62718d 100644 --- a/crates/solidcore/src/topology/vertexuse.rs +++ b/crates/solidcore/src/topology/vertexuse.rs @@ -65,9 +65,15 @@ //! `GlobalPointSolve` / `IsOnSeam` (`surface.surface_p4`; a plane's overrides are //! ported) and `GetSingularities` (`surface.surface_p3`) are wired. The gap evaluation of //! `SmVertexEdgeGap::Set` / `SmVertexFaceGap::Set` is [`crate::heal::gap`]. -//! `GlobalPointSolve` stands for the surface-cache sequence around it -//! (`smsurf_GetSurfaceCache`, the temporary `m_bHaveTSurfaceCache`, -//! `m_bProcessBoundaryCurves` and `m_bPointTestEnabled` changes). +//! `GlobalPointSolve` runs the surface-cache sequence around it: the face surface's +//! kept cache (`smsurf_GetSurfaceCache`, `query::face::p2::face_surface_cache`) is +//! checked out for the solve, which stands for the temporary `m_bHaveTSurfaceCache` +//! off (the face classifications inside find no cache); its fast drop tests the +//! solutions against the face when the cache is trimmed with point testing on (not +//! switched off by `bOKToMakeUVTrimCurve`). The Rust solver itself builds no surface +//! cache, so `m_bProcessBoundaryCurves` has nothing to act on. A cache already in use +//! by an enclosing caller is not fetched again and no point is tested (upstream +//! reaches it mostly while its tree is built, with point testing off). //! - `GetTurningAngDeg`, `ComputeTPoint`, `GetBrep`, the fresh-gap checks, //! `GetVertexFaceTrimCurveGap`, `RefreshGaps`, `IsOnSeam`, //! `IsSectorGapFreeAndOriented`, `AssertValid`, `Draw*` and the `Dump`s are not From a7a76e434b6a4e8b6d010845050a2d00032ec71e Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Fri, 23 Oct 2026 19:57:00 -0700 Subject: [PATCH 6/6] GlobalLineIntersect: the face's cache source already ran GetTree; findings and unit notes for the kept trimmed caches A face cache built while the B-rep is edited has no face data, and the base cache's GetTree would ask to rebuild it with the face (an Unported dispatch gap): the face source's own GetTree stands. Found with #694 merged (primitive_creation_p2 gen0201, gen0214 then pass). Co-Authored-By: Claude Opus 5.5 --- crates/solidcore/src/surface/surface/p8.rs | 4 ++ plan/findings.md | 50 +++++++++++++++++++ plan/units/heal.face_props.toml | 2 +- .../offset.offset_geometry_creation.toml | 2 +- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/solidcore/src/surface/surface/p8.rs b/crates/solidcore/src/surface/surface/p8.rs index ddd81a689..4cc628493 100644 --- a/crates/solidcore/src/surface/surface/p8.rs +++ b/crates/solidcore/src/surface/surface/p8.rs @@ -1441,8 +1441,12 @@ impl Surface { Err(_) => return Err(SolidError::null_pointer(OP_LINE)), }; face_cache = Some(source); + // the face's source ran the trimmed cache's own GetTree: the base + // cache's cannot rebuild a face-owned tree + let has_tree = owned.tree().is_some(); held = Some(&mut owned); match held.as_deref_mut() { + Some(c) if has_tree => &*c, Some(c) => Self::held_cache(k, c, OP_LINE)?, None => return Err(SolidError::null_pointer(OP_LINE)), } diff --git a/plan/findings.md b/plan/findings.md index 5ab32b172..1615e7cc4 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -18058,3 +18058,53 @@ labels. Read the first as the proposal to give the runner's `StaticHandle` a solver is unchanged. Unlike that sync, this hand-back is unconditional. - **Result.** `blend.fillet_intersector` 1292/1292 pass, known-mismatch entry removed. No tolerance changed and no floor added. + +## Face surfaces keep their trimmed surface caches; IntersectTSBound's cached branch (PR #697, 2026-10-24) + +Follows "classification face queries through `ClassGeometryMut`" (#639) and "Region +nesting once shell ray face preparation meets the forwarded face hooks". + +- **Model.** A surface's cache slot (`Surface::cache_tree_state`'s `Arc`, shared with its + evaluation copies) now also holds the face surface's `SmTrimSrfCache` + (`TrimCacheSlot`: empty, stored, or checked out while in use). + `query::face::p2::face_surface_cache` is `smsurf_GetSurfaceCache` through + `SmCacheMgrTSrf::CacheMakeOrValidate`: a stored cache is reused whether it was built + with the face or standalone, unless `m_bFaceWasModified` and editing is off (then it is + made again); a new one is built with the face, or standalone while the owner is + cleared, and stored. A lookup while the cache is checked out gets an unkept build (the + old behaviour); upstream would find the same object. `PointClassify`'s + `GetObjectCache` reads the stored cache, `SmFace::Notify(PRE_EDIT)` marks it modified. + A face-unaware `smsurf_GetSurfaceCache` made while a face's owner is cleared keeps the + standalone cache too. The 200-entry LRU of upstream's global cache queue is not + modelled. +- **Consumers.** `IntersectTSBound` (cached leaf-node branch, its `SmNewMarkAndLock` and + `GetOrCreateUVTrimCurve`), `GlobalLineIntersect` of a face-owned surface (ray path; + the caller-held cache now only stands for a surface without face), + `ComputeUVPoint`'s global solve (cache checked out around it, fast-drop trim test on + the B-rep when the cache is trimmed with point testing on), `EvaluateBinormal`. +- **Marks.** The context marks and locks are atomics, so a query holding `&Kernel` claims + a mark (`Context::claim_mark`) as upstream's `const` queries do; no `&mut Kernel` + cascade through `CurveOnClassify`'s many callers. Brett to confirm this choice. +- **The history fixes found on the way.** `IntersectVertices` and `IntersectEdges` clear + the surface's owner while they read the vertex/face and edge/face gaps + (SmCurveClass.cpp:14700, 15315, `SmTemporaryChangeValue` on `m_pOwner`), so upstream's + first cache of a heal.face_props face is standalone (C++ probe in the shim, not + committed: after the first `SetProps`, `m_bHaveTSurfaceCache` 0, containment 0, point + test 0, tree built, no UV trim curves). The port's gap reads now run in an + owner-cleared scope. The classification snapshots, `HasSeamProblem`'s seam objects and + `curve_class::p4`'s binormal and sector scratch copies share the B-rep's cache slots + (`Brep::clone_sharing_surface_caches`), else each copy started without cache and built + one with the face (the copy sites of #681 that this needed; the others are unchanged). +- **Harness.** The shared graph import's gap refill would now build face caches with + UV trim curves the C++ B-rep lacks (topology.brep_p4: 61 failures, last edge use field = + "has a UV trim curve"); the refill now runs as during a Boolean operation and drops the + caches it kept. topology.brep_p4 also seeds the C++ read's cache states. + intersect.curve_class_p9's ComputeUVPoint fixture attaches a point-test-off cache + first, as its shim does. +- **Results.** heal.face_props 1431 -> 1439 pass, 97 -> 89 deferred (the 15 + surface.surface_p2 cache-history deferrals go); offset.offset_geometry_creation 832 -> + 835 pass; nothing else changes on main. Merged locally with #694: topology.brep_p6's 7 + region-nesting cases pass (Mark2 included), topology.topology_traverser stays 909/909, + creation_p2 gen0201 and gen0214 pass. `skin_concentric_loops` still differs + (record[7] cpp 1, rust 2: the annular cap comes + out as two faces), so #694 keeps that known mismatch; `skin_primitive_annular` (sweep.api_surfaces) was not re-run after the last change. diff --git a/plan/units/heal.face_props.toml b/plan/units/heal.face_props.toml index 7223b89ce..92b0b3566 100644 --- a/plan/units/heal.face_props.toml +++ b/plan/units/heal.face_props.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_trimming_tools", "SM_API_test:TestSmTrimSurfaceWith3 status = "partial" claimed_by = "" claimed_at = "" -notes = "PR #639 (2026-10-23): the runner builds the down face use side (SmBrepConstructor), which fixes the seam EDGE-vs-VERTEX snap; difftest 1528 cases, 1431 pass, 0 failed, 97 deferred (topology.face_p4 FindVertexSector 82, surface.surface_p2 trimmed surface cache history 15; was FindVertexSector 69 + EdgeCurveOnClassify 28). PARTIAL: difftest 1528 cases (28 fixture + 1500 generated: SetProps/Gaps/Stage2/Stage3 scripts, SmLoopProps::SetProps of every loop (op 8), seeded state, ReSet, HasProps, HasProblems, IsPoint3dOnPole, loop tree build + FindItem, on one-face B-reps with a plane or SmSurfOfRevolution surface (cylinder, cone with/without apex, sphere, torus; open or closed) owned by the face, a vertex loop or edge loops with holes, face tolerances up to 1e9; 9 fixture cases are FFI-guard regression cases, UB-131 and UB-128), 1314 pass bit-identical (max_rel_err 0), 0 failed, 214 deferred (topology.face_p4 210: SmFace::HasSeamProblem for closed faces in SetProps_Stage2; query.face_p4 4: SmFace::ClassifyLoops in SetProps_Stage3); --perturb 1314/1314 detected. Upstream defects UB-131 (SetProps_Gaps early return makes the Stage2/3 face re-run ineffective) and UB-128 (SetProps_Stage3 on a plane with a degenerate domain and edge loops segfaults in ClassifyLoops; the guard rejects a plane side below 1e-6 when Stage 3 can be reached); the guard rejects the crashing scripts. Snapshots compare the flat corner points too; no generated surface has a nonzero m_lBadFlatCorners (harness limitation, findings.md). Stage 3 is compared only through op 8 (direct Stage 3 cases defer at ClassifyLoops). Added Surface::get_singularities_of_face (face tolerance for the owned surface; other UNPORTED(topology.face_p2) tolerance stubs on surfaces unchanged). Wired: topology.loopuse (PR #102), SmSurface::GetSingularities (surface.surface_p3), SmFace::GetVertices (query.face_p2 co-port). Remaining UNPORTED stubs: topology.face_p4 HasSeamProblem, query.face_p4 ClassifyLoops, intersect.curve_class_p2 CurveClassification::ReSet (dead branch). No UNPORTED(heal.face_props) stubs existed on main. FaceProps is generic over a LoopObject handle; the loop tree is an arena (Looptree). CO-PORTED in heal.face_props (PR #122): SmMixedLinkType, SmTriedLinkType, SmHealerOpType (SmHealProbArray.h) -> heal::face_props::{MixedLinkType, TriedLinkType, HealerOpType} (owner heal.heal_data_p2 extends); SmContainmentType (SmTypes.h) -> heal::face_props::ContainmentType. 25 fns; largest: SmFaceProps::SetProps_Stage3, SmFaceProps::SetProps_Stage2, SmLoopProps::SetProps, SmFaceProps::ReSet; unit-SCC #22 (220 units); 100 upstream test entry points reach this unit PR #137 (intersect.curve_class_p4): the shim's face builder, seed and snapshot moved verbatim into harness/cpp/shim/sc_one_face_brep.h, shared with the intersect.curve_class_p4 shim; the runner's FaceIn, rust_brep, ffi_face, snapshot, seed and gen_face are pub(crate) for that runner, the ffi gained encode_face and pub seed_ok. Re-run: 1528 cases, 1314 pass, 214 deferred (query.face_p4 4, topology.face_p4 210), 0 failed, --perturb SENSITIVE 1314/1314. Merge of query.face_p3 (PR #132): query.face_p4 and query.face_p3 wired, the 4 ClassifyLoops deferrals reach further: 1528 cases, 1317 pass, 211 deferred (topology.face_p4 210, topology.face_p2 1), 0 failed. topology.face_p4: FaceProps::set_props_stage2_with takes the SetProps_Stage2 HasSeamProblem call as a callback (face::p4::has_seam_problem passes itself); set_props_stage2 keeps the stub for callers without classification objects." +notes = "PR #697 (2026-10-24, face surfaces keep their trimmed caches): 1528 cases, 1439 pass, 0 failed, 89 deferred (topology.face_p4 88, intersect.curve_class_p4 1); the 15 cache-history (tolerance growth) deferrals go: IntersectVertices' gap reads now run with the surface owner cleared, so the standalone trimmed cache upstream keeps is made there and reused (8 pass, the rest defer further on at FindVertexSector). PR #639 (2026-10-23): the runner builds the down face use side (SmBrepConstructor), which fixes the seam EDGE-vs-VERTEX snap; difftest 1528 cases, 1431 pass, 0 failed, 97 deferred (topology.face_p4 FindVertexSector 82, surface.surface_p2 trimmed surface cache history 15; was FindVertexSector 69 + EdgeCurveOnClassify 28). PARTIAL: difftest 1528 cases (28 fixture + 1500 generated: SetProps/Gaps/Stage2/Stage3 scripts, SmLoopProps::SetProps of every loop (op 8), seeded state, ReSet, HasProps, HasProblems, IsPoint3dOnPole, loop tree build + FindItem, on one-face B-reps with a plane or SmSurfOfRevolution surface (cylinder, cone with/without apex, sphere, torus; open or closed) owned by the face, a vertex loop or edge loops with holes, face tolerances up to 1e9; 9 fixture cases are FFI-guard regression cases, UB-131 and UB-128), 1314 pass bit-identical (max_rel_err 0), 0 failed, 214 deferred (topology.face_p4 210: SmFace::HasSeamProblem for closed faces in SetProps_Stage2; query.face_p4 4: SmFace::ClassifyLoops in SetProps_Stage3); --perturb 1314/1314 detected. Upstream defects UB-131 (SetProps_Gaps early return makes the Stage2/3 face re-run ineffective) and UB-128 (SetProps_Stage3 on a plane with a degenerate domain and edge loops segfaults in ClassifyLoops; the guard rejects a plane side below 1e-6 when Stage 3 can be reached); the guard rejects the crashing scripts. Snapshots compare the flat corner points too; no generated surface has a nonzero m_lBadFlatCorners (harness limitation, findings.md). Stage 3 is compared only through op 8 (direct Stage 3 cases defer at ClassifyLoops). Added Surface::get_singularities_of_face (face tolerance for the owned surface; other UNPORTED(topology.face_p2) tolerance stubs on surfaces unchanged). Wired: topology.loopuse (PR #102), SmSurface::GetSingularities (surface.surface_p3), SmFace::GetVertices (query.face_p2 co-port). Remaining UNPORTED stubs: topology.face_p4 HasSeamProblem, query.face_p4 ClassifyLoops, intersect.curve_class_p2 CurveClassification::ReSet (dead branch). No UNPORTED(heal.face_props) stubs existed on main. FaceProps is generic over a LoopObject handle; the loop tree is an arena (Looptree). CO-PORTED in heal.face_props (PR #122): SmMixedLinkType, SmTriedLinkType, SmHealerOpType (SmHealProbArray.h) -> heal::face_props::{MixedLinkType, TriedLinkType, HealerOpType} (owner heal.heal_data_p2 extends); SmContainmentType (SmTypes.h) -> heal::face_props::ContainmentType. 25 fns; largest: SmFaceProps::SetProps_Stage3, SmFaceProps::SetProps_Stage2, SmLoopProps::SetProps, SmFaceProps::ReSet; unit-SCC #22 (220 units); 100 upstream test entry points reach this unit PR #137 (intersect.curve_class_p4): the shim's face builder, seed and snapshot moved verbatim into harness/cpp/shim/sc_one_face_brep.h, shared with the intersect.curve_class_p4 shim; the runner's FaceIn, rust_brep, ffi_face, snapshot, seed and gen_face are pub(crate) for that runner, the ffi gained encode_face and pub seed_ok. Re-run: 1528 cases, 1314 pass, 214 deferred (query.face_p4 4, topology.face_p4 210), 0 failed, --perturb SENSITIVE 1314/1314. Merge of query.face_p3 (PR #132): query.face_p4 and query.face_p3 wired, the 4 ClassifyLoops deferrals reach further: 1528 cases, 1317 pass, 211 deferred (topology.face_p4 210, topology.face_p2 1), 0 failed. topology.face_p4: FaceProps::set_props_stage2_with takes the SetProps_Stage2 HasSeamProblem call as a callback (face::p4::has_seam_problem passes itself); set_props_stage2 keeps the stub for callers without classification objects." diff --git a/plan/units/offset.offset_geometry_creation.toml b/plan/units/offset.offset_geometry_creation.toml index 2d5174616..fc9322eb2 100644 --- a/plan/units/offset.offset_geometry_creation.toml +++ b/plan/units/offset.offset_geometry_creation.toml @@ -9,4 +9,4 @@ tests = ["SM_API_test:TestSmOffsetBrepFull", "SM_API_test:TestSmShellBrepFull", status = "partial" claimed_by = "" claimed_at = "" -notes = "main CI e33985411 (2026-10-23): PARTIAL offset.offset_geometry_creation cases=898 passed=832 failed=0 deferred=66 blocked_by=surface.trim_srf_cache(1),topology.face_p2(49),upstream:HANG-26(16). Post-#307 (main e3425b27d1f45a7c60e56239567b967f6b7d4d41, CI run 39861450243): native 898 cases, 795 passed with values, 103 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, floors=[]; deferred: topology.face_p2 58 (50 unbound-face SmFace handle of a surface's face + 8 SmFace::GetTolerance), intersect.surface_drop_lib_p3 28 (SmSurface::DropCurve), upstream:HANG-26 16, surface.surface_p2 1 (SmSurfOfExtrusion as SmSurface). #307 resolved 49 of the 62 crv_on_surf deferrals: 62 crv_on_surf -> 49 pass + 11 face_p2 unbound-face + 1 face_p2 GetTolerance + 1 surface_drop_lib_p3. The 11 new unbound-face cases (gen0016 gen0105 gen0173 gen0274 gen0339 gen0341 gen0357 gen0494 gen0577 gen0619 gen0791) are in extend_trim_edges -> drop_and_trim_curve -> surface_drop_curve, not CrvOnSurf; gen0681 is the new GetTolerance case and gen0600 moved to surface_drop_lib_p3. Status remains partial. Historical #549 evidence (branch followup/offset-ssi-context, clean 903ff58289254e32b493e636f397693b510b16fd baseline, merged with 6d3bc74f6): native 898 cases, 746 passed with values, 152 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, floors=[]; clean 903ff baseline 715/183/0. 31 genuine deferred->pass gains (gen0024 gen0037 gen0040 gen0074 gen0096 gen0120 gen0182 gen0213 gen0302 gen0304 gen0313 gen0368 gen0383 gen0386 gen0410 gen0413 gen0454 gen0457 gen0460 gen0475 gen0503 gen0505 gen0560 gen0623 gen0629 gen0682 gen0708 gen0726 gen0755 gen0759 gen0799), zero pass->defer and zero pass->fail transitions. Blocked by curve.crv_on_surf(62), intersect.surface_drop_lib_p3(27), surface.surface_p2(1), topology.face_p2(46), upstream:HANG-26(16); the 46 face_p2 are 39 cache-tree unbound-face deferrals (surface_drop_lib/surface_drop_curve) plus 7 SmFace::GetTolerance, all in other units' paths. Change: SurfaceSolveContext prepares each intersected surface's cache with the caller's surface handle (upstream GetSurfaceCache reaches the owning face); LiftCurve and DomainWithContinuity thread the same context. MateEdges passes one evaluation copy for both surfaces when the extended-surface handles are equal, preserving SurfaceIntersector::doing_self_intersection (pointer identity, one cache) as the C++ does. Status remains partial. Historical notes: PR #533 evidence against immutable authoritative f87f16b47 (not the latest ad2f42ec4 integration, which is being retested): native 898 cases, 715 passed with values, 183 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, floors=[]; all 713 baseline passes retained. Two real gains: nurb_box_trim_and_replace 1040 values; gen0736 981 values. cylinder_seam_vertex_and_edge_caps and gen0218 remain deferred, not gains. Sensitivity 715/715. Shared #537 CacheFace/BrepCacheFace reused; immutable prepared queries/tagged solver dispatch/lazy cache. Remaining blockers: curve.crv_on_surf 29, intersect.surface_drop_lib_p3 16, surface.surface_p2 1, topology.face_p2 121, upstream:HANG-26. Executive p1/p2 reports byte-identical to fresh f87f16b47 main. Status remains partial; no complete offset SSI/shell certification. Historical notes follow unchanged. PR #522, integrated through main b8d3ce6d6: 898 cases, 713 passed with values, 185 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, no absolute floors; sensitivity 713/713. Native baseline on 161925132: 874 cases, 631 passed, 243 deferred, 0 failed. All 631 old passes remain passing; 58 old deferrals and 24 new signed/zero-distance native cases pass. The 80 analytic-import deferrals become 58 passes, 5 topology.face_p2, 1 intersect.surface_drop_lib_p3, and 16 genuine upstream:HANG-26 guards. Remaining blockers: curve.crv_on_surf 29, intersect.surface_drop_lib_p3 16, surface.surface_p2 1, topology.face_p2 123, upstream:HANG-26 16. Exact version-33 input records preserve concrete types, owners and NURBS; real SmCylinder/SmTorus fixtures avoid relying on misleading .smb names. Native import/decoder/guard tests pass; 384 fuzz probes have zero differences, crashes, panics or timeouts. Executive p1/p2 reports remain byte-identical to baseline. No kernel algorithm duplication, native operation replay, tolerance change or complete offset/shell certification is claimed; status remains partial. Historical notes below are retained for provenance; their older validation counts and analytic-import limitations are superseded. PR #288 keeps the full creator API and its existing vertex algorithm; the new narrow VertexBackend delegates to that same implementation. Original vertex/tolerance reads and result edge/face geometry use typed cross-arena handles, with owned cap/curve outputs taken once. The numerical creators are not duplicated. The broader executive arena/SSI backend remains outside this vertex follow-up; #201 coordination retains that boundary. Main B-rep, face-domain and exact revolution import updates are preserved. Current native evidence: 874 cases, 585 passed, 289 deferred, 0 rejected, 0 failed; max_rel_err=0.0, rel_tol=1e-09; no new absolute floors. Historical upstream notes (not current validation): partial (PR #156, stacked on PR #224 and PR #147): 874 cases (5 fixtures, 69 built in code, 800 generated two-B-rep scripts), 565 pass bit for bit, 0 fail, 309 deferred after main e5f3e441 (#149 wired topology.brep_p2's stubs; 441 pass, 433 deferred before, 166 of them topology.brep_p2): harness:analytic-geometry-export 107 (non-plane analytic faces imported as their B-spline part), geom.tol 82 (the trimmed surface cache of a face's own surface, returned by the seeded GetCreateExtendedSurface: SmTol::GetZoneTol3d(const SmObject*), reached before the B-rep editing flag; labelled topology.brep_p1, then surface.surface_cache_p2, before main's relabelling to geom.tol), topology.face_p4 43, curve.crv_on_surf 27 (offset-surface rail: CrvOnSurf over a non-B-spline surface), intersect.surface_drop_curve 17, topology.face_p2 13, blend.fillet_standard_solver_p1 11, intersect.surface_drop_lib_p3 7, surface.sphere 2 (SmSphere::IsNurbSurfaceSphere; surface.cone_p2 before #153), 0 both-errored, with_values=565 no_values=0 (560 compare an upstream result, network_vertex_three_edges among them an error status from the 3-edge branch; 2, gen0264 and gen0685, are guard agreements: the shim refuses the only op as an upstream null dereference and no C++ runs; gen0191 and gen0255, passing since #149, are three-value records too, not probed); no compared case runs CreateExtendedSurface (the seeded lookups always hit); HaveTangentGaps mirrors upstream's unchecked NormalizedEvaluate with the SM_UNDEF_DOUBLE point; 327 passes complete a successful unit op; --perturb SENSITIVE 565/565; --seeds 4000: 4074 cases, 2650 pass, 0 fail, 1424 deferred; --fuzz 2000 FUZZ-PASS at the #156 head and FUZZ-FAIL with 1 timeout at the follow-up head, fuzz01915, a 1e300-control-point draw slow on the Rust side only (the verdict depends on the draw: earlier runs drew the known gen0577 budget flake (also a risk at a zeroed header) and two more such draws, the curve-cache per-call rebuild of curve.curve_p5's Caches deviation, see the curve.curve_p8 findings entry; a negative tolerance is now refused by the runner as by the FFI); green-par 234 units failed=0 (main e5f3e441) (VertexOffset keeps only its per-edge checks: it walks no loops, so wire and network vertices are compared; HaveTangentGaps accepts n = 0, maintainer review of #156; second round: the run header accepts dist == 0 and approx == 0, only a negative tolerance is refused; all 800 generated scripts return and agree at each zero, 67 compared results change at dist == 0 through FaceuseOffset or VertexOffset, 2 at approx == 0 through VertexOffset; 8 *_zero_distance / *_zero_approx cases on explicitly named bases whose compared output changes at zero (one a B-spline face offset), all pass). Wired: surface.offset_surface (PR #62: CreateOffsetSurface dispatch, SetAllowExtension(FALSE), the rail's SmOffsetSurface), offset.offset_executive_p1 (PR #175: GetCreateExtendedSurface through OffsetExecutive::get_create_extended_surface; the runner wraps #175's OffsetExecutive behind this unit's OffsetExecutive trait, which is no longer a co-port) and topology.edgeuse_p3 (PR #117: NormalizedEvaluate, CreateUVTrimCurve). The shim seeds the executive's extended-surface cache before each op (the runner seeds #175's the same way, insert_extended_surface), so neither side runs CreateExtendedSurface (HANG-16, HANG-17). Needs PR #147's owner-keeping surface-cache copy (clone_for_evaluation): without it 5 cases fail (the trimmed cache of a face's surface ran standalone). Hang guards (-6, script stops): HANG-26 (non-local FaceuseOffset on an inexact analytic face, already a harness:analytic-geometry-export deferral) and HANG-27 (EdgeuseOffset with |dist| > 1e6, deferred as upstream:HANG-27; bound chosen for the fuzz time budget, not the termination boundary ~1e250). Crash guards (-2) mirror the shim; local pairs name store surfaces only. Follow-up after this PR merges: offset.offset_executive_p1's own UNPORTED(offset.offset_geometry_creation) EdgeuseOffset/VertexOffset stay stubbed (they need an OffsetBrep over the executive's original/offset Brep ids, and #175's code after EdgeuseOffset, cpp:2956-3071, is not ported and reaches topology.brep_p7/p8). CO-PORTED here: SsiData (SmSSIData). TOUCHED elsewhere: nurbs.b_spline_surface_p2 create_sphere_from_arcs (WR-103 reproduced, arcs as Curve); offset.offset_executive_p1 insert_extended_surface. 15 fns; largest: SmOffsetGeometryCreation::EdgeuseOffset, SmOffsetGeometryCreation::TrimExtendedEdge, SmOffsetGeometryCreation::ExtendTrimEdges, SmOffsetGeometryCreation::MateEdges; cycle back-edges (call into later units; stub or co-port): offset.offset_executive_p1(9); unit-SCC #26 (2 units); 11 upstream test entry points reach this unit Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 861 cases, 542 pass, 0 fail, 319 deferred (harness:analytic-geometry-export 126, surface.surface_cache_p2 82, topology.face_p4 43, curve.crv_on_surf 27, blend.fillet_standard_solver_p1 11, topology.face_p2 13, intersect.surface_drop_curve 8, intersect.surface_drop_lib_p3 7, surface.sphere 2); of the 166 topology.brep_p2 deferrals (SmBrep::Notify 161, MateLaminaEdgeToSurface 5; both wired, the B-rep edits through OffsetBrep) 120 pass and the rest relabel. LiftCurve now returns the iso curve with its own kind (an SmLine / SmCircle; surface.surface_p11), which ExtendTrimEdges stores as the offset edge's curve. On a face the runner imports as its B-spline part the Rust runs the B-spline CreateIsoParametricCurve instead of the subclass's, so ExtendTrimEdges (and EdgeuseOffset's ExtendTrimEdges path) defer as harness:analytic-geometry-export when the lift's UV curve (the edge use's or its radial's) is a degree-1 iso line and the offset edge use's face is not exact; 6 cases that passed on main fall in that class (their subclass override happened to return a B-spline) and now defer (gen0009, gen0119, gen0148, gen0376, gen0520, gen0707). Exact SmSurfOfRevolution faces (#149 review round, after da779c51): the shim's Export appends each exactly-SmSurfOfRevolution face surface's binary WriteToDB record and the runner reads it back with SurfOfRevolution::read_from_db (face marked exact); put_curve exports every SmBSplineCurve kind (type + NURB); the non-local FaceuseOffset HANG-26 stop is keyed on the face's subclass type as the shim's. 861 cases, 552 pass, 0 fail, 309 deferred (harness:analytic-geometry-export 107, surface.surface_cache_p2 82, topology.face_p4 43, curve.crv_on_surf 27, intersect.surface_drop_curve 17, topology.face_p2 13, blend.fillet_standard_solver_p1 11, intersect.surface_drop_lib_p3 7, surface.sphere 2): 10 harness deferrals pass (the revolution lifts gen0009, gen0148, gen0520, gen0683 and gen0070, gen0178, gen0187, gen0597, gen0698, gen0767), 9 relabel to intersect.surface_drop_curve; gen0119 / gen0707 (SmSurfOfExtrusion) and gen0376 (SmSphere) stay deferred (no Rust surface kind for them; tracked in issue #76, Kernel-aware Surface dispatch for analytic surface kinds). topology.face_p4 (PR #239) wires face_p3's GetUpwardLoopuses, so UpdateFaceSurfaceDomain runs past CalculateUVDomainFromUVTrimCurves: on a face imported as its B-spline part it now defers under harness:analytic-geometry-export (its virtual surface calls and the compared face surface are the B-spline kind's; 13 such cases mismatched the analytic SmSurfOfRevolution). After main's exact SmSurfOfRevolution import (#315) only inexact faces defer there: 874 cases, 585 pass, 0 failed, 289 deferred (main without this PR: 565/309). Owner zone-tol cache (surface/owner-zone-tol): 874 cases, 589 pass, 0 fail, 285 deferred. Face-cache/solve follow-up (branch followup/offset-face-tolerance, merged 422f71321, CI run 39879315744 baseline 818/80/0): native 898 cases, 820 passed with values, 0 failed, 78 deferred; max_rel_err=0, rel_tol=1e-9, floors=[]; +2 genuine deferred->pass (cylinder_seam_vertex_and_edge_caps, gen0218); no pass->defer, pass->fail or relocation; blocked_by=topology.face_p2(58),upstream:HANG-26(16),upstream:cache-history(4). The excluded face-bound ExtendTrimEdges cache experiment (819 pass / 2 fail on base 89c94bf) and its iso guard (818 pass / 1 fail) are in plan/findings.md. 3 CreateUVTrimCurves cases remain #463 scope; this PR does not change them. Lazy surface cache trees (#568, PR #571, on #463 4243bec79): native 898 cases, 822 passed with values, 76 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, floors=[]; deferred: topology.face_p2 59, upstream:HANG-26 16, surface.trim_srf_cache 1. The Export appends each face surface's cache state after the C++ build and the runner seeds the imported surfaces (Surface::set_cache_tree_state); the upstream:cache-history deferral and OP_LIFT_OFF_DOMAIN are removed and its 4 cases pass (gen0266 compares a success, gen0129 gen0152 gen0258 SM_ERR on both sides). gen0308 now defers at surface.trim_srf_cache (the tracer constructor's GetTree on a face-owned surface) instead of topology.face_p2. Status remains partial." +notes = "PR #697 (2026-10-24, face surfaces keep their trimmed caches): 898 cases, 835 passed, 0 failed, 63 deferred (3 deferred -> pass). main CI e33985411 (2026-10-23): PARTIAL offset.offset_geometry_creation cases=898 passed=832 failed=0 deferred=66 blocked_by=surface.trim_srf_cache(1),topology.face_p2(49),upstream:HANG-26(16). Post-#307 (main e3425b27d1f45a7c60e56239567b967f6b7d4d41, CI run 39861450243): native 898 cases, 795 passed with values, 103 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, floors=[]; deferred: topology.face_p2 58 (50 unbound-face SmFace handle of a surface's face + 8 SmFace::GetTolerance), intersect.surface_drop_lib_p3 28 (SmSurface::DropCurve), upstream:HANG-26 16, surface.surface_p2 1 (SmSurfOfExtrusion as SmSurface). #307 resolved 49 of the 62 crv_on_surf deferrals: 62 crv_on_surf -> 49 pass + 11 face_p2 unbound-face + 1 face_p2 GetTolerance + 1 surface_drop_lib_p3. The 11 new unbound-face cases (gen0016 gen0105 gen0173 gen0274 gen0339 gen0341 gen0357 gen0494 gen0577 gen0619 gen0791) are in extend_trim_edges -> drop_and_trim_curve -> surface_drop_curve, not CrvOnSurf; gen0681 is the new GetTolerance case and gen0600 moved to surface_drop_lib_p3. Status remains partial. Historical #549 evidence (branch followup/offset-ssi-context, clean 903ff58289254e32b493e636f397693b510b16fd baseline, merged with 6d3bc74f6): native 898 cases, 746 passed with values, 152 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, floors=[]; clean 903ff baseline 715/183/0. 31 genuine deferred->pass gains (gen0024 gen0037 gen0040 gen0074 gen0096 gen0120 gen0182 gen0213 gen0302 gen0304 gen0313 gen0368 gen0383 gen0386 gen0410 gen0413 gen0454 gen0457 gen0460 gen0475 gen0503 gen0505 gen0560 gen0623 gen0629 gen0682 gen0708 gen0726 gen0755 gen0759 gen0799), zero pass->defer and zero pass->fail transitions. Blocked by curve.crv_on_surf(62), intersect.surface_drop_lib_p3(27), surface.surface_p2(1), topology.face_p2(46), upstream:HANG-26(16); the 46 face_p2 are 39 cache-tree unbound-face deferrals (surface_drop_lib/surface_drop_curve) plus 7 SmFace::GetTolerance, all in other units' paths. Change: SurfaceSolveContext prepares each intersected surface's cache with the caller's surface handle (upstream GetSurfaceCache reaches the owning face); LiftCurve and DomainWithContinuity thread the same context. MateEdges passes one evaluation copy for both surfaces when the extended-surface handles are equal, preserving SurfaceIntersector::doing_self_intersection (pointer identity, one cache) as the C++ does. Status remains partial. Historical notes: PR #533 evidence against immutable authoritative f87f16b47 (not the latest ad2f42ec4 integration, which is being retested): native 898 cases, 715 passed with values, 183 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, floors=[]; all 713 baseline passes retained. Two real gains: nurb_box_trim_and_replace 1040 values; gen0736 981 values. cylinder_seam_vertex_and_edge_caps and gen0218 remain deferred, not gains. Sensitivity 715/715. Shared #537 CacheFace/BrepCacheFace reused; immutable prepared queries/tagged solver dispatch/lazy cache. Remaining blockers: curve.crv_on_surf 29, intersect.surface_drop_lib_p3 16, surface.surface_p2 1, topology.face_p2 121, upstream:HANG-26. Executive p1/p2 reports byte-identical to fresh f87f16b47 main. Status remains partial; no complete offset SSI/shell certification. Historical notes follow unchanged. PR #522, integrated through main b8d3ce6d6: 898 cases, 713 passed with values, 185 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, no absolute floors; sensitivity 713/713. Native baseline on 161925132: 874 cases, 631 passed, 243 deferred, 0 failed. All 631 old passes remain passing; 58 old deferrals and 24 new signed/zero-distance native cases pass. The 80 analytic-import deferrals become 58 passes, 5 topology.face_p2, 1 intersect.surface_drop_lib_p3, and 16 genuine upstream:HANG-26 guards. Remaining blockers: curve.crv_on_surf 29, intersect.surface_drop_lib_p3 16, surface.surface_p2 1, topology.face_p2 123, upstream:HANG-26 16. Exact version-33 input records preserve concrete types, owners and NURBS; real SmCylinder/SmTorus fixtures avoid relying on misleading .smb names. Native import/decoder/guard tests pass; 384 fuzz probes have zero differences, crashes, panics or timeouts. Executive p1/p2 reports remain byte-identical to baseline. No kernel algorithm duplication, native operation replay, tolerance change or complete offset/shell certification is claimed; status remains partial. Historical notes below are retained for provenance; their older validation counts and analytic-import limitations are superseded. PR #288 keeps the full creator API and its existing vertex algorithm; the new narrow VertexBackend delegates to that same implementation. Original vertex/tolerance reads and result edge/face geometry use typed cross-arena handles, with owned cap/curve outputs taken once. The numerical creators are not duplicated. The broader executive arena/SSI backend remains outside this vertex follow-up; #201 coordination retains that boundary. Main B-rep, face-domain and exact revolution import updates are preserved. Current native evidence: 874 cases, 585 passed, 289 deferred, 0 rejected, 0 failed; max_rel_err=0.0, rel_tol=1e-09; no new absolute floors. Historical upstream notes (not current validation): partial (PR #156, stacked on PR #224 and PR #147): 874 cases (5 fixtures, 69 built in code, 800 generated two-B-rep scripts), 565 pass bit for bit, 0 fail, 309 deferred after main e5f3e441 (#149 wired topology.brep_p2's stubs; 441 pass, 433 deferred before, 166 of them topology.brep_p2): harness:analytic-geometry-export 107 (non-plane analytic faces imported as their B-spline part), geom.tol 82 (the trimmed surface cache of a face's own surface, returned by the seeded GetCreateExtendedSurface: SmTol::GetZoneTol3d(const SmObject*), reached before the B-rep editing flag; labelled topology.brep_p1, then surface.surface_cache_p2, before main's relabelling to geom.tol), topology.face_p4 43, curve.crv_on_surf 27 (offset-surface rail: CrvOnSurf over a non-B-spline surface), intersect.surface_drop_curve 17, topology.face_p2 13, blend.fillet_standard_solver_p1 11, intersect.surface_drop_lib_p3 7, surface.sphere 2 (SmSphere::IsNurbSurfaceSphere; surface.cone_p2 before #153), 0 both-errored, with_values=565 no_values=0 (560 compare an upstream result, network_vertex_three_edges among them an error status from the 3-edge branch; 2, gen0264 and gen0685, are guard agreements: the shim refuses the only op as an upstream null dereference and no C++ runs; gen0191 and gen0255, passing since #149, are three-value records too, not probed); no compared case runs CreateExtendedSurface (the seeded lookups always hit); HaveTangentGaps mirrors upstream's unchecked NormalizedEvaluate with the SM_UNDEF_DOUBLE point; 327 passes complete a successful unit op; --perturb SENSITIVE 565/565; --seeds 4000: 4074 cases, 2650 pass, 0 fail, 1424 deferred; --fuzz 2000 FUZZ-PASS at the #156 head and FUZZ-FAIL with 1 timeout at the follow-up head, fuzz01915, a 1e300-control-point draw slow on the Rust side only (the verdict depends on the draw: earlier runs drew the known gen0577 budget flake (also a risk at a zeroed header) and two more such draws, the curve-cache per-call rebuild of curve.curve_p5's Caches deviation, see the curve.curve_p8 findings entry; a negative tolerance is now refused by the runner as by the FFI); green-par 234 units failed=0 (main e5f3e441) (VertexOffset keeps only its per-edge checks: it walks no loops, so wire and network vertices are compared; HaveTangentGaps accepts n = 0, maintainer review of #156; second round: the run header accepts dist == 0 and approx == 0, only a negative tolerance is refused; all 800 generated scripts return and agree at each zero, 67 compared results change at dist == 0 through FaceuseOffset or VertexOffset, 2 at approx == 0 through VertexOffset; 8 *_zero_distance / *_zero_approx cases on explicitly named bases whose compared output changes at zero (one a B-spline face offset), all pass). Wired: surface.offset_surface (PR #62: CreateOffsetSurface dispatch, SetAllowExtension(FALSE), the rail's SmOffsetSurface), offset.offset_executive_p1 (PR #175: GetCreateExtendedSurface through OffsetExecutive::get_create_extended_surface; the runner wraps #175's OffsetExecutive behind this unit's OffsetExecutive trait, which is no longer a co-port) and topology.edgeuse_p3 (PR #117: NormalizedEvaluate, CreateUVTrimCurve). The shim seeds the executive's extended-surface cache before each op (the runner seeds #175's the same way, insert_extended_surface), so neither side runs CreateExtendedSurface (HANG-16, HANG-17). Needs PR #147's owner-keeping surface-cache copy (clone_for_evaluation): without it 5 cases fail (the trimmed cache of a face's surface ran standalone). Hang guards (-6, script stops): HANG-26 (non-local FaceuseOffset on an inexact analytic face, already a harness:analytic-geometry-export deferral) and HANG-27 (EdgeuseOffset with |dist| > 1e6, deferred as upstream:HANG-27; bound chosen for the fuzz time budget, not the termination boundary ~1e250). Crash guards (-2) mirror the shim; local pairs name store surfaces only. Follow-up after this PR merges: offset.offset_executive_p1's own UNPORTED(offset.offset_geometry_creation) EdgeuseOffset/VertexOffset stay stubbed (they need an OffsetBrep over the executive's original/offset Brep ids, and #175's code after EdgeuseOffset, cpp:2956-3071, is not ported and reaches topology.brep_p7/p8). CO-PORTED here: SsiData (SmSSIData). TOUCHED elsewhere: nurbs.b_spline_surface_p2 create_sphere_from_arcs (WR-103 reproduced, arcs as Curve); offset.offset_executive_p1 insert_extended_surface. 15 fns; largest: SmOffsetGeometryCreation::EdgeuseOffset, SmOffsetGeometryCreation::TrimExtendedEdge, SmOffsetGeometryCreation::ExtendTrimEdges, SmOffsetGeometryCreation::MateEdges; cycle back-edges (call into later units; stub or co-port): offset.offset_executive_p1(9); unit-SCC #26 (2 units); 11 upstream test entry points reach this unit Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 861 cases, 542 pass, 0 fail, 319 deferred (harness:analytic-geometry-export 126, surface.surface_cache_p2 82, topology.face_p4 43, curve.crv_on_surf 27, blend.fillet_standard_solver_p1 11, topology.face_p2 13, intersect.surface_drop_curve 8, intersect.surface_drop_lib_p3 7, surface.sphere 2); of the 166 topology.brep_p2 deferrals (SmBrep::Notify 161, MateLaminaEdgeToSurface 5; both wired, the B-rep edits through OffsetBrep) 120 pass and the rest relabel. LiftCurve now returns the iso curve with its own kind (an SmLine / SmCircle; surface.surface_p11), which ExtendTrimEdges stores as the offset edge's curve. On a face the runner imports as its B-spline part the Rust runs the B-spline CreateIsoParametricCurve instead of the subclass's, so ExtendTrimEdges (and EdgeuseOffset's ExtendTrimEdges path) defer as harness:analytic-geometry-export when the lift's UV curve (the edge use's or its radial's) is a degree-1 iso line and the offset edge use's face is not exact; 6 cases that passed on main fall in that class (their subclass override happened to return a B-spline) and now defer (gen0009, gen0119, gen0148, gen0376, gen0520, gen0707). Exact SmSurfOfRevolution faces (#149 review round, after da779c51): the shim's Export appends each exactly-SmSurfOfRevolution face surface's binary WriteToDB record and the runner reads it back with SurfOfRevolution::read_from_db (face marked exact); put_curve exports every SmBSplineCurve kind (type + NURB); the non-local FaceuseOffset HANG-26 stop is keyed on the face's subclass type as the shim's. 861 cases, 552 pass, 0 fail, 309 deferred (harness:analytic-geometry-export 107, surface.surface_cache_p2 82, topology.face_p4 43, curve.crv_on_surf 27, intersect.surface_drop_curve 17, topology.face_p2 13, blend.fillet_standard_solver_p1 11, intersect.surface_drop_lib_p3 7, surface.sphere 2): 10 harness deferrals pass (the revolution lifts gen0009, gen0148, gen0520, gen0683 and gen0070, gen0178, gen0187, gen0597, gen0698, gen0767), 9 relabel to intersect.surface_drop_curve; gen0119 / gen0707 (SmSurfOfExtrusion) and gen0376 (SmSphere) stay deferred (no Rust surface kind for them; tracked in issue #76, Kernel-aware Surface dispatch for analytic surface kinds). topology.face_p4 (PR #239) wires face_p3's GetUpwardLoopuses, so UpdateFaceSurfaceDomain runs past CalculateUVDomainFromUVTrimCurves: on a face imported as its B-spline part it now defers under harness:analytic-geometry-export (its virtual surface calls and the compared face surface are the B-spline kind's; 13 such cases mismatched the analytic SmSurfOfRevolution). After main's exact SmSurfOfRevolution import (#315) only inexact faces defer there: 874 cases, 585 pass, 0 failed, 289 deferred (main without this PR: 565/309). Owner zone-tol cache (surface/owner-zone-tol): 874 cases, 589 pass, 0 fail, 285 deferred. Face-cache/solve follow-up (branch followup/offset-face-tolerance, merged 422f71321, CI run 39879315744 baseline 818/80/0): native 898 cases, 820 passed with values, 0 failed, 78 deferred; max_rel_err=0, rel_tol=1e-9, floors=[]; +2 genuine deferred->pass (cylinder_seam_vertex_and_edge_caps, gen0218); no pass->defer, pass->fail or relocation; blocked_by=topology.face_p2(58),upstream:HANG-26(16),upstream:cache-history(4). The excluded face-bound ExtendTrimEdges cache experiment (819 pass / 2 fail on base 89c94bf) and its iso guard (818 pass / 1 fail) are in plan/findings.md. 3 CreateUVTrimCurves cases remain #463 scope; this PR does not change them. Lazy surface cache trees (#568, PR #571, on #463 4243bec79): native 898 cases, 822 passed with values, 76 deferred, 0 failed; max_rel_err=0, rel_tol=1e-9, floors=[]; deferred: topology.face_p2 59, upstream:HANG-26 16, surface.trim_srf_cache 1. The Export appends each face surface's cache state after the C++ build and the runner seeds the imported surfaces (Surface::set_cache_tree_state); the upstream:cache-history deferral and OP_LIFT_OFF_DOMAIN are removed and its 4 cases pass (gen0266 compares a success, gen0129 gen0152 gen0258 SM_ERR on both sides). gen0308 now defers at surface.trim_srf_cache (the tracer constructor's GetTree on a face-owned surface) instead of topology.face_p2. Status remains partial."