gled-model 2.32.1

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
use crate::asset_id::AssetId;
use crate::patching::definition::FixtureDefinition;
use emath::Pos2;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(
    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
)]
#[serde(transparent)]
pub struct FixtureId(pub Uuid);

impl FixtureId {
    pub fn new() -> Self {
        Self(Uuid::new_v4())
    }
}

impl std::fmt::Display for FixtureId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

/// One scene unit is one centimetre, so the 2000-unit scene covers 20 x 20 m.
pub const UNITS_PER_METRE: f32 = 100.0;

/// Where a fixture sits inside one layout. Every layout keeps its own position
/// per fixture, so a layout may arrange fixtures however it likes.
///
/// The definition's physical size is authoritative: `scale` is a deviation from
/// it, per axis, so a layout may deliberately distort a fixture.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct LayoutPosition {
    pub pos: Pos2,
    pub angle: f32,
    pub scale: [f32; 2],
}

impl Default for LayoutPosition {
    fn default() -> Self {
        Self {
            pos: Pos2::new(100.0, 100.0),
            angle: 0.0,
            scale: [1.0, 1.0],
        }
    }
}

impl LayoutPosition {
    pub fn new(pos: Pos2, angle: f32, scale: [f32; 2]) -> Self {
        Self { pos, angle, scale }
    }

    pub fn at(pos: Pos2) -> Self {
        Self {
            pos,
            ..Default::default()
        }
    }
}

/// An instance of a fixture definition. Its address is derived from the
/// topology, and its position from whichever layout it is looked up in, so
/// neither is stored here.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Fixture {
    pub id: FixtureId,
    pub name: String,
    /// `None` when no definition is assigned; such a fixture gets no address
    /// and no mode.
    pub definition: Option<AssetId<FixtureDefinition>>,
}

impl Fixture {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            id: FixtureId::new(),
            name: name.into(),
            definition: None,
        }
    }

    /// A new fixture of `definition`.
    pub fn with_definition(
        name: impl Into<String>,
        definition: AssetId<FixtureDefinition>,
    ) -> Self {
        Self {
            definition: Some(definition),
            ..Self::new(name)
        }
    }
}

impl Default for Fixture {
    fn default() -> Self {
        Self::new("Fixture")
    }
}

/// Channels addressable in one DMX universe. gled-gpu checks at compile time
/// that its output buffer holds the same number per universe, so derived
/// addresses and the DMX packing cannot drift apart.
pub const CHANNELS_PER_UNIVERSE: u16 = 512;

/// Channels of one lamp: gled renders RGB triples.
pub const CHANNELS_PER_LAMP: u16 = 3;

/// Whole lamps in one universe; its last two channels stay unused.
pub const LAMPS_PER_UNIVERSE: u16 = CHANNELS_PER_UNIVERSE / CHANNELS_PER_LAMP;

/// The highest universe a fixture can be addressed in: the highest Art-Net
/// port address, which has 15 bits (net, sub-net and universe).
pub const MAX_UNIVERSE: u16 = 0x7fff;

/// A DMX address: a universe and a channel in it. Derived from the patch
/// (see `Patch::addresses`), which keeps the universe at most
/// [`MAX_UNIVERSE`] and the channel inside the universe.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ArtnetDMXAddress {
    universe: u16,
    channel: u16,
}

impl ArtnetDMXAddress {
    pub fn into_pair(self) -> (u16, u16) {
        (self.universe, self.channel)
    }

    pub fn from_pair((universe, channel): (u16, u16)) -> Self {
        Self { universe, channel }
    }

    pub fn universe(self) -> u16 {
        self.universe
    }

    pub fn channel(self) -> u16 {
        self.channel
    }

    /// The lamp of its universe that the channel `channel_offset` past this
    /// address belongs to: lamp `i` starts at channel `3 i`.
    pub fn lamp_index(self, channel_offset: u16) -> usize {
        // in usize: a malformed definition's offset may reach `u16::MAX`
        (usize::from(self.channel) + usize::from(channel_offset)) / usize::from(CHANNELS_PER_LAMP)
    }
}

impl std::fmt::Display for ArtnetDMXAddress {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let (universe, channel) = self.into_pair();
        write!(f, "{universe}.{channel}")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn address_pair_round_trip() {
        for pair in [
            (0u16, 0u16),
            (1, 5),
            (7, 511),
            (127, 0),
            (128, 256),
            (1000, 3),
        ] {
            assert_eq!(ArtnetDMXAddress::from_pair(pair).into_pair(), pair);
        }
    }

    #[test]
    fn a_pixels_lamp_index_counts_rgb_triples_from_the_address() {
        let address = ArtnetDMXAddress::from_pair((2, 6));
        assert_eq!(address.lamp_index(0), 2);
        assert_eq!(address.lamp_index(3), 3);
        assert_eq!(address.lamp_index(5), 3);
        // a saturated offset of a malformed definition does not overflow
        assert_eq!(
            ArtnetDMXAddress::from_pair((0, 511)).lamp_index(u16::MAX),
            (511 + 65_535) / 3
        );
    }

    #[test]
    fn the_highest_art_net_universe_round_trips() {
        let address = ArtnetDMXAddress::from_pair((MAX_UNIVERSE, 511));
        assert_eq!(address.into_pair(), (32767, 511));
    }

    #[test]
    fn address_keeps_universe_and_channel_apart() {
        let address = ArtnetDMXAddress::from_pair((3, 17));
        assert_eq!(address.universe(), 3);
        assert_eq!(address.channel(), 17);
    }
}