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}