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 region_pixels(&self, area: Rectangle) -> SpriteRegionPixels<'_, C> {
129 let clipped = clip_to_bounds(area, W, H);
130 SpriteRegionPixels {
131 pixels: &self.pixels[..usize::from(W) * usize::from(H)],
132 stride: usize::from(W),
133 x: clipped.top_left.x.max(0) as usize,
134 y: clipped.top_left.y.max(0) as usize,
135 width: clipped.size.width as usize,
136 height: clipped.size.height as usize,
137 current_x: 0,
138 current_y: 0,
139 }
140 }
141
142 pub fn draw_region_at<T>(
144 &self,
145 target: &mut T,
146 source_area: Rectangle,
147 dest_top_left: Point,
148 ) -> Result<(), T::Error>
149 where
150 T: DrawTarget<Color = C>,
151 {
152 let clipped = clip_to_bounds(source_area, W, H);
153 if clipped.is_zero_sized() {
154 return Ok(());
155 }
156
157 let offset = clipped.top_left - source_area.top_left;
158 let target_area = Rectangle::new(dest_top_left + offset, clipped.size);
159 target.fill_contiguous(&target_area, self.region_pixels(clipped))
160 }
161
162 pub fn set_pixel(&mut self, point: Point, color: C) -> Result<(), DirtySpriteError> {
163 if let Some(idx) = self.index(point)
164 && self.pixels[idx] != color
165 {
166 self.pixels[idx] = color;
167 self.dirty.add(Rectangle::new(point, Size::new(1, 1)))?;
168 }
169 Ok(())
170 }
171
172 pub fn flush_dirty<T>(&mut self, target: &mut T) -> Result<(), T::Error>
174 where
175 T: DrawTarget<Color = C>,
176 {
177 self.flush_dirty_at(target, Point::zero())
178 }
179
180 pub fn flush_dirty_at<T>(&mut self, target: &mut T, origin: Point) -> Result<(), T::Error>
182 where
183 T: DrawTarget<Color = C>,
184 {
185 let mut regions = [None; MAX_REGIONS];
186 for (slot, region) in regions.iter_mut().zip(self.dirty.iter()) {
187 *slot = Some(region);
188 }
189
190 for region in regions.into_iter().flatten() {
191 self.draw_region_at(target, region, origin + region.top_left)?;
192 }
193 self.clear_dirty();
194 Ok(())
195 }
196
197 fn index(&self, point: Point) -> Option<usize> {
198 if point.x < 0 || point.y < 0 || point.x >= i32::from(W) || point.y >= i32::from(H) {
199 return None;
200 }
201
202 Some(point.y as usize * usize::from(W) + point.x as usize)
203 }
204}
205
206impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize> OriginDimensions
207 for DirtySprite<C, W, H, N, MAX_REGIONS>
208where
209 C: PixelColor + Copy + Default,
210{
211 fn size(&self) -> Size {
212 Size::new(u32::from(W), u32::from(H))
213 }
214}
215
216impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize> DrawTarget
217 for DirtySprite<C, W, H, N, MAX_REGIONS>
218where
219 C: PixelColor + Copy + Default,
220{
221 type Color = C;
222 type Error = Infallible;
223
224 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
225 where
226 I: IntoIterator<Item = Pixel<Self::Color>>,
227 {
228 for Pixel(point, color) in pixels {
229 let _ = self.set_pixel(point, color);
230 }
231 Ok(())
232 }
233
234 fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
235 self.pixels[..usize::from(W) * usize::from(H)].fill(color);
236 let _ = self.dirty.add(Rectangle::new(Point::zero(), self.size()));
237 Ok(())
238 }
239}
240
241pub struct SpriteRegionPixels<'a, C>
243where
244 C: PixelColor + Copy + Default,
245{
246 pixels: &'a [C],
247 stride: usize,
248 x: usize,
249 y: usize,
250 width: usize,
251 height: usize,
252 current_x: usize,
253 current_y: usize,
254}
255
256impl<C> Iterator for SpriteRegionPixels<'_, C>
257where
258 C: PixelColor + Copy + Default,
259{
260 type Item = C;
261
262 fn next(&mut self) -> Option<Self::Item> {
263 if self.current_y >= self.height {
264 return None;
265 }
266
267 let index = (self.y + self.current_y) * self.stride + self.x + self.current_x;
268 let color = self.pixels.get(index).copied();
269 self.current_x += 1;
270 if self.current_x >= self.width {
271 self.current_x = 0;
272 self.current_y += 1;
273 }
274 color
275 }
276}
277
278fn clip_to_bounds(rect: Rectangle, width: u16, height: u16) -> Rectangle {
279 let bounds = Rectangle::new(
280 Point::zero(),
281 Size::new(u32::from(width), u32::from(height)),
282 );
283 rect.intersection(&bounds)
284}
285
286fn intersects_or_touches(a: Rectangle, b: Rectangle) -> bool {
287 let a_br = a.bottom_right().unwrap_or(a.top_left);
288 let b_br = b.bottom_right().unwrap_or(b.top_left);
289
290 a.top_left.x <= b_br.x + 1
291 && a_br.x + 1 >= b.top_left.x
292 && a.top_left.y <= b_br.y + 1
293 && a_br.y + 1 >= b.top_left.y
294}
295
296fn bounding_rect(a: Rectangle, b: Rectangle) -> Rectangle {
297 let a_br = a.bottom_right().unwrap_or(a.top_left);
298 let b_br = b.bottom_right().unwrap_or(b.top_left);
299 let min_x = a.top_left.x.min(b.top_left.x);
300 let min_y = a.top_left.y.min(b.top_left.y);
301 let max_x = a_br.x.max(b_br.x);
302 let max_y = a_br.y.max(b_br.y);
303
304 Rectangle::new(
305 Point::new(min_x, min_y),
306 Size::new((max_x - min_x + 1) as u32, (max_y - min_y + 1) as u32),
307 )
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use embedded_graphics::{pixelcolor::Rgb565, prelude::*, primitives::PrimitiveStyle};
314
315 #[test]
316 fn tracks_dirty_regions_for_drawn_shapes() {
317 let mut sprite = DirtySprite::<Rgb565, 8, 8, 64, 8>::new(Rgb565::BLACK).unwrap();
318 Rectangle::new(Point::new(1, 2), Size::new(3, 4))
319 .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE))
320 .draw(&mut sprite)
321 .unwrap();
322
323 let regions: std::vec::Vec<_> = sprite.dirty_regions().collect();
324 assert_eq!(
325 regions,
326 std::vec![Rectangle::new(Point::new(1, 2), Size::new(3, 4))]
327 );
328 }
329
330 #[test]
331 fn rejects_too_small_buffer() {
332 let err = match DirtySprite::<Rgb565, 8, 8, 63, 8>::new(Rgb565::BLACK) {
333 Ok(_) => panic!("expected buffer validation to fail"),
334 Err(err) => err,
335 };
336 assert_eq!(err, DirtySpriteError::BufferTooSmall);
337 }
338}