From 0acba70343d3135dcdbff40b6058044ad261d232 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Mon, 26 Oct 2026 15:22:43 +0100 Subject: [PATCH 1/3] difftest: topology_intersector_p1 runner support for edits before FaceFace Adds a SetEditing script op (shim op 34, m_bEditingEnabled), a ChainFaces marker that lets a FaceFace without input vertices follow a VertexFace or EdgeFace (the FFI guard now allows exactly that), Vertexuse handles in the runner's topology_entity, and crossing-plane fixtures. Existing cases are unchanged. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011gJAwcLRBPQXYLmvXd3pWf --- .../units/boolean_topology_intersector_p1.rs | 131 +++++++++++++++++- .../units/boolean_topology_intersector_p1.rs | 41 ++++-- .../shim/boolean_topology_intersector_p1.cpp | 14 ++ 3 files changed, 176 insertions(+), 10 deletions(-) diff --git a/crates/difftest/src/units/boolean_topology_intersector_p1.rs b/crates/difftest/src/units/boolean_topology_intersector_p1.rs index 9e442f3e05..890f3bfca9 100644 --- a/crates/difftest/src/units/boolean_topology_intersector_p1.rs +++ b/crates/difftest/src/units/boolean_topology_intersector_p1.rs @@ -424,6 +424,17 @@ pub(super) enum OpIn { CloseLoop { v: Hin, }, + /// A marker, dropped by the resolver: later `FaceFace` ops (with no input + /// vertices) may follow a `VertexFace` or `EdgeFace` op, taking only face handles + /// (faces keep their handles through those edits; edges and vertices do not). + /// Existing cases never carry it, so their resolution is unchanged. + ChainFaces, + /// `pBrep->m_bEditingEnabled = enable` on the intersector's B-rep `which` (0 or 1), + /// which `IntersectTopology` sets around its stages and the edits need (shim op 34). + SetEditing { + which: u8, + enable: bool, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1056,6 +1067,7 @@ impl ClassGeometry for Two { K_FACE => EntityRef::Face(fid(o)), K_LOOP => EntityRef::Loop(LoopId::from_index(o.i)), K_EDGEUSE => EntityRef::Edgeuse(EdgeuseId::from_index(o.i)), + K_VERTEXUSE => EntityRef::Vertexuse(VertexuseId::from_index(o.i)), _ => return None, }; Some((b, e)) @@ -2558,6 +2570,8 @@ fn resolve( let mut curves = 0usize; let mut has_mgr = false; let mut mutated = false; + let mut chain = false; + let mut soft = false; // the intersector's B-reps (the FFI's `have_breps`) let mut have_breps = [true, c.breps[1].is_some()]; let mut closed = false; @@ -2594,13 +2608,19 @@ fn resolve( | OpIn::ShellVerts { .. } | OpIn::WireLamina { .. } | OpIn::MgrNew { .. } + | OpIn::SetEditing { .. } | OpIn::CommonEdges | OpIn::CommonVertices | OpIn::IntersectTopo | OpIn::SetRels | OpIn::Iir ); - if mutated && takes_handles { + if matches!(op, OpIn::ChainFaces) { + chain = true; + continue; + } + let face_only = matches!(op, OpIn::FaceFace { verts, .. } if verts.is_empty()); + if (mutated || (soft && !face_only)) && takes_handles { return Err(fail("after_mutation")); } // part 2: what the FFI refuses @@ -2869,6 +2889,12 @@ fn resolve( } op.clone() } + OpIn::SetEditing { which, .. } => { + if *which > 1 || (*which == 1 && c.breps[1].is_none()) { + return Err(fail("brep")); + } + op.clone() + } OpIn::MgrNew { .. } => { if has_mgr || c.breps[1].is_none() { return Err(fail("manager")); @@ -2940,6 +2966,8 @@ fn resolve( OpIn::CloseLoop { v } => OpIn::CloseLoop { v: hk(*v, &[K_VERTEX], true)?, }, + // handled above (never reaches here) + OpIn::ChainFaces => continue, }; if matches!( r, @@ -2958,7 +2986,11 @@ fn resolve( | OpIn::EdgeFaceCoin { .. } | OpIn::CloseLoop { .. } ) { - mutated = true; + if chain && !mutated && matches!(r, OpIn::VertexFace { .. } | OpIn::EdgeFace { .. }) { + soft = true; + } else { + mutated = true; + } } out.push(ROp { op: r }); } @@ -3247,6 +3279,9 @@ fn encode(i: &Inputs, ops: &[ROp]) -> Result, HarnessError> { capture, override_tol, } => v.extend([33.0, *capture, *override_tol]), + OpIn::SetEditing { which, enable } => { + v.extend([34.0, f64::from(*which), flag(*enable)]); + } OpIn::RemoveRel { a, b } => { v.push(41.0); push_h(&mut v, *a); @@ -3307,6 +3342,8 @@ fn encode(i: &Inputs, ops: &[ROp]) -> Result, HarnessError> { v.push(52.0); push_h(&mut v, *bv); } + // dropped by the resolver + OpIn::ChainFaces => {} } } Ok(v) @@ -3926,6 +3963,10 @@ fn run_rust_script(i: &Inputs, p: Prepared) -> Result<(Vec, Vec), Fa o.push(r.max_gap); o.push(r.min_unstitched); } + OpIn::SetEditing { which, enable } => { + o.push(34.0); + two.set_editing_enabled(brep_h(i8::try_from(*which).unwrap_or(0)), *enable)?; + } OpIn::MgrNew { capture, override_tol, @@ -5536,6 +5577,92 @@ fn edge_cases() -> Vec<(String, Value)> { }, ); } + // Crossing faces: the intersection edge is inserted inside each face + // (`SmBrep::MakeEdgeInFace`), appended after the cases above. + let plane = |o: [f64; 3], n: [f64; 3], dom: [f64; 4]| BrepIn::Face { + surface: SrfIn::Plane { o, n }, + dom, + }; + let cross = |name: &str, + a: BrepIn, + b: BrepIn, + approx: f64, + tail: Vec, + add: &mut dyn FnMut(&str, Inputs)| { + let mut ops = vec![OpIn::FaceFace { + verts: vec![], + faces1: vec![f(0, 0)], + faces2: vec![f(1, 0)], + ignore: false, + nesting: false, + }]; + ops.extend(tail); + add( + name, + Inputs { + brep_a: a, + brep_b: Some(b), + ctor: 0, + approx, + ang: 0.1, + ops, + }, + ); + }; + let xy = [-1.0, -1.0, 1.0, 1.0]; + for (name, na, nb, da, db) in [ + ( + "cross_planes_perpendicular", + [0.0, 0.0, 1.0], + [1.0, 0.0, 0.0], + xy, + xy, + ), + ( + "cross_planes_tilted", + [0.0, 0.0, 1.0], + [1.0, 0.0, 1.0], + xy, + xy, + ), + ( + "cross_planes_partial_overlap", + [0.0, 0.0, 1.0], + [0.0, 1.0, 0.0], + [-1.0, -1.0, 1.0, 1.0], + [-3.0, -1.0, 0.5, 1.0], + ), + ( + "cross_planes_one_wide", + [0.0, 0.0, 1.0], + [1.0, 1.0, 0.0], + [-3.0, -3.0, 3.0, 3.0], + [-0.5, -1.0, 0.5, 1.0], + ), + ] { + for (suffix, tail) in [ + ("", vec![]), + ("_resolve", vec![OpIn::Resolve, OpIn::Imprint]), + ] { + cross( + &format!("{name}{suffix}"), + plane([0.0, 0.0, 0.0], na, da), + plane([0.0, 0.0, 0.0], nb, db), + 1e-5, + tail, + &mut add, + ); + } + } + // A plane through a B-spline surface and a file face. + cross( + "cross_plane_file_sphere", + plane([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [-3.0, -3.0, 3.0, 3.0]), + file("sphere.smb"), + 1e-5, + vec![], + &mut add, + ); out } diff --git a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs index 105ba7d040..688f6b4039 100644 --- a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs +++ b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs @@ -73,7 +73,8 @@ //! `.smb` files, well-formed clamped B-splines, handles inside the B-reps' exports (it //! builds the B-reps once more to count), the registered curves that exist, edges with a //! curve and vertices and faces with a surface where upstream dereferences them, no -//! handle-taking op after an op that may delete topology (23 to 28, 30), ops 7 and 8 +//! handle-taking op after an op that may delete topology (23 to 28, 30; only a `FaceFace` +//! without input vertices may follow a `VertexFace` or an `EdgeFace`), ops 7 and 8 //! only while the intersector's primary B-rep is set and 27 to 30 only while both are //! (either constructor sets both from the blocks, then `SetBreps` may change them; they //! dereference them unchecked); ops 29 to 33 need the @@ -157,6 +158,7 @@ pub enum ScriptOp { ShellVerts = 31, WireLamina = 32, MgrNew = 33, + SetEditing = 34, } impl ScriptOp { @@ -173,10 +175,10 @@ impl ScriptOp { BrepMate, CanImprint, CheckInterior, CheckSsi, ClearSsi, CommonEdges, CommonVertices, EdgeFace, FaceFace, FaceFaceCoin, FixupEnd, FixupMid, Getters, Imprint, ImprintFace, IsMated, Lowest, MappedBrep, MgrNew, OtherMate, RegisterSsi, Relate, Resolve, SetBreps, - SetEdgeClass, SetSubset, SetTols, ShellVerts, SplitTwo, ThisMate, VertexFace, + SetEdgeClass, SetEditing, SetSubset, SetTols, ShellVerts, SplitTwo, ThisMate, VertexFace, WireLamina, }; - const ALL: [ScriptOp; 32] = [ + const ALL: [ScriptOp; 33] = [ Relate, OtherMate, ThisMate, @@ -209,6 +211,7 @@ impl ScriptOp { ShellVerts, WireLamina, MgrNew, + SetEditing, ]; ALL.iter().copied().find(|o| o.code() == code) } @@ -716,6 +719,9 @@ fn script( rd.finite()?; // ang let n_ops = rd.index(MAX_OPS)?; let mut mutated = false; + // after a `VertexFace` / `EdgeFace` only: face handles stay valid (neither deletes + // a face), so a `FaceFace` without input vertices may still follow + let mut soft = false; let mut has_mgr = false; // the intersector's B-reps: both from the blocks with either constructor (1 sets // them with SetPrimaryBrep / SetOtherBrep), then whatever SetBreps sets @@ -730,12 +736,12 @@ fn script( Brep, ]; for _ in 0..n_ops { - let code = rd.int(1, extra.map_or(33, |x| x.max_code().max(33)))?; + let code = rd.int(1, extra.map_or(34, |x| x.max_code().max(34)))?; let Some(op) = ScriptOp::from_code(code) else { let extra = extra.ok_or_else(|| bad("bad script op"))?; let mut st = ScriptState { ctor, - mutated, + mutated: mutated || soft, has_mgr, have_breps, closed: false, @@ -746,6 +752,7 @@ fn script( return Err(bad("bad script op")); } mutated = st.mutated; + soft = false; has_mgr = st.has_mgr; have_breps = st.have_breps; if st.closed { @@ -765,10 +772,11 @@ fn script( | ScriptOp::ShellVerts | ScriptOp::WireLamina | ScriptOp::MgrNew + | ScriptOp::SetEditing | ScriptOp::CommonEdges | ScriptOp::CommonVertices ); - if mutated && takes_handles { + if (mutated || (soft && !matches!(op, ScriptOp::FaceFace))) && takes_handles { return Err(bad("handle-taking op after a mutating op")); } // the ops that dereference the intersector's B-reps without checking @@ -876,7 +884,13 @@ fn script( rd.flag()?; } ScriptOp::FaceFace => { - c.list(rd, &[Vertex], false)?; + if soft { + if rd.index(MAX_LIST)? != 0 { + return Err(bad("vertex handles after a mutating op")); + } + } else { + c.list(rd, &[Vertex], false)?; + } c.list(rd, &[Face], false)?; c.list(rd, &[Face], false)?; rd.flag()?; @@ -908,6 +922,13 @@ fn script( rd.flag()?; rd.flag()?; } + ScriptOp::SetEditing => { + let which = rd.index(2)?; + if which == 1 && c.breps[1].is_none() { + return Err(bad("no second B-rep")); + } + rd.flag()?; + } ScriptOp::MgrNew => { if has_mgr || c.breps[1].is_none() { return Err(bad("manager already made or no second B-rep")); @@ -918,7 +939,11 @@ fn script( } } if op.mutates() { - mutated = true; + if !mutated && matches!(op, ScriptOp::VertexFace | ScriptOp::EdgeFace) { + soft = true; + } else { + mutated = true; + } } } Ok(()) diff --git a/harness/cpp/shim/boolean_topology_intersector_p1.cpp b/harness/cpp/shim/boolean_topology_intersector_p1.cpp index 28ce9196b8..34948e07f4 100644 --- a/harness/cpp/shim/boolean_topology_intersector_p1.cpp +++ b/harness/cpp/shim/boolean_topology_intersector_p1.cpp @@ -917,6 +917,7 @@ enum TiScriptOp : int64_t S_SHELL_VERTS = 31, S_WIRE_LAMINA = 32, S_MGR_NEW = 33, + S_SET_EDITING = 34, }; // Handle kinds (`kind` of a `brep kind index` triple). @@ -1661,6 +1662,19 @@ void run_script(Rd& rd, Two& t, std::vector& o) o.push_back(mn); break; } + case S_SET_EDITING: + { + // pBrep->m_bEditingEnabled = flag (IntersectTopology sets it around the stages) + int64_t which = rd.idx(); + SmBoolean enable = rd.bflag(); + if (which < 0 || which > 1 || t.brep[which] == nullptr) + { + o.push_back(ST_NULL_DEREF); + break; + } + t.brep[which]->m_bEditingEnabled = enable; + break; + } case S_MGR_NEW: { double capture = rd.num(); From 8d2286619233a4aa5fb29479c8551efc3d75ccbe Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Mon, 26 Oct 2026 15:23:01 +0100 Subject: [PATCH 2/3] plan: finding for FaceFaceIntersect glue across two B-reps Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011gJAwcLRBPQXYLmvXd3pWf --- ...ean.topology_intersector_p1--cross-brep-glue-edges.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 plan/findings/boolean.topology_intersector_p1--cross-brep-glue-edges.md diff --git a/plan/findings/boolean.topology_intersector_p1--cross-brep-glue-edges.md b/plan/findings/boolean.topology_intersector_p1--cross-brep-glue-edges.md new file mode 100644 index 0000000000..f714cc60bf --- /dev/null +++ b/plan/findings/boolean.topology_intersector_p1--cross-brep-glue-edges.md @@ -0,0 +1,9 @@ +# FaceFaceIntersect with faces of two B-reps: GlueEdgesGeneral across B-reps + +Unit: boolean.topology_intersector_p1 (PR #1165 runner work). + +Inputs: plane_square.smb x sphere.smb (and torus.smb), `SetEditing` on both B-reps, one `EdgeFace` over all edges and both faces, then `FaceFace` with faces f(0,i), f(1,j) (8 cases, temporary names `cand_*`, not committed). + +Both sides reach `MakeEdgeInFace` for the interval (Vertex/Vertex ends) in both faces. Upstream then calls `m_pBrep->GlueEdgesGeneral(pNewEdge1, ..., pNewEdge2)` with edges of two different B-reps. C++ continues (state length 36); the Rust runner returns "not an edge of this B-rep" because `brep_glue_edges_general` takes one arena (state length 25). + +Suspected cause: upstream only calls this with both faces in one (merged) B-rep; the two-B-rep call is outside the contract and `topology::brep::p7::glue_edges_general` cannot model it. A fixture needs both faces in one B-rep, or the FFI should reject the cross-B-rep case as undefined. From e70348c517508a23925e623e201c285627d446d9 Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Mon, 26 Oct 2026 15:33:35 +0100 Subject: [PATCH 3/3] difftest: topology_intersector_p1 merged-B-rep fixtures reaching MakeEdgeInFace Adds a MergeBreps runner op (shim op 35, SmBrep::MergeBrep) that puts both operands in the primary B-rep as SmMerge does, and merged_* cases that run SetEditing, EdgeFace and FaceFace on faces of that one B-rep. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011gJAwcLRBPQXYLmvXd3pWf --- .../units/boolean_topology_intersector_p1.rs | 77 +++++++++++++++++++ .../units/boolean_topology_intersector_p1.rs | 21 ++++- .../shim/boolean_topology_intersector_p1.cpp | 17 ++++ ...p1--addendum-2026-10-26-merged-fixtures.md | 3 + 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 plan/findings/boolean.topology_intersector_p1--addendum-2026-10-26-merged-fixtures.md diff --git a/crates/difftest/src/units/boolean_topology_intersector_p1.rs b/crates/difftest/src/units/boolean_topology_intersector_p1.rs index 890f3bfca9..e061f92b03 100644 --- a/crates/difftest/src/units/boolean_topology_intersector_p1.rs +++ b/crates/difftest/src/units/boolean_topology_intersector_p1.rs @@ -435,6 +435,11 @@ pub(super) enum OpIn { which: u8, enable: bool, }, + /// `pPrimaryBrep->MergeBrep(*pOtherBrep, NULL, NULL, FALSE)`, as `SmMerge` does before + /// it intersects, so both operands' faces share an owner. Only as the first op (shim + /// op 35); the other B-rep stays as it was, and the primary's handles then number the + /// primary's entities followed by the copies. + MergeBreps, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -2609,6 +2614,7 @@ fn resolve( | OpIn::WireLamina { .. } | OpIn::MgrNew { .. } | OpIn::SetEditing { .. } + | OpIn::MergeBreps | OpIn::CommonEdges | OpIn::CommonVertices | OpIn::IntersectTopo @@ -2968,6 +2974,12 @@ fn resolve( }, // handled above (never reaches here) OpIn::ChainFaces => continue, + OpIn::MergeBreps => { + if idx != 0 || c.breps[1].is_none() { + return Err(fail("merge")); + } + op.clone() + } }; if matches!( r, @@ -3344,6 +3356,7 @@ fn encode(i: &Inputs, ops: &[ROp]) -> Result, HarnessError> { } // dropped by the resolver OpIn::ChainFaces => {} + OpIn::MergeBreps => v.push(35.0), } } Ok(v) @@ -3379,6 +3392,20 @@ fn put_cut(o: &mut Vec, cut: Option<&(usize, String)>) { } } +/// `target->MergeBrep(other, NULL, NULL, FALSE)`: `topology::brep::p3::merge_brep`. +fn merge_primary(target: &mut Brep, other: &mut Brep) -> Result<(), HarnessError> { + use solidcore::topology::brep::p3 as bp3; + let mut k = Kernel::default(); + let serial = bp3::notify_serial(&mut k.context, other) + .map_err(|e| harness(format!("merge: {e}")))?; + let source_key = move |e| bp3::topology_ref(serial, e); + let other = &*other; + bp3::with_kernel_callbacks(&mut k, target, |k, b, env| { + bp3::merge_brep(k, env, b, other, &source_key, None, None, false) + }) + .map_err(|e| harness(format!("merge: {e}"))) +} + /// Exports and imports the B-reps and resolves the script; `Err(Ok(output))` is the /// output both sides report when the B-reps cannot be built or an op cannot be /// resolved. @@ -3392,6 +3419,13 @@ fn prepare(k: &Kernel, i: &Inputs) -> Result Some(import(b).map_err(|e| Box::new(Err(e)))?.0), None => None, }; + let mut brep_a = brep_a; + let mut brep_b = brep_b; + if matches!(i.ops.first(), Some(OpIn::MergeBreps)) { + if let Some(other) = brep_b.as_mut() { + merge_primary(&mut brep_a, other).map_err(|e| Box::new(Err(e)))?; + } + } let count3 = |b: &Brep| [b.vertices().len(), b.edges().len(), b.faces().len()]; let counts = Counts { breps: [Some(count3(&brep_a)), brep_b.as_ref().map(count3)], @@ -3963,6 +3997,11 @@ fn run_rust_script(i: &Inputs, p: Prepared) -> Result<(Vec, Vec), Fa o.push(r.max_gap); o.push(r.min_unstitched); } + OpIn::MergeBreps => { + // (merged in `prepare`, which the resolution needs the counts of) + o.push(35.0); + o.push(0.0); + } OpIn::SetEditing { which, enable } => { o.push(34.0); two.set_editing_enabled(brep_h(i8::try_from(*which).unwrap_or(0)), *enable)?; @@ -5654,6 +5693,44 @@ fn edge_cases() -> Vec<(String, Value)> { ); } } + // Faces of one B-rep, as `SmMerge` intersects them: the other operand is merged into + // the primary B-rep first, so `GlueEdgesGeneral` sees edges of one owner. The edge + // intersections come first (they give the interval ends their vertices). + for (fa, na) in [("plane_square.smb", 4i64), ("disk.smb", 2)] { + for (fb, nb) in [("sphere.smb", 1i64), ("cone.smb", 2), ("torus.smb", 2)] { + add( + &format!("merged_{fa}_{fb}"), + Inputs { + brep_a: file(fa), + brep_b: Some(file(fb)), + ctor: 0, + approx: 1e-5, + ang: 0.1, + ops: vec![ + OpIn::MergeBreps, + OpIn::SetEditing { + which: 0, + enable: true, + }, + OpIn::ChainFaces, + OpIn::EdgeFace { + edges: (0..na + nb).map(|k| e(0, k)).collect(), + faces: vec![f(0, 0), f(0, 1)], + manifold: false, + ignore: false, + }, + OpIn::FaceFace { + verts: vec![], + faces1: vec![f(0, 0)], + faces2: vec![f(0, 1)], + ignore: false, + nesting: false, + }, + ], + }, + ); + } + } // A plane through a B-spline surface and a file face. cross( "cross_plane_file_sphere", diff --git a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs index 688f6b4039..98c8cb21fd 100644 --- a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs +++ b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs @@ -159,6 +159,7 @@ pub enum ScriptOp { WireLamina = 32, MgrNew = 33, SetEditing = 34, + MergeBreps = 35, } impl ScriptOp { @@ -175,10 +176,10 @@ impl ScriptOp { BrepMate, CanImprint, CheckInterior, CheckSsi, ClearSsi, CommonEdges, CommonVertices, EdgeFace, FaceFace, FaceFaceCoin, FixupEnd, FixupMid, Getters, Imprint, ImprintFace, IsMated, Lowest, MappedBrep, MgrNew, OtherMate, RegisterSsi, Relate, Resolve, SetBreps, - SetEdgeClass, SetEditing, SetSubset, SetTols, ShellVerts, SplitTwo, ThisMate, VertexFace, + MergeBreps, SetEdgeClass, SetEditing, SetSubset, SetTols, ShellVerts, SplitTwo, ThisMate, VertexFace, WireLamina, }; - const ALL: [ScriptOp; 33] = [ + const ALL: [ScriptOp; 34] = [ Relate, OtherMate, ThisMate, @@ -212,6 +213,7 @@ impl ScriptOp { WireLamina, MgrNew, SetEditing, + MergeBreps, ]; ALL.iter().copied().find(|o| o.code() == code) } @@ -735,8 +737,8 @@ fn script( HandleKind::Curve, Brep, ]; - for _ in 0..n_ops { - let code = rd.int(1, extra.map_or(34, |x| x.max_code().max(34)))?; + for op_index in 0..n_ops { + let code = rd.int(1, extra.map_or(35, |x| x.max_code().max(35)))?; let Some(op) = ScriptOp::from_code(code) else { let extra = extra.ok_or_else(|| bad("bad script op"))?; let mut st = ScriptState { @@ -773,6 +775,7 @@ fn script( | ScriptOp::WireLamina | ScriptOp::MgrNew | ScriptOp::SetEditing + | ScriptOp::MergeBreps | ScriptOp::CommonEdges | ScriptOp::CommonVertices ); @@ -922,6 +925,16 @@ fn script( rd.flag()?; rd.flag()?; } + ScriptOp::MergeBreps => { + // the first op only; the primary B-rep then holds both operands + let (Some(a), Some(b)) = (c.breps[0], c.breps[1]) else { + return Err(bad("no second B-rep")); + }; + if op_index != 0 { + return Err(bad("merge is not the first op")); + } + c.breps[0] = Some([a[0] + b[0], a[1] + b[1], a[2] + b[2]]); + } ScriptOp::SetEditing => { let which = rd.index(2)?; if which == 1 && c.breps[1].is_none() { diff --git a/harness/cpp/shim/boolean_topology_intersector_p1.cpp b/harness/cpp/shim/boolean_topology_intersector_p1.cpp index 34948e07f4..1d7dd5ff2a 100644 --- a/harness/cpp/shim/boolean_topology_intersector_p1.cpp +++ b/harness/cpp/shim/boolean_topology_intersector_p1.cpp @@ -918,6 +918,7 @@ enum TiScriptOp : int64_t S_WIRE_LAMINA = 32, S_MGR_NEW = 33, S_SET_EDITING = 34, + S_MERGE_BREPS = 35, }; // Handle kinds (`kind` of a `brep kind index` triple). @@ -1662,6 +1663,22 @@ void run_script(Rd& rd, Two& t, std::vector& o) o.push_back(mn); break; } + case S_MERGE_BREPS: + { + // the first op only: copies the other B-rep into the primary one, as SmMerge + // does before it intersects, so both operands' faces share an owner + if (t.brep[1] == nullptr) + { + o.push_back(ST_NULL_DEREF); + break; + } + SmStatus s = t.brep[0]->MergeBrep(*t.brep[1], NULL, NULL, FALSE); + o.push_back(st(s)); + t.g[0] = Graph(); + walk(t.brep[0], t.g[0]); + t.index_geometry(); + break; + } case S_SET_EDITING: { // pBrep->m_bEditingEnabled = flag (IntersectTopology sets it around the stages) diff --git a/plan/findings/boolean.topology_intersector_p1--addendum-2026-10-26-merged-fixtures.md b/plan/findings/boolean.topology_intersector_p1--addendum-2026-10-26-merged-fixtures.md new file mode 100644 index 0000000000..0c2b9d7b73 --- /dev/null +++ b/plan/findings/boolean.topology_intersector_p1--addendum-2026-10-26-merged-fixtures.md @@ -0,0 +1,3 @@ +Updates: plan/findings/boolean.topology_intersector_p1--cross-brep-glue-edges.md + +Whether C++ is defined for the cross-B-rep `GlueEdgesGeneral` was not established (no ASan run), so no FFI guard was added for it. The committed fixtures take the route `SmMerge` takes: a `MergeBreps` runner op (shim op 35, `SmBrep::MergeBrep`) puts both operands into the primary B-rep, then `SetEditing`, `EdgeFace`, `FaceFace` on faces of that one B-rep. `merged_*` cases (plane_square or disk x sphere, cone, torus) reach `MakeEdgeInFace` in both faces and agree with C++ at 1e-9.