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    apply_edge_splits, apply_edge_splits_with_map, build_imprints, build_pcurve_on_surface,
15    build_pcurve_on_surface_range,
16    fragment_solid, interpolate_curve, merge_curve_continuation_edges, merge_same_surface_faces,
17    project_point_to_curve, project_point_to_surface, solid_signed_volume, AffineTransform,
18    DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage, KernelTolerances, NurbsCurve,
19    PointClass, SolidClassifier, Vec3,
20};
21use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
22use serde::{Deserialize, Serialize};
23use web_time::Instant;
24
25thread_local! {
26    /// Running count (per thread) of one-use edges the EDGE-CONFORMANCE
27    /// repair lanes merged or bridged.  The lanes only fire on an assembly
28    /// that would otherwise refuse, so a zero delta across an operation is
29    /// an honest witness that the boolean assembled cleanly without the
30    /// repair.  Test-observable; not part of any result.
31    pub(crate) static CONFORMANCE_REPAIRS: std::cell::Cell<u64> =
32        const { std::cell::Cell::new(0) };
33}
34
35#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
36#[serde(rename_all = "lowercase")]
37pub enum BooleanOperation {
38    Union,
39    Intersect,
40    Subtract,
41}
42
43#[derive(Clone, Debug, Deserialize)]
44pub struct BooleanOptions {
45    #[serde(default = "default_tolerance")]
46    pub tolerance: f64,
47    /// Optional complete accuracy/search policy.  `tolerance` remains the
48    /// backwards-compatible model-identity shorthand when this is absent.
49    #[serde(default)]
50    pub tolerances: Option<KernelTolerances>,
51    #[serde(default)]
52    pub imprint: ImprintOptions,
53    /// Coalesce adjacent result fragments after assembly. This defaults to
54    /// true to preserve the kernel's normal clean-boundary behavior.
55    #[serde(default = "default_true")]
56    pub merge_coplanar_faces: bool,
57}
58
59fn default_tolerance() -> f64 {
60    1e-7
61}
62
63fn default_true() -> bool {
64    true
65}
66
67impl Default for BooleanOptions {
68    fn default() -> Self {
69        Self {
70            tolerance: default_tolerance(),
71            tolerances: None,
72            imprint: ImprintOptions::default(),
73            merge_coplanar_faces: true,
74        }
75    }
76}
77
78mod select;
79mod assemble;
80mod rim;
81#[cfg(test)]
82mod tests;
83
84use rim::*;
85use select::*;
86// The non-API `assemble` helpers (Assembler, SourceEdge, conform/repair/polish
87// internals) are consumed only by this module's own `#[cfg(test)] mod tests`.
88#[cfg(test)]
89use assemble::*;
90pub(crate) use assemble::{
91    assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
92    edge_interior_lies_on, finalize_assembled_solid,
93};
94// Preserve the pre-split `crate::boolean::apply_assembly_heal_chain` path.
95// Currently referenced only inside `assemble`, so the re-export is unused.
96#[allow(unused_imports)]
97pub(crate) use assemble::apply_assembly_heal_chain;
98
99pub fn boolean_operation(
100    first: &BrepSolid,
101    second: &BrepSolid,
102    operation: BooleanOperation,
103    options: &BooleanOptions,
104) -> Result<BrepSolid, String> {
105    match boolean_operation_with_diagnostics(first, second, operation, options) {
106        Ok(outcome) => Ok(outcome.value),
107        Err(error) => {
108            // Perturbation-fallback lane (Simulation of Simplicity). The exact
109            // arrangement only errs on a DEGENERACY here (coincident /
110            // near-tangent carrier faces make it structurally inconsistent) —
111            // e.g. a partial torus whose equatorial circles lie exactly in a box
112            // face. Retry once on a rigidly perturbed second operand; accept only
113            // an oracle-clean, volume-consistent result. See
114            // `perturbation_retry`.
115            if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
116                || !is_degeneracy_error(&error)
117            {
118                return Err(error);
119            }
120            match perturbation_retry(first, second, operation, options) {
121                Some(solid) => Ok(solid),
122                None => Err(error),
123            }
124        }
125    }
126}
127
128/// Whether a boolean error looks like an arrangement DEGENERACY worth a
129/// perturbation retry (as opposed to a legitimate conservative refusal —
130/// empty/disjoint operands — which perturbation cannot and must not "fix").
131fn is_degeneracy_error(message: &str) -> bool {
132    message.contains("invalid topology")
133        || message.contains("non-integral genus")
134        || message.contains("non-positive volume")
135        || message.contains("open edges")
136        // Tangent-node / singular surface intersections (e.g. two equal-radius
137        // cylinders whose axes intersect, a uniform-radius tube corner): the SoS
138        // perturbation breaks the exact tangency into a clean transversal crossing.
139        || message.contains("singular/tangent-node")
140}
141
142/// Deterministic perturbation-fallback (Simulation of Simplicity), the textbook
143/// CAD answer to an exact/near-tangent arrangement degeneracy (see
144/// `BOOLEAN-NEXT-STEPS.md`). Rigidly TRANSLATE the second operand by a tiny
145/// epsilon so the degenerate contact becomes a clean transversal crossing, redo
146/// the boolean, and accept the result ONLY if it is (a) topologically valid —
147/// guaranteed by `assemble`'s own gate returning `Ok` — (b) semantically equal to
148/// the exact CSG of the ORIGINAL operands (the point-classification oracle), and
149/// (c) volume-consistent with the CSG inequality bounds. A wrong perturbed result
150/// is rejected on (b)/(c), so this lane preserves the engine's fail-safe property:
151/// it can only turn a degeneracy-error into a CORRECT solid, never a wrong one.
152///
153/// INVARIANT — no recursion: this calls `boolean_operation_with_diagnostics`
154/// (the EXACT path), never `boolean_operation`, so the fallback can never
155/// re-enter itself. A future refactor must preserve that.
156fn perturbation_retry(
157    first: &BrepSolid,
158    second: &BrepSolid,
159    operation: BooleanOperation,
160    options: &BooleanOptions,
161) -> Option<BrepSolid> {
162    // Generic translation directions: each has a substantial component on ALL
163    // three axes, so whatever axis the coincidence normal lies along (an
164    // equatorial plane flush with a box face, a cap plane flush with a side
165    // face, …) at least one direction has a component that lifts the contact off
166    // exact tangency. Axis-aligned directions are deliberately excluded — a nudge
167    // parallel to the coincident plane leaves the degeneracy in place (verified:
168    // pure ±X / ±Z never rescue the flush-torus case). Fixed (not hash-seeded):
169    // fixed vectors already satisfy the plan doc's determinism intent and four
170    // diverse directions cover any axis-aligned normal.
171    const DIRECTIONS: [[f64; 3]; 4] = [
172        [0.4034, 0.7973, 0.4491],
173        [0.7973, 0.4491, 0.4034],
174        [0.4491, 0.4034, 0.7973],
175        [0.5774, -0.5774, 0.5774],
176    ];
177    // Fractions of operand scale, ordered SMALLEST-FIRST so the accepted solid
178    // carries the least geometric offset. Sized above the near-tangent sliver
179    // dead zone and below local feature size; the ladder + accept-gate make the
180    // exact magnitude non-critical (a rung that lands in a dead zone simply fails
181    // the gate and the next rung is tried).
182    const FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
183
184    let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
185    let scale = crate::tolerance::solid_scale(first)
186        .max(crate::tolerance::solid_scale(second))
187        .max(1.0);
188
189    // Original operand volumes for the CSG inequality backstop (the oracle skips
190    // points near boundaries, so a wrong thin shell hugging the flush face is
191    // invisible to it but shows up here).
192    let va = solid_signed_volume(first).ok().map(f64::abs);
193    let vb = solid_signed_volume(second).ok().map(f64::abs);
194
195    for &fraction in &FRACTIONS {
196        let magnitude = scale * fraction;
197        for dir in &DIRECTIONS {
198            let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
199            let translate = match AffineTransform::new([
200                1.0, 0.0, 0.0, offset[0], //
201                0.0, 1.0, 0.0, offset[1], //
202                0.0, 0.0, 1.0, offset[2], //
203                0.0, 0.0, 0.0, 1.0,
204            ]) {
205                Ok(transform) => transform,
206                Err(_) => continue,
207            };
208            let moved = match crate::transform_brep(second, translate, false) {
209                Ok(solid) => solid,
210                Err(_) => continue,
211            };
212            let candidate =
213                match boolean_operation_with_diagnostics(first, &moved, operation, options) {
214                    Ok(outcome) => outcome.value,
215                    Err(_) => continue,
216                };
217            // A perturbation must not "rescue" a case into emptiness.
218            if candidate.shells.is_empty() {
219                continue;
220            }
221            // (c) Volume must satisfy the CSG inequality for the ORIGINAL
222            // operands (generous 1% relative slack: catches gross wrongness —
223            // doubled/halved bodies, missing lobes — without rejecting the
224            // legitimate sub-micron offset).
225            if let (Some(va), Some(vb)) = (va, vb) {
226                if let Ok(vr) = solid_signed_volume(&candidate) {
227                    if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
228                        continue;
229                    }
230                }
231            }
232            // (b) Semantic agreement with the exact CSG of the ORIGINAL
233            // operands. Reject a vacuous verdict (skip-band swallowed every
234            // sample so nothing was actually checked).
235            match crate::oracle::boolean_semantic_disagreement(
236                first, second, operation, &candidate, 400,
237            ) {
238                Ok(report) if report.considered >= 30 && !report.is_flagged() => {
239                    if debug {
240                        eprintln!(
241                            "[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
242                             magnitude={magnitude:.3e} offset={offset:?} \
243                             (oracle considered={} rate={:.4})",
244                            report.considered, report.disagreement_rate
245                        );
246                    }
247                    return Some(candidate);
248                }
249                _ => continue,
250            }
251        }
252    }
253    if debug {
254        eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
255    }
256    None
257}
258
259/// CSG volume inequality bounds (no extra boolean needed) with 1% relative slack.
260/// `va`, `vb`, `vr` are absolute volumes of operand A, operand B, and the result.
261fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
262    let slack = 1e-2 * (va + vb).max(1.0);
263    match operation {
264        // max(A,B) ≤ A∪B ≤ A+B
265        BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
266        // A−B ≤ A (and ≥ 0)
267        BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
268        // A∩B ≤ min(A,B)
269        BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
270    }
271}
272
273// ---------------------------------------------------------------------------
274// N-ary booleans (Golovanov T4.4)
275//
276// Union / intersect / subtract of N solids in ONE imprint+fragment pass.  This
277// is a NEW ADDITIVE entry point: the binary `boolean_operation` above is left
278// byte-identical.  The pipeline mirrors the binary one but generalizes the two
279// operand-specific stages to N operands:
280//
281//   1. Imprint EVERY unordered pair (i, j) so every mutual intersection edge is
282//      present on both operands' faces, and accumulate all pairs into ONE global
283//      imprint (operand indices remapped to 0..N, piece/vertex ids made globally
284//      unique, per-face piece lists and per-edge split parameters merged).
285//   2. Split + fragment each operand by that single global imprint, so each
286//      operand's faces are cut by ALL of its intersections at once.
287//   3. Select fragments in one pass: the per-fragment keep decision generalizes
288//      from "vs one other solid" to "vs the SET of other solids" (see
289//      `nary_keep`).  Because every pair is imprinted, a fragment's interior lies
290//      wholly inside or wholly outside each other operand, so a single interior
291//      test point per (fragment, other-solid) is an exact membership verdict.
292//   4. Assemble with the same machinery (`assemble_fragments`), then merge and
293//      validate exactly as the binary path does.
294// ---------------------------------------------------------------------------
295
296/// Reverse a fragment's orientation (surface sense + every coedge), the same
297/// transform the binary Subtract path applies to the second operand's kept
298/// fragments.  Extracted so the n-ary selector can reuse it verbatim.
299fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), String> {
300    fragment.same_sense = !fragment.same_sense;
301    for loop_record in &mut fragment.loops {
302        loop_record.coedges.reverse();
303        for coedge in &mut loop_record.coedges {
304            coedge.forward = !coedge.forward;
305            coedge.pcurve = coedge.pcurve.reversed()?;
306        }
307    }
308    Ok(())
309}
310
311/// Imprint every unordered pair of operands and fold the pairwise
312/// `ImprintResultRecord`s into ONE global imprint whose operand indices are the
313/// operands' global positions (0..N).  Piece and vertex ids are re-based to be
314/// globally unique; `by_face` piece lists and `edge_splits` parameter lists are
315/// MERGED per key so `fragment_face` (which takes the first `by_face` match) and
316/// `apply_edge_splits` (which collects one entry per edge) see every cut.
317fn build_nary_imprint(
318    operands: &[BrepSolid],
319    options: &ImprintOptions,
320) -> Result<ImprintResultRecord, String> {
321    let mut section_evidence = false;
322    let mut vertices: Vec<ImprintVertex> = Vec::new();
323    let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
324    let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
325    let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
326    let mut next_vertex_id: u64 = 1;
327    let mut next_piece_id: u64 = 1;
328
329    for i in 0..operands.len() {
330        for j in (i + 1)..operands.len() {
331            let pair = build_imprints(&operands[i], &operands[j], options)?;
332            let remap = |operand: u8| -> u8 {
333                if operand == 0 {
334                    i as u8
335                } else {
336                    j as u8
337                }
338            };
339
340            section_evidence = section_evidence || pair.section_evidence;
341            let mut vertex_map: HashMap<u64, u64> = HashMap::default();
342            for vertex in &pair.vertices {
343                let global = next_vertex_id;
344                next_vertex_id += 1;
345                vertex_map.insert(vertex.id, global);
346                vertices.push(ImprintVertex {
347                    id: global,
348                    point: vertex.point,
349                });
350            }
351
352            let mut piece_map: HashMap<u64, u64> = HashMap::default();
353            for piece in &pair.pieces {
354                let global = next_piece_id;
355                next_piece_id += 1;
356                piece_map.insert(piece.id, global);
357                let mut mapped = piece.clone();
358                mapped.id = global;
359                mapped.start_vertex_id = *vertex_map
360                    .get(&piece.start_vertex_id)
361                    .ok_or_else(|| "n-ary imprint: piece references unknown vertex".to_string())?;
362                mapped.end_vertex_id = *vertex_map
363                    .get(&piece.end_vertex_id)
364                    .ok_or_else(|| "n-ary imprint: piece references unknown vertex".to_string())?;
365                for pcurve in &mut mapped.pcurves {
366                    pcurve.operand = remap(pcurve.operand);
367                }
368                mapped.support_faces = [
369                    FaceKey {
370                        operand: remap(piece.support_faces[0].operand),
371                        face_id: piece.support_faces[0].face_id,
372                    },
373                    FaceKey {
374                        operand: remap(piece.support_faces[1].operand),
375                        face_id: piece.support_faces[1].face_id,
376                    },
377                ];
378                pieces.push(mapped);
379            }
380
381            for entry in &pair.by_face {
382                let list = by_face
383                    .entry((remap(entry.operand), entry.face_id))
384                    .or_default();
385                for piece_id in &entry.piece_ids {
386                    let global = *piece_map.get(piece_id).ok_or_else(|| {
387                        "n-ary imprint: by_face references unknown piece".to_string()
388                    })?;
389                    list.push(global);
390                }
391            }
392
393            for split in &pair.edge_splits {
394                edge_splits
395                    .entry((remap(split.operand), split.edge_id))
396                    .or_default()
397                    .extend(split.parameters.iter().copied());
398            }
399        }
400    }
401
402    Ok(ImprintResultRecord {
403        vertices,
404        pieces,
405        barrier_edges: Vec::new(),
406        section_evidence,
407        by_face: by_face
408            .into_iter()
409            .map(|((operand, face_id), piece_ids)| FaceImprints {
410                operand,
411                face_id,
412                piece_ids,
413            })
414            .collect(),
415        edge_splits: edge_splits
416            .into_iter()
417            .map(|((operand, edge_id), parameters)| EdgeSplitRecord {
418                operand,
419                edge_id,
420                parameters,
421            })
422            .collect(),
423    })
424}
425
426/// N-ary union / intersect / subtract of `operands` in ONE imprint+fragment
427/// pass (Golovanov T4.4).  Additive: does not touch the binary path.
428///
429/// - `Union`   : the boundary of `∪ operands`.
430/// - `Intersect`: the boundary of `∩ operands`.
431/// - `Subtract`: `operands[0]` minus the union of `operands[1..]`.
432///
433/// `N = 1` returns the sole operand; `N = 2` runs the same general pipeline and
434/// reproduces the binary result (same volume, valid).  Returns a clear `Err`
435/// when the operands cannot assemble into a closed, valid solid.
436pub fn boolean_operation_nary(
437    operands: &[BrepSolid],
438    operation: BooleanOperation,
439) -> Result<BrepSolid, String> {
440    if operands.is_empty() {
441        return Err("boolean_operation_nary: no operands provided".into());
442    }
443    if operands.len() == 1 {
444        return Ok(operands[0].clone());
445    }
446    if operands.len() > 255 {
447        return Err("boolean_operation_nary: at most 255 operands supported".into());
448    }
449    let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
450    let options = BooleanOptions::default();
451
452    // One tolerance policy for the whole set: the largest-scale operand's policy
453    // dominates (every derived band is monotonic in scale), matching how the
454    // binary path takes `for_pair` = max of the two scales.
455    let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
456    for operand in &operands[1..] {
457        let candidate = KernelTolerances::for_solid(operand, options.tolerance);
458        if candidate.sew_search > policy.sew_search {
459            policy = candidate;
460        }
461    }
462    policy.check()?;
463    let tolerance = policy.model;
464
465    // Per-operand fuse-first healing, exactly as the binary path (a no-op on
466    // clean input, so clean operands are untouched).
467    let mut healed = operands.to_vec();
468    for operand in &mut healed {
469        crate::heal::heal_operands(operand, &policy)?;
470        // Same operand normalization as the binary path: seam edges for
471        // seamless full-period band faces.
472        normalize_operand_band_seams(operand)?;
473    }
474
475    // 1. Imprint every pair into one global imprint.
476    let mut imprint_options = options.imprint.clone();
477    imprint_options.tolerance = tolerance;
478    let imprint = build_nary_imprint(&healed, &imprint_options)?;
479
480    // 2. Split every operand's edges, then fragment its faces, by that imprint.
481    let mut split = Vec::with_capacity(healed.len());
482    for (index, operand) in healed.iter().enumerate() {
483        split.push(apply_edge_splits(operand, index as u8, &imprint)?);
484    }
485    let mut fragments = Vec::with_capacity(split.len());
486    for (index, solid) in split.iter().enumerate() {
487        fragments.push(fragment_solid(solid, index as u8, &imprint)?);
488    }
489
490    // 3. Select fragments in one pass against the set of other operands.
491    let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
492
493    // 4. Assemble + normalize + validate, mirroring the binary tail.
494    let solids: HashMap<u8, &BrepSolid> = split
495        .iter()
496        .enumerate()
497        .map(|(index, solid)| (index as u8, solid))
498        .collect();
499    let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
500    let solid = if options.merge_coplanar_faces
501        && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
502    {
503        let merged = merge_same_surface_faces(&solid, tolerance)?;
504        merge_curve_continuation_edges(&merged, tolerance)?
505    } else {
506        solid
507    };
508    let validation = solid.validate_detailed(&policy);
509    if !validation.issues.is_empty() {
510        return Err(format!(
511            "boolean_operation_nary: invalid result: {:?}",
512            validation.issues
513        ));
514    }
515    if debug {
516        if let Ok(report) =
517            crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
518        {
519            if report.is_flagged() {
520                eprintln!(
521                    "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
522                    report.disagreement_rate,
523                    report.disagreements.len(),
524                    report.considered,
525                    report.sample_disagreement()
526                );
527            }
528        }
529    }
530    Ok(solid)
531}
532
533pub fn boolean_operation_with_diagnostics(
534    first: &BrepSolid,
535    second: &BrepSolid,
536    operation: BooleanOperation,
537    options: &BooleanOptions,
538) -> Result<KernelOutcome<BrepSolid>, String> {
539    let policy = options
540        .tolerances
541        .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
542    policy.check()?;
543    let tolerance = policy.model;
544
545    // Fuse-first operand healing (Lever A): before any intersection touches the
546    // operands, snap each one's near-coincident / off-plane vertices to exact
547    // and re-anchor the incident edge curves onto them, so noisy
548    // near-degenerate input (points that should coincide but drifted, vertices
549    // a few microns off a planar face) cannot tip a fixed downstream band over
550    // the edge.  Healing is per-operand, validate-gated, and — because a clean
551    // operand has no vertices within `heal_tol` and none off its planes — a
552    // no-op that leaves the boolean output byte-identical on clean inputs.
553    let mut first_owned = first.clone();
554    let mut second_owned = second.clone();
555    crate::heal::heal_operands(&mut first_owned, &policy)?;
556    crate::heal::heal_operands(&mut second_owned, &policy)?;
557    // Seamless full-period band faces (STEP import) break the seam-aware
558    // imprint/arrangement machinery; normalize them to the seam-carrying
559    // topology native periodic faces use. No-op on operands without such
560    // faces. See `insert_periodic_band_seam_edges`.
561    normalize_operand_band_seams(&mut first_owned)?;
562    normalize_operand_band_seams(&mut second_owned)?;
563    let first = &first_owned;
564    let second = &second_owned;
565
566    let mut diagnostics = KernelDiagnostics::default();
567    let operation_started = Instant::now();
568    diagnostics.count_n(
569        "collect.faces",
570        first
571            .shells
572            .iter()
573            .chain(&second.shells)
574            .map(|shell| shell.faces.len() as u64)
575            .sum(),
576    );
577    diagnostics.measure_max("tolerance.model", policy.model);
578    diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
579
580    let mut imprint_options = options.imprint.clone();
581    imprint_options.tolerance = tolerance;
582    let stage_started = Instant::now();
583    let imprint = build_imprints(first, second, &imprint_options)?;
584    if std::env::var("BREP_DEBUG_BOOL").is_ok() {
585        for piece in &imprint.pieces {
586            let start = piece.curve.evaluate(piece.t0);
587            let end = piece.curve.evaluate(piece.t1);
588            eprintln!(
589                "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
590                piece.id, piece.support_faces, piece.t0, piece.t1, start, end
591            );
592        }
593        for entry in &imprint.by_face {
594            eprintln!(
595                "by_face operand={} face={} pieces={:?}",
596                entry.operand, entry.face_id, entry.piece_ids
597            );
598        }
599        for split in &imprint.edge_splits {
600            eprintln!(
601                "edge_split operand={} edge={} params={:?}",
602                split.operand, split.edge_id, split.parameters
603            );
604        }
605    }
606    diagnostics.measure_max(
607        "timing.imprint_ms",
608        stage_started.elapsed().as_secs_f64() * 1_000.0,
609    );
610    diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
611    diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
612    diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
613    let stage_started = Instant::now();
614    let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
615    let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
616    diagnostics.measure_max(
617        "timing.edge_split_ms",
618        stage_started.elapsed().as_secs_f64() * 1_000.0,
619    );
620    let stage_started = Instant::now();
621    let fragments_a = fragment_solid(&split_first, 0, &imprint)?;
622    let fragments_b = fragment_solid(&split_second, 1, &imprint)?;
623    diagnostics.measure_max(
624        "timing.fragment_ms",
625        stage_started.elapsed().as_secs_f64() * 1_000.0,
626    );
627    diagnostics.count_n(
628        "fragment.candidates",
629        (fragments_a.len() + fragments_b.len()) as u64,
630    );
631    // Operand surface samples for the LEGITIMATE-EMPTY adjudication below:
632    // every fragment test point is a 3D point ON its operand's boundary and
633    // inside its face trim, so they witness where the operands' materials sit
634    // relative to each other without extra geometry work.
635    let a_surface_samples: Vec<Vec3> = fragments_a
636        .iter()
637        .flat_map(|fragment| {
638            std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
639        })
640        .collect();
641    let b_surface_samples: Vec<Vec3> = fragments_b
642        .iter()
643        .flat_map(|fragment| {
644            std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
645        })
646        .collect();
647    let stage_started = Instant::now();
648    let mut barrier_edges: HashSet<(u8, u64)> = imprint
649        .pieces
650        .iter()
651        .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
652        .collect();
653    barrier_edges.extend(imprint.barrier_edges.iter().copied());
654    // The barrier set is keyed on ORIGINAL edge ids, but a barrier edge the
655    // imprint also SPLIT (rotated equator-tangency: the overlapped cap ring
656    // gains a junction at the sphere-seam crossing) reaches fragment
657    // selection as its minted sub-edge ids. Remap the barrier through the
658    // split ledger — keeping the originals too for unsplit references.
659    for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
660        let minted: Vec<(u8, u64)> = barrier_edges
661            .iter()
662            .filter(|(barrier_operand, _)| *barrier_operand == operand)
663            .filter_map(|(_, edge_id)| split_map.get(edge_id))
664            .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
665            .collect();
666        barrier_edges.extend(minted);
667    }
668    let selected = select_fragments(
669        fragments_a,
670        fragments_b,
671        first,
672        second,
673        operation,
674        tolerance,
675        &barrier_edges,
676    )?;
677    diagnostics.measure_max(
678        "timing.select_ms",
679        stage_started.elapsed().as_secs_f64() * 1_000.0,
680    );
681    diagnostics.count_n("select.fragments", selected.len() as u64);
682    // LEGITIMATE-EMPTY RESULTS: an intersect of DISJOINT operands (or a
683    // subtract whose left operand is entirely CONSUMED by the right) selects
684    // zero fragments, and assembly used to refuse with "operation produced no
685    // boundary faces" — 384 of the 474 post-wrapped-band pool errors were
686    // this, not bugs. Empty is only blessed on TWO independent proofs:
687    //   (a) the imprint recorded NO section evidence (no accepted pierce
688    //       seed, no traced SSI branch, no minted piece) — a silently-lost
689    //       section (the trial-489 class) always leaves upstream evidence
690    //       even when every downstream sub-segment is clipped away, while
691    //       pure material disjointness leaves none (verified: 489's
692    //       recreated silent-miss state keeps erroring; per-face surface
693    //       samples ALONE missed its 235 mm³ pocket, which is why (a) is
694    //       required and sampling alone was rejected);
695    //   (b) the operands' surface samples agree (every fragment test point
696    //       classified against the other solid):
697    //       intersect — no sample of either operand strictly inside the
698    //       other; subtract — no left-operand sample strictly outside the
699    //       right.
700    // Contact/graze pairs (evidence exists, material still disjoint) stay
701    // errors — conservative by design. Escape hatch: BREP_EMPTY_BOOLEAN=0
702    // restores the unconditional error.
703    if selected.is_empty()
704        && !imprint.section_evidence
705        && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
706    {
707        let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, String> {
708            for &sample in samples {
709                if classify_point(sample, other, tolerance)?.class == PointClass::In {
710                    return Ok(true);
711                }
712            }
713            Ok(false)
714        };
715        let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, String> {
716            for &sample in samples {
717                if classify_point(sample, other, tolerance)?.class == PointClass::Out {
718                    return Ok(true);
719                }
720            }
721            Ok(false)
722        };
723        let legitimate = match operation {
724            BooleanOperation::Intersect => {
725                !strictly_inside(&a_surface_samples, second)?
726                    && !strictly_inside(&b_surface_samples, first)?
727            }
728            BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
729            BooleanOperation::Union => false,
730        };
731        if legitimate {
732            diagnostics.event(
733                DiagnosticSeverity::Info,
734                KernelStage::Select,
735                "boolean.empty_result",
736                format!("{operation:?} of witnessed-non-overlapping operands is empty"),
737            );
738            diagnostics.measure_max(
739                "timing.total_ms",
740                operation_started.elapsed().as_secs_f64() * 1_000.0,
741            );
742            return Ok(KernelOutcome {
743                value: BrepSolid {
744                    id: 0,
745                    vertices: Vec::new(),
746                    edges: Vec::new(),
747                    shells: Vec::new(),
748                    genus: 0,
749                },
750                diagnostics,
751            });
752        }
753    }
754    let solids = [(0, &split_first), (1, &split_second)]
755        .into_iter()
756        .collect::<HashMap<_, _>>();
757    let stage_started = Instant::now();
758    let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
759    diagnostics.measure_max(
760        "timing.assemble_ms",
761        stage_started.elapsed().as_secs_f64() * 1_000.0,
762    );
763    let stage_started = Instant::now();
764    if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
765        let issues = solid.validate();
766        eprintln!(
767            "pre-merge validation: {} issue(s){}",
768            issues.len(),
769            issues
770                .first()
771                .map(|issue| format!(" — first: {}", issue.message))
772                .unwrap_or_default()
773        );
774    }
775    let solid = if options.merge_coplanar_faces
776        && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
777    {
778        let merged = merge_same_surface_faces(&solid, tolerance)?;
779        // Face merging can make previously separate collinear boundary
780        // segments incident to the same pair of faces. Run continuation
781        // cleanup again, matching the post-merge normalization performed by
782        // the former assembly pipeline.
783        merge_curve_continuation_edges(&merged, tolerance)?
784    } else {
785        solid
786    };
787    diagnostics.measure_max(
788        "timing.face_merge_ms",
789        stage_started.elapsed().as_secs_f64() * 1_000.0,
790    );
791    let validation = solid.validate_detailed(&policy);
792    diagnostics.count_n("validate.issues", validation.issues.len() as u64);
793    diagnostics.count_n(
794        "validate.wire_warnings",
795        validation.wire_warnings.len() as u64,
796    );
797    diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
798    for warning in &validation.wire_warnings {
799        diagnostics.event(
800            DiagnosticSeverity::Warning,
801            KernelStage::Validate,
802            "validate.uv_wire",
803            warning.message.clone(),
804        );
805    }
806    for issue in &validation.issues {
807        diagnostics.event(
808            DiagnosticSeverity::Error,
809            KernelStage::Validate,
810            "validate.brep",
811            issue.message.clone(),
812        );
813    }
814    diagnostics.measure_max(
815        "timing.total_ms",
816        operation_started.elapsed().as_secs_f64() * 1_000.0,
817    );
818    if !validation.issues.is_empty() {
819        return Err(format!(
820            "boolean_operation: invalid result: {:?}",
821            validation.issues
822        ));
823    }
824    // Optional semantic oracle (off by default, no perf hit): under
825    // BREP_DEBUG_BOOL, cross-check the well-formed result against the CSG
826    // point-membership expectation and scan for residual coincident-but-unwelded
827    // geometry. Purely diagnostic and NON-rejecting — a statistical check must
828    // never fail a valid op (see oracle.rs) — so it only ever warns.
829    if std::env::var("BREP_DEBUG_BOOL").is_ok() {
830        if let Ok(report) =
831            crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
832        {
833            if report.is_flagged() {
834                eprintln!(
835                    "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
836                    report.disagreement_rate,
837                    report.disagreements.len(),
838                    report.considered,
839                    report.sample_disagreement()
840                );
841            }
842        }
843        let fusable_band = crate::tolerance::assembler_weld(policy.model);
844        let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
845        if !fusables.is_empty() {
846            eprintln!(
847                "[oracle] {} residual fusable(s) after weld; sample {:?}",
848                fusables.len(),
849                fusables.first()
850            );
851        }
852    }
853    Ok(KernelOutcome {
854        value: solid,
855        diagnostics,
856    })
857}
858