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}