max_inscribed_circle/lib.rs
1//! Maximum inscribed circle (MIC) solvers for polygons.
2//!
3//! The maximum inscribed circle is the largest circle that fits entirely inside
4//! a polygon. Holes are respected: the circle has to clear interior rings as
5//! well as the exterior, so the solve runs against every boundary segment.
6//!
7//! # Quick start
8//!
9//! ```
10//! use max_inscribed_circle::geo_types::{LineString, Polygon};
11//! use max_inscribed_circle::{maximum_inscribed_circle, MicOptions};
12//!
13//! let square = Polygon::new(
14//! LineString::from(vec![(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0), (0.0, 0.0)]),
15//! vec![],
16//! );
17//!
18//! let mic = maximum_inscribed_circle(&square, &MicOptions::default())?;
19//! assert!((mic.center.x() - 2.0).abs() < 1e-9);
20//! assert!((mic.radius - 2.0).abs() < 1e-9);
21//! # Ok::<(), max_inscribed_circle::MicError>(())
22//! ```
23//!
24//! # Solve pipeline
25//!
26//! Two stages, both native Rust. [`MicResult::used_engine`] and
27//! [`MicResult::approach`] report which one produced the answer:
28//!
29//! 1. **Closed-form fast paths** for single-ring inputs of four or five
30//! vertices: triangle incenter, axis-aligned rectangle, then a convex-quad
31//! Chebyshev solve. Every result is re-measured against the segment table
32//! and falls through to the grid when the ring is not the class it claimed.
33//! 2. **Grid search** over the bounding box, a branch and bound driven by the
34//! 1-Lipschitz property of boundary distance, refining to a tolerance of
35//! `diag * 1e-7`, then a compass hill climb on the clearance field.
36//!
37//! The radius a result reports is always the measured distance from its centre
38//! to the nearest boundary segment, so a returned circle fits the polygon. A
39//! degenerate input, such as a zero-area ring or a non-finite coordinate, fails
40//! fast with [`MicError::InvalidInput`] instead of returning a meaningless
41//! circle.
42//!
43//! # Entry points
44//!
45//! | Function | Use |
46//! |----------|-----|
47//! | [`maximum_inscribed_circle`] | one polygon |
48//! | [`maximum_inscribed_circle_multipolygon`] | best circle across components |
49//! | [`maximum_inscribed_circle_slice`] | rayon-parallel batch, input order preserved |
50//! | [`maximum_inscribed_circle_with_workspace`] | reuse a [`MicWorkspace`] across calls |
51//!
52//! # GEOS comparison
53//!
54//! The `bench-geos` feature compiles `mic::solver::bench_geos`, which calls
55//! the GEOS `MaximumInscribedCircle` construct so benchmarks can cross-check
56//! the native solver. It is not an engine: nothing in [`mic`] calls it, and
57//! the default feature set does not compile it. GEOS runs the same
58//! cell-refinement branch and bound as the grid stage, so agreement between
59//! them means two implementations of one algorithm match, not that an
60//! independent method confirms the radius.
61//!
62//! # Features
63//!
64//! | Feature | Effect |
65//! |---------|--------|
66//! | `bench-geos` | comparison only: links the GEOS C library so benchmarks can compare solvers |
67//! | `geo-traits` | generic solves over any `geo_traits` polygon, plus `geo_types::Geometry` dispatch |
68//! | `shewchuk` | exact `orient2d` predicates from the `robust` crate in the containment and orientation tests |
69//! | `simd` | 4-lane batched segment distance via the stable `wide` crate |
70//! | `hotpath` | `#[hotpath::measure]` instrumentation on the solver entry points |
71//! | `python` | PyO3 extension module named `max_inscribed_circle` |
72//! | `extension-module` | build the Python module as an importable extension |
73//!
74//! Default features are empty. The `bench-geos` feature links against the GEOS
75//! C library, and its build script expects `GEOS_LIB_DIR` and `GEOS_VERSION`
76//! when GEOS cannot be found through `pkg-config`.
77//!
78//! # C ABI
79//!
80//! [`ffi`] exposes `mic_solve`, `mic_solve_multipolygon` and
81//! `mic_options_default`: flat coordinate arrays in, `#[repr(C)]` structs out,
82//! `0` on success and `-1` on error, with panics caught at the boundary. The
83
84#![warn(missing_docs)]
85#![cfg_attr(docsrs, feature(doc_cfg))]
86
87pub mod ffi;
88pub mod mic;
89pub mod tuning;
90
91#[cfg(feature = "python")]
92#[cfg_attr(docsrs, doc(cfg(feature = "python")))]
93pub mod python;
94
95#[cfg(feature = "geo-traits")]
96#[cfg_attr(docsrs, doc(cfg(feature = "geo-traits")))]
97pub mod geo_interop;
98
99/// Pinned `geo-types` re-export so downstream crates lock to the same
100/// version this crate solves over.
101pub use geo_types;
102
103pub use mic::workspace::MicWorkspace;
104pub use mic::{
105 maximum_inscribed_circle, maximum_inscribed_circle_multipolygon,
106 maximum_inscribed_circle_slice, maximum_inscribed_circle_with_workspace, MicApproach, MicError,
107 MicOptions, MicResult, MicUsedEngine,
108};
109
110#[cfg(feature = "python")]
111use pyo3::prelude::*;
112
113/// Python extension module (feature `python`).
114///
115/// Build with maturin or with
116/// `cargo build --features python,extension-module`, then import
117/// `max_inscribed_circle`.
118#[cfg(feature = "python")]
119#[pymodule]
120pub fn max_inscribed_circle(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
121 python::register(py, m)?;
122 m.add("__version__", env!("CARGO_PKG_VERSION"))?;
123 Ok(())
124}