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 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 #[serde(default)]
53 pub tolerances: Option<KernelTolerances>,
54 #[serde(default)]
55 pub imprint: ImprintOptions,
56 #[serde(default = "default_true")]
59 pub merge_coplanar_faces: bool,
60 #[serde(default)]
70 pub keep_unmerged_name_substrs: Vec<String>,
71 #[serde(default = "default_true")]
75 pub untouched_passthrough: bool,
76 #[serde(default = "default_true")]
80 pub disjoint_fast_path: bool,
81 #[serde(default = "default_true")]
91 pub contact_fast_path: bool,
92 #[serde(default)]
100 pub touched_assembly: bool,
101 #[serde(default = "default_true")]
109 pub sector_classification: bool,
110 #[serde(default = "default_true")]
114 pub sheet_trim: bool,
115 #[serde(default)]
119 pub keep_cells: bool,
120 #[serde(default)]
125 pub seam_reseat_later_references: Option<Vec<String>>,
126}
127
128fn 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
137fn 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
146fn 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
155fn 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, refresh_derived_genus,
210};
211use assemble::{assemble_fragments_scoped, AssemblyScope};
212#[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) => {
224 if below_tolerance_refusal_enabled()
227 && outcome.value.shells.iter().all(|shell| shell.faces.is_empty())
228 && !matches!(operation, BooleanOperation::Union)
229 {
230 if let Some(refusal) = below_tolerance_refusal(first, second, None) {
231 return Err(refusal);
232 }
233 }
234 Ok(outcome.value)
235 }
236 Err(error) => {
237 if below_tolerance_refusal_enabled() {
238 if let Some(refusal) = below_tolerance_refusal(first, second, Some(&error)) {
239 return Err(refusal);
240 }
241 }
242 if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
250 || !error.class.perturbation_eligible()
251 {
252 return Err(error);
253 }
254 if std::env::var("BREP_OPEN_OPERAND_GATE").as_deref() != Ok("0") {
276 if let Some((operand, open)) = [first, second]
277 .iter()
278 .enumerate()
279 .find_map(|(operand, solid)| {
280 let open = one_use_edges(solid);
281 (open > 0).then_some((operand, open))
282 })
283 {
284 return Err(error.with_message(|message| format!(
285 "{message}; operand {operand} is not a closed shell: {open} of its edges bound \
286 only one face, so no boolean of it encloses a solid and no perturbation \
287 can close it"
288 )));
289 }
290 }
291 if let Some((operand, face_id, deviation, weld)) =
292 planar_operand_off_its_plane(first, second, options)
293 {
294 return Err(error.with_message(|message| format!(
295 "{message}; operand {operand} face {face_id} is a plane whose straight \
296 boundary sits {deviation:.3e} off it (assembly weld {weld:.1e}), which \
297 no perturbation can reconcile — the body's planes must contain their \
298 boundary before it can take part in an exact boolean"
299 )));
300 }
301 if matches!(operation, BooleanOperation::Subtract)
316 && std::env::var("BREP_PINCH_GATE").as_deref() != Ok("0")
317 && subtract_pinches_at_internal_tangency(first, second, options.tolerance)
318 .unwrap_or(false)
319 {
320 return Err(error.with_message(|message| format!(
321 "{message}; internal tangency: the operands touch tangentially with \
322 co-directed normals, so the exact difference pinches to zero thickness \
323 (non-manifold, unrepresentable in a boundary model) — refusing rather \
324 than returning a perturbed sliver"
325 )));
326 }
327 if matches!(error.class, RefusalClass::TangentNodeSingularity)
343 && std::env::var("BREP_TANGENT_NODE_GATE").as_deref() != Ok("0")
344 {
345 return Err(error);
346 }
347 match perturbation_retry(first, second, operation, options) {
348 Ok(solid) => Ok(solid),
349 Err(refused) if refused.is_empty() => Err(error),
350 Err(refused) => {
351 Err(error.with_message(|message| format!("{message}; {}", refused.join("; "))))
352 }
353 }
354 }
355 }
356}
357
358const PINCH_PROBE_STEPS: usize = 8;
363
364const PINCH_ANGULAR_TOLERANCE: f64 = 1e-4;
367
368const PINCH_SEPARATION_STEP: f64 = 1e-2;
372
373fn subtract_pinches_at_internal_tangency(
395 first: &BrepSolid,
396 second: &BrepSolid,
397 tolerance: f64,
398) -> Result<bool, KernelRefusal> {
399 let contact = (tolerance * 100.0).max(1e-12);
402 let probes_a = first
403 .shells
404 .iter()
405 .flat_map(|shell| &shell.faces)
406 .map(PinchProbe::of)
407 .collect::<Result<Vec<_>, _>>()?;
408 let probes_b = second
409 .shells
410 .iter()
411 .flat_map(|shell| &shell.faces)
412 .map(PinchProbe::of)
413 .collect::<Result<Vec<_>, _>>()?;
414 for probe_a in &probes_a {
415 for probe_b in &probes_b {
416 let pad = 1e-2 * probe_a.extent().max(probe_b.extent());
423 if probe_a.separation(probe_b) > contact + pad {
424 continue;
425 }
426 if classify_surface_pair(
433 &probe_a.face.surface,
434 &probe_b.face.surface,
435 tolerance,
436 PINCH_ANGULAR_TOLERANCE,
437 ).or_refuse(KernelStage::Validate, "csg.boolean.mod")?
438 .relation
439 == SurfacePairRelation::Cosurface
440 {
441 continue;
442 }
443 if faces_touch_with_codirected_normals(probe_a, probe_b, contact)? {
444 return Ok(true);
445 }
446 }
447 }
448 Ok(false)
449}
450
451struct PinchProbe<'a> {
454 face: &'a FaceRecord,
455 samples: Vec<(f64, f64, Vec3)>,
457 minimum: [f64; 3],
458 maximum: [f64; 3],
459}
460
461impl<'a> PinchProbe<'a> {
462 fn of(face: &'a FaceRecord) -> Result<Self, KernelRefusal> {
463 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
464 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
465 let steps = PINCH_PROBE_STEPS as f64;
466 let mut samples = Vec::with_capacity((PINCH_PROBE_STEPS + 1).pow(2));
467 let mut minimum = [f64::INFINITY; 3];
468 let mut maximum = [f64::NEG_INFINITY; 3];
469 for i in 0..=PINCH_PROBE_STEPS {
470 let u = u0 + (u1 - u0) * i as f64 / steps;
471 for j in 0..=PINCH_PROBE_STEPS {
472 let v = v0 + (v1 - v0) * j as f64 / steps;
473 let Ok(point) = face.surface.evaluate(u, v) else {
474 continue;
475 };
476 for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
477 minimum[axis] = minimum[axis].min(value);
478 maximum[axis] = maximum[axis].max(value);
479 }
480 samples.push((u, v, point));
481 }
482 }
483 Ok(Self {
484 face,
485 samples,
486 minimum,
487 maximum,
488 })
489 }
490
491 fn extent(&self) -> f64 {
493 (0..3)
494 .map(|axis| self.maximum[axis] - self.minimum[axis])
495 .fold(0.0f64, f64::max)
496 }
497
498 fn separation(&self, other: &Self) -> f64 {
500 let mut gap: f64 = 0.0;
501 for axis in 0..3 {
502 gap = gap.max(self.minimum[axis] - other.maximum[axis]);
503 gap = gap.max(other.minimum[axis] - self.maximum[axis]);
504 }
505 gap
506 }
507}
508
509fn faces_touch_with_codirected_normals(
514 probe_a: &PinchProbe<'_>,
515 probe_b: &PinchProbe<'_>,
516 contact: f64,
517) -> Result<bool, KernelRefusal> {
518 for (probe, other) in [(probe_a, probe_b), (probe_b, probe_a)] {
519 let source = probe.face;
520 let target = other.face;
521 for &(u, v, point) in &probe.samples {
522 let projection = project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?;
523 if projection.distance > contact {
524 continue;
525 }
526 let (Ok(source_normal), Ok(target_normal)) = (
529 source.surface.normal(u, v),
530 target.surface.normal(projection.u, projection.v),
531 ) else {
532 continue;
533 };
534 let source_outward = outward_normal(source_normal, source.same_sense);
535 let target_outward = outward_normal(target_normal, target.same_sense);
536 if source_outward.cross(target_outward).length() > PINCH_ANGULAR_TOLERANCE
537 || source_outward.dot(target_outward) <= 0.0
538 {
539 continue;
540 }
541 if parameter_point_in_face(source, Vec2 { x: u, y: v }, 1e-6).or_refuse(KernelStage::Validate, "parameter_point_in_face")?
542 == PolygonClass::Outside
543 || parameter_point_in_face(
544 target,
545 Vec2 {
546 x: projection.u,
547 y: projection.v,
548 },
549 1e-6,
550 ).or_refuse(KernelStage::Validate, "csg.boolean.mod")? == PolygonClass::Outside
551 {
552 continue;
553 }
554 if contact_separates_locally(source, target, u, v, contact)? {
555 return Ok(true);
556 }
557 }
558 }
559 Ok(false)
560}
561
562fn outward_normal(normal: Vec3, same_sense: bool) -> Vec3 {
563 if same_sense {
564 normal
565 } else {
566 normal.scale(-1.0)
567 }
568}
569
570fn contact_separates_locally(
576 source: &FaceRecord,
577 target: &FaceRecord,
578 u: f64,
579 v: f64,
580 contact: f64,
581) -> Result<bool, KernelRefusal> {
582 let [u0, u1] = source.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
583 let [v0, v1] = source.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
584 let step_u = (u1 - u0) * PINCH_SEPARATION_STEP;
585 let step_v = (v1 - v0) * PINCH_SEPARATION_STEP;
586 for (probe_u, probe_v) in [
587 ((u + step_u).min(u1), v),
588 ((u - step_u).max(u0), v),
589 (u, (v + step_v).min(v1)),
590 (u, (v - step_v).max(v0)),
591 ] {
592 if (probe_u - u).abs() < f64::EPSILON && (probe_v - v).abs() < f64::EPSILON {
593 continue;
594 }
595 let Ok(point) = source.surface.evaluate(probe_u, probe_v) else {
596 continue;
597 };
598 if project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?.distance > contact * 10.0 {
601 return Ok(true);
602 }
603 }
604 Ok(false)
605}
606
607fn perturbation_retry(
661 first: &BrepSolid,
662 second: &BrepSolid,
663 operation: BooleanOperation,
664 options: &BooleanOptions,
665) -> Result<BrepSolid, Vec<String>> {
666 const DIRECTIONS: [[f64; 3]; 4] = [
677 [0.4034, 0.7973, 0.4491],
678 [0.7973, 0.4491, 0.4034],
679 [0.4491, 0.4034, 0.7973],
680 [0.5774, -0.5774, 0.5774],
681 ];
682 const FRACTIONS: [f64; 5] = PERTURBATION_FRACTIONS;
688
689 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
690 let scale = crate::tolerance::solid_scale(first)
691 .max(crate::tolerance::solid_scale(second))
692 .max(1.0);
693
694 let _caller = crate::mass_caller("boolean.perturbation_rescue");
698 let va = solid_signed_volume(first).ok().map(f64::abs);
699 let vb = solid_signed_volume(second).ok().map(f64::abs);
700 let hold_identity = std::env::var("BREP_PERTURB_IDENTITY").as_deref() != Ok("0");
701 let hold_mirror = hold_identity && std::env::var("BREP_PERTURB_MIRROR").as_deref() != Ok("0");
704 let early_exit = std::env::var("BREP_MIRROR_EARLY_EXIT").as_deref() != Ok("0");
707 let mut trends = [MirrorTrend::default(); 4];
708 let mut identity: Option<Option<InclusionExclusion>> = None;
710 let mut refused = Vec::new();
711 let ladder = std::env::var("BREP_DEBUG_PERTURB_LADDER").is_ok();
715 let ladder_start = web_time::Instant::now();
716 if ladder {
717 let measured = inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
718 match &measured {
719 Some(identity) => eprintln!(
720 "[ladder] identity {} = {:.9} from the exact lane's {} ({:.9}); V(first) {:?} V(second) {:?}",
721 identity.formula, identity.expected, identity.reading, identity.read, va, vb
722 ),
723 None => eprintln!("[ladder] no identity: no exact-lane boolean of these operands clears in either order"),
724 }
725 identity = Some(measured);
726 }
727 let rung_miss = |identity: &Option<Option<InclusionExclusion>>, candidate: &BrepSolid| -> String {
728 let volume = body_volume(candidate);
729 match (identity, volume) {
730 (Some(Some(identity)), Some(volume)) => format!(
731 "volume {volume:.9} miss {:+.3e} bar {:.3e}",
732 volume - identity.expected,
733 crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * identity.expected.abs())
734 ),
735 (_, Some(volume)) => format!("volume {volume:.9}"),
736 _ => "no volume".to_string(),
737 }
738 };
739
740 for &fraction in &FRACTIONS {
741 let magnitude = scale * fraction;
742 for (direction, dir) in DIRECTIONS.iter().enumerate() {
743 if trends[direction].closed {
744 if ladder {
745 eprintln!("[ladder] dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}: not tried, its direction is closed");
746 }
747 continue;
748 }
749 let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
750 let translate = match AffineTransform::new([
751 1.0, 0.0, 0.0, offset[0], 0.0, 1.0, 0.0, offset[1], 0.0, 0.0, 1.0, offset[2], 0.0, 0.0, 0.0, 1.0,
755 ]) {
756 Ok(transform) => transform,
757 Err(_) => continue,
758 };
759 let moved = match crate::transform_brep(second, translate, false) {
760 Ok(solid) => solid,
761 Err(_) => continue,
762 };
763 let rung = format!("dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}");
764 let rung_start = web_time::Instant::now();
765 let candidate =
766 match boolean_operation_with_diagnostics(first, &moved, operation, options) {
767 Ok(outcome) => outcome.value,
768 Err(error) => {
769 if ladder {
770 eprintln!("[ladder] {rung}: exact lane refused {:?}", error.class);
771 }
772 continue;
773 }
774 };
775 let built = rung_start.elapsed();
776 if candidate.shells.is_empty() {
778 if ladder {
779 eprintln!("[ladder] {rung}: empty");
780 }
781 continue;
782 }
783 if let (Some(va), Some(vb)) = (va, vb) {
788 if let Ok(vr) = solid_signed_volume(&candidate) {
789 if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
790 if ladder {
791 eprintln!("[ladder] {rung}: outside the CSG bound, {}", rung_miss(&identity, &candidate));
792 }
793 continue;
794 }
795 }
796 }
797 let bounded = rung_start.elapsed();
802 if hold_identity {
804 let identity = identity.get_or_insert_with(|| {
805 let identity =
806 inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
807 if debug && identity.is_none() {
808 eprintln!(
809 "[perturb] no exact-lane boolean of these operands clears in either \
810 order; no identity to hold"
811 );
812 }
813 identity
814 });
815 if let Some(identity) = identity {
816 let expected = identity.expected;
817 let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * expected.abs());
818 let volume = body_volume(&candidate);
819 let miss = volume.map(|volume| (volume - expected).abs());
820 if !miss.is_some_and(|miss| miss <= bar) {
821 let sentence = format!(
822 "perturbation rung dir={dir:?} fraction={fraction:.1e} \
823 (offset {magnitude:.3e}) refused: its body reads {} against the \
824 inclusion–exclusion identity {} = {expected:.9} (the exact lane's \
825 {} reads {:.9}), a miss of {} beyond the volume drift bar {bar:.3e}",
826 volume.map_or("no volume".to_string(), |volume| format!("{volume:.9}")),
827 identity.formula,
828 identity.reading,
829 identity.read,
830 miss.map_or("unmeasured".to_string(), |miss| format!("{miss:.3e}")),
831 );
832 if debug {
833 eprintln!("[perturb] {sentence}");
834 }
835 if ladder {
836 eprintln!(
837 "[ladder] {rung}: refused by the identity, volume {} miss {} bar {bar:.3e}",
838 volume.map_or("none".to_string(), |volume| format!("{volume:.9}")),
839 volume.map_or("unmeasured".to_string(), |volume| format!("{:+.3e}", volume - expected)),
840 );
841 }
842 refused.push(sentence);
843 continue;
844 }
845 } else if hold_mirror {
846 let mirror_start = web_time::Instant::now();
849 let mirrored = AffineTransform::new([
850 1.0, 0.0, 0.0, -offset[0], 0.0, 1.0, 0.0, -offset[1], 0.0, 0.0, 1.0, -offset[2], 0.0, 0.0, 0.0, 1.0,
854 ])
855 .ok()
856 .and_then(|transform| crate::transform_brep(second, transform, false).ok());
857 let mirror = mirrored.as_ref().and_then(|mirrored| {
858 inclusion_exclusion_volume(first, mirrored, operation, options, va, vb, true)
859 });
860 let mirror_time = mirror_start.elapsed();
861 let volume = body_volume(&candidate);
862 let mut measured = None;
863 let verdict = match (&mirror, volume) {
864 (Some(mirror), Some(volume)) => {
865 let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * mirror.expected.abs());
866 let miss = 0.5 * (volume - mirror.expected).abs();
867 measured = Some((0.5 * (volume - mirror.expected), bar));
868 if ladder {
869 eprintln!(
870 "[ladder] {rung}: mirror at −offset reads {} = {:.9} (the mirrored {} {:.9}); body {volume:.9}, first-order translation error {:+.3e} bar {bar:.3e}",
871 mirror.formula, mirror.expected, mirror.reading, mirror.read, 0.5 * (volume - mirror.expected)
872 );
873 }
874 (miss > bar).then(|| format!(
875 "perturbation rung dir={dir:?} fraction={fraction:.1e} \
876 (offset {magnitude:.3e}) refused: no exact-lane boolean of the \
877 operands clears, so the rung is held to its mirror: its body reads \
878 {volume:.9} with the second operand moved +offset and {} = {:.9} \
879 with it moved −offset (the mirrored {} reads {:.9}), a first-order \
880 translation error of {miss:.3e} beyond the volume drift bar {bar:.3e}",
881 mirror.formula, mirror.expected, mirror.reading, mirror.read
882 ))
883 }
884 _ => Some(format!(
885 "perturbation rung dir={dir:?} fraction={fraction:.1e} \
886 (offset {magnitude:.3e}) refused: no exact-lane boolean of the operands \
887 clears, and none clears with the second operand moved −offset either, \
888 so nothing checks the rung's body{}",
889 volume.map_or(String::new(), |volume| format!(" ({volume:.9})"))
890 )),
891 };
892 match verdict {
893 None => trends[direction].cleared(),
894 Some(sentence) => {
895 if debug {
896 eprintln!("[perturb] {sentence}");
897 }
898 if ladder {
899 eprintln!(
900 "[ladder] {rung}: refused by its mirror (rung {:.0} ms: built {:.0}, bound {:.0}, mirror {:.0} ms; ladder {:.1} s)",
901 rung_start.elapsed().as_secs_f64() * 1e3,
902 built.as_secs_f64() * 1e3,
903 (bounded - built).as_secs_f64() * 1e3,
904 mirror_time.as_secs_f64() * 1e3,
905 ladder_start.elapsed().as_secs_f64()
906 );
907 }
908 refused.push(sentence);
909 let closure = measured.and_then(|(error, bar)| {
912 let earlier = trends[direction].refused(magnitude, error, early_exit)?;
913 Some((earlier, error, bar))
914 });
915 if let Some(((earlier_offset, earlier_error), error, bar)) = closure {
916 let closed = format!(
917 "perturbation direction dir={dir:?} closed after offset {magnitude:.3e}: its \
918 rungs' first-order translation errors read {earlier_error:+.3e} at offset \
919 {earlier_offset:.3e} and {error:+.3e} at {magnitude:.3e}, one sign and \
920 growing, so no larger offset along it comes back to the volume drift bar \
921 {bar:.3e}"
922 );
923 if debug {
924 eprintln!("[perturb] {closed}");
925 }
926 if ladder {
927 eprintln!("[ladder] {closed}");
928 }
929 refused.push(closed);
930 }
931 continue;
932 }
933 }
934 }
935 }
936 match crate::oracle::boolean_semantic_disagreement(
940 first, second, operation, &candidate, 400,
941 ) {
942 Ok(report) if report.considered >= 30 && !report.is_flagged() => {
943 if ladder {
944 eprintln!(
945 "[ladder] {rung}: accepted (oracle {} considered, rate {:.4}), {}",
946 report.considered, report.disagreement_rate, rung_miss(&identity, &candidate)
947 );
948 }
949 if debug {
950 eprintln!(
951 "[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
952 magnitude={magnitude:.3e} offset={offset:?} \
953 (oracle considered={} rate={:.4})",
954 report.considered, report.disagreement_rate
955 );
956 }
957 return Ok(candidate);
958 }
959 report => {
960 if ladder {
961 let verdict = match &report {
962 Ok(report) => format!("oracle {} considered, rate {:.4}", report.considered, report.disagreement_rate),
963 Err(error) => format!("oracle error {error:?}"),
964 };
965 eprintln!("[ladder] {rung}: refused by the oracle ({verdict}), {}", rung_miss(&identity, &candidate));
966 }
967 continue;
968 }
969 }
970 }
971 }
972 if debug {
973 eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
974 }
975 Err(refused)
976}
977
978const PERTURBATION_FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
981
982#[derive(Clone, Copy, Default)]
1006struct MirrorTrend {
1007 last: Option<(f64, f64)>,
1009 closed: bool,
1010}
1011
1012impl MirrorTrend {
1013 fn refused(&mut self, offset: f64, error: f64, close: bool) -> Option<(f64, f64)> {
1018 let earlier = self
1019 .last
1020 .filter(|&(_, earlier)| earlier.signum() == error.signum() && error.abs() >= earlier.abs())
1021 .filter(|_| close);
1022 self.last = Some((offset, error));
1023 self.closed |= earlier.is_some();
1024 earlier
1025 }
1026
1027 fn cleared(&mut self) {
1029 self.last = None;
1030 }
1031}
1032
1033pub(crate) fn below_tolerance_refusal_enabled() -> bool {
1048 crate::switch!("BREP_BELOW_TOLERANCE_REFUSAL", true)
1049}
1050
1051const BELOW_TOLERANCE_BAND: f64 = 10.0 * crate::tolerance::WELD_FLOOR;
1054
1055fn below_tolerance_refusal(first: &BrepSolid, second: &BrepSolid, error: Option<&KernelRefusal>) -> Option<KernelRefusal> {
1056 let (face_a, face_b, separation) = near_coincident_planar_faces(first, second)?;
1057 let message = format!(
1058 "boolean: face {face_a} of the first operand and face {face_b} of the second are planes nearly on one \
1059 another, {separation:.3e} apart at most over their shared extent: the slivers between them are below the \
1060 modelling tolerance ({BELOW_TOLERANCE_BAND:.0e}), so the kernel cannot resolve them{}",
1061 error.map(|error| format!(" (the exact arrangement said: {})", error.message.chars().take(160).collect::<String>())).unwrap_or_default()
1062 );
1063 Some(KernelRefusal::unsupported(KernelStage::Intersect, "below_modelling_tolerance", message))
1064}
1065
1066fn near_coincident_planar_faces(first: &BrepSolid, second: &BrepSolid) -> Option<(u64, u64, f64)> {
1070 struct Planar {
1071 id: u64,
1072 origin: Vec3,
1073 normal: Vec3,
1074 corners: Vec<Vec3>,
1075 bounds: crate::spatial::Aabb,
1076 }
1077 let read = |solid: &BrepSolid| -> Vec<Planar> {
1078 let at = |id: u64| solid.vertices.iter().find(|vertex| vertex.id == id).map(|vertex| vertex.point);
1079 solid
1080 .shells
1081 .iter()
1082 .flat_map(|shell| &shell.faces)
1083 .filter_map(|face| {
1084 if !face.surface.is_affine().ok()? {
1085 return None;
1086 }
1087 let origin = face.surface.control_points[0][0].point().ok()?;
1088 let a = face.surface.control_points[1][0].point().ok()?.sub(origin);
1089 let b = face.surface.control_points[0][1].point().ok()?.sub(origin);
1090 let normal = a.cross(b).normalized().ok()?;
1091 let corners: Vec<Vec3> = face
1092 .loops
1093 .iter()
1094 .flat_map(|lp| &lp.coedges)
1095 .filter_map(|coedge| solid.edges.iter().find(|edge| edge.id == coedge.edge_id))
1096 .flat_map(|edge| [at(edge.start_vertex_id), at(edge.end_vertex_id)])
1097 .flatten()
1098 .collect();
1099 if corners.is_empty() {
1100 return None;
1101 }
1102 let bounds = crate::spatial::Aabb::from_points(corners.clone());
1103 Some(Planar { id: face.id, origin, normal, corners, bounds })
1104 })
1105 .collect()
1106 };
1107 let (faces_a, faces_b) = (read(first), read(second));
1108 let scale = crate::tolerance::solid_scale(first).max(crate::tolerance::solid_scale(second)).max(1.0);
1109 let floor = 1e-9 * scale;
1110 for a in &faces_a {
1111 for b in &faces_b {
1112 if a.normal.cross(b.normal).length() > 1e-3 {
1113 continue;
1114 }
1115 if !a.bounds.intersects(b.bounds, BELOW_TOLERANCE_BAND) {
1116 continue;
1117 }
1118 let separation = a
1119 .corners
1120 .iter()
1121 .map(|corner| corner.sub(b.origin).dot(b.normal).abs())
1122 .chain(b.corners.iter().map(|corner| corner.sub(a.origin).dot(a.normal).abs()))
1123 .fold(0.0f64, f64::max);
1124 if separation > floor && separation <= BELOW_TOLERANCE_BAND {
1125 return Some((a.id, b.id, separation));
1126 }
1127 }
1128 }
1129 None
1130}
1131
1132fn one_use_edges(solid: &BrepSolid) -> usize {
1133 let mut uses: HashMap<u64, usize> = HashMap::default();
1134 for coedge in solid
1135 .shells
1136 .iter()
1137 .flat_map(|shell| &shell.faces)
1138 .flat_map(|face| &face.loops)
1139 .flat_map(|loop_record| &loop_record.coedges)
1140 {
1141 *uses.entry(coedge.edge_id).or_default() += 1;
1142 }
1143 solid
1144 .edges
1145 .iter()
1146 .filter(|edge| !edge.degenerate && uses.get(&edge.id) == Some(&1))
1147 .count()
1148}
1149
1150fn body_volume(solid: &BrepSolid) -> Option<f64> {
1152 if solid.shells.is_empty() {
1153 return Some(0.0);
1154 }
1155 solid_signed_volume(solid).ok().map(f64::abs)
1156}
1157
1158struct InclusionExclusion {
1161 formula: &'static str,
1164 reading: &'static str,
1166 read: f64,
1168 expected: f64,
1170}
1171
1172fn inclusion_exclusion_volume(
1183 first: &BrepSolid,
1184 second: &BrepSolid,
1185 operation: BooleanOperation,
1186 options: &BooleanOptions,
1187 va: Option<f64>,
1188 vb: Option<f64>,
1189 own_first: bool,
1190) -> Option<InclusionExclusion> {
1191 use BooleanOperation::{Intersect, Subtract, Union};
1192 let (va, vb) = (va?, vb?);
1193 let own = match operation {
1194 Intersect => (Intersect, false, "first ∩ second"),
1195 Union => (Union, false, "first ∪ second"),
1196 Subtract => (Subtract, false, "first − second"),
1197 };
1198 let readings = [
1199 (Intersect, false, "first ∩ second"),
1200 (Intersect, true, "second ∩ first"),
1201 (Union, false, "first ∪ second"),
1202 (Union, true, "second ∪ first"),
1203 (Subtract, false, "first − second"),
1204 (Subtract, true, "second − first"),
1205 ];
1206 let own_reading = own_first.then_some(own);
1207 let leading = usize::from(own_first);
1208 for (index, (other, swapped, reading)) in own_reading.into_iter().chain(readings).enumerate() {
1209 if index >= leading && !swapped && std::mem::discriminant(&other) == std::mem::discriminant(&operation) {
1210 continue;
1211 }
1212 let (x, y) = if swapped { (second, first) } else { (first, second) };
1213 let Some(read) = boolean_operation_with_diagnostics(x, y, other, options)
1214 .ok()
1215 .and_then(|outcome| body_volume(&outcome.value))
1216 else {
1217 continue;
1218 };
1219 let intersection = match (other, swapped) {
1220 (Intersect, _) => read,
1221 (Union, _) => va + vb - read,
1222 (Subtract, false) => va - read,
1223 (Subtract, true) => vb - read,
1224 };
1225 let (formula, expected) = match operation {
1226 Intersect => ("V(first ∩ second)", intersection),
1227 Union => ("V(first) + V(second) − V(first ∩ second)", va + vb - intersection),
1228 Subtract => ("V(first) − V(first ∩ second)", va - intersection),
1229 };
1230 return Some(InclusionExclusion { formula, reading, read, expected });
1231 }
1232 None
1233}
1234
1235fn planar_operand_off_its_plane(
1244 first: &BrepSolid,
1245 second: &BrepSolid,
1246 options: &BooleanOptions,
1247) -> Option<(u8, u64, f64, f64)> {
1248 let policy = options
1249 .tolerances
1250 .clone()
1251 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
1252 let weld = crate::tolerance::assembler_weld(policy.model);
1253 let mut worst: Option<(u8, u64, f64)> = None;
1254 for (operand, solid) in [(0u8, first), (1u8, second)] {
1255 let vertex_point = |id: u64| solid.vertices.iter().find(|v| v.id == id).map(|v| v.point);
1256 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
1257 let Some(crate::AnalyticSurface::Plane {
1258 origin,
1259 u_dir,
1260 v_dir,
1261 ..
1262 }) = face.surface.analytic()
1263 else {
1264 continue;
1265 };
1266 let [loop_record] = face.loops.as_slice() else {
1267 continue;
1268 };
1269 let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
1270 continue;
1271 };
1272 let mut off_plane = 0.0f64;
1273 let mut straight = true;
1274 for coedge in &loop_record.coedges {
1275 let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
1276 straight = false;
1277 break;
1278 };
1279 let (Some(start), Some(end)) =
1280 (vertex_point(edge.start_vertex_id), vertex_point(edge.end_vertex_id))
1281 else {
1282 straight = false;
1283 break;
1284 };
1285 let chord = end.sub(start);
1286 let length = chord.length();
1287 if edge.degenerate || length <= weld {
1288 straight = false;
1289 break;
1290 }
1291 let direction = chord.scale(1.0 / length);
1292 for fraction in [0.25, 0.5, 0.75] {
1293 let Ok(point) = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction) else {
1294 straight = false;
1295 break;
1296 };
1297 let delta = point.sub(start);
1298 let along = delta.dot(direction);
1299 if delta.sub(direction.scale(along)).length() > weld
1300 || along < -weld
1301 || along > length + weld
1302 {
1303 straight = false;
1304 break;
1305 }
1306 }
1307 if !straight {
1308 break;
1309 }
1310 off_plane = off_plane.max(start.sub(*origin).dot(normal).abs());
1311 }
1312 if straight && off_plane > weld && worst.is_none_or(|(_, _, gap)| off_plane > gap) {
1313 worst = Some((operand, face.id, off_plane));
1314 }
1315 }
1316 }
1317 worst.map(|(operand, face_id, gap)| (operand, face_id, gap, weld))
1318}
1319
1320fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
1323 let slack = 1e-2 * (va + vb).max(1.0);
1324 match operation {
1325 BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
1327 BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
1329 BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
1331 }
1332}
1333
1334fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), KernelRefusal> {
1361 fragment.same_sense = !fragment.same_sense;
1362 for loop_record in &mut fragment.loops {
1363 loop_record.coedges.reverse();
1364 for coedge in &mut loop_record.coedges {
1365 coedge.forward = !coedge.forward;
1366 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Validate, "reversed")?;
1367 }
1368 }
1369 Ok(())
1370}
1371
1372fn build_nary_imprint(
1379 operands: &[BrepSolid],
1380 options: &ImprintOptions,
1381) -> Result<ImprintResultRecord, KernelRefusal> {
1382 let mut section_evidence = false;
1383 let mut vertices: Vec<ImprintVertex> = Vec::new();
1384 let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
1385 let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
1386 let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
1387 let mut next_vertex_id: u64 = 1;
1388 let mut next_piece_id: u64 = 1;
1389
1390 for i in 0..operands.len() {
1391 for j in (i + 1)..operands.len() {
1392 let pair = build_imprints(&operands[i], &operands[j], options)?;
1393 let remap = |operand: u8| -> u8 {
1394 if operand == 0 {
1395 i as u8
1396 } else {
1397 j as u8
1398 }
1399 };
1400
1401 section_evidence = section_evidence || pair.section_evidence;
1402 let mut vertex_map: HashMap<u64, u64> = HashMap::default();
1403 for vertex in &pair.vertices {
1404 let global = next_vertex_id;
1405 next_vertex_id += 1;
1406 vertex_map.insert(vertex.id, global);
1407 vertices.push(ImprintVertex {
1408 id: global,
1409 point: vertex.point,
1410 });
1411 }
1412
1413 let mut piece_map: HashMap<u64, u64> = HashMap::default();
1414 for piece in &pair.pieces {
1415 let global = next_piece_id;
1416 next_piece_id += 1;
1417 piece_map.insert(piece.id, global);
1418 let mut mapped = piece.clone();
1419 mapped.id = global;
1420 mapped.start_vertex_id = *vertex_map
1421 .get(&piece.start_vertex_id)
1422 .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
1423 mapped.end_vertex_id = *vertex_map
1424 .get(&piece.end_vertex_id)
1425 .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
1426 for pcurve in &mut mapped.pcurves {
1427 pcurve.operand = remap(pcurve.operand);
1428 }
1429 mapped.support_faces = [
1430 FaceKey {
1431 operand: remap(piece.support_faces[0].operand),
1432 face_id: piece.support_faces[0].face_id,
1433 },
1434 FaceKey {
1435 operand: remap(piece.support_faces[1].operand),
1436 face_id: piece.support_faces[1].face_id,
1437 },
1438 ];
1439 pieces.push(mapped);
1440 }
1441
1442 for entry in &pair.by_face {
1443 let list = by_face
1444 .entry((remap(entry.operand), entry.face_id))
1445 .or_default();
1446 for piece_id in &entry.piece_ids {
1447 let global = *piece_map.get(piece_id).ok_or_else(|| {
1448 KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: by_face references unknown piece")
1449 })?;
1450 list.push(global);
1451 }
1452 }
1453
1454 for split in &pair.edge_splits {
1455 edge_splits
1456 .entry((remap(split.operand), split.edge_id))
1457 .or_default()
1458 .extend(split.parameters.iter().copied());
1459 }
1460 }
1461 }
1462
1463 Ok(ImprintResultRecord {
1464 vertices,
1465 pieces,
1466 tangent_nodes: Vec::new(),
1467 barrier_edges: Vec::new(),
1468 section_evidence,
1469 cosurface_pairs: Vec::new(),
1470 tangent_sections: Vec::new(),
1471 by_face: by_face
1472 .into_iter()
1473 .map(|((operand, face_id), piece_ids)| FaceImprints {
1474 operand,
1475 face_id,
1476 piece_ids,
1477 })
1478 .collect(),
1479 edge_splits: edge_splits
1480 .into_iter()
1481 .map(|((operand, edge_id), parameters)| EdgeSplitRecord {
1482 operand,
1483 edge_id,
1484 parameters,
1485 })
1486 .collect(),
1487 })
1488}
1489
1490pub fn boolean_operation_nary(
1501 operands: &[BrepSolid],
1502 operation: BooleanOperation,
1503) -> Result<BrepSolid, KernelRefusal> {
1504 boolean_operation_nary_with_options(operands, operation, &BooleanOptions::default())
1505}
1506
1507pub fn boolean_operation_nary_with_options(
1513 operands: &[BrepSolid],
1514 operation: BooleanOperation,
1515 options: &BooleanOptions,
1516) -> Result<BrepSolid, KernelRefusal> {
1517 if operands.is_empty() {
1518 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: no operands provided"));
1519 }
1520 if operands.len() == 1 {
1521 return Ok(operands[0].clone());
1522 }
1523 if operands.len() > 255 {
1524 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: at most 255 operands supported"));
1525 }
1526 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
1527
1528 let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
1532 for operand in &operands[1..] {
1533 let candidate = KernelTolerances::for_solid(operand, options.tolerance);
1534 if candidate.sew_search > policy.sew_search {
1535 policy = candidate;
1536 }
1537 }
1538 policy.check().or_input(KernelStage::Collect, "tolerances")?;
1539 let tolerance = policy.model;
1540
1541 let mut healed = operands.to_vec();
1544 for operand in &mut healed {
1545 crate::heal::heal_operands(operand, &policy)?;
1546 crate::heal::refit_inherited_trims(operand, &policy)?;
1549 normalize_operand_band_seams(operand)?;
1552 }
1553
1554 let mut imprint_options = options.imprint.clone();
1556 imprint_options.tolerance = tolerance;
1557 let imprint = build_nary_imprint(&healed, &imprint_options)?;
1558
1559 let mut split = Vec::with_capacity(healed.len());
1561 for (index, operand) in healed.iter().enumerate() {
1562 split.push(apply_edge_splits(operand, index as u8, &imprint)?);
1563 }
1564 let mut fragments = Vec::with_capacity(split.len());
1565 for (index, solid) in split.iter().enumerate() {
1566 fragments.push(fragment_solid(solid, index as u8, &imprint)?);
1567 }
1568
1569 let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
1571
1572 let solids: HashMap<u8, &BrepSolid> = split
1574 .iter()
1575 .enumerate()
1576 .map(|(index, solid)| (index as u8, solid))
1577 .collect();
1578 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
1579 let solid = if options.merge_coplanar_faces
1580 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
1581 {
1582 let merged = merge_same_surface_faces_excluding(
1583 &solid,
1584 tolerance,
1585 &options.keep_unmerged_name_substrs,
1586 )?;
1587 merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
1588 } else {
1589 solid
1590 };
1591 let validation = solid.validate_detailed(&policy);
1592 if !validation.issues.is_empty() {
1593 return Err(KernelRefusal::new(
1594 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
1595 KernelStage::Validate,
1596 format!(
1597 "boolean_operation_nary: invalid result: {:?}",
1598 validation.issues
1599 )));
1600 }
1601 if debug {
1602 if let Ok(report) =
1603 crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
1604 {
1605 if report.is_flagged() {
1606 eprintln!(
1607 "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
1608 report.disagreement_rate,
1609 report.disagreements.len(),
1610 report.considered,
1611 report.sample_disagreement()
1612 );
1613 }
1614 }
1615 }
1616 Ok(solid)
1617}
1618
1619fn attribute_to_tangent_node(error: KernelRefusal, nodes: &[Vec3]) -> KernelRefusal {
1650 let Some(node) = nodes.first() else {
1651 return error;
1652 };
1653 if !error.class.perturbation_eligible()
1654 || matches!(error.class, RefusalClass::TangentNodeSingularity)
1655 {
1656 return error;
1657 }
1658 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
1659 eprintln!(
1660 "boolean: tear downstream of the tangent node at ({:.6},{:.6},{:.6}) attributed to it; the stage read: {}",
1661 node.x, node.y, node.z, error.message
1662 );
1663 }
1664 KernelRefusal::new(
1665 RefusalClass::TangentNodeSingularity,
1666 error.stage,
1667 format!(
1668 "boolean: unsupported singular/tangent-node surface intersection \
1669 at the tangent node at ({:.6},{:.6},{:.6}): the section through it did not \
1670 assemble into a closed shell",
1671 node.x, node.y, node.z
1672 ),
1673 )
1674}
1675
1676fn attribute_to_tangent_section(
1698 error: KernelRefusal,
1699 sections: &[crate::imprint::TangentSectionRecord],
1700 origins: &[FaceKey],
1701) -> KernelRefusal {
1702 if sections.is_empty()
1703 || !error.class.perturbation_eligible()
1704 || matches!(error.class, RefusalClass::TangentNodeSingularity)
1705 {
1706 return error;
1707 }
1708 let named = refusal_result_faces(&error.message)
1709 .into_iter()
1710 .filter_map(|id| origins.get(id.checked_sub(1)? as usize).copied())
1711 .collect::<Vec<FaceKey>>();
1712 let involving = |fallback: bool| {
1716 sections.iter().find(|section| {
1717 section.grazing.is_empty() != fallback && named.iter().any(|face| section.faces.contains(face))
1718 })
1719 };
1720 let Some(section) = involving(false).or_else(|| involving(true)) else {
1721 return error;
1722 };
1723 let at = |point: Vec3| {
1726 let shown = |value: f64| if value.abs() < 5e-7 { 0.0 } else { value };
1727 format!("({:.6},{:.6},{:.6})", shown(point.x), shown(point.y), shown(point.z))
1728 };
1729 let pierces = (!section.grazing.is_empty()).then(|| {
1735 let shown = section.grazing.len().min(4);
1736 let mut listed = section.grazing[..shown]
1737 .iter()
1738 .map(|(point, tangency)| format!("{} at |tan·n| = {tangency:.4}", at(*point)))
1739 .collect::<Vec<_>>()
1740 .join(", ");
1741 if section.grazing.len() > shown {
1742 listed.push_str(&format!(" and {} more", section.grazing.len() - shown));
1743 }
1744 listed
1745 });
1746 let pierce_only = section.pierce_only();
1747 let shape = if let (true, Some(pierces)) = (pierce_only, pierces.as_deref()) {
1748 format!(
1749 "cross at a grazing angle: a trim edge pierces the other carrier inside both \
1750 trims at {pierces}, below the marcher's start gate, and no transverse section was \
1751 marched between the pierces — a near-tangent crossing born on a tangent contact \
1752 (a third-order singularity) the imprint does not build yet"
1753 )
1754 } else if section.planar {
1755 format!(
1756 "touch along a tangent curve from {} to {} (length {:.6}, {} stations{}) which the \
1757 imprint does not build yet",
1758 at(section.start),
1759 at(section.end),
1760 section.length,
1761 section.stations,
1762 if section.closed { ", closed" } else { "" }
1763 )
1764 } else {
1765 format!(
1766 "touch along a curve through {} with neither carrier planar, a curved/curved \
1767 tangent contact the lane does not trace yet",
1768 at(section.start)
1769 )
1770 };
1771 let shape = match (pierce_only, pierces) {
1772 (false, Some(pierces)) => format!(
1773 "{shape}; a trim edge also pierces the other carrier inside both trims at {pierces}, \
1774 below the marcher's start gate, with no transverse section marched"
1775 ),
1776 _ => shape,
1777 };
1778 let involved = named
1779 .iter()
1780 .filter(|face| section.faces.contains(face))
1781 .map(|face| format!("{}/{}", face.operand, face.face_id))
1782 .collect::<Vec<_>>()
1783 .join(", ");
1784 KernelRefusal::new(
1785 RefusalClass::TangentNodeSingularity,
1786 error.stage,
1787 format!(
1788 "boolean: unsupported tangent-curve surface intersection between faces {}/{} and {}/{}: \
1789 the carriers {shape}; the refusal names face(s) {involved} the contact involves; {}",
1790 section.first.operand, section.first.face_id, section.second.operand, section.second.face_id,
1791 error.message
1792 ),
1793 )
1794}
1795
1796fn refusal_result_faces(message: &str) -> Vec<u64> {
1801 let mut ids = Vec::new();
1802 let digits_after = |text: &str| -> Option<u64> {
1803 let end = text.find(|c: char| !c.is_ascii_digit()).unwrap_or(text.len());
1804 text[..end].parse().ok()
1805 };
1806 for (index, _) in message.match_indices("face ") {
1807 if let Some(id) = digits_after(&message[index + "face ".len()..]) {
1808 ids.push(id);
1809 }
1810 }
1811 if let Some(start) = message.find("on faces [") {
1812 let rest = &message[start + "on faces [".len()..];
1813 let list = &rest[..rest.find(']').unwrap_or(rest.len())];
1814 for entry in list.split(", ") {
1815 let entry = entry.trim_start();
1816 if let Some(id) = digits_after(entry) {
1817 if entry[id.to_string().len()..].starts_with(" '") {
1818 ids.push(id);
1819 }
1820 }
1821 }
1822 }
1823 ids.sort_unstable();
1824 ids.dedup();
1825 ids
1826}
1827
1828pub fn boolean_operation_with_diagnostics(
1829 first: &BrepSolid,
1830 second: &BrepSolid,
1831 operation: BooleanOperation,
1832 options: &BooleanOptions,
1833) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1834 let _account = profile::Scope::new("boolean.other");
1835 let closed_inputs = matches!(std::env::var("BREP_CLOSED_OPERAND_INPUT").as_deref(), Err(_) | Ok("1") | Ok("on"));
1836 if let (true, Some(later)) = (reseat::seam_reseat_enabled(), &options.seam_reseat_later_references) {
1837 let started = Instant::now();
1838 let (first, second) = reseat::reseat_tangent_seams(first, second, later, options.tolerance);
1839 profile_stage("seam_prepare_ms", started.elapsed().as_secs_f64() * 1000.0);
1840 return boolean_operation_with_diagnostics_impl(&first, &second, operation, options, closed_inputs);
1841 }
1842 boolean_operation_with_diagnostics_impl(first, second, operation, options, closed_inputs)
1843}
1844
1845fn boolean_operation_with_diagnostics_impl(
1846 first: &BrepSolid,
1847 second: &BrepSolid,
1848 operation: BooleanOperation,
1849 options: &BooleanOptions,
1850 closed_inputs: bool,
1851) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1852 let _coedge_memo = crate::topology::CoedgeMemoScope::open(crate::topology::coedge_memo_enabled());
1855 if matches!(operation, BooleanOperation::Intersect)
1858 && [first, second].iter().any(|body| body.kind == crate::BodyKind::Sheet)
1859 && match std::env::var("BREP_SHEET_TRIM").as_deref() {
1860 Ok("1") => true,
1861 Ok("0") => false,
1862 _ => options.sheet_trim,
1863 }
1864 {
1865 return sheet::trim(first, second, options);
1866 }
1867 if matches!(operation, BooleanOperation::Union | BooleanOperation::Subtract) {
1876 if let Some(operand) = [first, second]
1877 .iter()
1878 .position(|solid| solid.kind == crate::BodyKind::Sheet)
1879 {
1880 return Err(KernelRefusal::input(
1881 KernelStage::Collect,
1882 "sheet_operand",
1883 format!(
1884 "boolean: operand {operand} is a Sheet, which encloses no solid; union and \
1885 subtract of a sheet are refused; Intersect trims a Sheet by a Solid"
1886 ),
1887 ));
1888 }
1889 }
1890 let mut outcome = boolean_operation_with_diagnostics_core(first, second, operation, options)?;
1896 let closure_started = closed_inputs.then(Instant::now);
1897 if closed_inputs {
1898 for (operand, solid) in [first, second].into_iter().enumerate() {
1899 let open = one_use_edges(solid);
1900 if open > 0 {
1901 return Err(KernelRefusal::input(KernelStage::Collect, "operand_closure", format!(
1902 "boolean: operand {operand} is not a closed shell: {open} of its edges bound only one face, so its inside is undefined"
1903 )));
1904 }
1905 }
1906 }
1907 if let Some(started) = closure_started {
1908 let ms = started.elapsed().as_secs_f64() * 1000.0;
1909 profile_stage("operand_closure_ms", ms);
1910 outcome.diagnostics.measure_max("timing.operand_closure_ms", ms);
1911 }
1912 Ok(outcome)
1913}
1914
1915fn boolean_operation_with_diagnostics_core(
1916 first: &BrepSolid, second: &BrepSolid, operation: BooleanOperation, options: &BooleanOptions,
1917) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1918
1919 if let Some(offset) = crate::recentre::control_offset_at_entry() {
1922 return boolean_operation_with_diagnostics_at_offset(first, second, operation, options, offset);
1923 }
1924 let mut bounds = operand_bounds(first);
1927 bounds.include(operand_bounds(second));
1928 if let Some(offset) = crate::recentre::guard_offset(bounds) {
1929 crate::recentre::profile_fired("boolean", offset, bounds);
1930 let mut outcome = boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Guard)?;
1931 outcome.diagnostics.count_n("recentre.fired", 1);
1932 outcome.diagnostics.measure_max("recentre.distance", offset.length());
1933 return Ok(outcome);
1934 }
1935 let mut tangent_nodes = Vec::new();
1936 let (mut outcome, soundness_candidates) =
1937 boolean_pipeline(first, second, operation, options, &mut tangent_nodes)
1938 .map_err(|error| attribute_to_tangent_node(error, &tangent_nodes))?;
1939 let stage_started = Instant::now();
1952 let accepted = match &soundness_candidates {
1953 Some(candidates) => crate::accept_sound_against_scoped(&[first, second], outcome.value, "boolean", candidates),
1956 None => crate::accept_sound_against(&[first, second], outcome.value, "boolean"),
1957 };
1958 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1959 profile_stage("soundness_ms", stage_ms);
1960 outcome.diagnostics.measure_max("timing.soundness_ms", stage_ms);
1961 outcome.value = accepted?;
1962 Ok(outcome)
1963}
1964
1965pub fn boolean_operation_with_diagnostics_at_offset(
1969 first: &BrepSolid,
1970 second: &BrepSolid,
1971 operation: BooleanOperation,
1972 options: &BooleanOptions,
1973 offset: Vec3,
1974) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1975 boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Control)
1976}
1977
1978fn boolean_translated(
1980 first: &BrepSolid,
1981 second: &BrepSolid,
1982 operation: BooleanOperation,
1983 options: &BooleanOptions,
1984 offset: Vec3,
1985 kind: crate::recentre::EntryKind,
1986) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1987 let _entry = crate::recentre::TranslatedEntry::open(kind);
1988 let translate = |solid: &BrepSolid, offset: Vec3| {
1989 crate::recentre::translate_solid(solid, offset).map_err(|message| {
1990 KernelRefusal::internal(
1991 KernelStage::Collect,
1992 "translation_control",
1993 format!("boolean: the translation control could not move an operand: {message}"),
1994 )
1995 })
1996 };
1997 let moved_first = translate(first, offset)?;
1998 let moved_second = translate(second, offset)?;
1999 let outcome = boolean_operation_with_diagnostics(&moved_first, &moved_second, operation, options)?;
2000 let value = translate(&outcome.value, offset.scale(-1.0))?;
2001 Ok(KernelOutcome { value, diagnostics: outcome.diagnostics })
2002}
2003
2004fn profile_stage(code: &str, ms: f64) {
2008 let row = match code {
2009 "heal_ms" => Some("boolean.operand_prepare"),
2010 "imprint_ms" => Some("boolean.imprint"),
2011 "edge_split_ms" => Some("boolean.edge_split"),
2012 "fragment_ms" => Some("boolean.fragment"),
2013 "select_ms" => Some("boolean.select"),
2014 "assemble_ms" => Some("boolean.assemble"),
2015 "face_merge_ms" => Some("boolean.face_merge"),
2016 "soundness_ms" => Some("boolean.soundness"),
2017 "operand_closure_ms" => Some("boolean.operand_closure"),
2018 "seam_prepare_ms" => Some("boolean.seam_prepare"),
2019 "final_validate_ms" => Some("boolean.final_validate"),
2020 _ => None,
2021 };
2022 if let Some(row) = row { profile::record(row, ms); }
2023 if crate::profile::profile_enabled() {
2024 profile_line!("boolean.{code}={ms:.2}");
2025 }
2026}
2027
2028struct BooleanStageDump {
2033 directory: std::path::PathBuf,
2034 serial: usize,
2035}
2036
2037impl BooleanStageDump {
2038 fn open() -> Option<Self> {
2039 static SERIAL: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
2040 let directory = std::env::var("BREP_BOOLEAN_DUMP").ok().filter(|value| !value.is_empty())?;
2041 let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2042 Some(Self { directory: directory.into(), serial })
2043 }
2044
2045 fn write<T: Serialize + ?Sized>(&self, stage: &str, value: &T) {
2046 let path = self.directory.join(format!("{:03}-{stage}.json", self.serial));
2047 match serde_json::to_string(value) {
2048 Ok(text) => {
2049 if let Err(error) = std::fs::write(&path, text) {
2050 eprintln!("BREP_BOOLEAN_DUMP {}: {error}", path.display());
2051 }
2052 }
2053 Err(error) => eprintln!("BREP_BOOLEAN_DUMP {stage}: {error}"),
2054 }
2055 }
2056}
2057
2058fn boolean_policy(
2060 first: &BrepSolid,
2061 second: &BrepSolid,
2062 options: &BooleanOptions,
2063) -> Result<KernelTolerances, KernelRefusal> {
2064 let policy = options
2065 .tolerances
2066 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
2067 policy.check().or_input(KernelStage::Collect, "tolerances")?;
2068 Ok(policy)
2069}
2070
2071fn prepared_operands(
2074 first: &BrepSolid,
2075 second: &BrepSolid,
2076 policy: &KernelTolerances,
2077) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
2078 let stage_started = Instant::now();
2087 let mut first_owned = first.clone();
2088 let mut second_owned = second.clone();
2089 crate::heal::heal_operands(&mut first_owned, policy)?;
2090 crate::heal::heal_operands(&mut second_owned, policy)?;
2091 profile_stage("heal.operands_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
2092 let refit_started = Instant::now();
2096 crate::heal::refit_inherited_trims(&mut first_owned, policy)?;
2097 crate::heal::refit_inherited_trims(&mut second_owned, policy)?;
2098 profile_stage("heal.refit_ms", refit_started.elapsed().as_secs_f64() * 1_000.0);
2099 if crate::profile::profile_enabled() {
2100 let (hits, misses) = crate::reading_memo::counts();
2101 profile_line!("boolean.reading_memo hits={hits} misses={misses}");
2102 }
2103 profile_stage("heal_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
2104 normalize_operand_band_seams(&mut first_owned)?;
2109 normalize_operand_band_seams(&mut second_owned)?;
2110 Ok((first_owned, second_owned))
2111}
2112
2113fn boolean_imprint_options(options: &BooleanOptions, tolerance: f64) -> ImprintOptions {
2114 let mut imprint_options = options.imprint.clone();
2115 imprint_options.tolerance = tolerance;
2116 imprint_options
2117}
2118
2119pub fn boolean_split_operands(
2129 first: &BrepSolid,
2130 second: &BrepSolid,
2131 options: &BooleanOptions,
2132) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
2133 let policy = boolean_policy(first, second, options)?;
2134 let (first, second) = prepared_operands(first, second, &policy)?;
2135 let imprint = build_imprints(&first, &second, &boolean_imprint_options(options, policy.model))?;
2136 let (split_first, _) = apply_edge_splits_with_map(&first, 0, &imprint)?;
2137 let (split_second, _) = apply_edge_splits_with_map(&second, 1, &imprint)?;
2138 Ok((split_first, split_second))
2139}
2140
2141fn operand_bounds(solid: &BrepSolid) -> crate::Aabb {
2145 let mut bounds = crate::Aabb::from_points(solid.vertices.iter().map(|vertex| vertex.point));
2146 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2147 match crate::Aabb::from_surface_controls(&face.surface) {
2148 Ok(face_bounds) => bounds.include(face_bounds),
2149 Err(_) => {
2150 return crate::Aabb {
2151 minimum: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
2152 maximum: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
2153 }
2154 }
2155 }
2156 }
2157 bounds
2158}
2159
2160fn line_or_point_contact(first: &BrepSolid, second: &BrepSolid, bounds: [crate::Aabb; 2]) -> bool {
2166 apart_axes(bounds) >= 2 && plain_hull_solid(first) && plain_hull_solid(second)
2167}
2168
2169fn boxes_share_no_interior(first: &BrepSolid, second: &BrepSolid, bounds: [crate::Aabb; 2]) -> bool {
2172 apart_axes(bounds) >= 1 && plain_hull_solid(first) && plain_hull_solid(second)
2173}
2174
2175fn apart_axes([a, b]: [crate::Aabb; 2]) -> usize {
2177 let apart = |a_min: f64, a_max: f64, b_min: f64, b_max: f64| a_max <= b_min || b_max <= a_min;
2178 [
2179 apart(a.minimum.x, a.maximum.x, b.minimum.x, b.maximum.x),
2180 apart(a.minimum.y, a.maximum.y, b.minimum.y, b.maximum.y),
2181 apart(a.minimum.z, a.maximum.z, b.minimum.z, b.maximum.z),
2182 ]
2183 .into_iter()
2184 .filter(|&apart| apart)
2185 .count()
2186}
2187
2188fn plain_hull_solid(solid: &BrepSolid) -> bool {
2190 solid.kind == crate::BodyKind::Solid
2191 && solid.cells.is_empty()
2192 && solid.shells.iter().flat_map(|shell| &shell.faces).all(|face| {
2193 face.surface.control_points.iter().flatten().all(|control| control.w > 0.0 && control.w.is_finite())
2194 })
2195}
2196
2197fn soundness_candidates(
2202 solid: &BrepSolid,
2203 scope: &AssemblyScope,
2204 bounds: [crate::Aabb; 2],
2205 band: f64,
2206) -> HashSet<u64> {
2207 let mut candidates = HashSet::default();
2208 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2209 if scope.examined.contains(&face.id) {
2210 candidates.insert(face.id);
2211 continue;
2212 }
2213 let Some(&operand) = scope.operand_of.get(&face.id) else {
2214 candidates.insert(face.id);
2215 continue;
2216 };
2217 let other = bounds[usize::from(operand == 0)];
2218 match crate::Aabb::from_surface_controls(&face.surface) {
2219 Ok(face_bounds) if !face_bounds.intersects(other, band) => {}
2220 _ => {
2221 candidates.insert(face.id);
2222 }
2223 }
2224 }
2225 candidates
2226}
2227
2228fn empty_solid() -> BrepSolid {
2230 BrepSolid {
2231 mass_properties_cache: Default::default(),
2232 cells: Default::default(),
2233 id: 0,
2234 vertices: Vec::new(),
2235 edges: Vec::new(),
2236 shells: Vec::new(),
2237 genus: 0,
2238 kind: crate::BodyKind::Solid,
2239 }
2240}
2241
2242fn concatenate_disjoint(first: &BrepSolid, second: &BrepSolid) -> BrepSolid {
2248 let mut vertices = Vec::new();
2249 let mut edges = Vec::new();
2250 let mut shells = Vec::new();
2251 let mut used_face_names = HashSet::<String>::default();
2252 let (mut next_face_id, mut next_loop_id, mut next_coedge_id) = (1u64, 1u64, 1u64);
2253 for operand in [first, second] {
2254 let vertex_base = vertices.len() as u64;
2255 let edge_base = edges.len() as u64;
2256 let mut vertex_ids: HashMap<u64, u64> = HashMap::default();
2257 for (index, vertex) in operand.vertices.iter().enumerate() {
2258 let id = vertex_base + index as u64 + 1;
2259 vertex_ids.insert(vertex.id, id);
2260 vertices.push(VertexRecord { id, point: vertex.point });
2261 }
2262 let mut edge_ids: HashMap<u64, u64> = HashMap::default();
2263 for (index, edge) in operand.edges.iter().enumerate() {
2264 let id = edge_base + index as u64 + 1;
2265 edge_ids.insert(edge.id, id);
2266 edges.push(EdgeRecord {
2267 id,
2268 curve: edge.curve.clone(),
2269 t0: edge.t0,
2270 t1: edge.t1,
2271 start_vertex_id: vertex_ids.get(&edge.start_vertex_id).copied().unwrap_or(edge.start_vertex_id),
2272 end_vertex_id: vertex_ids.get(&edge.end_vertex_id).copied().unwrap_or(edge.end_vertex_id),
2273 degenerate: edge.degenerate,
2274 name: edge.name.clone(),
2275 });
2276 }
2277 for shell in &operand.shells {
2278 let mut faces = Vec::with_capacity(shell.faces.len());
2279 for face in &shell.faces {
2280 let loops = face
2281 .loops
2282 .iter()
2283 .map(|loop_record| {
2284 let coedges = loop_record
2285 .coedges
2286 .iter()
2287 .map(|coedge| {
2288 let id = next_coedge_id;
2289 next_coedge_id += 1;
2290 CoedgeRecord {
2291 id,
2292 edge_id: edge_ids.get(&coedge.edge_id).copied().unwrap_or(coedge.edge_id),
2293 forward: coedge.forward,
2294 pcurve: coedge.pcurve.clone(),
2295 }
2296 })
2297 .collect();
2298 let id = next_loop_id;
2299 next_loop_id += 1;
2300 LoopRecord { id, coedges }
2301 })
2302 .collect();
2303 let name = face.name.clone().map(|base| {
2304 if used_face_names.insert(base.clone()) {
2305 base
2306 } else {
2307 let mut suffix = 1usize;
2308 loop {
2309 let candidate = format!("{base}_{suffix}");
2310 if used_face_names.insert(candidate.clone()) {
2311 break candidate;
2312 }
2313 suffix += 1;
2314 }
2315 }
2316 });
2317 faces.push(FaceRecord {
2318 id: next_face_id,
2319 surface: face.surface.clone(),
2320 same_sense: face.same_sense,
2321 loops,
2322 name,
2323 });
2324 next_face_id += 1;
2325 }
2326 shells.push(ShellRecord { id: shells.len() as u64 + 1, faces });
2327 }
2328 }
2329 BrepSolid {
2330 mass_properties_cache: Default::default(),
2331 cells: Default::default(),
2332 id: 1,
2333 vertices,
2334 edges,
2335 shells,
2336 genus: first.genus + second.genus,
2337 kind: crate::BodyKind::Solid,
2338 }
2339}
2340
2341fn boolean_pipeline(
2344 first: &BrepSolid,
2345 second: &BrepSolid,
2346 operation: BooleanOperation,
2347 options: &BooleanOptions,
2348 tangent_nodes: &mut Vec<Vec3>,
2349) -> Result<(KernelOutcome<BrepSolid>, Option<HashSet<u64>>), KernelRefusal> {
2350 let _work = crate::imprint::work_profile::Scope::open("boolean");
2351 let replaced_operands = operand_state::replace_operands(first, second, operation, options);
2352 let (first, second) = match &replaced_operands {
2353 Some((first, second)) => (first, second),
2354 None => (first, second),
2355 };
2356 let policy = boolean_policy(first, second, options)?;
2357 let tolerance = policy.model;
2358 let bounds = [operand_bounds(first), operand_bounds(second)];
2373 let disjoint = disjoint_fast_path(options) && !bounds[0].intersects(bounds[1], policy.sew_search);
2374 let contact = !disjoint
2375 && contact_fast_path(options)
2376 && options.seam_reseat_later_references.is_some()
2377 && line_or_point_contact(first, second, bounds);
2378 let contact = contact
2383 || (!disjoint
2384 && matches!(operation, BooleanOperation::Intersect)
2385 && contact_fast_path(options)
2386 && boxes_share_no_interior(first, second, bounds));
2387 if disjoint || contact {
2388 let operation_started = Instant::now();
2389 let mut diagnostics = KernelDiagnostics::default();
2390 diagnostics.count_n(
2391 "collect.faces",
2392 first.shells.iter().chain(&second.shells).map(|shell| shell.faces.len() as u64).sum(),
2393 );
2394 diagnostics.count(if contact { "boolean.contact_fast_path" } else { "boolean.disjoint_fast_path" });
2395 diagnostics.measure_max("tolerance.model", policy.model);
2396 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
2397 let value = match operation {
2398 BooleanOperation::Union => concatenate_disjoint(first, second),
2399 BooleanOperation::Subtract => first.clone(),
2400 BooleanOperation::Intersect => {
2401 diagnostics.event(
2402 DiagnosticSeverity::Info,
2403 KernelStage::Select,
2404 "boolean.empty_result",
2405 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
2406 );
2407 empty_solid()
2408 }
2409 };
2410 let stage_ms = operation_started.elapsed().as_secs_f64() * 1_000.0;
2411 profile_stage("disjoint_fast_path_ms", stage_ms);
2412 diagnostics.measure_max("timing.total_ms", stage_ms);
2413 return Ok((KernelOutcome { value, diagnostics }, Some(HashSet::default())));
2414 }
2415 let heal_started = Instant::now();
2416 let (first_owned, second_owned) = prepared_operands(first, second, &policy)?;
2417 let heal_ms = heal_started.elapsed().as_secs_f64() * 1_000.0;
2418 let first = &first_owned;
2419 let second = &second_owned;
2420
2421 let mut diagnostics = KernelDiagnostics::default();
2422 diagnostics.measure_max("timing.heal_ms", heal_ms);
2425 let fit_scope = crate::PcurveFitScope::open();
2430 let polyline_scope = crate::PolylineFitScope::open();
2434 let operation_started = Instant::now();
2435 diagnostics.count_n(
2436 "collect.faces",
2437 first
2438 .shells
2439 .iter()
2440 .chain(&second.shells)
2441 .map(|shell| shell.faces.len() as u64)
2442 .sum(),
2443 );
2444 diagnostics.measure_max("tolerance.model", policy.model);
2445 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
2446
2447 let imprint_options = boolean_imprint_options(options, tolerance);
2448 let stage_started = Instant::now();
2449 let imprint = build_imprints(first, second, &imprint_options)?;
2450 tangent_nodes.extend_from_slice(&imprint.tangent_nodes);
2451 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
2452 for piece in &imprint.pieces {
2453 let start = piece.curve.evaluate(piece.t0);
2454 let end = piece.curve.evaluate(piece.t1);
2455 eprintln!(
2456 "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
2457 piece.id, piece.support_faces, piece.t0, piece.t1, start, end
2458 );
2459 }
2460 for entry in &imprint.by_face {
2461 eprintln!(
2462 "by_face operand={} face={} pieces={:?}",
2463 entry.operand, entry.face_id, entry.piece_ids
2464 );
2465 }
2466 for split in &imprint.edge_splits {
2467 eprintln!(
2468 "edge_split operand={} edge={} params={:?}",
2469 split.operand, split.edge_id, split.parameters
2470 );
2471 }
2472 }
2473 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2474 profile_stage("imprint_ms", stage_ms);
2475 diagnostics.measure_max("timing.imprint_ms", stage_ms);
2476 diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
2477 diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
2478 diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
2479 let (march_swap_rescues, clip_swap_rescues) = crate::imprint::take_rescue_firings();
2480 diagnostics.count_n("intersect.march_swap_rescue", march_swap_rescues);
2481 diagnostics.count_n("intersect.clip_swap_rescue", clip_swap_rescues);
2482 let touch = TouchCensus::build([first, second], &imprint);
2483 touch.report(&mut diagnostics);
2484 let stage_started = Instant::now();
2485 let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
2486 let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
2487 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2488 profile_stage("edge_split_ms", stage_ms);
2489 diagnostics.measure_max("timing.edge_split_ms", stage_ms);
2490 let stage_started = Instant::now();
2491 crate::classification::classify_profile_begin();
2492 let fragments_a = crate::fragment::fragment_solid_against(&split_first, 0, &imprint, crate::fragment::bounded_carriers(second).then_some((bounds[1], policy.sew_search)))?;
2493 let fragments_b = crate::fragment::fragment_solid_against(&split_second, 1, &imprint, crate::fragment::bounded_carriers(first).then_some((bounds[0], policy.sew_search)))?;
2494 crate::classification::classify_profile_report("fragment");
2495 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2496 profile_stage("fragment_ms", stage_ms);
2497 diagnostics.measure_max("timing.fragment_ms", stage_ms);
2498 diagnostics.count_n(
2499 "fragment.candidates",
2500 (fragments_a.len() + fragments_b.len()) as u64,
2501 );
2502 touch.report_fragments(&[&fragments_a, &fragments_b], &mut diagnostics);
2503 let a_surface_samples: Vec<Vec3> = fragments_a
2508 .iter()
2509 .flat_map(|fragment| {
2510 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
2511 })
2512 .collect();
2513 let b_surface_samples: Vec<Vec3> = fragments_b
2514 .iter()
2515 .flat_map(|fragment| {
2516 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
2517 })
2518 .collect();
2519 let stage_started = Instant::now();
2520 let mut barrier_edges: HashSet<(u8, u64)> = imprint
2521 .pieces
2522 .iter()
2523 .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
2524 .collect();
2525 barrier_edges.extend(imprint.barrier_edges.iter().copied());
2526 for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
2532 let minted: Vec<(u8, u64)> = barrier_edges
2533 .iter()
2534 .filter(|(barrier_operand, _)| *barrier_operand == operand)
2535 .filter_map(|(_, edge_id)| split_map.get(edge_id))
2536 .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
2537 .collect();
2538 barrier_edges.extend(minted);
2539 }
2540 crate::classification::classify_profile_begin();
2541 let coincident = coincident_pairs(
2542 &fragments_a,
2543 &fragments_b,
2544 first,
2545 second,
2546 &imprint.cosurface_pairs,
2547 tolerance,
2548 )?;
2549 let sector_mode = sector::sector_mode(options);
2550 let mut select_census = sector::SelectCensus::default();
2551 let mut selected = select_fragments(
2552 fragments_a,
2553 fragments_b,
2554 first,
2555 second,
2556 operation,
2557 tolerance,
2558 &barrier_edges,
2559 &imprint,
2560 &coincident,
2561 sector_mode,
2562 &mut select_census,
2563 )?;
2564 let resewn = resew_coincident_rims(
2565 &mut selected,
2566 [&split_first, &split_second],
2567 [first, second],
2568 &imprint.cosurface_pairs,
2569 operation,
2570 tolerance,
2571 )?;
2572 diagnostics.count_n("select.coincident_pairs", coincident.len() as u64);
2573 diagnostics.count_n("select.rims_resewn", resewn as u64);
2574 crate::classification::classify_profile_report("select");
2575 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2576 profile_stage("select_ms", stage_ms);
2577 diagnostics.measure_max("timing.select_ms", stage_ms);
2578 diagnostics.count_n("select.fragments", selected.len() as u64);
2579 diagnostics.count_n("select.sector_eligible", select_census.eligible);
2581 diagnostics.count_n("select.sector_primary", select_census.sector_primary);
2582 diagnostics.count_n("select.sector_fallback", select_census.sector_fallback);
2583 diagnostics.count_n("select.sector_disagree", select_census.disagree);
2584 diagnostics.count_n("select.ray_casts", select_census.ray_casts);
2585 diagnostics.count_n("select.on_votes", select_census.on_votes);
2586 diagnostics.count_n("select.overturns", select_census.overturns);
2587 if sector_mode != sector::SectorMode::Off || std::env::var_os("BREP_SECTOR_CENSUS").is_some() {
2588 sector::sector_census_line(|| {
2589 format!(
2590 "selection mode={sector_mode:?} select_ms={stage_ms:.3} selected={} eligible={} primary={} fallback={} disagree={} ray_casts={} on_votes={} overturns={}",
2591 selected.len(),
2592 select_census.eligible,
2593 select_census.sector_primary,
2594 select_census.sector_fallback,
2595 select_census.disagree,
2596 select_census.ray_casts,
2597 select_census.on_votes,
2598 select_census.overturns
2599 )
2600 });
2601 }
2602 if selected.is_empty()
2624 && !imprint.section_evidence
2625 && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
2626 {
2627 let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
2628 for &sample in samples {
2629 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::In {
2630 return Ok(true);
2631 }
2632 }
2633 Ok(false)
2634 };
2635 let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
2636 for &sample in samples {
2637 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::Out {
2638 return Ok(true);
2639 }
2640 }
2641 Ok(false)
2642 };
2643 let legitimate = match operation {
2644 BooleanOperation::Intersect => {
2645 !strictly_inside(&a_surface_samples, second)?
2646 && !strictly_inside(&b_surface_samples, first)?
2647 }
2648 BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
2649 BooleanOperation::Union => false,
2650 };
2651 if legitimate {
2652 diagnostics.event(
2653 DiagnosticSeverity::Info,
2654 KernelStage::Select,
2655 "boolean.empty_result",
2656 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
2657 );
2658 fit_scope.close().report_into(&mut diagnostics);
2659 polyline_scope.close().report_into(&mut diagnostics);
2660 diagnostics.measure_max(
2661 "timing.total_ms",
2662 operation_started.elapsed().as_secs_f64() * 1_000.0,
2663 );
2664 return Ok((
2665 KernelOutcome {
2666 value: empty_solid(),
2667 diagnostics,
2668 },
2669 None,
2670 ));
2671 }
2672 }
2673 let solids = [(0, &split_first), (1, &split_second)]
2674 .into_iter()
2675 .collect::<HashMap<_, _>>();
2676 let dump = BooleanStageDump::open();
2677 if let Some(dump) = &dump {
2678 dump.write("first", &split_first);
2679 dump.write("second", &split_second);
2680 dump.write("selected", &selected);
2681 }
2682 let stage_started = Instant::now();
2683 let assembled_origins: Vec<FaceKey> = if imprint.tangent_sections.is_empty() {
2687 Vec::new()
2688 } else {
2689 selected
2690 .iter()
2691 .map(|fragment| FaceKey { operand: fragment.operand, face_id: fragment.source_face_id })
2692 .collect()
2693 };
2694 let attributable: Vec<crate::imprint::TangentSectionRecord> = imprint
2701 .tangent_sections
2702 .iter()
2703 .filter(|section| imprint.tangent_nodes.is_empty() || section.grazing.is_empty())
2704 .cloned()
2705 .collect();
2706 let attribute = |error: KernelRefusal| {
2707 attribute_to_tangent_section(error, &attributable, &assembled_origins)
2708 };
2709 let cellular_result = matches!(operation, BooleanOperation::Union)
2710 && cellular::enabled(options)
2711 && cellular::complete_contact(first, second, &selected, tolerance);
2712 let (solid, scope) = if cellular_result {
2713 (assemble::assemble_cellular_fragments(selected, &solids, &imprint, tolerance)
2714 .map_err(attribute)?, None)
2715 } else if untouched_passthrough(options) {
2716 assemble_fragments_scoped(selected, &solids, &imprint, tolerance, &touch, touched_assembly(options))
2717 .map_err(attribute)?
2718 } else {
2719 (assemble_fragments(selected, &solids, &imprint, tolerance).map_err(attribute)?, None)
2720 };
2721 if let Some(scope) = &scope {
2722 diagnostics.count_n("passthrough.faces_examined", scope.examined.len() as u64);
2723 }
2724 if let Some(dump) = &dump {
2725 dump.write("assembled", &solid);
2726 }
2727 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2728 profile_stage("assemble_ms", stage_ms);
2729 diagnostics.measure_max("timing.assemble_ms", stage_ms);
2730 let stage_started = Instant::now();
2731 if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
2732 let issues = solid.validate();
2733 eprintln!(
2734 "pre-merge validation: {} issue(s){}",
2735 issues.len(),
2736 issues
2737 .first()
2738 .map(|issue| format!(" — first: {}", issue.message))
2739 .unwrap_or_default()
2740 );
2741 }
2742 let solid = if solid.kind != crate::BodyKind::Cellular && options.merge_coplanar_faces
2743 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
2744 {
2745 let merged = match &scope {
2746 Some(scope) => merge_same_surface_faces_scoped(
2750 &solid,
2751 tolerance,
2752 &options.keep_unmerged_name_substrs,
2753 &scope.examined,
2754 )?,
2755 None => merge_same_surface_faces_excluding(
2756 &solid,
2757 tolerance,
2758 &options.keep_unmerged_name_substrs,
2759 )?,
2760 };
2761 merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
2766 } else {
2767 solid
2768 };
2769 if let Some(dump) = &dump {
2770 dump.write("merged", &solid);
2771 }
2772 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2773 profile_stage("face_merge_ms", stage_ms);
2774 diagnostics.measure_max("timing.face_merge_ms", stage_ms);
2775 let validation_started = Instant::now();
2776 let validation = solid.validate_detailed_scoped(&policy, scope.as_ref().map(|scope| &scope.examined));
2777 profile_stage("final_validate_ms", validation_started.elapsed().as_secs_f64() * 1000.0);
2778 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
2779 diagnostics.count_n(
2780 "validate.wire_warnings",
2781 validation.wire_warnings.len() as u64,
2782 );
2783 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
2784 for warning in &validation.wire_warnings {
2785 diagnostics.event(
2786 DiagnosticSeverity::Warning,
2787 KernelStage::Validate,
2788 "validate.uv_wire",
2789 warning.message.clone(),
2790 );
2791 }
2792 for issue in &validation.issues {
2793 diagnostics.event(
2794 DiagnosticSeverity::Error,
2795 KernelStage::Validate,
2796 "validate.brep",
2797 issue.message.clone(),
2798 );
2799 }
2800 fit_scope.close().report_into(&mut diagnostics);
2801 polyline_scope.close().report_into(&mut diagnostics);
2802 diagnostics.measure_max(
2803 "timing.total_ms",
2804 operation_started.elapsed().as_secs_f64() * 1_000.0,
2805 );
2806 if !validation.issues.is_empty() {
2807 return Err(KernelRefusal::new(
2808 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
2809 KernelStage::Validate,
2810 format!(
2811 "boolean_operation: invalid result: {:?}",
2812 validation.issues
2813 )));
2814 }
2815 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
2821 if let Ok(report) =
2822 crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
2823 {
2824 if report.is_flagged() {
2825 eprintln!(
2826 "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
2827 report.disagreement_rate,
2828 report.disagreements.len(),
2829 report.considered,
2830 report.sample_disagreement()
2831 );
2832 }
2833 }
2834 let fusable_band = crate::tolerance::assembler_weld(policy.model);
2835 let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
2836 if !fusables.is_empty() {
2837 eprintln!(
2838 "[oracle] {} residual fusable(s) after weld; sample {:?}",
2839 fusables.len(),
2840 fusables.first()
2841 );
2842 }
2843 }
2844 let candidates = scope.as_ref().map(|scope| {
2845 let candidates = soundness_candidates(
2846 &solid,
2847 scope,
2848 [operand_bounds(first), operand_bounds(second)],
2849 policy.sew_search,
2850 );
2851 diagnostics.count_n("passthrough.soundness_candidates", candidates.len() as u64);
2852 crate::soundness::census::profile_operation(&solid, &[first, second], Some(&scope.touched), "boolean");
2853 candidates
2854 });
2855 Ok((
2856 KernelOutcome {
2857 value: solid,
2858 diagnostics,
2859 },
2860 candidates,
2861 ))
2862}
2863