pub mod assembly;
mod classify;
mod types;
use assembly::validate_boolean_result;
pub(crate) use assembly::{assemble_solid, assemble_solid_mixed};
pub use types::{BooleanOp, BooleanOptions, FaceSpec};
#[cfg(not(target_arch = "wasm32"))]
pub(super) fn timer_now() -> std::time::Instant {
std::time::Instant::now()
}
#[cfg(not(target_arch = "wasm32"))]
pub(super) fn timer_elapsed_ms(t: std::time::Instant) -> f64 {
t.elapsed().as_secs_f64() * 1000.0
}
#[cfg(target_arch = "wasm32")]
pub(super) fn timer_now() -> () {}
#[cfg(target_arch = "wasm32")]
pub(super) fn timer_elapsed_ms(_t: ()) -> f64 {
0.0
}
use brepkit_math::vec::{Point3, Vec3};
use brepkit_topology::Topology;
use brepkit_topology::edge::EdgeCurve;
use brepkit_topology::face::{FaceId, FaceSurface};
use brepkit_topology::solid::SolidId;
#[allow(clippy::too_many_lines)]
pub fn boolean(
topo: &mut Topology,
op: BooleanOp,
a: SolidId,
b: SolidId,
) -> Result<SolidId, crate::OperationsError> {
let tol = brepkit_math::tolerance::Tolerance::new();
{
use brepkit_algo::classifier::try_build_analytic_classifier;
let ca = try_build_analytic_classifier(topo, a);
let cb = try_build_analytic_classifier(topo, b);
let TrivialRelation {
identical,
a_in_b,
b_in_a,
} = detect_trivial_relation(topo, a, b, ca.as_ref(), cb.as_ref(), tol);
if identical {
return match op {
BooleanOp::Fuse | BooleanOp::Intersect => Ok(crate::copy::copy_solid(topo, a)?),
BooleanOp::Cut => Err(crate::OperationsError::EmptyResult {
reason: "Cut of identical solids".into(),
}),
};
}
if op == BooleanOp::Cut && a_in_b && !b_in_a {
return Err(crate::OperationsError::EmptyResult {
reason: "Cut with target fully contained in tool".into(),
});
}
if op == BooleanOp::Cut
&& b_in_a
&& !a_in_b
&& let Some(classifier) = ca.as_ref()
{
let tool_simple = topo
.solid(b)
.map(|s| s.inner_shells().is_empty())
.unwrap_or(false);
if tool_simple
&& solid_strictly_inside(topo, b, classifier, tol)
&& let Ok(result) = build_contained_cut_hollow(topo, a, b)
&& validate_boolean_result(topo, result).is_ok()
{
return Ok(result);
}
}
if (b_in_a || a_in_b) && op != BooleanOp::Cut {
return match (op, b_in_a, a_in_b) {
(BooleanOp::Fuse, true, _) => Ok(crate::copy::copy_solid(topo, a)?),
(BooleanOp::Fuse, _, true) => Ok(crate::copy::copy_solid(topo, b)?),
(BooleanOp::Intersect, true, _) => Ok(crate::copy::copy_solid(topo, b)?),
(BooleanOp::Intersect, _, true) => Ok(crate::copy::copy_solid(topo, a)?),
_ => Err(crate::OperationsError::InvalidInput {
reason: "containment shortcut: unexpected state".into(),
}),
};
}
if let (
Some(brepkit_algo::classifier::AnalyticClassifier::Cylinder {
origin: oa,
axis: aa,
radius: ra,
z_min: za_min,
z_max: za_max,
}),
Some(brepkit_algo::classifier::AnalyticClassifier::Cylinder {
origin: ob,
axis: ab,
radius: rb,
z_min: zb_min,
z_max: zb_max,
}),
) = (ca.as_ref(), cb.as_ref())
{
let same_axis_dir = aa.dot(*ab) > 1.0 - tol.angular;
let origin_offset = *ob - *oa;
let along_axis = origin_offset.dot(*aa);
let perpendicular = origin_offset - *aa * along_axis;
let coaxial = same_axis_dir && perpendicular.length() < tol.linear;
let same_radius = (ra - rb).abs() < tol.linear;
if coaxial && same_radius {
let za = (*za_min, *za_max);
let zb = (*zb_min + along_axis, *zb_max + along_axis);
if let Some(result) =
coaxial_cylinder_shortcut(topo, op, *oa, *aa, *ra, za, zb, tol)?
{
return Ok(result);
}
}
}
if let (
Some(brepkit_algo::classifier::AnalyticClassifier::Cone {
origin: oa,
axis: aa,
z_min: za_min,
z_max: za_max,
r_at_z_min: rmin_a,
r_at_z_max: rmax_a,
}),
Some(brepkit_algo::classifier::AnalyticClassifier::Cone {
origin: ob,
axis: ab,
z_min: zb_min,
z_max: zb_max,
r_at_z_min: rmin_b,
r_at_z_max: rmax_b,
}),
) = (ca.as_ref(), cb.as_ref())
{
let same_axis_dir = aa.dot(*ab) > 1.0 - tol.angular;
let same_apex = (*oa - *ob).length() < tol.linear;
let slope_a = if za_max.abs() > tol.linear {
Some(rmax_a / *za_max)
} else if za_min.abs() > tol.linear {
Some(rmin_a / *za_min)
} else {
None
};
let slope_b = if zb_max.abs() > tol.linear {
Some(rmax_b / *zb_max)
} else if zb_min.abs() > tol.linear {
Some(rmin_b / *zb_min)
} else {
None
};
let same_half_angle = match (slope_a, slope_b) {
(Some(sa), Some(sb)) => (sa - sb).abs() < tol.linear,
_ => false,
};
if let (true, Some(slope)) = (same_axis_dir && same_apex && same_half_angle, slope_a)
&& let Some(result) = coaxial_cone_shortcut(
topo,
op,
*oa,
*aa,
slope,
(*za_min, *za_max),
(*zb_min, *zb_max),
tol,
)?
{
return Ok(result);
}
}
if let (
Some(brepkit_algo::classifier::AnalyticClassifier::Box {
min: a_min,
max: a_max,
}),
Some(brepkit_algo::classifier::AnalyticClassifier::Box {
min: b_min,
max: b_max,
}),
) = (ca.as_ref(), cb.as_ref())
&& let Some(result) = box_pair_shortcut(topo, op, *a_min, *a_max, *b_min, *b_max, tol)?
{
return Ok(result);
}
if op == BooleanOp::Intersect {
let (box_args, sphere_args) = match (ca.as_ref(), cb.as_ref()) {
(
Some(brepkit_algo::classifier::AnalyticClassifier::Box {
min: bmin,
max: bmax,
}),
Some(brepkit_algo::classifier::AnalyticClassifier::Sphere { center, radius }),
) => (Some((*bmin, *bmax)), Some((*center, *radius))),
(
Some(brepkit_algo::classifier::AnalyticClassifier::Sphere { center, radius }),
Some(brepkit_algo::classifier::AnalyticClassifier::Box {
min: bmin,
max: bmax,
}),
) => (Some((*bmin, *bmax)), Some((*center, *radius))),
_ => (None, None),
};
if let (Some((bmin, bmax)), Some((sc, sr))) = (box_args, sphere_args) {
let segs = brepkit_topology::explorer::solid_vertices(topo, a)
.map(|v| v.len())
.unwrap_or(0)
.max(
brepkit_topology::explorer::solid_vertices(topo, b)
.map(|v| v.len())
.unwrap_or(0),
)
.max(16);
if let Some(result) =
box_sphere_intersect_shortcut(topo, bmin, bmax, sc, sr, segs, tol)?
{
return Ok(result);
}
}
}
if let (
Some(brepkit_algo::classifier::AnalyticClassifier::Sphere {
center: ca_center,
radius: ra,
}),
Some(brepkit_algo::classifier::AnalyticClassifier::Sphere {
center: cb_center,
radius: rb,
}),
) = (ca.as_ref(), cb.as_ref())
{
let coincident = (*ca_center - *cb_center).length() < tol.linear;
if coincident
&& let Some(result) =
concentric_sphere_shortcut(topo, op, a, b, *ca_center, *ra, *rb, tol)?
{
return Ok(result);
}
}
if let (
Some(brepkit_algo::classifier::AnalyticClassifier::Torus {
center: ca_center,
axis: aa,
major_radius: maj_a,
minor_radius: min_a,
}),
Some(brepkit_algo::classifier::AnalyticClassifier::Torus {
center: cb_center,
axis: ab,
major_radius: maj_b,
minor_radius: min_b,
}),
) = (ca.as_ref(), cb.as_ref())
{
let coincident = (*ca_center - *cb_center).length() < tol.linear;
let coaxial = aa.dot(*ab).abs() > 1.0 - tol.angular;
let same_major = (maj_a - maj_b).abs() < tol.linear;
if coincident
&& coaxial
&& same_major
&& let Some(result) = coaxial_torus_shortcut(
topo, op, a, b, *ca_center, *aa, *maj_a, *min_a, *min_b, tol,
)?
{
return Ok(result);
}
}
}
if op == BooleanOp::Intersect {
let bb_a = crate::measure::solid_bounding_box(topo, a).ok();
let bb_b = crate::measure::solid_bounding_box(topo, b).ok();
if let Some((a_box, b_box)) = bb_a.zip(bb_b)
&& aabbs_separated(&a_box, &b_box, tol.linear)
{
return Ok(topo.add_empty_solid());
}
}
if op == BooleanOp::Fuse && solids_provably_disjoint(topo, a, b, tol.linear) {
let copy_a = crate::copy::copy_solid(topo, a)?;
let copy_b = crate::copy::copy_solid(topo, b)?;
let merged = crate::compound_ops::merge_disjoint_solids(topo, &[copy_a, copy_b])?;
log::debug!("Fuse short-circuited via disjoint shell merge");
return Ok(merged);
}
if op == BooleanOp::Cut && solids_provably_disjoint(topo, a, b, tol.linear) {
let copy_a = crate::copy::copy_solid(topo, a)?;
log::debug!("Cut short-circuited: disjoint tool removes nothing");
return Ok(copy_a);
}
let algo_op = match op {
BooleanOp::Fuse => brepkit_algo::bop::BooleanOp::Fuse,
BooleanOp::Cut => brepkit_algo::bop::BooleanOp::Cut,
BooleanOp::Intersect => brepkit_algo::bop::BooleanOp::Intersect,
};
let gfa_a = if solid_has_flattenable_nurbs(topo, a, tol.linear)? {
let copy_a = crate::copy::copy_solid(topo, a)?;
let _ = flatten_planar_nurbs_faces(topo, copy_a, tol.linear)?;
copy_a
} else {
a
};
let gfa_b = if solid_has_flattenable_nurbs(topo, b, tol.linear)? {
let copy_b = crate::copy::copy_solid(topo, b)?;
let _ = flatten_planar_nurbs_faces(topo, copy_b, tol.linear)?;
copy_b
} else {
b
};
let gfa_start = timer_now();
match brepkit_algo::gfa::boolean(topo, algo_op, gfa_a, gfa_b) {
Ok(result) => {
let result_faces = brepkit_topology::explorer::solid_faces(topo, result)
.map(|f| f.len())
.unwrap_or(0);
if op == BooleanOp::Intersect && result_faces == 0 {
log::info!(
"GFA intersect empty in {:.1}ms (no common faces)",
timer_elapsed_ms(gfa_start)
);
return Ok(topo.add_empty_solid());
}
if result_faces > 0 {
let _ = crate::heal::remove_degenerate_edges(topo, result, tol.linear)?;
let _ = crate::heal::remove_wire_spurs(topo, result)?;
if has_free_edges(topo, result).unwrap_or(false) {
if let Err(e) =
unify_coincident_boundary_edges(topo, result, (tol.linear * 10.0).max(1e-6))
{
log::debug!("unify_coincident_boundary_edges failed: {e}");
}
}
let (f_pre, e_pre, v_pre) =
brepkit_topology::explorer::solid_entity_counts(topo, result)?;
#[allow(clippy::cast_possible_wrap)]
let euler_pre = (v_pre as i64) - (e_pre as i64) + (f_pre as i64);
let merged_vertices = euler_pre > 2;
if merged_vertices {
let _ = merge_result_vertices(topo, result, tol);
}
let (f2, e2, v2) = if merged_vertices {
brepkit_topology::explorer::solid_entity_counts(topo, result)?
} else {
(f_pre, e_pre, v_pre)
};
#[allow(clippy::cast_possible_wrap)]
let euler_pre2 = (v2 as i64) - (e2 as i64) + (f2 as i64);
#[allow(clippy::cast_possible_wrap)]
let inner_shell_surplus = 2 * (topo.solid(result)?.inner_shells().len() as i64);
let inner_wire_count_pre = solid_inner_wire_count(topo, result)?;
let euler_balanced_pre = euler_pre2 - inner_shell_surplus == 2
|| euler_balanced(euler_pre2 - inner_shell_surplus, inner_wire_count_pre, 1);
let manifold_pre = if euler_balanced_pre {
Some(is_closed_manifold(topo, result)?)
} else {
None
};
let (multi_balanced_pre, manifold_pre) = if euler_balanced_pre {
(false, manifold_pre)
} else {
let comps = crate::boolean::assembly::face_components(topo, result);
#[allow(clippy::cast_possible_wrap)]
let expected = (comps.len() as i64) * 2;
if comps.len() >= 2
&& euler_pre2 - inner_shell_surplus - inner_wire_count_pre == expected
&& components_are_disjoint_pieces(topo, &comps)
{
let m = is_closed_manifold(topo, result)?;
(m, Some(m))
} else {
(false, None)
}
};
let needs_unify =
!(euler_balanced_pre || multi_balanced_pre) || manifold_pre == Some(false);
let mut unified = false;
if needs_unify {
for _ in 0..3 {
if crate::heal::unify_faces(topo, result)? == 0 {
break;
}
unified = true;
}
}
let (f, e, v) = if unified {
brepkit_topology::explorer::solid_entity_counts(topo, result)?
} else {
(f2, e2, v2)
};
#[allow(clippy::cast_possible_wrap)]
let euler = (v as i64) - (e as i64) + (f as i64);
let open_shell_ok = op != BooleanOp::Intersect || !has_free_edges(topo, result)?;
let inner_wire_count = if unified {
solid_inner_wire_count(topo, result)?
} else {
inner_wire_count_pre
};
let closed_manifold = match manifold_pre {
Some(m) if !unified => m,
_ => is_closed_manifold(topo, result)?,
};
let hollow_ok = inner_shell_surplus == 0 || closed_manifold;
let euler_eff = euler - inner_shell_surplus;
let euler_ok = hollow_ok
&& (euler_eff == 2
|| (euler_balanced(euler_eff, inner_wire_count, 1) && closed_manifold));
if euler_ok && open_shell_ok && validate_boolean_result(topo, result).is_ok() {
log::info!(
"GFA boolean succeeded in {:.1}ms ({result_faces} faces)",
timer_elapsed_ms(gfa_start)
);
return Ok(result);
}
let components_vec = crate::boolean::assembly::face_components(topo, result);
let components = components_vec.len();
let cut_safe = op != BooleanOp::Cut
|| brepkit_algo::classifier::try_build_analytic_classifier(topo, b)
.as_ref()
.is_none_or(|cls_b| {
all_component_centers_outside(topo, &components_vec, cls_b, tol)
});
let intersect_safe = op != BooleanOp::Intersect
|| components_vec.iter().all(|comp| {
let Some(centre) = component_aabb_centre(topo, comp) else {
return true;
};
[a, b].iter().all(|&operand| {
!matches!(
crate::classify::classify_point_robust(
topo, operand, centre, 0.1, tol.linear,
),
Ok(crate::classify::PointClassification::Outside) | Err(_)
)
})
});
if matches!(op, BooleanOp::Cut | BooleanOp::Fuse | BooleanOp::Intersect)
&& components >= 2
&& euler_balanced(euler, inner_wire_count, i64::try_from(components).unwrap_or(i64::MAX))
&& components_are_disjoint_pieces(topo, &components_vec)
&& cut_safe
&& intersect_safe
&& closed_manifold
&& validate_boolean_result(topo, result).is_ok()
{
log::info!(
"GFA multi-region succeeded in {:.1}ms ({result_faces} faces, {components} pieces)",
timer_elapsed_ms(gfa_start)
);
return Ok(result);
}
log::debug!(
"GFA reject detail {op:?}: euler={euler} euler_eff={euler_eff} \
inner_wires={inner_wire_count} inner_shell_surplus={inner_shell_surplus} \
euler_ok={euler_ok} open_shell_ok={open_shell_ok} \
closed_manifold={closed_manifold} components={components} \
cut_safe={cut_safe} intersect_safe={intersect_safe} \
euler_multi_ok={} surplus={} bound={} disjoint={}",
euler_balanced(
euler,
inner_wire_count,
i64::try_from(components).unwrap_or(i64::MAX)
),
euler - inner_wire_count,
i64::try_from(components)
.unwrap_or(i64::MAX)
.saturating_mul(2),
components_are_disjoint_pieces(topo, &components_vec)
);
}
log::warn!(
"GFA result not accepted in {:.1}ms (faces={result_faces}, \
validate={:?}), falling back",
timer_elapsed_ms(gfa_start),
validate_boolean_result(topo, result).err()
);
}
Err(e) => {
log::warn!(
"GFA boolean failed in {:.1}ms ({e}), falling back",
timer_elapsed_ms(gfa_start)
);
}
}
if op == BooleanOp::Cut {
let components = crate::boolean::assembly::face_components(topo, a);
if components.len() >= 2
&& components_are_disjoint_pieces(topo, &components)
&& let Ok(result) = cut_multi_region_input(topo, a, b, components.len())
{
return Ok(result);
}
}
if op == BooleanOp::Fuse && topo.solid(b).is_ok_and(|s| s.inner_shells().is_empty()) {
let tool_components = crate::boolean::assembly::face_components(topo, b);
if tool_components.len() >= 2
&& components_are_disjoint_pieces(topo, &tool_components)
&& let Ok(result) = fuse_multi_component_tool(topo, a, tool_components)
{
return Ok(result);
}
}
log::debug!(
target: "brepkit_approx",
"boolean {op:?}: GFA unusable — using mesh (co-refinement) fallback; analytic surface types will be lost"
);
let opts = BooleanOptions::default();
let raw = match mesh_boolean_fallback(topo, op, a, b, opts.deflection, tol, &opts) {
Ok(raw) => raw,
Err(crate::OperationsError::EmptyResult { .. }) if op == BooleanOp::Intersect => {
return Ok(topo.add_empty_solid());
}
Err(e) => return Err(e),
};
let result = crate::copy::copy_solid(topo, raw)?;
let _ = crate::heal::remove_degenerate_edges(topo, result, tol.linear)?;
for _ in 0..3 {
if crate::heal::unify_faces(topo, result)? == 0 {
break;
}
}
Ok(enforce_manifold_shell(topo, result).unwrap_or(result))
}
pub fn boolean_with_options(
topo: &mut Topology,
op: BooleanOp,
a: SolidId,
b: SolidId,
opts: BooleanOptions,
) -> Result<SolidId, crate::OperationsError> {
let result = boolean(topo, op, a, b)?;
if opts.unify_faces {
let unify_opts = brepkit_heal::upgrade::unify_same_domain::UnifyOptions::default();
if let Err(e) =
brepkit_heal::upgrade::unify_same_domain::unify_same_domain(topo, result, &unify_opts)
{
log::debug!("boolean unify_faces post-processing failed: {e}");
}
}
Ok(result)
}
pub fn compound_cut(
topo: &mut Topology,
target: SolidId,
tools: &[SolidId],
opts: BooleanOptions,
) -> Result<SolidId, crate::OperationsError> {
let mut result = target;
let mut batched = false;
if tools.len() >= 2
&& let Some(clusters) = cluster_tools_by_aabb(topo, tools)
&& !clusters.is_empty()
{
let merged = clusters.iter().try_fold(None::<SolidId>, |acc, cluster| {
let fused = fuse_cluster(topo, cluster)?;
match acc {
None => Ok(Some(fused)),
Some(prev) => boolean(topo, BooleanOp::Fuse, prev, fused).map(Some),
}
});
if let Ok(Some(tool)) = merged
&& let Ok(cut) = boolean(topo, BooleanOp::Cut, target, tool)
{
result = cut;
batched = true;
} else {
log::debug!("compound_cut: batched tool path failed, using sequential cuts");
}
}
if !batched {
for &tool in tools {
result = boolean(topo, BooleanOp::Cut, result, tool)?;
}
}
if opts.unify_faces {
let unify_opts = brepkit_heal::upgrade::unify_same_domain::UnifyOptions::default();
if let Err(e) =
brepkit_heal::upgrade::unify_same_domain::unify_same_domain(topo, result, &unify_opts)
{
log::debug!("compound_cut unify_faces failed: {e}");
}
}
Ok(result)
}
pub(crate) fn fuse_cluster(
topo: &mut Topology,
cluster: &[SolidId],
) -> Result<SolidId, crate::OperationsError> {
let Some((&first, rest)) = cluster.split_first() else {
return Err(crate::OperationsError::InvalidInput {
reason: "fuse_cluster requires a non-empty cluster".into(),
});
};
if cluster.len() >= 3
&& let Ok(fused) = brepkit_algo::gfa::fuse_n(topo, cluster)
&& validate_boolean_result(topo, fused).is_ok()
{
return Ok(fused);
}
rest.iter()
.try_fold(first, |a, &t| boolean(topo, BooleanOp::Fuse, a, t))
}
fn cluster_tools_by_aabb(topo: &Topology, tools: &[SolidId]) -> Option<Vec<Vec<SolidId>>> {
fn find(parent: &mut Vec<usize>, i: usize) -> usize {
if parent[i] != i {
let root = find(parent, parent[i]);
parent[i] = root;
}
parent[i]
}
let tol = brepkit_math::tolerance::Tolerance::new().linear;
let mut boxes = Vec::with_capacity(tools.len());
for &t in tools {
boxes.push(crate::measure::solid_bounding_box(topo, t).ok()?);
}
let mut parent: Vec<usize> = (0..tools.len()).collect();
for i in 0..boxes.len() {
for j in (i + 1)..boxes.len() {
if boxes[i].expanded(tol).intersects(boxes[j]) {
let (ri, rj) = (find(&mut parent, i), find(&mut parent, j));
if ri != rj {
parent[ri] = rj;
}
}
}
}
let mut clusters: std::collections::BTreeMap<usize, Vec<SolidId>> =
std::collections::BTreeMap::new();
for i in 0..tools.len() {
let root = find(&mut parent, i);
clusters.entry(root).or_default().push(tools[i]);
}
Some(clusters.into_values().collect())
}
pub fn boolean_with_evolution(
topo: &mut Topology,
op: BooleanOp,
a: SolidId,
b: SolidId,
) -> Result<(SolidId, crate::evolution::EvolutionMap), crate::OperationsError> {
use brepkit_topology::explorer::solid_faces;
let trivial = a != b && {
use brepkit_algo::classifier::try_build_analytic_classifier;
let tol = brepkit_math::tolerance::Tolerance::new();
let ca = try_build_analytic_classifier(topo, a);
let cb = try_build_analytic_classifier(topo, b);
let rel = detect_trivial_relation(topo, a, b, ca.as_ref(), cb.as_ref(), tol);
rel.identical || rel.a_in_b || rel.b_in_a
};
if a != b && !trivial {
let input_indices: Vec<usize> = solid_faces(topo, a)?
.into_iter()
.chain(solid_faces(topo, b)?)
.map(brepkit_topology::arena::Id::index)
.collect();
let algo_op = match op {
BooleanOp::Fuse => brepkit_algo::bop::BooleanOp::Fuse,
BooleanOp::Cut => brepkit_algo::bop::BooleanOp::Cut,
BooleanOp::Intersect => brepkit_algo::bop::BooleanOp::Intersect,
};
if let Ok((result, origins)) =
brepkit_algo::gfa::boolean_with_face_origins(topo, algo_op, a, b)
{
let tol = brepkit_math::tolerance::Tolerance::default();
let healed_ok = crate::heal::remove_degenerate_edges(topo, result, tol.linear).is_ok()
&& crate::heal::remove_wire_spurs(topo, result).is_ok();
if healed_ok && validate_boolean_result(topo, result).is_ok() {
let mut evo = crate::evolution::EvolutionMap::new();
let mut sourced: std::collections::HashSet<usize> =
std::collections::HashSet::new();
for (out_idx, src) in origins {
if let Some(in_idx) = src {
evo.add_modified(in_idx, out_idx);
sourced.insert(in_idx);
}
}
for in_idx in input_indices {
if !sourced.contains(&in_idx) {
evo.add_deleted(in_idx);
}
}
return Ok((result, evo));
}
}
}
log::debug!("boolean_with_evolution: faithful GFA provenance unavailable, using heuristic");
let input_faces_a = collect_face_signatures(topo, a)?;
let input_faces_b = collect_face_signatures(topo, b)?;
let mut input_faces: Vec<(usize, Vec3, Point3)> =
Vec::with_capacity(input_faces_a.len() + input_faces_b.len());
input_faces.extend(input_faces_a);
input_faces.extend(input_faces_b);
let result = boolean(topo, op, a, b)?;
let output_faces = collect_face_signatures(topo, result)?;
let evo = crate::evolution::build_evolution_by_geometry(&input_faces, &output_faces);
Ok((result, evo))
}
fn box_pair_shortcut(
topo: &mut Topology,
op: BooleanOp,
a_min: Point3,
a_max: Point3,
b_min: Point3,
b_max: Point3,
tol: brepkit_math::tolerance::Tolerance,
) -> Result<Option<SolidId>, crate::OperationsError> {
let eps = tol.linear;
let (min, max) = match op {
BooleanOp::Intersect => {
let lo = Point3::new(
a_min.x().max(b_min.x()),
a_min.y().max(b_min.y()),
a_min.z().max(b_min.z()),
);
let hi = Point3::new(
a_max.x().min(b_max.x()),
a_max.y().min(b_max.y()),
a_max.z().min(b_max.z()),
);
if hi.x() <= lo.x() + eps || hi.y() <= lo.y() + eps || hi.z() <= lo.z() + eps {
return Ok(None);
}
(lo, hi)
}
BooleanOp::Fuse => {
let x_match =
(a_min.x() - b_min.x()).abs() < eps && (a_max.x() - b_max.x()).abs() < eps;
let y_match =
(a_min.y() - b_min.y()).abs() < eps && (a_max.y() - b_max.y()).abs() < eps;
let z_match =
(a_min.z() - b_min.z()).abs() < eps && (a_max.z() - b_max.z()).abs() < eps;
let matched = u8::from(x_match) + u8::from(y_match) + u8::from(z_match);
if matched < 2 {
return Ok(None);
}
if a_max.x() < b_min.x() - eps
|| b_max.x() < a_min.x() - eps
|| a_max.y() < b_min.y() - eps
|| b_max.y() < a_min.y() - eps
|| a_max.z() < b_min.z() - eps
|| b_max.z() < a_min.z() - eps
{
return Ok(None);
}
(
Point3::new(
a_min.x().min(b_min.x()),
a_min.y().min(b_min.y()),
a_min.z().min(b_min.z()),
),
Point3::new(
a_max.x().max(b_max.x()),
a_max.y().max(b_max.y()),
a_max.z().max(b_max.z()),
),
)
}
BooleanOp::Cut => {
return box_pair_cut_shortcut(topo, a_min, a_max, b_min, b_max, eps);
}
};
let dx = max.x() - min.x();
let dy = max.y() - min.y();
let dz = max.z() - min.z();
if dx <= eps || dy <= eps || dz <= eps {
return Ok(None);
}
let bx = crate::primitives::make_box(topo, dx, dy, dz)?;
if min.x().abs() > eps || min.y().abs() > eps || min.z().abs() > eps {
let xform = brepkit_math::mat::Mat4::translation(min.x(), min.y(), min.z());
crate::transform::transform_solid(topo, bx, &xform)?;
}
Ok(Some(bx))
}
fn box_pair_cut_shortcut(
topo: &mut Topology,
a_min: Point3,
a_max: Point3,
b_min: Point3,
b_max: Point3,
eps: f64,
) -> Result<Option<SolidId>, crate::OperationsError> {
let x_spans = b_min.x() <= a_min.x() + eps && b_max.x() >= a_max.x() - eps;
let y_spans = b_min.y() <= a_min.y() + eps && b_max.y() >= a_max.y() - eps;
let z_spans = b_min.z() <= a_min.z() + eps && b_max.z() >= a_max.z() - eps;
let spans_count = u8::from(x_spans) + u8::from(y_spans) + u8::from(z_spans);
if spans_count != 2 {
return Ok(None);
}
let (a_lo, a_hi, b_lo, b_hi) = if !x_spans {
(a_min.x(), a_max.x(), b_min.x(), b_max.x())
} else if !y_spans {
(a_min.y(), a_max.y(), b_min.y(), b_max.y())
} else {
(a_min.z(), a_max.z(), b_min.z(), b_max.z())
};
if b_hi <= a_lo + eps || b_lo >= a_hi - eps {
return Ok(None);
}
let cuts: Vec<(f64, f64)> = {
let mut pieces = Vec::with_capacity(2);
if b_lo > a_lo + eps {
pieces.push((a_lo, b_lo)); }
if b_hi < a_hi - eps {
pieces.push((b_hi, a_hi)); }
pieces
};
if cuts.is_empty() {
return Ok(None);
}
let piece_solids: Vec<SolidId> = cuts
.iter()
.map(|&(lo, hi)| -> Result<SolidId, crate::OperationsError> {
let (dx, dy, dz, tx, ty, tz) = if !x_spans {
(
hi - lo,
a_max.y() - a_min.y(),
a_max.z() - a_min.z(),
lo,
a_min.y(),
a_min.z(),
)
} else if !y_spans {
(
a_max.x() - a_min.x(),
hi - lo,
a_max.z() - a_min.z(),
a_min.x(),
lo,
a_min.z(),
)
} else {
(
a_max.x() - a_min.x(),
a_max.y() - a_min.y(),
hi - lo,
a_min.x(),
a_min.y(),
lo,
)
};
let bx = crate::primitives::make_box(topo, dx, dy, dz)?;
if tx.abs() > eps || ty.abs() > eps || tz.abs() > eps {
let xform = brepkit_math::mat::Mat4::translation(tx, ty, tz);
crate::transform::transform_solid(topo, bx, &xform)?;
}
Ok(bx)
})
.collect::<Result<_, _>>()?;
if piece_solids.len() == 1 {
return Ok(Some(piece_solids[0]));
}
let mut all_faces: Vec<brepkit_topology::face::FaceId> = Vec::new();
for &p in &piece_solids {
let p_data = topo.solid(p)?;
for &fid in topo.shell(p_data.outer_shell())?.faces() {
all_faces.push(fid);
}
}
Ok(Some(make_solid_from_face_subset(topo, &all_faces)?))
}
#[allow(clippy::too_many_arguments)]
fn coaxial_cylinder_shortcut(
topo: &mut Topology,
op: BooleanOp,
origin: Point3,
axis: Vec3,
radius: f64,
a_range: (f64, f64),
b_range: (f64, f64),
tol: brepkit_math::tolerance::Tolerance,
) -> Result<Option<SolidId>, crate::OperationsError> {
let (za_min, za_max) = a_range;
let (zb_min, zb_max) = b_range;
let touches_or_overlaps = zb_min <= za_max + tol.linear && za_min <= zb_max + tol.linear;
let (z_min, z_max) = match op {
BooleanOp::Fuse => {
if !touches_or_overlaps {
return Ok(None);
}
(za_min.min(zb_min), za_max.max(zb_max))
}
BooleanOp::Intersect => {
let lo = za_min.max(zb_min);
let hi = za_max.min(zb_max);
if hi <= lo + tol.linear {
return Ok(None);
}
(lo, hi)
}
BooleanOp::Cut => return Ok(None), };
let height = z_max - z_min;
if height <= tol.linear {
return Ok(None);
}
let cyl = crate::primitives::make_cylinder(topo, radius, height)?;
let world_origin = Point3::new(
origin.x() + axis.x() * z_min,
origin.y() + axis.y() * z_min,
origin.z() + axis.z() * z_min,
);
let xform = xform_from_canonical_z(world_origin, axis, tol);
crate::transform::transform_solid(topo, cyl, &xform)?;
Ok(Some(cyl))
}
#[allow(clippy::too_many_arguments)]
fn coaxial_cone_shortcut(
topo: &mut Topology,
op: BooleanOp,
apex: Point3,
axis: Vec3,
slope: f64,
a_range: (f64, f64),
b_range: (f64, f64),
tol: brepkit_math::tolerance::Tolerance,
) -> Result<Option<SolidId>, crate::OperationsError> {
let (za_min, za_max) = a_range;
let (zb_min, zb_max) = b_range;
let touches_or_overlaps = zb_min <= za_max + tol.linear && za_min <= zb_max + tol.linear;
let (z_min, z_max) = match op {
BooleanOp::Fuse => {
if !touches_or_overlaps {
return Ok(None);
}
(za_min.min(zb_min), za_max.max(zb_max))
}
BooleanOp::Intersect => {
let lo = za_min.max(zb_min);
let hi = za_max.min(zb_max);
if hi <= lo + tol.linear {
return Ok(None);
}
(lo, hi)
}
BooleanOp::Cut => return Ok(None),
};
let height = z_max - z_min;
if height <= tol.linear {
return Ok(None);
}
let r_at_z_min = slope * z_min;
let r_at_z_max = slope * z_max;
if r_at_z_min < -tol.linear || r_at_z_max < -tol.linear {
return Ok(None);
}
let r_bot = r_at_z_min.abs();
let r_top = r_at_z_max.abs();
if r_bot <= tol.linear && r_top <= tol.linear {
return Ok(None);
}
let cone = crate::primitives::make_cone(topo, r_bot, r_top, height)?;
let world_origin = Point3::new(
apex.x() + axis.x() * z_min,
apex.y() + axis.y() * z_min,
apex.z() + axis.z() * z_min,
);
let dot = axis.z().clamp(-1.0, 1.0);
if 1.0 - dot.abs() > tol.angular {
return Ok(None);
}
let xform = xform_from_canonical_z(world_origin, axis, tol);
crate::transform::transform_solid(topo, cone, &xform)?;
Ok(Some(cone))
}
#[allow(clippy::too_many_arguments)]
fn box_sphere_intersect_shortcut(
topo: &mut Topology,
box_min: Point3,
box_max: Point3,
sphere_center: Point3,
sphere_radius: f64,
sphere_segments: usize,
tol: brepkit_math::tolerance::Tolerance,
) -> Result<Option<SolidId>, crate::OperationsError> {
let r = sphere_radius;
let eps = tol.linear;
if r <= eps {
return Ok(None);
}
if box_max.x() <= box_min.x() + eps
|| box_max.y() <= box_min.y() + eps
|| box_max.z() <= box_min.z() + eps
{
return Ok(None);
}
let faces: [(Vec3, f64); 6] = [
(Vec3::new(-1.0, 0.0, 0.0), -box_min.x()),
(Vec3::new(1.0, 0.0, 0.0), box_max.x()),
(Vec3::new(0.0, -1.0, 0.0), -box_min.y()),
(Vec3::new(0.0, 1.0, 0.0), box_max.y()),
(Vec3::new(0.0, 0.0, -1.0), -box_min.z()),
(Vec3::new(0.0, 0.0, 1.0), box_max.z()),
];
let signed_dist = |n: Vec3, d: f64| -> f64 {
n.x() * sphere_center.x() + n.y() * sphere_center.y() + n.z() * sphere_center.z() - d
};
let mut cuts: Vec<usize> = Vec::new();
for (i, &(n, d)) in faces.iter().enumerate() {
let s = signed_dist(n, d);
if s >= r - eps {
return Ok(None);
}
if s.abs() < r - eps {
cuts.push(i);
}
}
if cuts.is_empty() {
let sphere = crate::primitives::make_sphere(topo, r, sphere_segments)?;
if sphere_center.x().abs() > eps
|| sphere_center.y().abs() > eps
|| sphere_center.z().abs() > eps
{
let xform = brepkit_math::mat::Mat4::translation(
sphere_center.x(),
sphere_center.y(),
sphere_center.z(),
);
crate::transform::transform_solid(topo, sphere, &xform)?;
}
return Ok(Some(sphere));
}
if cuts.len() == 3 {
return build_box_sphere_octant(topo, &faces, &cuts, sphere_center, r, tol);
}
Ok(None)
}
fn build_box_sphere_octant(
topo: &mut Topology,
faces: &[(Vec3, f64); 6],
cuts: &[usize],
sphere_center: Point3,
r: f64,
tol: brepkit_math::tolerance::Tolerance,
) -> Result<Option<SolidId>, crate::OperationsError> {
use brepkit_math::curves::Circle3D;
use brepkit_math::surfaces::SphericalSurface;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::{Face, FaceSurface};
use brepkit_topology::shell::Shell;
use brepkit_topology::solid::Solid;
use brepkit_topology::vertex::Vertex;
use brepkit_topology::wire::{OrientedEdge, Wire};
let cut_planes: Vec<(Vec3, f64)> = cuts.iter().map(|&i| faces[i]).collect();
let n0 = cut_planes[0].0;
let n1 = cut_planes[1].0;
let n2 = cut_planes[2].0;
if n0.dot(n1).abs() > tol.angular
|| n0.dot(n2).abs() > tol.angular
|| n1.dot(n2).abs() > tol.angular
{
return Ok(None);
}
let coord_from_axis = |axis: Vec3, d: f64| -> f64 {
if axis.x().abs() > 0.5 {
d * axis.x().signum()
} else if axis.y().abs() > 0.5 {
d * axis.y().signum()
} else {
d * axis.z().signum()
}
};
let mut o = [0.0_f64; 3];
for &(n, d) in &cut_planes {
if n.x().abs() > 0.5 {
o[0] = coord_from_axis(n, d);
} else if n.y().abs() > 0.5 {
o[1] = coord_from_axis(n, d);
} else {
o[2] = coord_from_axis(n, d);
}
}
let o = Point3::new(o[0], o[1], o[2]);
let in_dirs: Vec<Vec3> = cut_planes
.iter()
.map(|&(n, _)| Vec3::new(-n.x(), -n.y(), -n.z()))
.collect();
let mut sphere_pts: [Point3; 3] = [Point3::new(0.0, 0.0, 0.0); 3];
for (idx, &dir) in in_dirs.iter().enumerate() {
let vx = o.x() - sphere_center.x();
let vy = o.y() - sphere_center.y();
let vz = o.z() - sphere_center.z();
let v_dot_d = vx * dir.x() + vy * dir.y() + vz * dir.z();
let v_sq = vx * vx + vy * vy + vz * vz;
let disc = v_dot_d * v_dot_d - v_sq + r * r;
if disc < -tol.linear * tol.linear {
return Ok(None);
}
let t = -v_dot_d + disc.max(0.0).sqrt();
if t <= tol.linear {
return Ok(None);
}
sphere_pts[idx] = Point3::new(
o.x() + t * dir.x(),
o.y() + t * dir.y(),
o.z() + t * dir.z(),
);
}
let v_o = topo.add_vertex(Vertex::new(o, tol.linear));
let v_x = topo.add_vertex(Vertex::new(sphere_pts[0], tol.linear));
let v_y = topo.add_vertex(Vertex::new(sphere_pts[1], tol.linear));
let v_z = topo.add_vertex(Vertex::new(sphere_pts[2], tol.linear));
let e_ox = topo.add_edge(Edge::new(v_o, v_x, EdgeCurve::Line));
let e_oy = topo.add_edge(Edge::new(v_o, v_y, EdgeCurve::Line));
let e_oz = topo.add_edge(Edge::new(v_o, v_z, EdgeCurve::Line));
let mut build_arc_edge =
|n: Vec3,
p_start: Point3,
p_end: Point3,
start_vid,
end_vid|
-> Result<brepkit_topology::edge::EdgeId, crate::OperationsError> {
let dist = n.x() * (sphere_center.x() - p_start.x())
+ n.y() * (sphere_center.y() - p_start.y())
+ n.z() * (sphere_center.z() - p_start.z());
let circle_center = Point3::new(
sphere_center.x() - dist * n.x(),
sphere_center.y() - dist * n.y(),
sphere_center.z() - dist * n.z(),
);
let circle_r = (r * r - dist * dist).max(0.0).sqrt();
if circle_r <= tol.linear {
return Err(crate::OperationsError::InvalidInput {
reason: "box-sphere octant: degenerate arc radius".into(),
});
}
let dx = p_start.x() - circle_center.x();
let dy = p_start.y() - circle_center.y();
let dz = p_start.z() - circle_center.z();
let len = (dx * dx + dy * dy + dz * dz).sqrt();
if len <= tol.linear {
return Err(crate::OperationsError::InvalidInput {
reason: "box-sphere octant: degenerate arc reference".into(),
});
}
let u_ref = Vec3::new(dx / len, dy / len, dz / len);
let inward = Vec3::new(-n.x(), -n.y(), -n.z());
let circle =
Circle3D::new_with_ref(circle_center, inward, circle_r, u_ref).map_err(|e| {
crate::OperationsError::InvalidInput {
reason: format!("box-sphere octant: circle construction failed: {e}"),
}
})?;
let _ = p_end; Ok(topo.add_edge(Edge::new(start_vid, end_vid, EdgeCurve::Circle(circle))))
};
let arc_yz = build_arc_edge(n0, sphere_pts[1], sphere_pts[2], v_y, v_z)?;
let arc_zx = build_arc_edge(n1, sphere_pts[2], sphere_pts[0], v_z, v_x)?;
let arc_xy = build_arc_edge(n2, sphere_pts[0], sphere_pts[1], v_x, v_y)?;
let qd0_wire = Wire::new(
vec![
OrientedEdge::new(e_oy, true), OrientedEdge::new(arc_yz, true), OrientedEdge::new(e_oz, false), ],
true,
)
.map_err(crate::OperationsError::Topology)?;
let qd0_id = topo.add_wire(qd0_wire);
let qd0_face = topo.add_face(Face::new(
qd0_id,
Vec::new(),
FaceSurface::Plane {
normal: n0,
d: cut_planes[0].1,
},
));
let qd1_wire = Wire::new(
vec![
OrientedEdge::new(e_oz, true), OrientedEdge::new(arc_zx, true), OrientedEdge::new(e_ox, false), ],
true,
)
.map_err(crate::OperationsError::Topology)?;
let qd1_id = topo.add_wire(qd1_wire);
let qd1_face = topo.add_face(Face::new(
qd1_id,
Vec::new(),
FaceSurface::Plane {
normal: n1,
d: cut_planes[1].1,
},
));
let qd2_wire = Wire::new(
vec![
OrientedEdge::new(e_ox, true), OrientedEdge::new(arc_xy, true), OrientedEdge::new(e_oy, false), ],
true,
)
.map_err(crate::OperationsError::Topology)?;
let qd2_id = topo.add_wire(qd2_wire);
let qd2_face = topo.add_face(Face::new(
qd2_id,
Vec::new(),
FaceSurface::Plane {
normal: n2,
d: cut_planes[2].1,
},
));
let sph_wire = Wire::new(
vec![
OrientedEdge::new(arc_zx, false), OrientedEdge::new(arc_yz, false), OrientedEdge::new(arc_xy, false), ],
true,
)
.map_err(crate::OperationsError::Topology)?;
let sph_wire_id = topo.add_wire(sph_wire);
let sphere_surface = SphericalSurface::new(sphere_center, r).map_err(|e| {
crate::OperationsError::InvalidInput {
reason: format!("box-sphere octant: sphere surface construction failed: {e}"),
}
})?;
let sphere_face = topo.add_face(Face::new(
sph_wire_id,
Vec::new(),
FaceSurface::Sphere(sphere_surface),
));
let shell = Shell::new(vec![qd0_face, qd1_face, qd2_face, sphere_face])
.map_err(crate::OperationsError::Topology)?;
let shell_id = topo.add_shell(shell);
let solid = topo.add_solid(Solid::new(shell_id, Vec::new()));
Ok(Some(solid))
}
#[allow(clippy::too_many_arguments)]
fn concentric_sphere_shortcut(
topo: &mut Topology,
op: BooleanOp,
a: SolidId,
b: SolidId,
center: Point3,
r_a: f64,
r_b: f64,
tol: brepkit_math::tolerance::Tolerance,
) -> Result<Option<SolidId>, crate::OperationsError> {
if r_a <= tol.linear || r_b <= tol.linear {
return Ok(None);
}
let r_result = match op {
BooleanOp::Fuse => r_a.max(r_b),
BooleanOp::Intersect => {
r_a.min(r_b)
}
BooleanOp::Cut => return Ok(None),
};
let segments_a = brepkit_topology::explorer::solid_vertices(topo, a)
.map(|v| v.len())
.unwrap_or(0);
let segments_b = brepkit_topology::explorer::solid_vertices(topo, b)
.map(|v| v.len())
.unwrap_or(0);
let segments = segments_a.max(segments_b).max(4);
let sphere = crate::primitives::make_sphere(topo, r_result, segments)?;
if center.x().abs() > tol.linear
|| center.y().abs() > tol.linear
|| center.z().abs() > tol.linear
{
let xform = brepkit_math::mat::Mat4::translation(center.x(), center.y(), center.z());
crate::transform::transform_solid(topo, sphere, &xform)?;
}
Ok(Some(sphere))
}
#[allow(clippy::too_many_arguments)]
fn coaxial_torus_shortcut(
topo: &mut Topology,
op: BooleanOp,
a: SolidId,
b: SolidId,
center: Point3,
axis: Vec3,
major_radius: f64,
minor_a: f64,
minor_b: f64,
tol: brepkit_math::tolerance::Tolerance,
) -> Result<Option<SolidId>, crate::OperationsError> {
if minor_a <= tol.linear || minor_b <= tol.linear || major_radius <= tol.linear {
return Ok(None);
}
let minor_result = match op {
BooleanOp::Fuse => minor_a.max(minor_b),
BooleanOp::Intersect => {
minor_a.min(minor_b)
}
BooleanOp::Cut => return Ok(None),
};
if minor_result >= major_radius {
return Ok(None);
}
let segments_a = brepkit_topology::explorer::solid_vertices(topo, a)
.map(|v| v.len())
.unwrap_or(0);
let segments_b = brepkit_topology::explorer::solid_vertices(topo, b)
.map(|v| v.len())
.unwrap_or(0);
let segments = segments_a.max(segments_b).max(8);
let torus = crate::primitives::make_torus(topo, major_radius, minor_result, segments)?;
let xform = xform_from_canonical_z(center, axis, tol);
crate::transform::transform_solid(topo, torus, &xform)?;
Ok(Some(torus))
}
fn xform_from_canonical_z(
world_origin: Point3,
axis: Vec3,
tol: brepkit_math::tolerance::Tolerance,
) -> brepkit_math::mat::Mat4 {
let translate =
brepkit_math::mat::Mat4::translation(world_origin.x(), world_origin.y(), world_origin.z());
let canonical = Vec3::new(0.0, 0.0, 1.0);
let dot = canonical.dot(axis).clamp(-1.0, 1.0);
if 1.0 - dot < tol.angular {
return translate;
}
if 1.0 + dot < tol.angular {
return translate * brepkit_math::mat::Mat4::rotation_x(std::f64::consts::PI);
}
let sin_t = (1.0 - dot * dot).sqrt();
let kx = -axis.y() / sin_t;
let ky = axis.x() / sin_t;
let one_minus_cos = 1.0 - dot;
let r00 = one_minus_cos.mul_add(kx * kx, dot);
let r01 = one_minus_cos * kx * ky;
let r02 = sin_t * ky;
let r10 = one_minus_cos * kx * ky;
let r11 = one_minus_cos.mul_add(ky * ky, dot);
let r12 = -sin_t * kx;
let r20 = -sin_t * ky;
let r21 = sin_t * kx;
let r22 = dot;
let rot = brepkit_math::mat::Mat4([
[r00, r01, r02, 0.0],
[r10, r11, r12, 0.0],
[r20, r21, r22, 0.0],
[0.0, 0.0, 0.0, 1.0],
]);
translate * rot
}
fn aabbs_separated(
a: &brepkit_math::aabb::Aabb3,
b: &brepkit_math::aabb::Aabb3,
margin: f64,
) -> bool {
a.max.x() < b.min.x() + margin
|| b.max.x() < a.min.x() + margin
|| a.max.y() < b.min.y() + margin
|| b.max.y() < a.min.y() + margin
|| a.max.z() < b.min.z() + margin
|| b.max.z() < a.min.z() + margin
}
fn aabbs_clear_gap(
a: &brepkit_math::aabb::Aabb3,
b: &brepkit_math::aabb::Aabb3,
margin: f64,
) -> bool {
b.min.x() - a.max.x() > margin
|| a.min.x() - b.max.x() > margin
|| b.min.y() - a.max.y() > margin
|| a.min.y() - b.max.y() > margin
|| b.min.z() - a.max.z() > margin
|| a.min.z() - b.max.z() > margin
}
fn solids_provably_disjoint(topo: &Topology, a: SolidId, b: SolidId, margin: f64) -> bool {
let comps_a = assembly::face_components(topo, a);
let comps_b = assembly::face_components(topo, b);
if comps_a.is_empty() || comps_b.is_empty() {
return false;
}
let boxes = |comps: &[Vec<FaceId>]| -> Option<Vec<brepkit_math::aabb::Aabb3>> {
comps
.iter()
.map(|faces| crate::measure::face_set_bounding_box(topo, faces).ok())
.collect()
};
let (Some(boxes_a), Some(boxes_b)) = (boxes(&comps_a), boxes(&comps_b)) else {
return false;
};
boxes_a
.iter()
.all(|ba| boxes_b.iter().all(|bb| aabbs_clear_gap(ba, bb, margin)))
}
struct TrivialRelation {
identical: bool,
a_in_b: bool,
b_in_a: bool,
}
fn detect_trivial_relation(
topo: &Topology,
a: SolidId,
b: SolidId,
ca: Option<&brepkit_algo::classifier::AnalyticClassifier>,
cb: Option<&brepkit_algo::classifier::AnalyticClassifier>,
tol: brepkit_math::tolerance::Tolerance,
) -> TrivialRelation {
let sample_aabb = |topo: &Topology, solid: SolidId| -> Option<(Point3, Point3)> {
let bb = crate::measure::solid_bounding_box(topo, solid).ok()?;
Some((bb.min, bb.max))
};
let aabb_a = sample_aabb(topo, a);
let aabb_b = sample_aabb(topo, b);
let aabb_encloses =
|inner: &Option<(Point3, Point3)>, outer: &Option<(Point3, Point3)>| -> bool {
let Some(((i_min, i_max), (o_min, o_max))) = inner.zip(*outer) else {
return false;
};
let margin = tol.linear;
i_min.x() >= o_min.x() - margin
&& i_min.y() >= o_min.y() - margin
&& i_min.z() >= o_min.z() - margin
&& i_max.x() <= o_max.x() + margin
&& i_max.y() <= o_max.y() + margin
&& i_max.z() <= o_max.z() + margin
};
let aabb_strictly_contains =
|inner: &Option<(Point3, Point3)>, outer: &Option<(Point3, Point3)>| -> bool {
if !aabb_encloses(inner, outer) {
return false;
}
let Some(((i_min, i_max), (o_min, o_max))) = inner.zip(*outer) else {
return false;
};
let dims = [
(o_max.x() - o_min.x(), i_max.x() - i_min.x()),
(o_max.y() - o_min.y(), i_max.y() - i_min.y()),
(o_max.z() - o_min.z(), i_max.z() - i_min.z()),
];
dims.iter()
.all(|(outer_d, inner_d)| *outer_d > *inner_d * 1.1)
};
let center_outside =
|topo: &Topology, inner: SolidId, outer: SolidId, bb: &Option<(Point3, Point3)>| -> bool {
let Some((lo, hi)) = *bb else { return false };
let c = Point3::new(
0.5 * (lo.x() + hi.x()),
0.5 * (lo.y() + hi.y()),
0.5 * (lo.z() + hi.z()),
);
let (dx, dy, dz) = (hi.x() - lo.x(), hi.y() - lo.y(), hi.z() - lo.z());
let defl = (dx.mul_add(dx, dy.mul_add(dy, dz * dz)).sqrt() * 0.01).max(1e-6);
let inside_inner = matches!(
crate::classify::classify_point(topo, inner, c, defl, tol.linear),
Ok(crate::classify::PointClassification::Inside)
);
let outside_outer = matches!(
crate::classify::classify_point(topo, outer, c, defl, tol.linear),
Ok(crate::classify::PointClassification::Outside)
);
inside_inner && outside_outer
};
let all_b_verts_in_a = ca.is_some_and(|c| all_vertices_inside_or_on(topo, b, c, tol));
let all_a_verts_in_b = cb.is_some_and(|c| all_vertices_inside_or_on(topo, a, c, tol));
let aabbs_match = aabb_a
.zip(aabb_b)
.map(|((a_min, a_max), (b_min, b_max))| {
let eps = tol.linear;
(a_min.x() - b_min.x()).abs() < eps
&& (a_min.y() - b_min.y()).abs() < eps
&& (a_min.z() - b_min.z()).abs() < eps
&& (a_max.x() - b_max.x()).abs() < eps
&& (a_max.y() - b_max.y()).abs() < eps
&& (a_max.z() - b_max.z()).abs() < eps
})
.unwrap_or(false);
let b_in_a = ((all_b_verts_in_a && aabb_encloses(&aabb_b, &aabb_a))
|| (ca.is_none() && aabb_strictly_contains(&aabb_b, &aabb_a)))
&& !center_outside(topo, b, a, &aabb_b);
let a_in_b = ((all_a_verts_in_b && aabb_encloses(&aabb_a, &aabb_b))
|| (cb.is_none() && aabb_strictly_contains(&aabb_a, &aabb_b)))
&& !center_outside(topo, a, b, &aabb_a);
TrivialRelation {
identical: aabbs_match && all_b_verts_in_a && all_a_verts_in_b,
a_in_b,
b_in_a,
}
}
fn all_vertices_inside_or_on(
topo: &Topology,
solid: SolidId,
classifier: &brepkit_algo::classifier::AnalyticClassifier,
tol: brepkit_math::tolerance::Tolerance,
) -> bool {
let Ok(s) = topo.solid(solid) else {
return false;
};
let Ok(sh) = topo.shell(s.outer_shell()) else {
return false;
};
for &fid in sh.faces() {
let Ok(f) = topo.face(fid) else { return false };
let Ok(w) = topo.wire(f.outer_wire()) else {
return false;
};
for oe in w.edges() {
let Ok(e) = topo.edge(oe.edge()) else {
return false;
};
for vid in [e.start(), e.end()] {
let Ok(v) = topo.vertex(vid) else {
return false;
};
if classifier.classify(v.point(), tol) == Some(brepkit_algo::FaceClass::Outside) {
return false;
}
}
}
}
true
}
fn solid_strictly_inside(
topo: &Topology,
inner: SolidId,
classifier: &brepkit_algo::classifier::AnalyticClassifier,
tol: brepkit_math::tolerance::Tolerance,
) -> bool {
let Ok(s) = topo.solid(inner) else {
return false;
};
let Ok(sh) = topo.shell(s.outer_shell()) else {
return false;
};
let mut saw_vertex = false;
for &fid in sh.faces() {
let Ok(f) = topo.face(fid) else { return false };
let mut wires = vec![f.outer_wire()];
wires.extend_from_slice(f.inner_wires());
for wid in wires {
let Ok(w) = topo.wire(wid) else {
return false;
};
for oe in w.edges() {
let Ok(e) = topo.edge(oe.edge()) else {
return false;
};
for vid in [e.start(), e.end()] {
let Ok(v) = topo.vertex(vid) else {
return false;
};
if classifier.classify(v.point(), tol) != Some(brepkit_algo::FaceClass::Inside)
{
return false;
}
saw_vertex = true;
}
}
}
}
saw_vertex
}
fn build_contained_cut_hollow(
topo: &mut Topology,
blank: SolidId,
tool: SolidId,
) -> Result<SolidId, crate::OperationsError> {
let result = crate::copy::copy_solid(topo, blank)?;
let tool_copy = crate::copy::copy_solid(topo, tool)?;
let cavity_shell = topo.solid(tool_copy)?.outer_shell();
let cavity_faces = topo.shell(cavity_shell)?.faces().to_vec();
for fid in cavity_faces {
let face = topo.face_mut(fid)?;
let flipped = !face.is_reversed();
face.set_reversed(flipped);
}
topo.solid_mut(result)?.add_inner_shell(cavity_shell);
Ok(result)
}
fn mesh_boolean_fallback(
topo: &mut Topology,
op: BooleanOp,
a: SolidId,
b: SolidId,
deflection: f64,
tol: brepkit_math::tolerance::Tolerance,
opts: &BooleanOptions,
) -> Result<SolidId, crate::OperationsError> {
let mesh_a = crate::tessellate::tessellate_solid_for_boolean(topo, a, deflection, 0.0)?;
let mesh_b = crate::tessellate::tessellate_solid_for_boolean(topo, b, deflection, 0.0)?;
log::debug!(
"mesh fallback {op:?}: tessellated operands to {} + {} triangles at deflection {deflection}",
mesh_a.indices.len() / 3,
mesh_b.indices.len() / 3,
);
let mb_result = crate::mesh_boolean::mesh_boolean(&mesh_a, &mesh_b, op, tol.linear)?;
if mb_result.boundary_edge_count > 0 || mb_result.non_manifold_edge_count > 0 {
log::warn!(
"boolean {op:?}: mesh boolean fallback output is NOT a closed 2-manifold \
({} boundary edge(s), {} non-manifold edge(s) after position welding) — \
downstream healing may not recover; exported geometry may be broken",
mb_result.boundary_edge_count,
mb_result.non_manifold_edge_count,
);
}
let face_specs = mesh_result_to_face_specs(&mb_result);
if face_specs.is_empty() {
return Err(crate::OperationsError::EmptyResult {
reason: "mesh boolean produced no output faces".into(),
});
}
log::debug!(
"mesh fallback {op:?}: {} face specs -> assemble_solid_mixed",
face_specs.len()
);
let result = assemble_solid_mixed(topo, &face_specs, tol)?;
let _ = crate::heal::remove_degenerate_edges(topo, result, tol.linear)?;
if opts.unify_faces {
let _ = crate::heal::unify_faces(topo, result)?;
}
let collapsed =
brepkit_heal::upgrade::collapse_collinear_vertices::collapse_collinear_wire_vertices(
topo, result, tol,
)
.unwrap_or_else(|e| {
log::warn!("boolean {op:?}: collapse_collinear_wire_vertices failed: {e}");
0
});
if collapsed > 0 {
log::info!(
"boolean {op:?}: collapsed {collapsed} collinear interior wire vertex/vertices post-mesh-assembly",
);
}
let wires_split =
brepkit_heal::upgrade::split_self_intersecting_wires::split_self_intersecting_inner_wires(
topo, result,
)
.unwrap_or_else(|e| {
log::warn!("boolean {op:?}: split_self_intersecting_inner_wires failed: {e}");
0
});
if wires_split > 0 {
log::info!(
"boolean {op:?}: split {wires_split} self-intersecting inner wire(s) post-mesh-assembly",
);
}
if opts.heal_after_boolean {
let _ = crate::heal::heal_solid(topo, result, tol.linear)?;
}
assembly::validate_boolean_result_lenient(topo, result)?;
log::info!(
"boolean {op:?}: mesh boolean path → solid {} ({} faces, surface types lost)",
result.index(),
face_specs.len()
);
Ok(result)
}
fn mesh_result_to_face_specs(result: &crate::mesh_boolean::MeshBooleanResult) -> Vec<FaceSpec> {
let mut specs = Vec::new();
for tri in result.mesh.indices.chunks_exact(3) {
let v0 = result.mesh.positions[tri[0] as usize];
let v1 = result.mesh.positions[tri[1] as usize];
let v2 = result.mesh.positions[tri[2] as usize];
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let Ok(normal) = edge1.cross(edge2).normalize() else {
continue;
};
let d = crate::dot_normal_point(normal, v0);
specs.push(FaceSpec::Planar {
vertices: vec![v0, v1, v2],
normal,
d,
inner_wires: vec![],
});
}
specs
}
fn all_component_centers_outside(
topo: &Topology,
components: &[Vec<FaceId>],
classifier: &brepkit_algo::classifier::AnalyticClassifier,
tol: brepkit_math::tolerance::Tolerance,
) -> bool {
use brepkit_algo::FaceClass;
for comp in components {
let mut min = Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut max = Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for &fid in comp {
let Ok(face) = topo.face(fid) else { continue };
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied())
{
let Ok(wire) = topo.wire(wid) else { continue };
for oe in wire.edges() {
let Ok(edge) = topo.edge(oe.edge()) else {
continue;
};
for vid in [edge.start(), edge.end()] {
if let Ok(v) = topo.vertex(vid) {
let p = v.point();
min = Point3::new(
min.x().min(p.x()),
min.y().min(p.y()),
min.z().min(p.z()),
);
max = Point3::new(
max.x().max(p.x()),
max.y().max(p.y()),
max.z().max(p.z()),
);
}
}
}
}
}
let centre = Point3::new(
(min.x() + max.x()) * 0.5,
(min.y() + max.y()) * 0.5,
(min.z() + max.z()) * 0.5,
);
if matches!(classifier.classify(centre, tol), Some(FaceClass::Inside)) {
return false;
}
}
true
}
fn component_aabb_centre(topo: &Topology, comp: &[FaceId]) -> Option<Point3> {
let mut min = Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut max = Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for &fid in comp {
let Ok(face) = topo.face(fid) else { continue };
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
let Ok(wire) = topo.wire(wid) else { continue };
for oe in wire.edges() {
let Ok(edge) = topo.edge(oe.edge()) else {
continue;
};
for vid in [edge.start(), edge.end()] {
if let Ok(v) = topo.vertex(vid) {
let p = v.point();
min =
Point3::new(min.x().min(p.x()), min.y().min(p.y()), min.z().min(p.z()));
max =
Point3::new(max.x().max(p.x()), max.y().max(p.y()), max.z().max(p.z()));
}
}
}
}
}
if min.x() > max.x() {
return None;
}
Some(Point3::new(
(min.x() + max.x()) * 0.5,
(min.y() + max.y()) * 0.5,
(min.z() + max.z()) * 0.5,
))
}
fn component_encloses_point(
topo: &Topology,
faces: &[FaceId],
p: Point3,
deflection: f64,
) -> Option<bool> {
let dir = Vec3::new(2.0_f64.sqrt(), 3.0_f64.sqrt(), 5.0_f64.sqrt())
.normalize()
.ok()?;
let mut crossings = 0usize;
let mut any_triangle = false;
for &fid in faces {
let mesh = crate::tessellate::tessellate_with_uvs(topo, fid, deflection).ok()?;
let pos = &mesh.mesh.positions;
for tri in mesh.mesh.indices.chunks_exact(3) {
let (a, b, c) = (
pos[tri[0] as usize],
pos[tri[1] as usize],
pos[tri[2] as usize],
);
any_triangle = true;
if let Some(hit) =
brepkit_math::ray_triangle::watertight_ray_triangle_intersect(p, dir, a, b, c)
&& hit.t > 1e-9
{
crossings += 1;
}
}
}
any_triangle.then_some(crossings % 2 == 1)
}
fn any_vertex_of(topo: &Topology, faces: &[FaceId]) -> Option<Point3> {
for &fid in faces {
let face = topo.face(fid).ok()?;
let wire = topo.wire(face.outer_wire()).ok()?;
if let Some(oe) = wire.edges().first()
&& let Ok(edge) = topo.edge(oe.edge())
&& let Ok(v) = topo.vertex(edge.start())
{
return Some(v.point());
}
}
None
}
fn components_are_disjoint_pieces(topo: &Topology, components: &[Vec<FaceId>]) -> bool {
let aabbs: Vec<(Point3, Point3)> = components
.iter()
.map(|comp| {
let mut min = Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut max = Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for &fid in comp {
let Ok(face) = topo.face(fid) else {
continue;
};
for wid in
std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied())
{
let Ok(wire) = topo.wire(wid) else {
continue;
};
for oe in wire.edges() {
let Ok(edge) = topo.edge(oe.edge()) else {
continue;
};
for vid in [edge.start(), edge.end()] {
if let Ok(v) = topo.vertex(vid) {
let p = v.point();
min = Point3::new(
min.x().min(p.x()),
min.y().min(p.y()),
min.z().min(p.z()),
);
max = Point3::new(
max.x().max(p.x()),
max.y().max(p.y()),
max.z().max(p.z()),
);
}
}
}
}
}
(min, max)
})
.collect();
let eps = 1e-7;
let contains = |(o_min, o_max): (Point3, Point3), (i_min, i_max): (Point3, Point3)| {
o_min.x() - eps <= i_min.x()
&& o_min.y() - eps <= i_min.y()
&& o_min.z() - eps <= i_min.z()
&& o_max.x() + eps >= i_max.x()
&& o_max.y() + eps >= i_max.y()
&& o_max.z() + eps >= i_max.z()
};
for i in 0..aabbs.len() {
for j in (i + 1)..aabbs.len() {
let (outer, inner) = if contains(aabbs[i], aabbs[j]) {
(i, j)
} else if contains(aabbs[j], aabbs[i]) {
(j, i)
} else {
continue;
};
let (o_min, o_max) = aabbs[outer];
let diag = ((o_max.x() - o_min.x()).powi(2)
+ (o_max.y() - o_min.y()).powi(2)
+ (o_max.z() - o_min.z()).powi(2))
.sqrt();
let deflection = (diag / 200.0).max(1e-4);
let Some(probe) = any_vertex_of(topo, &components[inner]) else {
return false;
};
match component_encloses_point(topo, &components[outer], probe, deflection) {
Some(true) => return false,
Some(false) => {}
None => return false,
}
}
}
true
}
fn fuse_multi_component_tool(
topo: &mut Topology,
a: SolidId,
b_components: Vec<Vec<brepkit_topology::face::FaceId>>,
) -> Result<SolidId, crate::OperationsError> {
let mut result = a;
for comp_faces in b_components {
let comp_solid_raw = make_solid_from_face_subset(topo, &comp_faces)?;
let comp_solid = crate::copy::copy_solid(topo, comp_solid_raw)?;
result = boolean(topo, BooleanOp::Fuse, result, comp_solid)?;
}
Ok(result)
}
fn cut_multi_region_input(
topo: &mut Topology,
a: SolidId,
b: SolidId,
comp_count: usize,
) -> Result<SolidId, crate::OperationsError> {
let components = crate::boolean::assembly::face_components(topo, a);
debug_assert_eq!(components.len(), comp_count);
let mut per_component_results: Vec<SolidId> = Vec::with_capacity(components.len());
for comp_faces in components {
let comp_solid_raw = make_solid_from_face_subset(topo, &comp_faces)?;
let comp_solid = crate::copy::copy_solid(topo, comp_solid_raw)?;
match boolean(topo, BooleanOp::Cut, comp_solid, b) {
Ok(r) => per_component_results.push(r),
Err(
crate::OperationsError::EmptyResult { .. }
| crate::OperationsError::InvalidInput { .. },
) => {
per_component_results.push(comp_solid);
}
Err(e) => return Err(e),
}
}
let mut all_faces: Vec<brepkit_topology::face::FaceId> = Vec::new();
for &r in &per_component_results {
let r_data = topo.solid(r)?;
for &fid in topo.shell(r_data.outer_shell())?.faces() {
all_faces.push(fid);
}
}
make_solid_from_face_subset(topo, &all_faces)
}
fn make_solid_from_face_subset(
topo: &mut Topology,
faces: &[brepkit_topology::face::FaceId],
) -> Result<SolidId, crate::OperationsError> {
use brepkit_topology::face::{Face, FaceSurface};
use brepkit_topology::wire::{OrientedEdge, Wire};
let mut normalized: Vec<brepkit_topology::face::FaceId> = Vec::with_capacity(faces.len());
for &fid in faces {
let face = topo.face(fid)?;
if !face.is_reversed() {
normalized.push(fid);
continue;
}
let flipped_surface = match face.surface() {
FaceSurface::Plane { normal, d } => FaceSurface::Plane {
normal: -*normal,
d: -*d,
},
FaceSurface::Nurbs(_)
| FaceSurface::Cylinder(_)
| FaceSurface::Cone(_)
| FaceSurface::Sphere(_)
| FaceSurface::Torus(_) => {
normalized.push(fid);
continue;
}
};
let outer_wid = face.outer_wire();
let inner_wids: Vec<_> = face.inner_wires().to_vec();
let outer_wire = topo.wire(outer_wid)?;
let outer_reversed: Vec<OrientedEdge> = outer_wire
.edges()
.iter()
.rev()
.map(|oe| OrientedEdge::new(oe.edge(), !oe.is_forward()))
.collect();
let new_outer_wire =
Wire::new(outer_reversed, true).map_err(crate::OperationsError::Topology)?;
let new_outer_wid = topo.add_wire(new_outer_wire);
let mut new_inner_wids = Vec::with_capacity(inner_wids.len());
for iw in &inner_wids {
let w = topo.wire(*iw)?;
let rev: Vec<OrientedEdge> = w
.edges()
.iter()
.rev()
.map(|oe| OrientedEdge::new(oe.edge(), !oe.is_forward()))
.collect();
let new_w = Wire::new(rev, true).map_err(crate::OperationsError::Topology)?;
new_inner_wids.push(topo.add_wire(new_w));
}
let new_face = Face::new(new_outer_wid, new_inner_wids, flipped_surface);
normalized.push(topo.add_face(new_face));
}
let shell = brepkit_topology::shell::Shell::new(normalized)
.map_err(crate::OperationsError::Topology)?;
let shell_id = topo.add_shell(shell);
let solid = brepkit_topology::solid::Solid::new(shell_id, Vec::new());
Ok(topo.add_solid(solid))
}
fn solid_inner_wire_count(topo: &Topology, solid: SolidId) -> Result<i64, crate::OperationsError> {
let mut count: i64 = 0;
for fid in brepkit_topology::explorer::solid_faces(topo, solid)? {
let face = topo.face(fid)?;
#[allow(clippy::cast_possible_wrap)]
{
count += face.inner_wires().len() as i64;
}
}
Ok(count)
}
const fn euler_balanced(euler: i64, inner_wires: i64, components: i64) -> bool {
let surplus = euler - inner_wires;
surplus <= components.saturating_mul(2) && surplus % 2 == 0
}
fn solid_edge_use_counts(
topo: &Topology,
solid: SolidId,
) -> Result<std::collections::HashMap<usize, usize>, crate::OperationsError> {
let mut counts: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
for fid in brepkit_topology::explorer::solid_faces(topo, solid)? {
let face = topo.face(fid)?;
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
let wire = topo.wire(wid)?;
for oe in wire.edges() {
*counts.entry(oe.edge().index()).or_insert(0) += 1;
}
}
}
Ok(counts)
}
fn is_closed_manifold(topo: &Topology, solid: SolidId) -> Result<bool, crate::OperationsError> {
let s = topo.solid(solid)?;
let shell_ids: Vec<_> = std::iter::once(s.outer_shell())
.chain(s.inner_shells().iter().copied())
.collect();
for shell_id in shell_ids {
let shell = topo.shell(shell_id)?;
if !shell_is_closed_manifold(topo, shell)? {
return Ok(false);
}
}
Ok(true)
}
fn shell_is_closed_manifold(
topo: &Topology,
shell: &brepkit_topology::shell::Shell,
) -> Result<bool, crate::OperationsError> {
use std::collections::HashMap;
let mut counts: HashMap<usize, usize> = HashMap::new();
for &fid in shell.faces() {
let face = topo.face(fid)?;
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
let wire = topo.wire(wid)?;
for oe in wire.edges() {
*counts.entry(oe.edge().index()).or_insert(0) += 1;
}
}
}
if counts.is_empty() {
return Ok(false);
}
Ok(counts.values().all(|&c| c == 2))
}
fn has_free_edges(topo: &Topology, solid: SolidId) -> Result<bool, crate::OperationsError> {
let counts = solid_edge_use_counts(topo, solid)?;
Ok(counts.values().any(|&c| c == 1))
}
fn solid_has_flattenable_nurbs(
topo: &Topology,
solid: SolidId,
tol: f64,
) -> Result<bool, crate::OperationsError> {
use brepkit_geometry::convert::{
RecognizedCurve, RecognizedSurface, recognize_curve, recognize_surface,
};
use brepkit_topology::edge::EdgeCurve;
use brepkit_topology::explorer::solid_faces;
let mut seen = std::collections::HashSet::new();
for fid in solid_faces(topo, solid)? {
let face = topo.face(fid)?;
if let FaceSurface::Nurbs(nurbs) = face.surface()
&& matches!(
recognize_surface(nurbs, tol),
RecognizedSurface::Plane { .. }
)
{
return Ok(true);
}
for &wid in std::iter::once(&face.outer_wire()).chain(face.inner_wires()) {
let wire = topo.wire(wid)?;
for oe in wire.edges() {
let eid = oe.edge();
if !seen.insert(eid.index()) {
continue;
}
if let EdgeCurve::NurbsCurve(nurbs) = topo.edge(eid)?.curve()
&& matches!(recognize_curve(nurbs, tol), RecognizedCurve::Line { .. })
{
return Ok(true);
}
}
}
}
Ok(false)
}
fn flatten_planar_nurbs_faces(
topo: &mut Topology,
solid: SolidId,
tol: f64,
) -> Result<usize, crate::OperationsError> {
use brepkit_geometry::convert::{
RecognizedCurve, RecognizedSurface, recognize_curve, recognize_surface,
};
use brepkit_topology::edge::{EdgeCurve, EdgeId};
use brepkit_topology::explorer::solid_faces;
let face_ids = solid_faces(topo, solid)?;
let planar: Vec<(FaceId, Vec3, f64)> = face_ids
.iter()
.filter_map(|&fid| {
let face = topo.face(fid).ok()?;
let FaceSurface::Nurbs(nurbs) = face.surface() else {
return None;
};
match recognize_surface(nurbs, tol) {
RecognizedSurface::Plane { normal, d } => {
let (u0, u1) = nurbs.domain_u();
let (v0, v1) = nurbs.domain_v();
let mid_n = nurbs.normal(0.5 * (u0 + u1), 0.5 * (v0 + v1)).ok();
let (normal, d) = match mid_n {
Some(n) if normal.dot(n) < 0.0 => (-normal, -d),
_ => (normal, d),
};
Some((fid, normal, d))
}
_ => None,
}
})
.collect();
let count = planar.len();
for (fid, normal, d) in planar {
topo.face_mut(fid)?
.set_surface(FaceSurface::Plane { normal, d });
}
let mut straight_edges: Vec<EdgeId> = Vec::new();
let mut seen = std::collections::HashSet::new();
for &fid in &face_ids {
let face = topo.face(fid)?;
for &wid in std::iter::once(&face.outer_wire()).chain(face.inner_wires()) {
let wire = topo.wire(wid)?;
for oe in wire.edges() {
let eid = oe.edge();
if !seen.insert(eid.index()) {
continue;
}
let EdgeCurve::NurbsCurve(nurbs) = topo.edge(eid)?.curve() else {
continue;
};
if matches!(recognize_curve(nurbs, tol), RecognizedCurve::Line { .. }) {
straight_edges.push(eid);
}
}
}
}
for eid in straight_edges {
topo.edge_mut(eid)?.set_curve(EdgeCurve::Line);
}
Ok(count)
}
#[doc(hidden)]
pub fn flatten_planar_nurbs_faces_for_tests(
topo: &mut Topology,
solid: SolidId,
tol: f64,
) -> Result<usize, crate::OperationsError> {
flatten_planar_nurbs_faces(topo, solid, tol)
}
#[allow(clippy::items_after_statements, clippy::type_complexity)]
fn merge_result_vertices(
topo: &mut Topology,
solid: SolidId,
tol: brepkit_math::tolerance::Tolerance,
) -> Result<(), crate::OperationsError> {
use std::collections::{BTreeMap, HashMap};
let shell_id = topo.solid(solid)?.outer_shell();
let face_ids: Vec<_> = topo.shell(shell_id)?.faces().to_vec();
let scale = 1.0 / tol.linear;
let quantize = |p: brepkit_math::vec::Point3| -> (i64, i64, i64) {
(
(p.x() * scale).round() as i64,
(p.y() * scale).round() as i64,
(p.z() * scale).round() as i64,
)
};
let mut canonical: BTreeMap<(i64, i64, i64), brepkit_topology::vertex::VertexId> =
BTreeMap::new();
let mut replacements: HashMap<
brepkit_topology::vertex::VertexId,
brepkit_topology::vertex::VertexId,
> = HashMap::new();
for &fid in &face_ids {
let face = topo.face(fid)?;
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
let wire = topo.wire(wid)?;
for oe in wire.edges() {
let edge = topo.edge(oe.edge())?;
for vid in [edge.start(), edge.end()] {
let pos = topo.vertex(vid)?.point();
let key = quantize(pos);
let canon = *canonical.entry(key).or_insert(vid);
if canon != vid {
replacements.insert(vid, canon);
}
}
}
}
}
if replacements.is_empty() {
return Ok(());
}
let mut edge_cache: HashMap<
(
brepkit_topology::edge::EdgeId,
brepkit_topology::vertex::VertexId,
brepkit_topology::vertex::VertexId,
),
brepkit_topology::edge::EdgeId,
> = HashMap::new();
struct FaceSnap {
surface: brepkit_topology::face::FaceSurface,
reversed: bool,
outer_oes: Vec<(
brepkit_topology::edge::EdgeId,
bool,
brepkit_topology::edge::EdgeCurve,
brepkit_topology::vertex::VertexId,
brepkit_topology::vertex::VertexId,
Option<f64>, // edge tolerance
)>,
outer_closed: bool,
inner_wires: Vec<(
Vec<(
brepkit_topology::edge::EdgeId,
bool,
brepkit_topology::edge::EdgeCurve,
brepkit_topology::vertex::VertexId,
brepkit_topology::vertex::VertexId,
Option<f64>,
)>,
bool, // wire closed flag
)>,
}
let mut snaps = Vec::with_capacity(face_ids.len());
for &fid in &face_ids {
let face = topo.face(fid)?;
let surface = face.surface().clone();
let reversed = face.is_reversed();
let outer_wire = topo.wire(face.outer_wire())?;
let outer_closed = outer_wire.is_closed();
let outer_oes: Vec<_> = outer_wire
.edges()
.iter()
.map(|oe| -> Result<_, crate::OperationsError> {
let e = topo.edge(oe.edge())?;
Ok((
oe.edge(),
oe.is_forward(),
e.curve().clone(),
e.start(),
e.end(),
e.tolerance(),
))
})
.collect::<Result<_, _>>()?;
let inner_wids = face.inner_wires().to_vec();
let mut inner_wires = Vec::new();
for iw in inner_wids {
let w = topo.wire(iw)?;
let closed = w.is_closed();
let oes: Vec<_> = w
.edges()
.iter()
.map(|oe| -> Result<_, crate::OperationsError> {
let e = topo.edge(oe.edge())?;
Ok((
oe.edge(),
oe.is_forward(),
e.curve().clone(),
e.start(),
e.end(),
e.tolerance(),
))
})
.collect::<Result<_, _>>()?;
inner_wires.push((oes, closed));
}
snaps.push(FaceSnap {
surface,
reversed,
outer_oes,
outer_closed,
inner_wires,
});
}
#[allow(clippy::type_complexity)]
let remap_oes = |oes: &[(
brepkit_topology::edge::EdgeId,
bool,
brepkit_topology::edge::EdgeCurve,
brepkit_topology::vertex::VertexId,
brepkit_topology::vertex::VertexId,
Option<f64>,
)],
replacements: &HashMap<
brepkit_topology::vertex::VertexId,
brepkit_topology::vertex::VertexId,
>,
edge_cache: &mut HashMap<
(
brepkit_topology::edge::EdgeId,
brepkit_topology::vertex::VertexId,
brepkit_topology::vertex::VertexId,
),
brepkit_topology::edge::EdgeId,
>,
topo: &mut Topology|
-> Vec<brepkit_topology::wire::OrientedEdge> {
oes.iter()
.map(|(eid, fwd, curve, start, end, edge_tol)| {
let ns = replacements.get(start).copied().unwrap_or(*start);
let ne = replacements.get(end).copied().unwrap_or(*end);
if ns == *start && ne == *end {
return brepkit_topology::wire::OrientedEdge::new(*eid, *fwd);
}
let key = (*eid, ns, ne);
let new_eid = *edge_cache.entry(key).or_insert_with(|| {
topo.add_edge(brepkit_topology::edge::Edge::with_tolerance(
ns,
ne,
curve.clone(),
*edge_tol,
))
});
brepkit_topology::wire::OrientedEdge::new(new_eid, *fwd)
})
.collect()
};
let mut new_face_ids = Vec::with_capacity(snaps.len());
for snap in &snaps {
let outer_oes = remap_oes(&snap.outer_oes, &replacements, &mut edge_cache, topo);
let Ok(outer_wire) = brepkit_topology::wire::Wire::new(outer_oes, snap.outer_closed) else {
continue;
};
let outer_id = topo.add_wire(outer_wire);
let mut inner_ids = Vec::new();
for (inner_oes_snap, inner_closed) in &snap.inner_wires {
let oes = remap_oes(inner_oes_snap, &replacements, &mut edge_cache, topo);
if let Ok(w) = brepkit_topology::wire::Wire::new(oes, *inner_closed) {
inner_ids.push(topo.add_wire(w));
}
}
let mut new_face =
brepkit_topology::face::Face::new(outer_id, inner_ids, snap.surface.clone());
if snap.reversed {
new_face.set_reversed(true);
}
new_face_ids.push(topo.add_face(new_face));
}
let new_shell = brepkit_topology::shell::Shell::new(new_face_ids)?;
let new_shell_id = topo.add_shell(new_shell);
let solid_mut = topo.solid_mut(solid)?;
solid_mut.set_outer_shell(new_shell_id);
Ok(())
}
#[allow(
clippy::too_many_lines,
clippy::type_complexity,
clippy::items_after_statements
)]
fn unify_coincident_boundary_edges(
topo: &mut Topology,
solid: SolidId,
tol_merge: f64,
) -> Result<bool, crate::OperationsError> {
use brepkit_topology::edge::{Edge, EdgeCurve, EdgeId};
use brepkit_topology::vertex::VertexId;
use brepkit_topology::wire::{OrientedEdge, Wire, WireId};
use std::collections::HashMap;
let shell_id = topo.solid(solid)?.outer_shell();
let face_ids: Vec<_> = topo.shell(shell_id)?.faces().to_vec();
let scale = 1.0 / tol_merge;
let q = |p: Point3| -> (i64, i64, i64) {
(
(p.x() * scale).round() as i64,
(p.y() * scale).round() as i64,
(p.z() * scale).round() as i64,
)
};
let mut vcanon: HashMap<(i64, i64, i64), VertexId> = HashMap::new();
for &fid in &face_ids {
let face = topo.face(fid)?;
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
let wire = topo.wire(wid)?;
for oe in wire.edges() {
let edge = topo.edge(oe.edge())?;
for vid in [edge.start(), edge.end()] {
let key = q(topo.vertex(vid)?.point());
vcanon.entry(key).or_insert(vid);
}
}
}
}
type OeSnap = (EdgeId, bool, EdgeCurve, VertexId, VertexId, Option<f64>);
struct FaceSnap {
surface: FaceSurface,
reversed: bool,
outer: Vec<OeSnap>,
outer_closed: bool,
inners: Vec<(Vec<OeSnap>, bool)>,
}
let snap_wire =
|topo: &Topology, wid: WireId| -> Result<(Vec<OeSnap>, bool), crate::OperationsError> {
let w = topo.wire(wid)?;
let closed = w.is_closed();
let oes = w
.edges()
.iter()
.map(|oe| -> Result<OeSnap, crate::OperationsError> {
let e = topo.edge(oe.edge())?;
Ok((
oe.edge(),
oe.is_forward(),
e.curve().clone(),
e.start(),
e.end(),
e.tolerance(),
))
})
.collect::<Result<_, _>>()?;
Ok((oes, closed))
};
let mut snaps = Vec::with_capacity(face_ids.len());
for &fid in &face_ids {
let face = topo.face(fid)?;
let surface = face.surface().clone();
let reversed = face.is_reversed();
let (outer, outer_closed) = snap_wire(topo, face.outer_wire())?;
let mut inners = Vec::new();
for iw in face.inner_wires() {
inners.push(snap_wire(topo, *iw)?);
}
snaps.push(FaceSnap {
surface,
reversed,
outer,
outer_closed,
inners,
});
}
type EdgeKey = (
(i64, i64, i64),
(i64, i64, i64),
(i64, i64, i64),
&'static str,
);
let mut ecanon: HashMap<EdgeKey, (EdgeId, VertexId, VertexId)> = HashMap::new();
let mut changed = false;
let canon_vid = |topo: &Topology, vid: VertexId| -> Result<VertexId, crate::OperationsError> {
Ok(*vcanon.get(&q(topo.vertex(vid)?.point())).unwrap_or(&vid))
};
let rebuild = |topo: &mut Topology,
oes: &[OeSnap],
ecanon: &mut HashMap<EdgeKey, (EdgeId, VertexId, VertexId)>,
changed: &mut bool|
-> Result<Vec<OrientedEdge>, crate::OperationsError> {
let mut out = Vec::with_capacity(oes.len());
for (eid, fwd, curve, start, end, etol) in oes {
let cs = canon_vid(topo, *start)?;
let ce = canon_vid(topo, *end)?;
if cs == ce {
*changed = true;
continue;
}
let sp = topo.vertex(*start)?.point();
let ep = topo.vertex(*end)?.point();
let (t0, t1) = curve.domain_with_endpoints(sp, ep);
let mid = curve.evaluate_with_endpoints((t0 + t1) * 0.5, sp, ep);
let (cs_q, ce_q) = (q(topo.vertex(cs)?.point()), q(topo.vertex(ce)?.point()));
let (lo, hi) = if cs_q <= ce_q {
(cs_q, ce_q)
} else {
(ce_q, cs_q)
};
let key = (lo, hi, q(mid), curve.type_tag());
let trav_start = if *fwd { cs } else { ce };
if let Some(&(c_eid, c_start, _c_end)) = ecanon.get(&key) {
*changed = true;
out.push(OrientedEdge::new(c_eid, c_start == trav_start));
} else {
let (eid_use, e_start) = if cs == *start && ce == *end {
(*eid, *start)
} else {
*changed = true;
(
topo.add_edge(Edge::with_tolerance(cs, ce, curve.clone(), *etol)),
cs,
)
};
ecanon.insert(key, (eid_use, e_start, ce));
out.push(OrientedEdge::new(eid_use, e_start == trav_start));
}
}
Ok(out)
};
let mut new_face_ids = Vec::with_capacity(snaps.len());
for snap in &snaps {
let outer_oes = rebuild(topo, &snap.outer, &mut ecanon, &mut changed)?;
let Ok(outer_wire) = Wire::new(outer_oes, snap.outer_closed) else {
return Ok(false);
};
let outer_id = topo.add_wire(outer_wire);
let mut inner_ids = Vec::new();
for (inner_oes, inner_closed) in &snap.inners {
let oes = rebuild(topo, inner_oes, &mut ecanon, &mut changed)?;
let Ok(w) = Wire::new(oes, *inner_closed) else {
return Ok(false);
};
inner_ids.push(topo.add_wire(w));
}
let mut new_face =
brepkit_topology::face::Face::new(outer_id, inner_ids, snap.surface.clone());
if snap.reversed {
new_face.set_reversed(true);
}
new_face_ids.push(topo.add_face(new_face));
}
if !changed {
return Ok(false);
}
let new_shell = brepkit_topology::shell::Shell::new(new_face_ids)?;
let new_shell_id = topo.add_shell(new_shell);
topo.solid_mut(solid)?.set_outer_shell(new_shell_id);
Ok(true)
}
#[allow(clippy::too_many_lines)]
fn enforce_manifold_shell(
topo: &mut Topology,
solid: SolidId,
) -> Result<SolidId, crate::OperationsError> {
use std::collections::{HashMap, HashSet, VecDeque};
let shell_id = topo.solid(solid)?.outer_shell();
let face_ids = topo.shell(shell_id)?.faces().to_vec();
let mut edge_face_count: HashMap<usize, u32> = HashMap::new();
for &fid in &face_ids {
if let Ok(face) = topo.face(fid) {
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied())
{
if let Ok(wire) = topo.wire(wid) {
for oe in wire.edges() {
*edge_face_count.entry(oe.edge().index()).or_default() += 1;
}
}
}
}
}
let nm_count = edge_face_count.values().filter(|&&c| c > 2).count();
if nm_count <= 3 {
return Ok(solid);
}
log::debug!(
"enforce_manifold_shell: {} non-manifold edges in {} faces",
nm_count,
face_ids.len()
);
let mut vpair_faces: HashMap<(usize, usize), Vec<brepkit_topology::face::FaceId>> =
HashMap::new();
for &fid in &face_ids {
if let Ok(face) = topo.face(fid) {
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied())
{
if let Ok(wire) = topo.wire(wid) {
for oe in wire.edges() {
if let Ok(e) = topo.edge(oe.edge()) {
let si = e.start().index();
let ei = e.end().index();
let key = if si <= ei { (si, ei) } else { (ei, si) };
vpair_faces.entry(key).or_default().push(fid);
}
}
}
}
}
}
let available: HashSet<brepkit_topology::face::FaceId> = face_ids.iter().copied().collect();
let mut processed: HashSet<brepkit_topology::face::FaceId> = HashSet::new();
let mut shells: Vec<Vec<brepkit_topology::face::FaceId>> = Vec::new();
for &start_face in &face_ids {
if processed.contains(&start_face) {
continue;
}
let mut shell_faces = vec![start_face];
processed.insert(start_face);
let mut shell_edge_count: HashMap<usize, u32> = HashMap::new();
if let Ok(face) = topo.face(start_face) {
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied())
{
if let Ok(wire) = topo.wire(wid) {
for oe in wire.edges() {
*shell_edge_count.entry(oe.edge().index()).or_default() += 1;
}
}
}
}
let mut queue = VecDeque::new();
queue.push_back(start_face);
while let Some(current) = queue.pop_front() {
let Ok(face) = topo.face(current) else {
continue;
};
let mut all_edges = Vec::new();
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied())
{
if let Ok(wire) = topo.wire(wid) {
for oe in wire.edges() {
if let Ok(e) = topo.edge(oe.edge()) {
let si = e.start().index();
let ei = e.end().index();
let key = if si <= ei { (si, ei) } else { (ei, si) };
all_edges.push((key, oe.edge()));
}
}
}
}
for (vpair, edge_id) in all_edges {
let eidx = edge_id.index();
if shell_edge_count.get(&eidx).copied().unwrap_or(0) >= 2 {
continue;
}
let candidates: Vec<brepkit_topology::face::FaceId> = vpair_faces
.get(&vpair)
.map(|fs| {
fs.iter()
.copied()
.filter(|&f| {
f != current && available.contains(&f) && !processed.contains(&f)
})
.collect()
})
.unwrap_or_default();
if candidates.is_empty() {
continue;
}
let selected = candidates[0];
if processed.contains(&selected) {
continue;
}
processed.insert(selected);
shell_faces.push(selected);
queue.push_back(selected);
if let Ok(sel_face) = topo.face(selected) {
for wid in std::iter::once(sel_face.outer_wire())
.chain(sel_face.inner_wires().iter().copied())
{
if let Ok(wire) = topo.wire(wid) {
for sel_oe in wire.edges() {
*shell_edge_count.entry(sel_oe.edge().index()).or_default() += 1;
}
}
}
}
}
}
shells.push(shell_faces);
}
let remaining: Vec<brepkit_topology::face::FaceId> = available
.iter()
.filter(|f| !processed.contains(f))
.copied()
.collect();
if !remaining.is_empty() {
shells.push(remaining);
}
if shells.len() <= 1 {
return Ok(solid);
}
log::debug!(
"enforce_manifold_shell: split into {} shells (sizes: {:?})",
shells.len(),
shells.iter().map(Vec::len).collect::<Vec<_>>(),
);
let mut best_idx = 0;
let mut best_count = 0;
for (i, faces) in shells.iter().enumerate() {
if faces.len() > best_count {
best_count = faces.len();
best_idx = i;
}
}
let outer = brepkit_topology::shell::Shell::new(shells[best_idx].clone())
.map_err(crate::OperationsError::Topology)?;
let outer_id = topo.add_shell(outer);
let mut inner_ids = Vec::new();
for (i, faces) in shells.iter().enumerate() {
if i != best_idx
&& !faces.is_empty()
&& let Ok(inner) = brepkit_topology::shell::Shell::new(faces.clone())
{
inner_ids.push(topo.add_shell(inner));
}
}
Ok(topo.add_solid(brepkit_topology::solid::Solid::new(outer_id, inner_ids)))
}
pub(crate) fn sample_edge_curve(curve: &EdgeCurve, n: usize) -> Vec<Point3> {
match curve {
EdgeCurve::Circle(c) => (0..n)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let t = std::f64::consts::TAU * (i as f64) / (n as f64);
c.evaluate(t)
})
.collect(),
EdgeCurve::Ellipse(e) => (0..n)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let t = std::f64::consts::TAU * (i as f64) / (n as f64);
e.evaluate(t)
})
.collect(),
EdgeCurve::NurbsCurve(nc) => {
let (u0, u1) = nc.domain();
let start_pt = nc.evaluate(u0);
let end_pt = nc.evaluate(u1);
let is_closed = (start_pt - end_pt).length() < 1e-6;
let divisor = if is_closed { n } else { n - 1 };
(0..n)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let t = u0 + (u1 - u0) * (i as f64) / (divisor as f64);
nc.evaluate(t)
})
.collect()
}
EdgeCurve::Line => vec![],
}
}
pub fn face_polygon(
topo: &Topology,
face_id: FaceId,
) -> Result<Vec<Point3>, crate::OperationsError> {
let face = topo.face(face_id)?;
let wire = topo.wire(face.outer_wire())?;
let mut pts = Vec::new();
for oe in wire.edges() {
let edge = topo.edge(oe.edge())?;
let curve = edge.curve();
let start_vid = edge.start();
let end_vid = edge.end();
let is_closed_edge = start_vid == end_vid
&& matches!(
curve,
EdgeCurve::Circle(_) | EdgeCurve::Ellipse(_) | EdgeCurve::NurbsCurve(_)
);
if is_closed_edge {
let mut sampled = sample_edge_curve(curve, types::CLOSED_CURVE_SAMPLES);
if !oe.is_forward() {
sampled.reverse();
}
pts.extend(sampled);
} else {
let vid = oe.oriented_start(edge);
pts.push(topo.vertex(vid)?.point());
}
}
Ok(pts)
}
pub fn collect_face_signatures(
topo: &Topology,
solid_id: SolidId,
) -> Result<Vec<(usize, Vec3, Point3)>, crate::OperationsError> {
let solid = topo.solid(solid_id)?;
let shell = topo.shell(solid.outer_shell())?;
let mut result = Vec::with_capacity(shell.faces().len());
for &fid in shell.faces() {
let face = topo.face(fid)?;
let verts = face_polygon(topo, fid)?;
let normal = if let FaceSurface::Plane { normal, .. } = face.surface() {
*normal
} else if verts.len() >= 3 {
let e1 = verts[1] - verts[0];
let e2 = verts[2] - verts[0];
e1.cross(e2).normalize().unwrap_or(Vec3::new(0.0, 0.0, 1.0))
} else {
Vec3::new(0.0, 0.0, 1.0)
};
let centroid = classify::polygon_centroid(&verts);
result.push((fid.index(), normal, centroid));
}
Ok(result)
}
#[cfg(test)]
mod tests;