Skip to main content

gled_model/
patching.rs

1//! The patch: which fixtures a project has, how they are chained and
2//! addressed (the topology), and the layouts that arrange them for effects.
3//! Fixture definitions, the lamps that can be patched, are assets.
4
5pub mod addressing;
6pub mod color_orders;
7pub mod definition;
8pub mod fixture;
9pub mod geometry;
10pub mod layout;
11pub mod topology;
12
13use crate::asset::Asset;
14use crate::collection::Collections;
15use crate::naming::unique_name;
16use crate::patching::definition::FixtureDefinition;
17use crate::patching::fixture::{
18    ArtnetDMXAddress, CHANNELS_PER_UNIVERSE, Fixture, FixtureId, LayoutPosition, MAX_UNIVERSE,
19};
20use crate::patching::layout::{Layout, LayoutRef};
21use crate::patching::topology::Topology;
22use serde::{Deserialize, Serialize};
23use std::collections::{BTreeMap, BTreeSet};
24use std::sync::Arc;
25
26/// All fixture data of a project: the topology and the layouts derived from it.
27#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
28#[serde(default)]
29pub struct Patch {
30    pub topology: Topology,
31    pub layouts: Vec<Layout>,
32}
33
34impl Patch {
35    pub fn layout(&self, layout: LayoutRef) -> Option<&Layout> {
36        match layout {
37            LayoutRef::Topology => Some(&self.topology.default_layout),
38            LayoutRef::Layout(index) => self.layouts.get(index),
39        }
40    }
41
42    pub fn layout_mut(&mut self, layout: LayoutRef) -> Option<&mut Layout> {
43        match layout {
44            LayoutRef::Topology => Some(&mut self.topology.default_layout),
45            LayoutRef::Layout(index) => self.layouts.get_mut(index),
46        }
47    }
48
49    pub fn fixture(&self, id: FixtureId) -> Option<&Fixture> {
50        self.topology.fixtures.get(&id)
51    }
52
53    /// The fixture's name, or its id where the topology does not hold it.
54    pub fn fixture_name(&self, id: FixtureId) -> String {
55        self.fixture(id)
56            .map(|fixture| fixture.name.clone())
57            .unwrap_or_else(|| id.to_string())
58    }
59
60    /// Every layout, the topology first, in stack order.
61    pub fn layout_refs(&self) -> Vec<LayoutRef> {
62        std::iter::once(LayoutRef::Topology)
63            .chain((0..self.layouts.len()).map(LayoutRef::Layout))
64            .collect()
65    }
66
67    pub fn add_layout(&mut self) -> usize {
68        let name = unique_name("Layout", self.layouts.iter().map(|l| l.name.as_str()));
69        self.layouts.push(Layout::new(name));
70        self.layouts.len() - 1
71    }
72
73    pub fn duplicate_layout(&mut self, index: usize) -> Option<usize> {
74        let mut copy = self.layouts.get(index)?.clone();
75        copy.name = unique_name(
76            &format!("{} copy", copy.name),
77            self.layouts.iter().map(|l| l.name.as_str()),
78        );
79        self.layouts.insert(index + 1, copy);
80        Some(index + 1)
81    }
82
83    pub fn remove_layout(&mut self, index: usize) {
84        if index < self.layouts.len() {
85            self.layouts.remove(index);
86        }
87    }
88
89    /// Adds a fixture to the topology, and therefore to the default layout.
90    pub fn add_fixture(&mut self, fixture: Fixture, position: LayoutPosition) -> FixtureId {
91        self.topology.add_fixture(fixture, position)
92    }
93
94    pub fn remove_fixture(&mut self, id: FixtureId) {
95        self.topology.remove_fixture(id, &mut self.layouts);
96    }
97
98    /// Adds `id` to a user layout at its default-layout position, so a new
99    /// layout starts out as a copy of the physical arrangement.
100    pub fn add_member(&mut self, layout: LayoutRef, id: FixtureId) {
101        let Some(position) = self.topology.default_layout.members.get(&id).copied() else {
102            return;
103        };
104        if let Some(layout) = self.layout_mut(layout) {
105            layout.members.entry(id).or_insert(position);
106        }
107    }
108
109    /// Includes or excludes a fixture. Membership of the default layout cannot
110    /// be changed — it holds every fixture by construction.
111    pub fn set_member(&mut self, layout: LayoutRef, id: FixtureId, member: bool) {
112        if layout == LayoutRef::Topology {
113            return;
114        }
115        if member {
116            self.add_member(layout, id);
117        } else if let Some(layout) = self.layout_mut(layout) {
118            layout.forget(id);
119        }
120    }
121
122    /// The definition asset a fixture uses, if it has one and it is loaded.
123    /// There is no built-in fallback — definitions live in the asset dir.
124    pub fn definition(
125        &self,
126        id: FixtureId,
127        collections: &Collections,
128    ) -> Option<Arc<Asset<FixtureDefinition>>> {
129        let definition = self.fixture(id)?.definition?;
130        Asset::get(definition, collections)
131    }
132
133    /// The address of every fixture, derived by walking each chain from its
134    /// port. A fixture never straddles a universe boundary; it starts the next
135    /// universe instead.
136    ///
137    /// A fixture whose definition is missing has an unknown channel width, so
138    /// it and everything after it in its chain get no address at all rather
139    /// than a wrong one. The same goes for a fixture past [`MAX_UNIVERSE`]:
140    /// Art-Net cannot address its universe; and for a fixture wider than a
141    /// universe (a malformed definition), which fits nowhere without
142    /// straddling a boundary and leaves the channels after it unknown.
143    pub fn addresses(&self, collections: &Collections) -> BTreeMap<FixtureId, ArtnetDMXAddress> {
144        let mut addresses = BTreeMap::new();
145        for head in self.topology.chain_heads() {
146            let Some(port) = self.topology.ports.get(&head) else {
147                continue;
148            };
149            let mut universe = port.universe;
150            let mut channel = port.channel.min(CHANNELS_PER_UNIVERSE);
151            for id in self.topology.chain_from(head) {
152                let Some(definition) = self.definition(id, collections) else {
153                    break;
154                };
155                let width = definition.data.channel_count();
156                // The output reports it (`PatchError::FixtureTooWide`); this
157                // runs every frame in the patch inspector, so it stays quiet.
158                if width > CHANNELS_PER_UNIVERSE {
159                    break;
160                }
161                if channel + width > CHANNELS_PER_UNIVERSE {
162                    universe = universe.saturating_add(1);
163                    channel = 0;
164                }
165                if universe > MAX_UNIVERSE {
166                    break;
167                }
168                addresses.insert(id, ArtnetDMXAddress::from_pair((universe, channel)));
169                channel += width;
170            }
171        }
172        addresses
173    }
174
175    /// Every universe the rig's chains reach, ignoring `placing` — the chain
176    /// about to be given one.
177    ///
178    /// A chain wider than a universe runs into the next, so the universe a
179    /// port names is not the only one its chain occupies. A fixture never
180    /// straddles a boundary, so each universe a chain reaches has a fixture
181    /// starting in it.
182    fn occupied_universes(
183        &self,
184        collections: &Collections,
185        placing: Option<FixtureId>,
186    ) -> BTreeSet<u16> {
187        let mine = |id: FixtureId| placing == Some(self.topology.chain_head_of(id));
188
189        let mut occupied: BTreeSet<u16> = self
190            .topology
191            .ports
192            .iter()
193            .filter(|(id, _)| !mine(**id))
194            .map(|(_, port)| port.universe)
195            .collect();
196        occupied.extend(
197            self.addresses(collections)
198                .iter()
199                .filter(|(id, _)| !mine(**id))
200                .map(|(_, address)| address.universe()),
201        );
202        occupied
203    }
204
205    /// A universe for a chain that is about to need one: the lowest no other
206    /// chain reaches. Starting it inside a universe an existing chain has
207    /// already spilled into would overlap them.
208    pub fn lowest_free_universe(
209        &self,
210        collections: &Collections,
211        placing: Option<FixtureId>,
212    ) -> u16 {
213        let occupied = self.occupied_universes(collections, placing);
214        (0..)
215            .find(|universe| !occupied.contains(universe))
216            .unwrap_or(0)
217    }
218
219    /// Which mode of its own definition `id` uses in `mode` of `layout`.
220    /// Unassigned fixtures fall back to their definition's default mode.
221    /// `None` when the fixture has no definition to take a mode from.
222    pub fn fixture_mode(
223        &self,
224        layout: LayoutRef,
225        mode: usize,
226        id: FixtureId,
227        collections: &Collections,
228    ) -> Option<usize> {
229        let definition = self.definition(id, collections)?;
230        let definition = &definition.data;
231        if definition.modes.is_empty() {
232            return None;
233        }
234        let assigned = self
235            .layout(layout)
236            .and_then(|layout| layout.mode(mode).fixture_modes.get(&id).copied());
237        Some(match assigned {
238            Some(index) if index < definition.modes.len() => index,
239            _ => definition.default_mode(),
240        })
241    }
242
243    pub fn set_fixture_mode(
244        &mut self,
245        layout: LayoutRef,
246        mode: usize,
247        id: FixtureId,
248        value: usize,
249    ) {
250        if let Some(layout) = self.layout_mut(layout) {
251            layout.mode_mut(mode).fixture_modes.insert(id, value);
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests;