Skip to main content

gled_model/patching/definition/
generators.rs

1//! Laying out many pixels at once: a grid, a ring, a path. Positions are in
2//! metres in the definition's frame, x to the right and y down like the
3//! canvas, and pixels come out in DMX order.
4
5use crate::patching::definition::{ColorOrder, Pixel};
6use std::f32::consts::{FRAC_PI_2, TAU};
7use strum::{Display, EnumIter};
8
9/// What every generated pixel shares.
10#[derive(Debug, Clone, PartialEq)]
11pub struct PixelTemplate {
12    /// Pixels are named `prefix` plus a running number.
13    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
33/// One pixel at `at_m`.
34pub fn single(template: &PixelTemplate, at_m: [f32; 2]) -> Vec<Pixel> {
35    vec![template.pixel(0, at_m, 0.0)]
36}
37
38/// Which way the DMX order runs through a grid. It reads as the generator
39/// form words it.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Display, EnumIter)]
41pub enum GridOrder {
42    /// Row by row, every row in the same direction.
43    #[default]
44    #[strum(serialize = "row by row")]
45    Rows,
46    #[strum(serialize = "column by column")]
47    Columns,
48    /// Row by row, every other row backwards, as a strip folded into a matrix.
49    #[strum(serialize = "rows, snaking")]
50    SerpentineRows,
51    #[strum(serialize = "columns, snaking")]
52    SerpentineColumns,
53}
54
55/// The corner the first pixel sits in. It reads as the generator form words
56/// it.
57#[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
80/// A grid of pixels centred on `centre_m`.
81pub 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    /// Where the first pixel sits, in degrees clockwise from twelve o'clock.
126    pub start_deg: f32,
127    pub clockwise: bool,
128    /// Turn each pixel along the ring, as LEDs on a bent strip are.
129    pub along_ring: bool,
130}
131
132/// Pixels evenly around a circle.
133pub fn ring(template: &PixelTemplate, spec: &RingSpec) -> Vec<Pixel> {
134    let step = if spec.clockwise { TAU } else { -TAU } / spec.count.max(1) as f32;
135    // y points down, so a growing angle runs clockwise; 0 is three o'clock
136    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/// A path of straight segments through `points_m`.
156#[derive(Debug, Clone, PartialEq)]
157pub struct PathSpec {
158    pub points_m: Vec<[f32; 2]>,
159    /// Back from the last point to the first: a loop.
160    pub closed: bool,
161    pub count: usize,
162    /// On an open path, keep half a pitch free at either end instead of
163    /// putting the first and last pixel on the end points.
164    pub inset_ends: bool,
165}
166
167/// Pixels spread evenly along a path by arc length, each turned along its
168/// segment. On an open path the first sits on its start and the last on its
169/// end, as the old SVG lamps placed their LEDs; on a loop they are spaced
170/// evenly all the way round.
171pub 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        // a path without length: every pixel on its one point
190        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
228/// A smooth curve through `points`, as straight pieces: `steps` of them
229/// between two neighbouring points. It passes through every point (a
230/// Catmull-Rom spline), so a path clicked corner by corner becomes the curve
231/// a bent strip makes. A closed curve runs back round to the first point.
232pub 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        // the last piece ends where the curve began
261        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        // y points down: clockwise from twelve is three o'clock, to the right
339        assert!(close(pixels[1].position_m, [0.5, 0.0]));
340        // turned along the ring: at twelve the pixel lies horizontally
341        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        // turned along the segment it sits on
364        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        // a right triangle with sides 1, 1 and sqrt 2
370        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        // between the corners it bulges rather than running straight
394        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}