max-inscribed-circle 0.1.1

Finds the largest circle that fits inside a polygon, using closed-form fast paths then a branch-and-bound grid over a BVH nearest-boundary index.
Documentation
//! Nearest-boundary distance queries over a segment table, accelerated by a
//! bounding-volume hierarchy.
//!
//! A uniform grid looks attractive and is wrong for this problem. The solver
//! asks for the distance at points deep inside the polygon, where the nearest
//! boundary is far away, so a ring-by-ring grid walk visits a number of cells
//! that grows with the segment count: measured at the centre of a regular
//! polygon, one query cost 57 ns at 16 segments and 48.8 us at 4096, which is
//! linear. The hierarchy below descends only into nodes whose bounding box can
//! still hold a closer segment, so the cost tracks tree depth rather than table
//! size.

use super::super::input::SegmentIndex;

/// A handful of segments per leaf keeps the tree shallow and lets a leaf test
/// finish within one cache line's worth of work.
const LEAF_SIZE: usize = 4;

#[derive(Debug, Clone, Copy)]
struct Node {
    min_x: f64,
    min_y: f64,
    max_x: f64,
    max_y: f64,
    /// Left child index for an internal node, or the first slot of the leaf run.
    left: u32,
    /// Right child index for an internal node; unused for a leaf.
    right: u32,
    /// Segment count for a leaf. Zero marks an internal node.
    count: u32,
}

/// Flat bounding-volume hierarchy over the segment bounding boxes.
#[derive(Debug, Clone)]
pub struct Bvh {
    nodes: Vec<Node>,
    /// Segment order, permuted so each leaf owns a contiguous run.
    order: Vec<u32>,
}

impl Bvh {
    /// Build the hierarchy over the segment table.
    pub fn new(segments: &SegmentIndex) -> Self {
        let n = segments.len();
        if n == 0 {
            return Self {
                nodes: Vec::new(),
                order: Vec::new(),
            };
        }

        let mut order: Vec<u32> = (0..n as u32).collect();
        let boxes: Vec<(f64, f64, f64, f64)> = (0..n)
            .map(|i| {
                (
                    segments.bbox_minx[i],
                    segments.bbox_miny[i],
                    segments.bbox_maxx[i],
                    segments.bbox_maxy[i],
                )
            })
            .collect();

        let mut nodes = Vec::with_capacity(2 * n.div_ceil(LEAF_SIZE) + 1);
        build(&mut nodes, &mut order, &boxes, 0, n);

        Self { nodes, order }
    }

    /// Squared distance from (x, y) to the closest segment and its index.
    #[inline]
    pub fn nearest_distance_sq(
        &self,
        segments: &SegmentIndex,
        x: f64,
        y: f64,
    ) -> Option<(f64, usize)> {
        if self.nodes.is_empty() || segments.is_empty() {
            return None;
        }
        let mut best_sq = f64::INFINITY;
        let mut best_idx = 0usize;
        self.descend(0, segments, x, y, &mut best_sq, &mut best_idx);
        Some((best_sq, best_idx))
    }

    /// Nearest distance, or `None` once some segment is proven within `threshold_sq`.
    ///
    /// A branch-and-bound caller often does not need the exact distance: it only
    /// needs to know whether a point can beat an incumbent. Once one segment is
    /// found closer than that threshold, the point is useless and the exact
    /// value does not matter, so the descent stops there. This is the difference
    /// between proving "nothing is closer than the best found" and merely
    /// finding "something closer than a much looser bar", and it is what makes
    /// the common, hopeless cell cheap.
    ///
    /// `seed` is a segment index known to be near, typically the support of the
    /// parent cell, whose distance is taken as the starting candidate.
    #[inline]
    pub fn nearest_distance_sq_above(
        &self,
        segments: &SegmentIndex,
        x: f64,
        y: f64,
        threshold_sq: f64,
        seed: Option<usize>,
    ) -> Option<(f64, usize)> {
        if self.nodes.is_empty() || segments.is_empty() {
            return None;
        }
        let mut best_sq = f64::INFINITY;
        let mut best_idx = 0usize;
        if let Some(si) = seed {
            if si < segments.len() {
                let d_sq = segments.point_segment_distance_sq(si, x, y);
                if d_sq <= threshold_sq {
                    return None;
                }
                if d_sq < best_sq {
                    best_sq = d_sq;
                    best_idx = si;
                }
            }
        }
        if self.descend_above(0, segments, x, y, &mut best_sq, &mut best_idx, threshold_sq) {
            return None;
        }
        Some((best_sq, best_idx))
    }

    /// Returns true when a segment within `threshold_sq` was found.
    #[allow(clippy::too_many_arguments)]
    fn descend_above(
        &self,
        node_idx: usize,
        segments: &SegmentIndex,
        x: f64,
        y: f64,
        best_sq: &mut f64,
        best_idx: &mut usize,
        threshold_sq: f64,
    ) -> bool {
        let node = self.nodes[node_idx];
        if bbox_dist_sq(&node, x, y) >= *best_sq {
            return false;
        }
        if node.count > 0 {
            let start = node.left as usize;
            for &seg in &self.order[start..start + node.count as usize] {
                let d_sq = segments.point_segment_distance_sq(seg as usize, x, y);
                if d_sq <= threshold_sq {
                    return true;
                }
                if d_sq < *best_sq {
                    *best_sq = d_sq;
                    *best_idx = seg as usize;
                }
            }
            return false;
        }
        let left = node.left as usize;
        let right = node.right as usize;
        let dl = bbox_dist_sq(&self.nodes[left], x, y);
        let dr = bbox_dist_sq(&self.nodes[right], x, y);
        let (first, second) = if dl <= dr {
            (left, right)
        } else {
            (right, left)
        };
        if self.descend_above(first, segments, x, y, best_sq, best_idx, threshold_sq) {
            return true;
        }
        if bbox_dist_sq(&self.nodes[second], x, y) < *best_sq
            && self.descend_above(second, segments, x, y, best_sq, best_idx, threshold_sq)
        {
            return true;
        }
        false
    }

