Skip to main content

gled_protocol/
project_update.rs

1//! Project changes on the wire. A whole project is tens of KB, mostly the
2//! scene grid and the patch; an edit usually touches one scene. So after the
3//! first full state, an interface gets only the scenes that changed, the patch
4//! when it changed, and the rest of the project when any of it changed.
5
6use std::{collections::HashMap, sync::Arc};
7
8use gled_model::{
9    asset::{
10        project::Project,
11        scene::{grid::GridLocation, instance::SceneInstance},
12    },
13    patching::Patch,
14};
15use serde::{Deserialize, Serialize};
16
17/// How a project replica becomes the engine's project.
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub enum ProjectUpdate {
20    Unchanged,
21    /// Another project, or none.
22    Replaced(Option<Box<Project>>),
23    Changed(ProjectDiff),
24}
25
26/// The differences between two projects.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct ProjectDiff {
29    /// Scene instances that changed, `None` where one was removed.
30    pub scenes: Vec<(GridLocation, Option<SceneInstance>)>,
31    /// The new patch, if it changed.
32    pub patch: Option<Box<Patch>>,
33    /// Everything but the scenes and the patch, if any of it changed. Its own
34    /// scenes and patch are empty.
35    pub rest: Option<Box<Project>>,
36}
37
38impl ProjectUpdate {
39    /// What turns `old` into `new`.
40    pub fn between(old: Option<&Project>, new: Option<&Project>) -> Self {
41        match (old, new) {
42            (None, None) => Self::Unchanged,
43            (_, None) => Self::Replaced(None),
44            (None, Some(new)) => Self::Replaced(Some(Box::new(new.clone()))),
45            // The engine's unchanged project is the same value: no need to
46            // compare it.
47            (Some(old), Some(new)) if std::ptr::eq(old, new) => Self::Unchanged,
48            // The diff compares every part once; an empty one means equal
49            // projects, as `rest` holds every field but the scenes and patch.
50            (Some(old), Some(new)) => {
51                let diff = ProjectDiff::between(old, new);
52                if diff.is_empty() {
53                    Self::Unchanged
54                } else {
55                    Self::Changed(diff)
56                }
57            }
58        }
59    }
60
61    /// Applies the update to a replica that was equal to the `old` project
62    /// it was made from. A replica shared with others is copied first.
63    pub fn apply_to(self, project: &mut Option<Arc<Project>>) {
64        match self {
65            Self::Unchanged => {}
66            Self::Replaced(new) => *project = new.map(|new| Arc::new(*new)),
67            Self::Changed(diff) => {
68                if let Some(project) = project {
69                    diff.apply_to(Arc::make_mut(project));
70                }
71            }
72        }
73    }
74}
75
76impl ProjectDiff {
77    fn is_empty(&self) -> bool {
78        self.scenes.is_empty() && self.patch.is_none() && self.rest.is_none()
79    }
80
81    fn between(old: &Project, new: &Project) -> Self {
82        let mut scenes: Vec<_> = new
83            .scenes_instances_grid
84            .iter()
85            .filter(|(location, scene)| old.scenes_instances_grid.get(location) != Some(scene))
86            .map(|(location, scene)| (*location, Some(scene.clone())))
87            .collect();
88        scenes.extend(
89            old.scenes_instances_grid
90                .keys()
91                .filter(|location| !new.scenes_instances_grid.contains_key(location))
92                .map(|location| (*location, None)),
93        );
94        let patch = (old.patch != new.patch).then(|| Box::new(new.patch.clone()));
95        let new_rest = rest(new);
96        let rest = (rest(old) != new_rest).then(|| Box::new(new_rest));
97        Self {
98            scenes,
99            patch,
100            rest,
101        }
102    }
103
104    fn apply_to(self, project: &mut Project) {
105        if let Some(rest) = self.rest {
106            let scenes = std::mem::take(&mut project.scenes_instances_grid);
107            let patch = std::mem::take(&mut project.patch);
108            *project = *rest;
109            project.scenes_instances_grid = scenes;
110            project.patch = patch;
111        }
112        if let Some(patch) = self.patch {
113            project.patch = *patch;
114        }
115        for (location, scene) in self.scenes {
116            match scene {
117                Some(scene) => {
118                    project.scenes_instances_grid.insert(location, scene);
119                }
120                None => {
121                    project.scenes_instances_grid.remove(&location);
122                }
123            }
124        }
125    }
126}
127
128/// The project without its scenes and patch. Listing every field makes a new
129/// one a compile error here instead of a change that never reaches the
130/// interfaces.
131fn rest(project: &Project) -> Project {
132    let Project {
133        palette,
134        patch: _,
135        grid_width,
136        grid_height,
137        grid_highlight,
138        scenes_instances_grid: _,
139        channel_overwrites,
140        output_routings,
141        artnet_config,
142        tap_input_events,
143        blackout_input_events,
144        blackout_hold_input_events,
145        half_input_events,
146        double_input_events,
147        midi_active_mappings,
148        main_dimmer,
149        audio_input_device,
150        artnet_control_config,
151        osc_config,
152    } = project;
153    Project {
154        palette: palette.clone(),
155        patch: Patch::default(),
156        grid_width: *grid_width,
157        grid_height: *grid_height,
158        grid_highlight: *grid_highlight,
159        channel_overwrites: channel_overwrites.clone(),
160        output_routings: output_routings.clone(),
161        artnet_config: artnet_config.clone(),
162        tap_input_events: tap_input_events.clone(),
163        blackout_input_events: blackout_input_events.clone(),
164        blackout_hold_input_events: blackout_hold_input_events.clone(),
165        half_input_events: half_input_events.clone(),
166        double_input_events: double_input_events.clone(),
167        midi_active_mappings: midi_active_mappings.clone(),
168        main_dimmer: *main_dimmer,
169        audio_input_device: audio_input_device.clone(),
170        artnet_control_config: *artnet_control_config,
171        osc_config: *osc_config,
172        scenes_instances_grid: HashMap::new(),
173    }
174}