Skip to main content

rasterrocket_render/
tiling.rs

1//! Tiling pattern source for the compositing pipeline.
2//!
3//! [`TiledPattern`] holds a pre-rasterised tile bitmap and implements
4//! [`crate::Pattern`] by repeating the tile across device space using modular
5//! arithmetic.  One tile is computed once and then referenced immutably during
6//! the fill/stroke operation.
7
8use crate::pipe::Pattern;
9
10/// A pre-rasterised tiling pattern.
11///
12/// The tile is a row-major RGB8 pixel buffer of `width × height` pixels,
13/// anchored at `(phase_x, phase_y)` in device space.  `fill_span` tiles the
14/// buffer by wrapping coordinates with `rem_euclid`.
15///
16/// # Invariants
17///
18/// - `width > 0` and `height > 0` (asserted in [`TiledPattern::new`])
19/// - `pixels.len() == width * height * 3`
20pub struct TiledPattern {
21    /// Rasterised tile pixels (RGB8, row-major).
22    pixels: Vec<u8>,
23    /// Tile width in device pixels.
24    width: i32,
25    /// Tile height in device pixels.
26    height: i32,
27    /// X offset of the pattern origin in device space.
28    ///
29    /// Used to phase the tiling so that the pattern is anchored at the correct
30    /// position as specified by the pattern matrix translation.
31    phase_x: i32,
32    /// Y offset of the pattern origin in device space.
33    phase_y: i32,
34}
35
36impl TiledPattern {
37    /// Construct a new [`TiledPattern`].
38    ///
39    /// # Panics
40    ///
41    /// Panics if `width` or `height` is ≤ 0, or if `pixels.len()` does not
42    /// equal `width * height * 3`.
43    #[must_use]
44    #[expect(
45        clippy::cast_sign_loss,
46        reason = "width/height are asserted positive just above the cast"
47    )]
48    pub fn new(pixels: Vec<u8>, width: i32, height: i32, phase_x: i32, phase_y: i32) -> Self {
49        assert!(
50            width > 0,
51            "TiledPattern: width must be positive, got {width}"
52        );
53        assert!(
54            height > 0,
55            "TiledPattern: height must be positive, got {height}"
56        );
57        let expected = width as usize * height as usize * 3;
58        assert!(
59            pixels.len() == expected,
60            "TiledPattern: pixels.len() {} does not match width({width}) * height({height}) * 3 = {expected}",
61            pixels.len()
62        );
63        Self {
64            pixels,
65            width,
66            height,
67            phase_x,
68            phase_y,
69        }
70    }
71}
72
73impl Pattern for TiledPattern {
74    #[expect(
75        clippy::cast_sign_loss,
76        reason = "rem_euclid guarantees non-negative results; safe cast to usize"
77    )]
78    fn fill_span(&self, y: i32, x0: i32, x1: i32, out: &mut [u8]) {
79        let ty = (y - self.phase_y).rem_euclid(self.height) as usize;
80        let w = self.width as usize;
81        let row_start = ty * w * 3;
82        let row = &self.pixels[row_start..row_start + w * 3];
83
84        let mut out_pos = 0usize;
85        for x in x0..=x1 {
86            let tx = (x - self.phase_x).rem_euclid(self.width) as usize;
87            out[out_pos] = row[tx * 3];
88            out[out_pos + 1] = row[tx * 3 + 1];
89            out[out_pos + 2] = row[tx * 3 + 2];
90            out_pos += 3;
91        }
92    }
93}