Skip to main content

straight_skeleton/
skeleton.rs

1//! The computed straight skeleton and its provenance queries.
2
3use alloc::vec;
4use alloc::vec::Vec;
5
6use crate::polygon::{EdgeId, VertexId};
7use crate::Point;
8
9/// Identifies a [`Node`] of a [`Skeleton`].
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct NodeId(pub u32);
13
14/// Identifies an [`Arc`] of a [`Skeleton`].
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct ArcId(pub u32);
18
19/// What produced a [`Node`].
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub enum NodeKind {
23    /// A node sitting on the input boundary, at offset 0. There is exactly one
24    /// of these per input vertex, and it carries that vertex's id.
25    Boundary(VertexId),
26    /// An interior node, created when the shrinking wavefront collapsed an edge
27    /// to nothing and two skeleton arcs met.
28    EdgeEvent,
29    /// An interior node, created when a reflex vertex ran into an opposing
30    /// wavefront edge and tore the wavefront into two loops.
31    SplitEvent,
32    /// A node where the wavefront stopped because every incident edge had
33    /// reached its per-edge distance limit.
34    ///
35    /// Only produced by [`skeleton_constrained`]; a plain skeleton never has
36    /// these.
37    ///
38    /// [`skeleton_constrained`]: crate::skeleton_constrained
39    LimitReached,
40}
41
42/// A vertex of the skeleton graph.
43///
44/// # Examples
45///
46/// ```
47/// use straight_skeleton::{skeleton, NodeKind, Point, Polygon};
48///
49/// let square = Polygon::from_outer(&[
50///     Point::new(0, 0), Point::new(10, 0), Point::new(10, 10), Point::new(0, 10),
51/// ])?;
52/// let skel = skeleton(&square)?;
53///
54/// // Every input vertex gets a boundary node at offset 0.
55/// let boundary: Vec<_> = skel.nodes().iter().filter(|n| n.is_boundary()).collect();
56/// assert_eq!(boundary.len(), 4);
57/// assert!(boundary.iter().all(|n| n.offset == 0.0));
58/// # Ok::<(), Box<dyn std::error::Error>>(())
59/// ```
60#[derive(Clone, Debug, PartialEq)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62pub struct Node {
63    /// The node's position, rounded to the integer lattice.
64    ///
65    /// Skeleton nodes are generally irrational even for integer input, so this
66    /// is the nearest lattice point. Use [`Node::exact`] when the rounding
67    /// matters.
68    pub position: Point,
69    /// The node's unrounded position.
70    ///
71    /// The algorithm computes in `f32` throughout, so this is the value it
72    /// actually arrived at, not a narrowing of something wider.
73    pub exact: [f32; 2],
74    /// How far the wavefront had travelled when this node was created.
75    ///
76    /// For a plain [`skeleton`], this is the node's distance to the supporting
77    /// line of each of its [`Node::sources`]. It is the node's height on a
78    /// roof, and the offset at which the node appears on an offset curve.
79    ///
80    /// For a [`skeleton_constrained`], it is the wavefront's **time**, which is
81    /// no longer the same thing: an edge that stopped at `limit` stays `limit`
82    /// away however long the wavefront runs on. The distance to a source edge
83    /// `e`'s line is `min(offset, limit_e)`.
84    ///
85    /// [`skeleton`]: crate::skeleton
86    /// [`skeleton_constrained`]: crate::skeleton_constrained
87    pub offset: f32,
88    /// What produced this node.
89    pub kind: NodeKind,
90    /// The input edges whose wavefronts arrived here together.
91    ///
92    /// Always at least 2 entries, and 3 or more where several skeleton arcs
93    /// meet. Each one's supporting line is [`Node::offset`] away (see that
94    /// field for the constrained case).
95    ///
96    /// # This is not quite "nearest"
97    ///
98    /// For a **convex** polygon these really are the nearest input edges, since
99    /// there the straight skeleton coincides with the medial axis.
100    ///
101    /// Elsewhere they may not be, and the difference is the definition of a
102    /// straight skeleton rather than a wrinkle in this implementation. A
103    /// straight skeleton bisects edges' infinite **supporting lines**, which is
104    /// what keeps every arc straight. A medial axis bisects the nearest
105    /// **features**, and so grows parabolic arcs around reflex vertices. Around
106    /// a reflex corner the two part company: a plus-shape's centre is at offset
107    /// 5 from the four arms' walls, but the nearest input feature is a reflex
108    /// corner 7.07 away.
109    ///
110    /// So read `sources` as *"the input edges whose faces meet here"* — which is
111    /// the useful notion anyway, and the one a roof needs. See [`Arc::sources`].
112    pub sources: Vec<EdgeId>,
113}
114
115impl Node {
116    /// Whether this node lies on the input boundary.
117    #[inline]
118    pub fn is_boundary(&self) -> bool {
119        matches!(self.kind, NodeKind::Boundary(_))
120    }
121
122    /// The input vertex this node sits on, if it is a boundary node.
123    #[inline]
124    pub fn input_vertex(&self) -> Option<VertexId> {
125        match self.kind {
126            NodeKind::Boundary(v) => Some(v),
127            _ => None,
128        }
129    }
130}
131
132/// An edge of the skeleton graph: a straight segment traced by one wavefront
133/// vertex as it moved.
134///
135/// # Provenance
136///
137/// Every arc separates the [faces] of **exactly two input edges** — the two in
138/// [`Arc::sources`] — and every point along it is equidistant from those two
139/// edges' supporting lines. This is not an approximation or a post-hoc lookup:
140/// it is what an arc *is*. A wavefront vertex exists precisely where two
141/// shrinking edges meet, and it carries those two edge ids with it as it sweeps
142/// the arc out.
143///
144/// So tracing an arc back to the input it came from is a field access, at zero
145/// cost. See [`Node::sources`] for the one caveat: on a non-convex polygon
146/// these are the edges whose faces meet here, which is not always the same as
147/// the Euclidean-nearest edges.
148///
149/// [faces]: Skeleton::face
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
152pub struct Arc {
153    /// The arc's two endpoints. `nodes[0]` is always the lower-offset end, so
154    /// arcs point "uphill", away from the boundary.
155    pub nodes: [NodeId; 2],
156    /// The two input edges whose faces this arc separates.
157    pub sources: [EdgeId; 2],
158}
159
160impl Arc {
161    /// The endpoint nearer the input boundary.
162    #[inline]
163    pub fn lower(&self) -> NodeId {
164        self.nodes[0]
165    }
166
167    /// The endpoint further from the input boundary.
168    #[inline]
169    pub fn upper(&self) -> NodeId {
170        self.nodes[1]
171    }
172
173    /// The endpoint that is not `n`, or `None` if `n` is not an endpoint.
174    #[inline]
175    pub fn other(&self, n: NodeId) -> Option<NodeId> {
176        if self.nodes[0] == n {
177            Some(self.nodes[1])
178        } else if self.nodes[1] == n {
179            Some(self.nodes[0])
180        } else {
181            None
182        }
183    }
184}
185
186/// A loop of the wavefront that stopped rather than collapsing: the offset
187/// polygon a [`skeleton_constrained`] leaves behind.
188///
189/// # What it is
190///
191/// The wavefront of a plain [`skeleton`] always shrinks away to nothing — that
192/// is what it means for the skeleton to be finished. Limits change that. Once
193/// every edge around some loop has stopped, the loop stops too, and simply
194/// stays there. What it stays as is the input polygon offset inward by the
195/// limit: the *flat* left in the middle of a truncated roof.
196///
197/// So a constrained skeleton is not only the stubs reaching in from the
198/// boundary. It is those stubs **and** the outline they stop on, and this is
199/// that outline.
200///
201/// ```text
202///     +-------------------------+        +-------------------------+
203///     |                         |        | \                     / |
204///     |                         |        |   +-----------------+   |
205///     |                         |   ->   |   |    residual     |   |
206///     |                         |        |   +-----------------+   |
207///     |                         |        | /                     \ |
208///     +-------------------------+        +-------------------------+
209///        every edge limited                the arcs stop at the limit,
210///                                          and this is where they stop
211/// ```
212///
213/// # Why it is not made of [`Arc`]s
214///
215/// Because it would be a lie about what an `Arc` is. Every arc bisects the
216/// supporting lines of exactly two input edges, which is what makes
217/// [`Arc::sources`] meaningful and what the whole provenance story rests on. A
218/// residual segment is *parallel* to one input edge and belongs to it alone.
219/// Putting one in `arcs` would break the invariant every consumer of `sources`
220/// relies on, so it lives here with the shape it actually has: each segment
221/// names the one edge it came from.
222///
223/// # Winding
224///
225/// Inherited from the input, so the polygon's interior stays on the left of
226/// every segment: the loop around the outer boundary runs counter-clockwise,
227/// and a loop around a hole runs clockwise.
228///
229/// [`skeleton`]: crate::skeleton
230/// [`skeleton_constrained`]: crate::skeleton_constrained
231///
232/// # Examples
233///
234/// ```
235/// use straight_skeleton::{skeleton, skeleton_constrained, Point, Polygon};
236///
237/// let square = Polygon::from_outer(&[
238///     Point::new(0, 0), Point::new(100, 0), Point::new(100, 100), Point::new(0, 100),
239/// ])?;
240///
241/// // Stop every edge at 20: what is left is the 60x60 square in the middle.
242/// let skel = skeleton_constrained(&square, &[20.0; 4])?;
243/// let flat = &skel.residual()[0];
244/// assert_eq!(flat.len(), 4);
245///
246/// let mut corners: Vec<Point> = flat.nodes.iter().map(|&n| skel.node(n).position).collect();
247/// corners.sort();
248/// assert_eq!(corners, vec![
249///     Point::new(20, 20), Point::new(20, 80), Point::new(80, 20), Point::new(80, 80),
250/// ]);
251///
252/// // An unconstrained skeleton has none: its wavefront always collapses.
253/// assert!(skeleton(&square)?.residual().is_empty());
254/// # Ok::<(), Box<dyn std::error::Error>>(())
255/// ```
256#[derive(Clone, Debug, PartialEq, Eq)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
258pub struct ResidualLoop {
259    /// The loop's corners, in wavefront order.
260    ///
261    /// These are ordinary skeleton nodes — the far ends of the arcs that stopped
262    /// here — so their [`Node::offset`] is where the wavefront got to.
263    pub nodes: Vec<NodeId>,
264    /// The input edge each segment came from: `edges[i]` owns the segment from
265    /// `nodes[i]` to `nodes[i + 1]`, wrapping at the end.
266    ///
267    /// Always the same length as [`ResidualLoop::nodes`]. Every point on that
268    /// segment is `min(offset, limit)` from `edges[i]`'s supporting line, and
269    /// parallel to it.
270    pub edges: Vec<EdgeId>,
271}
272
273impl ResidualLoop {
274    /// How many corners, and so also how many segments.
275    #[inline]
276    pub fn len(&self) -> usize {
277        self.nodes.len()
278    }
279
280    /// Whether the loop is empty. It never is; this exists to satisfy the
281    /// convention that a type with `len` has `is_empty`.
282    #[inline]
283    pub fn is_empty(&self) -> bool {
284        self.nodes.is_empty()
285    }
286
287    /// The segments, as `(from, to, source edge)`, in wavefront order.
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use straight_skeleton::{skeleton_constrained, Point, Polygon};
293    ///
294    /// let square = Polygon::from_outer(&[
295    ///     Point::new(0, 0), Point::new(100, 0), Point::new(100, 100), Point::new(0, 100),
296    /// ])?;
297    /// let skel = skeleton_constrained(&square, &[20.0; 4])?;
298    ///
299    /// // Four sides, each parallel to the wall it came from.
300    /// let sides: Vec<_> = skel.residual()[0].segments().collect();
301    /// assert_eq!(sides.len(), 4);
302    /// # Ok::<(), Box<dyn std::error::Error>>(())
303    /// ```
304    pub fn segments(&self) -> impl Iterator<Item = (NodeId, NodeId, EdgeId)> + '_ {
305        (0..self.nodes.len()).map(move |i| {
306            (
307                self.nodes[i],
308                self.nodes[(i + 1) % self.nodes.len()],
309                self.edges[i],
310            )
311        })
312    }
313}
314
315/// The straight skeleton of a [`Polygon`].
316///
317/// A skeleton is a planar graph of [`Node`]s joined by [`Arc`]s. Build one with
318/// [`skeleton`] or [`skeleton_constrained`].
319///
320/// [`Polygon`]: crate::Polygon
321/// [`skeleton`]: crate::skeleton
322/// [`skeleton_constrained`]: crate::skeleton_constrained
323///
324/// # Examples
325///
326/// ```
327/// use straight_skeleton::{skeleton, Point, Polygon};
328///
329/// // A 10x10 square's skeleton is an X: four boundary nodes, one centre node,
330/// // four arcs running corner to centre.
331/// let square = Polygon::from_outer(&[
332///     Point::new(0, 0), Point::new(10, 0), Point::new(10, 10), Point::new(0, 10),
333/// ])?;
334/// let skel = skeleton(&square)?;
335///
336/// assert_eq!(skel.node_count(), 5);
337/// assert_eq!(skel.arc_count(), 4);
338///
339/// // The interior node is the centre, at offset 5 (half the width).
340/// let centre = skel.nodes().iter().find(|n| !n.is_boundary()).unwrap();
341/// assert_eq!(centre.position, Point::new(5, 5));
342/// assert!((centre.offset - 5.0).abs() < 1e-4);
343///
344/// // ...and it is equidistant from all four input edges.
345/// assert_eq!(centre.sources.len(), 4);
346/// # Ok::<(), Box<dyn std::error::Error>>(())
347/// ```
348#[derive(Clone, Debug, PartialEq, Default)]
349#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
350pub struct Skeleton {
351    pub(crate) nodes: Vec<Node>,
352    pub(crate) arcs: Vec<Arc>,
353    /// `node_arcs[i]` lists the arcs incident to node `i`. Built once at the
354    /// end of the algorithm so that traversal queries are O(degree).
355    pub(crate) node_arcs: Vec<Vec<ArcId>>,
356    /// `edge_nodes[i]` is the pair of boundary nodes at input edge `i`'s start
357    /// and end vertices, which is where [`Skeleton::face`] begins its walk.
358    pub(crate) edge_nodes: Vec<[NodeId; 2]>,
359    /// The wavefront loops that stopped instead of collapsing. Empty unless
360    /// per-edge limits bound.
361    pub(crate) residual: Vec<ResidualLoop>,
362}
363
364impl Skeleton {
365    /// All nodes.
366    #[inline]
367    pub fn nodes(&self) -> &[Node] {
368        &self.nodes
369    }
370
371    /// All arcs.
372    #[inline]
373    pub fn arcs(&self) -> &[Arc] {
374        &self.arcs
375    }
376
377    /// The wavefront loops that stopped rather than collapsing — the offset
378    /// polygon a constrained skeleton leaves behind.
379    ///
380    /// **Empty for a plain [`skeleton`]**, whose wavefront always shrinks away
381    /// to nothing. Non-empty only where [`skeleton_constrained`]'s limits bound
382    /// hard enough to stop a whole loop, and then there is one entry per loop
383    /// still standing: the outer offset outline, plus one around each hole that
384    /// survived.
385    ///
386    /// This is the other half of a constrained result. The [`arcs`] are the
387    /// stubs reaching in from the boundary; this is the outline they stop on.
388    /// See [`ResidualLoop`] for why it is not made of arcs.
389    ///
390    /// [`skeleton`]: crate::skeleton
391    /// [`skeleton_constrained`]: crate::skeleton_constrained
392    /// [`arcs`]: Skeleton::arcs
393    ///
394    /// # Examples
395    ///
396    /// ```
397    /// use straight_skeleton::{skeleton_constrained, Point, Polygon};
398    ///
399    /// // An L-shape, every wall stopped at 20.
400    /// let l = Polygon::from_outer(&[
401    ///     Point::new(0, 0), Point::new(200, 0), Point::new(200, 100),
402    ///     Point::new(100, 100), Point::new(100, 200), Point::new(0, 200),
403    /// ])?;
404    /// let skel = skeleton_constrained(&l, &[20.0; 6])?;
405    ///
406    /// // What is left is the same L, 20 in from every wall.
407    /// assert_eq!(skel.residual().len(), 1);
408    /// let mut corners: Vec<Point> =
409    ///     skel.residual()[0].nodes.iter().map(|&n| skel.node(n).position).collect();
410    /// corners.sort();
411    /// assert_eq!(corners, vec![
412    ///     Point::new(20, 20), Point::new(20, 180), Point::new(80, 80),
413    ///     Point::new(80, 180), Point::new(180, 20), Point::new(180, 80),
414    /// ]);
415    /// # Ok::<(), Box<dyn std::error::Error>>(())
416    /// ```
417    #[inline]
418    pub fn residual(&self) -> &[ResidualLoop] {
419        &self.residual
420    }
421
422    /// Number of nodes.
423    #[inline]
424    pub fn node_count(&self) -> usize {
425        self.nodes.len()
426    }
427
428    /// Number of arcs.
429    #[inline]
430    pub fn arc_count(&self) -> usize {
431        self.arcs.len()
432    }
433
434    /// Looks up a node.
435    ///
436    /// # Panics
437    ///
438    /// Panics if `n` does not belong to this skeleton.
439    #[inline]
440    pub fn node(&self, n: NodeId) -> &Node {
441        &self.nodes[n.0 as usize]
442    }
443
444    /// Looks up an arc.
445    ///
446    /// # Panics
447    ///
448    /// Panics if `a` does not belong to this skeleton.
449    #[inline]
450    pub fn arc(&self, a: ArcId) -> &Arc {
451        &self.arcs[a.0 as usize]
452    }
453
454    /// Iterates node ids.
455    pub fn node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
456        (0..self.nodes.len() as u32).map(NodeId)
457    }
458
459    /// Iterates arc ids.
460    pub fn arc_ids(&self) -> impl Iterator<Item = ArcId> + '_ {
461        (0..self.arcs.len() as u32).map(ArcId)
462    }
463
464    /// The arcs incident to a node.
465    ///
466    /// # Panics
467    ///
468    /// Panics if `n` does not belong to this skeleton.
469    #[inline]
470    pub fn arcs_at(&self, n: NodeId) -> &[ArcId] {
471        &self.node_arcs[n.0 as usize]
472    }
473
474    /// The arc's two endpoints, as positions.
475    ///
476    /// # Panics
477    ///
478    /// Panics if `a` does not belong to this skeleton.
479    #[inline]
480    pub fn arc_segment(&self, a: ArcId) -> (Point, Point) {
481        let arc = self.arc(a);
482        (
483            self.node(arc.nodes[0]).position,
484            self.node(arc.nodes[1]).position,
485        )
486    }
487
488    /// The two input edges a given arc came from.
489    ///
490    /// Every point along the arc is equidistant from these two edges'
491    /// supporting lines, and the arc separates their two faces. See [`Arc`] for
492    /// why this is exact rather than a search, and [`Node::sources`] for why
493    /// "came from" is more accurate than "closest to".
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// use straight_skeleton::{skeleton, Point, Polygon};
499    ///
500    /// let square = Polygon::from_outer(&[
501    ///     Point::new(0, 0), Point::new(10, 0), Point::new(10, 10), Point::new(0, 10),
502    /// ])?;
503    /// let skel = skeleton(&square)?;
504    ///
505    /// // Each of the square's four arcs bisects two adjacent input edges.
506    /// for a in skel.arc_ids() {
507    ///     let [e0, e1] = skel.closest_input_edges(a);
508    ///     assert_ne!(e0, e1);
509    /// }
510    /// # Ok::<(), Box<dyn std::error::Error>>(())
511    /// ```
512    ///
513    /// # Panics
514    ///
515    /// Panics if `a` does not belong to this skeleton.
516    #[inline]
517    pub fn closest_input_edges(&self, a: ArcId) -> [EdgeId; 2] {
518        self.arc(a).sources
519    }
520
521    /// The input edges a given node came from.
522    ///
523    /// See [`Node::sources`], which this returns.
524    ///
525    /// # Panics
526    ///
527    /// Panics if `n` does not belong to this skeleton.
528    #[inline]
529    pub fn closest_input_edges_to_node(&self, n: NodeId) -> &[EdgeId] {
530        &self.node(n).sources
531    }
532
533    /// The largest offset reached by any node: the radius of the largest disc
534    /// that fits inside the polygon.
535    ///
536    /// For a roof, this is the ridge height. Returns 0 for an empty skeleton.
537    pub fn max_offset(&self) -> f32 {
538        self.nodes.iter().map(|n| n.offset).fold(0.0, f32::max)
539    }
540
541    /// The boundary node sitting on a given input vertex.
542    ///
543    /// # Examples
544    ///
545    /// ```
546    /// use straight_skeleton::{skeleton, Point, Polygon, VertexId};
547    ///
548    /// let square = Polygon::from_outer(&[
549    ///     Point::new(0, 0), Point::new(10, 0), Point::new(10, 10), Point::new(0, 10),
550    /// ])?;
551    /// let skel = skeleton(&square)?;
552    ///
553    /// let n = skel.boundary_node(VertexId(2)).unwrap();
554    /// assert_eq!(skel.node(n).position, Point::new(10, 10));
555    /// # Ok::<(), Box<dyn std::error::Error>>(())
556    /// ```
557    pub fn boundary_node(&self, v: VertexId) -> Option<NodeId> {
558        // The algorithm emits one boundary node per input vertex, in order,
559        // before any interior node, so the ids line up.
560        let id = NodeId(v.0 as u32);
561        match self.nodes.get(v.0 as usize)?.kind {
562            NodeKind::Boundary(w) if w == v => Some(id),
563            _ => None,
564        }
565    }
566
567    /// The **face** of an input edge: the closed region the wavefront of that
568    /// edge swept out, as a loop of nodes.
569    ///
570    /// Every input edge has exactly one face, and the faces tile the polygon.
571    /// The returned loop starts with the edge's own two endpoints, then follows
572    /// the skeleton arcs that bound the face back around. Each face is planar
573    /// when nodes are lifted to `z = offset`, which is exactly why a straight
574    /// skeleton builds roofs: **one face is one roof plane**. See the `roof`
575    /// example.
576    ///
577    /// Returns `None` if the face cannot be walked, which should not happen for
578    /// a skeleton of a valid polygon.
579    ///
580    /// # Examples
581    ///
582    /// ```
583    /// use straight_skeleton::{skeleton, EdgeId, Point, Polygon};
584    ///
585    /// // Each of a square's four edges has a triangular face running to the
586    /// // centre.
587    /// let square = Polygon::from_outer(&[
588    ///     Point::new(0, 0), Point::new(10, 0), Point::new(10, 10), Point::new(0, 10),
589    /// ])?;
590    /// let skel = skeleton(&square)?;
591    ///
592    /// let face = skel.face(EdgeId(0)).unwrap();
593    /// assert_eq!(face.len(), 3);
594    ///
595    /// // A rectangle's long edges get quadrilateral faces, because the ridge
596    /// // gives them a fourth corner.
597    /// let rect = Polygon::from_outer(&[
598    ///     Point::new(0, 0), Point::new(20, 0), Point::new(20, 10), Point::new(0, 10),
599    /// ])?;
600    /// let skel = skeleton(&rect)?;
601    /// assert_eq!(skel.face(EdgeId(0)).unwrap().len(), 4); // long edge
602    /// assert_eq!(skel.face(EdgeId(1)).unwrap().len(), 3); // short edge
603    /// # Ok::<(), Box<dyn std::error::Error>>(())
604    /// ```
605    pub fn face(&self, e: EdgeId) -> Option<Vec<NodeId>> {
606        let [start, end] = *self.edge_nodes.get(e.0 as usize)?;
607        let mut loop_ = vec![start, end];
608
609        // Walk from the edge's far endpoint back to its near one, taking only
610        // arcs that this edge is a source of — those are precisely the arcs
611        // bounding its face.
612        let mut cur = end;
613        let mut came_from: Option<ArcId> = None;
614        loop {
615            let next_arc = self
616                .arcs_at(cur)
617                .iter()
618                .copied()
619                .find(|&a| Some(a) != came_from && self.arc(a).sources.contains(&e));
620
621            let other = match next_arc {
622                Some(a) => {
623                    came_from = Some(a);
624                    self.arc(a).other(cur)?
625                }
626                // No arc leads on, which on a plain skeleton means the walk is
627                // lost. On a constrained one it usually means the opposite: the
628                // wavefront stopped here rather than collapsing, so what bounds
629                // the face is not an arc at all but the residual segment `e`
630                // stopped as. Cross it and carry on.
631                None => {
632                    came_from = None;
633                    self.residual_step(e, cur)?
634                }
635            };
636
637            if other == start {
638                return Some(loop_);
639            }
640            loop_.push(other);
641            cur = other;
642            // A face cannot have more corners than the skeleton has nodes.
643            if loop_.len() > self.nodes.len() + 2 {
644                return None;
645            }
646        }
647    }
648
649    /// Crosses `e`'s residual segment, arriving at `cur`.
650    ///
651    /// Segments run in their edge's own direction, and [`Skeleton::face`] walks
652    /// a face the other way — from the edge's far end back to its near one — so
653    /// it always meets a segment at the `to` end and leaves by the `from` end.
654    fn residual_step(&self, e: EdgeId, cur: NodeId) -> Option<NodeId> {
655        self.residual
656            .iter()
657            .flat_map(|l| l.segments())
658            .find(|&(_, to, edge)| edge == e && to == cur)
659            .map(|(from, _, _)| from)
660    }
661
662    /// How many input edges the polygon had.
663    ///
664    /// Each one owns exactly one [`face`](Skeleton::face), so this is also the
665    /// number of faces.
666    ///
667    /// # Examples
668    ///
669    /// ```
670    /// use straight_skeleton::{skeleton, Point, Polygon};
671    ///
672    /// let square = Polygon::from_outer(&[
673    ///     Point::new(0, 0), Point::new(10, 0), Point::new(10, 10), Point::new(0, 10),
674    /// ])?;
675    /// assert_eq!(skeleton(&square)?.input_edge_count(), 4);
676    /// # Ok::<(), Box<dyn std::error::Error>>(())
677    /// ```
678    #[inline]
679    pub fn input_edge_count(&self) -> usize {
680        self.edge_nodes.len()
681    }
682
683    /// Every input edge's face, in edge order.
684    ///
685    /// Returns `None` if any face cannot be walked.
686    pub fn faces(&self) -> Option<Vec<Vec<NodeId>>> {
687        (0..self.edge_nodes.len() as u16)
688            .map(|i| self.face(EdgeId(i)))
689            .collect()
690    }
691
692    /// Rebuilds the node-to-arc adjacency. Called once when the algorithm
693    /// finishes.
694    pub(crate) fn build_adjacency(&mut self) {
695        self.node_arcs.clear();
696        self.node_arcs.resize(self.nodes.len(), Vec::new());
697        for (i, arc) in self.arcs.iter().enumerate() {
698            let id = ArcId(i as u32);
699            self.node_arcs[arc.nodes[0].0 as usize].push(id);
700            // A degenerate zero-length arc would otherwise be listed twice.
701            if arc.nodes[0] != arc.nodes[1] {
702                self.node_arcs[arc.nodes[1].0 as usize].push(id);
703            }
704        }
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711    use alloc::vec;
712
713    fn node(kind: NodeKind, offset: f32) -> Node {
714        Node {
715            position: Point::ORIGIN,
716            exact: [0.0, 0.0],
717            offset,
718            kind,
719            sources: vec![EdgeId(0), EdgeId(1)],
720        }
721    }
722
723    #[test]
724    fn arc_other_endpoint() {
725        let a = Arc {
726            nodes: [NodeId(1), NodeId(2)],
727            sources: [EdgeId(0), EdgeId(1)],
728        };
729        assert_eq!(a.other(NodeId(1)), Some(NodeId(2)));
730        assert_eq!(a.other(NodeId(2)), Some(NodeId(1)));
731        assert_eq!(a.other(NodeId(3)), None);
732        assert_eq!(a.lower(), NodeId(1));
733        assert_eq!(a.upper(), NodeId(2));
734    }
735
736    #[test]
737    fn node_kind_helpers() {
738        let b = node(NodeKind::Boundary(VertexId(7)), 0.0);
739        assert!(b.is_boundary());
740        assert_eq!(b.input_vertex(), Some(VertexId(7)));
741
742        let i = node(NodeKind::EdgeEvent, 3.0);
743        assert!(!i.is_boundary());
744        assert_eq!(i.input_vertex(), None);
745    }
746
747    #[test]
748    fn adjacency_lists_every_incident_arc() {
749        let mut s = Skeleton {
750            nodes: vec![
751                node(NodeKind::Boundary(VertexId(0)), 0.0),
752                node(NodeKind::Boundary(VertexId(1)), 0.0),
753                node(NodeKind::EdgeEvent, 5.0),
754            ],
755            arcs: vec![
756                Arc {
757                    nodes: [NodeId(0), NodeId(2)],
758                    sources: [EdgeId(0), EdgeId(1)],
759                },
760                Arc {
761                    nodes: [NodeId(1), NodeId(2)],
762                    sources: [EdgeId(1), EdgeId(2)],
763                },
764            ],
765            node_arcs: Vec::new(),
766            edge_nodes: Vec::new(),
767            residual: Vec::new(),
768        };
769        s.build_adjacency();
770
771        assert_eq!(s.arcs_at(NodeId(0)), &[ArcId(0)]);
772        assert_eq!(s.arcs_at(NodeId(1)), &[ArcId(1)]);
773        assert_eq!(s.arcs_at(NodeId(2)), &[ArcId(0), ArcId(1)]);
774    }
775
776    #[test]
777    fn max_offset_of_empty_skeleton_is_zero() {
778        assert_eq!(Skeleton::default().max_offset(), 0.0);
779    }
780
781    #[test]
782    fn max_offset_finds_the_ridge() {
783        let mut s = Skeleton {
784            nodes: vec![
785                node(NodeKind::Boundary(VertexId(0)), 0.0),
786                node(NodeKind::EdgeEvent, 5.0),
787                node(NodeKind::EdgeEvent, 2.0),
788            ],
789            arcs: vec![],
790            node_arcs: Vec::new(),
791            edge_nodes: Vec::new(),
792            residual: Vec::new(),
793        };
794        s.build_adjacency();
795        assert_eq!(s.max_offset(), 5.0);
796    }
797}