pub(crate) mod profile;
use crate::arrangement::Vec2;
use crate::classification::{classify_point, parameter_point_in_face, PolygonClass};
use crate::fragment::{FaceFragmentRecord, FragmentEdgeSource};
use crate::imprint::{
EdgeSplitRecord, FaceImprints, FaceKey, ImprintOptions, ImprintPieceRecord,
ImprintResultRecord, ImprintVertex,
};
use crate::tolerance::{assembler_weld, commit_weld};
use crate::topology::{
adaptive_coedge_error, BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord,
ShellRecord, VertexRecord,
};
use crate::{
KernelRefusal, OrRefuse, RefusalClass,
apply_edge_splits, apply_edge_splits_with_map, build_imprints, build_pcurve_on_surface,
build_pcurve_on_surface_range, classify_surface_pair,
fragment_solid, interpolate_curve, merge_curve_continuation_edges,
merge_same_surface_faces_excluding, merge_same_surface_faces_scoped,
project_point_to_curve, project_point_to_surface, solid_signed_volume, solid_signed_volume_for_sign, AffineTransform,
DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage, KernelTolerances, NurbsCurve,
PointClass, SolidClassifier, SurfacePairRelation, Vec3,
};
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use serde::{Deserialize, Serialize};
use web_time::Instant;
thread_local! {
pub(crate) static CONFORMANCE_REPAIRS: std::cell::Cell<u64> =
const { std::cell::Cell::new(0) };
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum BooleanOperation {
Union,
Intersect,
Subtract,
}
#[derive(Clone, Debug, Deserialize)]
pub struct BooleanOptions {
#[serde(default = "default_tolerance")]
pub tolerance: f64,
#[serde(default)]
pub tolerances: Option<KernelTolerances>,
#[serde(default)]
pub imprint: ImprintOptions,
#[serde(default = "default_true")]
pub merge_coplanar_faces: bool,
#[serde(default)]
pub keep_unmerged_name_substrs: Vec<String>,
#[serde(default = "default_true")]
pub untouched_passthrough: bool,
#[serde(default = "default_true")]
pub disjoint_fast_path: bool,
#[serde(default = "default_true")]
pub contact_fast_path: bool,
#[serde(default)]
pub touched_assembly: bool,
#[serde(default = "default_true")]
pub sector_classification: bool,
#[serde(default = "default_true")]
pub sheet_trim: bool,
#[serde(default)]
pub keep_cells: bool,
#[serde(default)]
pub seam_reseat_later_references: Option<Vec<String>>,
}
fn touched_assembly(options: &BooleanOptions) -> bool {
match std::env::var("BREP_TOUCHED_ASSEMBLY").as_deref() {
Ok("0") => false,
Ok(_) => true,
Err(_) => options.touched_assembly,
}
}
fn disjoint_fast_path(options: &BooleanOptions) -> bool {
match std::env::var("BREP_DISJOINT_FAST_PATH").as_deref() {
Ok("0") => false,
Ok(_) => true,
Err(_) => options.disjoint_fast_path,
}
}
fn contact_fast_path(options: &BooleanOptions) -> bool {
match std::env::var("BREP_CONTACT_FAST_PATH").as_deref() {
Ok("0") => false,
Ok(_) => true,
Err(_) => options.contact_fast_path,
}
}
fn untouched_passthrough(options: &BooleanOptions) -> bool {
match std::env::var("BREP_UNTOUCHED_PASSTHROUGH").as_deref() {
Ok("0") => false,
Ok(_) => true,
Err(_) => options.untouched_passthrough,
}
}
fn default_tolerance() -> f64 {
1e-7
}
fn default_true() -> bool {
true
}
impl Default for BooleanOptions {
fn default() -> Self {
Self {
tolerance: default_tolerance(),
tolerances: None,
imprint: ImprintOptions::default(),
merge_coplanar_faces: true,
keep_unmerged_name_substrs: Vec::new(),
untouched_passthrough: true,
disjoint_fast_path: true,
contact_fast_path: true,
touched_assembly: false,
sector_classification: true,
sheet_trim: true,
keep_cells: false,
seam_reseat_later_references: None,
}
}
}
mod select;
mod sheet;
mod cellular;
mod sector;
mod assemble;
mod operand_state;
mod rim;
mod coincident;
mod touch;
mod reseat;
use rim::*;
use select::*;
use coincident::*;
use touch::TouchCensus;
pub(crate) use assemble::{
assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
clear_candidate_memo, edge_interior_lies_on, finalize_assembled_solid,
};
use assemble::{assemble_fragments_scoped, AssemblyScope};
#[allow(unused_imports)]
pub(crate) use assemble::apply_assembly_heal_chain;
pub fn boolean_operation(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
) -> Result<BrepSolid, KernelRefusal> {
match boolean_operation_with_diagnostics(first, second, operation, options) {
Ok(outcome) => Ok(outcome.value),
Err(error) => {
if std::env::var("BREP_NO_PERTURB").as_deref() == Ok("1")
|| !error.class.perturbation_eligible()
{
return Err(error);
}
if std::env::var("BREP_OPEN_OPERAND_GATE").as_deref() != Ok("0") {
if let Some((operand, open)) = [first, second]
.iter()
.enumerate()
.find_map(|(operand, solid)| {
let open = one_use_edges(solid);
(open > 0).then_some((operand, open))
})
{
return Err(error.with_message(|message| format!(
"{message}; operand {operand} is not a closed shell: {open} of its edges bound \
only one face, so no boolean of it encloses a solid and no perturbation \
can close it"
)));
}
}
if let Some((operand, face_id, deviation, weld)) =
planar_operand_off_its_plane(first, second, options)
{
return Err(error.with_message(|message| format!(
"{message}; operand {operand} face {face_id} is a plane whose straight \
boundary sits {deviation:.3e} off it (assembly weld {weld:.1e}), which \
no perturbation can reconcile — the body's planes must contain their \
boundary before it can take part in an exact boolean"
)));
}
if matches!(operation, BooleanOperation::Subtract)
&& std::env::var("BREP_PINCH_GATE").as_deref() != Ok("0")
&& subtract_pinches_at_internal_tangency(first, second, options.tolerance)
.unwrap_or(false)
{
return Err(error.with_message(|message| format!(
"{message}; internal tangency: the operands touch tangentially with \
co-directed normals, so the exact difference pinches to zero thickness \
(non-manifold, unrepresentable in a boundary model) — refusing rather \
than returning a perturbed sliver"
)));
}
if matches!(error.class, RefusalClass::TangentNodeSingularity)
&& std::env::var("BREP_TANGENT_NODE_GATE").as_deref() != Ok("0")
{
return Err(error);
}
match perturbation_retry(first, second, operation, options) {
Ok(solid) => Ok(solid),
Err(refused) if refused.is_empty() => Err(error),
Err(refused) => {
Err(error.with_message(|message| format!("{message}; {}", refused.join("; "))))
}
}
}
}
}
const PINCH_PROBE_STEPS: usize = 8;
const PINCH_ANGULAR_TOLERANCE: f64 = 1e-4;
const PINCH_SEPARATION_STEP: f64 = 1e-2;
fn subtract_pinches_at_internal_tangency(
first: &BrepSolid,
second: &BrepSolid,
tolerance: f64,
) -> Result<bool, KernelRefusal> {
let contact = (tolerance * 100.0).max(1e-12);
let probes_a = first
.shells
.iter()
.flat_map(|shell| &shell.faces)
.map(PinchProbe::of)
.collect::<Result<Vec<_>, _>>()?;
let probes_b = second
.shells
.iter()
.flat_map(|shell| &shell.faces)
.map(PinchProbe::of)
.collect::<Result<Vec<_>, _>>()?;
for probe_a in &probes_a {
for probe_b in &probes_b {
let pad = 1e-2 * probe_a.extent().max(probe_b.extent());
if probe_a.separation(probe_b) > contact + pad {
continue;
}
if classify_surface_pair(
&probe_a.face.surface,
&probe_b.face.surface,
tolerance,
PINCH_ANGULAR_TOLERANCE,
).or_refuse(KernelStage::Validate, "csg.boolean.mod")?
.relation
== SurfacePairRelation::Cosurface
{
continue;
}
if faces_touch_with_codirected_normals(probe_a, probe_b, contact)? {
return Ok(true);
}
}
}
Ok(false)
}
struct PinchProbe<'a> {
face: &'a FaceRecord,
samples: Vec<(f64, f64, Vec3)>,
minimum: [f64; 3],
maximum: [f64; 3],
}
impl<'a> PinchProbe<'a> {
fn of(face: &'a FaceRecord) -> Result<Self, KernelRefusal> {
let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
let steps = PINCH_PROBE_STEPS as f64;
let mut samples = Vec::with_capacity((PINCH_PROBE_STEPS + 1).pow(2));
let mut minimum = [f64::INFINITY; 3];
let mut maximum = [f64::NEG_INFINITY; 3];
for i in 0..=PINCH_PROBE_STEPS {
let u = u0 + (u1 - u0) * i as f64 / steps;
for j in 0..=PINCH_PROBE_STEPS {
let v = v0 + (v1 - v0) * j as f64 / steps;
let Ok(point) = face.surface.evaluate(u, v) else {
continue;
};
for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
minimum[axis] = minimum[axis].min(value);
maximum[axis] = maximum[axis].max(value);
}
samples.push((u, v, point));
}
}
Ok(Self {
face,
samples,
minimum,
maximum,
})
}
fn extent(&self) -> f64 {
(0..3)
.map(|axis| self.maximum[axis] - self.minimum[axis])
.fold(0.0f64, f64::max)
}
fn separation(&self, other: &Self) -> f64 {
let mut gap: f64 = 0.0;
for axis in 0..3 {
gap = gap.max(self.minimum[axis] - other.maximum[axis]);
gap = gap.max(other.minimum[axis] - self.maximum[axis]);
}
gap
}
}
fn faces_touch_with_codirected_normals(
probe_a: &PinchProbe<'_>,
probe_b: &PinchProbe<'_>,
contact: f64,
) -> Result<bool, KernelRefusal> {
for (probe, other) in [(probe_a, probe_b), (probe_b, probe_a)] {
let source = probe.face;
let target = other.face;
for &(u, v, point) in &probe.samples {
let projection = project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?;
if projection.distance > contact {
continue;
}
let (Ok(source_normal), Ok(target_normal)) = (
source.surface.normal(u, v),
target.surface.normal(projection.u, projection.v),
) else {
continue;
};
let source_outward = outward_normal(source_normal, source.same_sense);
let target_outward = outward_normal(target_normal, target.same_sense);
if source_outward.cross(target_outward).length() > PINCH_ANGULAR_TOLERANCE
|| source_outward.dot(target_outward) <= 0.0
{
continue;
}
if parameter_point_in_face(source, Vec2 { x: u, y: v }, 1e-6).or_refuse(KernelStage::Validate, "parameter_point_in_face")?
== PolygonClass::Outside
|| parameter_point_in_face(
target,
Vec2 {
x: projection.u,
y: projection.v,
},
1e-6,
).or_refuse(KernelStage::Validate, "csg.boolean.mod")? == PolygonClass::Outside
{
continue;
}
if contact_separates_locally(source, target, u, v, contact)? {
return Ok(true);
}
}
}
Ok(false)
}
fn outward_normal(normal: Vec3, same_sense: bool) -> Vec3 {
if same_sense {
normal
} else {
normal.scale(-1.0)
}
}
fn contact_separates_locally(
source: &FaceRecord,
target: &FaceRecord,
u: f64,
v: f64,
contact: f64,
) -> Result<bool, KernelRefusal> {
let [u0, u1] = source.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
let [v0, v1] = source.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
let step_u = (u1 - u0) * PINCH_SEPARATION_STEP;
let step_v = (v1 - v0) * PINCH_SEPARATION_STEP;
for (probe_u, probe_v) in [
((u + step_u).min(u1), v),
((u - step_u).max(u0), v),
(u, (v + step_v).min(v1)),
(u, (v - step_v).max(v0)),
] {
if (probe_u - u).abs() < f64::EPSILON && (probe_v - v).abs() < f64::EPSILON {
continue;
}
let Ok(point) = source.surface.evaluate(probe_u, probe_v) else {
continue;
};
if project_point_to_surface(&target.surface, point).or_refuse(KernelStage::Validate, "project_point_to_surface")?.distance > contact * 10.0 {
return Ok(true);
}
}
Ok(false)
}
fn perturbation_retry(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
) -> Result<BrepSolid, Vec<String>> {
const DIRECTIONS: [[f64; 3]; 4] = [
[0.4034, 0.7973, 0.4491],
[0.7973, 0.4491, 0.4034],
[0.4491, 0.4034, 0.7973],
[0.5774, -0.5774, 0.5774],
];
const FRACTIONS: [f64; 5] = PERTURBATION_FRACTIONS;
let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
let scale = crate::tolerance::solid_scale(first)
.max(crate::tolerance::solid_scale(second))
.max(1.0);
let _caller = crate::mass_caller("boolean.perturbation_rescue");
let va = solid_signed_volume(first).ok().map(f64::abs);
let vb = solid_signed_volume(second).ok().map(f64::abs);
let hold_identity = std::env::var("BREP_PERTURB_IDENTITY").as_deref() != Ok("0");
let hold_mirror = hold_identity && std::env::var("BREP_PERTURB_MIRROR").as_deref() != Ok("0");
let early_exit = std::env::var("BREP_MIRROR_EARLY_EXIT").as_deref() != Ok("0");
let mut trends = [MirrorTrend::default(); 4];
let mut identity: Option<Option<InclusionExclusion>> = None;
let mut refused = Vec::new();
let ladder = std::env::var("BREP_DEBUG_PERTURB_LADDER").is_ok();
let ladder_start = web_time::Instant::now();
if ladder {
let measured = inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
match &measured {
Some(identity) => eprintln!(
"[ladder] identity {} = {:.9} from the exact lane's {} ({:.9}); V(first) {:?} V(second) {:?}",
identity.formula, identity.expected, identity.reading, identity.read, va, vb
),
None => eprintln!("[ladder] no identity: no exact-lane boolean of these operands clears in either order"),
}
identity = Some(measured);
}
let rung_miss = |identity: &Option<Option<InclusionExclusion>>, candidate: &BrepSolid| -> String {
let volume = body_volume(candidate);
match (identity, volume) {
(Some(Some(identity)), Some(volume)) => format!(
"volume {volume:.9} miss {:+.3e} bar {:.3e}",
volume - identity.expected,
crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * identity.expected.abs())
),
(_, Some(volume)) => format!("volume {volume:.9}"),
_ => "no volume".to_string(),
}
};
for &fraction in &FRACTIONS {
let magnitude = scale * fraction;
for (direction, dir) in DIRECTIONS.iter().enumerate() {
if trends[direction].closed {
if ladder {
eprintln!("[ladder] dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}: not tried, its direction is closed");
}
continue;
}
let offset = [dir[0] * magnitude, dir[1] * magnitude, dir[2] * magnitude];
let translate = match AffineTransform::new([
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,
]) {
Ok(transform) => transform,
Err(_) => continue,
};
let moved = match crate::transform_brep(second, translate, false) {
Ok(solid) => solid,
Err(_) => continue,
};
let rung = format!("dir={dir:?} fraction={fraction:.1e} offset {magnitude:.3e}");
let rung_start = web_time::Instant::now();
let candidate =
match boolean_operation_with_diagnostics(first, &moved, operation, options) {
Ok(outcome) => outcome.value,
Err(error) => {
if ladder {
eprintln!("[ladder] {rung}: exact lane refused {:?}", error.class);
}
continue;
}
};
let built = rung_start.elapsed();
if candidate.shells.is_empty() {
if ladder {
eprintln!("[ladder] {rung}: empty");
}
continue;
}
if let (Some(va), Some(vb)) = (va, vb) {
if let Ok(vr) = solid_signed_volume(&candidate) {
if !volume_within_csg_bounds(operation, va, vb, vr.abs()) {
if ladder {
eprintln!("[ladder] {rung}: outside the CSG bound, {}", rung_miss(&identity, &candidate));
}
continue;
}
}
}
let bounded = rung_start.elapsed();
if hold_identity {
let identity = identity.get_or_insert_with(|| {
let identity =
inclusion_exclusion_volume(first, second, operation, options, va, vb, false);
if debug && identity.is_none() {
eprintln!(
"[perturb] no exact-lane boolean of these operands clears in either \
order; no identity to hold"
);
}
identity
});
if let Some(identity) = identity {
let expected = identity.expected;
let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * expected.abs());
let volume = body_volume(&candidate);
let miss = volume.map(|volume| (volume - expected).abs());
if !miss.is_some_and(|miss| miss <= bar) {
let sentence = format!(
"perturbation rung dir={dir:?} fraction={fraction:.1e} \
(offset {magnitude:.3e}) refused: its body reads {} against the \
inclusion–exclusion identity {} = {expected:.9} (the exact lane's \
{} reads {:.9}), a miss of {} beyond the volume drift bar {bar:.3e}",
volume.map_or("no volume".to_string(), |volume| format!("{volume:.9}")),
identity.formula,
identity.reading,
identity.read,
miss.map_or("unmeasured".to_string(), |miss| format!("{miss:.3e}")),
);
if debug {
eprintln!("[perturb] {sentence}");
}
if ladder {
eprintln!(
"[ladder] {rung}: refused by the identity, volume {} miss {} bar {bar:.3e}",
volume.map_or("none".to_string(), |volume| format!("{volume:.9}")),
volume.map_or("unmeasured".to_string(), |volume| format!("{:+.3e}", volume - expected)),
);
}
refused.push(sentence);
continue;
}
} else if hold_mirror {
let mirror_start = web_time::Instant::now();
let mirrored = AffineTransform::new([
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,
])
.ok()
.and_then(|transform| crate::transform_brep(second, transform, false).ok());
let mirror = mirrored.as_ref().and_then(|mirrored| {
inclusion_exclusion_volume(first, mirrored, operation, options, va, vb, true)
});
let mirror_time = mirror_start.elapsed();
let volume = body_volume(&candidate);
let mut measured = None;
let verdict = match (&mirror, volume) {
(Some(mirror), Some(volume)) => {
let bar = crate::VOLUME_DRIFT_ABS.max(crate::VOLUME_DRIFT_REL * mirror.expected.abs());
let miss = 0.5 * (volume - mirror.expected).abs();
measured = Some((0.5 * (volume - mirror.expected), bar));
if ladder {
eprintln!(
"[ladder] {rung}: mirror at −offset reads {} = {:.9} (the mirrored {} {:.9}); body {volume:.9}, first-order translation error {:+.3e} bar {bar:.3e}",
mirror.formula, mirror.expected, mirror.reading, mirror.read, 0.5 * (volume - mirror.expected)
);
}
(miss > bar).then(|| format!(
"perturbation rung dir={dir:?} fraction={fraction:.1e} \
(offset {magnitude:.3e}) refused: no exact-lane boolean of the \
operands clears, so the rung is held to its mirror: its body reads \
{volume:.9} with the second operand moved +offset and {} = {:.9} \
with it moved −offset (the mirrored {} reads {:.9}), a first-order \
translation error of {miss:.3e} beyond the volume drift bar {bar:.3e}",
mirror.formula, mirror.expected, mirror.reading, mirror.read
))
}
_ => Some(format!(
"perturbation rung dir={dir:?} fraction={fraction:.1e} \
(offset {magnitude:.3e}) refused: no exact-lane boolean of the operands \
clears, and none clears with the second operand moved −offset either, \
so nothing checks the rung's body{}",
volume.map_or(String::new(), |volume| format!(" ({volume:.9})"))
)),
};
match verdict {
None => trends[direction].cleared(),
Some(sentence) => {
if debug {
eprintln!("[perturb] {sentence}");
}
if ladder {
eprintln!(
"[ladder] {rung}: refused by its mirror (rung {:.0} ms: built {:.0}, bound {:.0}, mirror {:.0} ms; ladder {:.1} s)",
rung_start.elapsed().as_secs_f64() * 1e3,
built.as_secs_f64() * 1e3,
(bounded - built).as_secs_f64() * 1e3,
mirror_time.as_secs_f64() * 1e3,
ladder_start.elapsed().as_secs_f64()
);
}
refused.push(sentence);
let closure = measured.and_then(|(error, bar)| {
let earlier = trends[direction].refused(magnitude, error, early_exit)?;
Some((earlier, error, bar))
});
if let Some(((earlier_offset, earlier_error), error, bar)) = closure {
let closed = format!(
"perturbation direction dir={dir:?} closed after offset {magnitude:.3e}: its \
rungs' first-order translation errors read {earlier_error:+.3e} at offset \
{earlier_offset:.3e} and {error:+.3e} at {magnitude:.3e}, one sign and \
growing, so no larger offset along it comes back to the volume drift bar \
{bar:.3e}"
);
if debug {
eprintln!("[perturb] {closed}");
}
if ladder {
eprintln!("[ladder] {closed}");
}
refused.push(closed);
}
continue;
}
}
}
}
match crate::oracle::boolean_semantic_disagreement(
first, second, operation, &candidate, 400,
) {
Ok(report) if report.considered >= 30 && !report.is_flagged() => {
if ladder {
eprintln!(
"[ladder] {rung}: accepted (oracle {} considered, rate {:.4}), {}",
report.considered, report.disagreement_rate, rung_miss(&identity, &candidate)
);
}
if debug {
eprintln!(
"[perturb] rescued {operation:?}: dir={dir:?} fraction={fraction:.1e} \
magnitude={magnitude:.3e} offset={offset:?} \
(oracle considered={} rate={:.4})",
report.considered, report.disagreement_rate
);
}
return Ok(candidate);
}
report => {
if ladder {
let verdict = match &report {
Ok(report) => format!("oracle {} considered, rate {:.4}", report.considered, report.disagreement_rate),
Err(error) => format!("oracle error {error:?}"),
};
eprintln!("[ladder] {rung}: refused by the oracle ({verdict}), {}", rung_miss(&identity, &candidate));
}
continue;
}
}
}
}
if debug {
eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
}
Err(refused)
}
const PERTURBATION_FRACTIONS: [f64; 5] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
#[derive(Clone, Copy, Default)]
struct MirrorTrend {
last: Option<(f64, f64)>,
closed: bool,
}
impl MirrorTrend {
fn refused(&mut self, offset: f64, error: f64, close: bool) -> Option<(f64, f64)> {
let earlier = self
.last
.filter(|&(_, earlier)| earlier.signum() == error.signum() && error.abs() >= earlier.abs())
.filter(|_| close);
self.last = Some((offset, error));
self.closed |= earlier.is_some();
earlier
}
fn cleared(&mut self) {
self.last = None;
}
}
fn one_use_edges(solid: &BrepSolid) -> usize {
let mut uses: HashMap<u64, usize> = HashMap::default();
for coedge in solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
{
*uses.entry(coedge.edge_id).or_default() += 1;
}
solid
.edges
.iter()
.filter(|edge| !edge.degenerate && uses.get(&edge.id) == Some(&1))
.count()
}
fn body_volume(solid: &BrepSolid) -> Option<f64> {
if solid.shells.is_empty() {
return Some(0.0);
}
solid_signed_volume(solid).ok().map(f64::abs)
}
struct InclusionExclusion {
formula: &'static str,
reading: &'static str,
read: f64,
expected: f64,
}
fn inclusion_exclusion_volume(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
va: Option<f64>,
vb: Option<f64>,
own_first: bool,
) -> Option<InclusionExclusion> {
use BooleanOperation::{Intersect, Subtract, Union};
let (va, vb) = (va?, vb?);
let own = match operation {
Intersect => (Intersect, false, "first ∩ second"),
Union => (Union, false, "first ∪ second"),
Subtract => (Subtract, false, "first − second"),
};
let readings = [
(Intersect, false, "first ∩ second"),
(Intersect, true, "second ∩ first"),
(Union, false, "first ∪ second"),
(Union, true, "second ∪ first"),
(Subtract, false, "first − second"),
(Subtract, true, "second − first"),
];
let own_reading = own_first.then_some(own);
let leading = usize::from(own_first);
for (index, (other, swapped, reading)) in own_reading.into_iter().chain(readings).enumerate() {
if index >= leading && !swapped && std::mem::discriminant(&other) == std::mem::discriminant(&operation) {
continue;
}
let (x, y) = if swapped { (second, first) } else { (first, second) };
let Some(read) = boolean_operation_with_diagnostics(x, y, other, options)
.ok()
.and_then(|outcome| body_volume(&outcome.value))
else {
continue;
};
let intersection = match (other, swapped) {
(Intersect, _) => read,
(Union, _) => va + vb - read,
(Subtract, false) => va - read,
(Subtract, true) => vb - read,
};
let (formula, expected) = match operation {
Intersect => ("V(first ∩ second)", intersection),
Union => ("V(first) + V(second) − V(first ∩ second)", va + vb - intersection),
Subtract => ("V(first) − V(first ∩ second)", va - intersection),
};
return Some(InclusionExclusion { formula, reading, read, expected });
}
None
}
fn planar_operand_off_its_plane(
first: &BrepSolid,
second: &BrepSolid,
options: &BooleanOptions,
) -> Option<(u8, u64, f64, f64)> {
let policy = options
.tolerances
.clone()
.unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
let weld = crate::tolerance::assembler_weld(policy.model);
let mut worst: Option<(u8, u64, f64)> = None;
for (operand, solid) in [(0u8, first), (1u8, second)] {
let vertex_point = |id: u64| solid.vertices.iter().find(|v| v.id == id).map(|v| v.point);
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
let Some(crate::AnalyticSurface::Plane {
origin,
u_dir,
v_dir,
..
}) = face.surface.analytic()
else {
continue;
};
let [loop_record] = face.loops.as_slice() else {
continue;
};
let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
continue;
};
let mut off_plane = 0.0f64;
let mut straight = true;
for coedge in &loop_record.coedges {
let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
straight = false;
break;
};
let (Some(start), Some(end)) =
(vertex_point(edge.start_vertex_id), vertex_point(edge.end_vertex_id))
else {
straight = false;
break;
};
let chord = end.sub(start);
let length = chord.length();
if edge.degenerate || length <= weld {
straight = false;
break;
}
let direction = chord.scale(1.0 / length);
for fraction in [0.25, 0.5, 0.75] {
let Ok(point) = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction) else {
straight = false;
break;
};
let delta = point.sub(start);
let along = delta.dot(direction);
if delta.sub(direction.scale(along)).length() > weld
|| along < -weld
|| along > length + weld
{
straight = false;
break;
}
}
if !straight {
break;
}
off_plane = off_plane.max(start.sub(*origin).dot(normal).abs());
}
if straight && off_plane > weld && worst.is_none_or(|(_, _, gap)| off_plane > gap) {
worst = Some((operand, face.id, off_plane));
}
}
}
worst.map(|(operand, face_id, gap)| (operand, face_id, gap, weld))
}
fn volume_within_csg_bounds(operation: BooleanOperation, va: f64, vb: f64, vr: f64) -> bool {
let slack = 1e-2 * (va + vb).max(1.0);
match operation {
BooleanOperation::Union => vr >= va.max(vb) - slack && vr <= va + vb + slack,
BooleanOperation::Subtract => vr <= va + slack && vr >= -slack,
BooleanOperation::Intersect => vr <= va.min(vb) + slack && vr >= -slack,
}
}
fn reverse_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), KernelRefusal> {
fragment.same_sense = !fragment.same_sense;
for loop_record in &mut fragment.loops {
loop_record.coedges.reverse();
for coedge in &mut loop_record.coedges {
coedge.forward = !coedge.forward;
coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Validate, "reversed")?;
}
}
Ok(())
}
fn build_nary_imprint(
operands: &[BrepSolid],
options: &ImprintOptions,
) -> Result<ImprintResultRecord, KernelRefusal> {
let mut section_evidence = false;
let mut vertices: Vec<ImprintVertex> = Vec::new();
let mut pieces: Vec<ImprintPieceRecord> = Vec::new();
let mut by_face: HashMap<(u8, u64), Vec<u64>> = HashMap::default();
let mut edge_splits: HashMap<(u8, u64), Vec<f64>> = HashMap::default();
let mut next_vertex_id: u64 = 1;
let mut next_piece_id: u64 = 1;
for i in 0..operands.len() {
for j in (i + 1)..operands.len() {
let pair = build_imprints(&operands[i], &operands[j], options)?;
let remap = |operand: u8| -> u8 {
if operand == 0 {
i as u8
} else {
j as u8
}
};
section_evidence = section_evidence || pair.section_evidence;
let mut vertex_map: HashMap<u64, u64> = HashMap::default();
for vertex in &pair.vertices {
let global = next_vertex_id;
next_vertex_id += 1;
vertex_map.insert(vertex.id, global);
vertices.push(ImprintVertex {
id: global,
point: vertex.point,
});
}
let mut piece_map: HashMap<u64, u64> = HashMap::default();
for piece in &pair.pieces {
let global = next_piece_id;
next_piece_id += 1;
piece_map.insert(piece.id, global);
let mut mapped = piece.clone();
mapped.id = global;
mapped.start_vertex_id = *vertex_map
.get(&piece.start_vertex_id)
.ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
mapped.end_vertex_id = *vertex_map
.get(&piece.end_vertex_id)
.ok_or_else(|| KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: piece references unknown vertex"))?;
for pcurve in &mut mapped.pcurves {
pcurve.operand = remap(pcurve.operand);
}
mapped.support_faces = [
FaceKey {
operand: remap(piece.support_faces[0].operand),
face_id: piece.support_faces[0].face_id,
},
FaceKey {
operand: remap(piece.support_faces[1].operand),
face_id: piece.support_faces[1].face_id,
},
];
pieces.push(mapped);
}
for entry in &pair.by_face {
let list = by_face
.entry((remap(entry.operand), entry.face_id))
.or_default();
for piece_id in &entry.piece_ids {
let global = *piece_map.get(piece_id).ok_or_else(|| {
KernelRefusal::internal(KernelStage::Intersect, "nary.imprint", "n-ary imprint: by_face references unknown piece")
})?;
list.push(global);
}
}
for split in &pair.edge_splits {
edge_splits
.entry((remap(split.operand), split.edge_id))
.or_default()
.extend(split.parameters.iter().copied());
}
}
}
Ok(ImprintResultRecord {
vertices,
pieces,
tangent_nodes: Vec::new(),
barrier_edges: Vec::new(),
section_evidence,
cosurface_pairs: Vec::new(),
tangent_sections: Vec::new(),
by_face: by_face
.into_iter()
.map(|((operand, face_id), piece_ids)| FaceImprints {
operand,
face_id,
piece_ids,
})
.collect(),
edge_splits: edge_splits
.into_iter()
.map(|((operand, edge_id), parameters)| EdgeSplitRecord {
operand,
edge_id,
parameters,
})
.collect(),
})
}
pub fn boolean_operation_nary(
operands: &[BrepSolid],
operation: BooleanOperation,
) -> Result<BrepSolid, KernelRefusal> {
boolean_operation_nary_with_options(operands, operation, &BooleanOptions::default())
}
pub fn boolean_operation_nary_with_options(
operands: &[BrepSolid],
operation: BooleanOperation,
options: &BooleanOptions,
) -> Result<BrepSolid, KernelRefusal> {
if operands.is_empty() {
return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: no operands provided"));
}
if operands.len() == 1 {
return Ok(operands[0].clone());
}
if operands.len() > 255 {
return Err(KernelRefusal::input(KernelStage::Collect, "operands", "boolean_operation_nary: at most 255 operands supported"));
}
let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
let mut policy = KernelTolerances::for_solid(&operands[0], options.tolerance);
for operand in &operands[1..] {
let candidate = KernelTolerances::for_solid(operand, options.tolerance);
if candidate.sew_search > policy.sew_search {
policy = candidate;
}
}
policy.check().or_input(KernelStage::Collect, "tolerances")?;
let tolerance = policy.model;
let mut healed = operands.to_vec();
for operand in &mut healed {
crate::heal::heal_operands(operand, &policy)?;
crate::heal::refit_inherited_trims(operand, &policy)?;
normalize_operand_band_seams(operand)?;
}
let mut imprint_options = options.imprint.clone();
imprint_options.tolerance = tolerance;
let imprint = build_nary_imprint(&healed, &imprint_options)?;
let mut split = Vec::with_capacity(healed.len());
for (index, operand) in healed.iter().enumerate() {
split.push(apply_edge_splits(operand, index as u8, &imprint)?);
}
let mut fragments = Vec::with_capacity(split.len());
for (index, solid) in split.iter().enumerate() {
fragments.push(fragment_solid(solid, index as u8, &imprint)?);
}
let selected = select_fragments_nary(&fragments, &healed, operation, tolerance, debug)?;
let solids: HashMap<u8, &BrepSolid> = split
.iter()
.enumerate()
.map(|(index, solid)| (index as u8, solid))
.collect();
let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
let solid = if options.merge_coplanar_faces
&& std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
{
let merged = merge_same_surface_faces_excluding(
&solid,
tolerance,
&options.keep_unmerged_name_substrs,
)?;
merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
} else {
solid
};
let validation = solid.validate_detailed(&policy);
if !validation.issues.is_empty() {
return Err(KernelRefusal::new(
RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
KernelStage::Validate,
format!(
"boolean_operation_nary: invalid result: {:?}",
validation.issues
)));
}
if debug {
if let Ok(report) =
crate::oracle::boolean_semantic_disagreement_nary(&healed, operation, &solid, 300)
{
if report.is_flagged() {
eprintln!(
"[oracle] n-ary semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
report.disagreement_rate,
report.disagreements.len(),
report.considered,
report.sample_disagreement()
);
}
}
}
Ok(solid)
}
fn attribute_to_tangent_node(error: KernelRefusal, nodes: &[Vec3]) -> KernelRefusal {
let Some(node) = nodes.first() else {
return error;
};
if !error.class.perturbation_eligible()
|| matches!(error.class, RefusalClass::TangentNodeSingularity)
{
return error;
}
if std::env::var("BREP_DEBUG_BOOL").is_ok() {
eprintln!(
"boolean: tear downstream of the tangent node at ({:.6},{:.6},{:.6}) attributed to it; the stage read: {}",
node.x, node.y, node.z, error.message
);
}
KernelRefusal::new(
RefusalClass::TangentNodeSingularity,
error.stage,
format!(
"boolean: unsupported singular/tangent-node surface intersection \
at the tangent node at ({:.6},{:.6},{:.6}): the section through it did not \
assemble into a closed shell",
node.x, node.y, node.z
),
)
}
fn attribute_to_tangent_section(
error: KernelRefusal,
sections: &[crate::imprint::TangentSectionRecord],
origins: &[FaceKey],
) -> KernelRefusal {
if sections.is_empty()
|| !error.class.perturbation_eligible()
|| matches!(error.class, RefusalClass::TangentNodeSingularity)
{
return error;
}
let named = refusal_result_faces(&error.message)
.into_iter()
.filter_map(|id| origins.get(id.checked_sub(1)? as usize).copied())
.collect::<Vec<FaceKey>>();
let involving = |fallback: bool| {
sections.iter().find(|section| {
section.grazing.is_empty() != fallback && named.iter().any(|face| section.faces.contains(face))
})
};
let Some(section) = involving(false).or_else(|| involving(true)) else {
return error;
};
let at = |point: Vec3| {
let shown = |value: f64| if value.abs() < 5e-7 { 0.0 } else { value };
format!("({:.6},{:.6},{:.6})", shown(point.x), shown(point.y), shown(point.z))
};
let pierces = (!section.grazing.is_empty()).then(|| {
let shown = section.grazing.len().min(4);
let mut listed = section.grazing[..shown]
.iter()
.map(|(point, tangency)| format!("{} at |tan·n| = {tangency:.4}", at(*point)))
.collect::<Vec<_>>()
.join(", ");
if section.grazing.len() > shown {
listed.push_str(&format!(" and {} more", section.grazing.len() - shown));
}
listed
});
let pierce_only = section.pierce_only();
let shape = if let (true, Some(pierces)) = (pierce_only, pierces.as_deref()) {
format!(
"cross at a grazing angle: a trim edge pierces the other carrier inside both \
trims at {pierces}, below the marcher's start gate, and no transverse section was \
marched between the pierces — a near-tangent crossing born on a tangent contact \
(a third-order singularity) the imprint does not build yet"
)
} else if section.planar {
format!(
"touch along a tangent curve from {} to {} (length {:.6}, {} stations{}) which the \
imprint does not build yet",
at(section.start),
at(section.end),
section.length,
section.stations,
if section.closed { ", closed" } else { "" }
)
} else {
format!(
"touch along a curve through {} with neither carrier planar, a curved/curved \
tangent contact the lane does not trace yet",
at(section.start)
)
};
let shape = match (pierce_only, pierces) {
(false, Some(pierces)) => format!(
"{shape}; a trim edge also pierces the other carrier inside both trims at {pierces}, \
below the marcher's start gate, with no transverse section marched"
),
_ => shape,
};
let involved = named
.iter()
.filter(|face| section.faces.contains(face))
.map(|face| format!("{}/{}", face.operand, face.face_id))
.collect::<Vec<_>>()
.join(", ");
KernelRefusal::new(
RefusalClass::TangentNodeSingularity,
error.stage,
format!(
"boolean: unsupported tangent-curve surface intersection between faces {}/{} and {}/{}: \
the carriers {shape}; the refusal names face(s) {involved} the contact involves; {}",
section.first.operand, section.first.face_id, section.second.operand, section.second.face_id,
error.message
),
)
}
fn refusal_result_faces(message: &str) -> Vec<u64> {
let mut ids = Vec::new();
let digits_after = |text: &str| -> Option<u64> {
let end = text.find(|c: char| !c.is_ascii_digit()).unwrap_or(text.len());
text[..end].parse().ok()
};
for (index, _) in message.match_indices("face ") {
if let Some(id) = digits_after(&message[index + "face ".len()..]) {
ids.push(id);
}
}
if let Some(start) = message.find("on faces [") {
let rest = &message[start + "on faces [".len()..];
let list = &rest[..rest.find(']').unwrap_or(rest.len())];
for entry in list.split(", ") {
let entry = entry.trim_start();
if let Some(id) = digits_after(entry) {
if entry[id.to_string().len()..].starts_with(" '") {
ids.push(id);
}
}
}
}
ids.sort_unstable();
ids.dedup();
ids
}
pub fn boolean_operation_with_diagnostics(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
let _account = profile::Scope::new("boolean.other");
let closed_inputs = matches!(std::env::var("BREP_CLOSED_OPERAND_INPUT").as_deref(), Err(_) | Ok("1") | Ok("on"));
if let (true, Some(later)) = (reseat::seam_reseat_enabled(), &options.seam_reseat_later_references) {
let started = Instant::now();
let (first, second) = reseat::reseat_tangent_seams(first, second, later, options.tolerance);
profile_stage("seam_prepare_ms", started.elapsed().as_secs_f64() * 1000.0);
return boolean_operation_with_diagnostics_impl(&first, &second, operation, options, closed_inputs);
}
boolean_operation_with_diagnostics_impl(first, second, operation, options, closed_inputs)
}
fn boolean_operation_with_diagnostics_impl(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
closed_inputs: bool,
) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
let _coedge_memo = crate::topology::CoedgeMemoScope::open(crate::topology::coedge_memo_enabled());
if matches!(operation, BooleanOperation::Intersect)
&& [first, second].iter().any(|body| body.kind == crate::BodyKind::Sheet)
&& match std::env::var("BREP_SHEET_TRIM").as_deref() {
Ok("1") => true,
Ok("0") => false,
_ => options.sheet_trim,
}
{
return sheet::trim(first, second, options);
}
if matches!(operation, BooleanOperation::Union | BooleanOperation::Subtract) {
if let Some(operand) = [first, second]
.iter()
.position(|solid| solid.kind == crate::BodyKind::Sheet)
{
return Err(KernelRefusal::input(
KernelStage::Collect,
"sheet_operand",
format!(
"boolean: operand {operand} is a Sheet, which encloses no solid; union and \
subtract of a sheet are refused; Intersect trims a Sheet by a Solid"
),
));
}
}
let mut outcome = boolean_operation_with_diagnostics_core(first, second, operation, options)?;
let closure_started = closed_inputs.then(Instant::now);
if closed_inputs {
for (operand, solid) in [first, second].into_iter().enumerate() {
let open = one_use_edges(solid);
if open > 0 {
return Err(KernelRefusal::input(KernelStage::Collect, "operand_closure", format!(
"boolean: operand {operand} is not a closed shell: {open} of its edges bound only one face, so its inside is undefined"
)));
}
}
}
if let Some(started) = closure_started {
let ms = started.elapsed().as_secs_f64() * 1000.0;
profile_stage("operand_closure_ms", ms);
outcome.diagnostics.measure_max("timing.operand_closure_ms", ms);
}
Ok(outcome)
}
fn boolean_operation_with_diagnostics_core(
first: &BrepSolid, second: &BrepSolid, operation: BooleanOperation, options: &BooleanOptions,
) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
if let Some(offset) = crate::recentre::control_offset_at_entry() {
return boolean_operation_with_diagnostics_at_offset(first, second, operation, options, offset);
}
let mut bounds = operand_bounds(first);
bounds.include(operand_bounds(second));
if let Some(offset) = crate::recentre::guard_offset(bounds) {
crate::recentre::profile_fired("boolean", offset, bounds);
let mut outcome = boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Guard)?;
outcome.diagnostics.count_n("recentre.fired", 1);
outcome.diagnostics.measure_max("recentre.distance", offset.length());
return Ok(outcome);
}
let mut tangent_nodes = Vec::new();
let (mut outcome, soundness_candidates) =
boolean_pipeline(first, second, operation, options, &mut tangent_nodes)
.map_err(|error| attribute_to_tangent_node(error, &tangent_nodes))?;
let stage_started = Instant::now();
let accepted = match &soundness_candidates {
Some(candidates) => crate::accept_sound_against_scoped(&[first, second], outcome.value, "boolean", candidates),
None => crate::accept_sound_against(&[first, second], outcome.value, "boolean"),
};
let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
profile_stage("soundness_ms", stage_ms);
outcome.diagnostics.measure_max("timing.soundness_ms", stage_ms);
outcome.value = accepted?;
Ok(outcome)
}
pub fn boolean_operation_with_diagnostics_at_offset(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
offset: Vec3,
) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
boolean_translated(first, second, operation, options, offset, crate::recentre::EntryKind::Control)
}
fn boolean_translated(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
offset: Vec3,
kind: crate::recentre::EntryKind,
) -> Result<KernelOutcome<BrepSolid>, KernelRefusal> {
let _entry = crate::recentre::TranslatedEntry::open(kind);
let translate = |solid: &BrepSolid, offset: Vec3| {
crate::recentre::translate_solid(solid, offset).map_err(|message| {
KernelRefusal::internal(
KernelStage::Collect,
"translation_control",
format!("boolean: the translation control could not move an operand: {message}"),
)
})
};
let moved_first = translate(first, offset)?;
let moved_second = translate(second, offset)?;
let outcome = boolean_operation_with_diagnostics(&moved_first, &moved_second, operation, options)?;
let value = translate(&outcome.value, offset.scale(-1.0))?;
Ok(KernelOutcome { value, diagnostics: outcome.diagnostics })
}
fn profile_stage(code: &str, ms: f64) {
let row = match code {
"heal_ms" => Some("boolean.operand_prepare"),
"imprint_ms" => Some("boolean.imprint"),
"edge_split_ms" => Some("boolean.edge_split"),
"fragment_ms" => Some("boolean.fragment"),
"select_ms" => Some("boolean.select"),
"assemble_ms" => Some("boolean.assemble"),
"face_merge_ms" => Some("boolean.face_merge"),
"soundness_ms" => Some("boolean.soundness"),
"operand_closure_ms" => Some("boolean.operand_closure"),
"seam_prepare_ms" => Some("boolean.seam_prepare"),
"final_validate_ms" => Some("boolean.final_validate"),
_ => None,
};
if let Some(row) = row { profile::record(row, ms); }
if std::env::var("BREP_PROFILE").is_ok() {
eprintln!("boolean.{code}={ms:.2}");
}
}
struct BooleanStageDump {
directory: std::path::PathBuf,
serial: usize,
}
impl BooleanStageDump {
fn open() -> Option<Self> {
static SERIAL: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let directory = std::env::var("BREP_BOOLEAN_DUMP").ok().filter(|value| !value.is_empty())?;
let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Some(Self { directory: directory.into(), serial })
}
fn write<T: Serialize + ?Sized>(&self, stage: &str, value: &T) {
let path = self.directory.join(format!("{:03}-{stage}.json", self.serial));
match serde_json::to_string(value) {
Ok(text) => {
if let Err(error) = std::fs::write(&path, text) {
eprintln!("BREP_BOOLEAN_DUMP {}: {error}", path.display());
}
}
Err(error) => eprintln!("BREP_BOOLEAN_DUMP {stage}: {error}"),
}
}
}
fn boolean_policy(
first: &BrepSolid,
second: &BrepSolid,
options: &BooleanOptions,
) -> Result<KernelTolerances, KernelRefusal> {
let policy = options
.tolerances
.unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
policy.check().or_input(KernelStage::Collect, "tolerances")?;
Ok(policy)
}
fn prepared_operands(
first: &BrepSolid,
second: &BrepSolid,
policy: &KernelTolerances,
) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
let stage_started = Instant::now();
let mut first_owned = first.clone();
let mut second_owned = second.clone();
crate::heal::heal_operands(&mut first_owned, policy)?;
crate::heal::heal_operands(&mut second_owned, policy)?;
profile_stage("heal.operands_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
let refit_started = Instant::now();
crate::heal::refit_inherited_trims(&mut first_owned, policy)?;
crate::heal::refit_inherited_trims(&mut second_owned, policy)?;
profile_stage("heal.refit_ms", refit_started.elapsed().as_secs_f64() * 1_000.0);
if std::env::var("BREP_PROFILE").is_ok() {
let (hits, misses) = crate::reading_memo::counts();
eprintln!("boolean.reading_memo hits={hits} misses={misses}");
}
profile_stage("heal_ms", stage_started.elapsed().as_secs_f64() * 1_000.0);
normalize_operand_band_seams(&mut first_owned)?;
normalize_operand_band_seams(&mut second_owned)?;
Ok((first_owned, second_owned))
}
fn boolean_imprint_options(options: &BooleanOptions, tolerance: f64) -> ImprintOptions {
let mut imprint_options = options.imprint.clone();
imprint_options.tolerance = tolerance;
imprint_options
}
pub fn boolean_split_operands(
first: &BrepSolid,
second: &BrepSolid,
options: &BooleanOptions,
) -> Result<(BrepSolid, BrepSolid), KernelRefusal> {
let policy = boolean_policy(first, second, options)?;
let (first, second) = prepared_operands(first, second, &policy)?;
let imprint = build_imprints(&first, &second, &boolean_imprint_options(options, policy.model))?;
let (split_first, _) = apply_edge_splits_with_map(&first, 0, &imprint)?;
let (split_second, _) = apply_edge_splits_with_map(&second, 1, &imprint)?;
Ok((split_first, split_second))
}
fn operand_bounds(solid: &BrepSolid) -> crate::Aabb {
let mut bounds = crate::Aabb::from_points(solid.vertices.iter().map(|vertex| vertex.point));
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
match crate::Aabb::from_surface_controls(&face.surface) {
Ok(face_bounds) => bounds.include(face_bounds),
Err(_) => {
return crate::Aabb {
minimum: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
maximum: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
}
}
}
}
bounds
}
fn line_or_point_contact(first: &BrepSolid, second: &BrepSolid, bounds: [crate::Aabb; 2]) -> bool {
let plain = |solid: &BrepSolid| {
solid.kind == crate::BodyKind::Solid
&& solid.cells.is_empty()
&& solid.shells.iter().flat_map(|shell| &shell.faces).all(|face| {
face.surface.control_points.iter().flatten().all(|control| control.w > 0.0 && control.w.is_finite())
})
};
let [a, b] = bounds;
let apart = |a_min: f64, a_max: f64, b_min: f64, b_max: f64| a_max <= b_min || b_max <= a_min;
let axes = [
apart(a.minimum.x, a.maximum.x, b.minimum.x, b.maximum.x),
apart(a.minimum.y, a.maximum.y, b.minimum.y, b.maximum.y),
apart(a.minimum.z, a.maximum.z, b.minimum.z, b.maximum.z),
];
axes.iter().filter(|&&apart| apart).count() >= 2 && plain(first) && plain(second)
}
fn soundness_candidates(
solid: &BrepSolid,
scope: &AssemblyScope,
bounds: [crate::Aabb; 2],
band: f64,
) -> HashSet<u64> {
let mut candidates = HashSet::default();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
if scope.examined.contains(&face.id) {
candidates.insert(face.id);
continue;
}
let Some(&operand) = scope.operand_of.get(&face.id) else {
candidates.insert(face.id);
continue;
};
let other = bounds[usize::from(operand == 0)];
match crate::Aabb::from_surface_controls(&face.surface) {
Ok(face_bounds) if !face_bounds.intersects(other, band) => {}
_ => {
candidates.insert(face.id);
}
}
}
candidates
}
fn empty_solid() -> BrepSolid {
BrepSolid {
mass_properties_cache: Default::default(),
cells: Default::default(),
id: 0,
vertices: Vec::new(),
edges: Vec::new(),
shells: Vec::new(),
genus: 0,
kind: crate::BodyKind::Solid,
}
}
fn concatenate_disjoint(first: &BrepSolid, second: &BrepSolid) -> BrepSolid {
let mut vertices = Vec::new();
let mut edges = Vec::new();
let mut shells = Vec::new();
let mut used_face_names = HashSet::<String>::default();
let (mut next_face_id, mut next_loop_id, mut next_coedge_id) = (1u64, 1u64, 1u64);
for operand in [first, second] {
let vertex_base = vertices.len() as u64;
let edge_base = edges.len() as u64;
let mut vertex_ids: HashMap<u64, u64> = HashMap::default();
for (index, vertex) in operand.vertices.iter().enumerate() {
let id = vertex_base + index as u64 + 1;
vertex_ids.insert(vertex.id, id);
vertices.push(VertexRecord { id, point: vertex.point });
}
let mut edge_ids: HashMap<u64, u64> = HashMap::default();
for (index, edge) in operand.edges.iter().enumerate() {
let id = edge_base + index as u64 + 1;
edge_ids.insert(edge.id, id);
edges.push(EdgeRecord {
id,
curve: edge.curve.clone(),
t0: edge.t0,
t1: edge.t1,
start_vertex_id: vertex_ids.get(&edge.start_vertex_id).copied().unwrap_or(edge.start_vertex_id),
end_vertex_id: vertex_ids.get(&edge.end_vertex_id).copied().unwrap_or(edge.end_vertex_id),
degenerate: edge.degenerate,
name: edge.name.clone(),
});
}
for shell in &operand.shells {
let mut faces = Vec::with_capacity(shell.faces.len());
for face in &shell.faces {
let loops = face
.loops
.iter()
.map(|loop_record| {
let coedges = loop_record
.coedges
.iter()
.map(|coedge| {
let id = next_coedge_id;
next_coedge_id += 1;
CoedgeRecord {
id,
edge_id: edge_ids.get(&coedge.edge_id).copied().unwrap_or(coedge.edge_id),
forward: coedge.forward,
pcurve: coedge.pcurve.clone(),
}
})
.collect();
let id = next_loop_id;
next_loop_id += 1;
LoopRecord { id, coedges }
})
.collect();
let name = face.name.clone().map(|base| {
if used_face_names.insert(base.clone()) {
base
} else {
let mut suffix = 1usize;
loop {
let candidate = format!("{base}_{suffix}");
if used_face_names.insert(candidate.clone()) {
break candidate;
}
suffix += 1;
}
}
});
faces.push(FaceRecord {
id: next_face_id,
surface: face.surface.clone(),
same_sense: face.same_sense,
loops,
name,
});
next_face_id += 1;
}
shells.push(ShellRecord { id: shells.len() as u64 + 1, faces });
}
}
BrepSolid {
mass_properties_cache: Default::default(),
cells: Default::default(),
id: 1,
vertices,
edges,
shells,
genus: first.genus + second.genus,
kind: crate::BodyKind::Solid,
}
}
fn boolean_pipeline(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
tangent_nodes: &mut Vec<Vec3>,
) -> Result<(KernelOutcome<BrepSolid>, Option<HashSet<u64>>), KernelRefusal> {
let replaced_operands = operand_state::replace_operands(first, second, operation, options);
let (first, second) = match &replaced_operands {
Some((first, second)) => (first, second),
None => (first, second),
};
let policy = boolean_policy(first, second, options)?;
let tolerance = policy.model;
let bounds = [operand_bounds(first), operand_bounds(second)];
let disjoint = disjoint_fast_path(options) && !bounds[0].intersects(bounds[1], policy.sew_search);
let contact = !disjoint
&& contact_fast_path(options)
&& options.seam_reseat_later_references.is_some()
&& line_or_point_contact(first, second, bounds);
if disjoint || contact {
let operation_started = Instant::now();
let mut diagnostics = KernelDiagnostics::default();
diagnostics.count_n(
"collect.faces",
first.shells.iter().chain(&second.shells).map(|shell| shell.faces.len() as u64).sum(),
);
diagnostics.count(if contact { "boolean.contact_fast_path" } else { "boolean.disjoint_fast_path" });
diagnostics.measure_max("tolerance.model", policy.model);
diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
let value = match operation {
BooleanOperation::Union => concatenate_disjoint(first, second),
BooleanOperation::Subtract => first.clone(),
BooleanOperation::Intersect => {
diagnostics.event(
DiagnosticSeverity::Info,
KernelStage::Select,
"boolean.empty_result",
format!("{operation:?} of witnessed-non-overlapping operands is empty"),
);
empty_solid()
}
};
let stage_ms = operation_started.elapsed().as_secs_f64() * 1_000.0;
profile_stage("disjoint_fast_path_ms", stage_ms);
diagnostics.measure_max("timing.total_ms", stage_ms);
return Ok((KernelOutcome { value, diagnostics }, Some(HashSet::default())));
}
let heal_started = Instant::now();
let (first_owned, second_owned) = prepared_operands(first, second, &policy)?;
let heal_ms = heal_started.elapsed().as_secs_f64() * 1_000.0;
let first = &first_owned;
let second = &second_owned;
let mut diagnostics = KernelDiagnostics::default();
diagnostics.measure_max("timing.heal_ms", heal_ms);
let fit_scope = crate::PcurveFitScope::open();
let polyline_scope = crate::PolylineFitScope::open();
let operation_started = Instant::now();
diagnostics.count_n(
"collect.faces",
first
.shells
.iter()
.chain(&second.shells)
.map(|shell| shell.faces.len() as u64)
.sum(),
);
diagnostics.measure_max("tolerance.model", policy.model);
diagnostics.measure_max("tolerance.sew_search", policy.sew_search);
let imprint_options = boolean_imprint_options(options, tolerance);
let stage_started = Instant::now();
let imprint = build_imprints(first, second, &imprint_options)?;
tangent_nodes.extend_from_slice(&imprint.tangent_nodes);
if std::env::var("BREP_DEBUG_BOOL").is_ok() {
for piece in &imprint.pieces {
let start = piece.curve.evaluate(piece.t0);
let end = piece.curve.evaluate(piece.t1);
eprintln!(
"piece {} supports={:?} t=[{:.6},{:.6}] start={:?} end={:?}",
piece.id, piece.support_faces, piece.t0, piece.t1, start, end
);
}
for entry in &imprint.by_face {
eprintln!(
"by_face operand={} face={} pieces={:?}",
entry.operand, entry.face_id, entry.piece_ids
);
}
for split in &imprint.edge_splits {
eprintln!(
"edge_split operand={} edge={} params={:?}",
split.operand, split.edge_id, split.parameters
);
}
}
let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
profile_stage("imprint_ms", stage_ms);
diagnostics.measure_max("timing.imprint_ms", stage_ms);
diagnostics.count_n("intersect.pieces", imprint.pieces.len() as u64);
diagnostics.count_n("intersect.vertices", imprint.vertices.len() as u64);
diagnostics.count_n("intersect.edge_splits", imprint.edge_splits.len() as u64);
let (march_swap_rescues, clip_swap_rescues) = crate::imprint::take_rescue_firings();
diagnostics.count_n("intersect.march_swap_rescue", march_swap_rescues);
diagnostics.count_n("intersect.clip_swap_rescue", clip_swap_rescues);
let touch = TouchCensus::build([first, second], &imprint);
touch.report(&mut diagnostics);
let stage_started = Instant::now();
let (split_first, split_map_first) = apply_edge_splits_with_map(first, 0, &imprint)?;
let (split_second, split_map_second) = apply_edge_splits_with_map(second, 1, &imprint)?;
let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
profile_stage("edge_split_ms", stage_ms);
diagnostics.measure_max("timing.edge_split_ms", stage_ms);
let stage_started = Instant::now();
crate::classification::classify_profile_begin();
let fragments_a = crate::fragment::fragment_solid_against(&split_first, 0, &imprint, crate::fragment::bounded_carriers(second).then_some((bounds[1], policy.sew_search)))?;
let fragments_b = crate::fragment::fragment_solid_against(&split_second, 1, &imprint, crate::fragment::bounded_carriers(first).then_some((bounds[0], policy.sew_search)))?;
crate::classification::classify_profile_report("fragment");
let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
profile_stage("fragment_ms", stage_ms);
diagnostics.measure_max("timing.fragment_ms", stage_ms);
diagnostics.count_n(
"fragment.candidates",
(fragments_a.len() + fragments_b.len()) as u64,
);
touch.report_fragments(&[&fragments_a, &fragments_b], &mut diagnostics);
let a_surface_samples: Vec<Vec3> = fragments_a
.iter()
.flat_map(|fragment| {
std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
})
.collect();
let b_surface_samples: Vec<Vec3> = fragments_b
.iter()
.flat_map(|fragment| {
std::iter::once(fragment.test_point).chain(fragment.extra_test_points.iter().copied())
})
.collect();
let stage_started = Instant::now();
let mut barrier_edges: HashSet<(u8, u64)> = imprint
.pieces
.iter()
.filter_map(|piece| piece.shared_edge.map(|(operand, edge_id, _)| (operand, edge_id)))
.collect();
barrier_edges.extend(imprint.barrier_edges.iter().copied());
for (operand, split_map) in [(0u8, &split_map_first), (1u8, &split_map_second)] {
let minted: Vec<(u8, u64)> = barrier_edges
.iter()
.filter(|(barrier_operand, _)| *barrier_operand == operand)
.filter_map(|(_, edge_id)| split_map.get(edge_id))
.flat_map(|sub_ids| sub_ids.iter().map(|sub_id| (operand, *sub_id)))
.collect();
barrier_edges.extend(minted);
}
crate::classification::classify_profile_begin();
let coincident = coincident_pairs(
&fragments_a,
&fragments_b,
first,
second,
&imprint.cosurface_pairs,
tolerance,
)?;
let sector_mode = sector::sector_mode(options);
let mut select_census = sector::SelectCensus::default();
let mut selected = select_fragments(
fragments_a,
fragments_b,
first,
second,
operation,
tolerance,
&barrier_edges,
&imprint,
&coincident,
sector_mode,
&mut select_census,
)?;
let resewn = resew_coincident_rims(
&mut selected,
[&split_first, &split_second],
[first, second],
&imprint.cosurface_pairs,
operation,
tolerance,
)?;
diagnostics.count_n("select.coincident_pairs", coincident.len() as u64);
diagnostics.count_n("select.rims_resewn", resewn as u64);
crate::classification::classify_profile_report("select");
let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
profile_stage("select_ms", stage_ms);
diagnostics.measure_max("timing.select_ms", stage_ms);
diagnostics.count_n("select.fragments", selected.len() as u64);
diagnostics.count_n("select.sector_eligible", select_census.eligible);
diagnostics.count_n("select.sector_primary", select_census.sector_primary);
diagnostics.count_n("select.sector_fallback", select_census.sector_fallback);
diagnostics.count_n("select.sector_disagree", select_census.disagree);
diagnostics.count_n("select.ray_casts", select_census.ray_casts);
diagnostics.count_n("select.on_votes", select_census.on_votes);
diagnostics.count_n("select.overturns", select_census.overturns);
if sector_mode != sector::SectorMode::Off || std::env::var_os("BREP_SECTOR_CENSUS").is_some() {
sector::sector_census_line(|| {
format!(
"selection mode={sector_mode:?} select_ms={stage_ms:.3} selected={} eligible={} primary={} fallback={} disagree={} ray_casts={} on_votes={} overturns={}",
selected.len(),
select_census.eligible,
select_census.sector_primary,
select_census.sector_fallback,
select_census.disagree,
select_census.ray_casts,
select_census.on_votes,
select_census.overturns
)
});
}
if selected.is_empty()
&& !imprint.section_evidence
&& std::env::var("BREP_EMPTY_BOOLEAN").as_deref() != Ok("0")
{
let strictly_inside = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
for &sample in samples {
if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::In {
return Ok(true);
}
}
Ok(false)
};
let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, KernelRefusal> {
for &sample in samples {
if classify_point(sample, other, tolerance).or_refuse(KernelStage::Validate, "classify_point")?.class == PointClass::Out {
return Ok(true);
}
}
Ok(false)
};
let legitimate = match operation {
BooleanOperation::Intersect => {
!strictly_inside(&a_surface_samples, second)?
&& !strictly_inside(&b_surface_samples, first)?
}
BooleanOperation::Subtract => !outside_witness(&a_surface_samples, second)?,
BooleanOperation::Union => false,
};
if legitimate {
diagnostics.event(
DiagnosticSeverity::Info,
KernelStage::Select,
"boolean.empty_result",
format!("{operation:?} of witnessed-non-overlapping operands is empty"),
);
fit_scope.close().report_into(&mut diagnostics);
polyline_scope.close().report_into(&mut diagnostics);
diagnostics.measure_max(
"timing.total_ms",
operation_started.elapsed().as_secs_f64() * 1_000.0,
);
return Ok((
KernelOutcome {
value: empty_solid(),
diagnostics,
},
None,
));
}
}
let solids = [(0, &split_first), (1, &split_second)]
.into_iter()
.collect::<HashMap<_, _>>();
let dump = BooleanStageDump::open();
if let Some(dump) = &dump {
dump.write("first", &split_first);
dump.write("second", &split_second);
dump.write("selected", &selected);
}
let stage_started = Instant::now();
let assembled_origins: Vec<FaceKey> = if imprint.tangent_sections.is_empty() {
Vec::new()
} else {
selected
.iter()
.map(|fragment| FaceKey { operand: fragment.operand, face_id: fragment.source_face_id })
.collect()
};
let attributable: Vec<crate::imprint::TangentSectionRecord> = imprint
.tangent_sections
.iter()
.filter(|section| imprint.tangent_nodes.is_empty() || section.grazing.is_empty())
.cloned()
.collect();
let attribute = |error: KernelRefusal| {
attribute_to_tangent_section(error, &attributable, &assembled_origins)
};
let cellular_result = matches!(operation, BooleanOperation::Union)
&& cellular::enabled(options)
&& cellular::complete_contact(first, second, &selected, tolerance);
let (solid, scope) = if cellular_result {
(assemble::assemble_cellular_fragments(selected, &solids, &imprint, tolerance)
.map_err(attribute)?, None)
} else if untouched_passthrough(options) {
assemble_fragments_scoped(selected, &solids, &imprint, tolerance, &touch, touched_assembly(options))
.map_err(attribute)?
} else {
(assemble_fragments(selected, &solids, &imprint, tolerance).map_err(attribute)?, None)
};
if let Some(scope) = &scope {
diagnostics.count_n("passthrough.faces_examined", scope.examined.len() as u64);
}
if let Some(dump) = &dump {
dump.write("assembled", &solid);
}
let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
profile_stage("assemble_ms", stage_ms);
diagnostics.measure_max("timing.assemble_ms", stage_ms);
let stage_started = Instant::now();
if std::env::var("BREP_DEBUG_PREMERGE_VALIDATE").is_ok() {
let issues = solid.validate();
eprintln!(
"pre-merge validation: {} issue(s){}",
issues.len(),
issues
.first()
.map(|issue| format!(" — first: {}", issue.message))
.unwrap_or_default()
);
}
let solid = if solid.kind != crate::BodyKind::Cellular && options.merge_coplanar_faces
&& std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
{
let merged = match &scope {
Some(scope) => merge_same_surface_faces_scoped(
&solid,
tolerance,
&options.keep_unmerged_name_substrs,
&scope.examined,
)?,
None => merge_same_surface_faces_excluding(
&solid,
tolerance,
&options.keep_unmerged_name_substrs,
)?,
};
merge_curve_continuation_edges(&merged, tolerance).map_err(|refusal| KernelRefusal { stage: KernelStage::Validate, ..refusal })?
} else {
solid
};
if let Some(dump) = &dump {
dump.write("merged", &solid);
}
let stage_ms = stage_started.elapsed().as_secs_f64() * 1_000.0;
profile_stage("face_merge_ms", stage_ms);
diagnostics.measure_max("timing.face_merge_ms", stage_ms);
let validation_started = Instant::now();
let validation = solid.validate_detailed_scoped(&policy, scope.as_ref().map(|scope| &scope.examined));
profile_stage("final_validate_ms", validation_started.elapsed().as_secs_f64() * 1000.0);
diagnostics.count_n("validate.issues", validation.issues.len() as u64);
diagnostics.count_n(
"validate.wire_warnings",
validation.wire_warnings.len() as u64,
);
diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
for warning in &validation.wire_warnings {
diagnostics.event(
DiagnosticSeverity::Warning,
KernelStage::Validate,
"validate.uv_wire",
warning.message.clone(),
);
}
for issue in &validation.issues {
diagnostics.event(
DiagnosticSeverity::Error,
KernelStage::Validate,
"validate.brep",
issue.message.clone(),
);
}
fit_scope.close().report_into(&mut diagnostics);
polyline_scope.close().report_into(&mut diagnostics);
diagnostics.measure_max(
"timing.total_ms",
operation_started.elapsed().as_secs_f64() * 1_000.0,
);
if !validation.issues.is_empty() {
return Err(KernelRefusal::new(
RefusalClass::InvalidResultTopology { issues: validation.issues.len() as u32 },
KernelStage::Validate,
format!(
"boolean_operation: invalid result: {:?}",
validation.issues
)));
}
if std::env::var("BREP_DEBUG_BOOL").is_ok() {
if let Ok(report) =
crate::oracle::boolean_semantic_disagreement(first, second, operation, &solid, 200)
{
if report.is_flagged() {
eprintln!(
"[oracle] semantic disagreement rate {:.4} ({} of {} decidable points); sample {:?}",
report.disagreement_rate,
report.disagreements.len(),
report.considered,
report.sample_disagreement()
);
}
}
let fusable_band = crate::tolerance::assembler_weld(policy.model);
let fusables = crate::oracle::boolean_residual_fusables(&solid, fusable_band);
if !fusables.is_empty() {
eprintln!(
"[oracle] {} residual fusable(s) after weld; sample {:?}",
fusables.len(),
fusables.first()
);
}
}
let candidates = scope.as_ref().map(|scope| {
let candidates = soundness_candidates(
&solid,
scope,
[operand_bounds(first), operand_bounds(second)],
policy.sew_search,
);
diagnostics.count_n("passthrough.soundness_candidates", candidates.len() as u64);
crate::soundness::census::profile_operation(&solid, &[first, second], Some(&scope.touched), "boolean");
candidates
});
Ok((
KernelOutcome {
value: solid,
diagnostics,
},
candidates,
))
}