1use crate::arrangement::Vec2;
2use crate::classification::{classify_point, parameter_point_in_face, PolygonClass};
3use crate::fragment::{FaceFragmentRecord, FragmentEdgeSource};
4use crate::imprint::{
5 EdgeSplitRecord, FaceImprints, FaceKey, ImprintOptions, ImprintPieceRecord,
6 ImprintResultRecord, ImprintVertex,
7};
8use crate::tolerance::{assembler_weld, commit_weld};
9use crate::topology::{
10 adaptive_coedge_error, BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord,
11 ShellRecord, VertexRecord,
12};
13use crate::{
14 apply_edge_splits, apply_edge_splits_with_map, build_imprints, build_pcurve_on_surface,
15 build_pcurve_on_surface_range, classify_surface_pair,
16 fragment_solid, interpolate_curve, merge_curve_continuation_edges,
17 merge_same_surface_faces_excluding,
18 project_point_to_curve, project_point_to_surface, solid_signed_volume, AffineTransform,
19 DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage, KernelTolerances, NurbsCurve,
20 PointClass, SolidClassifier, SurfacePairRelation, Vec3,
21};
22use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
23use serde::{Deserialize, Serialize};
24use web_time::Instant;
25
26thread_local! {
27 pub(crate) static CONFORMANCE_REPAIRS: std::cell::Cell<u64> =
33 const { std::cell::Cell::new(0) };
34}
35
36#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
37#[serde(rename_all = "lowercase")]
38pub enum BooleanOperation {
39 Union,
40 Intersect,
41 Subtract,
42}
43
44#[derive(Clone, Debug, Deserialize)]
45pub struct BooleanOptions {
46 #[serde(default = "default_tolerance")]
47 pub tolerance: f64,
48 #[serde(default)]
51 pub tolerances: Option<KernelTolerances>,
52 #[serde(default)]
53 pub imprint: ImprintOptions,
54 #[serde(default = "default_true")]
57 pub merge_coplanar_faces: bool,
58 #[serde(default)]
68 pub keep_unmerged_name_substrs: Vec<String>,
69}
70
71fn default_tolerance() -> f64 {
72 1e-7
73}
74
75fn default_true() -> bool {
76 true
77}
78
79impl Default for BooleanOptions {
80 fn default() -> Self {
81 Self {
82 tolerance: default_tolerance(),
83 tolerances: None,
84 imprint: ImprintOptions::default(),
85 merge_coplanar_faces: true,
86 keep_unmerged_name_substrs: Vec::new(),
87 }
88 }
89}
90
91mod select;
92mod assemble;
93mod rim;
94#[cfg(test)]
95mod tests;
96
97use rim::*;
98use select::*;
99#[cfg(test)]
102use assemble::*;
103pub(crate) use assemble::{
104 assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
105 edge_interior_lies_on, finalize_assembled_solid,
106};
107#[allow(unused_imports)]
110pub(crate) use assemble::apply_assembly_heal_chain;
111
112pub fn boolean_operation(
113 first: &BrepSolid,
114 second: &BrepSolid,
115 operation: BooleanOperation,
116 options: &BooleanOptions,
117) -> Result<BrepSolid, String> {
118 match boolean_operation_with_diagnostics(first, second, operation, options) {
119 Ok(outcome) => Ok(outcome.value),
120 Err(error) => {
121 if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
129 || !is_degeneracy_error(&error)
130 {
131 return Err(error);
132 }
133 if matches!(operation, BooleanOperation::Subtract)
148 && std::env::var("BREP_PINCH_GATE").as_deref() != Ok("0")
149 && subtract_pinches_at_internal_tangency(first, second, options.tolerance)
150 .unwrap_or(false)
151 {
152 return Err(format!(
153 "{error}; internal tangency: the operands touch tangentially with \
154 co-directed normals, so the exact difference pinches to zero thickness \
155 (non-manifold, unrepresentable in a boundary model) — refusing rather \
156 than returning a perturbed sliver"
157 ));
158 }
159 match perturbation_retry(first, second, operation, options) {
160 Some(solid) => Ok(solid),
161 None => Err(error),
162 }
163 }
164 }
165}
166
167fn is_degeneracy_error(message: &str) -> bool {
171 message.contains("invalid topology")
172 || message.contains("non-integral genus")
173 || message.contains("non-positive volume")
174 || message.contains("open edges")
175 || message.contains("singular/tangent-node")
179}
180
181const PINCH_PROBE_STEPS: usize = 8;
186
187const PINCH_ANGULAR_TOLERANCE: f64 = 1e-4;
190
191const PINCH_SEPARATION_STEP: f64 = 1e-2;
195
196fn subtract_pinches_at_internal_tangency(
218 first: &BrepSolid,
219 second: &BrepSolid,
220 tolerance: f64,
221) -> Result<bool, String> {
222 let contact = (tolerance * 100.0).max(1e-12);
225 let probes_a = first
226 .shells
227 .iter()
228 .flat_map(|shell| &shell.faces)
229 .map(PinchProbe::of)
230 .collect::<Result<Vec<_>, _>>()?;
231 let probes_b = second
232 .shells
233 .iter()
234 .flat_map(|shell| &shell.faces)
235 .map(PinchProbe::of)
236 .collect::<Result<Vec<_>, _>>()?;
237 for probe_a in &probes_a {
238 for probe_b in &probes_b {
239 let pad = 1e-2 * probe_a.extent().max(probe_b.extent());
246 if probe_a.separation(probe_b) > contact + pad {
247 continue;
248 }
249 if classify_surface_pair(
256 &probe_a.face.surface,
257 &probe_b.face.surface,
258 tolerance,
259 PINCH_ANGULAR_TOLERANCE,
260 )?
261 .relation
262 == SurfacePairRelation::Cosurface
263 {
264 continue;
265 }
266 if faces_touch_with_codirected_normals(probe_a, probe_b, contact)? {
267 return Ok(true);
268 }
269 }
270 }
271 Ok(false)
272}
273
274struct PinchProbe<'a> {
277 face: &'a FaceRecord,
278 samples: Vec<(f64, f64, Vec3)>,
280 minimum: [f64; 3],
281 maximum: [f64; 3],
282}
283
284impl<'a> PinchProbe<'a> {
285 fn of(face: &'a FaceRecord) -> Result<Self, String> {
286 let [u0, u1] = face.surface.domain_u()?;
287 let [v0, v1] = face.surface.domain_v()?;
288 let steps = PINCH_PROBE_STEPS as f64;
289 let mut samples = Vec::with_capacity((PINCH_PROBE_STEPS + 1).pow(2));
290 let mut minimum = [f64::INFINITY; 3];
291 let mut maximum = [f64::NEG_INFINITY; 3];
292 for i in 0..=PINCH_PROBE_STEPS {
293 let u = u0 + (u1 - u0) * i as f64 / steps;
294 for j in 0..=PINCH_PROBE_STEPS {
295 let v = v0 + (v1 - v0) * j as f64 / steps;
296 let Ok(point) = face.surface.evaluate(u, v) else {
297 continue;
298 };
299 for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
300 minimum[axis] = minimum[axis].min(value);
301 maximum[axis] = maximum[axis].max(value);
302 }
303 samples.push((u, v, point));
304 }
305 }
306 Ok(Self {
307 face,
308 samples,
309 minimum,
310 maximum,
311 })
312 }
313
314 fn extent(&self) -> f64 {
316 (0..3)
317 .map(|axis| self.maximum[axis] - self.minimum[axis])
318 .fold(0.0f64, f64::max)
319 }
320
321 fn separation(&self, other: &Self) -> f64 {
323 let mut gap: f64 = 0.0;
324 for axis in 0..3 {
325 gap = gap.max(self.minimum[axis] - other.maximum[axis]);
326 gap = gap.max(other.minimum[axis] - self.maximum[axis]);
327 }
328 gap
329 }
330}
331
332fn faces_touch_with_codirected_normals(
337 probe_a: &PinchProbe<'_>,
338 probe_b: &PinchProbe<'_>,
339 contact: f64,
340) -> Result<bool, String> {
341 for (probe, other) in [(probe_a, probe_b), (probe_b, probe_a)] {
342 let source = probe.face;
343 let target = other.face;
344 for &(u, v, point) in &probe.samples {
345 let projection = project_point_to_surface(&target.surface, point)?;
346 if projection.distance > contact {
347 continue;
348 }
349 let (Ok(source_normal), Ok(target_normal)) = (
352 source.surface.normal(u, v),
353 target.surface.normal(projection.u, projection.v),
354 ) else {
355 continue;
356 };
357 let source_outward = outward_normal(source_normal, source.same_sense);
358 let target_outward = outward_normal(target_normal, target.same_sense);
359 if source_outward.cross(target_outward).length() > PINCH_ANGULAR_TOLERANCE
360 || source_outward.dot(target_outward) <= 0.0
361 {
362 continue;
363 }
364 if parameter_point_in_face(source, Vec2 { x: u, y: v }, 1e-6)?
365 == PolygonClass::Outside
366 || parameter_point_in_face(
367 target,
368 Vec2 {
369 x: projection.u,
370 y: projection.v,
371 },
372 1e-6,
373 )? == PolygonClass::Outside
374 {
375 continue;
376 }
377 if contact_separates_locally(source, target, u, v, contact)? {
378 return Ok(true);
379 }
380 }
381 }
382 Ok(false)
383}
384
385fn outward_normal(normal: Vec3, same_sense: bool) -> Vec3 {
386 if same_sense {
387 normal
388 } else {
389 normal.scale(-1.0)
390 }
391}
392
393fn contact_separates_locally(
399 source: &FaceRecord,
400 target: &FaceRecord,
401 u: f64,
402 v: f64,
403 contact: f64,
404) -> Result<bool, String> {
405 let [u0, u1] = source.surface.domain_u()?;
406 let [v0, v1] = source.surface.domain_v()?;
407 let step_u = (u1 - u0) * PINCH_SEPARATION_STEP;
408 let step_v = (v1 - v0) * PINCH_SEPARATION_STEP;
409 for (probe_u, probe_v) in [
410 ((u + step_u).min(u1), v),
411 ((u - step_u).max(u0), v),
412 (u, (v + step_v).min(v1)),
413 (u, (v - step_v).max(v0)),
414 ] {
415 if (probe_u - u).abs() < f64::EPSILON && (probe_v - v).abs() < f64::EPSILON {
416 continue;
417 }
418 let Ok(point) = source.surface.evaluate(probe_u, probe_v) else {
419 continue;
420 };
421 if project_point_to_surface(&target.surface, point)?.distance > contact * 10.0 {
424 return Ok(true);
425 }
426 }
427 Ok(false)
428}
429
430fn perturbation_retry(
445 first: &BrepSolid,
446 second: &BrepSolid,
447 operation: BooleanOperation,
448 options: &BooleanOptions,
449) -> Option<BrepSolid> {
450 const DIRECTIONS: [[f64; 3]; 4] = [
460 [0.4034, 0.7973, 0.4491],
461 [0.7973, 0.4491, 0.4034],
462 [0.4491, 0.4034, 0.7973],
463 [0.5774, -0.5774, 0.5774],
464 ];
465 const FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
471
472 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
473 let scale = crate::tolerance::solid_scale(first)
474 .max(crate::tolerance::solid_scale(second))
475 .max(1.0);
476
477 let va = solid_signed_volume(first).ok().map(f64::abs);
481 let vb = solid_signed_volume(second).ok().map(f64::abs);
482
483 for &fraction in &FRACTIONS {
484 let magnitude = scale * fraction;
485 for dir in &DIRECTIONS {
486 let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
487 let translate = match AffineTransform::new([
488 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,
492 ]) {
493 Ok(transform) => transform,
494 Err(_) => continue,
495 };
496 let moved = match crate::transform_brep(second, translate, false) {
497 Ok(solid) => solid,
498 Err(_) => continue,
499 };
500 let candidate =
501 match boolean_operation_with_diagnostics(first, &moved, operation, options) {
502 Ok(outcome) => outcome.value,
503 Err(_) => continue,
504 };
505 if candidate.shells.is_empty() {
507 continue;
508 }
509 if let (Some(va), Some(vb)) = (va, vb) {
514 if let Ok(vr) = solid_signed_volume(&candidate) {
515 if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
516 continue;
517 }
518 }
519 }
520 match crate::oracle::boolean_semantic_disagreement(
524 first, second, operation, &candidate, 400,
525 ) {
526 Ok(report) if report.considered >= 30 && !report.is_flagged() => {
527 if debug {
528 eprintln!(
529 "[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
530 magnitude={magnitude:.3e} offset={offset:?} \
531 (oracle considered={} rate={:.4})",
532 report.considered, report.disagreement_rate
533 );
534 }
535 return Some(candidate);
536 }
537 _ => continue,
538 }
539 }
540 }
541 if debug {
542 eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
543 }
544 None
545}
546
547fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
550 let slack = 1e-2 * (va + vb).max(1.0);
551 match operation {
552 BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
554 BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
556 BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
558 }
559}
560
561fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), String> {
588 fragment.same_sense = !fragment.same_sense;
589 for loop_record in &mut fragment.loops {
590 loop_record.coedges.reverse();
591 for coedge in &mut loop_record.coedges {
592 coedge.forward = !coedge.forward;
593 coedge.pcurve = coedge.pcurve.reversed()?;
594 }
595 }
596 Ok(())
597}
598
599fn build_nary_imprint(
606 operands: &[BrepSolid],
607 options: &ImprintOptions,
608) -> Result<ImprintResultRecord, String> {
609 let mut section_evidence = false;
610 let mut vertices: Vec<ImprintVertex> = Vec::new();
611 let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
612 let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
613 let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
614 let mut next_vertex_id: u64 = 1;
615 let mut next_piece_id: u64 = 1;
616
617 for i in 0..operands.len() {
618 for j in (i + 1)..operands.len() {
619 let pair = build_imprints(&operands[i], &operands[j], options)?;
620 let remap = |operand: u8| -> u8 {
621 if operand == 0 {
622 i as u8
623 } else {
624 j as u8
625 }
626 };
627
628 section_evidence = section_evidence || pair.section_evidence;
629 let mut vertex_map: HashMap<u64, u64> = HashMap::default();
630 for vertex in &pair.vertices {
631 let global = next_vertex_id;
632 next_vertex_id += 1;
633 vertex_map.insert(vertex.id, global);
634 vertices.push(ImprintVertex {
635 id: global,
636 point: vertex.point,
637 });
638 }
639
640 let mut piece_map: HashMap<u64, u64> = HashMap::default();
641 for piece in &pair.pieces {
642 let global = next_piece_id;
643 next_piece_id += 1;
644 piece_map.insert(piece.id, global);
645 let mut mapped = piece.clone();
646 mapped.id = global;
647 mapped.start_vertex_id = *vertex_map
648 .get(&piece.start_vertex_id)
649 .ok_or_else(|| "n-ary imprint: piece references unknown vertex".to_string())?;
650 mapped.end_vertex_id = *vertex_map
651 .get(&piece.end_vertex_id)
652 .ok_or_else(|| "n-ary imprint: piece references unknown vertex".to_string())?;
653 for pcurve in &mut mapped.pcurves {
654 pcurve.operand = remap(pcurve.operand);
655 }
656 mapped.support_faces = [
657 FaceKey {
658 operand: remap(piece.support_faces[0].operand),
659 face_id: piece.support_faces[0].face_id,
660 },
661 FaceKey {
662 operand: remap(piece.support_faces[1].operand),
663 face_id: piece.support_faces[1].face_id,
664 },
665 ];
666 pieces.push(mapped);
667 }
668
669 for entry in &pair.by_face {
670 let list = by_face
671 .entry((remap(entry.operand), entry.face_id))
672 .or_default();
673 for piece_id in &entry.piece_ids {
674 let global = *piece_map.get(piece_id).ok_or_else(|| {
675 "n-ary imprint: by_face references unknown piece".to_string()
676 })?;
677 list.push(global);
678 }
679 }
680
681 for split in &pair.edge_splits {
682 edge_splits
683 .entry((remap(split.operand), split.edge_id))
684 .or_default()
685 .extend(split.parameters.iter().copied());
686 }
687 }
688 }
689
690 Ok(ImprintResultRecord {
691 vertices,
692 pieces,
693 barrier_edges: Vec::new(),
694 section_evidence,
695 by_face: by_face
696 .into_iter()
697 .map(|((operand, face_id), piece_ids)| FaceImprints {
698 operand,
699 face_id,
700 piece_ids,
701 })
702 .collect(),
703 edge_splits: edge_splits
704 .into_iter()
705 .map(|((operand, edge_id), parameters)| EdgeSplitRecord {
706 operand,
707 edge_id,
708 parameters,
709 })
710 .collect(),
711 })
712}
713
714pub fn boolean_operation_nary(
725 operands: &[BrepSolid],
726 operation: BooleanOperation,
727) -> Result<BrepSolid, String> {
728 if operands.is_empty() {
729 return Err("boolean_operation_nary: no operands provided".into());
730 }
731 if operands.len() == 1 {
732 return Ok(operands[0].clone());
733 }
734 if operands.len() > 255 {
735 return Err("boolean_operation_nary: at most 255 operands supported".into());
736 }
737 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
738 let options = BooleanOptions::default();
739
740 let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
744 for operand in &operands[1..] {
745 let candidate = KernelTolerances::for_solid(operand, options.tolerance);
746 if candidate.sew_search > policy.sew_search {
747 policy = candidate;
748 }
749 }
750 policy.check()?;
751 let tolerance = policy.model;
752
753 let mut healed = operands.to_vec();
756 for operand in &mut healed {
757 crate::heal::heal_operands(operand, &policy)?;
758 normalize_operand_band_seams(operand)?;
761 }
762
763 let mut imprint_options = options.imprint.clone();
765 imprint_options.tolerance = tolerance;
766 let imprint = build_nary_imprint(&healed, &imprint_options)?;
767
768 let mut split = Vec::with_capacity(healed.len());
770 for (index, operand) in healed.iter().enumerate() {
771 split.push(apply_edge_splits(operand, index as u8, &imprint)?);
772 }
773 let mut fragments = Vec::with_capacity(split.len());
774 for (index, solid) in split.iter().enumerate() {
775 fragments.push(fragment_solid(solid, index as u8, &imprint)?);
776 }
777
778 let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
780
781 let solids: HashMap<u8, &BrepSolid> = split
783 .iter()
784 .enumerate()
785 .map(|(index, solid)| (index as u8, solid))
786 .collect();
787 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
788 let solid = if options.merge_coplanar_faces
789 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
790 {
791 let merged = merge_same_surface_faces_excluding(
792 &solid,
793 tolerance,
794 &options.keep_unmerged_name_substrs,
795 )?;
796 merge_curve_continuation_edges(&merged, tolerance)?
797 } else {
798 solid
799 };
800 let validation = solid.validate_detailed(&policy);
801 if !validation.issues.is_empty() {
802 return Err(format!(
803 "boolean_operation_nary: invalid result: {:?}",
804 validation.issues
805 ));
806 }
807 if debug {
808 if let Ok(report) =
809 crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
810 {
811 if report.is_flagged() {
812 eprintln!(
813 "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
814 report.disagreement_rate,
815 report.disagreements.len(),
816 report.considered,
817 report.sample_disagreement()
818 );
819 }
820 }
821 }
822 Ok(solid)
823}
824
825pub fn boolean_operation_with_diagnostics(
826 first: &BrepSolid,
827 second: &BrepSolid,
828 operation: BooleanOperation,
829 options: &BooleanOptions,
830) -> Result<KernelOutcome<BrepSolid>, String> {
831 let policy = options
832 .tolerances
833 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
834 policy.check()?;
835 let tolerance = policy.model;
836
837 let mut first_owned = first.clone();
846 let mut second_owned = second.clone();
847 crate::heal::heal_operands(&mut first_owned, &policy)?;
848 crate::heal::heal_operands(&mut second_owned, &policy)?;
849 normalize_operand_band_seams(&mut first_owned)?;
854 normalize_operand_band_seams(&mut second_owned)?;
855 let first = &first_owned;
856 let second = &second_owned;
857
858 let mut diagnostics = KernelDiagnostics::default();
859 let operation_started = Instant::now();
860 diagnostics.count_n(
861 "collect.faces",
862 first
863 .shells
864 .iter()
865 .chain(&second.shells)
866 .map(|shell| shell.faces.len() as u64)
867 .sum(),
868 );
869 diagnostics.measure_max("tolerance.model", policy.model);
870 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
871
872 let mut imprint_options = options.imprint.clone();
873 imprint_options.tolerance = tolerance;
874 let stage_started = Instant::now();
875 let imprint = build_imprints(first, second, &imprint_options)?;
876 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
877 for piece in &imprint.pieces {
878 let start = piece.curve.evaluate(piece.t0);
879 let end = piece.curve.evaluate(piece.t1);
880 eprintln!(
881 "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
882 piece.id, piece.support_faces, piece.t0, piece.t1, start, end
883 );
884 }
885 for entry in &imprint.by_face {
886 eprintln!(
887 "by_face operand={} face={} pieces={:?}",
888 entry.operand, entry.face_id, entry.piece_ids
889 );
890 }
891 for split in &imprint.edge_splits {
892 eprintln!(
893 "edge_split operand={} edge={} params={:?}",
894 split.operand, split.edge_id, split.parameters
895 );
896 }
897 }
898 diagnostics.measure_max(
899 "timing.imprint_ms",
900 stage_started.elapsed().as_secs_f64() * 1_000.0,
901 );
902 diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
903 diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
904 diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
905 let stage_started = Instant::now();
906 let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
907 let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
908 diagnostics.measure_max(
909 "timing.edge_split_ms",
910 stage_started.elapsed().as_secs_f64() * 1_000.0,
911 );
912 let stage_started = Instant::now();
913 let fragments_a = fragment_solid(&split_first, 0, &imprint)?;
914 let fragments_b = fragment_solid(&split_second, 1, &imprint)?;
915 diagnostics.measure_max(
916 "timing.fragment_ms",
917 stage_started.elapsed().as_secs_f64() * 1_000.0,
918 );
919 diagnostics.count_n(
920 "fragment.candidates",
921 (fragments_a.len() + fragments_b.len()) as u64,
922 );
923 let a_surface_samples: Vec<Vec3> = fragments_a
928 .iter()
929 .flat_map(|fragment| {
930 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
931 })
932 .collect();
933 let b_surface_samples: Vec<Vec3> = fragments_b
934 .iter()
935 .flat_map(|fragment| {
936 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
937 })
938 .collect();
939 let stage_started = Instant::now();
940 let mut barrier_edges: HashSet<(u8, u64)> = imprint
941 .pieces
942 .iter()
943 .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
944 .collect();
945 barrier_edges.extend(imprint.barrier_edges.iter().copied());
946 for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
952 let minted: Vec<(u8, u64)> = barrier_edges
953 .iter()
954 .filter(|(barrier_operand, _)| *barrier_operand == operand)
955 .filter_map(|(_, edge_id)| split_map.get(edge_id))
956 .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
957 .collect();
958 barrier_edges.extend(minted);
959 }
960 let selected = select_fragments(
961 fragments_a,
962 fragments_b,
963 first,
964 second,
965 operation,
966 tolerance,
967 &barrier_edges,
968 )?;
969 diagnostics.measure_max(
970 "timing.select_ms",
971 stage_started.elapsed().as_secs_f64() * 1_000.0,
972 );
973 diagnostics.count_n("select.fragments", selected.len() as u64);
974 if selected.is_empty()
996 && !imprint.section_evidence
997 && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
998 {
999 let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, String> {
1000 for &sample in samples {
1001 if classify_point(sample, other, tolerance)?.class == PointClass::In {
1002 return Ok(true);
1003 }
1004 }
1005 Ok(false)
1006 };
1007 let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, String> {
1008 for &sample in samples {
1009 if classify_point(sample, other, tolerance)?.class == PointClass::Out {
1010 return Ok(true);
1011 }
1012 }
1013 Ok(false)
1014 };
1015 let legitimate = match operation {
1016 BooleanOperation::Intersect => {
1017 !strictly_inside(&a_surface_samples, second)?
1018 && !strictly_inside(&b_surface_samples, first)?
1019 }
1020 BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
1021 BooleanOperation::Union => false,
1022 };
1023 if legitimate {
1024 diagnostics.event(
1025 DiagnosticSeverity::Info,
1026 KernelStage::Select,
1027 "boolean.empty_result",
1028 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
1029 );
1030 diagnostics.measure_max(
1031 "timing.total_ms",
1032 operation_started.elapsed().as_secs_f64() * 1_000.0,
1033 );
1034 return Ok(KernelOutcome {
1035 value: BrepSolid {
1036 id: 0,
1037 vertices: Vec::new(),
1038 edges: Vec::new(),
1039 shells: Vec::new(),
1040 genus: 0,
1041 },
1042 diagnostics,
1043 });
1044 }
1045 }
1046 let solids = [(0, &split_first), (1, &split_second)]
1047 .into_iter()
1048 .collect::<HashMap<_, _>>();
1049 let stage_started = Instant::now();
1050 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
1051 diagnostics.measure_max(
1052 "timing.assemble_ms",
1053 stage_started.elapsed().as_secs_f64() * 1_000.0,
1054 );
1055 let stage_started = Instant::now();
1056 if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
1057 let issues = solid.validate();
1058 eprintln!(
1059 "pre-merge validation: {} issue(s){}",
1060 issues.len(),
1061 issues
1062 .first()
1063 .map(|issue| format!(" — first: {}", issue.message))
1064 .unwrap_or_default()
1065 );
1066 }
1067 let solid = if options.merge_coplanar_faces
1068 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
1069 {
1070 let merged = merge_same_surface_faces_excluding(
1071 &solid,
1072 tolerance,
1073 &options.keep_unmerged_name_substrs,
1074 )?;
1075 merge_curve_continuation_edges(&merged, tolerance)?
1080 } else {
1081 solid
1082 };
1083 diagnostics.measure_max(
1084 "timing.face_merge_ms",
1085 stage_started.elapsed().as_secs_f64() * 1_000.0,
1086 );
1087 let validation = solid.validate_detailed(&policy);
1088 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
1089 diagnostics.count_n(
1090 "validate.wire_warnings",
1091 validation.wire_warnings.len() as u64,
1092 );
1093 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
1094 for warning in &validation.wire_warnings {
1095 diagnostics.event(
1096 DiagnosticSeverity::Warning,
1097 KernelStage::Validate,
1098 "validate.uv_wire",
1099 warning.message.clone(),
1100 );
1101 }
1102 for issue in &validation.issues {
1103 diagnostics.event(
1104 DiagnosticSeverity::Error,
1105 KernelStage::Validate,
1106 "validate.brep",
1107 issue.message.clone(),
1108 );
1109 }
1110 diagnostics.measure_max(
1111 "timing.total_ms",
1112 operation_started.elapsed().as_secs_f64() * 1_000.0,
1113 );
1114 if !validation.issues.is_empty() {
1115 return Err(format!(
1116 "boolean_operation: invalid result: {:?}",
1117 validation.issues
1118 ));
1119 }
1120 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
1126 if let Ok(report) =
1127 crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
1128 {
1129 if report.is_flagged() {
1130 eprintln!(
1131 "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
1132 report.disagreement_rate,
1133 report.disagreements.len(),
1134 report.considered,
1135 report.sample_disagreement()
1136 );
1137 }
1138 }
1139 let fusable_band = crate::tolerance::assembler_weld(policy.model);
1140 let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
1141 if !fusables.is_empty() {
1142 eprintln!(
1143 "[oracle] {} residual fusable(s) after weld; sample {:?}",
1144 fusables.len(),
1145 fusables.first()
1146 );
1147 }
1148 }
1149 Ok(KernelOutcome {
1150 value: solid,
1151 diagnostics,
1152 })
1153}
1154