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,
16 fragment_solid, interpolate_curve, merge_curve_continuation_edges, merge_same_surface_faces,
17 project_point_to_curve, project_point_to_surface, solid_signed_volume, AffineTransform,
18 DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage, KernelTolerances, NurbsCurve,
19 PointClass, SolidClassifier, Vec3,
20};
21use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
22use serde::{Deserialize, Serialize};
23use web_time::Instant;
24
25thread_local! {
26 pub(crate) static CONFORMANCE_REPAIRS: std::cell::Cell<u64> =
32 const { std::cell::Cell::new(0) };
33}
34
35#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
36#[serde(rename_all = "lowercase")]
37pub enum BooleanOperation {
38 Union,
39 Intersect,
40 Subtract,
41}
42
43#[derive(Clone, Debug, Deserialize)]
44pub struct BooleanOptions {
45 #[serde(default = "default_tolerance")]
46 pub tolerance: f64,
47 #[serde(default)]
50 pub tolerances: Option<KernelTolerances>,
51 #[serde(default)]
52 pub imprint: ImprintOptions,
53 #[serde(default = "default_true")]
56 pub merge_coplanar_faces: bool,
57}
58
59fn default_tolerance() -> f64 {
60 1e-7
61}
62
63fn default_true() -> bool {
64 true
65}
66
67impl Default for BooleanOptions {
68 fn default() -> Self {
69 Self {
70 tolerance: default_tolerance(),
71 tolerances: None,
72 imprint: ImprintOptions::default(),
73 merge_coplanar_faces: true,
74 }
75 }
76}
77
78mod select;
79mod assemble;
80mod rim;
81#[cfg(test)]
82mod tests;
83
84use rim::*;
85use select::*;
86#[cfg(test)]
89use assemble::*;
90pub(crate) use assemble::{
91 assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
92 edge_interior_lies_on, finalize_assembled_solid,
93};
94#[allow(unused_imports)]
97pub(crate) use assemble::apply_assembly_heal_chain;
98
99pub fn boolean_operation(
100 first: &BrepSolid,
101 second: &BrepSolid,
102 operation: BooleanOperation,
103 options: &BooleanOptions,
104) -> Result<BrepSolid, String> {
105 match boolean_operation_with_diagnostics(first, second, operation, options) {
106 Ok(outcome) => Ok(outcome.value),
107 Err(error) => {
108 if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
116 || !is_degeneracy_error(&error)
117 {
118 return Err(error);
119 }
120 match perturbation_retry(first, second, operation, options) {
121 Some(solid) => Ok(solid),
122 None => Err(error),
123 }
124 }
125 }
126}
127
128fn is_degeneracy_error(message: &str) -> bool {
132 message.contains("invalid topology")
133 || message.contains("non-integral genus")
134 || message.contains("non-positive volume")
135 || message.contains("open edges")
136 || message.contains("singular/tangent-node")
140}
141
142fn perturbation_retry(
157 first: &BrepSolid,
158 second: &BrepSolid,
159 operation: BooleanOperation,
160 options: &BooleanOptions,
161) -> Option<BrepSolid> {
162 const DIRECTIONS: [[f64; 3]; 4] = [
172 [0.4034, 0.7973, 0.4491],
173 [0.7973, 0.4491, 0.4034],
174 [0.4491, 0.4034, 0.7973],
175 [0.5774, -0.5774, 0.5774],
176 ];
177 const FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
183
184 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
185 let scale = crate::tolerance::solid_scale(first)
186 .max(crate::tolerance::solid_scale(second))
187 .max(1.0);
188
189 let va = solid_signed_volume(first).ok().map(f64::abs);
193 let vb = solid_signed_volume(second).ok().map(f64::abs);
194
195 for &fraction in &FRACTIONS {
196 let magnitude = scale * fraction;
197 for dir in &DIRECTIONS {
198 let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
199 let translate = match AffineTransform::new([
200 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,
204 ]) {
205 Ok(transform) => transform,
206 Err(_) => continue,
207 };
208 let moved = match crate::transform_brep(second, translate, false) {
209 Ok(solid) => solid,
210 Err(_) => continue,
211 };
212 let candidate =
213 match boolean_operation_with_diagnostics(first, &moved, operation, options) {
214 Ok(outcome) => outcome.value,
215 Err(_) => continue,
216 };
217 if candidate.shells.is_empty() {
219 continue;
220 }
221 if let (Some(va), Some(vb)) = (va, vb) {
226 if let Ok(vr) = solid_signed_volume(&candidate) {
227 if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
228 continue;
229 }
230 }
231 }
232 match crate::oracle::boolean_semantic_disagreement(
236 first, second, operation, &candidate, 400,
237 ) {
238 Ok(report) if report.considered >= 30 && !report.is_flagged() => {
239 if debug {
240 eprintln!(
241 "[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
242 magnitude={magnitude:.3e} offset={offset:?} \
243 (oracle considered={} rate={:.4})",
244 report.considered, report.disagreement_rate
245 );
246 }
247 return Some(candidate);
248 }
249 _ => continue,
250 }
251 }
252 }
253 if debug {
254 eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
255 }
256 None
257}
258
259fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
262 let slack = 1e-2 * (va + vb).max(1.0);
263 match operation {
264 BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
266 BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
268 BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
270 }
271}
272
273fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), String> {
300 fragment.same_sense = !fragment.same_sense;
301 for loop_record in &mut fragment.loops {
302 loop_record.coedges.reverse();
303 for coedge in &mut loop_record.coedges {
304 coedge.forward = !coedge.forward;
305 coedge.pcurve = coedge.pcurve.reversed()?;
306 }
307 }
308 Ok(())
309}
310
311fn build_nary_imprint(
318 operands: &[BrepSolid],
319 options: &ImprintOptions,
320) -> Result<ImprintResultRecord, String> {
321 let mut section_evidence = false;
322 let mut vertices: Vec<ImprintVertex> = Vec::new();
323 let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
324 let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
325 let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
326 let mut next_vertex_id: u64 = 1;
327 let mut next_piece_id: u64 = 1;
328
329 for i in 0..operands.len() {
330 for j in (i + 1)..operands.len() {
331 let pair = build_imprints(&operands[i], &operands[j], options)?;
332 let remap = |operand: u8| -> u8 {
333 if operand == 0 {
334 i as u8
335 } else {
336 j as u8
337 }
338 };
339
340 section_evidence = section_evidence || pair.section_evidence;
341 let mut vertex_map: HashMap<u64, u64> = HashMap::default();
342 for vertex in &pair.vertices {
343 let global = next_vertex_id;
344 next_vertex_id += 1;
345 vertex_map.insert(vertex.id, global);
346 vertices.push(ImprintVertex {
347 id: global,
348 point: vertex.point,
349 });
350 }
351
352 let mut piece_map: HashMap<u64, u64> = HashMap::default();
353 for piece in &pair.pieces {
354 let global = next_piece_id;
355 next_piece_id += 1;
356 piece_map.insert(piece.id, global);
357 let mut mapped = piece.clone();
358 mapped.id = global;
359 mapped.start_vertex_id = *vertex_map
360 .get(&piece.start_vertex_id)
361 .ok_or_else(|| "n-ary imprint: piece references unknown vertex".to_string())?;
362 mapped.end_vertex_id = *vertex_map
363 .get(&piece.end_vertex_id)
364 .ok_or_else(|| "n-ary imprint: piece references unknown vertex".to_string())?;
365 for pcurve in &mut mapped.pcurves {
366 pcurve.operand = remap(pcurve.operand);
367 }
368 mapped.support_faces = [
369 FaceKey {
370 operand: remap(piece.support_faces[0].operand),
371 face_id: piece.support_faces[0].face_id,
372 },
373 FaceKey {
374 operand: remap(piece.support_faces[1].operand),
375 face_id: piece.support_faces[1].face_id,
376 },
377 ];
378 pieces.push(mapped);
379 }
380
381 for entry in &pair.by_face {
382 let list = by_face
383 .entry((remap(entry.operand), entry.face_id))
384 .or_default();
385 for piece_id in &entry.piece_ids {
386 let global = *piece_map.get(piece_id).ok_or_else(|| {
387 "n-ary imprint: by_face references unknown piece".to_string()
388 })?;
389 list.push(global);
390 }
391 }
392
393 for split in &pair.edge_splits {
394 edge_splits
395 .entry((remap(split.operand), split.edge_id))
396 .or_default()
397 .extend(split.parameters.iter().copied());
398 }
399 }
400 }
401
402 Ok(ImprintResultRecord {
403 vertices,
404 pieces,
405 barrier_edges: Vec::new(),
406 section_evidence,
407 by_face: by_face
408 .into_iter()
409 .map(|((operand, face_id), piece_ids)| FaceImprints {
410 operand,
411 face_id,
412 piece_ids,
413 })
414 .collect(),
415 edge_splits: edge_splits
416 .into_iter()
417 .map(|((operand, edge_id), parameters)| EdgeSplitRecord {
418 operand,
419 edge_id,
420 parameters,
421 })
422 .collect(),
423 })
424}
425
426pub fn boolean_operation_nary(
437 operands: &[BrepSolid],
438 operation: BooleanOperation,
439) -> Result<BrepSolid, String> {
440 if operands.is_empty() {
441 return Err("boolean_operation_nary: no operands provided".into());
442 }
443 if operands.len() == 1 {
444 return Ok(operands[0].clone());
445 }
446 if operands.len() > 255 {
447 return Err("boolean_operation_nary: at most 255 operands supported".into());
448 }
449 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
450 let options = BooleanOptions::default();
451
452 let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
456 for operand in &operands[1..] {
457 let candidate = KernelTolerances::for_solid(operand, options.tolerance);
458 if candidate.sew_search > policy.sew_search {
459 policy = candidate;
460 }
461 }
462 policy.check()?;
463 let tolerance = policy.model;
464
465 let mut healed = operands.to_vec();
468 for operand in &mut healed {
469 crate::heal::heal_operands(operand, &policy)?;
470 normalize_operand_band_seams(operand)?;
473 }
474
475 let mut imprint_options = options.imprint.clone();
477 imprint_options.tolerance = tolerance;
478 let imprint = build_nary_imprint(&healed, &imprint_options)?;
479
480 let mut split = Vec::with_capacity(healed.len());
482 for (index, operand) in healed.iter().enumerate() {
483 split.push(apply_edge_splits(operand, index as u8, &imprint)?);
484 }
485 let mut fragments = Vec::with_capacity(split.len());
486 for (index, solid) in split.iter().enumerate() {
487 fragments.push(fragment_solid(solid, index as u8, &imprint)?);
488 }
489
490 let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
492
493 let solids: HashMap<u8, &BrepSolid> = split
495 .iter()
496 .enumerate()
497 .map(|(index, solid)| (index as u8, solid))
498 .collect();
499 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
500 let solid = if options.merge_coplanar_faces
501 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
502 {
503 let merged = merge_same_surface_faces(&solid, tolerance)?;
504 merge_curve_continuation_edges(&merged, tolerance)?
505 } else {
506 solid
507 };
508 let validation = solid.validate_detailed(&policy);
509 if !validation.issues.is_empty() {
510 return Err(format!(
511 "boolean_operation_nary: invalid result: {:?}",
512 validation.issues
513 ));
514 }
515 if debug {
516 if let Ok(report) =
517 crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
518 {
519 if report.is_flagged() {
520 eprintln!(
521 "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
522 report.disagreement_rate,
523 report.disagreements.len(),
524 report.considered,
525 report.sample_disagreement()
526 );
527 }
528 }
529 }
530 Ok(solid)
531}
532
533pub fn boolean_operation_with_diagnostics(
534 first: &BrepSolid,
535 second: &BrepSolid,
536 operation: BooleanOperation,
537 options: &BooleanOptions,
538) -> Result<KernelOutcome<BrepSolid>, String> {
539 let policy = options
540 .tolerances
541 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
542 policy.check()?;
543 let tolerance = policy.model;
544
545 let mut first_owned = first.clone();
554 let mut second_owned = second.clone();
555 crate::heal::heal_operands(&mut first_owned, &policy)?;
556 crate::heal::heal_operands(&mut second_owned, &policy)?;
557 normalize_operand_band_seams(&mut first_owned)?;
562 normalize_operand_band_seams(&mut second_owned)?;
563 let first = &first_owned;
564 let second = &second_owned;
565
566 let mut diagnostics = KernelDiagnostics::default();
567 let operation_started = Instant::now();
568 diagnostics.count_n(
569 "collect.faces",
570 first
571 .shells
572 .iter()
573 .chain(&second.shells)
574 .map(|shell| shell.faces.len() as u64)
575 .sum(),
576 );
577 diagnostics.measure_max("tolerance.model", policy.model);
578 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
579
580 let mut imprint_options = options.imprint.clone();
581 imprint_options.tolerance = tolerance;
582 let stage_started = Instant::now();
583 let imprint = build_imprints(first, second, &imprint_options)?;
584 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
585 for piece in &imprint.pieces {
586 let start = piece.curve.evaluate(piece.t0);
587 let end = piece.curve.evaluate(piece.t1);
588 eprintln!(
589 "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
590 piece.id, piece.support_faces, piece.t0, piece.t1, start, end
591 );
592 }
593 for entry in &imprint.by_face {
594 eprintln!(
595 "by_face operand={} face={} pieces={:?}",
596 entry.operand, entry.face_id, entry.piece_ids
597 );
598 }
599 for split in &imprint.edge_splits {
600 eprintln!(
601 "edge_split operand={} edge={} params={:?}",
602 split.operand, split.edge_id, split.parameters
603 );
604 }
605 }
606 diagnostics.measure_max(
607 "timing.imprint_ms",
608 stage_started.elapsed().as_secs_f64() * 1_000.0,
609 );
610 diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
611 diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
612 diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
613 let stage_started = Instant::now();
614 let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
615 let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
616 diagnostics.measure_max(
617 "timing.edge_split_ms",
618 stage_started.elapsed().as_secs_f64() * 1_000.0,
619 );
620 let stage_started = Instant::now();
621 let fragments_a = fragment_solid(&split_first, 0, &imprint)?;
622 let fragments_b = fragment_solid(&split_second, 1, &imprint)?;
623 diagnostics.measure_max(
624 "timing.fragment_ms",
625 stage_started.elapsed().as_secs_f64() * 1_000.0,
626 );
627 diagnostics.count_n(
628 "fragment.candidates",
629 (fragments_a.len() + fragments_b.len()) as u64,
630 );
631 let a_surface_samples: Vec<Vec3> = fragments_a
636 .iter()
637 .flat_map(|fragment| {
638 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
639 })
640 .collect();
641 let b_surface_samples: Vec<Vec3> = fragments_b
642 .iter()
643 .flat_map(|fragment| {
644 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
645 })
646 .collect();
647 let stage_started = Instant::now();
648 let mut barrier_edges: HashSet<(u8, u64)> = imprint
649 .pieces
650 .iter()
651 .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
652 .collect();
653 barrier_edges.extend(imprint.barrier_edges.iter().copied());
654 for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
660 let minted: Vec<(u8, u64)> = barrier_edges
661 .iter()
662 .filter(|(barrier_operand, _)| *barrier_operand == operand)
663 .filter_map(|(_, edge_id)| split_map.get(edge_id))
664 .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
665 .collect();
666 barrier_edges.extend(minted);
667 }
668 let selected = select_fragments(
669 fragments_a,
670 fragments_b,
671 first,
672 second,
673 operation,
674 tolerance,
675 &barrier_edges,
676 )?;
677 diagnostics.measure_max(
678 "timing.select_ms",
679 stage_started.elapsed().as_secs_f64() * 1_000.0,
680 );
681 diagnostics.count_n("select.fragments", selected.len() as u64);
682 if selected.is_empty()
704 && !imprint.section_evidence
705 && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
706 {
707 let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, String> {
708 for &sample in samples {
709 if classify_point(sample, other, tolerance)?.class == PointClass::In {
710 return Ok(true);
711 }
712 }
713 Ok(false)
714 };
715 let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, String> {
716 for &sample in samples {
717 if classify_point(sample, other, tolerance)?.class == PointClass::Out {
718 return Ok(true);
719 }
720 }
721 Ok(false)
722 };
723 let legitimate = match operation {
724 BooleanOperation::Intersect => {
725 !strictly_inside(&a_surface_samples, second)?
726 && !strictly_inside(&b_surface_samples, first)?
727 }
728 BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
729 BooleanOperation::Union => false,
730 };
731 if legitimate {
732 diagnostics.event(
733 DiagnosticSeverity::Info,
734 KernelStage::Select,
735 "boolean.empty_result",
736 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
737 );
738 diagnostics.measure_max(
739 "timing.total_ms",
740 operation_started.elapsed().as_secs_f64() * 1_000.0,
741 );
742 return Ok(KernelOutcome {
743 value: BrepSolid {
744 id: 0,
745 vertices: Vec::new(),
746 edges: Vec::new(),
747 shells: Vec::new(),
748 genus: 0,
749 },
750 diagnostics,
751 });
752 }
753 }
754 let solids = [(0, &split_first), (1, &split_second)]
755 .into_iter()
756 .collect::<HashMap<_, _>>();
757 let stage_started = Instant::now();
758 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
759 diagnostics.measure_max(
760 "timing.assemble_ms",
761 stage_started.elapsed().as_secs_f64() * 1_000.0,
762 );
763 let stage_started = Instant::now();
764 if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
765 let issues = solid.validate();
766 eprintln!(
767 "pre-merge validation: {} issue(s){}",
768 issues.len(),
769 issues
770 .first()
771 .map(|issue| format!(" — first: {}", issue.message))
772 .unwrap_or_default()
773 );
774 }
775 let solid = if options.merge_coplanar_faces
776 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
777 {
778 let merged = merge_same_surface_faces(&solid, tolerance)?;
779 merge_curve_continuation_edges(&merged, tolerance)?
784 } else {
785 solid
786 };
787 diagnostics.measure_max(
788 "timing.face_merge_ms",
789 stage_started.elapsed().as_secs_f64() * 1_000.0,
790 );
791 let validation = solid.validate_detailed(&policy);
792 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
793 diagnostics.count_n(
794 "validate.wire_warnings",
795 validation.wire_warnings.len() as u64,
796 );
797 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
798 for warning in &validation.wire_warnings {
799 diagnostics.event(
800 DiagnosticSeverity::Warning,
801 KernelStage::Validate,
802 "validate.uv_wire",
803 warning.message.clone(),
804 );
805 }
806 for issue in &validation.issues {
807 diagnostics.event(
808 DiagnosticSeverity::Error,
809 KernelStage::Validate,
810 "validate.brep",
811 issue.message.clone(),
812 );
813 }
814 diagnostics.measure_max(
815 "timing.total_ms",
816 operation_started.elapsed().as_secs_f64() * 1_000.0,
817 );
818 if !validation.issues.is_empty() {
819 return Err(format!(
820 "boolean_operation: invalid result: {:?}",
821 validation.issues
822 ));
823 }
824 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
830 if let Ok(report) =
831 crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
832 {
833 if report.is_flagged() {
834 eprintln!(
835 "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
836 report.disagreement_rate,
837 report.disagreements.len(),
838 report.considered,
839 report.sample_disagreement()
840 );
841 }
842 }
843 let fusable_band = crate::tolerance::assembler_weld(policy.model);
844 let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
845 if !fusables.is_empty() {
846 eprintln!(
847 "[oracle] {} residual fusable(s) after weld; sample {:?}",
848 fusables.len(),
849 fusables.first()
850 );
851 }
852 }
853 Ok(KernelOutcome {
854 value: solid,
855 diagnostics,
856 })
857}
858