map-tile-toolkit 0.2.0

Clip integer polylines and polygons into per-tile pieces on a simple tile grid
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
//! Clipping one polygon **ring** to a single tile's buffered box, keeping the **original** vertices
//! and closing the result with synthetic clip-boundary corners so the ring stays a closed, correctly
//! wound loop (see `docs/polygon-slicer.md`). The engine behind the polygon slicers.
//!
//! A vertex is kept by the same rule as the polyline clip (an incident edge touches the box); the
//! dropped outside excursions between kept arcs are replaced by detours that hug the box exterior
//! (`B⁺`, one unit outside `B`) via its corners. Each detour is built to be **homotopic** to the
//! excursion it replaces — it reproduces the excursion's winding around the box center — so the fill
//! inside the box is exact even when a ring wraps the tile. All synthetic geometry lies strictly
//! outside `B`, which lets [`Mosaic`](crate::Mosaic) recognise and drop it geometrically.
//!
//! [`close_ring`] is the shared core: given which edges touch the box and a way to measure an
//! excursion's winding, it closes the kept arcs. [`clip_ring`] (the single-tile clip) finds the
//! touching edges and windings by walking the whole ring; the all-tiles slicer feeds it the edges its
//! routing pass found and windings from its per-row crossing index, so both produce identical rings.

use core::cmp::Ordering;
use core::iter;

use geo_types::Coord;

use crate::TileError;
use crate::clip_polyline::line_meets_box;
use crate::geom::{point_in_ring, ring_orientation};
use crate::vertex::{PolyVertex, Vertex};

// Cohen–Sutherland outcode bits for a point vs the inclusive box `[min, max]`.
const LEFT: u8 = 1;
const RIGHT: u8 = 2;
const BOTTOM: u8 = 4;
const TOP: u8 = 8;
/// All four outside bits.
const BOX: u8 = LEFT | RIGHT | BOTTOM | TOP;
/// Not an outcode bit: set beside one for a point above the winding ray's line (`y > cy`).
const ABOVE: u8 = 16;

/// Outcode of `p` relative to the closed box `[min, max]` (0 iff `p` is inside).
fn outcode(p: Coord<i32>, min: Coord<i32>, max: Coord<i32>) -> u8 {
    let mut oc = 0;
    if p.x < min.x {
        oc |= LEFT;
    } else if p.x > max.x {
        oc |= RIGHT;
    }
    if p.y < min.y {
        oc |= BOTTOM;
    } else if p.y > max.y {
        oc |= TOP;
    }
    oc
}

/// Boundary slot `0..8` of an **outside** point, counter-clockwise with y up:
/// `0=SW, 1=S, 2=SE, 3=E, 4=NE, 5=N, 6=NW, 7=W`. Corners are the even slots. A zero outcode (inside)
/// cannot occur for a detour endpoint; it maps to `0` defensively so nothing can panic.
fn slot(oc: u8) -> u8 {
    match oc {
        _ if oc == BOTTOM | LEFT => 0,
        BOTTOM => 1,
        _ if oc == BOTTOM | RIGHT => 2,
        RIGHT => 3,
        _ if oc == RIGHT | TOP => 4,
        TOP => 5,
        _ if oc == TOP | LEFT => 6,
        LEFT => 7,
        _ => 0,
    }
}

/// The `B⁺` corner at an even `slot`, one unit outside the box. [`TileError::Overflow`] if the tile
/// sits so close to the `i32` edge that a corner can't be placed strictly outside it.
fn corner(slot: u8, min: Coord<i32>, max: Coord<i32>) -> Result<Coord<i32>, TileError> {
    let lo_x = min.x.checked_sub(1).ok_or(TileError::Overflow)?;
    let lo_y = min.y.checked_sub(1).ok_or(TileError::Overflow)?;
    let hi_x = max.x.checked_add(1).ok_or(TileError::Overflow)?;
    let hi_y = max.y.checked_add(1).ok_or(TileError::Overflow)?;
    Ok(match slot {
        0 => Coord { x: lo_x, y: lo_y }, // SW⁺
        2 => Coord { x: hi_x, y: lo_y }, // SE⁺
        4 => Coord { x: hi_x, y: hi_y }, // NE⁺
        _ => Coord { x: lo_x, y: hi_y }, // NW⁺ (slot 6)
    })
}

