use std::collections::HashMap;
use std::f64::consts::PI;
use crate::geometry::diagram::{
IntersectionPoint, RegionMask, discover_regions, mask_to_indices, to_exclusive_areas,
};
use crate::geometry::primitives::{Bounds, Point};
use crate::geometry::shapes::Polygon;
use crate::geometry::shapes::Rectangle;
use crate::geometry::shapes::polygon::shoelace_area;
use crate::geometry::traits::{
Area, BoundingBox, Centroid, Closed, DiagramShape, Distance, Perimeter, Polygonize,
};
const CONTAINS_EPS: f64 = 1e-9;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RotatedRectangle {
center: Point,
width: f64,
height: f64,
rotation: f64,
}
#[allow(dead_code)]
impl RotatedRectangle {
pub fn new(center: Point, width: f64, height: f64, rotation: f64) -> Self {
RotatedRectangle {
center,
width,
height,
rotation,
}
}
pub fn try_new(
center: Point,
width: f64,
height: f64,
rotation: f64,
) -> Result<Self, crate::error::DiagramError> {
if width <= 0.0 {
return Err(crate::error::DiagramError::InvalidShapeParameter {
shape: "RotatedRectangle",
param: "width",
value: width,
});
}
if height <= 0.0 {
return Err(crate::error::DiagramError::InvalidShapeParameter {
shape: "RotatedRectangle",
param: "height",
value: height,
});
}
Ok(RotatedRectangle {
center,
width,
height,
rotation,
})
}
pub fn center(&self) -> &Point {
&self.center
}
pub fn width(&self) -> f64 {
self.width
}
pub fn height(&self) -> f64 {
self.height
}
pub fn rotation(&self) -> f64 {
self.rotation
}
pub fn set_center(&mut self, center: Point) {
self.center = center;
}
pub fn corners(&self) -> [Point; 4] {
let cos = self.rotation.cos();
let sin = self.rotation.sin();
let hw = self.width / 2.0;
let hh = self.height / 2.0;
let local = [(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)];
local.map(|(lx, ly)| {
Point::new(
self.center.x() + lx * cos - ly * sin,
self.center.y() + lx * sin + ly * cos,
)
})
}
}
impl Area for RotatedRectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Perimeter for RotatedRectangle {
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
}
impl BoundingBox for RotatedRectangle {
fn bounds(&self) -> Bounds {
let cos = self.rotation.cos().abs();
let sin = self.rotation.sin().abs();
let hw = self.width / 2.0;
let hh = self.height / 2.0;
let half_width = hw * cos + hh * sin;
let half_height = hw * sin + hh * cos;
Bounds::new(
self.center.x() - half_width,
self.center.x() + half_width,
self.center.y() - half_height,
self.center.y() + half_height,
)
}
}
impl Centroid for RotatedRectangle {
fn centroid(&self) -> Point {
self.center
}
}
impl Distance for RotatedRectangle {
fn distance(&self, other: &Self) -> f64 {
if self.intersects(other) {
return 0.0;
}
let a = self.corners();
let b = other.corners();
let mut min_d = f64::INFINITY;
for i in 0..4 {
let a1 = a[i];
let a2 = a[(i + 1) % 4];
for j in 0..4 {
let b1 = b[j];
let b2 = b[(j + 1) % 4];
min_d = min_d.min(segment_segment_distance(a1, a2, b1, b2));
}
}
min_d
}
}
#[allow(dead_code)]
impl Closed for RotatedRectangle {
fn contains(&self, other: &Self) -> bool {
other.corners().iter().all(|p| self.contains_point(p))
}
fn contains_point(&self, point: &Point) -> bool {
let dx = point.x() - self.center.x();
let dy = point.y() - self.center.y();
let cos = self.rotation.cos();
let sin = self.rotation.sin();
let x_local = dx * cos + dy * sin;
let y_local = -dx * sin + dy * cos;
x_local.abs() <= self.width / 2.0 + CONTAINS_EPS
&& y_local.abs() <= self.height / 2.0 + CONTAINS_EPS
}
fn intersects(&self, other: &Self) -> bool {
let a = self.corners();
let b = other.corners();
!(has_separating_axis(&a, &b) || has_separating_axis(&b, &a))
}
fn intersection_area(&self, other: &Self) -> f64 {
let clipped = clip_convex(&self.corners(), &other.corners());
shoelace_area(&clipped)
}
fn intersection_points(&self, other: &Self) -> Vec<Point> {
let clipped = clip_convex(&self.corners(), &other.corners());
if shoelace_area(&clipped) <= CONTAINS_EPS {
return vec![];
}
clipped
}
}
#[inline]
fn cross(a: Point, b: Point, p: Point) -> f64 {
(b.x() - a.x()) * (p.y() - a.y()) - (b.y() - a.y()) * (p.x() - a.x())
}
fn line_segment_intersection(p1: Point, p2: Point, a: Point, b: Point) -> Option<Point> {
let dx = p2.x() - p1.x();
let dy = p2.y() - p1.y();
let sx = b.x() - a.x();
let sy = b.y() - a.y();
let denom = dx * sy - dy * sx;
if denom.abs() < 1e-15 {
return None;
}
let t = ((a.x() - p1.x()) * sy - (a.y() - p1.y()) * sx) / denom;
Some(Point::new(p1.x() + t * dx, p1.y() + t * dy))
}
fn clip_convex(subject: &[Point], clip: &[Point]) -> Vec<Point> {
if subject.len() < 3 || clip.len() < 3 {
return Vec::new();
}
let mut output = subject.to_vec();
let n = clip.len();
for e in 0..n {
if output.is_empty() {
break;
}
let a = clip[e];
let b = clip[(e + 1) % n];
let input = std::mem::take(&mut output);
let m = input.len();
for j in 0..m {
let prev = input[(j + m - 1) % m];
let cur = input[j];
let cur_in = cross(a, b, cur) >= 0.0;
let prev_in = cross(a, b, prev) >= 0.0;
if cur_in {
if !prev_in && let Some(ip) = line_segment_intersection(prev, cur, a, b) {
output.push(ip);
}
output.push(cur);
} else if prev_in && let Some(ip) = line_segment_intersection(prev, cur, a, b) {
output.push(ip);
}
}
}
output
}
fn has_separating_axis(poly: &[Point], other: &[Point]) -> bool {
let n = poly.len();
for i in 0..n {
let a = poly[i];
let b = poly[(i + 1) % n];
let nx = -(b.y() - a.y());
let ny = b.x() - a.x();
let (mut min1, mut max1) = (f64::INFINITY, f64::NEG_INFINITY);
for p in poly {
let d = p.x() * nx + p.y() * ny;
min1 = min1.min(d);
max1 = max1.max(d);
}
let (mut min2, mut max2) = (f64::INFINITY, f64::NEG_INFINITY);
for p in other {
let d = p.x() * nx + p.y() * ny;
min2 = min2.min(d);
max2 = max2.max(d);
}
if max1 < min2 || max2 < min1 {
return true;
}
}
false
}
fn point_segment_distance(p: Point, a: Point, b: Point) -> f64 {
let dx = b.x() - a.x();
let dy = b.y() - a.y();
let len2 = dx * dx + dy * dy;
if len2 <= f64::MIN_POSITIVE {
return p.distance(&a);
}
let t = (((p.x() - a.x()) * dx + (p.y() - a.y()) * dy) / len2).clamp(0.0, 1.0);
let proj = Point::new(a.x() + t * dx, a.y() + t * dy);
p.distance(&proj)
}
fn segment_segment_distance(a1: Point, a2: Point, b1: Point, b2: Point) -> f64 {
point_segment_distance(a1, b1, b2)
.min(point_segment_distance(a2, b1, b2))
.min(point_segment_distance(b1, a1, a2))
.min(point_segment_distance(b2, a1, a2))
}
fn intersection_area_of(shapes: &[RotatedRectangle], indices: &[usize]) -> f64 {
let Some((&first, rest)) = indices.split_first() else {
return 0.0;
};
let mut poly = shapes[first].corners().to_vec();
for &i in rest {
poly = clip_convex(&poly, &shapes[i].corners());
if poly.is_empty() {
return 0.0;
}
}
shoelace_area(&poly)
}
fn collect_intersections_rotated_rectangle(
rects: &[RotatedRectangle],
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 = rects[i].intersection_points(&rects[j]);
for point in pts {
let mut adopters = vec![i, j];
for (k, r) in rects.iter().enumerate().take(n_sets) {
if k != i && k != j && r.contains_point(&point) {
adopters.push(k);
}
}
adopters.sort_unstable();
intersections.push(IntersectionPoint::new(point, (i, j), adopters));
}
}
}
intersections
}
pub(crate) fn compute_exclusive_regions_clipped_rotated_rectangles(
shapes: &[RotatedRectangle],
container: &Rectangle,
) -> HashMap<RegionMask, f64> {
let n_sets = shapes.len();
let intersections = collect_intersections_rotated_rectangle(shapes, n_sets);
let regions = discover_regions(shapes, &intersections, n_sets);
let container_poly = container.corners().to_vec();
let mut overlapping_areas: HashMap<RegionMask, f64> = HashMap::new();
overlapping_areas.insert(0, container.area());
for &mask in ®ions {
let indices = mask_to_indices(mask, n_sets);
let mut poly = container_poly.clone();
for &i in &indices {
poly = clip_convex(&poly, &shapes[i].corners());
if poly.is_empty() {
break;
}
}
overlapping_areas.insert(mask, shoelace_area(&poly));
}
to_exclusive_areas(&overlapping_areas)
}
const VENN_N4: [(f64, f64, f64, f64, f64); 4] = [
(-0.8, 0.0, 2.4, 4.0, PI / 4.0),
(0.8, 0.0, 2.4, 4.0, -PI / 4.0),
(0.0, 1.0, 2.4, 4.0, PI / 4.0),
(0.0, 1.0, 2.4, 4.0, -PI / 4.0),
];
impl Polygonize for RotatedRectangle {
fn polygonize(&self, _n_vertices: usize) -> Polygon {
Polygon::new(self.corners().to_vec())
}
}
impl DiagramShape for RotatedRectangle {
fn compute_exclusive_regions(shapes: &[Self]) -> HashMap<RegionMask, f64> {
let n_sets = shapes.len();
let intersections = collect_intersections_rotated_rectangle(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);
overlapping_areas.insert(mask, intersection_area_of(shapes, &indices));
}
to_exclusive_areas(&overlapping_areas)
}
fn optimizer_params_from_circle(x: f64, y: f64, radius: f64) -> Vec<f64> {
let u = PI.ln() + 2.0 * radius.ln();
vec![x, y, u, 0.0, 0.0]
}
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 {
5 }
fn from_params(params: &[f64]) -> Self {
debug_assert_eq!(
params.len(),
5,
"RotatedRectangle requires 5 parameters: x, y, width, height, rotation"
);
RotatedRectangle::new(
Point::new(params[0], params[1]),
params[2].max(f64::MIN_POSITIVE),
params[3].max(f64::MIN_POSITIVE),
params[4],
)
}
fn to_params(&self) -> Vec<f64> {
vec![
self.center.x(),
self.center.y(),
self.width,
self.height,
self.rotation,
]
}
fn from_optimizer_params(params: &[f64]) -> Self {
debug_assert_eq!(
params.len(),
5,
"RotatedRectangle optimizer params: x, y, ln(area), ln(ratio), rotation"
);
let u = params[2];
let v = params[3];
let w = ((u + v) * 0.5).exp();
let h = ((u - v) * 0.5).exp();
let w = if w.is_finite() && w > 0.0 {
w
} else {
f64::MIN_POSITIVE
};
let h = if h.is_finite() && h > 0.0 {
h
} else {
f64::MIN_POSITIVE
};
RotatedRectangle::new(Point::new(params[0], params[1]), w, h, params[4])
}
fn to_optimizer_params(&self) -> Vec<f64> {
let u = (self.width * self.height).ln();
let v = (self.width / self.height).ln();
vec![self.center.x(), self.center.y(), u, v, self.rotation]
}
fn compute_exclusive_regions_clipped(
shapes: &[Self],
container: &Rectangle,
) -> Option<HashMap<RegionMask, f64>> {
Some(compute_exclusive_regions_clipped_rotated_rectangles(
shapes, container,
))
}
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),
],
4 => {
return Some(
VENN_N4
.iter()
.map(|&(x, y, w, h, phi)| {
RotatedRectangle::new(Point::new(x, y), w, h, phi)
})
.collect(),
);
}
_ => return None,
};
Some(
centers_and_side
.iter()
.map(|&((x, y), s)| RotatedRectangle::new(Point::new(x, y), s, s, 0.0))
.collect(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::FRAC_PI_2;
const EPS: f64 = 1e-9;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < EPS
}
#[test]
fn area_and_perimeter() {
let r = RotatedRectangle::new(Point::new(0.0, 0.0), 4.0, 3.0, 0.7);
assert!(approx(r.area(), 12.0));
assert!(approx(r.perimeter(), 14.0));
}
#[test]
fn corners_unrotated_match_axis_aligned() {
let r = RotatedRectangle::new(Point::new(0.0, 0.0), 2.0, 2.0, 0.0);
let c = r.corners();
let expected = [
Point::new(-1.0, -1.0),
Point::new(1.0, -1.0),
Point::new(1.0, 1.0),
Point::new(-1.0, 1.0),
];
for (got, want) in c.iter().zip(expected.iter()) {
assert!(approx(got.x(), want.x()) && approx(got.y(), want.y()));
}
}
#[test]
fn bounds_90_degrees_swaps_extents() {
let r = RotatedRectangle::new(Point::new(0.0, 0.0), 4.0, 2.0, FRAC_PI_2);
let b = r.bounds();
assert!(approx(b.x_max - b.x_min, 2.0));
assert!(approx(b.y_max - b.y_min, 4.0));
}
#[test]
fn bounds_45_degrees() {
let r = RotatedRectangle::new(Point::new(0.0, 0.0), 1.0, 1.0, std::f64::consts::FRAC_PI_4);
let b = r.bounds();
let diag = 2.0_f64.sqrt();
assert!(approx(b.x_max - b.x_min, diag));
assert!(approx(b.y_max - b.y_min, diag));
}
#[test]
fn contains_point_rotated_frame() {
let r = RotatedRectangle::new(Point::new(0.0, 0.0), 4.0, 2.0, FRAC_PI_2);
assert!(r.contains_point(&Point::new(0.0, 1.9)));
assert!(!r.contains_point(&Point::new(1.9, 0.0)));
assert!(r.contains_point(&Point::new(0.9, 0.0)));
}
#[test]
fn contains_self_and_smaller() {
let big = RotatedRectangle::new(Point::new(0.0, 0.0), 4.0, 4.0, 0.6);
let small = RotatedRectangle::new(Point::new(0.0, 0.0), 1.0, 1.0, 0.6);
assert!(big.contains(&big));
assert!(big.contains(&small));
assert!(!small.contains(&big));
}
fn rect(cx: f64, cy: f64, w: f64, h: f64) -> Rectangle {
Rectangle::new(Point::new(cx, cy), w, h)
}
fn rrect(cx: f64, cy: f64, w: f64, h: f64) -> RotatedRectangle {
RotatedRectangle::new(Point::new(cx, cy), w, h, 0.0)
}
#[test]
fn unrotated_intersection_area_matches_rectangle() {
let cases = [
((0.0, 0.0, 4.0, 4.0), (2.0, 0.0, 4.0, 4.0)),
((0.0, 0.0, 10.0, 10.0), (1.0, 0.0, 4.0, 4.0)),
((0.0, 0.0, 2.0, 2.0), (10.0, 0.0, 2.0, 2.0)),
((0.0, 0.0, 4.0, 3.0), (1.5, 0.5, 3.0, 2.0)),
];
for (a, b) in cases {
let ra = rect(a.0, a.1, a.2, a.3);
let rb = rect(b.0, b.1, b.2, b.3);
let rra = rrect(a.0, a.1, a.2, a.3);
let rrb = rrect(b.0, b.1, b.2, b.3);
assert!(
approx(rra.intersection_area(&rrb), ra.intersection_area(&rb)),
"mismatch for {a:?} vs {b:?}: {} != {}",
rra.intersection_area(&rrb),
ra.intersection_area(&rb)
);
}
}
#[test]
fn unrotated_exclusive_regions_match_rectangle() {
let rects = [
rect(0.0, 0.0, 4.0, 4.0),
rect(2.0, 0.0, 4.0, 4.0),
rect(1.0, 2.0, 4.0, 4.0),
];
let rrects = [
rrect(0.0, 0.0, 4.0, 4.0),
rrect(2.0, 0.0, 4.0, 4.0),
rrect(1.0, 2.0, 4.0, 4.0),
];
let want = Rectangle::compute_exclusive_regions(&rects);
let got = RotatedRectangle::compute_exclusive_regions(&rrects);
assert_eq!(want.len(), got.len());
for (mask, w) in &want {
assert!(
approx(*got.get(mask).unwrap_or(&f64::NAN), *w),
"region {mask:b}: {:?} != {w}",
got.get(mask)
);
}
}
#[test]
fn diamond_inside_square_full_overlap() {
let big = RotatedRectangle::new(Point::new(0.0, 0.0), 10.0, 10.0, 0.0);
let diamond =
RotatedRectangle::new(Point::new(0.0, 0.0), 2.0, 2.0, std::f64::consts::FRAC_PI_4);
assert!(approx(big.intersection_area(&diamond), diamond.area()));
}
#[test]
fn disjoint_rotated_boxes_zero_overlap() {
let a = RotatedRectangle::new(Point::new(0.0, 0.0), 2.0, 2.0, 0.5);
let b = RotatedRectangle::new(Point::new(10.0, 0.0), 2.0, 2.0, 0.5);
assert!(!a.intersects(&b));
assert!(approx(a.intersection_area(&b), 0.0));
assert!(a.distance(&b) > 0.0);
}
#[test]
fn rotation_invariance_of_overlap() {
let theta = 0.9_f64;
let a0 = RotatedRectangle::new(Point::new(0.0, 0.0), 4.0, 2.0, 0.0);
let b0 = RotatedRectangle::new(Point::new(1.5, 0.0), 3.0, 2.0, 0.0);
let base = a0.intersection_area(&b0);
let (c, s) = (theta.cos(), theta.sin());
let rot = |p: &Point| Point::new(p.x() * c - p.y() * s, p.x() * s + p.y() * c);
let a1 = RotatedRectangle::new(rot(a0.center()), 4.0, 2.0, theta);
let b1 = RotatedRectangle::new(rot(b0.center()), 3.0, 2.0, theta);
assert!(approx(a1.intersection_area(&b1), base));
}
#[test]
fn geometric_params_round_trip() {
let r = RotatedRectangle::new(Point::new(1.5, -2.0), 3.0, 4.0, 0.7);
let p = r.to_params();
assert_eq!(p, vec![1.5, -2.0, 3.0, 4.0, 0.7]);
assert_eq!(RotatedRectangle::from_params(&p), r);
}
#[test]
fn optimizer_params_round_trip() {
let r = RotatedRectangle::new(Point::new(1.5, -2.0), 3.0, 4.0, -0.4);
let p = r.to_optimizer_params();
let back = RotatedRectangle::from_optimizer_params(&p);
assert!(approx(back.center().x(), 1.5));
assert!(approx(back.center().y(), -2.0));
assert!(approx(back.width(), 3.0));
assert!(approx(back.height(), 4.0));
assert!(approx(back.rotation(), -0.4));
}
#[test]
fn optimizer_params_from_circle_equal_area() {
let radius = 2.0;
let p = RotatedRectangle::optimizer_params_from_circle(0.0, 0.0, radius);
assert!(approx(p[2], (PI * radius * radius).ln()));
assert!(approx(p[3], 0.0));
assert!(approx(p[4], 0.0));
}
#[test]
fn try_new_rejects_nonpositive() {
assert!(RotatedRectangle::try_new(Point::new(0.0, 0.0), 0.0, 1.0, 0.0).is_err());
assert!(RotatedRectangle::try_new(Point::new(0.0, 0.0), 1.0, -1.0, 0.0).is_err());
assert!(RotatedRectangle::try_new(Point::new(0.0, 0.0), 1.0, 1.0, 0.3).is_ok());
}
#[test]
fn canonical_venn_layouts() {
for n in 1..=3 {
let layout = RotatedRectangle::canonical_venn_layout(n).unwrap();
assert_eq!(layout.len(), n);
assert!(layout.iter().all(|r| r.rotation() == 0.0));
}
let layout4 = RotatedRectangle::canonical_venn_layout(4).unwrap();
assert_eq!(layout4.len(), 4);
assert!(layout4.iter().any(|r| r.rotation() != 0.0));
assert!(RotatedRectangle::canonical_venn_layout(5).is_none());
}
#[test]
fn clipped_complement_runs() {
let shapes = [rrect(-0.5, 0.0, 1.0, 1.0), rrect(0.5, 0.0, 1.0, 1.0)];
let container = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0);
let regions =
RotatedRectangle::compute_exclusive_regions_clipped(&shapes, &container).unwrap();
let complement = regions[&0];
let union: f64 = regions
.iter()
.filter(|(m, _)| **m != 0)
.map(|(_, a)| a)
.sum();
assert!(approx(complement + union, container.area()));
}
#[test]
fn capability_flag_is_derivative_free() {
assert_ne!(
RotatedRectangle::SUPPORTS_ANALYTIC_GRADIENT,
Rectangle::SUPPORTS_ANALYTIC_GRADIENT
);
assert!(RotatedRectangle::compute_exclusive_regions_with_gradient(&[]).is_none());
}
#[test]
fn fit_two_set_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::<RotatedRectangle>::new(&spec)
.seed(7)
.fit()
.unwrap();
assert_eq!(layout.shapes().len(), 2);
assert!(layout.loss().is_finite() && layout.loss() < 0.05);
assert!(layout.fitted().values().all(|v| v.is_finite()));
}
#[test]
fn fit_three_set() {
use crate::{DiagramSpecBuilder, Fitter, InputType};
let spec = DiagramSpecBuilder::new()
.set("A", 6.0)
.set("B", 6.0)
.set("C", 6.0)
.intersection(&["A", "B"], 2.0)
.intersection(&["A", "C"], 2.0)
.intersection(&["B", "C"], 2.0)
.intersection(&["A", "B", "C"], 1.0)
.input_type(InputType::Exclusive)
.build()
.unwrap();
let layout = Fitter::<RotatedRectangle>::new(&spec)
.seed(7)
.fit()
.unwrap();
assert_eq!(layout.shapes().len(), 3);
assert!(layout.loss().is_finite());
assert!(layout.fitted().values().all(|v| v.is_finite()));
}
#[test]
fn fit_with_complement_runs_derivative_free() {
use crate::{DiagramSpecBuilder, Fitter, InputType};
let spec = DiagramSpecBuilder::new()
.set("A", 3.0)
.set("B", 3.0)
.intersection(&["A", "B"], 1.0)
.complement(10.0)
.input_type(InputType::Exclusive)
.build()
.unwrap();
let layout = Fitter::<RotatedRectangle>::new(&spec)
.seed(7)
.fit()
.unwrap();
assert_eq!(layout.shapes().len(), 2);
assert!(layout.loss().is_finite());
}
}