use crate::patching::definition::{ColorOrder, Pixel};
use std::f32::consts::{FRAC_PI_2, TAU};
use strum::{Display, EnumIter};
#[derive(Debug, Clone, PartialEq)]
pub struct PixelTemplate {
pub prefix: String,
pub first_number: usize,
pub model: usize,
pub order: ColorOrder,
}
impl PixelTemplate {
fn pixel(&self, offset: usize, position_m: [f32; 2], rotation: f32) -> Pixel {
Pixel {
model: self.model,
rotation,
..Pixel::new(
format!("{}{}", self.prefix, self.first_number + offset),
[position_m[0], position_m[1], 0.0],
self.order,
)
}
}
}
pub fn single(template: &PixelTemplate, at_m: [f32; 2]) -> Vec<Pixel> {
vec![template.pixel(0, at_m, 0.0)]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Display, EnumIter)]
pub enum GridOrder {
#[default]
#[strum(serialize = "row by row")]
Rows,
#[strum(serialize = "column by column")]
Columns,
#[strum(serialize = "rows, snaking")]
SerpentineRows,
#[strum(serialize = "columns, snaking")]
SerpentineColumns,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Display, EnumIter)]
pub enum Corner {
#[default]
#[strum(serialize = "top left")]
TopLeft,
#[strum(serialize = "top right")]
TopRight,
#[strum(serialize = "bottom left")]
BottomLeft,
#[strum(serialize = "bottom right")]
BottomRight,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GridSpec {
pub columns: usize,
pub rows: usize,
pub pitch_m: [f32; 2],
pub centre_m: [f32; 2],
pub order: GridOrder,
pub start: Corner,
}
pub fn grid(template: &PixelTemplate, spec: &GridSpec) -> Vec<Pixel> {
let (outer, inner, by_rows, serpentine) = match spec.order {
GridOrder::Rows => (spec.rows, spec.columns, true, false),
GridOrder::Columns => (spec.columns, spec.rows, false, false),
GridOrder::SerpentineRows => (spec.rows, spec.columns, true, true),
GridOrder::SerpentineColumns => (spec.columns, spec.rows, false, true),
};
let flip_x = matches!(spec.start, Corner::TopRight | Corner::BottomRight);
let flip_y = matches!(spec.start, Corner::BottomLeft | Corner::BottomRight);
let origin = [
spec.centre_m[0] - spec.pitch_m[0] * spec.columns.saturating_sub(1) as f32 / 2.0,
spec.centre_m[1] - spec.pitch_m[1] * spec.rows.saturating_sub(1) as f32 / 2.0,
];
let mut pixels = Vec::with_capacity(spec.rows * spec.columns);
for a in 0..outer {
for b in 0..inner {
let b = if serpentine && a % 2 == 1 {
inner - 1 - b
} else {
b
};
let (mut column, mut row) = if by_rows { (b, a) } else { (a, b) };
if flip_x {
column = spec.columns - 1 - column;
}
if flip_y {
row = spec.rows - 1 - row;
}
let at = [
origin[0] + spec.pitch_m[0] * column as f32,
origin[1] + spec.pitch_m[1] * row as f32,
];
pixels.push(template.pixel(pixels.len(), at, 0.0));
}
}
pixels
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RingSpec {
pub centre_m: [f32; 2],
pub radius_m: f32,
pub count: usize,
pub start_deg: f32,
pub clockwise: bool,
pub along_ring: bool,
}
pub fn ring(template: &PixelTemplate, spec: &RingSpec) -> Vec<Pixel> {
let step = if spec.clockwise { TAU } else { -TAU } / spec.count.max(1) as f32;
let start = spec.start_deg.to_radians() - FRAC_PI_2;
(0..spec.count)
.map(|i| {
let angle = start + step * i as f32;
let (sin, cos) = angle.sin_cos();
let at = [
spec.centre_m[0] + spec.radius_m * cos,
spec.centre_m[1] + spec.radius_m * sin,
];
let rotation = if spec.along_ring {
angle + FRAC_PI_2
} else {
0.0
};
template.pixel(i, at, rotation)
})
.collect()
}
#[derive(Debug, Clone, PartialEq)]
pub struct PathSpec {
pub points_m: Vec<[f32; 2]>,
pub closed: bool,
pub count: usize,
pub inset_ends: bool,
}
pub fn path(template: &PixelTemplate, spec: &PathSpec) -> Vec<Pixel> {
let mut points = spec.points_m.clone();
if spec.closed && points.len() > 2 {
points.push(points[0]);
}
let segments: Vec<([f32; 2], [f32; 2], f32)> = points
.windows(2)
.map(|pair| {
let delta = [pair[1][0] - pair[0][0], pair[1][1] - pair[0][1]];
(pair[0], delta, delta[0].hypot(delta[1]))
})
.filter(|(_, _, length)| *length > 0.0)
.collect();
let total: f32 = segments.iter().map(|(_, _, length)| length).sum();
if spec.count == 0 {
return vec![];
}
if segments.is_empty() || total <= 0.0 {
let at = spec.points_m.first().copied().unwrap_or([0.0; 2]);
return (0..spec.count)
.map(|i| template.pixel(i, at, 0.0))
.collect();
}
let (first, step) = if spec.closed {
(0.0, total / spec.count as f32)
} else if spec.inset_ends {
let step = total / spec.count as f32;
(step / 2.0, step)
} else if spec.count == 1 {
(total / 2.0, 0.0)
} else {
(0.0, total / (spec.count - 1) as f32)
};
(0..spec.count)
.map(|i| {
let mut along = (first + step * i as f32).min(total);
let mut chosen = segments[segments.len() - 1];
let mut offset = chosen.2;
for segment in &segments {
if along <= segment.2 {
chosen = *segment;
offset = along;
break;
}
along -= segment.2;
}
let (start, delta, length) = chosen;
let t = offset / length;
let at = [start[0] + delta[0] * t, start[1] + delta[1] * t];
template.pixel(i, at, delta[1].atan2(delta[0]))
})
.collect()
}
pub fn smooth(points: &[[f32; 2]], closed: bool, steps: usize) -> Vec<[f32; 2]> {
let count = points.len();
if count < 3 || steps == 0 {
return points.to_vec();
}
let at = |index: isize| -> [f32; 2] {
if closed {
points[index.rem_euclid(count as isize) as usize]
} else {
points[index.clamp(0, count as isize - 1) as usize]
}
};
let spans = if closed { count } else { count - 1 };
let mut curve = vec![points[0]];
for span in 0..spans as isize {
let (p0, p1, p2, p3) = (at(span - 1), at(span), at(span + 1), at(span + 2));
for step in 1..=steps {
let t = step as f32 / steps as f32;
let (t2, t3) = (t * t, t * t * t);
curve.push([0, 1].map(|axis| {
0.5 * (2.0 * p1[axis]
+ (p2[axis] - p0[axis]) * t
+ (2.0 * p0[axis] - 5.0 * p1[axis] + 4.0 * p2[axis] - p3[axis]) * t2
+ (3.0 * p1[axis] - p0[axis] - 3.0 * p2[axis] + p3[axis]) * t3)
}));
}
}
if closed {
curve.pop();
}
curve
}
#[cfg(test)]
mod tests {
use super::*;
fn template() -> PixelTemplate {
PixelTemplate {
prefix: "pixel-".to_string(),
first_number: 3,
model: 0,
order: ColorOrder::Grb,
}
}
fn close(a: [f32; 3], b: [f32; 2]) -> bool {
(a[0] - b[0]).abs() < 1e-5 && (a[1] - b[1]).abs() < 1e-5
}
#[test]
fn generated_pixels_are_numbered_and_ordered_like_the_template() {
let pixels = single(&template(), [0.1, 0.2]);
assert_eq!(pixels[0].name, "pixel-3");
assert_eq!(pixels[0].color_order(), Some(ColorOrder::Grb));
}
fn three_by_two(order: GridOrder, start: Corner) -> Vec<Pixel> {
grid(
&template(),
&GridSpec {
columns: 3,
rows: 2,
pitch_m: [1.0, 1.0],
centre_m: [0.0, 0.0],
order,
start,
},
)
}
#[test]
fn a_grid_is_centred_and_runs_row_by_row() {
let pixels = three_by_two(GridOrder::Rows, Corner::TopLeft);
assert_eq!(pixels.len(), 6);
assert!(close(pixels[0].position_m, [-1.0, -0.5]));
assert!(close(pixels[3].position_m, [-1.0, 0.5]));
}
#[test]
fn a_serpentine_grid_runs_every_other_row_backwards() {
let pixels = three_by_two(GridOrder::SerpentineRows, Corner::TopLeft);
assert!(close(pixels[2].position_m, [1.0, -0.5]));
assert!(close(pixels[3].position_m, [1.0, 0.5]));
}
#[test]
fn a_grid_can_start_in_the_bottom_right_corner() {
let pixels = three_by_two(GridOrder::Columns, Corner::BottomRight);
assert!(close(pixels[0].position_m, [1.0, 0.5]));
assert!(close(pixels[1].position_m, [1.0, -0.5]));
}
#[test]
fn a_ring_starts_at_twelve_and_runs_clockwise() {
let spec = RingSpec {
centre_m: [0.0, 0.0],
radius_m: 0.5,
count: 4,
start_deg: 0.0,
clockwise: true,
along_ring: true,
};
let pixels = ring(&template(), &spec);
assert!(close(pixels[0].position_m, [0.0, -0.5]));
assert!(close(pixels[1].position_m, [0.5, 0.0]));
assert!(pixels[0].rotation.abs() < 1e-6);
}
fn corner(closed: bool, count: usize, inset_ends: bool) -> Vec<Pixel> {
path(
&template(),
&PathSpec {
points_m: vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]],
closed,
count,
inset_ends,
},
)
}
#[test]
fn an_open_path_has_pixels_on_both_ends_and_around_its_corner() {
let pixels = corner(false, 5, false);
assert!(close(pixels[0].position_m, [0.0, 0.0]));
assert!(close(pixels[2].position_m, [1.0, 0.0]));
assert!(close(pixels[3].position_m, [1.0, 0.5]));
assert!(close(pixels[4].position_m, [1.0, 1.0]));
assert!((pixels[3].rotation - FRAC_PI_2).abs() < 1e-6);
}
#[test]
fn a_closed_path_spaces_its_pixels_all_the_way_round() {
let pixels = corner(true, 4, false);
let perimeter = 2.0 + 2f32.sqrt();
let step = perimeter / 4.0;
assert!(close(pixels[0].position_m, [0.0, 0.0]));
assert!(close(pixels[1].position_m, [step, 0.0]));
assert_eq!(pixels.len(), 4);
}
#[test]
fn an_inset_path_keeps_half_a_pitch_free_at_its_ends() {
let pixels = corner(false, 4, true);
assert!(close(pixels[0].position_m, [0.25, 0.0]));
assert!(close(pixels[3].position_m, [1.0, 0.75]));
}
#[test]
fn a_smooth_curve_passes_through_every_point() {
let points = [[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]];
let curve = smooth(&points, false, 8);
assert_eq!(curve.len(), 1 + 2 * 8);
assert_eq!(curve[0], points[0]);
assert!(close([curve[8][0], curve[8][1], 0.0], points[1]));
assert!(close([curve[16][0], curve[16][1], 0.0], points[2]));
assert!(curve[4][1] > 0.5 + 1e-3, "{:?}", curve[4]);
}
#[test]
fn a_closed_smooth_curve_does_not_repeat_its_start() {
let square = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let curve = smooth(&square, true, 4);
assert_eq!(curve.len(), 4 * 4);
assert_ne!(curve.last(), curve.first());
}
#[test]
fn two_points_stay_a_straight_line() {
let points = [[0.0, 0.0], [1.0, 0.0]];
assert_eq!(smooth(&points, false, 8), points.to_vec());
}
}