/// Signed crossing of segment `a → b` with the ray `{ y = cy, x > max_x }` — the part of the `+x` ray
/// from the box center that lies outside the box: `+1` upward, `-1` downward, `0` if it misses
/// (half-open in `y`, so a vertex shared by two edges is counted once). Summed along a path this is
/// the path's winding around the box center; since every excursion and every detour lives outside the
/// box, only the outside portion of the ray can be crossed.
fn ray_crossing(a: Coord<i32>, b: Coord<i32>, cy: i32, max_x: i32) -> i64 {
    if (a.y > cy) == (b.y > cy) {
        return 0;
    }
    // Crossing `x` compared to `max_x` without division: `x − max_x = num / dy`.
    let dy = i128::from(b.y) - i128::from(a.y);
    let num = (i128::from(a.x) - i128::from(max_x)) * dy
        + (i128::from(b.x) - i128::from(a.x)) * (i128::from(cy) - i128::from(a.y));
    // `x > max_x` iff `num` and `dy` share a sign (their product is positive).
    if num.signum() * dy.signum() > 0 {
        if b.y > a.y { 1 } else { -1 }
    } else {
        0
    }
}

/// [`ray_crossing`] summed over a path's consecutive points.
fn ray_crossings(path: impl IntoIterator<Item = Coord<i32>>, cy: i32, max_x: i32) -> i64 {
    let mut path = path.into_iter();
    let Some(mut prev) = path.next() else {
        return 0;
    };
    let mut w = 0;
    for p in path {
        w += ray_crossing(prev, p, cy, max_x);
        prev = p;
    }
    w
}

/// Corner slots visited walking `advance` slots (CCW if positive, CW if negative) from `start`: every
/// even slot passed except the final one (where the detour's other endpoint sits). `advance == 0`
/// yields none (a direct chord).
fn corner_slots(start: u8, advance: i64) -> impl Iterator<Item = u8> {
    // One slot CCW (`+1`) or CW (`+7 ≡ −1`) around the 8-slot loop, staying in `u8`.
    let step = if advance > 0 { 1 } else { 7 };
    let mut s = start;
    (1..advance.unsigned_abs()).filter_map(move |_| {
        s = (s + step) % 8;
        s.is_multiple_of(2).then_some(s)
    })
}

/// How many corners [`corner_slots`] yields, in O(1): of the `|advance| − 1` intermediate slots,
/// every other one is a corner, starting with the first when `start` is odd.
fn corner_count(start: u8, advance: i64) -> u64 {
    let n = advance.unsigned_abs().saturating_sub(1);
    if start.is_multiple_of(2) {
        n / 2
    } else {
        n.div_ceil(2)
    }
}

/// The box's center `y` (overflow-safe midpoint) — the height of the winding ray.
pub(crate) fn center_y(min: Coord<i32>, max: Coord<i32>) -> i32 {
    i32::midpoint(min.y, max.y)
}

/// How to bridge one dropped gap between kept arcs.
enum Bridge {
    /// Keep the gap's original vertices verbatim (no more of them than a detour would need).
    Keep,
    /// Replace the gap with the synthetic `B⁺` corners from [`corner_slots`]`(start, advance)`.
    Detour { start: u8, advance: i64 },
}

