use crate::cancel::CancelToken;
use crate::linalg::Vec3;
use crate::manifold::Manifold;
use crate::types::{BooleanConfig, BooleanEngine, Error, OpType};
fn v(x: f64, y: f64, z: f64) -> Vec3 {
Vec3::new(x, y, z)
}
fn assert_close(a: f64, b: f64, tol: f64, what: &str) {
assert!(
(a - b).abs() <= tol * b.abs().max(1.0),
"{what}: {a} vs {b}"
);
}
fn check_ops_match(a: &Manifold, b: &Manifold, what: &str) {
for op in [OpType::Add, OpType::Subtract, OpType::Intersect] {
let exact = a.boolean_with_engine(b, op, BooleanEngine::Exact);
let robust = a.boolean_with_engine(b, op, BooleanEngine::Robust);
assert_eq!(robust.status(), Error::NoError, "{what} {op:?}");
assert!(!robust.as_impl().is_soup, "{what} {op:?}: output must be manifold");
assert_close(robust.volume(), exact.volume(), 1e-9, &format!("{what} {op:?} volume"));
assert_close(
robust.surface_area(),
exact.surface_area(),
1e-9,
&format!("{what} {op:?} area"),
);
assert_eq!(robust.genus(), exact.genus(), "{what} {op:?} genus");
}
}
#[test]
fn cube_cube_overlap_matches_exact() {
let a = Manifold::cube(v(2.0, 2.0, 2.0), false);
let b = a.translate(v(1.0, 1.0, 1.0));
check_ops_match(&a, &b, "cube/cube");
}
#[test]
fn cube_sphere_matches_exact() {
let a = Manifold::cube(v(2.0, 2.0, 2.0), true);
let b = Manifold::sphere(1.3, 12);
check_ops_match(&a, &b, "cube/sphere");
}
#[test]
fn sphere_cylinder_matches_exact() {
let a = Manifold::sphere(1.0, 12);
let b = Manifold::cylinder(3.0, 0.6, 0.6, 8).translate(v(0.0, 0.0, -1.5));
check_ops_match(&a, &b, "sphere/cylinder");
}
#[test]
fn tetra_tetra_matches_exact() {
let a = Manifold::tetrahedron();
let b = Manifold::tetrahedron().rotate(0.0, 0.0, 45.0).translate(v(0.2, 0.1, 0.3));
check_ops_match(&a, &b, "tetra/tetra");
}
#[test]
fn disjoint_and_empty_fast_paths() {
let a = Manifold::cube(v(1.0, 1.0, 1.0), false);
let b = a.translate(v(5.0, 0.0, 0.0));
let u = a.union_with_engine(&b, BooleanEngine::Robust);
assert_close(u.volume(), 2.0, 1e-12, "disjoint union volume");
assert!(a
.intersection_with_engine(&b, BooleanEngine::Robust)
.is_empty());
assert_close(
a.difference_with_engine(&b, BooleanEngine::Robust).volume(),
1.0,
1e-12,
"disjoint difference",
);
let empty = Manifold::empty();
assert_close(
a.union_with_engine(&empty, BooleanEngine::Robust).volume(),
1.0,
1e-12,
"union with empty",
);
assert!(a
.intersection_with_engine(&empty, BooleanEngine::Robust)
.is_empty());
}
#[test]
fn global_default_engine_config() {
assert_eq!(BooleanConfig::default_engine(), BooleanEngine::Exact);
BooleanConfig::set_default_engine(BooleanEngine::Auto);
assert_eq!(BooleanConfig::default_engine(), BooleanEngine::Auto);
let a = Manifold::cube(v(2.0, 2.0, 2.0), false);
let b = a.translate(v(1.0, 0.0, 0.0));
let via_auto = a.union(&b);
let via_exact = a.union_with_engine(&b, BooleanEngine::Exact);
assert_eq!(via_auto.num_vert(), via_exact.num_vert());
assert_eq!(via_auto.num_tri(), via_exact.num_tri());
assert_eq!(via_auto.volume(), via_exact.volume());
BooleanConfig::reset_to_defaults();
assert_eq!(BooleanConfig::default_engine(), BooleanEngine::Exact);
}
#[test]
fn auto_dispatches_soup_operands_to_robust() {
let mut tris = Vec::new();
let cube = |lo: [f64; 3], hi: [f64; 3]| {
Manifold::cube(v(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]), false)
.translate(v(lo[0], lo[1], lo[2]))
};
let c1 = cube([0.0; 3], [2.0; 3]);
let c2 = cube([2.0, 2.0, 0.0], [4.0, 4.0, 2.0]);
for m in [&c1, &c2] {
let gl = m.get_mesh_gl64(-1);
for t in 0..gl.num_tri() {
let tv = gl.get_tri_verts(t);
let p = |i: u64| {
let o = i as usize * 3;
v(
gl.vert_properties[o],
gl.vert_properties[o + 1],
gl.vert_properties[o + 2],
)
};
tris.push([p(tv[0]), p(tv[1]), p(tv[2])]);
}
}
let mut mesh = crate::types::MeshGL64::default();
mesh.num_prop = 3;
for t in &tris {
for p in t {
mesh.vert_properties.extend([p.x, p.y, p.z]);
}
}
mesh.tri_verts = (0..3 * tris.len() as u64).collect();
let soup = Manifold::from_mesh_gl64_robust(&mesh);
assert!(soup.as_impl().is_soup);
assert_close(soup.volume(), 16.0, 1e-12, "soup volume");
let cutter = Manifold::cube(v(1.0, 1.0, 1.0), false).translate(v(0.5, 0.5, 0.5));
assert_eq!(
soup.boolean_with_engine(&cutter, OpType::Subtract, BooleanEngine::Exact)
.status(),
Error::NotManifold
);
let diff = soup.boolean_with_engine(&cutter, OpType::Subtract, BooleanEngine::Auto);
assert_eq!(diff.status(), Error::NoError);
assert_close(diff.volume(), 15.0, 1e-9, "auto soup difference volume");
let cutter2 = Manifold::cube(v(1.0, 1.0, 1.0), false).translate(v(2.5, 2.5, 0.5));
let diff2 = diff.boolean_with_engine(&cutter2, OpType::Subtract, BooleanEngine::Auto);
assert_eq!(diff2.status(), Error::NoError);
assert_close(diff2.volume(), 14.0, 1e-9, "chained difference volume");
}
#[test]
fn robust_cancellation() {
let a = Manifold::sphere(1.0, 16);
let b = Manifold::sphere(1.0, 16).translate(v(0.5, 0.0, 0.0));
let token = CancelToken::new();
token.cancel();
let r = a.boolean_with_engine_and_token(&b, OpType::Add, BooleanEngine::Robust, Some(&token));
assert_eq!(r.status(), Error::Cancelled);
}
#[test]
fn coincident_cubes_union_and_subtract() {
let a = Manifold::cube(v(2.0, 2.0, 2.0), false);
let u = a.union_with_engine(&a.clone(), BooleanEngine::Robust);
assert_close(u.volume(), 8.0, 1e-12, "self union volume");
assert_close(u.surface_area(), 24.0, 1e-12, "self union area");
let d = a.difference_with_engine(&a.clone(), BooleanEngine::Robust);
assert!(d.is_empty() || d.volume().abs() < 1e-12, "self difference must vanish");
let i = a.intersection_with_engine(&a.clone(), BooleanEngine::Robust);
assert_close(i.volume(), 8.0, 1e-12, "self intersection volume");
}
#[test]
fn face_touching_union_merges_cleanly() {
let a = Manifold::cube(v(2.0, 2.0, 2.0), false);
let b = a.translate(v(0.0, 0.0, 2.0));
let u = a.union_with_engine(&b, BooleanEngine::Robust);
assert_eq!(u.status(), Error::NoError);
assert_close(u.volume(), 16.0, 1e-12, "stacked union volume");
assert_close(u.surface_area(), 40.0, 1e-12, "stacked union area");
}