gled-model 2.32.1

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
use crate::naming::unique_name;
use crate::patching::fixture::{Fixture, FixtureId, LayoutPosition};
use crate::patching::layout::Layout;
use bimap::BiMap;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};

/// Where a chain of fixtures attaches to the network. The universe is routed to
/// a physical device by the project's existing `OutputRoutings`.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[serde(default)]
pub struct Port {
    pub universe: u16,
    /// First DMX channel the chain occupies. Zero unless the chain is pinned
    /// somewhere other than the start of its universe.
    pub channel: u16,
}

impl Port {
    /// A port at the start of `universe`.
    pub fn new(universe: u16) -> Self {
        Self {
            universe,
            channel: 0,
        }
    }
}

/// The default layout plus how the fixtures are connected to ports and in which
/// order. One per project.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(default)]
pub struct Topology {
    pub fixtures: BTreeMap<FixtureId, Fixture>,
    /// Necessarily contains every fixture — the patching topology cannot be
    /// defined otherwise. Maintained by `add_fixture`/`remove_fixture`.
    pub default_layout: Layout,
    /// Directed parent → child edges. A fixture that is nobody's child is a
    /// chain head and owns a port.
    pub edges: BiMap<FixtureId, FixtureId>,
    pub ports: BTreeMap<FixtureId, Port>,
}

impl Default for Topology {
    fn default() -> Self {
        Self {
            fixtures: BTreeMap::new(),
            default_layout: Layout::new(Layout::DEFAULT_NAME),
            edges: BiMap::new(),
            ports: BTreeMap::new(),
        }
    }
}

impl Topology {
    pub fn add_fixture(&mut self, mut fixture: Fixture, position: LayoutPosition) -> FixtureId {
        fixture.name = unique_name(
            &fixture.name,
            self.fixtures.values().map(|other| other.name.as_str()),
        );
        let id = fixture.id;
        self.fixtures.insert(id, fixture);
        self.default_layout.members.insert(id, position);
        self.ports
            .insert(id, Port::new(self.lowest_unused_universe()));
        debug_assert!(self.invariant_holds());
        id
    }

    /// Removes a fixture everywhere: from the topology, its chain, and every
    /// user layout.
    pub fn remove_fixture(&mut self, id: FixtureId, layouts: &mut [Layout]) {
        // Stitching the chain back together would silently re-address unrelated
        // fixtures, so the successor becomes a chain head of its own instead.
        self.make_leaf(id);
        self.edges.remove_by_right(&id);
        self.ports.remove(&id);
        self.fixtures.remove(&id);
        self.default_layout.forget(id);
        for layout in layouts {
            layout.forget(id);
        }
        debug_assert!(self.invariant_holds());
    }

    /// Detaches whatever follows `id`, making that successor a chain head.
    pub fn make_leaf(&mut self, id: FixtureId) {
        if let Some((_, successor)) = self.edges.remove_by_left(&id) {
            self.ports
                .insert(successor, Port::new(self.lowest_unused_universe()));
        }
    }

    /// The head of the chain `id` belongs to.
    pub fn chain_head_of(&self, id: FixtureId) -> FixtureId {
        let mut head = id;
        let mut seen = 0;
        while let Some(parent) = self.edges.get_by_right(&head).copied() {
            head = parent;
            seen += 1;
            if seen > self.fixtures.len() {
                break;
            }
        }
        head
    }

    /// Connects `child` after `parent`. Rejected if it would form a cycle.
    ///
    /// A chain's network output belongs to the chain, not to the fixture that
    /// happens to be first: putting a new fixture in front of a chain carries
    /// the port's settings over to it.
    pub fn make_successor(&mut self, parent: FixtureId, child: FixtureId) -> bool {
        if parent == child || self.is_reachable(child, parent) {
            return false;
        }
        // Only a child that already leads a chain hands its port on: appending
        // a loose fixture must not re-address the chain it is appended to.
        let child_leads_chain = self.successor(child).is_some();
        self.make_leaf(parent);
        self.edges.remove_by_right(&child);
        let inherited = self.ports.remove(&child);
        self.edges.insert(parent, child);
        if let Some(port) = inherited
            && child_leads_chain
        {
            let head = self.chain_head_of(parent);
            self.ports.insert(head, port);
        }
        true
    }

    pub fn is_chain_head(&self, id: FixtureId) -> bool {
        self.edges.get_by_right(&id).is_none()
    }

    pub fn successor(&self, id: FixtureId) -> Option<FixtureId> {
        self.edges.get_by_left(&id).copied()
    }

    /// Every fixture from `head` down its chain, `head` first.
    pub fn chain_from(&self, head: FixtureId) -> Vec<FixtureId> {
        let mut chain = vec![];
        let mut current = Some(head);
        while let Some(id) = current {
            if chain.contains(&id) {
                break;
            }
            chain.push(id);
            current = self.successor(id);
        }
        chain
    }

    pub fn chain_heads(&self) -> Vec<FixtureId> {
        self.fixtures
            .keys()
            .copied()
            .filter(|id| self.is_chain_head(*id))
            .collect()
    }

    /// A provisional universe: the lowest no port names. It cannot see how
    /// wide a chain is, so a chain that overflows into the next universe is
    /// invisible to it — [`crate::patching::Patch::lowest_free_universe`] is
    /// what actually places a chain.
    fn lowest_unused_universe(&self) -> u16 {
        let used: BTreeSet<u16> = self.ports.values().map(|port| port.universe).collect();
        (0..).find(|universe| !used.contains(universe)).unwrap_or(0)
    }

