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    /// The picture behind the layout, by its path in the asset repository
57    /// (see `asset::picture`).
58    pub background: Option<PathBuf>,
59    pub members: BTreeMap<FixtureId, LayoutPosition>,
60    /// `modes[0]` is the default mode and is never removed.
61    #[serde(deserialize_with = "at_least_the_default_mode")]
62    pub modes: Vec<LayoutMode>,
63}
64
65/// A layout's modes as saved, with the default mode put back if a file has
66/// none: every lookup falls back to `modes[0]`.
67fn at_least_the_default_mode<'de, D: Deserializer<'de>>(
68    deserializer: D,
69) -> Result<Vec<LayoutMode>, D::Error> {
70    let mut modes = Vec::<LayoutMode>::deserialize(deserializer)?;
71    if modes.is_empty() {
72        modes.push(LayoutMode::default_mode());
73    }
74    Ok(modes)
75}
76
77impl Default for Layout {
78    fn default() -> Self {
79        Self::new("Layout")
80    }
81}
82
83impl Layout {
84    /// What the layout every fixture belongs to is called. Every collection in
85    /// patching has a default element, so nothing has to be learned to use one.
86    pub const DEFAULT_NAME: &'static str = "Default";
87
88    pub fn new(name: impl Into<String>) -> Self {
89        Self {
90            name: name.into(),
91            background: None,
92            members: BTreeMap::new(),
93            modes: vec![LayoutMode::default_mode()],
94        }
95    }
96
97    pub fn contains(&self, id: FixtureId) -> bool {
98        self.members.contains_key(&id)
99    }
100
101    /// The mode at `index`, falling back to the default mode.
102    pub fn mode(&self, index: usize) -> &LayoutMode {
103        self.modes.get(index).unwrap_or(&self.modes[0])
104    }
105
106    pub fn mode_mut(&mut self, index: usize) -> &mut LayoutMode {
107        let index = if index < self.modes.len() { index } else { 0 };
108        &mut self.modes[index]
109    }
110
111    /// Adds a mode with a name not yet used in this layout.
112    pub fn add_mode(&mut self) -> usize {
113        let name = unique_name("Mode", self.modes.iter().map(|mode| mode.name.as_str()));
114        self.modes.push(LayoutMode::new(name));
115        self.modes.len() - 1
116    }
117
118    /// Removes a mode. The default mode cannot be removed.
119    pub fn remove_mode(&mut self, index: usize) {
120        if index > 0 && index < self.modes.len() {
121            self.modes.remove(index);
122        }
123    }
124
125    pub fn forget(&mut self, id: FixtureId) {
126        self.members.remove(&id);
127        for mode in &mut self.modes {
128            mode.fixture_modes.remove(&id);
129        }
130    }
131}
132
133/// Which layout the editor is pointed at. The topology's default layout is
134/// addressed separately from the user layouts.
135#[derive(
136    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default,
137)]
138pub enum LayoutRef {
139    #[default]
140    Topology,
141    Layout(usize),
142}
143
144impl LayoutRef {
145    /// The position in the layout stack (`Patch::layout_refs`): 0 is the
146    /// topology, then the user layouts in order. OSC and MIDI address layouts
147    /// by this index.
148    pub fn stack_index(self) -> usize {
149        match self {
150            LayoutRef::Topology => 0,
151            LayoutRef::Layout(index) => index + 1,
152        }
153    }
154}
155
156/// Which layout, and which of its modes, drives a tile or an effect.
157#[derive(
158    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord,
159)]
160pub struct LayoutSelection {
161    pub layout: LayoutRef,
162    pub mode: usize,
163}
164
165impl LayoutSelection {
166    pub fn new(layout: LayoutRef, mode: usize) -> Self {
167        Self { layout, mode }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn a_new_layout_has_only_the_default_mode() {
177        let layout = Layout::new("Stage");
178        assert_eq!(layout.modes.len(), 1);
179        assert_eq!(layout.modes[0].name, LayoutMode::DEFAULT_NAME);
180        assert!(layout.modes[0].fixture_modes.is_empty());
181    }
182
183    #[test]
184    fn the_default_mode_cannot_be_removed() {
185        let mut layout = Layout::new("Stage");
186        layout.add_mode();
187        layout.remove_mode(0);
188        assert_eq!(layout.modes.len(), 2);
189        layout.remove_mode(1);
190        assert_eq!(layout.modes.len(), 1);
191    }
192
193    #[test]
194    fn an_out_of_range_mode_falls_back_to_the_default() {
195        let layout = Layout::new("Stage");
196        assert_eq!(layout.mode(7).name, LayoutMode::DEFAULT_NAME);
197    }
198
199    #[test]
200    fn a_layout_saved_without_modes_gets_its_default_mode_back() {
201        let layout: Layout = serde_json::from_str(
202            r#"{"name": "Stage", "background": null, "members": {}, "modes": []}"#,
203        )
204        .expect("deserialize");
205        assert_eq!(layout.modes, [LayoutMode::default_mode()]);
206        assert_eq!(layout.mode(3).name, LayoutMode::DEFAULT_NAME);
207    }
208
209    #[test]
210    fn forgetting_a_fixture_clears_its_mode_assignments() {
211        let mut layout = Layout::new("Stage");
212        let id = FixtureId::new();
213        layout.members.insert(id, LayoutPosition::default());
214        let mode = layout.add_mode();
215        layout.mode_mut(mode).fixture_modes.insert(id, 1);
216
217        layout.forget(id);
218
219        assert!(!layout.contains(id));
220        assert!(layout.mode(mode).fixture_modes.is_empty());
221    }
222
223    #[test]
224    fn the_stack_index_is_the_position_in_the_layout_stack() {
225        let mut patch = crate::patching::Patch::default();
226        patch.add_layout();
227        patch.add_layout();
228
229        let refs = patch.layout_refs();
230
231        assert_eq!(refs.len(), 3);
232        for (index, layout) in refs.into_iter().enumerate() {
233            assert_eq!(layout.stack_index(), index, "{layout:?}");
234        }
235    }
236}