/// Decide how to bridge a gap of `gap_len` dropped vertices from exit vertex `u` to entry vertex `v`
/// (both outside the box) whose excursion winds `w_exc` times around the box center ([`ray_crossing`]
/// summed over its edges). The detour reproduces that winding, so it is homotopic to the excursion in
/// the box exterior: the CCW corner path from `u`'s slot to `v`'s is the reference, and whole `∂B⁺`
/// loops (±8 slots each) make up the winding difference.
///
/// Every gap vertex is outside the box (both its edges miss it — that's why it was dropped), so keeping
/// the gap verbatim reproduces the real excursion with its exact winding, entirely outside the box.
/// Synthesizing only ever trades those originals for `B⁺` corners, so it's worth doing only when it
/// *saves* vertices — a longer excursion that would otherwise drag its far geometry into this tile.
/// Either way `Mosaic` recovers the real geometry from the tiles that own it.
fn plan_bridge(
    u: Coord<i32>,
    v: Coord<i32>,
    w_exc: i64,
    gap_len: usize,
    min: Coord<i32>,
    max: Coord<i32>,
    cy: i32,
) -> Result<Bridge, TileError> {
    let su = slot(outcode(u, min, max));
    let sv = slot(outcode(v, min, max));
    let base = i64::from((sv + 8 - su) % 8); // 0..8, CCW distance su → sv
    // Winding of the reference detour (at most three corners).
    let mut w_ref = 0;
    let mut prev = u;
    for s in corner_slots(su, base) {
        let c = corner(s, min, max)?;
        w_ref += ray_crossing(prev, c, cy, max.x);
        prev = c;
    }
    w_ref += ray_crossing(prev, v, cy, max.x);
    let advance = (w_exc - w_ref)
        .checked_mul(8)
        .and_then(|loops| loops.checked_add(base))
        .ok_or(TileError::Overflow)?;
    let keep = u64::try_from(gap_len).is_ok_and(|g| g <= corner_count(su, advance));
    Ok(if keep {
        Bridge::Keep
    } else {
        Bridge::Detour { start: su, advance }
    })
}

/// Append to `out` the distinct vertices of `ring` in cyclic order — consecutive duplicate positions
/// and a repeated closing vertex dropped, so zero-length edges can't distort the clip (matching the
/// polyline slicer's handling of consecutive duplicates). Returns how many were appended.
#[inline]
pub(crate) fn push_distinct<V: Vertex>(ring: &[V], out: &mut Vec<V>) -> usize {
    // Usually the ring is distinct already: then one bulk copy of it, without its closing vertices.
    if let Some(n) = distinct_prefix(ring) {
        out.extend_from_slice(&ring[..n]);
        return n;
    }
    let start = out.len();
    for &v in ring {
        if out.len() == start || out.last().map(Vertex::position) != Some(v.position()) {
            out.push(v);
        }
    }
    while out.len() - start >= 2 && out.last().map(Vertex::position) == Some(out[start].position())
    {
        out.pop();
    }
    out.len() - start
}

/// Append the `B⁺` fill box as a closed ring oriented like `orient` (a solid tile fill), all synthetic.
pub(crate) fn push_fill_box<V: PolyVertex>(
    out: &mut Vec<V>,
    orient: Ordering,
    min: Coord<i32>,
    max: Coord<i32>,
) -> Result<(), TileError> {
    let ring = fill_box(orient, corner(0, min, max)?, corner(4, min, max)?);
    out.extend(ring.into_iter().map(V::synthetic_at));
    Ok(())
}

/// The closed fill-box ring through the `B⁺` corners `sw` and `ne`, oriented like `orient`.
pub(crate) fn fill_box(orient: Ordering, sw: Coord<i32>, ne: Coord<i32>) -> [Coord<i32>; 5] {
    let se = Coord { x: ne.x, y: sw.y };
    let nw = Coord { x: sw.x, y: ne.y };
    // Counter-clockwise for a CCW ring, clockwise otherwise, so the fill matches the ring's sense.
    if orient == Ordering::Less {
        [sw, nw, ne, se, sw]
    } else {
        [sw, se, ne, nw, sw]
    }
}

/// Extend the kept arcs with touching edge `t` (edges must come in ascending order). Arcs are inclusive
/// vertex spans `(start, end)`, `end` unwrapped (it may pass the ring's length by wrapping to its
/// start). Edge `t` keeps vertices `t` and `t + 1`, so two touching edges at most two apart keep one
/// contiguous arc (the edge between them is a single-segment out-and-back bridge).
#[inline]
pub(crate) fn keep_edge(arcs: &mut Vec<(usize, usize)>, t: usize) {
    match arcs.last_mut() {
        Some((_, end)) if t <= *end + 1 => *end = t + 1,
        _ => arcs.push((t, t + 1)),
    }
}

/// Reusable working memory for [`clip_ring`]. It holds nothing between calls, so it never makes two
/// owners differ: it compares equal, clones empty, and only its capacity survives a call.
pub(crate) struct ClipScratch<V> {
    copy: Vec<V>,
    crossings: Vec<(u32, i64)>,
    arcs: Vec<(usize, usize)>,
}

impl<V> ClipScratch<V> {
    pub(crate) const fn new() -> Self {
        Self {
            copy: Vec::new(),
            crossings: Vec::new(),
            arcs: Vec::new(),
        }
    }
}

