#![allow(clippy::unwrap_used)]
use brepkit_math::mat::Mat4;
use brepkit_operations::boolean::{BooleanOp, BooleanOptions, boolean, boolean_with_options};
use brepkit_operations::copy::copy_solid;
use brepkit_operations::measure::solid_volume;
use brepkit_operations::primitives::{make_box, make_cylinder};
use brepkit_operations::transform::transform_solid;
use brepkit_topology::Topology;
use brepkit_topology::solid::SolidId;
use brepkit_topology::test_utils::make_unit_cube_manifold_at;
use brepkit_topology::validation::validate_shell_manifold;
fn check_manifold(topo: &Topology, solid: SolidId) -> usize {
let s = topo.solid(solid).unwrap();
let sh = topo.shell(s.outer_shell()).unwrap();
assert!(
validate_shell_manifold(sh, topo).is_ok(),
"result should be manifold"
);
sh.faces().len()
}
const DEFLECTION: f64 = 0.1;
fn assert_volume(topo: &Topology, solid: SolidId, expected: f64, tol_frac: f64) {
let vol = solid_volume(topo, solid, DEFLECTION).unwrap();
let diff = (vol - expected).abs();
assert!(
diff < expected * tol_frac,
"volume {vol:.6} not within {:.1}% of expected {expected:.6} (diff={diff:.6})",
tol_frac * 100.0
);
}
fn box_at(topo: &mut Topology, x: f64, y: f64, z: f64, sx: f64, sy: f64, sz: f64) -> SolidId {
let b = make_box(topo, sx, sy, sz).unwrap();
transform_solid(topo, b, &Mat4::translation(x, y, z)).unwrap();
b
}
#[test]
fn coplanar_fuse_shared_face() {
let mut topo = Topology::new();
let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
let b = make_unit_cube_manifold_at(&mut topo, 1.0, 0.0, 0.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
let faces = check_manifold(&topo, result);
assert!(
faces >= 6,
"fused cubes should have at least 6 faces, got {faces}"
);
}
#[test]
fn coplanar_cut_shared_face() {
let mut topo = Topology::new();
let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
let b = make_unit_cube_manifold_at(&mut topo, 1.0, 0.0, 0.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn coplanar_intersect_shared_face() {
let mut topo = Topology::new();
let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
let b = make_unit_cube_manifold_at(&mut topo, 1.0, 0.0, 0.0);
let _result = boolean(&mut topo, BooleanOp::Intersect, a, b);
}
#[test]
fn coplanar_offset_shared_face_y() {
let mut topo = Topology::new();
let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
let b = make_unit_cube_manifold_at(&mut topo, 0.0, 1.0, 0.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn coplanar_offset_shared_face_z() {
let mut topo = Topology::new();
let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
let b = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn fully_contained_fuse() {
let mut topo = Topology::new();
let big = box_at(&mut topo, 0.0, 0.0, 0.0, 4.0, 4.0, 4.0);
let small = box_at(&mut topo, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Fuse, big, small).unwrap();
check_manifold(&topo, result);
}
#[test]
fn fully_contained_cut() {
let mut topo = Topology::new();
let big = box_at(&mut topo, 0.0, 0.0, 0.0, 4.0, 4.0, 4.0);
let small = box_at(&mut topo, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Cut, big, small).unwrap();
check_manifold(&topo, result);
}
#[test]
fn fully_contained_intersect() {
let mut topo = Topology::new();
let big = box_at(&mut topo, 0.0, 0.0, 0.0, 4.0, 4.0, 4.0);
let small = box_at(&mut topo, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Intersect, big, small).unwrap();
check_manifold(&topo, result);
}
#[test]
fn near_miss_disjoint_fuse() {
let mut topo = Topology::new();
let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
let b = make_unit_cube_manifold_at(&mut topo, 1.00001, 0.0, 0.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn near_miss_barely_overlapping() {
let mut topo = Topology::new();
let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
let b = make_unit_cube_manifold_at(&mut topo, 0.99999, 0.0, 0.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn near_miss_cut_barely_touching() {
let mut topo = Topology::new();
let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
let b = make_unit_cube_manifold_at(&mut topo, 0.99999, 0.0, 0.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn thin_wall_cut() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
let b = box_at(&mut topo, 4.9, 0.0, 0.0, 0.2, 10.0, 10.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn thin_wall_intersect() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
let b = box_at(&mut topo, 4.5, -1.0, -1.0, 1.0, 12.0, 12.0);
let result = boolean(&mut topo, BooleanOp::Intersect, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn volume_fuse_overlapping_boxes() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 12.0, 0.01);
}
#[test]
fn volume_cut_overlapping_boxes() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 4.0, 0.01);
}
#[test]
fn volume_intersect_overlapping_boxes() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Intersect, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 4.0, 0.01);
}
#[test]
fn fuse_overlap_y_axis() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 0.0, 1.0, 0.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 12.0, 0.01);
}
#[test]
fn fuse_overlap_z_axis() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 12.0, 0.01);
}
#[test]
fn fuse_overlap_all_axes() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 15.0, 0.01);
}
#[test]
fn fuse_commutative() {
let mut topo = Topology::new();
let a1 = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b1 = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let r1 = boolean(&mut topo, BooleanOp::Fuse, a1, b1).unwrap();
let v1 = solid_volume(&topo, r1, DEFLECTION).unwrap();
let a2 = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b2 = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let r2 = boolean(&mut topo, BooleanOp::Fuse, b2, a2).unwrap();
let v2 = solid_volume(&topo, r2, DEFLECTION).unwrap();
assert!(
(v1 - v2).abs() < 0.01,
"fuse should be commutative: {v1} vs {v2}"
);
}
#[test]
fn intersect_commutative() {
let mut topo = Topology::new();
let a1 = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b1 = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let r1 = boolean(&mut topo, BooleanOp::Intersect, a1, b1).unwrap();
let v1 = solid_volume(&topo, r1, DEFLECTION).unwrap();
let a2 = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b2 = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let r2 = boolean(&mut topo, BooleanOp::Intersect, b2, a2).unwrap();
let v2 = solid_volume(&topo, r2, DEFLECTION).unwrap();
assert!(
(v1 - v2).abs() < 0.01,
"intersect should be commutative: {v1} vs {v2}"
);
}
#[test]
fn volume_identity_inclusion_exclusion() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 3.0, 3.0, 3.0);
let b = box_at(&mut topo, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0);
let fused = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
let vol_fuse = solid_volume(&topo, fused, DEFLECTION).unwrap();
let a2 = box_at(&mut topo, 0.0, 0.0, 0.0, 3.0, 3.0, 3.0);
let b2 = box_at(&mut topo, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0);
let inter = boolean(&mut topo, BooleanOp::Intersect, a2, b2).unwrap();
let vol_inter = solid_volume(&topo, inter, DEFLECTION).unwrap();
let vol_a = 27.0; let vol_b = 27.0;
let expected_fuse = vol_a + vol_b - vol_inter;
assert!(
(vol_fuse - expected_fuse).abs() < 0.5,
"inclusion-exclusion: fuse={vol_fuse:.2}, expected={expected_fuse:.2}, inter={vol_inter:.2}"
);
}
#[test]
fn chained_fuse_three_boxes() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let ab = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, ab);
let c = box_at(&mut topo, 2.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let abc = boolean(&mut topo, BooleanOp::Fuse, ab, c).unwrap();
check_manifold(&topo, abc);
assert_volume(&topo, abc, 16.0, 0.02);
}
#[test]
fn chained_cut_multiple_holes() {
let mut topo = Topology::new();
let base = box_at(&mut topo, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
let hole1 = box_at(&mut topo, 1.0, 1.0, -1.0, 2.0, 2.0, 12.0);
let r1 = boolean(&mut topo, BooleanOp::Cut, base, hole1).unwrap();
check_manifold(&topo, r1);
let hole2 = box_at(&mut topo, 5.0, 5.0, -1.0, 2.0, 2.0, 12.0);
let r2 = boolean(&mut topo, BooleanOp::Cut, r1, hole2).unwrap();
check_manifold(&topo, r2);
}
#[test]
fn chained_fuse_then_cut() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 3.0, 3.0, 3.0);
let b = box_at(&mut topo, 2.0, 0.0, 0.0, 3.0, 3.0, 3.0);
let fused = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
let cutter = box_at(&mut topo, 1.0, 1.0, -1.0, 2.0, 1.0, 5.0);
let result = boolean(&mut topo, BooleanOp::Cut, fused, cutter).unwrap();
check_manifold(&topo, result);
}
#[test]
fn disjoint_fuse_far_apart() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
let b = box_at(&mut topo, 100.0, 0.0, 0.0, 1.0, 1.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn disjoint_cut_no_effect() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 10.0, 0.0, 0.0, 1.0, 1.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 8.0, 0.01);
}
#[test]
fn disjoint_intersect_empty() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
let b = box_at(&mut topo, 10.0, 0.0, 0.0, 1.0, 1.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Intersect, a, b).unwrap();
assert_eq!(
brepkit_topology::explorer::solid_faces(&topo, result)
.unwrap()
.len(),
0,
"disjoint intersect should produce zero faces"
);
let vol = solid_volume(&topo, result, 0.05).unwrap();
assert!(
vol <= 1e-6,
"disjoint intersect volume should be ~0, got {vol}"
);
}
#[test]
fn cut_cylinder_from_box() {
let mut topo = Topology::new();
let base = box_at(&mut topo, 0.0, 0.0, 0.0, 4.0, 4.0, 4.0);
let cyl = make_cylinder(&mut topo, 1.0, 6.0).unwrap();
transform_solid(&mut topo, cyl, &Mat4::translation(2.0, 2.0, -1.0)).unwrap();
let result = boolean(&mut topo, BooleanOp::Cut, base, cyl).unwrap();
check_manifold(&topo, result);
}
#[test]
fn fuse_cylinder_and_box() {
let mut topo = Topology::new();
let base = box_at(&mut topo, 0.0, 0.0, 0.0, 4.0, 4.0, 2.0);
let cyl = make_cylinder(&mut topo, 1.0, 4.0).unwrap();
transform_solid(&mut topo, cyl, &Mat4::translation(2.0, 2.0, 0.0)).unwrap();
let result = boolean(&mut topo, BooleanOp::Fuse, base, cyl).unwrap();
check_manifold(&topo, result);
}
#[test]
fn intersect_cylinder_and_box() {
let mut topo = Topology::new();
let base = box_at(&mut topo, 0.0, 0.0, 0.0, 4.0, 4.0, 4.0);
let cyl = make_cylinder(&mut topo, 1.0, 6.0).unwrap();
transform_solid(&mut topo, cyl, &Mat4::translation(2.0, 2.0, -1.0)).unwrap();
let result = boolean(&mut topo, BooleanOp::Intersect, base, cyl).unwrap();
check_manifold(&topo, result);
let expected = std::f64::consts::PI * 1.0 * 1.0 * 4.0;
assert_volume(&topo, result, expected, 0.05);
}
#[test]
fn large_overlap_90_percent() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
let b = box_at(&mut topo, -1.0, -1.0, -1.0, 10.0, 10.0, 10.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn small_overlap_1_percent() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
let b = box_at(&mut topo, 9.0, 9.0, 9.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn fuse_asymmetric_boxes() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0);
let b = box_at(&mut topo, 0.5, 0.5, 0.5, 3.0, 1.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn cut_asymmetric_boxes() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 5.0, 3.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.5, -1.0, 2.0, 1.0, 4.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn self_fuse() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = copy_solid(&mut topo, a).unwrap();
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 8.0, 0.05);
}
#[test]
fn self_intersect() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = copy_solid(&mut topo, a).unwrap();
let result = boolean(&mut topo, BooleanOp::Intersect, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 8.0, 0.05);
}
#[test]
fn options_fine_deflection() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let opts = BooleanOptions {
deflection: 0.01,
..Default::default()
};
let result = boolean_with_options(&mut topo, BooleanOp::Fuse, a, b, opts).unwrap();
check_manifold(&topo, result);
}
#[test]
fn options_coarse_deflection() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let opts = BooleanOptions {
deflection: 1.0,
..Default::default()
};
let result = boolean_with_options(&mut topo, BooleanOp::Fuse, a, b, opts).unwrap();
check_manifold(&topo, result);
}
#[test]
fn edge_aligned_overlap_x() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 0.0, 2.0, 0.0, 2.0, 2.0, 2.0);
let fuse = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, fuse);
}
#[test]
fn multiple_cuts_from_same_base() {
let mut topo = Topology::new();
let base = box_at(&mut topo, 0.0, 0.0, 0.0, 10.0, 10.0, 2.0);
let h1 = box_at(&mut topo, 1.0, 1.0, -1.0, 2.0, 2.0, 4.0);
let r1 = boolean(&mut topo, BooleanOp::Cut, base, h1).unwrap();
check_manifold(&topo, r1);
let h2 = box_at(&mut topo, 5.0, 1.0, -1.0, 2.0, 2.0, 4.0);
let r2 = boolean(&mut topo, BooleanOp::Cut, r1, h2).unwrap();
check_manifold(&topo, r2);
let h3 = box_at(&mut topo, 1.0, 5.0, -1.0, 2.0, 2.0, 4.0);
let r3 = boolean(&mut topo, BooleanOp::Cut, r2, h3).unwrap();
check_manifold(&topo, r3);
let h4 = box_at(&mut topo, 5.0, 5.0, -1.0, 2.0, 2.0, 4.0);
let r4 = boolean(&mut topo, BooleanOp::Cut, r3, h4).unwrap();
check_manifold(&topo, r4);
}
#[test]
fn corner_overlap_fuse() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.5, 1.5, 1.5, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 15.875, 0.02);
}
#[test]
fn corner_overlap_cut() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.5, 1.5, 1.5, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 7.875, 0.02);
}
#[test]
fn corner_overlap_intersect() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.5, 1.5, 1.5, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Intersect, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 0.125, 0.05);
}
#[test]
fn half_overlap_fuse() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 12.0, 0.01);
}
#[test]
fn half_overlap_cut() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 4.0, 0.01);
}
#[test]
fn half_overlap_intersect() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let b = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0);
let result = boolean(&mut topo, BooleanOp::Intersect, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 4.0, 0.01);
}
#[test]
fn cut_no_overlap_preserves_volume() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 3.0, 3.0, 3.0);
let b = box_at(&mut topo, 10.0, 10.0, 10.0, 1.0, 1.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 27.0, 0.01);
}
#[test]
fn l_shape_fuse() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 3.0, 1.0, 1.0);
let b = box_at(&mut topo, 0.0, 0.0, 0.0, 1.0, 3.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 5.0, 0.02);
}
#[test]
fn t_shape_fuse() {
let mut topo = Topology::new();
let horizontal = box_at(&mut topo, 0.0, 0.0, 0.0, 6.0, 1.0, 1.0);
let vertical = box_at(&mut topo, 2.0, 0.0, 0.0, 2.0, 3.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Fuse, horizontal, vertical).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 10.0, 0.02);
}
#[test]
fn cross_shape_fuse() {
let mut topo = Topology::new();
let h = box_at(&mut topo, 0.0, 1.0, 0.0, 4.0, 2.0, 1.0);
let v = box_at(&mut topo, 1.0, 0.0, 0.0, 2.0, 4.0, 1.0);
let result = boolean(&mut topo, BooleanOp::Fuse, h, v).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 12.0, 0.02);
}
#[test]
fn fuse_two_cylinders() {
let mut topo = Topology::new();
let a = make_cylinder(&mut topo, 1.0, 3.0).unwrap();
let b = make_cylinder(&mut topo, 1.0, 3.0).unwrap();
transform_solid(&mut topo, b, &Mat4::translation(1.0, 0.0, 0.0)).unwrap();
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn cut_cylinder_from_cylinder() {
let mut topo = Topology::new();
let a = make_cylinder(&mut topo, 2.0, 3.0).unwrap();
let b = make_cylinder(&mut topo, 1.0, 5.0).unwrap();
transform_solid(&mut topo, b, &Mat4::translation(0.0, 0.0, -1.0)).unwrap();
let result = boolean(&mut topo, BooleanOp::Cut, a, b).unwrap();
check_manifold(&topo, result);
}
#[test]
fn volume_tiny_overlap() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 5.0, 5.0, 5.0);
let b = box_at(&mut topo, 4.8, 4.8, 4.8, 5.0, 5.0, 5.0);
let result = boolean(&mut topo, BooleanOp::Intersect, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 0.008, 0.10);
}
#[test]
fn volume_large_boxes() {
let mut topo = Topology::new();
let a = box_at(&mut topo, 0.0, 0.0, 0.0, 100.0, 100.0, 100.0);
let b = box_at(&mut topo, 50.0, 0.0, 0.0, 100.0, 100.0, 100.0);
let result = boolean(&mut topo, BooleanOp::Fuse, a, b).unwrap();
check_manifold(&topo, result);
assert_volume(&topo, result, 1_500_000.0, 0.01);
}
#[test]
fn cut_cylinder_from_box_volume() {
let mut topo = Topology::new();
let base = box_at(&mut topo, 0.0, 0.0, 0.0, 4.0, 4.0, 4.0);
let cyl = make_cylinder(&mut topo, 1.0, 6.0).unwrap();
transform_solid(&mut topo, cyl, &Mat4::translation(2.0, 2.0, -1.0)).unwrap();
let result = boolean(&mut topo, BooleanOp::Cut, base, cyl).unwrap();
check_manifold(&topo, result);
let box_vol = 64.0;
let cyl_overlap = std::f64::consts::PI * 1.0 * 1.0 * 4.0; let expected = box_vol - cyl_overlap;
assert_volume(&topo, result, expected, 0.05);
}
#[test]
fn fuse_cylinder_and_box_volume() {
let mut topo = Topology::new();
let base = box_at(&mut topo, 0.0, 0.0, 0.0, 4.0, 4.0, 2.0);
let cyl = make_cylinder(&mut topo, 1.0, 4.0).unwrap();
transform_solid(&mut topo, cyl, &Mat4::translation(2.0, 2.0, -1.0)).unwrap();
let result = boolean(&mut topo, BooleanOp::Fuse, base, cyl).unwrap();
check_manifold(&topo, result);
let box_vol = 32.0;
let cyl_vol = std::f64::consts::PI * 1.0 * 1.0 * 4.0;
let overlap = std::f64::consts::PI * 1.0 * 1.0 * 2.0;
let expected = box_vol + cyl_vol - overlap;
assert_volume(&topo, result, expected, 0.05);
}