Skip to main content

map_tile_toolkit/
slicer.rs

1//! The public slicing API: [`SlicerAll`] (every tile a polyline touches) and [`SlicerOne`] (one fixed
2//! tile), both over the stateless [`Grid`] engine.
3//!
4//! Each polyline added with [`add_feature`](SlicerAll::add_feature) (or
5//! [`add_feature_with`](SlicerAll::add_feature_with), to attach a **per-feature** attribute) is an
6//! independent **feature**, sliced and recorded per tile; one polyline can yield several runs in a
7//! tile (it left and re-entered), which stay grouped as that tile's feature. Read back with borrowed
8//! iterators: [`SlicerAll::iter_tiles`] → [`TileView::iter_features`] → [`FeatureView::iter_polylines`]
9//! (and [`FeatureView::attr`] for the attribute), or [`SlicerOne::iter_features`] directly. Runs are
10//! in each tile's local frame (origin at the tile's `[0, 0]` corner, so in-tile vertices land in
11//! `0..extent`); [`merge`](crate::merge) is the inverse.
12//!
13//! Attributes are a second, optional generic axis (`A`, default `()`): orthogonal to the per-vertex
14//! [`Vertex`] payload, stored once per feature-piece, and — because `A = ()` is zero-sized — free
15//! unless used. A feature split across tiles carries a clone of its attribute into each.
16//!
17//! Storage: each tile is a flattened [`TileBuf`] — one `verts` arena with `u32` run/feature boundary
18//! offsets, not nested `Vec`s. [`SlicerAll`] holds `Vec<TileBuf>` (stable slots) plus a `BTreeMap`
19//! from [`TileId`] to slot for find-or-insert and ordered reads; [`SlicerOne`] holds one `TileBuf`.
20
21use std::collections::BTreeMap;
22
23use geo_types::Coord;
24
25use crate::TileError;
26use crate::clip_polyline::to_local;
27use crate::grid::{Grid, RouteSink};
28use crate::tile::TileId;
29use crate::vertex::Vertex;
30
31/// One tile's accumulated geometry, flattened into a single vertex arena with `u32` boundary offsets
32/// instead of nested `Vec`s. Only non-empty features are recorded.
33/// V is the vertex type that must implement [Vertex] trait, defaults to `Coord<i32>`
34/// A is the attribute type that must be clonable, defaults to `()`
35#[derive(Debug, Clone, PartialEq, Eq)]
36struct TileBuf<V, A> {
37    /// The tile this buffer holds.
38    tile: TileId,
39    /// Every run's vertices concatenated, in the tile's local frame.
40    verts: Vec<V>,
41    /// End offset into `verts` per run: run `r` is `verts[run_ends[r-1]..run_ends[r]]` (`run_ends[-1] ≡ 0`).
42    run_ends: Vec<u32>,
43    /// End offset into `run_ends` per feature: feature `f` owns runs `run_ends[feat_ends[f-1]..feat_ends[f]]`, ≥1 each.
44    feat_ends: Vec<u32>,
45    /// Per-feature attribute, parallel to `feat_ends`. `A = ()` never allocates, so the channel is free unless used.
46    feat_attrs: Vec<A>,
47    /// [`SlicerAll`] step this tile was last written at — vs. the current step (run continuity) and the
48    /// feature start (new feature vs. re-entry). Unused by [`SlicerOne`].
49    open_step: u64,
50}
51
52impl<V, A> TileBuf<V, A> {
53    fn new(tile: TileId) -> Self {
54        Self {
55            tile,
56            verts: Vec::new(),
57            run_ends: Vec::new(),
58            feat_ends: Vec::new(),
59            feat_attrs: Vec::new(),
60            open_step: 0,
61        }
62    }
63
64    /// Append one run's vertices to the arena and record its end offset.
65    #[expect(
66        clippy::cast_possible_truncation,
67        reason = "a tile holds far fewer than u32::MAX vertices (a polyline is capped at u16 each)"
68    )]
69    fn push_run(&mut self, run: Vec<V>) {
70        self.verts.extend(run);
71        self.run_ends.push(self.verts.len() as u32);
72    }
73
74    /// Record non-empty `runs` as one new feature carrying `attr` in this tile (each added polyline
75    /// is independent).
76    #[expect(
77        clippy::cast_possible_truncation,
78        reason = "run count per tile stays far below u32::MAX"
79    )]
80    fn absorb(&mut self, runs: Vec<Vec<V>>, attr: A) {
81        debug_assert!(!runs.is_empty(), "empty features are never recorded");
82        for run in runs {
83            self.push_run(run);
84        }
85        self.feat_ends.push(self.run_ends.len() as u32);
86        self.feat_attrs.push(attr);
87    }
88}
89
90/// Iterate a tile's features from its flat offset arrays, reconstructing each as a [`FeatureView`].
91fn features_of<V: Vertex, A>(
92    buf: &TileBuf<V, A>,
93) -> impl Iterator<Item = FeatureView<'_, V, A>> + use<'_, V, A> {
94    let verts = buf.verts.as_slice();
95    let run_ends = buf.run_ends.as_slice();
96    let feat_ends = buf.feat_ends.as_slice();
97    let feat_attrs = buf.feat_attrs.as_slice();
98    (0..feat_ends.len()).map(move |f| {
99        let rs = if f == 0 { 0 } else { feat_ends[f - 1] as usize };
100        let re = feat_ends[f] as usize;
101        // Vertex offset where this feature's first run begins (end of the previous feature's run).
102        let start = if rs == 0 { 0 } else { run_ends[rs - 1] };
103        FeatureView {
104            verts,
105            start,
106            run_ends: &run_ends[rs..re],
107            attr: &feat_attrs[f],
108        }
109    })
110}
111
112/// A borrowed view of one tile's accumulated features, yielded by [`SlicerAll::iter_tiles`].
113pub struct TileView<'a, V: Vertex, A = ()> {
114    buf: &'a TileBuf<V, A>,
115}
116
117impl<'a, V: Vertex, A> TileView<'a, V, A> {
118    /// The tile this view is for.
119    #[must_use]
120    pub fn tile_id(&self) -> TileId {
121        self.buf.tile
122    }
123
124    /// Iterate this tile's features, in the order they were first added.
125    pub fn iter_features(&self) -> impl Iterator<Item = FeatureView<'a, V, A>> + use<'a, V, A> {
126        features_of(self.buf)
127    }
128}
129
130/// A borrowed view of one feature's clipped polylines within a tile, yielded by
131/// [`TileView::iter_features`] / [`SlicerOne::iter_features`].
132pub struct FeatureView<'a, V: Vertex, A = ()> {
133    /// The whole tile's vertex arena; polylines are subslices of it.
134    verts: &'a [V],
135    /// Vertex offset where this feature's first run begins.
136    start: u32,
137    /// End offsets (into `verts`) of this feature's runs.
138    run_ends: &'a [u32],
139    /// This feature's per-feature attribute (duplicated into every tile it touches). `&()` when the
140    /// attribute channel is unused.
141    attr: &'a A,
142}
143
144impl<'a, V: Vertex, A> FeatureView<'a, V, A> {
145    /// This feature's per-feature attribute in this tile — the value passed to
146    /// [`add_feature_with`](SlicerAll::add_feature_with), duplicated onto every tile the feature
147    /// touches. With the default `A = ()` this is a reference to the unit value.
148    #[must_use]
149    pub fn attr(&self) -> &'a A {
150        self.attr
151    }
152
153    /// Iterate this feature's polylines (runs) in this tile, each a vertex slice in the tile's local
154    /// frame. A feature yields several polylines where the input left the tile and re-entered.
155    pub fn iter_polylines(&self) -> impl Iterator<Item = &'a [V]> + use<'a, V, A> {
156        let verts = self.verts;
157        let mut prev = self.start as usize;
158        self.run_ends.iter().map(move |&end| {
159            let end = end as usize;
160            let run = &verts[prev..end];
161            prev = end;
162            run
163        })
164    }
165}
166
167/// Slices integer polylines into per-tile pieces on an integer grid, accumulating every tile each
168/// polyline touches.
169///
170/// Generic over two independent axes:
171/// - the [`Vertex`] type `V` (defaults to [`Coord<i32>`]) — plain coordinates or payload-carrying
172///   vertices (e.g. a **per-vertex** M value via [`Measured`](crate::Measured)); the payload rides
173///   through slicing unchanged;
174/// - a **per-feature** attribute type `A` (defaults to `()`) — an id, a property map, anything you
175///   want re-associated with each sliced piece. With `A = ()` the attribute channel is a zero-sized
176///   type and costs nothing.
177///
178/// The two are orthogonal: `SlicerAll<Measured<M>, Attrs>` carries per-vertex M *and* per-feature
179/// attributes without either taxing the other. Add a polyline with no attribute via
180/// [`add_feature`](Self::add_feature) (only when `A = ()`), or with one via
181/// [`add_feature_with`](Self::add_feature_with); read them back with
182/// [`iter_tiles`](Self::iter_tiles), reaching each piece's attribute through
183/// [`FeatureView::attr`]. A feature split across tiles has its attribute duplicated onto every tile
184/// it touches (so `A` must be [`Clone`]).
185///
186/// The slicer never panics: bad input (an oversized polyline, or coordinates that overflow the tile
187/// math) yields an [`TileError`] instead.
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct SlicerAll<V: Vertex = Coord<i32>, A = ()> {
190    grid: Grid,
191    /// Per-tile buffers in insertion order; slots are stable (never moved) so `index` can address
192    /// them by position.
193    tiles: Vec<TileBuf<V, A>>,
194    /// [`TileId`] → slot in `tiles`, ordered so reads come out by tile and find-or-insert is cheap.
195    index: BTreeMap<TileId, u32>,
196    /// Monotonic segment counter for the direct-build run continuity (see [`RouteSink::emit`]). A gap
197    /// is inserted at each polyline boundary so a run never continues across separate polylines.
198    step: u64,
199    /// The `step` value at the start of the feature currently being added (set in
200    /// [`begin_polyline`](RouteSink::begin_polyline)). A tile whose `open_step` predates it has not yet
201    /// been touched by this feature, so the next run there opens a new feature rather than extending.
202    feature_start: u64,
203    /// Most-recently resolved `(tile, slot)`, so consecutive writes to the same tile — the common
204    /// case for a dense polyline — skip the `index` lookup entirely. Slots are stable, so this never
205    /// goes stale; [`clear`](Self::clear) resets it.
206    last_slot: Option<(TileId, u32)>,
207    /// The attribute of the feature currently being routed (set by
208    /// [`add_feature_with`](Self::add_feature_with)), cloned into each tile the feature reaches.
209    /// `None` between features.
210    pending_attr: Option<A>,
211}
212
213impl<V: Vertex, A> SlicerAll<V, A> {
214    fn from_grid(grid: Grid) -> Self {
215        Self {
216            grid,
217            tiles: Vec::new(),
218            index: BTreeMap::new(),
219            step: 0,
220            feature_start: 0,
221            last_slot: None,
222            pending_attr: None,
223        }
224    }
225
226    /// Create a slicer with the given tile side / per-tile output resolution `extent` and `buffer`.
227    ///
228    /// Coordinates are integers in tile space: a vertex belongs to tile `x.div_euclid(extent)` and is
229    /// emitted at `x − tile·extent ∈ [0, extent)`. Project / simplify / affine-scale float source data
230    /// into this space (e.g. with `geo`) before slicing.
231    ///
232    /// # Errors
233    ///
234    /// - [`TileError::InvalidExtent`] if `extent` is `0` or greater than `i32::MAX`.
235    /// - [`TileError::BufferTooLarge`] if `buffer` is not strictly less than half the `extent`.
236    pub fn new(extent: u32, buffer: u16) -> Result<Self, TileError> {
237        Ok(Self::from_grid(Grid::new(extent, buffer)?))
238    }
239
240    /// The tile side / per-tile output resolution: kept vertices land in `0..extent`.
241    #[must_use]
242    pub fn extent(&self) -> u32 {
243        self.grid.extent()
244    }
245
246    /// The buffer kept around every tile, in tile-space units.
247    #[must_use]
248    pub fn buffer(&self) -> u16 {
249        self.grid.buffer()
250    }
251
252    /// Add `polyline` as an independent feature carrying `attr`: slice it into every tile it touches,
253    /// storing its runs as a fresh feature in each and duplicating `attr` onto every tile it reaches.
254    /// Chainable. Reach the attribute back through [`FeatureView::attr`].
255    ///
256    /// When `A = ()`, prefer [`add_feature`](Self::add_feature), which passes the unit attribute for
257    /// you.
258    ///
259    /// **Atomic:** on error the pieces written so far are rolled back, so a failed polyline leaves the
260    /// accumulator unchanged and usable — safe to skip the offending input and keep adding. (Errors
261    /// only arise for pathological input: an oversized polyline, an adversarial spread, or coordinates
262    /// near the `i32` limits.)
263    ///
264    /// # Errors
265    ///
266    /// [`TileError::PolylineTooLarge`], [`TileError::TooManyTiles`], or [`TileError::Overflow`].
267    #[cfg_attr(feature = "hotpath", hotpath::measure)]
268    pub fn add_feature_with<P: AsRef<[V]>>(
269        &mut self,
270        polyline: P,
271        attr: A,
272    ) -> Result<&mut Self, TileError>
273    where
274        A: Clone,
275    {
276        let grid = self.grid; // `Grid` is `Copy`, so the walk can borrow `self` mutably as the sink.
277        // Savepoint for rollback: how many tiles existed, and the last step used, before this feature.
278        // Every piece this feature writes lands in a tile created after `tiles_before`, or bumps an
279        // existing tile's `open_step` past `step_before` — so the two mark exactly what to undo.
280        let tiles_before = self.tiles.len();
281        let step_before = self.step;
282        self.pending_attr = Some(attr);
283        let result = grid.route(polyline.as_ref(), self);
284        self.pending_attr = None;
285        if let Err(err) = result {
286            self.rollback_feature(tiles_before, step_before);
287            return Err(err);
288        }
289        Ok(self)
290    }
291
292    /// Undo a partially-written feature after [`add_feature`](Self::add_feature) errored mid-walk,
293    /// restoring the observable state (tiles and their features) to the savepoint. Only ever runs on
294    /// the rare error path, so an `O(tiles)` scan is fine.
295    fn rollback_feature(&mut self, tiles_before: usize, step_before: u64) {
296        // Tiles that already existed but this feature appended to (`open_step` moved past the
297        // savepoint): drop the single feature span it added, and the runs/vertices behind it.
298        for buf in &mut self.tiles[..tiles_before] {
299            if buf.open_step > step_before {
300                buf.feat_ends.pop();
301                buf.feat_attrs.pop();
302                let runs_keep = buf.feat_ends.last().copied().unwrap_or(0) as usize;
303                let verts_keep = if runs_keep == 0 {
304                    0
305                } else {
306                    buf.run_ends[runs_keep - 1] as usize
307                };
308                buf.run_ends.truncate(runs_keep);
309                buf.verts.truncate(verts_keep);
310            }
311        }
312        // Tiles created during this feature are contiguous at the end: drop them and their index
313        // entries. (Their stale `open_step` on kept tiles is harmless — the step counter only climbs,
314        // so a later feature's start still outranks it and reads it as a fresh, separate feature.)
315        for buf in self.tiles.drain(tiles_before..) {
316            self.index.remove(&buf.tile);
317        }
318        self.last_slot = None;
319    }
320
321    /// The slot of `tile`'s buffer, creating it on first touch. A one-entry cache skips the `index`
322    /// lookup when the same tile was just written (consecutive segments in a tile — the common case).
323    #[expect(
324        clippy::cast_possible_truncation,
325        reason = "tile count stays far below u32::MAX"
326    )]
327    fn tile_slot(&mut self, tile: TileId) -> usize {
328        if let Some((last, slot)) = self.last_slot
329            && last == tile
330        {
331            return slot as usize;
332        }
333        let slot = if let Some(&slot) = self.index.get(&tile) {
334            slot
335        } else {
336            let slot = self.tiles.len() as u32;
337            self.tiles.push(TileBuf::new(tile));
338            self.index.insert(tile, slot);
339            slot
340        };
341        self.last_slot = Some((tile, slot));
342        slot as usize
343    }
344
345    /// Iterate the touched tiles, ordered by [`TileId`], borrowing so the accumulator can keep
346    /// growing afterwards. Each [`TileView`] exposes that tile's features.
347    pub fn iter_tiles(&self) -> impl Iterator<Item = TileView<'_, V, A>> {
348        let tiles = self.tiles.as_slice();
349        self.index.values().map(move |&slot| TileView {
350            buf: &tiles[slot as usize],
351        })
352    }
353
354    /// Number of tiles the accumulator has touched.
355    #[must_use]
356    pub fn len(&self) -> usize {
357        self.tiles.len()
358    }
359
360    /// Whether nothing has been accumulated yet.
361    #[must_use]
362    pub fn is_empty(&self) -> bool {
363        self.tiles.is_empty()
364    }
365
366    /// Discard everything accumulated so far, keeping the extent/buffer config.
367    pub fn clear(&mut self) {
368        self.tiles.clear();
369        self.index.clear();
370        self.step = 0;
371        self.feature_start = 0;
372        self.last_slot = None;
373        self.pending_attr = None;
374    }
375}
376
377impl<V: Vertex> SlicerAll<V, ()> {
378    /// Add `polyline` as an independent feature with no attribute — shorthand for
379    /// [`add_feature_with`](Self::add_feature_with)`(polyline, ())`. Available only when `A = ()`.
380    ///
381    /// # Errors
382    ///
383    /// [`TileError::PolylineTooLarge`], [`TileError::TooManyTiles`], or [`TileError::Overflow`].
384    pub fn add_feature<P: AsRef<[V]>>(&mut self, polyline: P) -> Result<&mut Self, TileError> {
385        self.add_feature_with(polyline, ())
386    }
387}
388
389impl<V: Vertex, A: Clone> RouteSink<V> for SlicerAll<V, A> {
390    /// A polyline boundary: burn one step so the first segment of this polyline cannot continue a run
391    /// left open by the previous polyline (its runs stay separate, even in a shared tile), and record
392    /// that step as this feature's start so `emit` knows which tiles it has already reached.
393    fn begin_polyline(&mut self) {
394        self.step = self.step.wrapping_add(1);
395        self.feature_start = self.step;
396    }
397
398    /// Advance to the next segment.
399    fn begin_segment(&mut self) {
400        self.step = self.step.wrapping_add(1);
401    }
402
403    /// Append segment `a`–`c` to `tile`'s buffer (localized by `origin`), extending the tile's open
404    /// run if the preceding one or two segments also landed here, else starting a new run — and, when
405    /// a new run, opening a new feature unless this feature already reached the tile (a re-entry).
406    #[expect(
407        clippy::cast_possible_truncation,
408        reason = "run/vertex counts per tile stay far below u32::MAX"
409    )]
410    fn emit(&mut self, tile: TileId, origin: Coord<i32>, a: V, c: V) -> Result<(), TileError> {
411        let step = self.step;
412        let feature_start = self.feature_start;
413        let slot = self.tile_slot(tile);
414        let buf = &mut self.tiles[slot];
415        // Continue the open run iff the previous segment (step − 1) also landed in this tile. The
416        // per-polyline gap in `begin_polyline` guarantees this never matches across polylines, and a
417        // fresh tile (`open_step == 0`) never matches (the first step is ≥ 2 after the gap).
418        let continues = buf.open_step == step.wrapping_sub(1);
419        // Bridge a single-segment excursion: this tile was last written exactly two steps ago (the one
420        // segment in between missed it) within *this* feature. That skipped segment connects the run's
421        // tail to this segment's `a` — both kept — so join them into one run rather than splitting,
422        // matching `slice_one`'s "break only at a dropped vertex". The `feature_start` guard keeps the
423        // two-step gap from spanning a polyline boundary (where it would be a coincidence, not a
424        // bridge).
425        let bridges =
426            !continues && buf.open_step == step.wrapping_sub(2) && buf.open_step >= feature_start;
427        let c_local = to_local(c, origin)?;
428        if continues {
429            buf.verts.push(c_local);
430            *buf.run_ends
431                .last_mut()
432                .expect("continuing implies an open run") = buf.verts.len() as u32;
433        } else if bridges {
434            // Append both endpoints of this segment: `a` is the bridge vertex just outside the tile,
435            // not yet in the run.
436            buf.verts.push(to_local(a, origin)?);
437            buf.verts.push(c_local);
438            *buf.run_ends
439                .last_mut()
440                .expect("bridging implies an open run") = buf.verts.len() as u32;
441        } else {
442            let a_local = to_local(a, origin)?;
443            buf.verts.push(a_local);
444            buf.verts.push(c_local);
445            buf.run_ends.push(buf.verts.len() as u32);
446            // Every step of this feature is ≥ `feature_start`, so an earlier `open_step` means this
447            // tile has not yet been touched by the current feature → open a new one. Otherwise the
448            // feature already has a run here (it left and re-entered) → extend that run span.
449            if buf.open_step < feature_start {
450                buf.feat_ends.push(buf.run_ends.len() as u32);
451                // A new feature span in this tile: record the current feature's attribute (cloned,
452                // so a feature spanning several tiles carries it into each).
453                buf.feat_attrs.push(
454                    self.pending_attr
455                        .as_ref()
456                        .expect("a feature is being routed")
457                        .clone(),
458                );
459            } else {
460                *buf.feat_ends
461                    .last_mut()
462                    .expect("a prior run this feature implies a feature span") =
463                    buf.run_ends.len() as u32;
464            }
465        }
466        buf.open_step = step;
467        Ok(())
468    }
469}
470
471/// Slices integer polylines into pieces for **one fixed tile**, accumulating only that tile.
472///
473/// The single-tile counterpart to [`SlicerAll`]: [`add_feature`](Self::add_feature) /
474/// [`add_feature_with`](Self::add_feature_with) work exactly the same, but each polyline is clipped
475/// only to this slicer's [`tile`](Self::tile). Because there is a single tile, the read API skips the
476/// tile level — [`iter_features`](Self::iter_features) yields the features directly.
477///
478/// Generic over the [`Vertex`] type `V` (defaults to [`Coord<i32>`]) and the per-feature attribute
479/// type `A` (defaults to `()`); see [`SlicerAll`] for both axes.
480#[derive(Debug, Clone, PartialEq, Eq)]
481pub struct SlicerOne<V: Vertex = Coord<i32>, A = ()> {
482    grid: Grid,
483    buf: TileBuf<V, A>,
484}
485
486impl<V: Vertex, A> SlicerOne<V, A> {
487    fn from_grid(grid: Grid, tile: TileId) -> Self {
488        Self {
489            grid,
490            buf: TileBuf::new(tile),
491        }
492    }
493
494    /// Create a slicer bound to `tile`, with the given tile side / per-tile output resolution `extent`
495    /// and `buffer` (see [`SlicerAll::new`](crate::SlicerAll::new) for the coordinate model).
496    ///
497    /// # Errors
498    ///
499    /// - [`TileError::InvalidExtent`] if `extent` is `0` or greater than `i32::MAX`.
500    /// - [`TileError::BufferTooLarge`] if `buffer` is not strictly less than half the `extent`.
501    pub fn new(extent: u32, buffer: u16, tile: TileId) -> Result<Self, TileError> {
502        Ok(Self::from_grid(Grid::new(extent, buffer)?, tile))
503    }
504
505    /// The tile side / per-tile output resolution: kept vertices land in `0..extent`.
506    #[must_use]
507    pub fn extent(&self) -> u32 {
508        self.grid.extent()
509    }
510
511    /// The buffer kept around every tile, in tile-space units.
512    #[must_use]
513    pub fn buffer(&self) -> u16 {
514        self.grid.buffer()
515    }
516
517    /// The tile this slicer clips into.
518    #[must_use]
519    pub fn tile(&self) -> TileId {
520        self.buf.tile
521    }
522
523    /// Add `polyline` as an independent feature carrying `attr`, clipped to this slicer's
524    /// [`tile`](Self::tile). Chainable. The feature (and its attribute) is recorded only if something
525    /// of `polyline` lands in the tile; otherwise `attr` is dropped. Reach it back through
526    /// [`FeatureView::attr`].
527    ///
528    /// When `A = ()`, prefer [`add_feature`](Self::add_feature).
529    ///
530    /// Atomic: the polyline is fully sliced first, so on error the accumulator is left unchanged.
531    ///
532    /// # Errors
533    ///
534    /// [`TileError::Overflow`] if the tile's box or a kept vertex overflows `i32`.
535    #[cfg_attr(feature = "hotpath", hotpath::measure)]
536    pub fn add_feature_with<P: AsRef<[V]>>(
537        &mut self,
538        polyline: P,
539        attr: A,
540    ) -> Result<&mut Self, TileError> {
541        let runs = self.grid.slice_one(polyline.as_ref(), self.buf.tile)?;
542        if !runs.is_empty() {
543            self.buf.absorb(runs, attr);
544        }
545        Ok(self)
546    }
547
548    /// Iterate this tile's features, in the order they were first added. Each [`FeatureView`] exposes
549    /// that feature's clipped polylines and its [`attr`](FeatureView::attr).
550    pub fn iter_features(&self) -> impl Iterator<Item = FeatureView<'_, V, A>> {
551        features_of(&self.buf)
552    }
553
554    /// Number of features accumulated for the tile.
555    #[must_use]
556    pub fn len(&self) -> usize {
557        self.buf.feat_ends.len()
558    }
559
560    /// Whether nothing has been accumulated yet.
561    #[must_use]
562    pub fn is_empty(&self) -> bool {
563        self.buf.feat_ends.is_empty()
564    }
565
566    /// Discard everything accumulated so far, keeping the extent/buffer/tile config.
567    pub fn clear(&mut self) {
568        self.buf = TileBuf::new(self.buf.tile);
569    }
570}
571
572impl<V: Vertex> SlicerOne<V, ()> {
573    /// Add `polyline` as an independent feature with no attribute — shorthand for
574    /// [`add_feature_with`](Self::add_feature_with)`(polyline, ())`. Available only when `A = ()`.
575    ///
576    /// # Errors
577    ///
578    /// [`TileError::Overflow`] if the tile's box or a kept vertex overflows `i32`.
579    pub fn add_feature<P: AsRef<[V]>>(&mut self, polyline: P) -> Result<&mut Self, TileError> {
580        self.add_feature_with(polyline, ())
581    }
582}