use super::*;
use crate::linalg::Vec3;
use crate::robust::intersection_graph::build_graph;
use crate::types::{Error, OpType, WindingRule};
fn cube(lo: Vec3, hi: Vec3) -> Vec<[Vec3; 3]> {
let v = [
Vec3::new(lo.x, lo.y, lo.z),
Vec3::new(hi.x, lo.y, lo.z),
Vec3::new(hi.x, hi.y, lo.z),
Vec3::new(lo.x, hi.y, lo.z),
Vec3::new(lo.x, lo.y, hi.z),
Vec3::new(hi.x, lo.y, hi.z),
Vec3::new(hi.x, hi.y, hi.z),
Vec3::new(lo.x, hi.y, hi.z),
];
let idx = [
[0, 3, 2],
[0, 2, 1], [4, 5, 6],
[4, 6, 7], [0, 1, 5],
[0, 5, 4], [3, 7, 6],
[3, 6, 2], [0, 4, 7],
[0, 7, 3], [1, 2, 6],
[1, 6, 5], ];
idx.iter().map(|t| [v[t[0]], v[t[1]], v[t[2]]]).collect()
}
fn all_windings(
graph: &IntersectionGraph,
complex: &CellComplex,
p: &[[Vec3; 3]],
q: &[[Vec3; 3]],
) -> Windings {
windings(graph, complex, [p, q])
}
fn inside_winding(
graph: &IntersectionGraph,
complex: &CellComplex,
wind: &Windings,
mesh: u8,
) -> Vec<[i32; 2]> {
let mut seen = Vec::new();
for (pi, piece) in graph.pieces.iter().enumerate() {
if piece.mesh != mesh {
continue;
}
let c = complex.cell(pi, ANTI);
if wind.known[c] && !seen.contains(&wind.w[c]) {
seen.push(wind.w[c]);
}
}
seen
}
#[test]
fn disjoint_cubes_have_three_cells() {
let p = cube(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0));
let q = cube(Vec3::new(2.0, 0.0, 0.0), Vec3::new(3.0, 1.0, 1.0));
let graph = build_graph(&p, &q);
let complex = build_cells(&graph);
assert_eq!(complex.num_cells, 4, "two disjoint shells, two cells each");
let wind = all_windings(&graph, &complex, &p, &q);
assert!(wind.known.iter().all(|&k| k), "seeding reaches every cell");
assert!(
wind.w.iter().any(|&w| w == [0, 0]),
"the region outside both must be present"
);
assert_eq!(inside_winding(&graph, &complex, &wind, 0), vec![[1, 0]]);
assert_eq!(inside_winding(&graph, &complex, &wind, 1), vec![[0, 1]]);
}
#[test]
fn overlapping_cubes_wind_to_two_in_the_lens() {
let p = cube(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
let q = cube(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 3.0, 3.0));
let graph = build_graph(&p, &q);
let complex = build_cells(&graph);
let wind = all_windings(&graph, &complex, &p, &q);
let mut regions: Vec<[i32; 2]> = Vec::new();
for c in 0..complex.num_cells {
if wind.known[c] && !regions.contains(&wind.w[c]) {
regions.push(wind.w[c]);
}
}
regions.sort();
assert_eq!(
regions,
vec![[0, 0], [0, 1], [1, 0], [1, 1]],
"outside, Q only, P only, and the overlap where both wind to one"
);
}
fn boolean_via_cells(p: &[[Vec3; 3]], q: &[[Vec3; 3]], op: OpType) -> crate::manifold::Manifold {
boolean_via_cells_rule(p, q, op, WindingRule::Positive)
}
fn boolean_via_cells_rule(
p: &[[Vec3; 3]],
q: &[[Vec3; 3]],
op: OpType,
rule: WindingRule,
) -> crate::manifold::Manifold {
let graph = build_graph(p, q);
let complex = build_cells(&graph);
let wind = all_windings(&graph, &complex, p, q);
let pieces = extract(&graph, &complex, &wind, op, rule);
crate::robust::assemble::assemble(&pieces, &graph.verts, &graph.verts_f64, |_| true, None)
}
#[test]
fn in_result_honors_the_winding_rule() {
let vectors = [[1, 0], [-1, 0], [2, 0], [0, -1], [-1, -1], [0, 0], [1, 1]];
for w in vectors {
for (rule, inside) in [
(WindingRule::Positive, (|v: i32| v >= 1) as fn(i32) -> bool),
(WindingRule::Nonzero, (|v: i32| v != 0) as fn(i32) -> bool),
] {
let (a, b) = (inside(w[0]), inside(w[1]));
for (op, want) in [
(OpType::Add, a || b),
(OpType::Intersect, a && b),
(OpType::Subtract, a && !b),
] {
assert_eq!(
in_result(op, rule, w),
want,
"{op:?} {rule:?} on {w:?}"
);
}
}
}
assert!(!in_result(OpType::Add, WindingRule::Positive, [-1, 0]));
assert!(in_result(OpType::Add, WindingRule::Nonzero, [-1, 0]));
assert!(!in_result(OpType::Subtract, WindingRule::Nonzero, [1, -1]));
assert!(in_result(OpType::Subtract, WindingRule::Positive, [1, -1]));
assert!(in_result(OpType::Intersect, WindingRule::Nonzero, [-1, -1]));
assert!(!in_result(OpType::Intersect, WindingRule::Positive, [-1, -1]));
}
#[test]
fn nonzero_rule_keeps_an_inverted_operand() {
let p = cube(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
let q = cube(Vec3::new(5.0, 0.0, 0.0), Vec3::new(6.0, 1.0, 1.0));
let flipped: Vec<[Vec3; 3]> = q.iter().map(|t| [t[0], t[2], t[1]]).collect();
let positive = boolean_via_cells_rule(&p, &flipped, OpType::Add, WindingRule::Positive);
assert_eq!(positive.status(), Error::NoError);
assert!((positive.volume() - 8.0).abs() < 1e-9, "{}", positive.volume());
let nonzero = boolean_via_cells_rule(&p, &flipped, OpType::Add, WindingRule::Nonzero);
assert_eq!(nonzero.status(), Error::NoError);
assert!(!nonzero.as_impl().is_soup, "nonzero result must close");
assert!((nonzero.volume() - 9.0).abs() < 1e-9, "{}", nonzero.volume());
}
#[test]
fn extracted_booleans_have_correct_volume() {
let p = cube(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
let q = cube(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 3.0, 3.0));
for (op, want) in [
(OpType::Add, 15.0),
(OpType::Intersect, 1.0),
(OpType::Subtract, 7.0),
] {
let m = boolean_via_cells(&p, &q, op);
assert_eq!(m.status(), Error::NoError, "{op:?} status");
assert!(!m.as_impl().is_soup, "{op:?} must close into a manifold");
assert!(
(m.volume() - want).abs() < 1e-9,
"{op:?} volume {}, want {want}",
m.volume()
);
}
}
#[test]
fn inverted_operand_yields_the_same_union() {
let p = cube(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
let q = cube(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 3.0, 3.0));
let flipped: Vec<[Vec3; 3]> = q.iter().map(|t| [t[0], t[2], t[1]]).collect();
let m = boolean_via_cells(&p, &flipped, OpType::Add);
assert_eq!(m.status(), Error::NoError, "inverted-operand union status");
assert!(!m.as_impl().is_soup, "result must still be a manifold");
assert!((m.volume() - 8.0).abs() < 1e-9, "volume {}", m.volume());
}
fn mesh_from_tris(tris: &[[Vec3; 3]]) -> crate::manifold::Manifold {
let mut mesh = crate::types::MeshGL::default();
mesh.num_prop = 3;
for t in tris {
for p in t {
mesh.vert_properties
.extend([p.x as f32, p.y as f32, p.z as f32]);
}
}
mesh.tri_verts = (0..(tris.len() * 3) as u32).collect();
mesh.merge();
crate::manifold::Manifold::from_mesh_gl_robust(&mesh)
}
#[test]
fn nonzero_rule_keeps_inside_out_geometry_through_the_public_api() {
use crate::types::BooleanEngine;
let mut soup = cube(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
let inverted = cube(Vec3::new(4.0, 0.0, 0.0), Vec3::new(6.0, 2.0, 2.0));
soup.extend(inverted.iter().map(|t| [t[0], t[2], t[1]]));
let a = mesh_from_tris(&soup);
let b = mesh_from_tris(&cube(Vec3::new(1.0, 0.5, 0.5), Vec3::new(5.0, 1.5, 1.5)));
for (rule, want) in [
(WindingRule::Positive, 11.0),
(WindingRule::Nonzero, 18.0),
] {
let out = a.boolean_with_engine_and_rule(&b, OpType::Add, BooleanEngine::Robust, rule);
assert_eq!(out.status(), Error::NoError, "{rule:?} status");
assert!(
(out.volume() - want).abs() < 1e-9,
"{rule:?} volume {}, want {want}",
out.volume()
);
}
}
#[test]
fn doubled_shell_steps_winding_by_two() {
let mut p = cube(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0));
let dup = p.clone();
p.extend(dup); let q = cube(Vec3::new(5.0, 0.0, 0.0), Vec3::new(6.0, 1.0, 1.0));
let graph = build_graph(&p, &q);
let complex = build_cells(&graph);
let wind = all_windings(&graph, &complex, &p, &q);
assert_eq!(
inside_winding(&graph, &complex, &wind, 0),
vec![[2, 0]],
"a double cover winds to two inside"
);
}