Skip to main content

agg_rust/
renderer_base.rs

1//! Base renderer with clipping.
2//!
3//! Port of `agg_renderer_base.h` — wraps a pixel format with a clip rectangle,
4//! ensuring all rendering operations are bounded within the visible area.
5
6use crate::basics::{CoverType, RectI};
7use crate::pixfmt_rgba::PixelFormat;
8
9// ============================================================================
10// RendererBase — clip-and-delegate renderer
11// ============================================================================
12
13/// Base renderer that clips all operations to a rectangle before delegating
14/// to the underlying pixel format.
15///
16/// Port of C++ `renderer_base<PixelFormat>`.
17pub struct RendererBase<PF: PixelFormat> {
18    ren: PF,
19    clip_box: RectI,
20}
21
22impl<PF: PixelFormat> RendererBase<PF> {
23    /// Create a new renderer wrapping the given pixel format.
24    /// The clip box is initialized to the full buffer extent.
25    pub fn new(ren: PF) -> Self {
26        let w = ren.width() as i32;
27        let h = ren.height() as i32;
28        Self {
29            ren,
30            clip_box: RectI::new(0, 0, w - 1, h - 1),
31        }
32    }
33
34    pub fn width(&self) -> u32 {
35        self.ren.width()
36    }
37    pub fn height(&self) -> u32 {
38        self.ren.height()
39    }
40
41    /// Set the clip rectangle (will be intersected with the buffer bounds).
42    pub fn clip_box_i(&mut self, x1: i32, y1: i32, x2: i32, y2: i32) -> bool {
43        let mut cb = RectI::new(x1, y1, x2, y2);
44        cb.normalize();
45        if cb.clip(&RectI::new(
46            0,
47            0,
48            self.ren.width() as i32 - 1,
49            self.ren.height() as i32 - 1,
50        )) {
51            self.clip_box = cb;
52            true
53        } else {
54            self.clip_box.x1 = 1;
55            self.clip_box.y1 = 1;
56            self.clip_box.x2 = 0;
57            self.clip_box.y2 = 0;
58            false
59        }
60    }
61
62    /// Reset clipping to the full buffer or to nothing.
63    pub fn reset_clipping(&mut self, visibility: bool) {
64        if visibility {
65            self.clip_box.x1 = 0;
66            self.clip_box.y1 = 0;
67            self.clip_box.x2 = self.ren.width() as i32 - 1;
68            self.clip_box.y2 = self.ren.height() as i32 - 1;
69        } else {
70            self.clip_box.x1 = 1;
71            self.clip_box.y1 = 1;
72            self.clip_box.x2 = 0;
73            self.clip_box.y2 = 0;
74        }
75    }
76
77    pub fn clip_box(&self) -> &RectI {
78        &self.clip_box
79    }
80    pub fn xmin(&self) -> i32 {
81        self.clip_box.x1
82    }
83    pub fn ymin(&self) -> i32 {
84        self.clip_box.y1
85    }
86    pub fn xmax(&self) -> i32 {
87        self.clip_box.x2
88    }
89    pub fn ymax(&self) -> i32 {
90        self.clip_box.y2
91    }
92
93    #[inline]
94    pub fn inbox(&self, x: i32, y: i32) -> bool {
95        x >= self.clip_box.x1
96            && y >= self.clip_box.y1
97            && x <= self.clip_box.x2
98            && y <= self.clip_box.y2
99    }
100
101    /// Get a reference to the underlying pixel format.
102    pub fn ren(&self) -> &PF {
103        &self.ren
104    }
105
106    /// Get a mutable reference to the underlying pixel format.
107    pub fn ren_mut(&mut self) -> &mut PF {
108        &mut self.ren
109    }
110
111    // ========================================================================
112    // Rendering operations (clip then delegate)
113    // ========================================================================
114
115    /// Clear the entire buffer to a solid color.
116    pub fn clear(&mut self, c: &PF::ColorType) {
117        let w = self.ren.width();
118        if w > 0 {
119            let h = self.ren.height();
120            for y in 0..h as i32 {
121                self.ren.copy_hline(0, y, w, c);
122            }
123        }
124    }
125
126    /// Copy a single pixel (clipped).
127    pub fn copy_pixel(&mut self, x: i32, y: i32, c: &PF::ColorType) {
128        if self.inbox(x, y) {
129            self.ren.copy_pixel(x, y, c);
130        }
131    }
132
133    /// Blend a single pixel (clipped).
134    pub fn blend_pixel(&mut self, x: i32, y: i32, c: &PF::ColorType, cover: CoverType) {
135        if self.inbox(x, y) {
136            self.ren.blend_pixel(x, y, c, cover);
137        }
138    }
139
140    /// Get the pixel at (x, y), or default if outside clip.
141    pub fn pixel(&self, x: i32, y: i32) -> PF::ColorType
142    where
143        PF::ColorType: Default,
144    {
145        if self.inbox(x, y) {
146            self.ren.pixel(x, y)
147        } else {
148            PF::ColorType::default()
149        }
150    }
151
152    /// Copy a horizontal line (clipped). x1, x2 are inclusive endpoints.
153    pub fn copy_hline(&mut self, mut x1: i32, y: i32, mut x2: i32, c: &PF::ColorType) {
154        if x1 > x2 {
155            std::mem::swap(&mut x1, &mut x2);
156        }
157        if y > self.ymax() || y < self.ymin() || x1 > self.xmax() || x2 < self.xmin() {
158            return;
159        }
160        x1 = x1.max(self.xmin());
161        x2 = x2.min(self.xmax());
162        self.ren.copy_hline(x1, y, (x2 - x1 + 1) as u32, c);
163    }
164
165    /// Blend a horizontal line (clipped). x1, x2 are inclusive endpoints.
166    pub fn blend_hline(
167        &mut self,
168        mut x1: i32,
169        y: i32,
170        mut x2: i32,
171        c: &PF::ColorType,
172        cover: CoverType,
173    ) {
174        if x1 > x2 {
175            std::mem::swap(&mut x1, &mut x2);
176        }
177        if y > self.ymax() || y < self.ymin() || x1 > self.xmax() || x2 < self.xmin() {
178            return;
179        }
180        x1 = x1.max(self.xmin());
181        x2 = x2.min(self.xmax());
182        self.ren.blend_hline(x1, y, (x2 - x1 + 1) as u32, c, cover);
183    }
184
185    /// Blend a vertical line (clipped). y1, y2 are inclusive endpoints.
186    pub fn blend_vline(
187        &mut self,
188        x: i32,
189        mut y1: i32,
190        mut y2: i32,
191        c: &PF::ColorType,
192        cover: CoverType,
193    ) {
194        if y1 > y2 {
195            std::mem::swap(&mut y1, &mut y2);
196        }
197        if x > self.xmax() || x < self.xmin() || y1 > self.ymax() || y2 < self.ymin() {
198            return;
199        }
200        y1 = y1.max(self.ymin());
201        y2 = y2.min(self.ymax());
202        for y in y1..=y2 {
203            self.ren.blend_pixel(x, y, c, cover);
204        }
205    }
206
207    /// Blend a filled rectangle (clipped).
208    pub fn blend_bar(
209        &mut self,
210        x1: i32,
211        y1: i32,
212        x2: i32,
213        y2: i32,
214        c: &PF::ColorType,
215        cover: CoverType,
216    ) {
217        let mut rc = crate::basics::RectI::new(x1, y1, x2, y2);
218        rc.normalize();
219        if !rc.clip(&self.clip_box) {
220            return;
221        }
222        for y in rc.y1..=rc.y2 {
223            self.ren
224                .blend_hline(rc.x1, y, (rc.x2 - rc.x1 + 1) as u32, c, cover);
225        }
226    }
227
228    /// Blend a solid horizontal span with per-pixel coverage (clipped).
229    pub fn blend_solid_hspan(
230        &mut self,
231        mut x: i32,
232        y: i32,
233        mut len: i32,
234        c: &PF::ColorType,
235        covers: &[CoverType],
236    ) {
237        if y > self.ymax() || y < self.ymin() {
238            return;
239        }
240
241        let mut covers_offset = 0usize;
242        if x < self.xmin() {
243            let d = self.xmin() - x;
244            len -= d;
245            if len <= 0 {
246                return;
247            }
248            covers_offset += d as usize;
249            x = self.xmin();
250        }
251        if x + len > self.xmax() + 1 {
252            len = self.xmax() - x + 1;
253            if len <= 0 {
254                return;
255            }
256        }
257        self.ren
258            .blend_solid_hspan(x, y, len as u32, c, &covers[covers_offset..]);
259    }
260
261    /// Blend a solid vertical span with per-pixel coverage (clipped).
262    pub fn blend_solid_vspan(
263        &mut self,
264        x: i32,
265        mut y: i32,
266        mut len: i32,
267        c: &PF::ColorType,
268        covers: &[CoverType],
269    ) {
270        if x > self.xmax() || x < self.xmin() {
271            return;
272        }
273
274        let mut covers_offset = 0usize;
275        if y < self.ymin() {
276            let d = self.ymin() - y;
277            len -= d;
278            if len <= 0 {
279                return;
280            }
281            covers_offset += d as usize;
282            y = self.ymin();
283        }
284        if y + len > self.ymax() + 1 {
285            len = self.ymax() - y + 1;
286            if len <= 0 {
287                return;
288            }
289        }
290        for i in 0..len as usize {
291            self.ren
292                .blend_pixel(x, y + i as i32, c, covers[covers_offset + i]);
293        }
294    }
295
296    /// Blend a horizontal span with per-pixel colors (clipped).
297    ///
298    /// If `covers` is non-empty, each pixel uses its corresponding coverage.
299    /// If `covers` is empty, all pixels use the uniform `cover` value.
300    pub fn blend_color_hspan(
301        &mut self,
302        mut x: i32,
303        y: i32,
304        mut len: i32,
305        colors: &[PF::ColorType],
306        covers: &[CoverType],
307        cover: CoverType,
308    ) {
309        if y > self.ymax() || y < self.ymin() {
310            return;
311        }
312
313        let mut colors_offset = 0usize;
314        let mut covers_offset = 0usize;
315        if x < self.xmin() {
316            let d = (self.xmin() - x) as usize;
317            len -= d as i32;
318            if len <= 0 {
319                return;
320            }
321            if !covers.is_empty() {
322                covers_offset += d;
323            }
324            colors_offset += d;
325            x = self.xmin();
326        }
327        if x + len > self.xmax() + 1 {
328            len = self.xmax() - x + 1;
329            if len <= 0 {
330                return;
331            }
332        }
333        self.ren.blend_color_hspan(
334            x,
335            y,
336            len as u32,
337            &colors[colors_offset..],
338            if covers.is_empty() {
339                &[]
340            } else {
341                &covers[covers_offset..]
342            },
343            cover,
344        );
345    }
346
347    /// Blend a vertical span with per-pixel colors (clipped).
348    ///
349    /// If `covers` is non-empty, each pixel uses its corresponding coverage.
350    /// If `covers` is empty, all pixels use the uniform `cover` value.
351    ///
352    /// Port of C++ `renderer_base::blend_color_vspan`.
353    pub fn blend_color_vspan(
354        &mut self,
355        x: i32,
356        mut y: i32,
357        mut len: i32,
358        colors: &[PF::ColorType],
359        covers: &[CoverType],
360        cover: CoverType,
361    ) {
362        if x > self.xmax() || x < self.xmin() {
363            return;
364        }
365
366        let mut colors_offset = 0usize;
367        let mut covers_offset = 0usize;
368        if y < self.ymin() {
369            let d = (self.ymin() - y) as usize;
370            len -= d as i32;
371            if len <= 0 {
372                return;
373            }
374            if !covers.is_empty() {
375                covers_offset += d;
376            }
377            colors_offset += d;
378            y = self.ymin();
379        }
380        if y + len > self.ymax() + 1 {
381            len = self.ymax() - y + 1;
382            if len <= 0 {
383                return;
384            }
385        }
386        // Iterate vertically, blending one pixel per row.
387        for i in 0..len as usize {
388            let c = if covers.is_empty() { cover } else { covers[covers_offset + i] };
389            self.ren.blend_pixel(x, y + i as i32, &colors[colors_offset + i], c);
390        }
391    }
392}
393
394// ============================================================================
395// Tests
396// ============================================================================
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401    use crate::color::Rgba8;
402    use crate::pixfmt_rgba::PixfmtRgba32;
403    use crate::rendering_buffer::RowAccessor;
404
405    const BPP: usize = 4;
406
407    fn make_renderer(w: u32, h: u32) -> (Vec<u8>, RowAccessor) {
408        let stride = (w * BPP as u32) as i32;
409        let buf = vec![0u8; (h * w * BPP as u32) as usize];
410        let mut ra = RowAccessor::new();
411        unsafe {
412            ra.attach(buf.as_ptr() as *mut u8, w, h, stride);
413        }
414        (buf, ra)
415    }
416
417    #[test]
418    fn test_new() {
419        let (_buf, mut ra) = make_renderer(100, 80);
420        let pf = PixfmtRgba32::new(&mut ra);
421        let ren = RendererBase::new(pf);
422        assert_eq!(ren.width(), 100);
423        assert_eq!(ren.height(), 80);
424        assert_eq!(ren.xmin(), 0);
425        assert_eq!(ren.ymin(), 0);
426        assert_eq!(ren.xmax(), 99);
427        assert_eq!(ren.ymax(), 79);
428    }
429
430    #[test]
431    fn test_clear() {
432        let (_buf, mut ra) = make_renderer(10, 10);
433        let pf = PixfmtRgba32::new(&mut ra);
434        let mut ren = RendererBase::new(pf);
435        let white = Rgba8::new(255, 255, 255, 255);
436        ren.clear(&white);
437        let p = ren.ren().pixel(5, 5);
438        assert_eq!(p.r, 255);
439        assert_eq!(p.a, 255);
440    }
441
442    #[test]
443    fn test_copy_pixel_clipped() {
444        let (_buf, mut ra) = make_renderer(10, 10);
445        let pf = PixfmtRgba32::new(&mut ra);
446        let mut ren = RendererBase::new(pf);
447        let red = Rgba8::new(255, 0, 0, 255);
448        // Inside clip box
449        ren.copy_pixel(5, 5, &red);
450        assert_eq!(ren.ren().pixel(5, 5).r, 255);
451        // Outside clip box — should be silently ignored
452        ren.copy_pixel(-1, 5, &red);
453        ren.copy_pixel(100, 5, &red);
454    }
455
456    #[test]
457    fn test_blend_hline_clipped() {
458        let (_buf, mut ra) = make_renderer(20, 10);
459        let pf = PixfmtRgba32::new(&mut ra);
460        let mut ren = RendererBase::new(pf);
461        let green = Rgba8::new(0, 255, 0, 255);
462        // Line extends beyond right edge
463        ren.blend_hline(15, 5, 25, &green, 255);
464        // Pixels within bounds should be drawn
465        assert_eq!(ren.ren().pixel(15, 5).g, 255);
466        assert_eq!(ren.ren().pixel(19, 5).g, 255);
467    }
468
469    #[test]
470    fn test_clip_box() {
471        let (_buf, mut ra) = make_renderer(100, 100);
472        let pf = PixfmtRgba32::new(&mut ra);
473        let mut ren = RendererBase::new(pf);
474        assert!(ren.clip_box_i(10, 10, 50, 50));
475        assert_eq!(ren.xmin(), 10);
476        assert_eq!(ren.ymin(), 10);
477        assert_eq!(ren.xmax(), 50);
478        assert_eq!(ren.ymax(), 50);
479    }
480
481    #[test]
482    fn test_clip_box_invalid() {
483        let (_buf, mut ra) = make_renderer(100, 100);
484        let pf = PixfmtRgba32::new(&mut ra);
485        let mut ren = RendererBase::new(pf);
486        // Clip box entirely outside buffer
487        assert!(!ren.clip_box_i(200, 200, 300, 300));
488    }
489
490    #[test]
491    fn test_blend_solid_hspan_clipped() {
492        let (_buf, mut ra) = make_renderer(20, 10);
493        let pf = PixfmtRgba32::new(&mut ra);
494        let mut ren = RendererBase::new(pf);
495        let blue = Rgba8::new(0, 0, 255, 255);
496        // Span starts before clip box left edge
497        let covers = vec![255u8; 10];
498        ren.blend_solid_hspan(-3, 5, 10, &blue, &covers);
499        // First 3 pixels should be clipped, pixels 0..6 should be drawn
500        assert_eq!(ren.ren().pixel(0, 5).b, 255);
501        assert_eq!(ren.ren().pixel(6, 5).b, 255);
502    }
503
504    #[test]
505    fn test_blend_solid_hspan_fully_clipped() {
506        let (_buf, mut ra) = make_renderer(20, 10);
507        let pf = PixfmtRgba32::new(&mut ra);
508        let mut ren = RendererBase::new(pf);
509        let red = Rgba8::new(255, 0, 0, 255);
510        let covers = [255u8; 5];
511        // Span entirely above clip box
512        ren.blend_solid_hspan(5, -1, 5, &red, &covers);
513        // Nothing should be drawn
514        assert_eq!(ren.ren().pixel(5, 0).r, 0);
515    }
516
517    #[test]
518    fn test_inbox() {
519        let (_buf, mut ra) = make_renderer(10, 10);
520        let pf = PixfmtRgba32::new(&mut ra);
521        let ren = RendererBase::new(pf);
522        assert!(ren.inbox(0, 0));
523        assert!(ren.inbox(9, 9));
524        assert!(!ren.inbox(-1, 0));
525        assert!(!ren.inbox(10, 0));
526        assert!(!ren.inbox(0, 10));
527    }
528
529    #[test]
530    fn test_reset_clipping() {
531        let (_buf, mut ra) = make_renderer(100, 100);
532        let pf = PixfmtRgba32::new(&mut ra);
533        let mut ren = RendererBase::new(pf);
534        ren.clip_box_i(10, 10, 50, 50);
535        ren.reset_clipping(true);
536        assert_eq!(ren.xmin(), 0);
537        assert_eq!(ren.ymin(), 0);
538        assert_eq!(ren.xmax(), 99);
539        assert_eq!(ren.ymax(), 99);
540
541        ren.reset_clipping(false);
542        assert!(!ren.inbox(0, 0));
543    }
544
545    #[test]
546    fn test_blend_color_hspan() {
547        let (_buf, mut ra) = make_renderer(20, 10);
548        let pf = PixfmtRgba32::new(&mut ra);
549        let mut ren = RendererBase::new(pf);
550        let colors = [
551            Rgba8::new(255, 0, 0, 255),
552            Rgba8::new(0, 255, 0, 255),
553            Rgba8::new(0, 0, 255, 255),
554        ];
555        ren.blend_color_hspan(5, 3, 3, &colors, &[], 255);
556        let p0 = ren.ren().pixel(5, 3);
557        assert_eq!(p0.r, 255);
558        let p1 = ren.ren().pixel(6, 3);
559        assert_eq!(p1.g, 255);
560        let p2 = ren.ren().pixel(7, 3);
561        assert_eq!(p2.b, 255);
562    }
563
564    #[test]
565    fn test_blend_color_hspan_clipped_left() {
566        let (_buf, mut ra) = make_renderer(20, 10);
567        let pf = PixfmtRgba32::new(&mut ra);
568        let mut ren = RendererBase::new(pf);
569        let colors = [
570            Rgba8::new(255, 0, 0, 255),
571            Rgba8::new(0, 255, 0, 255),
572            Rgba8::new(0, 0, 255, 255),
573        ];
574        // Starts at x=-1, so first color is clipped
575        ren.blend_color_hspan(-1, 3, 3, &colors, &[], 255);
576        // colors[0] (red) at x=-1 → clipped
577        // colors[1] (green) at x=0
578        let p0 = ren.ren().pixel(0, 3);
579        assert_eq!(p0.g, 255);
580        // colors[2] (blue) at x=1
581        let p1 = ren.ren().pixel(1, 3);
582        assert_eq!(p1.b, 255);
583    }
584
585    #[test]
586    fn test_blend_color_hspan_clipped_y() {
587        let (_buf, mut ra) = make_renderer(20, 10);
588        let pf = PixfmtRgba32::new(&mut ra);
589        let mut ren = RendererBase::new(pf);
590        let colors = [Rgba8::new(255, 0, 0, 255)];
591        // y=-1 → fully clipped
592        ren.blend_color_hspan(5, -1, 1, &colors, &[], 255);
593        let p = ren.ren().pixel(5, 0);
594        assert_eq!(p.r, 0);
595    }
596}