gled-model 2.32.1

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
//! The patch: which fixtures a project has, how they are chained and
//! addressed (the topology), and the layouts that arrange them for effects.
//! Fixture definitions, the lamps that can be patched, are assets.

pub mod addressing;
pub mod color_orders;
pub mod definition;
pub mod fixture;
pub mod geometry;
pub mod layout;
pub mod topology;

use crate::asset::Asset;
use crate::collection::Collections;
use crate::naming::unique_name;
use crate::patching::definition::FixtureDefinition;
use crate::patching::fixture::{
    ArtnetDMXAddress, CHANNELS_PER_UNIVERSE, Fixture, FixtureId, LayoutPosition, MAX_UNIVERSE,
};
use crate::patching::layout::{Layout, LayoutRef};
use crate::patching::topology::Topology;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;

/// All fixture data of a project: the topology and the layouts derived from it.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(default)]
pub struct Patch {
    pub topology: Topology,
    pub layouts: Vec<Layout>,
}

impl Patch {
    pub fn layout(&self, layout: LayoutRef) -> Option<&Layout> {
        match layout {
            LayoutRef::Topology => Some(&self.topology.default_layout),
            LayoutRef::Layout(index) => self.layouts.get(index),
        }
    }

    pub fn layout_mut(&mut self, layout: LayoutRef) -> Option<&mut Layout> {
        match layout {
            LayoutRef::Topology => Some(&mut self.topology.default_layout),
            LayoutRef::Layout(index) => self.layouts.get_mut(index),
        }
    }

    pub fn fixture(&self, id: FixtureId) -> Option<&Fixture> {
        self.topology.fixtures.get(&id)
    }

    /// The fixture's name, or its id where the topology does not hold it.
    pub fn fixture_name(&self, id: FixtureId) -> String {
        self.fixture(id)
            .map(|fixture| fixture.name.clone())
            .unwrap_or_else(|| id.to_string())
    }

    /// Every layout, the topology first, in stack order.
    pub fn layout_refs(&self) -> Vec<LayoutRef> {
        std::iter::once(LayoutRef::Topology)
            .chain((0..self.layouts.len()).map(LayoutRef::Layout))
            .collect()
    }

    pub fn add_layout(&mut self) -> usize {
        let name = unique_name("Layout", self.layouts.iter().map(|l| l.name.as_str()));
        self.layouts.push(Layout::new(name));
        self.layouts.len() - 1
    }

    pub fn duplicate_layout(&mut self, index: usize) -> Option<usize> {
        let mut copy = self.layouts.get(index)?.clone();
        copy.name = unique_name(
            &format!("{} copy", copy.name),
            self.layouts.iter().map(|l| l.name.as_str()),
        );
        self.layouts.insert(index + 1, copy);
        Some(index + 1)
    }

    pub fn remove_layout(&mut self, index: usize) {
        if index < self.layouts.len() {
            self.layouts.remove(index);
        }
    }

    /// Adds a fixture to the topology, and therefore to the default layout.
    pub fn add_fixture(&mut self, fixture: Fixture, position: LayoutPosition) -> FixtureId {
        self.topology.add_fixture(fixture, position)
    }

    pub fn remove_fixture(&mut self, id: FixtureId) {
        self.topology.remove_fixture(id, &mut self.layouts);
    }

    /// Adds `id` to a user layout at its default-layout position, so a new
    /// layout starts out as a copy of the physical arrangement.
    pub fn add_member(&mut self, layout: LayoutRef, id: FixtureId) {
        let Some(position) = self.topology.default_layout.members.get(&id).copied() else {
            return;
        };
        if let Some(layout) = self.layout_mut(layout) {
            layout.members.entry(id).or_insert(position);
        }
    }

    /// Includes or excludes a fixture. Membership of the default layout cannot
    /// be changed — it holds every fixture by construction.
    pub fn set_member(&mut self, layout: LayoutRef, id: FixtureId, member: bool) {
        if layout == LayoutRef::Topology {
            return;
        }
        if member {
            self.add_member(layout, id);
        } else if let Some(layout) = self.layout_mut(layout) {
            layout.forget(id);
        }
    }

    /// The definition asset a fixture uses, if it has one and it is loaded.
    /// There is no built-in fallback — definitions live in the asset dir.
    pub fn definition(
        &self,
        id: FixtureId,
        collections: &Collections,
    ) -> Option<Arc<Asset<FixtureDefinition>>> {
        let definition = self.fixture(id)?.definition?;
        Asset::get(definition, collections)
    }