impl<V> Clone for ClipScratch<V> {
    fn clone(&self) -> Self {
        Self::new()
    }
}

impl<V> PartialEq for ClipScratch<V> {
    fn eq(&self, _: &Self) -> bool {
        true
    }
}

impl<V> Eq for ClipScratch<V> {}

impl<V> core::fmt::Debug for ClipScratch<V> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("ClipScratch").finish_non_exhaustive()
    }
}

/// Close one ring's kept arcs into a single closed ring (first vertex repeated at the end), appended
/// to `out` in the ring's own coordinate frame.
///
/// `ring` holds the ring's distinct vertices in cyclic order (see [`push_distinct`]); edge `i` runs
/// `ring[i] → ring[(i + 1) % m]`. `arcs` holds, non-empty, the kept arcs that [`keep_edge`] built from
/// the edges touching the box `[min, max]` in ascending order — a vertex is kept iff an incident edge
/// touches; they are consumed. `winding(first, count)` must return
/// [`ray_crossing`] summed over the `count` consecutive edges from edge `first` (cyclically), for this
/// box's ray; the caller supplies it so the single-tile clip can walk the edges while the all-tiles
/// slicer answers from its per-row crossing index.
///
/// Output starts at the first arc in index order, so it is a pure function of the inputs.
///
/// # Errors
///
/// [`TileError::Overflow`] if a synthetic corner can't be placed strictly outside the box.
#[inline]
pub(crate) fn close_ring<V: PolyVertex>(
    ring: &[V],
    arcs: &mut [(usize, usize)],
    min: Coord<i32>,
    max: Coord<i32>,
    mut winding: impl FnMut(usize, usize) -> i64,
    out: &mut Vec<V>,
) -> Result<(), TileError> {
    let m = ring.len();
    // The last arc may run on, across the ring's seam, into the first.
    let mut first = 0;
    if let (Some(&(s0, e0)), Some(last)) = (arcs.first(), arcs.len().checked_sub(1))
        && last > 0
        && arcs[last].1 + 1 >= s0 + m
    {
        arcs[last].1 = e0 + m;
        first = 1;
    }
    let arcs = &arcs[first..];
    if let [(s, e)] = *arcs
        && e - s + 1 >= m
    {
        // Every vertex is kept: the ring is returned verbatim, re-closed.
        out.extend_from_slice(ring);
        out.push(ring[0]);
        return Ok(());
    }

    let cy = center_y(min, max);
    let ring_start = out.len();
    for (j, &(s, e)) in arcs.iter().enumerate() {
        out.extend((s..=e).map(|i| ring[i % m]));
        // Bridge the gap to the next arc (cyclically): the dropped vertices `e+1 .. next`.
        let next = arcs.get(j + 1).map_or(arcs[0].0 + m, |a| a.0);
        let (u, v) = (ring[e % m].position(), ring[next % m].position());
        let w_exc = winding(e % m, next - e);
        match plan_bridge(u, v, w_exc, next - e - 1, min, max, cy)? {
            Bridge::Keep => out.extend((e + 1..next).map(|i| ring[i % m])),
            Bridge::Detour { start, advance } => {
                let before = out.len();
                for s in corner_slots(start, advance) {
                    out.push(V::synthetic_at(corner(s, min, max)?));
                }
                debug_assert_eq!(
                    ray_crossings(
                        iter::once(u)
                            .chain(out[before..].iter().map(Vertex::position))
                            .chain(iter::once(v)),
                        cy,
                        max.x
                    ),
                    w_exc,
                    "detour winding must match the excursion it replaces"
                );
            }
        }
    }
    out.push(out[ring_start]); // close the ring
    Ok(())
}

/// The outcome of clipping one ring to a tile box.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RingClip {
    /// The ring does not appear in this tile (nothing was appended).
    Outside,
    /// The tile lies entirely inside the ring: a solid fill (the synthetic `B⁺` box). For an exterior
    /// ring this means a fully-filled tile; for a hole it means the tile is entirely a hole.
    Covers,
    /// The ring crosses the tile; the clipped closed ring (original vertices + synthetic detours).
    Clipped,
}

