1use core::convert::Infallible;
2
3use embedded_graphics::{
4 Pixel,
5 draw_target::DrawTarget,
6 geometry::{OriginDimensions, Point, Size},
7 pixelcolor::PixelColor,
8 primitives::Rectangle,
9};
10use heapless::Vec;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum DirtySpriteError {
14 BufferTooSmall,
15 TooManyRegions,
16}
17
18pub struct RegionSet<const MAX_REGIONS: usize> {
20 regions: Vec<Rectangle, MAX_REGIONS>,
21}
22
23impl<const MAX_REGIONS: usize> RegionSet<MAX_REGIONS> {
24 pub const fn new() -> Self {
25 Self {
26 regions: Vec::new(),
27 }
28 }
29
30 pub fn clear(&mut self) {
31 self.regions.clear();
32 }
33
34 pub fn is_empty(&self) -> bool {
35 self.regions.is_empty()
36 }
37
38 pub fn iter(&self) -> impl Iterator<Item = Rectangle> + '_ {
39 self.regions.iter().copied()
40 }
41
42 pub fn add(&mut self, rect: Rectangle) -> Result<(), DirtySpriteError> {
43 if rect.is_zero_sized() {
44 return Ok(());
45 }
46
47 if let Some(existing) = self
48 .regions
49 .iter_mut()
50 .find(|existing| intersects_or_touches(**existing, rect))
51 {
52 *existing = bounding_rect(*existing, rect);
53 self.compact();
54 return Ok(());
55 }
56
57 self.regions
58 .push(rect)
59 .map_err(|_| DirtySpriteError::TooManyRegions)
60 }
61
62 fn compact(&mut self) {
63 let mut i = 0;
64 while i < self.regions.len() {
65 let mut j = i + 1;
66 while j < self.regions.len() {
67 if intersects_or_touches(self.regions[i], self.regions[j]) {
68 let merged = bounding_rect(self.regions[i], self.regions[j]);
69 self.regions[i] = merged;
70 self.regions.swap_remove(j);
71 } else {
72 j += 1;
73 }
74 }
75 i += 1;
76 }
77 }
78}
79
80impl<const MAX_REGIONS: usize> Default for RegionSet<MAX_REGIONS> {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86pub struct DirtySprite<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize>
92where
93 C: PixelColor + Copy + Default,
94{
95 pixels: [C; N],
96 dirty: RegionSet<MAX_REGIONS>,
97}
98
99impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize>
100 DirtySprite<C, W, H, N, MAX_REGIONS>
101where
102 C: PixelColor + Copy + Default,
103{
104 pub fn new(clear: C) -> Result<Self, DirtySpriteError> {
105 if N < usize::from(W) * usize::from(H) {
106 return Err(DirtySpriteError::BufferTooSmall);
107 }
108
109 Ok(Self {
110 pixels: [clear; N],
111 dirty: RegionSet::new(),
112 })
113 }
114
115 pub fn dirty_regions(&self) -> impl Iterator<Item = Rectangle> + '_ {
116 self.dirty.iter()
117 }
118
119 pub fn clear_dirty(&mut self) {
120 self.dirty.clear();
121 }
122
123 pub fn pixel(&self, point: Point) -> Option<C> {
124 self.index(point).map(|idx| self.pixels[idx])
125 }
126
127 pub fn set_pixel(&mut self, point: Point, color: C) -> Result<(), DirtySpriteError> {
128 if let Some(idx) = self.index(point)
129 && self.pixels[idx] != color
130 {
131 self.pixels[idx] = color;
132 self.dirty.add(Rectangle::new(point, Size::new(1, 1)))?;
133 }
134 Ok(())
135 }
136
137 pub fn flush_dirty<T>(&mut self, target: &mut T) -> Result<(), T::Error>
139 where
140 T: DrawTarget<Color = C>,
141 {
142 for rect in self.dirty.iter() {
143 let clipped = clip_to_bounds(rect, W, H);
144 target.draw_iter(
145 points_in(clipped).filter_map(|point| {
146 self.index(point).map(|idx| Pixel(point, self.pixels[idx]))
147 }),
148 )?;
149 }
150 self.clear_dirty();
151 Ok(())
152 }
153
154 fn index(&self, point: Point) -> Option<usize> {
155 if point.x < 0 || point.y < 0 || point.x >= i32::from(W) || point.y >= i32::from(H) {
156 return None;
157 }
158
159 Some(point.y as usize * usize::from(W) + point.x as usize)
160 }
161}
162
163impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize> OriginDimensions
164 for DirtySprite<C, W, H, N, MAX_REGIONS>
165where
166 C: PixelColor + Copy + Default,
167{
168 fn size(&self) -> Size {
169 Size::new(u32::from(W), u32::from(H))
170 }
171}
172
173impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize> DrawTarget
174 for DirtySprite<C, W, H, N, MAX_REGIONS>
175where
176 C: PixelColor + Copy + Default,
177{
178 type Color = C;
179 type Error = Infallible;
180
181 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
182 where
183 I: IntoIterator<Item = Pixel<Self::Color>>,
184 {
185 for Pixel(point, color) in pixels {
186 let _ = self.set_pixel(point, color);
187 }
188 Ok(())
189 }
190
191 fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
192 self.pixels[..usize::from(W) * usize::from(H)].fill(color);
193 let _ = self.dirty.add(Rectangle::new(Point::zero(), self.size()));
194 Ok(())
195 }
196}
197
198fn clip_to_bounds(rect: Rectangle, width: u16, height: u16) -> Rectangle {
199 let bounds = Rectangle::new(
200 Point::zero(),
201 Size::new(u32::from(width), u32::from(height)),
202 );
203 rect.intersection(&bounds)
204}
205
206fn points_in(rect: Rectangle) -> impl Iterator<Item = Point> {
207 let top_left = rect.top_left;
208 let bottom_right = rect.bottom_right().unwrap_or(top_left);
209 (top_left.y..=bottom_right.y)
210 .flat_map(move |y| (top_left.x..=bottom_right.x).map(move |x| Point::new(x, y)))
211}
212
213fn intersects_or_touches(a: Rectangle, b: Rectangle) -> bool {
214 let a_br = a.bottom_right().unwrap_or(a.top_left);
215 let b_br = b.bottom_right().unwrap_or(b.top_left);
216
217 a.top_left.x <= b_br.x + 1
218 && a_br.x + 1 >= b.top_left.x
219 && a.top_left.y <= b_br.y + 1
220 && a_br.y + 1 >= b.top_left.y
221}
222
223fn bounding_rect(a: Rectangle, b: Rectangle) -> Rectangle {
224 let a_br = a.bottom_right().unwrap_or(a.top_left);
225 let b_br = b.bottom_right().unwrap_or(b.top_left);
226 let min_x = a.top_left.x.min(b.top_left.x);
227 let min_y = a.top_left.y.min(b.top_left.y);
228 let max_x = a_br.x.max(b_br.x);
229 let max_y = a_br.y.max(b_br.y);
230
231 Rectangle::new(
232 Point::new(min_x, min_y),
233 Size::new((max_x - min_x + 1) as u32, (max_y - min_y + 1) as u32),
234 )
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use embedded_graphics::{pixelcolor::Rgb565, prelude::*, primitives::PrimitiveStyle};
241
242 #[test]
243 fn tracks_dirty_regions_for_drawn_shapes() {
244 let mut sprite = DirtySprite::<Rgb565, 8, 8, 64, 8>::new(Rgb565::BLACK).unwrap();
245 Rectangle::new(Point::new(1, 2), Size::new(3, 4))
246 .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE))
247 .draw(&mut sprite)
248 .unwrap();
249
250 let regions: std::vec::Vec<_> = sprite.dirty_regions().collect();
251 assert_eq!(
252 regions,
253 std::vec![Rectangle::new(Point::new(1, 2), Size::new(3, 4))]
254 );
255 }
256
257 #[test]
258 fn rejects_too_small_buffer() {
259 let err = match DirtySprite::<Rgb565, 8, 8, 63, 8>::new(Rgb565::BLACK) {
260 Ok(_) => panic!("expected buffer validation to fail"),
261 Err(err) => err,
262 };
263 assert_eq!(err, DirtySpriteError::BufferTooSmall);
264 }
265}