diff --git a/crates/difftest/src/units/boolean_topology_intersector_p1.rs b/crates/difftest/src/units/boolean_topology_intersector_p1.rs index d61c5ad433..aa9ddeacde 100644 --- a/crates/difftest/src/units/boolean_topology_intersector_p1.rs +++ b/crates/difftest/src/units/boolean_topology_intersector_p1.rs @@ -1131,6 +1131,14 @@ impl CurveOnClassifyObjects for Objs<'_> { } impl ProtoBrep for Two { + fn merge_point_class_into_brep( + &mut self, + k: &mut Kernel, + pc: &mut PointClassification, + src_point: Point3, + ) -> solidcore::Result> { + solidcore::intersect::curve_class::p6::merge_into_object_mut(k, self, pc, src_point) + } fn brep_squeeze_edge( &mut self, k: &mut Kernel, diff --git a/crates/difftest/src/units/intersect_curve_class_p3.rs b/crates/difftest/src/units/intersect_curve_class_p3.rs index 2ae2c1025c..e65d28ba2e 100644 --- a/crates/difftest/src/units/intersect_curve_class_p3.rs +++ b/crates/difftest/src/units/intersect_curve_class_p3.rs @@ -30,10 +30,13 @@ //! state (see unit `intersect.curve_class_p1`, [`BareTopology`]), so their zone tolerances are //! compared too. //! +//! `InsertPointClass` runs through `insert_point_class_mut` on the pool's B-reps (the +//! `Combine` of a boundary merge moves a vertex to a surf/surf/surf point, +//! `SmVertex::SetPoint`), and both sides then report every vertex object's point. +//! //! Deferred cases: the zone tolerance of an edge, vertex or face classification on a bare //! stand-in next to a real B-rep (`topology.edge`, `topology.vertex`, `topology.face_p2`), -//! `MergeIntoObject` (`SmVertex::SetPoint`) and the 2D tolerance mapping that needs a surface -//! point solve. Generated scripts are filtered by the shim's run-time checks (scripts it +//! `MergeIntoObject` and the 2D tolerance mapping that needs a surface point solve. Generated scripts are filtered by the shim's run-time checks (scripts it //! rejects as undefined upstream are dropped). use serde::{Deserialize, Serialize}; @@ -63,7 +66,14 @@ use solidcore::surface::plane::Plane; use solidcore::surface::surface::Surface; use solidcore::surface::volume::Volume; use solidcore::{EntityRef, SolidError}; -use solidcore::topology::Brep; +use solidcore::geom::attribute::{AttributeStore, ObjectKey}; +use solidcore::geom::object::ObjectRef; +use solidcore::intersect::curve_class::p5::ClassGeometryMut; +use solidcore::intersect::curve_class::p6::{MergeContext, MergeIntervalBrep}; +use solidcore::topology::edge::NotifyEnv; +use solidcore::topology::topology::Topology; +use solidcore::topology::{Brep, EdgeId}; +use std::collections::BTreeMap; use solidcore_ffi::units::heal_face_props::FaceSpec; use solidcore_ffi::units::intersect_curve_class_p3::{self as ffi, kind, op}; @@ -585,6 +595,79 @@ struct Pool { /// Without `topo`: the vertex, edge and face stand-ins as B-rep records /// ([`BareTopology`]). bare: BareTopology, + /// The notification records and attributes of the B-rep edits + /// ([`MergeIntervalBrep`]: `Combine`'s `SmVertex::SetPoint`). + records: BTreeMap, + attributes: AttributeStore, +} + +/// The identity of an edge's curve (none of this unit's edits makes one). +fn pool_curve_ref(e: EdgeId, kind: i64) -> ObjectRef { + ObjectRef { + key: ObjectKey(((kind as u64) << 32) | u64::from(e.index())), + type_code: kind, + } +} + +impl ClassGeometryMut for Pool { + fn curve_mut(&mut self, o: H) -> Option<&mut Curve> { + match self.objs.get_mut(o.0) { + Some(PoolObj::Curve(c)) => Some(c), + _ => None, + } + } + fn topology_entity_mut(&mut self, o: H) -> Option<(&mut Brep, EntityRef)> { + if let Some(t) = self.topo.as_mut() { + let e = *t.entities.get(o.0.checked_sub(t.first)?)?; + Some((&mut t.brep, e)) + } else { + let e = self.bare.ent(o.0)?; + Some((&mut self.bare.brep, e)) + } + } +} + +/// The B-rep of the pool's topology handles, for the vertex move of `Combine` in +/// `InsertPointClass` ([`CurveClassification::insert_point_class_mut`]). +impl MergeIntervalBrep for Pool { + fn merge_context(&mut self, o: H) -> Option> { + let handles = self.handles.clone(); + let (brep, target, handle): (&mut Brep, EntityRef, Box H>) = + if let Some(t) = self.topo.as_mut() { + let target = *t.entities.get(o.0.checked_sub(t.first)?)?; + let (first, entities) = (t.first, t.entities.clone()); + let handle = move |e: EntityRef| { + entities + .iter() + .position(|x| *x == e) + .and_then(|i| handles.get(first + i).copied()) + .unwrap_or(H(usize::MAX, ClassObjectKind::Other)) + }; + (&mut t.brep, target, Box::new(handle)) + } else { + let target = self.bare.ent(o.0)?; + let ents = self.bare.ents.clone(); + let handle = move |e: EntityRef| { + ents.iter() + .position(|t| *t == Some(e)) + .and_then(|i| handles.get(i).copied()) + .unwrap_or(H(usize::MAX, ClassObjectKind::Other)) + }; + (&mut self.bare.brep, target, Box::new(handle)) + }; + Some(MergeContext { + brep, + target, + handle, + notifications: NotifyEnv { + records: &mut self.records, + attributes: &mut self.attributes, + context: None, + key_of: &super::boolean_topology_intersector_p1::p9_entity_ref, + }, + curve_ref: &pool_curve_ref, + }) + } } impl ClassGeometry for Pool { @@ -696,6 +779,8 @@ fn build_pool(objects: &[ObjIn]) -> Result { handles: Vec::new(), topo: None, bare: BareTopology::default(), + records: BTreeMap::new(), + attributes: AttributeStore::default(), }; for (i, o) in objects.iter().enumerate() { // the curve of an edge object, the surface of a face object (earlier pool objects; @@ -864,7 +949,7 @@ fn build_pc(k: &Kernel, pool: &Pool, p: &PcIn) -> Result, #[allow(clippy::too_many_lines)] fn rust_run(s: &Script) -> Result, Fail> { - let k = Kernel::default(); + let mut k = Kernel::default(); let mut pool = build_pool(&s.objects)?; if let Some(face) = &s.face { add_face(&k, &mut pool, face)?; @@ -1050,11 +1135,26 @@ fn rust_run(s: &Script) -> Result, Fail> { } OpIn::Insert { pc, prm, last, force, .. } => { let pc = build_pc(&k, &pool, pc)?; - let r = cc.insert_point_class(&k, &pool, &pc, *prm, *last, *force); + // a merge's surf/surf/surf point moves a vertex + // (`Combine`'s SmVertex::SetPoint), as in the shim + let r = cc + .insert_point_class_mut(&mut k, &mut pool, &pc, *prm, *last, *force); match st(r)? { Ok(made) => o.extend([0.0, b(made)]), Err(s) => o.push(s), } + // every vertex object's point: a merged surf/surf/surf point + // moves one (`Combine`'s SmVertex::SetPoint) + for h in &pool.handles { + if h.1 == ClassObjectKind::Vertex { + // a stand-in next to the real B-rep is a bare record + let p = pool + .classification_vertex_point(*h) + .or_else(|| pool.bare.vertex_point(h.0)) + .ok_or_else(|| harness("vertex object without a point"))?; + o.extend([p.x, p.y, p.z]); + } + } } OpIn::RemovePc { j, pos, .. } => { let r = cc.remove_point_class(*j, position(*pos)?); diff --git a/crates/difftest/src/units/intersect_curve_class_p6.rs b/crates/difftest/src/units/intersect_curve_class_p6.rs index 3af1c6a0eb..8309732854 100644 --- a/crates/difftest/src/units/intersect_curve_class_p6.rs +++ b/crates/difftest/src/units/intersect_curve_class_p6.rs @@ -12,6 +12,9 @@ //! Each case works on a fresh copy of that box (the shim builds one per case), and //! `MergeInterval` edits it through `MergeIntervalBrep`; a merge also reports the box //! after it (counts, sorted vertex and edge tolerances, the new vertices and edges). +//! `InsertCurveInterval` runs through `insert_curve_interval_mut` (its `Combine` may move +//! the picked vertex to a surf/surf/surf point, `SmVertex::SetPoint`) and reports that +//! vertex's point after it. //! The `imprint_*` cases pick other box objects and place lines on the box, with its //! editing enabled, so the vertex, face-split, coincident-edge and wire-edge branches //! run to completion. @@ -597,9 +600,13 @@ impl UnitDiff for Runner { let mut b = classification(&k, &i.b, i.tol, &g.first)?; a.set_fillet_data((i.flags & 2 != 0).then_some(Handle::Other), i.flags & 2 != 0); let result = match i.op { - 0 => a - .insert_curve_interval(&k, &g, &ivl(&i.input, i.tol, &g.first)?) - .map(|()| (None, None)), + // a surf/surf/surf point combined into a vertex boundary moves the box's + // vertex (SmVertex::SetPoint), as the shim's InsertCurveInterval does + 0 => { + let input = ivl(&i.input, i.tol, &g.first)?; + a.insert_curve_interval_mut(&mut k, &mut g, &input) + .map(|()| (None, None)) + } 1 => a.homogenize(&k, &g, &mut b).map(|()| (None, None)), 2 => a .fix_end_point_class(&k, &g, i.index, Vec3::new(x, y, z)) @@ -629,6 +636,15 @@ impl UnitDiff for Runner { if let Some(merged) = &merged { put_brep(&mut out, &g, merged).map_err(|e| h(e.to_string()))?; } + // the picked vertex after InsertCurveInterval (a combined + // surf/surf/surf point moves it through SmVertex::SetPoint) + if i.op == 0 { + let Some(EntityRef::Vertex(v)) = g.first.get(2).copied() else { + return Err(h("picked vertex is not a vertex")); + }; + let p = topo_vertex::point(&g.brep, v).map_err(|e| h(e.to_string()))?; + out.extend([p.x, p.y, p.z]); + } Ok(Output::new().values("out", out)) } } diff --git a/crates/difftest/src/units/topology_proto_topology_p1.rs b/crates/difftest/src/units/topology_proto_topology_p1.rs index 183a741fa3..6593385834 100644 --- a/crates/difftest/src/units/topology_proto_topology_p1.rs +++ b/crates/difftest/src/units/topology_proto_topology_p1.rs @@ -1644,6 +1644,53 @@ impl ProtoTopology for Pool { /// The bare stand-ins' members (upstream `SmEdge::GetInterval`, `GetCurve`, `GetTolerance` /// of the stand-in objects the shim makes), and new curves appended to the pool. impl ProtoBrep for Pool { + /// A vertex stand-in merges in its record (upstream's default-constructed + /// `SmVertex`: the `SetPoint` / `SetExact` / `SetTolerance` of `MergeIntoObject` + /// need no B-rep); an edge, face or region stand-in is joined to no B-rep, so its + /// edit keeps the default deferral. + fn merge_point_class_into_brep( + &mut self, + k: &mut Kernel, + pc: &mut PointClassification, + src_point: Point3, + ) -> solidcore::Result> { + let target = pc + .object() + .filter(|o| o.1 == ClassObjectKind::Vertex) + .and_then(|o| self.topo_entity(o)); + let Some(target @ EntityRef::Vertex(_)) = target else { + return pc.merge_into_object(k, src_point); + }; + let ents = self.topo.ents.clone(); + let kinds = self.kinds.clone(); + let handle = move |e: EntityRef| { + let i = ents.iter().position(|t| *t == Some(e)).unwrap_or(usize::MAX); + H(i, class_kind_of(kinds.get(i).copied().unwrap_or(-1))) + }; + let mut records = BTreeMap::new(); + let mut attributes = AttributeStore::default(); + let mut env = solidcore::topology::edge::NotifyEnv { + records: &mut records, + attributes: &mut attributes, + context: None, + key_of: &super::boolean_topology_intersector_p1::p9_entity_ref, + }; + let curve_ref = |e: solidcore::topology::EdgeId, kind: i64| ObjectRef { + key: ObjectKey(((kind as u64) << 32) | u64::from(e.index())), + type_code: kind, + }; + pc.merge_into_brep( + k, + src_point, + solidcore::intersect::point_class::BrepMerge { + brep: &mut self.topo.brep, + target, + handle: &handle, + notifications: &mut env, + curve_ref: &curve_ref, + }, + ) + } fn brep_squeeze_edge( &mut self, _k: &mut Kernel, diff --git a/crates/difftest/src/units/topology_proto_topology_p2.rs b/crates/difftest/src/units/topology_proto_topology_p2.rs index fd648544b1..3f1c65b589 100644 --- a/crates/difftest/src/units/topology_proto_topology_p2.rs +++ b/crates/difftest/src/units/topology_proto_topology_p2.rs @@ -230,7 +230,7 @@ pub(super) fn run_op( *skip = true; return Ok(()); }; - let _ = res(r.mgr.imprint_vertex_definition(&r.k, &mut r.pool, d), st, skip)?; + let _ = res(r.mgr.imprint_vertex_definition(&mut r.k, &mut r.pool, d), st, skip)?; if let Some(v) = r.mgr.vertex_def(d) { out.push(r.pool.id(v.final_vertex(1))); out.push(r.pool.id(v.final_vertex(2))); diff --git a/crates/solidcore/src/boolean/merge/p1.rs b/crates/solidcore/src/boolean/merge/p1.rs index 7c0bd33a9c..01ca2445ba 100644 --- a/crates/solidcore/src/boolean/merge/p1.rs +++ b/crates/solidcore/src/boolean/merge/p1.rs @@ -32,6 +32,12 @@ //! bounding boxes before any mutation; no translation is faked. Destruction is //! Rust ownership; the temporary-transform token is restored explicitly after the //! caller's complete scope, including error exits. There is no input-dependent recursion. +//! `BooleanPostProcess`'s `DeleteTopologicalVertex` runs without the caller's +//! notification identities, so the survivor edge's joined curve comes back owned under +//! a kernel-issued identity of that edge; the owner identity the curve of each +//! surviving edge had before the call is put back (upstream the owner is the same +//! `SmEdge*`), so the caller's identity-keyed walks (the temporary transform's inverse +//! `Transform`) still find the curve. use std::collections::BTreeMap; @@ -545,6 +551,18 @@ pub fn boolean_post_process( } if vertex::is_topological_vertex(k, brep, v)? { let editing = brep.editing_enabled(); + // The owners of the two edges' curves: the survivor's joined curve is owned + // by the same edge (`SetCurve`), under the caller's identity of that edge. + let mut owners = Vec::new(); + for e in vertex::edges(brep, v, None)? { + owners.push(( + e, + brep.edge(e)? + .curve + .as_ref() + .and_then(crate::curve::curve::p2::Curve::owner), + )); + } ignore_failure(crate::topology::brep::p5::delete_topological_vertex( k, brep, @@ -552,6 +570,16 @@ pub fn boolean_post_process( Some(v), None, ))?; + for (e, owner) in owners { + if !brep.edge_list().contains(&e) { + continue; + } + if let Some(curve) = brep.edge_mut(e)?.curve.as_mut() { + if curve.owner() != owner { + curve.set_owner(owner, None); + } + } + } } } // Combine a wire shell with a faceuse shell when the B-rep has just those two diff --git a/crates/solidcore/src/boolean/merge/p4/arena_pair.rs b/crates/solidcore/src/boolean/merge/p4/arena_pair.rs index a40aa6e982..6bc9197366 100644 --- a/crates/solidcore/src/boolean/merge/p4/arena_pair.rs +++ b/crates/solidcore/src/boolean/merge/p4/arena_pair.rs @@ -29,7 +29,7 @@ use crate::intersect::curve_class::p1::CurveClassification; use crate::intersect::curve_class::p5::ClassGeometryMut; use crate::intersect::curve_class::p6::{MergeContext, MergeIntervalBrep}; use crate::intersect::point_class::{ - ClassGeometry, ClassObject, ClassObjectKind, PointClassType, PointClassification, + ClassGeometry, ClassObject, ClassObjectKind, MergeOutputs, PointClassType, PointClassification, }; use crate::intersect::topology_solver::p1::TopologyObjects; use crate::kernel::Kernel; @@ -495,6 +495,16 @@ impl CurveOnClassifyObjects for Objs<'_, '_> { } impl ProtoBrep for ArenaPair<'_> { + /// `MergeIntoObject` on the operand the classification's object names, through + /// [`MergeIntervalBrep::merge_context`]. + fn merge_point_class_into_brep( + &mut self, + k: &mut Kernel, + pc: &mut PointClassification, + src_point: Point3, + ) -> crate::Result> { + crate::intersect::curve_class::p6::merge_into_object_mut(k, self, pc, src_point) + } fn edge_interval(&self, edge: H) -> Option { ed::interval(self.brep(edge)?, eid(edge)).ok() } diff --git a/crates/solidcore/src/intersect/curve_class/p3.rs b/crates/solidcore/src/intersect/curve_class/p3.rs index 9b10639549..e333772b34 100644 --- a/crates/solidcore/src/intersect/curve_class/p3.rs +++ b/crates/solidcore/src/intersect/curve_class/p3.rs @@ -11,7 +11,7 @@ //! | [`CurveClassification::find_interval`], [`FoundInterval`] | `SmCurveClassification::FindInterval` | //! | [`CurveClassification::find_point_class`] | `SmCurveClassification::FindPointClass` | //! | [`CurveClassification::is_coincident`] | `SmCurveClassification::IsCoincident` | -//! | [`CurveClassification::insert_point_class`] | `SmCurveClassification::InsertPointClass` | +//! | [`CurveClassification::insert_point_class`], [`CurveClassification::insert_point_class_mut`] | `SmCurveClassification::InsertPointClass` | //! | [`CurveClassification::remove_point_class`] | `SmCurveClassification::RemovePointClass` | //! | [`CurveClassification::merge_point_class`] | `SmCurveClassification::MergePointClass` | //! | [`CurveClassification::cleanup_and_validate`] | `SmCurveClassification::CleanupAndValidate` | @@ -26,6 +26,10 @@ //! through [`ClassGeometry::topology_entity`] (and [`ClassGeometry::topology_handle`] //! for the vertices of an edge); an adapter without those hooks keeps the //! [`SolidError::Unported`] deferral, as in `intersect::point_class`. +//! - **Vertex moves.** The `Combine` of a boundary merge moves the vertex of a vertex +//! classification to a propagated surf/surf/surf point (`SmVertex::SetPoint`): +//! [`CurveClassification::insert_point_class_mut`] makes it through a +//! [`MergeIntervalBrep`]; [`CurveClassification::insert_point_class`] defers there. //! - **Out-params.** `FindInterval` returns a [`FoundInterval`]; `InsertPointClass` returns //! `rbInsertionMade`; `sm_ChangeEdgePointClassToNearVertex` returns //! `rbChangedToVertexFlag`; `MergePointClass`'s optional output arrays are @@ -797,7 +801,9 @@ impl CurveClassification { /// /// Those of [`CurveClassification::find_interval`] and /// [`CurveClassification::insert_at`]; [`SolidError::Unported`] for topology - /// objects. + /// objects, and for a surf/surf/surf point a merge propagates to a vertex + /// classification (`SmVertex::SetPoint`; [`Self::insert_point_class_mut`] makes + /// the move). pub fn insert_point_class( &mut self, k: &Kernel, @@ -807,6 +813,53 @@ impl CurveClassification { last_curve_param: f64, force_insertion: bool, ) -> Result { + self.insert_point_class_in( + &mut ReadOnly { k, g: geometry }, + point_class, + curve_param, + last_curve_param, + force_insertion, + ) + } + + /// [`CurveClassification::insert_point_class`] moving the vertex of a boundary + /// classification a merged surf/surf/surf point propagates to + /// (`SmVertex::SetPoint` of `Combine`) in the B-rep a [`MergeIntervalBrep`] hands + /// out. + /// + /// # Errors + /// + /// Those of [`CurveClassification::insert_point_class`], without the deferral of + /// the move; [`SolidError::InvalidInput`] for a vertex the adapter cannot resolve + /// mutably. + pub fn insert_point_class_mut( + &mut self, + k: &mut Kernel, + geometry: &mut impl MergeIntervalBrep, + point_class: &PointClassification, + curve_param: f64, + last_curve_param: f64, + force_insertion: bool, + ) -> Result { + self.insert_point_class_in( + &mut Mutable { k, g: geometry }, + point_class, + curve_param, + last_curve_param, + force_insertion, + ) + } + + /// `InsertPointClass` over either kind of adapter. + pub(super) fn insert_point_class_in( + &mut self, + a: &mut impl Imprint, + point_class: &PointClassification, + curve_param: f64, + last_curve_param: f64, + force_insertion: bool, + ) -> Result { + let (k, geometry) = (a.kernel(), a.geometry()); // init output let mut insertion_made = false; let mut curve_param = curve_param; @@ -1194,7 +1247,7 @@ impl CurveClassification { // when merging a start boundary point classification else if position == IntervalPosition::Start { let found_ivl = self.at_mut(index)?; - if found_ivl.start.combine(point_class)? == Precedence::Other { + if a.combine(&mut found_ivl.start, point_class)? == Precedence::Other { found_ivl.start = *point_class; } insertion_made = true; @@ -1202,7 +1255,7 @@ impl CurveClassification { // remember to merge the neighbor interval's boundary point if index > 0 { let found2 = self.at_mut(index - 1)?; - if found2.end.combine(point_class)? == Precedence::Other { + if a.combine(&mut found2.end, point_class)? == Precedence::Other { found2.end = *point_class; } } @@ -1210,7 +1263,7 @@ impl CurveClassification { // when merging an end boundary point classification else if position == IntervalPosition::End { let found_ivl = self.at_mut(index)?; - if found_ivl.end.combine(point_class)? == Precedence::Other { + if a.combine(&mut found_ivl.end, point_class)? == Precedence::Other { found_ivl.end = *point_class; } insertion_made = true; @@ -1218,7 +1271,7 @@ impl CurveClassification { // remember to merge the neighbor interval's boundary point if index + 1 < self.size() { let found2 = self.at_mut(index + 1)?; - if found2.start.combine(point_class)? == Precedence::Other { + if a.combine(&mut found2.start, point_class)? == Precedence::Other { found2.start = *point_class; } } diff --git a/crates/solidcore/src/intersect/curve_class/p6.rs b/crates/solidcore/src/intersect/curve_class/p6.rs index 787ef73ecd..33b81d3df6 100644 --- a/crates/solidcore/src/intersect/curve_class/p6.rs +++ b/crates/solidcore/src/intersect/curve_class/p6.rs @@ -5,7 +5,7 @@ //! | Rust | Upstream | //! |------|----------| //! | [`CurveClassification::homogenize`] | `SmCurveClassification::Homogenize` | -//! | [`CurveClassification::insert_curve_interval`] | `SmCurveClassification::InsertCurveInterval` | +//! | [`CurveClassification::insert_curve_interval`], [`CurveClassification::insert_curve_interval_mut`] | `SmCurveClassification::InsertCurveInterval` | //! | [`CurveClassification::fix_end_point_class`] | `SmCurveClassification::FixEndPointClass` | //! | [`CurveClassification::merge_interval`], [`CurveClassification::merge_interval_mut`] | `SmCurveClassification::MergeInterval` | //! | [`MergeIntervalBrep`], [`MergeContext`] | the non-const `SmVertex`, `SmEdge`, `SmFace`, `SmRegion` and `SmBrep` objects behind the classification's pointers | @@ -36,6 +36,14 @@ //! reference build (no `SM_TOLERANT_WATCH`). The reference build is `SM_USE_OLDTOL`, so //! the coincident-edge branch raises the edge's tolerance to the source tolerance and //! refreshes the edge's first vertex, as upstream's `SM_OLDTOL_LINE` code. +//! +//! **`Combine` moves a vertex.** A surf/surf/surf point `Combine` propagates to a vertex +//! classification moves that vertex (`SmVertex::SetPoint`, which notifies +//! `ChangeGeometry` and sets the point; no tolerance or cache side effect). +//! [`CurveClassification::insert_curve_interval_mut`] (and part 3's +//! `insert_point_class_mut`) make the move on the arena a [`MergeIntervalBrep`] hands +//! out for the vertex, at the moment upstream makes it; the read-only entry points defer +//! there (`Unported(topology.vertex)`). use super::p1::{compute_face_point_class, compute_face_point_class_in, src_zone_tol_3d_of}; use super::p3::{ @@ -93,11 +101,14 @@ impl Default for MergeIntervalOutput { const OP: &str = "intersect::curve_class::p6"; -fn combine( +/// `target = target.Combine(other)` through adapter `a` (which makes or defers the +/// vertex move of the propagated surf/surf/surf point). +fn combine_in( + a: &mut impl Imprint, target: &mut PointClassification, other: &PointClassification, ) -> Result<()> { - if target.combine(other)? == Precedence::Other { + if a.combine(target, other)? == Precedence::Other { *target = *other; } Ok(()) @@ -141,15 +152,43 @@ impl CurveClassification { /// # Errors /// Propagates tolerance, geometry and unported p2/p7 failures. Invalid indices and /// a restart without progress return an error instead of recursing indefinitely. + /// A surf/surf/surf point propagated to a vertex classification defers + /// (`SmVertex::SetPoint`; [`Self::insert_curve_interval_mut`] makes the move). pub fn insert_curve_interval( &mut self, k: &Kernel, geometry: &impl ClassGeometry, input: &CurveInterval, + ) -> Result<()> { + self.insert_curve_interval_in(&mut ReadOnly { k, g: geometry }, input) + } + + /// [`CurveClassification::insert_curve_interval`] moving the vertex of a vertex + /// classification a combined surf/surf/surf point propagates to + /// (`SmVertex::SetPoint`) in the B-rep a [`MergeIntervalBrep`] hands out. + /// + /// # Errors + /// Those of [`CurveClassification::insert_curve_interval`], without the deferral of + /// the move; [`SolidError::InvalidInput`] for a vertex the adapter cannot resolve + /// mutably. + pub fn insert_curve_interval_mut( + &mut self, + k: &mut Kernel, + geometry: &mut impl MergeIntervalBrep, + input: &CurveInterval, + ) -> Result<()> { + self.insert_curve_interval_in(&mut Mutable { k, g: geometry }, input) + } + + /// `InsertCurveInterval` over either kind of adapter. + fn insert_curve_interval_in( + &mut self, + a: &mut impl Imprint, + input: &CurveInterval, ) -> Result<()> { let mut restarts = self.size().saturating_add(1); loop { - if !self.insert_curve_interval_step(k, geometry, input)? { + if !self.insert_curve_interval_step(a, input)? { return Ok(()); } restarts = restarts @@ -161,14 +200,13 @@ impl CurveClassification { // true means upstream's tail-recursive restart after removal of an interval. fn insert_curve_interval_step( &mut self, - k: &Kernel, - geometry: &impl ClassGeometry, + a: &mut impl Imprint, input: &CurveInterval, ) -> Result { - let tol = input.mid.obj_zone_tol_3d(k, geometry)?; - let a = self.find_interval(k, geometry, input.min(), BIG_DOUBLE, tol)?; - let b = self.find_interval(k, geometry, input.max(), BIG_DOUBLE, tol)?; - let (mut i, mut j, mut pa, mut pb) = (a.index, b.index, a.position, b.position); + let tol = input.mid.obj_zone_tol_3d(a.kernel(), a.geometry())?; + let fa = self.find_interval(a.kernel(), a.geometry(), input.min(), BIG_DOUBLE, tol)?; + let fb = self.find_interval(a.kernel(), a.geometry(), input.max(), BIG_DOUBLE, tol)?; + let (mut i, mut j, mut pa, mut pb) = (fa.index, fb.index, fa.position, fb.position); if pb == IntervalPosition::Start && j > i { pb = IntervalPosition::End; j -= 1; @@ -179,7 +217,8 @@ impl CurveClassification { } if i != j { for index in [i, j] { - let (coin, end) = are_coincident_maybe(k, geometry, self, self, index)?; + let (coin, end) = + are_coincident_maybe(a.kernel(), a.geometry(), self, self, index)?; match coin { Coincident::Unsure => return Err(SolidError::sm_err(OP)), Coincident::Yes => { @@ -198,18 +237,18 @@ impl CurveClassification { { for index in i..=j { let ivl = self.at_mut(index)?; - combine(&mut ivl.start, &input.start)?; - combine(&mut ivl.mid, &input.mid)?; - combine(&mut ivl.end, &input.end)?; + combine_in(a, &mut ivl.start, &input.start)?; + combine_in(a, &mut ivl.mid, &input.mid)?; + combine_in(a, &mut ivl.end, &input.end)?; } return Ok(false); } while i < j { let first = *self.at(i)?; let last = *self.at(j)?; - if interval_degenerate(k, geometry, self, &first)? { + if interval_degenerate(a.kernel(), a.geometry(), self, &first)? { i += 1; - } else if interval_degenerate(k, geometry, self, &last)? { + } else if interval_degenerate(a.kernel(), a.geometry(), self, &last)? { j -= 1; } else { break; @@ -255,21 +294,14 @@ impl CurveClassification { } else { &mut ivl.end }; - combine(end, &input.start)?; - combine(end, &input.end)?; + combine_in(a, end, &input.start)?; + combine_in(a, end, &input.end)?; return Ok(false); } if pa == IntervalPosition::Start { - combine(&mut self.at_mut(i)?.start, &input.start)?; + combine_in(a, &mut self.at_mut(i)?.start, &input.start)?; } else if pa == IntervalPosition::Inside { - if !self.insert_point_class( - k, - geometry, - &input.start, - input.min(), - BIG_DOUBLE, - false, - )? { + if !self.insert_point_class_in(a, &input.start, input.min(), BIG_DOUBLE, false)? { return Ok(false); } i += 1; @@ -277,17 +309,18 @@ impl CurveClassification { return Err(SolidError::sm_err(OP)); } if pb == IntervalPosition::End { - combine(&mut self.at_mut(i)?.end, &input.end)?; + combine_in(a, &mut self.at_mut(i)?.end, &input.end)?; } else if pb == IntervalPosition::Inside { - let found = self.find_interval(k, geometry, input.max(), BIG_DOUBLE, tol)?; + let found = + self.find_interval(a.kernel(), a.geometry(), input.max(), BIG_DOUBLE, tol)?; if found.position != IntervalPosition::Inside { return Ok(false); } - self.insert_point_class(k, geometry, &input.end, input.max(), BIG_DOUBLE, false)?; + self.insert_point_class_in(a, &input.end, input.max(), BIG_DOUBLE, false)?; } else { return Err(SolidError::sm_err(OP)); } - combine(&mut self.at_mut(i)?.mid, &input.mid)?; + combine_in(a, &mut self.at_mut(i)?.mid, &input.mid)?; Ok(false) } @@ -1205,6 +1238,13 @@ pub(super) trait Imprint { fn set_vertex_exact(&mut self, vertex: O, exact: bool) -> Result<()>; /// `pVertex->RefreshTolerance(FALSE)`. fn refresh_vertex_tolerance(&mut self, vertex: O) -> Result<()>; + /// `target.Combine(other)`, whose propagated surf/surf/surf point moves the + /// vertex of a vertex classification (`SmVertex::SetPoint`). + fn combine( + &mut self, + target: &mut PointClassification, + other: &PointClassification, + ) -> Result; } /// A read-only adapter: every B-rep edit defers. @@ -1324,6 +1364,13 @@ impl> Imprint for ReadOnly<'_, G> { "SmBrep mutable owning context (SmVertex::RefreshTolerance)", )) } + fn combine( + &mut self, + target: &mut PointClassification, + other: &PointClassification, + ) -> Result { + target.combine(other) + } } /// A mutable adapter: the B-rep edits are made. @@ -1593,6 +1640,33 @@ impl> Imprint for Mutable<'_, G> { }; topo_vertex::refresh_tolerance(ctx.brep, v, false) } + fn combine( + &mut self, + target: &mut PointClassification, + other: &PointClassification, + ) -> Result { + let g = &mut *self.g; + target.combine_setting_vertex_point(other, &mut |object, point| { + // `(SmVertex *)m_pObject`: no vertex is undefined behaviour upstream + let object = object.ok_or_else(|| null_object("classification has no vertex"))?; + let ctx = g.merge_context(object).ok_or_else(unresolved_mut)?; + let EntityRef::Vertex(v) = ctx.target else { + return Err(wrong_target("handle does not name a vertex")); + }; + let env = ctx.notifications; + let key_of = env.key_of; + topo_vertex::set_point( + ctx.brep, + v, + point, + env.records, + env.attributes, + env.context, + key_of(EntityRef::Vertex(v)), + &|vu| key_of(EntityRef::Vertexuse(vu)), + ) + }) + } } // ---- the objects MergeInterval casts its classification objects to ------------------------ diff --git a/crates/solidcore/src/intersect/point_class.rs b/crates/solidcore/src/intersect/point_class.rs index 25587c33e4..2633870c8f 100644 --- a/crates/solidcore/src/intersect/point_class.rs +++ b/crates/solidcore/src/intersect/point_class.rs @@ -57,7 +57,7 @@ //! | [`object_zone_tol_3d`] | `SmTol::GetZoneTol3d(const SmObject*)` | //! | [`PointClassification::xsect_tol_3d`] | `SmTol::GetXSectTol3d(const SmPointClassification*)` | //! | [`PointClassification::set_src_zone_tol_3d`] | `SetSrcZoneTol3d` | -//! | [`PointClassification::combine`], [`PointClassification::combine_in_brep`], [`Precedence`] | `Combine` | +//! | [`PointClassification::combine`], [`PointClassification::combine_in_brep`], [`PointClassification::combine_setting_vertex_point`], [`Precedence`] | `Combine` | //! | [`CombineBrep`] | the arena behind the classified object | //! | [`PointClassification::compute_point_parameters`], [`PointClassification::compute_point_parameters_mut`]| `ComputePointParameters` | //! | [`PointClassification::find_obj_point_3d`] | `FindObjPoint3d` | @@ -72,7 +72,9 @@ //! - **Mutable topology.** [`BrepMerge`] supplies an arena, resolved target, //! handle mapping and notification environment for [`PointClassification::merge_into_brep`], //! and [`CombineBrep`] the arena and notifications -//! [`PointClassification::combine_in_brep`] writes a propagated vertex point to. +//! [`PointClassification::combine_in_brep`] writes a propagated vertex point to; +//! [`PointClassification::combine_setting_vertex_point`] takes the caller's +//! `SmVertex::SetPoint` instead (an adapter resolving the vertex on demand). //! The older opaque-handle entrypoints cannot resolve their object's arena and return //! an `Unported(topology.brep_p1)` / `Unported(topology.vertex)` deferral until the //! caller supplies that context. Region/face owner queries retain their own blockers. @@ -1045,14 +1047,66 @@ impl PointClassification { other: &Self, context: CombineBrep<'_, '_, O>, ) -> Result { - self.combine_with(other, Some(context)) + let CombineBrep { + brep, + target, + handle, + notifications, + } = context; + self.combine_with( + other, + Some(&mut |object: Option, point: Point3| { + if object.is_some_and(|object| handle(target) != object) { + return Err(SolidError::invalid_input( + OP_COMBINE, + "target does not resolve classification object", + )); + } + let EntityRef::Vertex(v) = target else { + return Err(SolidError::invalid_input( + OP_COMBINE, + "combine target is not a vertex", + )); + }; + let key_of = notifications.key_of; + let this = key_of(EntityRef::Vertex(v)); + vertex::set_point( + brep, + v, + point, + notifications.records, + notifications.attributes, + notifications.context.as_deref_mut(), + this, + &|vu| key_of(EntityRef::Vertexuse(vu)), + ) + }), + ) + } + + /// `Combine(other)` with the caller's `SmVertex::SetPoint`: a surf/surf/surf point + /// propagated to a vertex classification is written by + /// `set_vertex_point(object, point)`, with this classification's object (the + /// vertex upstream casts `m_pObject` to) and the point, at the moment upstream + /// calls `pVertex->SetPoint(m_vSSSPoint)`. + /// + /// # Errors + /// + /// Those of [`Self::combine`], without the deferral; those of `set_vertex_point`. + pub fn combine_setting_vertex_point( + &mut self, + other: &Self, + set_vertex_point: &mut dyn FnMut(Option, Point3) -> Result<()>, + ) -> Result { + self.combine_with(other, Some(set_vertex_point)) } - /// [`Self::combine`] with an optional arena for the propagated vertex point. + /// [`Self::combine`] with an optional `SmVertex::SetPoint` for the propagated vertex + /// point. fn combine_with( &mut self, other: &Self, - arena: Option>, + set_vertex_point: Option<&mut dyn FnMut(Option, Point3) -> Result<()>>, ) -> Result { use PointClassType as Pc; let this_class = self.point_class; @@ -1100,40 +1154,14 @@ impl PointClassification { // set vertex position when appropriate if self.point_class == Pc::Vertex { - let Some(arena) = arena else { + let Some(set_vertex_point) = set_vertex_point else { // UNPORTED(topology.vertex): SmVertex::SetPoint return Err(SolidError::unported( "SmVertex::SetPoint", "topology.vertex", )); }; - if self - .object - .is_some_and(|object| (arena.handle)(arena.target) != object) - { - return Err(SolidError::invalid_input( - OP_COMBINE, - "target does not resolve classification object", - )); - } - let EntityRef::Vertex(v) = arena.target else { - return Err(SolidError::invalid_input( - OP_COMBINE, - "combine target is not a vertex", - )); - }; - let key_of = arena.notifications.key_of; - let this = key_of(EntityRef::Vertex(v)); - vertex::set_point( - arena.brep, - v, - self.sss_point, - arena.notifications.records, - arena.notifications.attributes, - arena.notifications.context.as_deref_mut(), - this, - &|vu| key_of(EntityRef::Vertexuse(vu)), - )?; + set_vertex_point(self.object, self.sss_point)?; } } diff --git a/crates/solidcore/src/topology/proto_topology/p2.rs b/crates/solidcore/src/topology/proto_topology/p2.rs index 3fe9721f02..14af414b52 100644 --- a/crates/solidcore/src/topology/proto_topology/p2.rs +++ b/crates/solidcore/src/topology/proto_topology/p2.rs @@ -7,7 +7,7 @@ //! //! | Rust | Upstream | //! |------|----------| -//! | [`ProtoBrep`] | the B-rep members these functions read through `SmEdge*` / `SmFace*` / `SmVertex*` (`GetInterval`, `GetCurve`, `GetTolerance`, `GetUVDomain`, `SetUVDomain`, `GetSurface`, `GetEdgeuseOfFace`, `IsConnectedToFace`) and `SmCurve::Copy`'s allocation | +//! | [`ProtoBrep`] | the B-rep members these functions read through `SmEdge*` / `SmFace*` / `SmVertex*` (`GetInterval`, `GetCurve`, `GetTolerance`, `GetUVDomain`, `SetUVDomain`, `GetSurface`, `GetEdgeuseOfFace`, `IsConnectedToFace`), `SmCurve::Copy`'s allocation, and the B-rep edits of `SmPointClassification::MergeIntoObject` ([`ProtoBrep::merge_point_class_into_brep`]) | //! | [`ProtoTopologyManager::imprint_vertex_definition`] | `SmVertexDefinition::Imprint` | //! | [`ProtoTopologyManager::imprint_edge_definition`] | `SmEdgeDefinition::Imprint` | //! | [`ProtoTopologyManager::resolve_proto_vertex`] | `SmProtoVertex::Resolve` | @@ -90,7 +90,8 @@ use crate::geom::solution_array::{ObjectKind, SolutionObject}; use crate::geom::tol::{xsect_tol3d, ScaledZero}; use crate::intersect::curve_class::{CurveClassification, CurveInterval}; use crate::intersect::point_class::{ - object_zone_tol_3d, ClassObject, ClassObjectKind, PointClassType, PointClassification, + object_zone_tol_3d, ClassObject, ClassObjectKind, MergeOutputs, PointClassType, + PointClassification, }; use crate::kernel::Kernel; use crate::math::{max2, max3, max4, Point2, Point3, Vec3, EFF_ZERO}; @@ -218,6 +219,29 @@ pub trait ProtoBrep: ProtoGeometry + ProtoTopology { tolerance: f64, boundary_intersections: bool, ) -> Result>; + /// `rPC.MergeIntoObject(srcPoint, &newEdges, &newVertices)` of + /// `SmVertexDefinition::Imprint`: the classification's point merged into the B-rep + /// behind its object, which upstream edits through the object's pointer (a new + /// shell vertex, vertex loop or edge split, or the existing vertex moved by + /// `SmVertex::SetPoint`). The owner of mutable B-reps implements it with + /// [`merge_into_object_mut`](crate::intersect::curve_class::p6::merge_into_object_mut); + /// the default has no mutable owning context and returns + /// [`PointClassification::merge_into_object`]'s result. + /// + /// # Errors + /// + /// Those of the merge. + fn merge_point_class_into_brep( + &mut self, + k: &mut Kernel, + pc: &mut PointClassification, + src_point: Point3, + ) -> Result> + where + O: ClassObject, + { + pc.merge_into_object(k, src_point) + } } /// The handle the curve point solves name (only the solution count and values are @@ -522,13 +546,13 @@ impl ProtoTopologyManager { /// /// # Errors /// - /// Those of [`PointClassification::merge_into_object`]; `SM_ERR` when a merge + /// Those of [`ProtoBrep::merge_point_class_into_brep`]; `SM_ERR` when a merge /// made no single vertex and the classification is not on a vertex; those of /// [`ProtoTopologyManager::update_edges`]; [`SolidError::InvalidInput`] for a /// definition without a proto vertex (upstream reaches the manager through it). pub fn imprint_vertex_definition( &mut self, - k: &Kernel, + k: &mut Kernel, brep: &mut impl ProtoBrep, vd: VertexDefId, ) -> Result<()> { @@ -544,9 +568,8 @@ impl ProtoTopologyManager { let position = v.position(); let v = self.vd_mut(vd)?; - let outputs = v - .brep_point_classif_mut(which) - .merge_into_object(k, position)?; + let outputs = + brep.merge_point_class_into_brep(k, v.brep_point_classif_mut(which), position)?; if outputs.new_vertices.len() == 1 { v.set_final_vertex(which, outputs.new_vertices.first().copied()); diff --git a/harness/cpp/shim/intersect_curve_class_p3.cpp b/harness/cpp/shim/intersect_curve_class_p3.cpp index 3ccaccbb75..2b95803cee 100644 --- a/harness/cpp/shim/intersect_curve_class_p3.cpp +++ b/harness/cpp/shim/intersect_curve_class_p3.cpp @@ -1459,6 +1459,14 @@ int32_t run(Rd& rd, std::vector& o) o.push_back(st(s)); if (s == SM_SUCCESS) o.push_back(flg(made)); + // every vertex object's point: a merged surf/surf/surf point moves one + // (SmPointClassification::Combine's SmVertex::SetPoint) + for (size_t i = 0; i < pool.kinds.size(); ++i) + if (pool.kinds[i] == K_VERTEX) + { + SmPoint3d p = ((SmVertex*)pool.ptrs[i])->GetPoint(); + o.insert(o.end(), {p.x, p.y, p.z}); + } break; } case OP_REMOVE_PC: diff --git a/harness/cpp/shim/intersect_curve_class_p6.cpp b/harness/cpp/shim/intersect_curve_class_p6.cpp index ae38ce8d02..3ce3415559 100644 --- a/harness/cpp/shim/intersect_curve_class_p6.cpp +++ b/harness/cpp/shim/intersect_curve_class_p6.cpp @@ -329,6 +329,13 @@ static int32_t run_impl( put_cc(o, cc, objs); put_cc(o, other, objs); if (op == 3) put_brep(o, box, new_vertices, new_edges); + // the picked vertex after InsertCurveInterval (a combined surf/surf/surf + // point moves it through SmVertex::SetPoint) + if (op == 0) + { + SmPoint3d p = objs.vertices[(ULONG)pick[2]]->GetPoint(); + o.insert(o.end(), {p.x, p.y, p.z}); + } } return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, len); SC_CATCH diff --git a/plan/findings.md b/plan/findings.md index a6afa8c9da..4fb7c3de84 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -18970,6 +18970,22 @@ round's complete input and 4125.906 mm net; the other rounds' tiny lower v exten bounds can differ in their last bits (at most 7e-20 in the checked run). The saved JSON inputs fix every bit for differential testing. +## boolean.merge_p4 / boolean.merge_p1: post-process re-owns the joined curve (2026-10-24) + +Found when SmVertexDefinition::Imprint started merging in the arena pair +(ProtoBrep::merge_point_class_into_brep): the far-translation fixtures (wire at +(1e8,-1e8,1e8), isolated vertex at its middle, operations 1, 3, 4) split the wire, then +BooleanPostProcess joins the two edges again (DeleteTopologicalVertex). The C++ result +curve is back at 1e8; the Rust one stayed at the origin-shifted position (dbls[6..20]: +cpp 1e8.., rust -0.5..0.5). Cause: boolean_post_process calls +delete_topological_vertex without the caller's notification identities, so the joined +curve of the surviving edge is owned under a kernel-issued key; the inverse +SmTemporaryBrepTransform (topology::brep::p5::transform with the adapter's keys) walks +curve owners by identity and skipped it. Fix in boolean.merge_p1: the surviving edge's +curve gets back the owner identity it had before the call (upstream the owner is the +same SmEdge*). All three now pass. A cleaner fix is for the manifold adapter to pass its +notification environment to boolean_post_process +(delete_topological_vertex_with_notifications); that needs boolean/merge/p4/kernel_adapter.rs. ## offset.offset_executive_p2: inset shell of a box loses the inner shell in the Rust Difference Unit `offset.offset_executive_p2`, cases `gen0000` and `gen0003` (static `ShellBrep` diff --git a/plan/units/boolean.merge_p1.toml b/plan/units/boolean.merge_p1.toml index 0a899a8411..328594bfce 100644 --- a/plan/units/boolean.merge_p1.toml +++ b/plan/units/boolean.merge_p1.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin status = "green" claimed_by = "" claimed_at = "" -notes = "GREEN (wire/merge-coincident-surfaces, 2026-10-24): FixFoundFaceCoincidence runs SmTopologyIntersector::MergeCoincidentSurfaces (boolean.topology_intersector_p3) through the manifold adapter: p1::check_face_coincidence (the all-edges-mated return) and p1::finish_face_coincidence (rbModifiedTopology from the B-reps' edge counts) wrap the merge, which ManifoldKernelAdapter::fix_face_coincidence runs on an ArenaPair (new IntersectorBrepP3, MergeIntervalBrep and ClassGeometryMut impls, loop-use and face-use handle kinds) with the retained mate store. difftest 248 cases, 248 passed, 0 deferred (main: 244 passed, 4 deferred on MergeCoincidentSurfaces); 6000 seeds: 6152/6152. Op 2 now drives the adapter (SmMerge(context) tolerances SM_EFF_ZERO_SQ and 30 degrees) and compares both operands' graphs after the call, the returned faces and both directions of the edge mates (the shim appends them; before, the other B-rep's edits and the mates were not compared). Callers of the changed path (boolean.merge_p4, boolean.api_brep, ...): no unit's numbers changed in the all-units run. Generator: op 2 also takes same-plane rectangles that overlap, are contained, enclose, touch, share edges with or miss the face; with #718 and #721 on main all pass (the touching and disjoint ones compare the B-reps, the others end in an error status both sides agree on), see plan/findings.md. Earlier history: main CI e33985411 (2026-10-23): PARTIAL boolean.merge_p1 cases=248 passed=241 failed=0 deferred=7 blocked_by=boolean.topology_intersector_p3(4),topology.brep_p5(2),topology.face_p2(1). CO-PORTED in boolean.api_brep (PR #81): SmMergeOptions (fields, ctor, defaults, copy ctor, getters/setters) -> solidcore::boolean::merge::p1::MergeOptions (crates/solidcore/src/boolean/merge/p1.rs; extend it, keep boolean/merge.rs as the split root); 24 fns; largest: SmMerge::BooleanPostProcess, SmMerge::FixFoundFaceCoincidence, sm_FindBrepRegionOfOtherFace, SmMerge::PropagateOtherSrcRegionAttribs; cycle back-edges (call into later units; stub or co-port): blend.fillet_executive_p1(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit; PR #163: merge control, face coincidence fast path, mapped vertex copy with topology/attribute notification, cleanup and region-attribute propagation; cross-B-rep radial classification keeps operand ids separate. Difftest 248 cases (152 fixtures + 96 generated), 175 passed, 73 deferred, 0 failed, max_rel_err=0, no absolute floors. Deferred: query.brep(14), topology.brep_p1(33), topology.brep_p9(3), topology.edgeuse_p2(20), topology.face_p2(3). DeleteEdge/DeleteVertex/DeleteTopologicalVertex remain tagged back-edges; scoped translation waits for query.brep bounding boxes; intersector construction is supplied by its owner. Wired to topology.shell (PR #154): SmRegion::IsVoid in sm_EdgeBordersSolid, SmShell::IsWireShell/IsFaceuseShell in BooleanPostProcess, region propagation takes a RegionLookup for the source regions' IsVoid. The wire/face shell combine block of BooleanPostProcess is wired end to end (GetCommonVertices = the B-rep vertices with an other mate in the shared mates, as TopologyIntersector::common_vertices after PR #146; then ignored-status shell::transfer_children and shell::region_remove_shell; boolean_post_process takes &mut Kernel for the mark lock) and is unreachable today because brep_shells / brep_vertices are UNPORTED(topology.brep_p1). No UNPORTED(boolean.merge_p1) callers on main. Review fixes: SM_USE_OLDTOL vertex tolerance growth restored; face tolerance growth calls a topology.face_p3 stub with old tolerance + dist_between. GetEdges/GetShells use topology.brep_p1 stubs (including count-only queries); no arena-order stand-ins. Reuses public topology_traverser::face_edges with per-loop uniqueness, stale-vertex sanity bound and unconditional merged-domain restore. Mapped/new vertex notification identities derive from ids; new MakeShellVertex path calls the intersector Relate (boolean::topology_intersector::p1::Relations::relate_vertices, wired in PR #146; the other operand is now mutable). merge_p5 now uses shared mutable bidirectional mates and the real MergeVertex adapter. 16 directed unequal-tolerance cases pass. Independent-operand radial queries and mated face-region cases are covered; full step-off numerics remain deferred, two cross-operand wire-sector cases compare real topology. Relate wired, not certified (PR #146): Relations::relate_vertices follows MakeShellVertex. PR #183 back-edge: MergeVertex calls topology::brep::p9::make_shell_vertex with its existing arena ids and source point; VertexCopy destination identities cover vertices and vertexuses, preserving caller-owned notification state. [topology.edgeuse_p3 merged (PR #117): the step-offs of RadialSectorClassify may create UV trim curves, so radial_sector_classify_between / find_radial_sector_between take `&mut Brep` and the other operand as `Option<&mut Brep>` (`None` = the same B-rep, replacing the pointer-equality test), and find_brep_region_of_other_face takes both operands `&mut`. 248 cases, 177 pass, 0 failed, 71 deferred (topology.brep_p1 33, intersect.surface_drop_lib_p3 18, query.brep 14, topology.brep_p9 3, topology.face_p2 3); the 20 topology.edgeuse_p2 step-off deferrals now run, 2 pass and 18 defer further on SmSurface::DropCurve] UPDATE (PR #145, topology.brep_p1): GetEdges/GetShells call topology::brep::p1 (brep_shells and boolean_post_process take &mut Kernel: upstream GetShells takes SmNewMarkAndLock on the context); GetShells returns the empty list on a B-rep without regions and still stops at SmRegion::GetShells otherwise, so the 33 topology.brep_p1 cases continue to the next stub (topology.shell 26, boolean.topology_intersector_p3 4, topology.face_p2 3): 248 cases, 153 pass, 0 failed, 95 deferred (topology.shell 48, topology.edgeuse_p2 20, query.brep 14, topology.face_p2 6, boolean.topology_intersector_p3 4, topology.brep_p9 3). After topology.shell (#154) merged and topology.brep_p1's GetShells is wired to it (SmRegion::GetShells under the mark lock): 248 cases, 201 pass, 0 failed, 47 deferred (topology.edgeuse_p2 20, query.brep 14, topology.face_p2 6, boolean.topology_intersector_p3 4, topology.brep_p9 3). Rerun with PR #183 (topology.brep_p9 wired, 7da151aa): 248 cases, 203 pass, 0 failed, 45 deferred (intersect.surface_drop_lib_p3 18, query.brep 14, topology.face_p2 6, boolean.topology_intersector_p3 4, topology.brep_p2 3); the 3 former topology.brep_p9 deferrals reach MakeShellVertexTopology (topology.brep_p2), and 2 earlier topology.edgeuse_p2 step-offs now pass. Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 248 cases, 206 pass, 0 fail, 42 deferred (intersect.surface_drop_lib_p3 18, query.brep 14, topology.face_p2 6, boolean.topology_intersector_p3 4); the 3 MakeShellVertexTopology deferrals pass. graph() now numbers every record in the C++ shim's walk order (Canon::of: regions, then GetEdges, then the per-kind stages), and op 3 reports the copied vertex by that number and reads its flags and attribute through its own key (plan/findings.md, topology.brep_p2). UPDATE (PR #204, topology.brep_p3; overseer decision): the shared difftest graph() / Canon::of (this unit's runner, used by others) number, compare and count only the records the walk reaches from the B-rep's roots, as the C++ export walk emits; records upstream deletes stay in the Rust arenas unlinked (topology.brep_p2's deviation) and were compared before. The unreached count is a report diagnostic (Output::diagnostics, unreached_records, never compared). Unit test graph_compares_only_reachable_records (an unlinked record leaves graph() unchanged, a still-linked extra vertex use changes it). See plan/findings.md Owner zone-tol cache (surface/owner-zone-tol): 248 cases, 212 pass, 0 fail, 36 deferred. UPDATE (PR #679): the runner's SmTemporaryBrepTransform probe applies and restores the shift through topology::brep::p5::transform. difftest 248 cases, 243 pass, 0 failed, 5 deferred (MergeCoincidentSurfaces 4, topology.face_p2 1); the 2 SmBrep::Transform deferrals pass." +notes = "2026-10-24 (claude/vertex-point): boolean_post_process puts back the owner identity of the surviving edge curve after DeleteTopologicalVertex (which runs without the caller notification identities and re-owned the joined curve under a kernel key, so the temporary transform inverse skipped it: boolean.merge_p4 far-translation). PASS boolean.merge_p1 cases=248 passed=248 failed=0 deferred=0 (Mac). GREEN (wire/merge-coincident-surfaces, 2026-10-24): FixFoundFaceCoincidence runs SmTopologyIntersector::MergeCoincidentSurfaces (boolean.topology_intersector_p3) through the manifold adapter: p1::check_face_coincidence (the all-edges-mated return) and p1::finish_face_coincidence (rbModifiedTopology from the B-reps' edge counts) wrap the merge, which ManifoldKernelAdapter::fix_face_coincidence runs on an ArenaPair (new IntersectorBrepP3, MergeIntervalBrep and ClassGeometryMut impls, loop-use and face-use handle kinds) with the retained mate store. difftest 248 cases, 248 passed, 0 deferred (main: 244 passed, 4 deferred on MergeCoincidentSurfaces); 6000 seeds: 6152/6152. Op 2 now drives the adapter (SmMerge(context) tolerances SM_EFF_ZERO_SQ and 30 degrees) and compares both operands' graphs after the call, the returned faces and both directions of the edge mates (the shim appends them; before, the other B-rep's edits and the mates were not compared). Callers of the changed path (boolean.merge_p4, boolean.api_brep, ...): no unit's numbers changed in the all-units run. Generator: op 2 also takes same-plane rectangles that overlap, are contained, enclose, touch, share edges with or miss the face; with #718 and #721 on main all pass (the touching and disjoint ones compare the B-reps, the others end in an error status both sides agree on), see plan/findings.md. Earlier history: main CI e33985411 (2026-10-23): PARTIAL boolean.merge_p1 cases=248 passed=241 failed=0 deferred=7 blocked_by=boolean.topology_intersector_p3(4),topology.brep_p5(2),topology.face_p2(1). CO-PORTED in boolean.api_brep (PR #81): SmMergeOptions (fields, ctor, defaults, copy ctor, getters/setters) -> solidcore::boolean::merge::p1::MergeOptions (crates/solidcore/src/boolean/merge/p1.rs; extend it, keep boolean/merge.rs as the split root); 24 fns; largest: SmMerge::BooleanPostProcess, SmMerge::FixFoundFaceCoincidence, sm_FindBrepRegionOfOtherFace, SmMerge::PropagateOtherSrcRegionAttribs; cycle back-edges (call into later units; stub or co-port): blend.fillet_executive_p1(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit; PR #163: merge control, face coincidence fast path, mapped vertex copy with topology/attribute notification, cleanup and region-attribute propagation; cross-B-rep radial classification keeps operand ids separate. Difftest 248 cases (152 fixtures + 96 generated), 175 passed, 73 deferred, 0 failed, max_rel_err=0, no absolute floors. Deferred: query.brep(14), topology.brep_p1(33), topology.brep_p9(3), topology.edgeuse_p2(20), topology.face_p2(3). DeleteEdge/DeleteVertex/DeleteTopologicalVertex remain tagged back-edges; scoped translation waits for query.brep bounding boxes; intersector construction is supplied by its owner. Wired to topology.shell (PR #154): SmRegion::IsVoid in sm_EdgeBordersSolid, SmShell::IsWireShell/IsFaceuseShell in BooleanPostProcess, region propagation takes a RegionLookup for the source regions' IsVoid. The wire/face shell combine block of BooleanPostProcess is wired end to end (GetCommonVertices = the B-rep vertices with an other mate in the shared mates, as TopologyIntersector::common_vertices after PR #146; then ignored-status shell::transfer_children and shell::region_remove_shell; boolean_post_process takes &mut Kernel for the mark lock) and is unreachable today because brep_shells / brep_vertices are UNPORTED(topology.brep_p1). No UNPORTED(boolean.merge_p1) callers on main. Review fixes: SM_USE_OLDTOL vertex tolerance growth restored; face tolerance growth calls a topology.face_p3 stub with old tolerance + dist_between. GetEdges/GetShells use topology.brep_p1 stubs (including count-only queries); no arena-order stand-ins. Reuses public topology_traverser::face_edges with per-loop uniqueness, stale-vertex sanity bound and unconditional merged-domain restore. Mapped/new vertex notification identities derive from ids; new MakeShellVertex path calls the intersector Relate (boolean::topology_intersector::p1::Relations::relate_vertices, wired in PR #146; the other operand is now mutable). merge_p5 now uses shared mutable bidirectional mates and the real MergeVertex adapter. 16 directed unequal-tolerance cases pass. Independent-operand radial queries and mated face-region cases are covered; full step-off numerics remain deferred, two cross-operand wire-sector cases compare real topology. Relate wired, not certified (PR #146): Relations::relate_vertices follows MakeShellVertex. PR #183 back-edge: MergeVertex calls topology::brep::p9::make_shell_vertex with its existing arena ids and source point; VertexCopy destination identities cover vertices and vertexuses, preserving caller-owned notification state. [topology.edgeuse_p3 merged (PR #117): the step-offs of RadialSectorClassify may create UV trim curves, so radial_sector_classify_between / find_radial_sector_between take `&mut Brep` and the other operand as `Option<&mut Brep>` (`None` = the same B-rep, replacing the pointer-equality test), and find_brep_region_of_other_face takes both operands `&mut`. 248 cases, 177 pass, 0 failed, 71 deferred (topology.brep_p1 33, intersect.surface_drop_lib_p3 18, query.brep 14, topology.brep_p9 3, topology.face_p2 3); the 20 topology.edgeuse_p2 step-off deferrals now run, 2 pass and 18 defer further on SmSurface::DropCurve] UPDATE (PR #145, topology.brep_p1): GetEdges/GetShells call topology::brep::p1 (brep_shells and boolean_post_process take &mut Kernel: upstream GetShells takes SmNewMarkAndLock on the context); GetShells returns the empty list on a B-rep without regions and still stops at SmRegion::GetShells otherwise, so the 33 topology.brep_p1 cases continue to the next stub (topology.shell 26, boolean.topology_intersector_p3 4, topology.face_p2 3): 248 cases, 153 pass, 0 failed, 95 deferred (topology.shell 48, topology.edgeuse_p2 20, query.brep 14, topology.face_p2 6, boolean.topology_intersector_p3 4, topology.brep_p9 3). After topology.shell (#154) merged and topology.brep_p1's GetShells is wired to it (SmRegion::GetShells under the mark lock): 248 cases, 201 pass, 0 failed, 47 deferred (topology.edgeuse_p2 20, query.brep 14, topology.face_p2 6, boolean.topology_intersector_p3 4, topology.brep_p9 3). Rerun with PR #183 (topology.brep_p9 wired, 7da151aa): 248 cases, 203 pass, 0 failed, 45 deferred (intersect.surface_drop_lib_p3 18, query.brep 14, topology.face_p2 6, boolean.topology_intersector_p3 4, topology.brep_p2 3); the 3 former topology.brep_p9 deferrals reach MakeShellVertexTopology (topology.brep_p2), and 2 earlier topology.edgeuse_p2 step-offs now pass. Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 248 cases, 206 pass, 0 fail, 42 deferred (intersect.surface_drop_lib_p3 18, query.brep 14, topology.face_p2 6, boolean.topology_intersector_p3 4); the 3 MakeShellVertexTopology deferrals pass. graph() now numbers every record in the C++ shim's walk order (Canon::of: regions, then GetEdges, then the per-kind stages), and op 3 reports the copied vertex by that number and reads its flags and attribute through its own key (plan/findings.md, topology.brep_p2). UPDATE (PR #204, topology.brep_p3; overseer decision): the shared difftest graph() / Canon::of (this unit's runner, used by others) number, compare and count only the records the walk reaches from the B-rep's roots, as the C++ export walk emits; records upstream deletes stay in the Rust arenas unlinked (topology.brep_p2's deviation) and were compared before. The unreached count is a report diagnostic (Output::diagnostics, unreached_records, never compared). Unit test graph_compares_only_reachable_records (an unlinked record leaves graph() unchanged, a still-linked extra vertex use changes it). See plan/findings.md Owner zone-tol cache (surface/owner-zone-tol): 248 cases, 212 pass, 0 fail, 36 deferred. UPDATE (PR #679): the runner's SmTemporaryBrepTransform probe applies and restores the shift through topology::brep::p5::transform. difftest 248 cases, 243 pass, 0 failed, 5 deferred (MergeCoincidentSurfaces 4, topology.face_p2 1); the 2 SmBrep::Transform deferrals pass." diff --git a/plan/units/boolean.merge_p3.toml b/plan/units/boolean.merge_p3.toml index 7bd830f79b..ff3a926282 100644 --- a/plan/units/boolean.merge_p3.toml +++ b/plan/units/boolean.merge_p3.toml @@ -9,4 +9,4 @@ tests = ["SM_API_test:TestSmMaterialCensus", "SM_API_test:TestSmPiecewiseMerge", status = "partial" claimed_by = "" claimed_at = "" -notes = "FACE CLASSIFY (2026-10-24, claude/face-classify-tol): the 4 query.face_p2 SmFace::PointClassify deferrals now classify on the mutable B-rep and stop later at topology.brep_p1 SmBrep mutable owning context. Mac difftest: passed=395 deferred=28 (topology.brep_p1 8, topology.face_p2 3, topology.vertex 17). PR #738: the runner's PiecewiseMerge (family 4) constructs the merge and runs IntersectInsertRelate, ClearRelationships, SetSubset, the controls, RegisterExistingSSI/ClearExistingSSI and ProcessWiresAndShellVertices on that engine (merge p4 kernel adapter), the one ManifoldBoolean uses: PARTIAL cases=423 passed=395 failed=0 deferred=28 blocked_by=query.face_p2(4),topology.brep_p1(4),topology.face_p2(3),topology.vertex(17) on main 76f27c381 (was 361 passed, 62 deferred, 48 of them at IntersectInsertRelate). main CI e33985411 (2026-10-23): PARTIAL boolean.merge_p3 cases=423 passed=361 failed=0 deferred=62 blocked_by=boolean.topology_intersector_p2(48),topology.edge(5),topology.vertex(9). PARTIAL (PR #245): all 5 functions ported; PR #287 update: 387 cases (131 fixtures, 33 of them family-6 engine-probe cases of DependencyMerge::with_engine against a context-only native SmMerge, NER/controls only; + 256 generated), 215 matched / 0 failed / 172 deferred, with_values=159; perturb 215/215; fuzz 2000 PASS (668 ran, 1332 rejected, 0 differs/crashes/panics/timeouts); valgrind clean on 8 family-6 programs. Counts below are as of #245 except the deferral split. Opus-fix verification: 354 cases (98 fixtures + 256 generated), 182 matched / 0 failed / 172 deferred; with_values=126 (103 leaf identity/order/null + limited Rust B-rep digest cases, 23 empty forests), no_values=56 (54 native error statuses, 2 explicit UB-190 guard-only refusals; 180 operation comparisons + 2 guard self-checks). Rust digest compares face/edge/vertex counts and ordered vertex positions, not full graph/adjacency/curve/surface parity; earlier C++ payload echo coverage claim corrected. Zero geometric-success comparisons. Deferred: boolean.merge_p2 87 (40 of them NonManifoldBoolean cases: ManifoldBoolean is wired to boolean.merge_p4 through DependencyMerge's constructed engine, which SmMerge::SmMerge builds), boolean.topology_intersector_p2 48, topology.brep_p3 37. Perturb 182/182 detected; fuzz 2000 PASS (708 ran, 1292 rejected, 0 differs/crashes/panics/timeouts); Valgrind 50 programs/10 per family, baseline equal, 0 memory errors (leak checking disabled for retained unknown failed-call objects). All three boolean.api_brep back-edges wired. All family success results now serialized from Rust; PiecewiseMerge is generic over PiecewiseAccess with no kernel engine implementation yet (merge_p2/p4). Pool uses canonical intersector/attributes/marks, owner-tagged dependencies, and no silent subset stub. No surface-list or SSI skip-relation parity claimed. UB-188/WR-196 are source-only notes; UB-190 records piecewise imprinting lifetime defect, guarded only for multi-surface other operands. Empty B-rep constructor defaults are explicit topology.brep_p3 stubs (no Brep::new substitution), with full downstream tails. Opus fixes verified on main #130: fmt/own include rustfmt, workspace/CPP checks, clippy x2, 1402 kernel tests, 4 serializer/adapter tests, 2 merge_p3 FFI tests, wasm and native gates pass; 213-unit matrix passes (114 PASS/99 PARTIAL, zero failures, zero green units with deferred cases). Latest-main 154853c7 (includes 46ddc4ff) merged cleanly; post-merge fmt/own include rustfmt, workspace/CPP checks, clippy x2, 1410 kernel tests, 4 adapter tests, 2 merge_p3 FFI tests (+ 1 API test), wasm and default merge_p3/api_brep difftests pass; pulled reports unchanged. Full-matrix/perturb/fuzz/Valgrind evidence is from reviewed f0957fc5. Prior case outcomes unchanged; payload now reads actual Rust state. 5 fns; largest: SmMerge::PiecewiseMerge, SmMerge::BooleanLists, SmMerge::BooleanTrees, SmMerge::merge_breps; unit-SCC #22 (220 units); 102 upstream test entry points reach this unit Re-run in PR #149 (merge with main through 39ceb995, #283): 354 cases, 182 pass, 0 fail, 172 deferred (unchanged; no case reaches the wired SmBrep::Notify / SetTolerance sites, which follow merge_p2 / merge_p4 / topology_intersector_p2 / brep_p3 calls): the arena-only boolean_lists' component CopyFaces notifies through brep_p2's ArenaNotifyState (overseer ruling, reversing the module's 'defers at that boundary'), and the runner pool's SetTolerance / Notify adapters call topology.brep_p2. Re-run in PR #229 (boolean.merge_p2 wired, merge with main through 7b57d8de, after #287): 387 cases, 222 pass, 0 fail, 165 deferred (query.brep 80, boolean.topology_intersector_p2 48, topology.brep_p3 37). SmMerge::SmMerge now builds #287's engine: DependencyMerge::construct fills it through MergeEngine::from_merge (MergeState::with_breps: options, flags, the computed tolerances), and ManifoldBoolean before construct is InvalidInput. The runner's Pool constructs the same engine and runs ManifoldBoolean on it with its B-reps moved in and back; family 3 constructs first, as the shim's MergeAccess does. All 87 former boolean.merge_p2 deferrals now pass (7) or stop at SmBrep::CalculateBoundingBox (query.brep) inside merge_p4's ManifoldBoolean (80). UPDATE (PR #204, topology.brep_p3): the UNPORTED(topology.brep_p3) stubs are wired: SmBrep(size, ratio, stack) -> topology::brep::p3::new_brep (pOptContextForStack is the allocating kernel), the copy constructor -> brep::p3::copy_brep and MergeBrep -> brep::p3::merge_brep, the last two through an arena-only notification store (ArenaNotifyState: default records, no attributes, no context, as the arena entry's component copies) with the source in a key namespace of its own (source_ref: notify_ref | 1 << 40). Unit test renamed: surface_list_makes_the_native_empty_brep_then_stops_at_the_surface_copy (now stops at surface.surface_p1). UPDATE (PR #204, Opus review of 255d52b7): the difftest runner's empty_brep (its UNPORTED(topology.brep_p3) SmBrep::SmBrep) calls topology::brep::p3::new_brep. Gate on the wired tree (PR #204 4c7a444b): 354 cases, 182 pass, 0 fail, 172 deferred; case by case the 37 SmBrep::SmBrep(copy) (topology.brep_p3) deferrals relabel to topology.brep_p4 SmBrep::StitchFaces, no outcome change." +notes = "2026-10-24 (claude/vertex-point, Mac): after merging main with #759: PARTIAL boolean.merge_p3 cases=423 passed=412 failed=0 deferred=11 blocked_by=topology.brep_p1(8),topology.face_p2(3). Earlier: vertex-definition imprint merges in the arena pair; the 9 SmVertex::GetPoint deferrals pass. after merging main (Mac): PARTIAL boolean.merge_p3 cases=423 passed=412 failed=0 deferred=11 blocked_by=query.face_p2(4),topology.brep_p1(4),topology.face_p2(3). FACE CLASSIFY (2026-10-24, claude/face-classify-tol): the 4 query.face_p2 SmFace::PointClassify deferrals now classify on the mutable B-rep and stop later at topology.brep_p1 SmBrep mutable owning context. Mac difftest: passed=395 deferred=28 (topology.brep_p1 8, topology.face_p2 3, topology.vertex 17). PR #738: the runner's PiecewiseMerge (family 4) constructs the merge and runs IntersectInsertRelate, ClearRelationships, SetSubset, the controls, RegisterExistingSSI/ClearExistingSSI and ProcessWiresAndShellVertices on that engine (merge p4 kernel adapter), the one ManifoldBoolean uses: PARTIAL cases=423 passed=395 failed=0 deferred=28 blocked_by=query.face_p2(4),topology.brep_p1(4),topology.face_p2(3),topology.vertex(17) on main 76f27c381 (was 361 passed, 62 deferred, 48 of them at IntersectInsertRelate). main CI e33985411 (2026-10-23): PARTIAL boolean.merge_p3 cases=423 passed=361 failed=0 deferred=62 blocked_by=boolean.topology_intersector_p2(48),topology.edge(5),topology.vertex(9). PARTIAL (PR #245): all 5 functions ported; PR #287 update: 387 cases (131 fixtures, 33 of them family-6 engine-probe cases of DependencyMerge::with_engine against a context-only native SmMerge, NER/controls only; + 256 generated), 215 matched / 0 failed / 172 deferred, with_values=159; perturb 215/215; fuzz 2000 PASS (668 ran, 1332 rejected, 0 differs/crashes/panics/timeouts); valgrind clean on 8 family-6 programs. Counts below are as of #245 except the deferral split. Opus-fix verification: 354 cases (98 fixtures + 256 generated), 182 matched / 0 failed / 172 deferred; with_values=126 (103 leaf identity/order/null + limited Rust B-rep digest cases, 23 empty forests), no_values=56 (54 native error statuses, 2 explicit UB-190 guard-only refusals; 180 operation comparisons + 2 guard self-checks). Rust digest compares face/edge/vertex counts and ordered vertex positions, not full graph/adjacency/curve/surface parity; earlier C++ payload echo coverage claim corrected. Zero geometric-success comparisons. Deferred: boolean.merge_p2 87 (40 of them NonManifoldBoolean cases: ManifoldBoolean is wired to boolean.merge_p4 through DependencyMerge's constructed engine, which SmMerge::SmMerge builds), boolean.topology_intersector_p2 48, topology.brep_p3 37. Perturb 182/182 detected; fuzz 2000 PASS (708 ran, 1292 rejected, 0 differs/crashes/panics/timeouts); Valgrind 50 programs/10 per family, baseline equal, 0 memory errors (leak checking disabled for retained unknown failed-call objects). All three boolean.api_brep back-edges wired. All family success results now serialized from Rust; PiecewiseMerge is generic over PiecewiseAccess with no kernel engine implementation yet (merge_p2/p4). Pool uses canonical intersector/attributes/marks, owner-tagged dependencies, and no silent subset stub. No surface-list or SSI skip-relation parity claimed. UB-188/WR-196 are source-only notes; UB-190 records piecewise imprinting lifetime defect, guarded only for multi-surface other operands. Empty B-rep constructor defaults are explicit topology.brep_p3 stubs (no Brep::new substitution), with full downstream tails. Opus fixes verified on main #130: fmt/own include rustfmt, workspace/CPP checks, clippy x2, 1402 kernel tests, 4 serializer/adapter tests, 2 merge_p3 FFI tests, wasm and native gates pass; 213-unit matrix passes (114 PASS/99 PARTIAL, zero failures, zero green units with deferred cases). Latest-main 154853c7 (includes 46ddc4ff) merged cleanly; post-merge fmt/own include rustfmt, workspace/CPP checks, clippy x2, 1410 kernel tests, 4 adapter tests, 2 merge_p3 FFI tests (+ 1 API test), wasm and default merge_p3/api_brep difftests pass; pulled reports unchanged. Full-matrix/perturb/fuzz/Valgrind evidence is from reviewed f0957fc5. Prior case outcomes unchanged; payload now reads actual Rust state. 5 fns; largest: SmMerge::PiecewiseMerge, SmMerge::BooleanLists, SmMerge::BooleanTrees, SmMerge::merge_breps; unit-SCC #22 (220 units); 102 upstream test entry points reach this unit Re-run in PR #149 (merge with main through 39ceb995, #283): 354 cases, 182 pass, 0 fail, 172 deferred (unchanged; no case reaches the wired SmBrep::Notify / SetTolerance sites, which follow merge_p2 / merge_p4 / topology_intersector_p2 / brep_p3 calls): the arena-only boolean_lists' component CopyFaces notifies through brep_p2's ArenaNotifyState (overseer ruling, reversing the module's 'defers at that boundary'), and the runner pool's SetTolerance / Notify adapters call topology.brep_p2. Re-run in PR #229 (boolean.merge_p2 wired, merge with main through 7b57d8de, after #287): 387 cases, 222 pass, 0 fail, 165 deferred (query.brep 80, boolean.topology_intersector_p2 48, topology.brep_p3 37). SmMerge::SmMerge now builds #287's engine: DependencyMerge::construct fills it through MergeEngine::from_merge (MergeState::with_breps: options, flags, the computed tolerances), and ManifoldBoolean before construct is InvalidInput. The runner's Pool constructs the same engine and runs ManifoldBoolean on it with its B-reps moved in and back; family 3 constructs first, as the shim's MergeAccess does. All 87 former boolean.merge_p2 deferrals now pass (7) or stop at SmBrep::CalculateBoundingBox (query.brep) inside merge_p4's ManifoldBoolean (80). UPDATE (PR #204, topology.brep_p3): the UNPORTED(topology.brep_p3) stubs are wired: SmBrep(size, ratio, stack) -> topology::brep::p3::new_brep (pOptContextForStack is the allocating kernel), the copy constructor -> brep::p3::copy_brep and MergeBrep -> brep::p3::merge_brep, the last two through an arena-only notification store (ArenaNotifyState: default records, no attributes, no context, as the arena entry's component copies) with the source in a key namespace of its own (source_ref: notify_ref | 1 << 40). Unit test renamed: surface_list_makes_the_native_empty_brep_then_stops_at_the_surface_copy (now stops at surface.surface_p1). UPDATE (PR #204, Opus review of 255d52b7): the difftest runner's empty_brep (its UNPORTED(topology.brep_p3) SmBrep::SmBrep) calls topology::brep::p3::new_brep. Gate on the wired tree (PR #204 4c7a444b): 354 cases, 182 pass, 0 fail, 172 deferred; case by case the 37 SmBrep::SmBrep(copy) (topology.brep_p3) deferrals relabel to topology.brep_p4 SmBrep::StitchFaces, no outcome change." diff --git a/plan/units/boolean.merge_p4.toml b/plan/units/boolean.merge_p4.toml index 2eff03450c..f18bf0404c 100644 --- a/plan/units/boolean.merge_p4.toml +++ b/plan/units/boolean.merge_p4.toml @@ -9,7 +9,7 @@ tests = ["prog_test:my_test_bbu", "SM_API_test:TestSmMaterialCensus", "SM_API_te status = "partial" claimed_by = "" claimed_at = "" -notes = "FACE CLASSIFY (2026-10-24, claude/face-classify-tol): the 66 query.face_p2 SmFace::PointClassify deferrals (fast-drop trim test of ComputePointParameters) now classify on the mutable B-rep and stop later at topology.brep_p1 SmBrep mutable owning context. Mac difftest unchanged in counts: passed=140 deferred=168 (topology.brep_p1 86, topology.vertex 82). PR #201 integration extends the production adapter with borrowed caller topology/curve identities and reserved region source-list handles while preserving the existing namespace-backed API. Actual notification records are restored with partial edits on Result exits and unwind; attributes and live contexts remain caller-owned. The automatic-tolerance constructor still belongs to boolean.merge_p2 (#229); borrowed cache access stops at the genuine query.brep bounding-box call and awaits live-kernel cache plumbing. Late consumption before cache/transform errors follows native ordering; operand slots, not Result alone, determine retained Rust ownership. Existing 311-case report is byte-identical, and 30/30 perturbations are detected; latent caller-key tests do not certify successful constructive Booleans. Historical port evidence follows: 1 fn, SmMerge::ManifoldBoolean; PR #243 partial. Full control-flow tail and scoped inverse transform; explicit two-context marks/editing and shared caller AttributeStore. No success path compared yet: all valid pairs stop at merge_p1 CalculateBoundingBox / query.brep. Null errors compare status plus post-state, including surviving graphs. KernelManifoldIntersector implements all 16 methods: shared MergeState, canonical mates, caller notification records/attributes, all four merge_p1 stages and the merge_p5 paired-kernel wire API. Common lists share the generic intersector helper; source-region keys resolve real arenas. Focused adapter tests are separate from Boolean success coverage. CloseIntersectionLoop is a live units.json/unused callgraph gap (SmMerge.cpp:2958), co-ported into boolean.topology_intersector_p2 by PR #281, which also wires it here: KernelManifoldIntersector::close_intersection_loop calls TopologyIntersector::close_intersection_loop over the ArenaPair (the trait method now takes the caller AttributeStore and has no default). The callback retains PR #201 caller notification keys and namespace-aware cache configuration. PointClassify caller logic stays outside its stub. CopyFaces is wired to brep_p7; no incoming UNPORTED(boolean.merge_p4), shared Relations face storage/map-access extensions only, no dependency algorithm co-ports, no absolute floors. Difftest: 311 cases, 30 null-error state matches, 278 query.brep deferrals, 3 native constructor rejections, 0 failed; with_values=30 no_values=0; real Boolean successes=0 (status-as-data makes cases_both_errored=0). Fuzz 2000: rejected=203 malformed=866 ran=931 differs=0 crashes=0 panics=0 timeouts=0. Perturb 30/30; fmt/include rustfmt, checks x2, clippy x2, 1414 tests (eight adapter regressions), wasm pass after main 154853c7 (progress-only). Three requested adapter mutants each fail their intended assertion and were restored byte-identically; production unchanged from CLEAN 0ceda887. Its coordinate/error-state regressions and 405-case full-leak Valgrind baseline comparison pass (0 errors/lost bytes). Its full green gate passed: 214 units, failed=0; every reported partial matches the plan and no plan-green unit deferred. Re-run in PR #149 (merge with main through 39ceb995, #283): 311 cases, 30 pass, 0 fail, 278 deferred (query.brep 278; unchanged): IsSheet / IsOrientedSheet (in the other B-rep's kernel) and DeleteStaleCaches call topology.brep_p2; ArenaPair implements brep_arena and BrepGlue (GlueVertices through p5::glue_with_notifications). UPDATE (PR #204, topology.brep_p3): new_component_brep's UNPORTED(topology.brep_p3) SmBrep::SmBrep is Brep::new_with_infinite_region (the kernel is held shared there: no context tolerance, sizes or SM_NO_CONSTRUCTION notification, the deviation of blend.fillet_geom_p1 / primitive.api_primitives). UPDATE (PR #204, topology.brep_p3, Opus review of 255d52b7): KernelManifoldIntersector::new caps namespaces at 26 bits (0x03ff_ffff, was 27): a 27-bit namespace set bit 63, inside topology.brep_p3's reserved key range (KERNEL_KEY_BASE), so ArenaPair's p3 edits would refuse keys its p5 glue accepts; notification_key_blocks_never_alias_across_operands_or_classes tests the new bound and that the largest namespace's keys are not reserved. #282's DELETE_FACES_NAMESPACE 0x00f5e700 is within it (COORD #282 x #204). SweepStore wiring retains the intersector/Relations under scoped caller notification keys, preserves existing records and region source-list identities, and issues distinct identities for extracted components. Namespace-backed access remains supported. FaceIntersect wired (2026-10-23): 311 cases, 140 passes (was 118), 0 failed, 168 deferred (topology.face_p3 99 -> 0; topology.edge 157, topology.vertex 11)." +notes = "2026-10-24 (claude/vertex-point, Mac): after merging main with #759: PARTIAL boolean.merge_p4 cases=311 passed=242 failed=0 deferred=66 rejected=3 blocked_by=topology.brep_p1(66). Earlier: SmVertexDefinition::Imprint merges through ProtoBrep::merge_point_class_into_brep (ArenaPair: merge_into_object_mut), so the 82 SmVertex::GetPoint and 20 SmBrep mutable owning context deferrals compare; the 3 far-translation cases that then failed (joined curve left untranslated: post-process DeleteTopologicalVertex re-owned it under a kernel key, so the inverse Transform skipped it) pass after boolean.merge_p1 restores the survivor curve owner. PARTIAL boolean.merge_p4 cases=311 passed=242 failed=0 deferred=66 max_rel_err=0e0 with_values=242 no_values=0 vacuous=0 rejected=3 blocked_by=query.face_p2(66). FACE CLASSIFY (2026-10-24, claude/face-classify-tol): the 66 query.face_p2 SmFace::PointClassify deferrals (fast-drop trim test of ComputePointParameters) now classify on the mutable B-rep and stop later at topology.brep_p1 SmBrep mutable owning context. Mac difftest unchanged in counts: passed=140 deferred=168 (topology.brep_p1 86, topology.vertex 82). PR #201 integration extends the production adapter with borrowed caller topology/curve identities and reserved region source-list handles while preserving the existing namespace-backed API. Actual notification records are restored with partial edits on Result exits and unwind; attributes and live contexts remain caller-owned. The automatic-tolerance constructor still belongs to boolean.merge_p2 (#229); borrowed cache access stops at the genuine query.brep bounding-box call and awaits live-kernel cache plumbing. Late consumption before cache/transform errors follows native ordering; operand slots, not Result alone, determine retained Rust ownership. Existing 311-case report is byte-identical, and 30/30 perturbations are detected; latent caller-key tests do not certify successful constructive Booleans. Historical port evidence follows: 1 fn, SmMerge::ManifoldBoolean; PR #243 partial. Full control-flow tail and scoped inverse transform; explicit two-context marks/editing and shared caller AttributeStore. No success path compared yet: all valid pairs stop at merge_p1 CalculateBoundingBox / query.brep. Null errors compare status plus post-state, including surviving graphs. KernelManifoldIntersector implements all 16 methods: shared MergeState, canonical mates, caller notification records/attributes, all four merge_p1 stages and the merge_p5 paired-kernel wire API. Common lists share the generic intersector helper; source-region keys resolve real arenas. Focused adapter tests are separate from Boolean success coverage. CloseIntersectionLoop is a live units.json/unused callgraph gap (SmMerge.cpp:2958), co-ported into boolean.topology_intersector_p2 by PR #281, which also wires it here: KernelManifoldIntersector::close_intersection_loop calls TopologyIntersector::close_intersection_loop over the ArenaPair (the trait method now takes the caller AttributeStore and has no default). The callback retains PR #201 caller notification keys and namespace-aware cache configuration. PointClassify caller logic stays outside its stub. CopyFaces is wired to brep_p7; no incoming UNPORTED(boolean.merge_p4), shared Relations face storage/map-access extensions only, no dependency algorithm co-ports, no absolute floors. Difftest: 311 cases, 30 null-error state matches, 278 query.brep deferrals, 3 native constructor rejections, 0 failed; with_values=30 no_values=0; real Boolean successes=0 (status-as-data makes cases_both_errored=0). Fuzz 2000: rejected=203 malformed=866 ran=931 differs=0 crashes=0 panics=0 timeouts=0. Perturb 30/30; fmt/include rustfmt, checks x2, clippy x2, 1414 tests (eight adapter regressions), wasm pass after main 154853c7 (progress-only). Three requested adapter mutants each fail their intended assertion and were restored byte-identically; production unchanged from CLEAN 0ceda887. Its coordinate/error-state regressions and 405-case full-leak Valgrind baseline comparison pass (0 errors/lost bytes). Its full green gate passed: 214 units, failed=0; every reported partial matches the plan and no plan-green unit deferred. Re-run in PR #149 (merge with main through 39ceb995, #283): 311 cases, 30 pass, 0 fail, 278 deferred (query.brep 278; unchanged): IsSheet / IsOrientedSheet (in the other B-rep's kernel) and DeleteStaleCaches call topology.brep_p2; ArenaPair implements brep_arena and BrepGlue (GlueVertices through p5::glue_with_notifications). UPDATE (PR #204, topology.brep_p3): new_component_brep's UNPORTED(topology.brep_p3) SmBrep::SmBrep is Brep::new_with_infinite_region (the kernel is held shared there: no context tolerance, sizes or SM_NO_CONSTRUCTION notification, the deviation of blend.fillet_geom_p1 / primitive.api_primitives). UPDATE (PR #204, topology.brep_p3, Opus review of 255d52b7): KernelManifoldIntersector::new caps namespaces at 26 bits (0x03ff_ffff, was 27): a 27-bit namespace set bit 63, inside topology.brep_p3's reserved key range (KERNEL_KEY_BASE), so ArenaPair's p3 edits would refuse keys its p5 glue accepts; notification_key_blocks_never_alias_across_operands_or_classes tests the new bound and that the largest namespace's keys are not reserved. #282's DELETE_FACES_NAMESPACE 0x00f5e700 is within it (COORD #282 x #204). SweepStore wiring retains the intersector/Relations under scoped caller notification keys, preserves existing records and region source-list identities, and issues distinct identities for extracted components. Namespace-backed access remains supported. FaceIntersect wired (2026-10-23): 311 cases, 140 passes (was 118), 0 failed, 168 deferred (topology.face_p3 99 -> 0; topology.edge 157, topology.vertex 11)." # Latest-main integration: merge_p2 (#229) adds the required make_faceuse adapter, # using shared mates/records, primary kernel and other context; process_wires passes # a TopologyCopy. Configuration now reuses the landed MergeState::with_breps; diff --git a/plan/units/boolean.topology_intersector_p2.toml b/plan/units/boolean.topology_intersector_p2.toml index 539aecccea..7a180ebd34 100644 --- a/plan/units/boolean.topology_intersector_p2.toml +++ b/plan/units/boolean.topology_intersector_p2.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_analytic_tsurf_creation", "prog_test:my_test_get_loo status = "partial" claimed_by = "" claimed_at = "" -notes = "FACE CLASSIFY (2026-10-24, claude/face-classify-tol): SetPointClassParameters now takes the mutable B-rep (set_point_class_parameters_mut), so a face surface's fast-drop trim test makes the face's trimmed cache and classifies. Mac difftest: PARTIAL cases=910 passed=817 failed=0 deferred=93 (was 815 passed / 95 deferred). partial (PR #203, on main's #146): difftest 905 cases (9 upstream-derived fixtures + 32 edge cases built in code + 800 generated two-B-rep scripts on part 1's script runner + 16 CloseIntersectionLoop edge cases + 48 seeded CloseIntersectionLoop scripts), 728 pass bit-identical (max_rel_err 0), 0 fail, 177 deferred (topology.face_p2 75: SmFace::GetTolerance (2 of them iir_negative_tol/iir_tiny_tol, formerly topology.brep_p2 SmBrep::GetMinEdgeBBoxSize before #149; 3 of them formerly topology.face_p4 GetUpwardLoopuses before #239) and query.brep 47: SmBrep::CalculateBoundingBox (41 in IntersectTopology/IntersectInsertRelate after SmBrep::GetFaces (wired to topology.brep_p1), 6 in CloseIntersectionLoop's BrepCurveSolve); topology.face_p4 31: SmFace::GetEdges/GetVertices/GetBrep (part 1's IntersectorBrep stubs, unwired since #239 landed); topology.face_p3 12: FaceIntersect; topology.brep_p3 8 / topology.brep_p1 4: SqueezeEdge / (since #149 wired SmBrep::Notify) m_pVertexListHead inside CombineCoincidentVertices (wired to topology.brep_p5); the 3 former m_bEditingEnabled deferrals pass since #285), 2 cases_both_errored (scripts cut at op 0), so 726 real passes; FUZZ-PASS 2000 (crashes/panics/timeouts 0). What passes cover (per op occurrence, non-deferred cases): RemoveRelationship 29 success / 35 SM_ERR; CheckForSqueezeVertices 75 success without a conflicting mate (empty lists) / 26 SM_ERR (existing relationship better), a conflict that combines defers inside CombineCoincidentVertices; DeleteFaces 68 success, all with an empty face list; FixGaps 119 success without a gap pair / 14 SM_ERR_NULL_POINTER (gap pair with an unmated vertex), a pair that combines defers inside CombineCoincidentVertices; SetRelationships 39 success on managers without definitions; FixupClassMates 56 success / 17 SM_ERR (sizes differ); FixupSplitVertexPair 57 success, 6 of them removing the interval; EdgeEdgeIntersect 136 success (9 more since main's topology.brep_p9), none with a non-empty first split list; EdgeFaceCoincidence 128 success, none imprinting; CloseIntersectionLoop 28 success without new topology / 19 success relating an edge pair (the single-common-edge drop) / 11 SM_ERR_NULL_POINTER (null or unmated vertex, no maps), the coincidence path defers (BrepCurveSolve, then MergeCurveClasses): no pass reaches BrepCurveSolve, IIREdgeBrep or makes new topology; IntersectTopology 1 success (the smTopoTest pair marked as not intersecting), IntersectInsertRelate none (SmFace::GetTolerance / SmBrep::CalculateBoundingBox). RegisterExistingSSI is now run by the port in part 1's script (op 16; a non-B-spline 3D curve, UB-184, is never registered). Also UB-175 [PSIRT] (~SmProtoTopologyManager frees proto edges before the vertices that still list them; the shim runs a verbatim IntersectInsertRelate copy that unlinks them first and answers -2, the port returns InvalidInput) and HANG-25 (IntersectInsertRelate at tolerance 0, refused by the FFI); op 47 also has a measured time-budget guard |approx| <= 0.1 (defined input, not UB; plan/findings.md). UNPORTED stubs added: topology.brep_p3 (DeleteFace), topology.brep_p4 (DeleteEdge/DeleteVertex), query.face_p1 (SmFace::Point3DClassify); part 1's stubs reused: topology.face_p4 (GetEdges/GetVertices/GetBrep), topology.face_p3 (FaceIntersect); CombineCoincidentVertices wired to topology.brep_p5 through IntersectorBrepP2::brep_combine_coincident_vertices (so check_for_squeeze_vertices and fix_gaps take &mut Kernel; follow-up to #203); MakeEdgeInFace goes through part 1's brep_p5 backend; GlueEdgesGeneral (topology.brep_p7, merged in #155) goes through IntersectorBrep::brep_glue_edges_general, so edge_face_coincidence takes &mut Kernel. SmBrep::GetFaces wired to topology.brep_p1 through IntersectorBrepP2::brep_faces (follow-up to #203). Back-edge wired: UNPORTED(boolean.topology_intersector_p2) RemoveRelationship in topology::proto_topology::p3 (GlueCoincidentBrepVertices) through the Mates trait (boolean::topology_intersector::p2::MapMates). CO-PORTED in boolean.topology_intersector_p2 (PR #281): SmTopologyIntersector::CloseIntersectionLoop (SmTopologyIntersector.cpp:6108-6354, called from SmMerge.cpp:2958) and its only callee SmTopologyIntersector::IIREdgeBrep (:2836-2971), both listed by no unit in harness/callgraph/units.json -> boolean::topology_intersector::p2::TopologyIntersector::{close_intersection_loop, iir_edge_brep}, wired into boolean.merge_p4's KernelManifoldIntersector in the same PR; IIREdgeBrep's MergeCurveClasses stays UNPORTED(boolean.topology_intersector_p3) (ported as TopologyIntersector::merge_curve_classes in draft #211). The two constructors were CO-PORTED in boolean.topology_intersector_p1 (PR #146) -> boolean::topology_intersector::p1::TopologyIntersector::{with_context, new}. Downstream: boolean.topology_intersector_p3 (#211) builds on this branch. 12 fns + 2 co-ported; largest: IntersectTopology, EdgeFaceCoincidence, FixupSplitVertexPair, IntersectInsertRelate; hazards: ptr-hash (none in this part's outputs), tbb (SM_USE_TBB blocks not built) Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 841 cases, 666 pass, 0 fail, 175 deferred; its 2 SmBrep::Notify deferrals relabel to topology.brep_p1. UPDATE (PR #204, topology.brep_p3): the UNPORTED(topology.brep_p3) brep_delete_face stub is removed; DeleteFaces calls IntersectorBrep::brep_delete_face (boolean.topology_intersector_p1's hook, SmBrep::DeleteFace with the defaults FALSE, TRUE, NULL, NULL that this call passes), so delete_faces takes k: &mut Kernel. Still behind SmFace::GetEdges (topology.face_p4): report unchanged (841 cases, 666 pass, 0 fail, 175 deferred). Re-run in PR #204 (07c89fab, main through e5f3e441): 841 cases, 667 pass, 0 fail, 174 deferred; case by case the 8 SmBrep::SqueezeEdge (topology.brep_p3) deferrals relabel to topology.brep_p1 SmBrep::UpdateVertexEdgeIntervals (the squeeze is wired), no outcome change Owner zone-tol cache (surface/owner-zone-tol): 910 cases, 736 pass, 0 fail, 174 deferred. FaceIntersect wired (2026-10-23): face_face_intersect_curves runs through IntersectorBrep::face_intersect; 910 cases, 775 passes (was 773), 0 failed, 135 deferred (topology.face_p3 85 -> 0; now topology.face_p2 68, topology.edge 40, topology.brep_p1 13, query.face_p2 6, topology.vertex 6, query.face_p1 1, topology.edgeuse_p3 1). UPDATE (PR #679, UpdateVertexEdgeIntervals wired in CombineCoincidentVertices): 910 cases, 788 pass, 0 failed, 122 deferred; the 13 UpdateVertexEdgeIntervals deferrals pass. UPDATE (owning-face caches in FaceIntersect): the Boolean arena pair lends its B-reps to FaceIntersect's surface intersection: PARTIAL boolean.topology_intersector_p2 cases=910 passed=802 failed=0 deferred=108 (was 788 passed, 122 deferred); the 54 topology.face_p2 left come through other call sites (DropAndTrimCurve's tracer caches, SimpleCoincidenceChecker inside GlobalCurveIntersect)." +notes = "2026-10-24 (claude/vertex-point, Mac): after merging main with #759: PARTIAL boolean.topology_intersector_p2 cases=910 passed=840 failed=0 deferred=70 blocked_by=query.face_p1(2),query.face_p2(24),topology.brep_p1(15),topology.edgeuse_p3(1),topology.face_p2(28). Earlier: vertex-definition imprint merges through ProtoBrep::merge_point_class_into_brep (runner Two and ArenaPair); the 23 SmVertex::GetPoint deferrals pass. PARTIAL boolean.topology_intersector_p2 cases=910 passed=838 failed=0 deferred=72 max_rel_err=0e0 with_values=836 no_values=2 vacuous=0 blocked_by=query.face_p1(1),query.face_p2(6),topology.brep_p1(1),topology.edgeuse_p3(1),topology.face_p2(63). FACE CLASSIFY (2026-10-24, claude/face-classify-tol): SetPointClassParameters now takes the mutable B-rep (set_point_class_parameters_mut), so a face surface's fast-drop trim test makes the face's trimmed cache and classifies. Mac difftest: PARTIAL cases=910 passed=817 failed=0 deferred=93 (was 815 passed / 95 deferred). partial (PR #203, on main's #146): difftest 905 cases (9 upstream-derived fixtures + 32 edge cases built in code + 800 generated two-B-rep scripts on part 1's script runner + 16 CloseIntersectionLoop edge cases + 48 seeded CloseIntersectionLoop scripts), 728 pass bit-identical (max_rel_err 0), 0 fail, 177 deferred (topology.face_p2 75: SmFace::GetTolerance (2 of them iir_negative_tol/iir_tiny_tol, formerly topology.brep_p2 SmBrep::GetMinEdgeBBoxSize before #149; 3 of them formerly topology.face_p4 GetUpwardLoopuses before #239) and query.brep 47: SmBrep::CalculateBoundingBox (41 in IntersectTopology/IntersectInsertRelate after SmBrep::GetFaces (wired to topology.brep_p1), 6 in CloseIntersectionLoop's BrepCurveSolve); topology.face_p4 31: SmFace::GetEdges/GetVertices/GetBrep (part 1's IntersectorBrep stubs, unwired since #239 landed); topology.face_p3 12: FaceIntersect; topology.brep_p3 8 / topology.brep_p1 4: SqueezeEdge / (since #149 wired SmBrep::Notify) m_pVertexListHead inside CombineCoincidentVertices (wired to topology.brep_p5); the 3 former m_bEditingEnabled deferrals pass since #285), 2 cases_both_errored (scripts cut at op 0), so 726 real passes; FUZZ-PASS 2000 (crashes/panics/timeouts 0). What passes cover (per op occurrence, non-deferred cases): RemoveRelationship 29 success / 35 SM_ERR; CheckForSqueezeVertices 75 success without a conflicting mate (empty lists) / 26 SM_ERR (existing relationship better), a conflict that combines defers inside CombineCoincidentVertices; DeleteFaces 68 success, all with an empty face list; FixGaps 119 success without a gap pair / 14 SM_ERR_NULL_POINTER (gap pair with an unmated vertex), a pair that combines defers inside CombineCoincidentVertices; SetRelationships 39 success on managers without definitions; FixupClassMates 56 success / 17 SM_ERR (sizes differ); FixupSplitVertexPair 57 success, 6 of them removing the interval; EdgeEdgeIntersect 136 success (9 more since main's topology.brep_p9), none with a non-empty first split list; EdgeFaceCoincidence 128 success, none imprinting; CloseIntersectionLoop 28 success without new topology / 19 success relating an edge pair (the single-common-edge drop) / 11 SM_ERR_NULL_POINTER (null or unmated vertex, no maps), the coincidence path defers (BrepCurveSolve, then MergeCurveClasses): no pass reaches BrepCurveSolve, IIREdgeBrep or makes new topology; IntersectTopology 1 success (the smTopoTest pair marked as not intersecting), IntersectInsertRelate none (SmFace::GetTolerance / SmBrep::CalculateBoundingBox). RegisterExistingSSI is now run by the port in part 1's script (op 16; a non-B-spline 3D curve, UB-184, is never registered). Also UB-175 [PSIRT] (~SmProtoTopologyManager frees proto edges before the vertices that still list them; the shim runs a verbatim IntersectInsertRelate copy that unlinks them first and answers -2, the port returns InvalidInput) and HANG-25 (IntersectInsertRelate at tolerance 0, refused by the FFI); op 47 also has a measured time-budget guard |approx| <= 0.1 (defined input, not UB; plan/findings.md). UNPORTED stubs added: topology.brep_p3 (DeleteFace), topology.brep_p4 (DeleteEdge/DeleteVertex), query.face_p1 (SmFace::Point3DClassify); part 1's stubs reused: topology.face_p4 (GetEdges/GetVertices/GetBrep), topology.face_p3 (FaceIntersect); CombineCoincidentVertices wired to topology.brep_p5 through IntersectorBrepP2::brep_combine_coincident_vertices (so check_for_squeeze_vertices and fix_gaps take &mut Kernel; follow-up to #203); MakeEdgeInFace goes through part 1's brep_p5 backend; GlueEdgesGeneral (topology.brep_p7, merged in #155) goes through IntersectorBrep::brep_glue_edges_general, so edge_face_coincidence takes &mut Kernel. SmBrep::GetFaces wired to topology.brep_p1 through IntersectorBrepP2::brep_faces (follow-up to #203). Back-edge wired: UNPORTED(boolean.topology_intersector_p2) RemoveRelationship in topology::proto_topology::p3 (GlueCoincidentBrepVertices) through the Mates trait (boolean::topology_intersector::p2::MapMates). CO-PORTED in boolean.topology_intersector_p2 (PR #281): SmTopologyIntersector::CloseIntersectionLoop (SmTopologyIntersector.cpp:6108-6354, called from SmMerge.cpp:2958) and its only callee SmTopologyIntersector::IIREdgeBrep (:2836-2971), both listed by no unit in harness/callgraph/units.json -> boolean::topology_intersector::p2::TopologyIntersector::{close_intersection_loop, iir_edge_brep}, wired into boolean.merge_p4's KernelManifoldIntersector in the same PR; IIREdgeBrep's MergeCurveClasses stays UNPORTED(boolean.topology_intersector_p3) (ported as TopologyIntersector::merge_curve_classes in draft #211). The two constructors were CO-PORTED in boolean.topology_intersector_p1 (PR #146) -> boolean::topology_intersector::p1::TopologyIntersector::{with_context, new}. Downstream: boolean.topology_intersector_p3 (#211) builds on this branch. 12 fns + 2 co-ported; largest: IntersectTopology, EdgeFaceCoincidence, FixupSplitVertexPair, IntersectInsertRelate; hazards: ptr-hash (none in this part's outputs), tbb (SM_USE_TBB blocks not built) Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 841 cases, 666 pass, 0 fail, 175 deferred; its 2 SmBrep::Notify deferrals relabel to topology.brep_p1. UPDATE (PR #204, topology.brep_p3): the UNPORTED(topology.brep_p3) brep_delete_face stub is removed; DeleteFaces calls IntersectorBrep::brep_delete_face (boolean.topology_intersector_p1's hook, SmBrep::DeleteFace with the defaults FALSE, TRUE, NULL, NULL that this call passes), so delete_faces takes k: &mut Kernel. Still behind SmFace::GetEdges (topology.face_p4): report unchanged (841 cases, 666 pass, 0 fail, 175 deferred). Re-run in PR #204 (07c89fab, main through e5f3e441): 841 cases, 667 pass, 0 fail, 174 deferred; case by case the 8 SmBrep::SqueezeEdge (topology.brep_p3) deferrals relabel to topology.brep_p1 SmBrep::UpdateVertexEdgeIntervals (the squeeze is wired), no outcome change Owner zone-tol cache (surface/owner-zone-tol): 910 cases, 736 pass, 0 fail, 174 deferred. FaceIntersect wired (2026-10-23): face_face_intersect_curves runs through IntersectorBrep::face_intersect; 910 cases, 775 passes (was 773), 0 failed, 135 deferred (topology.face_p3 85 -> 0; now topology.face_p2 68, topology.edge 40, topology.brep_p1 13, query.face_p2 6, topology.vertex 6, query.face_p1 1, topology.edgeuse_p3 1). UPDATE (PR #679, UpdateVertexEdgeIntervals wired in CombineCoincidentVertices): 910 cases, 788 pass, 0 failed, 122 deferred; the 13 UpdateVertexEdgeIntervals deferrals pass. UPDATE (owning-face caches in FaceIntersect): the Boolean arena pair lends its B-reps to FaceIntersect's surface intersection: PARTIAL boolean.topology_intersector_p2 cases=910 passed=802 failed=0 deferred=108 (was 788 passed, 122 deferred); the 54 topology.face_p2 left come through other call sites (DropAndTrimCurve's tracer caches, SimpleCoincidenceChecker inside GlobalCurveIntersect)." diff --git a/plan/units/intersect.curve_class_p3.toml b/plan/units/intersect.curve_class_p3.toml index 4f04589d9c..6227408933 100644 --- a/plan/units/intersect.curve_class_p3.toml +++ b/plan/units/intersect.curve_class_p3.toml @@ -6,7 +6,7 @@ soft_depends_on = ["curve.curve_p6", "geom.geom_utility_p1"] est_lines = 1986 layer = 34 tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_point", "prog_test:my_test_matrix", "prog_test:my_test_reversed_line_join", "SM_API_test:TestSmCreateRectangle", "SM_API_test:TestSmCreateRegularPolygon"] -status = "partial" +status = "green" claimed_by = "" claimed_at = "" -notes = "PR #296 (stand-in vertex/edge/face objects as B-rep records, BareTopology, used when the script has no real one-face B-rep; on main 5c3adfbee): 3708 cases, 3700 pass, 0 fail, 8 deferred (topology.vertex 8: SmVertex::SetPoint in MergeIntoObject); main 3617/91. Before: Topology members wired (branch wire/curve-class-topology-2): difftest 3708 cases (3292 + 416 face_* fixtures), 3617 passed, 0 failed, 91 deferred (topology.vertex 39, topology.face_p2 29, topology.edge 23: bare stand-ins only); SmEdge::GetVertex/GetOtherVertex, GetInterval, GetCurve and SmVertex::GetPoint resolve through ClassGeometry::topology_entity/topology_handle (p7 edge_of/vertex_of); a third of the generated scripts and the face_* fixtures carry a one-face B-rep (as p7) so InsertPointClass vertex/edge merges, sm_ChangeEdgePointClassToNearVertex (40 fixtures snap to a vertex) and CleanupAndValidate over edge intervals are compared; the shim still refuses those walks over stand-in edges. Before: After intersect.curve_class_p2 (PR #85) wired InsertAt/EditIntervalMin/EditIntervalMax/ConnectIntervals/GetClassifyCrvLoc: 3292 cases, 3163 pass, 0 failed, 129 deferred (topology.vertex 45, topology.face_p2 49, topology.edge 35). PARTIAL (PR #123): difftest 3292 cases (292 hand-written edge scripts over curve kinds, tolerance limits and per-op boundaries + 3000 generated scripts), 2156 pass bit-identical (max_rel_err 0), 0 failed, 1136 deferred (intersect.curve_class_p2 1022; topology.vertex 41 / topology.edge 32 / topology.face_p2 41: the zone tolerance lookups SmVertex/SmEdge/SmFace::GetTolerance of opaque handles) after intersect.curve_class_p7 (PR #65) wired GetPointClassByIndex, GetPointClassMateByIndex and AreCoincidentMaybe; --perturb SENSITIVE. Stubs: UNPORTED(intersect.curve_class_p2) (PR #85) is the maintainer's open PR, not merged here; UNPORTED(topology.edge)/UNPORTED(topology.vertex) stay because the opaque handle O cannot reach a Brep (see plan/findings.md). sm_FixProblemsBySqueezing tail recursion is a restart loop with the same visit order, bounded by interval count + 1 (header Deviations). Upstream defects UB-109, UB-110 (plan/upstream-issues.md). 9 fns; largest: SmCurveClassification::InsertPointClass, SmCurveClassification::FindInterval, SmCurveClassification::MergePointClass, sm_ChangeEdgePointClassToNearVertex; cycle back-edges (call into later units; stub or co-port): topology.edge(8), topology.vertex(2); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit" +notes = "2026-10-24 (claude/vertex-point, Mac): InsertPointClass runs through insert_point_class_mut on the runner pool (MergeIntervalBrep over the one-face B-rep and the bare records), so Combine moves the vertex (SmVertex::SetPoint); both sides report every vertex object point after each insert. PASS intersect.curve_class_p3 cases=3708 passed=3708 failed=0 deferred=0 max_rel_err=0e0 with_values=3708 no_values=0 vacuous=0. PR #296 (stand-in vertex/edge/face objects as B-rep records, BareTopology, used when the script has no real one-face B-rep; on main 5c3adfbee): 3708 cases, 3700 pass, 0 fail, 8 deferred (topology.vertex 8: SmVertex::SetPoint in MergeIntoObject); main 3617/91. Before: Topology members wired (branch wire/curve-class-topology-2): difftest 3708 cases (3292 + 416 face_* fixtures), 3617 passed, 0 failed, 91 deferred (topology.vertex 39, topology.face_p2 29, topology.edge 23: bare stand-ins only); SmEdge::GetVertex/GetOtherVertex, GetInterval, GetCurve and SmVertex::GetPoint resolve through ClassGeometry::topology_entity/topology_handle (p7 edge_of/vertex_of); a third of the generated scripts and the face_* fixtures carry a one-face B-rep (as p7) so InsertPointClass vertex/edge merges, sm_ChangeEdgePointClassToNearVertex (40 fixtures snap to a vertex) and CleanupAndValidate over edge intervals are compared; the shim still refuses those walks over stand-in edges. Before: After intersect.curve_class_p2 (PR #85) wired InsertAt/EditIntervalMin/EditIntervalMax/ConnectIntervals/GetClassifyCrvLoc: 3292 cases, 3163 pass, 0 failed, 129 deferred (topology.vertex 45, topology.face_p2 49, topology.edge 35). PARTIAL (PR #123): difftest 3292 cases (292 hand-written edge scripts over curve kinds, tolerance limits and per-op boundaries + 3000 generated scripts), 2156 pass bit-identical (max_rel_err 0), 0 failed, 1136 deferred (intersect.curve_class_p2 1022; topology.vertex 41 / topology.edge 32 / topology.face_p2 41: the zone tolerance lookups SmVertex/SmEdge/SmFace::GetTolerance of opaque handles) after intersect.curve_class_p7 (PR #65) wired GetPointClassByIndex, GetPointClassMateByIndex and AreCoincidentMaybe; --perturb SENSITIVE. Stubs: UNPORTED(intersect.curve_class_p2) (PR #85) is the maintainer's open PR, not merged here; UNPORTED(topology.edge)/UNPORTED(topology.vertex) stay because the opaque handle O cannot reach a Brep (see plan/findings.md). sm_FixProblemsBySqueezing tail recursion is a restart loop with the same visit order, bounded by interval count + 1 (header Deviations). Upstream defects UB-109, UB-110 (plan/upstream-issues.md). 9 fns; largest: SmCurveClassification::InsertPointClass, SmCurveClassification::FindInterval, SmCurveClassification::MergePointClass, sm_ChangeEdgePointClassToNearVertex; cycle back-edges (call into later units; stub or co-port): topology.edge(8), topology.vertex(2); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit" diff --git a/plan/units/intersect.curve_class_p6.toml b/plan/units/intersect.curve_class_p6.toml index 95ec4f4a02..71c7c45fb2 100644 --- a/plan/units/intersect.curve_class_p6.toml +++ b/plan/units/intersect.curve_class_p6.toml @@ -6,7 +6,7 @@ soft_depends_on = ["curve.crv_on_surf", "curve.curve_p6", "nurbs.b_spline_curve_ est_lines = 2213 layer = 35 tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_point", "prog_test:my_test_matrix", "prog_test:my_test_reversed_line_join", "SM_API_test:TestSmCreateRectangle", "SM_API_test:TestSmCreateRegularPolygon"] -status = "partial" +status = "green" claimed_by = "" claimed_at = "" -notes = "MergeInterval imprinting ported (PR #721, 2026-10-24): the tail after GetPointObjZoneTol3d (unknown-end repair, closed/degenerate branch, wire edge, edge in face with face split, coincident edge) edits the B-rep through MergeIntervalBrep (merge_interval_mut, merge_point_class_mut; the read-only entry points defer at the first edit). Runner: a fresh box copy per case, the box compared after a merge, 28 imprint_* cases (picked box objects, lines on the box, editing enabled). 4466 cases, 4446 pass, 0 failed, 20 deferred (topology.vertex 20: SetPoint in InsertCurveInterval's combine, not MergeInterval). PR #599 (2026-10-23): the runner passes the partner classification (the one the shim's SetFilletData names) to merge_interval_with, so the 10 intersect.curve_class_p7 GetOtherClassification deferrals pass with values (142 each): 4438 cases, 4326 pass, 0 failed, 112 deferred (intersect.curve_class_p6 92 MergeInterval imprinting, topology.vertex 20 SetPoint). PR #296 (runner imports the shim's SmApiCreateBox B-rep via heal.stitch_p1 export): 4438 cases, 4316 pass, 0 fail, 122 deferred (intersect.curve_class_p6 92 MergeInterval imprinting, topology.vertex 20 SetPoint, intersect.curve_class_p7 10), --perturb 4316/4316. After intersect.curve_class_p2 (PR #85) wired GetPointObjZoneTol3d into MergeInterval (now takes k and geometry): 4438 cases, 4216 pass, 0 failed, 222 deferred (topology.edge 80, topology.face_p2 60, intersect.curve_class_p6 52 = MergeInterval imprinting not ported in this unit, topology.vertex 20, intersect.curve_class_p7 10). PR #138: partial; 4438 cases, 3933 passes (3927 bit-identical kernel comparisons and 6 expected FFI rejection checks), 0 failures, 505 deferred, no absolute floors. InsertCurveInterval, Homogenize and FixEndPointClass preserve upstream control flow with p2/p4/p7 dependency calls; MergeInterval implements early exits only and defers topology mutation at p2 GetPointObjZoneTol3d. Native fixtures use real 3D lines, circles, ellipses, quadratic and constant B-splines plus box topology. Nonempty homogenization/endpoint repair and topology mutation are not certified; generic topology tolerance lookups also retain existing adapter deferrals. Native regression tests run locally: 27440 isolated operation/geometry/flag/classification probes, 96 partition/SSS/boundary probes, 660 mismatched fillet-partner probes checking Rust and raw C++ guards independently, caller-point and projection regressions; project CI unchanged. Audit reuses p1 types and p3 helpers; p2/p4/p7 remain explicit dependencies, with no duplicate co-ports. Maintainer review: require homogenized partner counts for fillet merges (UB-136); reuse p3 boundary-location helper; ignore ordinary squeeze errors while retaining the no-progress guard (HANG-14). 4 fns; largest: SmCurveClassification::MergeInterval, SmCurveClassification::InsertCurveInterval, SmCurveClassification::Homogenize, SmCurveClassification::FixEndPointClass; cycle back-edges (call into later units; stub or co-port): topology.edge(5), topology.vertex(5), topology.face_p4(2), topology.brep_p3(1), topology.brep_p5(1), topology.brep_p9(1), topology.edgeuse_p1(1), topology.face_p2(1), topology.vertexuse(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit Merge of PR #65: AreCoincidentMaybe wired to p7; difftest now 4438 cases, 4097 passed, 0 failed, 341 deferred.. REVIEW FLAG (PR #85 review, 2026-10-16): this unit is partial while 52 deferred cases are its own unported MergeInterval imprinting (no UNPORTED marker, not a back-edge); finish that remainder (see findings intersect.curve_class_p2). fix/cc-topology-handles (2026-10-22): MergeInterval's vertex/edge/vertex fillet branch now calls p7 sm_SetDoFilletingGeometry (todo 7, status ignored) through merge_interval_with(.., other); merge_interval (no partner) keeps a GetOtherClassification deferral (intersect.curve_class_p7). The imprinting tail (SmCurveClass.cpp 17212-18276: end points, closed/degenerate test, MergePointClass vertex/edge imprinting, face splits) still defers: it needs a mutable B-rep context that ClassGeometry (shared ref) cannot give, the same gap as point_class merge_into_object vs merge_into_brep. fix/cc-topology-handles iter 2: the MergeInterval imprinting tail (SmCurveClass.cpp:17212-18276, ~1060 lines: MergePointClass, face split, edge imprint) needs a mutable classification context (ClassGeometry gives only &Brep); merge_into_brep is the mutable model for one point. Next porter: add a mutable adapter (topology_entity_mut) and thread it through merge_interval and its callers (boolean.topology_intersector_p3)." +notes = "2026-10-24 (claude/vertex-point, Mac): InsertCurveInterval runs through insert_curve_interval_mut (Combine moves the vertex, SmVertex::SetPoint, through MergeIntervalBrep); op 0 now also reports the picked vertex point on both sides. PASS intersect.curve_class_p6 cases=4466 passed=4466 failed=0 deferred=0 max_rel_err=0e0 with_values=4460 no_values=6 vacuous=0. MergeInterval imprinting ported (PR #721, 2026-10-24): the tail after GetPointObjZoneTol3d (unknown-end repair, closed/degenerate branch, wire edge, edge in face with face split, coincident edge) edits the B-rep through MergeIntervalBrep (merge_interval_mut, merge_point_class_mut; the read-only entry points defer at the first edit). Runner: a fresh box copy per case, the box compared after a merge, 28 imprint_* cases (picked box objects, lines on the box, editing enabled). 4466 cases, 4446 pass, 0 failed, 20 deferred (topology.vertex 20: SetPoint in InsertCurveInterval's combine, not MergeInterval). PR #599 (2026-10-23): the runner passes the partner classification (the one the shim's SetFilletData names) to merge_interval_with, so the 10 intersect.curve_class_p7 GetOtherClassification deferrals pass with values (142 each): 4438 cases, 4326 pass, 0 failed, 112 deferred (intersect.curve_class_p6 92 MergeInterval imprinting, topology.vertex 20 SetPoint). PR #296 (runner imports the shim's SmApiCreateBox B-rep via heal.stitch_p1 export): 4438 cases, 4316 pass, 0 fail, 122 deferred (intersect.curve_class_p6 92 MergeInterval imprinting, topology.vertex 20 SetPoint, intersect.curve_class_p7 10), --perturb 4316/4316. After intersect.curve_class_p2 (PR #85) wired GetPointObjZoneTol3d into MergeInterval (now takes k and geometry): 4438 cases, 4216 pass, 0 failed, 222 deferred (topology.edge 80, topology.face_p2 60, intersect.curve_class_p6 52 = MergeInterval imprinting not ported in this unit, topology.vertex 20, intersect.curve_class_p7 10). PR #138: partial; 4438 cases, 3933 passes (3927 bit-identical kernel comparisons and 6 expected FFI rejection checks), 0 failures, 505 deferred, no absolute floors. InsertCurveInterval, Homogenize and FixEndPointClass preserve upstream control flow with p2/p4/p7 dependency calls; MergeInterval implements early exits only and defers topology mutation at p2 GetPointObjZoneTol3d. Native fixtures use real 3D lines, circles, ellipses, quadratic and constant B-splines plus box topology. Nonempty homogenization/endpoint repair and topology mutation are not certified; generic topology tolerance lookups also retain existing adapter deferrals. Native regression tests run locally: 27440 isolated operation/geometry/flag/classification probes, 96 partition/SSS/boundary probes, 660 mismatched fillet-partner probes checking Rust and raw C++ guards independently, caller-point and projection regressions; project CI unchanged. Audit reuses p1 types and p3 helpers; p2/p4/p7 remain explicit dependencies, with no duplicate co-ports. Maintainer review: require homogenized partner counts for fillet merges (UB-136); reuse p3 boundary-location helper; ignore ordinary squeeze errors while retaining the no-progress guard (HANG-14). 4 fns; largest: SmCurveClassification::MergeInterval, SmCurveClassification::InsertCurveInterval, SmCurveClassification::Homogenize, SmCurveClassification::FixEndPointClass; cycle back-edges (call into later units; stub or co-port): topology.edge(5), topology.vertex(5), topology.face_p4(2), topology.brep_p3(1), topology.brep_p5(1), topology.brep_p9(1), topology.edgeuse_p1(1), topology.face_p2(1), topology.vertexuse(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit Merge of PR #65: AreCoincidentMaybe wired to p7; difftest now 4438 cases, 4097 passed, 0 failed, 341 deferred.. REVIEW FLAG (PR #85 review, 2026-10-16): this unit is partial while 52 deferred cases are its own unported MergeInterval imprinting (no UNPORTED marker, not a back-edge); finish that remainder (see findings intersect.curve_class_p2). fix/cc-topology-handles (2026-10-22): MergeInterval's vertex/edge/vertex fillet branch now calls p7 sm_SetDoFilletingGeometry (todo 7, status ignored) through merge_interval_with(.., other); merge_interval (no partner) keeps a GetOtherClassification deferral (intersect.curve_class_p7). The imprinting tail (SmCurveClass.cpp 17212-18276: end points, closed/degenerate test, MergePointClass vertex/edge imprinting, face splits) still defers: it needs a mutable B-rep context that ClassGeometry (shared ref) cannot give, the same gap as point_class merge_into_object vs merge_into_brep. fix/cc-topology-handles iter 2: the MergeInterval imprinting tail (SmCurveClass.cpp:17212-18276, ~1060 lines: MergePointClass, face split, edge imprint) needs a mutable classification context (ClassGeometry gives only &Brep); merge_into_brep is the mutable model for one point. Next porter: add a mutable adapter (topology_entity_mut) and thread it through merge_interval and its callers (boolean.topology_intersector_p3)." diff --git a/plan/units/topology.proto_topology_p2.toml b/plan/units/topology.proto_topology_p2.toml index 05f1156f8f..b961b0841a 100644 --- a/plan/units/topology.proto_topology_p2.toml +++ b/plan/units/topology.proto_topology_p2.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_analytic_tsurf_creation", "prog_test:my_test_get_loo status = "partial" claimed_by = "" claimed_at = "" -notes = "PARTIAL (PR #114): difftest 1533 cases (1500 generated proto scripts with ops 91-108 of the shared topology.proto_topology_p1 engine, 30 edge cases (proto edge life cycle, resolve of same / closed / circle proto edges, edge split updates, combine every other WR-95, vertex definitions, imprint error returns, find-or-create) at unit/1e4/1e-4 scale, 3 upstream-derived fixtures (SM_API_test TestSmBoolean2DPlanarUnion boundary crossings at 1, 1e3, 1e-3)), 1228 pass bit-identical (max_rel_err 0), 305 deferred (topology.edge 123, topology.vertex 61, topology.face_p2 60, topology.face_p1 41, topology.brep_p9 20; SmProtoVertex::ContainsPoint wired by query.face_p2, surface point solves by surface.surface_point_solve_p3); --perturb SENSITIVE. 18 fns ported in topology::proto_topology::p2; B-rep members through the new ProtoBrep trait (no UNPORTED stubs into partial units); UNPORTED stubs: topology.brep_p5 (SmBrep::MakeEdgeInFace), topology.face_p1 (SmFace::CalculateUVDomainFromUVTrimCurves), query.face_p2 (sm_ClassifyParamOnFaces), surface.surface_p5 (SmSurface::DropPoint), topology.edgeuse_p3 (SmEdgeuse::EvaluateBinormal). Upstream defects UB-115..UB-118, WR-95. No UNPORTED(topology.proto_topology_p2) back-edges existed. Plan notes: 18 fns; largest: SmProtoTopologyManager::UpdateFaces, SmProtoTopologyManager::UpdateEdges, SmEdgeDefinition::Imprint, SmProtoTopologyManager::CanCombineVertexDefinitions; cycle back-edges (call into later units; stub or co-port): query.face_p2(1); unit-SCC #22 (220 units); 112 upstream test entry points reach this unit [PR #144: can_combine_vertex_definitions takes the ProtoTopology walks (are_connected of topology.proto_topology_p1 wired); re-run: cases=1533 passed=1247 failed=0 deferred=286 (topology.brep_p9 20, topology.edge 105, topology.face_p1 41, topology.face_p2 60, topology.vertex 60), --perturb SENSITIVE] PR #183 integration rerun: 1533 cases, 1251 pass, 282 deferred, 0 failed, max_rel_err=0. Review rerun: 1533 cases, 1247 pass, 286 deferred, 0 failed, max_rel_err=0. Previous-head four bare-edge guard matches are superseded. Issue #210 continuation rerun: all 20 opaque edge/region merges defer to topology.brep_p1 mutable owning context; the old p9 and shell owner blockers are superseded. Other blockers remain edge 105, face_p1 41, face_p2 60, vertex 60. UPDATE (PR #204, topology.brep_p3): new required trait method ProtoBrep::brep_squeeze_edge(k: &mut Kernel, brep, edge, survivor) (SmBrep::SqueezeEdge, called by topology.proto_topology_p3's GlueCoincidentBrepVertices); implementors: the intersector adapter (calls topology::brep::p3::squeeze_edge), the proto_p1 runner's Pool and this module's unit-test Geo (both defer under harness:proto-*-brep-edit, having no B-rep); 1533 cases, 1247 pass, 0 failed, 286 deferred, case-by-case unchanged. Shim batch 2 (topology stand-ins are records of one B-rep in the runner (TopoRecords; ClassGeometry topology_entity/handle, vertex point, zone tol hooks), point_class FindObjPoint3d resolves vertex/edge (#285 hunk) and face handles through them (stacked on #295)): 1533 cases, 1462 pass, 71 deferred (topology.face_p1 45, topology.brep_p1 25, topology.vertex 1), --perturb SENSITIVE 1462/1462 (was 1247/286). UPDATE (real owning-face UpdateFaces): owner callbacks retain actual trim-domain partial outputs and mutable UV classification; sm_ClassifyParamOnFaces follows native box/tolerance/order semantics. Added 34 real trimmed-face cases, including both operands, boundary/overlap, 8 registered direct-UV edge-definition cases and 2 ordinary-error partial-domain cases. Existing 45 bare-face UpdateFaces scripts remain explicitly harness-deferred; surface DropPoint/proto-face/binormal continuations and general classification cache-provider plumbing are not claimed. Unit remains partial. Native pre-main-refresh: p1 1862 pass, p2 1496 pass/71 defer, p3 3013 pass; no failures." +notes = "2026-10-24 (claude/vertex-point, Mac): imprint_vertex_definition takes &mut Kernel and merges through ProtoBrep::merge_point_class_into_brep; the runner pool merges a vertex stand-in in its record (the 1 SmVertex::GetPoint deferral passes); edge and region stand-ins are joined to no B-rep and keep the 25 topology.brep_p1 deferrals. PARTIAL topology.proto_topology_p2 cases=1567 passed=1497 failed=0 deferred=70 max_rel_err=0e0 with_values=1497 no_values=0 vacuous=0 blocked_by=harness:proto-real-face-update(45),topology.brep_p1(25). PARTIAL (PR #114): difftest 1533 cases (1500 generated proto scripts with ops 91-108 of the shared topology.proto_topology_p1 engine, 30 edge cases (proto edge life cycle, resolve of same / closed / circle proto edges, edge split updates, combine every other WR-95, vertex definitions, imprint error returns, find-or-create) at unit/1e4/1e-4 scale, 3 upstream-derived fixtures (SM_API_test TestSmBoolean2DPlanarUnion boundary crossings at 1, 1e3, 1e-3)), 1228 pass bit-identical (max_rel_err 0), 305 deferred (topology.edge 123, topology.vertex 61, topology.face_p2 60, topology.face_p1 41, topology.brep_p9 20; SmProtoVertex::ContainsPoint wired by query.face_p2, surface point solves by surface.surface_point_solve_p3); --perturb SENSITIVE. 18 fns ported in topology::proto_topology::p2; B-rep members through the new ProtoBrep trait (no UNPORTED stubs into partial units); UNPORTED stubs: topology.brep_p5 (SmBrep::MakeEdgeInFace), topology.face_p1 (SmFace::CalculateUVDomainFromUVTrimCurves), query.face_p2 (sm_ClassifyParamOnFaces), surface.surface_p5 (SmSurface::DropPoint), topology.edgeuse_p3 (SmEdgeuse::EvaluateBinormal). Upstream defects UB-115..UB-118, WR-95. No UNPORTED(topology.proto_topology_p2) back-edges existed. Plan notes: 18 fns; largest: SmProtoTopologyManager::UpdateFaces, SmProtoTopologyManager::UpdateEdges, SmEdgeDefinition::Imprint, SmProtoTopologyManager::CanCombineVertexDefinitions; cycle back-edges (call into later units; stub or co-port): query.face_p2(1); unit-SCC #22 (220 units); 112 upstream test entry points reach this unit [PR #144: can_combine_vertex_definitions takes the ProtoTopology walks (are_connected of topology.proto_topology_p1 wired); re-run: cases=1533 passed=1247 failed=0 deferred=286 (topology.brep_p9 20, topology.edge 105, topology.face_p1 41, topology.face_p2 60, topology.vertex 60), --perturb SENSITIVE] PR #183 integration rerun: 1533 cases, 1251 pass, 282 deferred, 0 failed, max_rel_err=0. Review rerun: 1533 cases, 1247 pass, 286 deferred, 0 failed, max_rel_err=0. Previous-head four bare-edge guard matches are superseded. Issue #210 continuation rerun: all 20 opaque edge/region merges defer to topology.brep_p1 mutable owning context; the old p9 and shell owner blockers are superseded. Other blockers remain edge 105, face_p1 41, face_p2 60, vertex 60. UPDATE (PR #204, topology.brep_p3): new required trait method ProtoBrep::brep_squeeze_edge(k: &mut Kernel, brep, edge, survivor) (SmBrep::SqueezeEdge, called by topology.proto_topology_p3's GlueCoincidentBrepVertices); implementors: the intersector adapter (calls topology::brep::p3::squeeze_edge), the proto_p1 runner's Pool and this module's unit-test Geo (both defer under harness:proto-*-brep-edit, having no B-rep); 1533 cases, 1247 pass, 0 failed, 286 deferred, case-by-case unchanged. Shim batch 2 (topology stand-ins are records of one B-rep in the runner (TopoRecords; ClassGeometry topology_entity/handle, vertex point, zone tol hooks), point_class FindObjPoint3d resolves vertex/edge (#285 hunk) and face handles through them (stacked on #295)): 1533 cases, 1462 pass, 71 deferred (topology.face_p1 45, topology.brep_p1 25, topology.vertex 1), --perturb SENSITIVE 1462/1462 (was 1247/286). UPDATE (real owning-face UpdateFaces): owner callbacks retain actual trim-domain partial outputs and mutable UV classification; sm_ClassifyParamOnFaces follows native box/tolerance/order semantics. Added 34 real trimmed-face cases, including both operands, boundary/overlap, 8 registered direct-UV edge-definition cases and 2 ordinary-error partial-domain cases. Existing 45 bare-face UpdateFaces scripts remain explicitly harness-deferred; surface DropPoint/proto-face/binormal continuations and general classification cache-provider plumbing are not claimed. Unit remains partial. Native pre-main-refresh: p1 1862 pass, p2 1496 pass/71 defer, p3 3013 pass; no failures."