use crate::geometry::primitives::Point;
use crate::geometry::shapes::{Polygon, Rectangle};
use crate::plotting::regions::{RegionPiece, poi_with_holes, point_in_polygon, signed_clearance};
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 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_w = max_x - min_x;
let bbox_h = max_y - min_y;
let bbox_short = bbox_w.min(bbox_h);
if bbox_short <= 0.0 {
return None;
}
let cx = centre.x();
let cy = centre.y();
let a = aspect_ratio;
let inv = 1.0 / (1.0 + a * a).sqrt();
let s_radial = 2.0 * r * inv;
let s_cap = bbox_h.min(bbox_w / a);
let mut lo = s_radial.min(s_cap);
let mut hi = s_cap;
let precision_clamped = precision.max(1e-12);
let mut iters = 0;
while hi - lo > precision_clamped && iters < 64 {
let mid = 0.5 * (lo + hi);
let hw = 0.5 * a * mid;
let hh = 0.5 * mid;
if rect_fits_in_piece(winning, cx, cy, hw, hh) {
lo = mid;
} else {
hi = mid;
}
iters += 1;
}
let s = lo;
let half_w = 0.5 * a * s;
let half_h = 0.5 * s;
let rect = Rectangle::new(centre, 2.0 * half_w, 2.0 * half_h);
let achieved_short = (2.0 * half_w).min(2.0 * half_h);
let score = (achieved_short / bbox_short).clamp(0.0, 1.0);
Some((rect, score))
}
fn rect_fits_in_piece(piece: &RegionPiece, cx: f64, cy: f64, hw: f64, hh: f64) -> bool {
if hw <= 0.0 || hh <= 0.0 {
return false;
}
let xl = cx - hw;
let xr = cx + hw;
let yb = cy - hh;
let yt = cy + hh;
let corners = [
Point::new(xl, yb),
Point::new(xr, yb),
Point::new(xr, yt),
Point::new(xl, yt),
];
for c in &corners {
if !point_in_polygon(c, &piece.outer) {
return false;
}
}
for hole in &piece.holes {
for c in &corners {
if point_in_polygon(c, hole) {
return false;
}
}
}
if ring_crosses_rect(piece.outer.vertices(), xl, xr, yb, yt) {
return false;
}
for hole in &piece.holes {
if ring_crosses_rect(hole.vertices(), xl, xr, yb, yt) {
return false;
}
if let Some(p) = hole.vertices().first()
&& p.x() > xl
&& p.x() < xr
&& p.y() > yb
&& p.y() < yt
{
return false;
}
}
true
}
fn ring_crosses_rect(verts: &[Point], xl: f64, xr: f64, yb: f64, yt: f64) -> bool {
let n = verts.len();
if n < 2 {
return false;
}
for i in 0..n {
let a = &verts[i];
let b = &verts[(i + 1) % n];
let (ax, ay) = (a.x(), a.y());
let (bx, by) = (b.x(), b.y());
if (ay - yb) * (by - yb) < 0.0 {
let x = ax + (yb - ay) * (bx - ax) / (by - ay);
if x > xl && x < xr {
return true;
}
}
if (ay - yt) * (by - yt) < 0.0 {
let x = ax + (yt - ay) * (bx - ax) / (by - ay);
if x > xl && x < xr {
return true;
}
}
if (ax - xl) * (bx - xl) < 0.0 {
let y = ay + (xl - ax) * (by - ay) / (bx - ax);
if y > yb && y < yt {
return true;
}
}
if (ax - xr) * (bx - xr) < 0.0 {
let y = ay + (xr - ax) * (by - ay) / (bx - ax);
if y > yb && y < yt {
return true;
}
}
}
false
}
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();
assert!(
(rect.width() - 10.0).abs() < 0.05,
"width = {}",
rect.width()
);
assert!(
(rect.height() - 10.0).abs() < 0.05,
"height = {}",
rect.height()
);
assert!(score > 0.99, "score = {score}");
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();
assert!(
(rect.width() - 10.0).abs() < 0.05,
"width = {}",
rect.width()
);
assert!(
(rect.height() - 5.0).abs() < 0.05,
"height = {}",
rect.height()
);
assert!((score - 0.5).abs() < 0.02, "score = {score}");
}
#[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.2, "score = {score}");
assert!(rect.width() <= 1.05, "width = {}", rect.width());
assert!(rect.height() <= 1.05, "height = {}", rect.height());
}
#[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 ({cx}, {cy}) lies inside the hole"
);
assert!(
score < baseline_score - 0.1,
"score {score} not lower than baseline {baseline_score}"
);
let half_w = rect.width() * 0.5;
let half_h = rect.height() * 0.5;
for (dx, dy) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
let x = cx + dx * half_w;
let y = cy + dy * half_h;
let strictly_in_hole = x > 4.0 && x < 6.0 && y > 4.0 && y < 6.0;
assert!(!strictly_in_hole, "corner ({x}, {y}) overlaps hole");
}
}
#[test]
fn test_inscribed_wide_and_short_anisotropic() {
let piece = RegionPiece {
outer: Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 2.0),
Point::new(0.0, 2.0),
]),
holes: vec![],
};
let pieces = vec![piece];
let (rect, _score) = largest_inscribed_rect(&pieces, 5.0, 0.01).unwrap();
assert!(
(rect.width() - 10.0).abs() < 0.05,
"width = {}",
rect.width()
);
assert!(
(rect.height() - 2.0).abs() < 0.05,
"height = {}",
rect.height()
);
}
#[test]
fn test_inscribed_concave_notch_outer() {
let outer = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 4.0),
Point::new(3.0, 5.0),
Point::new(10.0, 6.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
]);
let piece = RegionPiece {
outer: outer.clone(),
holes: vec![],
};
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();
let half_w = rect.width() * 0.5;
let half_h = rect.height() * 0.5;
let inset = 1e-6;
for (dx, dy) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
let x = cx + dx * (half_w - inset);
let y = cy + dy * (half_h - inset);
assert!(
crate::plotting::regions::point_in_polygon(&Point::new(x, y), &outer),
"near-corner ({x}, {y}) escaped outer polygon"
);
}
}
#[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 = {angle}, expected ≈ {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());
}
}