Skip to main content

gled_model/patching/
layout.rs

1use crate::naming::unique_name;
2use crate::patching::fixture::{FixtureId, LayoutPosition};
3use emath::{Pos2, Rect, Vec2};
4use serde::{Deserialize, Deserializer, Serialize};
5use std::collections::BTreeMap;
6use std::path::PathBuf;
7
8/// Side of the square editor scene, in scene units. One unit is a centimetre,
9/// so the scene covers 20 x 20 m. Effects map this square onto their [0,1] UV
10/// space, which is why it lives with the data rather than the editor.
11pub const SCENE_SIZE: f32 = 2000.0;
12
13/// The square the scene occupies. Effects and the live preview map exactly this
14/// rectangle onto their [0,1] UV space, so the editor shows the same one.
15pub fn scene_bounds() -> Rect {
16    Rect::from_min_size(Pos2::ZERO, Vec2::splat(SCENE_SIZE))
17}
18
19/// One mode of a layout: which mode of its own definition each fixture uses.
20///
21/// The map is sparse — a fixture that is not listed falls back to its
22/// definition's default mode. The layout's default mode is simply the one whose
23/// map is always empty, so a user who never opens this concept still gets
24/// sensible behaviour.
25#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
26pub struct LayoutMode {
27    pub name: String,
28    pub fixture_modes: BTreeMap<FixtureId, usize>,
29}
30
31impl LayoutMode {
32    pub const DEFAULT_NAME: &'static str = "Default";
33
34    pub fn default_mode() -> Self {
35        Self {
36            name: Self::DEFAULT_NAME.to_string(),
37            fixture_modes: BTreeMap::new(),
38        }
39    }
40
41    pub fn new(name: impl Into<String>) -> Self {
42        Self {
43            name: name.into(),
44            fixture_modes: BTreeMap::new(),
45        }
46    }
47}
48
49/// A subset of all fixtures in a user definable 2d arrangement.
50///
51/// Layouts may overlap freely. The topology's default layout is a `Layout` too,
52/// and is the one that necessarily contains every fixture.
53#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
54pub struct Layout {
55    pub name: String,
56    pub background: Option<PathBuf>,
57    pub members: BTreeMap<FixtureId, LayoutPosition>,
58    /// `modes[0]` is the default mode and is never removed.
59    #[serde(deserialize_with = "at_least_the_default_mode")]
60    pub modes: Vec<LayoutMode>,
61}
62
63/// A layout's modes as saved, with the default mode put back if a file has
64/// none: every lookup falls back to `modes[0]`.
65fn at_least_the_default_mode<'de, D: Deserializer<'de>>(
66    deserializer: D,
67) -> Result<Vec<LayoutMode>, D::Error> {
68    let mut modes = Vec::<LayoutMode>::deserialize(deserializer)?;
69    if modes.is_empty() {
70        modes.push(LayoutMode::default_mode());
71    }
72    Ok(modes)
73}
74
75impl Default for Layout {
76    fn default() -> Self {
77        Self::new("Layout")
78    }
79}
80
81impl Layout {
82    /// What the layout every fixture belongs to is called. Every collection in
83    /// patching has a default element, so nothing has to be learned to use one.
84    pub const DEFAULT_NAME: &'static str = "Default";
85
86    pub fn new(name: impl Into<String>) -> Self {
87        Self {
88            name: name.into(),
89            background: None,
90            members: BTreeMap::new(),
91            modes: vec![LayoutMode::default_mode()],
92        }
93    }
94
95    pub fn contains(&self, id: FixtureId) -> bool {
96        self.members.contains_key(&id)
97    }
98
99    /// The mode at `index`, falling back to the default mode.
100    pub fn mode(&self, index: usize) -> &LayoutMode {
101        self.modes.get(index).unwrap_or(&self.modes[0])
102    }
103
104    pub fn mode_mut(&mut self, index: usize) -> &mut LayoutMode {
105        let index = if index < self.modes.len() { index } else { 0 };
106        &mut self.modes[index]
107    }
108
109    /// Adds a mode with a name not yet used in this layout.
110    pub fn add_mode(&mut self) -> usize {
111        let name = unique_name("Mode", self.modes.iter().map(|mode| mode.name.as_str()));
112        self.modes.push(LayoutMode::new(name));
113        self.modes.len() - 1
114    }
115
116    /// Removes a mode. The default mode cannot be removed.
117    pub fn remove_mode(&mut self, index: usize) {
118        if index > 0 && index < self.modes.len() {
119            self.modes.remove(index);
120        }
121    }
122
123    pub fn forget(&mut self, id: FixtureId) {
124        self.members.remove(&id);
125        for mode in &mut self.modes {
126            mode.fixture_modes.remove(&id);
127        }
128    }
129}
130
131/// Which layout the editor is pointed at. The topology's default layout is
132/// addressed separately from the user layouts.
133#[derive(
134    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default,
135)]
136pub enum LayoutRef {
137    #[default]
138    Topology,
139    Layout(usize),
140}
141
142impl LayoutRef {
143    /// The position in the layout stack (`Patch::layout_refs`): 0 is the
144    /// topology, then the user layouts in order. OSC and MIDI address layouts
145    /// by this index.
146    pub fn stack_index(self) -> usize {
147        match self {
148            LayoutRef::Topology => 0,
149            LayoutRef::Layout(index) => index + 1,
150        }
151    }
152}
153
154/// Which layout, and which of its modes, drives a tile or an effect.
155#[derive(
156    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord,
157)]
158pub struct LayoutSelection {
159    pub layout: LayoutRef,
160    pub mode: usize,
161}
162
163impl LayoutSelection {
164    pub fn new(layout: LayoutRef, mode: usize) -> Self {
165        Self { layout, mode }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn a_new_layout_has_only_the_default_mode() {
175        let layout = Layout::new("Stage");
176        assert_eq!(layout.modes.len(), 1);
177        assert_eq!(layout.modes[0].name, LayoutMode::DEFAULT_NAME);
178        assert!(layout.modes[0].fixture_modes.is_empty());
179    }
180
181    #[test]
182    fn the_default_mode_cannot_be_removed() {
183        let mut layout = Layout::new("Stage");
184        layout.add_mode();
185        layout.remove_mode(0);
186        assert_eq!(layout.modes.len(), 2);
187        layout.remove_mode(1);
188        assert_eq!(layout.modes.len(), 1);
189    }
190
191    #[test]
192    fn an_out_of_range_mode_falls_back_to_the_default() {
193        let layout = Layout::new("Stage");
194        assert_eq!(layout.mode(7).name, LayoutMode::DEFAULT_NAME);
195    }
196
197    #[test]
198    fn a_layout_saved_without_modes_gets_its_default_mode_back() {
199        let layout: Layout = serde_json::from_str(
200            r#"{"name": "Stage", "background": null, "members": {}, "modes": []}"#,
201        )
202        .expect("deserialize");
203        assert_eq!(layout.modes, [LayoutMode::default_mode()]);
204        assert_eq!(layout.mode(3).name, LayoutMode::DEFAULT_NAME);
205    }
206
207    #[test]
208    fn forgetting_a_fixture_clears_its_mode_assignments() {
209        let mut layout = Layout::new("Stage");
210        let id = FixtureId::new();
211        layout.members.insert(id, LayoutPosition::default());
212        let mode = layout.add_mode();
213        layout.mode_mut(mode).fixture_modes.insert(id, 1);
214
215        layout.forget(id);
216
217        assert!(!layout.contains(id));
218        assert!(layout.mode(mode).fixture_modes.is_empty());
219    }
220
221    #[test]
222    fn the_stack_index_is_the_position_in_the_layout_stack() {
223        let mut patch = crate::patching::Patch::default();
224        patch.add_layout();
225        patch.add_layout();
226
227        let refs = patch.layout_refs();
228
229        assert_eq!(refs.len(), 3);
230        for (index, layout) in refs.into_iter().enumerate() {
231            assert_eq!(layout.stack_index(), index, "{layout:?}");
232        }
233    }
234}