Skip to main content

agg_rust/
renderer_outline_image.rs

1//! Image-patterned outline renderer.
2//!
3//! Port of `agg_renderer_outline_image.h`.
4//! Renders anti-aliased lines with image patterns applied along the stroke.
5//! The pattern is sampled from a source image and mapped onto each line segment.
6//!
7//! Copyright 2025-2026.
8
9use crate::basics::{iround, RectI};
10use crate::color::{Rgba, Rgba8};
11use crate::dda_line::Dda2LineInterpolator;
12use crate::line_aa_basics::*;
13use crate::pattern_filters_rgba::PatternFilter;
14use crate::pixfmt_rgba::PixelFormat;
15use crate::renderer_base::RendererBase;
16use crate::renderer_outline_aa::OutlineAaRenderer;
17
18// ============================================================================
19// Pattern source trait
20// ============================================================================
21
22/// Trait for image pattern sources.
23///
24/// Any type implementing this can be used as input to `LineImagePattern::create()`.
25/// The C++ equivalent uses a template parameter with `width()`, `height()`, `pixel()`.
26pub trait ImagePatternSource {
27    fn width(&self) -> f64;
28    fn height(&self) -> f64;
29    fn pixel(&self, x: i32, y: i32) -> Rgba8;
30}
31
32// ============================================================================
33// line_image_scale — scales a pattern source to a different height
34// ============================================================================
35
36/// Helper that wraps a pattern source and scales its height.
37///
38/// Port of C++ `line_image_scale<Source>`.
39/// Uses float (Rgba) arithmetic for both branches, matching C++ exactly.
40pub struct LineImageScale<'a, S: ImagePatternSource> {
41    source: &'a S,
42    height: f64,
43    scale: f64,
44    scale_inv: f64,
45}
46
47/// Convert Rgba8 to Rgba (float) — matching C++ `rgba(rgba8)` conversion.
48#[inline]
49fn rgba8_to_rgba(c: Rgba8) -> Rgba {
50    Rgba::new(
51        c.r as f64 / 255.0,
52        c.g as f64 / 255.0,
53        c.b as f64 / 255.0,
54        c.a as f64 / 255.0,
55    )
56}
57
58/// Convert Rgba (float) back to Rgba8 — matching C++ `rgba8(rgba)` conversion.
59/// Uses uround (v + 0.5) as i32, clamped to [0, 255].
60#[inline]
61fn rgba_to_rgba8(c: &Rgba) -> Rgba8 {
62    #[inline]
63    fn clamp_u8(v: f64) -> u32 {
64        let i = (v * 255.0 + 0.5) as i32;
65        i.clamp(0, 255) as u32
66    }
67    Rgba8::new(clamp_u8(c.r), clamp_u8(c.g), clamp_u8(c.b), clamp_u8(c.a))
68}
69
70impl<'a, S: ImagePatternSource> LineImageScale<'a, S> {
71    pub fn new(source: &'a S, height: f64) -> Self {
72        let sh = source.height();
73        Self {
74            source,
75            height,
76            scale: sh / height,
77            scale_inv: height / sh,
78        }
79    }
80}
81
82impl<'a, S: ImagePatternSource> ImagePatternSource for LineImageScale<'a, S> {
83    fn width(&self) -> f64 {
84        self.source.width()
85    }
86
87    fn height(&self) -> f64 {
88        self.height
89    }
90
91    /// Sample the scaled pattern at (x, y).
92    ///
93    /// Port of C++ `line_image_scale::pixel`.
94    /// Uses float (Rgba) arithmetic for both branches, matching C++ exactly:
95    /// - scale < 1.0: gradient interpolation between two rows
96    /// - scale >= 1.0: area-weighted average of multiple rows
97    fn pixel(&self, x: i32, y: i32) -> Rgba8 {
98        let h = self.source.height() as i32 - 1;
99
100        if self.scale < 1.0 {
101            // Interpolate between two nearest source rows
102            let src_y = (y as f64 + 0.5) * self.scale - 0.5;
103            let y1 = src_y.floor() as i32;
104            let y2 = y1 + 1;
105            let pix1 = if y1 < 0 {
106                Rgba::no_color()
107            } else {
108                rgba8_to_rgba(self.source.pixel(x, y1))
109            };
110            let pix2 = if y2 > h {
111                Rgba::no_color()
112            } else {
113                rgba8_to_rgba(self.source.pixel(x, y2))
114            };
115            let k = src_y - y1 as f64;
116            rgba_to_rgba8(&pix1.gradient(&pix2, k))
117        } else {
118            // Area-weighted average of source rows covering [src_y1, src_y2)
119            let src_y1 = (y as f64 + 0.5) * self.scale - 0.5;
120            let src_y2 = src_y1 + self.scale;
121            let mut y1 = src_y1.floor() as i32;
122            let y2 = src_y2.floor() as i32;
123
124            let mut c = Rgba::no_color();
125
126            // First partial row
127            if y1 >= 0 {
128                let weight = (y1 + 1) as f64 - src_y1;
129                let p = rgba8_to_rgba(self.source.pixel(x, y1));
130                c += p * weight;
131            }
132
133            // Full middle rows
134            y1 += 1;
135            while y1 < y2 {
136                if y1 <= h {
137                    c += rgba8_to_rgba(self.source.pixel(x, y1));
138                }
139                y1 += 1;
140            }
141
142            // Last partial row
143            if y2 <= h {
144                let weight = src_y2 - y2 as f64;
145                let p = rgba8_to_rgba(self.source.pixel(x, y2));
146                c += p * weight;
147            }
148
149            c *= self.scale_inv;
150            rgba_to_rgba8(&c)
151        }
152    }
153}
154
155// ============================================================================
156// line_image_pattern — the main pattern container
157// ============================================================================
158
159/// Ceiling of float to unsigned — port of C++ `uceil`.
160#[inline]
161fn uceil(v: f64) -> u32 {
162    v.ceil() as u32
163}
164
165/// Round float to signed int — port of C++ `uround`.
166#[inline]
167fn uround(v: f64) -> i32 {
168    (v + 0.5) as i32
169}
170
171/// Image pattern for line rendering.
172///
173/// Port of C++ `line_image_pattern<Filter>`.
174/// Stores a copy of the pattern image extended with dilation borders
175/// for the filter to access neighboring pixels.
176pub struct LineImagePattern<F: PatternFilter> {
177    _phantom: std::marker::PhantomData<F>,
178    /// 2D pixel buffer: rows[y][x]
179    buf: Vec<Vec<Rgba8>>,
180    dilation: u32,
181    dilation_hr: i32,
182    width: u32,
183    height: u32,
184    width_hr: i32,
185    half_height_hr: i32,
186    offset_y_hr: i32,
187}
188
189impl<F: PatternFilter> LineImagePattern<F> {
190    /// Create an empty pattern with the specified filter type.
191    pub fn new() -> Self {
192        let dilation = F::dilation() + 1;
193        Self {
194            _phantom: std::marker::PhantomData,
195            buf: Vec::new(),
196            dilation,
197            dilation_hr: (dilation as i32) << LINE_SUBPIXEL_SHIFT,
198            width: 0,
199            height: 0,
200            width_hr: 0,
201            half_height_hr: 0,
202            offset_y_hr: 0,
203        }
204    }
205
206    /// Create a pattern initialized from a source.
207    pub fn with_source<S: ImagePatternSource>(src: &S) -> Self {
208        let mut p = Self::new();
209        p.create(src);
210        p
211    }
212
213    /// Initialize or reinitialize the pattern from a source image.
214    ///
215    /// Port of C++ `line_image_pattern::create`.
216    pub fn create<S: ImagePatternSource>(&mut self, src: &S) {
217        self.height = uceil(src.height());
218        self.width = uceil(src.width());
219        self.width_hr = uround(src.width() * LINE_SUBPIXEL_SCALE as f64);
220        self.half_height_hr = uround(src.height() * LINE_SUBPIXEL_SCALE as f64 / 2.0);
221        self.offset_y_hr =
222            self.dilation_hr + self.half_height_hr - LINE_SUBPIXEL_SCALE / 2;
223        self.half_height_hr += LINE_SUBPIXEL_SCALE / 2;
224
225        let total_w = (self.width + self.dilation * 2) as usize;
226        let total_h = (self.height + self.dilation * 2) as usize;
227
228        // Allocate buffer
229        self.buf = vec![vec![Rgba8::new(0, 0, 0, 0); total_w]; total_h];
230
231        // Copy source pixels into center region
232        for y in 0..self.height as usize {
233            let row = &mut self.buf[y + self.dilation as usize];
234            for x in 0..self.width as usize {
235                row[x + self.dilation as usize] = src.pixel(x as i32, y as i32);
236            }
237        }
238
239        // Fill top/bottom dilation borders with no_color (transparent)
240        let no_color = Rgba8::new(0, 0, 0, 0);
241        for dy in 0..self.dilation as usize {
242            // Bottom border
243            let row_bot = &mut self.buf[self.dilation as usize + self.height as usize + dy];
244            for x in 0..self.width as usize {
245                row_bot[x + self.dilation as usize] = no_color;
246            }
247            // Top border
248            let row_top = &mut self.buf[self.dilation as usize - dy - 1];
249            for x in 0..self.width as usize {
250                row_top[x + self.dilation as usize] = no_color;
251            }
252        }
253
254        // Fill left/right dilation borders (wrap from opposite side)
255        // C++ wraps: right border gets left-edge pixels, left border gets right-edge pixels
256        for y in 0..total_h {
257            for dx in 0..self.dilation as usize {
258                // Right border: copy from left edge of center
259                let src_val = self.buf[y][self.dilation as usize + dx];
260                self.buf[y][self.dilation as usize + self.width as usize + dx] = src_val;
261
262                // Left border: copy from right edge of center
263                let src_val = self.buf[y]
264                    [self.dilation as usize + self.width as usize - 1 - dx];
265                self.buf[y][self.dilation as usize - 1 - dx] = src_val;
266            }
267        }
268    }
269
270    /// Pattern width in subpixel coordinates (for repeating).
271    pub fn pattern_width(&self) -> i32 {
272        self.width_hr
273    }
274
275    /// Line width in subpixel coordinates (half-height of pattern).
276    pub fn line_width(&self) -> i32 {
277        self.half_height_hr
278    }
279
280    /// Width in floating-point (returns height, matching C++ behavior).
281    pub fn width(&self) -> f64 {
282        self.height as f64
283    }
284
285    /// Get a pixel from the pattern at the given subpixel coordinates.
286    ///
287    /// Port of C++ `line_image_pattern::pixel`.
288    #[inline]
289    pub fn pixel(&self, p: &mut Rgba8, x: i32, y: i32) {
290        F::pixel_high_res(
291            &self.buf,
292            p,
293            x % self.width_hr + self.dilation_hr,
294            y + self.offset_y_hr,
295        );
296    }
297}
298
299// ============================================================================
300// line_image_pattern_pow2 — optimized version using power-of-2 masking
301// ============================================================================
302
303/// Power-of-2 optimized image pattern for line rendering.
304///
305/// Port of C++ `line_image_pattern_pow2<Filter>`.
306/// Uses bit masking instead of modulo for pattern wrapping.
307pub struct LineImagePatternPow2<F: PatternFilter> {
308    base: LineImagePattern<F>,
309    mask: u32,
310}
311
312impl<F: PatternFilter> LineImagePatternPow2<F> {
313    pub fn new() -> Self {
314        Self {
315            base: LineImagePattern::new(),
316            mask: LINE_SUBPIXEL_MASK as u32,
317        }
318    }
319
320    pub fn with_source<S: ImagePatternSource>(src: &S) -> Self {
321        let mut p = Self::new();
322        p.create(src);
323        p
324    }
325
326    pub fn create<S: ImagePatternSource>(&mut self, src: &S) {
327        self.base.create(src);
328        self.mask = 1;
329        while self.mask < self.base.width {
330            self.mask <<= 1;
331            self.mask |= 1;
332        }
333        self.mask <<= LINE_SUBPIXEL_SHIFT as u32 - 1;
334        self.mask |= LINE_SUBPIXEL_MASK as u32;
335        self.base.width_hr = self.mask as i32 + 1;
336    }
337
338    pub fn pattern_width(&self) -> i32 {
339        self.base.width_hr
340    }
341
342    pub fn line_width(&self) -> i32 {
343        self.base.half_height_hr
344    }
345
346    pub fn width(&self) -> f64 {
347        self.base.height as f64
348    }
349
350    #[inline]
351    pub fn pixel(&self, p: &mut Rgba8, x: i32, y: i32) {
352        F::pixel_high_res(
353            &self.base.buf,
354            p,
355            (x & self.mask as i32) + self.base.dilation_hr,
356            y + self.base.offset_y_hr,
357        );
358    }
359}
360
361// ============================================================================
362// ImageLinePattern trait — common interface for line image patterns
363// ============================================================================
364
365/// Trait for image-line patterns (both standard and pow2-optimized).
366///
367/// This abstracts over `LineImagePattern` and `LineImagePatternPow2` so that
368/// `RendererOutlineImage` can work with either type.
369pub trait ImageLinePattern {
370    /// Pattern width in subpixel coordinates (for repeating).
371    fn pattern_width(&self) -> i32;
372    /// Line width in subpixel coordinates (half-height of pattern).
373    fn line_width(&self) -> i32;
374    /// Width in floating-point (returns height, matching C++ behavior).
375    fn width(&self) -> f64;
376    /// Get a pixel from the pattern at the given subpixel coordinates.
377    fn pixel(&self, p: &mut Rgba8, x: i32, y: i32);
378}
379
380impl<F: PatternFilter> ImageLinePattern for LineImagePattern<F> {
381    fn pattern_width(&self) -> i32 { self.pattern_width() }
382    fn line_width(&self) -> i32 { self.line_width() }
383    fn width(&self) -> f64 { self.width() }
384    fn pixel(&self, p: &mut Rgba8, x: i32, y: i32) { self.pixel(p, x, y) }
385}
386
387impl<F: PatternFilter> ImageLinePattern for LineImagePatternPow2<F> {
388    fn pattern_width(&self) -> i32 { self.pattern_width() }
389    fn line_width(&self) -> i32 { self.line_width() }
390    fn width(&self) -> f64 { self.width() }
391    fn pixel(&self, p: &mut Rgba8, x: i32, y: i32) { self.pixel(p, x, y) }
392}
393
394// ============================================================================
395// distance_interpolator4 — for image pattern rendering
396// ============================================================================
397
398/// Distance interpolator for image-patterned lines.
399///
400/// Port of C++ `distance_interpolator4`.
401/// Tracks perpendicular distance, start/end join distances, and pattern offset distance.
402pub struct DistanceInterpolator4 {
403    dx: i32,
404    dy: i32,
405    dx_start: i32,
406    dy_start: i32,
407    dx_pict: i32,
408    dy_pict: i32,
409    dx_end: i32,
410    dy_end: i32,
411    dist: i32,
412    dist_start: i32,
413    dist_pict: i32,
414    dist_end: i32,
415    len: i32,
416}
417
418impl DistanceInterpolator4 {
419    #[allow(clippy::too_many_arguments)]
420    pub fn new(
421        x1: i32, y1: i32, x2: i32, y2: i32,
422        sx: i32, sy: i32, ex: i32, ey: i32,
423        len: i32, scale: f64, x: i32, y: i32,
424    ) -> Self {
425        let mut dx = x2 - x1;
426        let mut dy = y2 - y1;
427        let mut dx_start = line_mr(sx) - line_mr(x1);
428        let mut dy_start = line_mr(sy) - line_mr(y1);
429        let mut dx_end = line_mr(ex) - line_mr(x2);
430        let mut dy_end = line_mr(ey) - line_mr(y2);
431
432        let dist = iround(
433            (x + LINE_SUBPIXEL_SCALE / 2 - x2) as f64 * dy as f64
434                - (y + LINE_SUBPIXEL_SCALE / 2 - y2) as f64 * dx as f64,
435        );
436
437        let dist_start = (line_mr(x + LINE_SUBPIXEL_SCALE / 2) - line_mr(sx)) * dy_start
438            - (line_mr(y + LINE_SUBPIXEL_SCALE / 2) - line_mr(sy)) * dx_start;
439
440        let dist_end = (line_mr(x + LINE_SUBPIXEL_SCALE / 2) - line_mr(ex)) * dy_end
441            - (line_mr(y + LINE_SUBPIXEL_SCALE / 2) - line_mr(ey)) * dx_end;
442
443        let ilen = uround(len as f64 / scale);
444
445        let d = len as f64 * scale;
446        let dx_f = iround(((x2 - x1) << LINE_SUBPIXEL_SHIFT) as f64 / d);
447        let dy_f = iround(((y2 - y1) << LINE_SUBPIXEL_SHIFT) as f64 / d);
448        let dx_pict = -dy_f;
449        let dy_pict = dx_f;
450        let dist_pict = ((x + LINE_SUBPIXEL_SCALE / 2 - (x1 - dy_f)) as i64
451            * dy_pict as i64
452            - (y + LINE_SUBPIXEL_SCALE / 2 - (y1 + dx_f)) as i64
453                * dx_pict as i64)
454            >> LINE_SUBPIXEL_SHIFT;
455
456        dx <<= LINE_SUBPIXEL_SHIFT;
457        dy <<= LINE_SUBPIXEL_SHIFT;
458        dx_start <<= LINE_MR_SUBPIXEL_SHIFT;
459        dy_start <<= LINE_MR_SUBPIXEL_SHIFT;
460        dx_end <<= LINE_MR_SUBPIXEL_SHIFT;
461        dy_end <<= LINE_MR_SUBPIXEL_SHIFT;
462
463        Self {
464            dx, dy, dx_start, dy_start, dx_pict, dy_pict, dx_end, dy_end,
465            dist, dist_start, dist_pict: dist_pict as i32, dist_end,
466            len: ilen,
467        }
468    }
469
470    #[inline]
471    pub fn inc_x(&mut self, dy: i32) {
472        self.dist += self.dy;
473        self.dist_start += self.dy_start;
474        self.dist_pict += self.dy_pict;
475        self.dist_end += self.dy_end;
476        if dy > 0 {
477            self.dist -= self.dx;
478            self.dist_start -= self.dx_start;
479            self.dist_pict -= self.dx_pict;
480            self.dist_end -= self.dx_end;
481        }
482        if dy < 0 {
483            self.dist += self.dx;
484            self.dist_start += self.dx_start;
485            self.dist_pict += self.dx_pict;
486            self.dist_end += self.dx_end;
487        }
488    }
489
490    #[inline]
491    pub fn dec_x(&mut self, dy: i32) {
492        self.dist -= self.dy;
493        self.dist_start -= self.dy_start;
494        self.dist_pict -= self.dy_pict;
495        self.dist_end -= self.dy_end;
496        if dy > 0 {
497            self.dist -= self.dx;
498            self.dist_start -= self.dx_start;
499            self.dist_pict -= self.dx_pict;
500            self.dist_end -= self.dx_end;
501        }
502        if dy < 0 {
503            self.dist += self.dx;
504            self.dist_start += self.dx_start;
505            self.dist_pict += self.dx_pict;
506            self.dist_end += self.dx_end;
507        }
508    }
509
510    #[inline]
511    pub fn inc_y(&mut self, dx: i32) {
512        self.dist -= self.dx;
513        self.dist_start -= self.dx_start;
514        self.dist_pict -= self.dx_pict;
515        self.dist_end -= self.dx_end;
516        if dx > 0 {
517            self.dist += self.dy;
518            self.dist_start += self.dy_start;
519            self.dist_pict += self.dy_pict;
520            self.dist_end += self.dy_end;
521        }
522        if dx < 0 {
523            self.dist -= self.dy;
524            self.dist_start -= self.dy_start;
525            self.dist_pict -= self.dy_pict;
526            self.dist_end -= self.dy_end;
527        }
528    }
529
530    #[inline]
531    pub fn dec_y(&mut self, dx: i32) {
532        self.dist += self.dx;
533        self.dist_start += self.dx_start;
534        self.dist_pict += self.dx_pict;
535        self.dist_end += self.dx_end;
536        if dx > 0 {
537            self.dist += self.dy;
538            self.dist_start += self.dy_start;
539            self.dist_pict += self.dy_pict;
540            self.dist_end += self.dy_end;
541        }
542        if dx < 0 {
543            self.dist -= self.dy;
544            self.dist_start -= self.dy_start;
545            self.dist_pict -= self.dy_pict;
546            self.dist_end -= self.dy_end;
547        }
548    }
549
550    #[inline] pub fn dist(&self) -> i32 { self.dist }
551    #[inline] pub fn dist_start(&self) -> i32 { self.dist_start }
552    #[inline] pub fn dist_pict(&self) -> i32 { self.dist_pict }
553    #[inline] pub fn dist_end(&self) -> i32 { self.dist_end }
554    #[inline] pub fn dx_start(&self) -> i32 { self.dx_start }
555    #[inline] pub fn dy_start(&self) -> i32 { self.dy_start }
556    #[inline] pub fn dx_pict(&self) -> i32 { self.dx_pict }
557    #[inline] pub fn dy_pict(&self) -> i32 { self.dy_pict }
558    #[inline] pub fn dx_end(&self) -> i32 { self.dx_end }
559    #[inline] pub fn dy_end(&self) -> i32 { self.dy_end }
560    #[inline] pub fn len(&self) -> i32 { self.len }
561}
562
563// ============================================================================
564// renderer_outline_image — renders lines with image patterns
565// ============================================================================
566
567/// Image-patterned outline renderer.
568///
569/// Port of C++ `renderer_outline_image<BaseRenderer, ImagePattern>`.
570/// Renders anti-aliased lines using an image pattern sampled along the stroke.
571///
572/// The generic parameter `P` is the image line pattern type (e.g.,
573/// `LineImagePattern<PatternFilterBilinearRgba>` or
574/// `LineImagePatternPow2<PatternFilterBilinearRgba>`).
575pub struct RendererOutlineImage<'a, PF: PixelFormat, P: ImageLinePattern> {
576    ren: &'a mut RendererBase<PF>,
577    pattern: &'a P,
578    start: i32,
579    scale_x: f64,
580    clip_box: RectI,
581    clipping: bool,
582}
583
584impl<'a, PF: PixelFormat, P: ImageLinePattern> RendererOutlineImage<'a, PF, P>
585where
586    PF::ColorType: Default + Clone + From<Rgba8>,
587{
588    pub fn new(ren: &'a mut RendererBase<PF>, pattern: &'a P) -> Self {
589        Self {
590            ren,
591            pattern,
592            start: 0,
593            scale_x: 1.0,
594            clip_box: RectI::new(0, 0, 0, 0),
595            clipping: false,
596        }
597    }
598
599    pub fn reset_clipping(&mut self) {
600        self.clipping = false;
601    }
602
603    pub fn set_clip_box(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
604        self.clip_box = RectI::new(
605            line_coord_sat(x1),
606            line_coord_sat(y1),
607            line_coord_sat(x2),
608            line_coord_sat(y2),
609        );
610        self.clipping = true;
611    }
612
613    pub fn set_scale_x(&mut self, s: f64) {
614        self.scale_x = s;
615    }
616
617    pub fn scale_x(&self) -> f64 {
618        self.scale_x
619    }
620
621    pub fn set_start_x(&mut self, s: f64) {
622        self.start = iround(s * LINE_SUBPIXEL_SCALE as f64);
623    }
624
625    pub fn start_x(&self) -> f64 {
626        self.start as f64 / LINE_SUBPIXEL_SCALE as f64
627    }
628
629    pub fn subpixel_width(&self) -> i32 {
630        self.pattern.line_width()
631    }
632
633    pub fn pattern_width(&self) -> i32 {
634        self.pattern.pattern_width()
635    }
636
637    pub fn width(&self) -> f64 {
638        self.subpixel_width() as f64 / LINE_SUBPIXEL_SCALE as f64
639    }
640
641    /// Render a line segment with image pattern (no clipping).
642    ///
643    /// Port of C++ `renderer_outline_image::line3_no_clip`.
644    fn line3_no_clip(
645        &mut self,
646        lp: &LineParameters,
647        sx: i32, sy: i32,
648        ex: i32, ey: i32,
649    ) {
650        if lp.len > LINE_MAX_LENGTH {
651            let (lp1, lp2) = lp.divide();
652            let mx = lp1.x2 + (lp1.y2 - lp1.y1);
653            let my = lp1.y2 - (lp1.x2 - lp1.x1);
654            self.line3_no_clip(
655                &lp1,
656                (lp.x1 + sx) >> 1, (lp.y1 + sy) >> 1,
657                mx, my,
658            );
659            self.line3_no_clip(
660                &lp2,
661                mx, my,
662                (lp.x2 + ex) >> 1, (lp.y2 + ey) >> 1,
663            );
664            return;
665        }
666
667        let mut sx = sx;
668        let mut sy = sy;
669        let mut ex = ex;
670        let mut ey = ey;
671        fix_degenerate_bisectrix_start(lp, &mut sx, &mut sy);
672        fix_degenerate_bisectrix_end(lp, &mut ex, &mut ey);
673
674        // Run the line interpolator inline, sampling from the pattern
675        // and blending into the renderer.
676        self.render_line_image(lp, sx, sy, ex, ey);
677
678        self.start += uround(lp.len as f64 / self.scale_x);
679    }
680
681    /// Core rendering: walk along the line segment and sample the pattern.
682    ///
683    /// This combines the C++ `line_interpolator_image` constructor and
684    /// stepping methods into a single function to avoid borrow conflicts.
685    #[allow(clippy::too_many_arguments)]
686    fn render_line_image(
687        &mut self,
688        lp: &LineParameters,
689        sx: i32, sy: i32,
690        ex: i32, ey: i32,
691    ) {
692        const MAX_HW: usize = 64;
693        const DIST_SIZE: usize = MAX_HW + 1;
694        const COLOR_SIZE: usize = MAX_HW * 2 + 4;
695
696        let li_init = if lp.vertical {
697            Dda2LineInterpolator::new_relative(
698                line_dbl_hr(lp.x2 - lp.x1),
699                (lp.y2 - lp.y1).abs(),
700            )
701        } else {
702            Dda2LineInterpolator::new_relative(
703                line_dbl_hr(lp.y2 - lp.y1),
704                (lp.x2 - lp.x1).abs() + 1,
705            )
706        };
707
708        let di_init = DistanceInterpolator4::new(
709            lp.x1, lp.y1, lp.x2, lp.y2,
710            sx, sy, ex, ey,
711            lp.len, self.scale_x,
712            lp.x1 & !LINE_SUBPIXEL_MASK,
713            lp.y1 & !LINE_SUBPIXEL_MASK,
714        );
715
716        let ix = lp.x1 >> LINE_SUBPIXEL_SHIFT;
717        let iy = lp.y1 >> LINE_SUBPIXEL_SHIFT;
718
719        let count = if lp.vertical {
720            ((lp.y2 >> LINE_SUBPIXEL_SHIFT) - iy).abs()
721        } else {
722            ((lp.x2 >> LINE_SUBPIXEL_SHIFT) - ix).abs()
723        };
724
725        let width = self.pattern.line_width();
726        let max_extent = (width + LINE_SUBPIXEL_SCALE) >> LINE_SUBPIXEL_SHIFT;
727        let start = self.start + (max_extent + 2) * self.pattern.pattern_width();
728
729        // Pre-compute distance table
730        let mut dist_pos = [0i32; DIST_SIZE];
731        {
732            let mut dd = Dda2LineInterpolator::new_forward(
733                0,
734                if lp.vertical { lp.dy << LINE_SUBPIXEL_SHIFT } else { lp.dx << LINE_SUBPIXEL_SHIFT },
735                lp.len,
736            );
737            let stop = width + LINE_SUBPIXEL_SCALE * 2;
738            let mut i = 0;
739            while i < MAX_HW {
740                dist_pos[i] = dd.y();
741                if dist_pos[i] >= stop { break; }
742                dd.inc();
743                i += 1;
744            }
745            if i <= MAX_HW {
746                dist_pos[i] = 0x7FFF_0000;
747            }
748        }
749
750        let mut li = li_init;
751        let mut di = di_init;
752        let mut x = ix;
753        let mut y = iy;
754        let mut old_x = ix;
755        let mut old_y = iy;
756        let mut step = 0i32;
757
758        // ---- Backward stepping ----
759        let mut npix = 1i32;
760        if lp.vertical {
761            loop {
762                li.dec();
763                y -= lp.inc;
764                x = (lp.x1 + li.y()) >> LINE_SUBPIXEL_SHIFT;
765                if lp.inc > 0 { di.dec_y(x - old_x); }
766                else { di.inc_y(x - old_x); }
767                old_x = x;
768
769                let mut d1 = di.dist_start();
770                let mut d2 = d1;
771                let mut dx = 0usize;
772                if d1 < 0 { npix += 1; }
773                loop {
774                    d1 += di.dy_start();
775                    d2 -= di.dy_start();
776                    if d1 < 0 { npix += 1; }
777                    if d2 < 0 { npix += 1; }
778                    dx += 1;
779                    if dist_pos[dx] > width { break; }
780                }
781                if npix == 0 { break; }
782                npix = 0;
783                step -= 1;
784                if step < -max_extent { break; }
785            }
786        } else {
787            loop {
788                li.dec();
789                x -= lp.inc;
790                y = (lp.y1 + li.y()) >> LINE_SUBPIXEL_SHIFT;
791                if lp.inc > 0 { di.dec_x(y - old_y); }
792                else { di.inc_x(y - old_y); }
793                old_y = y;
794
795                let mut d1 = di.dist_start();
796                let mut d2 = d1;
797                let mut dy = 0usize;
798                if d1 < 0 { npix += 1; }
799                loop {
800                    d1 -= di.dx_start();
801                    d2 += di.dx_start();
802                    if d1 < 0 { npix += 1; }
803                    if d2 < 0 { npix += 1; }
804                    dy += 1;
805                    if dist_pos[dy] > width { break; }
806                }
807                if npix == 0 { break; }
808                npix = 0;
809                step -= 1;
810                if step < -max_extent { break; }
811            }
812        }
813
814        li.adjust_forward();
815
816        step -= max_extent;
817
818        // ---- Forward stepping ----
819        let mut colors = [Rgba8::new(0, 0, 0, 0); COLOR_SIZE];
820
821        if lp.vertical {
822            loop {
823                // step_ver
824                li.inc();
825                y += lp.inc;
826                x = (lp.x1 + li.y()) >> LINE_SUBPIXEL_SHIFT;
827                if lp.inc > 0 { di.inc_y(x - old_x); }
828                else { di.dec_y(x - old_x); }
829                old_x = x;
830
831                let s1 = di.dist() / lp.len;
832                let s2 = -s1;
833                let s1_adj = if lp.inc > 0 { -s1 } else { s1 };
834
835                let mut dist_start = di.dist_start();
836                let mut dist_pict = di.dist_pict() + start;
837                let mut dist_end = di.dist_end();
838
839                let center = MAX_HW + 2;
840                let mut p0 = center;
841                let mut p1 = center;
842
843                let mut n = 0;
844                colors[p1] = Rgba8::new(0, 0, 0, 0);
845                if dist_end > 0 {
846                    if dist_start <= 0 {
847                        self.pattern.pixel(&mut colors[p1], dist_pict, s2);
848                    }
849                    n += 1;
850                }
851                p1 += 1;
852
853                let mut dx = 1usize;
854                while dx < DIST_SIZE && dist_pos[dx] - s1_adj <= width {
855                    dist_start += di.dy_start();
856                    dist_pict += di.dy_pict();
857                    dist_end += di.dy_end();
858                    colors[p1] = Rgba8::new(0, 0, 0, 0);
859                    if dist_end > 0 && dist_start <= 0 {
860                        let mut d = dist_pos[dx];
861                        if lp.inc > 0 { d = -d; }
862                        self.pattern.pixel(&mut colors[p1], dist_pict, s2 + d);
863                        n += 1;
864                    }
865                    p1 += 1;
866                    dx += 1;
867                }
868
869                dx = 1;
870                dist_start = di.dist_start();
871                dist_pict = di.dist_pict() + start;
872                dist_end = di.dist_end();
873                while dx < DIST_SIZE && dist_pos[dx] + s1_adj <= width {
874                    dist_start -= di.dy_start();
875                    dist_pict -= di.dy_pict();
876                    dist_end -= di.dy_end();
877                    p0 -= 1;
878                    colors[p0] = Rgba8::new(0, 0, 0, 0);
879                    if dist_end > 0 && dist_start <= 0 {
880                        let mut d = dist_pos[dx];
881                        if lp.inc > 0 { d = -d; }
882                        self.pattern.pixel(&mut colors[p0], dist_pict, s2 - d);
883                        n += 1;
884                    }
885                    dx += 1;
886                }
887
888                // Blend horizontal span
889                let len = p1 - p0;
890                if len > 0 {
891                    let cvec: Vec<PF::ColorType> = colors[p0..p1]
892                        .iter()
893                        .map(|c| PF::ColorType::from(*c))
894                        .collect();
895                    self.ren.blend_color_hspan(
896                        x - dx as i32 + 1, y, len as i32,
897                        &cvec, &[], 255,
898                    );
899                }
900
901                step += 1;
902                if n == 0 || step >= count {
903                    break;
904                }
905            }
906        } else {
907            loop {
908                // step_hor
909                li.inc();
910                x += lp.inc;
911                y = (lp.y1 + li.y()) >> LINE_SUBPIXEL_SHIFT;
912                if lp.inc > 0 { di.inc_x(y - old_y); }
913                else { di.dec_x(y - old_y); }
914                old_y = y;
915
916                let s1 = di.dist() / lp.len;
917                let s2 = -s1;
918                let s1_adj = if lp.inc < 0 { -s1 } else { s1 };
919
920                let mut dist_start = di.dist_start();
921                let mut dist_pict = di.dist_pict() + start;
922                let mut dist_end = di.dist_end();
923
924                let center = MAX_HW + 2;
925                let mut p0 = center;
926                let mut p1 = center;
927
928                let mut n = 0;
929                colors[p1] = Rgba8::new(0, 0, 0, 0);
930                if dist_end > 0 {
931                    if dist_start <= 0 {
932                        self.pattern.pixel(&mut colors[p1], dist_pict, s2);
933                    }
934                    n += 1;
935                }
936                p1 += 1;
937
938                let mut dy = 1usize;
939                while dy < DIST_SIZE && dist_pos[dy] - s1_adj <= width {
940                    dist_start -= di.dx_start();
941                    dist_pict -= di.dx_pict();
942                    dist_end -= di.dx_end();
943                    colors[p1] = Rgba8::new(0, 0, 0, 0);
944                    if dist_end > 0 && dist_start <= 0 {
945                        let mut d = dist_pos[dy];
946                        if lp.inc > 0 { d = -d; }
947                        self.pattern.pixel(&mut colors[p1], dist_pict, s2 - d);
948                        n += 1;
949                    }
950                    p1 += 1;
951                    dy += 1;
952                }
953
954                dy = 1;
955                dist_start = di.dist_start();
956                dist_pict = di.dist_pict() + start;
957                dist_end = di.dist_end();
958                while dy < DIST_SIZE && dist_pos[dy] + s1_adj <= width {
959                    dist_start += di.dx_start();
960                    dist_pict += di.dx_pict();
961                    dist_end += di.dx_end();
962                    p0 -= 1;
963                    colors[p0] = Rgba8::new(0, 0, 0, 0);
964                    if dist_end > 0 && dist_start <= 0 {
965                        let mut d = dist_pos[dy];
966                        if lp.inc > 0 { d = -d; }
967                        self.pattern.pixel(&mut colors[p0], dist_pict, s2 + d);
968                        n += 1;
969                    }
970                    dy += 1;
971                }
972
973                // Blend vertical span
974                let len = p1 - p0;
975                if len > 0 {
976                    let cvec: Vec<PF::ColorType> = colors[p0..p1]
977                        .iter()
978                        .map(|c| PF::ColorType::from(*c))
979                        .collect();
980                    self.ren.blend_color_vspan(
981                        x, y - dy as i32 + 1, len as i32,
982                        &cvec, &[], 255,
983                    );
984                }
985
986                step += 1;
987                if n == 0 || step >= count {
988                    break;
989                }
990            }
991        }
992    }
993}
994
995impl<'a, PF: PixelFormat, P: ImageLinePattern> OutlineAaRenderer
996    for RendererOutlineImage<'a, PF, P>
997where
998    PF::ColorType: Default + Clone + From<Rgba8>,
999{
1000    fn accurate_join_only(&self) -> bool {
1001        true
1002    }
1003
1004    // Image pattern renderer only supports line3 (with both joins).
1005    fn line0(&mut self, _lp: &LineParameters) {}
1006    fn line1(&mut self, _lp: &LineParameters, _sx: i32, _sy: i32) {}
1007    fn line2(&mut self, _lp: &LineParameters, _ex: i32, _ey: i32) {}
1008
1009    fn line3(&mut self, lp: &LineParameters, sx: i32, sy: i32, ex: i32, ey: i32) {
1010        if self.clipping {
1011            let mut x1 = lp.x1;
1012            let mut y1 = lp.y1;
1013            let mut x2 = lp.x2;
1014            let mut y2 = lp.y2;
1015            let flags = clip_line_segment(&mut x1, &mut y1, &mut x2, &mut y2, &self.clip_box);
1016            let start = self.start;
1017            if (flags & 4) == 0 {
1018                if flags != 0 {
1019                    let lp2 = LineParameters::new(
1020                        x1, y1, x2, y2,
1021                        uround(calc_distance_i(x1, y1, x2, y2)),
1022                    );
1023                    let mut sx = sx;
1024                    let mut sy = sy;
1025                    let mut ex = ex;
1026                    let mut ey = ey;
1027                    if flags & 1 != 0 {
1028                        self.start += uround(
1029                            calc_distance_i(lp.x1, lp.y1, x1, y1) / self.scale_x,
1030                        );
1031                        sx = x1 + (y2 - y1);
1032                        sy = y1 - (x2 - x1);
1033                    } else {
1034                        while (sx - lp.x1).abs() + (sy - lp.y1).abs() > lp2.len {
1035                            sx = (lp.x1 + sx) >> 1;
1036                            sy = (lp.y1 + sy) >> 1;
1037                        }
1038                    }
1039                    if flags & 2 != 0 {
1040                        ex = x2 + (y2 - y1);
1041                        ey = y2 - (x2 - x1);
1042                    } else {
1043                        while (ex - lp.x2).abs() + (ey - lp.y2).abs() > lp2.len {
1044                            ex = (lp.x2 + ex) >> 1;
1045                            ey = (lp.y2 + ey) >> 1;
1046                        }
1047                    }
1048                    self.line3_no_clip(&lp2, sx, sy, ex, ey);
1049                } else {
1050                    self.line3_no_clip(lp, sx, sy, ex, ey);
1051                }
1052            }
1053            self.start = start + uround(lp.len as f64 / self.scale_x);
1054        } else {
1055            self.line3_no_clip(lp, sx, sy, ex, ey);
1056        }
1057    }
1058
1059    fn semidot(&mut self, _cmp: fn(i32) -> bool, _xc1: i32, _yc1: i32, _xc2: i32, _yc2: i32) {}
1060    fn pie(&mut self, _xc: i32, _yc: i32, _x1: i32, _y1: i32, _x2: i32, _y2: i32) {}
1061}
1062
1063// ============================================================================
1064// Helper
1065// ============================================================================
1066
1067fn calc_distance_i(x1: i32, y1: i32, x2: i32, y2: i32) -> f64 {
1068    let dx = (x2 - x1) as f64;
1069    let dy = (y2 - y1) as f64;
1070    (dx * dx + dy * dy).sqrt()
1071}
1072
1073// ============================================================================
1074// Tests
1075// ============================================================================
1076
1077#[cfg(test)]
1078mod tests {
1079    use super::*;
1080
1081    /// Simple solid-color pattern source for testing.
1082    struct SolidPatternSource {
1083        w: u32,
1084        h: u32,
1085        color: Rgba8,
1086    }
1087
1088    impl ImagePatternSource for SolidPatternSource {
1089        fn width(&self) -> f64 { self.w as f64 }
1090        fn height(&self) -> f64 { self.h as f64 }
1091        fn pixel(&self, _x: i32, _y: i32) -> Rgba8 { self.color }
1092    }
1093
1094    #[test]
1095    fn test_line_image_pattern_creation() {
1096        use crate::pattern_filters_rgba::PatternFilterBilinearRgba;
1097        let src = SolidPatternSource { w: 16, h: 8, color: Rgba8::new(255, 0, 0, 255) };
1098        let pat = LineImagePattern::<PatternFilterBilinearRgba>::with_source(&src);
1099        assert!(pat.pattern_width() > 0);
1100        assert!(pat.line_width() > 0);
1101    }
1102
1103    #[test]
1104    fn test_line_image_pattern_pixel() {
1105        use crate::pattern_filters_rgba::PatternFilterNn;
1106        let src = SolidPatternSource { w: 16, h: 8, color: Rgba8::new(255, 128, 64, 200) };
1107        let pat = LineImagePattern::<PatternFilterNn>::with_source(&src);
1108        let mut p = Rgba8::new(0, 0, 0, 0);
1109        pat.pixel(&mut p, 0, 0);
1110        assert!(p.a > 0, "Expected non-zero alpha");
1111    }
1112
1113    #[test]
1114    fn test_distance_interpolator4() {
1115        let di = DistanceInterpolator4::new(
1116            0, 0, 256, 0,
1117            -256, 0, 512, 0,
1118            256, 1.0, 0, 0,
1119        );
1120        assert_ne!(di.len(), 0);
1121    }
1122
1123    #[test]
1124    fn test_line_image_pattern_pow2() {
1125        use crate::pattern_filters_rgba::PatternFilterBilinearRgba;
1126        let src = SolidPatternSource { w: 16, h: 8, color: Rgba8::new(0, 255, 0, 255) };
1127        let pat = LineImagePatternPow2::<PatternFilterBilinearRgba>::with_source(&src);
1128        assert!(pat.pattern_width() > 0);
1129        assert!(pat.line_width() > 0);
1130        let mut p = Rgba8::new(0, 0, 0, 0);
1131        pat.pixel(&mut p, 0, 0);
1132        assert!(p.a > 0);
1133    }
1134
1135    #[test]
1136    fn test_renderer_outline_image_basic() {
1137        use crate::pattern_filters_rgba::PatternFilterBilinearRgba;
1138        use crate::pixfmt_rgba::PixfmtRgba32;
1139        use crate::rendering_buffer::RowAccessor;
1140
1141        let w = 100u32;
1142        let h = 100u32;
1143        let stride = (w * 4) as i32;
1144        let mut buf = vec![255u8; (h * w * 4) as usize];
1145        let mut ra = RowAccessor::new();
1146        unsafe { ra.attach(buf.as_mut_ptr(), w, h, stride); }
1147        let pf = PixfmtRgba32::new(&mut ra);
1148        let mut rb = RendererBase::new(pf);
1149
1150        let src = SolidPatternSource { w: 32, h: 16, color: Rgba8::new(255, 0, 0, 255) };
1151        let pat = LineImagePattern::<PatternFilterBilinearRgba>::with_source(&src);
1152
1153        let mut ren = RendererOutlineImage::new(&mut rb, &pat);
1154        ren.set_scale_x(1.0);
1155        ren.set_start_x(0.0);
1156
1157        // Draw a horizontal line
1158        let lp = LineParameters::new(
1159            10 * 256, 50 * 256,
1160            90 * 256, 50 * 256,
1161            80 * 256,
1162        );
1163        ren.line3(
1164            &lp,
1165            10 * 256 + (0), 50 * 256 - (80 * 256),
1166            90 * 256 + (0), 50 * 256 - (80 * 256),
1167        );
1168
1169        // Check some pixels were drawn (red channel should be non-zero near the line)
1170        let mut found = false;
1171        let pf2 = PixfmtRgba32::new(&mut ra);
1172        let rb2 = RendererBase::new(pf2);
1173        for y in 42..=58 {
1174            for x in 10..90 {
1175                let p = rb2.pixel(x, y);
1176                if p.r > 100 {
1177                    found = true;
1178                    break;
1179                }
1180            }
1181            if found { break; }
1182        }
1183        assert!(found, "Expected red pixels near row 50");
1184    }
1185}