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};
#[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>,
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,
},
}
}
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()
}
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::*;
#[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());
}
}