use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Frame {
pub width: u32,
pub height: u32,
data: Vec<u8>,
}
impl Frame {
#[must_use]
pub fn black(width: u32, height: u32) -> Self {
Self {
width,
height,
data: vec![0; (width as usize) * (height as usize) * 3],
}
}
pub fn from_rgb(width: u32, height: u32, data: Vec<u8>) -> Result<Self, FrameError> {
let want = (width as usize) * (height as usize) * 3;
if data.len() == want {
Ok(Self {
width,
height,
data,
})
} else {
Err(FrameError::WrongSize {
got: data.len(),
want,
})
}
}
#[inline]
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
#[inline]
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
&mut self.data
}
#[inline]
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.data
}
#[inline]
#[must_use]
pub fn row(&self, y: u32) -> &[[u8; 3]] {
let stride = (self.width as usize) * 3;
let start = (y as usize) * stride;
self.data[start..start + stride].as_chunks::<3>().0
}
#[inline]
pub fn row_mut(&mut self, y: u32) -> &mut [[u8; 3]] {
let stride = (self.width as usize) * 3;
let start = (y as usize) * stride;
self.data[start..start + stride].as_chunks_mut::<3>().0
}
pub fn rows(&self) -> impl Iterator<Item = &[[u8; 3]]> + '_ {
(0..self.height).map(|y| self.row(y))
}
#[inline]
#[must_use]
pub fn pixel(&self, x: u32, y: u32) -> [u8; 3] {
if x >= self.width || y >= self.height {
return [0; 3];
}
self.row(y)[x as usize]
}
#[inline]
pub fn set_pixel(&mut self, x: u32, y: u32, px: [u8; 3]) {
if x >= self.width || y >= self.height {
return;
}
self.row_mut(y)[x as usize] = px;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameError {
WrongSize { got: usize, want: usize },
}
impl std::fmt::Display for FrameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WrongSize { got, want } => {
write!(f, "frame data is {got} bytes, expected {want}")
}
}
}
}
impl std::error::Error for FrameError {}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "kebab-case")]
pub enum Rotation {
#[default]
None,
Cw90,
Ccw90,
Rot180,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct Panel {
pub receiver: u16,
#[serde(default)]
pub receiver_x: u32,
#[serde(default)]
pub receiver_y: u32,
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
#[serde(default)]
pub rotation: Rotation,
#[serde(default)]
pub flip_x: bool,
#[serde(default)]
pub flip_y: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Placement {
origin: (i64, i64),
col_step: (i64, i64),
row_step: (i64, i64),
}
impl Placement {
const fn is_row_copy(self) -> bool {
matches!(self.col_step, (1, 0)) && matches!(self.row_step, (0, 1))
}
}
impl Panel {
fn receiver_coords(&self, local_x: u32, local_y: u32) -> (u32, u32) {
let (max_x, max_y) = (self.width - 1, self.height - 1);
let lx = if self.flip_x { max_x - local_x } else { local_x };
let ly = if self.flip_y { max_y - local_y } else { local_y };
let (px, py) = match self.rotation {
Rotation::None => (lx, ly),
Rotation::Cw90 => (ly, max_x - lx),
Rotation::Ccw90 => (max_y - ly, lx),
Rotation::Rot180 => (max_x - lx, max_y - ly),
};
(self.receiver_x + px, self.receiver_y + py)
}
fn screen_coords(&self, receiver: (u32, u32), local_x: u32, local_y: u32) -> (u32, u32) {
let (px, py) = self.receiver_coords(local_x, local_y);
(receiver.0 + px, receiver.1 + py)
}
fn placement(&self, receiver: (u32, u32)) -> Placement {
let at = |x, y| {
let (sx, sy) = self.screen_coords(receiver, x, y);
(i64::from(sx), i64::from(sy))
};
let origin = at(0, 0);
let step = |p: (i64, i64)| (p.0 - origin.0, p.1 - origin.1);
let col_step = if self.width > 1 { step(at(1, 0)) } else { (0, 0) };
let row_step = if self.height > 1 { step(at(0, 1)) } else { (0, 0) };
Placement {
origin,
col_step,
row_step,
}
}
const fn native_size(&self) -> (u32, u32) {
match self.rotation {
Rotation::None | Rotation::Rot180 => (self.width, self.height),
Rotation::Cw90 | Rotation::Ccw90 => (self.height, self.width),
}
}
fn blit(&self, receiver: (u32, u32), src: &Frame, dst: &mut Frame) {
if self.width == 0 || self.height == 0 {
return;
}
let place = self.placement(receiver);
if place.is_row_copy() {
self.blit_rows(src, dst, place.origin);
return;
}
let (ox, oy) = place.origin;
let (cx, cy) = place.col_step;
let (rx, ry) = place.row_step;
for ly in 0..self.height {
let sy = self.y + ly;
let (mut x, mut y) = (ox + i64::from(ly) * rx, oy + i64::from(ly) * ry);
for lx in 0..self.width {
let px = src.pixel(self.x + lx, sy);
dst.set_pixel(x as u32, y as u32, px);
x += cx;
y += cy;
}
}
}
fn blit_rows(&self, src: &Frame, dst: &mut Frame, origin: (i64, i64)) {
let (rx, ry) = (origin.0 as u32, origin.1 as u32);
let dst_w = self.width.min(dst.width.saturating_sub(rx)) as usize;
let dst_rows = self.height.min(dst.height.saturating_sub(ry));
let src_w = dst_w.min(src.width.saturating_sub(self.x) as usize);
let src_rows = dst_rows.min(src.height.saturating_sub(self.y));
let (sx, dx) = (self.x as usize, rx as usize);
for ly in 0..dst_rows {
let row = &mut dst.row_mut(ry + ly)[dx..dx + dst_w];
if ly < src_rows {
row[..src_w].copy_from_slice(&src.row(self.y + ly)[sx..sx + src_w]);
row[src_w..].fill([0; 3]);
} else {
row.fill([0; 3]);
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct Receiver {
pub index: u16,
#[serde(default)]
pub x: u32,
#[serde(default)]
pub y: u32,
pub width: u32,
pub height: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct Canvas {
pub width: u32,
pub height: u32,
pub receivers: Vec<Receiver>,
pub panels: Vec<Panel>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayoutError(pub Vec<String>);
impl std::fmt::Display for LayoutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "canvas is not valid:\n {}", self.0.join("\n "))
}
}
impl std::error::Error for LayoutError {}
impl Canvas {
#[must_use]
pub fn single(width: u32, height: u32) -> Self {
Self::grid(width, height, 1, 1)
}
#[must_use]
pub fn grid(panel_w: u32, panel_h: u32, cols: u32, rows: u32) -> Self {
let (width, height) = (panel_w * cols, panel_h * rows);
let panels = (0..rows)
.flat_map(|row| {
(0..cols).map(move |col| Panel {
receiver: 0,
receiver_x: col * panel_w,
receiver_y: row * panel_h,
x: col * panel_w,
y: row * panel_h,
width: panel_w,
height: panel_h,
rotation: Rotation::None,
flip_x: false,
flip_y: false,
})
})
.collect();
Self {
width,
height,
receivers: vec![Receiver {
index: 0,
x: 0,
y: 0,
width,
height,
}],
panels,
}
}
#[must_use]
pub fn cards(panel_w: u32, panel_h: u32, cols: u32, rows: u32) -> Self {
let mut canvas = Self::grid(panel_w, panel_h, cols, rows);
canvas.receivers = canvas
.panels
.iter()
.enumerate()
.map(|(i, p)| Receiver {
index: i as u16,
x: p.x,
y: p.y,
width: panel_w,
height: panel_h,
})
.collect();
for (i, p) in canvas.panels.iter_mut().enumerate() {
p.receiver = i as u16;
p.receiver_x = 0;
p.receiver_y = 0;
}
canvas
}
pub fn validate(&self) -> Result<(), LayoutError> {
let mut problems = Vec::new();
for r in &self.receivers {
if r.x + r.width > self.width || r.y + r.height > self.height {
problems.push(format!(
"receiver {} at ({}, {}) size {}x{} extends past the {}x{} canvas",
r.index, r.x, r.y, r.width, r.height, self.width, self.height
));
}
}
for (i, p) in self.panels.iter().enumerate() {
if p.x + p.width > self.width || p.y + p.height > self.height {
problems.push(format!(
"panel {i} at ({}, {}) size {}x{} extends past the {}x{} canvas",
p.x, p.y, p.width, p.height, self.width, self.height
));
}
let Some(r) = self.receivers.iter().find(|r| r.index == p.receiver) else {
problems.push(format!(
"panel {i} names receiver {}, which is not defined",
p.receiver
));
continue;
};
let (nw, nh) = p.native_size();
if p.receiver_x + nw > r.width || p.receiver_y + nh > r.height {
problems.push(format!(
"panel {i} occupies ({}, {}) size {nw}x{nh} on receiver {}, which is only {}x{}",
p.receiver_x, p.receiver_y, r.index, r.width, r.height
));
}
}
if problems.is_empty() {
Ok(())
} else {
Err(LayoutError(problems))
}
}
#[must_use]
pub fn screen_frame(&self) -> Frame {
Frame::black(self.width, self.height)
}
#[must_use]
pub fn render(&self, src: &Frame) -> Frame {
let mut out = self.screen_frame();
self.render_into(src, &mut out);
out
}
pub fn render_into(&self, src: &Frame, out: &mut Frame) {
if (out.width, out.height) == (self.width, self.height) {
out.data.fill(0);
} else {
*out = self.screen_frame();
}
for panel in &self.panels {
let Some(r) = self.receivers.iter().find(|r| r.index == panel.receiver) else {
continue;
};
panel.blit((r.x, r.y), src, out);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gradient(w: u32, h: u32) -> Frame {
let mut f = Frame::black(w, h);
for y in 0..h {
for x in 0..w {
f.set_pixel(x, y, [x as u8, y as u8, 0]);
}
}
f
}
fn render_per_pixel(canvas: &Canvas, src: &Frame) -> Frame {
let mut out = canvas.screen_frame();
for panel in &canvas.panels {
let Some(r) = canvas.receivers.iter().find(|r| r.index == panel.receiver) else {
continue;
};
for ly in 0..panel.height {
for lx in 0..panel.width {
let px = src.pixel(panel.x + lx, panel.y + ly);
let (sx, sy) = panel.screen_coords((r.x, r.y), lx, ly);
out.set_pixel(sx, sy, px);
}
}
}
out
}
#[test]
fn a_single_panel_passes_the_image_through_unchanged() {
let canvas = Canvas::single(8, 4);
let src = gradient(8, 4);
assert_eq!(canvas.render(&src), src);
}
#[test]
fn single_is_a_one_by_one_grid() {
assert_eq!(Canvas::single(8, 4), Canvas::grid(8, 4, 1, 1));
}
#[test]
fn a_grid_tiles_panels_across_the_canvas() {
let canvas = Canvas::grid(4, 2, 2, 2);
assert_eq!((canvas.width, canvas.height), (8, 4));
assert_eq!(canvas.panels.len(), 4);
canvas.validate().unwrap();
assert_eq!(canvas.render(&gradient(8, 4)), gradient(8, 4));
}
#[test]
fn cards_put_one_receiver_under_each_panel_at_its_screen_position() {
let canvas = Canvas::cards(4, 2, 3, 2);
canvas.validate().unwrap();
assert_eq!(canvas.receivers.len(), 6);
let r = canvas.receivers[4];
assert_eq!((r.index, r.x, r.y, r.width, r.height), (4, 4, 2, 4, 2));
let p = &canvas.panels[4];
assert_eq!((p.receiver, p.receiver_x, p.receiver_y, p.x, p.y), (4, 0, 0, 4, 2));
assert_eq!(canvas.render(&gradient(12, 4)), gradient(12, 4));
}
#[test]
fn a_receiver_position_defaults_to_the_origin_in_layout_files() {
let r: Receiver = serde_json::from_str(r#"{"index":3,"width":8,"height":4}"#).unwrap();
assert_eq!((r.x, r.y), (0, 0));
}
#[test]
fn rows_are_contiguous_pixel_slices() {
let f = gradient(4, 2);
assert_eq!(f.row(1), &[[0, 1, 0], [1, 1, 0], [2, 1, 0], [3, 1, 0]]);
assert_eq!(f.rows().count(), 2);
assert_eq!(f.as_bytes().len(), 4 * 2 * 3);
assert_eq!(f.clone().into_bytes(), f.as_bytes());
}
#[test]
fn pixels_off_the_frame_read_black_and_ignore_writes() {
let mut f = gradient(4, 2);
assert_eq!(f.pixel(4, 0), [0; 3]);
assert_eq!(f.pixel(0, 2), [0; 3]);
f.set_pixel(4, 0, [9; 3]);
f.set_pixel(0, 2, [9; 3]);
assert_eq!(f, gradient(4, 2));
}
#[test]
fn rotation_maps_corners_where_expected() {
let mut canvas = Canvas {
width: 2,
height: 4,
receivers: vec![Receiver {
index: 0,
x: 0,
y: 0,
width: 4,
height: 2,
}],
panels: vec![Panel {
receiver: 0,
receiver_x: 0,
receiver_y: 0,
x: 0,
y: 0,
width: 2,
height: 4,
rotation: Rotation::Cw90,
flip_x: false,
flip_y: false,
}],
};
assert!(canvas.validate().is_err());
canvas.width = 4;
let mut src = Frame::black(2, 4);
src.set_pixel(0, 0, [255, 0, 0]); let out = canvas.render(&src);
assert_eq!(out.pixel(0, 1), [255, 0, 0]);
}
#[test]
fn flipping_mirrors_the_image() {
let mut canvas = Canvas::single(4, 1);
canvas.panels[0].flip_x = true;
let mut src = Frame::black(4, 1);
src.set_pixel(0, 0, [1, 2, 3]);
assert_eq!(canvas.render(&src).pixel(3, 0), [1, 2, 3]);
}
#[test]
fn every_mounting_matches_the_per_pixel_mapping() {
let src = gradient(23, 17);
let rotations = [
Rotation::None,
Rotation::Cw90,
Rotation::Ccw90,
Rotation::Rot180,
];
for rotation in rotations {
for (flip_x, flip_y) in [(false, false), (true, false), (false, true), (true, true)] {
let (w, h) = (7, 5);
let (nw, nh) = match rotation {
Rotation::None | Rotation::Rot180 => (w, h),
Rotation::Cw90 | Rotation::Ccw90 => (h, w),
};
let panel = |receiver, receiver_x, receiver_y, x, y| Panel {
receiver,
receiver_x,
receiver_y,
x,
y,
width: w,
height: h,
rotation,
flip_x,
flip_y,
};
let canvas = Canvas {
width: 23,
height: 17,
receivers: vec![
Receiver {
index: 0,
x: 0,
y: 0,
width: nw + 3,
height: nh + 2,
},
Receiver {
index: 5,
x: 11,
y: 6,
width: nw + 1,
height: nh,
},
],
panels: vec![
panel(0, 3, 2, 1, 4),
panel(5, 1, 0, 9, 11),
panel(5, 3, 2, 20, 15),
],
};
assert_eq!(
canvas.render(&src),
render_per_pixel(&canvas, &src),
"{rotation:?} flip_x={flip_x} flip_y={flip_y}"
);
}
}
}
#[test]
fn render_into_reuses_a_screen_sized_frame() {
let canvas = Canvas::grid(4, 2, 2, 2);
let mut out = Frame::black(1, 1);
canvas.render_into(&gradient(8, 4), &mut out);
assert_eq!(out, canvas.render(&gradient(8, 4)));
let before = out.as_bytes().as_ptr();
canvas.render_into(&Frame::black(8, 4), &mut out);
assert_eq!(out.as_bytes().as_ptr(), before);
assert_eq!(out, Frame::black(8, 4));
}
#[test]
fn two_receivers_side_by_side_render_at_their_screen_positions() {
let canvas = Canvas {
width: 8,
height: 2,
receivers: vec![
Receiver {
index: 0,
x: 0,
y: 0,
width: 4,
height: 2,
},
Receiver {
index: 1,
x: 4,
y: 0,
width: 4,
height: 2,
},
],
panels: vec![
Panel {
receiver: 0,
receiver_x: 0,
receiver_y: 0,
x: 0,
y: 0,
width: 4,
height: 2,
rotation: Rotation::None,
flip_x: false,
flip_y: false,
},
Panel {
receiver: 1,
receiver_x: 0,
receiver_y: 0,
x: 4,
y: 0,
width: 4,
height: 2,
rotation: Rotation::None,
flip_x: false,
flip_y: false,
},
],
};
canvas.validate().unwrap();
let src = gradient(8, 2);
assert_eq!(canvas.render(&src), src);
let mut swapped = canvas;
swapped.receivers[0].x = 4;
swapped.receivers[1].x = 0;
let out = swapped.render(&src);
assert_eq!(out.pixel(0, 0), src.pixel(4, 0));
assert_eq!(out.pixel(4, 1), src.pixel(0, 1));
}
#[test]
fn validation_rejects_a_panel_that_hangs_off_the_canvas() {
let mut canvas = Canvas::single(8, 4);
canvas.panels[0].x = 4;
let err = canvas.validate().unwrap_err();
assert!(err.to_string().starts_with("canvas is not valid:\n panel 0 at (4, 0)"));
}
#[test]
fn validation_rejects_a_receiver_that_hangs_off_the_canvas() {
let mut canvas = Canvas::cards(4, 2, 2, 1);
canvas.receivers[1].y = 1;
let err = canvas.validate().unwrap_err().to_string();
assert!(
err.contains("receiver 1 at (4, 1) size 4x2 extends past the 8x2 canvas"),
"{err}"
);
}
#[test]
fn validation_rejects_a_panel_that_hangs_off_its_receiver() {
let mut canvas = Canvas::cards(4, 2, 2, 1);
canvas.panels[1].receiver_x = 1;
let err = canvas.validate().unwrap_err().to_string();
assert!(
err.contains("panel 1 occupies (1, 0) size 4x2 on receiver 1, which is only 4x2"),
"{err}"
);
}
#[test]
fn validation_rejects_an_unknown_receiver() {
let mut canvas = Canvas::single(8, 4);
canvas.panels[0].receiver = 7;
assert!(canvas.validate().is_err());
}
}