use crate::fitter::clustering::{find_clusters, find_clusters_from_exclusive_regions};
use crate::fitter::packing::skyline_pack;
use crate::geometry::diagram::RegionMask;
use crate::geometry::primitives::{Bounds, Point};
use crate::geometry::shapes::Rectangle;
use crate::geometry::traits::DiagramShape;
use std::collections::HashMap;
use std::f64::consts::PI;
const CONTAINED_CHILD_OFFSET_FRACTION: f64 = 0.5;
pub fn normalize_layout<S>(shapes: &mut [S], padding_factor: f64)
where
S: DiagramShape + Clone,
{
normalize_layout_with_clusters::<S>(shapes, padding_factor, None);
}
pub fn normalize_layout_with_container<S>(shapes: &mut [S], container: &mut Rectangle)
where
S: DiagramShape + Clone,
{
let cx = container.center().x();
let cy = container.center().y();
if cx.abs() > 1e-12 || cy.abs() > 1e-12 {
for shape in shapes.iter_mut() {
*shape = translate_shape(shape, -cx, -cy);
}
*container = Rectangle::new(Point::new(0.0, 0.0), container.width(), container.height());
}
center_shapes_in_container(shapes, container);
}
fn center_shapes_in_container<S>(shapes: &mut [S], container: &Rectangle)
where
S: DiagramShape + Clone,
{
if shapes.is_empty() {
return;
}
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for shape in shapes.iter() {
let Bounds {
x_min: bx0,
x_max: bx1,
y_min: by0,
y_max: by1,
} = shape.bounds();
x_min = x_min.min(bx0);
x_max = x_max.max(bx1);
y_min = y_min.min(by0);
y_max = y_max.max(by1);
}
let half_w = container.width() / 2.0;
let half_h = container.height() / 2.0;
let tol = 1e-9 * half_w.max(half_h).max(1.0);
if x_min < -half_w - tol
|| x_max > half_w + tol
|| y_min < -half_h - tol
|| y_max > half_h + tol
{
return;
}
let dx = (x_min + x_max) / 2.0;
let dy = (y_min + y_max) / 2.0;
if dx.abs() < 1e-12 && dy.abs() < 1e-12 {
return;
}
for shape in shapes.iter_mut() {
*shape = translate_shape(shape, -dx, -dy);
}
}
pub fn normalize_layout_with_clusters<S>(
shapes: &mut [S],
padding_factor: f64,
exclusive_areas: Option<&HashMap<RegionMask, f64>>,
) where
S: DiagramShape + Clone,
{
if shapes.is_empty() {
return;
}
offset_contained_children(shapes, exclusive_areas);
let clusters = match exclusive_areas {
Some(areas) => {
let max_region = areas
.values()
.copied()
.fold(0.0_f64, |a, b| a.max(b.abs()))
.max(1.0);
let tol = 1e-10 * max_region;
find_clusters_from_exclusive_regions(shapes.len(), areas, tol)
}
None => find_clusters(shapes),
};
if clusters.len() == 1 {
let cluster = &clusters[0];
if cluster.len() > 1 {
rotate_cluster(shapes, cluster);
}
center_layout(shapes);
} else {
for cluster in &clusters {
if cluster.len() > 1 {
rotate_cluster(shapes, cluster);
}
}
pack_clusters(shapes, &clusters, padding_factor);
center_layout(shapes);
}
}
fn offset_contained_children<S>(
shapes: &mut [S],
exclusive_areas: Option<&HashMap<RegionMask, f64>>,
) where
S: DiagramShape + Clone,
{
let n = shapes.len();
if n < 2 {
return;
}
let owned;
let areas = match exclusive_areas {
Some(a) => a,
None => {
owned = S::compute_exclusive_regions(shapes);
&owned
}
};
let max_region = areas
.values()
.copied()
.fold(0.0_f64, |a, b| a.max(b.abs()))
.max(1.0);
let tol = 1e-10 * max_region;
let mut set_total = vec![0.0_f64; n];
for (&mask, &area) in areas {
if area <= 0.0 {
continue;
}
let mut bits = mask;
while bits != 0 {
let b = bits.trailing_zeros() as usize;
if b < n {
set_total[b] += area;
}
bits &= bits - 1;
}
}
let mut parent_of: Vec<Option<usize>> = vec![None; n];
let mut child_count: Vec<usize> = vec![0; n];
for (i, slot) in parent_of.iter_mut().enumerate() {
if let Some(p) = contained_child_parent(i, n, areas, tol) {
if set_total[i] < set_total[p] * (1.0 - 1e-6) {
*slot = Some(p);
child_count[p] += 1;
}
}
}
for (i, &maybe_parent) in parent_of.iter().enumerate() {
if let Some(p) = maybe_parent {
if child_count[p] == 1 {
reposition_contained_child(shapes, p, i);
}
}
}
}
fn contained_child_parent(
i: usize,
n: usize,
areas: &HashMap<RegionMask, f64>,
tol: f64,
) -> Option<usize> {
let bit_i: RegionMask = 1 << i;
if areas.get(&bit_i).copied().unwrap_or(0.0) > tol {
return None;
}
let mut other_bits: RegionMask = 0;
let mut saw_region = false;
for (&mask, &area) in areas {
if area <= tol || (mask & bit_i) == 0 {
continue;
}
saw_region = true;
other_bits |= mask & !bit_i;
}
if !saw_region || other_bits.count_ones() != 1 {
return None;
}
let p = other_bits.trailing_zeros() as usize;
if p >= n || p == i {
return None;
}
Some(p)
}
fn reposition_contained_child<S>(shapes: &mut [S], p: usize, i: usize)
where
S: DiagramShape + Clone,
{
let pc = shapes[p].centroid();
let cc = shapes[i].centroid();
let Bounds {
x_min,
x_max,
y_min,
y_max,
} = shapes[p].bounds();
let parent_scale = ((x_max - x_min).powi(2) + (y_max - y_min).powi(2)).sqrt();
let dx = cc.x() - pc.x();
let dy = cc.y() - pc.y();
let mag = (dx * dx + dy * dy).sqrt();
let (ux, uy) = if mag > 1e-9 * parent_scale.max(1.0) {
(dx / mag, dy / mag)
} else {
(1.0, 0.0)
};
let d_max = max_contained_offset(&shapes[p], &shapes[i], ux, uy);
if d_max <= 0.0 {
return;
}
let d = CONTAINED_CHILD_OFFSET_FRACTION * d_max;
let target_x = pc.x() + ux * d;
let target_y = pc.y() + uy * d;
let move_x = target_x - cc.x();
let move_y = target_y - cc.y();
if (move_x * move_x + move_y * move_y).sqrt() <= 1e-9 * d_max.max(1.0) {
return;
}
let moved = translate_shape(&shapes[i], move_x, move_y);
if !shapes[p].contains(&moved) {
return;
}
for (k, other) in shapes.iter().enumerate() {
if k == i || k == p {
continue;
}
if moved.intersects(other) || moved.contains(other) || other.contains(&moved) {
return;
}
}
shapes[i] = moved;
}
fn max_contained_offset<S>(parent: &S, child: &S, ux: f64, uy: f64) -> f64
where
S: DiagramShape + Clone,
{
let pc = parent.centroid();
let cc = child.centroid();
let contains_at = |d: f64| -> bool {
let tx = pc.x() + ux * d - cc.x();
let ty = pc.y() + uy * d - cc.y();
parent.contains(&translate_shape(child, tx, ty))
};
if !contains_at(0.0) {
return 0.0;
}
let Bounds {
x_min,
x_max,
y_min,
y_max,
} = parent.bounds();
let mut hi = ((x_max - x_min).powi(2) + (y_max - y_min).powi(2)).sqrt();
if hi <= 0.0 {
return 0.0;
}
let mut guard = 0;
while contains_at(hi) && guard < 8 {
hi *= 2.0;
guard += 1;
}
let mut lo = 0.0;
for _ in 0..48 {
let mid = 0.5 * (lo + hi);
if contains_at(mid) {
lo = mid;
} else {
hi = mid;
}
}
lo
}
fn rotate_cluster<S>(shapes: &mut [S], cluster: &[usize])
where
S: DiagramShape + Clone,
{
if cluster.len() < 2 {
return;
}
let idx0 = cluster[0];
let idx1 = cluster[1];
let c0 = shapes[idx0].centroid();
let c1 = shapes[idx1].centroid();
let dx = c1.x() - c0.x();
let dy = c1.y() - c0.y();
let theta = -dy.atan2(dx);
if theta.abs() > 1e-10 {
let pivot = c0;
for &idx in cluster {
shapes[idx] = rotate_shape(&shapes[idx], theta, &pivot);
}
}
let c0 = shapes[idx0].centroid();
let _c1 = shapes[idx1].centroid();
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for &idx in cluster {
let c = shapes[idx].centroid();
x_min = x_min.min(c.x());
x_max = x_max.max(c.x());
y_min = y_min.min(c.y());
y_max = y_max.max(c.y());
}
let x_center = (x_min + x_max) / 2.0;
let y_center = (y_min + y_max) / 2.0;
if c0.x() > x_center {
for &idx in cluster {
shapes[idx] = mirror_x_shape(&shapes[idx], x_center);
}
}
let c0 = shapes[idx0].centroid();
if c0.y() > y_center {
for &idx in cluster {
shapes[idx] = mirror_y_shape(&shapes[idx], y_center);
}
}
}
fn rotate_shape<S>(shape: &S, theta: f64, pivot: &Point) -> S
where
S: DiagramShape + Clone,
{
let params = shape.to_params();
let n = S::n_params();
if n >= 2 {
let x = params[0];
let y = params[1];
let dx = x - pivot.x();
let dy = y - pivot.y();
let cos_t = theta.cos();
let sin_t = theta.sin();
let new_x = pivot.x() + dx * cos_t - dy * sin_t;
let new_y = pivot.y() + dx * sin_t + dy * cos_t;
let mut new_params = params.clone();
new_params[0] = new_x;
new_params[1] = new_y;
if n >= 5 {
new_params[n - 1] = params[n - 1] + theta;
}
S::from_params(&new_params)
} else {
shape.clone()
}
}
fn mirror_x_shape<S>(shape: &S, x_center: f64) -> S
where
S: DiagramShape + Clone,
{
let params = shape.to_params();
let n = S::n_params();
if n >= 1 {
let mut new_params = params.clone();
new_params[0] = 2.0 * x_center - params[0];
if n >= 5 {
new_params[n - 1] = PI - params[n - 1]; }
S::from_params(&new_params)
} else {
shape.clone()
}
}
fn mirror_y_shape<S>(shape: &S, y_center: f64) -> S
where
S: DiagramShape + Clone,
{
let params = shape.to_params();
let n = S::n_params();
if n >= 2 {
let mut new_params = params.clone();
new_params[1] = 2.0 * y_center - params[1];
if n >= 5 {
new_params[n - 1] = PI - params[n - 1]; }
S::from_params(&new_params)
} else {
shape.clone()
}
}
fn pack_clusters<S>(shapes: &mut [S], clusters: &[Vec<usize>], padding_factor: f64)
where
S: DiagramShape + Clone,
{
if clusters.len() <= 1 {
return;
}
let mut cluster_boxes = Vec::new();
for cluster in clusters {
if cluster.is_empty() {
continue;
}
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for &idx in cluster {
let Bounds {
x_min: bx_min,
x_max: bx_max,
y_min: by_min,
y_max: by_max,
} = shapes[idx].bounds();
x_min = x_min.min(bx_min);
x_max = x_max.max(bx_max);
y_min = y_min.min(by_min);
y_max = y_max.max(by_max);
}
let width = x_max - x_min;
let height = y_max - y_min;
let center = Point::new((x_min + x_max) / 2.0, (y_min + y_max) / 2.0);
cluster_boxes.push((Rectangle::new(center, width, height), x_min, y_min));
}
let total_area: f64 = cluster_boxes
.iter()
.map(|(r, _, _)| r.width() * r.height())
.sum();
let padding = total_area.sqrt() * padding_factor;
let rectangles: Vec<Rectangle> = cluster_boxes.iter().map(|(r, _, _)| *r).collect();
let packed = skyline_pack(&rectangles, padding);
for (i, cluster) in clusters.iter().enumerate() {
if cluster.is_empty() {
continue;
}
let (old_box, _old_x_min, _old_y_min) = cluster_boxes[i];
let new_box = packed[i];
let old_center = old_box.center();
let new_center = new_box.center();
let dx = new_center.x() - old_center.x();
let dy = new_center.y() - old_center.y();
for &idx in cluster {
shapes[idx] = translate_shape(&shapes[idx], dx, dy);
}
}
}
fn translate_shape<S>(shape: &S, dx: f64, dy: f64) -> S
where
S: DiagramShape + Clone,
{
let params = shape.to_params();
let n = S::n_params();
if n >= 2 {
let mut new_params = params.clone();
new_params[0] += dx;
new_params[1] += dy;
S::from_params(&new_params)
} else {
shape.clone()
}
}
fn center_layout<S>(shapes: &mut [S])
where
S: DiagramShape + Clone,
{
if shapes.is_empty() {
return;
}
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for shape in shapes.iter() {
let Bounds {
x_min: bx_min,
x_max: bx_max,
y_min: by_min,
y_max: by_max,
} = shape.bounds();
x_min = x_min.min(bx_min);
x_max = x_max.max(bx_max);
y_min = y_min.min(by_min);
y_max = y_max.max(by_max);
}
let center_x = (x_min + x_max) / 2.0;
let center_y = (y_min + y_max) / 2.0;
for shape in shapes.iter_mut() {
*shape = translate_shape(shape, -center_x, -center_y);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::shapes::Circle;
use crate::geometry::traits::Centroid;
use crate::geometry::traits::Closed;
use crate::geometry::traits::DiagramShape;
const EPSILON: f64 = 1e-6;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn test_center_single_shape() {
let mut shapes = vec![Circle::new(Point::new(5.0, 3.0), 2.0)];
center_layout(&mut shapes);
let centroid = shapes[0].centroid();
assert!(approx_eq(centroid.x(), 0.0));
assert!(approx_eq(centroid.y(), 0.0));
}
#[test]
fn test_center_two_shapes() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 0.0), 1.0),
Circle::new(Point::new(10.0, 0.0), 1.0),
];
center_layout(&mut shapes);
assert!(approx_eq(shapes[0].centroid().x(), -5.0));
assert!(approx_eq(shapes[1].centroid().x(), 5.0));
assert!(approx_eq(shapes[0].centroid().y(), 0.0));
assert!(approx_eq(shapes[1].centroid().y(), 0.0));
}
#[test]
fn test_translate_shape() {
let shape = Circle::new(Point::new(1.0, 2.0), 3.0);
let translated = translate_shape(&shape, 4.0, 5.0);
assert!(approx_eq(translated.centroid().x(), 5.0));
assert!(approx_eq(translated.centroid().y(), 7.0));
assert_eq!(translated.radius(), 3.0);
}
#[test]
fn test_normalize_single_shape() {
let mut shapes = vec![Circle::new(Point::new(5.0, 3.0), 2.0)];
normalize_layout(&mut shapes, 0.015);
let centroid = shapes[0].centroid();
assert!(approx_eq(centroid.x(), 0.0));
assert!(approx_eq(centroid.y(), 0.0));
}
#[test]
fn test_normalize_two_overlapping() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 2.0), 2.0),
Circle::new(Point::new(3.0, 2.0), 2.0),
];
normalize_layout(&mut shapes, 0.015);
let c0 = shapes[0].centroid();
let c1 = shapes[1].centroid();
assert!(
approx_eq(c0.y(), c1.y()),
"Expected y coords to be equal, got {} and {}",
c0.y(),
c1.y()
);
let center_x = (c0.x() + c1.x()) / 2.0;
let center_y = (c0.y() + c1.y()) / 2.0;
assert!(approx_eq(center_x, 0.0));
assert!(approx_eq(center_y, 0.0));
assert!(c0.x() < c1.x());
}
#[test]
fn test_normalize_disjoint_clusters() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 0.0), 1.5),
Circle::new(Point::new(2.0, 0.0), 1.5),
Circle::new(Point::new(20.0, 0.0), 1.0),
Circle::new(Point::new(22.0, 0.0), 1.0),
];
normalize_layout(&mut shapes, 0.015);
let c0 = shapes[0].centroid();
let c1 = shapes[1].centroid();
let c2 = shapes[2].centroid();
let c3 = shapes[3].centroid();
assert!(
(c0.y() - c1.y()).abs() < 1e-3,
"Cluster 1 should be horizontal, got y diff: {}",
(c0.y() - c1.y()).abs()
);
let max_x = c0.x().max(c1.x()).max(c2.x()).max(c3.x());
let min_x = c0.x().min(c1.x()).min(c2.x()).min(c3.x());
let max_y = c0.y().max(c1.y()).max(c2.y()).max(c3.y());
let min_y = c0.y().min(c1.y()).min(c2.y()).min(c3.y());
let packed_width = max_x - min_x;
let _packed_height = max_y - min_y;
let original_bbox_width = 22.0 + 1.0 - (0.0 - 1.5);
assert!(
packed_width < original_bbox_width,
"Packed width {packed_width} should be less than original width {original_bbox_width}"
);
use crate::geometry::traits::BoundingBox;
let mut bb_x_min = f64::INFINITY;
let mut bb_x_max = f64::NEG_INFINITY;
let mut bb_y_min = f64::INFINITY;
let mut bb_y_max = f64::NEG_INFINITY;
for shape in &shapes {
let Bounds {
x_min: bx_min,
x_max: bx_max,
y_min: by_min,
y_max: by_max,
} = shape.bounds();
bb_x_min = bb_x_min.min(bx_min);
bb_x_max = bb_x_max.max(bx_max);
bb_y_min = bb_y_min.min(by_min);
bb_y_max = bb_y_max.max(by_max);
}
let bb_center_x = (bb_x_max + bb_x_min) / 2.0;
let bb_center_y = (bb_y_max + bb_y_min) / 2.0;
assert!(
(bb_center_x).abs() < 1e-6,
"Should be centered in x, got {bb_center_x}"
);
assert!(
(bb_center_y).abs() < 1e-6,
"Should be centered in y, got {bb_center_y}"
);
}
#[test]
fn test_normalize_with_container_centres_container() {
let mut shapes = vec![
Circle::new(Point::new(11.0, 6.0), 1.0),
Circle::new(Point::new(13.0, 6.0), 1.0),
];
let mut container = Rectangle::new(Point::new(12.0, 5.0), 6.0, 4.0);
normalize_layout_with_container(&mut shapes, &mut container);
assert!(approx_eq(container.center().x(), 0.0));
assert!(approx_eq(container.center().y(), 0.0));
assert!(approx_eq(container.width(), 6.0));
assert!(approx_eq(container.height(), 4.0));
let bb_cx = (shapes[0].centroid().x() + shapes[1].centroid().x()) / 2.0;
let bb_cy = shapes[0].centroid().y(); assert!(approx_eq(bb_cx, 0.0), "diagram not x-centred: {bb_cx}");
assert!(approx_eq(bb_cy, 0.0), "diagram not y-centred: {bb_cy}");
assert!(approx_eq(shapes[0].centroid().x(), -1.0));
assert!(approx_eq(shapes[1].centroid().x(), 1.0));
}
#[test]
fn test_normalize_with_container_centres_offset_diagram() {
let mut shapes = vec![Circle::new(Point::new(-2.0, -2.0), 1.0)];
let mut container = Rectangle::new(Point::new(0.0, 0.0), 10.0, 10.0);
normalize_layout_with_container(&mut shapes, &mut container);
assert!(approx_eq(shapes[0].centroid().x(), 0.0));
assert!(approx_eq(shapes[0].centroid().y(), 0.0));
assert!(approx_eq(container.center().x(), 0.0));
assert!(approx_eq(container.center().y(), 0.0));
}
#[test]
fn test_normalize_with_container_keeps_offset_when_shape_pokes_out() {
let mut shapes = vec![Circle::new(Point::new(3.0, 0.0), 2.0)];
let mut container = Rectangle::new(Point::new(2.0, 0.0), 4.0, 4.0);
normalize_layout_with_container(&mut shapes, &mut container);
assert!(approx_eq(shapes[0].centroid().x(), 1.0));
assert!(approx_eq(shapes[0].centroid().y(), 0.0));
assert!(approx_eq(container.center().x(), 0.0));
assert!(approx_eq(container.center().y(), 0.0));
}
#[test]
fn test_normalize_with_container_idempotent() {
let mut shapes = vec![Circle::new(Point::new(0.0, 0.0), 1.0)];
let mut container = Rectangle::new(Point::new(0.0, 0.0), 4.0, 3.0);
normalize_layout_with_container(&mut shapes, &mut container);
assert!(approx_eq(shapes[0].centroid().x(), 0.0));
assert!(approx_eq(shapes[0].centroid().y(), 0.0));
normalize_layout_with_container(&mut shapes, &mut container);
assert!(approx_eq(shapes[0].centroid().x(), 0.0));
assert!(approx_eq(shapes[0].centroid().y(), 0.0));
assert!(approx_eq(container.width(), 4.0));
assert!(approx_eq(container.height(), 3.0));
assert!(approx_eq(container.center().x(), 0.0));
assert!(approx_eq(container.center().y(), 0.0));
}
fn centroid_dist<S: DiagramShape>(a: &S, b: &S) -> f64 {
let (ca, cb) = (a.centroid(), b.centroid());
((ca.x() - cb.x()).powi(2) + (ca.y() - cb.y()).powi(2)).sqrt()
}
fn assert_regions_eq(before: &HashMap<RegionMask, f64>, after: &HashMap<RegionMask, f64>) {
let mut masks: Vec<_> = before.keys().chain(after.keys()).copied().collect();
masks.sort_unstable();
masks.dedup();
for mask in masks {
let lhs = before.get(&mask).copied().unwrap_or(0.0);
let rhs = after.get(&mask).copied().unwrap_or(0.0);
let scale = lhs.abs().max(rhs.abs()).max(1.0);
assert!(
(lhs - rhs).abs() <= 1e-8_f64.max(1e-6 * scale),
"region {mask} changed: {lhs} -> {rhs}"
);
}
}
#[test]
fn test_contained_child_offset_moderate() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 0.0), 5.0),
Circle::new(Point::new(1.0, 0.0), 1.0),
];
offset_contained_children(&mut shapes, None);
assert!(approx_eq(shapes[1].centroid().x(), 2.0));
assert!(approx_eq(shapes[1].centroid().y(), 0.0));
let d = centroid_dist(&shapes[0], &shapes[1]);
assert!(d > 1e-3, "should not be concentric, got {d}");
assert!(
shapes[0].contains(&shapes[1]),
"child must stay inside parent"
);
}
#[test]
fn test_contained_child_offset_preserves_regions() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 0.0), 5.0),
Circle::new(Point::new(-1.5, 0.7), 1.0),
];
let before = Circle::compute_exclusive_regions(&shapes);
offset_contained_children(&mut shapes, None);
let after = Circle::compute_exclusive_regions(&shapes);
assert_regions_eq(&before, &after);
assert!(approx_eq(centroid_dist(&shapes[0], &shapes[1]), 2.0));
}
#[test]
fn test_contained_child_near_concentric_fallback() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 0.0), 5.0),
Circle::new(Point::new(0.0, 0.0), 1.0),
];
offset_contained_children(&mut shapes, None);
assert!(approx_eq(shapes[1].centroid().x(), 2.0));
assert!(approx_eq(shapes[1].centroid().y(), 0.0));
}
#[test]
fn test_two_disjoint_children_not_moved() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 0.0), 10.0),
Circle::new(Point::new(-3.0, 0.0), 1.0),
Circle::new(Point::new(3.0, 0.0), 1.0),
];
let snapshot = shapes.clone();
offset_contained_children(&mut shapes, None);
for (a, b) in shapes.iter().zip(&snapshot) {
assert!(approx_eq(a.centroid().x(), b.centroid().x()));
assert!(approx_eq(a.centroid().y(), b.centroid().y()));
}
}
#[test]
fn test_nested_child_not_moved() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 0.0), 5.0), Circle::new(Point::new(0.5, 0.0), 2.5), Circle::new(Point::new(0.6, 0.0), 0.5), ];
let snapshot = shapes.clone();
offset_contained_children(&mut shapes, None);
for (a, b) in shapes.iter().zip(&snapshot) {
assert!(approx_eq(a.centroid().x(), b.centroid().x()));
assert!(approx_eq(a.centroid().y(), b.centroid().y()));
}
}
#[test]
fn test_coincident_equal_sets_not_moved() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 0.0), 2.0),
Circle::new(Point::new(0.0, 0.0), 2.0),
];
let before = Circle::compute_exclusive_regions(&shapes);
offset_contained_children(&mut shapes, None);
let after = Circle::compute_exclusive_regions(&shapes);
assert_regions_eq(&before, &after);
for s in &shapes {
assert!(approx_eq(s.centroid().x(), 0.0));
assert!(approx_eq(s.centroid().y(), 0.0));
}
}
#[test]
fn test_plain_overlap_unaffected() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 0.0), 2.0),
Circle::new(Point::new(2.0, 0.0), 2.0),
];
let snapshot = shapes.clone();
offset_contained_children(&mut shapes, None);
for (a, b) in shapes.iter().zip(&snapshot) {
assert!(approx_eq(a.centroid().x(), b.centroid().x()));
assert!(approx_eq(a.centroid().y(), b.centroid().y()));
}
}
#[test]
fn test_normalize_preserves_exclusive_regions() {
let mut shapes = vec![
Circle::new(Point::new(0.0, 1.0), 3.0),
Circle::new(Point::new(2.5, -0.5), 2.5),
Circle::new(Point::new(12.0, 8.0), 1.5),
];
let before = Circle::compute_exclusive_regions(&shapes);
normalize_layout(&mut shapes, 0.015);
let after = Circle::compute_exclusive_regions(&shapes);
let mut all_masks: Vec<_> = before.keys().chain(after.keys()).copied().collect();
all_masks.sort_unstable();
all_masks.dedup();
for mask in all_masks {
let lhs = before.get(&mask).copied().unwrap_or(0.0);
let rhs = after.get(&mask).copied().unwrap_or(0.0);
let scale = lhs.abs().max(rhs.abs()).max(1.0);
assert!(
(lhs - rhs).abs() <= 1e-8_f64.max(1e-6 * scale),
"mask {mask:b}: before={lhs:e}, after={rhs:e}"
);
}
}
}