Skip to main content

gled_model/patching/
topology.rs

1use crate::naming::unique_name;
2use crate::patching::fixture::{Fixture, FixtureId, LayoutPosition};
3use crate::patching::layout::Layout;
4use bimap::BiMap;
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, BTreeSet};
7
8/// Where a chain of fixtures attaches to the network. The universe is routed to
9/// a physical device by the project's existing `OutputRoutings`.
10#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
11#[serde(default)]
12pub struct Port {
13    pub universe: u16,
14    /// First DMX channel the chain occupies. Zero unless the chain is pinned
15    /// somewhere other than the start of its universe.
16    pub channel: u16,
17}
18
19impl Port {
20    /// A port at the start of `universe`.
21    pub fn new(universe: u16) -> Self {
22        Self {
23            universe,
24            channel: 0,
25        }
26    }
27}
28
29/// The default layout plus how the fixtures are connected to ports and in which
30/// order. One per project.
31#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
32#[serde(default)]
33pub struct Topology {
34    pub fixtures: BTreeMap<FixtureId, Fixture>,
35    /// Necessarily contains every fixture — the patching topology cannot be
36    /// defined otherwise. Maintained by `add_fixture`/`remove_fixture`.
37    pub default_layout: Layout,
38    /// Directed parent → child edges. A fixture that is nobody's child is a
39    /// chain head and owns a port.
40    pub edges: BiMap<FixtureId, FixtureId>,
41    pub ports: BTreeMap<FixtureId, Port>,
42}
43
44impl Default for Topology {
45    fn default() -> Self {
46        Self {
47            fixtures: BTreeMap::new(),
48            default_layout: Layout::new(Layout::DEFAULT_NAME),
49            edges: BiMap::new(),
50            ports: BTreeMap::new(),
51        }
52    }
53}
54
55impl Topology {
56    pub fn add_fixture(&mut self, mut fixture: Fixture, position: LayoutPosition) -> FixtureId {
57        fixture.name = unique_name(
58            &fixture.name,
59            self.fixtures.values().map(|other| other.name.as_str()),
60        );
61        let id = fixture.id;
62        self.fixtures.insert(id, fixture);
63        self.default_layout.members.insert(id, position);
64        self.ports
65            .insert(id, Port::new(self.lowest_unused_universe()));
66        debug_assert!(self.invariant_holds());
67        id
68    }
69
70    /// Removes a fixture everywhere: from the topology, its chain, and every
71    /// user layout.
72    pub fn remove_fixture(&mut self, id: FixtureId, layouts: &mut [Layout]) {
73        // Stitching the chain back together would silently re-address unrelated
74        // fixtures, so the successor becomes a chain head of its own instead.
75        self.make_leaf(id);
76        self.edges.remove_by_right(&id);
77        self.ports.remove(&id);
78        self.fixtures.remove(&id);
79        self.default_layout.forget(id);
80        for layout in layouts {
81            layout.forget(id);
82        }
83        debug_assert!(self.invariant_holds());
84    }
85
86    /// Detaches whatever follows `id`, making that successor a chain head.
87    pub fn make_leaf(&mut self, id: FixtureId) {
88        if let Some((_, successor)) = self.edges.remove_by_left(&id) {
89            self.ports
90                .insert(successor, Port::new(self.lowest_unused_universe()));
91        }
92    }
93
94    /// The head of the chain `id` belongs to.
95    pub fn chain_head_of(&self, id: FixtureId) -> FixtureId {
96        let mut head = id;
97        let mut seen = 0;
98        while let Some(parent) = self.edges.get_by_right(&head).copied() {
99            head = parent;
100            seen += 1;
101            if seen > self.fixtures.len() {
102                break;
103            }
104        }
105        head
106    }
107
108    /// Connects `child` after `parent`. Rejected if it would form a cycle.
109    ///
110    /// A chain's network output belongs to the chain, not to the fixture that
111    /// happens to be first: putting a new fixture in front of a chain carries
112    /// the port's settings over to it.
113    pub fn make_successor(&mut self, parent: FixtureId, child: FixtureId) -> bool {
114        if parent == child || self.is_reachable(child, parent) {
115            return false;
116        }
117        // Only a child that already leads a chain hands its port on: appending
118        // a loose fixture must not re-address the chain it is appended to.
119        let child_leads_chain = self.successor(child).is_some();
120        self.make_leaf(parent);
121        self.edges.remove_by_right(&child);
122        let inherited = self.ports.remove(&child);
123        self.edges.insert(parent, child);
124        if let Some(port) = inherited
125            && child_leads_chain
126        {
127            let head = self.chain_head_of(parent);
128            self.ports.insert(head, port);
129        }
130        true
131    }
132
133    pub fn is_chain_head(&self, id: FixtureId) -> bool {
134        self.edges.get_by_right(&id).is_none()
135    }
136
137    pub fn successor(&self, id: FixtureId) -> Option<FixtureId> {
138        self.edges.get_by_left(&id).copied()
139    }
140
141    /// Every fixture from `head` down its chain, `head` first.
142    pub fn chain_from(&self, head: FixtureId) -> Vec<FixtureId> {
143        let mut chain = vec![];
144        let mut current = Some(head);
145        while let Some(id) = current {
146            if chain.contains(&id) {
147                break;
148            }
149            chain.push(id);
150            current = self.successor(id);
151        }
152        chain
153    }
154
155    pub fn chain_heads(&self) -> Vec<FixtureId> {
156        self.fixtures
157            .keys()
158            .copied()
159            .filter(|id| self.is_chain_head(*id))
160            .collect()
161    }
162
163    /// A provisional universe: the lowest no port names. It cannot see how
164    /// wide a chain is, so a chain that overflows into the next universe is
165    /// invisible to it — [`crate::patching::Patch::lowest_free_universe`] is
166    /// what actually places a chain.
167    fn lowest_unused_universe(&self) -> u16 {
168        let used: BTreeSet<u16> = self.ports.values().map(|port| port.universe).collect();
169        (0..).find(|universe| !used.contains(universe)).unwrap_or(0)
170    }
171
172    /// `true` if `target` is reachable from `start` by following parent →
173    /// child edges. Used to keep connections acyclic.
174    pub fn is_reachable(&self, start: FixtureId, target: FixtureId) -> bool {
175        let mut seen = BTreeSet::new();
176        let mut current = Some(start);
177        while let Some(id) = current {
178            if id == target {
179                return true;
180            }
181            if !seen.insert(id) {
182                return false;
183            }
184            current = self.edges.get_by_left(&id).copied();
185        }
186        false
187    }
188
189    fn invariant_holds(&self) -> bool {
190        self.fixtures.len() == self.default_layout.members.len()
191            && self
192                .fixtures
193                .keys()
194                .all(|id| self.default_layout.contains(*id))
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    /// Ports saved before they carried a start channel must still load.
203    #[test]
204    fn a_port_without_a_start_channel_loads() {
205        let port: Port = serde_json::from_str(r#"{"universe":5}"#).expect("deserialize");
206        assert_eq!(
207            port,
208            Port {
209                universe: 5,
210                channel: 0
211            }
212        );
213    }
214
215    /// The network output belongs to the chain: prepending a fixture moves the
216    /// port's universe and start channel onto the new head.
217    #[test]
218    fn a_port_wanders_onto_a_newly_prepended_head() {
219        let mut topology = Topology::default();
220        let head = topology.add_fixture(Fixture::new("head"), LayoutPosition::default());
221        let tail = topology.add_fixture(Fixture::new("tail"), LayoutPosition::default());
222        let new_head = topology.add_fixture(Fixture::new("new head"), LayoutPosition::default());
223        topology.make_successor(head, tail);
224        topology.ports.insert(
225            head,
226            Port {
227                universe: 7,
228                channel: 24,
229            },
230        );
231
232        topology.make_successor(new_head, head);
233
234        assert_eq!(
235            topology.ports.get(&new_head),
236            Some(&Port {
237                universe: 7,
238                channel: 24
239            })
240        );
241        assert!(!topology.ports.contains_key(&head));
242    }
243
244    /// Appending a loose fixture must leave the chain it joins addressed as it
245    /// was.
246    #[test]
247    fn appending_a_fixture_keeps_the_chains_port() {
248        let mut topology = Topology::default();
249        let head = topology.add_fixture(Fixture::new("head"), LayoutPosition::default());
250        let tail = topology.add_fixture(Fixture::new("tail"), LayoutPosition::default());
251        topology.ports.insert(
252            head,
253            Port {
254                universe: 3,
255                channel: 0,
256            },
257        );
258
259        topology.make_successor(head, tail);
260
261        assert_eq!(
262            topology.ports.get(&head),
263            Some(&Port {
264                universe: 3,
265                channel: 0
266            })
267        );
268    }
269
270    fn topology_with(count: usize) -> (Topology, Vec<FixtureId>) {
271        let mut topology = Topology::default();
272        let ids = (0..count)
273            .map(|i| topology.add_fixture(Fixture::new(format!("f{i}")), LayoutPosition::default()))
274            .collect();
275        (topology, ids)
276    }
277
278    #[test]
279    fn default_layout_always_holds_every_fixture() {
280        let (mut topology, ids) = topology_with(1);
281        assert!(topology.default_layout.contains(ids[0]));
282        topology.remove_fixture(ids[0], &mut []);
283        assert!(!topology.default_layout.contains(ids[0]));
284        assert!(topology.fixtures.is_empty());
285    }
286
287    #[test]
288    fn removing_a_fixture_removes_it_from_every_layout() {
289        let (mut topology, ids) = topology_with(1);
290        let mut layouts = vec![Layout::new("a"), Layout::new("b")];
291        for layout in &mut layouts {
292            layout.members.insert(ids[0], LayoutPosition::default());
293        }
294
295        topology.remove_fixture(ids[0], &mut layouts);
296
297        assert!(layouts.iter().all(|layout| !layout.contains(ids[0])));
298    }
299
300    #[test]
301    fn new_chains_take_the_lowest_unused_universe() {
302        let (topology, ids) = topology_with(3);
303        let universes: Vec<u16> = ids.iter().map(|id| topology.ports[id].universe).collect();
304        assert_eq!(universes, vec![0, 1, 2]);
305    }
306
307    #[test]
308    fn connecting_a_fixture_takes_away_its_port() {
309        let (mut topology, ids) = topology_with(2);
310        assert!(topology.make_successor(ids[0], ids[1]));
311        assert!(!topology.ports.contains_key(&ids[1]));
312        assert_eq!(topology.chain_from(ids[0]), vec![ids[0], ids[1]]);
313        assert_eq!(topology.chain_heads(), vec![ids[0]]);
314    }
315
316    #[test]
317    fn disconnecting_gives_the_successor_its_own_port() {
318        let (mut topology, ids) = topology_with(2);
319        topology.make_successor(ids[0], ids[1]);
320        topology.make_leaf(ids[0]);
321        assert!(topology.ports.contains_key(&ids[1]));
322        assert_eq!(topology.chain_from(ids[0]), vec![ids[0]]);
323    }
324
325    #[test]
326    fn cycles_are_rejected() {
327        let (mut topology, ids) = topology_with(3);
328        topology.make_successor(ids[0], ids[1]);
329        topology.make_successor(ids[1], ids[2]);
330        assert!(!topology.make_successor(ids[2], ids[0]));
331        assert!(!topology.make_successor(ids[0], ids[0]));
332        assert_eq!(topology.chain_from(ids[0]), vec![ids[0], ids[1], ids[2]]);
333    }
334
335    #[test]
336    fn removing_a_middle_fixture_reheads_its_successor() {
337        let (mut topology, ids) = topology_with(3);
338        topology.make_successor(ids[0], ids[1]);
339        topology.make_successor(ids[1], ids[2]);
340
341        topology.remove_fixture(ids[1], &mut []);
342
343        assert_eq!(topology.chain_from(ids[0]), vec![ids[0]]);
344        assert!(topology.ports.contains_key(&ids[2]));
345    }
346
347    #[test]
348    fn fixture_names_are_unique() {
349        let mut topology = Topology::default();
350        topology.add_fixture(Fixture::new("Light"), LayoutPosition::default());
351        let second = topology.add_fixture(Fixture::new("Light"), LayoutPosition::default());
352        assert_eq!(topology.fixtures[&second].name, "Light 2");
353    }
354}