gled-model 2.31.1

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
//! The channel order of every DMX slot the patch drives in another order than
//! RGB, so the output can swap each triple before it leaves.

use crate::collection::Collections;
use crate::patching::Patch;
use crate::patching::definition::ColorOrder;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// Wire order of every lamp that is not RGB, by Art-Net universe. A lamp is
/// one RGB triple of a universe's buffer: lamp `i` starts at channel `3 i`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ColorOrders {
    /// Only universes holding at least one lamp that is not RGB.
    universes: BTreeMap<u16, Vec<ColorOrder>>,
}

impl ColorOrders {
    /// Records the order of one lamp. An RGB lamp in a universe without other
    /// orders stores nothing.
    pub fn set(&mut self, universe: u16, lamp: usize, order: ColorOrder) {
        if order.is_identity() && !self.universes.contains_key(&universe) {
            return;
        }
        let lamps = self.universes.entry(universe).or_default();
        if lamps.len() <= lamp {
            lamps.resize(lamp + 1, ColorOrder::Rgb);
        }
        lamps[lamp] = order;
    }

    pub fn order(&self, universe: u16, lamp: usize) -> ColorOrder {
        self.universes
            .get(&universe)
            .and_then(|lamps| lamps.get(lamp))
            .copied()
            .unwrap_or_default()
    }

    pub fn is_empty(&self) -> bool {
        self.universes.is_empty()
    }

    /// Reorders the RGB triples of one universe's buffer into each lamp's
    /// wire order.
    pub fn correct(&self, universe: u16, data: &mut [u8]) {
        let Some(lamps) = self.universes.get(&universe) else {
            return;
        };
        for (triple, order) in data.as_chunks_mut::<3>().0.iter_mut().zip(lamps) {
            order.correct(triple);
        }
    }
}

impl Patch {
    /// The wire order of every addressed pixel that is not RGB. It belongs to
    /// the DMX slot, not to what the slot samples, so layouts and modes do not
    /// change it.
    pub fn color_orders(&self, collections: &Collections) -> ColorOrders {
        let mut orders = ColorOrders::default();
        for (id, address) in self.addresses(collections) {
            let Some(definition) = self.definition(id, collections) else {
                continue;
            };
            let definition = &definition.data;
            for (index, pixel) in definition.pixels.iter().enumerate() {
                let Some(order) = pixel.color_order() else {
                    continue;
                };
                let lamp = address.lamp_index(definition.channel_offset(index));
                orders.set(address.universe(), lamp, order);
            }
        }
        orders
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::asset_id::AssetId;
    use crate::patching::definition::FixtureDefinition;
    use crate::patching::fixture::{Fixture, FixtureId, LayoutPosition};
    use crate::test_support::{collections_with, rgb_bar};

    /// A bar of `pixel_count` RGB pixels, 10 cm apart.
    fn bar(pixel_count: usize) -> FixtureDefinition {
        rgb_bar(pixel_count, pixel_count as f32 * 0.1)
    }

    fn add(patch: &mut Patch, definition: AssetId<FixtureDefinition>) -> FixtureId {
        patch.add_fixture(
            Fixture::with_definition("bar", definition),
            LayoutPosition::default(),
        )
    }

    #[test]
    fn a_grb_pixel_puts_its_order_into_its_lamp_slot() {
        let mut definition = bar(4);
        definition.pixels[2].set_color_order(ColorOrder::Grb);
        let (collections, ids) = collections_with(vec![definition]);
        let mut patch = Patch::default();
        add(&mut patch, ids[0]);

        let orders = patch.color_orders(&collections);

        assert_eq!(orders.order(0, 2), ColorOrder::Grb);
        assert_eq!(orders.order(0, 0), ColorOrder::Rgb);
    }

    #[test]
    fn color_orders_follow_the_chain_address() {
        let mut grb = bar(4);
        grb.pixels[0].set_color_order(ColorOrder::Grb);
        let (collections, ids) = collections_with(vec![bar(4), grb]);
        let mut patch = Patch::default();
        let head = add(&mut patch, ids[0]);
        let tail = add(&mut patch, ids[1]);
        patch.topology.make_successor(head, tail);

        // the head takes lamps 0..4, so the tail's first pixel is lamp 4
        assert_eq!(
            patch.color_orders(&collections).order(0, 4),
            ColorOrder::Grb
        );
    }

    #[test]
    fn an_rgb_patch_needs_no_reordering() {
        let (collections, ids) = collections_with(vec![bar(4)]);
        let mut patch = Patch::default();
        add(&mut patch, ids[0]);
        assert!(patch.color_orders(&collections).is_empty());
    }

    #[test]
    fn correcting_a_universe_touches_only_its_permuted_lamps() {
        let mut orders = ColorOrders::default();
        orders.set(0, 2, ColorOrder::Grb);
        let mut data = [0u8; 512];
        data[0..3].copy_from_slice(&[1, 2, 3]);
        data[6..9].copy_from_slice(&[1, 2, 3]);

        let mut other_universe = data;
        orders.correct(0, &mut data);
        orders.correct(1, &mut other_universe);

        assert_eq!(&data[0..3], &[1, 2, 3]);
        assert_eq!(&data[6..9], &[2, 1, 3]);
        assert_eq!(&other_universe[6..9], &[1, 2, 3]);
    }
}