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 rim;
95use rim::*;
98use select::*;
99pub(crate) use assemble::{
101 assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
102 edge_interior_lies_on, finalize_assembled_solid,
103};
104#[allow(unused_imports)]
106pub(crate) use assemble::apply_assembly_heal_chain;
107
108pub fn boolean_operation(
109 first: &BrepSolid,
110 second: &BrepSolid,
111 operation: BooleanOperation,
112 options: &BooleanOptions,
113) -> Result<BrepSolid, KernelRefusal> {
114 match boolean_operation_with_diagnostics(first, second, operation, options) {
115 Ok(outcome) => Ok(outcome.value),
116 Err(error) => {
117 if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
125 || !error.class.perturbation_eligible()
126 {
127 return Err(error);
128 }
129 if matches!(operation, BooleanOperation::Subtract)
144 && std::env::var("BREP_PINCH_GATE").as_deref() != Ok("0")
145 && subtract_pinches_at_internal_tangency(first, second, options.tolerance)
146 .unwrap_or(false)
147 {
148 return Err(error.with_message(|message| format!(
149 "{message}; internal tangency: the operands touch tangentially with \
150 co-directed normals, so the exact difference pinches to zero thickness \
151 (non-manifold, unrepresentable in a boundary model) — refusing rather \
152 than returning a perturbed sliver"
153 )));
154 }
155 match perturbation_retry(first, second, operation, options) {
156 Some(solid) => Ok(solid),
157 None => Err(error),
158 }
159 }
160 }
161}
162
163const PINCH_PROBE_STEPS: usize = 8;
168
169const PINCH_ANGULAR_TOLERANCE: f64 = 1e-4;
172
173const PINCH_SEPARATION_STEP: f64 = 1e-2;
177
178fn subtract_pinches_at_internal_tangency(
200 first: &BrepSolid,
201 second: &BrepSolid,
202 tolerance: f64,
203) -> Result<bool, KernelRefusal> {
204 let contact = (tolerance * 100.0).max(1e-12);
207 let probes_a = first
208 .shells
209 .iter()
210 .flat_map(|shell| &shell.faces)
211 .map(PinchProbe::of)
212 .collect::<Result<Vec<_>, _>>()?;
213 let probes_b = second
214 .shells
215 .iter()
216 .flat_map(|shell| &shell.faces)
217 .map(PinchProbe::of)
218 .collect::<Result<Vec<_>, _>>()?;
219 for probe_a in &probes_a {
220 for probe_b in &probes_b {
221 let pad = 1e-2 * probe_a.extent().max(probe_b.extent());
228 if probe_a.separation(probe_b) > contact + pad {
229 continue;
230 }
231 if classify_surface_pair(
238 &probe_a.face.surface,
239 &probe_b.face.surface,
240 tolerance,
241 PINCH_ANGULAR_TOLERANCE,
242 ).or_refuse(KernelStage::Validate, "csg.boolean.mod")?
243 .relation
244 == SurfacePairRelation::Cosurface
245 {
246 continue;
247 }
248 if faces_touch_with_codirected_normals(probe_a, probe_b, contact)? {
249 return Ok(true);
250 }
251 }
252 }
253 Ok(false)
254}
255
256struct PinchProbe<'a> {
259 face: &'a FaceRecord,
260 samples: Vec<(f64, f64, Vec3)>,
262 minimum: [f64; 3],
263 maximum: [f64; 3],
264}
265
266impl<'a> PinchProbe<'a> {
267 fn of(face: &'a FaceRecord) -> Result<Self, KernelRefusal> {
268 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
269 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
270 let steps = PINCH_PROBE_STEPS as f64;
271 let mut samples = Vec::with_capacity((PINCH_PROBE_STEPS + 1).pow(2));
272 let mut minimum = [f64::INFINITY; 3];
273 let mut maximum = [f64::NEG_INFINITY; 3];
274 for i in 0..=PINCH_PROBE_STEPS {
275 let u = u0 + (u1 - u0) * i as f64 / steps;
276 for j in 0..=PINCH_PROBE_STEPS {
277 let v = v0 + (v1 - v0) * j as f64 / steps;
278 let Ok(point) = face.surface.evaluate(u, v) else {
279 continue;
280 };
281 for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
282 minimum[axis] = minimum[axis].min(value);
283 maximum[axis] = maximum[axis].max(value);
284 }
285 samples.push((u, v, point));
286 }
287 }
288 Ok(Self {
289 face,
290 samples,
291 minimum,
292 maximum,
293 })
294 }
295
296 fn extent(&self) -> f64 {
298 (0..3)
299 .map(|axis| self.maximum[axis] - self.minimum[axis])
300 .fold(0.0f64, f64::max)
301 }
302
303 fn separation(&self, other: &Self) -> f64 {
305 let mut gap: f64 = 0.0;
306 for axis in 0..3 {
307 gap = gap.max(self.minimum[axis] - other.maximum[axis]);
308 gap = gap.max(other.minimum[axis] - self.maximum[axis]);
309 }
310 gap
311 }
312}
313
314fn faces_touch_with_codirected_normals(
319 probe_a: &PinchProbe<'_>,
320 probe_b: &PinchProbe<'_>,
321 contact: f64,
322) -> Result<bool, KernelRefusal> {
323 for (probe, other) in [(probe_a, probe_b), (probe_b, probe_a)] {
324 let source = probe.face;
325 let target = other.face;
326 for &(u, v, point) in &probe.samples {
327 let projection = project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?;
328 if projection.distance > contact {
329 continue;
330 }
331 let (Ok(source_normal), Ok(target_normal)) = (
334 source.surface.normal(u, v),
335 target.surface.normal(projection.u, projection.v),
336 ) else {
337 continue;
338 };
339 let source_outward = outward_normal(source_normal, source.same_sense);
340 let target_outward = outward_normal(target_normal, target.same_sense);
341 if source_outward.cross(target_outward).length() > PINCH_ANGULAR_TOLERANCE
342 || source_outward.dot(target_outward) <= 0.0
343 {
344 continue;
345 }
346 if parameter_point_in_face(source, Vec2 { x: u, y: v }, 1e-6).or_refuse(KernelStage::Validate, "parameter_point_in_face")?
347 == PolygonClass::Outside
348 || parameter_point_in_face(
349 target,
350 Vec2 {
351 x: projection.u,
352 y: projection.v,
353 },
354 1e-6,
355 ).or_refuse(KernelStage::Validate, "csg.boolean.mod")? == PolygonClass::Outside
356 {
357 continue;
358 }
359 if contact_separates_locally(source, target, u, v, contact)? {
360 return Ok(true);
361 }
362 }
363 }
364 Ok(false)
365}
366
367fn outward_normal(normal: Vec3, same_sense: bool) -> Vec3 {
368 if same_sense {
369 normal
370 } else {
371 normal.scale(-1.0)
372 }
373}
374
375fn contact_separates_locally(
381 source: &FaceRecord,
382 target: &FaceRecord,
383 u: f64,
384 v: f64,
385 contact: f64,
386) -> Result<bool, KernelRefusal> {
387 let [u0, u1] = source.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
388 let [v0, v1] = source.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
389 let step_u = (u1 - u0) * PINCH_SEPARATION_STEP;
390 let step_v = (v1 - v0) * PINCH_SEPARATION_STEP;
391 for (probe_u, probe_v) in [
392 ((u + step_u).min(u1), v),
393 ((u - step_u).max(u0), v),
394 (u, (v + step_v).min(v1)),
395 (u, (v - step_v).max(v0)),
396 ] {
397 if (probe_u - u).abs() < f64::EPSILON && (probe_v - v).abs() < f64::EPSILON {
398 continue;
399 }
400 let Ok(point) = source.surface.evaluate(probe_u, probe_v) else {
401 continue;
402 };
403 if project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?.distance > contact * 10.0 {
406 return Ok(true);
407 }
408 }
409 Ok(false)
410}
411
412fn perturbation_retry(
428 first: &BrepSolid,
429 second: &BrepSolid,
430 operation: BooleanOperation,
431 options: &BooleanOptions,
432) -> Option<BrepSolid> {
433 const DIRECTIONS: [[f64; 3]; 4] = [
444 [0.4034, 0.7973, 0.4491],
445 [0.7973, 0.4491, 0.4034],
446 [0.4491, 0.4034, 0.7973],
447 [0.5774, -0.5774, 0.5774],
448 ];
449 const FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
455
456 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
457 let scale = crate::tolerance::solid_scale(first)
458 .max(crate::tolerance::solid_scale(second))
459 .max(1.0);
460
461 let va = solid_signed_volume(first).ok().map(f64::abs);
465 let vb = solid_signed_volume(second).ok().map(f64::abs);
466
467 for &fraction in &FRACTIONS {
468 let magnitude = scale * fraction;
469 for dir in &DIRECTIONS {
470 let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
471 let translate = match AffineTransform::new([
472 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,
476 ]) {
477 Ok(transform) => transform,
478 Err(_) => continue,
479 };
480 let moved = match crate::transform_brep(second, translate, false) {
481 Ok(solid) => solid,
482 Err(_) => continue,
483 };
484 let candidate =
485 match boolean_operation_with_diagnostics(first, &moved, operation, options) {
486 Ok(outcome) => outcome.value,
487 Err(_) => continue,
488 };
489 if candidate.shells.is_empty() {
491 continue;
492 }
493 if let (Some(va), Some(vb)) = (va, vb) {
498 if let Ok(vr) = solid_signed_volume(&candidate) {
499 if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
500 continue;
501 }
502 }
503 }
504 match crate::oracle::boolean_semantic_disagreement(
508 first, second, operation, &candidate, 400,
509 ) {
510 Ok(report) if report.considered >= 30 && !report.is_flagged() => {
511 if debug {
512 eprintln!(
513 "[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
514 magnitude={magnitude:.3e} offset={offset:?} \
515 (oracle considered={} rate={:.4})",
516 report.considered, report.disagreement_rate
517 );
518 }
519 return Some(candidate);
520 }
521 _ => continue,
522 }
523 }
524 }
525 if debug {
526 eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
527 }
528 None
529}
530
531fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
534 let slack = 1e-2 * (va + vb).max(1.0);
535 match operation {
536 BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
538 BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
540 BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
542 }
543}
544
545fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), KernelRefusal> {
572 fragment.same_sense = !fragment.same_sense;
573 for loop_record in &mut fragment.loops {
574 loop_record.coedges.reverse();
575 for coedge in &mut loop_record.coedges {
576 coedge.forward = !coedge.forward;
577 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Validate, "reversed")?;
578 }
579 }
580 Ok(())
581}
582
583fn build_nary_imprint(
590 operands: &[BrepSolid],
591 options: &ImprintOptions,
592) -> Result<ImprintResultRecord, KernelRefusal> {
593 let mut section_evidence = false;
594 let mut vertices: Vec<ImprintVertex> = Vec::new();
595 let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
596 let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
597 let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
598 let mut next_vertex_id: u64 = 1;
599 let mut next_piece_id: u64 = 1;
600
601 for i in 0..operands.len() {
602 for j in (i + 1)..operands.len() {
603 let pair = build_imprints(&operands[i], &operands[j], options)?;
604 let remap = |operand: u8| -> u8 {
605 if operand == 0 {
606 i as u8
607 } else {
608 j as u8
609 }
610 };
611
612 section_evidence = section_evidence || pair.section_evidence;
613 let mut vertex_map: HashMap<u64, u64> = HashMap::default();
614 for vertex in &pair.vertices {
615 let global = next_vertex_id;
616 next_vertex_id += 1;
617 vertex_map.insert(vertex.id, global);
618 vertices.push(ImprintVertex {
619 id: global,
620 point: vertex.point,
621 });
622 }
623
624 let mut piece_map: HashMap<u64, u64> = HashMap::default();
625 for piece in &pair.pieces {
626 let global = next_piece_id;
627 next_piece_id += 1;
628 piece_map.insert(piece.id, global);
629 let mut mapped = piece.clone();
630 mapped.id = global;
631 mapped.start_vertex_id = *vertex_map
632 .get(&piece.start_vertex_id)
633 .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
634 mapped.end_vertex_id = *vertex_map
635 .get(&piece.end_vertex_id)
636 .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
637 for pcurve in &mut mapped.pcurves {
638 pcurve.operand = remap(pcurve.operand);
639 }
640 mapped.support_faces = [
641 FaceKey {
642 operand: remap(piece.support_faces[0].operand),
643 face_id: piece.support_faces[0].face_id,
644 },
645 FaceKey {
646 operand: remap(piece.support_faces[1].operand),
647 face_id: piece.support_faces[1].face_id,
648 },
649 ];
650 pieces.push(mapped);
651 }
652
653 for entry in &pair.by_face {
654 let list = by_face
655 .entry((remap(entry.operand), entry.face_id))
656 .or_default();
657 for piece_id in &entry.piece_ids {
658 let global = *piece_map.get(piece_id).ok_or_else(|| {
659 KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: by_face references unknown piece")
660 })?;
661 list.push(global);
662 }
663 }
664
665 for split in &pair.edge_splits {
666 edge_splits
667 .entry((remap(split.operand), split.edge_id))
668 .or_default()
669 .extend(split.parameters.iter().copied());
670 }
671 }
672 }
673
674 Ok(ImprintResultRecord {
675 vertices,
676 pieces,
677 tangent_nodes: Vec::new(),
678 barrier_edges: Vec::new(),
679 section_evidence,
680 by_face: by_face
681 .into_iter()
682 .map(|((operand, face_id), piece_ids)| FaceImprints {
683 operand,
684 face_id,
685 piece_ids,
686 })
687 .collect(),
688 edge_splits: edge_splits
689 .into_iter()
690 .map(|((operand, edge_id), parameters)| EdgeSplitRecord {
691 operand,
692 edge_id,
693 parameters,
694 })
695 .collect(),
696 })
697}
698
699pub fn boolean_operation_nary(
710 operands: &[BrepSolid],
711 operation: BooleanOperation,
712) -> Result<BrepSolid, KernelRefusal> {
713 if operands.is_empty() {
714 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: no operands provided"));
715 }
716 if operands.len() == 1 {
717 return Ok(operands[0].clone());
718 }
719 if operands.len() > 255 {
720 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: at most 255 operands supported"));
721 }
722 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
723 let options = BooleanOptions::default();
724
725 let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
729 for operand in &operands[1..] {
730 let candidate = KernelTolerances::for_solid(operand, options.tolerance);
731 if candidate.sew_search > policy.sew_search {
732 policy = candidate;
733 }
734 }
735 policy.check().or_input(KernelStage::Collect, "tolerances")?;
736 let tolerance = policy.model;
737
738 let mut healed = operands.to_vec();
741 for operand in &mut healed {
742 crate::heal::heal_operands(operand, &policy).or_refuse(KernelStage::Validate, "heal_operands")?;
743 normalize_operand_band_seams(operand)?;
746 }
747
748 let mut imprint_options = options.imprint.clone();
750 imprint_options.tolerance = tolerance;
751 let imprint = build_nary_imprint(&healed, &imprint_options)?;
752
753 let mut split = Vec::with_capacity(healed.len());
755 for (index, operand) in healed.iter().enumerate() {
756 split.push(apply_edge_splits(operand, index as u8, &imprint)?);
757 }
758 let mut fragments = Vec::with_capacity(split.len());
759 for (index, solid) in split.iter().enumerate() {
760 fragments.push(fragment_solid(solid, index as u8, &imprint)?);
761 }
762
763 let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
765
766 let solids: HashMap<u8, &BrepSolid> = split
768 .iter()
769 .enumerate()
770 .map(|(index, solid)| (index as u8, solid))
771 .collect();
772 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
773 let solid = if options.merge_coplanar_faces
774 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
775 {
776 let merged = merge_same_surface_faces_excluding(
777 &solid,
778 tolerance,
779 &options.keep_unmerged_name_substrs,
780 )?;
781 merge_curve_continuation_edges(&merged, tolerance).or_refuse(KernelStage::Validate, "merge_curve_continuation_edges")?
782 } else {
783 solid
784 };
785 let validation = solid.validate_detailed(&policy);
786 if !validation.issues.is_empty() {
787 return Err(KernelRefusal::new(
788 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
789 KernelStage::Validate,
790 format!(
791 "boolean_operation_nary: invalid result: {:?}",
792 validation.issues
793 )));
794 }
795 if debug {
796 if let Ok(report) =
797 crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
798 {
799 if report.is_flagged() {
800 eprintln!(
801 "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
802 report.disagreement_rate,
803 report.disagreements.len(),
804 report.considered,
805 report.sample_disagreement()
806 );
807 }
808 }
809 }
810 Ok(solid)
811}
812
813fn attribute_to_tangent_node(error: KernelRefusal, nodes: &[Vec3]) -> KernelRefusal {
829 let Some(node) = nodes.first() else {
830 return error;
831 };
832 if !error.class.perturbation_eligible()
833 || matches!(error.class, RefusalClass::TangentNodeSingularity)
834 {
835 return error;
836 }
837 KernelRefusal::new(
838 RefusalClass::TangentNodeSingularity,
839 error.stage,
840 format!(
841 "boolean: unsupported singular/tangent-node surface intersection \
842 at the tangent node at ({:.6},{:.6},{:.6}): the section through it did not \
843 assemble into a closed shell",
844 node.x, node.y, node.z
845 ),
846 )
847}
848
849pub fn boolean_operation_with_diagnostics(
850 first: &BrepSolid,
851 second: &BrepSolid,
852 operation: BooleanOperation,
853 options: &BooleanOptions,
854) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
855 let mut tangent_nodes = Vec::new();
856 boolean_pipeline(first, second, operation, options, &mut tangent_nodes)
857 .map_err(|error| attribute_to_tangent_node(error, &tangent_nodes))
858}
859
860fn boolean_pipeline(
861 first: &BrepSolid,
862 second: &BrepSolid,
863 operation: BooleanOperation,
864 options: &BooleanOptions,
865 tangent_nodes: &mut Vec<Vec3>,
866) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
867 let policy = options
868 .tolerances
869 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
870 policy.check().or_input(KernelStage::Collect, "tolerances")?;
871 let tolerance = policy.model;
872
873 let mut first_owned = first.clone();
882 let mut second_owned = second.clone();
883 crate::heal::heal_operands(&mut first_owned, &policy).or_refuse(KernelStage::Validate, "heal_operands")?;
884 crate::heal::heal_operands(&mut second_owned, &policy).or_refuse(KernelStage::Validate, "heal_operands")?;
885 normalize_operand_band_seams(&mut first_owned)?;
890 normalize_operand_band_seams(&mut second_owned)?;
891 let first = &first_owned;
892 let second = &second_owned;
893
894 let mut diagnostics = KernelDiagnostics::default();
895 let operation_started = Instant::now();
896 diagnostics.count_n(
897 "collect.faces",
898 first
899 .shells
900 .iter()
901 .chain(&second.shells)
902 .map(|shell| shell.faces.len() as u64)
903 .sum(),
904 );
905 diagnostics.measure_max("tolerance.model", policy.model);
906 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
907
908 let mut imprint_options = options.imprint.clone();
909 imprint_options.tolerance = tolerance;
910 let stage_started = Instant::now();
911 let imprint = build_imprints(first, second, &imprint_options)?;
912 tangent_nodes.extend_from_slice(&imprint.tangent_nodes);
913 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
914 for piece in &imprint.pieces {
915 let start = piece.curve.evaluate(piece.t0);
916 let end = piece.curve.evaluate(piece.t1);
917 eprintln!(
918 "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
919 piece.id, piece.support_faces, piece.t0, piece.t1, start, end
920 );
921 }
922 for entry in &imprint.by_face {
923 eprintln!(
924 "by_face operand={} face={} pieces={:?}",
925 entry.operand, entry.face_id, entry.piece_ids
926 );
927 }
928 for split in &imprint.edge_splits {
929 eprintln!(
930 "edge_split operand={} edge={} params={:?}",
931 split.operand, split.edge_id, split.parameters
932 );
933 }
934 }
935 diagnostics.measure_max(
936 "timing.imprint_ms",
937 stage_started.elapsed().as_secs_f64() * 1_000.0,
938 );
939 diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
940 diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
941 diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
942 let stage_started = Instant::now();
943 let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
944 let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
945 diagnostics.measure_max(
946 "timing.edge_split_ms",
947 stage_started.elapsed().as_secs_f64() * 1_000.0,
948 );
949 let stage_started = Instant::now();
950 let fragments_a = fragment_solid(&split_first, 0, &imprint)?;
951 let fragments_b = fragment_solid(&split_second, 1, &imprint)?;
952 diagnostics.measure_max(
953 "timing.fragment_ms",
954 stage_started.elapsed().as_secs_f64() * 1_000.0,
955 );
956 diagnostics.count_n(
957 "fragment.candidates",
958 (fragments_a.len() + fragments_b.len()) as u64,
959 );
960 let a_surface_samples: Vec<Vec3> = fragments_a
965 .iter()
966 .flat_map(|fragment| {
967 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
968 })
969 .collect();
970 let b_surface_samples: Vec<Vec3> = fragments_b
971 .iter()
972 .flat_map(|fragment| {
973 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
974 })
975 .collect();
976 let stage_started = Instant::now();
977 let mut barrier_edges: HashSet<(u8, u64)> = imprint
978 .pieces
979 .iter()
980 .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
981 .collect();
982 barrier_edges.extend(imprint.barrier_edges.iter().copied());
983 for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
989 let minted: Vec<(u8, u64)> = barrier_edges
990 .iter()
991 .filter(|(barrier_operand, _)| *barrier_operand == operand)
992 .filter_map(|(_, edge_id)| split_map.get(edge_id))
993 .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
994 .collect();
995 barrier_edges.extend(minted);
996 }
997 let selected = select_fragments(
998 fragments_a,
999 fragments_b,
1000 first,
1001 second,
1002 operation,
1003 tolerance,
1004 &barrier_edges,
1005 )?;
1006 diagnostics.measure_max(
1007 "timing.select_ms",
1008 stage_started.elapsed().as_secs_f64() * 1_000.0,
1009 );
1010 diagnostics.count_n("select.fragments", selected.len() as u64);
1011 if selected.is_empty()
1033 && !imprint.section_evidence
1034 && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
1035 {
1036 let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
1037 for &sample in samples {
1038 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::In {
1039 return Ok(true);
1040 }
1041 }
1042 Ok(false)
1043 };
1044 let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
1045 for &sample in samples {
1046 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::Out {
1047 return Ok(true);
1048 }
1049 }
1050 Ok(false)
1051 };
1052 let legitimate = match operation {
1053 BooleanOperation::Intersect => {
1054 !strictly_inside(&a_surface_samples, second)?
1055 && !strictly_inside(&b_surface_samples, first)?
1056 }
1057 BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
1058 BooleanOperation::Union => false,
1059 };
1060 if legitimate {
1061 diagnostics.event(
1062 DiagnosticSeverity::Info,
1063 KernelStage::Select,
1064 "boolean.empty_result",
1065 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
1066 );
1067 diagnostics.measure_max(
1068 "timing.total_ms",
1069 operation_started.elapsed().as_secs_f64() * 1_000.0,
1070 );
1071 return Ok(KernelOutcome {
1072 value: BrepSolid {
1073 id: 0,
1074 vertices: Vec::new(),
1075 edges: Vec::new(),
1076 shells: Vec::new(),
1077 genus: 0,
1078 },
1079 diagnostics,
1080 });
1081 }
1082 }
1083 let solids = [(0, &split_first), (1, &split_second)]
1084 .into_iter()
1085 .collect::<HashMap<_, _>>();
1086 let stage_started = Instant::now();
1087 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
1088 diagnostics.measure_max(
1089 "timing.assemble_ms",
1090 stage_started.elapsed().as_secs_f64() * 1_000.0,
1091 );
1092 let stage_started = Instant::now();
1093 if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
1094 let issues = solid.validate();
1095 eprintln!(
1096 "pre-merge validation: {} issue(s){}",
1097 issues.len(),
1098 issues
1099 .first()
1100 .map(|issue| format!(" — first: {}", issue.message))
1101 .unwrap_or_default()
1102 );
1103 }
1104 let solid = if options.merge_coplanar_faces
1105 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
1106 {
1107 let merged = merge_same_surface_faces_excluding(
1108 &solid,
1109 tolerance,
1110 &options.keep_unmerged_name_substrs,
1111 )?;
1112 merge_curve_continuation_edges(&merged, tolerance).or_refuse(KernelStage::Validate, "merge_curve_continuation_edges")?
1117 } else {
1118 solid
1119 };
1120 diagnostics.measure_max(
1121 "timing.face_merge_ms",
1122 stage_started.elapsed().as_secs_f64() * 1_000.0,
1123 );
1124 let validation = solid.validate_detailed(&policy);
1125 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
1126 diagnostics.count_n(
1127 "validate.wire_warnings",
1128 validation.wire_warnings.len() as u64,
1129 );
1130 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
1131 for warning in &validation.wire_warnings {
1132 diagnostics.event(
1133 DiagnosticSeverity::Warning,
1134 KernelStage::Validate,
1135 "validate.uv_wire",
1136 warning.message.clone(),
1137 );
1138 }
1139 for issue in &validation.issues {
1140 diagnostics.event(
1141 DiagnosticSeverity::Error,
1142 KernelStage::Validate,
1143 "validate.brep",
1144 issue.message.clone(),
1145 );
1146 }
1147 diagnostics.measure_max(
1148 "timing.total_ms",
1149 operation_started.elapsed().as_secs_f64() * 1_000.0,
1150 );
1151 if !validation.issues.is_empty() {
1152 return Err(KernelRefusal::new(
1153 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
1154 KernelStage::Validate,
1155 format!(
1156 "boolean_operation: invalid result: {:?}",
1157 validation.issues
1158 )));
1159 }
1160 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
1166 if let Ok(report) =
1167 crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
1168 {
1169 if report.is_flagged() {
1170 eprintln!(
1171 "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
1172 report.disagreement_rate,
1173 report.disagreements.len(),
1174 report.considered,
1175 report.sample_disagreement()
1176 );
1177 }
1178 }
1179 let fusable_band = crate::tolerance::assembler_weld(policy.model);
1180 let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
1181 if !fusables.is_empty() {
1182 eprintln!(
1183 "[oracle] {} residual fusable(s) after weld; sample {:?}",
1184 fusables.len(),
1185 fusables.first()
1186 );
1187 }
1188 }
1189 Ok(KernelOutcome {
1190 value: solid,
1191 diagnostics,
1192 })
1193}
1194