Skip to main content

gled_model/patching/
fixture.rs

1use crate::asset_id::AssetId;
2use crate::patching::definition::FixtureDefinition;
3use emath::Pos2;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(
8    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
9)]
10#[serde(transparent)]
11pub struct FixtureId(pub Uuid);
12
13impl FixtureId {
14    pub fn new() -> Self {
15        Self(Uuid::new_v4())
16    }
17}
18
19impl std::fmt::Display for FixtureId {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        self.0.fmt(f)
22    }
23}
24
25/// One scene unit is one centimetre, so the 2000-unit scene covers 20 x 20 m.
26pub const UNITS_PER_METRE: f32 = 100.0;
27
28/// Where a fixture sits inside one layout. Every layout keeps its own position
29/// per fixture, so a layout may arrange fixtures however it likes.
30///
31/// The definition's physical size is authoritative: `scale` is a deviation from
32/// it, per axis, so a layout may deliberately distort a fixture.
33#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
34pub struct LayoutPosition {
35    pub pos: Pos2,
36    pub angle: f32,
37    pub scale: [f32; 2],
38}
39
40impl Default for LayoutPosition {
41    fn default() -> Self {
42        Self {
43            pos: Pos2::new(100.0, 100.0),
44            angle: 0.0,
45            scale: [1.0, 1.0],
46        }
47    }
48}
49
50impl LayoutPosition {
51    pub fn new(pos: Pos2, angle: f32, scale: [f32; 2]) -> Self {
52        Self { pos, angle, scale }
53    }
54
55    pub fn at(pos: Pos2) -> Self {
56        Self {
57            pos,
58            ..Default::default()
59        }
60    }
61}
62
63/// An instance of a fixture definition. Its address is derived from the
64/// topology, and its position from whichever layout it is looked up in, so
65/// neither is stored here.
66#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
67pub struct Fixture {
68    pub id: FixtureId,
69    pub name: String,
70    /// `None` when no definition is assigned; such a fixture gets no address
71    /// and no mode.
72    pub definition: Option<AssetId<FixtureDefinition>>,
73}
74
75impl Fixture {
76    pub fn new(name: impl Into<String>) -> Self {
77        Self {
78            id: FixtureId::new(),
79            name: name.into(),
80            definition: None,
81        }
82    }
83
84    /// A new fixture of `definition`.
85    pub fn with_definition(
86        name: impl Into<String>,
87        definition: AssetId<FixtureDefinition>,
88    ) -> Self {
89        Self {
90            definition: Some(definition),
91            ..Self::new(name)
92        }
93    }
94}
95
96impl Default for Fixture {
97    fn default() -> Self {
98        Self::new("Fixture")
99    }
100}
101
102/// Channels addressable in one DMX universe. gled-gpu checks at compile time
103/// that its output buffer holds the same number per universe, so derived
104/// addresses and the DMX packing cannot drift apart.
105pub const CHANNELS_PER_UNIVERSE: u16 = 512;
106
107/// Channels of one lamp: gled renders RGB triples.
108pub const CHANNELS_PER_LAMP: u16 = 3;
109
110/// Whole lamps in one universe; its last two channels stay unused.
111pub const LAMPS_PER_UNIVERSE: u16 = CHANNELS_PER_UNIVERSE / CHANNELS_PER_LAMP;
112
113/// The highest universe a fixture can be addressed in: the highest Art-Net
114/// port address, which has 15 bits (net, sub-net and universe).
115pub const MAX_UNIVERSE: u16 = 0x7fff;
116
117/// A DMX address: a universe and a channel in it. Derived from the patch
118/// (see `Patch::addresses`), which keeps the universe at most
119/// [`MAX_UNIVERSE`] and the channel inside the universe.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
121pub struct ArtnetDMXAddress {
122    universe: u16,
123    channel: u16,
124}
125
126impl ArtnetDMXAddress {
127    pub fn into_pair(self) -> (u16, u16) {
128        (self.universe, self.channel)
129    }
130
131    pub fn from_pair((universe, channel): (u16, u16)) -> Self {
132        Self { universe, channel }
133    }
134
135    pub fn universe(self) -> u16 {
136        self.universe
137    }
138
139    pub fn channel(self) -> u16 {
140        self.channel
141    }
142
143    /// The lamp of its universe that the channel `channel_offset` past this
144    /// address belongs to: lamp `i` starts at channel `3 i`.
145    pub fn lamp_index(self, channel_offset: u16) -> usize {
146        // in usize: a malformed definition's offset may reach `u16::MAX`
147        (usize::from(self.channel) + usize::from(channel_offset)) / usize::from(CHANNELS_PER_LAMP)
148    }
149}
150
151impl std::fmt::Display for ArtnetDMXAddress {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        let (universe, channel) = self.into_pair();
154        write!(f, "{universe}.{channel}")
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn address_pair_round_trip() {
164        for pair in [
165            (0u16, 0u16),
166            (1, 5),
167            (7, 511),
168            (127, 0),
169            (128, 256),
170            (1000, 3),
171        ] {
172            assert_eq!(ArtnetDMXAddress::from_pair(pair).into_pair(), pair);
173        }
174    }
175
176    #[test]
177    fn a_pixels_lamp_index_counts_rgb_triples_from_the_address() {
178        let address = ArtnetDMXAddress::from_pair((2, 6));
179        assert_eq!(address.lamp_index(0), 2);
180        assert_eq!(address.lamp_index(3), 3);
181        assert_eq!(address.lamp_index(5), 3);
182        // a saturated offset of a malformed definition does not overflow
183        assert_eq!(
184            ArtnetDMXAddress::from_pair((0, 511)).lamp_index(u16::MAX),
185            (511 + 65_535) / 3
186        );
187    }
188
189    #[test]
190    fn the_highest_art_net_universe_round_trips() {
191        let address = ArtnetDMXAddress::from_pair((MAX_UNIVERSE, 511));
192        assert_eq!(address.into_pair(), (32767, 511));
193    }
194
195    #[test]
196    fn address_keeps_universe_and_channel_apart() {
197        let address = ArtnetDMXAddress::from_pair((3, 17));
198        assert_eq!(address.universe(), 3);
199        assert_eq!(address.channel(), 17);
200    }
201}