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}