Skip to main content

straight_skeleton/
lib.rs

1//! The **straight skeleton** of a polygon, on the `i16` integer lattice.
2//!
3//! Shrink a polygon by sliding every edge inward at the same speed, keeping the
4//! edges straight and letting them stay connected. The corners trace out a tree
5//! of straight line segments. That tree is the straight skeleton, and this
6//! crate computes it.
7//!
8//! ```text
9//!    +-----------------------+        +-----------------------+
10//!    |                       |        |\                     /|
11//!    |                       |        |  \                 /  |
12//!    |                       |        |    \_____________/    |
13//!    |                       |   ->   |    /             \    |
14//!    |                       |        |  /                 \  |
15//!    |                       |        |/                     \|
16//!    +-----------------------+        +-----------------------+
17//!         input polygon                   its straight skeleton
18//! ```
19//!
20//! Straight skeletons are how you find a polygon's medial ridge, generate
21//! mitred offset curves, or raise a roof over a floor plan — hip, mansard, or
22//! truncated, all off the same skeleton, since each node's distance from the
23//! boundary *is* the run the roof has had to rise over. See [`Roof`] and the
24//! `roof` example.
25//!
26//! # Quick start
27//!
28//! ```
29//! use straight_skeleton::{skeleton, Point, Polygon};
30//!
31//! // A 10x10 square.
32//! let square = Polygon::from_outer(&[
33//!     Point::new(0, 0),
34//!     Point::new(10, 0),
35//!     Point::new(10, 10),
36//!     Point::new(0, 10),
37//! ])?;
38//!
39//! let skel = skeleton(&square)?;
40//!
41//! // Its skeleton is an X: the four corners meet at the centre.
42//! assert_eq!(skel.arc_count(), 4);
43//! let centre = skel.nodes().iter().find(|n| !n.is_boundary()).unwrap();
44//! assert_eq!(centre.position, Point::new(5, 5));
45//! # Ok::<(), Box<dyn std::error::Error>>(())
46//! ```
47//!
48//! # Tracing output back to input
49//!
50//! Every skeleton [`Arc`] separates the faces of **exactly two** input edges,
51//! and carries those two ids in [`Arc::sources`]. This is not a
52//! nearest-neighbour search bolted on afterwards — it is what an arc *is*, so
53//! the lookup is a field access:
54//!
55//! ```
56//! use straight_skeleton::{skeleton, Point, Polygon};
57//!
58//! let square = Polygon::from_outer(&[
59//!     Point::new(0, 0), Point::new(10, 0), Point::new(10, 10), Point::new(0, 10),
60//! ])?;
61//! let skel = skeleton(&square)?;
62//!
63//! for arc in skel.arcs() {
64//!     let [e0, e1] = arc.sources;
65//!     // Every point on this arc is equidistant from the supporting lines of
66//!     // input edges e0 and e1.
67//!     assert_ne!(e0, e1);
68//! }
69//!
70//! // Nodes carry the same information, with 3+ sources where arcs meet.
71//! let centre = skel.nodes().iter().find(|n| !n.is_boundary()).unwrap();
72//! assert_eq!(centre.sources.len(), 4); // equidistant from all four edges
73//! # Ok::<(), Box<dyn std::error::Error>>(())
74//! ```
75//!
76//! Each input edge also owns one skeleton **[face]**, the region its wavefront
77//! swept. The faces tile the polygon, and every face is planar once lifted to
78//! `z = offset` — which is why [`Roof`] can raise a roof over a floor plan by
79//! reading the skeleton off rather than computing anything:
80//!
81//! ```
82//! use straight_skeleton::{skeleton, Point, Polygon, Roof};
83//!
84//! let plan = Polygon::from_outer(&[
85//!     Point::new(0, 0), Point::new(120, 0), Point::new(120, 80), Point::new(0, 80),
86//! ])?;
87//! let skel = skeleton(&plan)?;
88//!
89//! let roof = Roof::new(&skel, 0.5)?;
90//! assert_eq!(roof.panels().len(), 4);   // one flat panel per wall
91//! assert_eq!(roof.ridge_height(), 20);  // i16, like every other coordinate
92//!
93//! // The skeleton is the roof's *plan*, not its height, so a mansard reads off
94//! // the very same one — only its [`Profile`] differs.
95//! let mansard = Roof::mansard(&skel, 2.0, 10.0, 0.25)?;
96//! assert_eq!(mansard.panels().len(), 8);  // the break cuts each wall's in two
97//! # Ok::<(), Box<dyn std::error::Error>>(())
98//! ```
99//!
100//! One caveat, and it is a real one rather than an implementation wrinkle: a
101//! straight skeleton is **not** the medial axis. It bisects edges' infinite
102//! *supporting lines*, which is what keeps every arc straight; a medial axis
103//! bisects the nearest *features*, and grows parabolas around reflex vertices.
104//! The two agree exactly when the polygon is convex, and part company around
105//! reflex corners. So `sources` means "the edges whose faces meet here", which
106//! is the notion you actually want. [`Node::sources`] works through an example.
107//!
108//! [face]: Skeleton::face
109//!
110//! # Constrained skeletons
111//!
112//! [`skeleton_constrained`] caps how far each edge is allowed to travel,
113//! **individually**. An edge that hits its limit simply stops, and its
114//! neighbours slide along it instead of over it. Use it to truncate a roof to a
115//! given eave-to-ridge rise, or to build a variable-width offset.
116//!
117//! ```
118//! use straight_skeleton::{skeleton_constrained, Point, Polygon};
119//!
120//! let square = Polygon::from_outer(&[
121//!     Point::new(0, 0), Point::new(20, 0), Point::new(20, 20), Point::new(0, 20),
122//! ])?;
123//!
124//! // Stop every edge after travelling 3 units, well before the centre at 10.
125//! let limits = [3.0; 4];
126//! let skel = skeleton_constrained(&square, &limits)?;
127//!
128//! // Nothing gets further from the boundary than the limit allows.
129//! assert!(skel.max_offset() <= 3.0 + 1e-4);
130//! # Ok::<(), Box<dyn std::error::Error>>(())
131//! ```
132//!
133//! What the wavefront stops *as* is the other half of the answer, and
134//! [`Skeleton::residual`] returns it: the input polygon offset inward by the
135//! limit — the flat left in the middle of a truncated roof. The arcs are the
136//! stubs reaching in from the boundary; the residual is the outline they stop
137//! on.
138//!
139//! ```
140//! use straight_skeleton::{skeleton, skeleton_constrained, Point, Polygon};
141//!
142//! let square = Polygon::from_outer(&[
143//!     Point::new(0, 0), Point::new(100, 0), Point::new(100, 100), Point::new(0, 100),
144//! ])?;
145//!
146//! // Stop every edge at 20 and a 60x60 square is left standing in the middle.
147//! let skel = skeleton_constrained(&square, &[20.0; 4])?;
148//! assert_eq!(skel.residual()[0].len(), 4);
149//!
150//! // A plain skeleton has none: its wavefront always shrinks away to nothing.
151//! assert!(skeleton(&square)?.residual().is_empty());
152//! # Ok::<(), Box<dyn std::error::Error>>(())
153//! ```
154//!
155//! # Coordinates: `i32` and `f32`, and **no `f64`**
156//!
157//! Input and output coordinates are [`Point`]s of `i16`, and the algorithm runs
158//! entirely in `i32` and `f32`. There is no `f64` anywhere in it, and no `i64`
159//! either: the crate is meant to be portable to hardware where `f64` is slow or
160//! missing, and a type you only use "internally" is still a type the hardware
161//! has to have.
162//!
163//! That costs **one bit of coordinate range**. Coordinates are capped at
164//! [`Point::MIN_COORD`]`..=`[`Point::MAX_COORD`], i.e. `-16384..=16383`, and
165//! [`Polygon`] rejects anything outside it. One expression sets that cap — the
166//! orientation determinant, which needs `2 * d^2` for a largest coordinate
167//! difference `d`:
168//!
169//! | coordinates | `2 * d^2` | in `i32`? |
170//! |---|---|---|
171//! | full `i16` | 8_589_672_450 | **overflows**, reporting the *wrong side* |
172//! | capped | 2_147_352_578 | fits, with 131_069 to spare |
173//!
174//! So one bit buys **exact** predicates ([`predicates`]): no epsilon, no
175//! rounding, no overflow. `f32` cannot do that job at any range — the tests pin
176//! down a real triple *inside* the cap where it reports a genuine turn as
177//! collinear.
178//!
179//! The simulation itself is `f32`. Skeleton nodes are irrational in general, so
180//! there is no lattice to compute on; positions are rounded back to it at the
181//! boundary, and [`Node::exact`] keeps the unrounded value. The cap is also
182//! what leaves `f32` enough absolute resolution — about `0.002` at its worst —
183//! to work in. `docs/DESIGN.md` works through the analysis, including what it
184//! costs in robustness.
185//!
186//! # Feature flags
187//!
188//! The crate has **no required dependencies**. Everything below is opt-in.
189//!
190//! | Feature | Default | Effect |
191//! |---|---|---|
192//! | `std` | yes | `std::error::Error` impls, hardware `sqrt`. Disable for `no_std`. |
193//! | `serde` | no | `Serialize`/`Deserialize` on the public types. |
194//! | `geo-types` | no | Conversions to and from `geo_types`. |
195//! | `glam` | no | Conversions to and from `glam` vectors. |
196//! | `mint` | no | Conversions to and from `mint` vectors. |
197//! | `num-traits` | no | Generic numeric conversions. |
198//!
199//! # `no_std`
200//!
201//! Disable default features. The crate needs [`alloc`] but nothing else — the
202//! only `std` maths it uses is `sqrt`, which it carries its own implementation
203//! of rather than take a dependency on `libm`.
204//!
205//! ```toml
206//! straight-skeleton = { version = "0.1", default-features = false }
207//! ```
208//!
209//! [`alloc`]: https://doc.rust-lang.org/alloc/
210//! [`Arc::sources`]: crate::Arc::sources
211//! [`Node::exact`]: crate::Node::exact
212
213#![no_std]
214#![cfg_attr(docsrs, feature(doc_cfg))]
215
216extern crate alloc;
217
218#[cfg(any(feature = "std", test))]
219extern crate std;
220
221mod math;
222mod point;
223mod polygon;
224mod roof;
225mod skeleton;
226mod wavefront;
227
228pub mod predicates;
229
230#[cfg(any(
231    feature = "geo-types",
232    feature = "glam",
233    feature = "mint",
234    feature = "num-traits"
235))]
236mod interop;
237
238pub use point::Point;
239pub use polygon::{EdgeId, Polygon, PolygonError, RingId, VertexId};
240pub use roof::{Panel, PanelKind, Point3, Profile, Roof, RoofError, RoofVertex, RoofVertexId};
241pub use skeleton::{Arc, ArcId, Node, NodeId, NodeKind, ResidualLoop, Skeleton};
242pub use wavefront::SkeletonError;
243
244/// Computes the straight skeleton of a polygon.
245///
246/// Every edge slides inward at unit speed until the polygon has shrunk to
247/// nothing. The paths its vertices trace form the skeleton.
248///
249/// # Errors
250///
251/// Returns [`SkeletonError`] only for inputs the simulation cannot resolve.
252/// A [`Polygon`] is already validated at construction, so for the unconstrained
253/// transform this is not expected to fail — if it does, it is a bug.
254///
255/// # Complexity
256///
257/// `O(n)` space. For time, measured growth (`cargo run --release --example
258/// bench`) rather than a claim:
259///
260/// | input | 1024 vertices | 3200 vertices | scaling |
261/// |---|---|---|---|
262/// | convex | 1.2 ms (at 828) | — (coordinate cap) | ~n^1.2 |
263/// | comb, half reflex | 2.0 ms | 12 ms | ~n^1.3 rising to ~n^1.7 |
264/// | random star, half reflex | 2.9 ms | 16 ms | ~n^1.4 rising to ~n^1.6 |
265///
266/// The event count is linear and each event reschedules `O(1)` vertices. The one
267/// non-constant step is the split search, `O(n)` per reflex vertex, so the worst
268/// case is `O(n^2)` — the rising exponent is that term taking over as `n` grows.
269/// **Convex input never runs it**, having no reflex vertices to search from.
270///
271/// Beating `O(n^2)` needs the motorcycle-graph construction of Eppstein–Erickson
272/// or Cheng–Vigneron: a genuinely different algorithm, which would not obviously
273/// serve [`skeleton_constrained`] and cannot be bolted on as a pruner. CGAL and
274/// Surfer2 ship an `O(n^2)` worst case too. See `docs/ALGORITHM.md`.
275///
276/// Note that [`Polygon::new`] has its own worst-case `O(n^2)`, in the
277/// self-intersection check. It is normally far below this — 0.13 ms on the
278/// 3200-vertex comb — but a polygon whose edges all overlap in `x` defeats its
279/// pruning. See `docs/DESIGN.md`.
280///
281/// # Examples
282///
283/// ```
284/// use straight_skeleton::{skeleton, Point, Polygon};
285///
286/// // A triangle's skeleton is three arcs meeting at the incenter.
287/// let tri = Polygon::from_outer(&[
288///     Point::new(0, 0),
289///     Point::new(12, 0),
290///     Point::new(0, 9),
291/// ])?;
292/// let skel = skeleton(&tri)?;
293/// assert_eq!(skel.arc_count(), 3);
294///
295/// // The 9-12-15 triangle has inradius 3, so the incenter sits at (3, 3).
296/// let incenter = skel.nodes().iter().find(|n| !n.is_boundary()).unwrap();
297/// assert_eq!(incenter.position, Point::new(3, 3));
298/// assert!((incenter.offset - 3.0).abs() < 1e-4);
299/// # Ok::<(), Box<dyn std::error::Error>>(())
300/// ```
301pub fn skeleton(polygon: &Polygon) -> Result<Skeleton, SkeletonError> {
302    wavefront::compute(polygon, None)
303}
304
305/// Computes a straight skeleton in which each edge stops after travelling a
306/// given distance.
307///
308/// `limits[i]` is the furthest edge `EdgeId(i)` may travel; use
309/// [`f32::INFINITY`] to leave an edge unconstrained. Limits are per-edge and
310/// need not agree with one another.
311///
312/// # How it differs from [`skeleton`]
313///
314/// An edge that reaches its limit stops dead. Its neighbours do not: they keep
315/// moving, and the vertices joining them to the stopped edge slide *along* it.
316/// So the skeleton's arcs bend at the moment an adjacent edge stops, and the
317/// node at that bend is marked [`NodeKind::LimitReached`].
318///
319/// Passing all-[`f32::INFINITY`] limits reproduces [`skeleton`] exactly: both
320/// functions run the same weighted wavefront, and an unlimited edge is one
321/// whose speed never drops.
322///
323/// # Errors
324///
325/// - [`SkeletonError::LimitCountMismatch`] if `limits.len()` is not
326///   [`Polygon::edge_count`].
327/// - [`SkeletonError::InvalidLimit`] if a limit is negative or NaN.
328/// - [`SkeletonError::IncompatibleCollinearLimits`] if two collinear
329///   neighbouring edges are given different limits, which would tear the
330///   wavefront apart.
331///
332/// # Examples
333///
334/// ```
335/// use straight_skeleton::{skeleton, skeleton_constrained, Point, Polygon};
336///
337/// let square = Polygon::from_outer(&[
338///     Point::new(0, 0), Point::new(20, 0), Point::new(20, 20), Point::new(0, 20),
339/// ])?;
340///
341/// // Unlimited on every edge is exactly the plain skeleton.
342/// let unlimited = skeleton_constrained(&square, &[f32::INFINITY; 4])?;
343/// assert_eq!(unlimited.arcs().len(), skeleton(&square)?.arcs().len());
344///
345/// // Limits need not be uniform: hold one edge back while the rest advance.
346/// let mixed = skeleton_constrained(&square, &[2.0, f32::INFINITY, f32::INFINITY, f32::INFINITY])?;
347/// assert!(mixed.node_count() > 0);
348/// # Ok::<(), Box<dyn std::error::Error>>(())
349/// ```
350pub fn skeleton_constrained(polygon: &Polygon, limits: &[f32]) -> Result<Skeleton, SkeletonError> {
351    wavefront::compute(polygon, Some(limits))
352}