gled-model 2.32.1

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
pub mod io;
pub mod scene_instance_path;

use io::{ArtnetConfig, ArtnetControlConfig, ChannelOverwrites, OscConfig};

use super::{
    AssetTrait, midi_controller::MidiController, output_device::routing::OutputRoutings,
    scene::instance::SceneInstance,
};
use crate::{
    asset::{
        palette::Palette,
        project::scene_instance_path::SceneInstanceUnion,
        scene::{Scene, grid::GridLocation},
    },
    asset_id::AssetId,
    collection::Collections,
    input::{
        event::{GamepadEvent, InputEvent},
        key_code::KeyCode,
    },
    patching::Patch,
    sound::{
        AudioDeviceId,
        trigger_ref::{CollectSoundTriggers, SoundTriggerSet},
    },
};
use serde::{Deserialize, Serialize};
use std::{
    collections::{BTreeSet, HashMap},
    sync::Arc,
};
use strum::{Display, EnumIter};

/// Which line of the scene grid is marked. It reads as the label the status
/// bar shows.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Display, EnumIter)]
#[serde(rename_all = "lowercase")]
pub enum GridHighlight {
    #[default]
    #[strum(serialize = "Last Row")]
    Row,
    #[strum(serialize = "Last Column")]
    Column,
    None,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(default)]
pub struct Project {
    pub palette: Option<Palette>,
    /// All fixture data: the topology and the layouts derived from it.
    pub patch: Patch,
    #[serde(default = "Project::default_grid_width")]
    pub grid_width: usize,
    #[serde(default = "Project::default_grid_height")]
    pub grid_height: usize,
    pub grid_highlight: GridHighlight,
    pub scenes_instances_grid: HashMap<GridLocation, SceneInstance>,
    pub channel_overwrites: ChannelOverwrites,
    pub output_routings: Arc<OutputRoutings>,
    pub artnet_config: ArtnetConfig,
    pub tap_input_events: BTreeSet<InputEvent>,
    pub blackout_input_events: BTreeSet<InputEvent>,
    pub blackout_hold_input_events: BTreeSet<InputEvent>,
    pub half_input_events: BTreeSet<InputEvent>,
    pub double_input_events: BTreeSet<InputEvent>,
    pub midi_active_mappings: HashMap<String, Option<AssetId<MidiController>>>,
    pub main_dimmer: f32,
    pub audio_input_device: Option<AudioDeviceId>,
    pub artnet_control_config: ArtnetControlConfig,
    pub osc_config: OscConfig,
}

impl Default for Project {
    fn default() -> Self {
        Self {
            palette: None,
            patch: Patch::default(),
            grid_width: Self::DEFAULT_GRID_WIDTH,
            grid_height: Self::DEFAULT_GRID_HEIGHT,
            grid_highlight: GridHighlight::default(),
            scenes_instances_grid: HashMap::new(),
            channel_overwrites: Default::default(),
            output_routings: Default::default(),
            artnet_config: Default::default(),
            tap_input_events: std::iter::once(InputEvent::Key(KeyCode::T))
                .chain(std::iter::once(InputEvent::Gamepad(GamepadEvent::Mode(0))))
                .collect(),
            blackout_input_events: std::iter::once(InputEvent::Key(KeyCode::B)).collect(),
            blackout_hold_input_events: std::iter::once(InputEvent::Key(KeyCode::N))
                .chain(std::iter::once(InputEvent::Gamepad(GamepadEvent::Start(0))))
                .collect(),
            half_input_events: std::iter::once(InputEvent::Key(KeyCode::Minus)).collect(),
            double_input_events: std::iter::once(InputEvent::Key(KeyCode::Plus)).collect(),
            midi_active_mappings: HashMap::new(),
            main_dimmer: 1.0,
            audio_input_device: None,
            artnet_control_config: ArtnetControlConfig::default(),
            osc_config: OscConfig::default(),
        }
    }
}

impl Project {
    pub(crate) const MIN_GRID_WIDTH: usize = 2;
    pub(crate) const MIN_GRID_HEIGHT: usize = 2;
    pub(crate) const DEFAULT_GRID_WIDTH: usize = 8;
    pub(crate) const DEFAULT_GRID_HEIGHT: usize = 6;

    fn default_grid_width() -> usize {
        Self::DEFAULT_GRID_WIDTH
    }

    fn default_grid_height() -> usize {
        Self::DEFAULT_GRID_HEIGHT
    }

    pub fn grid_width(&self) -> usize {
        self.grid_width.max(Self::MIN_GRID_WIDTH)
    }

    pub fn grid_height(&self) -> usize {
        self.grid_height.max(Self::MIN_GRID_HEIGHT)
    }

    pub fn quick_row_index(&self) -> usize {
        self.grid_height() - 1
    }

    pub(crate) fn quick_col_index(&self) -> usize {
        self.grid_width() - 1
    }