    /// The address of every fixture, derived by walking each chain from its
    /// port. A fixture never straddles a universe boundary; it starts the next
    /// universe instead.
    ///
    /// A fixture whose definition is missing has an unknown channel width, so
    /// it and everything after it in its chain get no address at all rather
    /// than a wrong one. The same goes for a fixture past [`MAX_UNIVERSE`]:
    /// Art-Net cannot address its universe; and for a fixture wider than a
    /// universe (a malformed definition), which fits nowhere without
    /// straddling a boundary and leaves the channels after it unknown.
    pub fn addresses(&self, collections: &Collections) -> BTreeMap<FixtureId, ArtnetDMXAddress> {
        let mut addresses = BTreeMap::new();
        for head in self.topology.chain_heads() {
            let Some(port) = self.topology.ports.get(&head) else {
                continue;
            };
            let mut universe = port.universe;
            let mut channel = port.channel.min(CHANNELS_PER_UNIVERSE);
            for id in self.topology.chain_from(head) {
                let Some(definition) = self.definition(id, collections) else {
                    break;
                };
                let width = definition.data.channel_count();
                // The output reports it (`PatchError::FixtureTooWide`); this
                // runs every frame in the patch inspector, so it stays quiet.
                if width > CHANNELS_PER_UNIVERSE {
                    break;
                }
                if channel + width > CHANNELS_PER_UNIVERSE {
                    universe = universe.saturating_add(1);
                    channel = 0;
                }
                if universe > MAX_UNIVERSE {
                    break;
                }
                addresses.insert(id, ArtnetDMXAddress::from_pair((universe, channel)));
                channel += width;
            }
        }
        addresses
    }

    /// Every universe the rig's chains reach, ignoring `placing` — the chain
    /// about to be given one.
    ///
    /// A chain wider than a universe runs into the next, so the universe a
    /// port names is not the only one its chain occupies. A fixture never
    /// straddles a boundary, so each universe a chain reaches has a fixture
    /// starting in it.
    fn occupied_universes(
        &self,
        collections: &Collections,
        placing: Option<FixtureId>,
    ) -> BTreeSet<u16> {
        let mine = |id: FixtureId| placing == Some(self.topology.chain_head_of(id));

        let mut occupied: BTreeSet<u16> = self
            .topology
            .ports
            .iter()
            .filter(|(id, _)| !mine(**id))
            .map(|(_, port)| port.universe)
            .collect();
        occupied.extend(
            self.addresses(collections)
                .iter()
                .filter(|(id, _)| !mine(**id))
                .map(|(_, address)| address.universe()),
        );
        occupied
    }

    /// A universe for a chain that is about to need one: the lowest no other
    /// chain reaches. Starting it inside a universe an existing chain has
    /// already spilled into would overlap them.
    pub fn lowest_free_universe(
        &self,
        collections: &Collections,
        placing: Option<FixtureId>,
    ) -> u16 {
        let occupied = self.occupied_universes(collections, placing);
        (0..)
            .find(|universe| !occupied.contains(universe))
            .unwrap_or(0)
    }

    /// Which mode of its own definition `id` uses in `mode` of `layout`.
    /// Unassigned fixtures fall back to their definition's default mode.
    /// `None` when the fixture has no definition to take a mode from.
    pub fn fixture_mode(
        &self,
        layout: LayoutRef,
        mode: usize,
        id: FixtureId,
        collections: &Collections,
    ) -> Option<usize> {
        let definition = self.definition(id, collections)?;
        let definition = &definition.data;
        if definition.modes.is_empty() {
            return None;
        }
        let assigned = self
            .layout(layout)
            .and_then(|layout| layout.mode(mode).fixture_modes.get(&id).copied());
        Some(match assigned {
            Some(index) if index < definition.modes.len() => index,
            _ => definition.default_mode(),
        })
    }

    pub fn set_fixture_mode(
        &mut self,
        layout: LayoutRef,
        mode: usize,
        id: FixtureId,
        value: usize,
    ) {
        if let Some(layout) = self.layout_mut(layout) {
            layout.mode_mut(mode).fixture_modes.insert(id, value);
        }
    }
}

#[cfg(test)]
mod tests;