1use crate::arrangement::Vec2;
2use crate::classification::{classify_point, parameter_point_in_face, PolygonClass};
3use crate::fragment::{FaceFragmentRecord, FragmentEdgeSource};
4use crate::imprint::{
5 EdgeSplitRecord, FaceImprints, FaceKey, ImprintOptions, ImprintPieceRecord,
6 ImprintResultRecord, ImprintVertex,
7};
8use crate::tolerance::{assembler_weld, commit_weld};
9use crate::topology::{
10 adaptive_coedge_error, BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord,
11 ShellRecord, VertexRecord,
12};
13use crate::{
14 KernelRefusal, OrRefuse, RefusalClass,
15 apply_edge_splits, apply_edge_splits_with_map, build_imprints, build_pcurve_on_surface,
16 build_pcurve_on_surface_range, classify_surface_pair,
17 fragment_solid, interpolate_curve, merge_curve_continuation_edges,
18 merge_same_surface_faces_excluding,
19 project_point_to_curve, project_point_to_surface, solid_signed_volume, AffineTransform,
20 DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage, KernelTolerances, NurbsCurve,
21 PointClass, SolidClassifier, SurfacePairRelation, Vec3,
22};
23use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
24use serde::{Deserialize, Serialize};
25use web_time::Instant;
26
27thread_local! {
28 pub(crate) static CONFORMANCE_REPAIRS: std::cell::Cell<u64> =
34 const { std::cell::Cell::new(0) };
35}
36
37#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
38#[serde(rename_all = "lowercase")]
39pub enum BooleanOperation {
40 Union,
41 Intersect,
42 Subtract,
43}
44
45#[derive(Clone, Debug, Deserialize)]
46pub struct BooleanOptions {
47 #[serde(default = "default_tolerance")]
48 pub tolerance: f64,
49 #[serde(default)]
52 pub tolerances: Option<KernelTolerances>,
53 #[serde(default)]
54 pub imprint: ImprintOptions,
55 #[serde(default = "default_true")]
58 pub merge_coplanar_faces: bool,
59 #[serde(default)]
69 pub keep_unmerged_name_substrs: Vec<String>,
70}
71
72fn default_tolerance() -> f64 {
73 1e-7
74}
75
76fn default_true() -> bool {
77 true
78}
79
80impl Default for BooleanOptions {
81 fn default() -> Self {
82 Self {
83 tolerance: default_tolerance(),
84 tolerances: None,
85 imprint: ImprintOptions::default(),
86 merge_coplanar_faces: true,
87 keep_unmerged_name_substrs: Vec::new(),
88 }
89 }
90}
91
92mod select;
93mod assemble;
94mod operand_state;
95mod rim;
96mod coincident;
97
98use rim::*;
99use select::*;
100use coincident::*;
101pub(crate) use assemble::{
102 assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
103 edge_interior_lies_on, finalize_assembled_solid,
104};
105#[allow(unused_imports)]
107pub(crate) use assemble::apply_assembly_heal_chain;
108
109pub fn boolean_operation(
110 first: &BrepSolid,
111 second: &BrepSolid,
112 operation: BooleanOperation,
113 options: &BooleanOptions,
114) -> Result<BrepSolid, KernelRefusal> {
115 match boolean_operation_with_diagnostics(first, second, operation, options) {
116 Ok(outcome) => Ok(outcome.value),
117 Err(error) => {
118 if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
126 || !error.class.perturbation_eligible()
127 {
128 return Err(error);
129 }
130 if std::env::var("BREP_OPEN_OPERAND_GATE").as_deref() != Ok("0") {
152 if let Some((operand, open)) = [first, second]
153 .iter()
154 .enumerate()
155 .find_map(|(operand, solid)| {
156 let open = one_use_edges(solid);
157 (open > 0).then_some((operand, open))
158 })
159 {
160 return Err(error.with_message(|message| format!(
161 "{message}; operand {operand} is not a closed shell: {open} of its edges bound \
162 only one face, so no boolean of it encloses a solid and no perturbation \
163 can close it"
164 )));
165 }
166 }
167 if let Some((operand, face_id, deviation, weld)) =
168 planar_operand_off_its_plane(first, second, options)
169 {
170 return Err(error.with_message(|message| format!(
171 "{message}; operand {operand} face {face_id} is a plane whose straight \
172 boundary sits {deviation:.3e} off it (assembly weld {weld:.1e}), which \
173 no perturbation can reconcile — the body's planes must contain their \
174 boundary before it can take part in an exact boolean"
175 )));
176 }
177 if matches!(operation, BooleanOperation::Subtract)
192 && std::env::var("BREP_PINCH_GATE").as_deref() != Ok("0")
193 && subtract_pinches_at_internal_tangency(first, second, options.tolerance)
194 .unwrap_or(false)
195 {
196 return Err(error.with_message(|message| format!(
197 "{message}; internal tangency: the operands touch tangentially with \
198 co-directed normals, so the exact difference pinches to zero thickness \
199 (non-manifold, unrepresentable in a boundary model) — refusing rather \
200 than returning a perturbed sliver"
201 )));
202 }
203 if matches!(error.class, RefusalClass::TangentNodeSingularity)
219 && std::env::var("BREP_TANGENT_NODE_GATE").as_deref() != Ok("0")
220 {
221 return Err(error);
222 }
223 match perturbation_retry(first, second, operation, options) {
224 Ok(solid) => Ok(solid),
225 Err(refused) if refused.is_empty() => Err(error),
226 Err(refused) => {
227 Err(error.with_message(|message| format!("{message}; {}", refused.join("; "))))
228 }
229 }
230 }
231 }
232}
233
234const PINCH_PROBE_STEPS: usize = 8;
239
240const PINCH_ANGULAR_TOLERANCE: f64 = 1e-4;
243
244const PINCH_SEPARATION_STEP: f64 = 1e-2;
248
249fn subtract_pinches_at_internal_tangency(
271 first: &BrepSolid,
272 second: &BrepSolid,
273 tolerance: f64,
274) -> Result<bool, KernelRefusal> {
275 let contact = (tolerance * 100.0).max(1e-12);
278 let probes_a = first
279 .shells
280 .iter()
281 .flat_map(|shell| &shell.faces)
282 .map(PinchProbe::of)
283 .collect::<Result<Vec<_>, _>>()?;
284 let probes_b = second
285 .shells
286 .iter()
287 .flat_map(|shell| &shell.faces)
288 .map(PinchProbe::of)
289 .collect::<Result<Vec<_>, _>>()?;
290 for probe_a in &probes_a {
291 for probe_b in &probes_b {
292 let pad = 1e-2 * probe_a.extent().max(probe_b.extent());
299 if probe_a.separation(probe_b) > contact + pad {
300 continue;
301 }
302 if classify_surface_pair(
309 &probe_a.face.surface,
310 &probe_b.face.surface,
311 tolerance,
312 PINCH_ANGULAR_TOLERANCE,
313 ).or_refuse(KernelStage::Validate, "csg.boolean.mod")?
314 .relation
315 == SurfacePairRelation::Cosurface
316 {
317 continue;
318 }
319 if faces_touch_with_codirected_normals(probe_a, probe_b, contact)? {
320 return Ok(true);
321 }
322 }
323 }
324 Ok(false)
325}
326
327struct PinchProbe<'a> {
330 face: &'a FaceRecord,
331 samples: Vec<(f64, f64, Vec3)>,
333 minimum: [f64; 3],
334 maximum: [f64; 3],
335}
336
337impl<'a> PinchProbe<'a> {
338 fn of(face: &'a FaceRecord) -> Result<Self, KernelRefusal> {
339 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
340 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
341 let steps = PINCH_PROBE_STEPS as f64;
342 let mut samples = Vec::with_capacity((PINCH_PROBE_STEPS + 1).pow(2));
343 let mut minimum = [f64::INFINITY; 3];
344 let mut maximum = [f64::NEG_INFINITY; 3];
345 for i in 0..=PINCH_PROBE_STEPS {
346 let u = u0 + (u1 - u0) * i as f64 / steps;
347 for j in 0..=PINCH_PROBE_STEPS {
348 let v = v0 + (v1 - v0) * j as f64 / steps;
349 let Ok(point) = face.surface.evaluate(u, v) else {
350 continue;
351 };
352 for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
353 minimum[axis] = minimum[axis].min(value);
354 maximum[axis] = maximum[axis].max(value);
355 }
356 samples.push((u, v, point));
357 }
358 }
359 Ok(Self {
360 face,
361 samples,
362 minimum,
363 maximum,
364 })
365 }
366
367 fn extent(&self) -> f64 {
369 (0..3)
370 .map(|axis| self.maximum[axis] - self.minimum[axis])
371 .fold(0.0f64, f64::max)
372 }
373
374 fn separation(&self, other: &Self) -> f64 {
376 let mut gap: f64 = 0.0;
377 for axis in 0..3 {
378 gap = gap.max(self.minimum[axis] - other.maximum[axis]);
379 gap = gap.max(other.minimum[axis] - self.maximum[axis]);
380 }
381 gap
382 }
383}
384
385fn faces_touch_with_codirected_normals(
390 probe_a: &PinchProbe<'_>,
391 probe_b: &PinchProbe<'_>,
392 contact: f64,
393) -> Result<bool, KernelRefusal> {
394 for (probe, other) in [(probe_a, probe_b), (probe_b, probe_a)] {
395 let source = probe.face;
396 let target = other.face;
397 for &(u, v, point) in &probe.samples {
398 let projection = project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?;
399 if projection.distance > contact {
400 continue;
401 }
402 let (Ok(source_normal), Ok(target_normal)) = (
405 source.surface.normal(u, v),
406 target.surface.normal(projection.u, projection.v),
407 ) else {
408 continue;
409 };
410 let source_outward = outward_normal(source_normal, source.same_sense);
411 let target_outward = outward_normal(target_normal, target.same_sense);
412 if source_outward.cross(target_outward).length() > PINCH_ANGULAR_TOLERANCE
413 || source_outward.dot(target_outward) <= 0.0
414 {
415 continue;
416 }
417 if parameter_point_in_face(source, Vec2 { x: u, y: v }, 1e-6).or_refuse(KernelStage::Validate, "parameter_point_in_face")?
418 == PolygonClass::Outside
419 || parameter_point_in_face(
420 target,
421 Vec2 {
422 x: projection.u,
423 y: projection.v,
424 },
425 1e-6,
426 ).or_refuse(KernelStage::Validate, "csg.boolean.mod")? == PolygonClass::Outside
427 {
428 continue;
429 }
430 if contact_separates_locally(source, target, u, v, contact)? {
431 return Ok(true);
432 }
433 }
434 }
435 Ok(false)
436}
437
438fn outward_normal(normal: Vec3, same_sense: bool) -> Vec3 {
439 if same_sense {
440 normal
441 } else {
442 normal.scale(-1.0)
443 }
444}
445
446fn contact_separates_locally(
452 source: &FaceRecord,
453 target: &FaceRecord,
454 u: f64,
455 v: f64,
456 contact: f64,
457) -> Result<bool, KernelRefusal> {
458 let [u0, u1] = source.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
459 let [v0, v1] = source.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
460 let step_u = (u1 - u0) * PINCH_SEPARATION_STEP;
461 let step_v = (v1 - v0) * PINCH_SEPARATION_STEP;
462 for (probe_u, probe_v) in [
463 ((u + step_u).min(u1), v),
464 ((u - step_u).max(u0), v),
465 (u, (v + step_v).min(v1)),
466 (u, (v - step_v).max(v0)),
467 ] {
468 if (probe_u - u).abs() < f64::EPSILON && (probe_v - v).abs() < f64::EPSILON {
469 continue;
470 }
471 let Ok(point) = source.surface.evaluate(probe_u, probe_v) else {
472 continue;
473 };
474 if project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?.distance > contact * 10.0 {
477 return Ok(true);
478 }
479 }
480 Ok(false)
481}
482
483fn perturbation_retry(
537 first: &BrepSolid,
538 second: &BrepSolid,
539 operation: BooleanOperation,
540 options: &BooleanOptions,
541) -> Result<BrepSolid, Vec<String>> {
542 const DIRECTIONS: [[f64; 3]; 4] = [
553 [0.4034, 0.7973, 0.4491],
554 [0.7973, 0.4491, 0.4034],
555 [0.4491, 0.4034, 0.7973],
556 [0.5774, -0.5774, 0.5774],
557 ];
558 const FRACTIONS: [f64; 5] = PERTURBATION_FRACTIONS;
564
565 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
566 let scale = crate::tolerance::solid_scale(first)
567 .max(crate::tolerance::solid_scale(second))
568 .max(1.0);
569
570 let _caller = crate::mass_caller("boolean.perturbation_rescue");
574 let va = solid_signed_volume(first).ok().map(f64::abs);
575 let vb = solid_signed_volume(second).ok().map(f64::abs);
576 let hold_identity = std::env::var("BREP_PERTURB_IDENTITY").as_deref() != Ok("0");
577 let hold_mirror = hold_identity && std::env::var("BREP_PERTURB_MIRROR").as_deref() != Ok("0");
580 let early_exit = std::env::var("BREP_MIRROR_EARLY_EXIT").as_deref() != Ok("0");
583 let mut trends = [MirrorTrend::default(); 4];
584 let mut identity: Option<Option<InclusionExclusion>> = None;
586 let mut refused = Vec::new();
587 let ladder = std::env::var("BREP_DEBUG_PERTURB_LADDER").is_ok();
591 let ladder_start = web_time::Instant::now();
592 if ladder {
593 let measured = inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
594 match &measured {
595 Some(identity) => eprintln!(
596 "[ladder] identity {} = {:.9} from the exact lane's {} ({:.9}); V(first) {:?} V(second) {:?}",
597 identity.formula, identity.expected, identity.reading, identity.read, va, vb
598 ),
599 None => eprintln!("[ladder] no identity: no exact-lane boolean of these operands clears in either order"),
600 }
601 identity = Some(measured);
602 }
603 let rung_miss = |identity: &Option<Option<InclusionExclusion>>, candidate: &BrepSolid| -> String {
604 let volume = body_volume(candidate);
605 match (identity, volume) {
606 (Some(Some(identity)), Some(volume)) => format!(
607 "volume {volume:.9} miss {:+.3e} bar {:.3e}",
608 volume - identity.expected,
609 crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * identity.expected.abs())
610 ),
611 (_, Some(volume)) => format!("volume {volume:.9}"),
612 _ => "no volume".to_string(),
613 }
614 };
615
616 for &fraction in &FRACTIONS {
617 let magnitude = scale * fraction;
618 for (direction, dir) in DIRECTIONS.iter().enumerate() {
619 if trends[direction].closed {
620 if ladder {
621 eprintln!("[ladder] dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}: not tried, its direction is closed");
622 }
623 continue;
624 }
625 let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
626 let translate = match AffineTransform::new([
627 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,
631 ]) {
632 Ok(transform) => transform,
633 Err(_) => continue,
634 };
635 let moved = match crate::transform_brep(second, translate, false) {
636 Ok(solid) => solid,
637 Err(_) => continue,
638 };
639 let rung = format!("dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}");
640 let rung_start = web_time::Instant::now();
641 let candidate =
642 match boolean_operation_with_diagnostics(first, &moved, operation, options) {
643 Ok(outcome) => outcome.value,
644 Err(error) => {
645 if ladder {
646 eprintln!("[ladder] {rung}: exact lane refused {:?}", error.class);
647 }
648 continue;
649 }
650 };
651 let built = rung_start.elapsed();
652 if candidate.shells.is_empty() {
654 if ladder {
655 eprintln!("[ladder] {rung}: empty");
656 }
657 continue;
658 }
659 if let (Some(va), Some(vb)) = (va, vb) {
664 if let Ok(vr) = solid_signed_volume(&candidate) {
665 if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
666 if ladder {
667 eprintln!("[ladder] {rung}: outside the CSG bound, {}", rung_miss(&identity, &candidate));
668 }
669 continue;
670 }
671 }
672 }
673 let bounded = rung_start.elapsed();
678 if hold_identity {
680 let identity = identity.get_or_insert_with(|| {
681 let identity =
682 inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
683 if debug && identity.is_none() {
684 eprintln!(
685 "[perturb] no exact-lane boolean of these operands clears in either \
686 order; no identity to hold"
687 );
688 }
689 identity
690 });
691 if let Some(identity) = identity {
692 let expected = identity.expected;
693 let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * expected.abs());
694 let volume = body_volume(&candidate);
695 let miss = volume.map(|volume| (volume - expected).abs());
696 if !miss.is_some_and(|miss| miss <= bar) {
697 let sentence = format!(
698 "perturbation rung dir={dir:?} fraction={fraction:.1e} \
699 (offset {magnitude:.3e}) refused: its body reads {} against the \
700 inclusion–exclusion identity {} = {expected:.9} (the exact lane's \
701 {} reads {:.9}), a miss of {} beyond the volume drift bar {bar:.3e}",
702 volume.map_or("no volume".to_string(), |volume| format!("{volume:.9}")),
703 identity.formula,
704 identity.reading,
705 identity.read,
706 miss.map_or("unmeasured".to_string(), |miss| format!("{miss:.3e}")),
707 );
708 if debug {
709 eprintln!("[perturb] {sentence}");
710 }
711 if ladder {
712 eprintln!(
713 "[ladder] {rung}: refused by the identity, volume {} miss {} bar {bar:.3e}",
714 volume.map_or("none".to_string(), |volume| format!("{volume:.9}")),
715 volume.map_or("unmeasured".to_string(), |volume| format!("{:+.3e}", volume - expected)),
716 );
717 }
718 refused.push(sentence);
719 continue;
720 }
721 } else if hold_mirror {
722 let mirror_start = web_time::Instant::now();
725 let mirrored = AffineTransform::new([
726 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,
730 ])
731 .ok()
732 .and_then(|transform| crate::transform_brep(second, transform, false).ok());
733 let mirror = mirrored.as_ref().and_then(|mirrored| {
734 inclusion_exclusion_volume(first, mirrored, operation, options, va, vb, true)
735 });
736 let mirror_time = mirror_start.elapsed();
737 let volume = body_volume(&candidate);
738 let mut measured = None;
739 let verdict = match (&mirror, volume) {
740 (Some(mirror), Some(volume)) => {
741 let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * mirror.expected.abs());
742 let miss = 0.5 * (volume - mirror.expected).abs();
743 measured = Some((0.5 * (volume - mirror.expected), bar));
744 if ladder {
745 eprintln!(
746 "[ladder] {rung}: mirror at −offset reads {} = {:.9} (the mirrored {} {:.9}); body {volume:.9}, first-order translation error {:+.3e} bar {bar:.3e}",
747 mirror.formula, mirror.expected, mirror.reading, mirror.read, 0.5 * (volume - mirror.expected)
748 );
749 }
750 (miss > bar).then(|| format!(
751 "perturbation rung dir={dir:?} fraction={fraction:.1e} \
752 (offset {magnitude:.3e}) refused: no exact-lane boolean of the \
753 operands clears, so the rung is held to its mirror: its body reads \
754 {volume:.9} with the second operand moved +offset and {} = {:.9} \
755 with it moved −offset (the mirrored {} reads {:.9}), a first-order \
756 translation error of {miss:.3e} beyond the volume drift bar {bar:.3e}",
757 mirror.formula, mirror.expected, mirror.reading, mirror.read
758 ))
759 }
760 _ => Some(format!(
761 "perturbation rung dir={dir:?} fraction={fraction:.1e} \
762 (offset {magnitude:.3e}) refused: no exact-lane boolean of the operands \
763 clears, and none clears with the second operand moved −offset either, \
764 so nothing checks the rung's body{}",
765 volume.map_or(String::new(), |volume| format!(" ({volume:.9})"))
766 )),
767 };
768 match verdict {
769 None => trends[direction].cleared(),
770 Some(sentence) => {
771 if debug {
772 eprintln!("[perturb] {sentence}");
773 }
774 if ladder {
775 eprintln!(
776 "[ladder] {rung}: refused by its mirror (rung {:.0} ms: built {:.0}, bound {:.0}, mirror {:.0} ms; ladder {:.1} s)",
777 rung_start.elapsed().as_secs_f64() * 1e3,
778 built.as_secs_f64() * 1e3,
779 (bounded - built).as_secs_f64() * 1e3,
780 mirror_time.as_secs_f64() * 1e3,
781 ladder_start.elapsed().as_secs_f64()
782 );
783 }
784 refused.push(sentence);
785 let closure = measured.and_then(|(error, bar)| {
788 let earlier = trends[direction].refused(magnitude, error, early_exit)?;
789 Some((earlier, error, bar))
790 });
791 if let Some(((earlier_offset, earlier_error), error, bar)) = closure {
792 let closed = format!(
793 "perturbation direction dir={dir:?} closed after offset {magnitude:.3e}: its \
794 rungs' first-order translation errors read {earlier_error:+.3e} at offset \
795 {earlier_offset:.3e} and {error:+.3e} at {magnitude:.3e}, one sign and \
796 growing, so no larger offset along it comes back to the volume drift bar \
797 {bar:.3e}"
798 );
799 if debug {
800 eprintln!("[perturb] {closed}");
801 }
802 if ladder {
803 eprintln!("[ladder] {closed}");
804 }
805 refused.push(closed);
806 }
807 continue;
808 }
809 }
810 }
811 }
812 match crate::oracle::boolean_semantic_disagreement(
816 first, second, operation, &candidate, 400,
817 ) {
818 Ok(report) if report.considered >= 30 && !report.is_flagged() => {
819 if ladder {
820 eprintln!(
821 "[ladder] {rung}: accepted (oracle {} considered, rate {:.4}), {}",
822 report.considered, report.disagreement_rate, rung_miss(&identity, &candidate)
823 );
824 }
825 if debug {
826 eprintln!(
827 "[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
828 magnitude={magnitude:.3e} offset={offset:?} \
829 (oracle considered={} rate={:.4})",
830 report.considered, report.disagreement_rate
831 );
832 }
833 return Ok(candidate);
834 }
835 report => {
836 if ladder {
837 let verdict = match &report {
838 Ok(report) => format!("oracle {} considered, rate {:.4}", report.considered, report.disagreement_rate),
839 Err(error) => format!("oracle error {error:?}"),
840 };
841 eprintln!("[ladder] {rung}: refused by the oracle ({verdict}), {}", rung_miss(&identity, &candidate));
842 }
843 continue;
844 }
845 }
846 }
847 }
848 if debug {
849 eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
850 }
851 Err(refused)
852}
853
854const PERTURBATION_FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
857
858#[derive(Clone, Copy, Default)]
882struct MirrorTrend {
883 last: Option<(f64, f64)>,
885 closed: bool,
886}
887
888impl MirrorTrend {
889 fn refused(&mut self, offset: f64, error: f64, close: bool) -> Option<(f64, f64)> {
894 let earlier = self
895 .last
896 .filter(|&(_, earlier)| earlier.signum() == error.signum() && error.abs() >= earlier.abs())
897 .filter(|_| close);
898 self.last = Some((offset, error));
899 self.closed |= earlier.is_some();
900 earlier
901 }
902
903 fn cleared(&mut self) {
905 self.last = None;
906 }
907}
908
909fn one_use_edges(solid: &BrepSolid) -> usize {
911 let mut uses: HashMap<u64, usize> = HashMap::default();
912 for coedge in solid
913 .shells
914 .iter()
915 .flat_map(|shell| &shell.faces)
916 .flat_map(|face| &face.loops)
917 .flat_map(|loop_record| &loop_record.coedges)
918 {
919 *uses.entry(coedge.edge_id).or_default() += 1;
920 }
921 solid
922 .edges
923 .iter()
924 .filter(|edge| !edge.degenerate && uses.get(&edge.id) == Some(&1))
925 .count()
926}
927
928fn body_volume(solid: &BrepSolid) -> Option<f64> {
930 if solid.shells.is_empty() {
931 return Some(0.0);
932 }
933 solid_signed_volume(solid).ok().map(f64::abs)
934}
935
936struct InclusionExclusion {
939 formula: &'static str,
942 reading: &'static str,
944 read: f64,
946 expected: f64,
948}
949
950fn inclusion_exclusion_volume(
961 first: &BrepSolid,
962 second: &BrepSolid,
963 operation: BooleanOperation,
964 options: &BooleanOptions,
965 va: Option<f64>,
966 vb: Option<f64>,
967 own_first: bool,
968) -> Option<InclusionExclusion> {
969 use BooleanOperation::{Intersect, Subtract, Union};
970 let (va, vb) = (va?, vb?);
971 let own = match operation {
972 Intersect => (Intersect, false, "first ∩ second"),
973 Union => (Union, false, "first ∪ second"),
974 Subtract => (Subtract, false, "first − second"),
975 };
976 let readings = [
977 (Intersect, false, "first ∩ second"),
978 (Intersect, true, "second ∩ first"),
979 (Union, false, "first ∪ second"),
980 (Union, true, "second ∪ first"),
981 (Subtract, false, "first − second"),
982 (Subtract, true, "second − first"),
983 ];
984 let own_reading = own_first.then_some(own);
985 let leading = usize::from(own_first);
986 for (index, (other, swapped, reading)) in own_reading.into_iter().chain(readings).enumerate() {
987 if index >= leading && !swapped && std::mem::discriminant(&other) == std::mem::discriminant(&operation) {
988 continue;
989 }
990 let (x, y) = if swapped { (second, first) } else { (first, second) };
991 let Some(read) = boolean_operation_with_diagnostics(x, y, other, options)
992 .ok()
993 .and_then(|outcome| body_volume(&outcome.value))
994 else {
995 continue;
996 };
997 let intersection = match (other, swapped) {
998 (Intersect, _) => read,
999 (Union, _) => va + vb - read,
1000 (Subtract, false) => va - read,
1001 (Subtract, true) => vb - read,
1002 };
1003 let (formula, expected) = match operation {
1004 Intersect => ("V(first ∩ second)", intersection),
1005 Union => ("V(first) + V(second) − V(first ∩ second)", va + vb - intersection),
1006 Subtract => ("V(first) − V(first ∩ second)", va - intersection),
1007 };
1008 return Some(InclusionExclusion { formula, reading, read, expected });
1009 }
1010 None
1011}
1012
1013fn planar_operand_off_its_plane(
1022 first: &BrepSolid,
1023 second: &BrepSolid,
1024 options: &BooleanOptions,
1025) -> Option<(u8, u64, f64, f64)> {
1026 let policy = options
1027 .tolerances
1028 .clone()
1029 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
1030 let weld = crate::tolerance::assembler_weld(policy.model);
1031 let mut worst: Option<(u8, u64, f64)> = None;
1032 for (operand, solid) in [(0u8, first), (1u8, second)] {
1033 let vertex_point = |id: u64| solid.vertices.iter().find(|v| v.id == id).map(|v| v.point);
1034 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
1035 let Some(crate::AnalyticSurface::Plane {
1036 origin,
1037 u_dir,
1038 v_dir,
1039 ..
1040 }) = face.surface.analytic()
1041 else {
1042 continue;
1043 };
1044 let [loop_record] = face.loops.as_slice() else {
1045 continue;
1046 };
1047 let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
1048 continue;
1049 };
1050 let mut off_plane = 0.0f64;
1051 let mut straight = true;
1052 for coedge in &loop_record.coedges {
1053 let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
1054 straight = false;
1055 break;
1056 };
1057 let (Some(start), Some(end)) =
1058 (vertex_point(edge.start_vertex_id), vertex_point(edge.end_vertex_id))
1059 else {
1060 straight = false;
1061 break;
1062 };
1063 let chord = end.sub(start);
1064 let length = chord.length();
1065 if edge.degenerate || length <= weld {
1066 straight = false;
1067 break;
1068 }
1069 let direction = chord.scale(1.0 / length);
1070 for fraction in [0.25, 0.5, 0.75] {
1071 let Ok(point) = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction) else {
1072 straight = false;
1073 break;
1074 };
1075 let delta = point.sub(start);
1076 let along = delta.dot(direction);
1077 if delta.sub(direction.scale(along)).length() > weld
1078 || along < -weld
1079 || along > length + weld
1080 {
1081 straight = false;
1082 break;
1083 }
1084 }
1085 if !straight {
1086 break;
1087 }
1088 off_plane = off_plane.max(start.sub(*origin).dot(normal).abs());
1089 }
1090 if straight && off_plane > weld && worst.is_none_or(|(_, _, gap)| off_plane > gap) {
1091 worst = Some((operand, face.id, off_plane));
1092 }
1093 }
1094 }
1095 worst.map(|(operand, face_id, gap)| (operand, face_id, gap, weld))
1096}
1097
1098fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
1101 let slack = 1e-2 * (va + vb).max(1.0);
1102 match operation {
1103 BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
1105 BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
1107 BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
1109 }
1110}
1111
1112fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), KernelRefusal> {
1139 fragment.same_sense = !fragment.same_sense;
1140 for loop_record in &mut fragment.loops {
1141 loop_record.coedges.reverse();
1142 for coedge in &mut loop_record.coedges {
1143 coedge.forward = !coedge.forward;
1144 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Validate, "reversed")?;
1145 }
1146 }
1147 Ok(())
1148}
1149
1150fn build_nary_imprint(
1157 operands: &[BrepSolid],
1158 options: &ImprintOptions,
1159) -> Result<ImprintResultRecord, KernelRefusal> {
1160 let mut section_evidence = false;
1161 let mut vertices: Vec<ImprintVertex> = Vec::new();
1162 let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
1163 let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
1164 let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
1165 let mut next_vertex_id: u64 = 1;
1166 let mut next_piece_id: u64 = 1;
1167
1168 for i in 0..operands.len() {
1169 for j in (i + 1)..operands.len() {
1170 let pair = build_imprints(&operands[i], &operands[j], options)?;
1171 let remap = |operand: u8| -> u8 {
1172 if operand == 0 {
1173 i as u8
1174 } else {
1175 j as u8
1176 }
1177 };
1178
1179 section_evidence = section_evidence || pair.section_evidence;
1180 let mut vertex_map: HashMap<u64, u64> = HashMap::default();
1181 for vertex in &pair.vertices {
1182 let global = next_vertex_id;
1183 next_vertex_id += 1;
1184 vertex_map.insert(vertex.id, global);
1185 vertices.push(ImprintVertex {
1186 id: global,
1187 point: vertex.point,
1188 });
1189 }
1190
1191 let mut piece_map: HashMap<u64, u64> = HashMap::default();
1192 for piece in &pair.pieces {
1193 let global = next_piece_id;
1194 next_piece_id += 1;
1195 piece_map.insert(piece.id, global);
1196 let mut mapped = piece.clone();
1197 mapped.id = global;
1198 mapped.start_vertex_id = *vertex_map
1199 .get(&piece.start_vertex_id)
1200 .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
1201 mapped.end_vertex_id = *vertex_map
1202 .get(&piece.end_vertex_id)
1203 .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
1204 for pcurve in &mut mapped.pcurves {
1205 pcurve.operand = remap(pcurve.operand);
1206 }
1207 mapped.support_faces = [
1208 FaceKey {
1209 operand: remap(piece.support_faces[0].operand),
1210 face_id: piece.support_faces[0].face_id,
1211 },
1212 FaceKey {
1213 operand: remap(piece.support_faces[1].operand),
1214 face_id: piece.support_faces[1].face_id,
1215 },
1216 ];
1217 pieces.push(mapped);
1218 }
1219
1220 for entry in &pair.by_face {
1221 let list = by_face
1222 .entry((remap(entry.operand), entry.face_id))
1223 .or_default();
1224 for piece_id in &entry.piece_ids {
1225 let global = *piece_map.get(piece_id).ok_or_else(|| {
1226 KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: by_face references unknown piece")
1227 })?;
1228 list.push(global);
1229 }
1230 }
1231
1232 for split in &pair.edge_splits {
1233 edge_splits
1234 .entry((remap(split.operand), split.edge_id))
1235 .or_default()
1236 .extend(split.parameters.iter().copied());
1237 }
1238 }
1239 }
1240
1241 Ok(ImprintResultRecord {
1242 vertices,
1243 pieces,
1244 tangent_nodes: Vec::new(),
1245 barrier_edges: Vec::new(),
1246 section_evidence,
1247 cosurface_pairs: Vec::new(),
1248 by_face: by_face
1249 .into_iter()
1250 .map(|((operand, face_id), piece_ids)| FaceImprints {
1251 operand,
1252 face_id,
1253 piece_ids,
1254 })
1255 .collect(),
1256 edge_splits: edge_splits
1257 .into_iter()
1258 .map(|((operand, edge_id), parameters)| EdgeSplitRecord {
1259 operand,
1260 edge_id,
1261 parameters,
1262 })
1263 .collect(),
1264 })
1265}
1266
1267pub fn boolean_operation_nary(
1278 operands: &[BrepSolid],
1279 operation: BooleanOperation,
1280) -> Result<BrepSolid, KernelRefusal> {
1281 if operands.is_empty() {
1282 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: no operands provided"));
1283 }
1284 if operands.len() == 1 {
1285 return Ok(operands[0].clone());
1286 }
1287 if operands.len() > 255 {
1288 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: at most 255 operands supported"));
1289 }
1290 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
1291 let options = BooleanOptions::default();
1292
1293 let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
1297 for operand in &operands[1..] {
1298 let candidate = KernelTolerances::for_solid(operand, options.tolerance);
1299 if candidate.sew_search > policy.sew_search {
1300 policy = candidate;
1301 }
1302 }
1303 policy.check().or_input(KernelStage::Collect, "tolerances")?;
1304 let tolerance = policy.model;
1305
1306 let mut healed = operands.to_vec();
1309 for operand in &mut healed {
1310 crate::heal::heal_operands(operand, &policy)?;
1311 normalize_operand_band_seams(operand)?;
1314 }
1315
1316 let mut imprint_options = options.imprint.clone();
1318 imprint_options.tolerance = tolerance;
1319 let imprint = build_nary_imprint(&healed, &imprint_options)?;
1320
1321 let mut split = Vec::with_capacity(healed.len());
1323 for (index, operand) in healed.iter().enumerate() {
1324 split.push(apply_edge_splits(operand, index as u8, &imprint)?);
1325 }
1326 let mut fragments = Vec::with_capacity(split.len());
1327 for (index, solid) in split.iter().enumerate() {
1328 fragments.push(fragment_solid(solid, index as u8, &imprint)?);
1329 }
1330
1331 let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
1333
1334 let solids: HashMap<u8, &BrepSolid> = split
1336 .iter()
1337 .enumerate()
1338 .map(|(index, solid)| (index as u8, solid))
1339 .collect();
1340 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
1341 let solid = if options.merge_coplanar_faces
1342 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
1343 {
1344 let merged = merge_same_surface_faces_excluding(
1345 &solid,
1346 tolerance,
1347 &options.keep_unmerged_name_substrs,
1348 )?;
1349 merge_curve_continuation_edges(&merged, tolerance).or_refuse(KernelStage::Validate, "merge_curve_continuation_edges")?
1350 } else {
1351 solid
1352 };
1353 let validation = solid.validate_detailed(&policy);
1354 if !validation.issues.is_empty() {
1355 return Err(KernelRefusal::new(
1356 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
1357 KernelStage::Validate,
1358 format!(
1359 "boolean_operation_nary: invalid result: {:?}",
1360 validation.issues
1361 )));
1362 }
1363 if debug {
1364 if let Ok(report) =
1365 crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
1366 {
1367 if report.is_flagged() {
1368 eprintln!(
1369 "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
1370 report.disagreement_rate,
1371 report.disagreements.len(),
1372 report.considered,
1373 report.sample_disagreement()
1374 );
1375 }
1376 }
1377 }
1378 Ok(solid)
1379}
1380
1381fn attribute_to_tangent_node(error: KernelRefusal, nodes: &[Vec3]) -> KernelRefusal {
1406 let Some(node) = nodes.first() else {
1407 return error;
1408 };
1409 if !error.class.perturbation_eligible()
1410 || matches!(error.class, RefusalClass::TangentNodeSingularity)
1411 {
1412 return error;
1413 }
1414 KernelRefusal::new(
1415 RefusalClass::TangentNodeSingularity,
1416 error.stage,
1417 format!(
1418 "boolean: unsupported singular/tangent-node surface intersection \
1419 at the tangent node at ({:.6},{:.6},{:.6}): the section through it did not \
1420 assemble into a closed shell",
1421 node.x, node.y, node.z
1422 ),
1423 )
1424}
1425
1426pub fn boolean_operation_with_diagnostics(
1427 first: &BrepSolid,
1428 second: &BrepSolid,
1429 operation: BooleanOperation,
1430 options: &BooleanOptions,
1431) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1432 let mut tangent_nodes = Vec::new();
1433 let mut outcome = boolean_pipeline(first, second, operation, options, &mut tangent_nodes)
1434 .map_err(|error| attribute_to_tangent_node(error, &tangent_nodes))?;
1435 outcome.value = crate::accept_sound_against(&[first, second], outcome.value, "boolean")?;
1446 Ok(outcome)
1447}
1448
1449fn profile_stage(code: &str, ms: f64) {
1453 if std::env::var("BREP_PROFILE").is_ok() {
1454 eprintln!("boolean.{code}={ms:.2}");
1455 }
1456}
1457
1458struct BooleanStageDump {
1463 directory: std::path::PathBuf,
1464 serial: usize,
1465}
1466
1467impl BooleanStageDump {
1468 fn open() -> Option<Self> {
1469 static SERIAL: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1470 let directory = std::env::var("BREP_BOOLEAN_DUMP").ok().filter(|value| !value.is_empty())?;
1471 let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1472 Some(Self { directory: directory.into(), serial })
1473 }
1474
1475 fn write<T: Serialize + ?Sized>(&self, stage: &str, value: &T) {
1476 let path = self.directory.join(format!("{:03}-{stage}.json", self.serial));
1477 match serde_json::to_string(value) {
1478 Ok(text) => {
1479 if let Err(error) = std::fs::write(&path, text) {
1480 eprintln!("BREP_BOOLEAN_DUMP {}: {error}", path.display());
1481 }
1482 }
1483 Err(error) => eprintln!("BREP_BOOLEAN_DUMP {stage}: {error}"),
1484 }
1485 }
1486}
1487
1488fn boolean_policy(
1490 first: &BrepSolid,
1491 second: &BrepSolid,
1492 options: &BooleanOptions,
1493) -> Result<KernelTolerances, KernelRefusal> {
1494 let policy = options
1495 .tolerances
1496 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
1497 policy.check().or_input(KernelStage::Collect, "tolerances")?;
1498 Ok(policy)
1499}
1500
1501fn prepared_operands(
1504 first: &BrepSolid,
1505 second: &BrepSolid,
1506 policy: &KernelTolerances,
1507) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
1508 let stage_started = Instant::now();
1517 let mut first_owned = first.clone();
1518 let mut second_owned = second.clone();
1519 crate::heal::heal_operands(&mut first_owned, policy)?;
1520 crate::heal::heal_operands(&mut second_owned, policy)?;
1521 profile_stage("heal_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
1522 normalize_operand_band_seams(&mut first_owned)?;
1527 normalize_operand_band_seams(&mut second_owned)?;
1528 Ok((first_owned, second_owned))
1529}
1530
1531fn boolean_imprint_options(options: &BooleanOptions, tolerance: f64) -> ImprintOptions {
1532 let mut imprint_options = options.imprint.clone();
1533 imprint_options.tolerance = tolerance;
1534 imprint_options
1535}
1536
1537pub fn boolean_split_operands(
1547 first: &BrepSolid,
1548 second: &BrepSolid,
1549 options: &BooleanOptions,
1550) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
1551 let policy = boolean_policy(first, second, options)?;
1552 let (first, second) = prepared_operands(first, second, &policy)?;
1553 let imprint = build_imprints(&first, &second, &boolean_imprint_options(options, policy.model))?;
1554 let (split_first, _) = apply_edge_splits_with_map(&first, 0, &imprint)?;
1555 let (split_second, _) = apply_edge_splits_with_map(&second, 1, &imprint)?;
1556 Ok((split_first, split_second))
1557}
1558
1559fn boolean_pipeline(
1560 first: &BrepSolid,
1561 second: &BrepSolid,
1562 operation: BooleanOperation,
1563 options: &BooleanOptions,
1564 tangent_nodes: &mut Vec<Vec3>,
1565) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1566 let replaced_operands = operand_state::replace_operands(first, second, operation, options);
1567 let (first, second) = match &replaced_operands {
1568 Some((first, second)) => (first, second),
1569 None => (first, second),
1570 };
1571 let policy = boolean_policy(first, second, options)?;
1572 let tolerance = policy.model;
1573 let (first_owned, second_owned) = prepared_operands(first, second, &policy)?;
1574 let first = &first_owned;
1575 let second = &second_owned;
1576
1577 let mut diagnostics = KernelDiagnostics::default();
1578 let fit_scope = crate::PcurveFitScope::open();
1583 let polyline_scope = crate::PolylineFitScope::open();
1587 let operation_started = Instant::now();
1588 diagnostics.count_n(
1589 "collect.faces",
1590 first
1591 .shells
1592 .iter()
1593 .chain(&second.shells)
1594 .map(|shell| shell.faces.len() as u64)
1595 .sum(),
1596 );
1597 diagnostics.measure_max("tolerance.model", policy.model);
1598 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
1599
1600 let imprint_options = boolean_imprint_options(options, tolerance);
1601 let stage_started = Instant::now();
1602 let imprint = build_imprints(first, second, &imprint_options)?;
1603 tangent_nodes.extend_from_slice(&imprint.tangent_nodes);
1604 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
1605 for piece in &imprint.pieces {
1606 let start = piece.curve.evaluate(piece.t0);
1607 let end = piece.curve.evaluate(piece.t1);
1608 eprintln!(
1609 "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
1610 piece.id, piece.support_faces, piece.t0, piece.t1, start, end
1611 );
1612 }
1613 for entry in &imprint.by_face {
1614 eprintln!(
1615 "by_face operand={} face={} pieces={:?}",
1616 entry.operand, entry.face_id, entry.piece_ids
1617 );
1618 }
1619 for split in &imprint.edge_splits {
1620 eprintln!(
1621 "edge_split operand={} edge={} params={:?}",
1622 split.operand, split.edge_id, split.parameters
1623 );
1624 }
1625 }
1626 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1627 profile_stage("imprint_ms", stage_ms);
1628 diagnostics.measure_max("timing.imprint_ms", stage_ms);
1629 diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
1630 diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
1631 diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
1632 let stage_started = Instant::now();
1633 let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
1634 let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
1635 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1636 profile_stage("edge_split_ms", stage_ms);
1637 diagnostics.measure_max("timing.edge_split_ms", stage_ms);
1638 let stage_started = Instant::now();
1639 crate::classification::classify_profile_begin();
1640 let fragments_a = fragment_solid(&split_first, 0, &imprint)?;
1641 let fragments_b = fragment_solid(&split_second, 1, &imprint)?;
1642 crate::classification::classify_profile_report("fragment");
1643 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1644 profile_stage("fragment_ms", stage_ms);
1645 diagnostics.measure_max("timing.fragment_ms", stage_ms);
1646 diagnostics.count_n(
1647 "fragment.candidates",
1648 (fragments_a.len() + fragments_b.len()) as u64,
1649 );
1650 let a_surface_samples: Vec<Vec3> = fragments_a
1655 .iter()
1656 .flat_map(|fragment| {
1657 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
1658 })
1659 .collect();
1660 let b_surface_samples: Vec<Vec3> = fragments_b
1661 .iter()
1662 .flat_map(|fragment| {
1663 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
1664 })
1665 .collect();
1666 let stage_started = Instant::now();
1667 let mut barrier_edges: HashSet<(u8, u64)> = imprint
1668 .pieces
1669 .iter()
1670 .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
1671 .collect();
1672 barrier_edges.extend(imprint.barrier_edges.iter().copied());
1673 for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
1679 let minted: Vec<(u8, u64)> = barrier_edges
1680 .iter()
1681 .filter(|(barrier_operand, _)| *barrier_operand == operand)
1682 .filter_map(|(_, edge_id)| split_map.get(edge_id))
1683 .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
1684 .collect();
1685 barrier_edges.extend(minted);
1686 }
1687 crate::classification::classify_profile_begin();
1688 let coincident = coincident_pairs(
1689 &fragments_a,
1690 &fragments_b,
1691 first,
1692 second,
1693 &imprint.cosurface_pairs,
1694 tolerance,
1695 )?;
1696 let mut selected = select_fragments(
1697 fragments_a,
1698 fragments_b,
1699 first,
1700 second,
1701 operation,
1702 tolerance,
1703 &barrier_edges,
1704 &imprint.cosurface_pairs,
1705 &coincident,
1706 )?;
1707 let resewn = resew_coincident_rims(
1708 &mut selected,
1709 [&split_first, &split_second],
1710 [first, second],
1711 &imprint.cosurface_pairs,
1712 operation,
1713 tolerance,
1714 )?;
1715 diagnostics.count_n("select.coincident_pairs", coincident.len() as u64);
1716 diagnostics.count_n("select.rims_resewn", resewn as u64);
1717 crate::classification::classify_profile_report("select");
1718 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1719 profile_stage("select_ms", stage_ms);
1720 diagnostics.measure_max("timing.select_ms", stage_ms);
1721 diagnostics.count_n("select.fragments", selected.len() as u64);
1722 if selected.is_empty()
1744 && !imprint.section_evidence
1745 && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
1746 {
1747 let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
1748 for &sample in samples {
1749 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::In {
1750 return Ok(true);
1751 }
1752 }
1753 Ok(false)
1754 };
1755 let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
1756 for &sample in samples {
1757 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::Out {
1758 return Ok(true);
1759 }
1760 }
1761 Ok(false)
1762 };
1763 let legitimate = match operation {
1764 BooleanOperation::Intersect => {
1765 !strictly_inside(&a_surface_samples, second)?
1766 && !strictly_inside(&b_surface_samples, first)?
1767 }
1768 BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
1769 BooleanOperation::Union => false,
1770 };
1771 if legitimate {
1772 diagnostics.event(
1773 DiagnosticSeverity::Info,
1774 KernelStage::Select,
1775 "boolean.empty_result",
1776 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
1777 );
1778 fit_scope.close().report_into(&mut diagnostics);
1779 polyline_scope.close().report_into(&mut diagnostics);
1780 diagnostics.measure_max(
1781 "timing.total_ms",
1782 operation_started.elapsed().as_secs_f64() * 1_000.0,
1783 );
1784 return Ok(KernelOutcome {
1785 value: BrepSolid {
1786 mass_properties_cache: Default::default(),
1787 id: 0,
1788 vertices: Vec::new(),
1789 edges: Vec::new(),
1790 shells: Vec::new(),
1791 genus: 0,
1792 },
1793 diagnostics,
1794 });
1795 }
1796 }
1797 let solids = [(0, &split_first), (1, &split_second)]
1798 .into_iter()
1799 .collect::<HashMap<_, _>>();
1800 let dump = BooleanStageDump::open();
1801 if let Some(dump) = &dump {
1802 dump.write("first", &split_first);
1803 dump.write("second", &split_second);
1804 dump.write("selected", &selected);
1805 }
1806 let stage_started = Instant::now();
1807 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
1808 if let Some(dump) = &dump {
1809 dump.write("assembled", &solid);
1810 }
1811 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1812 profile_stage("assemble_ms", stage_ms);
1813 diagnostics.measure_max("timing.assemble_ms", stage_ms);
1814 let stage_started = Instant::now();
1815 if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
1816 let issues = solid.validate();
1817 eprintln!(
1818 "pre-merge validation: {} issue(s){}",
1819 issues.len(),
1820 issues
1821 .first()
1822 .map(|issue| format!(" — first: {}", issue.message))
1823 .unwrap_or_default()
1824 );
1825 }
1826 let solid = if options.merge_coplanar_faces
1827 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
1828 {
1829 let merged = merge_same_surface_faces_excluding(
1830 &solid,
1831 tolerance,
1832 &options.keep_unmerged_name_substrs,
1833 )?;
1834 merge_curve_continuation_edges(&merged, tolerance).or_refuse(KernelStage::Validate, "merge_curve_continuation_edges")?
1839 } else {
1840 solid
1841 };
1842 if let Some(dump) = &dump {
1843 dump.write("merged", &solid);
1844 }
1845 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1846 profile_stage("face_merge_ms", stage_ms);
1847 diagnostics.measure_max("timing.face_merge_ms", stage_ms);
1848 let validation = solid.validate_detailed(&policy);
1849 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
1850 diagnostics.count_n(
1851 "validate.wire_warnings",
1852 validation.wire_warnings.len() as u64,
1853 );
1854 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
1855 for warning in &validation.wire_warnings {
1856 diagnostics.event(
1857 DiagnosticSeverity::Warning,
1858 KernelStage::Validate,
1859 "validate.uv_wire",
1860 warning.message.clone(),
1861 );
1862 }
1863 for issue in &validation.issues {
1864 diagnostics.event(
1865 DiagnosticSeverity::Error,
1866 KernelStage::Validate,
1867 "validate.brep",
1868 issue.message.clone(),
1869 );
1870 }
1871 fit_scope.close().report_into(&mut diagnostics);
1872 polyline_scope.close().report_into(&mut diagnostics);
1873 diagnostics.measure_max(
1874 "timing.total_ms",
1875 operation_started.elapsed().as_secs_f64() * 1_000.0,
1876 );
1877 if !validation.issues.is_empty() {
1878 return Err(KernelRefusal::new(
1879 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
1880 KernelStage::Validate,
1881 format!(
1882 "boolean_operation: invalid result: {:?}",
1883 validation.issues
1884 )));
1885 }
1886 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
1892 if let Ok(report) =
1893 crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
1894 {
1895 if report.is_flagged() {
1896 eprintln!(
1897 "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
1898 report.disagreement_rate,
1899 report.disagreements.len(),
1900 report.considered,
1901 report.sample_disagreement()
1902 );
1903 }
1904 }
1905 let fusable_band = crate::tolerance::assembler_weld(policy.model);
1906 let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
1907 if !fusables.is_empty() {
1908 eprintln!(
1909 "[oracle] {} residual fusable(s) after weld; sample {:?}",
1910 fusables.len(),
1911 fusables.first()
1912 );
1913 }
1914 }
1915 Ok(KernelOutcome {
1916 value: solid,
1917 diagnostics,
1918 })
1919}
1920