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 InvalidRegionCapacity,
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 if MAX_REGIONS == 0 {
47 return Err(DirtySpriteError::InvalidRegionCapacity);
48 }
49
50 let mut merged = rect;
51 let mut index = 0;
52 while index < self.regions.len() {
53 if intersects_or_touches(self.regions[index], merged) {
54 merged = bounding_rect(self.regions.remove(index), merged);
55 index = 0;
57 } else {
58 index += 1;
59 }
60 }
61
62 if self.regions.push(merged).is_err() {
63 let mut all = merged;
64 for region in self.regions.iter().copied() {
65 all = bounding_rect(all, region);
66 }
67 self.regions.clear();
68 let _ = self.regions.push(all);
70 }
71 self.regions.as_mut_slice().sort_unstable_by_key(|region| {
72 (
73 region.top_left.y,
74 region.top_left.x,
75 region.size.height,
76 region.size.width,
77 )
78 });
79 Ok(())
80 }
81}
82
83impl<const MAX_REGIONS: usize> Default for RegionSet<MAX_REGIONS> {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89pub struct DirtySprite<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize>
95where
96 C: PixelColor + Copy + Default,
97{
98 pixels: [C; N],
99 dirty: RegionSet<MAX_REGIONS>,
100}
101
102impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize>
103 DirtySprite<C, W, H, N, MAX_REGIONS>
104where
105 C: PixelColor + Copy + Default,
106{
107 pub fn new(clear: C) -> Result<Self, DirtySpriteError> {
108 if N < usize::from(W) * usize::from(H) {
109 return Err(DirtySpriteError::BufferTooSmall);
110 }
111 if MAX_REGIONS == 0 {
112 return Err(DirtySpriteError::InvalidRegionCapacity);
113 }
114
115 Ok(Self {
116 pixels: [clear; N],
117 dirty: RegionSet::new(),
118 })
119 }
120
121 pub fn dirty_regions(&self) -> impl Iterator<Item = Rectangle> + '_ {
122 self.dirty.iter()
123 }
124
125 pub fn clear_dirty(&mut self) {
126 self.dirty.clear();
127 }
128
129 pub fn invalidate(&mut self, area: Rectangle) -> Result<(), DirtySpriteError> {
131 self.dirty.add(clip_to_bounds(area, W, H))
132 }
133
134 pub fn invalidate_all(&mut self) -> Result<(), DirtySpriteError> {
136 self.invalidate(Rectangle::new(Point::zero(), self.size()))
137 }
138
139 pub fn pixel(&self, point: Point) -> Option<C> {
140 self.index(point).map(|idx| self.pixels[idx])
141 }
142
143 pub fn region_pixels(&self, area: Rectangle) -> SpriteRegionPixels<'_, C> {
145 let clipped = clip_to_bounds(area, W, H);
146 SpriteRegionPixels {
147 pixels: &self.pixels[..usize::from(W) * usize::from(H)],
148 stride: usize::from(W),
149 x: clipped.top_left.x.max(0) as usize,
150 y: clipped.top_left.y.max(0) as usize,
151 width: clipped.size.width as usize,
152 height: clipped.size.height as usize,
153 current_x: 0,
154 current_y: 0,
155 }
156 }
157
158 pub fn draw_region_at<T>(
160 &self,
161 target: &mut T,
162 source_area: Rectangle,
163 dest_top_left: Point,
164 ) -> Result<(), T::Error>
165 where
166 T: DrawTarget<Color = C>,
167 {
168 let clipped = clip_to_bounds(source_area, W, H);
169 if clipped.is_zero_sized() {
170 return Ok(());
171 }
172
173 let offset = clipped.top_left - source_area.top_left;
174 let target_area = Rectangle::new(dest_top_left + offset, clipped.size);
175 target.fill_contiguous(&target_area, self.region_pixels(clipped))
176 }
177
178 pub fn set_pixel(&mut self, point: Point, color: C) -> Result<(), DirtySpriteError> {
179 if let Some(idx) = self.index(point)
180 && self.pixels[idx] != color
181 {
182 self.dirty.add(Rectangle::new(point, Size::new(1, 1)))?;
183 self.pixels[idx] = color;
184 }
185 Ok(())
186 }
187
188 pub fn flush_dirty<T>(&mut self, target: &mut T) -> Result<(), T::Error>
190 where
191 T: DrawTarget<Color = C>,
192 {
193 self.flush_dirty_at(target, Point::zero())
194 }
195
196 pub fn flush_dirty_at<T>(&mut self, target: &mut T, origin: Point) -> Result<(), T::Error>
198 where
199 T: DrawTarget<Color = C>,
200 {
201 let mut regions = [None; MAX_REGIONS];
202 for (slot, region) in regions.iter_mut().zip(self.dirty.iter()) {
203 *slot = Some(region);
204 }
205
206 for region in regions.into_iter().flatten() {
207 self.draw_region_at(target, region, origin + region.top_left)?;
208 }
209 self.clear_dirty();
210 Ok(())
211 }
212
213 fn index(&self, point: Point) -> Option<usize> {
214 if point.x < 0 || point.y < 0 || point.x >= i32::from(W) || point.y >= i32::from(H) {
215 return None;
216 }
217
218 Some(point.y as usize * usize::from(W) + point.x as usize)
219 }
220}
221
222impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize> OriginDimensions
223 for DirtySprite<C, W, H, N, MAX_REGIONS>
224where
225 C: PixelColor + Copy + Default,
226{
227 fn size(&self) -> Size {
228 Size::new(u32::from(W), u32::from(H))
229 }
230}
231
232impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize> DrawTarget
233 for DirtySprite<C, W, H, N, MAX_REGIONS>
234where
235 C: PixelColor + Copy + Default,
236{
237 type Color = C;
238 type Error = Infallible;
239
240 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
241 where
242 I: IntoIterator<Item = Pixel<Self::Color>>,
243 {
244 for Pixel(point, color) in pixels {
245 let _ = self.set_pixel(point, color);
246 }
247 Ok(())
248 }
249
250 fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
251 let _ = self.invalidate_all();
253 self.pixels[..usize::from(W) * usize::from(H)].fill(color);
254 Ok(())
255 }
256}
257
258pub struct SpriteRegionPixels<'a, C>
260where
261 C: PixelColor + Copy + Default,
262{
263 pixels: &'a [C],
264 stride: usize,
265 x: usize,
266 y: usize,
267 width: usize,
268 height: usize,
269 current_x: usize,
270 current_y: usize,
271}
272
273impl<C> Iterator for SpriteRegionPixels<'_, C>
274where
275 C: PixelColor + Copy + Default,
276{
277 type Item = C;
278
279 fn next(&mut self) -> Option<Self::Item> {
280 if self.current_y >= self.height {
281 return None;
282 }
283
284 let index = (self.y + self.current_y) * self.stride + self.x + self.current_x;
285 let color = self.pixels.get(index).copied();
286 self.current_x += 1;
287 if self.current_x >= self.width {
288 self.current_x = 0;
289 self.current_y += 1;
290 }
291 color
292 }
293}
294
295fn clip_to_bounds(rect: Rectangle, width: u16, height: u16) -> Rectangle {
296 let bounds = Rectangle::new(
297 Point::zero(),
298 Size::new(u32::from(width), u32::from(height)),
299 );
300 rect.intersection(&bounds)
301}
302
303fn intersects_or_touches(a: Rectangle, b: Rectangle) -> bool {
304 let a_br = a.bottom_right().unwrap_or(a.top_left);
305 let b_br = b.bottom_right().unwrap_or(b.top_left);
306
307 a.top_left.x <= b_br.x + 1
308 && a_br.x + 1 >= b.top_left.x
309 && a.top_left.y <= b_br.y + 1
310 && a_br.y + 1 >= b.top_left.y
311}
312
313fn bounding_rect(a: Rectangle, b: Rectangle) -> Rectangle {
314 let a_br = a.bottom_right().unwrap_or(a.top_left);
315 let b_br = b.bottom_right().unwrap_or(b.top_left);
316 let min_x = a.top_left.x.min(b.top_left.x);
317 let min_y = a.top_left.y.min(b.top_left.y);
318 let max_x = a_br.x.max(b_br.x);
319 let max_y = a_br.y.max(b_br.y);
320
321 Rectangle::new(
322 Point::new(min_x, min_y),
323 Size::new((max_x - min_x + 1) as u32, (max_y - min_y + 1) as u32),
324 )
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use embedded_graphics::{pixelcolor::Rgb565, prelude::*, primitives::PrimitiveStyle};
331
332 fn rect(x: i32, y: i32, width: u32, height: u32) -> Rectangle {
333 Rectangle::new(Point::new(x, y), Size::new(width, height))
334 }
335
336 fn regions<const N: usize>(set: &RegionSet<N>) -> std::vec::Vec<Rectangle> {
337 set.iter().collect()
338 }
339
340 #[test]
341 fn keeps_non_overlapping_regions_separate() {
342 let mut set = RegionSet::<4>::new();
343 set.add(rect(0, 0, 1, 1)).unwrap();
344 set.add(rect(3, 3, 1, 1)).unwrap();
345 assert_eq!(regions(&set), std::vec![rect(0, 0, 1, 1), rect(3, 3, 1, 1)]);
346 }
347
348 #[test]
349 fn merges_touching_intersecting_and_transitively_connected_regions() {
350 let mut set = RegionSet::<5>::new();
351 set.add(rect(0, 0, 2, 2)).unwrap();
352 set.add(rect(5, 0, 2, 2)).unwrap();
353 set.add(rect(1, 1, 2, 2)).unwrap();
354 assert_eq!(regions(&set), std::vec![rect(0, 0, 3, 3), rect(5, 0, 2, 2)]);
355
356 set.add(rect(3, 1, 2, 1)).unwrap();
357 assert_eq!(regions(&set), std::vec![rect(0, 0, 7, 3)]);
358 }
359
360 #[test]
361 fn overflow_collapses_every_region_and_new_rectangle_to_a_bounding_box() {
362 let mut set = RegionSet::<2>::new();
363 set.add(rect(0, 1, 1, 1)).unwrap();
364 set.add(rect(4, 4, 1, 1)).unwrap();
365 set.add(rect(8, 0, 2, 1)).unwrap();
366 assert_eq!(regions(&set), std::vec![rect(0, 0, 10, 5)]);
367 }
368
369 #[test]
370 fn rejects_zero_region_capacity() {
371 let err = match DirtySprite::<Rgb565, 2, 2, 4, 0>::new(Rgb565::BLACK) {
372 Ok(_) => panic!("expected region capacity validation to fail"),
373 Err(err) => err,
374 };
375 assert_eq!(err, DirtySpriteError::InvalidRegionCapacity);
376 assert_eq!(
377 RegionSet::<0>::new().add(rect(0, 0, 1, 1)),
378 Err(DirtySpriteError::InvalidRegionCapacity)
379 );
380 }
381
382 #[test]
383 fn invalidate_clips_every_edge_and_ignores_outside_and_empty_areas() {
384 let cases = [
385 (rect(-2, 1, 4, 2), Some(rect(0, 1, 2, 2))),
386 (rect(3, 1, 4, 2), Some(rect(3, 1, 1, 2))),
387 (rect(1, -2, 2, 4), Some(rect(1, 0, 2, 2))),
388 (rect(1, 3, 2, 4), Some(rect(1, 3, 2, 1))),
389 (rect(-5, -5, 2, 2), None),
390 (rect(1, 1, 0, 3), None),
391 ];
392
393 for (area, expected) in cases {
394 let mut sprite = DirtySprite::<Rgb565, 4, 4, 16, 4>::new(Rgb565::BLACK).unwrap();
395 sprite.invalidate(area).unwrap();
396 assert_eq!(
397 sprite.dirty_regions().collect::<std::vec::Vec<_>>(),
398 expected.into_iter().collect::<std::vec::Vec<_>>()
399 );
400 }
401
402 let mut sprite = DirtySprite::<Rgb565, 4, 4, 16, 1>::new(Rgb565::BLACK).unwrap();
403 sprite.invalidate_all().unwrap();
404 assert_eq!(
405 sprite.dirty_regions().collect::<std::vec::Vec<_>>(),
406 std::vec![rect(0, 0, 4, 4)]
407 );
408 }
409
410 #[test]
411 fn merging_is_deterministic_across_insertion_orders() {
412 let inputs = [rect(0, 0, 2, 2), rect(5, 0, 2, 2), rect(2, 0, 3, 2)];
413 let mut forward = RegionSet::<4>::new();
414 let mut reverse = RegionSet::<4>::new();
415 for area in inputs {
416 forward.add(area).unwrap();
417 }
418 for area in inputs.into_iter().rev() {
419 reverse.add(area).unwrap();
420 }
421 assert_eq!(regions(&forward), std::vec![rect(0, 0, 7, 2)]);
422 assert_eq!(regions(&forward), regions(&reverse));
423 }
424
425 #[test]
426 fn tracks_dirty_regions_for_drawn_shapes() {
427 let mut sprite = DirtySprite::<Rgb565, 8, 8, 64, 8>::new(Rgb565::BLACK).unwrap();
428 Rectangle::new(Point::new(1, 2), Size::new(3, 4))
429 .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE))
430 .draw(&mut sprite)
431 .unwrap();
432
433 assert_eq!(
434 sprite.dirty_regions().collect::<std::vec::Vec<_>>(),
435 std::vec![rect(1, 2, 3, 4)]
436 );
437 }
438
439 #[test]
440 fn rejects_too_small_buffer() {
441 let err = match DirtySprite::<Rgb565, 8, 8, 63, 8>::new(Rgb565::BLACK) {
442 Ok(_) => panic!("expected buffer validation to fail"),
443 Err(err) => err,
444 };
445 assert_eq!(err, DirtySpriteError::BufferTooSmall);
446 }
447
448 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
449 enum TargetError {
450 WriteFailed,
451 }
452
453 struct TestTarget {
454 fail_on_call: Option<usize>,
455 calls: usize,
456 pixels: std::vec::Vec<Pixel<Rgb565>>,
457 }
458
459 impl TestTarget {
460 fn successful() -> Self {
461 Self {
462 fail_on_call: None,
463 calls: 0,
464 pixels: std::vec::Vec::new(),
465 }
466 }
467 }
468
469 impl OriginDimensions for TestTarget {
470 fn size(&self) -> Size {
471 Size::new(8, 8)
472 }
473 }
474
475 impl DrawTarget for TestTarget {
476 type Color = Rgb565;
477 type Error = TargetError;
478
479 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
480 where
481 I: IntoIterator<Item = Pixel<Self::Color>>,
482 {
483 self.calls += 1;
484 if self.fail_on_call == Some(self.calls) {
485 return Err(TargetError::WriteFailed);
486 }
487 self.pixels.extend(pixels);
488 Ok(())
489 }
490 }
491
492 #[test]
493 fn successful_flush_clears_dirty_state() {
494 let mut sprite = DirtySprite::<Rgb565, 4, 4, 16, 4>::new(Rgb565::BLACK).unwrap();
495 sprite.set_pixel(Point::new(1, 1), Rgb565::WHITE).unwrap();
496 let mut target = TestTarget::successful();
497
498 sprite.flush_dirty(&mut target).unwrap();
499
500 assert!(sprite.dirty_regions().next().is_none());
501 assert_eq!(
502 target.pixels,
503 std::vec![Pixel(Point::new(1, 1), Rgb565::WHITE)]
504 );
505 }
506
507 #[test]
508 fn failed_flush_preserves_all_dirty_state_for_retry() {
509 let mut sprite = DirtySprite::<Rgb565, 4, 4, 16, 4>::new(Rgb565::BLACK).unwrap();
510 sprite.set_pixel(Point::new(0, 0), Rgb565::WHITE).unwrap();
511 sprite.set_pixel(Point::new(3, 3), Rgb565::WHITE).unwrap();
512 let expected = sprite.dirty_regions().collect::<std::vec::Vec<_>>();
513 let mut failing = TestTarget {
514 fail_on_call: Some(2),
515 calls: 0,
516 pixels: std::vec::Vec::new(),
517 };
518
519 assert_eq!(
520 sprite.flush_dirty(&mut failing),
521 Err(TargetError::WriteFailed)
522 );
523 assert_eq!(
524 sprite.dirty_regions().collect::<std::vec::Vec<_>>(),
525 expected
526 );
527
528 let mut retry = TestTarget::successful();
529 sprite.flush_dirty(&mut retry).unwrap();
530 assert!(sprite.dirty_regions().next().is_none());
531 assert_eq!(retry.pixels.len(), 2);
532 }
533}