/// Clip one closed `ring` to the inclusive buffered box `[min, max]`, keeping original vertices and
/// closing the result with synthetic `B⁺` corners. Appends the closed output ring to `out` in the
/// input's own coordinate frame (first vertex repeated at the end), and tells apart a whole-tile [`RingClip::Covers`]
/// fill from a partial [`RingClip::Clipped`] crossing (so the caller can drop a tile that a hole
/// entirely covers) and [`RingClip::Outside`] when the ring misses the tile.
///
/// # Errors
///
/// - [`TileError::Overflow`] if the tile sits so close to the `i32` edge that a synthetic corner can't
///   be placed strictly outside the box.
/// - [`TileError::GeometryTooLarge`] if the ring has more than `u32::MAX` distinct vertices.
pub(crate) fn clip_ring<V: PolyVertex>(
    ring: &[V],
    min: Coord<i32>,
    max: Coord<i32>,
    scratch: &mut ClipScratch<V>,
    out: &mut Vec<V>,
) -> Result<RingClip, TileError> {
    let ClipScratch {
        copy,
        crossings,
        arcs,
    } = scratch;
    // Borrow the ring when it is already distinct (the usual case), copying only to drop duplicates.
    let pts = if let Some(n) = distinct_prefix(ring) {
        &ring[..n]
    } else {
        copy.clear();
        push_distinct(ring, copy);
        copy.as_slice()
    };
    let m = pts.len();
    if m < 3 {
        return Ok(RingClip::Outside);
    }
    u32::try_from(m).map_err(|_| TileError::GeometryTooLarge)?;
    if pts.iter().all(|p| outcode(p.position(), min, max) == 0) {
        // Every vertex is inside (most rings of small polygons): every edge touches, and the ring is
        // kept verbatim, as `close_ring` would. Reserved exactly: a fresh slicer's buffer needs no regrowth.
        out.reserve(m + 1);
        out.extend_from_slice(pts);
        out.push(pts[0]);
        return Ok(RingClip::Clipped);
    }

    // One pass over the edges. Each vertex's outcode is computed once and shared by its two edges: a
    // shared outside bit rejects an edge (the bounding-box test of `segment_intersects`), an inside
    // endpoint accepts it, and only the rest need the exact line test. The same pass records every
    // edge crossing the winding ray (its endpoints on either side of `y = cy`, the only edges
    // `ray_crossing` can count), so an excursion's winding is a sum over those few, not a walk.
    let cy = center_y(min, max);
    let code = |p: Coord<i32>| outcode(p, min, max) | if p.y > cy { ABOVE } else { 0 };
    arcs.clear();
    crossings.clear();
    let (mut a, mut ca) = (pts[0].position(), code(pts[0].position()));
    for (i, b) in (0..).zip(pts[1..].iter().chain(iter::once(&pts[0]))) {
        let b = b.position();
        let cb = code(b);
        let (oa, ob) = (ca & BOX, cb & BOX);
        if oa & ob == 0 && (oa == 0 || ob == 0 || line_meets_box(a, b, min, max)) {
            keep_edge(arcs, i as usize);
        }
        if (ca ^ cb) & ABOVE != 0 {
            let w = ray_crossing(a, b, cy, max.x);
            if w != 0 {
                crossings.push((i, w));
            }
        }
        (a, ca) = (b, cb);
    }

    if arcs.is_empty() {
        // No edge touches the box → the tile is uniformly inside or outside the ring, and the box
        // center's `+x` ray meets edges only outside the box — exactly the recorded crossings — so their
        // parity is the even-odd containment of the whole tile.
        let inside = crossings.len() % 2 == 1;
        debug_assert_eq!(
            inside,
            point_in_ring(min, &pts.iter().map(Vertex::position).collect::<Vec<_>>()),
            "ray parity must agree with point-in-ring containment"
        );
        return if inside {
            push_fill_box(out, ring_orientation(pts), min, max)?;
            Ok(RingClip::Covers)
        } else {
            Ok(RingClip::Outside)
        };
    }

    // Signed crossings among edges `lo..hi` (both within `0..=m`).
    let sum = |lo: usize, hi: usize| -> i64 {
        let i = crossings.partition_point(|c| (c.0 as usize) < lo);
        let j = crossings.partition_point(|c| (c.0 as usize) < hi);
        crossings[i..j].iter().map(|c| c.1).sum()
    };
    let winding = |first: usize, count: usize| {
        let w = if first + count <= m {
            sum(first, first + count)
        } else {
            sum(first, m) + sum(0, first + count - m)
        };
        debug_assert_eq!(
            w,
            ray_crossings(
                (first..=first + count).map(|i| pts[i % m].position()),
                cy,
                max.x
            ),
            "indexed winding must match walking the excursion"
        );
        w
    };
    close_ring(pts, arcs, min, max, winding, out)?;
    Ok(RingClip::Clipped)
}

