Skip to main content

straight_skeleton/
wavefront.rs

1//! The wavefront simulation that computes the skeleton.
2//!
3//! See `docs/ALGORITHM.md` for the illustrated walkthrough. In brief: every
4//! edge of the polygon slides inward at its own speed, dragging its neighbours
5//! with it. Vertices trace out the skeleton's arcs. The simulation advances
6//! from one *event* to the next, where an event is any moment the wavefront's
7//! shape changes discontinuously.
8
9use alloc::collections::{BTreeMap, BinaryHeap};
10use alloc::vec;
11use alloc::vec::Vec;
12use core::cmp::Ordering;
13use core::fmt;
14
15use crate::math::{floor_i32, Vec2};
16use crate::polygon::{EdgeId, Polygon};
17use crate::skeleton::{Arc, Node, NodeId, NodeKind, ResidualLoop, Skeleton};
18use crate::Point;
19
20/// Tolerance for the simulation's rate and time comparisons.
21///
22/// This one is **not** a position tolerance, which is why it can be as tight as
23/// it is. It guards quantities that are `O(1)` by construction — closing rates
24/// and speeds, both dotted from unit vectors, and the times that come out of
25/// dividing by them. `f32` resolves about `1.2e-7` near 1, so `1e-4` sits some
26/// three orders of magnitude above the noise floor there while still being far
27/// too small to call a real approach a standstill.
28///
29/// Do not reach for this to compare positions. Coordinates run to 16383, where
30/// `f32` resolves only about `0.002` — coarser than `EPS` itself, so an `EPS`
31/// comparison between two positions out there is comparing noise. That is what
32/// [`MERGE_EPS`] is for.
33const EPS: f32 = 1e-4;
34
35/// Tolerance for treating two unit normals as parallel.
36///
37/// Compared against the cross product of two unit vectors, i.e. `sin` of the
38/// angle between them, so this is an angle of roughly `1e-9` radians.
39const PARALLEL_EPS: f32 = 1e-6;
40
41/// How close two wavefront vertices must be to count as arriving at the same
42/// place, and so be retired by a single vertex event.
43///
44/// The position tolerance, and necessarily far looser than [`EPS`]. At the far
45/// corner of the coordinate range `f32` resolves about `0.002`, and these
46/// positions are not measured but *extrapolated* from event times that were
47/// themselves computed, so the error compounds on top of that. `1e-2` clears the
48/// worst-case resolution with room to spare.
49///
50/// It is still a hundred times smaller than one lattice unit, so it cannot fuse
51/// vertices that belong to distinct integer positions. It can fuse two genuine
52/// skeleton features closer than `1e-2` to each other — see `docs/DESIGN.md` on
53/// what `f32` costs.
54const MERGE_EPS: f32 = 1e-2;
55
56/// Side length of a [`Sim::node_grid`] cell, and its reciprocal.
57///
58/// Exactly [`MERGE_EPS`], which is the largest cell size for which a point's
59/// whole merge neighbourhood is guaranteed to lie within the 3x3 block of cells
60/// around it. A larger cell would need a wider block; a smaller one only spreads
61/// the same nodes over more cells.
62const CELL: f32 = MERGE_EPS;
63const INV_CELL: f32 = 1.0 / CELL;
64
65/// Why a skeleton could not be computed.
66#[derive(Clone, Debug, PartialEq)]
67#[non_exhaustive]
68pub enum SkeletonError {
69    /// The per-edge limit slice did not have one entry per input edge.
70    LimitCountMismatch {
71        /// How many limits were supplied.
72        got: usize,
73        /// How many the polygon needs — one per edge.
74        expected: usize,
75    },
76    /// A per-edge limit was negative or NaN.
77    InvalidLimit {
78        /// The offending edge.
79        edge: EdgeId,
80        /// The value supplied.
81        value: f32,
82    },
83    /// Two collinear neighbouring edges were given different distance limits.
84    ///
85    /// When one stops and the other keeps going, the wavefront would have to
86    /// tear open between two parallel lines at different offsets, and the
87    /// vertex between them has nowhere to be. Give both edges the same limit,
88    /// or separate them with a non-collinear edge.
89    IncompatibleCollinearLimits {
90        /// The edge arriving at the problem vertex.
91        left: EdgeId,
92        /// The edge leaving it.
93        right: EdgeId,
94    },
95    /// The simulation exceeded its event budget.
96    ///
97    /// This is a guard against a non-terminating loop caused by degenerate
98    /// input, and should not be reachable. Please report it as a bug, with the
99    /// polygon that triggered it.
100    EventBudgetExhausted {
101        /// The budget that was exceeded.
102        budget: usize,
103    },
104}
105
106impl fmt::Display for SkeletonError {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            SkeletonError::LimitCountMismatch { got, expected } => write!(
110                f,
111                "got {got} per-edge limits but the polygon has {expected} edges"
112            ),
113            SkeletonError::InvalidLimit { edge, value } => {
114                write!(f, "edge {} has an invalid distance limit {value}", edge.0)
115            }
116            SkeletonError::IncompatibleCollinearLimits { left, right } => write!(
117                f,
118                "collinear edges {} and {} have different distance limits, \
119                 which would tear the wavefront apart",
120                left.0, right.0
121            ),
122            SkeletonError::EventBudgetExhausted { budget } => write!(
123                f,
124                "the wavefront simulation exceeded its budget of {budget} events; \
125                 this is a bug, please report it"
126            ),
127        }
128    }
129}
130
131#[cfg(feature = "std")]
132impl std::error::Error for SkeletonError {}
133
134/// The fixed geometry of one input edge, plus its distance limit.
135///
136/// A wavefront edge is its input edge's supporting line, translated inward. At
137/// time `t` the line is `{ x : normal · x = c + offset_at(t) }`.
138#[derive(Clone, Copy, Debug)]
139struct EdgeState {
140    /// Unit vector along the edge, from its start vertex to its end vertex.
141    dir: Vec2,
142    /// Unit normal pointing into the polygon's interior (i.e. to the left of
143    /// `dir`, per the CCW-outer-ring convention).
144    normal: Vec2,
145    /// The original supporting line's offset: `normal · x = c` at time 0.
146    c: f32,
147    /// How far this edge is allowed to travel. `INFINITY` when unconstrained.
148    limit: f32,
149}
150
151/// How far an edge with this limit has travelled by time `t`.
152///
153/// Free-standing, and taking the limit rather than the edge, because the split
154/// scan reads its limits out of [`EdgeLines`] rather than an [`EdgeState`] and
155/// must apply the very same rule. Two copies of this rule that could drift apart
156/// is a bug waiting to happen; one that both call is not.
157#[inline]
158fn offset_at(limit: f32, t: f32) -> f32 {
159    if t < limit {
160        t
161    } else {
162        limit
163    }
164}
165
166/// An edge's speed at time `t`: 1 while it is still moving, 0 once it has hit
167/// its limit. This is the single mechanism behind the constrained transform.
168///
169/// Free-standing for the same reason as [`offset_at`].
170#[inline]
171fn speed_at(limit: f32, t: f32) -> f32 {
172    if t < limit - EPS {
173        1.0
174    } else {
175        0.0
176    }
177}
178
179impl EdgeState {
180    /// The edge's speed at time `t`.
181    #[inline]
182    fn speed_at(&self, t: f32) -> f32 {
183        speed_at(self.limit, t)
184    }
185}
186
187/// A vertex of the shrinking wavefront.
188///
189/// Wavefront vertices live in an arena and are linked into circular lists (one
190/// per wavefront loop). A split event turns one loop into two; an edge event
191/// shortens a loop.
192#[derive(Clone, Debug)]
193struct WVertex {
194    /// Previous vertex in this wavefront loop.
195    prev: usize,
196    /// Next vertex in this wavefront loop.
197    next: usize,
198    /// Position at time [`WVertex::time`].
199    pos: Vec2,
200    /// The time at which `pos` was sampled.
201    time: f32,
202    /// Constant velocity since `time`.
203    vel: Vec2,
204    /// The input edge arriving here (from `prev`).
205    left: EdgeId,
206    /// The input edge leaving here (toward `next`).
207    right: EdgeId,
208    /// The skeleton node this vertex's current arc grows from.
209    node: NodeId,
210    /// False once the vertex has been consumed by an event.
211    active: bool,
212    /// Bumped whenever this vertex's geometry or links change, so that queued
213    /// events computed from the old state can be recognised as stale.
214    gen: u32,
215    /// Bumped every time this vertex is rescheduled, so that its own previous
216    /// event can be recognised as superseded.
217    evt: u32,
218    /// Input edges this vertex has been shown *not* to split, kept **sorted**.
219    ///
220    /// A vertex travels in a straight line, so it crosses any given edge's
221    /// moving line at exactly one moment. If it was not on a live stretch of
222    /// that edge at that moment, it never will be — the question is settled for
223    /// good, and the edge can be struck off.
224    ///
225    /// Not the rare case it looks like: on star-like input roughly three
226    /// quarters of all split events are rejections, averaging about three per
227    /// reflex vertex. That is what [`SPLIT_FANOUT`] exists to absorb.
228    ///
229    /// Sorted so that striking an edge off can find its place, and dedupe, in
230    /// `O(log r)`. [`Sim::scan_for_split`] does not search this at all — it
231    /// strikes the whole list out of its scratch buffer in one `O(r)` pass,
232    /// rather than test membership per edge inside its inner loop.
233    ///
234    /// Cleared whenever the vertex's velocity changes, since that is a
235    /// different trajectory and every answer has to be asked again.
236    rejected: Vec<EdgeId>,
237    /// [`Sim::split_lower_bound`]'s answer for this vertex's current trajectory.
238    ///
239    /// The scan is the simulation's one non-constant step, and without this it
240    /// is repeated every time the vertex is rescheduled — which a vertex's
241    /// neighbours make it do several times over, for an answer that cannot have
242    /// changed. See [`SplitCache`].
243    split_cache: SplitCache,
244}
245
246/// How many split candidates one scan keeps.
247///
248/// A rejected candidate would otherwise cost a whole fresh scan to replace, and
249/// rejections are not the rare case: on star-like input about three quarters of
250/// all split events are rejections, averaging ~3 per reflex vertex. Keeping the
251/// earliest few turns one scan *per rejection* into one scan per handful.
252///
253/// 8 sits comfortably above that measured mean and costs 64 bytes on a vertex
254/// that already owns a heap allocation.
255///
256/// It is a cache size and nothing else: the value cannot change the skeleton,
257/// only how often the scan reruns. Setting it to 1 — which forces a rescan at
258/// every single rejection, the path this constant exists to avoid — leaves the
259/// whole test suite passing and every shape in the snapshot corpus identical to
260/// within `f32` noise. Worth rechecking that way after touching the cache.
261const SPLIT_FANOUT: usize = 8;
262
263/// A reflex vertex's split lower bounds, remembered across reschedules.
264///
265/// # Why the answer keeps
266///
267/// The crossing time solves `normal · p(t) = c_e(t)` along a fixed trajectory.
268/// Writing the distance to `e`'s moving line at time `u` as
269/// `d(u) = d(t0) + closing * (u - t0)`, with `closing` constant, the crossing
270/// time is
271///
272/// ```text
273///     t = u + d(u) / -closing = t0 - d(t0) / closing
274/// ```
275///
276/// — the `u` cancels. The answer does not depend on *when* it was asked, only on
277/// the trajectory it was asked about, so re-deriving it at each reschedule
278/// recomputes a constant. This is the same fact [`Sim::split_lower_bound`]
279/// already leans on to keep a split's timing independent of the rest of the
280/// wavefront; the cache only stops paying for it twice.
281///
282/// Because the crossing times are absolute and the candidates are consumed in
283/// increasing time order, a rejected candidate is always followed by the next
284/// one in the list — which is why keeping several is worth the room.
285///
286/// # When it stops being true
287///
288/// Only [`Sim::handle_speed_change`] can falsify it, and it does so for *every*
289/// vertex rather than only the ones it moves: a bound is a race between a vertex
290/// and a target edge's line, so an edge stopping changes `closing` for everyone
291/// aiming at it. [`Sim::handle_split_event`] also drops it when it strikes an
292/// edge off, since that answer is now stale by exclusion rather than by
293/// geometry.
294#[derive(Clone, Debug, PartialEq)]
295enum SplitCache {
296    /// Not asked yet for this trajectory.
297    Unknown,
298    /// Asked. `cands[next..len]` are the untried candidates, earliest first.
299    Ready {
300        /// Candidate crossing times and their edges, ascending.
301        cands: [(f32, EdgeId); SPLIT_FANOUT],
302        /// How many of `cands` are meaningful.
303        len: u8,
304        /// How many have already been tried and struck off.
305        next: u8,
306        /// Whether `cands` held *every* candidate rather than the earliest
307        /// [`SPLIT_FANOUT`] of them. Running out of a complete list means there
308        /// is genuinely nothing left to split; running out of a truncated one
309        /// only means it is time to scan again.
310        complete: bool,
311    },
312}
313
314impl WVertex {
315    /// Where this vertex is at time `t`, assuming its velocity is unchanged.
316    #[inline]
317    fn at(&self, t: f32) -> Vec2 {
318        self.pos + self.vel * (t - self.time)
319    }
320}
321
322/// Something that changes the wavefront's structure.
323#[derive(Clone, Copy, Debug)]
324enum EventKind {
325    /// The wavefront edge between `a` and `a.next` collapsed to a point.
326    Edge {
327        /// The vertex at the edge's start.
328        a: usize,
329    },
330    /// Reflex vertex `v` reached input edge `edge`'s moving line.
331    ///
332    /// Only a *candidate*: whether `v` lands on a live stretch of that edge, or
333    /// merely on the infinite line it lies along, is settled when the event is
334    /// popped. See [`Sim::split_lower_bound`].
335    Split {
336        /// The reflex vertex doing the splitting.
337        v: usize,
338        /// The input edge whose line it reaches.
339        edge: EdgeId,
340    },
341    /// An input edge reached its distance limit and stopped moving.
342    SpeedChange {
343        /// The edge that stopped.
344        edge: EdgeId,
345    },
346}
347
348/// A queued event, ordered by time.
349///
350/// # Staleness
351///
352/// Events are never removed from the queue; they are recognised as obsolete
353/// when popped. That takes two independent stamps, and conflating them loses
354/// events:
355///
356/// - `owner` is the vertex whose event this is, stamped with that vertex's
357///   *event serial*. Rescheduling a vertex bumps its serial, so its previous
358///   event is superseded without disturbing anything else.
359/// - `refs` are the vertices whose geometry the event's timing was computed
360///   from, each stamped with its *structural generation*. If one of them moves,
361///   the timing is worthless.
362///
363/// `refs` must stay **O(1)** — at most the owner and its one neighbour — and
364/// that is load-bearing rather than merely tidy. An event whose timing depends
365/// on a distant part of the wavefront is invalidated by anything that happens
366/// there; every reschedule then registers more such dependencies, and the
367/// invalidation feeds back on itself until the simulation is quadratic in its
368/// own bookkeeping.
369///
370/// What keeps `refs` small is [`Sim::split_lower_bound`] asking a weaker
371/// question than "where does this vertex split?", so that a split's timing
372/// depends on nothing but the vertex's own trajectory. Anything added here that
373/// stamps a third vertex should be assumed to reintroduce the feedback.
374#[derive(Clone, Copy, Debug)]
375struct Event {
376    time: f32,
377    kind: EventKind,
378    /// The vertex this event belongs to, and its event serial when queued.
379    owner: (usize, u32),
380    /// Vertices whose geometry this event's timing depends on, with their
381    /// structural generations when queued. At most two.
382    refs: [(usize, u32); 2],
383    /// How many entries of `refs` are meaningful.
384    ref_count: u8,
385}
386
387impl Event {
388    fn new(time: f32, kind: EventKind, owner: (usize, u32), refs: &[(usize, u32)]) -> Self {
389        let mut r = [(0usize, 0u32); 2];
390        r[..refs.len()].copy_from_slice(refs);
391        Event {
392            time,
393            kind,
394            owner,
395            refs: r,
396            ref_count: refs.len() as u8,
397        }
398    }
399}
400
401impl PartialEq for Event {
402    fn eq(&self, other: &Self) -> bool {
403        self.cmp(other) == Ordering::Equal
404    }
405}
406impl Eq for Event {}
407
408impl PartialOrd for Event {
409    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
410        Some(self.cmp(other))
411    }
412}
413
414impl Ord for Event {
415    /// Reversed, so that `BinaryHeap` (a max-heap) yields the *earliest* event.
416    ///
417    /// Times are never NaN by the time an event is queued, but ordering must be
418    /// total regardless, so NaN sorts last rather than panicking.
419    fn cmp(&self, other: &Self) -> Ordering {
420        other
421            .time
422            .partial_cmp(&self.time)
423            .unwrap_or(Ordering::Equal)
424    }
425}
426
427/// Runs the wavefront simulation.
428pub(crate) fn compute(
429    polygon: &Polygon,
430    limits: Option<&[f32]>,
431) -> Result<Skeleton, SkeletonError> {
432    let edges = build_edge_states(polygon, limits)?;
433    let mut sim = Sim::new(polygon, edges)?;
434    sim.run()?;
435    let residual = sim.collect_residual();
436    let mut skel = sim.skeleton;
437    skel.residual = residual;
438    skel.edge_nodes = polygon
439        .edge_ids()
440        .map(|e| {
441            let s = e.start_vertex();
442            [NodeId(s.0 as u32), NodeId(polygon.next_vertex(s).0 as u32)]
443        })
444        .collect();
445    skel.build_adjacency();
446    Ok(skel)
447}
448
449/// Precomputes each edge's direction, inward normal, line offset, and limit.
450fn build_edge_states(
451    polygon: &Polygon,
452    limits: Option<&[f32]>,
453) -> Result<Vec<EdgeState>, SkeletonError> {
454    if let Some(l) = limits {
455        if l.len() != polygon.edge_count() {
456            return Err(SkeletonError::LimitCountMismatch {
457                got: l.len(),
458                expected: polygon.edge_count(),
459            });
460        }
461    }
462
463    let mut states = Vec::with_capacity(polygon.edge_count());
464    for e in polygon.edge_ids() {
465        let (a, b) = polygon.edge(e);
466        // Polygon validation rejects repeated vertices, so no edge is degenerate
467        // and `normalize` cannot fail here.
468        let dir = (b.to_vec2() - a.to_vec2())
469            .normalize()
470            .expect("polygon validation rules out zero-length edges");
471        let normal = dir.perp();
472        let c = normal.dot(a.to_vec2());
473
474        let limit = match limits {
475            None => f32::INFINITY,
476            Some(l) => {
477                let v = l[e.0 as usize];
478                if v.is_nan() || v < 0.0 {
479                    return Err(SkeletonError::InvalidLimit { edge: e, value: v });
480                }
481                v
482            }
483        };
484
485        states.push(EdgeState {
486            dir,
487            normal,
488            c,
489            limit,
490        });
491    }
492    Ok(states)
493}
494
495/// The edges' lines, transposed into one array per component.
496///
497/// Identical data to [`EdgeState`]'s `normal`, `c` and `limit`, laid out for the
498/// one loop that reads all of them for every edge in turn:
499/// [`Sim::scan_for_split`], the simulation's only non-constant step. An array of
500/// `EdgeState` interleaves the fields that loop wants with `dir`, which it does
501/// not, so it reads 24 bytes per edge to use 16 and cannot be vectorised.
502/// Transposed, the loop is a flat pass over contiguous `f32`s.
503///
504/// [`EdgeState`] stays the source of truth; these are built from it once and
505/// never touched again, since an edge's line is fixed for the whole simulation.
506#[derive(Debug)]
507struct EdgeLines {
508    /// Inward unit normals, by component.
509    nx: Vec<f32>,
510    ny: Vec<f32>,
511    /// Original line offsets: `normal · x = c` at time 0.
512    c: Vec<f32>,
513    /// Distance limits, `INFINITY` when unconstrained.
514    limit: Vec<f32>,
515}
516
517impl EdgeLines {
518    fn new(edges: &[EdgeState]) -> Self {
519        EdgeLines {
520            nx: edges.iter().map(|e| e.normal.x).collect(),
521            ny: edges.iter().map(|e| e.normal.y).collect(),
522            c: edges.iter().map(|e| e.c).collect(),
523            limit: edges.iter().map(|e| e.limit).collect(),
524        }
525    }
526}
527
528/// The simulation's mutable state.
529struct Sim<'a> {
530    polygon: &'a Polygon,
531    edges: Vec<EdgeState>,
532    /// [`Sim::edges`], transposed for the split scan.
533    lines: EdgeLines,
534    /// Scratch for the split scan's first pass, one slot per edge. Held here so
535    /// the scan does not allocate on every call.
536    scratch: Vec<f32>,
537    verts: Vec<WVertex>,
538    queue: BinaryHeap<Event>,
539    skeleton: Skeleton,
540    /// Each node's position in full `f32`, parallel to `skeleton.nodes`.
541    ///
542    /// `Node::exact` is narrowed to `f32` for the public API, which is far too
543    /// coarse to decide whether a vertex is standing on its own node. This
544    /// keeps the unnarrowed value for the simulation's own use.
545    node_pos: Vec<Vec2>,
546    /// Interior nodes bucketed by [`CELL`]-sized cell, so [`Sim::node_at`] can
547    /// find the node at a point without scanning every node there is.
548    ///
549    /// Boundary nodes are never merged into, so they are never inserted.
550    node_grid: BTreeMap<(i32, i32), Vec<u32>>,
551    /// Vertices needing a fresh event before the simulation may advance.
552    ///
553    /// Only ever holds a vertex that has just moved or been relinked, and that
554    /// vertex's `prev` — whose edge event is computed from it. Both are O(1) per
555    /// event, which is the whole point.
556    ///
557    /// Deduplicated by [`Sim::in_dirty`]: one event routinely reaches the same
558    /// vertex several ways, and rescheduling it once per route would queue an
559    /// event per route and immediately strand all but the last.
560    dirty: Vec<usize>,
561    /// Whether a vertex is already in [`Sim::dirty`], parallel to `verts`.
562    in_dirty: Vec<bool>,
563    /// `edge_verts[e]` lists every wavefront vertex ever created with `right ==
564    /// e` — that is, the owners of `e`'s wavefront stretches.
565    ///
566    /// A vertex's `left` and `right` are fixed for its whole life, so an entry
567    /// never becomes wrong, only inactive. Ids are appended as the arena grows
568    /// and so stay ascending, which is what lets [`Sim::live_stretch_at`] return
569    /// the same stretch a scan of the whole arena in index order would.
570    edge_verts: Vec<Vec<u32>>,
571    /// Current simulation time, monotonically non-decreasing.
572    now: f32,
573}
574
575impl<'a> Sim<'a> {
576    fn new(polygon: &'a Polygon, edges: Vec<EdgeState>) -> Result<Self, SkeletonError> {
577        let n = polygon.vertex_count();
578        let mut skeleton = Skeleton::default();
579        let mut verts = Vec::with_capacity(n * 2);
580        let mut node_pos: Vec<Vec2> = Vec::with_capacity(n * 2);
581        let mut edge_verts: Vec<Vec<u32>> = vec![Vec::new(); polygon.edge_count()];
582
583        // One wavefront vertex and one boundary node per input vertex.
584        for v in polygon.vertex_ids() {
585            let left = polygon.prev_vertex(v).outgoing_edge();
586            let right = v.outgoing_edge();
587            let pos = polygon.vertex(v).to_vec2();
588            edge_verts[right.0 as usize].push(verts.len() as u32);
589
590            let node = NodeId(skeleton.nodes.len() as u32);
591            node_pos.push(pos);
592            skeleton.nodes.push(Node {
593                position: polygon.vertex(v),
594                exact: [pos.x, pos.y],
595                offset: 0.0,
596                kind: NodeKind::Boundary(v),
597                sources: vec![left, right],
598            });
599
600            verts.push(WVertex {
601                prev: polygon.prev_vertex(v).0 as usize,
602                next: polygon.next_vertex(v).0 as usize,
603                pos,
604                time: 0.0,
605                vel: Vec2::ZERO,
606                left,
607                right,
608                node,
609                active: true,
610                gen: 0,
611                evt: 0,
612                rejected: Vec::new(),
613                split_cache: SplitCache::Unknown,
614            });
615        }
616
617        let mut sim = Sim {
618            polygon,
619            lines: EdgeLines::new(&edges),
620            scratch: vec![0.0; edges.len()],
621            edges,
622            in_dirty: vec![false; verts.len()],
623            edge_verts,
624            verts,
625            queue: BinaryHeap::new(),
626            node_pos,
627            node_grid: BTreeMap::new(),
628            dirty: Vec::new(),
629            skeleton,
630            now: 0.0,
631        };
632
633        for i in 0..n {
634            sim.verts[i].vel = sim.velocity_of(i, 0.0)?;
635        }
636        Ok(sim)
637    }
638
639    /// The velocity a wavefront vertex must have to stay on both of its edges'
640    /// moving lines.
641    ///
642    /// Solving `normal_left · v = speed_left` and `normal_right · v =
643    /// speed_right` is the crate's one unifying idea: with both speeds 1 it
644    /// reproduces the classic angular bisector at `1 / sin(θ/2)`; with one
645    /// speed 0 the vertex slides along the stopped edge; with both 0 it
646    /// freezes. The standard and constrained transforms are the same code.
647    fn velocity_of(&self, i: usize, t: f32) -> Result<Vec2, SkeletonError> {
648        let v = &self.verts[i];
649        let le = self.edges[v.left.0 as usize];
650        let re = self.edges[v.right.0 as usize];
651        let (w1, w2) = (le.speed_at(t), re.speed_at(t));
652        let (n1, n2) = (le.normal, re.normal);
653
654        let det = n1.cross(n2);
655        if det.abs() > PARALLEL_EPS {
656            // Cramer's rule on the 2x2 system.
657            return Ok(Vec2::new(
658                (w1 * n2.y - n1.y * w2) / det,
659                (n1.x * w2 - w1 * n2.x) / det,
660            ));
661        }
662
663        // The normals are parallel, so the two lines never meet and there is no
664        // solution. Which of the two parallel cases this is decides what to do.
665        if n1.dot(n2) < 0.0 {
666            // Antiparallel: the two edges face each other head-on. This vertex
667            // sits on both of their lines, and two antiparallel lines sharing a
668            // point are the *same* line — so the wavefront loop has pinched to
669            // zero width and its interior is empty. The loop is finished; it
670            // just has to be read off. Freezing the vertex is what lets that
671            // happen: it stays put while its loop collapses around it, and
672            // `handle_edge_event` recognises the two-vertex remnant and emits
673            // the ridge between them.
674            //
675            // This is not the 180° spike that polygon validation rejects: that
676            // is an input defect, whereas this arises mid-simulation on inputs
677            // as ordinary as a rectangle, whose skeleton *is* a ridge.
678            return Ok(Vec2::ZERO);
679        }
680
681        if (w1 - w2).abs() < EPS {
682            // Collinear and moving as one; the vertex just rides along.
683            Ok(n1 * w1)
684        } else {
685            // One collinear edge stopped and the other did not: the wavefront
686            // has no consistent shape here. Refuse rather than fabricate.
687            Err(SkeletonError::IncompatibleCollinearLimits {
688                left: v.left,
689                right: v.right,
690            })
691        }
692    }
693
694    /// Whether the wavefront vertex is a notch (interior angle > 180°).
695    ///
696    /// This depends only on the two edges' directions, which never change, so
697    /// it is stable for the vertex's whole life.
698    fn is_reflex(&self, i: usize) -> bool {
699        let v = &self.verts[i];
700        let d1 = self.edges[v.left.0 as usize].dir;
701        let d2 = self.edges[v.right.0 as usize].dir;
702        // Interior on the left, so a right turn is reflex.
703        d1.cross(d2) < -PARALLEL_EPS
704    }
705
706    fn run(&mut self) -> Result<(), SkeletonError> {
707        // Each edge that has a finite limit stops at exactly one moment.
708        for (i, e) in self.edges.iter().enumerate() {
709            if e.limit.is_finite() && e.limit > 0.0 {
710                self.queue.push(Event::new(
711                    e.limit,
712                    EventKind::SpeedChange {
713                        edge: EdgeId(i as u16),
714                    },
715                    (usize::MAX, 0),
716                    &[],
717                ));
718            }
719        }
720
721        for i in 0..self.verts.len() {
722            self.schedule(i)?;
723        }
724
725        // Every event either consumes a vertex or stops an edge, so the total
726        // is linear in the input. The generous multiplier makes this a
727        // bug-catching backstop, not a real limit.
728        let budget = 64 * (self.polygon.vertex_count() + 1) * (self.polygon.vertex_count() + 1);
729        let mut processed = 0usize;
730
731        while let Some(ev) = self.queue.pop() {
732            processed += 1;
733            if processed > budget {
734                return Err(SkeletonError::EventBudgetExhausted { budget });
735            }
736
737            if !matches!(ev.kind, EventKind::SpeedChange { .. }) && !self.is_fresh(&ev) {
738                continue;
739            }
740            // Events are popped in time order, but floating-point error can
741            // produce a time a hair behind `now`; never let the clock go back.
742            self.now = ev.time.max(self.now);
743
744            match ev.kind {
745                EventKind::Edge { a } => self.handle_edge_event(a, ev.time)?,
746                EventKind::Split { v, edge } => self.handle_split_event(v, edge, ev.time)?,
747                EventKind::SpeedChange { edge } => self.handle_speed_change(edge, ev.time)?,
748            }
749
750            // Nothing may advance past this point until every vertex whose
751            // event the last one invalidated has a fresh one queued.
752            while let Some(i) = self.dirty.pop() {
753                self.in_dirty[i] = false;
754                if self.verts[i].active {
755                    self.schedule(i)?;
756                }
757            }
758        }
759        Ok(())
760    }
761
762    /// Whether an event is still the current one for its owner, and still
763    /// computed from up-to-date geometry.
764    fn is_fresh(&self, ev: &Event) -> bool {
765        let (owner, evt) = ev.owner;
766        if !self.verts[owner].active || self.verts[owner].evt != evt {
767            return false;
768        }
769        ev.refs[..ev.ref_count as usize]
770            .iter()
771            .all(|&(i, gen)| self.verts[i].active && self.verts[i].gen == gen)
772    }
773
774    /// Records that this vertex has moved or been relinked.
775    ///
776    /// Two events are computed from a vertex's geometry, and no others: its own,
777    /// and its predecessor's edge event, which watches the two of them converge.
778    /// So marking the vertex and its `prev` is complete — and, being O(1), is
779    /// what keeps a single event from rippling across the whole wavefront.
780    ///
781    /// Split events are *not* in that set. A split's timing is computed from the
782    /// target edge's supporting line, which never moves off its own offset
783    /// track, so no vertex over there can invalidate it. See
784    /// [`Sim::split_lower_bound`].
785    fn touch(&mut self, i: usize) {
786        self.verts[i].gen = self.verts[i].gen.wrapping_add(1);
787        self.mark(i);
788        let prev = self.verts[i].prev;
789        self.mark(prev);
790    }
791
792    /// Queues a vertex for rescheduling once the current event is finished.
793    ///
794    /// Idempotent within an event. That matters for more than tidiness: a single
795    /// event reaches the same vertex through several routes — as the merged
796    /// vertex, as a neighbour's `prev`, and as an explicit mark — and scheduling
797    /// it once per route queues one event per route, of which only the last
798    /// survives. The rest sit in the heap until they are popped and discarded.
799    fn mark(&mut self, i: usize) {
800        if !self.in_dirty[i] {
801            self.in_dirty[i] = true;
802            self.dirty.push(i);
803        }
804    }
805
806    /// Computes and queues the next event for vertex `i`.
807    fn schedule(&mut self, i: usize) -> Result<(), SkeletonError> {
808        if !self.verts[i].active {
809            return Ok(());
810        }
811
812        let mut best: Option<Event> = None;
813        let mut consider = |ev: Option<Event>| {
814            if let Some(e) = ev {
815                if best.as_ref().map_or(true, |b| e.time < b.time) {
816                    best = Some(e);
817                }
818            }
819        };
820
821        consider(self.edge_event(i));
822        if self.is_reflex(i) {
823            consider(self.split_lower_bound(i));
824        }
825
826        // Supersede whatever this vertex had queued before.
827        self.verts[i].evt = self.verts[i].evt.wrapping_add(1);
828        if let Some(mut e) = best {
829            e.owner = (i, self.verts[i].evt);
830            self.queue.push(e);
831        }
832        Ok(())
833    }
834
835    /// When, if ever, the wavefront edge leaving vertex `i` collapses.
836    ///
837    /// Both endpoints of that edge always lie on its supporting line, so the
838    /// question is one-dimensional: track their separation *along* the edge and
839    /// find when it reaches zero. That keeps this exactly linear, with no
840    /// intersection test and no special cases.
841    fn edge_event(&self, i: usize) -> Option<Event> {
842        let a = &self.verts[i];
843        let j = a.next;
844        if j == i {
845            return None;
846        }
847        let b = &self.verts[j];
848
849        let d = self.edges[a.right.0 as usize].dir;
850        let sep = d.dot(b.at(self.now) - a.at(self.now));
851        let rate = d.dot(b.vel - a.vel);
852
853        // Not shrinking (or growing): no collapse.
854        if rate >= -EPS {
855            return None;
856        }
857        let dt = sep / -rate;
858        if !dt.is_finite() || dt < -EPS {
859            return None;
860        }
861        let t = self.now + dt.max(0.0);
862
863        Some(Event::new(
864            t,
865            EventKind::Edge { a: i },
866            (i, 0),
867            &[(i, a.gen), (j, b.gen)],
868        ))
869    }
870
871    /// The earliest moment reflex vertex `i` could possibly split something.
872    ///
873    /// # A lower bound, not an answer
874    ///
875    /// This returns the first time `i` reaches *any* input edge's moving line,
876    /// ignoring entirely whether it lands on a live stretch of that edge or
877    /// merely on the infinite line it lies along. So it can be early. It can
878    /// never be late, which is the only property that matters: a real split
879    /// happens at one of these times, so the smallest of them is a floor under
880    /// the true one, and popping events in time order stays correct.
881    ///
882    /// # Why bother being vague
883    ///
884    /// Because the vagueness is exactly what makes it cheap to keep true. An
885    /// edge's wavefront slides along its own offset track and never leaves it,
886    /// so the *time* `i` meets that track depends only on `i`'s trajectory and
887    /// the edge's original line — both fixed. Nothing happening elsewhere in the
888    /// wavefront can change it.
889    ///
890    /// Deciding here whether the landing is on a live stretch is what would ruin
891    /// that. It would need the current endpoints of the edge, making this
892    /// vertex's event depend on two vertices arbitrarily far away, so that any
893    /// event anywhere invalidated events everywhere — see [`Event`] for why that
894    /// feedback is fatal rather than merely wasteful.
895    ///
896    /// The question is instead settled in [`Sim::handle_split_event`], when the
897    /// event is popped — by which point it is not a prediction at all.
898    ///
899    /// Scans every input edge, so `O(n)`; it is the only non-constant step left
900    /// in the simulation. [`SplitCache`] is what keeps it from being repeated
901    /// for an answer that cannot have changed.
902    fn split_lower_bound(&mut self, i: usize) -> Option<Event> {
903        if matches!(self.verts[i].split_cache, SplitCache::Unknown) {
904            let c = self.scan_for_split(i);
905            self.verts[i].split_cache = c;
906        }
907
908        let SplitCache::Ready {
909            ref cands,
910            len,
911            next,
912            complete,
913        } = self.verts[i].split_cache
914        else {
915            unreachable!("just filled")
916        };
917
918        if next >= len {
919            if complete {
920                return None; // every candidate is spent; this vertex splits nothing
921            }
922            // The list was truncated, so there may be later candidates the scan
923            // never kept. Ask again, now that the tried ones are struck off.
924            let c = self.scan_for_split(i);
925            self.verts[i].split_cache = c;
926            return self.split_lower_bound(i);
927        }
928
929        let (t_cross, edge) = cands[next as usize];
930        let v = &self.verts[i];
931        // The crossing time is absolute, and nothing may be scheduled into the
932        // past; a bound already behind the clock fires at once.
933        Some(Event::new(
934            t_cross.max(self.now),
935            EventKind::Split { v: i, edge },
936            (i, 0),
937            &[(i, v.gen)],
938        ))
939    }
940
941    /// The scan behind [`Sim::split_lower_bound`]: the earliest
942    /// [`SPLIT_FANOUT`] moments `i` reaches any input edge's moving line.
943    ///
944    /// Two passes, because they want opposite things. Computing a crossing time
945    /// is the same handful of arithmetic for every edge with no reason to
946    /// branch, so the first pass does exactly that over [`EdgeLines`]' flat
947    /// arrays and writes `INFINITY` where there is no crossing — no `continue`,
948    /// nothing for the compiler to trip over, one straight vectorisable run.
949    /// Picking the earliest few is all branching and no arithmetic, so it gets
950    /// its own pass, over a contiguous `f32` buffer.
951    fn scan_for_split(&mut self, i: usize) -> SplitCache {
952        let n = self.edges.len();
953        let (p_now, vel) = {
954            let v = &self.verts[i];
955            (v.at(self.now), v.vel)
956        };
957        let now = self.now;
958
959        // Borrowed out so the pass below can write `scratch` while reading
960        // `lines`; handed straight back.
961        let mut scratch = core::mem::take(&mut self.scratch);
962        scratch.resize(n, 0.0);
963        let l = &self.lines;
964
965        // Zipped rather than indexed: equal-length slices walked together carry
966        // no bounds checks, which is the difference between this pass
967        // vectorising and not.
968        let pass = scratch[..n]
969            .iter_mut()
970            .zip(&l.nx[..n])
971            .zip(&l.ny[..n])
972            .zip(&l.c[..n])
973            .zip(&l.limit[..n]);
974
975        for ((((out, &nx), &ny), &c), &limit) in pass {
976            // The edge's line at `now`, and the rate it closes on the vertex.
977            // Interior is on the +normal side, so `dist` starts non-negative.
978            // Both selects compile to branchless moves, so they cost the pass
979            // nothing even in the common case where no limits are set at all.
980            let dist = nx * p_now.x + ny * p_now.y - (c + offset_at(limit, now));
981            let closing = nx * vel.x + ny * vel.y - speed_at(limit, now);
982            let dt = dist / -closing;
983
984            // `closing >= -EPS` means it never reaches this line at all;
985            // `dt < -EPS` means it already passed it. A vertex travels in a
986            // straight line, so either way the question is closed for good.
987            let reachable = closing < -EPS && dt.is_finite() && dt >= -EPS;
988            // Absolute, and deliberately not clamped to `now`: this is cached
989            // and read back at later times, so it must stay the trajectory's own
990            // answer rather than one relative to the clock that happened to ask.
991            *out = if reachable { now + dt } else { f32::INFINITY };
992        }
993
994        // A vertex cannot split the edges it rides on, nor any already ruled
995        // out. Struck out here rather than tested inside the pass above, where
996        // the lookups would serialise it.
997        let v = &self.verts[i];
998        scratch[v.left.0 as usize] = f32::INFINITY;
999        scratch[v.right.0 as usize] = f32::INFINITY;
1000        for &r in &v.rejected {
1001            scratch[r.0 as usize] = f32::INFINITY;
1002        }
1003
1004        let mut cands = [(f32::INFINITY, EdgeId(0)); SPLIT_FANOUT];
1005        let mut len = 0usize;
1006        let mut total = 0usize;
1007
1008        for (k, &t) in scratch[..n].iter().enumerate() {
1009            if t == f32::INFINITY {
1010                continue;
1011            }
1012            total += 1;
1013            if len == SPLIT_FANOUT && t >= cands[SPLIT_FANOUT - 1].0 {
1014                continue; // later than every one already kept
1015            }
1016            // Insertion sort into an 8-slot array. `>` rather than `>=` keeps
1017            // equal times in edge order, so the pick stays deterministic when
1018            // several edges are reached at the same instant.
1019            let mut p = len.min(SPLIT_FANOUT - 1);
1020            while p > 0 && cands[p - 1].0 > t {
1021                cands[p] = cands[p - 1];
1022                p -= 1;
1023            }
1024            cands[p] = (t, EdgeId(k as u16));
1025            len = (len + 1).min(SPLIT_FANOUT);
1026        }
1027
1028        self.scratch = scratch;
1029        SplitCache::Ready {
1030            cands,
1031            len: len as u8,
1032            next: 0,
1033            complete: total <= SPLIT_FANOUT,
1034        }
1035    }
1036
1037    /// The grid cell a position falls in.
1038    #[inline]
1039    fn cell_of(pos: Vec2) -> (i32, i32) {
1040        (floor_i32(pos.x * INV_CELL), floor_i32(pos.y * INV_CELL))
1041    }
1042
1043    /// Adds a node to the skeleton.
1044    fn push_node(&mut self, pos: Vec2, t: f32, kind: NodeKind, sources: Vec<EdgeId>) -> NodeId {
1045        let id = NodeId(self.skeleton.nodes.len() as u32);
1046        self.node_pos.push(pos);
1047        if !matches!(kind, NodeKind::Boundary(_)) {
1048            self.node_grid
1049                .entry(Self::cell_of(pos))
1050                .or_default()
1051                .push(id.0);
1052        }
1053        self.skeleton.nodes.push(Node {
1054            position: Point::from_vec2_rounded(pos),
1055            exact: [pos.x, pos.y],
1056            offset: t,
1057            kind,
1058            sources,
1059        });
1060        id
1061    }
1062
1063    /// Records the arc a wavefront vertex has traced since its last node.
1064    fn emit_arc(&mut self, vertex: usize, to: NodeId) {
1065        let v = &self.verts[vertex];
1066        let from = v.node;
1067        if from == to {
1068            return; // zero-length arc, nothing traced
1069        }
1070        let sources = [v.left, v.right];
1071        self.skeleton.arcs.push(Arc {
1072            nodes: [from, to],
1073            sources,
1074        });
1075    }
1076
1077    /// The maximal run of consecutive wavefront vertices that all sit on `pos`
1078    /// at time `t`, starting from the adjacent pair `(ia, ib)`.
1079    ///
1080    /// Several wavefront edges often vanish at the very same instant and place
1081    /// — a square's four corners all reach its centre together. Textbooks call
1082    /// that a *vertex event*, and handling it as a cascade of two-vertex merges
1083    /// does not work: fusing just two of the square's corners would leave a
1084    /// vertex trapped between two antiparallel edges. Collecting the whole
1085    /// coincident run and retiring it in one go handles the general case and
1086    /// the simultaneous case with the same code.
1087    fn coincident_chain(&self, ia: usize, ib: usize, t: f32, pos: Vec2) -> Vec<usize> {
1088        let mut chain = vec![ia, ib];
1089
1090        // Walk backward, then forward, while neighbours are at the same point.
1091        // The `contains` guard stops the walk from lapping a fully coincident
1092        // loop and revisiting where it started.
1093        loop {
1094            let p = self.verts[chain[0]].prev;
1095            if !self.verts[p].active || chain.contains(&p) {
1096                break;
1097            }
1098            if (self.verts[p].at(t) - pos).length() > MERGE_EPS {
1099                break;
1100            }
1101            chain.insert(0, p);
1102        }
1103        loop {
1104            let n = self.verts[chain[chain.len() - 1]].next;
1105            if !self.verts[n].active || chain.contains(&n) {
1106                break;
1107            }
1108            if (self.verts[n].at(t) - pos).length() > MERGE_EPS {
1109                break;
1110            }
1111            chain.push(n);
1112        }
1113        chain
1114    }
1115
1116    /// One or more adjacent vertices met at a point: the edges between them are
1117    /// gone.
1118    fn handle_edge_event(&mut self, ia: usize, t: f32) -> Result<(), SkeletonError> {
1119        let ib = self.verts[ia].next;
1120        if !self.verts[ib].active || ib == ia {
1121            return Ok(());
1122        }
1123
1124        // Seed from the colliding pair, then gather everything else arriving at
1125        // the same instant and place.
1126        let seed = (self.verts[ia].at(t) + self.verts[ib].at(t)) * 0.5;
1127        let chain = self.coincident_chain(ia, ib, t, seed);
1128
1129        // Averaging every arriving vertex's prediction spreads the
1130        // floating-point error rather than inheriting one vertex's.
1131        let pos = chain
1132            .iter()
1133            .fold(Vec2::ZERO, |acc, &i| acc + self.verts[i].at(t))
1134            * (1.0 / chain.len() as f32);
1135
1136        let mut sources = Vec::with_capacity(chain.len() + 1);
1137        for &i in &chain {
1138            sources.push(self.verts[i].left);
1139            sources.push(self.verts[i].right);
1140        }
1141        sources.sort_unstable();
1142        sources.dedup();
1143
1144        let node = self.node_at(pos, t, NodeKind::EdgeEvent, sources);
1145        for &i in &chain {
1146            self.emit_arc(i, node);
1147        }
1148
1149        let first = chain[0];
1150        let last = chain[chain.len() - 1];
1151        let iprev = self.verts[first].prev;
1152        let inext = self.verts[last].next;
1153        let (left, right) = (self.verts[first].left, self.verts[last].right);
1154
1155        let whole_loop = chain.contains(&iprev);
1156        for &i in &chain {
1157            self.deactivate(i);
1158        }
1159        if whole_loop {
1160            // The chain was the entire loop: every arc is recorded and there is
1161            // nothing left to move.
1162            return Ok(());
1163        }
1164
1165        // The chain fuses into one vertex inheriting the outermost edges.
1166        let merged = self.spawn(WVertex {
1167            prev: iprev,
1168            next: inext,
1169            pos,
1170            time: t,
1171            vel: Vec2::ZERO,
1172            left,
1173            right,
1174            node,
1175            active: true,
1176            gen: 0,
1177            evt: 0,
1178            rejected: Vec::new(),
1179            split_cache: SplitCache::Unknown,
1180        });
1181        self.verts[iprev].next = merged;
1182        self.verts[inext].prev = merged;
1183        self.touch(iprev);
1184        self.touch(inext);
1185
1186        if self.is_stalled(merged) {
1187            // The merge left the loop somewhere no ordinary event can advance
1188            // it; zip it shut rather than let the simulation stall.
1189            return self.resolve_needle(merged, t);
1190        }
1191
1192        self.verts[merged].vel = self.velocity_of(merged, t)?;
1193
1194        self.mark(merged);
1195        self.mark(iprev);
1196        self.mark(inext);
1197        Ok(())
1198    }
1199
1200    /// Whether a vertex's two edges face each other head-on.
1201    ///
1202    /// Such a vertex lies on both edges' lines at once, and two antiparallel
1203    /// lines through a common point are the same line — so its whole loop has
1204    /// flattened. See [`Sim::resolve_needle`].
1205    fn edges_antiparallel(&self, i: usize) -> bool {
1206        let v = &self.verts[i];
1207        let n1 = self.edges[v.left.0 as usize].normal;
1208        let n2 = self.edges[v.right.0 as usize].normal;
1209        n1.cross(n2).abs() <= PARALLEL_EPS && n1.dot(n2) < 0.0
1210    }
1211
1212    /// Whether a vertex has reached a state no ordinary event can advance, and
1213    /// so must be zipped shut by [`Sim::resolve_needle`].
1214    ///
1215    /// Two cases, and both stall the simulation if ignored. A needle's folded
1216    /// edges are parallel, so they can never collapse. A two-vertex loop has
1217    /// both of its edges spanning the same pair of points, so it encloses
1218    /// nothing and cannot shrink either.
1219    fn is_stalled(&self, i: usize) -> bool {
1220        self.edges_antiparallel(i) || self.verts[i].prev == self.verts[i].next
1221    }
1222
1223    /// Zips up a *needle*: a wavefront vertex whose two edges have met head-on.
1224    ///
1225    /// # What a needle is
1226    ///
1227    /// When a vertex's two edges are antiparallel, both of their lines pass
1228    /// through it, and two antiparallel lines sharing a point are the *same*
1229    /// line. The two edges have collided, the strip of material between them is
1230    /// gone, and the wavefront has folded back on itself: `prev` and `next` are
1231    /// both collinear with the vertex and lie on the *same* side of it.
1232    ///
1233    /// ```text
1234    ///     prev  o<---------------------o m          prev  o
1235    ///           o--------------------->'                  |
1236    ///     next                                            |   the strip is gone;
1237    ///                                          ==>        |   what is left is
1238    ///     the two edges have collided and now             |   one skeleton arc
1239    ///     lie on top of one another                 next  o
1240    /// ```
1241    ///
1242    /// It arises on thoroughly ordinary input: a rectangle's long sides collide
1243    /// to leave its ridge, and any hole 2d from a wall pinches the strip
1244    /// between them shut.
1245    ///
1246    /// # Why it needs its own handling
1247    ///
1248    /// The folded edges are parallel, so no further edge event can ever fire on
1249    /// them. Left alone the simulation simply stalls with the loop still live,
1250    /// silently dropping every arc it had yet to trace.
1251    ///
1252    /// # How it resolves
1253    ///
1254    /// The overlap runs from the vertex to whichever of `prev`/`next` is
1255    /// nearer, and that overlap is exactly one skeleton arc, bisecting the two
1256    /// edges that collided. Emit it, retire the arm (or both arms, when they
1257    /// are the same length), and splice what remains back into the loop. The
1258    /// splice can expose another needle, so this repeats until it does not.
1259    fn resolve_needle(&mut self, start: usize, t: f32) -> Result<(), SkeletonError> {
1260        let mut m = start;
1261        loop {
1262            let prev = self.verts[m].prev;
1263            let next = self.verts[m].next;
1264            if prev == m || next == m || !self.verts[prev].active || !self.verts[next].active {
1265                self.deactivate(m);
1266                return Ok(());
1267            }
1268
1269            let pm = self.verts[m].at(t);
1270
1271            // A two-vertex loop is the end of the line: emit its last segment.
1272            if prev == next {
1273                let p = self.verts[prev].at(t);
1274                let node = self.node_at(p, t, NodeKind::EdgeEvent, Vec::new());
1275                self.add_sources(node, m);
1276                self.add_sources(node, prev);
1277                self.emit_arc(m, node);
1278                self.emit_arc(prev, node);
1279                self.deactivate(m);
1280                self.deactivate(prev);
1281                return Ok(());
1282            }
1283
1284            let s = (self.verts[prev].at(t) - pm).length();
1285            let u = (self.verts[next].at(t) - pm).length();
1286            let take_prev = s <= u + MERGE_EPS;
1287            let take_next = u <= s + MERGE_EPS;
1288            let target = if take_prev {
1289                self.verts[prev].at(t)
1290            } else {
1291                self.verts[next].at(t)
1292            };
1293
1294            let node = self.node_at(target, t, NodeKind::EdgeEvent, Vec::new());
1295            self.add_sources(node, m);
1296            // The collapsed strip itself is a skeleton arc, bisecting the two
1297            // edges that just collided.
1298            self.emit_arc(m, node);
1299            let (mut new_left, mut new_right) = (self.verts[m].left, self.verts[m].right);
1300            self.deactivate(m);
1301
1302            let (mut lo, mut hi) = (prev, next);
1303            if take_prev {
1304                self.emit_arc(prev, node);
1305                self.add_sources(node, prev);
1306                new_left = self.verts[prev].left;
1307                lo = self.verts[prev].prev;
1308                self.deactivate(prev);
1309            }
1310            if take_next {
1311                self.emit_arc(next, node);
1312                self.add_sources(node, next);
1313                new_right = self.verts[next].right;
1314                hi = self.verts[next].next;
1315                self.deactivate(next);
1316            }
1317
1318            if !self.verts[lo].active || !self.verts[hi].active {
1319                return Ok(()); // the loop is spent
1320            }
1321
1322            let nv = self.spawn(WVertex {
1323                prev: lo,
1324                next: hi,
1325                pos: target,
1326                time: t,
1327                vel: Vec2::ZERO,
1328                left: new_left,
1329                right: new_right,
1330                node,
1331                active: true,
1332                gen: 0,
1333                evt: 0,
1334                rejected: Vec::new(),
1335                split_cache: SplitCache::Unknown,
1336            });
1337            self.verts[lo].next = nv;
1338            self.verts[hi].prev = nv;
1339            self.touch(lo);
1340            self.touch(hi);
1341
1342            if self.is_stalled(nv) {
1343                // Zipping one needle shut exposed another stall.
1344                m = nv;
1345                continue;
1346            }
1347
1348            self.verts[nv].vel = self.velocity_of(nv, t)?;
1349            self.mark(nv);
1350            self.mark(lo);
1351            self.mark(hi);
1352            return Ok(());
1353        }
1354    }
1355
1356    /// Adds a wavefront vertex's two edges to a node's source list.
1357    fn add_sources(&mut self, node: NodeId, vertex: usize) {
1358        let (l, r) = (self.verts[vertex].left, self.verts[vertex].right);
1359        let sources = &mut self.skeleton.nodes[node.0 as usize].sources;
1360        sources.push(l);
1361        sources.push(r);
1362        sources.sort_unstable();
1363        sources.dedup();
1364    }
1365
1366    /// The interior node at `pos`, creating one only if there is not one there
1367    /// already.
1368    ///
1369    /// Several events can converge on a single point: a hole sitting
1370    /// symmetrically between two walls pinches both strips shut at the same
1371    /// instant and place. Each would otherwise mint its own node, leaving
1372    /// duplicates stacked at one position and a graph disconnected where it
1373    /// should not be. Two skeleton nodes at the same point *are* the same node,
1374    /// so reusing one is exact, not an approximation.
1375    ///
1376    /// Boundary nodes are never reused: each belongs to a named input vertex.
1377    ///
1378    /// Only the 3x3 block of grid cells around `pos` is searched. A cell is
1379    /// [`MERGE_EPS`] across, so nothing within [`MERGE_EPS`] of `pos` can lie
1380    /// outside that block — the answer is the same one a scan of every node
1381    /// would give, including which node is picked when several are in range.
1382    fn node_at(&mut self, pos: Vec2, t: f32, kind: NodeKind, sources: Vec<EdgeId>) -> NodeId {
1383        let (cx, cy) = Self::cell_of(pos);
1384        let mut found: Option<u32> = None;
1385        for gx in cx - 1..=cx + 1 {
1386            for gy in cy - 1..=cy + 1 {
1387                let Some(bucket) = self.node_grid.get(&(gx, gy)) else {
1388                    continue;
1389                };
1390                for &i in bucket {
1391                    // Squared, to keep a square root out of the inner loop.
1392                    if (self.node_pos[i as usize] - pos).length_squared() <= MERGE_EPS * MERGE_EPS {
1393                        // Lowest id wins, matching what a scan in node order
1394                        // returned when several nodes are within range.
1395                        found = Some(found.map_or(i, |b| b.min(i)));
1396                    }
1397                }
1398            }
1399        }
1400
1401        if let Some(i) = found {
1402            let n = &mut self.skeleton.nodes[i as usize];
1403            n.sources.extend(sources);
1404            n.sources.sort_unstable();
1405            n.sources.dedup();
1406            return NodeId(i);
1407        }
1408        self.push_node(pos, t, kind, sources)
1409    }
1410
1411    /// Finds the live stretch of `edge` that `pos` is standing on at time `t`.
1412    ///
1413    /// An input edge can own several wavefront edges at once — every split of it
1414    /// leaves one more — so this asks which, if any, of them `pos` is actually
1415    /// on.
1416    ///
1417    /// This is an *observation*, not a prediction, and that distinction is the
1418    /// point. Events are popped in time order, so by the time this runs every
1419    /// event before `t` has been processed and the wavefront's shape at `t` is
1420    /// settled fact. The same question asked when the event was queued would
1421    /// have been a guess about a future that later events could change.
1422    fn live_stretch_at(&self, edge: EdgeId, pos: Vec2, t: f32) -> Option<usize> {
1423        let e = &self.edges[edge.0 as usize];
1424        // Only this edge's own stretch owners, rather than the whole arena.
1425        for &j in &self.edge_verts[edge.0 as usize] {
1426            let j = j as usize;
1427            let a = &self.verts[j];
1428            if !a.active {
1429                continue;
1430            }
1431            let b = &self.verts[a.next];
1432            let (pa, pb) = (a.at(t), b.at(t));
1433            let span = e.dir.dot(pb - pa);
1434            if span <= EPS {
1435                continue; // this stretch has already shrunk away
1436            }
1437            let along = e.dir.dot(pos - pa);
1438            if along >= -EPS && along <= span + EPS {
1439                return Some(j);
1440            }
1441        }
1442        None
1443    }
1444
1445    /// A reflex vertex reached an opposing edge's line. If it landed on a live
1446    /// stretch of that edge, it tears the wavefront in two.
1447    fn handle_split_event(&mut self, iv: usize, eo: EdgeId, t: f32) -> Result<(), SkeletonError> {
1448        if !self.verts[iv].active {
1449            return Ok(());
1450        }
1451
1452        let pos = self.verts[iv].at(t);
1453
1454        // Now the guess gets checked. `now == t`, so this is what the wavefront
1455        // genuinely looks like, not a forecast.
1456        let Some(iopp) = self.live_stretch_at(eo, pos, t) else {
1457            // It came down on the line but off the end of every live stretch of
1458            // it, so no split happens here. A vertex moves in a straight line
1459            // and so meets this line only once: the question is closed for good,
1460            // and the edge can be struck off before asking for the next
1461            // candidate.
1462            let v = &mut self.verts[iv];
1463            if let Err(at) = v.rejected.binary_search(&eo) {
1464                v.rejected.insert(at, eo); // keep it sorted for the binary search
1465            }
1466            // The bound just struck off is the one the cache was handing out, so
1467            // step past it. The next candidate is already known unless the scan
1468            // truncated, which is the whole point of keeping more than one.
1469            match &mut v.split_cache {
1470                SplitCache::Ready {
1471                    cands, len, next, ..
1472                } if *next < *len && cands[*next as usize].1 == eo => {
1473                    *next += 1;
1474                }
1475                // The struck-off edge was not the one on offer, so the cache is
1476                // about something else and cannot be stepped past coherently.
1477                c => *c = SplitCache::Unknown,
1478            }
1479            self.mark(iv);
1480            return Ok(());
1481        };
1482
1483        let ib = self.verts[iopp].next;
1484        if !self.verts[ib].active {
1485            return Ok(());
1486        }
1487        let (v_left, v_right, v_prev, v_next) = {
1488            let v = &self.verts[iv];
1489            (v.left, v.right, v.prev, v.next)
1490        };
1491
1492        let mut sources = vec![v_left, v_right, eo];
1493        sources.sort_unstable();
1494        sources.dedup();
1495        let node = self.node_at(pos, t, NodeKind::SplitEvent, sources);
1496        self.emit_arc(iv, node);
1497        self.deactivate(iv);
1498
1499        // The wavefront loop `... -> v_prev -> v -> v_next -> ... -> opp -> b -> ...`
1500        // becomes two loops. `v1` closes the chain that runs from `b` around to
1501        // `v_prev`; `v2` closes the chain from `v_next` around to `opp`.
1502        let v1 = self.spawn(WVertex {
1503            prev: v_prev,
1504            next: ib,
1505            pos,
1506            time: t,
1507            vel: Vec2::ZERO,
1508            left: v_left,
1509            right: eo,
1510            node,
1511            active: true,
1512            gen: 0,
1513            evt: 0,
1514            rejected: Vec::new(),
1515            split_cache: SplitCache::Unknown,
1516        });
1517        let v2 = self.spawn(WVertex {
1518            prev: iopp,
1519            next: v_next,
1520            pos,
1521            time: t,
1522            vel: Vec2::ZERO,
1523            left: eo,
1524            right: v_right,
1525            node,
1526            active: true,
1527            gen: 0,
1528            evt: 0,
1529            rejected: Vec::new(),
1530            split_cache: SplitCache::Unknown,
1531        });
1532
1533        self.verts[v_prev].next = v1;
1534        self.verts[ib].prev = v1;
1535        self.verts[iopp].next = v2;
1536        self.verts[v_next].prev = v2;
1537        self.touch(v_prev);
1538        self.touch(ib);
1539        self.touch(iopp);
1540        self.touch(v_next);
1541
1542        // A split can flatten either of the two new loops — for instance when a
1543        // reflex vertex lands on the far wall of a narrow channel, pinching
1544        // that side shut against the edge opposite.
1545        let flat_1 = self.is_stalled(v1);
1546        let flat_2 = self.is_stalled(v2);
1547        if flat_1 {
1548            self.resolve_needle(v1, t)?;
1549        } else {
1550            self.verts[v1].vel = self.velocity_of(v1, t)?;
1551        }
1552        if flat_2 {
1553            self.resolve_needle(v2, t)?;
1554        } else {
1555            self.verts[v2].vel = self.velocity_of(v2, t)?;
1556        }
1557
1558        for i in [v1, v2, v_prev, ib, iopp, v_next] {
1559            self.mark(i);
1560        }
1561        Ok(())
1562    }
1563
1564    /// An edge hit its distance limit and stopped.
1565    ///
1566    /// Every active vertex's trajectory can bend here, so each one gets a node
1567    /// (a kink in its arc) and a fresh velocity, and every split bound in the
1568    /// wavefront is invalidated — a bound is a race against a target edge's
1569    /// line, and one of those lines just stopped.
1570    ///
1571    /// Touching *everything* is heavy-handed, and deliberately so. Working out
1572    /// precisely which vertices still had valid bounds would be subtle
1573    /// bookkeeping guarding the invariant that keeps the whole simulation
1574    /// correct, for no real gain: a polygon has at most one stop per distinct
1575    /// limit, and the common cases — no limits at all, or one uniform limit —
1576    /// run this zero or one times.
1577    fn handle_speed_change(&mut self, _edge: EdgeId, t: f32) -> Result<(), SkeletonError> {
1578        let n = self.verts.len();
1579        for i in 0..n {
1580            if !self.verts[i].active {
1581                continue;
1582            }
1583            let new_vel = self.velocity_of(i, t)?;
1584            let old_vel = self.verts[i].vel;
1585
1586            if (new_vel - old_vel).length_squared() > EPS * EPS {
1587                // The trajectory bends here, so the arc it was tracing ends and
1588                // a new one begins.
1589                let pos = self.verts[i].at(t);
1590                let sources = {
1591                    let v = &self.verts[i];
1592                    let mut s = vec![v.left, v.right];
1593                    s.sort_unstable();
1594                    s.dedup();
1595                    s
1596                };
1597                let node = self.node_at(pos, t, NodeKind::LimitReached, sources);
1598                self.emit_arc(i, node);
1599
1600                let v = &mut self.verts[i];
1601                v.pos = pos;
1602                v.time = t;
1603                v.vel = new_vel;
1604                v.node = node;
1605                // A new trajectory: every edge struck off was struck off for a
1606                // path this vertex is no longer on, so ask again.
1607                v.rejected.clear();
1608            }
1609            // Unconditional, unlike the above. A split bound is a race between a
1610            // vertex and a *target* edge's line, so the edge that just stopped
1611            // changes the answer for every vertex aiming at it — including the
1612            // ones standing still, whose own velocity did not move.
1613            self.verts[i].split_cache = SplitCache::Unknown;
1614            self.touch(i);
1615        }
1616        Ok(())
1617    }
1618
1619    /// The wavefront loops still standing once the queue has run dry.
1620    ///
1621    /// # Why anything is left
1622    ///
1623    /// A moving wavefront always has a next event: its edges are closing on one
1624    /// another, so something collapses eventually. The only way a loop outlives
1625    /// the queue is if every vertex on it has stopped — which needs every edge
1626    /// around it to have hit its limit, since [`Sim::velocity_of`] only returns
1627    /// zero when both its edges have. So this is empty for a plain skeleton, and
1628    /// non-empty exactly where limits bound hard enough to freeze a whole loop.
1629    ///
1630    /// (The other way to sit still is a needle's antiparallel pair, but
1631    /// [`Sim::resolve_needle`] retires those on the spot rather than leaving
1632    /// them for the queue to not deliver.)
1633    ///
1634    /// Each surviving vertex's `node` is already the node its arc stopped at, so
1635    /// there is nothing to compute here: the loops are read straight off the
1636    /// `prev`/`next` links, which have kept the input's winding all along.
1637    fn collect_residual(&self) -> Vec<ResidualLoop> {
1638        let mut seen = vec![false; self.verts.len()];
1639        let mut loops = Vec::new();
1640
1641        for start in 0..self.verts.len() {
1642            if seen[start] || !self.verts[start].active {
1643                continue;
1644            }
1645
1646            let mut nodes = Vec::new();
1647            let mut edges = Vec::new();
1648            let mut i = start;
1649            loop {
1650                seen[i] = true;
1651                nodes.push(self.verts[i].node);
1652                // The wavefront edge leaving this vertex belongs to `right`, so
1653                // the segment from `nodes[k]` to `nodes[k + 1]` is `right`'s.
1654                edges.push(self.verts[i].right);
1655                i = self.verts[i].next;
1656                if i == start || seen[i] || !self.verts[i].active {
1657                    break;
1658                }
1659            }
1660
1661            // A loop needs three corners to enclose anything. Anything shorter
1662            // is a remnant the simulation should have retired, so drop it rather
1663            // than hand out a degenerate polygon.
1664            if nodes.len() >= 3 {
1665                loops.push(ResidualLoop { nodes, edges });
1666            }
1667        }
1668        loops
1669    }
1670
1671    fn spawn(&mut self, v: WVertex) -> usize {
1672        let id = self.verts.len();
1673        self.edge_verts[v.right.0 as usize].push(id as u32);
1674        self.verts.push(v);
1675        self.in_dirty.push(false);
1676        id
1677    }
1678
1679    fn deactivate(&mut self, i: usize) {
1680        self.verts[i].active = false;
1681        self.touch(i);
1682    }
1683}
1684
1685#[cfg(test)]
1686mod tests {
1687    use super::*;
1688
1689    #[test]
1690    fn events_pop_earliest_first() {
1691        let mut q = BinaryHeap::new();
1692        for t in [5.0, 1.0, 3.0, 9.0, 2.0] {
1693            q.push(Event::new(t, EventKind::Edge { a: 0 }, (0, 0), &[]));
1694        }
1695        let mut got = Vec::new();
1696        while let Some(e) = q.pop() {
1697            got.push(e.time);
1698        }
1699        assert_eq!(got, vec![1.0, 2.0, 3.0, 5.0, 9.0]);
1700    }
1701
1702    #[test]
1703    fn event_ordering_is_total_even_with_nan() {
1704        // Ord must not panic; NaN simply sorts last.
1705        let a = Event::new(f32::NAN, EventKind::Edge { a: 0 }, (0, 0), &[]);
1706        let b = Event::new(1.0, EventKind::Edge { a: 0 }, (0, 0), &[]);
1707        let _ = a.cmp(&b);
1708        let _ = b.cmp(&a);
1709    }
1710
1711    #[test]
1712    fn edge_speed_drops_at_the_limit() {
1713        assert_eq!(speed_at(3.0, 0.0), 1.0);
1714        assert_eq!(speed_at(3.0, 2.9), 1.0);
1715        assert_eq!(speed_at(3.0, 3.0), 0.0);
1716        assert_eq!(speed_at(3.0, 9.0), 0.0);
1717
1718        assert_eq!(offset_at(3.0, 1.0), 1.0);
1719        assert_eq!(offset_at(3.0, 3.0), 3.0);
1720        assert_eq!(offset_at(3.0, 9.0), 3.0, "offset clamps at the limit");
1721    }
1722
1723    #[test]
1724    fn unconstrained_edge_never_stops() {
1725        assert_eq!(speed_at(f32::INFINITY, 1e9), 1.0);
1726        assert_eq!(offset_at(f32::INFINITY, 1e9), 1e9);
1727    }
1728
1729    /// `EdgeState` and `EdgeLines` hold the same limits and must apply them the
1730    /// same way — the split scan reads one, the velocity solve the other, and a
1731    /// disagreement between them would be a wavefront that moved at one speed
1732    /// and was predicted at another.
1733    #[test]
1734    fn edge_state_and_edge_lines_agree() {
1735        let states: Vec<EdgeState> = [3.0, f32::INFINITY, 0.0]
1736            .iter()
1737            .map(|&limit| EdgeState {
1738                dir: Vec2::new(1.0, 0.0),
1739                normal: Vec2::new(0.0, 1.0),
1740                c: 7.0,
1741                limit,
1742            })
1743            .collect();
1744        let lines = EdgeLines::new(&states);
1745
1746        for (k, e) in states.iter().enumerate() {
1747            assert_eq!(lines.nx[k], e.normal.x);
1748            assert_eq!(lines.ny[k], e.normal.y);
1749            assert_eq!(lines.c[k], e.c);
1750            assert_eq!(lines.limit[k], e.limit);
1751            for t in [0.0f32, 1.0, 2.9, 3.0, 9.0, 1e9] {
1752                assert_eq!(speed_at(lines.limit[k], t), e.speed_at(t));
1753            }
1754        }
1755    }
1756
1757    #[test]
1758    fn vertex_position_extrapolates_linearly() {
1759        let v = WVertex {
1760            prev: 0,
1761            next: 0,
1762            pos: Vec2::new(1.0, 2.0),
1763            time: 1.0,
1764            vel: Vec2::new(3.0, -1.0),
1765            left: EdgeId(0),
1766            right: EdgeId(1),
1767            node: NodeId(0),
1768            active: true,
1769            gen: 0,
1770            evt: 0,
1771            rejected: Vec::new(),
1772            split_cache: SplitCache::Unknown,
1773        };
1774        assert_eq!(v.at(1.0), Vec2::new(1.0, 2.0));
1775        assert_eq!(v.at(3.0), Vec2::new(7.0, 0.0));
1776    }
1777}