use super::*;
use crate::projection_outline::{mesh_outline_2d, ProjectionAxis};
fn rect_ccw(x0: f64, y0: f64, x1: f64, y1: f64) -> Ring2D {
vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
}
fn rect_cw(x0: f64, y0: f64, x1: f64, y1: f64) -> Ring2D {
vec![[x0, y0], [x0, y1], [x1, y1], [x1, y0]]
}
fn signed_area(ring: &Ring2D) -> f64 {
let n = ring.len();
let mut a = 0.0;
for i in 0..n {
let j = (i + 1) % n;
a += ring[i][0] * ring[j][1] - ring[j][0] * ring[i][1];
}
a * 0.5
}
fn covered_area(set: &ContourSet) -> f64 {
set.rings.iter().map(signed_area).sum()
}
const TOL: f64 = 1e-9;
#[test]
fn union_of_overlapping_squares_merges_into_one_shape() {
let a = vec![rect_ccw(0.0, 0.0, 2.0, 2.0)];
let b = vec![rect_ccw(1.0, 1.0, 3.0, 3.0)];
let out = boolean_2d(&a, &b, BooleanOp2D::Union);
assert_eq!(out.shape_count(), 1, "overlapping squares are one region");
assert_eq!(out.rings.len(), 1, "an L-shape has no holes");
assert!((covered_area(&out) - 7.0).abs() < TOL, "{}", covered_area(&out));
}
#[test]
fn union_of_disjoint_squares_keeps_both_shapes() {
let a = vec![rect_ccw(0.0, 0.0, 1.0, 1.0)];
let b = vec![rect_ccw(5.0, 5.0, 6.0, 6.0)];
let out = boolean_2d(&a, &b, BooleanOp2D::Union);
assert_eq!(out.shape_count(), 2, "disjoint islands must both survive");
assert_eq!(out.shape_offsets, vec![0, 1]);
assert!((covered_area(&out) - 2.0).abs() < TOL);
}
#[test]
fn union_closing_a_ring_leaves_a_hole() {
let bars = vec![
rect_ccw(0.0, 0.0, 10.0, 1.0),
rect_ccw(0.0, 9.0, 10.0, 10.0),
rect_ccw(0.0, 0.0, 1.0, 10.0),
rect_ccw(9.0, 0.0, 10.0, 10.0),
];
let out = resolve_2d(&bars);
assert_eq!(out.shape_count(), 1);
assert_eq!(out.rings.len(), 2, "outer boundary + one hole");
let shape = out.shape(0).expect("shape 0");
assert!(signed_area(&shape[0]) > 0.0, "outer ring must be CCW");
assert!(signed_area(&shape[1]) < 0.0, "hole ring must be CW");
assert!((covered_area(&out) - 36.0).abs() < TOL, "{}", covered_area(&out));
}
#[test]
fn difference_splitting_the_subject_keeps_every_island() {
let bar = vec![rect_ccw(0.0, 0.0, 10.0, 1.0)];
let cutter = vec![rect_ccw(4.0, -1.0, 6.0, 2.0)];
let out = boolean_2d(&bar, &cutter, BooleanOp2D::Difference);
assert_eq!(out.shape_count(), 2, "both remnants must survive the cut");
assert!((covered_area(&out) - 8.0).abs() < TOL, "{}", covered_area(&out));
}
#[test]
fn difference_into_the_interior_makes_a_hole() {
let outer = vec![rect_ccw(0.0, 0.0, 10.0, 10.0)];
let inner = vec![rect_ccw(4.0, 4.0, 6.0, 6.0)];
let out = boolean_2d(&outer, &inner, BooleanOp2D::Difference);
assert_eq!(out.shape_count(), 1);
assert_eq!(out.rings.len(), 2, "outer boundary + punched hole");
assert!(signed_area(&out.rings[1]) < 0.0, "punched hole must be CW");
assert!((covered_area(&out) - 96.0).abs() < TOL);
}
#[test]
fn difference_covering_the_subject_is_empty() {
let a = vec![rect_ccw(1.0, 1.0, 2.0, 2.0)];
let b = vec![rect_ccw(0.0, 0.0, 10.0, 10.0)];
let out = boolean_2d(&a, &b, BooleanOp2D::Difference);
assert!(out.is_empty(), "a fully occluded element contributes nothing");
assert_eq!(out.shape_count(), 0);
}
#[test]
fn difference_against_many_clip_rings_subtracts_their_union() {
let bar = vec![rect_ccw(0.0, 0.0, 10.0, 1.0)];
let cutters = vec![
rect_ccw(2.0, -1.0, 4.0, 2.0),
rect_ccw(3.0, -1.0, 5.0, 2.0), rect_ccw(8.0, -1.0, 9.0, 2.0),
];
let out = boolean_2d(&bar, &cutters, BooleanOp2D::Difference);
assert!((covered_area(&out) - 6.0).abs() < TOL, "{}", covered_area(&out));
assert_eq!(out.shape_count(), 3, "remnants at [0,2], [5,8] and [9,10]");
}
#[test]
fn intersection_is_the_shared_region() {
let a = vec![rect_ccw(0.0, 0.0, 2.0, 2.0)];
let b = vec![rect_ccw(1.0, 1.0, 3.0, 3.0)];
let out = boolean_2d(&a, &b, BooleanOp2D::Intersection);
assert_eq!(out.shape_count(), 1);
assert!((covered_area(&out) - 1.0).abs() < TOL);
let bounds = out.bounds().expect("bounds");
assert!((bounds[0] - 1.0).abs() < TOL && (bounds[2] - 2.0).abs() < TOL);
}
#[test]
fn intersection_of_disjoint_sets_is_empty() {
let a = vec![rect_ccw(0.0, 0.0, 1.0, 1.0)];
let b = vec![rect_ccw(5.0, 5.0, 6.0, 6.0)];
assert!(boolean_2d(&a, &b, BooleanOp2D::Intersection).is_empty());
}
#[test]
fn intersecting_a_tile_clips_a_holed_subject_and_keeps_the_hole() {
let frame = vec![rect_ccw(0.0, 0.0, 10.0, 10.0), rect_cw(3.0, 3.0, 7.0, 7.0)];
let tile = vec![rect_ccw(-1.0, -1.0, 5.0, 11.0)];
let out = boolean_2d(&frame, &tile, BooleanOp2D::Intersection);
assert!((covered_area(&out) - 42.0).abs() < TOL, "{}", covered_area(&out));
}
#[test]
fn input_hole_winding_is_respected_not_normalised() {
let frame = vec![rect_ccw(0.0, 0.0, 10.0, 10.0), rect_cw(3.0, 3.0, 7.0, 7.0)];
let island = vec![rect_ccw(20.0, 20.0, 21.0, 21.0)];
let out = boolean_2d(&frame, &island, BooleanOp2D::Union);
assert_eq!(out.shape_count(), 2);
assert!((covered_area(&out) - 85.0).abs() < TOL, "{}", covered_area(&out));
}
#[test]
fn a_result_round_trips_through_another_boolean_unchanged() {
let frame = vec![rect_ccw(0.0, 0.0, 10.0, 10.0), rect_cw(3.0, 3.0, 7.0, 7.0)];
let once = resolve_2d(&frame);
let twice = resolve_2d(&once.rings);
assert_eq!(once.shape_offsets, twice.shape_offsets);
assert!((covered_area(&once) - covered_area(&twice)).abs() < TOL);
assert!((covered_area(&twice) - 84.0).abs() < TOL);
}
#[test]
fn accumulating_an_occluder_matches_a_single_union() {
let elements = [
rect_ccw(0.0, 0.0, 4.0, 4.0),
rect_ccw(3.0, 3.0, 7.0, 7.0),
rect_ccw(6.0, 0.0, 9.0, 9.0),
rect_ccw(20.0, 0.0, 21.0, 1.0),
];
let mut acc = ContourSet::default();
for e in &elements {
acc = boolean_2d(&acc.rings, std::slice::from_ref(e), BooleanOp2D::Union);
}
let all: Vec<Ring2D> = elements.to_vec();
let at_once = resolve_2d(&all);
assert_eq!(acc.shape_count(), at_once.shape_count());
assert!((covered_area(&acc) - covered_area(&at_once)).abs() < TOL);
}
#[test]
fn mesh_outline_rings_feed_straight_back_in() {
let positions: Vec<f32> = vec![
0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0,
];
let indices: Vec<u32> = vec![0, 1, 2, 0, 2, 3];
let outline =
mesh_outline_2d(&positions, &indices, ProjectionAxis::Z, false).expect("outline");
let rings: Vec<Ring2D> = outline
.contours
.iter()
.map(|ring| ring.iter().map(|p| [p[0] as f64, p[1] as f64]).collect())
.collect();
let resolved = resolve_2d(&rings);
assert_eq!(resolved.shape_count(), 1);
assert!(
(covered_area(&resolved) - 1.0).abs() < 1e-6,
"outline area must survive the round trip: {}",
covered_area(&resolved)
);
let cutter = vec![rect_ccw(0.25, -1.0, 0.75, 2.0)];
let cut = boolean_2d(&rings, &cutter, BooleanOp2D::Difference);
assert_eq!(cut.shape_count(), 2, "the strip splits the square in two");
assert!((covered_area(&cut) - 0.5).abs() < 1e-6);
}
#[test]
fn empty_operands_have_defined_answers() {
let a = vec![rect_ccw(0.0, 0.0, 1.0, 1.0)];
let none: Vec<Ring2D> = Vec::new();
assert!((covered_area(&boolean_2d(&a, &none, BooleanOp2D::Union)) - 1.0).abs() < TOL);
assert!((covered_area(&boolean_2d(&none, &a, BooleanOp2D::Union)) - 1.0).abs() < TOL);
assert!((covered_area(&boolean_2d(&a, &none, BooleanOp2D::Difference)) - 1.0).abs() < TOL);
assert!(boolean_2d(&none, &a, BooleanOp2D::Difference).is_empty());
assert!(boolean_2d(&a, &none, BooleanOp2D::Intersection).is_empty());
assert!(boolean_2d(&none, &a, BooleanOp2D::Intersection).is_empty());
assert!(boolean_2d(&none, &none, BooleanOp2D::Union).is_empty());
assert!(resolve_2d(&none).is_empty());
assert!(resolve_2d(&none).bounds().is_none());
}
#[test]
fn undersized_and_non_finite_rings_are_dropped_not_fatal() {
let good = rect_ccw(0.0, 0.0, 1.0, 1.0);
let subject = vec![
good.clone(),
vec![[0.0, 0.0], [1.0, 0.0]], vec![[5.0, 5.0], [f64::NAN, 6.0], [6.0, 5.0]], vec![[7.0, 7.0], [f64::INFINITY, 8.0], [8.0, 7.0]], vec![], ];
let out = resolve_2d(&subject);
assert_eq!(out.shape_count(), 1, "only the valid ring survives");
assert!((covered_area(&out) - 1.0).abs() < TOL);
}
#[test]
fn explicitly_closed_rings_are_accepted() {
let open = rect_ccw(0.0, 0.0, 2.0, 2.0);
let mut closed = open.clone();
closed.push(open[0]);
let a = resolve_2d(&[open]);
let b = resolve_2d(&[closed]);
assert_eq!(a.shape_count(), b.shape_count());
assert!((covered_area(&a) - covered_area(&b)).abs() < TOL);
assert!((covered_area(&b) - 4.0).abs() < TOL);
}
#[test]
fn a_zero_area_ring_contributes_nothing() {
let collinear = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
assert!(
sanitize(std::slice::from_ref(&collinear)).is_empty(),
"sanitize must drop a collinear ring, not just the overlay"
);
let out = resolve_2d(&[collinear]);
assert!(out.is_empty(), "a collapsed ring covers no area");
assert!(out.bounds().is_none(), "bounds must agree with is_empty");
}
#[test]
fn a_zero_signed_area_bowtie_is_kept_not_dropped() {
let bowtie = vec![[0.0, 0.0], [2.0, 2.0], [2.0, 0.0], [0.0, 2.0]];
assert_eq!(
signed_area(&bowtie),
0.0,
"precondition: the bow-tie's signed area is exactly zero"
);
assert_eq!(
sanitize(std::slice::from_ref(&bowtie)).len(),
1,
"sanitize must keep a bow-tie — it is not collinear"
);
let out = resolve_2d(&[bowtie]);
assert!(!out.is_empty(), "the bow-tie's lobes must survive");
assert!(
(covered_area(&out) - 2.0).abs() < TOL,
"both lobes fill under NonZero: {}",
covered_area(&out)
);
}
#[test]
fn shape_accessor_groups_outer_with_its_holes() {
let holed = vec![rect_ccw(0.0, 0.0, 10.0, 10.0), rect_cw(3.0, 3.0, 7.0, 7.0)];
let island = vec![rect_ccw(20.0, 20.0, 21.0, 21.0)];
let out = boolean_2d(&holed, &island, BooleanOp2D::Union);
assert_eq!(out.shape_count(), 2);
assert_eq!(out.rings.len(), 3);
let mut sizes: Vec<usize> = (0..out.shape_count())
.map(|s| out.shape(s).expect("shape").len())
.collect();
sizes.sort_unstable();
assert_eq!(sizes, vec![1, 2], "one plain island, one outer + hole");
assert!(out.shape(2).is_none(), "out-of-range shape index");
}
#[test]
fn bounds_span_every_shape() {
let a = vec![rect_ccw(-3.0, -2.0, 1.0, 1.0)];
let b = vec![rect_ccw(5.0, 5.0, 6.0, 8.0)];
let out = boolean_2d(&a, &b, BooleanOp2D::Union);
let bounds = out.bounds().expect("bounds");
assert!((bounds[0] + 3.0).abs() < TOL, "min x {}", bounds[0]);
assert!((bounds[1] + 2.0).abs() < TOL, "min y {}", bounds[1]);
assert!((bounds[2] - 6.0).abs() < TOL, "max x {}", bounds[2]);
assert!((bounds[3] - 8.0).abs() < TOL, "max y {}", bounds[3]);
}
#[test]
fn op_codes_decode_and_reject() {
assert_eq!(BooleanOp2D::from_u8(0), Some(BooleanOp2D::Union));
assert_eq!(BooleanOp2D::from_u8(1), Some(BooleanOp2D::Difference));
assert_eq!(BooleanOp2D::from_u8(2), Some(BooleanOp2D::Intersection));
assert_eq!(BooleanOp2D::from_u8(3), None);
}