use std::collections::HashMap;
use std::f64::consts::PI;
use crate::geometry::diagram::{
discover_regions, mask_to_indices, to_exclusive_areas, to_exclusive_areas_and_gradients,
IntersectionPoint, RegionMask,
};
use crate::geometry::primitives::Point;
use crate::geometry::shapes::{Polygon, Rectangle};
use crate::geometry::traits::{
Area, BoundingBox, Centroid, Closed, DiagramShape, Distance, ExclusiveRegionsAndGradient,
Perimeter, Polygonize,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Square {
center: Point,
side: f64,
}
impl Square {
pub fn new(center: Point, side: f64) -> Self {
debug_assert!(side > 0.0, "Square side must be positive");
Square { center, side }
}
pub fn center(&self) -> Point {
self.center
}
pub fn side(&self) -> f64 {
self.side
}
pub fn bounds(&self) -> (f64, f64, f64, f64) {
let h = self.side * 0.5;
(
self.center.x() - h,
self.center.x() + h,
self.center.y() - h,
self.center.y() + h,
)
}
fn as_rectangle(&self) -> Rectangle {
Rectangle::new(self.center, self.side, self.side)
}
}
impl Area for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
impl Centroid for Square {
fn centroid(&self) -> Point {
self.center
}
}
impl Perimeter for Square {
fn perimeter(&self) -> f64 {
4.0 * self.side
}
}
impl BoundingBox for Square {
fn bounding_box(&self) -> Rectangle {
self.as_rectangle()
}
}
impl Distance for Square {
fn distance(&self, other: &Self) -> f64 {
self.as_rectangle().distance(&other.as_rectangle())
}
}
impl Closed for Square {
fn contains(&self, other: &Self) -> bool {
self.as_rectangle().contains(&other.as_rectangle())
}
fn contains_point(&self, point: &Point) -> bool {
self.as_rectangle().contains_point(point)
}
fn intersects(&self, other: &Self) -> bool {
self.as_rectangle().intersects(&other.as_rectangle())
}
fn intersection_area(&self, other: &Self) -> f64 {
let (ax0, ax1, ay0, ay1) = self.bounds();
let (bx0, bx1, by0, by1) = other.bounds();
let dx = (ax1.min(bx1) - ax0.max(bx0)).max(0.0);
let dy = (ay1.min(by1) - ay0.max(by0)).max(0.0);
dx * dy
}
fn intersection_points(&self, other: &Self) -> Vec<Point> {
let (ax0, ax1, ay0, ay1) = self.bounds();
let (bx0, bx1, by0, by1) = other.bounds();
let mut points = Vec::new();
for &y in &[ay0, ay1] {
for &x in &[bx0, bx1] {
if x >= ax0 && x <= ax1 && y >= by0 && y <= by1 {
points.push(Point::new(x, y));
}
}
}
for &y in &[by0, by1] {
for &x in &[ax0, ax1] {
if x >= bx0 && x <= bx1 && y >= ay0 && y <= ay1 {
points.push(Point::new(x, y));
}
}
}
points.sort_by(|p, q| {
p.x()
.partial_cmp(&q.x())
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
p.y()
.partial_cmp(&q.y())
.unwrap_or(std::cmp::Ordering::Equal)
})
});
points.dedup_by(|a, b| (a.x() - b.x()).abs() < 1e-12 && (a.y() - b.y()).abs() < 1e-12);
points
}
}
fn collect_intersections_square(squares: &[Square], n_sets: usize) -> Vec<IntersectionPoint> {
let mut intersections = Vec::new();
for i in 0..n_sets {
for j in (i + 1)..n_sets {
let pts = squares[i].intersection_points(&squares[j]);
for point in pts {
let mut adopters = vec![i, j];
for (k, sq) in squares.iter().enumerate().take(n_sets) {
if k != i && k != j && sq.contains_point(&point) {
adopters.push(k);
}
}
adopters.sort_unstable();
intersections.push(IntersectionPoint::new(point, (i, j), adopters));
}
}
}
intersections
}
fn compute_exclusive_regions_with_gradient_squares(
shapes: &[Square],
) -> ExclusiveRegionsAndGradient {
let n_sets = shapes.len();
let n_params = n_sets * 3;
let intersections = collect_intersections_square(shapes, n_sets);
let regions = discover_regions(shapes, &intersections, n_sets);
let mut overlapping_areas: HashMap<RegionMask, f64> = HashMap::new();
let mut overlapping_grads: HashMap<RegionMask, Vec<f64>> = HashMap::new();
for &mask in ®ions {
let indices = mask_to_indices(mask, n_sets);
let mut x_min = f64::NEG_INFINITY;
let mut x_max = f64::INFINITY;
let mut y_min = f64::NEG_INFINITY;
let mut y_max = f64::INFINITY;
for &i in &indices {
let (a, b, c, d) = shapes[i].bounds();
if a > x_min {
x_min = a;
}
if b < x_max {
x_max = b;
}
if c > y_min {
y_min = c;
}
if d < y_max {
y_max = d;
}
}
let dx_raw = x_max - x_min;
let dy_raw = y_max - y_min;
let dx = dx_raw.max(0.0);
let dy = dy_raw.max(0.0);
overlapping_areas.insert(mask, dx * dy);
let mut grad = vec![0.0; n_params];
if dx_raw > 0.0 && dy_raw > 0.0 {
let mut tied_l: Vec<usize> = Vec::with_capacity(indices.len());
let mut tied_r: Vec<usize> = Vec::with_capacity(indices.len());
let mut tied_b: Vec<usize> = Vec::with_capacity(indices.len());
let mut tied_t: Vec<usize> = Vec::with_capacity(indices.len());
for &i in &indices {
let (a, b, c, d) = shapes[i].bounds();
#[allow(clippy::float_cmp)]
{
if a == x_min {
tied_l.push(i);
}
if b == x_max {
tied_r.push(i);
}
if c == y_min {
tied_b.push(i);
}
if d == y_max {
tied_t.push(i);
}
}
}
let w_l = 1.0 / tied_l.len() as f64;
let w_r = 1.0 / tied_r.len() as f64;
let w_b = 1.0 / tied_b.len() as f64;
let w_t = 1.0 / tied_t.len() as f64;
for &i in &tied_l {
grad[3 * i] -= dy * w_l;
grad[3 * i + 2] += dy * 0.5 * w_l;
}
for &i in &tied_r {
grad[3 * i] += dy * w_r;
grad[3 * i + 2] += dy * 0.5 * w_r;
}
for &i in &tied_b {
grad[3 * i + 1] -= dx * w_b;
grad[3 * i + 2] += dx * 0.5 * w_b;
}
for &i in &tied_t {
grad[3 * i + 1] += dx * w_t;
grad[3 * i + 2] += dx * 0.5 * w_t;
}
}
overlapping_grads.insert(mask, grad);
}
to_exclusive_areas_and_gradients(&overlapping_areas, &overlapping_grads, n_params)
}
impl DiagramShape for Square {
fn compute_exclusive_regions(shapes: &[Self]) -> HashMap<RegionMask, f64> {
let n_sets = shapes.len();
let intersections = collect_intersections_square(shapes, n_sets);
let regions = discover_regions(shapes, &intersections, n_sets);
let mut overlapping_areas: HashMap<RegionMask, f64> = HashMap::new();
for &mask in ®ions {
let indices = mask_to_indices(mask, n_sets);
let mut x_min = f64::NEG_INFINITY;
let mut x_max = f64::INFINITY;
let mut y_min = f64::NEG_INFINITY;
let mut y_max = f64::INFINITY;
for &i in &indices {
let (a, b, c, d) = shapes[i].bounds();
if a > x_min {
x_min = a;
}
if b < x_max {
x_max = b;
}
if c > y_min {
y_min = c;
}
if d < y_max {
y_max = d;
}
}
let dx = (x_max - x_min).max(0.0);
let dy = (y_max - y_min).max(0.0);
overlapping_areas.insert(mask, dx * dy);
}
to_exclusive_areas(&overlapping_areas)
}
fn params_from_circle(x: f64, y: f64, radius: f64) -> Vec<f64> {
vec![x, y, radius * PI.sqrt()]
}
fn mds_target_distance(
area_i: f64,
area_j: f64,
target_overlap: f64,
) -> Result<f64, crate::error::DiagramError> {
let s_i = area_i.sqrt();
let s_j = area_j.sqrt();
let half_sum = 0.5 * (s_i + s_j);
if target_overlap <= 0.0 {
return Ok(half_sum * 2.0_f64.sqrt());
}
let root = target_overlap.sqrt();
if root > half_sum {
return Ok(0.0);
}
let d = 2.0_f64.sqrt() * (half_sum - root);
Ok(d.max(0.0))
}
fn n_params() -> usize {
3 }
fn from_params(params: &[f64]) -> Self {
debug_assert_eq!(params.len(), 3, "Square requires 3 parameters: x, y, side");
Square::new(
Point::new(params[0], params[1]),
params[2].max(f64::MIN_POSITIVE),
)
}
fn to_params(&self) -> Vec<f64> {
vec![self.center.x(), self.center.y(), self.side]
}
fn compute_exclusive_regions_with_gradient(
shapes: &[Self],
) -> Option<ExclusiveRegionsAndGradient> {
Some(compute_exclusive_regions_with_gradient_squares(shapes))
}
fn canonical_venn_layout(n: usize) -> Option<Vec<Self>> {
let centers_and_side: &[((f64, f64), f64)] = match n {
1 => &[((0.0, 0.0), 2.0)],
2 => &[((-0.4, 0.0), 1.0), ((0.4, 0.0), 1.0)],
3 => &[
((0.0, 0.36), 1.0),
((0.42, -0.36), 1.0),
((-0.42, -0.36), 1.0),
],
_ => return None,
};
Some(
centers_and_side
.iter()
.map(|&((x, y), s)| Square::new(Point::new(x, y), s))
.collect(),
)
}
}
impl Polygonize for Square {
fn polygonize(&self, _n_vertices: usize) -> Polygon {
let (x_min, x_max, y_min, y_max) = self.bounds();
Polygon::new(vec![
Point::new(x_min, y_min),
Point::new(x_max, y_min),
Point::new(x_max, y_max),
Point::new(x_min, y_max),
])
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-10;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn test_new_and_accessors() {
let s = Square::new(Point::new(1.0, 2.0), 3.0);
assert_eq!(s.center().x(), 1.0);
assert_eq!(s.center().y(), 2.0);
assert_eq!(s.side(), 3.0);
}
#[test]
fn test_area_perimeter_centroid() {
let s = Square::new(Point::new(0.0, 0.0), 4.0);
assert!(approx_eq(s.area(), 16.0));
assert!(approx_eq(s.perimeter(), 16.0));
assert_eq!(s.centroid(), Point::new(0.0, 0.0));
}
#[test]
fn test_bounds_and_bounding_box() {
let s = Square::new(Point::new(2.0, 3.0), 4.0);
let (x0, x1, y0, y1) = s.bounds();
assert!(approx_eq(x0, 0.0));
assert!(approx_eq(x1, 4.0));
assert!(approx_eq(y0, 1.0));
assert!(approx_eq(y1, 5.0));
let bb = s.bounding_box();
assert!(approx_eq(bb.area(), 16.0));
}
#[test]
fn test_contains_point_inside_outside_boundary() {
let s = Square::new(Point::new(0.0, 0.0), 2.0);
assert!(s.contains_point(&Point::new(0.0, 0.0)));
assert!(s.contains_point(&Point::new(1.0, 1.0))); assert!(s.contains_point(&Point::new(1.0, 0.0))); assert!(!s.contains_point(&Point::new(1.5, 0.0)));
}
#[test]
fn test_contains_square_strict_equal_partial() {
let outer = Square::new(Point::new(0.0, 0.0), 4.0);
let inner = Square::new(Point::new(0.0, 0.0), 2.0);
let same = Square::new(Point::new(0.0, 0.0), 4.0);
let partial = Square::new(Point::new(2.0, 0.0), 4.0);
assert!(outer.contains(&inner));
assert!(!inner.contains(&outer));
assert!(outer.contains(&same)); assert!(!outer.contains(&partial));
}
#[test]
fn test_intersects_disjoint_touching_partial_nested() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let disjoint = Square::new(Point::new(5.0, 0.0), 2.0);
let touching = Square::new(Point::new(2.0, 0.0), 2.0); let partial = Square::new(Point::new(1.0, 0.0), 2.0);
let nested = Square::new(Point::new(0.0, 0.0), 1.0);
assert!(!a.intersects(&disjoint));
assert!(a.intersects(&touching));
assert!(a.intersects(&partial));
assert!(a.intersects(&nested));
}
#[test]
fn test_intersection_area_disjoint_partial_nested() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let disjoint = Square::new(Point::new(5.0, 0.0), 2.0);
let partial = Square::new(Point::new(1.0, 0.0), 2.0); let nested = Square::new(Point::new(0.0, 0.0), 1.0);
assert!(approx_eq(a.intersection_area(&disjoint), 0.0));
assert!(approx_eq(a.intersection_area(&partial), 2.0));
assert!(approx_eq(a.intersection_area(&nested), 1.0));
}
#[test]
fn test_intersection_points_partial_overlap() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let b = Square::new(Point::new(2.0, 2.0), 2.0); let pts = a.intersection_points(&b);
assert!(!pts.is_empty());
}
#[test]
fn test_compute_exclusive_regions_two_disjoint() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let b = Square::new(Point::new(10.0, 0.0), 2.0);
let regions = Square::compute_exclusive_regions(&[a, b]);
assert!(approx_eq(regions[&0b01], 4.0));
assert!(approx_eq(regions[&0b10], 4.0));
assert_eq!(regions.get(&0b11).copied().unwrap_or(0.0), 0.0);
}
#[test]
fn test_compute_exclusive_regions_two_partial() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let b = Square::new(Point::new(1.0, 0.0), 2.0);
let regions = Square::compute_exclusive_regions(&[a, b]);
assert!(approx_eq(regions[&0b01], 2.0));
assert!(approx_eq(regions[&0b10], 2.0));
assert!(approx_eq(regions[&0b11], 2.0));
}
#[test]
fn test_compute_exclusive_regions_three_way_grid() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let b = Square::new(Point::new(1.0, 0.0), 2.0);
let c = Square::new(Point::new(0.5, 1.0), 2.0);
let regions = Square::compute_exclusive_regions(&[a, b, c]);
assert!(
approx_eq(regions[&0b111], 1.0),
"triple โฉ area = {}, expected 1.0",
regions[&0b111]
);
}
#[test]
fn test_compute_exclusive_regions_nested() {
let outer = Square::new(Point::new(0.0, 0.0), 4.0);
let inner = Square::new(Point::new(0.0, 0.0), 2.0);
let regions = Square::compute_exclusive_regions(&[outer, inner]);
assert!(approx_eq(regions[&0b11], 4.0)); assert!(approx_eq(regions[&0b01], 16.0 - 4.0)); assert_eq!(regions.get(&0b10).copied().unwrap_or(0.0), 0.0);
}
fn fd_exclusive_region_gradients(shapes: &[Square], h: f64) -> HashMap<RegionMask, Vec<f64>> {
let n_sets = shapes.len();
let n_params = n_sets * 3;
let base = Square::compute_exclusive_regions(shapes);
let mut grads: HashMap<RegionMask, Vec<f64>> =
base.keys().map(|&m| (m, vec![0.0; n_params])).collect();
for i in 0..n_sets {
for k in 0..3 {
let perturb = |delta: f64| -> HashMap<RegionMask, f64> {
let mut copy: Vec<Square> = shapes.to_vec();
let (cx, cy, side) =
(copy[i].center().x(), copy[i].center().y(), copy[i].side());
let new = match k {
0 => Square::new(Point::new(cx + delta, cy), side),
1 => Square::new(Point::new(cx, cy + delta), side),
2 => Square::new(Point::new(cx, cy), side + delta),
_ => unreachable!(),
};
copy[i] = new;
Square::compute_exclusive_regions(©)
};
let plus = perturb(h);
let minus = perturb(-h);
for (&mask, g) in grads.iter_mut() {
let p = plus.get(&mask).copied().unwrap_or(0.0);
let m = minus.get(&mask).copied().unwrap_or(0.0);
g[3 * i + k] = (p - m) / (2.0 * h);
}
}
}
grads
}
fn assert_grad_matches_fd(
analytical: &HashMap<RegionMask, Vec<f64>>,
fd: &HashMap<RegionMask, Vec<f64>>,
tol: f64,
) {
for (&mask, ag) in analytical.iter() {
let fg = fd
.get(&mask)
.expect("FD missing mask present in analytical");
assert_eq!(ag.len(), fg.len(), "param count mismatch for mask {mask:b}");
for (k, (&a, &f)) in ag.iter().zip(fg.iter()).enumerate() {
assert!(
(a - f).abs() < tol,
"mask {mask:b} param {k}: analytical={a} fd={f} (tol={tol})"
);
}
}
}
#[test]
fn test_gradient_single_square_matches_2s_on_side() {
let s = 3.0;
let sq = Square::new(Point::new(1.5, -2.0), s);
let (areas, grads) = Square::compute_exclusive_regions_with_gradient(&[sq]).unwrap();
assert!(approx_eq(areas[&0b1], s * s));
let g = &grads[&0b1];
assert_eq!(g.len(), 3);
assert!(approx_eq(g[0], 0.0));
assert!(approx_eq(g[1], 0.0));
assert!(approx_eq(g[2], 2.0 * s));
}
#[test]
fn test_gradient_two_squares_partial_overlap_matches_fd() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let b = Square::new(Point::new(1.0, 0.0), 2.0);
let (_, grads) = Square::compute_exclusive_regions_with_gradient(&[a, b]).unwrap();
let fd = fd_exclusive_region_gradients(&[a, b], 1e-6);
assert_grad_matches_fd(&grads, &fd, 1e-5);
}
#[test]
fn test_gradient_three_squares_overlap_matches_fd() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let b = Square::new(Point::new(1.0, 0.0), 2.0);
let c = Square::new(Point::new(0.5, 1.0), 2.0);
let (_, grads) = Square::compute_exclusive_regions_with_gradient(&[a, b, c]).unwrap();
let fd = fd_exclusive_region_gradients(&[a, b, c], 1e-6);
assert_grad_matches_fd(&grads, &fd, 1e-5);
}
#[test]
fn test_gradient_generic_no_ties_matches_fd_tightly() {
let a = Square::new(Point::new(0.0, 0.0), 2.3);
let b = Square::new(Point::new(1.1, 0.4), 1.7);
let c = Square::new(Point::new(0.6, 1.2), 2.1);
let (_, grads) = Square::compute_exclusive_regions_with_gradient(&[a, b, c]).unwrap();
let fd = fd_exclusive_region_gradients(&[a, b, c], 1e-5);
assert_grad_matches_fd(&grads, &fd, 1e-7);
}
#[test]
fn test_gradient_disjoint_pair_is_zero_on_intersection() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let b = Square::new(Point::new(10.0, 0.0), 2.0);
let (_, grads) = Square::compute_exclusive_regions_with_gradient(&[a, b]).unwrap();
if let Some(g) = grads.get(&0b11) {
for &v in g {
assert!(approx_eq(v, 0.0), "expected zero on disjoint pair, got {v}");
}
}
let fd = fd_exclusive_region_gradients(&[a, b], 1e-5);
for &mask in &[0b01_usize, 0b10_usize] {
let ag = grads.get(&mask).expect("singleton missing");
let fg = fd.get(&mask).expect("FD singleton missing");
for (k, (&a, &f)) in ag.iter().zip(fg.iter()).enumerate() {
assert!(
(a - f).abs() < 1e-6,
"mask {mask:b} param {k}: analytical={a} fd={f}"
);
}
}
}
#[test]
fn test_gradient_nested_matches_fd() {
let outer = Square::new(Point::new(0.0, 0.0), 4.0);
let inner = Square::new(Point::new(0.0, 0.0), 2.0);
let (areas, grads) =
Square::compute_exclusive_regions_with_gradient(&[outer, inner]).unwrap();
let fd = fd_exclusive_region_gradients(&[outer, inner], 1e-5);
assert!(approx_eq(areas[&0b11], 4.0));
assert!(approx_eq(areas[&0b01], 12.0));
assert_grad_matches_fd(&grads, &fd, 1e-7);
}
#[test]
fn test_params_round_trip() {
let s = Square::new(Point::new(1.5, -2.0), 3.5);
let p = s.to_params();
let back = Square::from_params(&p);
assert_eq!(s, back);
}
#[test]
fn test_params_from_circle_equal_area() {
let r = 2.0;
let p = Square::params_from_circle(0.0, 0.0, r);
let s = Square::from_params(&p);
assert!(approx_eq(s.area(), PI * r * r));
}
#[test]
fn test_mds_target_distance_zero_overlap_is_diagonal_tangency() {
let area = 1.0;
let d = Square::mds_target_distance(area, area, 0.0).unwrap();
let s = area.sqrt();
let expected = 2.0_f64.sqrt() * s;
assert!(approx_eq(d, expected), "d = {d}, expected {expected}");
}
#[test]
fn test_mds_target_distance_full_overlap_is_zero() {
let area = 4.0;
let full_overlap = area; let d = Square::mds_target_distance(area, area, full_overlap).unwrap();
assert!(approx_eq(d, 0.0));
}
#[test]
fn test_polygonize_returns_4_ccw_vertices_with_correct_area() {
let s = Square::new(Point::new(0.0, 0.0), 2.0);
let p = s.polygonize(0);
assert_eq!(p.vertices().len(), 4);
let v = p.vertices();
let mut shoelace = 0.0;
for i in 0..4 {
let j = (i + 1) % 4;
shoelace += v[i].x() * v[j].y() - v[j].x() * v[i].y();
}
assert!(approx_eq(0.5 * shoelace, 4.0));
}
#[test]
fn test_fitter_end_to_end_two_partial_overlap() {
use crate::{DiagramSpecBuilder, Fitter, InputType};
let spec = DiagramSpecBuilder::new()
.set("A", 4.0)
.set("B", 4.0)
.intersection(&["A", "B"], 2.0)
.input_type(InputType::Exclusive)
.build()
.unwrap();
let layout = Fitter::<Square>::new(&spec).seed(42).fit().unwrap();
let fitted = layout.fitted();
assert!(
fitted.values().all(|&v| v.is_finite()),
"non-finite fitted areas in {fitted:?}"
);
assert!(
layout.loss().is_finite(),
"non-finite loss {}",
layout.loss()
);
assert_eq!(layout.shapes().len(), 2);
}
#[test]
fn test_distance_between_squares() {
let a = Square::new(Point::new(0.0, 0.0), 2.0);
let b_overlap = Square::new(Point::new(1.0, 0.0), 2.0);
let b_far = Square::new(Point::new(5.0, 0.0), 2.0);
assert!(approx_eq(a.distance(&b_overlap), 0.0));
assert!(approx_eq(a.distance(&b_far), 3.0)); }
fn assert_square(actual: &Square, x: f64, y: f64, side: f64) {
assert!(
approx_eq(actual.center().x(), x),
"center.x: {} vs {}",
actual.center().x(),
x
);
assert!(
approx_eq(actual.center().y(), y),
"center.y: {} vs {}",
actual.center().y(),
y
);
assert!(
approx_eq(actual.side(), side),
"side: {} vs {}",
actual.side(),
side
);
}
#[test]
fn test_canonical_venn_layout_n1() {
let shapes = Square::canonical_venn_layout(1).unwrap();
assert_eq!(shapes.len(), 1);
assert_square(&shapes[0], 0.0, 0.0, 2.0);
}
#[test]
fn test_canonical_venn_layout_n2() {
let shapes = Square::canonical_venn_layout(2).unwrap();
assert_eq!(shapes.len(), 2);
assert_square(&shapes[0], -0.4, 0.0, 1.0);
assert_square(&shapes[1], 0.4, 0.0, 1.0);
}
#[test]
fn test_canonical_venn_layout_n3() {
let shapes = Square::canonical_venn_layout(3).unwrap();
assert_eq!(shapes.len(), 3);
assert_square(&shapes[0], 0.0, 0.36, 1.0);
assert_square(&shapes[1], 0.42, -0.36, 1.0);
assert_square(&shapes[2], -0.42, -0.36, 1.0);
}
#[test]
fn test_canonical_venn_layout_unsupported() {
assert!(Square::canonical_venn_layout(0).is_none());
assert!(Square::canonical_venn_layout(4).is_none());
assert!(Square::canonical_venn_layout(5).is_none());
}
}