Skip to main content

gled_model/asset/
project.rs

1pub mod io;
2pub mod scene_instance_path;
3
4use io::{ArtnetConfig, ArtnetControlConfig, ChannelOverwrites, OscConfig};
5
6use super::{
7    AssetTrait, midi_controller::MidiController, output_device::routing::OutputRoutings,
8    scene::instance::SceneInstance,
9};
10use crate::{
11    asset::{
12        palette::Palette,
13        project::scene_instance_path::SceneInstanceUnion,
14        scene::{Scene, grid::GridLocation},
15    },
16    asset_id::AssetId,
17    collection::Collections,
18    input::{
19        event::{GamepadEvent, InputEvent},
20        key_code::KeyCode,
21    },
22    patching::Patch,
23    sound::{
24        AudioDeviceId,
25        trigger_ref::{CollectSoundTriggers, SoundTriggerSet},
26    },
27};
28use serde::{Deserialize, Serialize};
29use std::{
30    collections::{BTreeSet, HashMap},
31    sync::Arc,
32};
33use strum::{Display, EnumIter};
34
35/// Which line of the scene grid is marked. It reads as the label the status
36/// bar shows.
37#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Display, EnumIter)]
38#[serde(rename_all = "lowercase")]
39pub enum GridHighlight {
40    #[default]
41    #[strum(serialize = "Last Row")]
42    Row,
43    #[strum(serialize = "Last Column")]
44    Column,
45    None,
46}
47
48#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
49#[serde(default)]
50pub struct Project {
51    pub palette: Option<Palette>,
52    /// All fixture data: the topology and the layouts derived from it.
53    pub patch: Patch,
54    #[serde(default = "Project::default_grid_width")]
55    pub grid_width: usize,
56    #[serde(default = "Project::default_grid_height")]
57    pub grid_height: usize,
58    pub grid_highlight: GridHighlight,
59    pub scenes_instances_grid: HashMap<GridLocation, SceneInstance>,
60    pub channel_overwrites: ChannelOverwrites,
61    pub output_routings: Arc<OutputRoutings>,
62    pub artnet_config: ArtnetConfig,
63    pub tap_input_events: BTreeSet<InputEvent>,
64    pub blackout_input_events: BTreeSet<InputEvent>,
65    pub blackout_hold_input_events: BTreeSet<InputEvent>,
66    pub half_input_events: BTreeSet<InputEvent>,
67    pub double_input_events: BTreeSet<InputEvent>,
68    pub midi_active_mappings: HashMap<String, Option<AssetId<MidiController>>>,
69    pub main_dimmer: f32,
70    pub audio_input_device: Option<AudioDeviceId>,
71    pub artnet_control_config: ArtnetControlConfig,
72    pub osc_config: OscConfig,
73}
74
75impl Default for Project {
76    fn default() -> Self {
77        Self {
78            palette: None,
79            patch: Patch::default(),
80            grid_width: Self::DEFAULT_GRID_WIDTH,
81            grid_height: Self::DEFAULT_GRID_HEIGHT,
82            grid_highlight: GridHighlight::default(),
83            scenes_instances_grid: HashMap::new(),
84            channel_overwrites: Default::default(),
85            output_routings: Default::default(),
86            artnet_config: Default::default(),
87            tap_input_events: std::iter::once(InputEvent::Key(KeyCode::T))
88                .chain(std::iter::once(InputEvent::Gamepad(GamepadEvent::Mode(0))))
89                .collect(),
90            blackout_input_events: std::iter::once(InputEvent::Key(KeyCode::B)).collect(),
91            blackout_hold_input_events: std::iter::once(InputEvent::Key(KeyCode::N))
92                .chain(std::iter::once(InputEvent::Gamepad(GamepadEvent::Start(0))))
93                .collect(),
94            half_input_events: std::iter::once(InputEvent::Key(KeyCode::Minus)).collect(),
95            double_input_events: std::iter::once(InputEvent::Key(KeyCode::Plus)).collect(),
96            midi_active_mappings: HashMap::new(),
97            main_dimmer: 1.0,
98            audio_input_device: None,
99            artnet_control_config: ArtnetControlConfig::default(),
100            osc_config: OscConfig::default(),
101        }
102    }
103}
104
105impl Project {
106    pub(crate) const MIN_GRID_WIDTH: usize = 2;
107    pub(crate) const MIN_GRID_HEIGHT: usize = 2;
108    pub(crate) const DEFAULT_GRID_WIDTH: usize = 8;
109    pub(crate) const DEFAULT_GRID_HEIGHT: usize = 6;
110
111    fn default_grid_width() -> usize {
112        Self::DEFAULT_GRID_WIDTH
113    }
114
115    fn default_grid_height() -> usize {
116        Self::DEFAULT_GRID_HEIGHT
117    }
118
119    pub fn grid_width(&self) -> usize {
120        self.grid_width.max(Self::MIN_GRID_WIDTH)
121    }
122
123    pub fn grid_height(&self) -> usize {
124        self.grid_height.max(Self::MIN_GRID_HEIGHT)
125    }
126
127    pub fn quick_row_index(&self) -> usize {
128        self.grid_height() - 1
129    }
130
131    pub(crate) fn quick_col_index(&self) -> usize {
132        self.grid_width() - 1
133    }
134
135    pub fn set_grid_size(&mut self, width: usize, height: usize) {
136        self.grid_width = width.max(Self::MIN_GRID_WIDTH);
137        self.grid_height = height.max(Self::MIN_GRID_HEIGHT);
138
139        let max_width = self.grid_width();
140        let max_height = self.grid_height();
141        self.scenes_instances_grid
142            .retain(|location, _| location.col < max_width && location.row < max_height);
143    }
144
145    pub fn next_empty_grid_location(&self, start: GridLocation) -> GridLocation {
146        let grid_height = self.grid_height();
147        let grid_width = self.grid_width();
148        for row in 0..grid_height {
149            let row = (start.row + row) % grid_height;
150            for col in 0..grid_width {
151                let col = (start.col + col) % grid_width;
152                let location = GridLocation { row, col };
153                if !self.scenes_instances_grid.contains_key(&location) {
154                    return location;
155                }
156            }
157        }
158        start
159    }
160
161    pub fn get_scenes_instance(&mut self, pos: &GridLocation) -> Option<&mut SceneInstance> {
162        self.scenes_instances_grid.get_mut(pos)
163    }
164    pub fn scenes_instances_grid_len(&self) -> usize {
165        self.scenes_instances_grid.len()
166    }
167
168    pub fn location_by_location_or_quick_index(
169        &mut self,
170        index_or_grid: SceneInstanceUnion,
171    ) -> Option<GridLocation> {
172        match index_or_grid {
173            SceneInstanceUnion::Selected => None,
174            SceneInstanceUnion::Grid(location) => Some(location),
175            SceneInstanceUnion::Quick(quick_scene_instance_index) => match self.grid_highlight {
176                GridHighlight::Row => {
177                    if quick_scene_instance_index.index >= self.grid_width() {
178                        return None;
179                    }
180                    Some(GridLocation {
181                        row: self.quick_row_index(),
182                        col: quick_scene_instance_index.index,
183                    })
184                }
185                GridHighlight::Column => {
186                    if quick_scene_instance_index.index >= self.grid_height() {
187                        return None;
188                    }
189                    Some(GridLocation {
190                        row: quick_scene_instance_index.index,
191                        col: self.quick_col_index(),
192                    })
193                }
194                GridHighlight::None => None,
195            },
196        }
197    }
198
199    /// The locations of the placed scenes, row by row.
200    pub fn scene_locations_row_major(&self) -> Vec<GridLocation> {
201        let mut locations: Vec<GridLocation> = self.scenes_instances_grid.keys().copied().collect();
202        locations.sort_by_key(|location| (location.row, location.col));
203        locations
204    }
205
206    pub fn all_scene_instance_locations(&self) -> impl Iterator<Item = &GridLocation> {
207        self.scenes_instances_grid.keys()
208    }
209
210    /// Remove scene instance at path and update path to the next scene instance
211    pub fn remove_scene_instance(&mut self, pos: GridLocation) -> Option<SceneInstance> {
212        self.scenes_instances_grid.remove(&pos)
213    }
214
215    pub fn add_scene(
216        &mut self,
217        pos: GridLocation,
218        scene_id: AssetId<Scene>,
219        collections: &Collections,
220    ) {
221        let scene_instance = SceneInstance::from_scene_id(scene_id, collections);
222        self.add_scene_instance(pos, scene_instance);
223    }
224
225    pub fn add_scene_instance(&mut self, pos: GridLocation, scene_instance: SceneInstance) {
226        if pos.col >= self.grid_width() || pos.row >= self.grid_height() {
227            return;
228        }
229        let scene_instances = &mut self.scenes_instances_grid;
230        scene_instances.insert(pos, scene_instance);
231    }
232
233    pub fn grid_location_from_continuous_index(
234        &self,
235        index: usize,
236        start: &GridLocation,
237    ) -> GridLocation {
238        let grid_width = self.grid_width();
239        let grid_height = self.grid_height();
240        let span = grid_width * grid_height;
241        let index = (index + start.col + start.row * grid_width) % span;
242        let row = index / grid_width;
243        let col = index % grid_width;
244        GridLocation { row, col }
245    }
246}
247
248impl AssetTrait for Project {
249    const DIR_NAME: &'static str = "projects";
250    const NAME: &'static str = "Project";
251    const SHOW_NAME_IF_SELECTED: bool = true;
252}
253
254impl CollectSoundTriggers for Project {
255    fn collect_sound_triggers(&self, triggers: &mut SoundTriggerSet) {
256        for scene_instance in self.scenes_instances_grid.values() {
257            scene_instance.collect_sound_triggers(triggers);
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    /// The patch replaced `groups` and `svg`; a project saved before it must
267    /// still load, or every existing show silently vanishes.
268    #[test]
269    fn a_project_saved_before_the_patch_still_loads() {
270        let json = r#"{
271            "groups": {"0": "front"},
272            "svg": {"svg_contents": "<svg/>"},
273            "main_dimmer": 1.0,
274            "scenes_instances_grid": {
275                "0,0": {
276                    "id": "b9a3f854-42a8-4a14-adb8-5dd73699f8aa",
277                    "scene_id": "b9a3f854-42a8-4a14-adb8-5dd73699f8aa",
278                    "name": "New Scene",
279                    "groups_overwrite": {"0": "front"},
280                    "scene": {"effects": [{"group_index": 2}]}
281                }
282            }
283        }"#;
284
285        let project: Project =
286            serde_json::from_str(json).expect("a project saved before the patch must load");
287
288        assert_eq!(project.scenes_instances_grid.len(), 1);
289        assert!(project.patch.topology.fixtures.is_empty());
290    }
291}