    /// Indices of every segment within `max_dist_sq` of (x, y).
    pub fn supporting_segments(
        &self,
        segments: &SegmentIndex,
        x: f64,
        y: f64,
        max_dist_sq: f64,
    ) -> Vec<usize> {
        let mut out = Vec::new();
        if self.nodes.is_empty() {
            return out;
        }
        self.collect(0, segments, x, y, max_dist_sq, &mut out);
        out
    }

    fn descend(
        &self,
        node_idx: usize,
        segments: &SegmentIndex,
        x: f64,
        y: f64,
        best_sq: &mut f64,
        best_idx: &mut usize,
    ) {
        let node = self.nodes[node_idx];
        if bbox_dist_sq(&node, x, y) >= *best_sq {
            return;
        }
        if node.count > 0 {
            let start = node.left as usize;
            for &seg in &self.order[start..start + node.count as usize] {
                let d_sq = segments.point_segment_distance_sq(seg as usize, x, y);
                if d_sq < *best_sq {
                    *best_sq = d_sq;
                    *best_idx = seg as usize;
                }
            }
            return;
        }
        // Visit the nearer child first: its result tightens the bound before the
        // far child is tested, which is what makes the pruning effective.
        let left = node.left as usize;
        let right = node.right as usize;
        let dl = bbox_dist_sq(&self.nodes[left], x, y);
        let dr = bbox_dist_sq(&self.nodes[right], x, y);
        let (first, second) = if dl <= dr {
            (left, right)
        } else {
            (right, left)
        };
        self.descend(first, segments, x, y, best_sq, best_idx);
        if bbox_dist_sq(&self.nodes[second], x, y) < *best_sq {
            self.descend(second, segments, x, y, best_sq, best_idx);
        }
    }

    fn collect(
        &self,
        node_idx: usize,
        segments: &SegmentIndex,
        x: f64,
        y: f64,
        max_dist_sq: f64,
        out: &mut Vec<usize>,
    ) {
        let node = self.nodes[node_idx];
        if bbox_dist_sq(&node, x, y) > max_dist_sq {
            return;
        }
        if node.count > 0 {
            let start = node.left as usize;
            for &seg in &self.order[start..start + node.count as usize] {
                let d_sq = segments.point_segment_distance_sq(seg as usize, x, y);
                if d_sq <= max_dist_sq {
                    out.push(seg as usize);
                }
            }
            return;
        }
        self.collect(node.left as usize, segments, x, y, max_dist_sq, out);
        self.collect(node.right as usize, segments, x, y, max_dist_sq, out);
    }
}

/// Build the subtree owning `order[start..end]`, returning its node index.
///
/// The layout puts each internal node immediately before its left child, so a
/// node stores only `left` and the right child is `left + 1`. Splitting on the
/// widest axis of the segment centres keeps the boxes tight, which is what lets
/// the bound prune.
fn build(
    nodes: &mut Vec<Node>,
    order: &mut [u32],
    boxes: &[(f64, f64, f64, f64)],
    start: usize,
    end: usize,
) -> usize {
    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 i in start..end {
        let b = boxes[order[i] as usize];
        min_x = min_x.min(b.0);
        min_y = min_y.min(b.1);
        max_x = max_x.max(b.2);
        max_y = max_y.max(b.3);
    }

    let idx = nodes.len();
    nodes.push(Node {
        min_x,
        min_y,
        max_x,
        max_y,
        left: 0,
        right: 0,
        count: 0,
    });

    let count = end - start;
    if count <= LEAF_SIZE {
        nodes[idx].left = start as u32;
        nodes[idx].count = count as u32;
        return idx;
    }

    let (mut cmin_x, mut cmin_y) = (f64::INFINITY, f64::INFINITY);
    let (mut cmax_x, mut cmax_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
    for i in start..end {
        let b = boxes[order[i] as usize];
        let cx = (b.0 + b.2) * 0.5;
        let cy = (b.1 + b.3) * 0.5;
        cmin_x = cmin_x.min(cx);
        cmin_y = cmin_y.min(cy);
        cmax_x = cmax_x.max(cx);
        cmax_y = cmax_y.max(cy);
    }
    let axis = if cmax_x - cmin_x >= cmax_y - cmin_y {
        0
    } else {
        1
    };

    let mid = start + count / 2;
    order[start..end].select_nth_unstable_by(count / 2, |&a, &b| {
        let ca = centre(boxes[a as usize], axis);
        let cb = centre(boxes[b as usize], axis);
        ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
    });

    nodes[idx].left = (nodes.len()) as u32;
    nodes[idx].count = 0;
    build(nodes, order, boxes, start, mid);
    let right = build(nodes, order, boxes, mid, end);
    nodes[idx].right = right as u32;
    idx
}

#[inline]
fn centre(b: (f64, f64, f64, f64), axis: usize) -> f64 {
    if axis == 0 {
        (b.0 + b.2) * 0.5
    } else {
        (b.1 + b.3) * 0.5
    }
}

#[inline]
fn bbox_dist_sq(node: &Node, x: f64, y: f64) -> f64 {
    let dx = if x < node.min_x {
        node.min_x - x
    } else if x > node.max_x {
        x - node.max_x
    } else {
        0.0
    };
    let dy = if y < node.min_y {
        node.min_y - y
    } else if y > node.max_y {
        y - node.max_y
    } else {
        0.0
    };
    dx * dx + dy * dy
}