    /// `true` if `target` is reachable from `start` by following parent →
    /// child edges. Used to keep connections acyclic.
    pub fn is_reachable(&self, start: FixtureId, target: FixtureId) -> bool {
        let mut seen = BTreeSet::new();
        let mut current = Some(start);
        while let Some(id) = current {
            if id == target {
                return true;
            }
            if !seen.insert(id) {
                return false;
            }
            current = self.edges.get_by_left(&id).copied();
        }
        false
    }

    fn invariant_holds(&self) -> bool {
        self.fixtures.len() == self.default_layout.members.len()
            && self
                .fixtures
                .keys()
                .all(|id| self.default_layout.contains(*id))
    }
}

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

    /// Ports saved before they carried a start channel must still load.
    #[test]
    fn a_port_without_a_start_channel_loads() {
        let port: Port = serde_json::from_str(r#"{"universe":5}"#).expect("deserialize");
        assert_eq!(
            port,
            Port {
                universe: 5,
                channel: 0
            }
        );
    }

    /// The network output belongs to the chain: prepending a fixture moves the
    /// port's universe and start channel onto the new head.
    #[test]
    fn a_port_wanders_onto_a_newly_prepended_head() {
        let mut topology = Topology::default();
        let head = topology.add_fixture(Fixture::new("head"), LayoutPosition::default());
        let tail = topology.add_fixture(Fixture::new("tail"), LayoutPosition::default());
        let new_head = topology.add_fixture(Fixture::new("new head"), LayoutPosition::default());
        topology.make_successor(head, tail);
        topology.ports.insert(
            head,
            Port {
                universe: 7,
                channel: 24,
            },
        );

        topology.make_successor(new_head, head);

        assert_eq!(
            topology.ports.get(&new_head),
            Some(&Port {
                universe: 7,
                channel: 24
            })
        );
        assert!(!topology.ports.contains_key(&head));
    }

    /// Appending a loose fixture must leave the chain it joins addressed as it
    /// was.
    #[test]
    fn appending_a_fixture_keeps_the_chains_port() {
        let mut topology = Topology::default();
        let head = topology.add_fixture(Fixture::new("head"), LayoutPosition::default());
        let tail = topology.add_fixture(Fixture::new("tail"), LayoutPosition::default());
        topology.ports.insert(
            head,
            Port {
                universe: 3,
                channel: 0,
            },
        );

        topology.make_successor(head, tail);

        assert_eq!(
            topology.ports.get(&head),
            Some(&Port {
                universe: 3,
                channel: 0
            })
        );
    }

    fn topology_with(count: usize) -> (Topology, Vec<FixtureId>) {
        let mut topology = Topology::default();
        let ids = (0..count)
            .map(|i| topology.add_fixture(Fixture::new(format!("f{i}")), LayoutPosition::default()))
            .collect();
        (topology, ids)
    }

    #[test]
    fn default_layout_always_holds_every_fixture() {
        let (mut topology, ids) = topology_with(1);
        assert!(topology.default_layout.contains(ids[0]));
        topology.remove_fixture(ids[0], &mut []);
        assert!(!topology.default_layout.contains(ids[0]));
        assert!(topology.fixtures.is_empty());
    }

    #[test]
    fn removing_a_fixture_removes_it_from_every_layout() {
        let (mut topology, ids) = topology_with(1);
        let mut layouts = vec![Layout::new("a"), Layout::new("b")];
        for layout in &mut layouts {
            layout.members.insert(ids[0], LayoutPosition::default());
        }

        topology.remove_fixture(ids[0], &mut layouts);

        assert!(layouts.iter().all(|layout| !layout.contains(ids[0])));
    }

    #[test]
    fn new_chains_take_the_lowest_unused_universe() {
        let (topology, ids) = topology_with(3);
        let universes: Vec<u16> = ids.iter().map(|id| topology.ports[id].universe).collect();
        assert_eq!(universes, vec![0, 1, 2]);
    }

    #[test]
    fn connecting_a_fixture_takes_away_its_port() {
        let (mut topology, ids) = topology_with(2);
        assert!(topology.make_successor(ids[0], ids[1]));
        assert!(!topology.ports.contains_key(&ids[1]));
        assert_eq!(topology.chain_from(ids[0]), vec![ids[0], ids[1]]);
        assert_eq!(topology.chain_heads(), vec![ids[0]]);
    }

    #[test]
    fn disconnecting_gives_the_successor_its_own_port() {
        let (mut topology, ids) = topology_with(2);
        topology.make_successor(ids[0], ids[1]);
        topology.make_leaf(ids[0]);
        assert!(topology.ports.contains_key(&ids[1]));
        assert_eq!(topology.chain_from(ids[0]), vec![ids[0]]);
    }

    #[test]
    fn cycles_are_rejected() {
        let (mut topology, ids) = topology_with(3);
        topology.make_successor(ids[0], ids[1]);
        topology.make_successor(ids[1], ids[2]);
        assert!(!topology.make_successor(ids[2], ids[0]));
        assert!(!topology.make_successor(ids[0], ids[0]));
        assert_eq!(topology.chain_from(ids[0]), vec![ids[0], ids[1], ids[2]]);
    }

    #[test]
    fn removing_a_middle_fixture_reheads_its_successor() {
        let (mut topology, ids) = topology_with(3);
        topology.make_successor(ids[0], ids[1]);
        topology.make_successor(ids[1], ids[2]);

        topology.remove_fixture(ids[1], &mut []);

        assert_eq!(topology.chain_from(ids[0]), vec![ids[0]]);
        assert!(topology.ports.contains_key(&ids[2]));
    }

    #[test]
    fn fixture_names_are_unique() {
        let mut topology = Topology::default();
        topology.add_fixture(Fixture::new("Light"), LayoutPosition::default());
        let second = topology.add_fixture(Fixture::new("Light"), LayoutPosition::default());
        assert_eq!(topology.fixtures[&second].name, "Light 2");
    }
}