Skip to main content

agg_rust/
rasterizer_scanline_aa.rs

1//! High-level polygon scanline rasterizer with anti-aliasing.
2//!
3//! Port of `agg_rasterizer_scanline_aa_nogamma.h` — the heart of AGG's
4//! rendering pipeline. Accepts polygon contours (move_to/line_to/close),
5//! rasterizes them into anti-aliased scanlines, and feeds the scanlines
6//! to a renderer.
7//!
8//! This is the "nogamma" variant that returns raw coverage values (0..255).
9//! Gamma correction can be applied in the renderer or pixel format layer.
10
11use crate::basics::{
12    is_close, is_move_to, is_stop, is_vertex, FillingRule, VertexSource, POLY_SUBPIXEL_SHIFT,
13};
14use crate::rasterizer_cells_aa::{CellAa, RasterizerCellsAa, ScanlineHitTest};
15use crate::rasterizer_sl_clip::{poly_coord, RasterizerSlClipInt};
16
17// ============================================================================
18// AA scale constants
19// ============================================================================
20
21const AA_SHIFT: u32 = 8;
22const AA_SCALE: u32 = 1 << AA_SHIFT;
23const AA_MASK: u32 = AA_SCALE - 1;
24const AA_SCALE2: u32 = AA_SCALE * 2;
25const AA_MASK2: u32 = AA_SCALE2 - 1;
26
27// ============================================================================
28// Scanline trait — the interface that sweep_scanline feeds data into
29// ============================================================================
30
31/// Trait for scanline containers that accumulate coverage data.
32///
33/// Implementations include `ScanlineU8` (unpacked per-pixel coverage),
34/// `ScanlineP8` (packed/RLE), and `ScanlineBin` (binary, no coverage).
35pub trait Scanline {
36    /// Prepare for a new scanline, clearing all span data.
37    fn reset_spans(&mut self);
38
39    /// Add a single cell at position `x` with coverage `cover`.
40    fn add_cell(&mut self, x: i32, cover: u32);
41
42    /// Add a horizontal span of `len` pixels starting at `x`, all with `cover`.
43    fn add_span(&mut self, x: i32, len: u32, cover: u32);
44
45    /// Finalize the scanline at the given Y coordinate.
46    fn finalize(&mut self, y: i32);
47
48    /// Number of spans in this scanline (0 means empty).
49    fn num_spans(&self) -> u32;
50
51    /// The Y coordinate of this scanline.
52    fn y(&self) -> i32;
53}
54
55// ============================================================================
56// RasterizerScanlineAa — the high-level polygon rasterizer
57// ============================================================================
58
59#[derive(Debug, Clone, Copy, PartialEq)]
60enum Status {
61    Initial,
62    MoveTo,
63    LineTo,
64    Closed,
65}
66
67/// High-level polygon rasterizer with anti-aliased output.
68///
69/// Port of C++ `rasterizer_scanline_aa_nogamma<rasterizer_sl_clip_int>`.
70///
71/// Usage:
72/// 1. Optionally set `filling_rule()` and `clip_box()`
73/// 2. Define contours with `move_to_d()` / `line_to_d()` or `add_path()`
74/// 3. Call `rewind_scanlines()` then repeatedly `sweep_scanline()` to extract AA data
75pub struct RasterizerScanlineAa {
76    outline: RasterizerCellsAa,
77    clipper: RasterizerSlClipInt,
78    filling_rule: FillingRule,
79    auto_close: bool,
80    start_x: i32,
81    start_y: i32,
82    status: Status,
83    scan_y: i32,
84}
85
86impl RasterizerScanlineAa {
87    pub fn new() -> Self {
88        Self {
89            outline: RasterizerCellsAa::new(),
90            clipper: RasterizerSlClipInt::new(),
91            filling_rule: FillingRule::NonZero,
92            auto_close: true,
93            start_x: 0,
94            start_y: 0,
95            status: Status::Initial,
96            scan_y: 0,
97        }
98    }
99
100    /// Reset the rasterizer, discarding all polygon data.
101    pub fn reset(&mut self) {
102        self.outline.reset();
103        self.status = Status::Initial;
104    }
105
106    /// Set the filling rule (non-zero winding or even-odd).
107    pub fn filling_rule(&mut self, rule: FillingRule) {
108        self.filling_rule = rule;
109    }
110
111    /// Enable or disable automatic polygon closing on move_to.
112    pub fn auto_close(&mut self, flag: bool) {
113        self.auto_close = flag;
114    }
115
116    /// Set the clipping rectangle in floating-point coordinates.
117    pub fn clip_box(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
118        self.reset();
119        self.clipper.clip_box(
120            poly_coord(x1),
121            poly_coord(y1),
122            poly_coord(x2),
123            poly_coord(y2),
124        );
125    }
126
127    /// Disable clipping.
128    pub fn reset_clipping(&mut self) {
129        self.reset();
130        self.clipper.reset_clipping();
131    }
132
133    // ========================================================================
134    // Path building
135    // ========================================================================
136
137    /// Close the current polygon contour.
138    pub fn close_polygon(&mut self) {
139        if self.status == Status::LineTo {
140            self.clipper
141                .line_to(&mut self.outline, self.start_x, self.start_y);
142            self.status = Status::Closed;
143        }
144    }
145
146    /// Move to a new position in 24.8 fixed-point coordinates.
147    pub fn move_to(&mut self, x: i32, y: i32) {
148        if self.outline.sorted() {
149            self.reset();
150        }
151        if self.auto_close {
152            self.close_polygon();
153        }
154        // For ras_conv_int, downscale is identity
155        self.start_x = x;
156        self.start_y = y;
157        self.clipper.move_to(x, y);
158        self.status = Status::MoveTo;
159    }
160
161    /// Line to in 24.8 fixed-point coordinates.
162    pub fn line_to(&mut self, x: i32, y: i32) {
163        self.clipper.line_to(&mut self.outline, x, y);
164        self.status = Status::LineTo;
165    }
166
167    /// Move to a new position in floating-point coordinates.
168    pub fn move_to_d(&mut self, x: f64, y: f64) {
169        if self.outline.sorted() {
170            self.reset();
171        }
172        if self.auto_close {
173            self.close_polygon();
174        }
175        let sx = poly_coord(x);
176        let sy = poly_coord(y);
177        self.start_x = sx;
178        self.start_y = sy;
179        self.clipper.move_to(sx, sy);
180        self.status = Status::MoveTo;
181    }
182
183    /// Line to in floating-point coordinates.
184    pub fn line_to_d(&mut self, x: f64, y: f64) {
185        self.clipper
186            .line_to(&mut self.outline, poly_coord(x), poly_coord(y));
187        self.status = Status::LineTo;
188    }
189
190    /// Add a vertex (dispatches to move_to, line_to, or close based on command).
191    pub fn add_vertex(&mut self, x: f64, y: f64, cmd: u32) {
192        if is_move_to(cmd) {
193            self.move_to_d(x, y);
194        } else if is_vertex(cmd) {
195            self.line_to_d(x, y);
196        } else if is_close(cmd) {
197            self.close_polygon();
198        }
199    }
200
201    /// Add a single edge in 24.8 fixed-point coordinates.
202    pub fn edge(&mut self, x1: i32, y1: i32, x2: i32, y2: i32) {
203        if self.outline.sorted() {
204            self.reset();
205        }
206        self.clipper.move_to(x1, y1);
207        self.clipper.line_to(&mut self.outline, x2, y2);
208        self.status = Status::MoveTo;
209    }
210
211    /// Add a single edge in floating-point coordinates.
212    pub fn edge_d(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
213        if self.outline.sorted() {
214            self.reset();
215        }
216        self.clipper.move_to(poly_coord(x1), poly_coord(y1));
217        self.clipper
218            .line_to(&mut self.outline, poly_coord(x2), poly_coord(y2));
219        self.status = Status::MoveTo;
220    }
221
222    /// Add all vertices from a vertex source.
223    pub fn add_path(&mut self, vs: &mut dyn VertexSource, path_id: u32) {
224        let mut x = 0.0;
225        let mut y = 0.0;
226
227        vs.rewind(path_id);
228        if self.outline.sorted() {
229            self.reset();
230        }
231        loop {
232            let cmd = vs.vertex(&mut x, &mut y);
233            if is_stop(cmd) {
234                break;
235            }
236            self.add_vertex(x, y, cmd);
237        }
238    }
239
240    /// Snapshot the rasterizer's current cell array for external caching.
241    ///
242    /// Returns a `Vec<CellAa>` representing all cells accumulated so far. Call
243    /// this after `add_path()` but before `rewind_scanlines()`. The returned
244    /// cells are positioned at whatever coordinates were used when the path was
245    /// rasterized; for glyph caching, rasterize the glyph at origin (0, 0) so
246    /// the cells can be replayed at any position with `add_cells_offset()`.
247    ///
248    /// Integer-pixel offsets preserve sub-pixel coverage exactly, because
249    /// `area` and `cover` are computed relative to each cell's own pixel
250    /// boundary rather than absolute coordinates.
251    pub fn outline_cells(&mut self) -> Vec<CellAa> {
252        // Flush any partially-accumulated current cell before snapshotting.
253        self.outline.add_curr_cell();
254        self.outline.cells().to_vec()
255    }
256
257    /// Import pre-computed cells with a pixel offset, bypassing path→cell conversion.
258    ///
259    /// This is the replay half of the glyph caching workflow:
260    /// 1. Rasterize a glyph outline once at position (0, 0).
261    /// 2. Cache the cells via `outline_cells()`.
262    /// 3. For each subsequent rendering of the same glyph, call
263    ///    `add_cells_offset(cached, glyph_x as i32, glyph_y as i32)`.
264    ///
265    /// **Sub-pixel positioning**: integer offsets preserve coverage exactly.
266    /// For sub-pixel accuracy, cache one cell set per quantized sub-pixel slot
267    /// (typically 4 slots per axis = 16 total for 1/4-pixel precision).
268    ///
269    /// **Clipping**: cells are inserted without passing through the clipper.
270    /// Ensure the shifted cells stay within any configured `clip_box`.
271    pub fn add_cells_offset(&mut self, cells: &[CellAa], dx: i32, dy: i32) {
272        if self.outline.sorted() {
273            self.reset();
274        }
275        self.outline.add_cells_offset(cells, dx, dy);
276    }
277
278    // ========================================================================
279    // Bounding box
280    // ========================================================================
281
282    pub fn min_x(&self) -> i32 {
283        self.outline.min_x()
284    }
285    pub fn min_y(&self) -> i32 {
286        self.outline.min_y()
287    }
288    pub fn max_x(&self) -> i32 {
289        self.outline.max_x()
290    }
291    pub fn max_y(&self) -> i32 {
292        self.outline.max_y()
293    }
294
295    // ========================================================================
296    // Scanline sweeping
297    // ========================================================================
298
299    /// Sort cells and prepare for scanline sweeping.
300    /// Returns `false` if there are no cells (nothing to render).
301    pub fn rewind_scanlines(&mut self) -> bool {
302        if self.auto_close {
303            self.close_polygon();
304        }
305        self.outline.sort_cells();
306        if self.outline.total_cells() == 0 {
307            return false;
308        }
309        self.scan_y = self.outline.min_y();
310        true
311    }
312
313    /// Navigate to a specific scanline Y (for random access).
314    pub fn navigate_scanline(&mut self, y: i32) -> bool {
315        if self.auto_close {
316            self.close_polygon();
317        }
318        self.outline.sort_cells();
319        if self.outline.total_cells() == 0 || y < self.outline.min_y() || y > self.outline.max_y() {
320            return false;
321        }
322        self.scan_y = y;
323        true
324    }
325
326    /// Sort cells (explicit sort without starting a sweep).
327    pub fn sort(&mut self) {
328        if self.auto_close {
329            self.close_polygon();
330        }
331        self.outline.sort_cells();
332    }
333
334    /// Calculate alpha (coverage) from accumulated area.
335    ///
336    /// This is the "nogamma" variant — no gamma LUT, raw coverage.
337    #[inline]
338    pub fn calculate_alpha(&self, area: i32) -> u32 {
339        let mut cover = area >> (POLY_SUBPIXEL_SHIFT * 2 + 1 - AA_SHIFT);
340
341        if cover < 0 {
342            cover = -cover;
343        }
344        if self.filling_rule == FillingRule::EvenOdd {
345            cover &= AA_MASK2 as i32;
346            if cover > AA_SCALE as i32 {
347                cover = AA_SCALE2 as i32 - cover;
348            }
349        }
350        if cover > AA_MASK as i32 {
351            cover = AA_MASK as i32;
352        }
353        cover as u32
354    }
355
356    /// Extract the next scanline of anti-aliased coverage data.
357    ///
358    /// This is THE CORE function of the rasterizer. It iterates sorted cells
359    /// for the current scanline Y, accumulates coverage, and feeds spans
360    /// to the scanline object.
361    ///
362    /// Returns `false` when all scanlines have been consumed.
363    pub fn sweep_scanline<SL: Scanline>(&mut self, sl: &mut SL) -> bool {
364        loop {
365            if self.scan_y > self.outline.max_y() {
366                return false;
367            }
368            sl.reset_spans();
369
370            let cell_indices = self.outline.scanline_cells(self.scan_y as u32);
371            let mut num_cells = cell_indices.len();
372            let mut idx = 0;
373            let mut cover: i32 = 0;
374
375            while num_cells > 0 {
376                let cur_idx = cell_indices[idx];
377                let cur_cell = self.outline.cell(cur_idx);
378                let x = cur_cell.x;
379                let mut area = cur_cell.area;
380
381                cover += cur_cell.cover;
382
383                // Accumulate all cells with the same X
384                num_cells -= 1;
385                idx += 1;
386                while num_cells > 0 {
387                    let next_cell = self.outline.cell(cell_indices[idx]);
388                    if next_cell.x != x {
389                        break;
390                    }
391                    area += next_cell.area;
392                    cover += next_cell.cover;
393                    num_cells -= 1;
394                    idx += 1;
395                }
396
397                if area != 0 {
398                    let alpha = self.calculate_alpha((cover << (POLY_SUBPIXEL_SHIFT + 1)) - area);
399                    if alpha != 0 {
400                        sl.add_cell(x, alpha);
401                    }
402                    // The partial cell at x has been handled; next span starts at x+1
403                    let x_next = x + 1;
404
405                    if num_cells > 0 {
406                        let next_cell = self.outline.cell(cell_indices[idx]);
407                        if next_cell.x > x_next {
408                            let alpha = self.calculate_alpha(cover << (POLY_SUBPIXEL_SHIFT + 1));
409                            if alpha != 0 {
410                                sl.add_span(x_next, (next_cell.x - x_next) as u32, alpha);
411                            }
412                        }
413                    }
414                } else if num_cells > 0 {
415                    let next_cell = self.outline.cell(cell_indices[idx]);
416                    if next_cell.x > x {
417                        let alpha = self.calculate_alpha(cover << (POLY_SUBPIXEL_SHIFT + 1));
418                        if alpha != 0 {
419                            sl.add_span(x, (next_cell.x - x) as u32, alpha);
420                        }
421                    }
422                }
423            }
424
425            if sl.num_spans() > 0 {
426                break;
427            }
428            self.scan_y += 1;
429        }
430
431        sl.finalize(self.scan_y);
432        self.scan_y += 1;
433        true
434    }
435
436    /// Test if a specific pixel coordinate is inside the rasterized polygon.
437    pub fn hit_test(&mut self, tx: i32, ty: i32) -> bool {
438        if !self.navigate_scanline(ty) {
439            return false;
440        }
441        let mut sl = ScanlineHitTest::new(tx);
442        self.sweep_scanline_hit_test(&mut sl);
443        sl.hit()
444    }
445
446    /// Specialized sweep for ScanlineHitTest (avoids trait object overhead).
447    fn sweep_scanline_hit_test(&mut self, sl: &mut ScanlineHitTest) -> bool {
448        if self.scan_y > self.outline.max_y() {
449            return false;
450        }
451        sl.reset_spans();
452
453        let cell_indices = self.outline.scanline_cells(self.scan_y as u32);
454        let mut num_cells = cell_indices.len();
455        let mut idx = 0;
456        let mut cover: i32 = 0;
457
458        while num_cells > 0 {
459            let cur_cell = self.outline.cell(cell_indices[idx]);
460            let x = cur_cell.x;
461            let mut area = cur_cell.area;
462
463            cover += cur_cell.cover;
464
465            num_cells -= 1;
466            idx += 1;
467            while num_cells > 0 {
468                let next_cell = self.outline.cell(cell_indices[idx]);
469                if next_cell.x != x {
470                    break;
471                }
472                area += next_cell.area;
473                cover += next_cell.cover;
474                num_cells -= 1;
475                idx += 1;
476            }
477
478            if area != 0 {
479                let alpha = self.calculate_alpha((cover << (POLY_SUBPIXEL_SHIFT + 1)) - area);
480                if alpha != 0 {
481                    sl.add_cell(x, alpha);
482                }
483                let x_next = x + 1;
484                if num_cells > 0 {
485                    let next_cell = self.outline.cell(cell_indices[idx]);
486                    if next_cell.x > x_next {
487                        let alpha = self.calculate_alpha(cover << (POLY_SUBPIXEL_SHIFT + 1));
488                        if alpha != 0 {
489                            sl.add_span(x_next, (next_cell.x - x_next) as u32, alpha);
490                        }
491                    }
492                }
493            } else if num_cells > 0 {
494                let next_cell = self.outline.cell(cell_indices[idx]);
495                if next_cell.x > x {
496                    let alpha = self.calculate_alpha(cover << (POLY_SUBPIXEL_SHIFT + 1));
497                    if alpha != 0 {
498                        sl.add_span(x, (next_cell.x - x) as u32, alpha);
499                    }
500                }
501            }
502        }
503
504        sl.finalize(self.scan_y);
505        self.scan_y += 1;
506        true
507    }
508}
509
510impl Default for RasterizerScanlineAa {
511    fn default() -> Self {
512        Self::new()
513    }
514}
515
516// ============================================================================
517// Tests
518// ============================================================================
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523    use crate::basics::{PATH_FLAGS_NONE, POLY_SUBPIXEL_SCALE};
524    use crate::ellipse::Ellipse;
525    use crate::path_storage::PathStorage;
526
527    /// Minimal scanline for testing: just tracks cells and spans.
528    struct TestScanline {
529        spans: Vec<(i32, u32, u32)>, // (x, len, cover)
530        y_val: i32,
531    }
532
533    impl TestScanline {
534        fn new() -> Self {
535            Self {
536                spans: Vec::new(),
537                y_val: 0,
538            }
539        }
540    }
541
542    impl Scanline for TestScanline {
543        fn reset_spans(&mut self) {
544            self.spans.clear();
545        }
546        fn add_cell(&mut self, x: i32, cover: u32) {
547            self.spans.push((x, 1, cover));
548        }
549        fn add_span(&mut self, x: i32, len: u32, cover: u32) {
550            self.spans.push((x, len, cover));
551        }
552        fn finalize(&mut self, y: i32) {
553            self.y_val = y;
554        }
555        fn num_spans(&self) -> u32 {
556            self.spans.len() as u32
557        }
558        fn y(&self) -> i32 {
559            self.y_val
560        }
561    }
562
563    #[test]
564    fn test_new_rasterizer() {
565        let ras = RasterizerScanlineAa::new();
566        assert_eq!(ras.min_x(), i32::MAX);
567        assert_eq!(ras.min_y(), i32::MAX);
568    }
569
570    #[test]
571    fn test_filling_rule() {
572        let mut ras = RasterizerScanlineAa::new();
573        ras.filling_rule(FillingRule::EvenOdd);
574        assert_eq!(ras.filling_rule, FillingRule::EvenOdd);
575    }
576
577    #[test]
578    fn test_calculate_alpha_nonzero() {
579        let ras = RasterizerScanlineAa::new();
580        // Full coverage: area = POLY_SUBPIXEL_SCALE^2 * 2 → alpha should be 255
581        let full_area = (POLY_SUBPIXEL_SCALE as i32) << (POLY_SUBPIXEL_SHIFT + 1);
582        let alpha = ras.calculate_alpha(full_area);
583        assert_eq!(alpha, 255);
584    }
585
586    #[test]
587    fn test_calculate_alpha_zero_area() {
588        let ras = RasterizerScanlineAa::new();
589        assert_eq!(ras.calculate_alpha(0), 0);
590    }
591
592    #[test]
593    fn test_calculate_alpha_negative_area() {
594        let ras = RasterizerScanlineAa::new();
595        // Negative area should give same magnitude as positive
596        let area = 256 * 256; // = 65536
597        let alpha_pos = ras.calculate_alpha(area);
598        let alpha_neg = ras.calculate_alpha(-area);
599        assert_eq!(alpha_pos, alpha_neg);
600    }
601
602    #[test]
603    fn test_calculate_alpha_even_odd() {
604        let mut ras = RasterizerScanlineAa::new();
605        ras.filling_rule(FillingRule::EvenOdd);
606        // With even-odd, double-covered areas should wrap around
607        let full_area = (POLY_SUBPIXEL_SCALE as i32) << (POLY_SUBPIXEL_SHIFT + 1);
608        let double_area = full_area * 2;
609        let alpha = ras.calculate_alpha(double_area);
610        // Double coverage with even-odd should give ~0 (covered twice = uncovered)
611        assert!(
612            alpha < 10,
613            "Expected near-zero alpha for double even-odd, got {alpha}"
614        );
615    }
616
617    #[test]
618    fn test_triangle_sweep() {
619        let mut ras = RasterizerScanlineAa::new();
620        let s = POLY_SUBPIXEL_SCALE as i32;
621        // Triangle: (10,10) -> (20,10) -> (15,20) -> close
622        ras.move_to(10 * s, 10 * s);
623        ras.line_to(20 * s, 10 * s);
624        ras.line_to(15 * s, 20 * s);
625        ras.close_polygon();
626
627        assert!(ras.rewind_scanlines());
628
629        let mut sl = TestScanline::new();
630        let mut scanline_count = 0;
631        while ras.sweep_scanline(&mut sl) {
632            scanline_count += 1;
633            assert!(sl.num_spans() > 0);
634        }
635        assert!(scanline_count > 0, "Should have at least one scanline");
636        assert_eq!(ras.min_y(), 10);
637        assert_eq!(ras.max_y(), 20);
638    }
639
640    #[test]
641    fn test_triangle_hit_test() {
642        let mut ras = RasterizerScanlineAa::new();
643        let s = POLY_SUBPIXEL_SCALE as i32;
644        // Triangle: (10,10) -> (30,10) -> (20,30)
645        ras.move_to(10 * s, 10 * s);
646        ras.line_to(30 * s, 10 * s);
647        ras.line_to(20 * s, 30 * s);
648
649        // Center should be inside
650        assert!(ras.hit_test(20, 15));
651        // Far outside should not be hit
652        assert!(!ras.hit_test(0, 0));
653        assert!(!ras.hit_test(100, 100));
654    }
655
656    #[test]
657    fn test_move_to_d_line_to_d() {
658        let mut ras = RasterizerScanlineAa::new();
659        ras.move_to_d(10.0, 10.0);
660        ras.line_to_d(20.0, 10.0);
661        ras.line_to_d(15.0, 20.0);
662
663        assert!(ras.rewind_scanlines());
664    }
665
666    #[test]
667    fn test_edge_d() {
668        let mut ras = RasterizerScanlineAa::new();
669        ras.edge_d(10.0, 10.0, 20.0, 20.0);
670        ras.edge_d(20.0, 20.0, 10.0, 20.0);
671        ras.edge_d(10.0, 20.0, 10.0, 10.0);
672
673        assert!(ras.rewind_scanlines());
674    }
675
676    #[test]
677    fn test_add_path_with_path_storage() {
678        let mut ras = RasterizerScanlineAa::new();
679        let mut path = PathStorage::new();
680        path.move_to(10.0, 10.0);
681        path.line_to(50.0, 10.0);
682        path.line_to(30.0, 50.0);
683        path.close_polygon(PATH_FLAGS_NONE);
684
685        ras.add_path(&mut path, 0);
686        assert!(ras.rewind_scanlines());
687
688        let mut sl = TestScanline::new();
689        let mut count = 0;
690        while ras.sweep_scanline(&mut sl) {
691            count += 1;
692        }
693        assert!(count > 0);
694    }
695
696    #[test]
697    fn test_add_path_with_ellipse() {
698        let mut ras = RasterizerScanlineAa::new();
699        let mut ellipse = Ellipse::new(50.0, 50.0, 20.0, 20.0, 32, false);
700
701        ras.add_path(&mut ellipse, 0);
702        assert!(ras.rewind_scanlines());
703
704        // Center should be covered
705        assert!(ras.hit_test(50, 50));
706    }
707
708    #[test]
709    fn test_empty_rasterizer_no_scanlines() {
710        let mut ras = RasterizerScanlineAa::new();
711        assert!(!ras.rewind_scanlines());
712    }
713
714    #[test]
715    fn test_reset_clears_state() {
716        let mut ras = RasterizerScanlineAa::new();
717        let s = POLY_SUBPIXEL_SCALE as i32;
718        ras.move_to(10 * s, 10 * s);
719        ras.line_to(20 * s, 10 * s);
720        ras.line_to(15 * s, 20 * s);
721        ras.reset();
722        assert!(!ras.rewind_scanlines());
723    }
724
725    #[test]
726    fn test_clip_box() {
727        let mut ras = RasterizerScanlineAa::new();
728        ras.clip_box(0.0, 0.0, 50.0, 50.0);
729
730        // Triangle extending beyond clip box
731        ras.move_to_d(10.0, 10.0);
732        ras.line_to_d(100.0, 10.0);
733        ras.line_to_d(50.0, 100.0);
734
735        assert!(ras.rewind_scanlines());
736        // max_y should be clipped
737        assert!(ras.max_y() <= 50);
738    }
739
740    #[test]
741    fn test_navigate_scanline() {
742        let mut ras = RasterizerScanlineAa::new();
743        let s = POLY_SUBPIXEL_SCALE as i32;
744        ras.move_to(10 * s, 10 * s);
745        ras.line_to(20 * s, 10 * s);
746        ras.line_to(15 * s, 20 * s);
747
748        // Navigate to a scanline in the middle
749        assert!(ras.navigate_scanline(15));
750        let mut sl = TestScanline::new();
751        assert!(ras.sweep_scanline(&mut sl));
752        assert_eq!(sl.y(), 15);
753
754        // Navigate outside range should fail
755        assert!(!ras.navigate_scanline(0));
756        assert!(!ras.navigate_scanline(100));
757    }
758
759    #[test]
760    fn test_auto_close_on_move_to() {
761        let mut ras = RasterizerScanlineAa::new();
762        ras.move_to_d(10.0, 10.0);
763        ras.line_to_d(20.0, 10.0);
764        ras.line_to_d(15.0, 20.0);
765        // Don't close explicitly — auto_close should handle it on rewind
766        assert!(ras.rewind_scanlines());
767    }
768
769    /// Sweep all scanlines from a rasterizer into a flat list of (y, x, len, cover) tuples.
770    fn collect_scanlines(ras: &mut RasterizerScanlineAa) -> Vec<(i32, i32, u32, u32)> {
771        let mut sl = TestScanline::new();
772        let mut result = Vec::new();
773        while ras.sweep_scanline(&mut sl) {
774            for &(x, len, cover) in &sl.spans {
775                result.push((sl.y_val, x, len, cover));
776            }
777        }
778        result
779    }
780
781    /// Verify that outline_cells() + add_cells_offset(dx, dy) produces identical
782    /// scanline output to rasterizing the same path translated by (dx, dy) directly.
783    ///
784    /// This is the key correctness invariant for glyph cell caching: integer-pixel
785    /// offsets preserve sub-pixel coverage values because area/cover are computed
786    /// relative to each cell's own pixel boundary, not absolute coordinates.
787    #[test]
788    fn test_outline_cells_round_trip() {
789        // Triangle with varied edge angles to exercise sub-pixel coverage
790        let mut path = PathStorage::new();
791        path.move_to(10.0, 10.0);
792        path.line_to(30.0, 10.0);
793        path.line_to(20.0, 25.0);
794        path.close_polygon(PATH_FLAGS_NONE);
795
796        let dx = 15i32;
797        let dy = 20i32;
798
799        // --- Baseline: rasterize path translated by (dx, dy) directly ---
800        let mut path_shifted = path.clone();
801        path_shifted.translate_all_paths(dx as f64, dy as f64);
802        let mut ras_direct = RasterizerScanlineAa::new();
803        ras_direct.add_path(&mut path_shifted, 0);
804        assert!(ras_direct.rewind_scanlines());
805        let direct = collect_scanlines(&mut ras_direct);
806
807        // --- Cell cache: rasterize at origin, extract cells, replay at offset ---
808        let mut ras_source = RasterizerScanlineAa::new();
809        ras_source.add_path(&mut path, 0);
810        let cached_cells = ras_source.outline_cells();
811        assert!(!cached_cells.is_empty(), "outline_cells() should return non-empty cells");
812
813        let mut ras_replay = RasterizerScanlineAa::new();
814        ras_replay.add_cells_offset(&cached_cells, dx, dy);
815        assert!(ras_replay.rewind_scanlines());
816        let replayed = collect_scanlines(&mut ras_replay);
817
818        assert_eq!(
819            direct, replayed,
820            "cell cache replay must produce identical scanlines to direct rasterization"
821        );
822    }
823
824    /// Verify outline_cells() can be composed: two glyphs at different offsets
825    /// produce the same result whether rasterized directly or via cached cells.
826    #[test]
827    fn test_outline_cells_multiple_offsets() {
828        let mut path = PathStorage::new();
829        path.move_to(0.0, 0.0);
830        path.line_to(8.0, 0.0);
831        path.line_to(8.0, 12.0);
832        path.line_to(0.0, 12.0);
833        path.close_polygon(PATH_FLAGS_NONE);
834
835        // Cache cells once
836        let mut ras_source = RasterizerScanlineAa::new();
837        ras_source.add_path(&mut path, 0);
838        let cells = ras_source.outline_cells();
839
840        // Two different offsets to simulate two glyphs in a text run
841        for (dx, dy) in [(5i32, 10i32), (20i32, 10i32)] {
842            let mut path_shifted = path.clone();
843            path_shifted.translate_all_paths(dx as f64, dy as f64);
844            let mut ras_direct = RasterizerScanlineAa::new();
845            ras_direct.add_path(&mut path_shifted, 0);
846            assert!(ras_direct.rewind_scanlines());
847            let direct = collect_scanlines(&mut ras_direct);
848
849            let mut ras_replay = RasterizerScanlineAa::new();
850            ras_replay.add_cells_offset(&cells, dx, dy);
851            assert!(ras_replay.rewind_scanlines());
852            let replayed = collect_scanlines(&mut ras_replay);
853
854            assert_eq!(direct, replayed, "mismatch at offset ({dx}, {dy})");
855        }
856    }
857}