1pub(crate) mod profile;
2use crate::arrangement::Vec2;
3use crate::classification::{classify_point, parameter_point_in_face, PolygonClass};
4use crate::fragment::{FaceFragmentRecord, FragmentEdgeSource};
5use crate::imprint::{
6 EdgeSplitRecord, FaceImprints, FaceKey, ImprintOptions, ImprintPieceRecord,
7 ImprintResultRecord, ImprintVertex,
8};
9use crate::tolerance::{assembler_weld, commit_weld};
10use crate::topology::{
11 adaptive_coedge_error, BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord,
12 ShellRecord, VertexRecord,
13};
14use crate::{
15 KernelRefusal, OrRefuse, RefusalClass,
16 apply_edge_splits, apply_edge_splits_with_map, build_imprints, build_pcurve_on_surface,
17 build_pcurve_on_surface_range, classify_surface_pair,
18 fragment_solid, interpolate_curve, merge_curve_continuation_edges,
19 merge_same_surface_faces_excluding, merge_same_surface_faces_scoped,
20 project_point_to_curve, project_point_to_surface, solid_signed_volume, solid_signed_volume_for_sign, AffineTransform,
21 DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage, KernelTolerances, NurbsCurve,
22 PointClass, SolidClassifier, SurfacePairRelation, Vec3,
23};
24use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
25use serde::{Deserialize, Serialize};
26use web_time::Instant;
27
28thread_local! {
29 pub(crate) static CONFORMANCE_REPAIRS: std::cell::Cell<u64> =
35 const { std::cell::Cell::new(0) };
36}
37
38#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
39#[serde(rename_all = "lowercase")]
40pub enum BooleanOperation {
41 Union,
42 Intersect,
43 Subtract,
44}
45
46#[derive(Clone, Debug, Deserialize)]
47pub struct BooleanOptions {
48 #[serde(default = "default_tolerance")]
49 pub tolerance: f64,
50 #[serde(default)]
53 pub tolerances: Option<KernelTolerances>,
54 #[serde(default)]
55 pub imprint: ImprintOptions,
56 #[serde(default = "default_true")]
59 pub merge_coplanar_faces: bool,
60 #[serde(default)]
70 pub keep_unmerged_name_substrs: Vec<String>,
71 #[serde(default = "default_true")]
75 pub untouched_passthrough: bool,
76 #[serde(default = "default_true")]
80 pub disjoint_fast_path: bool,
81 #[serde(default = "default_true")]
91 pub contact_fast_path: bool,
92 #[serde(default)]
100 pub touched_assembly: bool,
101 #[serde(default = "default_true")]
109 pub sector_classification: bool,
110 #[serde(default = "default_true")]
114 pub sheet_trim: bool,
115 #[serde(default)]
119 pub keep_cells: bool,
120 #[serde(default)]
125 pub seam_reseat_later_references: Option<Vec<String>>,
126}
127
128fn touched_assembly(options: &BooleanOptions) -> bool {
130 match std::env::var("BREP_TOUCHED_ASSEMBLY").as_deref() {
131 Ok("0") => false,
132 Ok(_) => true,
133 Err(_) => options.touched_assembly,
134 }
135}
136
137fn disjoint_fast_path(options: &BooleanOptions) -> bool {
139 match std::env::var("BREP_DISJOINT_FAST_PATH").as_deref() {
140 Ok("0") => false,
141 Ok(_) => true,
142 Err(_) => options.disjoint_fast_path,
143 }
144}
145
146fn contact_fast_path(options: &BooleanOptions) -> bool {
148 match std::env::var("BREP_CONTACT_FAST_PATH").as_deref() {
149 Ok("0") => false,
150 Ok(_) => true,
151 Err(_) => options.contact_fast_path,
152 }
153}
154
155fn untouched_passthrough(options: &BooleanOptions) -> bool {
157 match std::env::var("BREP_UNTOUCHED_PASSTHROUGH").as_deref() {
158 Ok("0") => false,
159 Ok(_) => true,
160 Err(_) => options.untouched_passthrough,
161 }
162}
163
164fn default_tolerance() -> f64 {
165 1e-7
166}
167
168fn default_true() -> bool {
169 true
170}
171
172impl Default for BooleanOptions {
173 fn default() -> Self {
174 Self {
175 tolerance: default_tolerance(),
176 tolerances: None,
177 imprint: ImprintOptions::default(),
178 merge_coplanar_faces: true,
179 keep_unmerged_name_substrs: Vec::new(),
180 untouched_passthrough: true,
181 disjoint_fast_path: true,
182 contact_fast_path: true,
183 touched_assembly: false,
184 sector_classification: true,
185 sheet_trim: true,
186 keep_cells: false,
187 seam_reseat_later_references: None,
188 }
189 }
190}
191
192mod select;
193mod sheet;
194mod cellular;
195mod sector;
196mod assemble;
197mod operand_state;
198mod rim;
199mod coincident;
200mod touch;
201mod reseat;
202
203use rim::*;
204use select::*;
205use coincident::*;
206use touch::TouchCensus;
207pub(crate) use assemble::{
208 assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
209 clear_candidate_memo, edge_interior_lies_on, finalize_assembled_solid,
210};
211use assemble::{assemble_fragments_scoped, AssemblyScope};
212#[allow(unused_imports)]
214pub(crate) use assemble::apply_assembly_heal_chain;
215
216pub fn boolean_operation(
217 first: &BrepSolid,
218 second: &BrepSolid,
219 operation: BooleanOperation,
220 options: &BooleanOptions,
221) -> Result<BrepSolid, KernelRefusal> {
222 match boolean_operation_with_diagnostics(first, second, operation, options) {
223 Ok(outcome) => Ok(outcome.value),
224 Err(error) => {
225 if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
233 || !error.class.perturbation_eligible()
234 {
235 return Err(error);
236 }
237 if std::env::var("BREP_OPEN_OPERAND_GATE").as_deref() != Ok("0") {
259 if let Some((operand, open)) = [first, second]
260 .iter()
261 .enumerate()
262 .find_map(|(operand, solid)| {
263 let open = one_use_edges(solid);
264 (open > 0).then_some((operand, open))
265 })
266 {
267 return Err(error.with_message(|message| format!(
268 "{message}; operand {operand} is not a closed shell: {open} of its edges bound \
269 only one face, so no boolean of it encloses a solid and no perturbation \
270 can close it"
271 )));
272 }
273 }
274 if let Some((operand, face_id, deviation, weld)) =
275 planar_operand_off_its_plane(first, second, options)
276 {
277 return Err(error.with_message(|message| format!(
278 "{message}; operand {operand} face {face_id} is a plane whose straight \
279 boundary sits {deviation:.3e} off it (assembly weld {weld:.1e}), which \
280 no perturbation can reconcile — the body's planes must contain their \
281 boundary before it can take part in an exact boolean"
282 )));
283 }
284 if matches!(operation, BooleanOperation::Subtract)
299 && std::env::var("BREP_PINCH_GATE").as_deref() != Ok("0")
300 && subtract_pinches_at_internal_tangency(first, second, options.tolerance)
301 .unwrap_or(false)
302 {
303 return Err(error.with_message(|message| format!(
304 "{message}; internal tangency: the operands touch tangentially with \
305 co-directed normals, so the exact difference pinches to zero thickness \
306 (non-manifold, unrepresentable in a boundary model) — refusing rather \
307 than returning a perturbed sliver"
308 )));
309 }
310 if matches!(error.class, RefusalClass::TangentNodeSingularity)
326 && std::env::var("BREP_TANGENT_NODE_GATE").as_deref() != Ok("0")
327 {
328 return Err(error);
329 }
330 match perturbation_retry(first, second, operation, options) {
331 Ok(solid) => Ok(solid),
332 Err(refused) if refused.is_empty() => Err(error),
333 Err(refused) => {
334 Err(error.with_message(|message| format!("{message}; {}", refused.join("; "))))
335 }
336 }
337 }
338 }
339}
340
341const PINCH_PROBE_STEPS: usize = 8;
346
347const PINCH_ANGULAR_TOLERANCE: f64 = 1e-4;
350
351const PINCH_SEPARATION_STEP: f64 = 1e-2;
355
356fn subtract_pinches_at_internal_tangency(
378 first: &BrepSolid,
379 second: &BrepSolid,
380 tolerance: f64,
381) -> Result<bool, KernelRefusal> {
382 let contact = (tolerance * 100.0).max(1e-12);
385 let probes_a = first
386 .shells
387 .iter()
388 .flat_map(|shell| &shell.faces)
389 .map(PinchProbe::of)
390 .collect::<Result<Vec<_>, _>>()?;
391 let probes_b = second
392 .shells
393 .iter()
394 .flat_map(|shell| &shell.faces)
395 .map(PinchProbe::of)
396 .collect::<Result<Vec<_>, _>>()?;
397 for probe_a in &probes_a {
398 for probe_b in &probes_b {
399 let pad = 1e-2 * probe_a.extent().max(probe_b.extent());
406 if probe_a.separation(probe_b) > contact + pad {
407 continue;
408 }
409 if classify_surface_pair(
416 &probe_a.face.surface,
417 &probe_b.face.surface,
418 tolerance,
419 PINCH_ANGULAR_TOLERANCE,
420 ).or_refuse(KernelStage::Validate, "csg.boolean.mod")?
421 .relation
422 == SurfacePairRelation::Cosurface
423 {
424 continue;
425 }
426 if faces_touch_with_codirected_normals(probe_a, probe_b, contact)? {
427 return Ok(true);
428 }
429 }
430 }
431 Ok(false)
432}
433
434struct PinchProbe<'a> {
437 face: &'a FaceRecord,
438 samples: Vec<(f64, f64, Vec3)>,
440 minimum: [f64; 3],
441 maximum: [f64; 3],
442}
443
444impl<'a> PinchProbe<'a> {
445 fn of(face: &'a FaceRecord) -> Result<Self, KernelRefusal> {
446 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
447 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
448 let steps = PINCH_PROBE_STEPS as f64;
449 let mut samples = Vec::with_capacity((PINCH_PROBE_STEPS + 1).pow(2));
450 let mut minimum = [f64::INFINITY; 3];
451 let mut maximum = [f64::NEG_INFINITY; 3];
452 for i in 0..=PINCH_PROBE_STEPS {
453 let u = u0 + (u1 - u0) * i as f64 / steps;
454 for j in 0..=PINCH_PROBE_STEPS {
455 let v = v0 + (v1 - v0) * j as f64 / steps;
456 let Ok(point) = face.surface.evaluate(u, v) else {
457 continue;
458 };
459 for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
460 minimum[axis] = minimum[axis].min(value);
461 maximum[axis] = maximum[axis].max(value);
462 }
463 samples.push((u, v, point));
464 }
465 }
466 Ok(Self {
467 face,
468 samples,
469 minimum,
470 maximum,
471 })
472 }
473
474 fn extent(&self) -> f64 {
476 (0..3)
477 .map(|axis| self.maximum[axis] - self.minimum[axis])
478 .fold(0.0f64, f64::max)
479 }
480
481 fn separation(&self, other: &Self) -> f64 {
483 let mut gap: f64 = 0.0;
484 for axis in 0..3 {
485 gap = gap.max(self.minimum[axis] - other.maximum[axis]);
486 gap = gap.max(other.minimum[axis] - self.maximum[axis]);
487 }
488 gap
489 }
490}
491
492fn faces_touch_with_codirected_normals(
497 probe_a: &PinchProbe<'_>,
498 probe_b: &PinchProbe<'_>,
499 contact: f64,
500) -> Result<bool, KernelRefusal> {
501 for (probe, other) in [(probe_a, probe_b), (probe_b, probe_a)] {
502 let source = probe.face;
503 let target = other.face;
504 for &(u, v, point) in &probe.samples {
505 let projection = project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?;
506 if projection.distance > contact {
507 continue;
508 }
509 let (Ok(source_normal), Ok(target_normal)) = (
512 source.surface.normal(u, v),
513 target.surface.normal(projection.u, projection.v),
514 ) else {
515 continue;
516 };
517 let source_outward = outward_normal(source_normal, source.same_sense);
518 let target_outward = outward_normal(target_normal, target.same_sense);
519 if source_outward.cross(target_outward).length() > PINCH_ANGULAR_TOLERANCE
520 || source_outward.dot(target_outward) <= 0.0
521 {
522 continue;
523 }
524 if parameter_point_in_face(source, Vec2 { x: u, y: v }, 1e-6).or_refuse(KernelStage::Validate, "parameter_point_in_face")?
525 == PolygonClass::Outside
526 || parameter_point_in_face(
527 target,
528 Vec2 {
529 x: projection.u,
530 y: projection.v,
531 },
532 1e-6,
533 ).or_refuse(KernelStage::Validate, "csg.boolean.mod")? == PolygonClass::Outside
534 {
535 continue;
536 }
537 if contact_separates_locally(source, target, u, v, contact)? {
538 return Ok(true);
539 }
540 }
541 }
542 Ok(false)
543}
544
545fn outward_normal(normal: Vec3, same_sense: bool) -> Vec3 {
546 if same_sense {
547 normal
548 } else {
549 normal.scale(-1.0)
550 }
551}
552
553fn contact_separates_locally(
559 source: &FaceRecord,
560 target: &FaceRecord,
561 u: f64,
562 v: f64,
563 contact: f64,
564) -> Result<bool, KernelRefusal> {
565 let [u0, u1] = source.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
566 let [v0, v1] = source.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
567 let step_u = (u1 - u0) * PINCH_SEPARATION_STEP;
568 let step_v = (v1 - v0) * PINCH_SEPARATION_STEP;
569 for (probe_u, probe_v) in [
570 ((u + step_u).min(u1), v),
571 ((u - step_u).max(u0), v),
572 (u, (v + step_v).min(v1)),
573 (u, (v - step_v).max(v0)),
574 ] {
575 if (probe_u - u).abs() < f64::EPSILON && (probe_v - v).abs() < f64::EPSILON {
576 continue;
577 }
578 let Ok(point) = source.surface.evaluate(probe_u, probe_v) else {
579 continue;
580 };
581 if project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?.distance > contact * 10.0 {
584 return Ok(true);
585 }
586 }
587 Ok(false)
588}
589
590fn perturbation_retry(
644 first: &BrepSolid,
645 second: &BrepSolid,
646 operation: BooleanOperation,
647 options: &BooleanOptions,
648) -> Result<BrepSolid, Vec<String>> {
649 const DIRECTIONS: [[f64; 3]; 4] = [
660 [0.4034, 0.7973, 0.4491],
661 [0.7973, 0.4491, 0.4034],
662 [0.4491, 0.4034, 0.7973],
663 [0.5774, -0.5774, 0.5774],
664 ];
665 const FRACTIONS: [f64; 5] = PERTURBATION_FRACTIONS;
671
672 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
673 let scale = crate::tolerance::solid_scale(first)
674 .max(crate::tolerance::solid_scale(second))
675 .max(1.0);
676
677 let _caller = crate::mass_caller("boolean.perturbation_rescue");
681 let va = solid_signed_volume(first).ok().map(f64::abs);
682 let vb = solid_signed_volume(second).ok().map(f64::abs);
683 let hold_identity = std::env::var("BREP_PERTURB_IDENTITY").as_deref() != Ok("0");
684 let hold_mirror = hold_identity && std::env::var("BREP_PERTURB_MIRROR").as_deref() != Ok("0");
687 let early_exit = std::env::var("BREP_MIRROR_EARLY_EXIT").as_deref() != Ok("0");
690 let mut trends = [MirrorTrend::default(); 4];
691 let mut identity: Option<Option<InclusionExclusion>> = None;
693 let mut refused = Vec::new();
694 let ladder = std::env::var("BREP_DEBUG_PERTURB_LADDER").is_ok();
698 let ladder_start = web_time::Instant::now();
699 if ladder {
700 let measured = inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
701 match &measured {
702 Some(identity) => eprintln!(
703 "[ladder] identity {} = {:.9} from the exact lane's {} ({:.9}); V(first) {:?} V(second) {:?}",
704 identity.formula, identity.expected, identity.reading, identity.read, va, vb
705 ),
706 None => eprintln!("[ladder] no identity: no exact-lane boolean of these operands clears in either order"),
707 }
708 identity = Some(measured);
709 }
710 let rung_miss = |identity: &Option<Option<InclusionExclusion>>, candidate: &BrepSolid| -> String {
711 let volume = body_volume(candidate);
712 match (identity, volume) {
713 (Some(Some(identity)), Some(volume)) => format!(
714 "volume {volume:.9} miss {:+.3e} bar {:.3e}",
715 volume - identity.expected,
716 crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * identity.expected.abs())
717 ),
718 (_, Some(volume)) => format!("volume {volume:.9}"),
719 _ => "no volume".to_string(),
720 }
721 };
722
723 for &fraction in &FRACTIONS {
724 let magnitude = scale * fraction;
725 for (direction, dir) in DIRECTIONS.iter().enumerate() {
726 if trends[direction].closed {
727 if ladder {
728 eprintln!("[ladder] dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}: not tried, its direction is closed");
729 }
730 continue;
731 }
732 let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
733 let translate = match AffineTransform::new([
734 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,
738 ]) {
739 Ok(transform) => transform,
740 Err(_) => continue,
741 };
742 let moved = match crate::transform_brep(second, translate, false) {
743 Ok(solid) => solid,
744 Err(_) => continue,
745 };
746 let rung = format!("dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}");
747 let rung_start = web_time::Instant::now();
748 let candidate =
749 match boolean_operation_with_diagnostics(first, &moved, operation, options) {
750 Ok(outcome) => outcome.value,
751 Err(error) => {
752 if ladder {
753 eprintln!("[ladder] {rung}: exact lane refused {:?}", error.class);
754 }
755 continue;
756 }
757 };
758 let built = rung_start.elapsed();
759 if candidate.shells.is_empty() {
761 if ladder {
762 eprintln!("[ladder] {rung}: empty");
763 }
764 continue;
765 }
766 if let (Some(va), Some(vb)) = (va, vb) {
771 if let Ok(vr) = solid_signed_volume(&candidate) {
772 if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
773 if ladder {
774 eprintln!("[ladder] {rung}: outside the CSG bound, {}", rung_miss(&identity, &candidate));
775 }
776 continue;
777 }
778 }
779 }
780 let bounded = rung_start.elapsed();
785 if hold_identity {
787 let identity = identity.get_or_insert_with(|| {
788 let identity =
789 inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
790 if debug && identity.is_none() {
791 eprintln!(
792 "[perturb] no exact-lane boolean of these operands clears in either \
793 order; no identity to hold"
794 );
795 }
796 identity
797 });
798 if let Some(identity) = identity {
799 let expected = identity.expected;
800 let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * expected.abs());
801 let volume = body_volume(&candidate);
802 let miss = volume.map(|volume| (volume - expected).abs());
803 if !miss.is_some_and(|miss| miss <= bar) {
804 let sentence = format!(
805 "perturbation rung dir={dir:?} fraction={fraction:.1e} \
806 (offset {magnitude:.3e}) refused: its body reads {} against the \
807 inclusion–exclusion identity {} = {expected:.9} (the exact lane's \
808 {} reads {:.9}), a miss of {} beyond the volume drift bar {bar:.3e}",
809 volume.map_or("no volume".to_string(), |volume| format!("{volume:.9}")),
810 identity.formula,
811 identity.reading,
812 identity.read,
813 miss.map_or("unmeasured".to_string(), |miss| format!("{miss:.3e}")),
814 );
815 if debug {
816 eprintln!("[perturb] {sentence}");
817 }
818 if ladder {
819 eprintln!(
820 "[ladder] {rung}: refused by the identity, volume {} miss {} bar {bar:.3e}",
821 volume.map_or("none".to_string(), |volume| format!("{volume:.9}")),
822 volume.map_or("unmeasured".to_string(), |volume| format!("{:+.3e}", volume - expected)),
823 );
824 }
825 refused.push(sentence);
826 continue;
827 }
828 } else if hold_mirror {
829 let mirror_start = web_time::Instant::now();
832 let mirrored = AffineTransform::new([
833 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,
837 ])
838 .ok()
839 .and_then(|transform| crate::transform_brep(second, transform, false).ok());
840 let mirror = mirrored.as_ref().and_then(|mirrored| {
841 inclusion_exclusion_volume(first, mirrored, operation, options, va, vb, true)
842 });
843 let mirror_time = mirror_start.elapsed();
844 let volume = body_volume(&candidate);
845 let mut measured = None;
846 let verdict = match (&mirror, volume) {
847 (Some(mirror), Some(volume)) => {
848 let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * mirror.expected.abs());
849 let miss = 0.5 * (volume - mirror.expected).abs();
850 measured = Some((0.5 * (volume - mirror.expected), bar));
851 if ladder {
852 eprintln!(
853 "[ladder] {rung}: mirror at −offset reads {} = {:.9} (the mirrored {} {:.9}); body {volume:.9}, first-order translation error {:+.3e} bar {bar:.3e}",
854 mirror.formula, mirror.expected, mirror.reading, mirror.read, 0.5 * (volume - mirror.expected)
855 );
856 }
857 (miss > bar).then(|| format!(
858 "perturbation rung dir={dir:?} fraction={fraction:.1e} \
859 (offset {magnitude:.3e}) refused: no exact-lane boolean of the \
860 operands clears, so the rung is held to its mirror: its body reads \
861 {volume:.9} with the second operand moved +offset and {} = {:.9} \
862 with it moved −offset (the mirrored {} reads {:.9}), a first-order \
863 translation error of {miss:.3e} beyond the volume drift bar {bar:.3e}",
864 mirror.formula, mirror.expected, mirror.reading, mirror.read
865 ))
866 }
867 _ => Some(format!(
868 "perturbation rung dir={dir:?} fraction={fraction:.1e} \
869 (offset {magnitude:.3e}) refused: no exact-lane boolean of the operands \
870 clears, and none clears with the second operand moved −offset either, \
871 so nothing checks the rung's body{}",
872 volume.map_or(String::new(), |volume| format!(" ({volume:.9})"))
873 )),
874 };
875 match verdict {
876 None => trends[direction].cleared(),
877 Some(sentence) => {
878 if debug {
879 eprintln!("[perturb] {sentence}");
880 }
881 if ladder {
882 eprintln!(
883 "[ladder] {rung}: refused by its mirror (rung {:.0} ms: built {:.0}, bound {:.0}, mirror {:.0} ms; ladder {:.1} s)",
884 rung_start.elapsed().as_secs_f64() * 1e3,
885 built.as_secs_f64() * 1e3,
886 (bounded - built).as_secs_f64() * 1e3,
887 mirror_time.as_secs_f64() * 1e3,
888 ladder_start.elapsed().as_secs_f64()
889 );
890 }
891 refused.push(sentence);
892 let closure = measured.and_then(|(error, bar)| {
895 let earlier = trends[direction].refused(magnitude, error, early_exit)?;
896 Some((earlier, error, bar))
897 });
898 if let Some(((earlier_offset, earlier_error), error, bar)) = closure {
899 let closed = format!(
900 "perturbation direction dir={dir:?} closed after offset {magnitude:.3e}: its \
901 rungs' first-order translation errors read {earlier_error:+.3e} at offset \
902 {earlier_offset:.3e} and {error:+.3e} at {magnitude:.3e}, one sign and \
903 growing, so no larger offset along it comes back to the volume drift bar \
904 {bar:.3e}"
905 );
906 if debug {
907 eprintln!("[perturb] {closed}");
908 }
909 if ladder {
910 eprintln!("[ladder] {closed}");
911 }
912 refused.push(closed);
913 }
914 continue;
915 }
916 }
917 }
918 }
919 match crate::oracle::boolean_semantic_disagreement(
923 first, second, operation, &candidate, 400,
924 ) {
925 Ok(report) if report.considered >= 30 && !report.is_flagged() => {
926 if ladder {
927 eprintln!(
928 "[ladder] {rung}: accepted (oracle {} considered, rate {:.4}), {}",
929 report.considered, report.disagreement_rate, rung_miss(&identity, &candidate)
930 );
931 }
932 if debug {
933 eprintln!(
934 "[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
935 magnitude={magnitude:.3e} offset={offset:?} \
936 (oracle considered={} rate={:.4})",
937 report.considered, report.disagreement_rate
938 );
939 }
940 return Ok(candidate);
941 }
942 report => {
943 if ladder {
944 let verdict = match &report {
945 Ok(report) => format!("oracle {} considered, rate {:.4}", report.considered, report.disagreement_rate),
946 Err(error) => format!("oracle error {error:?}"),
947 };
948 eprintln!("[ladder] {rung}: refused by the oracle ({verdict}), {}", rung_miss(&identity, &candidate));
949 }
950 continue;
951 }
952 }
953 }
954 }
955 if debug {
956 eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
957 }
958 Err(refused)
959}
960
961const PERTURBATION_FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
964
965#[derive(Clone, Copy, Default)]
989struct MirrorTrend {
990 last: Option<(f64, f64)>,
992 closed: bool,
993}
994
995impl MirrorTrend {
996 fn refused(&mut self, offset: f64, error: f64, close: bool) -> Option<(f64, f64)> {
1001 let earlier = self
1002 .last
1003 .filter(|&(_, earlier)| earlier.signum() == error.signum() && error.abs() >= earlier.abs())
1004 .filter(|_| close);
1005 self.last = Some((offset, error));
1006 self.closed |= earlier.is_some();
1007 earlier
1008 }
1009
1010 fn cleared(&mut self) {
1012 self.last = None;
1013 }
1014}
1015
1016fn one_use_edges(solid: &BrepSolid) -> usize {
1018 let mut uses: HashMap<u64, usize> = HashMap::default();
1019 for coedge in solid
1020 .shells
1021 .iter()
1022 .flat_map(|shell| &shell.faces)
1023 .flat_map(|face| &face.loops)
1024 .flat_map(|loop_record| &loop_record.coedges)
1025 {
1026 *uses.entry(coedge.edge_id).or_default() += 1;
1027 }
1028 solid
1029 .edges
1030 .iter()
1031 .filter(|edge| !edge.degenerate && uses.get(&edge.id) == Some(&1))
1032 .count()
1033}
1034
1035fn body_volume(solid: &BrepSolid) -> Option<f64> {
1037 if solid.shells.is_empty() {
1038 return Some(0.0);
1039 }
1040 solid_signed_volume(solid).ok().map(f64::abs)
1041}
1042
1043struct InclusionExclusion {
1046 formula: &'static str,
1049 reading: &'static str,
1051 read: f64,
1053 expected: f64,
1055}
1056
1057fn inclusion_exclusion_volume(
1068 first: &BrepSolid,
1069 second: &BrepSolid,
1070 operation: BooleanOperation,
1071 options: &BooleanOptions,
1072 va: Option<f64>,
1073 vb: Option<f64>,
1074 own_first: bool,
1075) -> Option<InclusionExclusion> {
1076 use BooleanOperation::{Intersect, Subtract, Union};
1077 let (va, vb) = (va?, vb?);
1078 let own = match operation {
1079 Intersect => (Intersect, false, "first ∩ second"),
1080 Union => (Union, false, "first ∪ second"),
1081 Subtract => (Subtract, false, "first − second"),
1082 };
1083 let readings = [
1084 (Intersect, false, "first ∩ second"),
1085 (Intersect, true, "second ∩ first"),
1086 (Union, false, "first ∪ second"),
1087 (Union, true, "second ∪ first"),
1088 (Subtract, false, "first − second"),
1089 (Subtract, true, "second − first"),
1090 ];
1091 let own_reading = own_first.then_some(own);
1092 let leading = usize::from(own_first);
1093 for (index, (other, swapped, reading)) in own_reading.into_iter().chain(readings).enumerate() {
1094 if index >= leading && !swapped && std::mem::discriminant(&other) == std::mem::discriminant(&operation) {
1095 continue;
1096 }
1097 let (x, y) = if swapped { (second, first) } else { (first, second) };
1098 let Some(read) = boolean_operation_with_diagnostics(x, y, other, options)
1099 .ok()
1100 .and_then(|outcome| body_volume(&outcome.value))
1101 else {
1102 continue;
1103 };
1104 let intersection = match (other, swapped) {
1105 (Intersect, _) => read,
1106 (Union, _) => va + vb - read,
1107 (Subtract, false) => va - read,
1108 (Subtract, true) => vb - read,
1109 };
1110 let (formula, expected) = match operation {
1111 Intersect => ("V(first ∩ second)", intersection),
1112 Union => ("V(first) + V(second) − V(first ∩ second)", va + vb - intersection),
1113 Subtract => ("V(first) − V(first ∩ second)", va - intersection),
1114 };
1115 return Some(InclusionExclusion { formula, reading, read, expected });
1116 }
1117 None
1118}
1119
1120fn planar_operand_off_its_plane(
1129 first: &BrepSolid,
1130 second: &BrepSolid,
1131 options: &BooleanOptions,
1132) -> Option<(u8, u64, f64, f64)> {
1133 let policy = options
1134 .tolerances
1135 .clone()
1136 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
1137 let weld = crate::tolerance::assembler_weld(policy.model);
1138 let mut worst: Option<(u8, u64, f64)> = None;
1139 for (operand, solid) in [(0u8, first), (1u8, second)] {
1140 let vertex_point = |id: u64| solid.vertices.iter().find(|v| v.id == id).map(|v| v.point);
1141 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
1142 let Some(crate::AnalyticSurface::Plane {
1143 origin,
1144 u_dir,
1145 v_dir,
1146 ..
1147 }) = face.surface.analytic()
1148 else {
1149 continue;
1150 };
1151 let [loop_record] = face.loops.as_slice() else {
1152 continue;
1153 };
1154 let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
1155 continue;
1156 };
1157 let mut off_plane = 0.0f64;
1158 let mut straight = true;
1159 for coedge in &loop_record.coedges {
1160 let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
1161 straight = false;
1162 break;
1163 };
1164 let (Some(start), Some(end)) =
1165 (vertex_point(edge.start_vertex_id), vertex_point(edge.end_vertex_id))
1166 else {
1167 straight = false;
1168 break;
1169 };
1170 let chord = end.sub(start);
1171 let length = chord.length();
1172 if edge.degenerate || length <= weld {
1173 straight = false;
1174 break;
1175 }
1176 let direction = chord.scale(1.0 / length);
1177 for fraction in [0.25, 0.5, 0.75] {
1178 let Ok(point) = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction) else {
1179 straight = false;
1180 break;
1181 };
1182 let delta = point.sub(start);
1183 let along = delta.dot(direction);
1184 if delta.sub(direction.scale(along)).length() > weld
1185 || along < -weld
1186 || along > length + weld
1187 {
1188 straight = false;
1189 break;
1190 }
1191 }
1192 if !straight {
1193 break;
1194 }
1195 off_plane = off_plane.max(start.sub(*origin).dot(normal).abs());
1196 }
1197 if straight && off_plane > weld && worst.is_none_or(|(_, _, gap)| off_plane > gap) {
1198 worst = Some((operand, face.id, off_plane));
1199 }
1200 }
1201 }
1202 worst.map(|(operand, face_id, gap)| (operand, face_id, gap, weld))
1203}
1204
1205fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
1208 let slack = 1e-2 * (va + vb).max(1.0);
1209 match operation {
1210 BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
1212 BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
1214 BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
1216 }
1217}
1218
1219fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), KernelRefusal> {
1246 fragment.same_sense = !fragment.same_sense;
1247 for loop_record in &mut fragment.loops {
1248 loop_record.coedges.reverse();
1249 for coedge in &mut loop_record.coedges {
1250 coedge.forward = !coedge.forward;
1251 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Validate, "reversed")?;
1252 }
1253 }
1254 Ok(())
1255}
1256
1257fn build_nary_imprint(
1264 operands: &[BrepSolid],
1265 options: &ImprintOptions,
1266) -> Result<ImprintResultRecord, KernelRefusal> {
1267 let mut section_evidence = false;
1268 let mut vertices: Vec<ImprintVertex> = Vec::new();
1269 let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
1270 let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
1271 let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
1272 let mut next_vertex_id: u64 = 1;
1273 let mut next_piece_id: u64 = 1;
1274
1275 for i in 0..operands.len() {
1276 for j in (i + 1)..operands.len() {
1277 let pair = build_imprints(&operands[i], &operands[j], options)?;
1278 let remap = |operand: u8| -> u8 {
1279 if operand == 0 {
1280 i as u8
1281 } else {
1282 j as u8
1283 }
1284 };
1285
1286 section_evidence = section_evidence || pair.section_evidence;
1287 let mut vertex_map: HashMap<u64, u64> = HashMap::default();
1288 for vertex in &pair.vertices {
1289 let global = next_vertex_id;
1290 next_vertex_id += 1;
1291 vertex_map.insert(vertex.id, global);
1292 vertices.push(ImprintVertex {
1293 id: global,
1294 point: vertex.point,
1295 });
1296 }
1297
1298 let mut piece_map: HashMap<u64, u64> = HashMap::default();
1299 for piece in &pair.pieces {
1300 let global = next_piece_id;
1301 next_piece_id += 1;
1302 piece_map.insert(piece.id, global);
1303 let mut mapped = piece.clone();
1304 mapped.id = global;
1305 mapped.start_vertex_id = *vertex_map
1306 .get(&piece.start_vertex_id)
1307 .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
1308 mapped.end_vertex_id = *vertex_map
1309 .get(&piece.end_vertex_id)
1310 .ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
1311 for pcurve in &mut mapped.pcurves {
1312 pcurve.operand = remap(pcurve.operand);
1313 }
1314 mapped.support_faces = [
1315 FaceKey {
1316 operand: remap(piece.support_faces[0].operand),
1317 face_id: piece.support_faces[0].face_id,
1318 },
1319 FaceKey {
1320 operand: remap(piece.support_faces[1].operand),
1321 face_id: piece.support_faces[1].face_id,
1322 },
1323 ];
1324 pieces.push(mapped);
1325 }
1326
1327 for entry in &pair.by_face {
1328 let list = by_face
1329 .entry((remap(entry.operand), entry.face_id))
1330 .or_default();
1331 for piece_id in &entry.piece_ids {
1332 let global = *piece_map.get(piece_id).ok_or_else(|| {
1333 KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: by_face references unknown piece")
1334 })?;
1335 list.push(global);
1336 }
1337 }
1338
1339 for split in &pair.edge_splits {
1340 edge_splits
1341 .entry((remap(split.operand), split.edge_id))
1342 .or_default()
1343 .extend(split.parameters.iter().copied());
1344 }
1345 }
1346 }
1347
1348 Ok(ImprintResultRecord {
1349 vertices,
1350 pieces,
1351 tangent_nodes: Vec::new(),
1352 barrier_edges: Vec::new(),
1353 section_evidence,
1354 cosurface_pairs: Vec::new(),
1355 tangent_sections: Vec::new(),
1356 by_face: by_face
1357 .into_iter()
1358 .map(|((operand, face_id), piece_ids)| FaceImprints {
1359 operand,
1360 face_id,
1361 piece_ids,
1362 })
1363 .collect(),
1364 edge_splits: edge_splits
1365 .into_iter()
1366 .map(|((operand, edge_id), parameters)| EdgeSplitRecord {
1367 operand,
1368 edge_id,
1369 parameters,
1370 })
1371 .collect(),
1372 })
1373}
1374
1375pub fn boolean_operation_nary(
1386 operands: &[BrepSolid],
1387 operation: BooleanOperation,
1388) -> Result<BrepSolid, KernelRefusal> {
1389 if operands.is_empty() {
1390 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: no operands provided"));
1391 }
1392 if operands.len() == 1 {
1393 return Ok(operands[0].clone());
1394 }
1395 if operands.len() > 255 {
1396 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: at most 255 operands supported"));
1397 }
1398 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
1399 let options = BooleanOptions::default();
1400
1401 let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
1405 for operand in &operands[1..] {
1406 let candidate = KernelTolerances::for_solid(operand, options.tolerance);
1407 if candidate.sew_search > policy.sew_search {
1408 policy = candidate;
1409 }
1410 }
1411 policy.check().or_input(KernelStage::Collect, "tolerances")?;
1412 let tolerance = policy.model;
1413
1414 let mut healed = operands.to_vec();
1417 for operand in &mut healed {
1418 crate::heal::heal_operands(operand, &policy)?;
1419 crate::heal::refit_inherited_trims(operand, &policy)?;
1422 normalize_operand_band_seams(operand)?;
1425 }
1426
1427 let mut imprint_options = options.imprint.clone();
1429 imprint_options.tolerance = tolerance;
1430 let imprint = build_nary_imprint(&healed, &imprint_options)?;
1431
1432 let mut split = Vec::with_capacity(healed.len());
1434 for (index, operand) in healed.iter().enumerate() {
1435 split.push(apply_edge_splits(operand, index as u8, &imprint)?);
1436 }
1437 let mut fragments = Vec::with_capacity(split.len());
1438 for (index, solid) in split.iter().enumerate() {
1439 fragments.push(fragment_solid(solid, index as u8, &imprint)?);
1440 }
1441
1442 let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
1444
1445 let solids: HashMap<u8, &BrepSolid> = split
1447 .iter()
1448 .enumerate()
1449 .map(|(index, solid)| (index as u8, solid))
1450 .collect();
1451 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
1452 let solid = if options.merge_coplanar_faces
1453 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
1454 {
1455 let merged = merge_same_surface_faces_excluding(
1456 &solid,
1457 tolerance,
1458 &options.keep_unmerged_name_substrs,
1459 )?;
1460 merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
1461 } else {
1462 solid
1463 };
1464 let validation = solid.validate_detailed(&policy);
1465 if !validation.issues.is_empty() {
1466 return Err(KernelRefusal::new(
1467 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
1468 KernelStage::Validate,
1469 format!(
1470 "boolean_operation_nary: invalid result: {:?}",
1471 validation.issues
1472 )));
1473 }
1474 if debug {
1475 if let Ok(report) =
1476 crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
1477 {
1478 if report.is_flagged() {
1479 eprintln!(
1480 "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
1481 report.disagreement_rate,
1482 report.disagreements.len(),
1483 report.considered,
1484 report.sample_disagreement()
1485 );
1486 }
1487 }
1488 }
1489 Ok(solid)
1490}
1491
1492fn attribute_to_tangent_node(error: KernelRefusal, nodes: &[Vec3]) -> KernelRefusal {
1523 let Some(node) = nodes.first() else {
1524 return error;
1525 };
1526 if !error.class.perturbation_eligible()
1527 || matches!(error.class, RefusalClass::TangentNodeSingularity)
1528 {
1529 return error;
1530 }
1531 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
1532 eprintln!(
1533 "boolean: tear downstream of the tangent node at ({:.6},{:.6},{:.6}) attributed to it; the stage read: {}",
1534 node.x, node.y, node.z, error.message
1535 );
1536 }
1537 KernelRefusal::new(
1538 RefusalClass::TangentNodeSingularity,
1539 error.stage,
1540 format!(
1541 "boolean: unsupported singular/tangent-node surface intersection \
1542 at the tangent node at ({:.6},{:.6},{:.6}): the section through it did not \
1543 assemble into a closed shell",
1544 node.x, node.y, node.z
1545 ),
1546 )
1547}
1548
1549fn attribute_to_tangent_section(
1571 error: KernelRefusal,
1572 sections: &[crate::imprint::TangentSectionRecord],
1573 origins: &[FaceKey],
1574) -> KernelRefusal {
1575 if sections.is_empty()
1576 || !error.class.perturbation_eligible()
1577 || matches!(error.class, RefusalClass::TangentNodeSingularity)
1578 {
1579 return error;
1580 }
1581 let named = refusal_result_faces(&error.message)
1582 .into_iter()
1583 .filter_map(|id| origins.get(id.checked_sub(1)? as usize).copied())
1584 .collect::<Vec<FaceKey>>();
1585 let involving = |fallback: bool| {
1589 sections.iter().find(|section| {
1590 section.grazing.is_empty() != fallback && named.iter().any(|face| section.faces.contains(face))
1591 })
1592 };
1593 let Some(section) = involving(false).or_else(|| involving(true)) else {
1594 return error;
1595 };
1596 let at = |point: Vec3| {
1599 let shown = |value: f64| if value.abs() < 5e-7 { 0.0 } else { value };
1600 format!("({:.6},{:.6},{:.6})", shown(point.x), shown(point.y), shown(point.z))
1601 };
1602 let pierces = (!section.grazing.is_empty()).then(|| {
1608 let shown = section.grazing.len().min(4);
1609 let mut listed = section.grazing[..shown]
1610 .iter()
1611 .map(|(point, tangency)| format!("{} at |tan·n| = {tangency:.4}", at(*point)))
1612 .collect::<Vec<_>>()
1613 .join(", ");
1614 if section.grazing.len() > shown {
1615 listed.push_str(&format!(" and {} more", section.grazing.len() - shown));
1616 }
1617 listed
1618 });
1619 let pierce_only = section.pierce_only();
1620 let shape = if let (true, Some(pierces)) = (pierce_only, pierces.as_deref()) {
1621 format!(
1622 "cross at a grazing angle: a trim edge pierces the other carrier inside both \
1623 trims at {pierces}, below the marcher's start gate, and no transverse section was \
1624 marched between the pierces — a near-tangent crossing born on a tangent contact \
1625 (a third-order singularity) the imprint does not build yet"
1626 )
1627 } else if section.planar {
1628 format!(
1629 "touch along a tangent curve from {} to {} (length {:.6}, {} stations{}) which the \
1630 imprint does not build yet",
1631 at(section.start),
1632 at(section.end),
1633 section.length,
1634 section.stations,
1635 if section.closed { ", closed" } else { "" }
1636 )
1637 } else {
1638 format!(
1639 "touch along a curve through {} with neither carrier planar, a curved/curved \
1640 tangent contact the lane does not trace yet",
1641 at(section.start)
1642 )
1643 };
1644 let shape = match (pierce_only, pierces) {
1645 (false, Some(pierces)) => format!(
1646 "{shape}; a trim edge also pierces the other carrier inside both trims at {pierces}, \
1647 below the marcher's start gate, with no transverse section marched"
1648 ),
1649 _ => shape,
1650 };
1651 let involved = named
1652 .iter()
1653 .filter(|face| section.faces.contains(face))
1654 .map(|face| format!("{}/{}", face.operand, face.face_id))
1655 .collect::<Vec<_>>()
1656 .join(", ");
1657 KernelRefusal::new(
1658 RefusalClass::TangentNodeSingularity,
1659 error.stage,
1660 format!(
1661 "boolean: unsupported tangent-curve surface intersection between faces {}/{} and {}/{}: \
1662 the carriers {shape}; the refusal names face(s) {involved} the contact involves; {}",
1663 section.first.operand, section.first.face_id, section.second.operand, section.second.face_id,
1664 error.message
1665 ),
1666 )
1667}
1668
1669fn refusal_result_faces(message: &str) -> Vec<u64> {
1674 let mut ids = Vec::new();
1675 let digits_after = |text: &str| -> Option<u64> {
1676 let end = text.find(|c: char| !c.is_ascii_digit()).unwrap_or(text.len());
1677 text[..end].parse().ok()
1678 };
1679 for (index, _) in message.match_indices("face ") {
1680 if let Some(id) = digits_after(&message[index + "face ".len()..]) {
1681 ids.push(id);
1682 }
1683 }
1684 if let Some(start) = message.find("on faces [") {
1685 let rest = &message[start + "on faces [".len()..];
1686 let list = &rest[..rest.find(']').unwrap_or(rest.len())];
1687 for entry in list.split(", ") {
1688 let entry = entry.trim_start();
1689 if let Some(id) = digits_after(entry) {
1690 if entry[id.to_string().len()..].starts_with(" '") {
1691 ids.push(id);
1692 }
1693 }
1694 }
1695 }
1696 ids.sort_unstable();
1697 ids.dedup();
1698 ids
1699}
1700
1701pub fn boolean_operation_with_diagnostics(
1702 first: &BrepSolid,
1703 second: &BrepSolid,
1704 operation: BooleanOperation,
1705 options: &BooleanOptions,
1706) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1707 let _account = profile::Scope::new("boolean.other");
1708 let closed_inputs = matches!(std::env::var("BREP_CLOSED_OPERAND_INPUT").as_deref(), Err(_) | Ok("1") | Ok("on"));
1709 if let (true, Some(later)) = (reseat::seam_reseat_enabled(), &options.seam_reseat_later_references) {
1710 let started = Instant::now();
1711 let (first, second) = reseat::reseat_tangent_seams(first, second, later, options.tolerance);
1712 profile_stage("seam_prepare_ms", started.elapsed().as_secs_f64() * 1000.0);
1713 return boolean_operation_with_diagnostics_impl(&first, &second, operation, options, closed_inputs);
1714 }
1715 boolean_operation_with_diagnostics_impl(first, second, operation, options, closed_inputs)
1716}
1717
1718fn boolean_operation_with_diagnostics_impl(
1719 first: &BrepSolid,
1720 second: &BrepSolid,
1721 operation: BooleanOperation,
1722 options: &BooleanOptions,
1723 closed_inputs: bool,
1724) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1725 let _coedge_memo = crate::topology::CoedgeMemoScope::open(crate::topology::coedge_memo_enabled());
1728 if matches!(operation, BooleanOperation::Intersect)
1731 && [first, second].iter().any(|body| body.kind == crate::BodyKind::Sheet)
1732 && match std::env::var("BREP_SHEET_TRIM").as_deref() {
1733 Ok("1") => true,
1734 Ok("0") => false,
1735 _ => options.sheet_trim,
1736 }
1737 {
1738 return sheet::trim(first, second, options);
1739 }
1740 if matches!(operation, BooleanOperation::Union | BooleanOperation::Subtract) {
1749 if let Some(operand) = [first, second]
1750 .iter()
1751 .position(|solid| solid.kind == crate::BodyKind::Sheet)
1752 {
1753 return Err(KernelRefusal::input(
1754 KernelStage::Collect,
1755 "sheet_operand",
1756 format!(
1757 "boolean: operand {operand} is a Sheet, which encloses no solid; union and \
1758 subtract of a sheet are refused; Intersect trims a Sheet by a Solid"
1759 ),
1760 ));
1761 }
1762 }
1763 let mut outcome = boolean_operation_with_diagnostics_core(first, second, operation, options)?;
1769 let closure_started = closed_inputs.then(Instant::now);
1770 if closed_inputs {
1771 for (operand, solid) in [first, second].into_iter().enumerate() {
1772 let open = one_use_edges(solid);
1773 if open > 0 {
1774 return Err(KernelRefusal::input(KernelStage::Collect, "operand_closure", format!(
1775 "boolean: operand {operand} is not a closed shell: {open} of its edges bound only one face, so its inside is undefined"
1776 )));
1777 }
1778 }
1779 }
1780 if let Some(started) = closure_started {
1781 let ms = started.elapsed().as_secs_f64() * 1000.0;
1782 profile_stage("operand_closure_ms", ms);
1783 outcome.diagnostics.measure_max("timing.operand_closure_ms", ms);
1784 }
1785 Ok(outcome)
1786}
1787
1788fn boolean_operation_with_diagnostics_core(
1789 first: &BrepSolid, second: &BrepSolid, operation: BooleanOperation, options: &BooleanOptions,
1790) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1791
1792 if let Some(offset) = crate::recentre::control_offset_at_entry() {
1795 return boolean_operation_with_diagnostics_at_offset(first, second, operation, options, offset);
1796 }
1797 let mut bounds = operand_bounds(first);
1800 bounds.include(operand_bounds(second));
1801 if let Some(offset) = crate::recentre::guard_offset(bounds) {
1802 crate::recentre::profile_fired("boolean", offset, bounds);
1803 let mut outcome = boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Guard)?;
1804 outcome.diagnostics.count_n("recentre.fired", 1);
1805 outcome.diagnostics.measure_max("recentre.distance", offset.length());
1806 return Ok(outcome);
1807 }
1808 let mut tangent_nodes = Vec::new();
1809 let (mut outcome, soundness_candidates) =
1810 boolean_pipeline(first, second, operation, options, &mut tangent_nodes)
1811 .map_err(|error| attribute_to_tangent_node(error, &tangent_nodes))?;
1812 let stage_started = Instant::now();
1825 let accepted = match &soundness_candidates {
1826 Some(candidates) => crate::accept_sound_against_scoped(&[first, second], outcome.value, "boolean", candidates),
1829 None => crate::accept_sound_against(&[first, second], outcome.value, "boolean"),
1830 };
1831 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1832 profile_stage("soundness_ms", stage_ms);
1833 outcome.diagnostics.measure_max("timing.soundness_ms", stage_ms);
1834 outcome.value = accepted?;
1835 Ok(outcome)
1836}
1837
1838pub fn boolean_operation_with_diagnostics_at_offset(
1842 first: &BrepSolid,
1843 second: &BrepSolid,
1844 operation: BooleanOperation,
1845 options: &BooleanOptions,
1846 offset: Vec3,
1847) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1848 boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Control)
1849}
1850
1851fn boolean_translated(
1853 first: &BrepSolid,
1854 second: &BrepSolid,
1855 operation: BooleanOperation,
1856 options: &BooleanOptions,
1857 offset: Vec3,
1858 kind: crate::recentre::EntryKind,
1859) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1860 let _entry = crate::recentre::TranslatedEntry::open(kind);
1861 let translate = |solid: &BrepSolid, offset: Vec3| {
1862 crate::recentre::translate_solid(solid, offset).map_err(|message| {
1863 KernelRefusal::internal(
1864 KernelStage::Collect,
1865 "translation_control",
1866 format!("boolean: the translation control could not move an operand: {message}"),
1867 )
1868 })
1869 };
1870 let moved_first = translate(first, offset)?;
1871 let moved_second = translate(second, offset)?;
1872 let outcome = boolean_operation_with_diagnostics(&moved_first, &moved_second, operation, options)?;
1873 let value = translate(&outcome.value, offset.scale(-1.0))?;
1874 Ok(KernelOutcome { value, diagnostics: outcome.diagnostics })
1875}
1876
1877fn profile_stage(code: &str, ms: f64) {
1881 let row = match code {
1882 "heal_ms" => Some("boolean.operand_prepare"),
1883 "imprint_ms" => Some("boolean.imprint"),
1884 "edge_split_ms" => Some("boolean.edge_split"),
1885 "fragment_ms" => Some("boolean.fragment"),
1886 "select_ms" => Some("boolean.select"),
1887 "assemble_ms" => Some("boolean.assemble"),
1888 "face_merge_ms" => Some("boolean.face_merge"),
1889 "soundness_ms" => Some("boolean.soundness"),
1890 "operand_closure_ms" => Some("boolean.operand_closure"),
1891 "seam_prepare_ms" => Some("boolean.seam_prepare"),
1892 "final_validate_ms" => Some("boolean.final_validate"),
1893 _ => None,
1894 };
1895 if let Some(row) = row { profile::record(row, ms); }
1896 if std::env::var("BREP_PROFILE").is_ok() {
1897 eprintln!("boolean.{code}={ms:.2}");
1898 }
1899}
1900
1901struct BooleanStageDump {
1906 directory: std::path::PathBuf,
1907 serial: usize,
1908}
1909
1910impl BooleanStageDump {
1911 fn open() -> Option<Self> {
1912 static SERIAL: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1913 let directory = std::env::var("BREP_BOOLEAN_DUMP").ok().filter(|value| !value.is_empty())?;
1914 let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1915 Some(Self { directory: directory.into(), serial })
1916 }
1917
1918 fn write<T: Serialize + ?Sized>(&self, stage: &str, value: &T) {
1919 let path = self.directory.join(format!("{:03}-{stage}.json", self.serial));
1920 match serde_json::to_string(value) {
1921 Ok(text) => {
1922 if let Err(error) = std::fs::write(&path, text) {
1923 eprintln!("BREP_BOOLEAN_DUMP {}: {error}", path.display());
1924 }
1925 }
1926 Err(error) => eprintln!("BREP_BOOLEAN_DUMP {stage}: {error}"),
1927 }
1928 }
1929}
1930
1931fn boolean_policy(
1933 first: &BrepSolid,
1934 second: &BrepSolid,
1935 options: &BooleanOptions,
1936) -> Result<KernelTolerances, KernelRefusal> {
1937 let policy = options
1938 .tolerances
1939 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
1940 policy.check().or_input(KernelStage::Collect, "tolerances")?;
1941 Ok(policy)
1942}
1943
1944fn prepared_operands(
1947 first: &BrepSolid,
1948 second: &BrepSolid,
1949 policy: &KernelTolerances,
1950) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
1951 let stage_started = Instant::now();
1960 let mut first_owned = first.clone();
1961 let mut second_owned = second.clone();
1962 crate::heal::heal_operands(&mut first_owned, policy)?;
1963 crate::heal::heal_operands(&mut second_owned, policy)?;
1964 profile_stage("heal.operands_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
1965 let refit_started = Instant::now();
1969 crate::heal::refit_inherited_trims(&mut first_owned, policy)?;
1970 crate::heal::refit_inherited_trims(&mut second_owned, policy)?;
1971 profile_stage("heal.refit_ms", refit_started.elapsed().as_secs_f64() * 1_000.0);
1972 if std::env::var("BREP_PROFILE").is_ok() {
1973 let (hits, misses) = crate::reading_memo::counts();
1974 eprintln!("boolean.reading_memo hits={hits} misses={misses}");
1975 }
1976 profile_stage("heal_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
1977 normalize_operand_band_seams(&mut first_owned)?;
1982 normalize_operand_band_seams(&mut second_owned)?;
1983 Ok((first_owned, second_owned))
1984}
1985
1986fn boolean_imprint_options(options: &BooleanOptions, tolerance: f64) -> ImprintOptions {
1987 let mut imprint_options = options.imprint.clone();
1988 imprint_options.tolerance = tolerance;
1989 imprint_options
1990}
1991
1992pub fn boolean_split_operands(
2002 first: &BrepSolid,
2003 second: &BrepSolid,
2004 options: &BooleanOptions,
2005) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
2006 let policy = boolean_policy(first, second, options)?;
2007 let (first, second) = prepared_operands(first, second, &policy)?;
2008 let imprint = build_imprints(&first, &second, &boolean_imprint_options(options, policy.model))?;
2009 let (split_first, _) = apply_edge_splits_with_map(&first, 0, &imprint)?;
2010 let (split_second, _) = apply_edge_splits_with_map(&second, 1, &imprint)?;
2011 Ok((split_first, split_second))
2012}
2013
2014fn operand_bounds(solid: &BrepSolid) -> crate::Aabb {
2018 let mut bounds = crate::Aabb::from_points(solid.vertices.iter().map(|vertex| vertex.point));
2019 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2020 match crate::Aabb::from_surface_controls(&face.surface) {
2021 Ok(face_bounds) => bounds.include(face_bounds),
2022 Err(_) => {
2023 return crate::Aabb {
2024 minimum: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
2025 maximum: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
2026 }
2027 }
2028 }
2029 }
2030 bounds
2031}
2032
2033fn line_or_point_contact(first: &BrepSolid, second: &BrepSolid, bounds: [crate::Aabb; 2]) -> bool {
2039 let plain = |solid: &BrepSolid| {
2040 solid.kind == crate::BodyKind::Solid
2041 && solid.cells.is_empty()
2042 && solid.shells.iter().flat_map(|shell| &shell.faces).all(|face| {
2043 face.surface.control_points.iter().flatten().all(|control| control.w > 0.0 && control.w.is_finite())
2044 })
2045 };
2046 let [a, b] = bounds;
2047 let apart = |a_min: f64, a_max: f64, b_min: f64, b_max: f64| a_max <= b_min || b_max <= a_min;
2048 let axes = [
2049 apart(a.minimum.x, a.maximum.x, b.minimum.x, b.maximum.x),
2050 apart(a.minimum.y, a.maximum.y, b.minimum.y, b.maximum.y),
2051 apart(a.minimum.z, a.maximum.z, b.minimum.z, b.maximum.z),
2052 ];
2053 axes.iter().filter(|&&apart| apart).count() >= 2 && plain(first) && plain(second)
2054}
2055
2056fn soundness_candidates(
2061 solid: &BrepSolid,
2062 scope: &AssemblyScope,
2063 bounds: [crate::Aabb; 2],
2064 band: f64,
2065) -> HashSet<u64> {
2066 let mut candidates = HashSet::default();
2067 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2068 if scope.examined.contains(&face.id) {
2069 candidates.insert(face.id);
2070 continue;
2071 }
2072 let Some(&operand) = scope.operand_of.get(&face.id) else {
2073 candidates.insert(face.id);
2074 continue;
2075 };
2076 let other = bounds[usize::from(operand == 0)];
2077 match crate::Aabb::from_surface_controls(&face.surface) {
2078 Ok(face_bounds) if !face_bounds.intersects(other, band) => {}
2079 _ => {
2080 candidates.insert(face.id);
2081 }
2082 }
2083 }
2084 candidates
2085}
2086
2087fn empty_solid() -> BrepSolid {
2089 BrepSolid {
2090 mass_properties_cache: Default::default(),
2091 cells: Default::default(),
2092 id: 0,
2093 vertices: Vec::new(),
2094 edges: Vec::new(),
2095 shells: Vec::new(),
2096 genus: 0,
2097 kind: crate::BodyKind::Solid,
2098 }
2099}
2100
2101fn concatenate_disjoint(first: &BrepSolid, second: &BrepSolid) -> BrepSolid {
2107 let mut vertices = Vec::new();
2108 let mut edges = Vec::new();
2109 let mut shells = Vec::new();
2110 let mut used_face_names = HashSet::<String>::default();
2111 let (mut next_face_id, mut next_loop_id, mut next_coedge_id) = (1u64, 1u64, 1u64);
2112 for operand in [first, second] {
2113 let vertex_base = vertices.len() as u64;
2114 let edge_base = edges.len() as u64;
2115 let mut vertex_ids: HashMap<u64, u64> = HashMap::default();
2116 for (index, vertex) in operand.vertices.iter().enumerate() {
2117 let id = vertex_base + index as u64 + 1;
2118 vertex_ids.insert(vertex.id, id);
2119 vertices.push(VertexRecord { id, point: vertex.point });
2120 }
2121 let mut edge_ids: HashMap<u64, u64> = HashMap::default();
2122 for (index, edge) in operand.edges.iter().enumerate() {
2123 let id = edge_base + index as u64 + 1;
2124 edge_ids.insert(edge.id, id);
2125 edges.push(EdgeRecord {
2126 id,
2127 curve: edge.curve.clone(),
2128 t0: edge.t0,
2129 t1: edge.t1,
2130 start_vertex_id: vertex_ids.get(&edge.start_vertex_id).copied().unwrap_or(edge.start_vertex_id),
2131 end_vertex_id: vertex_ids.get(&edge.end_vertex_id).copied().unwrap_or(edge.end_vertex_id),
2132 degenerate: edge.degenerate,
2133 name: edge.name.clone(),
2134 });
2135 }
2136 for shell in &operand.shells {
2137 let mut faces = Vec::with_capacity(shell.faces.len());
2138 for face in &shell.faces {
2139 let loops = face
2140 .loops
2141 .iter()
2142 .map(|loop_record| {
2143 let coedges = loop_record
2144 .coedges
2145 .iter()
2146 .map(|coedge| {
2147 let id = next_coedge_id;
2148 next_coedge_id += 1;
2149 CoedgeRecord {
2150 id,
2151 edge_id: edge_ids.get(&coedge.edge_id).copied().unwrap_or(coedge.edge_id),
2152 forward: coedge.forward,
2153 pcurve: coedge.pcurve.clone(),
2154 }
2155 })
2156 .collect();
2157 let id = next_loop_id;
2158 next_loop_id += 1;
2159 LoopRecord { id, coedges }
2160 })
2161 .collect();
2162 let name = face.name.clone().map(|base| {
2163 if used_face_names.insert(base.clone()) {
2164 base
2165 } else {
2166 let mut suffix = 1usize;
2167 loop {
2168 let candidate = format!("{base}_{suffix}");
2169 if used_face_names.insert(candidate.clone()) {
2170 break candidate;
2171 }
2172 suffix += 1;
2173 }
2174 }
2175 });
2176 faces.push(FaceRecord {
2177 id: next_face_id,
2178 surface: face.surface.clone(),
2179 same_sense: face.same_sense,
2180 loops,
2181 name,
2182 });
2183 next_face_id += 1;
2184 }
2185 shells.push(ShellRecord { id: shells.len() as u64 + 1, faces });
2186 }
2187 }
2188 BrepSolid {
2189 mass_properties_cache: Default::default(),
2190 cells: Default::default(),
2191 id: 1,
2192 vertices,
2193 edges,
2194 shells,
2195 genus: first.genus + second.genus,
2196 kind: crate::BodyKind::Solid,
2197 }
2198}
2199
2200fn boolean_pipeline(
2203 first: &BrepSolid,
2204 second: &BrepSolid,
2205 operation: BooleanOperation,
2206 options: &BooleanOptions,
2207 tangent_nodes: &mut Vec<Vec3>,
2208) -> Result<(KernelOutcome<BrepSolid>, Option<HashSet<u64>>), KernelRefusal> {
2209 let replaced_operands = operand_state::replace_operands(first, second, operation, options);
2210 let (first, second) = match &replaced_operands {
2211 Some((first, second)) => (first, second),
2212 None => (first, second),
2213 };
2214 let policy = boolean_policy(first, second, options)?;
2215 let tolerance = policy.model;
2216 let bounds = [operand_bounds(first), operand_bounds(second)];
2231 let disjoint = disjoint_fast_path(options) && !bounds[0].intersects(bounds[1], policy.sew_search);
2232 let contact = !disjoint
2233 && contact_fast_path(options)
2234 && options.seam_reseat_later_references.is_some()
2235 && line_or_point_contact(first, second, bounds);
2236 if disjoint || contact {
2237 let operation_started = Instant::now();
2238 let mut diagnostics = KernelDiagnostics::default();
2239 diagnostics.count_n(
2240 "collect.faces",
2241 first.shells.iter().chain(&second.shells).map(|shell| shell.faces.len() as u64).sum(),
2242 );
2243 diagnostics.count(if contact { "boolean.contact_fast_path" } else { "boolean.disjoint_fast_path" });
2244 diagnostics.measure_max("tolerance.model", policy.model);
2245 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
2246 let value = match operation {
2247 BooleanOperation::Union => concatenate_disjoint(first, second),
2248 BooleanOperation::Subtract => first.clone(),
2249 BooleanOperation::Intersect => {
2250 diagnostics.event(
2251 DiagnosticSeverity::Info,
2252 KernelStage::Select,
2253 "boolean.empty_result",
2254 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
2255 );
2256 empty_solid()
2257 }
2258 };
2259 let stage_ms = operation_started.elapsed().as_secs_f64() * 1_000.0;
2260 profile_stage("disjoint_fast_path_ms", stage_ms);
2261 diagnostics.measure_max("timing.total_ms", stage_ms);
2262 return Ok((KernelOutcome { value, diagnostics }, Some(HashSet::default())));
2263 }
2264 let heal_started = Instant::now();
2265 let (first_owned, second_owned) = prepared_operands(first, second, &policy)?;
2266 let heal_ms = heal_started.elapsed().as_secs_f64() * 1_000.0;
2267 let first = &first_owned;
2268 let second = &second_owned;
2269
2270 let mut diagnostics = KernelDiagnostics::default();
2271 diagnostics.measure_max("timing.heal_ms", heal_ms);
2274 let fit_scope = crate::PcurveFitScope::open();
2279 let polyline_scope = crate::PolylineFitScope::open();
2283 let operation_started = Instant::now();
2284 diagnostics.count_n(
2285 "collect.faces",
2286 first
2287 .shells
2288 .iter()
2289 .chain(&second.shells)
2290 .map(|shell| shell.faces.len() as u64)
2291 .sum(),
2292 );
2293 diagnostics.measure_max("tolerance.model", policy.model);
2294 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
2295
2296 let imprint_options = boolean_imprint_options(options, tolerance);
2297 let stage_started = Instant::now();
2298 let imprint = build_imprints(first, second, &imprint_options)?;
2299 tangent_nodes.extend_from_slice(&imprint.tangent_nodes);
2300 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
2301 for piece in &imprint.pieces {
2302 let start = piece.curve.evaluate(piece.t0);
2303 let end = piece.curve.evaluate(piece.t1);
2304 eprintln!(
2305 "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
2306 piece.id, piece.support_faces, piece.t0, piece.t1, start, end
2307 );
2308 }
2309 for entry in &imprint.by_face {
2310 eprintln!(
2311 "by_face operand={} face={} pieces={:?}",
2312 entry.operand, entry.face_id, entry.piece_ids
2313 );
2314 }
2315 for split in &imprint.edge_splits {
2316 eprintln!(
2317 "edge_split operand={} edge={} params={:?}",
2318 split.operand, split.edge_id, split.parameters
2319 );
2320 }
2321 }
2322 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2323 profile_stage("imprint_ms", stage_ms);
2324 diagnostics.measure_max("timing.imprint_ms", stage_ms);
2325 diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
2326 diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
2327 diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
2328 let (march_swap_rescues, clip_swap_rescues) = crate::imprint::take_rescue_firings();
2329 diagnostics.count_n("intersect.march_swap_rescue", march_swap_rescues);
2330 diagnostics.count_n("intersect.clip_swap_rescue", clip_swap_rescues);
2331 let touch = TouchCensus::build([first, second], &imprint);
2332 touch.report(&mut diagnostics);
2333 let stage_started = Instant::now();
2334 let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
2335 let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
2336 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2337 profile_stage("edge_split_ms", stage_ms);
2338 diagnostics.measure_max("timing.edge_split_ms", stage_ms);
2339 let stage_started = Instant::now();
2340 crate::classification::classify_profile_begin();
2341 let fragments_a = crate::fragment::fragment_solid_against(&split_first, 0, &imprint, crate::fragment::bounded_carriers(second).then_some((bounds[1], policy.sew_search)))?;
2342 let fragments_b = crate::fragment::fragment_solid_against(&split_second, 1, &imprint, crate::fragment::bounded_carriers(first).then_some((bounds[0], policy.sew_search)))?;
2343 crate::classification::classify_profile_report("fragment");
2344 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2345 profile_stage("fragment_ms", stage_ms);
2346 diagnostics.measure_max("timing.fragment_ms", stage_ms);
2347 diagnostics.count_n(
2348 "fragment.candidates",
2349 (fragments_a.len() + fragments_b.len()) as u64,
2350 );
2351 touch.report_fragments(&[&fragments_a, &fragments_b], &mut diagnostics);
2352 let a_surface_samples: Vec<Vec3> = fragments_a
2357 .iter()
2358 .flat_map(|fragment| {
2359 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
2360 })
2361 .collect();
2362 let b_surface_samples: Vec<Vec3> = fragments_b
2363 .iter()
2364 .flat_map(|fragment| {
2365 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
2366 })
2367 .collect();
2368 let stage_started = Instant::now();
2369 let mut barrier_edges: HashSet<(u8, u64)> = imprint
2370 .pieces
2371 .iter()
2372 .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
2373 .collect();
2374 barrier_edges.extend(imprint.barrier_edges.iter().copied());
2375 for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
2381 let minted: Vec<(u8, u64)> = barrier_edges
2382 .iter()
2383 .filter(|(barrier_operand, _)| *barrier_operand == operand)
2384 .filter_map(|(_, edge_id)| split_map.get(edge_id))
2385 .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
2386 .collect();
2387 barrier_edges.extend(minted);
2388 }
2389 crate::classification::classify_profile_begin();
2390 let coincident = coincident_pairs(
2391 &fragments_a,
2392 &fragments_b,
2393 first,
2394 second,
2395 &imprint.cosurface_pairs,
2396 tolerance,
2397 )?;
2398 let sector_mode = sector::sector_mode(options);
2399 let mut select_census = sector::SelectCensus::default();
2400 let mut selected = select_fragments(
2401 fragments_a,
2402 fragments_b,
2403 first,
2404 second,
2405 operation,
2406 tolerance,
2407 &barrier_edges,
2408 &imprint,
2409 &coincident,
2410 sector_mode,
2411 &mut select_census,
2412 )?;
2413 let resewn = resew_coincident_rims(
2414 &mut selected,
2415 [&split_first, &split_second],
2416 [first, second],
2417 &imprint.cosurface_pairs,
2418 operation,
2419 tolerance,
2420 )?;
2421 diagnostics.count_n("select.coincident_pairs", coincident.len() as u64);
2422 diagnostics.count_n("select.rims_resewn", resewn as u64);
2423 crate::classification::classify_profile_report("select");
2424 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2425 profile_stage("select_ms", stage_ms);
2426 diagnostics.measure_max("timing.select_ms", stage_ms);
2427 diagnostics.count_n("select.fragments", selected.len() as u64);
2428 diagnostics.count_n("select.sector_eligible", select_census.eligible);
2430 diagnostics.count_n("select.sector_primary", select_census.sector_primary);
2431 diagnostics.count_n("select.sector_fallback", select_census.sector_fallback);
2432 diagnostics.count_n("select.sector_disagree", select_census.disagree);
2433 diagnostics.count_n("select.ray_casts", select_census.ray_casts);
2434 diagnostics.count_n("select.on_votes", select_census.on_votes);
2435 diagnostics.count_n("select.overturns", select_census.overturns);
2436 if sector_mode != sector::SectorMode::Off || std::env::var_os("BREP_SECTOR_CENSUS").is_some() {
2437 sector::sector_census_line(|| {
2438 format!(
2439 "selection mode={sector_mode:?} select_ms={stage_ms:.3} selected={} eligible={} primary={} fallback={} disagree={} ray_casts={} on_votes={} overturns={}",
2440 selected.len(),
2441 select_census.eligible,
2442 select_census.sector_primary,
2443 select_census.sector_fallback,
2444 select_census.disagree,
2445 select_census.ray_casts,
2446 select_census.on_votes,
2447 select_census.overturns
2448 )
2449 });
2450 }
2451 if selected.is_empty()
2473 && !imprint.section_evidence
2474 && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
2475 {
2476 let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
2477 for &sample in samples {
2478 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::In {
2479 return Ok(true);
2480 }
2481 }
2482 Ok(false)
2483 };
2484 let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
2485 for &sample in samples {
2486 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::Out {
2487 return Ok(true);
2488 }
2489 }
2490 Ok(false)
2491 };
2492 let legitimate = match operation {
2493 BooleanOperation::Intersect => {
2494 !strictly_inside(&a_surface_samples, second)?
2495 && !strictly_inside(&b_surface_samples, first)?
2496 }
2497 BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
2498 BooleanOperation::Union => false,
2499 };
2500 if legitimate {
2501 diagnostics.event(
2502 DiagnosticSeverity::Info,
2503 KernelStage::Select,
2504 "boolean.empty_result",
2505 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
2506 );
2507 fit_scope.close().report_into(&mut diagnostics);
2508 polyline_scope.close().report_into(&mut diagnostics);
2509 diagnostics.measure_max(
2510 "timing.total_ms",
2511 operation_started.elapsed().as_secs_f64() * 1_000.0,
2512 );
2513 return Ok((
2514 KernelOutcome {
2515 value: empty_solid(),
2516 diagnostics,
2517 },
2518 None,
2519 ));
2520 }
2521 }
2522 let solids = [(0, &split_first), (1, &split_second)]
2523 .into_iter()
2524 .collect::<HashMap<_, _>>();
2525 let dump = BooleanStageDump::open();
2526 if let Some(dump) = &dump {
2527 dump.write("first", &split_first);
2528 dump.write("second", &split_second);
2529 dump.write("selected", &selected);
2530 }
2531 let stage_started = Instant::now();
2532 let assembled_origins: Vec<FaceKey> = if imprint.tangent_sections.is_empty() {
2536 Vec::new()
2537 } else {
2538 selected
2539 .iter()
2540 .map(|fragment| FaceKey { operand: fragment.operand, face_id: fragment.source_face_id })
2541 .collect()
2542 };
2543 let attributable: Vec<crate::imprint::TangentSectionRecord> = imprint
2550 .tangent_sections
2551 .iter()
2552 .filter(|section| imprint.tangent_nodes.is_empty() || section.grazing.is_empty())
2553 .cloned()
2554 .collect();
2555 let attribute = |error: KernelRefusal| {
2556 attribute_to_tangent_section(error, &attributable, &assembled_origins)
2557 };
2558 let cellular_result = matches!(operation, BooleanOperation::Union)
2559 && cellular::enabled(options)
2560 && cellular::complete_contact(first, second, &selected, tolerance);
2561 let (solid, scope) = if cellular_result {
2562 (assemble::assemble_cellular_fragments(selected, &solids, &imprint, tolerance)
2563 .map_err(attribute)?, None)
2564 } else if untouched_passthrough(options) {
2565 assemble_fragments_scoped(selected, &solids, &imprint, tolerance, &touch, touched_assembly(options))
2566 .map_err(attribute)?
2567 } else {
2568 (assemble_fragments(selected, &solids, &imprint, tolerance).map_err(attribute)?, None)
2569 };
2570 if let Some(scope) = &scope {
2571 diagnostics.count_n("passthrough.faces_examined", scope.examined.len() as u64);
2572 }
2573 if let Some(dump) = &dump {
2574 dump.write("assembled", &solid);
2575 }
2576 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2577 profile_stage("assemble_ms", stage_ms);
2578 diagnostics.measure_max("timing.assemble_ms", stage_ms);
2579 let stage_started = Instant::now();
2580 if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
2581 let issues = solid.validate();
2582 eprintln!(
2583 "pre-merge validation: {} issue(s){}",
2584 issues.len(),
2585 issues
2586 .first()
2587 .map(|issue| format!(" — first: {}", issue.message))
2588 .unwrap_or_default()
2589 );
2590 }
2591 let solid = if solid.kind != crate::BodyKind::Cellular && options.merge_coplanar_faces
2592 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
2593 {
2594 let merged = match &scope {
2595 Some(scope) => merge_same_surface_faces_scoped(
2599 &solid,
2600 tolerance,
2601 &options.keep_unmerged_name_substrs,
2602 &scope.examined,
2603 )?,
2604 None => merge_same_surface_faces_excluding(
2605 &solid,
2606 tolerance,
2607 &options.keep_unmerged_name_substrs,
2608 )?,
2609 };
2610 merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
2615 } else {
2616 solid
2617 };
2618 if let Some(dump) = &dump {
2619 dump.write("merged", &solid);
2620 }
2621 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2622 profile_stage("face_merge_ms", stage_ms);
2623 diagnostics.measure_max("timing.face_merge_ms", stage_ms);
2624 let validation_started = Instant::now();
2625 let validation = solid.validate_detailed_scoped(&policy, scope.as_ref().map(|scope| &scope.examined));
2626 profile_stage("final_validate_ms", validation_started.elapsed().as_secs_f64() * 1000.0);
2627 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
2628 diagnostics.count_n(
2629 "validate.wire_warnings",
2630 validation.wire_warnings.len() as u64,
2631 );
2632 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
2633 for warning in &validation.wire_warnings {
2634 diagnostics.event(
2635 DiagnosticSeverity::Warning,
2636 KernelStage::Validate,
2637 "validate.uv_wire",
2638 warning.message.clone(),
2639 );
2640 }
2641 for issue in &validation.issues {
2642 diagnostics.event(
2643 DiagnosticSeverity::Error,
2644 KernelStage::Validate,
2645 "validate.brep",
2646 issue.message.clone(),
2647 );
2648 }
2649 fit_scope.close().report_into(&mut diagnostics);
2650 polyline_scope.close().report_into(&mut diagnostics);
2651 diagnostics.measure_max(
2652 "timing.total_ms",
2653 operation_started.elapsed().as_secs_f64() * 1_000.0,
2654 );
2655 if !validation.issues.is_empty() {
2656 return Err(KernelRefusal::new(
2657 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
2658 KernelStage::Validate,
2659 format!(
2660 "boolean_operation: invalid result: {:?}",
2661 validation.issues
2662 )));
2663 }
2664 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
2670 if let Ok(report) =
2671 crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
2672 {
2673 if report.is_flagged() {
2674 eprintln!(
2675 "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
2676 report.disagreement_rate,
2677 report.disagreements.len(),
2678 report.considered,
2679 report.sample_disagreement()
2680 );
2681 }
2682 }
2683 let fusable_band = crate::tolerance::assembler_weld(policy.model);
2684 let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
2685 if !fusables.is_empty() {
2686 eprintln!(
2687 "[oracle] {} residual fusable(s) after weld; sample {:?}",
2688 fusables.len(),
2689 fusables.first()
2690 );
2691 }
2692 }
2693 let candidates = scope.as_ref().map(|scope| {
2694 let candidates = soundness_candidates(
2695 &solid,
2696 scope,
2697 [operand_bounds(first), operand_bounds(second)],
2698 policy.sew_search,
2699 );
2700 diagnostics.count_n("passthrough.soundness_candidates", candidates.len() as u64);
2701 crate::soundness::census::profile_operation(&solid, &[first, second], Some(&scope.touched), "boolean");
2702 candidates
2703 });
2704 Ok((
2705 KernelOutcome {
2706 value: solid,
2707 diagnostics,
2708 },
2709 candidates,
2710 ))
2711}
2712