2022-06-17 17:26:04 +02:00
|
|
|
use std::iter::FusedIterator;
|
|
|
|
|
|
|
|
use image::{Rgb, RgbImage};
|
|
|
|
use thiserror::Error;
|
|
|
|
|
|
|
|
use crate::Layout;
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
|
|
|
|
#[allow(clippy::module_name_repetitions)]
|
|
|
|
pub enum StrandifierError {
|
2022-06-17 21:30:30 +02:00
|
|
|
#[error("Image too large")]
|
|
|
|
ImageTooLarge,
|
2022-06-17 17:26:04 +02:00
|
|
|
#[error("Invalid strand number")]
|
|
|
|
InvalidStrand,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Iterator over a strand's pixels, extracted from an [`RgbImage`].
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct Strandifier<'a> {
|
|
|
|
layout: Layout,
|
|
|
|
image: &'a RgbImage,
|
|
|
|
|
|
|
|
pixels_remaining: u32,
|
|
|
|
next_x: u32,
|
|
|
|
next_y: u32,
|
2022-06-17 21:30:30 +02:00
|
|
|
|
|
|
|
offset_x: u32,
|
|
|
|
offset_y: u32,
|
2022-06-17 17:26:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Strandifier<'a> {
|
|
|
|
/// Create an iterator over a strand's pixels.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
///
|
|
|
|
/// Returns `StrandifierError::WrongDimensions` if the `layout` does not match up with the
|
|
|
|
/// `image`'s dimensions.
|
|
|
|
///
|
|
|
|
/// Returns `StrandifierError::InvalidStrand` if the supplied strand number is higher than the
|
|
|
|
/// number of strands in the [`Layout`].
|
|
|
|
#[allow(clippy::missing_panics_doc)]
|
|
|
|
pub fn make_strand(
|
|
|
|
layout: Layout,
|
|
|
|
image: &'a RgbImage,
|
|
|
|
strand_num: u32,
|
|
|
|
) -> Result<Self, StrandifierError> {
|
2022-06-17 21:30:30 +02:00
|
|
|
if layout.width_px() < image.width() || layout.height_px() < image.height() {
|
|
|
|
return Err(StrandifierError::ImageTooLarge);
|
2022-06-17 17:26:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if strand_num > layout.num_strands() {
|
|
|
|
return Err(StrandifierError::InvalidStrand);
|
|
|
|
}
|
|
|
|
|
|
|
|
let panel_x = strand_num % layout.num_panels_h;
|
|
|
|
let panel_y = strand_num / layout.num_panels_h;
|
|
|
|
|
|
|
|
let first_x = panel_x * layout.gang_len;
|
|
|
|
let first_y = panel_y * layout.num_gangs;
|
|
|
|
|
2022-06-17 21:30:30 +02:00
|
|
|
let offset_x = (layout.width_px() - image.width()) / 2;
|
|
|
|
let offset_y = (layout.height_px() - image.height()) / 2;
|
|
|
|
|
2022-06-17 17:26:04 +02:00
|
|
|
Ok(Self {
|
|
|
|
layout,
|
|
|
|
image,
|
|
|
|
|
|
|
|
pixels_remaining: layout.strand_len(),
|
|
|
|
next_x: first_x,
|
|
|
|
next_y: first_y,
|
2022-06-17 21:30:30 +02:00
|
|
|
|
|
|
|
offset_x,
|
|
|
|
offset_y,
|
2022-06-17 17:26:04 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Iterator for Strandifier<'a> {
|
|
|
|
type Item = Rgb<u8>;
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
|
|
if self.pixels_remaining == 0 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
let x = self.next_x;
|
|
|
|
let y = self.next_y;
|
|
|
|
|
|
|
|
if (y % self.layout.num_gangs) % 2 == 0 {
|
|
|
|
// Right-moving gang
|
|
|
|
if x % self.layout.gang_len == self.layout.gang_len - 1 {
|
|
|
|
// End of gang - move down
|
|
|
|
self.next_y += 1;
|
|
|
|
} else {
|
|
|
|
// move right
|
|
|
|
self.next_x += 1;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Left-moving gang
|
|
|
|
if x % self.layout.gang_len == 0 {
|
|
|
|
// End of gang - move down
|
|
|
|
self.next_y += 1;
|
|
|
|
} else {
|
|
|
|
// move left
|
|
|
|
self.next_x -= 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self.pixels_remaining -= 1;
|
|
|
|
|
2022-06-17 21:30:30 +02:00
|
|
|
let opt_px: Option<_> = try {
|
|
|
|
let x = x.checked_sub(self.offset_x)?;
|
|
|
|
let y = y.checked_sub(self.offset_y)?;
|
|
|
|
|
|
|
|
*self.image.get_pixel_checked(x, y)?
|
|
|
|
};
|
|
|
|
|
|
|
|
Some(opt_px.unwrap_or(Rgb([0, 0, 0])))
|
2022-06-17 17:26:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
|
|
let remaining = self.pixels_remaining.try_into().unwrap();
|
|
|
|
|
|
|
|
(remaining, Some(remaining))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> FusedIterator for Strandifier<'a> {}
|
|
|
|
impl<'a> ExactSizeIterator for Strandifier<'a> {}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
fn test_image(layout: Layout) -> RgbImage {
|
|
|
|
let width = layout.width_px();
|
|
|
|
let height = layout.height_px();
|
|
|
|
|
|
|
|
RgbImage::from_fn(width, height, |x, y| {
|
|
|
|
Rgb([x.try_into().unwrap(), y.try_into().unwrap(), 0])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn strandifier_basic() {
|
|
|
|
let layout = Layout {
|
|
|
|
gang_len: 5,
|
|
|
|
num_gangs: 4,
|
|
|
|
|
|
|
|
num_panels_h: 3,
|
|
|
|
num_panels_v: 6,
|
|
|
|
};
|
|
|
|
let image = test_image(layout);
|
|
|
|
|
|
|
|
let strand = Strandifier::make_strand(layout, &image, 0).unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
strand.collect::<Vec<_>>(),
|
|
|
|
[
|
|
|
|
Rgb([0, 0, 0]),
|
|
|
|
Rgb([1, 0, 0]),
|
|
|
|
Rgb([2, 0, 0]),
|
|
|
|
Rgb([3, 0, 0]),
|
|
|
|
Rgb([4, 0, 0]),
|
|
|
|
//
|
|
|
|
Rgb([4, 1, 0]),
|
|
|
|
Rgb([3, 1, 0]),
|
|
|
|
Rgb([2, 1, 0]),
|
|
|
|
Rgb([1, 1, 0]),
|
|
|
|
Rgb([0, 1, 0]),
|
|
|
|
//
|
|
|
|
Rgb([0, 2, 0]),
|
|
|
|
Rgb([1, 2, 0]),
|
|
|
|
Rgb([2, 2, 0]),
|
|
|
|
Rgb([3, 2, 0]),
|
|
|
|
Rgb([4, 2, 0]),
|
|
|
|
//
|
|
|
|
Rgb([4, 3, 0]),
|
|
|
|
Rgb([3, 3, 0]),
|
|
|
|
Rgb([2, 3, 0]),
|
|
|
|
Rgb([1, 3, 0]),
|
|
|
|
Rgb([0, 3, 0]),
|
|
|
|
]
|
|
|
|
);
|
|
|
|
|
|
|
|
let strand = Strandifier::make_strand(layout, &image, 4).unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
strand.collect::<Vec<_>>(),
|
|
|
|
[
|
|
|
|
Rgb([5, 4, 0]),
|
|
|
|
Rgb([6, 4, 0]),
|
|
|
|
Rgb([7, 4, 0]),
|
|
|
|
Rgb([8, 4, 0]),
|
|
|
|
Rgb([9, 4, 0]),
|
|
|
|
//
|
|
|
|
Rgb([9, 5, 0]),
|
|
|
|
Rgb([8, 5, 0]),
|
|
|
|
Rgb([7, 5, 0]),
|
|
|
|
Rgb([6, 5, 0]),
|
|
|
|
Rgb([5, 5, 0]),
|
|
|
|
//
|
|
|
|
Rgb([5, 6, 0]),
|
|
|
|
Rgb([6, 6, 0]),
|
|
|
|
Rgb([7, 6, 0]),
|
|
|
|
Rgb([8, 6, 0]),
|
|
|
|
Rgb([9, 6, 0]),
|
|
|
|
//
|
|
|
|
Rgb([9, 7, 0]),
|
|
|
|
Rgb([8, 7, 0]),
|
|
|
|
Rgb([7, 7, 0]),
|
|
|
|
Rgb([6, 7, 0]),
|
|
|
|
Rgb([5, 7, 0]),
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn strandifier_thin() {
|
|
|
|
let layout = Layout {
|
|
|
|
gang_len: 1,
|
|
|
|
num_gangs: 6,
|
|
|
|
|
|
|
|
num_panels_h: 3,
|
|
|
|
num_panels_v: 4,
|
|
|
|
};
|
|
|
|
let image = test_image(layout);
|
|
|
|
|
|
|
|
let strand = Strandifier::make_strand(layout, &image, 0).unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
strand.collect::<Vec<_>>(),
|
|
|
|
[
|
|
|
|
Rgb([0, 0, 0]),
|
|
|
|
Rgb([0, 1, 0]),
|
|
|
|
Rgb([0, 2, 0]),
|
|
|
|
Rgb([0, 3, 0]),
|
|
|
|
Rgb([0, 4, 0]),
|
|
|
|
Rgb([0, 5, 0]),
|
|
|
|
]
|
|
|
|
);
|
|
|
|
|
|
|
|
let layout = Layout {
|
|
|
|
gang_len: 6,
|
|
|
|
num_gangs: 1,
|
|
|
|
|
|
|
|
num_panels_h: 3,
|
|
|
|
num_panels_v: 4,
|
|
|
|
};
|
|
|
|
let image = test_image(layout);
|
|
|
|
|
|
|
|
let strand = Strandifier::make_strand(layout, &image, 11).unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
strand.collect::<Vec<_>>(),
|
|
|
|
[
|
|
|
|
Rgb([12, 3, 0]),
|
|
|
|
Rgb([13, 3, 0]),
|
|
|
|
Rgb([14, 3, 0]),
|
|
|
|
Rgb([15, 3, 0]),
|
|
|
|
Rgb([16, 3, 0]),
|
|
|
|
Rgb([17, 3, 0]),
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn strandifier_errors() {
|
|
|
|
let layout = Layout {
|
|
|
|
gang_len: 5,
|
|
|
|
num_gangs: 4,
|
|
|
|
|
|
|
|
num_panels_h: 3,
|
|
|
|
num_panels_v: 6,
|
|
|
|
};
|
|
|
|
let image = test_image(layout);
|
|
|
|
assert_eq!(
|
|
|
|
Strandifier::make_strand(layout, &image, 20).unwrap_err(),
|
|
|
|
StrandifierError::InvalidStrand
|
|
|
|
);
|
|
|
|
|
|
|
|
let layout2 = Layout {
|
2022-06-17 21:30:30 +02:00
|
|
|
num_gangs: 3,
|
2022-06-17 17:26:04 +02:00
|
|
|
..layout
|
|
|
|
};
|
|
|
|
assert_eq!(
|
|
|
|
Strandifier::make_strand(layout2, &image, 0).unwrap_err(),
|
2022-06-17 21:30:30 +02:00
|
|
|
StrandifierError::ImageTooLarge
|
2022-06-17 17:26:04 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|