From 7b248280a53180587f5befb9af86dc6336ed7def Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Sat, 24 Oct 2026 02:49:34 -0700 Subject: [PATCH 1/4] claim: wire IntersectInsertRelate into Boolean2D and the PiecewiseMerge runner Co-Authored-By: Claude Opus 5.5 From d20ddd8ad8378a6227745ea700a84474797b6811 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Sat, 24 Oct 2026 03:08:04 -0700 Subject: [PATCH 2/4] Run Boolean2D's IntersectInsertRelate and DeleteFaces through merge p4's kernel adapter SmPrimitiveCreation::Boolean2D's local SmTopologyIntersector is now a KernelManifoldIntersector over the two arenas: IntersectInsertRelate, GetCommonEdges/GetCommonVertices, Relate, GetOtherMate/GetBrepMate and DeleteFaces all go through the one retained mate store, notifying under the caller's identities. The primitive_creation_p1 op-6 runner now builds its operands as the shim does (SmBrep with its infinite region, CreateFaceFromSurface faces instead of bare face records) and, on success, both sides record the result's counts and geometry. Co-Authored-By: Claude Opus 5.5 --- .../units/primitive_primitive_creation_p1.rs | 76 ++++++- .../primitive_creation/p1/boolean.rs | 192 +++++++++++------- .../shim/primitive_primitive_creation_p1.cpp | 7 + 3 files changed, 194 insertions(+), 81 deletions(-) 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/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/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); From 68c117d34735b48b3fbafda5f3d587f085733b2c Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Sat, 24 Oct 2026 04:12:02 -0700 Subject: [PATCH 3/4] Run the merge_p3 runner's PiecewiseMerge on the constructed merge engine Family 4 now constructs the merge as the shim's MergeAccess does and uses that engine's intersector as m_vTI for every PiecewiseMerge stage: IntersectInsertRelate, the mate clearing, SetSubset (by surface identity), the wire/lamina controls, RegisterExistingSSI/ClearExistingSSI and ProcessWiresAndShellVertices, so ManifoldBoolean sees the same mates. The pool's stand-in TopologyIntersector and its empty-relations wire processing are gone. The kernel adapter exposes the piecewise controls it already had (clear_relationships, wire edges, SSI registration) plus two getters. Boolean2D's unit tests (merge p3 and primitive p1) asserted the old IntersectInsertRelate stub; on their bare face records the intersector now runs and copying an unrelated face stops at its missing face use, which they now assert. Co-Authored-By: Claude Opus 5.5 --- crates/difftest/src/units/boolean_merge_p3.rs | 365 ++++++++---------- crates/solidcore/src/boolean/merge/p3.rs | 29 +- .../src/boolean/merge/p4/kernel_adapter.rs | 23 +- .../primitive_creation/p1/primitives.rs | 36 +- plan/units/boolean.merge_p3.toml | 2 +- .../primitive.primitive_creation_p1.toml | 2 +- 6 files changed, 229 insertions(+), 228 deletions(-) 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/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 de91726ce4..010a878195 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/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/plan/units/boolean.merge_p3.toml b/plan/units/boolean.merge_p3.toml index 4a52e9437c..ae27247476 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=topology.edge(9),topology.face_p2(2),topology.vertex(17) (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." From 96b3df135455cc75370629441ec36c78b0b52097 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Sat, 24 Oct 2026 04:43:14 -0700 Subject: [PATCH 4/4] plan: boolean.merge_p3 deferral split after merging main Co-Authored-By: Claude Opus 5.5 --- plan/units/boolean.merge_p3.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plan/units/boolean.merge_p3.toml b/plan/units/boolean.merge_p3.toml index ae27247476..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 = "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=topology.edge(9),topology.face_p2(2),topology.vertex(17) (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 = "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."