Skip to main content

rasterrocket_render/scanner/
iter.rs

1//! Coalescing span iterator over a scanner's intersection row.
2//!
3//! [`crate::ScanIterator`] mirrors `SplashXPathScanIterator::getNextSpan` from
4//! `splash/SplashXPathScanner.cc`. It merges adjacent and overlapping
5//! intersections into contiguous `(x0, x1)` spans (inclusive) and handles
6//! both even-odd and non-zero winding fill rules via the `eo_mask`.
7//!
8//! ## Winding-rule semantics
9//!
10//! The iterator maintains a running `count` — the sum of all
11//! [`Intersect::count`] values consumed so far. Whether the current pixel
12//! position is *inside* the path depends on the fill rule:
13//!
14//! - **Non-zero winding** (`eo = false`): inside when `count != 0`.
15//!   `eo_mask = !0` so `count & eo_mask` is non-zero iff `count != 0`.
16//! - **Even-odd** (`eo = true`): inside when the crossing count is odd.
17//!   `eo_mask = 1` so `count & eo_mask` isolates the low bit (parity).
18//!
19//! ## Algorithm (`next`)
20//!
21//! ### Phase 1 — find the span start
22//!
23//! Before consuming each entry, check `count & eo_mask`:
24//!
25//! - Already non-zero → we are inside from a previous span's count residue;
26//!   the current entry (`row[idx]`) is the span start. Record its `x0` and go
27//!   to phase 2 *without* consuming it yet (phase 2 will consume it).
28//! - Zero → consume the entry (add its `count`, advance `idx`). If now
29//!   non-zero → this entry is the span start; record its `x0` (from
30//!   `row[idx - 1]`) and go to phase 2 (the entry is already consumed).
31//! - Still zero → continue to the next entry.
32//!
33//! ### Phase 2 — collect the span
34//!
35//! Starting from `row[phase2_start_idx]` (inclusive), consume entries, tracking
36//! the maximum `x1` seen, until `count & eo_mask == 0`. Emit `(x0, x1)`.
37//!
38//! Each `Intersect` entry's `count` is accumulated **exactly once** across the
39//! two phases. After `next` returns, `count` equals the winding number
40//! immediately after the rightmost covered pixel `x1`.
41
42use super::{Intersect, XPathScanner};
43
44/// An iterator that yields `(x0, x1)` inclusive pixel spans for one scanline.
45///
46/// Spans are emitted in left-to-right order with no overlap.
47///
48/// Constructed via [`ScanIterator::new`]. Out-of-range `y` values produce an
49/// immediately-exhausted iterator (matching the C++ empty-row behaviour).
50pub struct ScanIterator<'a> {
51    /// The sorted intersection list for this scanline.
52    row: &'a [Intersect],
53    /// Index of the next un-consumed entry in `row`.
54    idx: usize,
55    /// Running winding count: sum of `Intersect::count` for all consumed entries.
56    ///
57    /// Represents the winding number immediately *after* the rightmost pixel
58    /// covered by the last consumed entry. Starts at 0 (outside the path).
59    count: i32,
60    /// Fill-rule mask.
61    ///
62    /// `!0` for non-zero winding (`count != 0` → inside); `1` for even-odd
63    /// (`count & 1 != 0` → inside). See module-level doc for details.
64    eo_mask: i32,
65}
66
67impl<'a> ScanIterator<'a> {
68    /// Create an iterator for scanline `y` of `scanner`.
69    ///
70    /// If `y` is outside the scanner's range, the iterator is immediately
71    /// exhausted (matching C++ behaviour where the empty `allIntersections[0]`
72    /// row is returned with `interIdx = line.size()`).
73    #[must_use]
74    pub fn new(scanner: &'a XPathScanner, y: i32) -> Self {
75        let row = scanner.row(y);
76        let eo_mask = if scanner.eo { 1i32 } else { !0i32 };
77        Self {
78            row,
79            idx: 0,
80            count: 0,
81            eo_mask,
82        }
83    }
84}
85
86impl Iterator for ScanIterator<'_> {
87    /// `(x0, x1)` — both inclusive pixel coordinates.
88    type Item = (i32, i32);
89
90    /// Advance to the next filled span.
91    ///
92    /// See the [module-level algorithm description](self) for a precise
93    /// account of the two-phase skip-then-collect approach and the
94    /// winding-rule semantics governing each phase.
95    fn next(&mut self) -> Option<(i32, i32)> {
96        // ── Phase 1: find the entry that starts the next filled span ──────────
97        //
98        // We peek at `count & eo_mask` *before* consuming each entry:
99        //   • Non-zero already → inside from previous call's count residue.
100        //     `row[idx]` is the span start; leave idx unchanged for phase 2.
101        //   • Zero → consume the entry; if count goes non-zero this entry is
102        //     the span start (idx was already advanced).
103        //   • Still zero → not inside; continue to the next entry.
104        let (x0, phase2_start) = loop {
105            if self.idx >= self.row.len() {
106                return None;
107            }
108            if (self.count & self.eo_mask) != 0 {
109                // Already inside (residue from previous span).
110                // The span starts at row[idx]; do NOT consume it here —
111                // phase 2 will consume it as the first collected entry.
112                let x0 = self.row[self.idx].x0;
113                break (x0, self.idx);
114            }
115            // Try consuming this entry.
116            let new_count = self.count.wrapping_add(self.row[self.idx].count);
117            self.idx += 1;
118            self.count = new_count;
119            if (self.count & self.eo_mask) != 0 {
120                // This entry pushed us inside; it is the span start.
121                let x0 = self.row[self.idx - 1].x0;
122                break (x0, self.idx - 1);
123            }
124            // Still outside; continue.
125        };
126
127        // ── Phase 2: collect entries while inside the path ────────────────────
128        //
129        // `phase2_start` is the index of the entry that starts the span.
130        //
131        // "transition" branch: that entry was already consumed in phase 1
132        //   (self.idx == phase2_start + 1); seed x1 from it, then continue.
133        // "already inside" branch: that entry has NOT been consumed yet
134        //   (self.idx == phase2_start); consume it now to seed x1.
135        let mut x1 = self.row[phase2_start].x1;
136        if self.idx == phase2_start {
137            // "already inside": consume the span-start entry.
138            self.count = self.count.wrapping_add(self.row[self.idx].count);
139            self.idx += 1;
140        }
141        // "transition": span-start is already consumed; self.idx is correct.
142
143        // Continue consuming while still inside.
144        while self.idx < self.row.len() && (self.count & self.eo_mask) != 0 {
145            x1 = x1.max(self.row[self.idx].x1);
146            self.count = self.count.wrapping_add(self.row[self.idx].count);
147            self.idx += 1;
148        }
149
150        Some((x0, x1))
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::path::PathBuilder;
158    use crate::scanner::XPathScanner;
159    use crate::xpath::XPath;
160
161    fn identity() -> [f64; 6] {
162        [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
163    }
164
165    fn rect_xpath(x0: f64, y0: f64, x1: f64, y1: f64) -> XPath {
166        let mut b = PathBuilder::new();
167        b.move_to(x0, y0).unwrap();
168        b.line_to(x1, y0).unwrap();
169        b.line_to(x1, y1).unwrap();
170        b.line_to(x0, y1).unwrap();
171        b.close(false).unwrap();
172        XPath::new(&b.build(), &identity(), 1.0, true)
173    }
174
175    #[test]
176    fn rect_span_single_row() {
177        let xpath = rect_xpath(2.0, 1.0, 6.0, 3.0);
178        let scanner = XPathScanner::new(&xpath, false, 0, 10);
179        let spans: Vec<_> = ScanIterator::new(&scanner, 2).collect();
180        assert!(!spans.is_empty(), "expected at least one span on y=2");
181        // The span should cover roughly [2, 5] (inclusive).
182        let (x0, x1) = spans[0];
183        assert!(x0 <= 2, "x0={x0}");
184        assert!(x1 >= 4, "x1={x1}");
185    }
186
187    #[test]
188    fn out_of_range_y_empty() {
189        let xpath = rect_xpath(0.0, 0.0, 4.0, 4.0);
190        let scanner = XPathScanner::new(&xpath, false, 0, 10);
191        assert!(ScanIterator::new(&scanner, 100).next().is_none());
192    }
193
194    #[test]
195    fn coalescing_adjacent_nz() {
196        // NZ winding: [0,3] count=-1 then [4,7] count=+1.
197        // count: 0 → (consume row[0]) -1 → inside; span starts at x0=0.
198        // phase2: x1=3, count=-1. Consume row[1]: x1=max(3,7)=7, count=0. Outside.
199        // Emit (0, 7). One span total.
200        use crate::scanner::Intersect;
201        let row = vec![
202            Intersect {
203                x0: 0,
204                x1: 3,
205                count: -1,
206            },
207            Intersect {
208                x0: 4,
209                x1: 7,
210                count: 1,
211            },
212        ];
213        let scanner_stub = XPathScanner {
214            eo: false,
215            x_min: 0,
216            x_max: 7,
217            y_min: 0,
218            y_max: 0,
219            row_start: vec![0, 2],
220            intersects: row,
221        };
222        let spans: Vec<_> = ScanIterator::new(&scanner_stub, 0).collect();
223        assert_eq!(spans, vec![(0, 7)]);
224    }
225
226    #[test]
227    fn two_disjoint_spans_nz() {
228        // NZ winding: two separate filled regions.
229        // [0,2,-1] → inside; [3,5,+1] → count=0, outside → emit (0,5).
230        // Wait, -1+1=0 so this is actually one span [0,5] that ends at row[1].
231        // Use a different configuration for genuinely two spans:
232        // [0,2,-1], [3,5,+1], [8,10,-1], [11,13,+1]
233        // Call 1: consume [0,2,-1] → inside, x0=0. Phase2: x1=2, count=-1.
234        //   Consume [3,5,+1]: x1=max(2,5)=5, count=0. Outside. Emit (0,5).
235        // Call 2: count=0. Consume [8,10,-1] → inside, x0=8. Phase2: x1=10, count=-1.
236        //   Consume [11,13,+1]: x1=max(10,13)=13, count=0. Outside. Emit (8,13).
237        use crate::scanner::Intersect;
238        let row = vec![
239            Intersect {
240                x0: 0,
241                x1: 2,
242                count: -1,
243            },
244            Intersect {
245                x0: 3,
246                x1: 5,
247                count: 1,
248            },
249            Intersect {
250                x0: 8,
251                x1: 10,
252                count: -1,
253            },
254            Intersect {
255                x0: 11,
256                x1: 13,
257                count: 1,
258            },
259        ];
260        let scanner_stub = XPathScanner {
261            eo: false,
262            x_min: 0,
263            x_max: 13,
264            y_min: 0,
265            y_max: 0,
266            row_start: vec![0, 4],
267            intersects: row,
268        };
269        let spans: Vec<_> = ScanIterator::new(&scanner_stub, 0).collect();
270        assert_eq!(spans, vec![(0, 5), (8, 13)]);
271    }
272
273    #[test]
274    fn eo_winding_two_spans() {
275        // Even-odd: crossings toggle inside/outside at each entry.
276        // All entries have count=1 (EO only looks at parity).
277        // [0,2,1]: count 0→1 (odd=inside), x0=0.
278        // [3,5,1]: count 1→2 (even=outside), x1=5. Emit (0,5). (spans don't
279        //          cleanly separate here since x1 extends through the exit entry)
280        //   Actually x1=max(2,5)=5 when consuming [3,5].
281        // [8,10,1]: count 2→3 (odd=inside), x0=8.
282        // [11,13,1]: count 3→4 (even=outside), x1=max(10,13)=13. Emit (8,13).
283        use crate::scanner::Intersect;
284        let row = vec![
285            Intersect {
286                x0: 0,
287                x1: 2,
288                count: 1,
289            },
290            Intersect {
291                x0: 3,
292                x1: 5,
293                count: 1,
294            },
295            Intersect {
296                x0: 8,
297                x1: 10,
298                count: 1,
299            },
300            Intersect {
301                x0: 11,
302                x1: 13,
303                count: 1,
304            },
305        ];
306        let scanner_stub = XPathScanner {
307            eo: true,
308            x_min: 0,
309            x_max: 13,
310            y_min: 0,
311            y_max: 0,
312            row_start: vec![0, 4],
313            intersects: row,
314        };
315        let spans: Vec<_> = ScanIterator::new(&scanner_stub, 0).collect();
316        assert_eq!(spans, vec![(0, 5), (8, 13)]);
317    }
318
319    #[test]
320    fn already_inside_residue() {
321        // Simulate the "already inside" case: count=-1 at the start of next().
322        // row has one entry [{5,7,+1}]. count should go to 0 after consuming it.
323        // We test this by directly manipulating iterator state.
324        use crate::scanner::Intersect;
325        let row = vec![Intersect {
326            x0: 5,
327            x1: 7,
328            count: 1,
329        }];
330        let scanner_stub = XPathScanner {
331            eo: false,
332            x_min: 5,
333            x_max: 7,
334            y_min: 0,
335            y_max: 0,
336            row_start: vec![0, 1],
337            intersects: row,
338        };
339        let mut it = ScanIterator::new(&scanner_stub, 0);
340        // Manually set count to -1 to simulate residue from a prior span.
341        it.count = -1;
342        // With count=-1 (inside), phase 1 immediately uses row[0] as the span
343        // start without consuming it; phase 2 consumes it: x1=7, count=0.
344        assert_eq!(it.next(), Some((5, 7)));
345        assert_eq!(it.next(), None);
346    }
347}