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::{
apply_edge_splits, apply_edge_splits_with_map, build_imprints, build_pcurve_on_surface,
build_pcurve_on_surface_range,
fragment_solid, interpolate_curve, merge_curve_continuation_edges, merge_same_surface_faces,
project_point_to_curve, project_point_to_surface, solid_signed_volume, AffineTransform,
DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage, KernelTolerances, NurbsCurve,
PointClass, SolidClassifier, 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,
}
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,
}
}
}
mod select;
mod assemble;
mod rim;
#[cfg(test)]
mod tests;
use rim::*;
use select::*;
#[cfg(test)]
use assemble::*;
pub(crate) use assemble::{
assemble_fragments, assemble_open_fragments, commit_nearby_edge_endpoints,
edge_interior_lies_on, finalize_assembled_solid,
};
#[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, String> {
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")
|| !is_degeneracy_error(&error)
{
return Err(error);
}
match perturbation_retry(first, second, operation, options) {
Some(solid) => Ok(solid),
None => Err(error),
}
}
}
}
fn is_degeneracy_error(message: &str) -> bool {
message.contains("invalid topology")
|| message.contains("non-integral genus")
|| message.contains("non-positive volume")
|| message.contains("open edges")
|| message.contains("singular/tangent-node")
}
fn perturbation_retry(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
) -> Option<BrepSolid> {
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] = [3e-5, 1e-4, 3e-4, 1e-3, 3e-3];
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 va = solid_signed_volume(first).ok().map(f64::abs);
let vb = solid_signed_volume(second).ok().map(f64::abs);
for &fraction in &FRACTIONS {
let magnitude = scale * fraction;
for dir in &DIRECTIONS {
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 candidate =
match boolean_operation_with_diagnostics(first, &moved, operation, options) {
Ok(outcome) => outcome.value,
Err(_) => continue,
};
if candidate.shells.is_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()) {
continue;
}
}
}
match crate::oracle::boolean_semantic_disagreement(
first, second, operation, &candidate, 400,
) {
Ok(report) if report.considered >= 30 && !report.is_flagged() => {
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 Some(candidate);
}
_ => continue,
}
}
}
if debug {
eprintln!("[perturb] ladder exhausted for {operation:?}; no rung produced a clean result");
}
None
}
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<(), String> {
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()?;
}
}
Ok(())
}
fn build_nary_imprint(
operands: &[BrepSolid],
options: &ImprintOptions,
) -> Result<ImprintResultRecord, String> {
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(|| "n-ary imprint: piece references unknown vertex".to_string())?;
mapped.end_vertex_id = *vertex_map
.get(&piece.end_vertex_id)
.ok_or_else(|| "n-ary imprint: piece references unknown vertex".to_string())?;
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(|| {
"n-ary imprint: by_face references unknown piece".to_string()
})?;
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,
barrier_edges: Vec::new(),
section_evidence,
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, String> {
if operands.is_empty() {
return Err("boolean_operation_nary: no operands provided".into());
}
if operands.len() == 1 {
return Ok(operands[0].clone());
}
if operands.len() > 255 {
return Err("boolean_operation_nary: at most 255 operands supported".into());
}
let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
let options = BooleanOptions::default();
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()?;
let tolerance = policy.model;
let mut healed = operands.to_vec();
for operand in &mut healed {
crate::heal::heal_operands(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(&solid, tolerance)?;
merge_curve_continuation_edges(&merged, tolerance)?
} else {
solid
};
let validation = solid.validate_detailed(&policy);
if !validation.issues.is_empty() {
return Err(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)
}
pub fn boolean_operation_with_diagnostics(
first: &BrepSolid,
second: &BrepSolid,
operation: BooleanOperation,
options: &BooleanOptions,
) -> Result<KernelOutcome<BrepSolid>, String> {
let policy = options
.tolerances
.unwrap_or_else(|| KernelTolerances::for_pair(first, second, options.tolerance));
policy.check()?;
let tolerance = policy.model;
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)?;
normalize_operand_band_seams(&mut first_owned)?;
normalize_operand_band_seams(&mut second_owned)?;
let first = &first_owned;
let second = &second_owned;
let mut diagnostics = KernelDiagnostics::default();
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 mut imprint_options = options.imprint.clone();
imprint_options.tolerance = tolerance;
let stage_started = Instant::now();
let imprint = build_imprints(first, second, &imprint_options)?;
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
);
}
}
diagnostics.measure_max(
"timing.imprint_ms",
stage_started.elapsed().as_secs_f64() * 1_000.0,
);
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 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)?;
diagnostics.measure_max(
"timing.edge_split_ms",
stage_started.elapsed().as_secs_f64() * 1_000.0,
);
let stage_started = Instant::now();
let fragments_a = fragment_solid(&split_first, 0, &imprint)?;
let fragments_b = fragment_solid(&split_second, 1, &imprint)?;
diagnostics.measure_max(
"timing.fragment_ms",
stage_started.elapsed().as_secs_f64() * 1_000.0,
);
diagnostics.count_n(
"fragment.candidates",
(fragments_a.len() + fragments_b.len()) as u64,
);
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);
}
let selected = select_fragments(
fragments_a,
fragments_b,
first,
second,
operation,
tolerance,
&barrier_edges,
)?;
diagnostics.measure_max(
"timing.select_ms",
stage_started.elapsed().as_secs_f64() * 1_000.0,
);
diagnostics.count_n("select.fragments", selected.len() as u64);
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, String> {
for &sample in samples {
if classify_point(sample, other, tolerance)?.class == PointClass::In {
return Ok(true);
}
}
Ok(false)
};
let outside_witness = |samples: &[Vec3], other: &BrepSolid| -> Result<bool, String> {
for &sample in samples {
if classify_point(sample, other, tolerance)?.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"),
);
diagnostics.measure_max(
"timing.total_ms",
operation_started.elapsed().as_secs_f64() * 1_000.0,
);
return Ok(KernelOutcome {
value: BrepSolid {
id: 0,
vertices: Vec::new(),
edges: Vec::new(),
shells: Vec::new(),
genus: 0,
},
diagnostics,
});
}
}
let solids = [(0, &split_first), (1, &split_second)]
.into_iter()
.collect::<HashMap<_, _>>();
let stage_started = Instant::now();
let solid = assemble_fragments(selected, &solids, &imprint, tolerance)?;
diagnostics.measure_max(
"timing.assemble_ms",
stage_started.elapsed().as_secs_f64() * 1_000.0,
);
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 options.merge_coplanar_faces
&& std::env::var("BREP_COPLANAR_MERGE").as_deref() != Ok("0")
{
let merged = merge_same_surface_faces(&solid, tolerance)?;
merge_curve_continuation_edges(&merged, tolerance)?
} else {
solid
};
diagnostics.measure_max(
"timing.face_merge_ms",
stage_started.elapsed().as_secs_f64() * 1_000.0,
);
let validation = solid.validate_detailed(&policy);
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(),
);
}
diagnostics.measure_max(
"timing.total_ms",
operation_started.elapsed().as_secs_f64() * 1_000.0,
);
if !validation.issues.is_empty() {
return Err(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()
);
}
}
Ok(KernelOutcome {
value: solid,
diagnostics,
})
}