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