    pub fn set_grid_size(&mut self, width: usize, height: usize) {
        self.grid_width = width.max(Self::MIN_GRID_WIDTH);
        self.grid_height = height.max(Self::MIN_GRID_HEIGHT);

        let max_width = self.grid_width();
        let max_height = self.grid_height();
        self.scenes_instances_grid
            .retain(|location, _| location.col < max_width && location.row < max_height);
    }

    pub fn next_empty_grid_location(&self, start: GridLocation) -> GridLocation {
        let grid_height = self.grid_height();
        let grid_width = self.grid_width();
        for row in 0..grid_height {
            let row = (start.row + row) % grid_height;
            for col in 0..grid_width {
                let col = (start.col + col) % grid_width;
                let location = GridLocation { row, col };
                if !self.scenes_instances_grid.contains_key(&location) {
                    return location;
                }
            }
        }
        start
    }

    pub fn get_scenes_instance(&mut self, pos: &GridLocation) -> Option<&mut SceneInstance> {
        self.scenes_instances_grid.get_mut(pos)
    }
    pub fn scenes_instances_grid_len(&self) -> usize {
        self.scenes_instances_grid.len()
    }

    pub fn location_by_location_or_quick_index(
        &mut self,
        index_or_grid: SceneInstanceUnion,
    ) -> Option<GridLocation> {
        match index_or_grid {
            SceneInstanceUnion::Selected => None,
            SceneInstanceUnion::Grid(location) => Some(location),
            SceneInstanceUnion::Quick(quick_scene_instance_index) => match self.grid_highlight {
                GridHighlight::Row => {
                    if quick_scene_instance_index.index >= self.grid_width() {
                        return None;
                    }
                    Some(GridLocation {
                        row: self.quick_row_index(),
                        col: quick_scene_instance_index.index,
                    })
                }
                GridHighlight::Column => {
                    if quick_scene_instance_index.index >= self.grid_height() {
                        return None;
                    }
                    Some(GridLocation {
                        row: quick_scene_instance_index.index,
                        col: self.quick_col_index(),
                    })
                }
                GridHighlight::None => None,
            },
        }
    }

    /// The locations of the placed scenes, row by row.
    pub fn scene_locations_row_major(&self) -> Vec<GridLocation> {
        let mut locations: Vec<GridLocation> = self.scenes_instances_grid.keys().copied().collect();
        locations.sort_by_key(|location| (location.row, location.col));
        locations
    }

    pub fn all_scene_instance_locations(&self) -> impl Iterator<Item = &GridLocation> {
        self.scenes_instances_grid.keys()
    }

    /// Remove scene instance at path and update path to the next scene instance
    pub fn remove_scene_instance(&mut self, pos: GridLocation) -> Option<SceneInstance> {
        self.scenes_instances_grid.remove(&pos)
    }

    pub fn add_scene(
        &mut self,
        pos: GridLocation,
        scene_id: AssetId<Scene>,
        collections: &Collections,
    ) {
        let scene_instance = SceneInstance::from_scene_id(scene_id, collections);
        self.add_scene_instance(pos, scene_instance);
    }

    pub fn add_scene_instance(&mut self, pos: GridLocation, scene_instance: SceneInstance) {
        if pos.col >= self.grid_width() || pos.row >= self.grid_height() {
            return;
        }
        let scene_instances = &mut self.scenes_instances_grid;
        scene_instances.insert(pos, scene_instance);
    }

    pub fn grid_location_from_continuous_index(
        &self,
        index: usize,
        start: &GridLocation,
    ) -> GridLocation {
        let grid_width = self.grid_width();
        let grid_height = self.grid_height();
        let span = grid_width * grid_height;
        let index = (index + start.col + start.row * grid_width) % span;
        let row = index / grid_width;
        let col = index % grid_width;
        GridLocation { row, col }
    }
}

impl AssetTrait for Project {
    const DIR_NAME: &'static str = "projects";
    const NAME: &'static str = "Project";
    const SHOW_NAME_IF_SELECTED: bool = true;
}

impl CollectSoundTriggers for Project {
    fn collect_sound_triggers(&self, triggers: &mut SoundTriggerSet) {
        for scene_instance in self.scenes_instances_grid.values() {
            scene_instance.collect_sound_triggers(triggers);
        }
    }
}

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

    /// The patch replaced `groups` and `svg`; a project saved before it must
    /// still load, or every existing show silently vanishes.
    #[test]
    fn a_project_saved_before_the_patch_still_loads() {
        let json = r#"{
            "groups": {"0": "front"},
            "svg": {"svg_contents": "<svg/>"},
            "main_dimmer": 1.0,
            "scenes_instances_grid": {
                "0,0": {
                    "id": "b9a3f854-42a8-4a14-adb8-5dd73699f8aa",
                    "scene_id": "b9a3f854-42a8-4a14-adb8-5dd73699f8aa",
                    "name": "New Scene",
                    "groups_overwrite": {"0": "front"},
                    "scene": {"effects": [{"group_index": 2}]}
                }
            }
        }"#;

        let project: Project =
            serde_json::from_str(json).expect("a project saved before the patch must load");

        assert_eq!(project.scenes_instances_grid.len(), 1);
        assert!(project.patch.topology.fixtures.is_empty());
    }
}