Skip to main content

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}