/// The length of `ring` without its repeated closing vertices, if that prefix has no consecutive
/// duplicate positions — exactly what [`push_distinct`] would copy, so it can be borrowed instead.
/// `None` when an inner duplicate needs the copy.
fn distinct_prefix<V: Vertex>(ring: &[V]) -> Option<usize> {
    let first = ring.first()?.position();
    let mut n = ring.len();
    while n >= 2 && ring[n - 1].position() == first {
        n -= 1;
    }
    ring[..n]
        .windows(2)
        .all(|w| w[0].position() != w[1].position())
        .then_some(n)
}

#[cfg(test)]
mod tests {
    use geo_types::Coord;

    use super::*;

    fn c(x: i32, y: i32) -> Coord<i32> {
        Coord { x, y }
    }

    // Tile (0,0), extent 10, buffer 0 → inclusive box [0,0]..[9,9].
    const MIN: Coord<i32> = Coord { x: 0, y: 0 };
    const MAX: Coord<i32> = Coord { x: 9, y: 9 };

    /// Even-odd winding of the *positions* of a clipped ring at `p`, to check the fill is correct.
    fn fill_at(ring: &[Coord<i32>], p: Coord<i32>) -> bool {
        point_in_ring(p, ring)
    }

    fn positions<V: Vertex>(ring: &[V]) -> Vec<Coord<i32>> {
        ring.iter().map(Vertex::position).collect()
    }

    /// The ring of a non-empty clip (panics on [`RingClip::Outside`]).
    fn clipped((rc, out): (RingClip, Vec<Coord<i32>>)) -> Vec<Coord<i32>> {
        assert_ne!(rc, RingClip::Outside, "expected geometry");
        out
    }

    /// [`clip_ring`] into a fresh output, with how the ring met the tile.
    fn clip(ring: &[Coord<i32>]) -> (RingClip, Vec<Coord<i32>>) {
        let mut out = Vec::new();
        let rc = clip_ring(ring, MIN, MAX, &mut ClipScratch::new(), &mut out).unwrap();
        (rc, out)
    }

    #[test]
    fn ring_of_two_distinct_vertices_is_outside() {
        // Distinct only after dropping consecutive duplicates, and then just a line there and back.
        let ring = [c(2, 2), c(2, 2), c(7, 7), c(7, 7), c(2, 2)];
        assert!(matches!(clip(&ring), (RingClip::Outside, out) if out.is_empty()));
    }

    #[test]
    fn defensive_edge_cases() {
        assert_eq!(
            slot(0),
            0,
            "an inside point never ends a detour; it maps to a valid slot"
        );
        assert_eq!(
            ray_crossings(iter::empty(), 0, 0),
            0,
            "an empty path crosses nothing"
        );
    }

    #[test]
    fn fully_inside_is_kept_verbatim() {
        let ring = [c(2, 2), c(7, 2), c(7, 7), c(2, 7), c(2, 2)];
        let out = clipped(clip(&ring));
        assert_eq!(out, ring.to_vec(), "an inside ring is returned unchanged");
    }

    #[test]
    fn single_vertex_gap_is_kept_not_synthesized() {
        // Box [0,9]. This square wraps the tile's SW corner; clipped to the box only its far corner
        // (50,50) is dropped (both its edges miss the box), a one-vertex gap between the exit (5,50)
        // and the re-entry (50,5). That lone vertex must be kept verbatim, not replaced by synthetic
        // `B⁺` corners — so every output vertex is an original input vertex.
        let ring = [c(5, 5), c(5, 50), c(50, 50), c(50, 5), c(5, 5)];
        let out = clipped(clip(&ring));
        let pts = positions(&out);
        let original: std::collections::BTreeSet<(i32, i32)> =
            [(5, 5), (5, 50), (50, 50), (50, 5)].into_iter().collect();
        assert!(
            pts.iter().all(|q| original.contains(&(q.x, q.y))),
            "no synthetic corners were introduced: {pts:?}"
        );
        assert!(
            pts.iter().any(|q| (q.x, q.y) == (50, 50)),
            "the single dropped vertex is kept intact"
        );
        // Fill is still correct: the tile's SW corner is outside the square, its NE inside.
        assert!(!fill_at(&pts, c(1, 1)));
        assert!(fill_at(&pts, c(8, 8)));
    }

