1use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct Frame {
13 pub width: u32,
14 pub height: u32,
15 data: Vec<u8>,
17}
18
19impl Frame {
20 #[must_use]
21 pub fn black(width: u32, height: u32) -> Self {
22 Self {
23 width,
24 height,
25 data: vec![0; (width as usize) * (height as usize) * 3],
26 }
27 }
28
29 pub fn from_rgb(width: u32, height: u32, data: Vec<u8>) -> Result<Self, FrameError> {
34 let want = (width as usize) * (height as usize) * 3;
35 if data.len() == want {
36 Ok(Self {
37 width,
38 height,
39 data,
40 })
41 } else {
42 Err(FrameError::WrongSize {
43 got: data.len(),
44 want,
45 })
46 }
47 }
48
49 #[inline]
51 #[must_use]
52 pub fn as_bytes(&self) -> &[u8] {
53 &self.data
54 }
55
56 #[inline]
58 pub fn as_bytes_mut(&mut self) -> &mut [u8] {
59 &mut self.data
60 }
61
62 #[inline]
63 #[must_use]
64 pub fn into_bytes(self) -> Vec<u8> {
65 self.data
66 }
67
68 #[inline]
70 #[must_use]
71 pub fn row(&self, y: u32) -> &[[u8; 3]] {
72 let stride = (self.width as usize) * 3;
73 let start = (y as usize) * stride;
74 self.data[start..start + stride].as_chunks::<3>().0
75 }
76
77 #[inline]
79 pub fn row_mut(&mut self, y: u32) -> &mut [[u8; 3]] {
80 let stride = (self.width as usize) * 3;
81 let start = (y as usize) * stride;
82 self.data[start..start + stride].as_chunks_mut::<3>().0
83 }
84
85 pub fn rows(&self) -> impl Iterator<Item = &[[u8; 3]]> + '_ {
87 (0..self.height).map(|y| self.row(y))
88 }
89
90 #[inline]
92 #[must_use]
93 pub fn pixel(&self, x: u32, y: u32) -> [u8; 3] {
94 if x >= self.width || y >= self.height {
95 return [0; 3];
96 }
97 self.row(y)[x as usize]
98 }
99
100 #[inline]
102 pub fn set_pixel(&mut self, x: u32, y: u32, px: [u8; 3]) {
103 if x >= self.width || y >= self.height {
104 return;
105 }
106 self.row_mut(y)[x as usize] = px;
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum FrameError {
112 WrongSize { got: usize, want: usize },
113}
114
115impl std::fmt::Display for FrameError {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 match self {
118 Self::WrongSize { got, want } => {
119 write!(f, "frame data is {got} bytes, expected {want}")
120 }
121 }
122 }
123}
124
125impl std::error::Error for FrameError {}
126
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
129#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
130#[serde(rename_all = "kebab-case")]
131pub enum Rotation {
132 #[default]
133 None,
134 Cw90,
136 Ccw90,
138 Rot180,
139}
140
141#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
143#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
144pub struct Panel {
145 pub receiver: u16,
147 #[serde(default)]
149 pub receiver_x: u32,
150 #[serde(default)]
151 pub receiver_y: u32,
152 pub x: u32,
154 pub y: u32,
155 pub width: u32,
157 pub height: u32,
158 #[serde(default)]
159 pub rotation: Rotation,
160 #[serde(default)]
161 pub flip_x: bool,
162 #[serde(default)]
163 pub flip_y: bool,
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
169struct Placement {
170 origin: (i64, i64),
171 col_step: (i64, i64),
172 row_step: (i64, i64),
173}
174
175impl Placement {
176 const fn is_row_copy(self) -> bool {
179 matches!(self.col_step, (1, 0)) && matches!(self.row_step, (0, 1))
180 }
181}
182
183impl Panel {
184 fn receiver_coords(&self, local_x: u32, local_y: u32) -> (u32, u32) {
187 let (max_x, max_y) = (self.width - 1, self.height - 1);
188 let lx = if self.flip_x { max_x - local_x } else { local_x };
190 let ly = if self.flip_y { max_y - local_y } else { local_y };
191 let (px, py) = match self.rotation {
192 Rotation::None => (lx, ly),
193 Rotation::Cw90 => (ly, max_x - lx),
194 Rotation::Ccw90 => (max_y - ly, lx),
195 Rotation::Rot180 => (max_x - lx, max_y - ly),
196 };
197 (self.receiver_x + px, self.receiver_y + py)
198 }
199
200 fn screen_coords(&self, receiver: (u32, u32), local_x: u32, local_y: u32) -> (u32, u32) {
203 let (px, py) = self.receiver_coords(local_x, local_y);
204 (receiver.0 + px, receiver.1 + py)
205 }
206
207 fn placement(&self, receiver: (u32, u32)) -> Placement {
210 let at = |x, y| {
211 let (sx, sy) = self.screen_coords(receiver, x, y);
212 (i64::from(sx), i64::from(sy))
213 };
214 let origin = at(0, 0);
215 let step = |p: (i64, i64)| (p.0 - origin.0, p.1 - origin.1);
216 let col_step = if self.width > 1 { step(at(1, 0)) } else { (0, 0) };
219 let row_step = if self.height > 1 { step(at(0, 1)) } else { (0, 0) };
220 Placement {
221 origin,
222 col_step,
223 row_step,
224 }
225 }
226
227 const fn native_size(&self) -> (u32, u32) {
229 match self.rotation {
230 Rotation::None | Rotation::Rot180 => (self.width, self.height),
231 Rotation::Cw90 | Rotation::Ccw90 => (self.height, self.width),
232 }
233 }
234
235 fn blit(&self, receiver: (u32, u32), src: &Frame, dst: &mut Frame) {
239 if self.width == 0 || self.height == 0 {
240 return;
241 }
242 let place = self.placement(receiver);
243 if place.is_row_copy() {
244 self.blit_rows(src, dst, place.origin);
245 return;
246 }
247 let (ox, oy) = place.origin;
248 let (cx, cy) = place.col_step;
249 let (rx, ry) = place.row_step;
250 for ly in 0..self.height {
251 let sy = self.y + ly;
252 let (mut x, mut y) = (ox + i64::from(ly) * rx, oy + i64::from(ly) * ry);
253 for lx in 0..self.width {
254 let px = src.pixel(self.x + lx, sy);
255 dst.set_pixel(x as u32, y as u32, px);
256 x += cx;
257 y += cy;
258 }
259 }
260 }
261
262 fn blit_rows(&self, src: &Frame, dst: &mut Frame, origin: (i64, i64)) {
263 let (rx, ry) = (origin.0 as u32, origin.1 as u32);
264 let dst_w = self.width.min(dst.width.saturating_sub(rx)) as usize;
266 let dst_rows = self.height.min(dst.height.saturating_sub(ry));
267 let src_w = dst_w.min(src.width.saturating_sub(self.x) as usize);
268 let src_rows = dst_rows.min(src.height.saturating_sub(self.y));
269 let (sx, dx) = (self.x as usize, rx as usize);
270 for ly in 0..dst_rows {
271 let row = &mut dst.row_mut(ry + ly)[dx..dx + dst_w];
272 if ly < src_rows {
273 row[..src_w].copy_from_slice(&src.row(self.y + ly)[sx..sx + src_w]);
274 row[src_w..].fill([0; 3]);
275 } else {
276 row.fill([0; 3]);
277 }
278 }
279 }
280}
281
282#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
284#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
285pub struct Receiver {
286 pub index: u16,
287 #[serde(default)]
291 pub x: u32,
292 #[serde(default)]
293 pub y: u32,
294 pub width: u32,
295 pub height: u32,
296}
297
298#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
300#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
301pub struct Canvas {
302 pub width: u32,
303 pub height: u32,
304 pub receivers: Vec<Receiver>,
305 pub panels: Vec<Panel>,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct LayoutError(pub Vec<String>);
311
312impl std::fmt::Display for LayoutError {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 write!(f, "canvas is not valid:\n {}", self.0.join("\n "))
315 }
316}
317
318impl std::error::Error for LayoutError {}
319
320impl Canvas {
321 #[must_use]
323 pub fn single(width: u32, height: u32) -> Self {
324 Self::grid(width, height, 1, 1)
325 }
326
327 #[must_use]
329 pub fn grid(panel_w: u32, panel_h: u32, cols: u32, rows: u32) -> Self {
330 let (width, height) = (panel_w * cols, panel_h * rows);
331 let panels = (0..rows)
332 .flat_map(|row| {
333 (0..cols).map(move |col| Panel {
334 receiver: 0,
335 receiver_x: col * panel_w,
336 receiver_y: row * panel_h,
337 x: col * panel_w,
338 y: row * panel_h,
339 width: panel_w,
340 height: panel_h,
341 rotation: Rotation::None,
342 flip_x: false,
343 flip_y: false,
344 })
345 })
346 .collect();
347 Self {
348 width,
349 height,
350 receivers: vec![Receiver {
351 index: 0,
352 x: 0,
353 y: 0,
354 width,
355 height,
356 }],
357 panels,
358 }
359 }
360
361 #[must_use]
364 pub fn cards(panel_w: u32, panel_h: u32, cols: u32, rows: u32) -> Self {
365 let mut canvas = Self::grid(panel_w, panel_h, cols, rows);
366 canvas.receivers = canvas
367 .panels
368 .iter()
369 .enumerate()
370 .map(|(i, p)| Receiver {
371 index: i as u16,
372 x: p.x,
373 y: p.y,
374 width: panel_w,
375 height: panel_h,
376 })
377 .collect();
378 for (i, p) in canvas.panels.iter_mut().enumerate() {
379 p.receiver = i as u16;
380 p.receiver_x = 0;
381 p.receiver_y = 0;
382 }
383 canvas
384 }
385
386 pub fn validate(&self) -> Result<(), LayoutError> {
392 let mut problems = Vec::new();
393 for r in &self.receivers {
394 if r.x + r.width > self.width || r.y + r.height > self.height {
395 problems.push(format!(
396 "receiver {} at ({}, {}) size {}x{} extends past the {}x{} canvas",
397 r.index, r.x, r.y, r.width, r.height, self.width, self.height
398 ));
399 }
400 }
401 for (i, p) in self.panels.iter().enumerate() {
402 if p.x + p.width > self.width || p.y + p.height > self.height {
403 problems.push(format!(
404 "panel {i} at ({}, {}) size {}x{} extends past the {}x{} canvas",
405 p.x, p.y, p.width, p.height, self.width, self.height
406 ));
407 }
408 let Some(r) = self.receivers.iter().find(|r| r.index == p.receiver) else {
409 problems.push(format!(
410 "panel {i} names receiver {}, which is not defined",
411 p.receiver
412 ));
413 continue;
414 };
415 let (nw, nh) = p.native_size();
416 if p.receiver_x + nw > r.width || p.receiver_y + nh > r.height {
417 problems.push(format!(
418 "panel {i} occupies ({}, {}) size {nw}x{nh} on receiver {}, which is only {}x{}",
419 p.receiver_x, p.receiver_y, r.index, r.width, r.height
420 ));
421 }
422 }
423 if problems.is_empty() {
424 Ok(())
425 } else {
426 Err(LayoutError(problems))
427 }
428 }
429
430 #[must_use]
432 pub fn screen_frame(&self) -> Frame {
433 Frame::black(self.width, self.height)
434 }
435
436 #[must_use]
439 pub fn render(&self, src: &Frame) -> Frame {
440 let mut out = self.screen_frame();
441 self.render_into(src, &mut out);
442 out
443 }
444
445 pub fn render_into(&self, src: &Frame, out: &mut Frame) {
449 if (out.width, out.height) == (self.width, self.height) {
450 out.data.fill(0);
451 } else {
452 *out = self.screen_frame();
453 }
454 for panel in &self.panels {
455 let Some(r) = self.receivers.iter().find(|r| r.index == panel.receiver) else {
456 continue;
457 };
458 panel.blit((r.x, r.y), src, out);
459 }
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 fn gradient(w: u32, h: u32) -> Frame {
468 let mut f = Frame::black(w, h);
469 for y in 0..h {
470 for x in 0..w {
471 f.set_pixel(x, y, [x as u8, y as u8, 0]);
472 }
473 }
474 f
475 }
476
477 fn render_per_pixel(canvas: &Canvas, src: &Frame) -> Frame {
479 let mut out = canvas.screen_frame();
480 for panel in &canvas.panels {
481 let Some(r) = canvas.receivers.iter().find(|r| r.index == panel.receiver) else {
482 continue;
483 };
484 for ly in 0..panel.height {
485 for lx in 0..panel.width {
486 let px = src.pixel(panel.x + lx, panel.y + ly);
487 let (sx, sy) = panel.screen_coords((r.x, r.y), lx, ly);
488 out.set_pixel(sx, sy, px);
489 }
490 }
491 }
492 out
493 }
494
495 #[test]
496 fn a_single_panel_passes_the_image_through_unchanged() {
497 let canvas = Canvas::single(8, 4);
498 let src = gradient(8, 4);
499 assert_eq!(canvas.render(&src), src);
500 }
501
502 #[test]
503 fn single_is_a_one_by_one_grid() {
504 assert_eq!(Canvas::single(8, 4), Canvas::grid(8, 4, 1, 1));
505 }
506
507 #[test]
508 fn a_grid_tiles_panels_across_the_canvas() {
509 let canvas = Canvas::grid(4, 2, 2, 2);
510 assert_eq!((canvas.width, canvas.height), (8, 4));
511 assert_eq!(canvas.panels.len(), 4);
512 canvas.validate().unwrap();
513 assert_eq!(canvas.render(&gradient(8, 4)), gradient(8, 4));
514 }
515
516 #[test]
517 fn cards_put_one_receiver_under_each_panel_at_its_screen_position() {
518 let canvas = Canvas::cards(4, 2, 3, 2);
519 canvas.validate().unwrap();
520 assert_eq!(canvas.receivers.len(), 6);
521 let r = canvas.receivers[4];
522 assert_eq!((r.index, r.x, r.y, r.width, r.height), (4, 4, 2, 4, 2));
523 let p = &canvas.panels[4];
524 assert_eq!((p.receiver, p.receiver_x, p.receiver_y, p.x, p.y), (4, 0, 0, 4, 2));
525 assert_eq!(canvas.render(&gradient(12, 4)), gradient(12, 4));
526 }
527
528 #[test]
529 fn a_receiver_position_defaults_to_the_origin_in_layout_files() {
530 let r: Receiver = serde_json::from_str(r#"{"index":3,"width":8,"height":4}"#).unwrap();
531 assert_eq!((r.x, r.y), (0, 0));
532 }
533
534 #[test]
535 fn rows_are_contiguous_pixel_slices() {
536 let f = gradient(4, 2);
537 assert_eq!(f.row(1), &[[0, 1, 0], [1, 1, 0], [2, 1, 0], [3, 1, 0]]);
538 assert_eq!(f.rows().count(), 2);
539 assert_eq!(f.as_bytes().len(), 4 * 2 * 3);
540 assert_eq!(f.clone().into_bytes(), f.as_bytes());
541 }
542
543 #[test]
544 fn pixels_off_the_frame_read_black_and_ignore_writes() {
545 let mut f = gradient(4, 2);
546 assert_eq!(f.pixel(4, 0), [0; 3]);
547 assert_eq!(f.pixel(0, 2), [0; 3]);
548 f.set_pixel(4, 0, [9; 3]);
549 f.set_pixel(0, 2, [9; 3]);
550 assert_eq!(f, gradient(4, 2));
551 }
552
553 #[test]
554 fn rotation_maps_corners_where_expected() {
555 let mut canvas = Canvas {
557 width: 2,
558 height: 4,
559 receivers: vec![Receiver {
560 index: 0,
561 x: 0,
562 y: 0,
563 width: 4,
564 height: 2,
565 }],
566 panels: vec![Panel {
567 receiver: 0,
568 receiver_x: 0,
569 receiver_y: 0,
570 x: 0,
571 y: 0,
572 width: 2,
573 height: 4,
574 rotation: Rotation::Cw90,
575 flip_x: false,
576 flip_y: false,
577 }],
578 };
579 assert!(canvas.validate().is_err());
581 canvas.width = 4;
582
583 let mut src = Frame::black(2, 4);
584 src.set_pixel(0, 0, [255, 0, 0]); let out = canvas.render(&src);
586 assert_eq!(out.pixel(0, 1), [255, 0, 0]);
588 }
589
590 #[test]
591 fn flipping_mirrors_the_image() {
592 let mut canvas = Canvas::single(4, 1);
593 canvas.panels[0].flip_x = true;
594 let mut src = Frame::black(4, 1);
595 src.set_pixel(0, 0, [1, 2, 3]);
596 assert_eq!(canvas.render(&src).pixel(3, 0), [1, 2, 3]);
597 }
598
599 #[test]
600 fn every_mounting_matches_the_per_pixel_mapping() {
601 let src = gradient(23, 17);
604 let rotations = [
605 Rotation::None,
606 Rotation::Cw90,
607 Rotation::Ccw90,
608 Rotation::Rot180,
609 ];
610 for rotation in rotations {
611 for (flip_x, flip_y) in [(false, false), (true, false), (false, true), (true, true)] {
612 let (w, h) = (7, 5);
613 let (nw, nh) = match rotation {
614 Rotation::None | Rotation::Rot180 => (w, h),
615 Rotation::Cw90 | Rotation::Ccw90 => (h, w),
616 };
617 let panel = |receiver, receiver_x, receiver_y, x, y| Panel {
618 receiver,
619 receiver_x,
620 receiver_y,
621 x,
622 y,
623 width: w,
624 height: h,
625 rotation,
626 flip_x,
627 flip_y,
628 };
629 let canvas = Canvas {
630 width: 23,
631 height: 17,
632 receivers: vec![
633 Receiver {
634 index: 0,
635 x: 0,
636 y: 0,
637 width: nw + 3,
638 height: nh + 2,
639 },
640 Receiver {
641 index: 5,
642 x: 11,
643 y: 6,
644 width: nw + 1,
645 height: nh,
646 },
647 ],
648 panels: vec![
649 panel(0, 3, 2, 1, 4),
650 panel(5, 1, 0, 9, 11),
651 panel(5, 3, 2, 20, 15),
652 ],
653 };
654 assert_eq!(
655 canvas.render(&src),
656 render_per_pixel(&canvas, &src),
657 "{rotation:?} flip_x={flip_x} flip_y={flip_y}"
658 );
659 }
660 }
661 }
662
663 #[test]
664 fn render_into_reuses_a_screen_sized_frame() {
665 let canvas = Canvas::grid(4, 2, 2, 2);
666 let mut out = Frame::black(1, 1);
667 canvas.render_into(&gradient(8, 4), &mut out);
668 assert_eq!(out, canvas.render(&gradient(8, 4)));
669 let before = out.as_bytes().as_ptr();
670 canvas.render_into(&Frame::black(8, 4), &mut out);
671 assert_eq!(out.as_bytes().as_ptr(), before);
672 assert_eq!(out, Frame::black(8, 4));
673 }
674
675 #[test]
676 fn two_receivers_side_by_side_render_at_their_screen_positions() {
677 let canvas = Canvas {
678 width: 8,
679 height: 2,
680 receivers: vec![
681 Receiver {
682 index: 0,
683 x: 0,
684 y: 0,
685 width: 4,
686 height: 2,
687 },
688 Receiver {
689 index: 1,
690 x: 4,
691 y: 0,
692 width: 4,
693 height: 2,
694 },
695 ],
696 panels: vec![
697 Panel {
698 receiver: 0,
699 receiver_x: 0,
700 receiver_y: 0,
701 x: 0,
702 y: 0,
703 width: 4,
704 height: 2,
705 rotation: Rotation::None,
706 flip_x: false,
707 flip_y: false,
708 },
709 Panel {
710 receiver: 1,
711 receiver_x: 0,
712 receiver_y: 0,
713 x: 4,
714 y: 0,
715 width: 4,
716 height: 2,
717 rotation: Rotation::None,
718 flip_x: false,
719 flip_y: false,
720 },
721 ],
722 };
723 canvas.validate().unwrap();
724 let src = gradient(8, 2);
725 assert_eq!(canvas.render(&src), src);
726
727 let mut swapped = canvas;
729 swapped.receivers[0].x = 4;
730 swapped.receivers[1].x = 0;
731 let out = swapped.render(&src);
732 assert_eq!(out.pixel(0, 0), src.pixel(4, 0));
733 assert_eq!(out.pixel(4, 1), src.pixel(0, 1));
734 }
735
736 #[test]
737 fn validation_rejects_a_panel_that_hangs_off_the_canvas() {
738 let mut canvas = Canvas::single(8, 4);
739 canvas.panels[0].x = 4;
740 let err = canvas.validate().unwrap_err();
741 assert!(err.to_string().starts_with("canvas is not valid:\n panel 0 at (4, 0)"));
742 }
743
744 #[test]
745 fn validation_rejects_a_receiver_that_hangs_off_the_canvas() {
746 let mut canvas = Canvas::cards(4, 2, 2, 1);
747 canvas.receivers[1].y = 1;
748 let err = canvas.validate().unwrap_err().to_string();
749 assert!(
750 err.contains("receiver 1 at (4, 1) size 4x2 extends past the 8x2 canvas"),
751 "{err}"
752 );
753 }
754
755 #[test]
756 fn validation_rejects_a_panel_that_hangs_off_its_receiver() {
757 let mut canvas = Canvas::cards(4, 2, 2, 1);
758 canvas.panels[1].receiver_x = 1;
759 let err = canvas.validate().unwrap_err().to_string();
760 assert!(
761 err.contains("panel 1 occupies (1, 0) size 4x2 on receiver 1, which is only 4x2"),
762 "{err}"
763 );
764 }
765
766 #[test]
767 fn validation_rejects_an_unknown_receiver() {
768 let mut canvas = Canvas::single(8, 4);
769 canvas.panels[0].receiver = 7;
770 assert!(canvas.validate().is_err());
771 }
772}