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#[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 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 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 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 #[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}