use cadrum::{Boolean, DVec3, Solid};
fn cube(x: f64, y: f64, z: f64, tx: f64, ty: f64, tz: f64) -> Solid {
Solid::cube(DVec3::ZERO, DVec3::new(x, y, z)).translate(DVec3::new(tx, ty, tz))
}
#[test]
fn test_union_two_cylinders() {
let a = Solid::cylinder(1.1, DVec3::Z * 1.0).translate(DVec3::new(1.0, 0.0, 0.0));
let b = Solid::cylinder(1.1, DVec3::Z * 1.0).translate(DVec3::new(-1.0, 0.0, 0.0));
let v: Vec<Solid> = (&a + &b).build_vec().unwrap();
assert_eq!(v.len(), 1, "overlapping cylinders should union to 1 solid");
}
#[test]
fn test_union_disjoint() {
let a = Solid::cylinder(1.1, DVec3::Z * 1.0);
let b = Solid::cylinder(1.1, DVec3::Z * 1.0).translate(DVec3::new(4.0, 0.0, 0.0));
let c = Solid::cylinder(1.1, DVec3::Z * 1.0).translate(DVec3::new(0.0, 1.0, 0.0));
let d = Solid::cylinder(1.1, DVec3::Z * 1.0).translate(DVec3::new(4.0, 1.0, 0.0));
let v: Vec<Solid> = [&a, &b, &c, &d].into_iter().map(Boolean::from).reduce(|a, b| a + b).unwrap().build_vec().unwrap();
assert!(v.len() == 2, "disjoint groups should be 2 solids, got {}", v.len());
}
#[test]
fn test_union_all_connected() {
let a = cube(10.0, 10.0, 10.0, 0.0, 0.0, 0.0);
let b = cube(10.0, 10.0, 10.0, 3.0, 3.0, 3.0);
let c = cube(10.0, 10.0, 10.0, 6.0, 6.0, 6.0);
let s: Solid = [&a, &b, &c].into_iter().map(Boolean::from).reduce(|a, b| a + b).unwrap().build().unwrap();
assert!(s.volume() > a.volume(), "union volume should grow");
}
#[test]
fn test_union_olympic_rings_out_of_order() {
let s = 1.0;
let step = 0.8;
let mk = |i: f64| Solid::cube(DVec3::ZERO, DVec3::splat(s)).translate(DVec3::new(i * step, 0.0, 0.0));
let ring1 = mk(0.0);
let ring2 = mk(1.0);
let ring3 = mk(2.0);
let ring4 = mk(3.0);
let ring5 = mk(4.0);
let out_of_order: Solid = [&ring1, &ring3, &ring5, &ring2, &ring4].into_iter().map(Boolean::from).reduce(|a, b| a + b).unwrap().build().unwrap();
let in_order: Solid = [&ring1, &ring2, &ring3, &ring4, &ring5].into_iter().map(Boolean::from).reduce(|a, b| a + b).unwrap().build().unwrap();
assert!((out_of_order.volume() - in_order.volume()).abs() < 1e-6, "order-independent: {} vs {}", out_of_order.volume(), in_order.volume());
}
#[test]
fn test_intersect_two_cubes() {
let a = cube(10.0, 10.0, 10.0, 0.0, 0.0, 0.0);
let b = cube(10.0, 10.0, 10.0, 5.0, 0.0, 0.0); let s: Solid = [&a, &b].into_iter().map(Boolean::from).reduce(|x, y| x * y).unwrap().build().unwrap();
assert!((s.volume() - 500.0).abs() < 1e-3, "got {}", s.volume());
}
#[test]
fn test_intersect_sphere_with_multiple_cylinders() {
let sphere = Solid::sphere(5.0);
let r = 0.8;
let len = 20.0;
let half = len / 2.0;
let cyl_x = Solid::cylinder(r, DVec3::X * len).translate(DVec3::new(-half, 0.0, 0.0));
let cyl_y = Solid::cylinder(r, DVec3::Y * len).translate(DVec3::new(0.0, -half, 0.0));
let cyl_z = Solid::cylinder(r, DVec3::Z * len).translate(DVec3::new(0.0, 0.0, -half));
let multi: Solid = [&sphere, &cyl_x, &cyl_y, &cyl_z].into_iter().map(Boolean::from).reduce(|x, y| x * y).unwrap().build().unwrap();
assert!(multi.volume() > 0.0 && multi.volume() < 10.0, "expected small intersection volume, got {}", multi.volume());
}
#[test]
fn test_subtract_sphere_with_multiple_holes() {
let sphere = Solid::sphere(5.0);
let len = 12.0;
let half = len / 2.0;
let r = 1.0;
let hole_x = Solid::cylinder(r, DVec3::X * len).translate(DVec3::new(-half, 0.0, 0.0));
let hole_y = Solid::cylinder(r, DVec3::Y * len).translate(DVec3::new(0.0, -half, 0.0));
let hole_z = Solid::cylinder(r, DVec3::Z * len).translate(DVec3::new(0.0, 0.0, -half));
let multi: Solid = (&sphere - &hole_x - &hole_y - &hole_z).build().unwrap();
assert!((multi.volume() - 441.7).abs() < 5.0, "got volume {}", multi.volume());
}
#[test]
fn test_operator_overloads() {
let a = Solid::cube(DVec3::ZERO, DVec3::splat(10.0));
let b = Solid::cube(DVec3::ZERO, DVec3::splat(10.0)).translate(DVec3::new(5.0, 5.0, 5.0));
let u: Solid = (&a + &b).build().expect("a + b should yield one solid");
println!("a + b (union): volume = {:.4}", u.volume());
let s: Solid = (&a - &b).build().expect("a - b should yield one solid");
println!("a - b (subtract): volume = {:.4}", s.volume());
let i: Solid = (&a * &b).build().expect("a * b should yield one solid");
println!("a * b (intersect): volume = {:.4}", i.volume());
let far = Solid::cube(DVec3::ZERO, DVec3::ONE).translate(DVec3::new(100.0, 0.0, 0.0));
match (&a * &far).build() {
Err(e @ cadrum::Error::OneFailed(0)) => println!("a * far (disjoint) -> {:?}", e),
Err(e) => panic!("expected OneFailed(0), got {:?}", e),
Ok(_) => panic!("expected OneFailed(0), got Ok"),
}
}
#[test]
fn test_singleton_build() {
let a = Solid::cube(DVec3::ZERO, DVec3::splat(10.0));
let expected = a.volume();
let b = std::iter::once(&a).map(Boolean::from).reduce(|x, y| x + y).unwrap(); let s: Solid = b.build().unwrap();
assert!((s.volume() - expected).abs() < 1e-6, "{} vs {}", s.volume(), expected);
}
#[test]
fn test_empty_returns_error() {
let solids: Vec<Solid> = Vec::new();
match solids.iter().map(Boolean::from).reduce(|x, y| x + y).unwrap_or_else(Boolean::default).build() {
Err(cadrum::Error::OneFailed(0)) => {}
other => panic!("expected OneFailed(0), got {:?}", other.is_ok()),
}
}
#[test]
fn test_build_direct() {
let a = Solid::cube(DVec3::ZERO, DVec3::splat(10.0));
let b = Solid::cube(DVec3::ZERO, DVec3::splat(10.0)).translate(DVec3::new(5.0, 0.0, 0.0));
let c = Solid::cube(DVec3::ZERO, DVec3::splat(10.0)).translate(DVec3::new(2.0, 0.0, 0.0));
let solids = vec![a, b, c];
let clauses = vec![1, -3, 0, 2, -3, 0];
let v = Solid::boolean(solids.iter(), clauses).build_vec().unwrap();
let total_volume: f64 = v.iter().map(|s| s.volume()).sum();
assert!(total_volume < 1500.0);
assert!(total_volume > 0.0);
}
#[test]
fn test_preserves_src_face_identity() {
let torus = Solid::torus(6.0, 2.0, DVec3::Y);
let cutter = Solid::half_space(DVec3::ZERO, -DVec3::Z);
let cutter_ids: std::collections::HashSet<u64> = cutter.iter_face().map(|f| f.id()).collect();
let half: Solid = (&torus * &cutter).build().unwrap();
let matched: Vec<u64> = half.iter_history().filter_map(|[p, s]| cutter_ids.contains(&s).then_some(p)).collect();
assert!(!matched.is_empty(), "history must contain at least one face sourced from cutter");
}