use std::collections::BinaryHeap;
use super::super::index::{NearestBoundaryIndex, PipIndex};
use super::super::input::HostPolygon;
use super::super::{MicApproach, MicResult, MicUsedEngine};
use crate::tuning;
use geo_types::Point;
const SQRT2: f64 = std::f64::consts::SQRT_2;
#[derive(Debug, Clone, Copy)]
struct GridCell {
x: f64,
y: f64,
h_size: f64,
max_dist: f64,
support: usize,
}
impl PartialEq for GridCell {
fn eq(&self, other: &Self) -> bool {
self.max_dist == other.max_dist
}
}
impl Eq for GridCell {}
impl PartialOrd for GridCell {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for GridCell {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.max_dist
.partial_cmp(&other.max_dist)
.unwrap_or(std::cmp::Ordering::Equal)
}
}
#[inline]
fn boundary_distance(nb: &NearestBoundaryIndex, x: f64, y: f64) -> f64 {
match nb.nearest_distance_sq(x, y) {
Some((d, _)) if d.is_finite() && d > 0.0 => d.sqrt(),
_ => 0.0,
}
}
#[inline]
fn empty_of_interior(inside: bool, d: f64, half: f64) -> bool {
!inside && d > half * SQRT2
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn solve_grid(
host: &HostPolygon,
pip: &PipIndex,
nb: &NearestBoundaryIndex,
tolerance: f64,
) -> Option<MicResult> {
let (min_x, min_y, max_x, max_y) = host.bounds()?;
let width = max_x - min_x;
let height = max_y - min_y;
let cell_size = width.max(height);
if cell_size <= 0.0 || !tolerance.is_finite() || tolerance <= 0.0 {
return None;
}
let side = tuning::MIC_GRID_SIDE;
let h_size = cell_size / (2.0 * side as f64);
let step = 2.0 * h_size;
let cells_x = ((width / step).ceil() as usize).max(1);
let cells_y = ((height / step).ceil() as usize).max(1);
let mut queue: BinaryHeap<GridCell> = BinaryHeap::with_capacity(cells_x * cells_y * 2);
let mut best_x = 0.0;
let mut best_y = 0.0;
let mut best_dist = 0.0;
let mut found = false;
for i in 0..cells_x {
let cx = min_x + h_size + i as f64 * step;
for j in 0..cells_y {
let cy = min_y + h_size + j as f64 * step;
let d = boundary_distance(nb, cx, cy);
let inside = pip.contains_strict_xy(cx, cy);
if empty_of_interior(inside, d, h_size) {
continue;
}
if inside && d > best_dist {
best_x = cx;
best_y = cy;
best_dist = d;
found = true;
}
queue.push(GridCell {
x: cx,
y: cy,
h_size,
max_dist: d + h_size * SQRT2,
support: usize::MAX,
});
}
}
let mut iterations = 0usize;
let mut exhausted_budget = false;
while let Some(cell) = queue.pop() {
iterations += 1;
if iterations > tuning::MIC_GRID_CELL_BUDGET {
exhausted_budget = true;
break;
}
let radius_bound = best_dist + tolerance;
if found && cell.max_dist <= radius_bound {
break;
}
let half = cell.h_size * 0.5;
let children = [
(cell.x - half, cell.y - half),
(cell.x + half, cell.y - half),
(cell.x - half, cell.y + half),
(cell.x + half, cell.y + half),
];
let slack = half * SQRT2 - (radius_bound - best_dist);
let threshold = if found && slack > 0.0 {
Some(best_dist - slack)
} else {
None
};
for (cx, cy) in children {
if cx + half < min_x || cx - half > max_x || cy + half < min_y || cy - half > max_y {
continue;
}
let (d, support) = match threshold {
Some(t) => match nb.nearest_distance_sq_above(
cx,
cy,
(t.max(0.0)) * (t.max(0.0)),
Some(cell.support),
) {
Some(v) => v,
None => continue,
},
None => {
let Some(v) = nb.nearest_distance_sq(cx, cy) else {
continue;
};
(v.0, v.1)
}
};
let d = d.max(0.0).sqrt();
let max_dist = d + half * SQRT2;
if max_dist <= radius_bound {
continue;
}
let inside = pip.contains_strict_xy(cx, cy);
if empty_of_interior(inside, d, half) {
continue;
}
if inside && d > best_dist {
best_x = cx;
best_y = cy;
best_dist = d;
found = true;
}
queue.push(GridCell {
x: cx,
y: cy,
h_size: half,
max_dist,
support,
});
}
}
if !found {
return None;
}
let (best_x, best_y) = if exhausted_budget {
let (px, py, _) = polish(nb, pip, best_x, best_y, best_dist, tolerance, h_size);
(px, py)
} else {
(best_x, best_y)
};
let segments = nb.segments();
let radius_sq = segments.batch_point_segment_distance_sq(best_x, best_y, 0, segments.len());
if !radius_sq.is_finite() {
return None;
}
let radius_sq = radius_sq.max(0.0);
let radius = radius_sq.sqrt();
let support_segments =
nb.supporting_segments(best_x, best_y, radius_sq, radius_sq.max(1.0) * 1e-10);
Some(MicResult {
center: Point::new(best_x, best_y),
radius,
radius_sq,
support_segments,
candidate_count: iterations,
used_engine: MicUsedEngine::Grid,
approach: MicApproach::GridSolver,
component_index: None,
})
}
fn polish(
nb: &NearestBoundaryIndex,
pip: &PipIndex,
start_x: f64,
start_y: f64,
start_d: f64,
tol: f64,
initial_step: f64,
) -> (f64, f64, f64) {
let mut best_x = start_x;
let mut best_y = start_y;
let mut best_d = start_d;
let mut step = initial_step;
for _ in 0..256 {
if step <= tol || !step.is_finite() {
break;
}
let mut moved = false;
for (dx, dy) in [
(-1.0, 0.0),
(1.0, 0.0),
(0.0, -1.0),
(0.0, 1.0),
(-1.0, -1.0),
(1.0, -1.0),
(-1.0, 1.0),
(1.0, 1.0),
] {
let nx = best_x + dx * step;
let ny = best_y + dy * step;
let Some((d_sq, _)) = nb.nearest_distance_sq(nx, ny) else {
continue;
};
let d = d_sq.max(0.0).sqrt();
if d > best_d && pip.contains_strict_xy(nx, ny) {
best_x = nx;
best_y = ny;
best_d = d;
moved = true;
}
}
if !moved {
step *= 0.5;
}
}
(best_x, best_y, best_d)
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn is_convex_polygon(host: &HostPolygon) -> bool {
if host.ring_count() > 1 {
return false;
}
let outer = host.outer_ring();
if outer.len() < 4 {
return false;
}
let n = outer.len() - 1;
let mut sign = 0i8;
for i in 0..n {
let p = outer[(i + n - 1) % n];
let c = outer[i];
let nxt = outer[(i + 1) % n];
let cross = (c[0] - p[0]) * (nxt[1] - c[1]) - (c[1] - p[1]) * (nxt[0] - c[0]);
if cross.abs() <= 1e-14 {
return false;
}
let s = if cross > 0.0 { 1 } else { -1 };
if sign == 0 {
sign = s;
} else if s != sign {
return false;
}
}
true
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn has_holes(host: &HostPolygon) -> bool {
host.rings.iter().any(|r| r.is_hole)
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn is_simple_shape(host: &HostPolygon) -> bool {
is_convex_polygon(host) && !has_holes(host)
}