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