map_tile_toolkit/mosaic.rs
1//! Stateful reassembly of sliced tiles back into whole features — the accumulating inverse of the
2//! [`Slicer`](crate::Slicer).
3//!
4//! [`Mosaic`] rejoins **polyline** tiles ([`Slicer::line`](crate::Slicer::line)); [`PolygonMosaic`] rejoins
5//! **polygon** tiles ([`Slicer::polygon`](crate::Slicer::polygon)). Both take each tile's geometry
6//! in its local frame, rebase it into the global frame (`local + tile·extent`), and link geometry
7//! across borders by shared **directed edges**. [`add`](Mosaic::add) rejects a tile inconsistent with
8//! those already present — naming the ones it conflicts with, and leaving the mosaic unchanged — in
9//! two ways:
10//!
11//! - **Payload:** slicing duplicates a border-crossing segment identically (position **and** payload)
12//! in every tile that carries it, so a shared edge whose vertices *disagree* is a conflict. (A single
13//! position may be shared by several features — edges only collide when their whole segment does — so
14//! unrelated features meeting at a point never conflict.)
15//! - **Membership:** every tile is complete in its own *core* (the cell it owns), so an edge with an
16//! endpoint in a present tile's core must be carried by that tile. A present tile that owns an
17//! endpoint but lacks the edge — a line spanning into a neighbor the neighbor never corroborates,
18//! or two tiles disagreeing at a shared junction — is a conflict. A buffer only widens the overlap
19//! two tiles share, and the whole overlap must match; any *disagreement* there surfaces here too,
20//! because the differing vertex lands in some tile's core. Only a pure *omission* of redundant
21//! overlap geometry goes unflagged — and that is harmless, since the core tile still supplies the
22//! edge and duplicates collapse on reassembly.
23//!
24//! [`iter_features`](Mosaic::iter_features) re-chains the distinct edges into maximal chains
25//! (`stitch`) — the same reconstruction a from-scratch merge of all tiles would produce.
26//!
27//! The two mosaics differ only in how a tile's input becomes directed edges:
28//!
29//! - [`Mosaic`] uses every consecutive vertex pair of each **run**. It needs only the `extent`.
30//! - [`PolygonMosaic`] takes each tile's **rings** and keeps only the edges that **touch** the tile's
31//! buffered box `B` (design doc §7). This drops all the slicer's synthetic geometry — the
32//! clip-boundary corners, fill boxes, and gap-bridging chords, which are routed strictly *outside*
33//! `B` — while every real ring edge survives in the tile that owns its start vertex (whose cell lies
34//! in `B`, so the edge touches `B` there). The *same* edge machinery then reassembles the rings, with
35//! orientation and holes falling out of the directed edges. Testing the edge rather than the vertex
36//! is what catches a bridge chord drawn directly between two *original* crossing vertices, which no
37//! per-vertex tag could. Because it re-derives `B`, it needs the `buffer` as well as the `extent`. A
38//! tile lying entirely inside a polygon (an all-synthetic fill) contributes no edges — correct, since
39//! a reassembled polygon's interior is implied by its winding, not by fill tiles.
40
41use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
42
43use geo_types::Coord;
44
45use crate::TileError;
46use crate::clip_polyline::segment_intersects;
47use crate::grid::Grid;
48use crate::tile::{TileId, tile_of};
49use crate::vertex::Vertex;
50
51/// A global directed edge's two vertices (identical across every tile that carries it) and the set of
52/// tiles that do.
53#[derive(Debug, Clone)]
54struct Edge<V> {
55 a: V,
56 b: V,
57 tiles: BTreeSet<TileId>,
58}
59
60/// A directed edge keyed by its endpoint positions.
61type EdgeKey = (Coord<i32>, Coord<i32>);
62
63/// The shared global directed-edge index behind both mosaics: it holds the cross-tile edge map and
64/// per-tile bookkeeping, detects conflicts, and re-chains features — everything except how a tile's
65/// raw geometry is turned into directed edges (runs for [`Mosaic`], synthetic-filtered rings for
66/// [`PolygonMosaic`]).
67#[derive(Debug, Clone)]
68struct EdgeIndex<V: Vertex> {
69 /// Global directed-edge index, keyed by endpoint positions. Every tile at a key holds the same
70 /// vertices (conflicts are rejected on commit).
71 edges: HashMap<EdgeKey, Edge<V>>,
72 /// The edge keys each tile contributed, for `O(tile)` purge and self-exclusion when re-adding.
73 tile_edges: BTreeMap<TileId, Vec<EdgeKey>>,
74}
75
76impl<V: Vertex> EdgeIndex<V> {
77 fn new() -> Self {
78 Self {
79 edges: HashMap::new(),
80 tile_edges: BTreeMap::new(),
81 }
82 }
83
84 /// Replace `tile`'s contribution with `new_edges` (its distinct directed edges, already rebased
85 /// into the global frame), after rejecting any conflict with the other tiles. `ext` is the signed
86 /// extent, used to map an endpoint to its owner tile for the membership check. On conflict nothing
87 /// is mutated.
88 fn commit(
89 &mut self,
90 tile: TileId,
91 new_edges: HashMap<EdgeKey, (V, V)>,
92 ext: i32,
93 ) -> Result<(), TileError> {
94 // Conflict scan against *other* tiles (this tile's own prior data is replaced, not compared).
95 let mut conflicts = BTreeSet::new();
96 // (a) Payload conflict: another tile carries the same edge (identical positions) but different
97 // vertices — only a payload (e.g. an M value) can differ, since the key is the positions.
98 for (key, (a, b)) in &new_edges {
99 if let Some(existing) = self.edges.get(key)
100 && !(existing.a == *a && existing.b == *b)
101 {
102 conflicts.extend(existing.tiles.iter().copied().filter(|&t| t != tile));
103 }
104 }
105 // (b) Membership conflict: a tile is complete in its own core, so every edge with an endpoint
106 // in a present tile's core must be carried by that tile. A present endpoint-tile that lacks the
107 // edge means the tiles came from inconsistent data. Checked both ways so detection is
108 // order-independent. (Any *disagreement* in a buffer overlap surfaces here too — the differing
109 // vertex lands in some core — so only a harmless omission of a redundant overlap edge slips.)
110 // This tile must carry every already-present edge that names it as an endpoint-tile.
111 for (key, existing) in &self.edges {
112 if (tile_of(key.0, ext) == tile || tile_of(key.1, ext) == tile)
113 && !new_edges.contains_key(key)
114 {
115 conflicts.extend(existing.tiles.iter().copied().filter(|&t| t != tile));
116 }
117 }
118 // Every already-present endpoint-tile of one of this tile's edges must carry that edge.
119 for key in new_edges.keys() {
120 for endpoint in [tile_of(key.0, ext), tile_of(key.1, ext)] {
121 if endpoint != tile
122 && self.tile_edges.contains_key(&endpoint)
123 && !self
124 .edges
125 .get(key)
126 .is_some_and(|e| e.tiles.contains(&endpoint))
127 {
128 conflicts.insert(endpoint);
129 }
130 }
131 }
132 if !conflicts.is_empty() {
133 return Err(TileError::Conflict(conflicts.into_iter().collect()));
134 }
135 // Commit: replace any prior contents for this tile, then index the new edges.
136 self.purge(tile);
137 let mut keys = Vec::with_capacity(new_edges.len());
138 for (key, (a, b)) in new_edges {
139 self.edges
140 .entry(key)
141 .or_insert_with(|| Edge {
142 a,
143 b,
144 tiles: BTreeSet::new(),
145 })
146 .tiles
147 .insert(tile);
148 keys.push(key);
149 }
150 if !keys.is_empty() {
151 self.tile_edges.insert(tile, keys);
152 }
153 Ok(())
154 }
155
156 /// Remove `tile` and all geometry it contributed. Returns whether the tile was present.
157 fn purge(&mut self, tile: TileId) -> bool {
158 let Some(keys) = self.tile_edges.remove(&tile) else {
159 return false;
160 };
161 for key in keys {
162 if let Some(edge) = self.edges.get_mut(&key) {
163 edge.tiles.remove(&tile);
164 if edge.tiles.is_empty() {
165 self.edges.remove(&key);
166 }
167 }
168 }
169 true
170 }
171
172 /// Re-chain the distinct edges into maximal chains, in the global frame (deterministic order).
173 fn iter_features(&self) -> impl Iterator<Item = Vec<V>> {
174 let mut ordered: Vec<&Edge<V>> = self.edges.values().collect();
175 ordered.sort_unstable_by_key(|edge| {
176 let (a, b) = (edge.a.position(), edge.b.position());
177 (a.x, a.y, b.x, b.y)
178 });
179 let runs: Vec<Vec<V>> = ordered.iter().map(|edge| vec![edge.a, edge.b]).collect();
180 stitch(&runs).into_iter()
181 }
182
183 fn contains(&self, tile: TileId) -> bool {
184 self.tile_edges.contains_key(&tile)
185 }
186
187 fn len(&self) -> usize {
188 self.tile_edges.len()
189 }
190
191 fn is_empty(&self) -> bool {
192 self.tile_edges.is_empty()
193 }
194
195 fn clear(&mut self) {
196 self.edges.clear();
197 self.tile_edges.clear();
198 }
199}
200
201/// Reassembles tiled **polyline** features back into whole features as tiles are [added](Self::add).
202///
203/// Generic over the [`Vertex`] type `V` (defaults to [`Coord<i32>`]). Needs only the `extent` the
204/// tiles were sliced at — never the buffer (see the module docs). A payload-carrying vertex (e.g.
205/// [`Measured`](crate::Measured)) additionally makes payload [`Conflict`](TileError::Conflict)s
206/// meaningful.
207#[derive(Debug, Clone)]
208pub struct Mosaic<V: Vertex = Coord<i32>> {
209 extent: u32,
210 index: EdgeIndex<V>,
211}
212
213impl<V: Vertex> Mosaic<V> {
214 /// Create an empty mosaic for tiles sliced at `extent`.
215 ///
216 /// # Errors
217 ///
218 /// [`TileError::InvalidExtent`] if `extent` is `0` or greater than `i32::MAX`.
219 pub fn new(extent: u32) -> Result<Self, TileError> {
220 if extent == 0 || extent > i32::MAX.cast_unsigned() {
221 return Err(TileError::InvalidExtent);
222 }
223 Ok(Self {
224 extent,
225 index: EdgeIndex::new(),
226 })
227 }
228
229 /// The extent every tile is sliced at.
230 #[must_use]
231 pub fn extent(&self) -> u32 {
232 self.extent
233 }
234
235 /// Add `tile`'s runs (in its local frame) to the mosaic. `runs` is anything sliceable to `[V]`
236 /// (e.g. `&[&[V]]` collected from a tile's features). Re-adding a tile replaces its contents. A
237 /// tile with no ≥2-vertex run contributes nothing.
238 ///
239 /// # Errors
240 ///
241 /// - [`TileError::Conflict`] if the tile disagrees with already-added tiles on a shared border
242 /// segment. The mosaic is unchanged.
243 /// - [`TileError::Overflow`] if a vertex overflows `i32` when rebased into the global frame.
244 #[cfg_attr(feature = "hotpath", hotpath::measure)]
245 pub fn add<L: AsRef<[V]>>(&mut self, tile: TileId, runs: &[L]) -> Result<(), TileError> {
246 let (off_x, off_y) = tile_offset(tile, self.extent);
247 // Rebase to the global frame and collect this tile's distinct directed edges. Nothing is
248 // mutated yet, so an overflow (or a later conflict) leaves the mosaic untouched.
249 let mut new_edges: HashMap<EdgeKey, (V, V)> = HashMap::new();
250 for run in runs {
251 let g = rebase(run.as_ref(), off_x, off_y)?;
252 for w in g.windows(2) {
253 let key = (w[0].position(), w[1].position());
254 if key.0 != key.1 {
255 new_edges.entry(key).or_insert((w[0], w[1]));
256 }
257 }
258 }
259 self.index
260 .commit(tile, new_edges, self.extent.cast_signed())
261 }
262
263 /// Remove `tile` and all geometry it contributed. Returns whether the tile was present.
264 pub fn purge(&mut self, tile: TileId) -> bool {
265 self.index.purge(tile)
266 }
267
268 /// Iterate every reassembled feature — geometry stitched across tile borders into maximal
269 /// polylines, in the global frame. A single iterator over the whole mosaic; deterministic order.
270 pub fn iter_features(&self) -> impl Iterator<Item = Vec<V>> {
271 self.index.iter_features()
272 }
273
274 /// Whether `tile` is currently in the mosaic.
275 #[must_use]
276 pub fn contains(&self, tile: TileId) -> bool {
277 self.index.contains(tile)
278 }
279
280 /// The number of tiles currently in the mosaic.
281 #[must_use]
282 pub fn len(&self) -> usize {
283 self.index.len()
284 }
285
286 /// Whether no tile has been added yet.
287 #[must_use]
288 pub fn is_empty(&self) -> bool {
289 self.index.is_empty()
290 }
291
292 /// Discard every tile, keeping the extent.
293 pub fn clear(&mut self) {
294 self.index.clear();
295 }
296}
297
298/// Reassembles tiled **polygon** features back into whole rings as tiles are [added](Self::add) — the
299/// accumulating inverse of the polygon slicer.
300///
301/// Generic over the [`Vertex`] type `V` (defaults to [`Coord<i32>`]). Needs the `extent` **and**
302/// `buffer` the tiles were sliced at, so it can re-derive each tile's buffered box and recognise the
303/// synthetic clip-boundary corners the slicer injected (see the module docs). Reassembly is by
304/// directed edge, so each ring comes back with its original orientation; a payload-carrying vertex
305/// additionally makes payload [`Conflict`](TileError::Conflict)s meaningful.
306#[derive(Debug, Clone)]
307pub struct PolygonMosaic<V: Vertex = Coord<i32>> {
308 grid: Grid,
309 index: EdgeIndex<V>,
310}
311
312impl<V: Vertex> PolygonMosaic<V> {
313 /// Create an empty polygon mosaic for tiles sliced at `extent` with `buffer`.
314 ///
315 /// # Errors
316 ///
317 /// - [`TileError::InvalidExtent`] if `extent` is `0`, or `extent + buffer` exceeds `i32::MAX`.
318 /// - [`TileError::BufferTooLarge`] if `buffer` is not strictly less than half the `extent`.
319 pub fn new(extent: u32, buffer: u16) -> Result<Self, TileError> {
320 Ok(Self {
321 grid: Grid::new(extent, buffer)?,
322 index: EdgeIndex::new(),
323 })
324 }
325
326 /// The extent every tile is sliced at.
327 #[must_use]
328 pub fn extent(&self) -> u32 {
329 self.grid.extent()
330 }
331
332 /// The buffer every tile is sliced with.
333 #[must_use]
334 pub fn buffer(&self) -> u16 {
335 self.grid.buffer()
336 }
337
338 /// Add `tile`'s rings (in its local frame) to the mosaic. `rings` is anything sliceable to `[V]`
339 /// (e.g. `&[&[V]]` collected from a tile's polygon features — exterior and hole rings alike, in any
340 /// order). Each ring may be given open or closed. Re-adding a tile replaces its contents. A tile
341 /// that contributes only synthetic geometry (a solid interior fill) records nothing.
342 ///
343 /// The synthetic clip-boundary corners are dropped geometrically before anything is indexed, so
344 /// they never enter the edge map and can raise no [`Conflict`](TileError::Conflict).
345 ///
346 /// # Errors
347 ///
348 /// - [`TileError::Conflict`] if the tile disagrees with already-added tiles on a shared border
349 /// edge. The mosaic is unchanged.
350 /// - [`TileError::Overflow`] if the tile's box or a vertex overflows `i32` in the global frame.
351 #[cfg_attr(feature = "hotpath", hotpath::measure)]
352 pub fn add<L: AsRef<[V]>>(&mut self, tile: TileId, rings: &[L]) -> Result<(), TileError> {
353 let (min, max) = self.grid.tile_buffered_bounds(tile)?;
354 let ext = self.grid.extent();
355 let (off_x, off_y) = tile_offset(tile, ext);
356 // Rebase each ring, drop its synthetic corners, and collect the surviving original directed
357 // edges. Nothing is mutated yet, so an overflow (or a later conflict) leaves the mosaic intact.
358 let mut new_edges: HashMap<EdgeKey, (V, V)> = HashMap::new();
359 for ring in rings {
360 ring_edges(ring.as_ref(), off_x, off_y, min, max, &mut new_edges)?;
361 }
362 self.index.commit(tile, new_edges, ext.cast_signed())
363 }
364
365 /// Remove `tile` and all geometry it contributed. Returns whether the tile was present.
366 pub fn purge(&mut self, tile: TileId) -> bool {
367 self.index.purge(tile)
368 }
369
370 /// Iterate every reassembled ring — geometry stitched across tile borders into maximal chains, in
371 /// the global frame. A single iterator over the whole mosaic; deterministic order.
372 pub fn iter_features(&self) -> impl Iterator<Item = Vec<V>> {
373 self.index.iter_features()
374 }
375
376 /// Whether `tile` is currently in the mosaic.
377 #[must_use]
378 pub fn contains(&self, tile: TileId) -> bool {
379 self.index.contains(tile)
380 }
381
382 /// The number of tiles currently in the mosaic (all-synthetic fill tiles do not count).
383 #[must_use]
384 pub fn len(&self) -> usize {
385 self.index.len()
386 }
387
388 /// Whether no tile has contributed geometry yet.
389 #[must_use]
390 pub fn is_empty(&self) -> bool {
391 self.index.is_empty()
392 }
393
394 /// Discard every tile, keeping the extent and buffer.
395 pub fn clear(&mut self) {
396 self.index.clear();
397 }
398}
399
400/// The global-frame offset `(tile.x·extent, tile.y·extent)` in `i64` (both factors within `i32`, so
401/// the product is below `2^62`).
402fn tile_offset(tile: TileId, extent: u32) -> (i64, i64) {
403 let e = i64::from(extent);
404 (i64::from(tile.x) * e, i64::from(tile.y) * e)
405}
406
407/// Rebase one vertex by `(off_x, off_y)` in `i64` (the offset is `tile · extent`, below `2^62`, and a
408/// vertex adds at most another `i32`, so the sum stays within `i64`), range-checking back into `i32`.
409fn rebase_vertex<V: Vertex>(v: V, off_x: i64, off_y: i64) -> Result<V, TileError> {
410 let p = v.position();
411 Ok(v.with_position(Coord {
412 x: i32::try_from(i64::from(p.x) + off_x).map_err(|_| TileError::Overflow)?,
413 y: i32::try_from(i64::from(p.y) + off_y).map_err(|_| TileError::Overflow)?,
414 }))
415}
416
417/// Rebase every vertex of `run` by `(off_x, off_y)`, range-checking each result back into `i32`.
418fn rebase<V: Vertex>(run: &[V], off_x: i64, off_y: i64) -> Result<Vec<V>, TileError> {
419 run.iter()
420 .map(|&v| rebase_vertex(v, off_x, off_y))
421 .collect()
422}
423
424/// Rebase one closed `ring` to the global frame and add to `new_edges` every directed edge that
425/// **touches** the tile's buffered box `[min, max]`.
426///
427/// This one test drops all the slicer's synthetic geometry and reassembles the original rings, because
428/// of a clean split (design doc §7):
429///
430/// - Every **real** ring edge `p → q` is emitted by the tile that owns `p`'s cell: `p` lies in that
431/// tile's core (⊆ its box), so the edge starts inside the box and touches it. Thus no real edge is
432/// ever lost — it survives in at least the one tile that owns its start.
433/// - Every **synthetic** edge — the clip-boundary corners, fill boxes, and gap-bridging chords the
434/// slicer injects — is routed strictly *outside* the box (design doc §5), so it never touches the box
435/// and is dropped. This catches even a direct bridge chord between two *original* crossing vertices,
436/// which a per-vertex "is this corner synthetic" test cannot (its endpoints are genuine vertices).
437///
438/// An edge that misses the box but *is* real is simply redundant here (its endpoints belong to other
439/// tiles' cores, which re-emit it), so dropping it is harmless. The rule is purely tile-local, hence
440/// order-independent.
441fn ring_edges<V: Vertex>(
442 ring: &[V],
443 off_x: i64,
444 off_y: i64,
445 min: Coord<i32>,
446 max: Coord<i32>,
447 new_edges: &mut HashMap<EdgeKey, (V, V)>,
448) -> Result<(), TileError> {
449 // Rebase to the global frame and drop consecutive duplicate positions in one pass (matching the
450 // slicer, so a zero-length edge never distorts the touch test) — no separate rebased copy.
451 let mut pts: Vec<V> = Vec::with_capacity(ring.len());
452 for &v in ring {
453 let g = rebase_vertex(v, off_x, off_y)?;
454 if pts.last().map(Vertex::position) != Some(g.position()) {
455 pts.push(g);
456 }
457 }
458 // Drop the repeated closing vertex.
459 while pts.len() >= 2 && pts[0].position() == pts[pts.len() - 1].position() {
460 pts.pop();
461 }
462 let m = pts.len();
463 if m < 3 {
464 return Ok(());
465 }
466 // Emit each cyclic edge that touches the tile's box; drop the rest (synthetic or redundant).
467 for i in 0..m {
468 let (p, q) = (pts[i], pts[(i + 1) % m]);
469 if p.position() != q.position() && segment_intersects(p.position(), q.position(), min, max)
470 {
471 new_edges
472 .entry((p.position(), q.position()))
473 .or_insert((p, q));
474 }
475 }
476 Ok(())
477}
478
479/// Rejoin overlapping runs into maximal polylines via their **directed edges** (consecutive vertex
480/// pairs, keyed by position): a border-crossing segment is the same edge in both tiles, so keeping the
481/// distinct edges drops every duplicate, and following each vertex to its outgoing edge re-chains them.
482///
483/// Order-independent and deterministic (edges/positions keep first-seen order). A simple polyline
484/// reconstructs exactly; where the geometry revisits a position, some covering chain is produced
485/// (deterministic but arbitrary).
486fn stitch<V: Vertex>(runs: &[Vec<V>]) -> Vec<Vec<V>> {
487 // Distinct directed edges, in first-seen order (dedup by endpoint positions).
488 let mut seen: HashSet<(Coord<i32>, Coord<i32>)> = HashSet::new();
489 let mut edges: Vec<(V, V)> = Vec::new();
490 for run in runs {
491 for w in run.windows(2) {
492 if seen.insert((w[0].position(), w[1].position())) {
493 edges.push((w[0], w[1]));
494 }
495 }
496 }
497 if edges.is_empty() {
498 return Vec::new();
499 }
500
501 // Adjacency: each start position → its outgoing edge indices (FIFO = first-seen). Track in-degree
502 // and first-seen position order so chain starts are found deterministically.
503 let mut out: HashMap<Coord<i32>, VecDeque<usize>> = HashMap::new();
504 let mut indeg: HashMap<Coord<i32>, usize> = HashMap::new();
505 let mut points: Vec<Coord<i32>> = Vec::new();
506 let mut known: HashSet<Coord<i32>> = HashSet::new();
507 for (i, (p, q)) in edges.iter().enumerate() {
508 let (pp, qp) = (p.position(), q.position());
509 out.entry(pp).or_default().push_back(i);
510 *indeg.entry(qp).or_insert(0) += 1;
511 indeg.entry(pp).or_insert(0);
512 for pt in [pp, qp] {
513 if known.insert(pt) {
514 points.push(pt);
515 }
516 }
517 }
518
519 let mut used = vec![false; edges.len()];
520 let mut chains: Vec<Vec<V>> = Vec::new();
521
522 // Phase 1: start a chain at each path source — a position with more outgoing than incoming edges
523 // accounts for `outdeg − indeg` chain starts (the endpoints of open polylines).
524 for &p in &points {
525 let outdeg = out.get(&p).map_or(0, VecDeque::len);
526 let ind = indeg.get(&p).copied().unwrap_or(0);
527 for _ in ind..outdeg {
528 if let Some(first) = next_unused(&mut out, p, &used) {
529 chains.push(build_chain(first, &edges, &mut used, &mut out));
530 }
531 }
532 }
533 // Phase 2: whatever edges remain form closed loops; start each at its first unused edge.
534 for i in 0..edges.len() {
535 if !used[i] {
536 chains.push(build_chain(i, &edges, &mut used, &mut out));
537 }
538 }
539 chains
540}
541
542/// Pop and return the next not-yet-used outgoing edge index from position `p`, discarding used ones
543/// from the front. `None` when `p` has no remaining unused outgoing edge. Used by [`stitch`].
544fn next_unused(
545 out: &mut HashMap<Coord<i32>, VecDeque<usize>>,
546 p: Coord<i32>,
547 used: &[bool],
548) -> Option<usize> {
549 let dq = out.get_mut(&p)?;
550 while let Some(&i) = dq.front() {
551 if used[i] {
552 dq.pop_front();
553 } else {
554 return dq.pop_front();
555 }
556 }
557 None
558}
559
560/// Follow outgoing edges from edge `first`, consuming each, until a position has no unused outgoing
561/// edge; return the vertex chain `[start, …]`. Used by [`stitch`].
562fn build_chain<V: Vertex>(
563 first: usize,
564 edges: &[(V, V)],
565 used: &mut [bool],
566 out: &mut HashMap<Coord<i32>, VecDeque<usize>>,
567) -> Vec<V> {
568 let mut chain = vec![edges[first].0];
569 let mut cur = first;
570 loop {
571 used[cur] = true;
572 let q = edges[cur].1;
573 chain.push(q);
574 match next_unused(out, q.position(), used) {
575 Some(next) => cur = next,
576 None => break,
577 }
578 }
579 chain
580}