use num_rational::BigRational;
use num_traits::{Signed, Zero};
use crate::linalg::Vec3;
use super::super::intersection_graph::{build_graph, IntersectionGraph};
use super::super::propagate::propagate;
use super::super::ray_shoot::{piece_centroid, winding_number};
use super::{classify_rings, Classification, Tag};
fn v(x: f64, y: f64, z: f64) -> Vec3 {
Vec3::new(x, y, z)
}
fn cube(lo: f64, hi: f64) -> Vec<[Vec3; 3]> {
cube_at([lo, lo, lo], [hi, hi, hi])
}
fn cube_at(lo: [f64; 3], hi: [f64; 3]) -> Vec<[Vec3; 3]> {
let p = |x: f64, y: f64, z: f64| v(x, y, z);
let (x0, y0, z0) = (lo[0], lo[1], lo[2]);
let (x1, y1, z1) = (hi[0], hi[1], hi[2]);
let quads = [
[p(x0, y0, z0), p(x0, y1, z0), p(x1, y1, z0), p(x1, y0, z0)],
[p(x0, y0, z1), p(x1, y0, z1), p(x1, y1, z1), p(x0, y1, z1)],
[p(x0, y0, z0), p(x1, y0, z0), p(x1, y0, z1), p(x0, y0, z1)],
[p(x0, y1, z0), p(x0, y1, z1), p(x1, y1, z1), p(x1, y1, z0)],
[p(x0, y0, z0), p(x0, y0, z1), p(x0, y1, z1), p(x0, y1, z0)],
[p(x1, y0, z0), p(x1, y1, z0), p(x1, y1, z1), p(x1, y0, z1)],
];
let mut out = Vec::new();
for q in quads {
out.push([q[0], q[1], q[2]]);
out.push([q[0], q[2], q[3]]);
}
out
}
fn run(
p: &[[Vec3; 3]],
q: &[[Vec3; 3]],
q_complement: bool,
) -> (IntersectionGraph, Vec<Option<Tag>>, Vec<bool>) {
let graph = build_graph(p, q);
let cls: Classification = classify_rings(&graph);
let prop = propagate(&graph, &cls);
let mut tags = prop.tags.clone();
for &(root, rep) in &prop.untagged {
let piece = &graph.pieces[rep];
let other: &[[Vec3; 3]] = if piece.mesh == 0 { q } else { p };
let other_complement = if piece.mesh == 0 { q_complement } else { false };
let centroid = piece_centroid(graph.piece_verts(rep));
let w = winding_number(¢roid, other);
let inside = if other_complement { w == 0 } else { w != 0 };
let tag = if inside { Tag::Inter } else { Tag::Union };
for pi in 0..graph.pieces.len() {
if !cls.discarded[pi] && prop.component[pi] == root {
tags[pi] = Some(tag);
}
}
}
(graph, tags, cls.discarded)
}
fn check_against_winding_oracle(
graph: &IntersectionGraph,
tags: &[Option<Tag>],
discarded: &[bool],
p: &[[Vec3; 3]],
q: &[[Vec3; 3]],
) {
for (pi, piece) in graph.pieces.iter().enumerate() {
if discarded[pi] {
continue;
}
let other: &[[Vec3; 3]] = if piece.mesh == 0 { q } else { p };
let centroid = piece_centroid(graph.piece_verts(pi));
if centroid_on_surface(¢roid, other) {
continue;
}
let tag = tags[pi].expect("piece left untagged");
let inside = winding_number(¢roid, other) != 0;
let expect = if inside { Tag::Inter } else { Tag::Union };
assert_eq!(
tag, expect,
"piece {pi} (mesh {}, tri {}) misclassified",
piece.mesh, piece.tri
);
}
}
fn centroid_on_surface(c: &super::super::exact::rational::R3, tris: &[[Vec3; 3]]) -> bool {
use super::super::exact::predicates::{orient3d_r, point_in_tri_2d, tri_normal_r, TriLoc};
use super::super::exact::rational::R3;
use super::super::exact::Sign;
use super::super::tri_tri::dominant_axis;
for t in tris {
let a = R3::from_vec3(t[0]);
let b = R3::from_vec3(t[1]);
let cc = R3::from_vec3(t[2]);
if orient3d_r(&a, &b, &cc, c) != Sign::Zero {
continue;
}
let n = tri_normal_r(&a, &b, &cc);
let axis = dominant_axis(&n);
let loc = point_in_tri_2d(
&c.project_drop(axis),
&a.project_drop(axis),
&b.project_drop(axis),
&cc.project_drop(axis),
);
if loc != TriLoc::Outside {
return true;
}
}
false
}
fn piece_area2(v: [&super::super::exact::rational::R3; 3]) -> BigRational {
let n = v[1].sub(v[0]).cross(&v[2].sub(v[0]));
let comps = [n.x.abs(), n.y.abs(), n.z.abs()];
let mut nonzero: Vec<&BigRational> = comps.iter().filter(|c| !c.is_zero()).collect();
assert!(nonzero.len() <= 1, "test helper requires axis-aligned pieces");
nonzero.pop().cloned().unwrap_or_else(BigRational::zero)
}
#[test]
fn overlapping_cubes_classify_correctly() {
let p = cube(0.0, 2.0);
let q_tris = cube_at([1.0, 1.0, 1.0], [3.0, 3.0, 3.0]);
let (graph, tags, discarded) = run(&p, &q_tris, false);
assert!(graph.any_intersections);
assert!(discarded.iter().all(|d| !d));
assert!(tags.iter().zip(&discarded).all(|(t, d)| *d || t.is_some()));
check_against_winding_oracle(&graph, &tags, &discarded, &p, &q_tris);
}
#[test]
fn skewered_cube_classifies_correctly() {
let p = cube(0.0, 4.0);
let q_tris = cube_at([1.5, 1.5, -2.0], [2.5, 2.5, 6.0]);
let (graph, tags, discarded) = run(&p, &q_tris, false);
assert!(graph.any_intersections);
check_against_winding_oracle(&graph, &tags, &discarded, &p, &q_tris);
}
#[test]
fn nested_cube_resolved_by_ray_shooting() {
let p = cube(0.0, 6.0);
let q_tris = cube(2.0, 4.0);
let (graph, tags, discarded) = run(&p, &q_tris, false);
assert!(!graph.any_intersections);
assert!(discarded.iter().all(|d| !d));
for (pi, piece) in graph.pieces.iter().enumerate() {
let expect = if piece.mesh == 0 { Tag::Union } else { Tag::Inter };
assert_eq!(tags[pi], Some(expect), "piece {pi}");
}
}
#[test]
fn face_touching_cubes_discard_shared_face() {
let p = cube_at([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
let q_tris = cube_at([0.0, 0.0, 2.0], [2.0, 2.0, 4.0]);
let (graph, tags, discarded) = run(&p, &q_tris, false);
assert!(graph.any_intersections);
let mut discarded_area2 = [BigRational::zero(), BigRational::zero()];
for (pi, piece) in graph.pieces.iter().enumerate() {
if discarded[pi] {
discarded_area2[piece.mesh as usize] =
&discarded_area2[piece.mesh as usize] + piece_area2(graph.piece_verts(pi));
}
}
let eight = BigRational::from_integer(8.into());
assert_eq!(discarded_area2[0], eight, "P-side discarded area");
assert_eq!(discarded_area2[1], eight, "Q-side discarded area");
for (pi, piece) in graph.pieces.iter().enumerate() {
if discarded[pi] {
continue;
}
assert_eq!(tags[pi], Some(Tag::Union), "piece {pi} of mesh {}", piece.mesh);
}
}
#[test]
fn identical_cubes_share_every_face_once_per_output() {
let p = cube(0.0, 2.0);
let q_tris = cube(0.0, 2.0);
let (graph, tags, discarded) = run(&p, &q_tris, false);
assert!(graph.any_intersections);
assert!(discarded.iter().all(|d| !d));
let full_surface2 = BigRational::from_integer(48.into()); let mut union_area2 = BigRational::zero();
let mut inter_area2 = BigRational::zero();
for (pi, _piece) in graph.pieces.iter().enumerate() {
match tags[pi].expect("all pieces must be tagged for identical cubes") {
Tag::Union => union_area2 = &union_area2 + piece_area2(graph.piece_verts(pi)),
Tag::Inter => inter_area2 = &inter_area2 + piece_area2(graph.piece_verts(pi)),
}
}
assert_eq!(union_area2, full_surface2, "union output must cover the cube once");
assert_eq!(inter_area2, full_surface2, "intersection output must cover the cube once");
}
#[test]
fn edge_touching_cubes_have_no_discards_and_all_union() {
let p = cube_at([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
let q_tris = cube_at([2.0, 2.0, 0.0], [4.0, 4.0, 2.0]);
let (graph, tags, discarded) = run(&p, &q_tris, false);
assert!(graph.any_intersections);
assert!(discarded.iter().all(|d| !d));
for (pi, _) in graph.pieces.iter().enumerate() {
assert_eq!(tags[pi], Some(Tag::Union), "piece {pi}");
}
check_against_winding_oracle(&graph, &tags, &discarded, &p, &q_tris);
}
#[test]
fn subtract_style_flipped_operand() {
let p = cube(0.0, 2.0);
let q_flipped: Vec<[Vec3; 3]> = cube_at([1.0, 1.0, 1.0], [3.0, 3.0, 3.0])
.iter()
.map(|t| [t[0], t[2], t[1]])
.collect();
let (graph, tags, _discarded) = run(&p, &q_flipped, true);
assert!(graph.any_intersections);
let p_union = tags
.iter()
.zip(&graph.pieces)
.filter(|(t, pc)| pc.mesh == 0 && **t == Some(Tag::Union))
.count();
let p_inter = tags
.iter()
.zip(&graph.pieces)
.filter(|(t, pc)| pc.mesh == 0 && **t == Some(Tag::Inter))
.count();
assert!(p_union > 0 && p_inter > 0);
}