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 boolean_operation_nary_with_options(operands, operation, &BooleanOptions::default())
1390}
1391
1392pub fn boolean_operation_nary_with_options(
1398 operands: &[BrepSolid],
1399 operation: BooleanOperation,
1400 options: &BooleanOptions,
1401) -> Result<BrepSolid, KernelRefusal> {
1402 if operands.is_empty() {
1403 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: no operands provided"));
1404 }
1405 if operands.len() == 1 {
1406 return Ok(operands[0].clone());
1407 }
1408 if operands.len() > 255 {
1409 return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: at most 255 operands supported"));
1410 }
1411 let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
1412
1413 let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
1417 for operand in &operands[1..] {
1418 let candidate = KernelTolerances::for_solid(operand, options.tolerance);
1419 if candidate.sew_search > policy.sew_search {
1420 policy = candidate;
1421 }
1422 }
1423 policy.check().or_input(KernelStage::Collect, "tolerances")?;
1424 let tolerance = policy.model;
1425
1426 let mut healed = operands.to_vec();
1429 for operand in &mut healed {
1430 crate::heal::heal_operands(operand, &policy)?;
1431 crate::heal::refit_inherited_trims(operand, &policy)?;
1434 normalize_operand_band_seams(operand)?;
1437 }
1438
1439 let mut imprint_options = options.imprint.clone();
1441 imprint_options.tolerance = tolerance;
1442 let imprint = build_nary_imprint(&healed, &imprint_options)?;
1443
1444 let mut split = Vec::with_capacity(healed.len());
1446 for (index, operand) in healed.iter().enumerate() {
1447 split.push(apply_edge_splits(operand, index as u8, &imprint)?);
1448 }
1449 let mut fragments = Vec::with_capacity(split.len());
1450 for (index, solid) in split.iter().enumerate() {
1451 fragments.push(fragment_solid(solid, index as u8, &imprint)?);
1452 }
1453
1454 let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
1456
1457 let solids: HashMap<u8, &BrepSolid> = split
1459 .iter()
1460 .enumerate()
1461 .map(|(index, solid)| (index as u8, solid))
1462 .collect();
1463 let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
1464 let solid = if options.merge_coplanar_faces
1465 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
1466 {
1467 let merged = merge_same_surface_faces_excluding(
1468 &solid,
1469 tolerance,
1470 &options.keep_unmerged_name_substrs,
1471 )?;
1472 merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
1473 } else {
1474 solid
1475 };
1476 let validation = solid.validate_detailed(&policy);
1477 if !validation.issues.is_empty() {
1478 return Err(KernelRefusal::new(
1479 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
1480 KernelStage::Validate,
1481 format!(
1482 "boolean_operation_nary: invalid result: {:?}",
1483 validation.issues
1484 )));
1485 }
1486 if debug {
1487 if let Ok(report) =
1488 crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
1489 {
1490 if report.is_flagged() {
1491 eprintln!(
1492 "[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
1493 report.disagreement_rate,
1494 report.disagreements.len(),
1495 report.considered,
1496 report.sample_disagreement()
1497 );
1498 }
1499 }
1500 }
1501 Ok(solid)
1502}
1503
1504fn attribute_to_tangent_node(error: KernelRefusal, nodes: &[Vec3]) -> KernelRefusal {
1535 let Some(node) = nodes.first() else {
1536 return error;
1537 };
1538 if !error.class.perturbation_eligible()
1539 || matches!(error.class, RefusalClass::TangentNodeSingularity)
1540 {
1541 return error;
1542 }
1543 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
1544 eprintln!(
1545 "boolean: tear downstream of the tangent node at ({:.6},{:.6},{:.6}) attributed to it; the stage read: {}",
1546 node.x, node.y, node.z, error.message
1547 );
1548 }
1549 KernelRefusal::new(
1550 RefusalClass::TangentNodeSingularity,
1551 error.stage,
1552 format!(
1553 "boolean: unsupported singular/tangent-node surface intersection \
1554 at the tangent node at ({:.6},{:.6},{:.6}): the section through it did not \
1555 assemble into a closed shell",
1556 node.x, node.y, node.z
1557 ),
1558 )
1559}
1560
1561fn attribute_to_tangent_section(
1583 error: KernelRefusal,
1584 sections: &[crate::imprint::TangentSectionRecord],
1585 origins: &[FaceKey],
1586) -> KernelRefusal {
1587 if sections.is_empty()
1588 || !error.class.perturbation_eligible()
1589 || matches!(error.class, RefusalClass::TangentNodeSingularity)
1590 {
1591 return error;
1592 }
1593 let named = refusal_result_faces(&error.message)
1594 .into_iter()
1595 .filter_map(|id| origins.get(id.checked_sub(1)? as usize).copied())
1596 .collect::<Vec<FaceKey>>();
1597 let involving = |fallback: bool| {
1601 sections.iter().find(|section| {
1602 section.grazing.is_empty() != fallback && named.iter().any(|face| section.faces.contains(face))
1603 })
1604 };
1605 let Some(section) = involving(false).or_else(|| involving(true)) else {
1606 return error;
1607 };
1608 let at = |point: Vec3| {
1611 let shown = |value: f64| if value.abs() < 5e-7 { 0.0 } else { value };
1612 format!("({:.6},{:.6},{:.6})", shown(point.x), shown(point.y), shown(point.z))
1613 };
1614 let pierces = (!section.grazing.is_empty()).then(|| {
1620 let shown = section.grazing.len().min(4);
1621 let mut listed = section.grazing[..shown]
1622 .iter()
1623 .map(|(point, tangency)| format!("{} at |tan·n| = {tangency:.4}", at(*point)))
1624 .collect::<Vec<_>>()
1625 .join(", ");
1626 if section.grazing.len() > shown {
1627 listed.push_str(&format!(" and {} more", section.grazing.len() - shown));
1628 }
1629 listed
1630 });
1631 let pierce_only = section.pierce_only();
1632 let shape = if let (true, Some(pierces)) = (pierce_only, pierces.as_deref()) {
1633 format!(
1634 "cross at a grazing angle: a trim edge pierces the other carrier inside both \
1635 trims at {pierces}, below the marcher's start gate, and no transverse section was \
1636 marched between the pierces — a near-tangent crossing born on a tangent contact \
1637 (a third-order singularity) the imprint does not build yet"
1638 )
1639 } else if section.planar {
1640 format!(
1641 "touch along a tangent curve from {} to {} (length {:.6}, {} stations{}) which the \
1642 imprint does not build yet",
1643 at(section.start),
1644 at(section.end),
1645 section.length,
1646 section.stations,
1647 if section.closed { ", closed" } else { "" }
1648 )
1649 } else {
1650 format!(
1651 "touch along a curve through {} with neither carrier planar, a curved/curved \
1652 tangent contact the lane does not trace yet",
1653 at(section.start)
1654 )
1655 };
1656 let shape = match (pierce_only, pierces) {
1657 (false, Some(pierces)) => format!(
1658 "{shape}; a trim edge also pierces the other carrier inside both trims at {pierces}, \
1659 below the marcher's start gate, with no transverse section marched"
1660 ),
1661 _ => shape,
1662 };
1663 let involved = named
1664 .iter()
1665 .filter(|face| section.faces.contains(face))
1666 .map(|face| format!("{}/{}", face.operand, face.face_id))
1667 .collect::<Vec<_>>()
1668 .join(", ");
1669 KernelRefusal::new(
1670 RefusalClass::TangentNodeSingularity,
1671 error.stage,
1672 format!(
1673 "boolean: unsupported tangent-curve surface intersection between faces {}/{} and {}/{}: \
1674 the carriers {shape}; the refusal names face(s) {involved} the contact involves; {}",
1675 section.first.operand, section.first.face_id, section.second.operand, section.second.face_id,
1676 error.message
1677 ),
1678 )
1679}
1680
1681fn refusal_result_faces(message: &str) -> Vec<u64> {
1686 let mut ids = Vec::new();
1687 let digits_after = |text: &str| -> Option<u64> {
1688 let end = text.find(|c: char| !c.is_ascii_digit()).unwrap_or(text.len());
1689 text[..end].parse().ok()
1690 };
1691 for (index, _) in message.match_indices("face ") {
1692 if let Some(id) = digits_after(&message[index + "face ".len()..]) {
1693 ids.push(id);
1694 }
1695 }
1696 if let Some(start) = message.find("on faces [") {
1697 let rest = &message[start + "on faces [".len()..];
1698 let list = &rest[..rest.find(']').unwrap_or(rest.len())];
1699 for entry in list.split(", ") {
1700 let entry = entry.trim_start();
1701 if let Some(id) = digits_after(entry) {
1702 if entry[id.to_string().len()..].starts_with(" '") {
1703 ids.push(id);
1704 }
1705 }
1706 }
1707 }
1708 ids.sort_unstable();
1709 ids.dedup();
1710 ids
1711}
1712
1713pub fn boolean_operation_with_diagnostics(
1714 first: &BrepSolid,
1715 second: &BrepSolid,
1716 operation: BooleanOperation,
1717 options: &BooleanOptions,
1718) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1719 let _account = profile::Scope::new("boolean.other");
1720 let closed_inputs = matches!(std::env::var("BREP_CLOSED_OPERAND_INPUT").as_deref(), Err(_) | Ok("1") | Ok("on"));
1721 if let (true, Some(later)) = (reseat::seam_reseat_enabled(), &options.seam_reseat_later_references) {
1722 let started = Instant::now();
1723 let (first, second) = reseat::reseat_tangent_seams(first, second, later, options.tolerance);
1724 profile_stage("seam_prepare_ms", started.elapsed().as_secs_f64() * 1000.0);
1725 return boolean_operation_with_diagnostics_impl(&first, &second, operation, options, closed_inputs);
1726 }
1727 boolean_operation_with_diagnostics_impl(first, second, operation, options, closed_inputs)
1728}
1729
1730fn boolean_operation_with_diagnostics_impl(
1731 first: &BrepSolid,
1732 second: &BrepSolid,
1733 operation: BooleanOperation,
1734 options: &BooleanOptions,
1735 closed_inputs: bool,
1736) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1737 let _coedge_memo = crate::topology::CoedgeMemoScope::open(crate::topology::coedge_memo_enabled());
1740 if matches!(operation, BooleanOperation::Intersect)
1743 && [first, second].iter().any(|body| body.kind == crate::BodyKind::Sheet)
1744 && match std::env::var("BREP_SHEET_TRIM").as_deref() {
1745 Ok("1") => true,
1746 Ok("0") => false,
1747 _ => options.sheet_trim,
1748 }
1749 {
1750 return sheet::trim(first, second, options);
1751 }
1752 if matches!(operation, BooleanOperation::Union | BooleanOperation::Subtract) {
1761 if let Some(operand) = [first, second]
1762 .iter()
1763 .position(|solid| solid.kind == crate::BodyKind::Sheet)
1764 {
1765 return Err(KernelRefusal::input(
1766 KernelStage::Collect,
1767 "sheet_operand",
1768 format!(
1769 "boolean: operand {operand} is a Sheet, which encloses no solid; union and \
1770 subtract of a sheet are refused; Intersect trims a Sheet by a Solid"
1771 ),
1772 ));
1773 }
1774 }
1775 let mut outcome = boolean_operation_with_diagnostics_core(first, second, operation, options)?;
1781 let closure_started = closed_inputs.then(Instant::now);
1782 if closed_inputs {
1783 for (operand, solid) in [first, second].into_iter().enumerate() {
1784 let open = one_use_edges(solid);
1785 if open > 0 {
1786 return Err(KernelRefusal::input(KernelStage::Collect, "operand_closure", format!(
1787 "boolean: operand {operand} is not a closed shell: {open} of its edges bound only one face, so its inside is undefined"
1788 )));
1789 }
1790 }
1791 }
1792 if let Some(started) = closure_started {
1793 let ms = started.elapsed().as_secs_f64() * 1000.0;
1794 profile_stage("operand_closure_ms", ms);
1795 outcome.diagnostics.measure_max("timing.operand_closure_ms", ms);
1796 }
1797 Ok(outcome)
1798}
1799
1800fn boolean_operation_with_diagnostics_core(
1801 first: &BrepSolid, second: &BrepSolid, operation: BooleanOperation, options: &BooleanOptions,
1802) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1803
1804 if let Some(offset) = crate::recentre::control_offset_at_entry() {
1807 return boolean_operation_with_diagnostics_at_offset(first, second, operation, options, offset);
1808 }
1809 let mut bounds = operand_bounds(first);
1812 bounds.include(operand_bounds(second));
1813 if let Some(offset) = crate::recentre::guard_offset(bounds) {
1814 crate::recentre::profile_fired("boolean", offset, bounds);
1815 let mut outcome = boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Guard)?;
1816 outcome.diagnostics.count_n("recentre.fired", 1);
1817 outcome.diagnostics.measure_max("recentre.distance", offset.length());
1818 return Ok(outcome);
1819 }
1820 let mut tangent_nodes = Vec::new();
1821 let (mut outcome, soundness_candidates) =
1822 boolean_pipeline(first, second, operation, options, &mut tangent_nodes)
1823 .map_err(|error| attribute_to_tangent_node(error, &tangent_nodes))?;
1824 let stage_started = Instant::now();
1837 let accepted = match &soundness_candidates {
1838 Some(candidates) => crate::accept_sound_against_scoped(&[first, second], outcome.value, "boolean", candidates),
1841 None => crate::accept_sound_against(&[first, second], outcome.value, "boolean"),
1842 };
1843 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
1844 profile_stage("soundness_ms", stage_ms);
1845 outcome.diagnostics.measure_max("timing.soundness_ms", stage_ms);
1846 outcome.value = accepted?;
1847 Ok(outcome)
1848}
1849
1850pub fn boolean_operation_with_diagnostics_at_offset(
1854 first: &BrepSolid,
1855 second: &BrepSolid,
1856 operation: BooleanOperation,
1857 options: &BooleanOptions,
1858 offset: Vec3,
1859) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1860 boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Control)
1861}
1862
1863fn boolean_translated(
1865 first: &BrepSolid,
1866 second: &BrepSolid,
1867 operation: BooleanOperation,
1868 options: &BooleanOptions,
1869 offset: Vec3,
1870 kind: crate::recentre::EntryKind,
1871) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
1872 let _entry = crate::recentre::TranslatedEntry::open(kind);
1873 let translate = |solid: &BrepSolid, offset: Vec3| {
1874 crate::recentre::translate_solid(solid, offset).map_err(|message| {
1875 KernelRefusal::internal(
1876 KernelStage::Collect,
1877 "translation_control",
1878 format!("boolean: the translation control could not move an operand: {message}"),
1879 )
1880 })
1881 };
1882 let moved_first = translate(first, offset)?;
1883 let moved_second = translate(second, offset)?;
1884 let outcome = boolean_operation_with_diagnostics(&moved_first, &moved_second, operation, options)?;
1885 let value = translate(&outcome.value, offset.scale(-1.0))?;
1886 Ok(KernelOutcome { value, diagnostics: outcome.diagnostics })
1887}
1888
1889fn profile_stage(code: &str, ms: f64) {
1893 let row = match code {
1894 "heal_ms" => Some("boolean.operand_prepare"),
1895 "imprint_ms" => Some("boolean.imprint"),
1896 "edge_split_ms" => Some("boolean.edge_split"),
1897 "fragment_ms" => Some("boolean.fragment"),
1898 "select_ms" => Some("boolean.select"),
1899 "assemble_ms" => Some("boolean.assemble"),
1900 "face_merge_ms" => Some("boolean.face_merge"),
1901 "soundness_ms" => Some("boolean.soundness"),
1902 "operand_closure_ms" => Some("boolean.operand_closure"),
1903 "seam_prepare_ms" => Some("boolean.seam_prepare"),
1904 "final_validate_ms" => Some("boolean.final_validate"),
1905 _ => None,
1906 };
1907 if let Some(row) = row { profile::record(row, ms); }
1908 if std::env::var("BREP_PROFILE").is_ok() {
1909 eprintln!("boolean.{code}={ms:.2}");
1910 }
1911}
1912
1913struct BooleanStageDump {
1918 directory: std::path::PathBuf,
1919 serial: usize,
1920}
1921
1922impl BooleanStageDump {
1923 fn open() -> Option<Self> {
1924 static SERIAL: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1925 let directory = std::env::var("BREP_BOOLEAN_DUMP").ok().filter(|value| !value.is_empty())?;
1926 let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1927 Some(Self { directory: directory.into(), serial })
1928 }
1929
1930 fn write<T: Serialize + ?Sized>(&self, stage: &str, value: &T) {
1931 let path = self.directory.join(format!("{:03}-{stage}.json", self.serial));
1932 match serde_json::to_string(value) {
1933 Ok(text) => {
1934 if let Err(error) = std::fs::write(&path, text) {
1935 eprintln!("BREP_BOOLEAN_DUMP {}: {error}", path.display());
1936 }
1937 }
1938 Err(error) => eprintln!("BREP_BOOLEAN_DUMP {stage}: {error}"),
1939 }
1940 }
1941}
1942
1943fn boolean_policy(
1945 first: &BrepSolid,
1946 second: &BrepSolid,
1947 options: &BooleanOptions,
1948) -> Result<KernelTolerances, KernelRefusal> {
1949 let policy = options
1950 .tolerances
1951 .unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
1952 policy.check().or_input(KernelStage::Collect, "tolerances")?;
1953 Ok(policy)
1954}
1955
1956fn prepared_operands(
1959 first: &BrepSolid,
1960 second: &BrepSolid,
1961 policy: &KernelTolerances,
1962) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
1963 let stage_started = Instant::now();
1972 let mut first_owned = first.clone();
1973 let mut second_owned = second.clone();
1974 crate::heal::heal_operands(&mut first_owned, policy)?;
1975 crate::heal::heal_operands(&mut second_owned, policy)?;
1976 profile_stage("heal.operands_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
1977 let refit_started = Instant::now();
1981 crate::heal::refit_inherited_trims(&mut first_owned, policy)?;
1982 crate::heal::refit_inherited_trims(&mut second_owned, policy)?;
1983 profile_stage("heal.refit_ms", refit_started.elapsed().as_secs_f64() * 1_000.0);
1984 if std::env::var("BREP_PROFILE").is_ok() {
1985 let (hits, misses) = crate::reading_memo::counts();
1986 eprintln!("boolean.reading_memo hits={hits} misses={misses}");
1987 }
1988 profile_stage("heal_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
1989 normalize_operand_band_seams(&mut first_owned)?;
1994 normalize_operand_band_seams(&mut second_owned)?;
1995 Ok((first_owned, second_owned))
1996}
1997
1998fn boolean_imprint_options(options: &BooleanOptions, tolerance: f64) -> ImprintOptions {
1999 let mut imprint_options = options.imprint.clone();
2000 imprint_options.tolerance = tolerance;
2001 imprint_options
2002}
2003
2004pub fn boolean_split_operands(
2014 first: &BrepSolid,
2015 second: &BrepSolid,
2016 options: &BooleanOptions,
2017) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
2018 let policy = boolean_policy(first, second, options)?;
2019 let (first, second) = prepared_operands(first, second, &policy)?;
2020 let imprint = build_imprints(&first, &second, &boolean_imprint_options(options, policy.model))?;
2021 let (split_first, _) = apply_edge_splits_with_map(&first, 0, &imprint)?;
2022 let (split_second, _) = apply_edge_splits_with_map(&second, 1, &imprint)?;
2023 Ok((split_first, split_second))
2024}
2025
2026fn operand_bounds(solid: &BrepSolid) -> crate::Aabb {
2030 let mut bounds = crate::Aabb::from_points(solid.vertices.iter().map(|vertex| vertex.point));
2031 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2032 match crate::Aabb::from_surface_controls(&face.surface) {
2033 Ok(face_bounds) => bounds.include(face_bounds),
2034 Err(_) => {
2035 return crate::Aabb {
2036 minimum: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
2037 maximum: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
2038 }
2039 }
2040 }
2041 }
2042 bounds
2043}
2044
2045fn line_or_point_contact(first: &BrepSolid, second: &BrepSolid, bounds: [crate::Aabb; 2]) -> bool {
2051 let plain = |solid: &BrepSolid| {
2052 solid.kind == crate::BodyKind::Solid
2053 && solid.cells.is_empty()
2054 && solid.shells.iter().flat_map(|shell| &shell.faces).all(|face| {
2055 face.surface.control_points.iter().flatten().all(|control| control.w > 0.0 && control.w.is_finite())
2056 })
2057 };
2058 let [a, b] = bounds;
2059 let apart = |a_min: f64, a_max: f64, b_min: f64, b_max: f64| a_max <= b_min || b_max <= a_min;
2060 let axes = [
2061 apart(a.minimum.x, a.maximum.x, b.minimum.x, b.maximum.x),
2062 apart(a.minimum.y, a.maximum.y, b.minimum.y, b.maximum.y),
2063 apart(a.minimum.z, a.maximum.z, b.minimum.z, b.maximum.z),
2064 ];
2065 axes.iter().filter(|&&apart| apart).count() >= 2 && plain(first) && plain(second)
2066}
2067
2068fn soundness_candidates(
2073 solid: &BrepSolid,
2074 scope: &AssemblyScope,
2075 bounds: [crate::Aabb; 2],
2076 band: f64,
2077) -> HashSet<u64> {
2078 let mut candidates = HashSet::default();
2079 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2080 if scope.examined.contains(&face.id) {
2081 candidates.insert(face.id);
2082 continue;
2083 }
2084 let Some(&operand) = scope.operand_of.get(&face.id) else {
2085 candidates.insert(face.id);
2086 continue;
2087 };
2088 let other = bounds[usize::from(operand == 0)];
2089 match crate::Aabb::from_surface_controls(&face.surface) {
2090 Ok(face_bounds) if !face_bounds.intersects(other, band) => {}
2091 _ => {
2092 candidates.insert(face.id);
2093 }
2094 }
2095 }
2096 candidates
2097}
2098
2099fn empty_solid() -> BrepSolid {
2101 BrepSolid {
2102 mass_properties_cache: Default::default(),
2103 cells: Default::default(),
2104 id: 0,
2105 vertices: Vec::new(),
2106 edges: Vec::new(),
2107 shells: Vec::new(),
2108 genus: 0,
2109 kind: crate::BodyKind::Solid,
2110 }
2111}
2112
2113fn concatenate_disjoint(first: &BrepSolid, second: &BrepSolid) -> BrepSolid {
2119 let mut vertices = Vec::new();
2120 let mut edges = Vec::new();
2121 let mut shells = Vec::new();
2122 let mut used_face_names = HashSet::<String>::default();
2123 let (mut next_face_id, mut next_loop_id, mut next_coedge_id) = (1u64, 1u64, 1u64);
2124 for operand in [first, second] {
2125 let vertex_base = vertices.len() as u64;
2126 let edge_base = edges.len() as u64;
2127 let mut vertex_ids: HashMap<u64, u64> = HashMap::default();
2128 for (index, vertex) in operand.vertices.iter().enumerate() {
2129 let id = vertex_base + index as u64 + 1;
2130 vertex_ids.insert(vertex.id, id);
2131 vertices.push(VertexRecord { id, point: vertex.point });
2132 }
2133 let mut edge_ids: HashMap<u64, u64> = HashMap::default();
2134 for (index, edge) in operand.edges.iter().enumerate() {
2135 let id = edge_base + index as u64 + 1;
2136 edge_ids.insert(edge.id, id);
2137 edges.push(EdgeRecord {
2138 id,
2139 curve: edge.curve.clone(),
2140 t0: edge.t0,
2141 t1: edge.t1,
2142 start_vertex_id: vertex_ids.get(&edge.start_vertex_id).copied().unwrap_or(edge.start_vertex_id),
2143 end_vertex_id: vertex_ids.get(&edge.end_vertex_id).copied().unwrap_or(edge.end_vertex_id),
2144 degenerate: edge.degenerate,
2145 name: edge.name.clone(),
2146 });
2147 }
2148 for shell in &operand.shells {
2149 let mut faces = Vec::with_capacity(shell.faces.len());
2150 for face in &shell.faces {
2151 let loops = face
2152 .loops
2153 .iter()
2154 .map(|loop_record| {
2155 let coedges = loop_record
2156 .coedges
2157 .iter()
2158 .map(|coedge| {
2159 let id = next_coedge_id;
2160 next_coedge_id += 1;
2161 CoedgeRecord {
2162 id,
2163 edge_id: edge_ids.get(&coedge.edge_id).copied().unwrap_or(coedge.edge_id),
2164 forward: coedge.forward,
2165 pcurve: coedge.pcurve.clone(),
2166 }
2167 })
2168 .collect();
2169 let id = next_loop_id;
2170 next_loop_id += 1;
2171 LoopRecord { id, coedges }
2172 })
2173 .collect();
2174 let name = face.name.clone().map(|base| {
2175 if used_face_names.insert(base.clone()) {
2176 base
2177 } else {
2178 let mut suffix = 1usize;
2179 loop {
2180 let candidate = format!("{base}_{suffix}");
2181 if used_face_names.insert(candidate.clone()) {
2182 break candidate;
2183 }
2184 suffix += 1;
2185 }
2186 }
2187 });
2188 faces.push(FaceRecord {
2189 id: next_face_id,
2190 surface: face.surface.clone(),
2191 same_sense: face.same_sense,
2192 loops,
2193 name,
2194 });
2195 next_face_id += 1;
2196 }
2197 shells.push(ShellRecord { id: shells.len() as u64 + 1, faces });
2198 }
2199 }
2200 BrepSolid {
2201 mass_properties_cache: Default::default(),
2202 cells: Default::default(),
2203 id: 1,
2204 vertices,
2205 edges,
2206 shells,
2207 genus: first.genus + second.genus,
2208 kind: crate::BodyKind::Solid,
2209 }
2210}
2211
2212fn boolean_pipeline(
2215 first: &BrepSolid,
2216 second: &BrepSolid,
2217 operation: BooleanOperation,
2218 options: &BooleanOptions,
2219 tangent_nodes: &mut Vec<Vec3>,
2220) -> Result<(KernelOutcome<BrepSolid>, Option<HashSet<u64>>), KernelRefusal> {
2221 let replaced_operands = operand_state::replace_operands(first, second, operation, options);
2222 let (first, second) = match &replaced_operands {
2223 Some((first, second)) => (first, second),
2224 None => (first, second),
2225 };
2226 let policy = boolean_policy(first, second, options)?;
2227 let tolerance = policy.model;
2228 let bounds = [operand_bounds(first), operand_bounds(second)];
2243 let disjoint = disjoint_fast_path(options) && !bounds[0].intersects(bounds[1], policy.sew_search);
2244 let contact = !disjoint
2245 && contact_fast_path(options)
2246 && options.seam_reseat_later_references.is_some()
2247 && line_or_point_contact(first, second, bounds);
2248 if disjoint || contact {
2249 let operation_started = Instant::now();
2250 let mut diagnostics = KernelDiagnostics::default();
2251 diagnostics.count_n(
2252 "collect.faces",
2253 first.shells.iter().chain(&second.shells).map(|shell| shell.faces.len() as u64).sum(),
2254 );
2255 diagnostics.count(if contact { "boolean.contact_fast_path" } else { "boolean.disjoint_fast_path" });
2256 diagnostics.measure_max("tolerance.model", policy.model);
2257 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
2258 let value = match operation {
2259 BooleanOperation::Union => concatenate_disjoint(first, second),
2260 BooleanOperation::Subtract => first.clone(),
2261 BooleanOperation::Intersect => {
2262 diagnostics.event(
2263 DiagnosticSeverity::Info,
2264 KernelStage::Select,
2265 "boolean.empty_result",
2266 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
2267 );
2268 empty_solid()
2269 }
2270 };
2271 let stage_ms = operation_started.elapsed().as_secs_f64() * 1_000.0;
2272 profile_stage("disjoint_fast_path_ms", stage_ms);
2273 diagnostics.measure_max("timing.total_ms", stage_ms);
2274 return Ok((KernelOutcome { value, diagnostics }, Some(HashSet::default())));
2275 }
2276 let heal_started = Instant::now();
2277 let (first_owned, second_owned) = prepared_operands(first, second, &policy)?;
2278 let heal_ms = heal_started.elapsed().as_secs_f64() * 1_000.0;
2279 let first = &first_owned;
2280 let second = &second_owned;
2281
2282 let mut diagnostics = KernelDiagnostics::default();
2283 diagnostics.measure_max("timing.heal_ms", heal_ms);
2286 let fit_scope = crate::PcurveFitScope::open();
2291 let polyline_scope = crate::PolylineFitScope::open();
2295 let operation_started = Instant::now();
2296 diagnostics.count_n(
2297 "collect.faces",
2298 first
2299 .shells
2300 .iter()
2301 .chain(&second.shells)
2302 .map(|shell| shell.faces.len() as u64)
2303 .sum(),
2304 );
2305 diagnostics.measure_max("tolerance.model", policy.model);
2306 diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
2307
2308 let imprint_options = boolean_imprint_options(options, tolerance);
2309 let stage_started = Instant::now();
2310 let imprint = build_imprints(first, second, &imprint_options)?;
2311 tangent_nodes.extend_from_slice(&imprint.tangent_nodes);
2312 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
2313 for piece in &imprint.pieces {
2314 let start = piece.curve.evaluate(piece.t0);
2315 let end = piece.curve.evaluate(piece.t1);
2316 eprintln!(
2317 "piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
2318 piece.id, piece.support_faces, piece.t0, piece.t1, start, end
2319 );
2320 }
2321 for entry in &imprint.by_face {
2322 eprintln!(
2323 "by_face operand={} face={} pieces={:?}",
2324 entry.operand, entry.face_id, entry.piece_ids
2325 );
2326 }
2327 for split in &imprint.edge_splits {
2328 eprintln!(
2329 "edge_split operand={} edge={} params={:?}",
2330 split.operand, split.edge_id, split.parameters
2331 );
2332 }
2333 }
2334 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2335 profile_stage("imprint_ms", stage_ms);
2336 diagnostics.measure_max("timing.imprint_ms", stage_ms);
2337 diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
2338 diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
2339 diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
2340 let (march_swap_rescues, clip_swap_rescues) = crate::imprint::take_rescue_firings();
2341 diagnostics.count_n("intersect.march_swap_rescue", march_swap_rescues);
2342 diagnostics.count_n("intersect.clip_swap_rescue", clip_swap_rescues);
2343 let touch = TouchCensus::build([first, second], &imprint);
2344 touch.report(&mut diagnostics);
2345 let stage_started = Instant::now();
2346 let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
2347 let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
2348 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2349 profile_stage("edge_split_ms", stage_ms);
2350 diagnostics.measure_max("timing.edge_split_ms", stage_ms);
2351 let stage_started = Instant::now();
2352 crate::classification::classify_profile_begin();
2353 let fragments_a = crate::fragment::fragment_solid_against(&split_first, 0, &imprint, crate::fragment::bounded_carriers(second).then_some((bounds[1], policy.sew_search)))?;
2354 let fragments_b = crate::fragment::fragment_solid_against(&split_second, 1, &imprint, crate::fragment::bounded_carriers(first).then_some((bounds[0], policy.sew_search)))?;
2355 crate::classification::classify_profile_report("fragment");
2356 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2357 profile_stage("fragment_ms", stage_ms);
2358 diagnostics.measure_max("timing.fragment_ms", stage_ms);
2359 diagnostics.count_n(
2360 "fragment.candidates",
2361 (fragments_a.len() + fragments_b.len()) as u64,
2362 );
2363 touch.report_fragments(&[&fragments_a, &fragments_b], &mut diagnostics);
2364 let a_surface_samples: Vec<Vec3> = fragments_a
2369 .iter()
2370 .flat_map(|fragment| {
2371 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
2372 })
2373 .collect();
2374 let b_surface_samples: Vec<Vec3> = fragments_b
2375 .iter()
2376 .flat_map(|fragment| {
2377 std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
2378 })
2379 .collect();
2380 let stage_started = Instant::now();
2381 let mut barrier_edges: HashSet<(u8, u64)> = imprint
2382 .pieces
2383 .iter()
2384 .filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
2385 .collect();
2386 barrier_edges.extend(imprint.barrier_edges.iter().copied());
2387 for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
2393 let minted: Vec<(u8, u64)> = barrier_edges
2394 .iter()
2395 .filter(|(barrier_operand, _)| *barrier_operand == operand)
2396 .filter_map(|(_, edge_id)| split_map.get(edge_id))
2397 .flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
2398 .collect();
2399 barrier_edges.extend(minted);
2400 }
2401 crate::classification::classify_profile_begin();
2402 let coincident = coincident_pairs(
2403 &fragments_a,
2404 &fragments_b,
2405 first,
2406 second,
2407 &imprint.cosurface_pairs,
2408 tolerance,
2409 )?;
2410 let sector_mode = sector::sector_mode(options);
2411 let mut select_census = sector::SelectCensus::default();
2412 let mut selected = select_fragments(
2413 fragments_a,
2414 fragments_b,
2415 first,
2416 second,
2417 operation,
2418 tolerance,
2419 &barrier_edges,
2420 &imprint,
2421 &coincident,
2422 sector_mode,
2423 &mut select_census,
2424 )?;
2425 let resewn = resew_coincident_rims(
2426 &mut selected,
2427 [&split_first, &split_second],
2428 [first, second],
2429 &imprint.cosurface_pairs,
2430 operation,
2431 tolerance,
2432 )?;
2433 diagnostics.count_n("select.coincident_pairs", coincident.len() as u64);
2434 diagnostics.count_n("select.rims_resewn", resewn as u64);
2435 crate::classification::classify_profile_report("select");
2436 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2437 profile_stage("select_ms", stage_ms);
2438 diagnostics.measure_max("timing.select_ms", stage_ms);
2439 diagnostics.count_n("select.fragments", selected.len() as u64);
2440 diagnostics.count_n("select.sector_eligible", select_census.eligible);
2442 diagnostics.count_n("select.sector_primary", select_census.sector_primary);
2443 diagnostics.count_n("select.sector_fallback", select_census.sector_fallback);
2444 diagnostics.count_n("select.sector_disagree", select_census.disagree);
2445 diagnostics.count_n("select.ray_casts", select_census.ray_casts);
2446 diagnostics.count_n("select.on_votes", select_census.on_votes);
2447 diagnostics.count_n("select.overturns", select_census.overturns);
2448 if sector_mode != sector::SectorMode::Off || std::env::var_os("BREP_SECTOR_CENSUS").is_some() {
2449 sector::sector_census_line(|| {
2450 format!(
2451 "selection mode={sector_mode:?} select_ms={stage_ms:.3} selected={} eligible={} primary={} fallback={} disagree={} ray_casts={} on_votes={} overturns={}",
2452 selected.len(),
2453 select_census.eligible,
2454 select_census.sector_primary,
2455 select_census.sector_fallback,
2456 select_census.disagree,
2457 select_census.ray_casts,
2458 select_census.on_votes,
2459 select_census.overturns
2460 )
2461 });
2462 }
2463 if selected.is_empty()
2485 && !imprint.section_evidence
2486 && std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
2487 {
2488 let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
2489 for &sample in samples {
2490 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::In {
2491 return Ok(true);
2492 }
2493 }
2494 Ok(false)
2495 };
2496 let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
2497 for &sample in samples {
2498 if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::Out {
2499 return Ok(true);
2500 }
2501 }
2502 Ok(false)
2503 };
2504 let legitimate = match operation {
2505 BooleanOperation::Intersect => {
2506 !strictly_inside(&a_surface_samples, second)?
2507 && !strictly_inside(&b_surface_samples, first)?
2508 }
2509 BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
2510 BooleanOperation::Union => false,
2511 };
2512 if legitimate {
2513 diagnostics.event(
2514 DiagnosticSeverity::Info,
2515 KernelStage::Select,
2516 "boolean.empty_result",
2517 format!("{operation:?} of witnessed-non-overlapping operands is empty"),
2518 );
2519 fit_scope.close().report_into(&mut diagnostics);
2520 polyline_scope.close().report_into(&mut diagnostics);
2521 diagnostics.measure_max(
2522 "timing.total_ms",
2523 operation_started.elapsed().as_secs_f64() * 1_000.0,
2524 );
2525 return Ok((
2526 KernelOutcome {
2527 value: empty_solid(),
2528 diagnostics,
2529 },
2530 None,
2531 ));
2532 }
2533 }
2534 let solids = [(0, &split_first), (1, &split_second)]
2535 .into_iter()
2536 .collect::<HashMap<_, _>>();
2537 let dump = BooleanStageDump::open();
2538 if let Some(dump) = &dump {
2539 dump.write("first", &split_first);
2540 dump.write("second", &split_second);
2541 dump.write("selected", &selected);
2542 }
2543 let stage_started = Instant::now();
2544 let assembled_origins: Vec<FaceKey> = if imprint.tangent_sections.is_empty() {
2548 Vec::new()
2549 } else {
2550 selected
2551 .iter()
2552 .map(|fragment| FaceKey { operand: fragment.operand, face_id: fragment.source_face_id })
2553 .collect()
2554 };
2555 let attributable: Vec<crate::imprint::TangentSectionRecord> = imprint
2562 .tangent_sections
2563 .iter()
2564 .filter(|section| imprint.tangent_nodes.is_empty() || section.grazing.is_empty())
2565 .cloned()
2566 .collect();
2567 let attribute = |error: KernelRefusal| {
2568 attribute_to_tangent_section(error, &attributable, &assembled_origins)
2569 };
2570 let cellular_result = matches!(operation, BooleanOperation::Union)
2571 && cellular::enabled(options)
2572 && cellular::complete_contact(first, second, &selected, tolerance);
2573 let (solid, scope) = if cellular_result {
2574 (assemble::assemble_cellular_fragments(selected, &solids, &imprint, tolerance)
2575 .map_err(attribute)?, None)
2576 } else if untouched_passthrough(options) {
2577 assemble_fragments_scoped(selected, &solids, &imprint, tolerance, &touch, touched_assembly(options))
2578 .map_err(attribute)?
2579 } else {
2580 (assemble_fragments(selected, &solids, &imprint, tolerance).map_err(attribute)?, None)
2581 };
2582 if let Some(scope) = &scope {
2583 diagnostics.count_n("passthrough.faces_examined", scope.examined.len() as u64);
2584 }
2585 if let Some(dump) = &dump {
2586 dump.write("assembled", &solid);
2587 }
2588 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2589 profile_stage("assemble_ms", stage_ms);
2590 diagnostics.measure_max("timing.assemble_ms", stage_ms);
2591 let stage_started = Instant::now();
2592 if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
2593 let issues = solid.validate();
2594 eprintln!(
2595 "pre-merge validation: {} issue(s){}",
2596 issues.len(),
2597 issues
2598 .first()
2599 .map(|issue| format!(" — first: {}", issue.message))
2600 .unwrap_or_default()
2601 );
2602 }
2603 let solid = if solid.kind != crate::BodyKind::Cellular && options.merge_coplanar_faces
2604 && std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
2605 {
2606 let merged = match &scope {
2607 Some(scope) => merge_same_surface_faces_scoped(
2611 &solid,
2612 tolerance,
2613 &options.keep_unmerged_name_substrs,
2614 &scope.examined,
2615 )?,
2616 None => merge_same_surface_faces_excluding(
2617 &solid,
2618 tolerance,
2619 &options.keep_unmerged_name_substrs,
2620 )?,
2621 };
2622 merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
2627 } else {
2628 solid
2629 };
2630 if let Some(dump) = &dump {
2631 dump.write("merged", &solid);
2632 }
2633 let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
2634 profile_stage("face_merge_ms", stage_ms);
2635 diagnostics.measure_max("timing.face_merge_ms", stage_ms);
2636 let validation_started = Instant::now();
2637 let validation = solid.validate_detailed_scoped(&policy, scope.as_ref().map(|scope| &scope.examined));
2638 profile_stage("final_validate_ms", validation_started.elapsed().as_secs_f64() * 1000.0);
2639 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
2640 diagnostics.count_n(
2641 "validate.wire_warnings",
2642 validation.wire_warnings.len() as u64,
2643 );
2644 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
2645 for warning in &validation.wire_warnings {
2646 diagnostics.event(
2647 DiagnosticSeverity::Warning,
2648 KernelStage::Validate,
2649 "validate.uv_wire",
2650 warning.message.clone(),
2651 );
2652 }
2653 for issue in &validation.issues {
2654 diagnostics.event(
2655 DiagnosticSeverity::Error,
2656 KernelStage::Validate,
2657 "validate.brep",
2658 issue.message.clone(),
2659 );
2660 }
2661 fit_scope.close().report_into(&mut diagnostics);
2662 polyline_scope.close().report_into(&mut diagnostics);
2663 diagnostics.measure_max(
2664 "timing.total_ms",
2665 operation_started.elapsed().as_secs_f64() * 1_000.0,
2666 );
2667 if !validation.issues.is_empty() {
2668 return Err(KernelRefusal::new(
2669 RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
2670 KernelStage::Validate,
2671 format!(
2672 "boolean_operation: invalid result: {:?}",
2673 validation.issues
2674 )));
2675 }
2676 if std::env::var("BREP_DEBUG_BOOL").is_ok() {
2682 if let Ok(report) =
2683 crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
2684 {
2685 if report.is_flagged() {
2686 eprintln!(
2687 "[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
2688 report.disagreement_rate,
2689 report.disagreements.len(),
2690 report.considered,
2691 report.sample_disagreement()
2692 );
2693 }
2694 }
2695 let fusable_band = crate::tolerance::assembler_weld(policy.model);
2696 let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
2697 if !fusables.is_empty() {
2698 eprintln!(
2699 "[oracle] {} residual fusable(s) after weld; sample {:?}",
2700 fusables.len(),
2701 fusables.first()
2702 );
2703 }
2704 }
2705 let candidates = scope.as_ref().map(|scope| {
2706 let candidates = soundness_candidates(
2707 &solid,
2708 scope,
2709 [operand_bounds(first), operand_bounds(second)],
2710 policy.sew_search,
2711 );
2712 diagnostics.count_n("passthrough.soundness_candidates", candidates.len() as u64);
2713 crate::soundness::census::profile_operation(&solid, &[first, second], Some(&scope.touched), "boolean");
2714 candidates
2715 });
2716 Ok((
2717 KernelOutcome {
2718 value: solid,
2719 diagnostics,
2720 },
2721 candidates,
2722 ))
2723}
2724