gled-model 2.31.2

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
use crate::naming::unique_name;
use crate::patching::fixture::{FixtureId, LayoutPosition};
use emath::{Pos2, Rect, Vec2};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;

/// Side of the square editor scene, in scene units. One unit is a centimetre,
/// so the scene covers 20 x 20 m. Effects map this square onto their [0,1] UV
/// space, which is why it lives with the data rather than the editor.
pub const SCENE_SIZE: f32 = 2000.0;

/// The square the scene occupies. Effects and the live preview map exactly this
/// rectangle onto their [0,1] UV space, so the editor shows the same one.
pub fn scene_bounds() -> Rect {
    Rect::from_min_size(Pos2::ZERO, Vec2::splat(SCENE_SIZE))
}

/// One mode of a layout: which mode of its own definition each fixture uses.
///
/// The map is sparse — a fixture that is not listed falls back to its
/// definition's default mode. The layout's default mode is simply the one whose
/// map is always empty, so a user who never opens this concept still gets
/// sensible behaviour.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
pub struct LayoutMode {
    pub name: String,
    pub fixture_modes: BTreeMap<FixtureId, usize>,
}

impl LayoutMode {
    pub const DEFAULT_NAME: &'static str = "Default";

    pub fn default_mode() -> Self {
        Self {
            name: Self::DEFAULT_NAME.to_string(),
            fixture_modes: BTreeMap::new(),
        }
    }

    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            fixture_modes: BTreeMap::new(),
        }
    }
}

/// A subset of all fixtures in a user definable 2d arrangement.
///
/// Layouts may overlap freely. The topology's default layout is a `Layout` too,
/// and is the one that necessarily contains every fixture.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Layout {
    pub name: String,
    /// The picture behind the layout, by its path in the asset repository
    /// (see `asset::picture`).
    pub background: Option<PathBuf>,
    pub members: BTreeMap<FixtureId, LayoutPosition>,
    /// `modes[0]` is the default mode and is never removed.
    #[serde(deserialize_with = "at_least_the_default_mode")]
    pub modes: Vec<LayoutMode>,
}

/// A layout's modes as saved, with the default mode put back if a file has
/// none: every lookup falls back to `modes[0]`.
fn at_least_the_default_mode<'de, D: Deserializer<'de>>(
    deserializer: D,
) -> Result<Vec<LayoutMode>, D::Error> {
    let mut modes = Vec::<LayoutMode>::deserialize(deserializer)?;
    if modes.is_empty() {
        modes.push(LayoutMode::default_mode());
    }
    Ok(modes)
}

impl Default for Layout {
    fn default() -> Self {
        Self::new("Layout")
    }
}

impl Layout {
    /// What the layout every fixture belongs to is called. Every collection in
    /// patching has a default element, so nothing has to be learned to use one.
    pub const DEFAULT_NAME: &'static str = "Default";

    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            background: None,
            members: BTreeMap::new(),
            modes: vec![LayoutMode::default_mode()],
        }
    }

    pub fn contains(&self, id: FixtureId) -> bool {
        self.members.contains_key(&id)
    }

    /// The mode at `index`, falling back to the default mode.
    pub fn mode(&self, index: usize) -> &LayoutMode {
        self.modes.get(index).unwrap_or(&self.modes[0])
    }

    pub fn mode_mut(&mut self, index: usize) -> &mut LayoutMode {
        let index = if index < self.modes.len() { index } else { 0 };
        &mut self.modes[index]
    }

    /// Adds a mode with a name not yet used in this layout.
    pub fn add_mode(&mut self) -> usize {
        let name = unique_name("Mode", self.modes.iter().map(|mode| mode.name.as_str()));
        self.modes.push(LayoutMode::new(name));
        self.modes.len() - 1
    }

    /// Removes a mode. The default mode cannot be removed.
    pub fn remove_mode(&mut self, index: usize) {
        if index > 0 && index < self.modes.len() {
            self.modes.remove(index);
        }
    }

    pub fn forget(&mut self, id: FixtureId) {
        self.members.remove(&id);
        for mode in &mut self.modes {
            mode.fixture_modes.remove(&id);
        }
    }
}

/// Which layout the editor is pointed at. The topology's default layout is
/// addressed separately from the user layouts.
#[derive(
    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default,
)]
pub enum LayoutRef {
    #[default]
    Topology,
    Layout(usize),
}

impl LayoutRef {
    /// The position in the layout stack (`Patch::layout_refs`): 0 is the
    /// topology, then the user layouts in order. OSC and MIDI address layouts
    /// by this index.
    pub fn stack_index(self) -> usize {
        match self {
            LayoutRef::Topology => 0,
            LayoutRef::Layout(index) => index + 1,
        }
    }
}

/// Which layout, and which of its modes, drives a tile or an effect.
#[derive(
    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord,
)]
pub struct LayoutSelection {
    pub layout: LayoutRef,
    pub mode: usize,
}

impl LayoutSelection {
    pub fn new(layout: LayoutRef, mode: usize) -> Self {
        Self { layout, mode }
    }
}

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

    #[test]
    fn a_new_layout_has_only_the_default_mode() {
        let layout = Layout::new("Stage");
        assert_eq!(layout.modes.len(), 1);
        assert_eq!(layout.modes[0].name, LayoutMode::DEFAULT_NAME);
        assert!(layout.modes[0].fixture_modes.is_empty());
    }

    #[test]
    fn the_default_mode_cannot_be_removed() {
        let mut layout = Layout::new("Stage");
        layout.add_mode();
        layout.remove_mode(0);
        assert_eq!(layout.modes.len(), 2);
        layout.remove_mode(1);
        assert_eq!(layout.modes.len(), 1);
    }

    #[test]
    fn an_out_of_range_mode_falls_back_to_the_default() {
        let layout = Layout::new("Stage");
        assert_eq!(layout.mode(7).name, LayoutMode::DEFAULT_NAME);
    }

    #[test]
    fn a_layout_saved_without_modes_gets_its_default_mode_back() {
        let layout: Layout = serde_json::from_str(
            r#"{"name": "Stage", "background": null, "members": {}, "modes": []}"#,
        )
        .expect("deserialize");
        assert_eq!(layout.modes, [LayoutMode::default_mode()]);
        assert_eq!(layout.mode(3).name, LayoutMode::DEFAULT_NAME);
    }

    #[test]
    fn forgetting_a_fixture_clears_its_mode_assignments() {
        let mut layout = Layout::new("Stage");
        let id = FixtureId::new();
        layout.members.insert(id, LayoutPosition::default());
        let mode = layout.add_mode();
        layout.mode_mut(mode).fixture_modes.insert(id, 1);

        layout.forget(id);

        assert!(!layout.contains(id));
        assert!(layout.mode(mode).fixture_modes.is_empty());
    }

    #[test]
    fn the_stack_index_is_the_position_in_the_layout_stack() {
        let mut patch = crate::patching::Patch::default();
        patch.add_layout();
        patch.add_layout();

        let refs = patch.layout_refs();

        assert_eq!(refs.len(), 3);
        for (index, layout) in refs.into_iter().enumerate() {
            assert_eq!(layout.stack_index(), index, "{layout:?}");
        }
    }
}