    #[test]
    fn fully_outside_disjoint_is_none() {
        // A small ring far to the right, not enclosing the tile.
        let ring = [
            c(100, 100),
            c(110, 100),
            c(110, 110),
            c(100, 110),
            c(100, 100),
        ];
        assert!(matches!(clip(&ring), (RingClip::Outside, _)));
    }

    #[test]
    fn containment_fills_the_box() {
        // A big ring that encloses the whole tile with no edge touching it → solid fill.
        let ring = [c(-50, -50), c(50, -50), c(50, 50), c(-50, 50), c(-50, -50)];
        let rc = clip(&ring);
        assert!(
            matches!(rc, (RingClip::Covers, _)),
            "a fully-enclosed tile is reported as a solid fill"
        );
        let out = clipped(rc);
        let pts = positions(&out);
        // Every vertex is synthetic (strictly outside the box) and the whole tile is filled.
        assert!(pts.iter().all(|q| outcode(*q, MIN, MAX) != 0));
        assert!(fill_at(&pts, c(5, 5)));
        assert!(fill_at(&pts, c(0, 0)));
        assert!(fill_at(&pts, c(9, 9)));
    }

    #[test]
    fn edge_through_tile_all_vertices_outside() {
        // Every vertex is outside the box, but the hypotenuse (line x+y=9) cuts through the tile: a
        // big right triangle covering the lower-left half-plane {x+y<9}. The filled side must win.
        let ring = [c(-100, -100), c(109, -100), c(-100, 109), c(-100, -100)];
        let out = clipped(clip(&ring));
        let pts = positions(&out);
        assert!(
            fill_at(&pts, c(1, 1)),
            "lower-left of the cut is filled (x+y=2 < 9)"
        );
        assert!(
            !fill_at(&pts, c(8, 8)),
            "upper-right of the cut is not filled (x+y=16 > 9)"
        );
    }

    #[test]
    fn crossing_ring_keeps_original_crossing_vertices() {
        // A ring straddling the right edge: two vertices inside, two outside.
        let ring = [c(4, 3), c(15, 3), c(15, 6), c(4, 6), c(4, 3)];
        let out = clipped(clip(&ring));
        let pts = positions(&out);
        // Original inside/near vertices are preserved; the fill inside the box is the left strip.
        assert!(pts.contains(&c(4, 3)) && pts.contains(&c(4, 6)));
        assert!(fill_at(&pts, c(6, 4)), "inside the ring, inside the tile");
        assert!(!fill_at(&pts, c(1, 8)), "outside the ring");
    }

    #[test]
    fn encircling_notch_wrap_case() {
        // A ring that surrounds the tile (all around, far outside) with a thin notch stabbing UP into
        // the tile from the bottom. The excursion between the two notch crossings wraps the whole box,
        // so a naive detour would fill only the notch. Correct fill = tile minus the notch.
        let ring = [
            c(3, -50), // up into the tile (notch left side)
            c(3, 6),
            c(6, 6),
            c(6, -50),  // back down (notch right side)
            c(60, -50), // around the outside, far from the box, all the way around …
            c(60, 60),
            c(-60, 60),
            c(-60, -50),
            c(3, -50),
        ];
        let out = clipped(clip(&ring));
        let pts = positions(&out);
        // Inside the notch (x in 3..6, low y) is NOT filled; the rest of the tile IS filled.
        assert!(!fill_at(&pts, c(4, 2)), "the notch is a hole in the fill");
        assert!(fill_at(&pts, c(1, 5)), "left of the notch is filled");
        assert!(fill_at(&pts, c(8, 5)), "right of the notch is filled");
        assert!(fill_at(&pts, c(4, 8)), "above the notch is filled");
    }
}