use crate::cancel::CancelToken;
use crate::linalg::Vec3;
use crate::manifold::Manifold;
use crate::types::{BooleanConfig, BooleanEngine, Error, OpType};
#[path = "stl_fixtures.rs"]
mod stl_fixtures;
use stl_fixtures::import_stl_like_demo;
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());
}
fn cube_tris(lo: f64, hi: f64) -> Vec<[Vec3; 3]> {
let quads: [([f64; 3], [f64; 3], [f64; 3], [f64; 3]); 6] = [
([0., 0., 0.], [0., 1., 0.], [1., 1., 0.], [1., 0., 0.]), ([0., 0., 1.], [1., 0., 1.], [1., 1., 1.], [0., 1., 1.]), ([0., 0., 0.], [1., 0., 0.], [1., 0., 1.], [0., 0., 1.]), ([0., 1., 0.], [0., 1., 1.], [1., 1., 1.], [1., 1., 0.]), ([0., 0., 0.], [0., 0., 1.], [0., 1., 1.], [0., 1., 0.]), ([1., 0., 0.], [1., 1., 0.], [1., 1., 1.], [1., 0., 1.]), ];
let s = hi - lo;
let m = |q: [f64; 3]| v(lo + q[0] * s, lo + q[1] * s, lo + q[2] * s);
let mut out = Vec::new();
for (a, b, c, d) in quads {
out.push([m(a), m(b), m(c)]);
out.push([m(a), m(c), m(d)]);
}
out
}
fn flipped(tris: &[[Vec3; 3]]) -> Vec<[Vec3; 3]> {
tris.iter().map(|t| [t[0], t[2], t[1]]).collect()
}
fn mesh_from_tris(tris: &[[Vec3; 3]]) -> Manifold {
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..(tris.len() * 3) as u64).collect();
Manifold::from_mesh_gl64_robust(&mesh)
}
fn signed_volume(m: &Manifold) -> f64 {
crate::robust::soup::impl_to_tris(m.as_impl())
.iter()
.map(|t| crate::linalg::dot(t[0], crate::linalg::cross(t[1], t[2])) / 6.0)
.sum()
}
#[test]
fn disjoint_union_classifies_inverted_operand() {
use crate::types::WindingRule;
let a = mesh_from_tris(&cube_tris(0.0, 2.0));
let b = mesh_from_tris(&flipped(&cube_tris(5.0, 7.0)));
assert_eq!(a.status(), Error::NoError);
assert_eq!(b.status(), Error::NoError);
assert!(signed_volume(&b) < 0.0, "fixture B must import inverted");
let pos = a.boolean_with_engine_and_rule(
&b,
OpType::Add,
BooleanEngine::Robust,
WindingRule::Positive,
);
assert_eq!(pos.status(), Error::NoError);
assert_close(signed_volume(&pos), 8.0, 1e-12, "positive-rule disjoint union");
let nz = a.boolean_with_engine_and_rule(
&b,
OpType::Add,
BooleanEngine::Robust,
WindingRule::Nonzero,
);
assert_eq!(nz.status(), Error::NoError);
assert_close(signed_volume(&nz), 16.0, 1e-12, "nonzero-rule disjoint union");
}
#[test]
fn disjoint_subtract_classifies_inverted_minuend() {
use crate::types::WindingRule;
let a = mesh_from_tris(&flipped(&cube_tris(0.0, 2.0)));
let b = mesh_from_tris(&cube_tris(5.0, 7.0));
assert_eq!(a.status(), Error::NoError);
assert_eq!(b.status(), Error::NoError);
assert!(signed_volume(&a) < 0.0, "fixture A must import inverted");
let pos = a.boolean_with_engine_and_rule(
&b,
OpType::Subtract,
BooleanEngine::Robust,
WindingRule::Positive,
);
assert_eq!(pos.status(), Error::NoError);
assert!(
pos.is_empty() || pos.volume() == 0.0,
"an inverted minuend bounds no material, got {}",
pos.volume()
);
let nz = a.boolean_with_engine_and_rule(
&b,
OpType::Subtract,
BooleanEngine::Robust,
WindingRule::Nonzero,
);
assert_eq!(nz.status(), Error::NoError);
assert_close(signed_volume(&nz), 8.0, 1e-12, "nonzero-rule disjoint subtract");
}
#[test]
fn clean_disjoint_subtract_keeps_fast_path_output() {
let a = mesh_from_tris(&cube_tris(0.0, 2.0));
let b = mesh_from_tris(&cube_tris(5.0, 7.0));
let d = a.difference_with_engine(&b, BooleanEngine::Robust);
assert_eq!(d.status(), Error::NoError);
assert_eq!(d.num_tri(), a.num_tri());
assert_eq!(d.num_vert(), a.num_vert());
assert_eq!(signed_volume(&d), signed_volume(&a));
}
#[test]
fn clean_disjoint_union_keeps_fast_path_output() {
let a = mesh_from_tris(&cube_tris(0.0, 2.0));
let b = mesh_from_tris(&cube_tris(5.0, 7.0));
let u = a.union_with_engine(&b, BooleanEngine::Robust);
assert_eq!(u.status(), Error::NoError);
assert_eq!(u.num_tri(), a.num_tri() + b.num_tri());
assert_eq!(u.num_tri(), 24);
assert_eq!(u.num_vert(), 16);
assert_eq!(signed_volume(&u), 16.0);
assert_eq!(u.volume(), 16.0);
}
#[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 auto_dispatches_self_intersecting_operands_to_robust() {
let a = import_stl_like_demo(include_bytes!("testdata/92068.stl"));
let b = import_stl_like_demo(include_bytes!("testdata/39926.stl"))
.translate(v(0.3, 0.0, 0.0));
assert_eq!(a.status(), Error::NoError, "operand A import");
assert_eq!(b.status(), Error::NoError, "operand B import");
assert!(!a.as_impl().is_soup, "operand A welds to a manifold");
assert!(!b.as_impl().is_soup, "operand B welds to a manifold");
assert!(
a.has_self_intersections(),
"92068's shells are coincident duplicates"
);
let auto = a.union_with_engine(&b, BooleanEngine::Auto);
let robust = a.union_with_engine(&b, BooleanEngine::Robust);
assert_eq!(auto.status(), Error::NoError, "auto union status");
assert_eq!(
auto.volume(),
robust.volume(),
"Auto must resolve to the robust engine"
);
}
#[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");
}