Skip to main content

gled_model/patching/
color_orders.rs

1//! The channel order of every DMX slot the patch drives in another order than
2//! RGB, so the output can swap each triple before it leaves.
3
4use crate::collection::Collections;
5use crate::patching::Patch;
6use crate::patching::definition::ColorOrder;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10/// Wire order of every lamp that is not RGB, by Art-Net universe. A lamp is
11/// one RGB triple of a universe's buffer: lamp `i` starts at channel `3 i`.
12#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct ColorOrders {
14    /// Only universes holding at least one lamp that is not RGB.
15    universes: BTreeMap<u16, Vec<ColorOrder>>,
16}
17
18impl ColorOrders {
19    /// Records the order of one lamp. An RGB lamp in a universe without other
20    /// orders stores nothing.
21    pub fn set(&mut self, universe: u16, lamp: usize, order: ColorOrder) {
22        if order.is_identity() && !self.universes.contains_key(&universe) {
23            return;
24        }
25        let lamps = self.universes.entry(universe).or_default();
26        if lamps.len() <= lamp {
27            lamps.resize(lamp + 1, ColorOrder::Rgb);
28        }
29        lamps[lamp] = order;
30    }
31
32    pub fn order(&self, universe: u16, lamp: usize) -> ColorOrder {
33        self.universes
34            .get(&universe)
35            .and_then(|lamps| lamps.get(lamp))
36            .copied()
37            .unwrap_or_default()
38    }
39
40    pub fn is_empty(&self) -> bool {
41        self.universes.is_empty()
42    }
43
44    /// Reorders the RGB triples of one universe's buffer into each lamp's
45    /// wire order.
46    pub fn correct(&self, universe: u16, data: &mut [u8]) {
47        let Some(lamps) = self.universes.get(&universe) else {
48            return;
49        };
50        for (triple, order) in data.as_chunks_mut::<3>().0.iter_mut().zip(lamps) {
51            order.correct(triple);
52        }
53    }
54}
55
56impl Patch {
57    /// The wire order of every addressed pixel that is not RGB. It belongs to
58    /// the DMX slot, not to what the slot samples, so layouts and modes do not
59    /// change it.
60    pub fn color_orders(&self, collections: &Collections) -> ColorOrders {
61        let mut orders = ColorOrders::default();
62        for (id, address) in self.addresses(collections) {
63            let Some(definition) = self.definition(id, collections) else {
64                continue;
65            };
66            let definition = &definition.data;
67            for (index, pixel) in definition.pixels.iter().enumerate() {
68                let Some(order) = pixel.color_order() else {
69                    continue;
70                };
71                let lamp = address.lamp_index(definition.channel_offset(index));
72                orders.set(address.universe(), lamp, order);
73            }
74        }
75        orders
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::asset_id::AssetId;
83    use crate::patching::definition::FixtureDefinition;
84    use crate::patching::fixture::{Fixture, FixtureId, LayoutPosition};
85    use crate::test_support::{collections_with, rgb_bar};
86
87    /// A bar of `pixel_count` RGB pixels, 10 cm apart.
88    fn bar(pixel_count: usize) -> FixtureDefinition {
89        rgb_bar(pixel_count, pixel_count as f32 * 0.1)
90    }
91
92    fn add(patch: &mut Patch, definition: AssetId<FixtureDefinition>) -> FixtureId {
93        patch.add_fixture(
94            Fixture::with_definition("bar", definition),
95            LayoutPosition::default(),
96        )
97    }
98
99    #[test]
100    fn a_grb_pixel_puts_its_order_into_its_lamp_slot() {
101        let mut definition = bar(4);
102        definition.pixels[2].set_color_order(ColorOrder::Grb);
103        let (collections, ids) = collections_with(vec![definition]);
104        let mut patch = Patch::default();
105        add(&mut patch, ids[0]);
106
107        let orders = patch.color_orders(&collections);
108
109        assert_eq!(orders.order(0, 2), ColorOrder::Grb);
110        assert_eq!(orders.order(0, 0), ColorOrder::Rgb);
111    }
112
113    #[test]
114    fn color_orders_follow_the_chain_address() {
115        let mut grb = bar(4);
116        grb.pixels[0].set_color_order(ColorOrder::Grb);
117        let (collections, ids) = collections_with(vec![bar(4), grb]);
118        let mut patch = Patch::default();
119        let head = add(&mut patch, ids[0]);
120        let tail = add(&mut patch, ids[1]);
121        patch.topology.make_successor(head, tail);
122
123        // the head takes lamps 0..4, so the tail's first pixel is lamp 4
124        assert_eq!(
125            patch.color_orders(&collections).order(0, 4),
126            ColorOrder::Grb
127        );
128    }
129
130    #[test]
131    fn an_rgb_patch_needs_no_reordering() {
132        let (collections, ids) = collections_with(vec![bar(4)]);
133        let mut patch = Patch::default();
134        add(&mut patch, ids[0]);
135        assert!(patch.color_orders(&collections).is_empty());
136    }
137
138    #[test]
139    fn correcting_a_universe_touches_only_its_permuted_lamps() {
140        let mut orders = ColorOrders::default();
141        orders.set(0, 2, ColorOrder::Grb);
142        let mut data = [0u8; 512];
143        data[0..3].copy_from_slice(&[1, 2, 3]);
144        data[6..9].copy_from_slice(&[1, 2, 3]);
145
146        let mut other_universe = data;
147        orders.correct(0, &mut data);
148        orders.correct(1, &mut other_universe);
149
150        assert_eq!(&data[0..3], &[1, 2, 3]);
151        assert_eq!(&data[6..9], &[2, 1, 3]);
152        assert_eq!(&other_universe[6..9], &[1, 2, 3]);
153    }
154}