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::{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    // ========================================================================
241    // Bounding box
242    // ========================================================================
243
244    pub fn min_x(&self) -> i32 {
245        self.outline.min_x()
246    }
247    pub fn min_y(&self) -> i32 {
248        self.outline.min_y()
249    }
250    pub fn max_x(&self) -> i32 {
251        self.outline.max_x()
252    }
253    pub fn max_y(&self) -> i32 {
254        self.outline.max_y()
255    }
256
257    // ========================================================================
258    // Scanline sweeping
259    // ========================================================================
260
261    /// Sort cells and prepare for scanline sweeping.
262    /// Returns `false` if there are no cells (nothing to render).
263    pub fn rewind_scanlines(&mut self) -> bool {
264        if self.auto_close {
265            self.close_polygon();
266        }
267        self.outline.sort_cells();
268        if self.outline.total_cells() == 0 {
269            return false;
270        }
271        self.scan_y = self.outline.min_y();
272        true
273    }
274
275    /// Navigate to a specific scanline Y (for random access).
276    pub fn navigate_scanline(&mut self, y: i32) -> bool {
277        if self.auto_close {
278            self.close_polygon();
279        }
280        self.outline.sort_cells();
281        if self.outline.total_cells() == 0 || y < self.outline.min_y() || y > self.outline.max_y() {
282            return false;
283        }
284        self.scan_y = y;
285        true
286    }
287
288    /// Sort cells (explicit sort without starting a sweep).
289    pub fn sort(&mut self) {
290        if self.auto_close {
291            self.close_polygon();
292        }
293        self.outline.sort_cells();
294    }
295
296    /// Calculate alpha (coverage) from accumulated area.
297    ///
298    /// This is the "nogamma" variant — no gamma LUT, raw coverage.
299    #[inline]
300    pub fn calculate_alpha(&self, area: i32) -> u32 {
301        let mut cover = area >> (POLY_SUBPIXEL_SHIFT * 2 + 1 - AA_SHIFT);
302
303        if cover < 0 {
304            cover = -cover;
305        }
306        if self.filling_rule == FillingRule::EvenOdd {
307            cover &= AA_MASK2 as i32;
308            if cover > AA_SCALE as i32 {
309                cover = AA_SCALE2 as i32 - cover;
310            }
311        }
312        if cover > AA_MASK as i32 {
313            cover = AA_MASK as i32;
314        }
315        cover as u32
316    }
317
318    /// Extract the next scanline of anti-aliased coverage data.
319    ///
320    /// This is THE CORE function of the rasterizer. It iterates sorted cells
321    /// for the current scanline Y, accumulates coverage, and feeds spans
322    /// to the scanline object.
323    ///
324    /// Returns `false` when all scanlines have been consumed.
325    pub fn sweep_scanline<SL: Scanline>(&mut self, sl: &mut SL) -> bool {
326        loop {
327            if self.scan_y > self.outline.max_y() {
328                return false;
329            }
330            sl.reset_spans();
331
332            let cell_indices = self.outline.scanline_cells(self.scan_y as u32);
333            let mut num_cells = cell_indices.len();
334            let mut idx = 0;
335            let mut cover: i32 = 0;
336
337            while num_cells > 0 {
338                let cur_idx = cell_indices[idx];
339                let cur_cell = self.outline.cell(cur_idx);
340                let x = cur_cell.x;
341                let mut area = cur_cell.area;
342
343                cover += cur_cell.cover;
344
345                // Accumulate all cells with the same X
346                num_cells -= 1;
347                idx += 1;
348                while num_cells > 0 {
349                    let next_cell = self.outline.cell(cell_indices[idx]);
350                    if next_cell.x != x {
351                        break;
352                    }
353                    area += next_cell.area;
354                    cover += next_cell.cover;
355                    num_cells -= 1;
356                    idx += 1;
357                }
358
359                if area != 0 {
360                    let alpha = self.calculate_alpha((cover << (POLY_SUBPIXEL_SHIFT + 1)) - area);
361                    if alpha != 0 {
362                        sl.add_cell(x, alpha);
363                    }
364                    // The partial cell at x has been handled; next span starts at x+1
365                    let x_next = x + 1;
366
367                    if num_cells > 0 {
368                        let next_cell = self.outline.cell(cell_indices[idx]);
369                        if next_cell.x > x_next {
370                            let alpha = self.calculate_alpha(cover << (POLY_SUBPIXEL_SHIFT + 1));
371                            if alpha != 0 {
372                                sl.add_span(x_next, (next_cell.x - x_next) as u32, alpha);
373                            }
374                        }
375                    }
376                } else if num_cells > 0 {
377                    let next_cell = self.outline.cell(cell_indices[idx]);
378                    if next_cell.x > x {
379                        let alpha = self.calculate_alpha(cover << (POLY_SUBPIXEL_SHIFT + 1));
380                        if alpha != 0 {
381                            sl.add_span(x, (next_cell.x - x) as u32, alpha);
382                        }
383                    }
384                }
385            }
386
387            if sl.num_spans() > 0 {
388                break;
389            }
390            self.scan_y += 1;
391        }
392
393        sl.finalize(self.scan_y);
394        self.scan_y += 1;
395        true
396    }
397
398    /// Test if a specific pixel coordinate is inside the rasterized polygon.
399    pub fn hit_test(&mut self, tx: i32, ty: i32) -> bool {
400        if !self.navigate_scanline(ty) {
401            return false;
402        }
403        let mut sl = ScanlineHitTest::new(tx);
404        self.sweep_scanline_hit_test(&mut sl);
405        sl.hit()
406    }
407
408    /// Specialized sweep for ScanlineHitTest (avoids trait object overhead).
409    fn sweep_scanline_hit_test(&mut self, sl: &mut ScanlineHitTest) -> bool {
410        if self.scan_y > self.outline.max_y() {
411            return false;
412        }
413        sl.reset_spans();
414
415        let cell_indices = self.outline.scanline_cells(self.scan_y as u32);
416        let mut num_cells = cell_indices.len();
417        let mut idx = 0;
418        let mut cover: i32 = 0;
419
420        while num_cells > 0 {
421            let cur_cell = self.outline.cell(cell_indices[idx]);
422            let x = cur_cell.x;
423            let mut area = cur_cell.area;
424
425            cover += cur_cell.cover;
426
427            num_cells -= 1;
428            idx += 1;
429            while num_cells > 0 {
430                let next_cell = self.outline.cell(cell_indices[idx]);
431                if next_cell.x != x {
432                    break;
433                }
434                area += next_cell.area;
435                cover += next_cell.cover;
436                num_cells -= 1;
437                idx += 1;
438            }
439
440            if area != 0 {
441                let alpha = self.calculate_alpha((cover << (POLY_SUBPIXEL_SHIFT + 1)) - area);
442                if alpha != 0 {
443                    sl.add_cell(x, alpha);
444                }
445                let x_next = x + 1;
446                if num_cells > 0 {
447                    let next_cell = self.outline.cell(cell_indices[idx]);
448                    if next_cell.x > x_next {
449                        let alpha = self.calculate_alpha(cover << (POLY_SUBPIXEL_SHIFT + 1));
450                        if alpha != 0 {
451                            sl.add_span(x_next, (next_cell.x - x_next) as u32, alpha);
452                        }
453                    }
454                }
455            } else if num_cells > 0 {
456                let next_cell = self.outline.cell(cell_indices[idx]);
457                if next_cell.x > x {
458                    let alpha = self.calculate_alpha(cover << (POLY_SUBPIXEL_SHIFT + 1));
459                    if alpha != 0 {
460                        sl.add_span(x, (next_cell.x - x) as u32, alpha);
461                    }
462                }
463            }
464        }
465
466        sl.finalize(self.scan_y);
467        self.scan_y += 1;
468        true
469    }
470}
471
472impl Default for RasterizerScanlineAa {
473    fn default() -> Self {
474        Self::new()
475    }
476}
477
478// ============================================================================
479// Tests
480// ============================================================================
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use crate::basics::{PATH_FLAGS_NONE, POLY_SUBPIXEL_SCALE};
486    use crate::ellipse::Ellipse;
487    use crate::path_storage::PathStorage;
488
489    /// Minimal scanline for testing: just tracks cells and spans.
490    struct TestScanline {
491        spans: Vec<(i32, u32, u32)>, // (x, len, cover)
492        y_val: i32,
493    }
494
495    impl TestScanline {
496        fn new() -> Self {
497            Self {
498                spans: Vec::new(),
499                y_val: 0,
500            }
501        }
502    }
503
504    impl Scanline for TestScanline {
505        fn reset_spans(&mut self) {
506            self.spans.clear();
507        }
508        fn add_cell(&mut self, x: i32, cover: u32) {
509            self.spans.push((x, 1, cover));
510        }
511        fn add_span(&mut self, x: i32, len: u32, cover: u32) {
512            self.spans.push((x, len, cover));
513        }
514        fn finalize(&mut self, y: i32) {
515            self.y_val = y;
516        }
517        fn num_spans(&self) -> u32 {
518            self.spans.len() as u32
519        }
520        fn y(&self) -> i32 {
521            self.y_val
522        }
523    }
524
525    #[test]
526    fn test_new_rasterizer() {
527        let ras = RasterizerScanlineAa::new();
528        assert_eq!(ras.min_x(), i32::MAX);
529        assert_eq!(ras.min_y(), i32::MAX);
530    }
531
532    #[test]
533    fn test_filling_rule() {
534        let mut ras = RasterizerScanlineAa::new();
535        ras.filling_rule(FillingRule::EvenOdd);
536        assert_eq!(ras.filling_rule, FillingRule::EvenOdd);
537    }
538
539    #[test]
540    fn test_calculate_alpha_nonzero() {
541        let ras = RasterizerScanlineAa::new();
542        // Full coverage: area = POLY_SUBPIXEL_SCALE^2 * 2 → alpha should be 255
543        let full_area = (POLY_SUBPIXEL_SCALE as i32) << (POLY_SUBPIXEL_SHIFT + 1);
544        let alpha = ras.calculate_alpha(full_area);
545        assert_eq!(alpha, 255);
546    }
547
548    #[test]
549    fn test_calculate_alpha_zero_area() {
550        let ras = RasterizerScanlineAa::new();
551        assert_eq!(ras.calculate_alpha(0), 0);
552    }
553
554    #[test]
555    fn test_calculate_alpha_negative_area() {
556        let ras = RasterizerScanlineAa::new();
557        // Negative area should give same magnitude as positive
558        let area = 256 * 256; // = 65536
559        let alpha_pos = ras.calculate_alpha(area);
560        let alpha_neg = ras.calculate_alpha(-area);
561        assert_eq!(alpha_pos, alpha_neg);
562    }
563
564    #[test]
565    fn test_calculate_alpha_even_odd() {
566        let mut ras = RasterizerScanlineAa::new();
567        ras.filling_rule(FillingRule::EvenOdd);
568        // With even-odd, double-covered areas should wrap around
569        let full_area = (POLY_SUBPIXEL_SCALE as i32) << (POLY_SUBPIXEL_SHIFT + 1);
570        let double_area = full_area * 2;
571        let alpha = ras.calculate_alpha(double_area);
572        // Double coverage with even-odd should give ~0 (covered twice = uncovered)
573        assert!(
574            alpha < 10,
575            "Expected near-zero alpha for double even-odd, got {alpha}"
576        );
577    }
578
579    #[test]
580    fn test_triangle_sweep() {
581        let mut ras = RasterizerScanlineAa::new();
582        let s = POLY_SUBPIXEL_SCALE as i32;
583        // Triangle: (10,10) -> (20,10) -> (15,20) -> close
584        ras.move_to(10 * s, 10 * s);
585        ras.line_to(20 * s, 10 * s);
586        ras.line_to(15 * s, 20 * s);
587        ras.close_polygon();
588
589        assert!(ras.rewind_scanlines());
590
591        let mut sl = TestScanline::new();
592        let mut scanline_count = 0;
593        while ras.sweep_scanline(&mut sl) {
594            scanline_count += 1;
595            assert!(sl.num_spans() > 0);
596        }
597        assert!(scanline_count > 0, "Should have at least one scanline");
598        assert_eq!(ras.min_y(), 10);
599        assert_eq!(ras.max_y(), 20);
600    }
601
602    #[test]
603    fn test_triangle_hit_test() {
604        let mut ras = RasterizerScanlineAa::new();
605        let s = POLY_SUBPIXEL_SCALE as i32;
606        // Triangle: (10,10) -> (30,10) -> (20,30)
607        ras.move_to(10 * s, 10 * s);
608        ras.line_to(30 * s, 10 * s);
609        ras.line_to(20 * s, 30 * s);
610
611        // Center should be inside
612        assert!(ras.hit_test(20, 15));
613        // Far outside should not be hit
614        assert!(!ras.hit_test(0, 0));
615        assert!(!ras.hit_test(100, 100));
616    }
617
618    #[test]
619    fn test_move_to_d_line_to_d() {
620        let mut ras = RasterizerScanlineAa::new();
621        ras.move_to_d(10.0, 10.0);
622        ras.line_to_d(20.0, 10.0);
623        ras.line_to_d(15.0, 20.0);
624
625        assert!(ras.rewind_scanlines());
626    }
627
628    #[test]
629    fn test_edge_d() {
630        let mut ras = RasterizerScanlineAa::new();
631        ras.edge_d(10.0, 10.0, 20.0, 20.0);
632        ras.edge_d(20.0, 20.0, 10.0, 20.0);
633        ras.edge_d(10.0, 20.0, 10.0, 10.0);
634
635        assert!(ras.rewind_scanlines());
636    }
637
638    #[test]
639    fn test_add_path_with_path_storage() {
640        let mut ras = RasterizerScanlineAa::new();
641        let mut path = PathStorage::new();
642        path.move_to(10.0, 10.0);
643        path.line_to(50.0, 10.0);
644        path.line_to(30.0, 50.0);
645        path.close_polygon(PATH_FLAGS_NONE);
646
647        ras.add_path(&mut path, 0);
648        assert!(ras.rewind_scanlines());
649
650        let mut sl = TestScanline::new();
651        let mut count = 0;
652        while ras.sweep_scanline(&mut sl) {
653            count += 1;
654        }
655        assert!(count > 0);
656    }
657
658    #[test]
659    fn test_add_path_with_ellipse() {
660        let mut ras = RasterizerScanlineAa::new();
661        let mut ellipse = Ellipse::new(50.0, 50.0, 20.0, 20.0, 32, false);
662
663        ras.add_path(&mut ellipse, 0);
664        assert!(ras.rewind_scanlines());
665
666        // Center should be covered
667        assert!(ras.hit_test(50, 50));
668    }
669
670    #[test]
671    fn test_empty_rasterizer_no_scanlines() {
672        let mut ras = RasterizerScanlineAa::new();
673        assert!(!ras.rewind_scanlines());
674    }
675
676    #[test]
677    fn test_reset_clears_state() {
678        let mut ras = RasterizerScanlineAa::new();
679        let s = POLY_SUBPIXEL_SCALE as i32;
680        ras.move_to(10 * s, 10 * s);
681        ras.line_to(20 * s, 10 * s);
682        ras.line_to(15 * s, 20 * s);
683        ras.reset();
684        assert!(!ras.rewind_scanlines());
685    }
686
687    #[test]
688    fn test_clip_box() {
689        let mut ras = RasterizerScanlineAa::new();
690        ras.clip_box(0.0, 0.0, 50.0, 50.0);
691
692        // Triangle extending beyond clip box
693        ras.move_to_d(10.0, 10.0);
694        ras.line_to_d(100.0, 10.0);
695        ras.line_to_d(50.0, 100.0);
696
697        assert!(ras.rewind_scanlines());
698        // max_y should be clipped
699        assert!(ras.max_y() <= 50);
700    }
701
702    #[test]
703    fn test_navigate_scanline() {
704        let mut ras = RasterizerScanlineAa::new();
705        let s = POLY_SUBPIXEL_SCALE as i32;
706        ras.move_to(10 * s, 10 * s);
707        ras.line_to(20 * s, 10 * s);
708        ras.line_to(15 * s, 20 * s);
709
710        // Navigate to a scanline in the middle
711        assert!(ras.navigate_scanline(15));
712        let mut sl = TestScanline::new();
713        assert!(ras.sweep_scanline(&mut sl));
714        assert_eq!(sl.y(), 15);
715
716        // Navigate outside range should fail
717        assert!(!ras.navigate_scanline(0));
718        assert!(!ras.navigate_scanline(100));
719    }
720
721    #[test]
722    fn test_auto_close_on_move_to() {
723        let mut ras = RasterizerScanlineAa::new();
724        ras.move_to_d(10.0, 10.0);
725        ras.line_to_d(20.0, 10.0);
726        ras.line_to_d(15.0, 20.0);
727        // Don't close explicitly — auto_close should handle it on rewind
728        assert!(ras.rewind_scanlines());
729    }
730}