1use crate::patching::definition::{ColorOrder, Pixel};
6use std::f32::consts::{FRAC_PI_2, TAU};
7use strum::{Display, EnumIter};
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct PixelTemplate {
12 pub prefix: String,
14 pub first_number: usize,
15 pub model: usize,
16 pub order: ColorOrder,
17}
18
19impl PixelTemplate {
20 fn pixel(&self, offset: usize, position_m: [f32; 2], rotation: f32) -> Pixel {
21 Pixel {
22 model: self.model,
23 rotation,
24 ..Pixel::new(
25 format!("{}{}", self.prefix, self.first_number + offset),
26 [position_m[0], position_m[1], 0.0],
27 self.order,
28 )
29 }
30 }
31}
32
33pub fn single(template: &PixelTemplate, at_m: [f32; 2]) -> Vec<Pixel> {
35 vec![template.pixel(0, at_m, 0.0)]
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Display, EnumIter)]
41pub enum GridOrder {
42 #[default]
44 #[strum(serialize = "row by row")]
45 Rows,
46 #[strum(serialize = "column by column")]
47 Columns,
48 #[strum(serialize = "rows, snaking")]
50 SerpentineRows,
51 #[strum(serialize = "columns, snaking")]
52 SerpentineColumns,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Display, EnumIter)]
58pub enum Corner {
59 #[default]
60 #[strum(serialize = "top left")]
61 TopLeft,
62 #[strum(serialize = "top right")]
63 TopRight,
64 #[strum(serialize = "bottom left")]
65 BottomLeft,
66 #[strum(serialize = "bottom right")]
67 BottomRight,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
71pub struct GridSpec {
72 pub columns: usize,
73 pub rows: usize,
74 pub pitch_m: [f32; 2],
75 pub centre_m: [f32; 2],
76 pub order: GridOrder,
77 pub start: Corner,
78}
79
80pub fn grid(template: &PixelTemplate, spec: &GridSpec) -> Vec<Pixel> {
82 let (outer, inner, by_rows, serpentine) = match spec.order {
83 GridOrder::Rows => (spec.rows, spec.columns, true, false),
84 GridOrder::Columns => (spec.columns, spec.rows, false, false),
85 GridOrder::SerpentineRows => (spec.rows, spec.columns, true, true),
86 GridOrder::SerpentineColumns => (spec.columns, spec.rows, false, true),
87 };
88 let flip_x = matches!(spec.start, Corner::TopRight | Corner::BottomRight);
89 let flip_y = matches!(spec.start, Corner::BottomLeft | Corner::BottomRight);
90 let origin = [
91 spec.centre_m[0] - spec.pitch_m[0] * spec.columns.saturating_sub(1) as f32 / 2.0,
92 spec.centre_m[1] - spec.pitch_m[1] * spec.rows.saturating_sub(1) as f32 / 2.0,
93 ];
94
95 let mut pixels = Vec::with_capacity(spec.rows * spec.columns);
96 for a in 0..outer {
97 for b in 0..inner {
98 let b = if serpentine && a % 2 == 1 {
99 inner - 1 - b
100 } else {
101 b
102 };
103 let (mut column, mut row) = if by_rows { (b, a) } else { (a, b) };
104 if flip_x {
105 column = spec.columns - 1 - column;
106 }
107 if flip_y {
108 row = spec.rows - 1 - row;
109 }
110 let at = [
111 origin[0] + spec.pitch_m[0] * column as f32,
112 origin[1] + spec.pitch_m[1] * row as f32,
113 ];
114 pixels.push(template.pixel(pixels.len(), at, 0.0));
115 }
116 }
117 pixels
118}
119
120#[derive(Debug, Clone, Copy, PartialEq)]
121pub struct RingSpec {
122 pub centre_m: [f32; 2],
123 pub radius_m: f32,
124 pub count: usize,
125 pub start_deg: f32,
127 pub clockwise: bool,
128 pub along_ring: bool,
130}
131
132pub fn ring(template: &PixelTemplate, spec: &RingSpec) -> Vec<Pixel> {
134 let step = if spec.clockwise { TAU } else { -TAU } / spec.count.max(1) as f32;
135 let start = spec.start_deg.to_radians() - FRAC_PI_2;
137 (0..spec.count)
138 .map(|i| {
139 let angle = start + step * i as f32;
140 let (sin, cos) = angle.sin_cos();
141 let at = [
142 spec.centre_m[0] + spec.radius_m * cos,
143 spec.centre_m[1] + spec.radius_m * sin,
144 ];
145 let rotation = if spec.along_ring {
146 angle + FRAC_PI_2
147 } else {
148 0.0
149 };
150 template.pixel(i, at, rotation)
151 })
152 .collect()
153}
154
155#[derive(Debug, Clone, PartialEq)]
157pub struct PathSpec {
158 pub points_m: Vec<[f32; 2]>,
159 pub closed: bool,
161 pub count: usize,
162 pub inset_ends: bool,
165}
166
167pub fn path(template: &PixelTemplate, spec: &PathSpec) -> Vec<Pixel> {
172 let mut points = spec.points_m.clone();
173 if spec.closed && points.len() > 2 {
174 points.push(points[0]);
175 }
176 let segments: Vec<([f32; 2], [f32; 2], f32)> = points
177 .windows(2)
178 .map(|pair| {
179 let delta = [pair[1][0] - pair[0][0], pair[1][1] - pair[0][1]];
180 (pair[0], delta, delta[0].hypot(delta[1]))
181 })
182 .filter(|(_, _, length)| *length > 0.0)
183 .collect();
184 let total: f32 = segments.iter().map(|(_, _, length)| length).sum();
185 if spec.count == 0 {
186 return vec![];
187 }
188 if segments.is_empty() || total <= 0.0 {
189 let at = spec.points_m.first().copied().unwrap_or([0.0; 2]);
191 return (0..spec.count)
192 .map(|i| template.pixel(i, at, 0.0))
193 .collect();
194 }
195
196 let (first, step) = if spec.closed {
197 (0.0, total / spec.count as f32)
198 } else if spec.inset_ends {
199 let step = total / spec.count as f32;
200 (step / 2.0, step)
201 } else if spec.count == 1 {
202 (total / 2.0, 0.0)
203 } else {
204 (0.0, total / (spec.count - 1) as f32)
205 };
206
207 (0..spec.count)
208 .map(|i| {
209 let mut along = (first + step * i as f32).min(total);
210 let mut chosen = segments[segments.len() - 1];
211 let mut offset = chosen.2;
212 for segment in &segments {
213 if along <= segment.2 {
214 chosen = *segment;
215 offset = along;
216 break;
217 }
218 along -= segment.2;
219 }
220 let (start, delta, length) = chosen;
221 let t = offset / length;
222 let at = [start[0] + delta[0] * t, start[1] + delta[1] * t];
223 template.pixel(i, at, delta[1].atan2(delta[0]))
224 })
225 .collect()
226}
227
228pub fn smooth(points: &[[f32; 2]], closed: bool, steps: usize) -> Vec<[f32; 2]> {
233 let count = points.len();
234 if count < 3 || steps == 0 {
235 return points.to_vec();
236 }
237 let at = |index: isize| -> [f32; 2] {
238 if closed {
239 points[index.rem_euclid(count as isize) as usize]
240 } else {
241 points[index.clamp(0, count as isize - 1) as usize]
242 }
243 };
244 let spans = if closed { count } else { count - 1 };
245 let mut curve = vec![points[0]];
246 for span in 0..spans as isize {
247 let (p0, p1, p2, p3) = (at(span - 1), at(span), at(span + 1), at(span + 2));
248 for step in 1..=steps {
249 let t = step as f32 / steps as f32;
250 let (t2, t3) = (t * t, t * t * t);
251 curve.push([0, 1].map(|axis| {
252 0.5 * (2.0 * p1[axis]
253 + (p2[axis] - p0[axis]) * t
254 + (2.0 * p0[axis] - 5.0 * p1[axis] + 4.0 * p2[axis] - p3[axis]) * t2
255 + (3.0 * p1[axis] - p0[axis] - 3.0 * p2[axis] + p3[axis]) * t3)
256 }));
257 }
258 }
259 if closed {
260 curve.pop();
262 }
263 curve
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 fn template() -> PixelTemplate {
271 PixelTemplate {
272 prefix: "pixel-".to_string(),
273 first_number: 3,
274 model: 0,
275 order: ColorOrder::Grb,
276 }
277 }
278
279 fn close(a: [f32; 3], b: [f32; 2]) -> bool {
280 (a[0] - b[0]).abs() < 1e-5 && (a[1] - b[1]).abs() < 1e-5
281 }
282
283 #[test]
284 fn generated_pixels_are_numbered_and_ordered_like_the_template() {
285 let pixels = single(&template(), [0.1, 0.2]);
286 assert_eq!(pixels[0].name, "pixel-3");
287 assert_eq!(pixels[0].color_order(), Some(ColorOrder::Grb));
288 }
289
290 fn three_by_two(order: GridOrder, start: Corner) -> Vec<Pixel> {
291 grid(
292 &template(),
293 &GridSpec {
294 columns: 3,
295 rows: 2,
296 pitch_m: [1.0, 1.0],
297 centre_m: [0.0, 0.0],
298 order,
299 start,
300 },
301 )
302 }
303
304 #[test]
305 fn a_grid_is_centred_and_runs_row_by_row() {
306 let pixels = three_by_two(GridOrder::Rows, Corner::TopLeft);
307 assert_eq!(pixels.len(), 6);
308 assert!(close(pixels[0].position_m, [-1.0, -0.5]));
309 assert!(close(pixels[3].position_m, [-1.0, 0.5]));
310 }
311
312 #[test]
313 fn a_serpentine_grid_runs_every_other_row_backwards() {
314 let pixels = three_by_two(GridOrder::SerpentineRows, Corner::TopLeft);
315 assert!(close(pixels[2].position_m, [1.0, -0.5]));
316 assert!(close(pixels[3].position_m, [1.0, 0.5]));
317 }
318
319 #[test]
320 fn a_grid_can_start_in_the_bottom_right_corner() {
321 let pixels = three_by_two(GridOrder::Columns, Corner::BottomRight);
322 assert!(close(pixels[0].position_m, [1.0, 0.5]));
323 assert!(close(pixels[1].position_m, [1.0, -0.5]));
324 }
325
326 #[test]
327 fn a_ring_starts_at_twelve_and_runs_clockwise() {
328 let spec = RingSpec {
329 centre_m: [0.0, 0.0],
330 radius_m: 0.5,
331 count: 4,
332 start_deg: 0.0,
333 clockwise: true,
334 along_ring: true,
335 };
336 let pixels = ring(&template(), &spec);
337 assert!(close(pixels[0].position_m, [0.0, -0.5]));
338 assert!(close(pixels[1].position_m, [0.5, 0.0]));
340 assert!(pixels[0].rotation.abs() < 1e-6);
342 }
343
344 fn corner(closed: bool, count: usize, inset_ends: bool) -> Vec<Pixel> {
345 path(
346 &template(),
347 &PathSpec {
348 points_m: vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]],
349 closed,
350 count,
351 inset_ends,
352 },
353 )
354 }
355
356 #[test]
357 fn an_open_path_has_pixels_on_both_ends_and_around_its_corner() {
358 let pixels = corner(false, 5, false);
359 assert!(close(pixels[0].position_m, [0.0, 0.0]));
360 assert!(close(pixels[2].position_m, [1.0, 0.0]));
361 assert!(close(pixels[3].position_m, [1.0, 0.5]));
362 assert!(close(pixels[4].position_m, [1.0, 1.0]));
363 assert!((pixels[3].rotation - FRAC_PI_2).abs() < 1e-6);
365 }
366
367 #[test]
368 fn a_closed_path_spaces_its_pixels_all_the_way_round() {
369 let pixels = corner(true, 4, false);
371 let perimeter = 2.0 + 2f32.sqrt();
372 let step = perimeter / 4.0;
373 assert!(close(pixels[0].position_m, [0.0, 0.0]));
374 assert!(close(pixels[1].position_m, [step, 0.0]));
375 assert_eq!(pixels.len(), 4);
376 }
377
378 #[test]
379 fn an_inset_path_keeps_half_a_pitch_free_at_its_ends() {
380 let pixels = corner(false, 4, true);
381 assert!(close(pixels[0].position_m, [0.25, 0.0]));
382 assert!(close(pixels[3].position_m, [1.0, 0.75]));
383 }
384
385 #[test]
386 fn a_smooth_curve_passes_through_every_point() {
387 let points = [[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]];
388 let curve = smooth(&points, false, 8);
389 assert_eq!(curve.len(), 1 + 2 * 8);
390 assert_eq!(curve[0], points[0]);
391 assert!(close([curve[8][0], curve[8][1], 0.0], points[1]));
392 assert!(close([curve[16][0], curve[16][1], 0.0], points[2]));
393 assert!(curve[4][1] > 0.5 + 1e-3, "{:?}", curve[4]);
395 }
396
397 #[test]
398 fn a_closed_smooth_curve_does_not_repeat_its_start() {
399 let square = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
400 let curve = smooth(&square, true, 4);
401 assert_eq!(curve.len(), 4 * 4);
402 assert_ne!(curve.last(), curve.first());
403 }
404
405 #[test]
406 fn two_points_stay_a_straight_line() {
407 let points = [[0.0, 0.0], [1.0, 0.0]];
408 assert_eq!(smooth(&points, false, 8), points.to_vec());
409 }
410}