diff --git a/crates/difftest/src/units/boolean_merge_p3.rs b/crates/difftest/src/units/boolean_merge_p3.rs index 4c4dba22c9..f94d5a5eb2 100644 --- a/crates/difftest/src/units/boolean_merge_p3.rs +++ b/crates/difftest/src/units/boolean_merge_p3.rs @@ -8,27 +8,29 @@ //! context-only native `SmMerge` with the same explicit members and at most one //! operand: it compares the NER status, the restored manifold flag, the lamina mode //! the engine's intersector received and the null result (error-path coverage only). +//! Family 4 constructs the merge (`MergeAccess(context, a, b, 0.0, 0.0, options)`, as +//! the shim) and runs `PiecewiseMerge` with that engine's intersector as `m_vTI`: +//! `IntersectInsertRelate`, the mate clearing, subset, controls, SSI registration and +//! wire processing all act on the one engine that `ManifoldBoolean` then uses. use std::collections::BTreeSet; use crate::{Case, CaseContext, HarnessError, Output, Rng, UnitDiff}; use port::{BooleanDriver, PiecewiseAccess}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use solidcore::boolean::merge::p1::{MergeState, VertexCopy}; -use solidcore::boolean::merge::p2::{MergeOperand, TopologyCopy}; -use solidcore::boolean::merge::p4::{KernelManifoldIntersector, ManifoldState}; -use solidcore::boolean::merge::p5::process_wires_and_shell_vertices; +use solidcore::boolean::merge::p1::MergeState; +use solidcore::boolean::merge::p2::MergeOperand; +use solidcore::boolean::merge::p4::{ + BooleanKernels, KernelManifoldIntersector, ManifoldIntersector, ManifoldState, +}; use solidcore::boolean::merge::{p1::MergeOptions, p3 as port}; -use solidcore::boolean::topology_intersector::p1::{Relations, TopologyIntersector}; +use solidcore::boolean::topology_intersector::p1::TopologyIntersector; use solidcore::geom::a_object::BooleanOperation; use solidcore::geom::attribute::{ids, Attribute, AttributeStore, Behavior, ObjectKey}; use solidcore::geom::context::MarkLock; -use solidcore::geom::solution_array::{ObjectKind, SolutionObject}; -use solidcore::intersect::point_class::{ClassObject, ClassObjectKind}; use solidcore::kernel::Kernel; -use solidcore::topology::topology::Topology; -use solidcore::topology::topology_traverser::Marks; -use solidcore::topology::{brep::p1 as bp, Brep}; +use solidcore::topology::brep_cache::{arena_identity, CacheObject}; +use solidcore::topology::{brep::p1 as bp, Brep, FaceId}; use solidcore::SolidError; use solidcore_ffi::units::boolean_merge_p3 as ffi; @@ -90,7 +92,7 @@ fn encode(i: &Inputs) -> Vec { data.extend(i.tree.iter().map(|&v| v as f64)); data } -/// The deferral tag of `ManifoldBoolean` calls with no Rust-side engine (family 4). +/// The deferral tag of merge calls with no constructed Rust-side engine. const HARNESS_ENGINE_STATE: &str = "harness:merge-engine-state"; /// Family 6: `NonManifoldBoolean` on an adopted engine (see the module docs). const ENGINE_PROBE: u8 = 6; @@ -355,33 +357,10 @@ fn list_result(result: &port::BooleanListsResult, values: &mut Vec) -> soli Ok(()) } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum PoolRef { - Brep(usize), - Surface(usize, usize), -} -impl ClassObject for PoolRef { - fn class_kind(&self) -> ClassObjectKind { - match self { - Self::Surface(..) => ClassObjectKind::Surface, - Self::Brep(..) => ClassObjectKind::Other, - } - } -} -impl SolutionObject for PoolRef { - fn kind(&self) -> ObjectKind { - match self { - Self::Surface(..) => ObjectKind::Surface, - Self::Brep(..) => ObjectKind::Other, - } - } -} - struct Pool { breps: Vec, original_count: usize, retired: BTreeSet, - intersector: TopologyIntersector, attributes: AttributeStore, primary: Option, other: Option, @@ -421,9 +400,7 @@ impl Pool { .as_mut() { if surface.owner().is_some() { - surface.set_owner(Some(solidcore::topology::brep_cache::arena_identity( - solidcore::topology::brep_cache::CacheObject::Face(face), - ))); + surface.set_owner(Some(arena_identity(CacheObject::Face(face)))); } } // a new owner key drops the cached face tolerance: record it again @@ -436,12 +413,6 @@ impl Pool { breps, original_count: i.a.len() + i.b.len(), retired: BTreeSet::new(), - intersector: TopologyIntersector::new( - Some(PoolRef::Brep(0)), - Some(PoolRef::Brep(i.a.len())), - 0.0, - 0.0, - ), attributes: AttributeStore::new(), primary: Some(0), other: Some(i.a.len()), @@ -501,6 +472,66 @@ impl Pool { })?, )) } + /// The constructed merge (`boolean.merge_p2`), whose intersector is `m_vTI`. + fn engine_mut(&mut self) -> solidcore::Result<&mut port::MergeEngine> { + self.engine.as_mut().ok_or_else(|| { + SolidError::unported( + "SmMerge::PiecewiseMerge (pool engine state)", + HARNESS_ENGINE_STATE, + ) + }) + } + /// Run `action` on the merge and the current operands, moved out of their slots + /// and back (the engine and both B-reps return on every exit). + fn with_engine_pair( + &mut self, + action: impl FnOnce(&mut port::MergeEngine, &mut Brep, &mut Brep) -> solidcore::Result, + ) -> solidcore::Result { + let (Some(p), Some(o)) = (self.primary, self.other) else { + return Err(SolidError::invalid_input( + "harness::merge_p3", + "the merge needs both operands", + )); + }; + if p == o { + return Err(SolidError::invalid_input( + "harness::merge_p3", + "a merge of one B-rep with itself", + )); + } + self.engine_mut()?; + let mut a = self.take_brep(p)?; + let mut b = match self.take_brep(o) { + Ok(b) => b, + Err(error) => { + self.restore_brep(Some(p), Some(a)); + return Err(error); + } + }; + let result = match self.engine.as_mut() { + Some(engine) => action(engine, &mut a, &mut b), + None => Err(SolidError::null_pointer("harness::merge_p3")), + }; + self.restore_brep(Some(p), Some(a)); + self.restore_brep(Some(o), Some(b)); + result + } + /// The face of a `GetSurfaces` handle: the `index`-th face owning its surface. + fn surface_face(&self, (id, index): (usize, usize)) -> solidcore::Result { + let brep = self.brep(id)?; + let mut owners = Vec::new(); + for face in bp::faces(brep, None)? { + let owned = brep.face(face)?.surface.as_ref().is_some_and(|surface| { + surface.owner() == Some(arena_identity(CacheObject::Face(face))) + }); + if owned { + owners.push(face); + } + } + owners.get(index).copied().ok_or_else(|| { + SolidError::invalid_input("harness::merge_p3", "bad surface handle") + }) + } fn record(&self, id: usize, values: &mut Vec) -> solidcore::Result<()> { let brep = self.brep(id)?; let identity = if id < self.original_count { @@ -541,10 +572,6 @@ impl BooleanDriver for Pool { angle_tol, options, )?; - self.intersector - .set_this_approx_tol_3d(merge.intersector.this_approx_tol_3d()); - self.intersector - .set_this_ang_tol_rad(merge.intersector.this_ang_tol_rad()); self.manifold = merge.manifold_boolean.unwrap_or(self.manifold); self.engine = Some(port::MergeEngine::from_merge(&merge)?); Ok(()) @@ -562,8 +589,8 @@ impl BooleanDriver for Pool { // ManifoldBoolean (boolean.merge_p4) on the engine `construct` built, run by // port::DependencyMerge on the pool's B-reps, moved in and back. let Some(engine) = self.engine.take() else { - // Family 4 runs PiecewiseMerge on the native SmMerge(c, a, b, ...) the shim - // built, but no Rust engine holds the pool's piecewise intersector state. + // Every family constructs the merge before it runs: no engine means no + // SmMerge object was built on the Rust side. return Err(SolidError::unported( "SmMerge::ManifoldBoolean (pool engine state)", HARNESS_ENGINE_STATE, @@ -611,8 +638,6 @@ impl PiecewiseAccess for Pool { fn set_operands(&mut self, a: Option, b: Option) { self.primary = a; self.other = b; - self.intersector.set_primary_brep(a.map(PoolRef::Brep)); - self.intersector.set_other_brep(b.map(PoolRef::Brep)); } fn surfaces(&self, id: usize) -> solidcore::Result> { let brep = self.brep(id)?; @@ -622,22 +647,36 @@ impl PiecewiseAccess for Pool { .map(|(s, _)| (id, s)) .collect()) } - fn intersect_insert_relate(&mut self, _k: &mut Kernel) -> solidcore::Result<()> { - // UNPORTED(boolean.topology_intersector_p2): SmTopologyIntersector::IntersectInsertRelate - Err(SolidError::unported( - "SmTopologyIntersector::IntersectInsertRelate", - "boolean.topology_intersector_p2", - )) + fn intersect_insert_relate(&mut self, k: &mut Kernel) -> solidcore::Result<()> { + // m_vTI.IntersectInsertRelate() on the constructed merge's intersector + self.with_engine_pair(|engine, a, b| { + engine.intersector.intersect_insert_relate( + &mut BooleanKernels { + primary: k, + other: None, + }, + a, + b, + &mut engine.attributes, + ) + }) } fn clear_relationships(&mut self) -> solidcore::Result<()> { - self.intersector.clear_relationships() + self.engine_mut()?.intersector.clear_relationships() } fn set_subset(&mut self, surfaces: &[Self::Surface]) { - let handles: Vec<_> = surfaces + // SetSubset by surface identity (the canonical B-rep cache's convention). The + // handles come from GetSurfaces of live B-reps, so each resolves. + let identities: Vec<_> = surfaces .iter() - .map(|&(b, s)| PoolRef::Surface(b, s)) + .filter_map(|&surface| { + let face = self.surface_face(surface).ok()?; + KernelManifoldIntersector::surface_identity(self.brep(surface.0).ok()?, face).ok() + }) .collect(); - self.intersector.set_subset(&handles); + if let Some(engine) = self.engine.as_mut() { + engine.intersector.set_surface_subset(&identities); + } } fn surface_map( &mut self, @@ -701,108 +740,42 @@ impl PiecewiseAccess for Pool { } fn set_controls(&mut self, manifold: bool, wires: bool, lamina: u64) { self.manifold = manifold; - self.intersector.intersect_wire_edges = wires; - self.intersector.intersect_lamina_edges = lamina; + if let Some(engine) = self.engine.as_mut() { + engine.intersector.set_intersect_wire_edges(wires); + engine.intersector.set_intersect_lamina_edges(lamina); + } } fn process_wires(&mut self, k: &mut Kernel, lock: &MarkLock) -> solidcore::Result<()> { - // ProcessWiresAndShellVertices(SM_BO_MERGE, keep, mark1, mark2) (merge_p5) with - // merge_p2's copy environment. The pool's intersector names only B-reps and - // surfaces, so its topology mates are empty, and the pool marks no topology: - // empty Relations and Marks are its actual state, not a second mate map. - let (Some(p), Some(o)) = (self.primary, self.other) else { - return Err(SolidError::invalid_input( - "harness::merge_p3", - "wire processing without both operands", - )); - }; - self.brep(p)?; - self.brep(o)?; - if p == o { - return Err(SolidError::invalid_input( - "harness::merge_p3", - "wire processing of one B-rep with itself", - )); - } - let (low, high) = (p.min(o), p.max(o)); - let (head, tail) = self.breps.split_at_mut(high); - let (lower, upper) = ( - head.get_mut(low) - .ok_or(SolidError::null_pointer("harness::merge_p3"))?, - tail.first_mut() - .ok_or(SolidError::null_pointer("harness::merge_p3"))?, - ); - let (primary, other) = if p < o { - (lower, upper) - } else { - (upper, lower) - }; - let mut records = std::collections::BTreeMap::new(); - for (base, brep) in [(0u64, &*primary), (1_000_000, &*other)] { - for v in brep.vertices().ids() { - records.insert(ObjectKey(base + u64::from(v.index())), Topology::new()); - } - for e in brep.edges().ids() { - records.insert( - ObjectKey(2_000_000 + base + u64::from(e.index())), - Topology::new(), - ); - } - } - let object = |key: u64, type_code: i64| solidcore::geom::object::ObjectRef { - key: ObjectKey(key), - type_code, - }; - let mut copy = TopologyCopy { - vertices: VertexCopy { - records: &mut records, - attributes: &mut self.attributes, - source_key: &|v| object(1_000_000 + u64::from(v.index()), 16021), - destination_key: &|e| match e { - solidcore::EntityRef::Vertex(v) => object(u64::from(v.index()), 16021), - solidcore::EntityRef::Vertexuse(v) => { - object(6_000_000 + u64::from(v.index()), 16006) - } - _ => object(9_000_000, 16001), + // ProcessWiresAndShellVertices(SM_BO_MERGE, keep, mark1, mark2) (merge_p5) on the + // merge's own mates, state and attributes. + self.with_engine_pair(|engine, a, b| { + let mut keep = Vec::new(); // local and discarded, as upstream + engine.intersector.process_wires( + &mut BooleanKernels { + primary: k, + other: None, }, - }, - edge_source_key: &|e| object(3_000_000 + u64::from(e.index()), 16016), - edge_destination_key: &|e| object(2_000_000 + u64::from(e.index()), 16016), - face_source_key: &|f| object(5_000_000 + u64::from(f.index()), 16002), - face_destination_key: &|f| object(4_000_000 + u64::from(f.index()), 16002), - }; - let mut keep = Vec::new(); // local and discarded, as upstream - process_wires_and_shell_vertices( - k, - primary, - other, - &mut Marks::default(), - &mut Marks::default(), - &mut Relations::default(), - &mut copy, - None, - BooleanOperation::Merge, - lock.mark_type1(), - lock.mark_type2(), - &mut keep, - ) + a, + b, + &mut engine.state, + &mut engine.attributes, + BooleanOperation::Merge, + lock.mark_type1(), + lock.mark_type2(), + &mut keep, + ) + }) } fn release_marks(&mut self, k: &mut Kernel, lock: &mut MarkLock) -> solidcore::Result<()> { lock.release(std::slice::from_mut(&mut k.context)) } fn register_empty_ssi(&mut self, a: Self::Surface, b: Self::Surface) -> solidcore::Result<()> { - register_ssi(&mut self.intersector, a, b, &[], &[], &[], &[], &[]) + let (a, b) = (self.surface_face(a)?, self.surface_face(b)?); + self.engine_mut()?.intersector.register_empty_ssi(a, b); + Ok(()) } fn clear_existing_ssi(&mut self) -> solidcore::Result<()> { - // The fixture adapter only registers empty curve arrays. No curve handles - // can be created here until RegisterExistingSSI is wired above. - let deleted = self.intersector.clear_existing_ssi(); - if !deleted.is_empty() { - // UNPORTED(boolean.topology_intersector_p2): SmTopologyIntersector::RegisterExistingSSI (curve storage) - return Err(SolidError::unported( - "SmTopologyIntersector::RegisterExistingSSI (curve storage)", - "boolean.topology_intersector_p2", - )); - } + self.engine_mut()?.intersector.clear_existing_ssi(); Ok(()) } fn notify_pre_edit(&mut self, k: &mut Kernel, a: usize) -> solidcore::Result<()> { @@ -881,24 +854,6 @@ fn set_tolerance( } } -#[allow(clippy::too_many_arguments)] // all five native SSI arrays remain explicit -fn register_ssi( - _intersector: &mut TopologyIntersector, - _a: (usize, usize), - _b: (usize, usize), - _curves_3d: &[PoolRef], - _uv1: &[PoolRef], - _uv2: &[PoolRef], - _orientations: &[solidcore::surface::surface::p2::TsectCurveType], - _deviations: &[f64], -) -> solidcore::Result<()> { - // UNPORTED(boolean.topology_intersector_p2): SmTopologyIntersector::RegisterExistingSSI - Err(SolidError::unported( - "SmTopologyIntersector::RegisterExistingSSI", - "boolean.topology_intersector_p2", - )) -} - /// `SmBrep::Notify(op, caller, data1, data2)` (`topology::brep::p2::notify`). The fixture /// pool carries no attribute records, so the notification runs on an empty store over /// the B-rep's regions, as `heal.stitch_p1`'s `PRE_EDIT`; no Rust caller holds the @@ -998,21 +953,24 @@ fn rust(i: &Inputs) -> Result { }) .and_then(|result| single_result(&pool, result, manifold, lamina, &mut values)) } - 4 => port::piecewise_merge( - &mut k, - &mut pool, - None, - i.options.unwrap_or(0) & 1 != 0, - i.options.unwrap_or(0) & 2 != 0, - ) - .and_then(|result| { - single_result( - &pool, - result, - pool.manifold, - pool.intersector.intersect_lamina_edges, - &mut values, - ) + // MergeAccess merge(context, a, b, 0.0, 0.0, options), then PiecewiseMerge + 4 => pool + .construct(&mut k, &mut Some(0), &mut Some(1), 0.0, 0.0, opts.as_ref()) + .and_then(|()| { + port::piecewise_merge( + &mut k, + &mut pool, + None, + i.options.unwrap_or(0) & 1 != 0, + i.options.unwrap_or(0) & 2 != 0, + ) + }) + .and_then(|result| { + let lamina = pool + .engine + .as_ref() + .map_or(0, |engine| engine.intersector.intersect_lamina_edges()); + single_result(&pool, result, pool.manifold, lamina, &mut values) }), _ => return Err(harness("fixture-only family")), }; @@ -1254,13 +1212,19 @@ mod tests { #[test] fn piecewise_adapter_uses_canonical_subset_attributes_controls_and_marks() { + fn engine(pool: &Pool) -> &KernelManifoldIntersector { + &pool.engine.as_ref().unwrap().intersector + } let mut pool = Pool::load(&inputs()).unwrap(); + let mut k = Kernel::default(); + pool.construct(&mut k, &mut Some(0), &mut Some(1), 0.0, 0.0, None) + .unwrap(); assert_eq!(pool.surfaces(0).unwrap().len(), 6); assert!(pool.surfaces(1).unwrap().is_empty()); pool.set_subset(&[(0, 0), (0, 0)]); - assert_eq!(pool.intersector.subset().unwrap().len(), 1); + assert_eq!(engine(&pool).surface_subset().unwrap().len(), 1); pool.set_subset(&[]); - assert!(pool.intersector.subset().is_none()); + assert!(engine(&pool).surface_subset().is_none()); pool.surface_map((0, 0), false).unwrap(); assert_eq!(pool.surface_map_users((0, 0)).unwrap(), Some(vec![(0, 0)])); let key = pool.surface_key((0, 0)).unwrap(); @@ -1269,11 +1233,10 @@ mod tests { pool.surface_map((0, 0), false).unwrap(); assert_eq!(pool.attributes.attributes(key).len(), 2); pool.set_controls(false, false, 3); - assert!(!pool.manifold_flag() && !pool.intersector.intersect_wire_edges); - assert_eq!(pool.intersector.intersect_lamina_edges, 3); + assert!(!pool.manifold_flag() && !engine(&pool).intersect_wire_edges()); + assert_eq!(engine(&pool).intersect_lamina_edges(), 3); pool.clear_relationships().unwrap(); pool.clear_existing_ssi().unwrap(); - let mut k = Kernel::default(); let mut lock = MarkLock::new( std::slice::from_mut(&mut k.context), 0, @@ -1293,6 +1256,16 @@ mod tests { fn piecewise_dependency_errors_name_the_next_missing_owner() { let mut pool = Pool::load(&inputs()).unwrap(); let mut k = Kernel::default(); + // without SmMerge's constructor the pool holds no m_vTI + assert!(matches!( + pool.clear_relationships(), + Err(SolidError::Unported { + unit: HARNESS_ENGINE_STATE, + .. + }) + )); + pool.construct(&mut k, &mut Some(0), &mut Some(1), 0.0, 0.0, None) + .unwrap(); // the face B-rep is built (SmBrep constructor, topology.brep_p3; // CreateFaceFromSurface, topology.brep_p8) let r = pool.new_face_brep(&mut k, 0); @@ -1300,13 +1273,9 @@ mod tests { // SmBrep::Notify is topology.brep_p2's (#149): PRE_EDIT of the fixture B-rep runs let r = pool.notify_pre_edit(&mut k, 0); assert!(r.is_ok(), "notify_pre_edit: {r:?}"); - assert!(matches!( - pool.register_empty_ssi((0, 0), (0, 1)), - Err(SolidError::Unported { - unit: "boolean.topology_intersector_p2", - .. - }) - )); + // RegisterExistingSSI of empty arrays on the merge's own intersector + let r = pool.register_empty_ssi((0, 0), (0, 1)); + assert!(r.is_ok(), "register_empty_ssi: {r:?}"); // SmMerge's constructor and copy environment are boolean.merge_p2's (#229) and // SmBrep::Point3DClassify is query.brep's: ProcessWiresAndShellVertices runs to // completion on the fixture diff --git a/crates/difftest/src/units/primitive_primitive_creation_p1.rs b/crates/difftest/src/units/primitive_primitive_creation_p1.rs index 6568379d91..d38a766a8d 100644 --- a/crates/difftest/src/units/primitive_primitive_creation_p1.rs +++ b/crates/difftest/src/units/primitive_primitive_creation_p1.rs @@ -97,10 +97,16 @@ fn maps_record(maps: &EntityMaps, out: &mut Vec) { } } } -fn boolean_face(brep: &mut Brep, k: &Kernel, data: &[f64]) -> solidcore::Result<()> { +/// `CreateFaceFromSurface(surface, domain, face)` on the native fixture's canonical +/// plane (its STEP UV domain adjusted first) or unit sphere, as the shim builds it. +fn boolean_face( + brep: &mut Brep, + k: &mut Kernel, + data: &[f64], + notifications: &mut solidcore::topology::edge::NotifyEnv<'_>, +) -> solidcore::Result<()> { use solidcore::geom::extent2d::Extent2d; use solidcore::surface::{plane::p1::Plane, sphere::Sphere, surface::Surface}; - use solidcore::topology::{Face, ShellId}; let angle = value(data, 1).to_radians(); let frame = Axis2Placement::new( Vec3::new(0.0, 0.0, value(data, 2)), @@ -121,10 +127,20 @@ fn boolean_face(brep: &mut Brep, k: &Kernel, data: &[f64]) -> solidcore::Result< // IsPlanar uses the revolution base; Surface has no sphere variant yet. Surface::from(Sphere::create_canonical(k, &frame, 1.0)?.base().clone()) }; - let mut face = Face::new(ShellId::from_index(0)); - face.surface = Some(surface); - face.uv_domain = domain; - brep.add_face_record(face)?; + let mut marks = solidcore::topology::topology_traverser::Marks::default(); + let this_face = |f| topology_key(EntityRef::Face(f)); + let mut fenv = solidcore::topology::brep::p8::FaceBuildEnv { + env: notifications, + marks: &mut marks, + this_face: &this_face, + }; + solidcore::topology::brep::p8::create_face_from_surface( + k, + brep, + Some(surface), + &domain, + &mut fenv, + )?; Ok(()) } @@ -461,15 +477,25 @@ fn rust(i: &Inputs) -> solidcore::Result> { } 6 => { - let mut a = Brep::new(); - let mut b = Brep::new(); + // SmBrep(SM_USE_DEFAULT, SM_USE_DEFAULT, context), with its infinite region + let mut a = solidcore::topology::brep::p3::new_brep( + &mut k, + solidcore::geom::tol::USE_DEFAULT, + solidcore::geom::tol::USE_DEFAULT, + )?; + let mut b = solidcore::topology::brep::p3::new_brep( + &mut k, + solidcore::geom::tol::USE_DEFAULT, + solidcore::geom::tol::USE_DEFAULT, + )?; let split = 7 + 7 * value(v, 4) as usize; for face in v.get(7..split).unwrap_or_default().as_chunks::<7>().0 { - boolean_face(&mut a, &k, face)?; + boolean_face(&mut a, &mut k, face, &mut notifications)?; } for face in v.get(split..).unwrap_or_default().as_chunks::<7>().0 { - boolean_face(&mut b, &k, face)?; + boolean_face(&mut b, &mut k, face, &mut notifications)?; } + let had_faces = !solidcore::topology::brep::p1::faces(&a, None)?.is_empty(); a.set_tolerance(value(v, 6)); b.set_tolerance(value(v, 6)); a.set_editing_enabled(value(v, 2) != 0.0); @@ -500,12 +526,32 @@ fn rust(i: &Inputs) -> solidcore::Result> { Err(solidcore::SolidError::Upstream { code, .. }) => code, Err(_) => solidcore::error::SM_ERR, }; + // On success Boolean2D returns A (here moved into the output slot) and + // deletes B; on error or empty input both survive. + let consumed = status == 0 + && had_faces + && solidcore::topology::brep::p1::faces(&a, None)?.is_empty(); record.extend([ f64::from(status), f64::from(a.editing_enabled()), - f64::from(b.editing_enabled()), + if consumed { + -1.0 + } else { + f64::from(b.editing_enabled()) + }, f64::from(output.is_some()), ]); + if consumed { + let result = output + .as_ref() + .ok_or_else(|| solidcore::SolidError::null_pointer("difftest::primitive_p1"))?; + record.extend([ + solidcore::topology::brep::p1::faces(result, None)?.len() as f64, + solidcore::topology::brep::p1::edges(result, None)?.len() as f64, + solidcore::topology::brep::p1::vertices(result, None)?.len() as f64, + ]); + crate::util::primitive::append(&k, result, &mut record)?; + } } _ => unreachable!(), @@ -522,6 +568,14 @@ fn output(input: &Inputs, data: Vec) -> Result { Ok(Output::new() .ints("maps", head.iter().map(|v| *v as i64).collect::>()) .values("geometry", data.get(prefix..).unwrap_or_default().to_vec())) + } else if op == 6 && data.len() > 4 { + // A successful Boolean2D: the statuses, the result's counts, then its geometry. + let head = data + .get(..7) + .ok_or_else(|| HarnessError::new("missing Boolean2D result record"))?; + Ok(Output::new() + .ints("maps", head.iter().map(|v| *v as i64).collect::>()) + .values("geometry", data.get(7..).unwrap_or_default().to_vec())) } else if op == 0 || op == 5 || op == 6 { Ok(Output::new().ints( "maps", diff --git a/crates/solidcore/src/boolean/merge/p3.rs b/crates/solidcore/src/boolean/merge/p3.rs index 5ad8453331..09e2d27e66 100644 --- a/crates/solidcore/src/boolean/merge/p3.rs +++ b/crates/solidcore/src/boolean/merge/p3.rs @@ -1104,16 +1104,27 @@ mod tests { operation, Some(&mut state.env(None)), ); - assert!( - matches!( - result, - Err(SolidError::Unported { - upstream: "SmTopologyIntersector::IntersectInsertRelate", - unit: "boolean.topology_intersector_p2", - }) - ), - "{operation:?}: {result:?}" + // IntersectInsertRelate runs (the edge-less fixture faces relate nothing); + // copying an unrelated B face then needs its face use, which a bare face + // record lacks, so Union, ExclusiveOr and Merge stop there + let copies = matches!( + operation, + BooleanOperation::Union | BooleanOperation::ExclusiveOr | BooleanOperation::Merge ); + if copies { + assert!( + matches!( + result, + Err(SolidError::InvalidInput { + operation: "topology::face::p2", + .. + }) + ), + "{operation:?}: {result:?}" + ); + } else { + assert!(result.is_ok(), "{operation:?}: {result:?}"); + } assert!(kernel.context.doing_boolean()); } let mut kernel = Kernel::default(); diff --git a/crates/solidcore/src/boolean/merge/p4/kernel_adapter.rs b/crates/solidcore/src/boolean/merge/p4/kernel_adapter.rs index 7ea56a0d5e..ce730ab088 100644 --- a/crates/solidcore/src/boolean/merge/p4/kernel_adapter.rs +++ b/crates/solidcore/src/boolean/merge/p4/kernel_adapter.rs @@ -329,8 +329,9 @@ impl<'keys> ManifoldKernelAdapter<'keys> { Ok(()) } - /// Clear the two canonical mate maps between piecewise stages. - pub(crate) fn clear_relationships(&mut self) -> Result<()> { + /// Clear the two canonical mate maps between piecewise stages + /// (`m_pBToO->RemoveAll(); m_pOToB->RemoveAll()`). + pub fn clear_relationships(&mut self) -> Result<()> { self.merge.intersector.with_intersector( &mut self.intersector, arena_pair::encode, @@ -340,12 +341,12 @@ impl<'keys> ManifoldKernelAdapter<'keys> { } /// `m_vTI.m_bIntersectWireEdges`. - pub(crate) fn set_intersect_wire_edges(&mut self, enabled: bool) { + pub fn set_intersect_wire_edges(&mut self, enabled: bool) { self.intersector.intersect_wire_edges = enabled; } /// The empty-array `RegisterExistingSSI` call used by `PiecewiseMerge`. - pub(crate) fn register_empty_ssi(&mut self, primary: FaceId, other: FaceId) { + pub fn register_empty_ssi(&mut self, primary: FaceId, other: FaceId) { self.intersector.register_empty_ssi( Some(arena_pair::surface_handle(false, primary)), Some(arena_pair::surface_handle(true, other)), @@ -353,7 +354,7 @@ impl<'keys> ManifoldKernelAdapter<'keys> { } /// Clear the piecewise stage's registered surface pairs. - pub(crate) fn clear_existing_ssi(&mut self) { + pub fn clear_existing_ssi(&mut self) { let _ = self.intersector.clear_existing_ssi(); } @@ -538,6 +539,18 @@ impl<'keys> ManifoldKernelAdapter<'keys> { self.intersector.intersect_lamina_edges } + /// `m_vTI.m_bIntersectWireEdges`. + #[must_use] + pub fn intersect_wire_edges(&self) -> bool { + self.intersector.intersect_wire_edges + } + + /// The partial-merge surface subset ([`Self::set_surface_subset`]), if any. + #[must_use] + pub fn surface_subset(&self) -> Option<&BTreeSet> { + self.subset.as_ref() + } + /// Run the orchestration with the options held in the canonical `MergeState`. pub fn manifold_boolean( &mut self, diff --git a/crates/solidcore/src/primitive/primitive_creation/p1/boolean.rs b/crates/solidcore/src/primitive/primitive_creation/p1/boolean.rs index de9190dd25..5171978d13 100644 --- a/crates/solidcore/src/primitive/primitive_creation/p1/boolean.rs +++ b/crates/solidcore/src/primitive/primitive_creation/p1/boolean.rs @@ -4,64 +4,32 @@ //! //! | Rust | Upstream | //! |------|----------| -//! | `finish` (private) | `SmPrimitiveCreation::Boolean2D`, after `IntersectInsertRelate` | +//! | `finish` (private) | `SmPrimitiveCreation::Boolean2D`, from `IntersectInsertRelate` on | //! //! Deviations: marks are caller-owned records, separately for each arena. Both //! operands use the passed Kernel. On success the result owns the primary arena; -//! the two consumed input slots become empty arenas. No recursion. +//! the two consumed input slots become empty arenas. The local `SmTopologyIntersector` +//! is merge p4's [`KernelManifoldIntersector`] over the two arenas, notifying under +//! the caller's `key_of` for both operands (the identities are unique within one +//! B-rep, as the arena notification scheme's are), a split edge's new curve under its +//! edge's identity with the curve's type code. No recursion. use super::primitives::{detailed_face, stitch_faces, Boolean2d}; -use crate::boolean::topology_intersector::MateMap; -use crate::error::{ignore_status, Result, SolidError}; -use crate::geom::context::MarkLock; +use crate::boolean::merge::p4::{ + BooleanKernels, KernelManifoldIntersector, ManifoldIntersector, ManifoldKernelAdapter, + ManifoldNotificationKeys, +}; +use crate::error::{ignore_status, EntityRef, Result, SolidError}; +use crate::geom::attribute::AttributeStore; +use crate::geom::context::{Context, MarkLock}; +use crate::geom::object::ObjectRef; use crate::kernel::Kernel; use crate::nurbs::b_spline_surface_n_lib::p2::OrientType; use crate::topology::edge; use crate::topology::edgeuse::p1 as eu; use crate::topology::topology_traverser::{Entity, Marks}; -use crate::topology::{Brep, EdgeId, FaceId, VertexId}; +use crate::topology::{Brep, EdgeId, FaceId}; -#[derive(Default)] -struct PlanarIntersections { - edges: Vec<(EdgeId, EdgeId)>, - vertices: Vec<(VertexId, VertexId)>, - faces: MateMap, -} -fn intersect_insert_relate( - _k: &mut Kernel, - _a: &mut Brep, - _b: &mut Brep, - _approx_tolerance: f64, - _angle_radians: f64, -) -> Result { - // UNPORTED(boolean.topology_intersector_p2): SmTopologyIntersector::IntersectInsertRelate - // merge::p4::arena_pair::ArenaPair adapts two Brep arenas, but its handles - // and notification records use an operand-qualified namespace. This entry - // has only the primary NotifyEnv's key_of resolver, not a mapping for both - // operands. Reusing it requires retaining its intersector/relations through - // DeleteFaces and translating common pairs in native order; bare FaceIds - // in PlanarIntersections cannot stand in for that identity/session state. - Err(SolidError::unported( - "SmTopologyIntersector::IntersectInsertRelate", - "boolean.topology_intersector_p2", - )) -} -fn delete_faces( - _k: &Kernel, - _a: &mut Brep, - _intersections: &mut PlanarIntersections, - _faces: &[FaceId], -) -> Result<()> { - // UNPORTED(boolean.topology_intersector_p2): SmTopologyIntersector::DeleteFaces - // ArenaPair's DeleteFaces must reuse the intersector/relations from the - // intersection stage and the same operand-qualified notification mapping. - // PlanarIntersections does not yet retain that session; a fresh adapter or - // a primary-only key_of resolver would lose the corresponding mate state. - Err(SolidError::unported( - "SmTopologyIntersector::DeleteFaces", - "boolean.topology_intersector_p2", - )) -} fn remove_topology( k: &mut Kernel, a: &mut Brep, @@ -86,38 +54,109 @@ pub(super) fn finish( marks: (&mut Marks, &mut Marks), notifications: &mut edge::NotifyEnv<'_>, ) -> Result<()> { + // SmTopologyIntersector sTI(*pContext, pBrepA, pBrepB, + // SM_ZONE_TO_APPROXTOL3D(dZoneTol3d), 20.0*SM_PI/180.0) let tolerance = crate::math::max2(a.tolerance(), b.tolerance()); - // Preserve the constructor's explicit tolerances when wiring the pending - // edit adapter. Its output retains native common-edge/vertex visit order. - let mut intersections = intersect_insert_relate( - k, + let key_of = notifications.key_of; + let curve = |e: EdgeId, kind: i64| { + Ok(ObjectRef { + type_code: kind, + ..key_of(EntityRef::Edge(e)) + }) + }; + let no_sources = |_| { + Err(SolidError::invalid_input( + OP, + "Boolean2D propagates no region sources", + )) + }; + let keys = ManifoldNotificationKeys { + primary: key_of, + other: key_of, + primary_curve: &curve, + other_curve: &curve, + source_lists: &no_sources, + }; + let attributes = &mut *notifications.attributes; + let context = &mut notifications.context; + KernelManifoldIntersector::with_notifications(keys, notifications.records, |ti| { + ti.set_intersection_tolerances(tolerance / 2.0, 20.0 * crate::math::PI / 180.0); + let session = Session { + ti, + attributes, + context, + key_of, + }; + finish_with(k, a, b, first, operation, result, marks, session) + }) +} + +const OP: &str = "primitive::boolean_2d"; + +/// The local `SmTopologyIntersector` and the caller's notification state. +struct Session<'s, 'k, 'c> { + ti: &'s mut ManifoldKernelAdapter<'k>, + attributes: &'s mut AttributeStore, + context: &'s mut Option<&'c mut Context>, + key_of: &'s dyn Fn(EntityRef) -> ObjectRef, +} + +impl Session<'_, '_, '_> { + fn env(&mut self) -> edge::NotifyEnv<'_> { + edge::NotifyEnv { + records: &mut self.ti.records, + attributes: &mut *self.attributes, + context: self.context.as_deref_mut(), + key_of: self.key_of, + } + } +} + +#[allow(clippy::too_many_arguments, clippy::too_many_lines)] +fn finish_with( + k: &mut Kernel, + a: &mut Brep, + b: &mut Brep, + first: FaceId, + operation: Boolean2d, + result: &mut Option, + marks: (&mut Marks, &mut Marks), + mut session: Session<'_, '_, '_>, +) -> Result<()> { + // intersect, merge, and relate the intersection geometry between the two topology graphs + session.ti.intersect_insert_relate( + &mut BooleanKernels { + primary: k, + other: None, + }, a, b, - tolerance / 2.0, - 20.0 * core::f64::consts::PI / 180.0, + session.attributes, )?; let mut lock = MarkLock::new_pair(core::slice::from_mut(&mut k.context), 0, 0)?; let mark_a = lock.mark_type1(); let mark_b = lock.mark_type2(); let (marks_a, marks_b) = marks; let outcome = (|| { - for (primary, other) in &intersections.edges { + // get coincident edges, then coincident vertices + let (edges, other_edges) = session.ti.common_edges(a, b)?; + for (primary, other) in edges.iter().zip(&other_edges) { marks_a.mark(k, Entity::Edge(*primary), mark_a); marks_b.mark(k, Entity::Edge(*other), mark_b); } - for (primary, other) in &intersections.vertices { + let (vertices, other_vertices) = session.ti.common_vertices(a, b)?; + for (primary, other) in vertices.iter().zip(&other_vertices) { marks_a.mark(k, Entity::Vertex(*primary), mark_a); marks_b.mark(k, Entity::Vertex(*other), mark_b); } // The primary side propagates a failed radial-sector status; the other // side skips it. Mark each face BEFORE the radial query, as upstream. - for (primary, other) in intersections.edges.clone() { + for (&primary, &other) in edges.iter().zip(&other_edges) { for edgeuse in edge::edgeuses(a, primary, None, None)? .into_iter() .step_by(2) { - let face = eu::face(a, edgeuse)? - .ok_or_else(|| SolidError::null_pointer("primitive::boolean_2d"))?; + let face = eu::face(a, edgeuse)?.ok_or_else(|| SolidError::null_pointer(OP))?; if marks_a.is_marked(k, Entity::Face(face), mark_a) { continue; } @@ -134,14 +173,14 @@ pub(super) fn finish( )?; if let Some(faceuse) = sector.coincident_faceuse { let mate = eu::faceuse_face(b, faceuse)? - .ok_or_else(|| SolidError::null_pointer("primitive::boolean_2d"))?; + .ok_or_else(|| SolidError::null_pointer(OP))?; marks_b.mark(k, Entity::Face(mate), mark_b); - ignore_status(intersections.faces.record(face, mate))?; + // sTI.Relate(pF,pOFace), its status unchecked + ignore_status(session.ti.relate_faces(a, face, b, mate))?; } } for edgeuse in edge::edgeuses(b, other, None, None)?.into_iter().step_by(2) { - let face = eu::face(b, edgeuse)? - .ok_or_else(|| SolidError::null_pointer("primitive::boolean_2d"))?; + let face = eu::face(b, edgeuse)?.ok_or_else(|| SolidError::null_pointer(OP))?; if marks_b.is_marked(k, Entity::Face(face), mark_b) { continue; } @@ -162,15 +201,16 @@ pub(super) fn finish( }; if let Some(faceuse) = sector.coincident_faceuse { let mate = eu::faceuse_face(a, faceuse)? - .ok_or_else(|| SolidError::null_pointer("primitive::boolean_2d"))?; + .ok_or_else(|| SolidError::null_pointer(OP))?; marks_a.mark(k, Entity::Face(mate), mark_a); - intersections.faces.record(mate, face)?; + session.ti.relate_faces(a, mate, b, face)?; } } } let mut delete = Vec::new(); for face in crate::topology::brep::p1::faces(a, None)? { - let common = intersections.faces.other(face).is_some(); + // sTI.GetOtherMate(pF) + let common = session.ti.face_mate(face).is_some(); if matches!(operation, Boolean2d::ExclusiveOr | Boolean2d::Difference) && common || operation == Boolean2d::Intersection && !common { @@ -180,10 +220,12 @@ pub(super) fn finish( // Keep the original A faces until every B classification/copy is done. let mut covering = first; for face in crate::topology::brep::p1::faces(b, None)? { + // sTI.GetBrepMate(pOFace) + let common = session.ti.merge.intersector.faces.primary(face).is_some(); if matches!( operation, Boolean2d::ExclusiveOr | Boolean2d::Union | Boolean2d::Merge - ) && intersections.faces.primary(face).is_none() + ) && !common { let boundary = crate::topology::face::p2::create_curves_from_face( k, @@ -197,7 +239,7 @@ pub(super) fn finish( .face(covering)? .surface .as_ref() - .ok_or_else(|| SolidError::null_pointer("primitive::boolean_2d"))?; + .ok_or_else(|| SolidError::null_pointer(OP))?; let surface = if original.owner().is_some() { original.copy(k)? } else { @@ -211,14 +253,24 @@ pub(super) fn finish( boundary, surface, domain, - notifications, + &mut session.env(), )?; } } - ignore_status(delete_faces(k, a, &mut intersections, &delete))?; + // sTI.DeleteFaces(sDelFaces), its status unchecked + ignore_status(session.ti.delete_faces( + &mut BooleanKernels { + primary: k, + other: None, + }, + a, + session.attributes, + &delete, + None, + ))?; if operation != Boolean2d::Merge { stitch_faces(k, a, a.tolerance())?; - remove_topology(k, a, notifications)?; + remove_topology(k, a, &mut session.env())?; } *result = Some(core::mem::take(a)); *b = Brep::new(); diff --git a/crates/solidcore/src/primitive/primitive_creation/p1/primitives.rs b/crates/solidcore/src/primitive/primitive_creation/p1/primitives.rs index 06a26d2c77..320fa47099 100644 --- a/crates/solidcore/src/primitive/primitive_creation/p1/primitives.rs +++ b/crates/solidcore/src/primitive/primitive_creation/p1/primitives.rs @@ -1329,13 +1329,18 @@ mod tests { &mut notifications, ) .unwrap_err(); - assert!(matches!( - error, - SolidError::Unported { - unit: "boolean.topology_intersector_p2", - .. - } - )); + // past IntersectInsertRelate: copying B's unrelated face needs its face use, + // which the bare fixture face record lacks + assert!( + matches!( + error, + SolidError::InvalidInput { + operation: "topology::face::p2", + .. + } + ), + "{error:?}" + ); let covered_a = a .face(fa) .unwrap() @@ -1390,14 +1395,17 @@ mod tests { &mut notifications, ) .unwrap_err(); - if invalid { - assert!( - matches!(error, SolidError::InvalidInput { .. }), - "{angle}: {error}" - ); + // a coplanar pair goes on through IntersectInsertRelate and stops copying + // B's bare face record (no face use); the others fail the coplanar check + let expected = if invalid { + "primitive::primitive_creation::p1::boolean_2d" } else { - assert!(error.is_unported(), "{angle}: {error}"); - } + "topology::face::p2" + }; + assert!( + matches!(error, SolidError::InvalidInput { operation, .. } if operation == expected), + "{angle}: {error}" + ); assert!(!a.editing_enabled()); assert!(b.editing_enabled()); assert!(!k.context.doing_boolean()); diff --git a/harness/cpp/shim/primitive_primitive_creation_p1.cpp b/harness/cpp/shim/primitive_primitive_creation_p1.cpp index 4175be17e6..3e3568cadf 100644 --- a/harness/cpp/shim/primitive_primitive_creation_p1.cpp +++ b/harness/cpp/shim/primitive_primitive_creation_p1.cpp @@ -322,6 +322,13 @@ SC_EXPORT int32_t sc_primitive_primitive_creation_p1_eval(int32_t op, const doub const bool consumed=result==&a; if(consumed) clean_b.Clear(); values.insert(values.end(),{st(status),flg(a.m_bEditingEnabled),consumed?-1.0:flg(b->m_bEditingEnabled),result?1.0:0.0}); + if(consumed) { + // the result's counts and geometry, as op 4 records a constructed B-rep + SmTArray faces; SmTArray edges; SmTArray vertices; + a.GetFaces(faces);a.GetEdges(edges);a.GetVertices(vertices); + values.insert(values.end(),{(double)faces.GetSize(),(double)edges.GetSize(),(double)vertices.GetSize()}); + primitive_record::append(values,&a); + } } else { if(n!=13) return SC_ERR_ARG; SmBSplineCurve* curve=pc1_curve(data); SmObjDelete clean_curve(curve); diff --git a/plan/units/boolean.merge_p3.toml b/plan/units/boolean.merge_p3.toml index 4a52e9437c..92a4c4bbdc 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 = "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 = "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/primitive.primitive_creation_p1.toml b/plan/units/primitive.primitive_creation_p1.toml index 2a2810f8c1..af688a5d16 100644 --- a/plan/units/primitive.primitive_creation_p1.toml +++ b/plan/units/primitive.primitive_creation_p1.toml @@ -9,4 +9,4 @@ tests = ["SM_API_test:TestSmCreatePlanarCircle", "SM_API_test:TestSmCreatePlane" status = "partial" claimed_by = "" claimed_at = "" -notes = "main CI e33985411 (2026-10-23): PARTIAL primitive.primitive_creation_p1 cases=773 passed=740 failed=0 deferred=24 rejected=9 blocked_by=boolean.topology_intersector_p2(10),surface.trim_srf_cache(8),topology.edgeuse_p3(2),topology.face_p2(4). PARTIAL (PR #213 review, 2026-10-20, canonical main e5f3e441): 773 cases, 689 passed, 0 failed, 75 deferred (boolean.topology_intersector_p2 10, topology.brep_p3 6, topology.brep_p8 59), 9 rejected, max_rel_err=0, no absolute floors. Both MakeFaceWithCurves adapters now call topology::brep::p7 with caller region/domain, loops, 3D/UV curves and orientations. Constructors now forward caller NotifyEnv records/attributes/keys required by current p7; the primitive fixture creates a real infinite region for current p9 validation. StitchAndOrient, Transform (scale=None, scale_tolerances=true), and RemoveTopologicalEdgesAndVertices (no keep lists, caller NotifyEnv forwarded) call topology::brep::p5. IntersectInsertRelate and DeleteFaces still require a retained session adapting Boolean2D to merge_p4 ArenaPair: common-pair identities, relations, and operand-qualified notifications must survive both stages; their downstream mutations are not certified. Boolean2D uses EFF_ZERO_DEG (0.0001 degrees); kernel tests pin angular rejection/state restoration and plane-domain coverage before intersection. 32 new boolean cases compare non-planar/offset/tilted rejection and empty paths; 10 valid coplanar cases defer at IntersectInsertRelate. Four rear-X-tilt cone inputs and 44 nonempty line/circle/arc/skew-line sweeps defer at face construction, using both B-spline and real analytic generators. Four negative full-circle sweeps are rejected by an exact cylinder-branch preflight after a native nontermination was confirmed in AdjustSTEPUVDomain (plan/upstream-issues.md); negative line/open-arc sweeps remain accepted. Bottom cylindrical-box points preserve signed zero. Primitive topology, caps and downstream boolean mutations remain uncertified. 19 fns; largest: SmPrimitiveCreation::Boolean2D, SmPrimitiveCreation::CreateLinearSweep, SmPrimitiveCreation::CreateSphereNoPole, sm_CreateAnalyticRotationalSweep; cycle back-edges (call into later units; stub or co-port): boolean.topology_intersector_p1(5), sweep.topology_sweep(4), boolean.topology_intersector_p2(3); unit-SCC #22 (220 units); 131 upstream test entry points reach this unit; CO-PORTED in sweep.api_brep (PR #110): SmSweepOptions -> solidcore::primitive::primitive_creation::SweepOptions (crates/solidcore/src/primitive/primitive_creation.rs, parent module of p1..p3; the type comes from SmPrimitiveCreation.h) Review integration (PR #213): merge_p3 BooleanLists delegates planar operations to PrimitiveCreation::boolean_2d with fresh operand marks and caller notifications. Boolean2D still needs a retained ArenaPair intersector/relations session with an operand-qualified notification/key namespace for both Breps; its primary-only NotifyEnv and typed common-pair lists are not that adapter. The existing merge_p4 ArenaPair adapter is present, not missing. Failed circle construction maps non-Unported geometry errors to SM_ERR. API UPDATED in primitive.primitive_creation_p3 (PR #233), after main 4db3d660: create_cylindrical_box, create_sphere_no_pole and boolean_2d take caller NotifyEnv for their p7 face-construction tails; notification key_of supplies face identities. UPDATE 2026-10-23 (branch claude/edge-handles): the 4 curve.curve_p2 GetNaturalInterval deferrals resolve (curve-solver ParameterDomains); two of them (linear_circle_cap1_distance0, linear_analytic_circle_cap1_distance0) then compared face counts from the B-rep arena (4, including two face records with no face use) against the shim's SmBrep::GetFaces (2); the runner now counts faces, edges and vertices through topology::brep::p1::faces/edges/vertices as the shim does (findings.md). PARTIAL primitive.primitive_creation_p1 cases=773 passed=740 failed=0 deferred=24 max_rel_err=0e0 with_values=740 no_values=0 vacuous=0 rejected=9 blocked_by=boolean.topology_intersector_p2(10),surface.trim_srf_cache(8),topology.edgeuse_p3(2),topology.face_p2(4). UPDATE PR #613: use caller notification identities consistently in cylindrical-box and pole-free-sphere faces and final region nesting. Geometry records now compare sampled curves/surfaces, tolerances and per-edge stored trims: 773 cases, 754 pass bit-identical, 0 fail, 10 deferred (boolean.topology_intersector_p2), 9 native rejections; local macOS C9 reference." +notes = "PR #738: Boolean2D's IntersectInsertRelate, GetCommonEdges/Vertices, Relate, GetOtherMate/GetBrepMate and DeleteFaces run on merge p4's KernelManifoldIntersector; the op-6 runner builds real CreateFaceFromSurface faces in SmBrep(SM_USE_DEFAULT) operands and compares a successful result's counts and geometry. The 10 coplanar cases now defer earlier, in CoverCoincidentSurface: STEPInversion of a face-owned surface reaches GlobalPointSolve's trimmed-cache point test (SmFace::PointClassify, no face context there, surface/surface/p4.rs). PARTIAL cases=773 passed=754 failed=0 deferred=10 rejected=9. main CI e33985411 (2026-10-23): PARTIAL primitive.primitive_creation_p1 cases=773 passed=740 failed=0 deferred=24 rejected=9 blocked_by=boolean.topology_intersector_p2(10),surface.trim_srf_cache(8),topology.edgeuse_p3(2),topology.face_p2(4). PARTIAL (PR #213 review, 2026-10-20, canonical main e5f3e441): 773 cases, 689 passed, 0 failed, 75 deferred (boolean.topology_intersector_p2 10, topology.brep_p3 6, topology.brep_p8 59), 9 rejected, max_rel_err=0, no absolute floors. Both MakeFaceWithCurves adapters now call topology::brep::p7 with caller region/domain, loops, 3D/UV curves and orientations. Constructors now forward caller NotifyEnv records/attributes/keys required by current p7; the primitive fixture creates a real infinite region for current p9 validation. StitchAndOrient, Transform (scale=None, scale_tolerances=true), and RemoveTopologicalEdgesAndVertices (no keep lists, caller NotifyEnv forwarded) call topology::brep::p5. IntersectInsertRelate and DeleteFaces still require a retained session adapting Boolean2D to merge_p4 ArenaPair: common-pair identities, relations, and operand-qualified notifications must survive both stages; their downstream mutations are not certified. Boolean2D uses EFF_ZERO_DEG (0.0001 degrees); kernel tests pin angular rejection/state restoration and plane-domain coverage before intersection. 32 new boolean cases compare non-planar/offset/tilted rejection and empty paths; 10 valid coplanar cases defer at IntersectInsertRelate. Four rear-X-tilt cone inputs and 44 nonempty line/circle/arc/skew-line sweeps defer at face construction, using both B-spline and real analytic generators. Four negative full-circle sweeps are rejected by an exact cylinder-branch preflight after a native nontermination was confirmed in AdjustSTEPUVDomain (plan/upstream-issues.md); negative line/open-arc sweeps remain accepted. Bottom cylindrical-box points preserve signed zero. Primitive topology, caps and downstream boolean mutations remain uncertified. 19 fns; largest: SmPrimitiveCreation::Boolean2D, SmPrimitiveCreation::CreateLinearSweep, SmPrimitiveCreation::CreateSphereNoPole, sm_CreateAnalyticRotationalSweep; cycle back-edges (call into later units; stub or co-port): boolean.topology_intersector_p1(5), sweep.topology_sweep(4), boolean.topology_intersector_p2(3); unit-SCC #22 (220 units); 131 upstream test entry points reach this unit; CO-PORTED in sweep.api_brep (PR #110): SmSweepOptions -> solidcore::primitive::primitive_creation::SweepOptions (crates/solidcore/src/primitive/primitive_creation.rs, parent module of p1..p3; the type comes from SmPrimitiveCreation.h) Review integration (PR #213): merge_p3 BooleanLists delegates planar operations to PrimitiveCreation::boolean_2d with fresh operand marks and caller notifications. Boolean2D still needs a retained ArenaPair intersector/relations session with an operand-qualified notification/key namespace for both Breps; its primary-only NotifyEnv and typed common-pair lists are not that adapter. The existing merge_p4 ArenaPair adapter is present, not missing. Failed circle construction maps non-Unported geometry errors to SM_ERR. API UPDATED in primitive.primitive_creation_p3 (PR #233), after main 4db3d660: create_cylindrical_box, create_sphere_no_pole and boolean_2d take caller NotifyEnv for their p7 face-construction tails; notification key_of supplies face identities. UPDATE 2026-10-23 (branch claude/edge-handles): the 4 curve.curve_p2 GetNaturalInterval deferrals resolve (curve-solver ParameterDomains); two of them (linear_circle_cap1_distance0, linear_analytic_circle_cap1_distance0) then compared face counts from the B-rep arena (4, including two face records with no face use) against the shim's SmBrep::GetFaces (2); the runner now counts faces, edges and vertices through topology::brep::p1::faces/edges/vertices as the shim does (findings.md). PARTIAL primitive.primitive_creation_p1 cases=773 passed=740 failed=0 deferred=24 max_rel_err=0e0 with_values=740 no_values=0 vacuous=0 rejected=9 blocked_by=boolean.topology_intersector_p2(10),surface.trim_srf_cache(8),topology.edgeuse_p3(2),topology.face_p2(4). UPDATE PR #613: use caller notification identities consistently in cylindrical-box and pole-free-sphere faces and final region nesting. Geometry records now compare sampled curves/surfaces, tolerances and per-edge stored trims: 773 cases, 754 pass bit-identical, 0 fail, 10 deferred (boolean.topology_intersector_p2), 9 native rejections; local macOS C9 reference."