Skip to main content

brep_kernel/csg/boolean/
mod.rs

1pub(crate) mod profile;
2use crate::arrangement::Vec2;
3use crate::classification::{classify_point, parameter_point_in_face, PolygonClass};
4use crate::fragment::{FaceFragmentRecord, FragmentEdgeSource};
5use crate::imprint::{
6    EdgeSplitRecord, FaceImprints, FaceKey, ImprintOptions, ImprintPieceRecord,
7    ImprintResultRecord, ImprintVertex,
8};
9use crate::tolerance::{assembler_weld, commit_weld};
10use crate::topology::{
11    adaptive_coedge_error, BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord,
12    ShellRecord, VertexRecord,
13};
14use crate::{
15    KernelRefusal, OrRefuse, RefusalClass,
16    apply_edge_splits, apply_edge_splits_with_map, build_imprints, build_pcurve_on_surface,
17    build_pcurve_on_surface_range, classify_surface_pair,
18    fragment_solid, interpolate_curve, merge_curve_continuation_edges,
19    merge_same_surface_faces_excluding, merge_same_surface_faces_scoped,
20    project_point_to_curve, project_point_to_surface, solid_signed_volume, solid_signed_volume_for_sign, AffineTransform,
21    DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage, KernelTolerances, NurbsCurve,
22    PointClass, SolidClassifier, SurfacePairRelation, Vec3,
23};
24use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
25use serde::{Deserialize, Serialize};
26use web_time::Instant;
27
28thread_local! {
29    /// Running count (per thread) of one-use edges the EDGE-CONFORMANCE
30    /// repair lanes merged or bridged.  The lanes only fire on an assembly
31    /// that would otherwise refuse, so a zero delta across an operation is
32    /// an honest witness that the boolean assembled cleanly without the
33    /// repair.  Test-observable; not part of any result.
34    pub(crate) static CONFORMANCE_REPAIRS: std::cell::Cell<u64> =
35        const { std::cell::Cell::new(0) };
36}
37
38#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
39#[serde(rename_all = "lowercase")]
40pub enum BooleanOperation {
41    Union,
42    Intersect,
43    Subtract,
44}
45
46#[derive(Clone, Debug, Deserialize)]
47pub struct BooleanOptions {
48    #[serde(default = "default_tolerance")]
49    pub tolerance: f64,
50    /// Optional complete accuracy/search policy.  `tolerance` remains the
51    /// backwards-compatible model-identity shorthand when this is absent.
52    #[serde(default)]
53    pub tolerances: Option<KernelTolerances>,
54    #[serde(default)]
55    pub imprint: ImprintOptions,
56    /// Coalesce adjacent result fragments after assembly. This defaults to
57    /// true to preserve the kernel's normal clean-boundary behavior.
58    #[serde(default = "default_true")]
59    pub merge_coplanar_faces: bool,
60    /// Face-name substrings that PIN a result face out of the coplanar/cosurface
61    /// merge (only consulted when [`Self::merge_coplanar_faces`] is on). A face whose
62    /// `name` CONTAINS any of these is emitted unchanged instead of being
63    /// coalesced with a mergeable neighbour; faces matching none merge exactly as
64    /// before. Default empty — no caller sees a behavior change unless it opts
65    /// in. Sheet metal uses this to keep every thickness/side wall face per
66    /// outline segment in the folded solid (so a flange can still attach to a
67    /// specific segment: fusing two collinear thickness faces would erase the
68    /// segment boundary).
69    #[serde(default)]
70    pub keep_unmerged_name_substrs: Vec<String>,
71    /// On by default since the 2026-10-10 census read the 94-case manifest
72    /// unchanged with it; `BREP_UNTOUCHED_PASSTHROUGH` overrides either way
73    /// (`0` off, anything else on).
74    #[serde(default = "default_true")]
75    pub untouched_passthrough: bool,
76    /// On by default since the 2026-10-10 census;
77    /// `BREP_DISJOINT_FAST_PATH` overrides either way (`0` off, anything
78    /// else on).
79    #[serde(default = "default_true")]
80    pub disjoint_fast_path: bool,
81    /// Lane X row 12: when, in at least two axes, one operand's control-net
82    /// box ends at or before the other's begins (exactly, no band), the
83    /// operands can share no interior and touch on a line or at a point: the
84    /// disjoint path's answers hold. Every control weight must be positive
85    /// (the hull property), both operands plain solids, and the caller must
86    /// say what follows (`seam_reseat_later_references` is `Some`), so a
87    /// caller that cannot keeps today's ids. On by default since the row 12
88    /// census; `BREP_CONTACT_FAST_PATH` overrides either way (`0` off,
89    /// anything else on).
90    #[serde(default = "default_true")]
91    pub contact_fast_path: bool,
92    /// OFF by default: the 94-case census read unchanged with it, but the
93    /// kernel battery's `offset_subtract_fixture` pins a result whose
94    /// untouched planar face carries an open trim joint of 8.7e-4 from its
95    /// coarse polyline operand, which the whole-solid joints pass closes and
96    /// the scoped pass leaves as the operand had it; and the row buys no
97    /// measurable time (the record of 2026-10-10). `BREP_TOUCHED_ASSEMBLY`
98    /// overrides either way (`0` off, anything else on).
99    #[serde(default)]
100    pub touched_assembly: bool,
101    /// On by default since the 2026-10-11 census (the whole kernel battery,
102    /// the 94-case manifest and `hlr_tests` green with it on, every manifest
103    /// fragment the sector decides agreeing with the ray cast);
104    /// `BREP_SECTOR_CLASS` overrides either way (`0`/`off` the escape hatch,
105    /// `check` for the differential run that logs disagreements to
106    /// `BREP_SECTOR_CENSUS`, anything else on). Appended at the end of the
107    /// struct (2026-10-11).
108    #[serde(default = "default_true")]
109    pub sector_classification: bool,
110    /// Non-manifold plan 1.6: trim a declared Sheet by a Solid with Intersect.
111    /// On after the 2026-10-14 Sheet census and whole-kernel qualification.
112    /// BREP_SHEET_TRIM overrides this option (1 enables, 0 disables).
113    #[serde(default = "default_true")]
114    pub sheet_trim: bool,
115    /// Preserve the closed operand regions at a lower-dimensional union
116    /// contact as explicit cells. Opt-in pending the Cellular census.
117    /// BREP_KEEP_CELLS overrides this option (1 enables, 0 disables).
118    #[serde(default)]
119    pub keep_cells: bool,
120    /// The references later features hold (their `inputParams` text), when
121    /// the caller knows them: `Some` admits the operand seam re-seat
122    /// (`BREP_SEAM_RESEAT`, lane X row 11b) for every cylinder wall none of
123    /// them names; `None` (the caller cannot tell) never re-seats.
124    #[serde(default)]
125    pub seam_reseat_later_references: Option<Vec<String>>,
126}
127
128/// [`BooleanOptions::touched_assembly`] under its environment override.
129fn touched_assembly(options: &BooleanOptions) -> bool {
130    match std::env::var("BREP_TOUCHED_ASSEMBLY").as_deref() {
131        Ok("0") => false,
132        Ok(_) => true,
133        Err(_) => options.touched_assembly,
134    }
135}
136
137/// [`BooleanOptions::disjoint_fast_path`] under its environment override.
138fn disjoint_fast_path(options: &BooleanOptions) -> bool {
139    match std::env::var("BREP_DISJOINT_FAST_PATH").as_deref() {
140        Ok("0") => false,
141        Ok(_) => true,
142        Err(_) => options.disjoint_fast_path,
143    }
144}
145
146/// [`BooleanOptions::contact_fast_path`] under its environment override.
147fn contact_fast_path(options: &BooleanOptions) -> bool {
148    match std::env::var("BREP_CONTACT_FAST_PATH").as_deref() {
149        Ok("0") => false,
150        Ok(_) => true,
151        Err(_) => options.contact_fast_path,
152    }
153}
154
155/// [`BooleanOptions::untouched_passthrough`] under its environment override.
156fn untouched_passthrough(options: &BooleanOptions) -> bool {
157    match std::env::var("BREP_UNTOUCHED_PASSTHROUGH").as_deref() {
158        Ok("0") => false,
159        Ok(_) => true,
160        Err(_) => options.untouched_passthrough,
161    }
162}
163
164fn default_tolerance() -> f64 {
165    1e-7
166}
167
168fn default_true() -> bool {
169    true
170}
171
172impl Default for BooleanOptions {
173    fn default() -> Self {
174        Self {
175            tolerance: default_tolerance(),
176            tolerances: None,
177            imprint: ImprintOptions::default(),
178            merge_coplanar_faces: true,
179            keep_unmerged_name_substrs: Vec::new(),
180            untouched_passthrough: true,
181            disjoint_fast_path: true,
182            contact_fast_path: true,
183            touched_assembly: false,
184            sector_classification: true,
185            sheet_trim: true,
186            keep_cells: false,
187            seam_reseat_later_references: None,
188        }
189    }
190}
191
192mod select;
193mod sheet;
194mod cellular;
195mod sector;
196mod assemble;
197mod operand_state;
198mod rim;
199mod coincident;
200mod touch;
201mod reseat;
202
203use rim::*;
204use select::*;
205use coincident::*;
206use touch::TouchCensus;
207pub(crate) use assemble::{
208    assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
209    clear_candidate_memo, edge_interior_lies_on, finalize_assembled_solid,
210};
211use assemble::{assemble_fragments_scoped, AssemblyScope};
212// Currently referenced only inside `assemble`, so the re-export is unused.
213#[allow(unused_imports)]
214pub(crate) use assemble::apply_assembly_heal_chain;
215
216pub fn boolean_operation(
217    first: &BrepSolid,
218    second: &BrepSolid,
219    operation: BooleanOperation,
220    options: &BooleanOptions,
221) -> Result<BrepSolid, KernelRefusal> {
222    match boolean_operation_with_diagnostics(first, second, operation, options) {
223        Ok(outcome) => Ok(outcome.value),
224        Err(error) => {
225            // Perturbation-fallback lane (Simulation of Simplicity). The exact
226            // arrangement only errs on a DEGENERACY here (coincident /
227            // near-tangent carrier faces make it structurally inconsistent) —
228            // e.g. a partial torus whose equatorial circles lie exactly in a box
229            // face. Retry once on a rigidly perturbed second operand; accept only
230            // an oracle-clean, volume-consistent result. See
231            // `perturbation_retry`.
232            if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
233                || !error.class.perturbation_eligible()
234            {
235                return Err(error);
236            }
237            // OPERAND-CONSISTENCY GATE. A rigid nudge lifts a coincidence
238            // BETWEEN the operands; it cannot reconcile an operand with
239            // itself. A planar face whose straight boundary sits farther off
240            // its plane than the assembly weld — the shape the STEP importer's
241            // planar split repairs, so it reaches here only on a body built
242            // some other way — fails the exact arrangement for every rung
243            // alike: the 2026-09-12 mesh-import report paid twenty ~7 s rungs
244            // for one 3.4e-3 gap before the import split existed. Name the
245            // gap and stop. The predicate is deliberately the splitter's own,
246            // not a pcurve-error band: vendor bodies whose fitted edges stand
247            // a few 1e-4 off curved faces are legitimate input (per-entity
248            // tolerances) and keep every rung.
249            // OPERAND-CLOSURE GATE. A boolean of a body that is not a closed
250            // shell has no solid answer, exact or nudged: its inside is not
251            // defined, and its volume depends on the reference point. The
252            // slimmed fuzz fixtures (13, 14 and 15, operand A cut from a larger
253            // part by a box filter) carry 54, 97 and 66 edges that bound one
254            // face; every one of their 20 rungs refused on the exact lane, and
255            // 15's identity was read from an intersection with the open shell.
256            // Only consulted on the failure path, so nothing that succeeds
257            // today changes. Escape hatch: BREP_OPEN_OPERAND_GATE=0.
258            if std::env::var("BREP_OPEN_OPERAND_GATE").as_deref() != Ok("0") {
259                if let Some((operand, open)) = [first, second]
260                    .iter()
261                    .enumerate()
262                    .find_map(|(operand, solid)| {
263                        let open = one_use_edges(solid);
264                        (open > 0).then_some((operand, open))
265                    })
266                {
267                    return Err(error.with_message(|message| format!(
268                        "{message}; operand {operand} is not a closed shell: {open} of its edges bound \
269                         only one face, so no boolean of it encloses a solid and no perturbation \
270                         can close it"
271                    )));
272                }
273            }
274            if let Some((operand, face_id, deviation, weld)) =
275                planar_operand_off_its_plane(first, second, options)
276            {
277                return Err(error.with_message(|message| format!(
278                    "{message}; operand {operand} face {face_id} is a plane whose straight \
279                     boundary sits {deviation:.3e} off it (assembly weld {weld:.1e}), which \
280                     no perturbation can reconcile — the body's planes must contain their \
281                     boundary before it can take part in an exact boolean"
282                )));
283            }
284            // INTERNAL-TANGENCY PINCH GATE. Perturbation is only legitimate
285            // where the exact arrangement is UNSTABLE but its ANSWER is not:
286            // the nearby transversal case must carry the same topology the
287            // degenerate one does. An internal tangency in a DIFFERENCE breaks
288            // exactly that premise — the exact `A − B` pinches to zero
289            // thickness along the contact, so an epsilon either tears the wall
290            // open (a through-slot where the exact answer has intact material)
291            // or leaves a sub-micron web. Both are within epsilon in Hausdorff
292            // distance and in volume, so neither the oracle (boundary skip
293            // band) nor the 1% CSG volume bound can see them, and the lane
294            // would ship the tolerant-kernel sliver this kernel refuses. Only
295            // consulted on the failure path, so nothing that succeeds today can
296            // change; the honest exact-path error is returned instead.
297            // Escape hatch for tamper-verification: BREP_PINCH_GATE=0.
298            if matches!(operation, BooleanOperation::Subtract)
299                && std::env::var("BREP_PINCH_GATE").as_deref() != Ok("0")
300                && subtract_pinches_at_internal_tangency(first, second, options.tolerance)
301                    .unwrap_or(false)
302            {
303                return Err(error.with_message(|message| format!(
304                    "{message}; internal tangency: the operands touch tangentially with \
305                     co-directed normals, so the exact difference pinches to zero thickness \
306                     (non-manifold, unrepresentable in a boundary model) — refusing rather \
307                     than returning a perturbed sliver"
308                )));
309            }
310            // TANGENT-NODE GATE, same doctrine as the pinch gate above and for
311            // the same reason. A 4-valent tangent node is refused because the
312            // kernel cannot imprint it, and perturbation is only legitimate
313            // where the exact arrangement is UNSTABLE but its ANSWER is not:
314            // the nudged configuration resolves the kiss one way or the other
315            // and the kernel has no claim on which. Until 2026-09-13 the ladder
316            // could not produce a candidate here at all; with `fit_polyline`
317            // measuring its own result the equal-radius torus pair's LAST rung
318            // (3.0e-3 of scale) assembles, validates, and passes the semantic
319            // oracle at 399 samples with a zero disagreement rate, reading
320            // 299.4998500 against the ideal union's 299.6 — a 0.03% error that
321            // is the nudge's, not the kernel's answer. Shipping that would
322            // answer a question this class exists to decline, so the class does
323            // not take the ladder. Escape hatch for measuring it:
324            // BREP_TANGENT_NODE_GATE=0.
325            if matches!(error.class, RefusalClass::TangentNodeSingularity)
326                && std::env::var("BREP_TANGENT_NODE_GATE").as_deref() != Ok("0")
327            {
328                return Err(error);
329            }
330            match perturbation_retry(first, second, operation, options) {
331                Ok(solid) => Ok(solid),
332                Err(refused) if refused.is_empty() => Err(error),
333                Err(refused) => {
334                    Err(error.with_message(|message| format!("{message}; {}", refused.join("; "))))
335                }
336            }
337        }
338    }
339}
340
341/// Grid resolution (per parameter direction, endpoints included) used to probe a
342/// face pair for a tangential contact. Endpoints and the midpoint are both
343/// sampled, so a contact sitting on a periodic seam (a cylinder's `u = 0`
344/// ruling) and one sitting at a face's parametric centre are both hit exactly.
345const PINCH_PROBE_STEPS: usize = 8;
346
347/// `|n_a × n_b|` bound below which two unit normals count as parallel — the same
348/// bound `csg::imprint`'s pair classifier uses (`PAIR_ANGULAR_TOLERANCE`).
349const PINCH_ANGULAR_TOLERANCE: f64 = 1e-4;
350
351/// Parameter-space step (fraction of the domain span) taken around a contact
352/// sample to prove the contact is LOWER-DIMENSIONAL — a tangency curve/point
353/// rather than a cosurface patch.
354const PINCH_SEPARATION_STEP: f64 = 1e-2;
355
356/// Would the exact `first − second` PINCH to zero thickness at a tangential
357/// contact?
358///
359/// True when some face of `first` and some face of `second` touch tangentially
360/// (surfaces within the contact band, normals parallel) with CO-DIRECTED outward
361/// normals, at a contact that is lower-dimensional (the surfaces separate as you
362/// step away from it).
363///
364/// Co-directed outward normals at a tangency mean one solid lies locally INSIDE
365/// the other: subtracting leaves material on both sides of the contact that
366/// meets there at zero thickness — a non-manifold pinch no boundary model can
367/// represent. Anti-directed normals are the harmless EXTERNAL tangency (a
368/// cylinder resting against a wall): the difference simply keeps `first` intact.
369/// The lower-dimensionality requirement excludes a CO-SURFACE contact (a pocket
370/// wall flush with an outer wall — co-directed normals, but the surfaces stay
371/// coincident in every direction), where the difference is perfectly well
372/// behaved.
373///
374/// Conservative by construction: it only reports a pinch it can actually witness
375/// on the probe grid, and a miss just leaves the perturbation lane to its
376/// existing gates.
377fn subtract_pinches_at_internal_tangency(
378    first: &BrepSolid,
379    second: &BrepSolid,
380    tolerance: f64,
381) -> Result<bool, KernelRefusal> {
382    // Same contact band the imprint pair classifier uses for its sampled
383    // "these carriers touch" verdict.
384    let contact = (tolerance * 100.0).max(1e-12);
385    let probes_a = first
386        .shells
387        .iter()
388        .flat_map(|shell| &shell.faces)
389        .map(PinchProbe::of)
390        .collect::<Result<Vec<_>, _>>()?;
391    let probes_b = second
392        .shells
393        .iter()
394        .flat_map(|shell| &shell.faces)
395        .map(PinchProbe::of)
396        .collect::<Result<Vec<_>, _>>()?;
397    for probe_a in &probes_a {
398        for probe_b in &probes_b {
399            // Sampled-hull cull, so the (failure-path-only) cost stays linear in
400            // the faces that actually touch rather than quadratic in every face.
401            // The hull is built from the probe grid, so it can under-cover a
402            // curved carrier between samples: pad it by a percent of the pair's
403            // size before culling. Over-keeping a pair only costs a probe that
404            // finds nothing.
405            let pad = 1e-2 * probe_a.extent().max(probe_b.extent());
406            if probe_a.separation(probe_b) > contact + pad {
407                continue;
408            }
409            // CO-SURFACE contacts are 2-dimensional, not tangencies: two flush
410            // walls (a pocket's side coincident with an outer wall, a cylinder
411            // cap lying in a box face) have co-directed normals wherever the
412            // solids nest, yet the difference there is perfectly well behaved.
413            // Only a LOWER-dimensional contact pinches, so drop the pair the
414            // pair classifier calls cosurface before probing it.
415            if classify_surface_pair(
416                &probe_a.face.surface,
417                &probe_b.face.surface,
418                tolerance,
419                PINCH_ANGULAR_TOLERANCE,
420            ).or_refuse(KernelStage::Validate, "csg.boolean.mod")?
421            .relation
422                == SurfacePairRelation::Cosurface
423            {
424                continue;
425            }
426            if faces_touch_with_codirected_normals(probe_a, probe_b, contact)? {
427                return Ok(true);
428            }
429        }
430    }
431    Ok(false)
432}
433
434/// A face's probe grid plus the grid's bounding box, built once per face so the
435/// pair loop only pays for a hull comparison.
436struct PinchProbe<'a> {
437    face: &'a FaceRecord,
438    /// `(u, v, point)` on the face's carrier surface.
439    samples: Vec<(f64, f64, Vec3)>,
440    minimum: [f64; 3],
441    maximum: [f64; 3],
442}
443
444impl<'a> PinchProbe<'a> {
445    fn of(face: &'a FaceRecord) -> Result<Self, KernelRefusal> {
446        let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
447        let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
448        let steps = PINCH_PROBE_STEPS as f64;
449        let mut samples = Vec::with_capacity((PINCH_PROBE_STEPS + 1).pow(2));
450        let mut minimum = [f64::INFINITY; 3];
451        let mut maximum = [f64::NEG_INFINITY; 3];
452        for i in 0..=PINCH_PROBE_STEPS {
453            let u = u0 + (u1 - u0) * i as f64 / steps;
454            for j in 0..=PINCH_PROBE_STEPS {
455                let v = v0 + (v1 - v0) * j as f64 / steps;
456                let Ok(point) = face.surface.evaluate(u, v) else {
457                    continue;
458                };
459                for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
460                    minimum[axis] = minimum[axis].min(value);
461                    maximum[axis] = maximum[axis].max(value);
462                }
463                samples.push((u, v, point));
464            }
465        }
466        Ok(Self {
467            face,
468            samples,
469            minimum,
470            maximum,
471        })
472    }
473
474    /// Largest side of the sampled hull (0 when nothing sampled).
475    fn extent(&self) -> f64 {
476        (0..3)
477            .map(|axis| self.maximum[axis] - self.minimum[axis])
478            .fold(0.0f64, f64::max)
479    }
480
481    /// Axis-aligned gap between the two sampled hulls (0 when they overlap).
482    fn separation(&self, other: &Self) -> f64 {
483        let mut gap: f64 = 0.0;
484        for axis in 0..3 {
485            gap = gap.max(self.minimum[axis] - other.maximum[axis]);
486            gap = gap.max(other.minimum[axis] - self.maximum[axis]);
487        }
488        gap
489    }
490}
491
492/// One face pair of [`subtract_pinches_at_internal_tangency`]: probe both
493/// surfaces on a grid, keep samples that land on the other surface inside BOTH
494/// trims with parallel co-directed outward normals, and accept only where the
495/// contact provably separates nearby.
496fn faces_touch_with_codirected_normals(
497    probe_a: &PinchProbe<'_>,
498    probe_b: &PinchProbe<'_>,
499    contact: f64,
500) -> Result<bool, KernelRefusal> {
501    for (probe, other) in [(probe_a, probe_b), (probe_b, probe_a)] {
502        let source = probe.face;
503        let target = other.face;
504        for &(u, v, point) in &probe.samples {
505            let projection = project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?;
506            if projection.distance > contact {
507                continue;
508            }
509            // A pole (collapsed du × dv) has no reliable orientation here;
510            // skipping it keeps the predicate conservative.
511            let (Ok(source_normal), Ok(target_normal)) = (
512                source.surface.normal(u, v),
513                target.surface.normal(projection.u, projection.v),
514            ) else {
515                continue;
516            };
517            let source_outward = outward_normal(source_normal, source.same_sense);
518            let target_outward = outward_normal(target_normal, target.same_sense);
519            if source_outward.cross(target_outward).length() > PINCH_ANGULAR_TOLERANCE
520                || source_outward.dot(target_outward) <= 0.0
521            {
522                continue;
523            }
524            if parameter_point_in_face(source, Vec2 { x: u, y: v }, 1e-6).or_refuse(KernelStage::Validate, "parameter_point_in_face")?
525                == PolygonClass::Outside
526                || parameter_point_in_face(
527                    target,
528                    Vec2 {
529                        x: projection.u,
530                        y: projection.v,
531                    },
532                    1e-6,
533                ).or_refuse(KernelStage::Validate, "csg.boolean.mod")? == PolygonClass::Outside
534            {
535                continue;
536            }
537            if contact_separates_locally(source, target, u, v, contact)? {
538                return Ok(true);
539            }
540        }
541    }
542    Ok(false)
543}
544
545fn outward_normal(normal: Vec3, same_sense: bool) -> Vec3 {
546    if same_sense {
547        normal
548    } else {
549        normal.scale(-1.0)
550    }
551}
552
553/// Does the contact at `source(u, v)` LEAVE the contact band when you step away
554/// from it in parameter space? A tangency curve or point does (stepping across
555/// the tangency separates the surfaces quadratically); a cosurface patch does
556/// not (it stays coincident in every direction). Steps are clamped into the
557/// domain, so a sample on a seam or a domain edge probes only inward.
558fn contact_separates_locally(
559    source: &FaceRecord,
560    target: &FaceRecord,
561    u: f64,
562    v: f64,
563    contact: f64,
564) -> Result<bool, KernelRefusal> {
565    let [u0, u1] = source.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
566    let [v0, v1] = source.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
567    let step_u = (u1 - u0) * PINCH_SEPARATION_STEP;
568    let step_v = (v1 - v0) * PINCH_SEPARATION_STEP;
569    for (probe_u, probe_v) in [
570        ((u + step_u).min(u1), v),
571        ((u - step_u).max(u0), v),
572        (u, (v + step_v).min(v1)),
573        (u, (v - step_v).max(v0)),
574    ] {
575        if (probe_u - u).abs() < f64::EPSILON && (probe_v - v).abs() < f64::EPSILON {
576            continue;
577        }
578        let Ok(point) = source.surface.evaluate(probe_u, probe_v) else {
579            continue;
580        };
581        // A generous multiple of the band: the step must clear it decisively,
582        // never on projection noise.
583        if project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?.distance > contact * 10.0 {
584            return Ok(true);
585        }
586    }
587    Ok(false)
588}
589
590/// Deterministic perturbation-fallback (Simulation of Simplicity), the textbook
591/// CAD answer to an exact/near-tangent arrangement degeneracy, adopted after six
592/// incremental arrangement/imprint/fragment fixes for the near-tangent class were
593/// each falsified, rather than attempting a seventh. Rigidly TRANSLATE the second operand by a tiny
594/// epsilon so the degenerate contact becomes a clean transversal crossing, redo
595/// the boolean, and accept the result ONLY if it is (a) topologically valid —
596/// guaranteed by `assemble`'s own gate returning `Ok` — (b) semantically equal to
597/// the exact CSG of the ORIGINAL operands (the point-classification oracle), and
598/// (c) volume-consistent with the CSG inequality bounds. A wrong perturbed result
599/// is rejected on (b)/(c), so this lane preserves the engine's fail-safe property:
600/// it can only turn a degeneracy-error into a CORRECT solid, never a wrong one.
601///
602/// INVARIANT — no recursion: this calls `boolean_operation_with_diagnostics`
603/// (the EXACT path), never `boolean_operation`, so the fallback can never
604/// re-enter itself. A future refactor must preserve that.
605///
606/// (d) THE IDENTITY. Where the exact lane answers one of the other operations
607/// on the same operands, inclusion–exclusion names the volume this result
608/// must have, and a rung that misses it by more than the case gate's own drift
609/// band (`VOLUME_DRIFT_REL` of that volume, `VOLUME_DRIFT_ABS` floor) is
610/// refused by name. (c)'s 1% of the operands' total cannot see an error in a
611/// small removed volume: on the helmet's cutter − helmet (2026-09-15) the rung
612/// at fraction 3e-4 moved the helmet 5.2e-3, 2.3% of its size, passed the
613/// oracle at 1 in 336, and read 999.999899266 against V(cutter) − V(cutter ∩
614/// helmet) = 999.999905792: 6.9% too much material removed. Where no other
615/// operation clears on the exact lane there is no identity to hold, and the
616/// rung is held to (e) instead. Escape hatch for measuring it:
617/// BREP_PERTURB_IDENTITY=0 (which also lifts (e)).
618///
619/// (e) THE MIRROR. Where no exact-lane boolean of the operands clears in
620/// either order, the rung is held to itself moved the other way: the same
621/// operation, or failing that any sibling reading, on the second operand moved
622/// by −offset, with V(first) and V(second) from the mass integrator and the
623/// operation's sign, names the volume the mirrored rung encloses. A rigid
624/// translation changes the answer to first order in the offset, so half the
625/// difference between the rung's body and its mirror is the body's
626/// first-order translation error, and it must clear the same drift band. A
627/// rung whose mirror clears no boolean at all has nothing to check it and is
628/// refused by name. On 22_sag_boundary_identity_t164 (2026-09-15) the four
629/// shipped rescues at offset 9.7e-4 disagreed with each other through V(∩) by
630/// 4.3e-2 (the difference moves the part; the others move the sphere), against
631/// a bar of 5e-6. Escape hatch: BREP_PERTURB_MIRROR=0.
632///
633/// (d) and (e) run before (b), the oracle, which costs more than either.
634///
635/// (f) THE EARLY EXIT. Along one direction the mirror's error grows with the
636/// offset (`MirrorTrend` states the law and its measurement), so a direction
637/// whose two refused rungs read one sign, growing, is not tried at larger
638/// offsets, and the refusal names it. booleanIssue's refusal walked 20 rungs,
639/// each with its mirror, in 40.5 s against the case's 30 s. Escape hatch:
640/// BREP_MIRROR_EARLY_EXIT=0.
641///
642/// `Err` carries one sentence per rung refused on (d), empty when none was.
643fn perturbation_retry(
644    first: &BrepSolid,
645    second: &BrepSolid,
646    operation: BooleanOperation,
647    options: &BooleanOptions,
648) -> Result<BrepSolid, Vec<String>> {
649    // Generic translation directions: each has a substantial component on ALL
650    // three axes, so whatever axis the coincidence normal lies along (an
651    // equatorial plane flush with a box face, a cap plane flush with a side
652    // face, …) at least one direction has a component that lifts the contact off
653    // exact tangency. Axis-aligned directions are deliberately excluded — a nudge
654    // parallel to the coincident plane leaves the degeneracy in place (verified:
655    // pure ±X / ±Z never rescue the flush-torus case). Fixed, not hash-seeded
656    // as the lane was first sketched: the seeding's only purpose was to rule out
657    // RNG/clock nondeterminism, which fixed vectors already satisfy, and four
658    // diverse directions cover any axis-aligned normal.
659    const DIRECTIONS: [[f64; 3]; 4] = [
660        [0.4034, 0.7973, 0.4491],
661        [0.7973, 0.4491, 0.4034],
662        [0.4491, 0.4034, 0.7973],
663        [0.5774, -0.5774, 0.5774],
664    ];
665    // Fractions of operand scale, ordered SMALLEST-FIRST so the accepted solid
666    // carries the least geometric offset. Sized above the near-tangent sliver
667    // dead zone and below local feature size; the ladder + accept-gate make the
668    // exact magnitude non-critical (a rung that lands in a dead zone simply fails
669    // the gate and the next rung is tried).
670    const FRACTIONS: [f64; 5] = PERTURBATION_FRACTIONS;
671
672    let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
673    let scale = crate::tolerance::solid_scale(first)
674        .max(crate::tolerance::solid_scale(second))
675        .max(1.0);
676
677    // Original operand volumes for the CSG inequality backstop (the oracle skips
678    // points near boundaries, so a wrong thin shell hugging the flush face is
679    // invisible to it but shows up here).
680    let _caller = crate::mass_caller("boolean.perturbation_rescue");
681    let va = solid_signed_volume(first).ok().map(f64::abs);
682    let vb = solid_signed_volume(second).ok().map(f64::abs);
683    let hold_identity = std::env::var("BREP_PERTURB_IDENTITY").as_deref() != Ok("0");
684    // (e) is held only with (d): `BREP_PERTURB_MIRROR=0` alone ships a rung
685    // with no identity on (a)–(c), as before.
686    let hold_mirror = hold_identity && std::env::var("BREP_PERTURB_MIRROR").as_deref() != Ok("0");
687    // (f) A direction whose mirror errors say no larger offset can clear is
688    // closed. Escape hatch: `BREP_MIRROR_EARLY_EXIT=0` walks every rung.
689    let early_exit = std::env::var("BREP_MIRROR_EARLY_EXIT").as_deref() != Ok("0");
690    let mut trends = [MirrorTrend::default(); 4];
691    // Measured once, on the first rung that clears (a)–(c).
692    let mut identity: Option<Option<InclusionExclusion>> = None;
693    let mut refused = Vec::new();
694    // `BREP_DEBUG_PERTURB_LADDER=1`: every rung's disposition on stderr, and
695    // for every rung that builds, its miss against the identity, measured
696    // before the ladder starts. Changes no decision.
697    let ladder = std::env::var("BREP_DEBUG_PERTURB_LADDER").is_ok();
698    let ladder_start = web_time::Instant::now();
699    if ladder {
700        let measured = inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
701        match &measured {
702            Some(identity) => eprintln!(
703                "[ladder] identity {} = {:.9} from the exact lane's {} ({:.9}); V(first) {:?} V(second) {:?}",
704                identity.formula, identity.expected, identity.reading, identity.read, va, vb
705            ),
706            None => eprintln!("[ladder] no identity: no exact-lane boolean of these operands clears in either order"),
707        }
708        identity = Some(measured);
709    }
710    let rung_miss = |identity: &Option<Option<InclusionExclusion>>, candidate: &BrepSolid| -> String {
711        let volume = body_volume(candidate);
712        match (identity, volume) {
713            (Some(Some(identity)), Some(volume)) => format!(
714                "volume {volume:.9} miss {:+.3e} bar {:.3e}",
715                volume - identity.expected,
716                crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * identity.expected.abs())
717            ),
718            (_, Some(volume)) => format!("volume {volume:.9}"),
719            _ => "no volume".to_string(),
720        }
721    };
722
723    for &fraction in &FRACTIONS {
724        let magnitude = scale * fraction;
725        for (direction, dir) in DIRECTIONS.iter().enumerate() {
726            if trends[direction].closed {
727                if ladder {
728                    eprintln!("[ladder] dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}: not tried, its direction is closed");
729                }
730                continue;
731            }
732            let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
733            let translate = match AffineTransform::new([
734                1.0, 0.0, 0.0, offset[0], //
735                0.0, 1.0, 0.0, offset[1], //
736                0.0, 0.0, 1.0, offset[2], //
737                0.0, 0.0, 0.0, 1.0,
738            ]) {
739                Ok(transform) => transform,
740                Err(_) => continue,
741            };
742            let moved = match crate::transform_brep(second, translate, false) {
743                Ok(solid) => solid,
744                Err(_) => continue,
745            };
746            let rung = format!("dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}");
747            let rung_start = web_time::Instant::now();
748            let candidate =
749                match boolean_operation_with_diagnostics(first, &moved, operation, options) {
750                    Ok(outcome) => outcome.value,
751                    Err(error) => {
752                        if ladder {
753                            eprintln!("[ladder] {rung}: exact lane refused {:?}", error.class);
754                        }
755                        continue;
756                    }
757                };
758            let built = rung_start.elapsed();
759            // A perturbation must not "rescue" a case into emptiness.
760            if candidate.shells.is_empty() {
761                if ladder {
762                    eprintln!("[ladder] {rung}: empty");
763                }
764                continue;
765            }
766            // (c) Volume must satisfy the CSG inequality for the ORIGINAL
767            // operands (generous 1% relative slack: catches gross wrongness —
768            // doubled/halved bodies, missing lobes — without rejecting the
769            // legitimate sub-micron offset).
770            if let (Some(va), Some(vb)) = (va, vb) {
771                if let Ok(vr) = solid_signed_volume(&candidate) {
772                    if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
773                        if ladder {
774                            eprintln!("[ladder] {rung}: outside the CSG bound, {}", rung_miss(&identity, &candidate));
775                        }
776                        continue;
777                    }
778                }
779            }
780            // (d) and (e) run before (b): each costs a volume, and (e) one
781            // exact-lane boolean, where the oracle classifies 400 points. A
782            // rung ships only when every gate clears, so the order decides
783            // which gate names a refusal, never which rung ships.
784            let bounded = rung_start.elapsed();
785            // (d) The exact lane's inclusion–exclusion identity.
786            if hold_identity {
787                let identity = identity.get_or_insert_with(|| {
788                    let identity =
789                        inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
790                    if debug && identity.is_none() {
791                        eprintln!(
792                            "[perturb] no exact-lane boolean of these operands clears in either \
793                             order; no identity to hold"
794                        );
795                    }
796                    identity
797                });
798                if let Some(identity) = identity {
799                    let expected = identity.expected;
800                    let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * expected.abs());
801                    let volume = body_volume(&candidate);
802                    let miss = volume.map(|volume| (volume - expected).abs());
803                    if !miss.is_some_and(|miss| miss <= bar) {
804                        let sentence = format!(
805                            "perturbation rung dir={dir:?} fraction={fraction:.1e} \
806                             (offset {magnitude:.3e}) refused: its body reads {} against the \
807                             inclusion–exclusion identity {} = {expected:.9} (the exact lane's \
808                             {} reads {:.9}), a miss of {} beyond the volume drift bar {bar:.3e}",
809                            volume.map_or("no volume".to_string(), |volume| format!("{volume:.9}")),
810                            identity.formula,
811                            identity.reading,
812                            identity.read,
813                            miss.map_or("unmeasured".to_string(), |miss| format!("{miss:.3e}")),
814                        );
815                        if debug {
816                            eprintln!("[perturb] {sentence}");
817                        }
818                        if ladder {
819                            eprintln!(
820                                "[ladder] {rung}: refused by the identity, volume {} miss {} bar {bar:.3e}",
821                                volume.map_or("none".to_string(), |volume| format!("{volume:.9}")),
822                                volume.map_or("unmeasured".to_string(), |volume| format!("{:+.3e}", volume - expected)),
823                            );
824                        }
825                        refused.push(sentence);
826                        continue;
827                    }
828                } else if hold_mirror {
829                    // (e) No exact reading of these operands: the rung's
830                    // mirror.
831                    let mirror_start = web_time::Instant::now();
832                    let mirrored = AffineTransform::new([
833                        1.0, 0.0, 0.0, -offset[0], //
834                        0.0, 1.0, 0.0, -offset[1], //
835                        0.0, 0.0, 1.0, -offset[2], //
836                        0.0, 0.0, 0.0, 1.0,
837                    ])
838                    .ok()
839                    .and_then(|transform| crate::transform_brep(second, transform, false).ok());
840                    let mirror = mirrored.as_ref().and_then(|mirrored| {
841                        inclusion_exclusion_volume(first, mirrored, operation, options, va, vb, true)
842                    });
843                    let mirror_time = mirror_start.elapsed();
844                    let volume = body_volume(&candidate);
845                    let mut measured = None;
846                    let verdict = match (&mirror, volume) {
847                        (Some(mirror), Some(volume)) => {
848                            let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * mirror.expected.abs());
849                            let miss = 0.5 * (volume - mirror.expected).abs();
850                            measured = Some((0.5 * (volume - mirror.expected), bar));
851                            if ladder {
852                                eprintln!(
853                                    "[ladder] {rung}: mirror at −offset reads {} = {:.9} (the mirrored {} {:.9}); body {volume:.9}, first-order translation error {:+.3e} bar {bar:.3e}",
854                                    mirror.formula, mirror.expected, mirror.reading, mirror.read, 0.5 * (volume - mirror.expected)
855                                );
856                            }
857                            (miss > bar).then(|| format!(
858                                "perturbation rung dir={dir:?} fraction={fraction:.1e} \
859                                 (offset {magnitude:.3e}) refused: no exact-lane boolean of the \
860                                 operands clears, so the rung is held to its mirror: its body reads \
861                                 {volume:.9} with the second operand moved +offset and {} = {:.9} \
862                                 with it moved −offset (the mirrored {} reads {:.9}), a first-order \
863                                 translation error of {miss:.3e} beyond the volume drift bar {bar:.3e}",
864                                mirror.formula, mirror.expected, mirror.reading, mirror.read
865                            ))
866                        }
867                        _ => Some(format!(
868                            "perturbation rung dir={dir:?} fraction={fraction:.1e} \
869                             (offset {magnitude:.3e}) refused: no exact-lane boolean of the operands \
870                             clears, and none clears with the second operand moved −offset either, \
871                             so nothing checks the rung's body{}",
872                            volume.map_or(String::new(), |volume| format!(" ({volume:.9})"))
873                        )),
874                    };
875                    match verdict {
876                        None => trends[direction].cleared(),
877                        Some(sentence) => {
878                            if debug {
879                                eprintln!("[perturb] {sentence}");
880                            }
881                            if ladder {
882                                eprintln!(
883                                    "[ladder] {rung}: refused by its mirror (rung {:.0} ms: built {:.0}, bound {:.0}, mirror {:.0} ms; ladder {:.1} s)",
884                                    rung_start.elapsed().as_secs_f64() * 1e3,
885                                    built.as_secs_f64() * 1e3,
886                                    (bounded - built).as_secs_f64() * 1e3,
887                                    mirror_time.as_secs_f64() * 1e3,
888                                    ladder_start.elapsed().as_secs_f64()
889                                );
890                            }
891                            refused.push(sentence);
892                            // (f) Two refused rungs of one sign, growing, close
893                            // the direction.
894                            let closure = measured.and_then(|(error, bar)| {
895                                let earlier = trends[direction].refused(magnitude, error, early_exit)?;
896                                Some((earlier, error, bar))
897                            });
898                            if let Some(((earlier_offset, earlier_error), error, bar)) = closure {
899                                let closed = format!(
900                                    "perturbation direction dir={dir:?} closed after offset {magnitude:.3e}: its \
901                                     rungs' first-order translation errors read {earlier_error:+.3e} at offset \
902                                     {earlier_offset:.3e} and {error:+.3e} at {magnitude:.3e}, one sign and \
903                                     growing, so no larger offset along it comes back to the volume drift bar \
904                                     {bar:.3e}"
905                                );
906                                if debug {
907                                    eprintln!("[perturb] {closed}");
908                                }
909                                if ladder {
910                                    eprintln!("[ladder] {closed}");
911                                }
912                                refused.push(closed);
913                            }
914                            continue;
915                        }
916                    }
917                }
918            }
919            // (b) Semantic agreement with the exact CSG of the ORIGINAL
920            // operands. Reject a vacuous verdict (skip-band swallowed every
921            // sample so nothing was actually checked).
922            match crate::oracle::boolean_semantic_disagreement(
923                first, second, operation, &candidate, 400,
924            ) {
925                Ok(report) if report.considered >= 30 && !report.is_flagged() => {
926                    if ladder {
927                        eprintln!(
928                            "[ladder] {rung}: accepted (oracle {} considered, rate {:.4}), {}",
929                            report.considered, report.disagreement_rate, rung_miss(&identity, &candidate)
930                        );
931                    }
932                    if debug {
933                        eprintln!(
934                            "[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
935                             magnitude={magnitude:.3e} offset={offset:?} \
936                             (oracle considered={} rate={:.4})",
937                            report.considered, report.disagreement_rate
938                        );
939                    }
940                    return Ok(candidate);
941                }
942                report => {
943                    if ladder {
944                        let verdict = match &report {
945                            Ok(report) => format!("oracle {} considered, rate {:.4}", report.considered, report.disagreement_rate),
946                            Err(error) => format!("oracle error {error:?}"),
947                        };
948                        eprintln!("[ladder] {rung}: refused by the oracle ({verdict}), {}", rung_miss(&identity, &candidate));
949                    }
950                    continue;
951                }
952            }
953        }
954    }
955    if debug {
956        eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
957    }
958    Err(refused)
959}
960
961/// The perturbation ladder's offsets, as fractions of the operands' scale,
962/// smallest first.
963const PERTURBATION_FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
964
965/// (f) One ladder direction's rungs refused by their mirrors.
966///
967/// Along one direction a rung's signed first-order translation error, half
968/// its body less its mirror's reading, reads s(δ) = kδ + r: a slope k from the
969/// translation, and a remainder r from the degeneracy the rung steps over,
970/// which does not grow with the offset. Two refused rungs at δ₁ < δ₂ with one
971/// sign and |s(δ₂)| ≥ |s(δ₁)| fix k's sign as theirs, so every larger offset
972/// reads farther from zero than s(δ₂), which is already beyond the drift bar:
973/// the direction is closed. Rungs of one sign that shrink toward zero, or that
974/// change sign, say nothing about k, and the direction stays open; a rung whose
975/// mirror clears starts the direction's record again.
976///
977/// Measured 2026-09-15 (`BREP_DEBUG_PERTURB_LADDER=1` prints s). On
978/// 22_sag_boundary_identity_t164, all four operations and all four
979/// directions, s/δ keeps one sign and holds to 3.6% over the ladder's 100× of
980/// offset (r ≈ 0; −22.23 .. −22.34 on the intersection's first direction). On
981/// booleanIssue's flush partial torus union, s/δ holds to 0.5% and 8.5% on two
982/// directions, and to 1.2% on a third past its smallest rung, whose remainder
983/// reads −43.4 against the slope's −24.5. The first direction's smallest rung
984/// reads −1.891e-2 where its slope, +2.3 .. +2.6, gives +3.5e-3 (r ≈ −2.2e-2),
985/// and its next measured rung +3.510e-2: a change of sign, which keeps it open.
986/// A closed direction's skipped rung would have had to fall to the bar, 8.3e2 ..
987/// 1.5e6 times below the smallest refused |s| on those rows, to have cleared.
988#[derive(Clone, Copy, Default)]
989struct MirrorTrend {
990    /// The last rung refused by its mirror: its offset and signed error.
991    last: Option<(f64, f64)>,
992    closed: bool,
993}
994
995impl MirrorTrend {
996    /// Records a rung refused by its mirror at `offset` with signed error
997    /// `error`. When it and the direction's last refused rung close the
998    /// direction, and `close` allows it, the direction is closed and the
999    /// earlier rung's (offset, signed error) returned.
1000    fn refused(&mut self, offset: f64, error: f64, close: bool) -> Option<(f64, f64)> {
1001        let earlier = self
1002            .last
1003            .filter(|&(_, earlier)| earlier.signum() == error.signum() && error.abs() >= earlier.abs())
1004            .filter(|_| close);
1005        self.last = Some((offset, error));
1006        self.closed |= earlier.is_some();
1007        earlier
1008    }
1009
1010    /// A rung whose mirror clears: the direction's record starts again.
1011    fn cleared(&mut self) {
1012        self.last = None;
1013    }
1014}
1015
1016/// Non-degenerate edges a solid's faces use exactly once.
1017fn one_use_edges(solid: &BrepSolid) -> usize {
1018    let mut uses: HashMap<u64, usize> = HashMap::default();
1019    for coedge in solid
1020        .shells
1021        .iter()
1022        .flat_map(|shell| &shell.faces)
1023        .flat_map(|face| &face.loops)
1024        .flat_map(|loop_record| &loop_record.coedges)
1025    {
1026        *uses.entry(coedge.edge_id).or_default() += 1;
1027    }
1028    solid
1029        .edges
1030        .iter()
1031        .filter(|edge| !edge.degenerate && uses.get(&edge.id) == Some(&1))
1032        .count()
1033}
1034
1035/// A body's enclosed volume, zero for an empty one.
1036fn body_volume(solid: &BrepSolid) -> Option<f64> {
1037    if solid.shells.is_empty() {
1038        return Some(0.0);
1039    }
1040    solid_signed_volume(solid).ok().map(f64::abs)
1041}
1042
1043/// What the exact lane's other operations say `operation` on these operands
1044/// must enclose.
1045struct InclusionExclusion {
1046    /// The rescued operation's volume in terms of V(first), V(second) and
1047    /// V(first ∩ second).
1048    formula: &'static str,
1049    /// The exact-lane boolean that supplied V(first ∩ second).
1050    reading: &'static str,
1051    /// That boolean's enclosed volume.
1052    read: f64,
1053    /// The volume `formula` gives.
1054    expected: f64,
1055}
1056
1057/// The volume `operation` on these operands must have, by inclusion–exclusion.
1058/// Any one exact-lane boolean of the same two operands fixes V(first ∩ second),
1059/// and with the operands' own volumes that fixes all four operations. The
1060/// readings are tried intersection, union, then difference, each in both
1061/// operand orders: the exact lane is not order-symmetric (the helmet's cutter
1062/// ∩ helmet refuses where helmet ∩ cutter clears). The rescued operation itself,
1063/// in its own order, is skipped, since it is the one that refused, unless
1064/// `own_first` asks for it to be read first (a rung's mirror, where it is a
1065/// different pair of operands). `None` when no reading clears or an operand has
1066/// no volume.
1067fn inclusion_exclusion_volume(
1068    first: &BrepSolid,
1069    second: &BrepSolid,
1070    operation: BooleanOperation,
1071    options: &BooleanOptions,
1072    va: Option<f64>,
1073    vb: Option<f64>,
1074    own_first: bool,
1075) -> Option<InclusionExclusion> {
1076    use BooleanOperation::{Intersect, Subtract, Union};
1077    let (va, vb) = (va?, vb?);
1078    let own = match operation {
1079        Intersect => (Intersect, false, "first ∩ second"),
1080        Union => (Union, false, "first ∪ second"),
1081        Subtract => (Subtract, false, "first − second"),
1082    };
1083    let readings = [
1084        (Intersect, false, "first ∩ second"),
1085        (Intersect, true, "second ∩ first"),
1086        (Union, false, "first ∪ second"),
1087        (Union, true, "second ∪ first"),
1088        (Subtract, false, "first − second"),
1089        (Subtract, true, "second − first"),
1090    ];
1091    let own_reading = own_first.then_some(own);
1092    let leading = usize::from(own_first);
1093    for (index, (other, swapped, reading)) in own_reading.into_iter().chain(readings).enumerate() {
1094        if index >= leading && !swapped && std::mem::discriminant(&other) == std::mem::discriminant(&operation) {
1095            continue;
1096        }
1097        let (x, y) = if swapped { (second, first) } else { (first, second) };
1098        let Some(read) = boolean_operation_with_diagnostics(x, y, other, options)
1099            .ok()
1100            .and_then(|outcome| body_volume(&outcome.value))
1101        else {
1102            continue;
1103        };
1104        let intersection = match (other, swapped) {
1105            (Intersect, _) => read,
1106            (Union, _) => va + vb - read,
1107            (Subtract, false) => va - read,
1108            (Subtract, true) => vb - read,
1109        };
1110        let (formula, expected) = match operation {
1111            Intersect => ("V(first ∩ second)", intersection),
1112            Union => ("V(first) + V(second) − V(first ∩ second)", va + vb - intersection),
1113            Subtract => ("V(first) − V(first ∩ second)", va - intersection),
1114        };
1115        return Some(InclusionExclusion { formula, reading, read, expected });
1116    }
1117    None
1118}
1119
1120/// The worst planar face of either operand whose STRAIGHT boundary sits
1121/// farther off its plane than the assembly weld radius:
1122/// `(operand index, face id, gap, weld)`. Mirrors the STEP importer's planar
1123/// split predicate (`step_import::builder::planar_split`) on a finished
1124/// solid: a plane, a single loop of straight non-degenerate edges, and a
1125/// boundary vertex beyond the weld. `None` means no such face, so a failure
1126/// may still be a genuine degeneracy worth a perturbation rung. Only
1127/// consulted on the failure path.
1128fn planar_operand_off_its_plane(
1129    first: &BrepSolid,
1130    second: &BrepSolid,
1131    options: &BooleanOptions,
1132) -> Option<(u8, u64, f64, f64)> {
1133    let policy = options
1134        .tolerances
1135        .clone()
1136        .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
1137    let weld = crate::tolerance::assembler_weld(policy.model);
1138    let mut worst: Option<(u8, u64, f64)> = None;
1139    for (operand, solid) in [(0u8, first), (1u8, second)] {
1140        let vertex_point = |id: u64| solid.vertices.iter().find(|v| v.id == id).map(|v| v.point);
1141        for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
1142            let Some(crate::AnalyticSurface::Plane {
1143                origin,
1144                u_dir,
1145                v_dir,
1146                ..
1147            }) = face.surface.analytic()
1148            else {
1149                continue;
1150            };
1151            let [loop_record] = face.loops.as_slice() else {
1152                continue;
1153            };
1154            let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
1155                continue;
1156            };
1157            let mut off_plane = 0.0f64;
1158            let mut straight = true;
1159            for coedge in &loop_record.coedges {
1160                let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
1161                    straight = false;
1162                    break;
1163                };
1164                let (Some(start), Some(end)) =
1165                    (vertex_point(edge.start_vertex_id), vertex_point(edge.end_vertex_id))
1166                else {
1167                    straight = false;
1168                    break;
1169                };
1170                let chord = end.sub(start);
1171                let length = chord.length();
1172                if edge.degenerate || length <= weld {
1173                    straight = false;
1174                    break;
1175                }
1176                let direction = chord.scale(1.0 / length);
1177                for fraction in [0.25, 0.5, 0.75] {
1178                    let Ok(point) = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction) else {
1179                        straight = false;
1180                        break;
1181                    };
1182                    let delta = point.sub(start);
1183                    let along = delta.dot(direction);
1184                    if delta.sub(direction.scale(along)).length() > weld
1185                        || along < -weld
1186                        || along > length + weld
1187                    {
1188                        straight = false;
1189                        break;
1190                    }
1191                }
1192                if !straight {
1193                    break;
1194                }
1195                off_plane = off_plane.max(start.sub(*origin).dot(normal).abs());
1196            }
1197            if straight && off_plane > weld && worst.is_none_or(|(_, _, gap)| off_plane > gap) {
1198                worst = Some((operand, face.id, off_plane));
1199            }
1200        }
1201    }
1202    worst.map(|(operand, face_id, gap)| (operand, face_id, gap, weld))
1203}
1204
1205/// CSG volume inequality bounds (no extra boolean needed) with 1% relative slack.
1206/// `va`, `vb`, `vr` are absolute volumes of operand A, operand B, and the result.
1207fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
1208    let slack = 1e-2 * (va + vb).max(1.0);
1209    match operation {
1210        // max(A,B) ≤ A∪B ≤ A+B
1211        BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
1212        // A−B ≤ A (and ≥ 0)
1213        BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
1214        // A∩B ≤ min(A,B)
1215        BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
1216    }
1217}
1218
1219// ---------------------------------------------------------------------------
1220// N-ary booleans (Golovanov T4.4)
1221//
1222// Union / intersect / subtract of N solids in ONE imprint+fragment pass.  This
1223// is a NEW ADDITIVE entry point: the binary `boolean_operation` above is left
1224// byte-identical.  The pipeline mirrors the binary one but generalizes the two
1225// operand-specific stages to N operands:
1226//
1227//   1. Imprint EVERY unordered pair (i, j) so every mutual intersection edge is
1228//      present on both operands' faces, and accumulate all pairs into ONE global
1229//      imprint (operand indices remapped to 0..N, piece/vertex ids made globally
1230//      unique, per-face piece lists and per-edge split parameters merged).
1231//   2. Split + fragment each operand by that single global imprint, so each
1232//      operand's faces are cut by ALL of its intersections at once.
1233//   3. Select fragments in one pass: the per-fragment keep decision generalizes
1234//      from "vs one other solid" to "vs the SET of other solids" (see
1235//      `nary_keep`).  Because every pair is imprinted, a fragment's interior lies
1236//      wholly inside or wholly outside each other operand, so a single interior
1237//      test point per (fragment, other-solid) is an exact membership verdict.
1238//   4. Assemble with the same machinery (`assemble_fragments`), then merge and
1239//      validate exactly as the binary path does.
1240// ---------------------------------------------------------------------------
1241
1242/// Reverse a fragment's orientation (surface sense + every coedge), the same
1243/// transform the binary Subtract path applies to the second operand's kept
1244/// fragments.  Extracted so the n-ary selector can reuse it verbatim.
1245fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), KernelRefusal> {
1246    fragment.same_sense = !fragment.same_sense;
1247    for loop_record in &mut fragment.loops {
1248        loop_record.coedges.reverse();
1249        for coedge in &mut loop_record.coedges {
1250            coedge.forward = !coedge.forward;
1251            coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Validate, "reversed")?;
1252        }
1253    }
1254    Ok(())
1255}
1256
1257/// Imprint every unordered pair of operands and fold the pairwise
1258/// `ImprintResultRecord`s into ONE global imprint whose operand indices are the
1259/// operands' global positions (0..N).  Piece and vertex ids are re-based to be
1260/// globally unique; `by_face` piece lists and `edge_splits` parameter lists are
1261/// MERGED per key so `fragment_face` (which takes the first `by_face` match) and
1262/// `apply_edge_splits` (which collects one entry per edge) see every cut.
1263fn build_nary_imprint(
1264    operands: &[BrepSolid],
1265    options: &ImprintOptions,
1266) -> Result<ImprintResultRecord, KernelRefusal> {
1267    let mut section_evidence = false;
1268    let mut vertices: Vec<ImprintVertex> = Vec::new();
1269    let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
1270    let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
1271    let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
1272    let mut next_vertex_id: u64 = 1;
1273    let mut next_piece_id: u64 = 1;
1274
1275    for i in 0..operands.len() {
1276        for j in (i + 1)..operands.len() {
1277            let pair = build_imprints(&operands[i], &operands[j], options)?;
1278            let remap = |operand: u8| -> u8 {
1279                if operand == 0 {
1280                    i as u8
1281                } else {
1282                    j as u8
1283                }
1284            };
1285
1286            section_evidence = section_evidence || pair.section_evidence;
1287            let mut vertex_map: HashMap<u64, u64> = HashMap::default();
1288            for vertex in &pair.vertices {
1289                let global = next_vertex_id;
1290                next_vertex_id += 1;
1291                vertex_map.insert(vertex.id, global);
1292                vertices.push(ImprintVertex {
1293                    id: global,
1294                    point: vertex.point,
1295                });
1296            }
1297
1298            let mut piece_map: HashMap<u64, u64> = HashMap::default();
1299            for piece in &pair.pieces {
1300                let global = next_piece_id;
1301                next_piece_id += 1;
1302                piece_map.insert(piece.id, global);
1303                let mut mapped = piece.clone();
1304                mapped.id = global;
1305                mapped.start_vertex_id = *vertex_map
1306                    .get(&piece.start_vertex_id)
1307                    .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
1308                mapped.end_vertex_id = *vertex_map
1309                    .get(&piece.end_vertex_id)
1310                    .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
1311                for pcurve in &mut mapped.pcurves {
1312                    pcurve.operand = remap(pcurve.operand);
1313                }
1314                mapped.support_faces = [
1315                    FaceKey {
1316                        operand: remap(piece.support_faces[0].operand),
1317                        face_id: piece.support_faces[0].face_id,
1318                    },
1319                    FaceKey {
1320                        operand: remap(piece.support_faces[1].operand),
1321                        face_id: piece.support_faces[1].face_id,
1322                    },
1323                ];
1324                pieces.push(mapped);
1325            }
1326
1327            for entry in &pair.by_face {
1328                let list = by_face
1329                    .entry((remap(entry.operand), entry.face_id))
1330                    .or_default();
1331                for piece_id in &entry.piece_ids {
1332                    let global = *piece_map.get(piece_id).ok_or_else(|| {
1333                        KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: by_face references unknown piece")
1334                    })?;
1335                    list.push(global);
1336                }
1337            }
1338
1339            for split in &pair.edge_splits {
1340                edge_splits
1341                    .entry((remap(split.operand), split.edge_id))
1342                    .or_default()
1343                    .extend(split.parameters.iter().copied());
1344            }
1345        }
1346    }
1347
1348    Ok(ImprintResultRecord {
1349        vertices,
1350        pieces,
1351        tangent_nodes: Vec::new(),
1352        barrier_edges: Vec::new(),
1353        section_evidence,
1354        cosurface_pairs: Vec::new(),
1355        tangent_sections: Vec::new(),
1356        by_face: by_face
1357            .into_iter()
1358            .map(|((operand, face_id), piece_ids)| FaceImprints {
1359                operand,
1360                face_id,
1361                piece_ids,
1362            })
1363            .collect(),
1364        edge_splits: edge_splits
1365            .into_iter()
1366            .map(|((operand, edge_id), parameters)| EdgeSplitRecord {
1367                operand,
1368                edge_id,
1369                parameters,
1370            })
1371            .collect(),
1372    })
1373}
1374
1375/// N-ary union / intersect / subtract of `operands` in ONE imprint+fragment
1376/// pass (Golovanov T4.4).  Additive: does not touch the binary path.
1377///
1378/// - `Union`   : the boundary of `∪ operands`.
1379/// - `Intersect`: the boundary of `∩ operands`.
1380/// - `Subtract`: `operands[0]` minus the union of `operands[1..]`.
1381///
1382/// `N = 1` returns the sole operand; `N = 2` runs the same general pipeline and
1383/// reproduces the binary result (same volume, valid).  Returns a clear `Err`
1384/// when the operands cannot assemble into a closed, valid solid.
1385pub fn boolean_operation_nary(
1386    operands: &[BrepSolid],
1387    operation: BooleanOperation,
1388) -> Result<BrepSolid, KernelRefusal> {
1389    boolean_operation_nary_with_options(operands, operation, &BooleanOptions::default())
1390}
1391
1392/// [`boolean_operation_nary`] under the caller's [`BooleanOptions`]: the
1393/// tolerance, the coplanar-face merge and its `keep_unmerged_name_substrs`
1394/// list are honoured exactly as the binary path honours them, so a caller that
1395/// falls back from a sequence of binary unions to one arrangement (the sheet
1396/// metal walk with adjacent bends) keeps the same face partition contract.
1397pub fn boolean_operation_nary_with_options(
1398    operands: &[BrepSolid],
1399    operation: BooleanOperation,
1400    options: &BooleanOptions,
1401) -> Result<BrepSolid, KernelRefusal> {
1402    if operands.is_empty() {
1403        return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: no operands provided"));
1404    }
1405    if operands.len() == 1 {
1406        return Ok(operands[0].clone());
1407    }
1408    if operands.len() > 255 {
1409        return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: at most 255 operands supported"));
1410    }
1411    let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
1412
1413    // One tolerance policy for the whole set: the largest-scale operand's policy
1414    // dominates (every derived band is monotonic in scale), matching how the
1415    // binary path takes `for_pair` = max of the two scales.
1416    let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
1417    for operand in &operands[1..] {
1418        let candidate = KernelTolerances::for_solid(operand, options.tolerance);
1419        if candidate.sew_search > policy.sew_search {
1420            policy = candidate;
1421        }
1422    }
1423    policy.check().or_input(KernelStage::Collect, "tolerances")?;
1424    let tolerance = policy.model;
1425
1426    // Per-operand fuse-first healing, exactly as the binary path (a no-op on
1427    // clean input, so clean operands are untouched).
1428    let mut healed = operands.to_vec();
1429    for operand in &mut healed {
1430        crate::heal::heal_operands(operand, &policy)?;
1431        // Then refit any inherited trim off the pcurve construction contract
1432        // whose edge lies on every carrier (a no-op without one).
1433        crate::heal::refit_inherited_trims(operand, &policy)?;
1434        // Same operand normalization as the binary path: seam edges for
1435        // seamless full-period band faces.
1436        normalize_operand_band_seams(operand)?;
1437    }
1438
1439    // 1. Imprint every pair into one global imprint.
1440    let mut imprint_options = options.imprint.clone();
1441    imprint_options.tolerance = tolerance;
1442    let imprint = build_nary_imprint(&healed, &imprint_options)?;
1443
1444    // 2. Split every operand's edges, then fragment its faces, by that imprint.
1445    let mut split = Vec::with_capacity(healed.len());
1446    for (index, operand) in healed.iter().enumerate() {
1447        split.push(apply_edge_splits(operand, index as u8, &imprint)?);
1448    }
1449    let mut fragments = Vec::with_capacity(split.len());
1450    for (index, solid) in split.iter().enumerate() {
1451        fragments.push(fragment_solid(solid, index as u8, &imprint)?);
1452    }
1453
1454    // 3. Select fragments in one pass against the set of other operands.
1455    let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
1456
1457    // 4. Assemble + normalize + validate, mirroring the binary tail.
1458    let solids: HashMap<u8, &BrepSolid> = split
1459        .iter()
1460        .enumerate()
1461        .map(|(index, solid)| (index as u8, solid))
1462        .collect();
1463    let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
1464    let solid = if options.merge_coplanar_faces
1465        && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
1466    {
1467        let merged = merge_same_surface_faces_excluding(
1468            &solid,
1469            tolerance,
1470            &options.keep_unmerged_name_substrs,
1471        )?;
1472        merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
1473    } else {
1474        solid
1475    };
1476    let validation = solid.validate_detailed(&policy);
1477    if !validation.issues.is_empty() {
1478        return Err(KernelRefusal::new(
1479            RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
1480            KernelStage::Validate,
1481            format!(
1482            "boolean_operation_nary: invalid result: {:?}",
1483            validation.issues
1484        )));
1485    }
1486    if debug {
1487        if let Ok(report) =
1488            crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
1489        {
1490            if report.is_flagged() {
1491                eprintln!(
1492                    "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
1493                    report.disagreement_rate,
1494                    report.disagreements.len(),
1495                    report.considered,
1496                    report.sample_disagreement()
1497                );
1498            }
1499        }
1500    }
1501    Ok(solid)
1502}
1503
1504/// TANGENT-NODE ATTRIBUTION.
1505///
1506/// The imprint's second-order filter decides which tangential contacts are
1507/// isolated NODES the arrangement can carve, and marches those instead of
1508/// refusing. It cannot decide a THIRD-order singularity — where the transverse
1509/// branch is tangent to a contact curve, every second-order quantity vanishes
1510/// together and the contact reads as a clean saddle at every stopping rule
1511/// (`imprint/tangent_contact.rs`). The assembly is the authority on whether an
1512/// admitted node was really imprintable, so a tear downstream of one is
1513/// reported AGAINST THE NODE rather than as an anonymous degeneracy: same
1514/// refusal class the whole tangent-node family carries, naming the point.
1515///
1516/// Only classes that are ALREADY perturbation-eligible are re-attributed, so a
1517/// refusal that could never take the ladder still cannot. The stage's own
1518/// message — the one-use edges, the genus — is NOT carried into the refusal:
1519/// `tangent_node_assembly` and `torus_torus_singular_refusal` hold the
1520/// tangent-node refusal to naming its cause without the arrangement's
1521/// symptom text ("open between", "genus", "pcurve"), the policy the class
1522/// exists for. It is printed under `BREP_DEBUG_BOOL` instead, so what the
1523/// assembly actually read is still there for a reader of the trace.
1524///
1525/// It does decide WHICH refusals are retried, though, and since 2026-09-13 that
1526/// matters: `boolean_operation`'s failure arm gates `TangentNodeSingularity` out
1527/// of the perturbation ladder (the TANGENT-NODE GATE there) and reads the class
1528/// AFTER this rewrite. So a tear downstream of an ADMITTED node — whatever class
1529/// the pipeline actually raised — declines the ladder along with the tangent-node
1530/// family proper. That is the conservative direction and the same claim the class
1531/// makes (a tear after a node is the node proving unimprintable). Anything added
1532/// to this rewrite widens that gate: read the gate when you touch the
1533/// attribution.
1534fn attribute_to_tangent_node(error: KernelRefusal, nodes: &[Vec3]) -> KernelRefusal {
1535    let Some(node) = nodes.first() else {
1536        return error;
1537    };
1538    if !error.class.perturbation_eligible()
1539        || matches!(error.class, RefusalClass::TangentNodeSingularity)
1540    {
1541        return error;
1542    }
1543    if std::env::var("BREP_DEBUG_BOOL").is_ok() {
1544        eprintln!(
1545            "boolean: tear downstream of the tangent node at ({:.6},{:.6},{:.6}) attributed to it; the stage read: {}",
1546            node.x, node.y, node.z, error.message
1547        );
1548    }
1549    KernelRefusal::new(
1550        RefusalClass::TangentNodeSingularity,
1551        error.stage,
1552        format!(
1553            "boolean: unsupported singular/tangent-node surface intersection \
1554             at the tangent node at ({:.6},{:.6},{:.6}): the section through it did not \
1555             assemble into a closed shell",
1556            node.x, node.y, node.z
1557        ),
1558    )
1559}
1560
1561/// The tangent-curve lane's attribution: an ASSEMBLY refusal whose faces the
1562/// imprint found touching along a CURVE is reported against that contact, by
1563/// its faces and its ends, in the tangent-node class — the arrangement's own
1564/// message (an open loop, a one-use edge) is the symptom, the contact is the
1565/// cause, and the class keeps the refusal out of the perturbation ladder
1566/// exactly as an admitted node's tear is kept out
1567/// (`attribute_to_tangent_node`). The original message is carried along so
1568/// nothing the stage said is hidden.
1569///
1570/// REACH: the faces the refusal names, and only those. `origins` maps the
1571/// assembled result's face ids (minted one per selected fragment, from 1, in
1572/// order) back to operand faces; the ids are read off the message's
1573/// `face <id>` (validate's open-loop issues) and `on faces [<id> '…`
1574/// (one-use edges) entries, and the rewrite happens only when one of them is
1575/// a face the section INVOLVES (`TangentSectionRecord::faces`: the carriers'
1576/// faces and the faces across the edges its ends lie on). A refusal naming no
1577/// face, or only faces the contact does not touch — a model with a benign
1578/// recorded contact (a blind pocket touching a wall) that fails elsewhere —
1579/// keeps its class and enters the ladder as it always did. With
1580/// `ImprintOptions::tangent_curve_lane` off the sections are empty and
1581/// nothing is rewritten.
1582fn attribute_to_tangent_section(
1583    error: KernelRefusal,
1584    sections: &[crate::imprint::TangentSectionRecord],
1585    origins: &[FaceKey],
1586) -> KernelRefusal {
1587    if sections.is_empty()
1588        || !error.class.perturbation_eligible()
1589        || matches!(error.class, RefusalClass::TangentNodeSingularity)
1590    {
1591        return error;
1592    }
1593    let named = refusal_result_faces(&error.message)
1594        .into_iter()
1595        .filter_map(|id| origins.get(id.checked_sub(1)? as usize).copied())
1596        .collect::<Vec<FaceKey>>();
1597    // A record the lane's own sites minted before one the grazing-pierce
1598    // path minted (`TangentSectionRecord::grazing`): the sprocket slit star's
1599    // cutter names its traced contact, not the pierce of a neighbouring pair.
1600    let involving = |fallback: bool| {
1601        sections.iter().find(|section| {
1602            section.grazing.is_empty() != fallback && named.iter().any(|face| section.faces.contains(face))
1603        })
1604    };
1605    let Some(section) = involving(false).or_else(|| involving(true)) else {
1606        return error;
1607    };
1608    // A clipped end sits on a trim at round-off, and `-0.000000` names no
1609    // place: print what six decimals can show.
1610    let at = |point: Vec3| {
1611        let shown = |value: f64| if value.abs() < 5e-7 { 0.0 } else { value };
1612        format!("({:.6},{:.6},{:.6})", shown(point.x), shown(point.y), shown(point.z))
1613    };
1614    // The grazing pierces (`imprint/driver.rs`, 2026-10-17): trim edges that
1615    // pierce the other carrier inside both trims below the seed gate while
1616    // the pair marched nothing. A record the lane built stands on its contact
1617    // and carries them as a note; a record standing on the pierces ALONE
1618    // (its `start` IS the first pierce) is named by them.
1619    let pierces = (!section.grazing.is_empty()).then(|| {
1620        let shown = section.grazing.len().min(4);
1621        let mut listed = section.grazing[..shown]
1622            .iter()
1623            .map(|(point, tangency)| format!("{} at |tan·n| = {tangency:.4}", at(*point)))
1624            .collect::<Vec<_>>()
1625            .join(", ");
1626        if section.grazing.len() > shown {
1627            listed.push_str(&format!(" and {} more", section.grazing.len() - shown));
1628        }
1629        listed
1630    });
1631    let pierce_only = section.pierce_only();
1632    let shape = if let (true, Some(pierces)) = (pierce_only, pierces.as_deref()) {
1633        format!(
1634            "cross at a grazing angle: a trim edge pierces the other carrier inside both \
1635             trims at {pierces}, below the marcher's start gate, and no transverse section was \
1636             marched between the pierces — a near-tangent crossing born on a tangent contact \
1637             (a third-order singularity) the imprint does not build yet"
1638        )
1639    } else if section.planar {
1640        format!(
1641            "touch along a tangent curve from {} to {} (length {:.6}, {} stations{}) which the \
1642             imprint does not build yet",
1643            at(section.start),
1644            at(section.end),
1645            section.length,
1646            section.stations,
1647            if section.closed { ", closed" } else { "" }
1648        )
1649    } else {
1650        format!(
1651            "touch along a curve through {} with neither carrier planar, a curved/curved \
1652             tangent contact the lane does not trace yet",
1653            at(section.start)
1654        )
1655    };
1656    let shape = match (pierce_only, pierces) {
1657        (false, Some(pierces)) => format!(
1658            "{shape}; a trim edge also pierces the other carrier inside both trims at {pierces}, \
1659             below the marcher's start gate, with no transverse section marched"
1660        ),
1661        _ => shape,
1662    };
1663    let involved = named
1664        .iter()
1665        .filter(|face| section.faces.contains(face))
1666        .map(|face| format!("{}/{}", face.operand, face.face_id))
1667        .collect::<Vec<_>>()
1668        .join(", ");
1669    KernelRefusal::new(
1670        RefusalClass::TangentNodeSingularity,
1671        error.stage,
1672        format!(
1673            "boolean: unsupported tangent-curve surface intersection between faces {}/{} and {}/{}: \
1674             the carriers {shape}; the refusal names face(s) {involved} the contact involves; {}",
1675            section.first.operand, section.first.face_id, section.second.operand, section.second.face_id,
1676            error.message
1677        ),
1678    )
1679}
1680
1681/// The RESULT face ids an assembly refusal names: every `face <id>` (the
1682/// validator's "loop … of face <id> is open …") and every `<id> '` entry of an
1683/// `on faces [...]` list (the one-use edge census). Nothing else in the
1684/// message is read.
1685fn refusal_result_faces(message: &str) -> Vec<u64> {
1686    let mut ids = Vec::new();
1687    let digits_after = |text: &str| -> Option<u64> {
1688        let end = text.find(|c: char| !c.is_ascii_digit()).unwrap_or(text.len());
1689        text[..end].parse().ok()
1690    };
1691    for (index, _) in message.match_indices("face ") {
1692        if let Some(id) = digits_after(&message[index + "face ".len()..]) {
1693            ids.push(id);
1694        }
1695    }
1696    if let Some(start) = message.find("on faces [") {
1697        let rest = &message[start + "on faces [".len()..];
1698        let list = &rest[..rest.find(']').unwrap_or(rest.len())];
1699        for entry in list.split(", ") {
1700            let entry = entry.trim_start();
1701            if let Some(id) = digits_after(entry) {
1702                if entry[id.to_string().len()..].starts_with(" '") {
1703                    ids.push(id);
1704                }
1705            }
1706        }
1707    }
1708    ids.sort_unstable();
1709    ids.dedup();
1710    ids
1711}
1712
1713pub fn boolean_operation_with_diagnostics(
1714    first: &BrepSolid,
1715    second: &BrepSolid,
1716    operation: BooleanOperation,
1717    options: &BooleanOptions,
1718) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1719    let _account = profile::Scope::new("boolean.other");
1720    let closed_inputs = matches!(std::env::var("BREP_CLOSED_OPERAND_INPUT").as_deref(), Err(_) | Ok("1") | Ok("on"));
1721    if let (true, Some(later)) = (reseat::seam_reseat_enabled(), &options.seam_reseat_later_references) {
1722        let started = Instant::now();
1723        let (first, second) = reseat::reseat_tangent_seams(first, second, later, options.tolerance);
1724        profile_stage("seam_prepare_ms", started.elapsed().as_secs_f64() * 1000.0);
1725        return boolean_operation_with_diagnostics_impl(&first, &second, operation, options, closed_inputs);
1726    }
1727    boolean_operation_with_diagnostics_impl(first, second, operation, options, closed_inputs)
1728}
1729
1730fn boolean_operation_with_diagnostics_impl(
1731    first: &BrepSolid,
1732    second: &BrepSolid,
1733    operation: BooleanOperation,
1734    options: &BooleanOptions,
1735    closed_inputs: bool,
1736) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1737    // One coedge-check memo per operation (`BREP_VALIDATE_COEDGE_MEMO`): the
1738    // assembly, face-merge and closing validations re-read coedges unchanged.
1739    let _coedge_memo = crate::topology::CoedgeMemoScope::open(crate::topology::coedge_memo_enabled());
1740    // Plan 1.6: only a declared Sheet enters the trim path. The unchanged
1741    // Solid path continues below; no kind is inferred from failed closure.
1742    if matches!(operation, BooleanOperation::Intersect)
1743        && [first, second].iter().any(|body| body.kind == crate::BodyKind::Sheet)
1744        && match std::env::var("BREP_SHEET_TRIM").as_deref() {
1745            Ok("1") => true,
1746            Ok("0") => false,
1747            _ => options.sheet_trim,
1748        }
1749    {
1750        return sheet::trim(first, second, options);
1751    }
1752    // OPERAND-KIND GATE. A body that DECLARES itself a Sheet has no
1753    // inside, so a union or subtract with it has no solid answer; the
1754    // survey (record 2026-10-12, table 2) measured the arrangement
1755    // returning a validating solid of volume 3.5 when the sheet's opening
1756    // lay outside the other operand. Read from the declaration only,
1757    // before any arrangement work (nothing infers a kind from the edge
1758    // census; an undeclared open body is the closure gate's below).
1759    // Intersect is not gated here.
1760    if matches!(operation, BooleanOperation::Union | BooleanOperation::Subtract) {
1761        if let Some(operand) = [first, second]
1762            .iter()
1763            .position(|solid| solid.kind == crate::BodyKind::Sheet)
1764        {
1765            return Err(KernelRefusal::input(
1766                KernelStage::Collect,
1767                "sheet_operand",
1768                format!(
1769                    "boolean: operand {operand} is a Sheet, which encloses no solid; union and \
1770                     subtract of a sheet are refused; Intersect trims a Sheet by a Solid"
1771                ),
1772            ));
1773        }
1774    }
1775    // Inside/outside is undefined for an open operand, even if its opening
1776    // misses the other operand and the constructed result happens to validate.
1777    // Count existing edge uses, without modifying/healing either operand.
1778    // Keep failures on their original path and preserve their class/evidence;
1779    // the existing perturbation gate already diagnoses open failed operands.
1780    let mut outcome = boolean_operation_with_diagnostics_core(first, second, operation, options)?;
1781    let closure_started = closed_inputs.then(Instant::now);
1782    if closed_inputs {
1783        for (operand, solid) in [first, second].into_iter().enumerate() {
1784            let open = one_use_edges(solid);
1785            if open > 0 {
1786                return Err(KernelRefusal::input(KernelStage::Collect, "operand_closure", format!(
1787                    "boolean: operand {operand} is not a closed shell: {open} of its edges bound only one face, so its inside is undefined"
1788                )));
1789            }
1790        }
1791    }
1792    if let Some(started) = closure_started {
1793        let ms = started.elapsed().as_secs_f64() * 1000.0;
1794        profile_stage("operand_closure_ms", ms);
1795        outcome.diagnostics.measure_max("timing.operand_closure_ms", ms);
1796    }
1797    Ok(outcome)
1798}
1799
1800fn boolean_operation_with_diagnostics_core(
1801    first: &BrepSolid, second: &BrepSolid, operation: BooleanOperation, options: &BooleanOptions,
1802) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1803
1804    // The translation control (`BREP_TRANSLATION_CONTROL`, `brep/recentre.rs`):
1805    // the outermost entry runs the operation on translated operands.
1806    if let Some(offset) = crate::recentre::control_offset_at_entry() {
1807        return boolean_operation_with_diagnostics_at_offset(first, second, operation, options, offset);
1808    }
1809    // The recentre guard: operands far from the origin in their own
1810    // units run at the origin.
1811    let mut bounds = operand_bounds(first);
1812    bounds.include(operand_bounds(second));
1813    if let Some(offset) = crate::recentre::guard_offset(bounds) {
1814        crate::recentre::profile_fired("boolean", offset, bounds);
1815        let mut outcome = boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Guard)?;
1816        outcome.diagnostics.count_n("recentre.fired", 1);
1817        outcome.diagnostics.measure_max("recentre.distance", offset.length());
1818        return Ok(outcome);
1819    }
1820    let mut tangent_nodes = Vec::new();
1821    let (mut outcome, soundness_candidates) =
1822        boolean_pipeline(first, second, operation, options, &mut tangent_nodes)
1823            .map_err(|error| attribute_to_tangent_node(error, &tangent_nodes))?;
1824    // The assembly's single exit. A boolean can hand back a body whose walls
1825    // pass through each other while `validate()` is silent and the volume
1826    // measures right, which is exactly the failure a regularized boolean must
1827    // not report as success. A crossing the OPERANDS already had is theirs and
1828    // passes through — the defect came in with the input and refusing it here
1829    // would blame the wrong operation — so the operands are only scanned when
1830    // the result is flagged, and the common case pays for one scan. The
1831    // acceptance's refusal is typed at its origin (an `UnsoundResult` naming
1832    // the defect and the faces); it was classed `Internal` here while
1833    // `accept_sound_against` still returned text. Timed as its own stage: the
1834    // record's phase split read only the pipeline's `timing.*` measures, and
1835    // on a 108-face disjoint union this scan was 185 of 226 ms end to end.
1836    let stage_started = Instant::now();
1837    let accepted = match &soundness_candidates {
1838        // Pass-through: the faces the operation touched, their
1839        // neighbours and every face whose box reaches the other operand.
1840        Some(candidates) => crate::accept_sound_against_scoped(&[first, second], outcome.value, "boolean", candidates),
1841        None => crate::accept_sound_against(&[first, second], outcome.value, "boolean"),
1842    };
1843    let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1844    profile_stage("soundness_ms", stage_ms);
1845    outcome.diagnostics.measure_max("timing.soundness_ms", stage_ms);
1846    outcome.value = accepted?;
1847    Ok(outcome)
1848}
1849
1850/// [`boolean_operation_with_diagnostics`] with both operands translated by
1851/// `offset` for the operation and the result translated back. Refusal
1852/// messages name the translated coordinates.
1853pub fn boolean_operation_with_diagnostics_at_offset(
1854    first: &BrepSolid,
1855    second: &BrepSolid,
1856    operation: BooleanOperation,
1857    options: &BooleanOptions,
1858    offset: Vec3,
1859) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1860    boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Control)
1861}
1862
1863/// The translated run behind the control and the guard.
1864fn boolean_translated(
1865    first: &BrepSolid,
1866    second: &BrepSolid,
1867    operation: BooleanOperation,
1868    options: &BooleanOptions,
1869    offset: Vec3,
1870    kind: crate::recentre::EntryKind,
1871) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1872    let _entry = crate::recentre::TranslatedEntry::open(kind);
1873    let translate = |solid: &BrepSolid, offset: Vec3| {
1874        crate::recentre::translate_solid(solid, offset).map_err(|message| {
1875            KernelRefusal::internal(
1876                KernelStage::Collect,
1877                "translation_control",
1878                format!("boolean: the translation control could not move an operand: {message}"),
1879            )
1880        })
1881    };
1882    let moved_first = translate(first, offset)?;
1883    let moved_second = translate(second, offset)?;
1884    let outcome = boolean_operation_with_diagnostics(&moved_first, &moved_second, operation, options)?;
1885    let value = translate(&outcome.value, offset.scale(-1.0))?;
1886    Ok(KernelOutcome { value, diagnostics: outcome.diagnostics })
1887}
1888
1889/// Stage wall-clock line on stderr when `BREP_PROFILE` is set — the same switch
1890/// `imprint.profile` and `assemble.*_ms` use — so a REFUSED boolean, whose
1891/// `KernelDiagnostics` never reach a caller, still shows where its time went.
1892fn profile_stage(code: &str, ms: f64) {
1893    let row = match code {
1894        "heal_ms" => Some("boolean.operand_prepare"),
1895        "imprint_ms" => Some("boolean.imprint"),
1896        "edge_split_ms" => Some("boolean.edge_split"),
1897        "fragment_ms" => Some("boolean.fragment"),
1898        "select_ms" => Some("boolean.select"),
1899        "assemble_ms" => Some("boolean.assemble"),
1900        "face_merge_ms" => Some("boolean.face_merge"),
1901        "soundness_ms" => Some("boolean.soundness"),
1902        "operand_closure_ms" => Some("boolean.operand_closure"),
1903        "seam_prepare_ms" => Some("boolean.seam_prepare"),
1904        "final_validate_ms" => Some("boolean.final_validate"),
1905        _ => None,
1906    };
1907    if let Some(row) = row { profile::record(row, ms); }
1908    if crate::profile::profile_enabled() {
1909        profile_line!("boolean.{code}={ms:.2}");
1910    }
1911}
1912
1913/// `BREP_BOOLEAN_DUMP` names a directory. Every binary boolean then writes its
1914/// stages there as JSON — the two split operands, the selected fragments, the
1915/// assembled solid and the merged one — numbered per process, so a probe can
1916/// read a face's loops at each stage without replaying the document.
1917struct BooleanStageDump {
1918    directory: std::path::PathBuf,
1919    serial: usize,
1920}
1921
1922impl BooleanStageDump {
1923    fn open() -> Option<Self> {
1924        static SERIAL: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1925        let directory = std::env::var("BREP_BOOLEAN_DUMP").ok().filter(|value| !value.is_empty())?;
1926        let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1927        Some(Self { directory: directory.into(), serial })
1928    }
1929
1930    fn write<T: Serialize + ?Sized>(&self, stage: &str, value: &T) {
1931        let path = self.directory.join(format!("{:03}-{stage}.json", self.serial));
1932        match serde_json::to_string(value) {
1933            Ok(text) => {
1934                if let Err(error) = std::fs::write(&path, text) {
1935                    eprintln!("BREP_BOOLEAN_DUMP {}: {error}", path.display());
1936                }
1937            }
1938            Err(error) => eprintln!("BREP_BOOLEAN_DUMP {stage}: {error}"),
1939        }
1940    }
1941}
1942
1943/// The tolerance policy a binary boolean of `first` and `second` runs under.
1944fn boolean_policy(
1945    first: &BrepSolid,
1946    second: &BrepSolid,
1947    options: &BooleanOptions,
1948) -> Result<KernelTolerances, KernelRefusal> {
1949    let policy = options
1950        .tolerances
1951        .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
1952    policy.check().or_input(KernelStage::Collect, "tolerances")?;
1953    Ok(policy)
1954}
1955
1956/// The operands as the imprint reads them: healed, with their band seams
1957/// normalized.
1958fn prepared_operands(
1959    first: &BrepSolid,
1960    second: &BrepSolid,
1961    policy: &KernelTolerances,
1962) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
1963    // Fuse-first operand healing (Lever A): before any intersection touches the
1964    // operands, snap each one's near-coincident / off-plane vertices to exact
1965    // and re-anchor the incident edge curves onto them, so noisy
1966    // near-degenerate input (points that should coincide but drifted, vertices
1967    // a few microns off a planar face) cannot tip a fixed downstream band over
1968    // the edge.  Healing is per-operand, validate-gated, and — because a clean
1969    // operand has no vertices within `heal_tol` and none off its planes — a
1970    // no-op that leaves the boolean output byte-identical on clean inputs.
1971    let stage_started = Instant::now();
1972    let mut first_owned = first.clone();
1973    let mut second_owned = second.clone();
1974    crate::heal::heal_operands(&mut first_owned, policy)?;
1975    crate::heal::heal_operands(&mut second_owned, policy)?;
1976    profile_stage("heal.operands_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
1977    // Then refit, on each operand's own carriers, any inherited trim off the
1978    // pcurve construction contract whose edge lies on every incident carrier
1979    // (`heal::refit_inherited_trims`; a byte-identical no-op without one).
1980    let refit_started = Instant::now();
1981    crate::heal::refit_inherited_trims(&mut first_owned, policy)?;
1982    crate::heal::refit_inherited_trims(&mut second_owned, policy)?;
1983    profile_stage("heal.refit_ms", refit_started.elapsed().as_secs_f64() * 1_000.0);
1984    if crate::profile::profile_enabled() {
1985        let (hits, misses) = crate::reading_memo::counts();
1986        profile_line!("boolean.reading_memo hits={hits} misses={misses}");
1987    }
1988    profile_stage("heal_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
1989    // Seamless full-period band faces (STEP import) break the seam-aware
1990    // imprint/arrangement machinery; normalize them to the seam-carrying
1991    // topology native periodic faces use. No-op on operands without such
1992    // faces. See `insert_periodic_band_seam_edges`.
1993    normalize_operand_band_seams(&mut first_owned)?;
1994    normalize_operand_band_seams(&mut second_owned)?;
1995    Ok((first_owned, second_owned))
1996}
1997
1998fn boolean_imprint_options(options: &BooleanOptions, tolerance: f64) -> ImprintOptions {
1999    let mut imprint_options = options.imprint.clone();
2000    imprint_options.tolerance = tolerance;
2001    imprint_options
2002}
2003
2004/// The two operands a binary boolean's arrangement reads: prepared as the
2005/// boolean prepares them, with every boundary edge split where the imprint's
2006/// sections meet it. These are the `NNN-first` and `NNN-second` stages
2007/// `BREP_BOOLEAN_DUMP` writes, not the operands at the boolean's entry.
2008///
2009/// A boolean must read the same on these as on the operands it split them
2010/// from: the splits are vertices its own imprint puts on those edges anyway.
2011/// Where it does not, an imprint lane is deciding on what the input happens to
2012/// have split, and that is a defect.
2013pub fn boolean_split_operands(
2014    first: &BrepSolid,
2015    second: &BrepSolid,
2016    options: &BooleanOptions,
2017) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
2018    let policy = boolean_policy(first, second, options)?;
2019    let (first, second) = prepared_operands(first, second, &policy)?;
2020    let imprint = build_imprints(&first, &second, &boolean_imprint_options(options, policy.model))?;
2021    let (split_first, _) = apply_edge_splits_with_map(&first, 0, &imprint)?;
2022    let (split_second, _) = apply_edge_splits_with_map(&second, 1, &imprint)?;
2023    Ok((split_first, split_second))
2024}
2025
2026/// Conservative bounds of an operand: its vertices and every face's control
2027/// net (a NURBS lies in the hull of its controls). A control net that cannot
2028/// be read bounds everything, so nothing is left out for want of a reading.
2029fn operand_bounds(solid: &BrepSolid) -> crate::Aabb {
2030    let mut bounds = crate::Aabb::from_points(solid.vertices.iter().map(|vertex| vertex.point));
2031    for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2032        match crate::Aabb::from_surface_controls(&face.surface) {
2033            Ok(face_bounds) => bounds.include(face_bounds),
2034            Err(_) => {
2035                return crate::Aabb {
2036                    minimum: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
2037                    maximum: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
2038                }
2039            }
2040        }
2041    }
2042    bounds
2043}
2044
2045/// Row 12's gate: the boxes are separated or touch exactly in at least two
2046/// axes. A trimmed face lies in its carrier's control-net box when every
2047/// weight is positive, so a point interior to both operands would lie
2048/// strictly inside both boxes in every axis; two such axes leave a line or a
2049/// point of contact, which has no area to merge.
2050fn line_or_point_contact(first: &BrepSolid, second: &BrepSolid, bounds: [crate::Aabb; 2]) -> bool {
2051    let plain = |solid: &BrepSolid| {
2052        solid.kind == crate::BodyKind::Solid
2053            && solid.cells.is_empty()
2054            && solid.shells.iter().flat_map(|shell| &shell.faces).all(|face| {
2055                face.surface.control_points.iter().flatten().all(|control| control.w > 0.0 && control.w.is_finite())
2056            })
2057    };
2058    let [a, b] = bounds;
2059    let apart = |a_min: f64, a_max: f64, b_min: f64, b_max: f64| a_max <= b_min || b_max <= a_min;
2060    let axes = [
2061        apart(a.minimum.x, a.maximum.x, b.minimum.x, b.maximum.x),
2062        apart(a.minimum.y, a.maximum.y, b.minimum.y, b.maximum.y),
2063        apart(a.minimum.z, a.maximum.z, b.minimum.z, b.maximum.z),
2064    ];
2065    axes.iter().filter(|&&apart| apart).count() >= 2 && plain(first) && plain(second)
2066}
2067
2068/// The result faces the soundness scan reads under pass-through: the
2069/// examined faces (touched and their neighbours) and every other face whose
2070/// control-net box reaches the OTHER operand's bounds within `band`. A face
2071/// outside both is an operand's own, clear of everything the operation added.
2072fn soundness_candidates(
2073    solid: &BrepSolid,
2074    scope: &AssemblyScope,
2075    bounds: [crate::Aabb; 2],
2076    band: f64,
2077) -> HashSet<u64> {
2078    let mut candidates = HashSet::default();
2079    for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2080        if scope.examined.contains(&face.id) {
2081            candidates.insert(face.id);
2082            continue;
2083        }
2084        let Some(&operand) = scope.operand_of.get(&face.id) else {
2085            candidates.insert(face.id);
2086            continue;
2087        };
2088        let other = bounds[usize::from(operand == 0)];
2089        match crate::Aabb::from_surface_controls(&face.surface) {
2090            Ok(face_bounds) if !face_bounds.intersects(other, band) => {}
2091            _ => {
2092                candidates.insert(face.id);
2093            }
2094        }
2095    }
2096    candidates
2097}
2098
2099/// The empty solid a legitimately empty boolean returns.
2100fn empty_solid() -> BrepSolid {
2101    BrepSolid {
2102        mass_properties_cache: Default::default(),
2103        cells: Default::default(),
2104        id: 0,
2105        vertices: Vec::new(),
2106        edges: Vec::new(),
2107        shells: Vec::new(),
2108        genus: 0,
2109        kind: crate::BodyKind::Solid,
2110    }
2111}
2112
2113/// The two operands as one solid, shell by shell: ids minted afresh in the
2114/// assembler's order (the first operand's faces, then the second's), the
2115/// second operand's face names suffixed where they clash with the first's
2116/// as the assembler suffixes a split face, geometry copied byte for byte,
2117/// genus the sum. Nothing is welded: the operands are disjoint.
2118fn concatenate_disjoint(first: &BrepSolid, second: &BrepSolid) -> BrepSolid {
2119    let mut vertices = Vec::new();
2120    let mut edges = Vec::new();
2121    let mut shells = Vec::new();
2122    let mut used_face_names = HashSet::<String>::default();
2123    let (mut next_face_id, mut next_loop_id, mut next_coedge_id) = (1u64, 1u64, 1u64);
2124    for operand in [first, second] {
2125        let vertex_base = vertices.len() as u64;
2126        let edge_base = edges.len() as u64;
2127        let mut vertex_ids: HashMap<u64, u64> = HashMap::default();
2128        for (index, vertex) in operand.vertices.iter().enumerate() {
2129            let id = vertex_base + index as u64 + 1;
2130            vertex_ids.insert(vertex.id, id);
2131            vertices.push(VertexRecord { id, point: vertex.point });
2132        }
2133        let mut edge_ids: HashMap<u64, u64> = HashMap::default();
2134        for (index, edge) in operand.edges.iter().enumerate() {
2135            let id = edge_base + index as u64 + 1;
2136            edge_ids.insert(edge.id, id);
2137            edges.push(EdgeRecord {
2138                id,
2139                curve: edge.curve.clone(),
2140                t0: edge.t0,
2141                t1: edge.t1,
2142                start_vertex_id: vertex_ids.get(&edge.start_vertex_id).copied().unwrap_or(edge.start_vertex_id),
2143                end_vertex_id: vertex_ids.get(&edge.end_vertex_id).copied().unwrap_or(edge.end_vertex_id),
2144                degenerate: edge.degenerate,
2145                name: edge.name.clone(),
2146            });
2147        }
2148        for shell in &operand.shells {
2149            let mut faces = Vec::with_capacity(shell.faces.len());
2150            for face in &shell.faces {
2151                let loops = face
2152                    .loops
2153                    .iter()
2154                    .map(|loop_record| {
2155                        let coedges = loop_record
2156                            .coedges
2157                            .iter()
2158                            .map(|coedge| {
2159                                let id = next_coedge_id;
2160                                next_coedge_id += 1;
2161                                CoedgeRecord {
2162                                    id,
2163                                    edge_id: edge_ids.get(&coedge.edge_id).copied().unwrap_or(coedge.edge_id),
2164                                    forward: coedge.forward,
2165                                    pcurve: coedge.pcurve.clone(),
2166                                }
2167                            })
2168                            .collect();
2169                        let id = next_loop_id;
2170                        next_loop_id += 1;
2171                        LoopRecord { id, coedges }
2172                    })
2173                    .collect();
2174                let name = face.name.clone().map(|base| {
2175                    if used_face_names.insert(base.clone()) {
2176                        base
2177                    } else {
2178                        let mut suffix = 1usize;
2179                        loop {
2180                            let candidate = format!("{base}_{suffix}");
2181                            if used_face_names.insert(candidate.clone()) {
2182                                break candidate;
2183                            }
2184                            suffix += 1;
2185                        }
2186                    }
2187                });
2188                faces.push(FaceRecord {
2189                    id: next_face_id,
2190                    surface: face.surface.clone(),
2191                    same_sense: face.same_sense,
2192                    loops,
2193                    name,
2194                });
2195                next_face_id += 1;
2196            }
2197            shells.push(ShellRecord { id: shells.len() as u64 + 1, faces });
2198        }
2199    }
2200    BrepSolid {
2201        mass_properties_cache: Default::default(),
2202        cells: Default::default(),
2203        id: 1,
2204        vertices,
2205        edges,
2206        shells,
2207        genus: first.genus + second.genus,
2208        kind: crate::BodyKind::Solid,
2209    }
2210}
2211
2212/// The binary boolean. The second value is the soundness scan's candidate
2213/// faces under pass-through (`None` reads the whole result).
2214fn boolean_pipeline(
2215    first: &BrepSolid,
2216    second: &BrepSolid,
2217    operation: BooleanOperation,
2218    options: &BooleanOptions,
2219    tangent_nodes: &mut Vec<Vec3>,
2220) -> Result<(KernelOutcome<BrepSolid>, Option<HashSet<u64>>), KernelRefusal> {
2221    let replaced_operands = operand_state::replace_operands(first, second, operation, options);
2222    let (first, second) = match &replaced_operands {
2223        Some((first, second)) => (first, second),
2224        None => (first, second),
2225    };
2226    let policy = boolean_policy(first, second, options)?;
2227    let tolerance = policy.model;
2228    // DISJOINT FAST PATH. A box test on the control-net hulls, not "no
2229    // section evidence": a tool inside the other operand also leaves no
2230    // evidence and must stay on the pipeline, where a union is the outer
2231    // operand, a subtract the outer with a void shell and an intersect the
2232    // tool. Operands clear of each other by more than the search band can
2233    // touch nothing, so nothing is prepared, imprinted, assembled or
2234    // scanned: a union is the two operands side by side, a subtract the
2235    // first operand, an intersect the legitimate-empty result below. The
2236    // soundness scan is skipped as well (an empty candidate set): the
2237    // operands cannot cross each other, and a crossing an operand carried in
2238    // passes through the full acceptance unrepaired exactly the same.
2239    //
2240    // CONTACT FAST PATH (row 12): boxes that only meet along a line or at a
2241    // point, read exactly; see `line_or_point_contact`.
2242    let bounds = [operand_bounds(first), operand_bounds(second)];
2243    let disjoint = disjoint_fast_path(options) && !bounds[0].intersects(bounds[1], policy.sew_search);
2244    let contact = !disjoint
2245        && contact_fast_path(options)
2246        && options.seam_reseat_later_references.is_some()
2247        && line_or_point_contact(first, second, bounds);
2248    if disjoint || contact {
2249        let operation_started = Instant::now();
2250        let mut diagnostics = KernelDiagnostics::default();
2251        diagnostics.count_n(
2252            "collect.faces",
2253            first.shells.iter().chain(&second.shells).map(|shell| shell.faces.len() as u64).sum(),
2254        );
2255        diagnostics.count(if contact { "boolean.contact_fast_path" } else { "boolean.disjoint_fast_path" });
2256        diagnostics.measure_max("tolerance.model", policy.model);
2257        diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
2258        let value = match operation {
2259            BooleanOperation::Union => concatenate_disjoint(first, second),
2260            BooleanOperation::Subtract => first.clone(),
2261            BooleanOperation::Intersect => {
2262                diagnostics.event(
2263                    DiagnosticSeverity::Info,
2264                    KernelStage::Select,
2265                    "boolean.empty_result",
2266                    format!("{operation:?} of witnessed-non-overlapping operands is empty"),
2267                );
2268                empty_solid()
2269            }
2270        };
2271        let stage_ms = operation_started.elapsed().as_secs_f64() * 1_000.0;
2272        profile_stage("disjoint_fast_path_ms", stage_ms);
2273        diagnostics.measure_max("timing.total_ms", stage_ms);
2274        return Ok((KernelOutcome { value, diagnostics }, Some(HashSet::default())));
2275    }
2276    let heal_started = Instant::now();
2277    let (first_owned, second_owned) = prepared_operands(first, second, &policy)?;
2278    let heal_ms = heal_started.elapsed().as_secs_f64() * 1_000.0;
2279    let first = &first_owned;
2280    let second = &second_owned;
2281
2282    let mut diagnostics = KernelDiagnostics::default();
2283    // Operand preparation ran before `timing.total_ms` opens and was absent
2284    // from every phase split; it is 1.6 of a disjoint box union's 7.2 ms.
2285    diagnostics.measure_max("timing.heal_ms", heal_ms);
2286    // Every pcurve fit made while this scope is open — the imprint's sections,
2287    // the assembler's rebuilt trims, the merge's re-fits — is tallied, so the
2288    // outcome can say whether every curved trim reached its floor instead of
2289    // letting a sample-ceiling exit pass for a met one (`pcurve.unmet_floor`).
2290    let fit_scope = crate::PcurveFitScope::open();
2291    // And every POLYLINE fit: a marched section's interpolant is measured
2292    // against the tolerance it was given, so `fit.polyline.unmet_tolerance`
2293    // reports a section that could not reach it (`geometry/fit.rs`).
2294    let polyline_scope = crate::PolylineFitScope::open();
2295    let operation_started = Instant::now();
2296    diagnostics.count_n(
2297        "collect.faces",
2298        first
2299            .shells
2300            .iter()
2301            .chain(&second.shells)
2302            .map(|shell| shell.faces.len() as u64)
2303            .sum(),
2304    );
2305    diagnostics.measure_max("tolerance.model", policy.model);
2306    diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
2307
2308    let imprint_options = boolean_imprint_options(options, tolerance);
2309    let stage_started = Instant::now();
2310    let imprint = build_imprints(first, second, &imprint_options)?;
2311    tangent_nodes.extend_from_slice(&imprint.tangent_nodes);
2312    if std::env::var("BREP_DEBUG_BOOL").is_ok() {
2313        for piece in &imprint.pieces {
2314            let start = piece.curve.evaluate(piece.t0);
2315            let end = piece.curve.evaluate(piece.t1);
2316            eprintln!(
2317                "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
2318                piece.id, piece.support_faces, piece.t0, piece.t1, start, end
2319            );
2320        }
2321        for entry in &imprint.by_face {
2322            eprintln!(
2323                "by_face operand={} face={} pieces={:?}",
2324                entry.operand, entry.face_id, entry.piece_ids
2325            );
2326        }
2327        for split in &imprint.edge_splits {
2328            eprintln!(
2329                "edge_split operand={} edge={} params={:?}",
2330                split.operand, split.edge_id, split.parameters
2331            );
2332        }
2333    }
2334    let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2335    profile_stage("imprint_ms", stage_ms);
2336    diagnostics.measure_max("timing.imprint_ms", stage_ms);
2337    diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
2338    diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
2339    diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
2340    let (march_swap_rescues, clip_swap_rescues) = crate::imprint::take_rescue_firings();
2341    diagnostics.count_n("intersect.march_swap_rescue", march_swap_rescues);
2342    diagnostics.count_n("intersect.clip_swap_rescue", clip_swap_rescues);
2343    let touch = TouchCensus::build([first, second], &imprint);
2344    touch.report(&mut diagnostics);
2345    let stage_started = Instant::now();
2346    let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
2347    let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
2348    let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2349    profile_stage("edge_split_ms", stage_ms);
2350    diagnostics.measure_max("timing.edge_split_ms", stage_ms);
2351    let stage_started = Instant::now();
2352    crate::classification::classify_profile_begin();
2353    let fragments_a = crate::fragment::fragment_solid_against(&split_first, 0, &imprint, crate::fragment::bounded_carriers(second).then_some((bounds[1], policy.sew_search)))?;
2354    let fragments_b = crate::fragment::fragment_solid_against(&split_second, 1, &imprint, crate::fragment::bounded_carriers(first).then_some((bounds[0], policy.sew_search)))?;
2355    crate::classification::classify_profile_report("fragment");
2356    let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2357    profile_stage("fragment_ms", stage_ms);
2358    diagnostics.measure_max("timing.fragment_ms", stage_ms);
2359    diagnostics.count_n(
2360        "fragment.candidates",
2361        (fragments_a.len() + fragments_b.len()) as u64,
2362    );
2363    touch.report_fragments(&[&fragments_a, &fragments_b], &mut diagnostics);
2364    // Operand surface samples for the LEGITIMATE-EMPTY adjudication below:
2365    // every fragment test point is a 3D point ON its operand's boundary and
2366    // inside its face trim, so they witness where the operands' materials sit
2367    // relative to each other without extra geometry work.
2368    let a_surface_samples: Vec<Vec3> = fragments_a
2369        .iter()
2370        .flat_map(|fragment| {
2371            std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
2372        })
2373        .collect();
2374    let b_surface_samples: Vec<Vec3> = fragments_b
2375        .iter()
2376        .flat_map(|fragment| {
2377            std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
2378        })
2379        .collect();
2380    let stage_started = Instant::now();
2381    let mut barrier_edges: HashSet<(u8, u64)> = imprint
2382        .pieces
2383        .iter()
2384        .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
2385        .collect();
2386    barrier_edges.extend(imprint.barrier_edges.iter().copied());
2387    // The barrier set is keyed on ORIGINAL edge ids, but a barrier edge the
2388    // imprint also SPLIT (rotated equator-tangency: the overlapped cap ring
2389    // gains a junction at the sphere-seam crossing) reaches fragment
2390    // selection as its minted sub-edge ids. Remap the barrier through the
2391    // split ledger — keeping the originals too for unsplit references.
2392    for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
2393        let minted: Vec<(u8, u64)> = barrier_edges
2394            .iter()
2395            .filter(|(barrier_operand, _)| *barrier_operand == operand)
2396            .filter_map(|(_, edge_id)| split_map.get(edge_id))
2397            .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
2398            .collect();
2399        barrier_edges.extend(minted);
2400    }
2401    crate::classification::classify_profile_begin();
2402    let coincident = coincident_pairs(
2403        &fragments_a,
2404        &fragments_b,
2405        first,
2406        second,
2407        &imprint.cosurface_pairs,
2408        tolerance,
2409    )?;
2410    let sector_mode = sector::sector_mode(options);
2411    let mut select_census = sector::SelectCensus::default();
2412    let mut selected = select_fragments(
2413        fragments_a,
2414        fragments_b,
2415        first,
2416        second,
2417        operation,
2418        tolerance,
2419        &barrier_edges,
2420        &imprint,
2421        &coincident,
2422        sector_mode,
2423        &mut select_census,
2424    )?;
2425    let resewn = resew_coincident_rims(
2426        &mut selected,
2427        [&split_first, &split_second],
2428        [first, second],
2429        &imprint.cosurface_pairs,
2430        operation,
2431        tolerance,
2432    )?;
2433    diagnostics.count_n("select.coincident_pairs", coincident.len() as u64);
2434    diagnostics.count_n("select.rims_resewn", resewn as u64);
2435    crate::classification::classify_profile_report("select");
2436    let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2437    profile_stage("select_ms", stage_ms);
2438    diagnostics.measure_max("timing.select_ms", stage_ms);
2439    diagnostics.count_n("select.fragments", selected.len() as u64);
2440    // One census line per selection as well.
2441    diagnostics.count_n("select.sector_eligible", select_census.eligible);
2442    diagnostics.count_n("select.sector_primary", select_census.sector_primary);
2443    diagnostics.count_n("select.sector_fallback", select_census.sector_fallback);
2444    diagnostics.count_n("select.sector_disagree", select_census.disagree);
2445    diagnostics.count_n("select.ray_casts", select_census.ray_casts);
2446    diagnostics.count_n("select.on_votes", select_census.on_votes);
2447    diagnostics.count_n("select.overturns", select_census.overturns);
2448    if sector_mode != sector::SectorMode::Off || std::env::var_os("BREP_SECTOR_CENSUS").is_some() {
2449        sector::sector_census_line(|| {
2450            format!(
2451                "selection mode={sector_mode:?} select_ms={stage_ms:.3} selected={} eligible={} primary={} fallback={} disagree={} ray_casts={} on_votes={} overturns={}",
2452                selected.len(),
2453                select_census.eligible,
2454                select_census.sector_primary,
2455                select_census.sector_fallback,
2456                select_census.disagree,
2457                select_census.ray_casts,
2458                select_census.on_votes,
2459                select_census.overturns
2460            )
2461        });
2462    }
2463    // LEGITIMATE-EMPTY RESULTS: an intersect of DISJOINT operands (or a
2464    // subtract whose left operand is entirely CONSUMED by the right) selects
2465    // zero fragments, and assembly used to refuse with "operation produced no
2466    // boundary faces" — 384 of the 474 post-wrapped-band pool errors were
2467    // this, not bugs. Empty is only blessed on TWO independent proofs:
2468    //   (a) the imprint recorded NO section evidence (no accepted pierce
2469    //       seed, no traced SSI branch, no minted piece) — a silently-lost
2470    //       section (the trial-489 class) always leaves upstream evidence
2471    //       even when every downstream sub-segment is clipped away, while
2472    //       pure material disjointness leaves none (verified: 489's
2473    //       recreated silent-miss state keeps erroring; per-face surface
2474    //       samples ALONE missed its 235 mm³ pocket, which is why (a) is
2475    //       required and sampling alone was rejected);
2476    //   (b) the operands' surface samples agree (every fragment test point
2477    //       classified against the other solid):
2478    //       intersect — no sample of either operand strictly inside the
2479    //       other; subtract — no left-operand sample strictly outside the
2480    //       right.
2481    // Contact/graze pairs (evidence exists, material still disjoint) stay
2482    // errors — conservative by design. Escape hatch: BREP_EMPTY_BOOLEAN=0
2483    // restores the unconditional error.
2484    if selected.is_empty()
2485        && !imprint.section_evidence
2486        && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
2487    {
2488        let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
2489            for &sample in samples {
2490                if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::In {
2491                    return Ok(true);
2492                }
2493            }
2494            Ok(false)
2495        };
2496        let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
2497            for &sample in samples {
2498                if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::Out {
2499                    return Ok(true);
2500                }
2501            }
2502            Ok(false)
2503        };
2504        let legitimate = match operation {
2505            BooleanOperation::Intersect => {
2506                !strictly_inside(&a_surface_samples, second)?
2507                    && !strictly_inside(&b_surface_samples, first)?
2508            }
2509            BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
2510            BooleanOperation::Union => false,
2511        };
2512        if legitimate {
2513            diagnostics.event(
2514                DiagnosticSeverity::Info,
2515                KernelStage::Select,
2516                "boolean.empty_result",
2517                format!("{operation:?} of witnessed-non-overlapping operands is empty"),
2518            );
2519            fit_scope.close().report_into(&mut diagnostics);
2520            polyline_scope.close().report_into(&mut diagnostics);
2521            diagnostics.measure_max(
2522                "timing.total_ms",
2523                operation_started.elapsed().as_secs_f64() * 1_000.0,
2524            );
2525            return Ok((
2526                KernelOutcome {
2527                    value: empty_solid(),
2528                    diagnostics,
2529                },
2530                None,
2531            ));
2532        }
2533    }
2534    let solids = [(0, &split_first), (1, &split_second)]
2535        .into_iter()
2536        .collect::<HashMap<_, _>>();
2537    let dump = BooleanStageDump::open();
2538    if let Some(dump) = &dump {
2539        dump.write("first", &split_first);
2540        dump.write("second", &split_second);
2541        dump.write("selected", &selected);
2542    }
2543    let stage_started = Instant::now();
2544    // Result face `k` is minted from `selected[k - 1]` (one face per fragment,
2545    // ids from 1 in order): the map the tangent-section attribution reads a
2546    // refusal's face ids through.
2547    let assembled_origins: Vec<FaceKey> = if imprint.tangent_sections.is_empty() {
2548        Vec::new()
2549    } else {
2550        selected
2551            .iter()
2552            .map(|fragment| FaceKey { operand: fragment.operand, face_id: fragment.source_face_id })
2553            .collect()
2554    };
2555    // A record the grazing-pierce path minted (`TangentSectionRecord::
2556    // grazing` non-empty, whether or not the lane then found a contact to
2557    // name) yields to a tangent NODE the imprint classified: the node is the
2558    // nearer cause and `attribute_to_tangent_node` names it on the way out
2559    // (the G1 elbow over its own-radius arm: the arm's cap circle also
2560    // pierces the elbow under the seed gate, and the lane sees the join).
2561    let attributable: Vec<crate::imprint::TangentSectionRecord> = imprint
2562        .tangent_sections
2563        .iter()
2564        .filter(|section| imprint.tangent_nodes.is_empty() || section.grazing.is_empty())
2565        .cloned()
2566        .collect();
2567    let attribute = |error: KernelRefusal| {
2568        attribute_to_tangent_section(error, &attributable, &assembled_origins)
2569    };
2570    let cellular_result = matches!(operation, BooleanOperation::Union)
2571        && cellular::enabled(options)
2572        && cellular::complete_contact(first, second, &selected, tolerance);
2573    let (solid, scope) = if cellular_result {
2574        (assemble::assemble_cellular_fragments(selected, &solids, &imprint, tolerance)
2575            .map_err(attribute)?, None)
2576    } else if untouched_passthrough(options) {
2577        assemble_fragments_scoped(selected, &solids, &imprint, tolerance, &touch, touched_assembly(options))
2578            .map_err(attribute)?
2579    } else {
2580        (assemble_fragments(selected, &solids, &imprint, tolerance).map_err(attribute)?, None)
2581    };
2582    if let Some(scope) = &scope {
2583        diagnostics.count_n("passthrough.faces_examined", scope.examined.len() as u64);
2584    }
2585    if let Some(dump) = &dump {
2586        dump.write("assembled", &solid);
2587    }
2588    let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2589    profile_stage("assemble_ms", stage_ms);
2590    diagnostics.measure_max("timing.assemble_ms", stage_ms);
2591    let stage_started = Instant::now();
2592    if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
2593        let issues = solid.validate();
2594        eprintln!(
2595            "pre-merge validation: {} issue(s){}",
2596            issues.len(),
2597            issues
2598                .first()
2599                .map(|issue| format!(" — first: {}", issue.message))
2600                .unwrap_or_default()
2601        );
2602    }
2603    let solid = if solid.kind != crate::BodyKind::Cellular && options.merge_coplanar_faces
2604        && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
2605    {
2606        let merged = match &scope {
2607            // Every pair merges as before (an operand's own coplanar
2608            // neighbours included); the closing validation reads the
2609            // examined faces and the merged ones.
2610            Some(scope) => merge_same_surface_faces_scoped(
2611                &solid,
2612                tolerance,
2613                &options.keep_unmerged_name_substrs,
2614                &scope.examined,
2615            )?,
2616            None => merge_same_surface_faces_excluding(
2617                &solid,
2618                tolerance,
2619                &options.keep_unmerged_name_substrs,
2620            )?,
2621        };
2622        // Face merging can make previously separate collinear boundary
2623        // segments incident to the same pair of faces. Run continuation
2624        // cleanup again, matching the post-merge normalization performed by
2625        // the former assembly pipeline.
2626        merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
2627    } else {
2628        solid
2629    };
2630    if let Some(dump) = &dump {
2631        dump.write("merged", &solid);
2632    }
2633    let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2634    profile_stage("face_merge_ms", stage_ms);
2635    diagnostics.measure_max("timing.face_merge_ms", stage_ms);
2636    let validation_started = Instant::now();
2637    let validation = solid.validate_detailed_scoped(&policy, scope.as_ref().map(|scope| &scope.examined));
2638    profile_stage("final_validate_ms", validation_started.elapsed().as_secs_f64() * 1000.0);
2639    diagnostics.count_n("validate.issues", validation.issues.len() as u64);
2640    diagnostics.count_n(
2641        "validate.wire_warnings",
2642        validation.wire_warnings.len() as u64,
2643    );
2644    diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
2645    for warning in &validation.wire_warnings {
2646        diagnostics.event(
2647            DiagnosticSeverity::Warning,
2648            KernelStage::Validate,
2649            "validate.uv_wire",
2650            warning.message.clone(),
2651        );
2652    }
2653    for issue in &validation.issues {
2654        diagnostics.event(
2655            DiagnosticSeverity::Error,
2656            KernelStage::Validate,
2657            "validate.brep",
2658            issue.message.clone(),
2659        );
2660    }
2661    fit_scope.close().report_into(&mut diagnostics);
2662    polyline_scope.close().report_into(&mut diagnostics);
2663    diagnostics.measure_max(
2664        "timing.total_ms",
2665        operation_started.elapsed().as_secs_f64() * 1_000.0,
2666    );
2667    if !validation.issues.is_empty() {
2668        return Err(KernelRefusal::new(
2669            RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
2670            KernelStage::Validate,
2671            format!(
2672            "boolean_operation: invalid result: {:?}",
2673            validation.issues
2674        )));
2675    }
2676    // Optional semantic oracle (off by default, no perf hit): under
2677    // BREP_DEBUG_BOOL, cross-check the well-formed result against the CSG
2678    // point-membership expectation and scan for residual coincident-but-unwelded
2679    // geometry. Purely diagnostic and NON-rejecting — a statistical check must
2680    // never fail a valid op (see oracle.rs) — so it only ever warns.
2681    if std::env::var("BREP_DEBUG_BOOL").is_ok() {
2682        if let Ok(report) =
2683            crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
2684        {
2685            if report.is_flagged() {
2686                eprintln!(
2687                    "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
2688                    report.disagreement_rate,
2689                    report.disagreements.len(),
2690                    report.considered,
2691                    report.sample_disagreement()
2692                );
2693            }
2694        }
2695        let fusable_band = crate::tolerance::assembler_weld(policy.model);
2696        let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
2697        if !fusables.is_empty() {
2698            eprintln!(
2699                "[oracle] {} residual fusable(s) after weld; sample {:?}",
2700                fusables.len(),
2701                fusables.first()
2702            );
2703        }
2704    }
2705    let candidates = scope.as_ref().map(|scope| {
2706        let candidates = soundness_candidates(
2707            &solid,
2708            scope,
2709            [operand_bounds(first), operand_bounds(second)],
2710            policy.sew_search,
2711        );
2712        diagnostics.count_n("passthrough.soundness_candidates", candidates.len() as u64);
2713        crate::soundness::census::profile_operation(&solid, &[first, second], Some(&scope.touched), "boolean");
2714        candidates
2715    });
2716    Ok((
2717        KernelOutcome {
2718            value: solid,
2719            diagnostics,
2720        },
2721        candidates,
2722    ))
2723}
2724