use crate::geometry::primitives::Point;
use crate::geometry::shapes::{Polygon, Rectangle};
use crate::plotting::regions::{poi_with_holes, signed_clearance, RegionPiece};
pub fn largest_inscribed_rect(
pieces: &[RegionPiece],
aspect_ratio: f64,
precision: f64,
) -> Option<(Rectangle, f64)> {
if aspect_ratio <= 0.0 || pieces.is_empty() {
return None;
}
let (centre, r) = poi_with_holes(pieces, precision)?;
if r <= 0.0 {
return None;
}
let inv = 1.0 / (1.0 + aspect_ratio * aspect_ratio).sqrt();
let half_w = r * aspect_ratio * inv;
let half_h = r * inv;
let rect = Rectangle::new(centre, 2.0 * half_w, 2.0 * half_h);
let mut winning: Option<&RegionPiece> = None;
let mut best_d = 0.0;
for piece in pieces {
let d = signed_clearance(centre.x(), centre.y(), piece);
if d > best_d {
best_d = d;
winning = Some(piece);
}
}
let winning = winning?;
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for p in winning.outer.vertices() {
min_x = min_x.min(p.x());
max_x = max_x.max(p.x());
min_y = min_y.min(p.y());
max_y = max_y.max(p.y());
}
let bbox_short = (max_x - min_x).min(max_y - min_y);
let achieved_short = (2.0 * half_w).min(2.0 * half_h);
let score = if bbox_short > 0.0 {
(achieved_short / bbox_short).clamp(0.0, 1.0)
} else {
0.0
};
Some((rect, score))
}
pub fn principal_axis(piece: &RegionPiece) -> (f64, f64) {
fn moments(ring: &Polygon) -> (f64, f64, f64, f64, f64, f64) {
let v = ring.vertices();
let n = v.len();
if n < 3 {
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
}
let mut a2 = 0.0; let mut mx6 = 0.0; let mut my6 = 0.0; let mut mxx12 = 0.0; let mut myy12 = 0.0; let mut mxy24 = 0.0; for i in 0..n {
let j = (i + 1) % n;
let (xi, yi) = (v[i].x(), v[i].y());
let (xj, yj) = (v[j].x(), v[j].y());
let cross = xi * yj - xj * yi;
a2 += cross;
mx6 += (xi + xj) * cross;
my6 += (yi + yj) * cross;
mxx12 += (xi * xi + xi * xj + xj * xj) * cross;
myy12 += (yi * yi + yi * yj + yj * yj) * cross;
mxy24 += (xi * yj + 2.0 * xi * yi + 2.0 * xj * yj + xj * yi) * cross;
}
(
a2 / 2.0,
mx6 / 6.0,
my6 / 6.0,
mxx12 / 12.0,
myy12 / 12.0,
mxy24 / 24.0,
)
}
let (mut a, mut mx, mut my, mut mxx, mut myy, mut mxy) = moments(&piece.outer);
for h in &piece.holes {
let (a_h, mx_h, my_h, mxx_h, myy_h, mxy_h) = moments(h);
a += a_h;
mx += mx_h;
my += my_h;
mxx += mxx_h;
myy += myy_h;
mxy += mxy_h;
}
if a <= 1e-12 {
return (0.0, 1.0);
}
let cx = mx / a;
let cy = my / a;
let mu_xx = mxx / a - cx * cx;
let mu_yy = myy / a - cy * cy;
let mu_xy = mxy / a - cx * cy;
let trace = mu_xx + mu_yy;
let disc = ((mu_xx - mu_yy).powi(2) + 4.0 * mu_xy * mu_xy)
.max(0.0)
.sqrt();
let lambda_max = 0.5 * (trace + disc);
let lambda_min = 0.5 * (trace - disc);
if lambda_max <= 0.0 || lambda_min <= 0.0 {
return (0.0, 1.0);
}
let elongation = (lambda_max / lambda_min).sqrt();
let angle = 0.5 * (2.0 * mu_xy).atan2(mu_xx - mu_yy);
(angle, elongation)
}
pub fn fit_label_in_region(
pieces: &[RegionPiece],
w: f64,
h: f64,
precision: f64,
) -> Option<Point> {
if !(w.is_finite() && h.is_finite()) || w <= 0.0 || h <= 0.0 {
return None;
}
let (rect, _score) = largest_inscribed_rect(pieces, w / h, precision)?;
let eps = 1e-9 * w.max(h);
if rect.width() + eps >= w && rect.height() + eps >= h {
Some(*rect.center())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::primitives::Point;
use crate::geometry::shapes::{Circle, Ellipse, Polygon};
use crate::geometry::traits::Polygonize;
fn axis_aligned_square_piece(side: f64) -> RegionPiece {
let s = side;
RegionPiece {
outer: Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(s, 0.0),
Point::new(s, s),
Point::new(0.0, s),
]),
holes: vec![],
}
}
#[test]
fn test_inscribed_square_aspect_1() {
let pieces = vec![axis_aligned_square_piece(10.0)];
let (rect, score) = largest_inscribed_rect(&pieces, 1.0, 0.01).unwrap();
let expected = 10.0 / std::f64::consts::SQRT_2;
assert!(
(rect.width() - expected).abs() < 0.1,
"width = {}",
rect.width()
);
assert!(
(rect.height() - expected).abs() < 0.1,
"height = {}",
rect.height()
);
assert!((score - 1.0 / std::f64::consts::SQRT_2).abs() < 0.05);
assert!((rect.center().x() - 5.0).abs() < 0.2);
assert!((rect.center().y() - 5.0).abs() < 0.2);
}
#[test]
fn test_inscribed_square_aspect_2() {
let pieces = vec![axis_aligned_square_piece(10.0)];
let (rect, score) = largest_inscribed_rect(&pieces, 2.0, 0.01).unwrap();
let inv = 1.0 / 5.0_f64.sqrt();
let exp_w = 10.0 * 2.0 * inv;
let exp_h = 10.0 * inv;
assert!(
(rect.width() - exp_w).abs() < 0.1,
"width = {}",
rect.width()
);
assert!(
(rect.height() - exp_h).abs() < 0.1,
"height = {}",
rect.height()
);
assert!((score - exp_h / 10.0).abs() < 0.02);
}
#[test]
fn test_inscribed_l_shape() {
let l_shape = RegionPiece {
outer: Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(4.0, 0.0),
Point::new(4.0, 1.0),
Point::new(1.0, 1.0),
Point::new(1.0, 4.0),
Point::new(0.0, 4.0),
]),
holes: vec![],
};
let pieces = vec![l_shape];
let (rect, score) = largest_inscribed_rect(&pieces, 1.0, 0.01).unwrap();
assert!(rect.width() > 0.0 && rect.height() > 0.0);
assert!(rect.center().x() < 1.5);
assert!(rect.center().y() < 1.5);
assert!(score > 0.05, "score = {}", score);
}
#[test]
fn test_inscribed_with_hole() {
let outer = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
]);
let hole = Polygon::new(vec![
Point::new(4.0, 4.0),
Point::new(4.0, 6.0),
Point::new(6.0, 6.0),
Point::new(6.0, 4.0),
]);
let piece = RegionPiece {
outer,
holes: vec![hole],
};
let baseline = vec![axis_aligned_square_piece(10.0)];
let (_, baseline_score) = largest_inscribed_rect(&baseline, 1.0, 0.01).unwrap();
let pieces = vec![piece];
let (rect, score) = largest_inscribed_rect(&pieces, 1.0, 0.01).unwrap();
let cx = rect.center().x();
let cy = rect.center().y();
assert!(
!(4.0..=6.0).contains(&cx) || !(4.0..=6.0).contains(&cy),
"centre ({}, {}) lies inside the hole",
cx,
cy
);
assert!(
score < baseline_score - 0.05,
"score {} not lower than baseline {}",
score,
baseline_score
);
}
#[test]
fn test_inscribed_thin_triangle() {
let piece = RegionPiece {
outer: Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(5.0, 0.1),
]),
holes: vec![],
};
let pieces = vec![piece];
if let Some((rect, _score)) = largest_inscribed_rect(&pieces, 1.0, 0.001) {
assert!(rect.height() < 0.1, "height = {}", rect.height());
assert!(rect.width() < 0.1, "width = {}", rect.width());
}
}
#[test]
fn test_inscribed_zero_aspect_returns_none() {
let pieces = vec![axis_aligned_square_piece(10.0)];
assert!(largest_inscribed_rect(&pieces, 0.0, 0.01).is_none());
assert!(largest_inscribed_rect(&pieces, -1.0, 0.01).is_none());
}
#[test]
fn test_inscribed_empty_pieces_returns_none() {
let pieces: Vec<RegionPiece> = vec![];
assert!(largest_inscribed_rect(&pieces, 1.0, 0.01).is_none());
}
#[test]
fn test_principal_axis_circle_polygon() {
let circle = Circle::new(Point::new(3.0, 4.0), 5.0);
let polygon = circle.polygonize(64);
let piece = RegionPiece {
outer: polygon,
holes: vec![],
};
let (_angle, elongation) = principal_axis(&piece);
assert!(
(elongation - 1.0).abs() < 0.05,
"elongation = {}",
elongation
);
}
#[test]
fn test_principal_axis_rotated_ellipse() {
let theta = std::f64::consts::PI / 6.0;
let ellipse = Ellipse::new(Point::new(0.0, 0.0), 4.0, 1.0, theta);
let polygon = ellipse.polygonize(128);
let piece = RegionPiece {
outer: polygon,
holes: vec![],
};
let (angle, elongation) = principal_axis(&piece);
let mut diff = (angle - theta).abs();
if diff > std::f64::consts::FRAC_PI_2 {
diff = std::f64::consts::PI - diff;
}
assert!(diff < 0.05, "angle = {}, expected ≈ {}", angle, theta);
assert!(elongation > 3.5, "elongation = {}", elongation);
}
#[test]
fn test_principal_axis_with_hole() {
let outer = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
]);
let hole = Polygon::new(vec![
Point::new(1.0, 1.0),
Point::new(1.0, 2.0),
Point::new(2.0, 2.0),
Point::new(2.0, 1.0),
]);
let piece = RegionPiece {
outer,
holes: vec![hole],
};
let (_angle, elongation) = principal_axis(&piece);
assert!(elongation > 1.0, "elongation = {}", elongation);
}
#[test]
fn test_fit_label_comfortably_fits_square() {
let pieces = vec![axis_aligned_square_piece(10.0)];
let anchor = fit_label_in_region(&pieces, 2.0, 1.0, 0.01).unwrap();
assert!((anchor.x() - 5.0).abs() < 0.2, "x = {}", anchor.x());
assert!((anchor.y() - 5.0).abs() < 0.2, "y = {}", anchor.y());
}
#[test]
fn test_fit_label_too_wide_returns_none() {
let pieces = vec![axis_aligned_square_piece(10.0)];
assert!(fit_label_in_region(&pieces, 11.0, 1.0, 0.01).is_none());
}
#[test]
fn test_fit_label_too_tall_returns_none() {
let pieces = vec![axis_aligned_square_piece(10.0)];
assert!(fit_label_in_region(&pieces, 1.0, 11.0, 0.01).is_none());
}
#[test]
fn test_fit_label_larger_than_bbox_returns_none() {
let pieces = vec![axis_aligned_square_piece(10.0)];
assert!(fit_label_in_region(&pieces, 20.0, 20.0, 0.01).is_none());
}
#[test]
fn test_fit_label_with_hole_shrinks_fit() {
let outer = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
]);
let hole = Polygon::new(vec![
Point::new(4.0, 4.0),
Point::new(4.0, 6.0),
Point::new(6.0, 6.0),
Point::new(6.0, 4.0),
]);
let pieces = vec![RegionPiece {
outer,
holes: vec![hole],
}];
let unholed = vec![axis_aligned_square_piece(10.0)];
assert!(fit_label_in_region(&unholed, 5.0, 5.0, 0.01).is_some());
assert!(fit_label_in_region(&pieces, 5.0, 5.0, 0.01).is_none());
let small = fit_label_in_region(&pieces, 0.5, 0.5, 0.01).unwrap();
let inside_hole = (4.0..=6.0).contains(&small.x()) && (4.0..=6.0).contains(&small.y());
assert!(
!inside_hole,
"small-label centre ({}, {}) lies inside the hole",
small.x(),
small.y()
);
}
#[test]
fn test_fit_label_complement_region() {
let outer = Polygon::new(vec![
Point::new(-5.0, -5.0),
Point::new(5.0, -5.0),
Point::new(5.0, 5.0),
Point::new(-5.0, 5.0),
]);
let hole_a = Polygon::new(vec![
Point::new(-2.5, -0.5),
Point::new(-2.5, 0.5),
Point::new(-3.5, 0.5),
Point::new(-3.5, -0.5),
]);
let hole_b = Polygon::new(vec![
Point::new(2.5, -0.5),
Point::new(2.5, 0.5),
Point::new(3.5, 0.5),
Point::new(3.5, -0.5),
]);
let pieces = vec![RegionPiece {
outer,
holes: vec![hole_a, hole_b],
}];
let anchor = fit_label_in_region(&pieces, 1.0, 0.4, 0.01).unwrap();
assert!(anchor.x() >= -5.0 && anchor.x() <= 5.0);
assert!(anchor.y() >= -5.0 && anchor.y() <= 5.0);
let in_hole_a = (-3.5..=-2.5).contains(&anchor.x()) && (-0.5..=0.5).contains(&anchor.y());
let in_hole_b = (2.5..=3.5).contains(&anchor.x()) && (-0.5..=0.5).contains(&anchor.y());
assert!(!in_hole_a && !in_hole_b);
}
#[test]
fn test_fit_label_invalid_dimensions() {
let pieces = vec![axis_aligned_square_piece(10.0)];
assert!(fit_label_in_region(&pieces, 0.0, 1.0, 0.01).is_none());
assert!(fit_label_in_region(&pieces, 1.0, 0.0, 0.01).is_none());
assert!(fit_label_in_region(&pieces, -1.0, 1.0, 0.01).is_none());
assert!(fit_label_in_region(&pieces, 1.0, -1.0, 0.01).is_none());
assert!(fit_label_in_region(&pieces, f64::NAN, 1.0, 0.01).is_none());
assert!(fit_label_in_region(&pieces, 1.0, f64::INFINITY, 0.01).is_none());
}
}