Skip to main content

brep_kernel/csg/boolean/
mod.rs

1use crate::arrangement::Vec2;
2use crate::classification::{classify_point, parameter_point_in_face, PolygonClass};
3use crate::fragment::{FaceFragmentRecord, FragmentEdgeSource};
4use crate::imprint::{
5    EdgeSplitRecord, FaceImprints, FaceKey, ImprintOptions, ImprintPieceRecord,
6    ImprintResultRecord, ImprintVertex,
7};
8use crate::tolerance::{assembler_weld, commit_weld};
9use crate::topology::{
10    adaptive_coedge_error, BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord,
11    ShellRecord, VertexRecord,
12};
13use crate::{
14    KernelRefusal, OrRefuse, RefusalClass,
15    apply_edge_splits, apply_edge_splits_with_map, build_imprints, build_pcurve_on_surface,
16    build_pcurve_on_surface_range, classify_surface_pair,
17    fragment_solid, interpolate_curve, merge_curve_continuation_edges,
18    merge_same_surface_faces_excluding,
19    project_point_to_curve, project_point_to_surface, solid_signed_volume, AffineTransform,
20    DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage, KernelTolerances, NurbsCurve,
21    PointClass, SolidClassifier, SurfacePairRelation, Vec3,
22};
23use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
24use serde::{Deserialize, Serialize};
25use web_time::Instant;
26
27thread_local! {
28    /// Running count (per thread) of one-use edges the EDGE-CONFORMANCE
29    /// repair lanes merged or bridged.  The lanes only fire on an assembly
30    /// that would otherwise refuse, so a zero delta across an operation is
31    /// an honest witness that the boolean assembled cleanly without the
32    /// repair.  Test-observable; not part of any result.
33    pub(crate) static CONFORMANCE_REPAIRS: std::cell::Cell<u64> =
34        const { std::cell::Cell::new(0) };
35}
36
37#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
38#[serde(rename_all = "lowercase")]
39pub enum BooleanOperation {
40    Union,
41    Intersect,
42    Subtract,
43}
44
45#[derive(Clone, Debug, Deserialize)]
46pub struct BooleanOptions {
47    #[serde(default = "default_tolerance")]
48    pub tolerance: f64,
49    /// Optional complete accuracy/search policy.  `tolerance` remains the
50    /// backwards-compatible model-identity shorthand when this is absent.
51    #[serde(default)]
52    pub tolerances: Option<KernelTolerances>,
53    #[serde(default)]
54    pub imprint: ImprintOptions,
55    /// Coalesce adjacent result fragments after assembly. This defaults to
56    /// true to preserve the kernel's normal clean-boundary behavior.
57    #[serde(default = "default_true")]
58    pub merge_coplanar_faces: bool,
59    /// Face-name substrings that PIN a result face out of the coplanar/cosurface
60    /// merge (only consulted when [`merge_coplanar_faces`] is on). A face whose
61    /// `name` CONTAINS any of these is emitted unchanged instead of being
62    /// coalesced with a mergeable neighbour; faces matching none merge exactly as
63    /// before. Default empty — no caller sees a behavior change unless it opts
64    /// in. Sheet metal uses this to keep every thickness/side wall face per
65    /// outline segment in the folded solid (so a flange can still attach to a
66    /// specific segment: fusing two collinear thickness faces would erase the
67    /// segment boundary).
68    #[serde(default)]
69    pub keep_unmerged_name_substrs: Vec<String>,
70}
71
72fn default_tolerance() -> f64 {
73    1e-7
74}
75
76fn default_true() -> bool {
77    true
78}
79
80impl Default for BooleanOptions {
81    fn default() -> Self {
82        Self {
83            tolerance: default_tolerance(),
84            tolerances: None,
85            imprint: ImprintOptions::default(),
86            merge_coplanar_faces: true,
87            keep_unmerged_name_substrs: Vec::new(),
88        }
89    }
90}
91
92mod select;
93mod assemble;
94mod rim;
95// BREP private tests: 105eeef69a3ee87f
96
97use rim::*;
98use select::*;
99// BREP private tests: bf6d0f8b58224c78
100pub(crate) use assemble::{
101    assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
102    edge_interior_lies_on, finalize_assembled_solid,
103};
104// Currently referenced only inside `assemble`, so the re-export is unused.
105#[allow(unused_imports)]
106pub(crate) use assemble::apply_assembly_heal_chain;
107
108pub fn boolean_operation(
109    first: &BrepSolid,
110    second: &BrepSolid,
111    operation: BooleanOperation,
112    options: &BooleanOptions,
113) -> Result<BrepSolid, KernelRefusal> {
114    match boolean_operation_with_diagnostics(first, second, operation, options) {
115        Ok(outcome) => Ok(outcome.value),
116        Err(error) => {
117            // Perturbation-fallback lane (Simulation of Simplicity). The exact
118            // arrangement only errs on a DEGENERACY here (coincident /
119            // near-tangent carrier faces make it structurally inconsistent) —
120            // e.g. a partial torus whose equatorial circles lie exactly in a box
121            // face. Retry once on a rigidly perturbed second operand; accept only
122            // an oracle-clean, volume-consistent result. See
123            // `perturbation_retry`.
124            if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
125                || !error.class.perturbation_eligible()
126            {
127                return Err(error);
128            }
129            // INTERNAL-TANGENCY PINCH GATE. Perturbation is only legitimate
130            // where the exact arrangement is UNSTABLE but its ANSWER is not:
131            // the nearby transversal case must carry the same topology the
132            // degenerate one does. An internal tangency in a DIFFERENCE breaks
133            // exactly that premise — the exact `A − B` pinches to zero
134            // thickness along the contact, so an epsilon either tears the wall
135            // open (a through-slot where the exact answer has intact material)
136            // or leaves a sub-micron web. Both are within epsilon in Hausdorff
137            // distance and in volume, so neither the oracle (boundary skip
138            // band) nor the 1% CSG volume bound can see them, and the lane
139            // would ship the tolerant-kernel sliver this kernel refuses. Only
140            // consulted on the failure path, so nothing that succeeds today can
141            // change; the honest exact-path error is returned instead.
142            // Escape hatch for tamper-verification: BREP_PINCH_GATE=0.
143            if matches!(operation, BooleanOperation::Subtract)
144                && std::env::var("BREP_PINCH_GATE").as_deref() != Ok("0")
145                && subtract_pinches_at_internal_tangency(first, second, options.tolerance)
146                    .unwrap_or(false)
147            {
148                return Err(error.with_message(|message| format!(
149                    "{message}; internal tangency: the operands touch tangentially with \
150                     co-directed normals, so the exact difference pinches to zero thickness \
151                     (non-manifold, unrepresentable in a boundary model) — refusing rather \
152                     than returning a perturbed sliver"
153                )));
154            }
155            match perturbation_retry(first, second, operation, options) {
156                Some(solid) => Ok(solid),
157                None => Err(error),
158            }
159        }
160    }
161}
162
163/// Grid resolution (per parameter direction, endpoints included) used to probe a
164/// face pair for a tangential contact. Endpoints and the midpoint are both
165/// sampled, so a contact sitting on a periodic seam (a cylinder's `u = 0`
166/// ruling) and one sitting at a face's parametric centre are both hit exactly.
167const PINCH_PROBE_STEPS: usize = 8;
168
169/// `|n_a × n_b|` bound below which two unit normals count as parallel — the same
170/// bound `csg::imprint`'s pair classifier uses (`PAIR_ANGULAR_TOLERANCE`).
171const PINCH_ANGULAR_TOLERANCE: f64 = 1e-4;
172
173/// Parameter-space step (fraction of the domain span) taken around a contact
174/// sample to prove the contact is LOWER-DIMENSIONAL — a tangency curve/point
175/// rather than a cosurface patch.
176const PINCH_SEPARATION_STEP: f64 = 1e-2;
177
178/// Would the exact `first − second` PINCH to zero thickness at a tangential
179/// contact?
180///
181/// True when some face of `first` and some face of `second` touch tangentially
182/// (surfaces within the contact band, normals parallel) with CO-DIRECTED outward
183/// normals, at a contact that is lower-dimensional (the surfaces separate as you
184/// step away from it).
185///
186/// Co-directed outward normals at a tangency mean one solid lies locally INSIDE
187/// the other: subtracting leaves material on both sides of the contact that
188/// meets there at zero thickness — a non-manifold pinch no boundary model can
189/// represent. Anti-directed normals are the harmless EXTERNAL tangency (a
190/// cylinder resting against a wall): the difference simply keeps `first` intact.
191/// The lower-dimensionality requirement excludes a CO-SURFACE contact (a pocket
192/// wall flush with an outer wall — co-directed normals, but the surfaces stay
193/// coincident in every direction), where the difference is perfectly well
194/// behaved.
195///
196/// Conservative by construction: it only reports a pinch it can actually witness
197/// on the probe grid, and a miss just leaves the perturbation lane to its
198/// existing gates.
199fn subtract_pinches_at_internal_tangency(
200    first: &BrepSolid,
201    second: &BrepSolid,
202    tolerance: f64,
203) -> Result<bool, KernelRefusal> {
204    // Same contact band the imprint pair classifier uses for its sampled
205    // "these carriers touch" verdict.
206    let contact = (tolerance * 100.0).max(1e-12);
207    let probes_a = first
208        .shells
209        .iter()
210        .flat_map(|shell| &shell.faces)
211        .map(PinchProbe::of)
212        .collect::<Result<Vec<_>, _>>()?;
213    let probes_b = second
214        .shells
215        .iter()
216        .flat_map(|shell| &shell.faces)
217        .map(PinchProbe::of)
218        .collect::<Result<Vec<_>, _>>()?;
219    for probe_a in &probes_a {
220        for probe_b in &probes_b {
221            // Sampled-hull cull, so the (failure-path-only) cost stays linear in
222            // the faces that actually touch rather than quadratic in every face.
223            // The hull is built from the probe grid, so it can under-cover a
224            // curved carrier between samples: pad it by a percent of the pair's
225            // size before culling. Over-keeping a pair only costs a probe that
226            // finds nothing.
227            let pad = 1e-2 * probe_a.extent().max(probe_b.extent());
228            if probe_a.separation(probe_b) > contact + pad {
229                continue;
230            }
231            // CO-SURFACE contacts are 2-dimensional, not tangencies: two flush
232            // walls (a pocket's side coincident with an outer wall, a cylinder
233            // cap lying in a box face) have co-directed normals wherever the
234            // solids nest, yet the difference there is perfectly well behaved.
235            // Only a LOWER-dimensional contact pinches, so drop the pair the
236            // pair classifier calls cosurface before probing it.
237            if classify_surface_pair(
238                &probe_a.face.surface,
239                &probe_b.face.surface,
240                tolerance,
241                PINCH_ANGULAR_TOLERANCE,
242            ).or_refuse(KernelStage::Validate, "csg.boolean.mod")?
243            .relation
244                == SurfacePairRelation::Cosurface
245            {
246                continue;
247            }
248            if faces_touch_with_codirected_normals(probe_a, probe_b, contact)? {
249                return Ok(true);
250            }
251        }
252    }
253    Ok(false)
254}
255
256/// A face's probe grid plus the grid's bounding box, built once per face so the
257/// pair loop only pays for a hull comparison.
258struct PinchProbe<'a> {
259    face: &'a FaceRecord,
260    /// `(u, v, point)` on the face's carrier surface.
261    samples: Vec<(f64, f64, Vec3)>,
262    minimum: [f64; 3],
263    maximum: [f64; 3],
264}
265
266impl<'a> PinchProbe<'a> {
267    fn of(face: &'a FaceRecord) -> Result<Self, KernelRefusal> {
268        let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
269        let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
270        let steps = PINCH_PROBE_STEPS as f64;
271        let mut samples = Vec::with_capacity((PINCH_PROBE_STEPS + 1).pow(2));
272        let mut minimum = [f64::INFINITY; 3];
273        let mut maximum = [f64::NEG_INFINITY; 3];
274        for i in 0..=PINCH_PROBE_STEPS {
275            let u = u0 + (u1 - u0) * i as f64 / steps;
276            for j in 0..=PINCH_PROBE_STEPS {
277                let v = v0 + (v1 - v0) * j as f64 / steps;
278                let Ok(point) = face.surface.evaluate(u, v) else {
279                    continue;
280                };
281                for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
282                    minimum[axis] = minimum[axis].min(value);
283                    maximum[axis] = maximum[axis].max(value);
284                }
285                samples.push((u, v, point));
286            }
287        }
288        Ok(Self {
289            face,
290            samples,
291            minimum,
292            maximum,
293        })
294    }
295
296    /// Largest side of the sampled hull (0 when nothing sampled).
297    fn extent(&self) -> f64 {
298        (0..3)
299            .map(|axis| self.maximum[axis] - self.minimum[axis])
300            .fold(0.0f64, f64::max)
301    }
302
303    /// Axis-aligned gap between the two sampled hulls (0 when they overlap).
304    fn separation(&self, other: &Self) -> f64 {
305        let mut gap: f64 = 0.0;
306        for axis in 0..3 {
307            gap = gap.max(self.minimum[axis] - other.maximum[axis]);
308            gap = gap.max(other.minimum[axis] - self.maximum[axis]);
309        }
310        gap
311    }
312}
313
314/// One face pair of [`subtract_pinches_at_internal_tangency`]: probe both
315/// surfaces on a grid, keep samples that land on the other surface inside BOTH
316/// trims with parallel co-directed outward normals, and accept only where the
317/// contact provably separates nearby.
318fn faces_touch_with_codirected_normals(
319    probe_a: &PinchProbe<'_>,
320    probe_b: &PinchProbe<'_>,
321    contact: f64,
322) -> Result<bool, KernelRefusal> {
323    for (probe, other) in [(probe_a, probe_b), (probe_b, probe_a)] {
324        let source = probe.face;
325        let target = other.face;
326        for &(u, v, point) in &probe.samples {
327            let projection = project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?;
328            if projection.distance > contact {
329                continue;
330            }
331            // A pole (collapsed du × dv) has no reliable orientation here;
332            // skipping it keeps the predicate conservative.
333            let (Ok(source_normal), Ok(target_normal)) = (
334                source.surface.normal(u, v),
335                target.surface.normal(projection.u, projection.v),
336            ) else {
337                continue;
338            };
339            let source_outward = outward_normal(source_normal, source.same_sense);
340            let target_outward = outward_normal(target_normal, target.same_sense);
341            if source_outward.cross(target_outward).length() > PINCH_ANGULAR_TOLERANCE
342                || source_outward.dot(target_outward) <= 0.0
343            {
344                continue;
345            }
346            if parameter_point_in_face(source, Vec2 { x: u, y: v }, 1e-6).or_refuse(KernelStage::Validate, "parameter_point_in_face")?
347                == PolygonClass::Outside
348                || parameter_point_in_face(
349                    target,
350                    Vec2 {
351                        x: projection.u,
352                        y: projection.v,
353                    },
354                    1e-6,
355                ).or_refuse(KernelStage::Validate, "csg.boolean.mod")? == PolygonClass::Outside
356            {
357                continue;
358            }
359            if contact_separates_locally(source, target, u, v, contact)? {
360                return Ok(true);
361            }
362        }
363    }
364    Ok(false)
365}
366
367fn outward_normal(normal: Vec3, same_sense: bool) -> Vec3 {
368    if same_sense {
369        normal
370    } else {
371        normal.scale(-1.0)
372    }
373}
374
375/// Does the contact at `source(u, v)` LEAVE the contact band when you step away
376/// from it in parameter space? A tangency curve or point does (stepping across
377/// the tangency separates the surfaces quadratically); a cosurface patch does
378/// not (it stays coincident in every direction). Steps are clamped into the
379/// domain, so a sample on a seam or a domain edge probes only inward.
380fn contact_separates_locally(
381    source: &FaceRecord,
382    target: &FaceRecord,
383    u: f64,
384    v: f64,
385    contact: f64,
386) -> Result<bool, KernelRefusal> {
387    let [u0, u1] = source.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
388    let [v0, v1] = source.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
389    let step_u = (u1 - u0) * PINCH_SEPARATION_STEP;
390    let step_v = (v1 - v0) * PINCH_SEPARATION_STEP;
391    for (probe_u, probe_v) in [
392        ((u + step_u).min(u1), v),
393        ((u - step_u).max(u0), v),
394        (u, (v + step_v).min(v1)),
395        (u, (v - step_v).max(v0)),
396    ] {
397        if (probe_u - u).abs() < f64::EPSILON && (probe_v - v).abs() < f64::EPSILON {
398            continue;
399        }
400        let Ok(point) = source.surface.evaluate(probe_u, probe_v) else {
401            continue;
402        };
403        // A generous multiple of the band: the step must clear it decisively,
404        // never on projection noise.
405        if project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?.distance > contact * 10.0 {
406            return Ok(true);
407        }
408    }
409    Ok(false)
410}
411
412/// Deterministic perturbation-fallback (Simulation of Simplicity), the textbook
413/// CAD answer to an exact/near-tangent arrangement degeneracy, adopted after six
414/// incremental arrangement/imprint/fragment fixes for the near-tangent class were
415/// each falsified, rather than attempting a seventh. Rigidly TRANSLATE the second operand by a tiny
416/// epsilon so the degenerate contact becomes a clean transversal crossing, redo
417/// the boolean, and accept the result ONLY if it is (a) topologically valid —
418/// guaranteed by `assemble`'s own gate returning `Ok` — (b) semantically equal to
419/// the exact CSG of the ORIGINAL operands (the point-classification oracle), and
420/// (c) volume-consistent with the CSG inequality bounds. A wrong perturbed result
421/// is rejected on (b)/(c), so this lane preserves the engine's fail-safe property:
422/// it can only turn a degeneracy-error into a CORRECT solid, never a wrong one.
423///
424/// INVARIANT — no recursion: this calls `boolean_operation_with_diagnostics`
425/// (the EXACT path), never `boolean_operation`, so the fallback can never
426/// re-enter itself. A future refactor must preserve that.
427fn perturbation_retry(
428    first: &BrepSolid,
429    second: &BrepSolid,
430    operation: BooleanOperation,
431    options: &BooleanOptions,
432) -> Option<BrepSolid> {
433    // Generic translation directions: each has a substantial component on ALL
434    // three axes, so whatever axis the coincidence normal lies along (an
435    // equatorial plane flush with a box face, a cap plane flush with a side
436    // face, …) at least one direction has a component that lifts the contact off
437    // exact tangency. Axis-aligned directions are deliberately excluded — a nudge
438    // parallel to the coincident plane leaves the degeneracy in place (verified:
439    // pure ±X / ±Z never rescue the flush-torus case). Fixed, not hash-seeded
440    // as the lane was first sketched: the seeding's only purpose was to rule out
441    // RNG/clock nondeterminism, which fixed vectors already satisfy, and four
442    // diverse directions cover any axis-aligned normal.
443    const DIRECTIONS: [[f64; 3]; 4] = [
444        [0.4034, 0.7973, 0.4491],
445        [0.7973, 0.4491, 0.4034],
446        [0.4491, 0.4034, 0.7973],
447        [0.5774, -0.5774, 0.5774],
448    ];
449    // Fractions of operand scale, ordered SMALLEST-FIRST so the accepted solid
450    // carries the least geometric offset. Sized above the near-tangent sliver
451    // dead zone and below local feature size; the ladder + accept-gate make the
452    // exact magnitude non-critical (a rung that lands in a dead zone simply fails
453    // the gate and the next rung is tried).
454    const FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
455
456    let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
457    let scale = crate::tolerance::solid_scale(first)
458        .max(crate::tolerance::solid_scale(second))
459        .max(1.0);
460
461    // Original operand volumes for the CSG inequality backstop (the oracle skips
462    // points near boundaries, so a wrong thin shell hugging the flush face is
463    // invisible to it but shows up here).
464    let va = solid_signed_volume(first).ok().map(f64::abs);
465    let vb = solid_signed_volume(second).ok().map(f64::abs);
466
467    for &fraction in &FRACTIONS {
468        let magnitude = scale * fraction;
469        for dir in &DIRECTIONS {
470            let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
471            let translate = match AffineTransform::new([
472                1.0, 0.0, 0.0, offset[0], //
473                0.0, 1.0, 0.0, offset[1], //
474                0.0, 0.0, 1.0, offset[2], //
475                0.0, 0.0, 0.0, 1.0,
476            ]) {
477                Ok(transform) => transform,
478                Err(_) => continue,
479            };
480            let moved = match crate::transform_brep(second, translate, false) {
481                Ok(solid) => solid,
482                Err(_) => continue,
483            };
484            let candidate =
485                match boolean_operation_with_diagnostics(first, &moved, operation, options) {
486                    Ok(outcome) => outcome.value,
487                    Err(_) => continue,
488                };
489            // A perturbation must not "rescue" a case into emptiness.
490            if candidate.shells.is_empty() {
491                continue;
492            }
493            // (c) Volume must satisfy the CSG inequality for the ORIGINAL
494            // operands (generous 1% relative slack: catches gross wrongness —
495            // doubled/halved bodies, missing lobes — without rejecting the
496            // legitimate sub-micron offset).
497            if let (Some(va), Some(vb)) = (va, vb) {
498                if let Ok(vr) = solid_signed_volume(&candidate) {
499                    if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
500                        continue;
501                    }
502                }
503            }
504            // (b) Semantic agreement with the exact CSG of the ORIGINAL
505            // operands. Reject a vacuous verdict (skip-band swallowed every
506            // sample so nothing was actually checked).
507            match crate::oracle::boolean_semantic_disagreement(
508                first, second, operation, &candidate, 400,
509            ) {
510                Ok(report) if report.considered >= 30 && !report.is_flagged() => {
511                    if debug {
512                        eprintln!(
513                            "[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
514                             magnitude={magnitude:.3e} offset={offset:?} \
515                             (oracle considered={} rate={:.4})",
516                            report.considered, report.disagreement_rate
517                        );
518                    }
519                    return Some(candidate);
520                }
521                _ => continue,
522            }
523        }
524    }
525    if debug {
526        eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
527    }
528    None
529}
530
531/// CSG volume inequality bounds (no extra boolean needed) with 1% relative slack.
532/// `va`, `vb`, `vr` are absolute volumes of operand A, operand B, and the result.
533fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
534    let slack = 1e-2 * (va + vb).max(1.0);
535    match operation {
536        // max(A,B) ≤ A∪B ≤ A+B
537        BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
538        // A−B ≤ A (and ≥ 0)
539        BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
540        // A∩B ≤ min(A,B)
541        BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
542    }
543}
544
545// ---------------------------------------------------------------------------
546// N-ary booleans (Golovanov T4.4)
547//
548// Union / intersect / subtract of N solids in ONE imprint+fragment pass.  This
549// is a NEW ADDITIVE entry point: the binary `boolean_operation` above is left
550// byte-identical.  The pipeline mirrors the binary one but generalizes the two
551// operand-specific stages to N operands:
552//
553//   1. Imprint EVERY unordered pair (i, j) so every mutual intersection edge is
554//      present on both operands' faces, and accumulate all pairs into ONE global
555//      imprint (operand indices remapped to 0..N, piece/vertex ids made globally
556//      unique, per-face piece lists and per-edge split parameters merged).
557//   2. Split + fragment each operand by that single global imprint, so each
558//      operand's faces are cut by ALL of its intersections at once.
559//   3. Select fragments in one pass: the per-fragment keep decision generalizes
560//      from "vs one other solid" to "vs the SET of other solids" (see
561//      `nary_keep`).  Because every pair is imprinted, a fragment's interior lies
562//      wholly inside or wholly outside each other operand, so a single interior
563//      test point per (fragment, other-solid) is an exact membership verdict.
564//   4. Assemble with the same machinery (`assemble_fragments`), then merge and
565//      validate exactly as the binary path does.
566// ---------------------------------------------------------------------------
567
568/// Reverse a fragment's orientation (surface sense + every coedge), the same
569/// transform the binary Subtract path applies to the second operand's kept
570/// fragments.  Extracted so the n-ary selector can reuse it verbatim.
571fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), KernelRefusal> {
572    fragment.same_sense = !fragment.same_sense;
573    for loop_record in &mut fragment.loops {
574        loop_record.coedges.reverse();
575        for coedge in &mut loop_record.coedges {
576            coedge.forward = !coedge.forward;
577            coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Validate, "reversed")?;
578        }
579    }
580    Ok(())
581}
582
583/// Imprint every unordered pair of operands and fold the pairwise
584/// `ImprintResultRecord`s into ONE global imprint whose operand indices are the
585/// operands' global positions (0..N).  Piece and vertex ids are re-based to be
586/// globally unique; `by_face` piece lists and `edge_splits` parameter lists are
587/// MERGED per key so `fragment_face` (which takes the first `by_face` match) and
588/// `apply_edge_splits` (which collects one entry per edge) see every cut.
589fn build_nary_imprint(
590    operands: &[BrepSolid],
591    options: &ImprintOptions,
592) -> Result<ImprintResultRecord, KernelRefusal> {
593    let mut section_evidence = false;
594    let mut vertices: Vec<ImprintVertex> = Vec::new();
595    let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
596    let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
597    let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
598    let mut next_vertex_id: u64 = 1;
599    let mut next_piece_id: u64 = 1;
600
601    for i in 0..operands.len() {
602        for j in (i + 1)..operands.len() {
603            let pair = build_imprints(&operands[i], &operands[j], options)?;
604            let remap = |operand: u8| -> u8 {
605                if operand == 0 {
606                    i as u8
607                } else {
608                    j as u8
609                }
610            };
611
612            section_evidence = section_evidence || pair.section_evidence;
613            let mut vertex_map: HashMap<u64, u64> = HashMap::default();
614            for vertex in &pair.vertices {
615                let global = next_vertex_id;
616                next_vertex_id += 1;
617                vertex_map.insert(vertex.id, global);
618                vertices.push(ImprintVertex {
619                    id: global,
620                    point: vertex.point,
621                });
622            }
623
624            let mut piece_map: HashMap<u64, u64> = HashMap::default();
625            for piece in &pair.pieces {
626                let global = next_piece_id;
627                next_piece_id += 1;
628                piece_map.insert(piece.id, global);
629                let mut mapped = piece.clone();
630                mapped.id = global;
631                mapped.start_vertex_id = *vertex_map
632                    .get(&piece.start_vertex_id)
633                    .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
634                mapped.end_vertex_id = *vertex_map
635                    .get(&piece.end_vertex_id)
636                    .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
637                for pcurve in &mut mapped.pcurves {
638                    pcurve.operand = remap(pcurve.operand);
639                }
640                mapped.support_faces = [
641                    FaceKey {
642                        operand: remap(piece.support_faces[0].operand),
643                        face_id: piece.support_faces[0].face_id,
644                    },
645                    FaceKey {
646                        operand: remap(piece.support_faces[1].operand),
647                        face_id: piece.support_faces[1].face_id,
648                    },
649                ];
650                pieces.push(mapped);
651            }
652
653            for entry in &pair.by_face {
654                let list = by_face
655                    .entry((remap(entry.operand), entry.face_id))
656                    .or_default();
657                for piece_id in &entry.piece_ids {
658                    let global = *piece_map.get(piece_id).ok_or_else(|| {
659                        KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: by_face references unknown piece")
660                    })?;
661                    list.push(global);
662                }
663            }
664
665            for split in &pair.edge_splits {
666                edge_splits
667                    .entry((remap(split.operand), split.edge_id))
668                    .or_default()
669                    .extend(split.parameters.iter().copied());
670            }
671        }
672    }
673
674    Ok(ImprintResultRecord {
675        vertices,
676        pieces,
677        tangent_nodes: Vec::new(),
678        barrier_edges: Vec::new(),
679        section_evidence,
680        by_face: by_face
681            .into_iter()
682            .map(|((operand, face_id), piece_ids)| FaceImprints {
683                operand,
684                face_id,
685                piece_ids,
686            })
687            .collect(),
688        edge_splits: edge_splits
689            .into_iter()
690            .map(|((operand, edge_id), parameters)| EdgeSplitRecord {
691                operand,
692                edge_id,
693                parameters,
694            })
695            .collect(),
696    })
697}
698
699/// N-ary union / intersect / subtract of `operands` in ONE imprint+fragment
700/// pass (Golovanov T4.4).  Additive: does not touch the binary path.
701///
702/// - `Union`   : the boundary of `∪ operands`.
703/// - `Intersect`: the boundary of `∩ operands`.
704/// - `Subtract`: `operands[0]` minus the union of `operands[1..]`.
705///
706/// `N = 1` returns the sole operand; `N = 2` runs the same general pipeline and
707/// reproduces the binary result (same volume, valid).  Returns a clear `Err`
708/// when the operands cannot assemble into a closed, valid solid.
709pub fn boolean_operation_nary(
710    operands: &[BrepSolid],
711    operation: BooleanOperation,
712) -> Result<BrepSolid, KernelRefusal> {
713    if operands.is_empty() {
714        return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: no operands provided"));
715    }
716    if operands.len() == 1 {
717        return Ok(operands[0].clone());
718    }
719    if operands.len() > 255 {
720        return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: at most 255 operands supported"));
721    }
722    let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
723    let options = BooleanOptions::default();
724
725    // One tolerance policy for the whole set: the largest-scale operand's policy
726    // dominates (every derived band is monotonic in scale), matching how the
727    // binary path takes `for_pair` = max of the two scales.
728    let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
729    for operand in &operands[1..] {
730        let candidate = KernelTolerances::for_solid(operand, options.tolerance);
731        if candidate.sew_search > policy.sew_search {
732            policy = candidate;
733        }
734    }
735    policy.check().or_input(KernelStage::Collect, "tolerances")?;
736    let tolerance = policy.model;
737
738    // Per-operand fuse-first healing, exactly as the binary path (a no-op on
739    // clean input, so clean operands are untouched).
740    let mut healed = operands.to_vec();
741    for operand in &mut healed {
742        crate::heal::heal_operands(operand, &policy).or_refuse(KernelStage::Validate, "heal_operands")?;
743        // Same operand normalization as the binary path: seam edges for
744        // seamless full-period band faces.
745        normalize_operand_band_seams(operand)?;
746    }
747
748    // 1. Imprint every pair into one global imprint.
749    let mut imprint_options = options.imprint.clone();
750    imprint_options.tolerance = tolerance;
751    let imprint = build_nary_imprint(&healed, &imprint_options)?;
752
753    // 2. Split every operand's edges, then fragment its faces, by that imprint.
754    let mut split = Vec::with_capacity(healed.len());
755    for (index, operand) in healed.iter().enumerate() {
756        split.push(apply_edge_splits(operand, index as u8, &imprint)?);
757    }
758    let mut fragments = Vec::with_capacity(split.len());
759    for (index, solid) in split.iter().enumerate() {
760        fragments.push(fragment_solid(solid, index as u8, &imprint)?);
761    }
762
763    // 3. Select fragments in one pass against the set of other operands.
764    let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
765
766    // 4. Assemble + normalize + validate, mirroring the binary tail.
767    let solids: HashMap<u8, &BrepSolid> = split
768        .iter()
769        .enumerate()
770        .map(|(index, solid)| (index as u8, solid))
771        .collect();
772    let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
773    let solid = if options.merge_coplanar_faces
774        && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
775    {
776        let merged = merge_same_surface_faces_excluding(
777            &solid,
778            tolerance,
779            &options.keep_unmerged_name_substrs,
780        )?;
781        merge_curve_continuation_edges(&merged, tolerance).or_refuse(KernelStage::Validate, "merge_curve_continuation_edges")?
782    } else {
783        solid
784    };
785    let validation = solid.validate_detailed(&policy);
786    if !validation.issues.is_empty() {
787        return Err(KernelRefusal::new(
788            RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
789            KernelStage::Validate,
790            format!(
791            "boolean_operation_nary: invalid result: {:?}",
792            validation.issues
793        )));
794    }
795    if debug {
796        if let Ok(report) =
797            crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
798        {
799            if report.is_flagged() {
800                eprintln!(
801                    "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
802                    report.disagreement_rate,
803                    report.disagreements.len(),
804                    report.considered,
805                    report.sample_disagreement()
806                );
807            }
808        }
809    }
810    Ok(solid)
811}
812
813/// TANGENT-NODE ATTRIBUTION.
814///
815/// The imprint's second-order filter decides which tangential contacts are
816/// isolated NODES the arrangement can carve, and marches those instead of
817/// refusing. It cannot decide a THIRD-order singularity — where the transverse
818/// branch is tangent to a contact curve, every second-order quantity vanishes
819/// together and the contact reads as a clean saddle at every stopping rule
820/// (`imprint/tangent_contact.rs`). The assembly is the authority on whether an
821/// admitted node was really imprintable, so a tear downstream of one is
822/// reported AGAINST THE NODE rather than as an anonymous degeneracy: same
823/// refusal class the whole tangent-node family carries, naming the point.
824///
825/// Only classes that are ALREADY perturbation-eligible are re-attributed, so
826/// the retry gate sees exactly what it sees today — this changes what a
827/// refusal SAYS, never which refusals are retried.
828fn attribute_to_tangent_node(error: KernelRefusal, nodes: &[Vec3]) -> KernelRefusal {
829    let Some(node) = nodes.first() else {
830        return error;
831    };
832    if !error.class.perturbation_eligible()
833        || matches!(error.class, RefusalClass::TangentNodeSingularity)
834    {
835        return error;
836    }
837    KernelRefusal::new(
838        RefusalClass::TangentNodeSingularity,
839        error.stage,
840        format!(
841            "boolean: unsupported singular/tangent-node surface intersection \
842             at the tangent node at ({:.6},{:.6},{:.6}): the section through it did not \
843             assemble into a closed shell",
844            node.x, node.y, node.z
845        ),
846    )
847}
848
849pub fn boolean_operation_with_diagnostics(
850    first: &BrepSolid,
851    second: &BrepSolid,
852    operation: BooleanOperation,
853    options: &BooleanOptions,
854) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
855    let mut tangent_nodes = Vec::new();
856    boolean_pipeline(first, second, operation, options, &mut tangent_nodes)
857        .map_err(|error| attribute_to_tangent_node(error, &tangent_nodes))
858}
859
860fn boolean_pipeline(
861    first: &BrepSolid,
862    second: &BrepSolid,
863    operation: BooleanOperation,
864    options: &BooleanOptions,
865    tangent_nodes: &mut Vec<Vec3>,
866) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
867    let policy = options
868        .tolerances
869        .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
870    policy.check().or_input(KernelStage::Collect, "tolerances")?;
871    let tolerance = policy.model;
872
873    // Fuse-first operand healing (Lever A): before any intersection touches the
874    // operands, snap each one's near-coincident / off-plane vertices to exact
875    // and re-anchor the incident edge curves onto them, so noisy
876    // near-degenerate input (points that should coincide but drifted, vertices
877    // a few microns off a planar face) cannot tip a fixed downstream band over
878    // the edge.  Healing is per-operand, validate-gated, and — because a clean
879    // operand has no vertices within `heal_tol` and none off its planes — a
880    // no-op that leaves the boolean output byte-identical on clean inputs.
881    let mut first_owned = first.clone();
882    let mut second_owned = second.clone();
883    crate::heal::heal_operands(&mut first_owned, &policy).or_refuse(KernelStage::Validate, "heal_operands")?;
884    crate::heal::heal_operands(&mut second_owned, &policy).or_refuse(KernelStage::Validate, "heal_operands")?;
885    // Seamless full-period band faces (STEP import) break the seam-aware
886    // imprint/arrangement machinery; normalize them to the seam-carrying
887    // topology native periodic faces use. No-op on operands without such
888    // faces. See `insert_periodic_band_seam_edges`.
889    normalize_operand_band_seams(&mut first_owned)?;
890    normalize_operand_band_seams(&mut second_owned)?;
891    let first = &first_owned;
892    let second = &second_owned;
893
894    let mut diagnostics = KernelDiagnostics::default();
895    let operation_started = Instant::now();
896    diagnostics.count_n(
897        "collect.faces",
898        first
899            .shells
900            .iter()
901            .chain(&second.shells)
902            .map(|shell| shell.faces.len() as u64)
903            .sum(),
904    );
905    diagnostics.measure_max("tolerance.model", policy.model);
906    diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
907
908    let mut imprint_options = options.imprint.clone();
909    imprint_options.tolerance = tolerance;
910    let stage_started = Instant::now();
911    let imprint = build_imprints(first, second, &imprint_options)?;
912    tangent_nodes.extend_from_slice(&imprint.tangent_nodes);
913    if std::env::var("BREP_DEBUG_BOOL").is_ok() {
914        for piece in &imprint.pieces {
915            let start = piece.curve.evaluate(piece.t0);
916            let end = piece.curve.evaluate(piece.t1);
917            eprintln!(
918                "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
919                piece.id, piece.support_faces, piece.t0, piece.t1, start, end
920            );
921        }
922        for entry in &imprint.by_face {
923            eprintln!(
924                "by_face operand={} face={} pieces={:?}",
925                entry.operand, entry.face_id, entry.piece_ids
926            );
927        }
928        for split in &imprint.edge_splits {
929            eprintln!(
930                "edge_split operand={} edge={} params={:?}",
931                split.operand, split.edge_id, split.parameters
932            );
933        }
934    }
935    diagnostics.measure_max(
936        "timing.imprint_ms",
937        stage_started.elapsed().as_secs_f64() * 1_000.0,
938    );
939    diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
940    diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
941    diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
942    let stage_started = Instant::now();
943    let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
944    let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
945    diagnostics.measure_max(
946        "timing.edge_split_ms",
947        stage_started.elapsed().as_secs_f64() * 1_000.0,
948    );
949    let stage_started = Instant::now();
950    let fragments_a = fragment_solid(&split_first, 0, &imprint)?;
951    let fragments_b = fragment_solid(&split_second, 1, &imprint)?;
952    diagnostics.measure_max(
953        "timing.fragment_ms",
954        stage_started.elapsed().as_secs_f64() * 1_000.0,
955    );
956    diagnostics.count_n(
957        "fragment.candidates",
958        (fragments_a.len() + fragments_b.len()) as u64,
959    );
960    // Operand surface samples for the LEGITIMATE-EMPTY adjudication below:
961    // every fragment test point is a 3D point ON its operand's boundary and
962    // inside its face trim, so they witness where the operands' materials sit
963    // relative to each other without extra geometry work.
964    let a_surface_samples: Vec<Vec3> = fragments_a
965        .iter()
966        .flat_map(|fragment| {
967            std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
968        })
969        .collect();
970    let b_surface_samples: Vec<Vec3> = fragments_b
971        .iter()
972        .flat_map(|fragment| {
973            std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
974        })
975        .collect();
976    let stage_started = Instant::now();
977    let mut barrier_edges: HashSet<(u8, u64)> = imprint
978        .pieces
979        .iter()
980        .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
981        .collect();
982    barrier_edges.extend(imprint.barrier_edges.iter().copied());
983    // The barrier set is keyed on ORIGINAL edge ids, but a barrier edge the
984    // imprint also SPLIT (rotated equator-tangency: the overlapped cap ring
985    // gains a junction at the sphere-seam crossing) reaches fragment
986    // selection as its minted sub-edge ids. Remap the barrier through the
987    // split ledger — keeping the originals too for unsplit references.
988    for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
989        let minted: Vec<(u8, u64)> = barrier_edges
990            .iter()
991            .filter(|(barrier_operand, _)| *barrier_operand == operand)
992            .filter_map(|(_, edge_id)| split_map.get(edge_id))
993            .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
994            .collect();
995        barrier_edges.extend(minted);
996    }
997    let selected = select_fragments(
998        fragments_a,
999        fragments_b,
1000        first,
1001        second,
1002        operation,
1003        tolerance,
1004        &barrier_edges,
1005    )?;
1006    diagnostics.measure_max(
1007        "timing.select_ms",
1008        stage_started.elapsed().as_secs_f64() * 1_000.0,
1009    );
1010    diagnostics.count_n("select.fragments", selected.len() as u64);
1011    // LEGITIMATE-EMPTY RESULTS: an intersect of DISJOINT operands (or a
1012    // subtract whose left operand is entirely CONSUMED by the right) selects
1013    // zero fragments, and assembly used to refuse with "operation produced no
1014    // boundary faces" — 384 of the 474 post-wrapped-band pool errors were
1015    // this, not bugs. Empty is only blessed on TWO independent proofs:
1016    //   (a) the imprint recorded NO section evidence (no accepted pierce
1017    //       seed, no traced SSI branch, no minted piece) — a silently-lost
1018    //       section (the trial-489 class) always leaves upstream evidence
1019    //       even when every downstream sub-segment is clipped away, while
1020    //       pure material disjointness leaves none (verified: 489's
1021    //       recreated silent-miss state keeps erroring; per-face surface
1022    //       samples ALONE missed its 235 mm³ pocket, which is why (a) is
1023    //       required and sampling alone was rejected);
1024    //   (b) the operands' surface samples agree (every fragment test point
1025    //       classified against the other solid):
1026    //       intersect — no sample of either operand strictly inside the
1027    //       other; subtract — no left-operand sample strictly outside the
1028    //       right.
1029    // Contact/graze pairs (evidence exists, material still disjoint) stay
1030    // errors — conservative by design. Escape hatch: BREP_EMPTY_BOOLEAN=0
1031    // restores the unconditional error.
1032    if selected.is_empty()
1033        && !imprint.section_evidence
1034        && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
1035    {
1036        let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
1037            for &sample in samples {
1038                if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::In {
1039                    return Ok(true);
1040                }
1041            }
1042            Ok(false)
1043        };
1044        let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
1045            for &sample in samples {
1046                if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::Out {
1047                    return Ok(true);
1048                }
1049            }
1050            Ok(false)
1051        };
1052        let legitimate = match operation {
1053            BooleanOperation::Intersect => {
1054                !strictly_inside(&a_surface_samples, second)?
1055                    && !strictly_inside(&b_surface_samples, first)?
1056            }
1057            BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
1058            BooleanOperation::Union => false,
1059        };
1060        if legitimate {
1061            diagnostics.event(
1062                DiagnosticSeverity::Info,
1063                KernelStage::Select,
1064                "boolean.empty_result",
1065                format!("{operation:?} of witnessed-non-overlapping operands is empty"),
1066            );
1067            diagnostics.measure_max(
1068                "timing.total_ms",
1069                operation_started.elapsed().as_secs_f64() * 1_000.0,
1070            );
1071            return Ok(KernelOutcome {
1072                value: BrepSolid {
1073                    id: 0,
1074                    vertices: Vec::new(),
1075                    edges: Vec::new(),
1076                    shells: Vec::new(),
1077                    genus: 0,
1078                },
1079                diagnostics,
1080            });
1081        }
1082    }
1083    let solids = [(0, &split_first), (1, &split_second)]
1084        .into_iter()
1085        .collect::<HashMap<_, _>>();
1086    let stage_started = Instant::now();
1087    let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
1088    diagnostics.measure_max(
1089        "timing.assemble_ms",
1090        stage_started.elapsed().as_secs_f64() * 1_000.0,
1091    );
1092    let stage_started = Instant::now();
1093    if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
1094        let issues = solid.validate();
1095        eprintln!(
1096            "pre-merge validation: {} issue(s){}",
1097            issues.len(),
1098            issues
1099                .first()
1100                .map(|issue| format!(" — first: {}", issue.message))
1101                .unwrap_or_default()
1102        );
1103    }
1104    let solid = if options.merge_coplanar_faces
1105        && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
1106    {
1107        let merged = merge_same_surface_faces_excluding(
1108            &solid,
1109            tolerance,
1110            &options.keep_unmerged_name_substrs,
1111        )?;
1112        // Face merging can make previously separate collinear boundary
1113        // segments incident to the same pair of faces. Run continuation
1114        // cleanup again, matching the post-merge normalization performed by
1115        // the former assembly pipeline.
1116        merge_curve_continuation_edges(&merged, tolerance).or_refuse(KernelStage::Validate, "merge_curve_continuation_edges")?
1117    } else {
1118        solid
1119    };
1120    diagnostics.measure_max(
1121        "timing.face_merge_ms",
1122        stage_started.elapsed().as_secs_f64() * 1_000.0,
1123    );
1124    let validation = solid.validate_detailed(&policy);
1125    diagnostics.count_n("validate.issues", validation.issues.len() as u64);
1126    diagnostics.count_n(
1127        "validate.wire_warnings",
1128        validation.wire_warnings.len() as u64,
1129    );
1130    diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
1131    for warning in &validation.wire_warnings {
1132        diagnostics.event(
1133            DiagnosticSeverity::Warning,
1134            KernelStage::Validate,
1135            "validate.uv_wire",
1136            warning.message.clone(),
1137        );
1138    }
1139    for issue in &validation.issues {
1140        diagnostics.event(
1141            DiagnosticSeverity::Error,
1142            KernelStage::Validate,
1143            "validate.brep",
1144            issue.message.clone(),
1145        );
1146    }
1147    diagnostics.measure_max(
1148        "timing.total_ms",
1149        operation_started.elapsed().as_secs_f64() * 1_000.0,
1150    );
1151    if !validation.issues.is_empty() {
1152        return Err(KernelRefusal::new(
1153            RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
1154            KernelStage::Validate,
1155            format!(
1156            "boolean_operation: invalid result: {:?}",
1157            validation.issues
1158        )));
1159    }
1160    // Optional semantic oracle (off by default, no perf hit): under
1161    // BREP_DEBUG_BOOL, cross-check the well-formed result against the CSG
1162    // point-membership expectation and scan for residual coincident-but-unwelded
1163    // geometry. Purely diagnostic and NON-rejecting — a statistical check must
1164    // never fail a valid op (see oracle.rs) — so it only ever warns.
1165    if std::env::var("BREP_DEBUG_BOOL").is_ok() {
1166        if let Ok(report) =
1167            crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
1168        {
1169            if report.is_flagged() {
1170                eprintln!(
1171                    "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
1172                    report.disagreement_rate,
1173                    report.disagreements.len(),
1174                    report.considered,
1175                    report.sample_disagreement()
1176                );
1177            }
1178        }
1179        let fusable_band = crate::tolerance::assembler_weld(policy.model);
1180        let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
1181        if !fusables.is_empty() {
1182            eprintln!(
1183                "[oracle] {} residual fusable(s) after weld; sample {:?}",
1184                fusables.len(),
1185                fusables.first()
1186            );
1187        }
1188    }
1189    Ok(KernelOutcome {
1190        value: solid,
1191        diagnostics,
1192    })
1193}
1194