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}