From 66dbb21f0d6e13207d270775a1e87c7d5b4da99d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20J=C3=B6hnk?= Date: Wed, 3 Jul 2024 08:24:23 +0200 Subject: [PATCH] feat(projects): use archives for project files to be able to include media files etc --- Cargo.lock | 222 ++++++++++++-- crates/components/fixtures/src/config.rs | 75 +++++ crates/components/fixtures/src/lib.rs | 1 + crates/components/fixtures/src/manager.rs | 87 ++++++ crates/components/fixtures/src/module.rs | 1 + .../components/sequencer/src/effects/mod.rs | 1 + .../sequencer/src/effects/module.rs | 1 + .../sequencer/src/effects/project_loading.rs | 40 +++ crates/components/sequencer/src/lib.rs | 1 + crates/components/sequencer/src/module.rs | 1 + .../sequencer/src/project_loading.rs | 27 ++ crates/components/surfaces/src/lib.rs | 1 + crates/components/surfaces/src/module.rs | 5 +- .../surfaces/src/project_loading.rs | 45 +++ crates/mizer/src/api/handler.rs | 4 +- crates/mizer/src/lib.rs | 1 + crates/mizer/src/mizer.rs | 151 +++------ crates/mizer/src/module_context.rs | 8 +- crates/mizer/src/project_handler.rs | 65 ++++ crates/mizer/src/runtime_builder.rs | 6 +- crates/projects/Cargo.toml | 2 + crates/projects/src/effects.rs | 31 -- crates/projects/src/fixtures.rs | 119 +------- crates/projects/src/handler_context.rs | 86 ++++++ crates/projects/src/lib.rs | 288 ++---------------- .../projects/src/project_file/archive_file.rs | 56 ++++ crates/projects/src/project_file/mod.rs | 113 +++++++ crates/projects/src/sequencer.rs | 20 -- crates/projects/src/surfaces.rs | 18 -- crates/projects/src/versioning/migration.rs | 82 ++++- .../migrations/adapt_fader_config.rs | 8 +- .../src/versioning/migrations/archive_file.rs | 67 ++++ .../migrations/migrate_position_presets.rs | 8 +- .../projects/src/versioning/migrations/mod.rs | 13 +- .../src/versioning/migrations/rename_ports.rs | 12 +- ...rework_layout_controls_to_not_use_nodes.rs | 8 +- .../migrations/rework_midi_config.rs | 8 +- crates/projects/src/versioning/mod.rs | 50 +-- crates/runtime/module/Cargo.toml | 1 + crates/runtime/module/src/lib.rs | 26 ++ 40 files changed, 1077 insertions(+), 682 deletions(-) create mode 100644 crates/components/fixtures/src/config.rs create mode 100644 crates/components/sequencer/src/effects/project_loading.rs create mode 100644 crates/components/sequencer/src/project_loading.rs create mode 100644 crates/components/surfaces/src/project_loading.rs create mode 100644 crates/mizer/src/project_handler.rs delete mode 100644 crates/projects/src/effects.rs create mode 100644 crates/projects/src/handler_context.rs create mode 100644 crates/projects/src/project_file/archive_file.rs create mode 100644 crates/projects/src/project_file/mod.rs delete mode 100644 crates/projects/src/sequencer.rs delete mode 100644 crates/projects/src/surfaces.rs create mode 100644 crates/projects/src/versioning/migrations/archive_file.rs diff --git a/Cargo.lock b/Cargo.lock index 6f553bcee..0bbf3753d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,6 +201,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arboard" version = "3.3.1" @@ -845,9 +854,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "by_address" @@ -1382,6 +1391,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + [[package]] name = "convert_case" version = "0.4.0" @@ -1621,11 +1636,26 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if 1.0.0", ] @@ -1729,9 +1759,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -2043,6 +2073,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "deflate64" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" + [[package]] name = "deranged" version = "0.3.11" @@ -2085,6 +2121,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "derive_builder" version = "0.9.0" @@ -2220,6 +2267,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "dlib" version = "0.5.2" @@ -2781,9 +2839,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide 0.7.2", @@ -4540,11 +4598,17 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "loom" @@ -4568,6 +4632,16 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "mach" version = "0.3.2" @@ -4624,9 +4698,9 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memmap2" @@ -5322,7 +5396,7 @@ dependencies = [ "mizer-util", "rayon", "tracing", - "zip", + "zip 0.6.6", ] [[package]] @@ -5492,6 +5566,7 @@ dependencies = [ "mizer-processing", "mizer-settings", "mizer-status-bus", + "serde", "test-case", "tracing", ] @@ -5848,6 +5923,7 @@ dependencies = [ "mizer-fixtures", "mizer-layouts", "mizer-media", + "mizer-module", "mizer-node", "mizer-nodes", "mizer-pixel-nodes", @@ -5865,6 +5941,7 @@ dependencies = [ "serde_yaml", "test-case", "tracing", + "zip 2.1.3", ] [[package]] @@ -6016,7 +6093,7 @@ dependencies = [ "temp-dir", "tracing", "ureq", - "zip", + "zip 0.6.6", ] [[package]] @@ -7357,6 +7434,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -9510,18 +9597,18 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -9551,9 +9638,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -9572,9 +9659,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -11608,9 +11695,23 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] [[package]] name = "zip" @@ -11621,15 +11722,58 @@ dependencies = [ "aes", "byteorder", "bzip2", - "constant_time_eq", + "constant_time_eq 0.1.5", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zip" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq 0.3.0", "crc32fast", "crossbeam-utils", + "deflate64", + "displaydoc", "flate2", "hmac", - "pbkdf2", + "indexmap 2.2.6", + "lzma-rs", + "memchr", + "pbkdf2 0.12.2", + "rand 0.8.5", "sha1", + "thiserror", "time", - "zstd", + "zeroize", + "zopfli", + "zstd 0.13.1", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", ] [[package]] @@ -11638,7 +11782,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe 7.1.0", ] [[package]] @@ -11651,11 +11804,20 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", "pkg-config", diff --git a/crates/components/fixtures/src/config.rs b/crates/components/fixtures/src/config.rs new file mode 100644 index 000000000..b1fa37429 --- /dev/null +++ b/crates/components/fixtures/src/config.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; +use crate::fixture::FixtureConfiguration; +use crate::programmer::{Color, Position, Preset, Presets}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct FixtureConfig { + pub id: u32, + pub name: String, + pub fixture: String, + pub channel: u16, + pub universe: Option, + #[serde(default)] + pub mode: Option, + #[serde(default)] + pub configuration: FixtureConfiguration, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct PresetsStore { + #[serde(default)] + pub intensity: Vec>, + #[serde(default)] + pub shutter: Vec>, + #[serde(default)] + pub color: Vec>, + #[serde(default)] + pub position: Vec>, +} + +impl PresetsStore { + pub fn load(&self, presets: &Presets) { + for preset in self.intensity.iter() { + presets.intensity.insert(preset.id, preset.clone()); + } + for preset in self.shutter.iter() { + presets.shutter.insert(preset.id, preset.clone()); + } + for preset in self.color.iter() { + presets.color.insert(preset.id, preset.clone()); + } + for preset in self.position.iter() { + presets.position.insert(preset.id, preset.clone()); + } + } + + pub fn store(presets: &Presets) -> Self { + let intensity = presets + .intensity + .iter() + .map(|entry| entry.value().clone()) + .collect(); + let shutter = presets + .shutter + .iter() + .map(|entry| entry.value().clone()) + .collect(); + let color = presets + .color + .iter() + .map(|entry| entry.value().clone()) + .collect(); + let position = presets + .position + .iter() + .map(|entry| entry.value().clone()) + .collect(); + + Self { + intensity, + shutter, + color, + position, + } + } +} diff --git a/crates/components/fixtures/src/lib.rs b/crates/components/fixtures/src/lib.rs index 3a4aaf638..9d60a0b91 100644 --- a/crates/components/fixtures/src/lib.rs +++ b/crates/components/fixtures/src/lib.rs @@ -10,6 +10,7 @@ pub use crate::module::*; pub use crate::priority::*; use crate::programmer::Color; +mod config; mod contracts; pub mod definition; pub mod fixture; diff --git a/crates/components/fixtures/src/manager.rs b/crates/components/fixtures/src/manager.rs index 91cfc40a0..ef4e2440e 100644 --- a/crates/components/fixtures/src/manager.rs +++ b/crates/components/fixtures/src/manager.rs @@ -3,6 +3,7 @@ use std::sync::{Arc, Mutex, MutexGuard}; use dashmap::DashMap; use itertools::Itertools; +use mizer_module::{LoadProjectContext, ProjectHandler, ProjectHandlerContext, SaveProjectContext}; use mizer_protocol_dmx::DmxConnectionManager; @@ -15,6 +16,7 @@ use crate::programmer::{ GenericPreset, Group, Position, Preset, PresetId, PresetType, Presets, Programmer, }; use crate::{FixtureId, FixturePriority, FixtureStates, GroupId}; +use crate::config::{FixtureConfig, PresetsStore}; #[derive(Clone)] pub struct FixtureManager { @@ -405,6 +407,91 @@ impl FixtureManager { fixture.set_to_default(); } } + + fn clear(&self) { + self.fixtures.clear(); + self.groups.clear(); + self.presets.clear(); + self.states.clear(); + } +} + +impl ProjectHandler for FixtureManager { + fn get_name(&self) -> &'static str { + "fixtures" + } + + fn new_project(&mut self, _context: &mut impl ProjectHandlerContext) -> anyhow::Result<()> { + self.clear(); + self.presets.load_defaults(); + + Ok(()) + } + + fn load_project(&mut self, context: &mut impl LoadProjectContext) -> anyhow::Result<()> { + profiling::scope!("FixtureManager::load_project"); + self.clear(); + let fixtures = context.read_file::>("patch")?; + for fixture in fixtures { + let def = self.get_definition(&fixture.fixture); + if let Some(def) = def { + self.add_fixture( + fixture.id, + fixture.name, + def, + fixture.mode, + fixture.channel, + fixture.universe, + fixture.configuration, + ); + } else { + tracing::warn!( + "No fixture definition for fixture id {}. Missing fixture definition: {}", + fixture.id, + fixture.fixture + ); + context.report_issue(format!( + "No fixture definition for fixture id {}. Missing fixture definition: {}", + fixture.id, + fixture.fixture + )); + } + } + let groups = context.read_file::>("groups")?; + for group in groups { + self.groups.insert(group.id, group); + } + let presets = context.read_file::("presets")?; + presets.load(&self.presets); + + Ok(()) + } + + fn save_project(&self, context: &mut impl SaveProjectContext) -> anyhow::Result<()> { + profiling::scope!("FixtureManager::save_project"); + let mut fixtures = Vec::with_capacity(self.fixtures.len()); + for fixture in self.get_fixtures() { + fixtures.push(FixtureConfig { + id: fixture.id, + name: fixture.name.clone(), + universe: fixture.universe.into(), + channel: fixture.channel, + fixture: fixture.definition.id.clone(), + mode: fixture.current_mode.name.clone().into(), + configuration: fixture.configuration.clone(), + }); + } + context.write_file("patch", &fixtures)?; + let mut groups = Vec::with_capacity(self.groups.len()); + for group in self.get_groups() { + groups.push(group.deref().clone()); + } + context.write_file("groups", &groups)?; + let presets = PresetsStore::store(&self.presets); + context.write_file("presets", &presets)?; + + Ok(()) + } } struct MutexLogWrapper<'a, T>(MutexGuard<'a, T>); diff --git a/crates/components/fixtures/src/module.rs b/crates/components/fixtures/src/module.rs index fc2998da8..ceefdf80e 100644 --- a/crates/components/fixtures/src/module.rs +++ b/crates/components/fixtures/src/module.rs @@ -34,6 +34,7 @@ impl Module context.provide_api(library.clone()); context.provide(library); context.provide_api(manager.clone()); + context.add_project_handler(manager.clone()); context.provide(manager); context.add_processor(FixtureProcessor); context.add_debug_ui_pane(FixturesDebugUiPane); diff --git a/crates/components/sequencer/src/effects/mod.rs b/crates/components/sequencer/src/effects/mod.rs index a0dd1623f..d8b52b3be 100644 --- a/crates/components/sequencer/src/effects/mod.rs +++ b/crates/components/sequencer/src/effects/mod.rs @@ -11,6 +11,7 @@ mod engine; mod instance; mod module; mod processor; +mod project_loading; mod spline; #[cfg(test)] diff --git a/crates/components/sequencer/src/effects/module.rs b/crates/components/sequencer/src/effects/module.rs index 348604084..659a269bf 100644 --- a/crates/components/sequencer/src/effects/module.rs +++ b/crates/components/sequencer/src/effects/module.rs @@ -13,6 +13,7 @@ impl Module for EffectsModule { fn register(self, context: &mut impl ModuleContext) -> anyhow::Result<()> { let engine = EffectEngine::new(); context.provide_api(engine.clone()); + context.add_project_handler(engine.clone()); context.provide(engine); context.add_processor(EffectsProcessor); context.add_debug_ui_pane(EffectsDebugUiPane); diff --git a/crates/components/sequencer/src/effects/project_loading.rs b/crates/components/sequencer/src/effects/project_loading.rs new file mode 100644 index 000000000..ce3ed3fd1 --- /dev/null +++ b/crates/components/sequencer/src/effects/project_loading.rs @@ -0,0 +1,40 @@ +use mizer_module::*; +use crate::{Effect, EffectEngine}; + +impl ProjectHandler for EffectEngine { + fn get_name(&self) -> &'static str { + "effects" + } + + fn new_project(&mut self, _context: &mut impl ProjectHandlerContext) -> anyhow::Result<()> { + self.clear(); + self.load_defaults(); + + Ok(()) + } + + fn load_project(&mut self, context: &mut impl LoadProjectContext) -> anyhow::Result<()> { + self.clear(); + let effects = context.read_file::>("effects")?; + profiling::scope!("EffectEngine::load_project"); + tracing::debug!("load effect engine"); + for effect in &effects { + self.effects.insert(effect.id, effect.clone()); + } + + Ok(()) + } + + fn save_project(&self, context: &mut impl SaveProjectContext) -> anyhow::Result<()> { + profiling::scope!("EffectEngine::save_project"); + tracing::debug!("save effect engine"); + let mut effects = Vec::with_capacity(self.effects.len()); + for effect in self.effects.iter() { + effects.push(effect.value().clone()); + } + + context.write_file("effects", &effects)?; + + Ok(()) + } +} diff --git a/crates/components/sequencer/src/lib.rs b/crates/components/sequencer/src/lib.rs index da80d92f2..32f671eec 100644 --- a/crates/components/sequencer/src/lib.rs +++ b/crates/components/sequencer/src/lib.rs @@ -15,6 +15,7 @@ mod sequencer; mod state; mod value; mod cue_preset; +mod project_loading; #[cfg(test)] mod tests { diff --git a/crates/components/sequencer/src/module.rs b/crates/components/sequencer/src/module.rs index cc1c8db4c..9aed4084a 100644 --- a/crates/components/sequencer/src/module.rs +++ b/crates/components/sequencer/src/module.rs @@ -13,6 +13,7 @@ impl Module for SequencerModule { fn register(self, context: &mut impl ModuleContext) -> anyhow::Result<()> { let sequencer = Sequencer::new(); context.provide_api(sequencer.clone()); + context.add_project_handler(sequencer.clone()); context.provide(sequencer); context.add_processor(SequenceProcessor); diff --git a/crates/components/sequencer/src/project_loading.rs b/crates/components/sequencer/src/project_loading.rs new file mode 100644 index 000000000..4068848c8 --- /dev/null +++ b/crates/components/sequencer/src/project_loading.rs @@ -0,0 +1,27 @@ +use mizer_module::*; +use crate::{Sequence, Sequencer}; + +impl ProjectHandler for Sequencer { + fn get_name(&self) -> &'static str { + "sequencer" + } + + fn new_project(&mut self, context: &mut impl ProjectHandlerContext) -> anyhow::Result<()> { + self.clear(); + + Ok(()) + } + + fn load_project(&mut self, context: &mut impl LoadProjectContext) -> anyhow::Result<()> { + let sequences = context.read_file::>("sequences")?; + self.load_sequences(sequences); + + Ok(()) + } + + fn save_project(&self, context: &mut impl SaveProjectContext) -> anyhow::Result<()> { + context.write_file("sequences", self.sequences())?; + + Ok(()) + } +} diff --git a/crates/components/surfaces/src/lib.rs b/crates/components/surfaces/src/lib.rs index b738853bf..ccb85a954 100644 --- a/crates/components/surfaces/src/lib.rs +++ b/crates/components/surfaces/src/lib.rs @@ -7,3 +7,4 @@ pub mod commands; mod models; mod module; mod registry; +mod project_loading; \ No newline at end of file diff --git a/crates/components/surfaces/src/module.rs b/crates/components/surfaces/src/module.rs index a75b8a797..fd7f47d96 100644 --- a/crates/components/surfaces/src/module.rs +++ b/crates/components/surfaces/src/module.rs @@ -1,6 +1,6 @@ use mizer_message_bus::MessageBus; use mizer_module::*; - +use crate::project_loading::SurfaceProjectHandler; use crate::registry::SurfaceRegistry; use crate::Surface; @@ -21,7 +21,8 @@ impl Module for SurfaceModule { let api = SurfaceRegistryApi { bus: registry.bus.clone(), }; - context.provide(registry); + context.provide(SurfaceProjectHandler); + context.provide_api(api); Ok(()) diff --git a/crates/components/surfaces/src/project_loading.rs b/crates/components/surfaces/src/project_loading.rs new file mode 100644 index 000000000..60935fa9e --- /dev/null +++ b/crates/components/surfaces/src/project_loading.rs @@ -0,0 +1,45 @@ +use mizer_module::{LoadProjectContext, ProjectHandler, ProjectHandlerContext, SaveProjectContext}; +use crate::{Surface, SurfaceRegistry}; + +pub struct SurfaceProjectHandler; + +impl ProjectHandler for SurfaceProjectHandler { + fn get_name(&self) -> &'static str { + "surfaces" + } + + fn new_project(&mut self, context: &mut impl ProjectHandlerContext) -> anyhow::Result<()> { + let Some(registry) = context.try_get_mut::() else { + context.report_issue("Unable to load surfaces"); + + return Ok(()); + }; + registry.clear_surfaces(); + + Ok(()) + } + + fn load_project(&mut self, context: &mut impl LoadProjectContext) -> anyhow::Result<()> { + let surfaces = context.read_file::>("surfaces")?; + let Some(registry) = context.try_get_mut::() else { + context.report_issue("Unable to load surfaces"); + + return Ok(()); + }; + registry.clear_surfaces(); + registry.add_surfaces(surfaces); + + Ok(()) + } + + fn save_project(&self, context: &mut impl SaveProjectContext) -> anyhow::Result<()> { + let Some(registry) = context.try_get_mut::() else { + context.report_issue("Unable to load surfaces"); + + return Ok(()); + }; + context.write_file("surfaces", registry.list_surfaces())?; + + Ok(()) + } +} diff --git a/crates/mizer/src/api/handler.rs b/crates/mizer/src/api/handler.rs index f144167fc..140a198d7 100644 --- a/crates/mizer/src/api/handler.rs +++ b/crates/mizer/src/api/handler.rs @@ -90,9 +90,9 @@ impl ApiHandler { } ApiCommand::NewProject(sender) => { profiling::scope!("ApiCommand::NewProject"); - mizer.new_project(); + let result = mizer.new_project(); sender - .send(Ok(())) + .send(result) .expect("api command sender disconnected"); } ApiCommand::LoadProject(path, sender) => { diff --git a/crates/mizer/src/lib.rs b/crates/mizer/src/lib.rs index 06cd85c67..8b40206d8 100644 --- a/crates/mizer/src/lib.rs +++ b/crates/mizer/src/lib.rs @@ -9,3 +9,4 @@ mod flags; mod mizer; mod module_context; mod runtime_builder; +mod project_handler; \ No newline at end of file diff --git a/crates/mizer/src/mizer.rs b/crates/mizer/src/mizer.rs index 0910b4fa7..49a68a501 100644 --- a/crates/mizer/src/mizer.rs +++ b/crates/mizer/src/mizer.rs @@ -5,23 +5,17 @@ use anyhow::Context; use mizer_api::handlers::Handlers; use mizer_console::ConsoleCategory; -use mizer_fixtures::manager::FixtureManager; use mizer_media::{MediaDiscovery, MediaServer}; use mizer_message_bus::MessageBus; -use mizer_module::Runtime; -use mizer_project_files::{history::ProjectHistory, Project, ProjectManager, ProjectManagerMut}; -use mizer_protocol_dmx::*; -use mizer_protocol_mqtt::MqttConnectionManager; -use mizer_protocol_osc::OscConnectionManager; +use mizer_module::{Injector, ProjectHandlerContext, Runtime}; +use mizer_project_files::{HandlerContext, history::ProjectHistory}; use mizer_runtime::DefaultRuntime; -use mizer_sequencer::{EffectEngine, Sequencer}; use mizer_session::SessionState; use mizer_status_bus::{ProjectStatus, StatusBus}; -use mizer_surfaces::SurfaceRegistry; -use mizer_timecode::TimecodeManager; use crate::api::*; use crate::flags::Flags; +use crate::project_handler::ErasedProjectHandler; pub struct Mizer { pub flags: Flags, @@ -32,6 +26,25 @@ pub struct Mizer { pub session_events: MessageBus, pub project_history: ProjectHistory, pub status_bus: StatusBus, + pub(crate) project_handlers: Vec>, +} + +pub(crate) struct NewProjectContext<'a> { + injector: &'a mut Injector, +} + +impl<'a> ProjectHandlerContext for NewProjectContext<'a> { + fn try_get(&self) -> Option<&T> { + self.injector.get() + } + + fn try_get_mut(&mut self) -> Option<&mut T> { + self.injector.get_mut() + } + + fn report_issue(&mut self, issue: impl Into) { + mizer_console::error!(ConsoleCategory::Projects, "{}", issue.into()); + } } impl Mizer { @@ -66,40 +79,29 @@ impl Mizer { } #[profiling::function] - pub fn new_project(&mut self) { + pub fn new_project(&mut self) -> anyhow::Result<()> { tracing::info!("Creating new project..."); mizer_util::message!("New Project", 0); self.runtime .add_status_message("Creating new project...", None); - self.close_project(); let injector = self.runtime.injector_mut(); - let fixture_manager = injector.get::().unwrap(); - fixture_manager.new_project(); - let effects_engine = injector.get_mut::().unwrap(); - effects_engine.new_project(); - let sequencer = injector.get::().unwrap(); - sequencer.new_project(); - let timecode_manager = injector.get::().unwrap(); - timecode_manager.new_project(); - let dmx_manager = injector.get_mut::().unwrap(); - dmx_manager.new_project(); - let mqtt_manager = injector.get_mut::().unwrap(); - mqtt_manager.new_project(); - let osc_manager = injector.get_mut::().unwrap(); - osc_manager.new_project(); - let surface_registry = injector.get_mut::().unwrap(); - surface_registry.new_project(); - self.runtime.new_project(); + let mut context = NewProjectContext { + injector + }; + for project_handler in &mut self.project_handlers { + project_handler.new_project(&mut context)?; + } self.send_session_update(); self.runtime .add_status_message("Created new project", Some(Duration::from_secs(10))); self.status_bus.send_current_project(ProjectStatus::New); mizer_console::info(ConsoleCategory::Projects, "New project created"); + + Ok(()) } #[profiling::function] pub fn load_project_from(&mut self, path: PathBuf) -> anyhow::Result<()> { - self.close_project(); self.project_path = Some(path); self.load_project()?; @@ -112,42 +114,10 @@ impl Mizer { if let Some(ref path) = self.project_path { self.runtime.add_status_message("Loading project...", None); tracing::info!("Loading project {:?}...", path); - let project = Project::load_file(path)?; - { - let injector = self.runtime.injector_mut(); - let manager: &FixtureManager = injector.get().unwrap(); - manager.load(&project).context("loading fixtures")?; - let effects_engine = injector.get_mut::().unwrap(); - effects_engine.load(&project)?; - let sequencer = injector.get::().unwrap(); - sequencer.load(&project).context("loading sequences")?; - let timecode_manager = injector.get::().unwrap(); - timecode_manager - .load(&project) - .context("loading timecodes")?; - let dmx_manager = injector.get_mut::().unwrap(); - dmx_manager - .load(&project) - .context("loading dmx connections")?; - let mqtt_manager = injector.get_mut::().unwrap(); - mqtt_manager - .load(&project) - .context("loading mqtt connections")?; - let osc_manager = injector.get_mut::().unwrap(); - osc_manager - .load(&project) - .context("loading osc connections")?; - let surface_registry = injector.get_mut::().unwrap(); - surface_registry - .load(&project) - .context("loading surfaces")?; + let mut context = HandlerContext::open(self.runtime.injector_mut(), path)?; + for project_handler in &mut self.project_handlers { + project_handler.load_project(&mut context)?; } - self.media_server_api - .load(&project) - .context("loading media files")?; - start_media_discovery(&project.media.import_paths, &self.media_server_api) - .context("starting media discovery")?; - self.runtime.load(&project).context("loading project")?; tracing::info!("Loading project...Done"); if self.flags.generate_graph { @@ -183,32 +153,16 @@ impl Mizer { } #[profiling::function] - pub fn save_project(&self) -> anyhow::Result<()> { + pub fn save_project(&mut self) -> anyhow::Result<()> { mizer_util::message!("Saving Project", 0); if let Some(ref path) = self.project_path { self.runtime.add_status_message("Saving project...", None); tracing::info!("Saving project to {:?}...", path); - let mut project = Project::new(); - self.runtime.save(&mut project); - let injector = self.runtime.injector(); - let fixture_manager = injector.get::().unwrap(); - fixture_manager.save(&mut project); - let dmx_manager = injector.get::().unwrap(); - dmx_manager.save(&mut project); - let mqtt_manager = injector.get::().unwrap(); - mqtt_manager.save(&mut project); - let osc_manager = injector.get::().unwrap(); - osc_manager.save(&mut project); - let sequencer = injector.get::().unwrap(); - sequencer.save(&mut project); - let timecode_manager = injector.get::().unwrap(); - timecode_manager.save(&mut project); - let effects_engine = injector.get::().unwrap(); - effects_engine.save(&mut project); - let surface_registry = injector.get::().unwrap(); - surface_registry.save(&mut project); - self.media_server_api.save(&mut project); - project.save_file(path)?; + let mut context = HandlerContext::new(self.runtime.injector_mut()); + for project_handler in &mut self.project_handlers { + project_handler.save_project(&mut context)?; + } + context.save(path)?; tracing::info!("Saving project...Done"); self.runtime.add_status_message( format!("Project saved ({path:?})"), @@ -219,31 +173,6 @@ impl Mizer { Ok(()) } - #[profiling::function] - pub fn close_project(&mut self) { - mizer_util::message!("Closing Project", 0); - self.runtime.clear(); - let injector = self.runtime.injector_mut(); - let fixture_manager = injector.get::().unwrap(); - fixture_manager.clear(); - let dmx_manager = injector.get_mut::().unwrap(); - dmx_manager.clear(); - let mqtt_manager = injector.get_mut::().unwrap(); - mqtt_manager.clear(); - let osc_manager = injector.get_mut::().unwrap(); - osc_manager.clear(); - let sequencer = injector.get::().unwrap(); - sequencer.clear(); - let timecode_manager = injector.get::().unwrap(); - timecode_manager.clear(); - self.project_path = None; - self.media_server_api.clear(); - let effects_engine = injector.get_mut::().unwrap(); - effects_engine.clear(); - self.send_session_update(); - self.status_bus.send_current_project(ProjectStatus::None); - } - #[profiling::function] fn send_session_update(&self) { let history = match self.project_history.load() { diff --git a/crates/mizer/src/module_context.rs b/crates/mizer/src/module_context.rs index f8a96cff0..dfb1615a0 100644 --- a/crates/mizer/src/module_context.rs +++ b/crates/mizer/src/module_context.rs @@ -1,11 +1,12 @@ use mizer_debug_ui_impl::{DebugUiImpl, DebugUiPane}; use std::future::Future; -use mizer_module::{ApiInjector, ModuleContext, Runtime}; +use mizer_module::{ApiInjector, ModuleContext, ProjectHandler, Runtime}; use mizer_processing::Processor; use mizer_runtime::DefaultRuntime; use mizer_settings::Settings; use mizer_status_bus::StatusHandle; +use crate::project_handler::ErasedProjectHandler; pub struct SetupContext { pub runtime: DefaultRuntime, @@ -13,6 +14,7 @@ pub struct SetupContext { pub settings: Settings, pub handle: tokio::runtime::Handle, pub debug_ui_panes: Vec>>, + pub project_handlers: Vec>, } impl ModuleContext for SetupContext { @@ -67,6 +69,10 @@ impl ModuleContext for SetupContext { fn status_handle(&self) -> StatusHandle { self.runtime.access().status_bus.handle() } + + fn add_project_handler(&mut self, handler: impl ProjectHandler + 'static) { + self.project_handlers.push(Box::new(handler)); + } } impl SetupContext { diff --git a/crates/mizer/src/project_handler.rs b/crates/mizer/src/project_handler.rs new file mode 100644 index 000000000..825f9d2ef --- /dev/null +++ b/crates/mizer/src/project_handler.rs @@ -0,0 +1,65 @@ +use mizer_module::{LoadProjectContext, ProjectHandler, ProjectHandlerContext, SaveProjectContext}; +use mizer_project_files::HandlerContext; + +pub(crate) trait ErasedProjectHandler { + fn new_project(&mut self, context: &mut crate::mizer::NewProjectContext) -> anyhow::Result<()>; + fn load_project(&mut self, context: &mut HandlerContext) -> anyhow::Result<()>; + fn save_project(&self, context: &mut HandlerContext) -> anyhow::Result<()>; +} + +impl ErasedProjectHandler for T { + fn new_project(&mut self, context: &mut crate::mizer::NewProjectContext) -> anyhow::Result<()> { + let mut context = ContextWrapper::new(self, context); + T::new_project(self, &mut context) + } + + fn load_project(&mut self, context: &mut HandlerContext) -> anyhow::Result<()> { + let mut context = ContextWrapper::new(self, context); + T::load_project(self, &mut context) + } + + fn save_project(&self, context: &mut HandlerContext) -> anyhow::Result<()> { + let mut context = ContextWrapper::new(self, context); + T::save_project(self, &mut context) + } +} + +struct ContextWrapper<'a, TContext> { + module: &'static str, + context: &'a mut TContext, +} + +impl<'a, TContext> ContextWrapper<'a, TContext> { + fn new(handler: &impl ProjectHandler, context: &'a mut TContext) -> Self { + Self { + module: handler.get_name(), + context, + } + } +} + +impl<'a, TContext: ProjectHandlerContext> ProjectHandlerContext for ContextWrapper<'a, TContext> { + fn try_get(&self) -> Option<&T> { + self.context.try_get() + } + + fn try_get_mut(&mut self) -> Option<&mut T> { + self.context.try_get_mut() + } + + fn report_issue(&mut self, issue: impl Into) { + self.context.report_issue(issue) + } +} + +impl<'a, TContext: LoadProjectContext> LoadProjectContext for ContextWrapper<'a, TContext> { + fn read_file(&self, filename: &str) -> anyhow::Result { + self.context.read_file(&format!("{}/{filename}", self.module)) + } +} + +impl<'a, TContext: SaveProjectContext> SaveProjectContext for ContextWrapper<'a, TContext> { + fn write_file(&mut self, filename: &str, content: T) -> anyhow::Result<()> { + self.context.write_file(&format!("{}/{filename}", self.module), content) + } +} diff --git a/crates/mizer/src/runtime_builder.rs b/crates/mizer/src/runtime_builder.rs index a8bf8758a..f266c2244 100644 --- a/crates/mizer/src/runtime_builder.rs +++ b/crates/mizer/src/runtime_builder.rs @@ -83,6 +83,7 @@ pub fn build_runtime( settings: settings_manager.read().settings, handle: handle.clone(), debug_ui_panes: Vec::new(), + project_handlers: Vec::new(), }; load_modules(&mut context, &flags); @@ -110,6 +111,7 @@ pub fn build_runtime( let mut mizer = Mizer { project_path: flags.file.clone(), + project_handlers: context.project_handlers, flags, runtime: context.runtime, handlers, @@ -152,10 +154,10 @@ fn open_project(mizer: &mut Mizer, settings: Settings) -> anyhow::Result<()> { mizer_console::error!(mizer_console::ConsoleCategory::Projects, "Failed to load last project"); } } else { - mizer.new_project(); + mizer.new_project()?; } } else { - mizer.new_project(); + mizer.new_project()?; } Ok(()) diff --git a/crates/projects/Cargo.toml b/crates/projects/Cargo.toml index c13732326..951bbf501 100644 --- a/crates/projects/Cargo.toml +++ b/crates/projects/Cargo.toml @@ -25,12 +25,14 @@ mizer-protocol-dmx = { path = "../components/connections/protocols/dmx" } mizer-protocol-mqtt = { path = "../components/connections/protocols/mqtt" } mizer-protocol-osc = { path = "../components/connections/protocols/osc" } mizer-surfaces = { path = "../components/surfaces" } +mizer-module = { path = "../runtime/module" } directories-next = "2.0" fs2 = "0.4" indexmap = { version = "2.2", features = ["serde"] } enum_dispatch = "0.3" enum-iterator = "2.0" profiling = "1.0" +zip = "2.1" [dev-dependencies] test-case = "3.3" diff --git a/crates/projects/src/effects.rs b/crates/projects/src/effects.rs deleted file mode 100644 index 38219f686..000000000 --- a/crates/projects/src/effects.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::{Project, ProjectManagerMut}; -use mizer_sequencer::EffectEngine; - -impl ProjectManagerMut for EffectEngine { - fn new_project(&mut self) { - tracing::debug!("new effect engine"); - self.load_defaults(); - } - - fn load(&mut self, project: &Project) -> anyhow::Result<()> { - profiling::scope!("EffectEngine::load"); - tracing::debug!("load effect engine"); - for effect in &project.effects { - self.effects.insert(effect.id, effect.clone()); - } - Ok(()) - } - - fn save(&self, project: &mut Project) { - profiling::scope!("EffectEngine::save"); - tracing::debug!("save effect engine"); - for effect in self.effects.iter() { - project.effects.push(effect.value().clone()); - } - } - - fn clear(&mut self) { - tracing::debug!("clear effect engine"); - EffectEngine::clear(self); - } -} diff --git a/crates/projects/src/fixtures.rs b/crates/projects/src/fixtures.rs index 84b157cf8..17fb38260 100644 --- a/crates/projects/src/fixtures.rs +++ b/crates/projects/src/fixtures.rs @@ -1,75 +1,5 @@ -use crate::{FixtureConfig, Project, ProjectManager}; -use mizer_fixtures::manager::FixtureManager; -use mizer_fixtures::programmer::{Color, Position, Preset, Presets}; +use mizer_fixtures::programmer::{Color, Position, Preset}; use serde::{Deserialize, Serialize}; -use std::ops::Deref; - -impl ProjectManager for FixtureManager { - fn new_project(&self) { - self.presets.load_defaults(); - } - - fn load(&self, project: &Project) -> anyhow::Result<()> { - profiling::scope!("FixtureManager::load"); - for fixture in &project.fixtures { - let def = self.get_definition(&fixture.fixture); - if let Some(def) = def { - self.add_fixture( - fixture.id, - fixture.name.clone(), - def, - fixture.mode.clone(), - fixture.channel, - fixture.universe, - fixture.configuration.clone(), - ); - } else { - tracing::warn!( - "No fixture definition for fixture id {}. Missing fixture definition: {}", - fixture.id, - fixture.fixture - ); - mizer_console::error!( - mizer_console::ConsoleCategory::Projects, - "No fixture definition for fixture id {}. Missing fixture definition: {}", - fixture.id, - fixture.fixture - ); - } - } - for group in &project.groups { - self.groups.insert(group.id, group.clone()); - } - project.presets.load(&self.presets); - Ok(()) - } - - fn save(&self, project: &mut Project) { - profiling::scope!("FixtureManager::save"); - for fixture in self.get_fixtures() { - project.fixtures.push(FixtureConfig { - id: fixture.id, - name: fixture.name.clone(), - universe: fixture.universe.into(), - channel: fixture.channel, - fixture: fixture.definition.id.clone(), - mode: fixture.current_mode.name.clone().into(), - configuration: fixture.configuration.clone(), - }); - } - for group in self.get_groups() { - project.groups.push(group.deref().clone()); - } - project.presets = PresetsStore::store(&self.presets); - } - - fn clear(&self) { - self.fixtures.clear(); - self.groups.clear(); - self.presets.clear(); - self.states.clear(); - } -} #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct PresetsStore { @@ -82,50 +12,3 @@ pub struct PresetsStore { #[serde(default)] pub position: Vec>, } - -impl PresetsStore { - fn load(&self, presets: &Presets) { - for preset in self.intensity.iter() { - presets.intensity.insert(preset.id, preset.clone()); - } - for preset in self.shutter.iter() { - presets.shutter.insert(preset.id, preset.clone()); - } - for preset in self.color.iter() { - presets.color.insert(preset.id, preset.clone()); - } - for preset in self.position.iter() { - presets.position.insert(preset.id, preset.clone()); - } - } - - fn store(presets: &Presets) -> Self { - let intensity = presets - .intensity - .iter() - .map(|entry| entry.value().clone()) - .collect(); - let shutter = presets - .shutter - .iter() - .map(|entry| entry.value().clone()) - .collect(); - let color = presets - .color - .iter() - .map(|entry| entry.value().clone()) - .collect(); - let position = presets - .position - .iter() - .map(|entry| entry.value().clone()) - .collect(); - - Self { - intensity, - shutter, - color, - position, - } - } -} diff --git a/crates/projects/src/handler_context.rs b/crates/projects/src/handler_context.rs new file mode 100644 index 000000000..2d823b093 --- /dev/null +++ b/crates/projects/src/handler_context.rs @@ -0,0 +1,86 @@ +use std::fs::File; +use std::io::Write; +use std::path::Path; +use serde::de::DeserializeOwned; +use serde::Serialize; +use zip::unstable::LittleEndianWriteExt; +use mizer_console::ConsoleCategory; +use mizer_module::{Injector, LoadProjectContext, ProjectHandlerContext, SaveProjectContext}; +use crate::project_file::{ProjectArchive, ProjectFile}; +use crate::versioning::{migrate, Migrations}; + +pub struct HandlerContext<'a> { + archive: ProjectArchive, + injector: &'a mut Injector, +} + +impl<'a> HandlerContext<'a> { + pub fn new(injector: &'a mut Injector) -> Self { + Self { + archive: Default::default(), + injector + } + } + + pub fn open(injector: &'a mut Injector, path: impl AsRef) -> anyhow::Result { + let mut file = ProjectFile::open(path.as_ref())?; + migrate(&mut file)?; + let archive = match file { + ProjectFile::Archive(archive) => archive, + _ => return Err(anyhow::anyhow!("Invalid project file")), + }; + + Ok(Self { + archive, + injector, + }) + } + + pub fn save(mut self, path: impl AsRef) -> anyhow::Result<()> { + { + let mut writer = self.archive.write()?; + writer.write_file("VERSION")?; + writer.write_u32_le(Migrations::latest_version())?; + } + let mut file = File::create(path.as_ref())?; + file.write_all(&self.archive.0)?; + + Ok(()) + } +} + +impl<'a> ProjectHandlerContext for HandlerContext<'a> { + fn try_get(&self) -> Option<&T> { + self.injector.get() + } + + fn try_get_mut(&mut self) -> Option<&mut T> { + self.injector.get_mut() + } + + fn report_issue(&mut self, issue: impl Into) { + mizer_console::error!(ConsoleCategory::Projects, "{}", issue.into()); + } +} + +impl<'a> LoadProjectContext for HandlerContext<'a> { + fn read_file(&self, filename: &str) -> anyhow::Result { + tracing::trace!("Reading file {}", filename); + let mut file = self.archive.read()?; + let mut file = file.read_file(&format!("{filename}.json"))?; + let content = serde_json::from_reader(&mut file)?; + + Ok(content) + } +} + +impl<'a> SaveProjectContext for HandlerContext<'a> { + fn write_file(&mut self, filename: &str, content: T) -> anyhow::Result<()> { + tracing::trace!("Writing file {}", filename); + let mut writer = self.archive.write()?; + writer.write_file(&format!("{filename}.json"))?; + serde_json::to_writer(writer, &content)?; + + Ok(()) + } +} diff --git a/crates/projects/src/lib.rs b/crates/projects/src/lib.rs index 72e6d393c..5b0ca71d2 100644 --- a/crates/projects/src/lib.rs +++ b/crates/projects/src/lib.rs @@ -1,6 +1,6 @@ use std::convert::TryFrom; use std::fs::File; -use std::io::Read; +use std::io::{Cursor, Read, Seek}; use std::net::Ipv4Addr; use std::path::Path; @@ -22,17 +22,18 @@ use mizer_timecode::{TimecodeControl, TimecodeTrack}; use crate::fixtures::PresetsStore; use crate::media::Media; +use crate::project_file::ProjectFile; use crate::versioning::{migrate, Migrations}; +pub use crate::handler_context::HandlerContext; mod connections; -mod effects; mod fixtures; pub mod history; mod media; -mod sequencer; -mod surfaces; mod timecode; mod versioning; +mod handler_context; +mod project_file; lazy_static! { static ref CHANNEL_REGEX: Regex = RegexBuilder::new( @@ -46,7 +47,7 @@ lazy_static! { #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Project { #[serde(default)] - pub version: usize, + pub version: u32, #[serde(default)] pub playback: PlaybackSettings, #[serde(default)] @@ -104,22 +105,20 @@ impl Project { #[profiling::function] pub fn load_file>(path: P) -> anyhow::Result { - let mut file = File::open(path)?; - let mut project_file = String::new(); - file.read_to_string(&mut project_file)?; + let path = path.as_ref(); + let mut project_file = ProjectFile::open(path.as_ref())?; migrate(&mut project_file)?; - let project = serde_yaml::from_str(&project_file)?; - - Ok(project) + todo!(); } - pub fn load(content: &str) -> anyhow::Result { - let mut content = content.to_string(); - migrate(&mut content)?; - let project = serde_yaml::from_str(&content)?; - - Ok(project) - } + // pub fn load(content: &str) -> anyhow::Result { + // let mut content = content.to_string(); + // let file_content = unsafe { content.as_mut_vec() }; + // migrate(file_content)?; + // let project = serde_yaml::from_str(&content)?; + // + // Ok(project) + // } // TODO: do file persistence on background thread #[profiling::function] @@ -240,256 +239,3 @@ pub enum ConnectionTypes { Mqtt(MqttAddress), Osc(OscAddress), } - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use mizer_node::NodePosition; - - use super::*; - - #[test] - fn load_empty_project() -> anyhow::Result<()> { - let content = "nodes: []\nchannels: []"; - - let result = Project::load(content)?; - - assert_eq!(result.nodes.len(), 0, "no nodes"); - assert_eq!(result.channels.len(), 0, "no channels"); - assert_eq!(result.fixtures.len(), 0, "no fixtures"); - Ok(()) - } - - #[test] - fn load_single_node() -> anyhow::Result<()> { - let content = r#" - nodes: - - type: opc-output - path: /opc-output-0 - config: - host: 0.0.0.0 - width: 10 - height: 20 - designer: - position: - x: 1 - y: 2 - scale: 3 - "#; - - let result = Project::load(content)?; - - assert_eq!(result.nodes.len(), 1); - assert_eq!( - result.nodes[0], - Node { - path: "/opc-output-0".into(), - config: mizer_nodes::Node::OpcOutput(mizer_nodes::OpcOutputNode { - host: "0.0.0.0".into(), - port: 7890, - width: 10, - height: 20 - }), - designer: NodeDesigner { - hidden: false, - position: NodePosition { x: 1., y: 2. }, - scale: 3., - color: Default::default(), - }, - } - ); - Ok(()) - } - - #[test] - fn load_channel() -> anyhow::Result<()> { - let content = r#" - nodes: - - type: pixel-pattern - path: /pixel-pattern-0 - config: - pattern: rgb-iterate - - type: opc-output - path: /opc-output-0 - config: - host: 127.0.0.1 - width: 25 - height: 50 - channels: - - Output@/pixel-pattern-0 -> Pixels@/opc-output-0 - "#; - - let result = Project::load(content)?; - - assert_eq!(result.nodes.len(), 2); - assert_eq!(result.channels.len(), 1); - assert_eq!( - result.nodes[0], - Node { - path: "/pixel-pattern-0".into(), - config: mizer_nodes::Node::PixelPattern(mizer_nodes::PixelPatternGeneratorNode { - pattern: mizer_pixel_nodes::Pattern::RgbIterate - }), - designer: Default::default(), - } - ); - assert_eq!( - result.nodes[1], - Node { - path: "/opc-output-0".into(), - config: mizer_nodes::Node::OpcOutput(mizer_nodes::OpcOutputNode { - host: "127.0.0.1".into(), - port: 7890, - width: 25, - height: 50 - }), - designer: Default::default(), - } - ); - assert_eq!( - result.channels[0], - Channel { - from_path: "/pixel-pattern-0".into(), - from_channel: "Output".into(), - to_path: "/opc-output-0".into(), - to_channel: "Pixels".into() - } - ); - Ok(()) - } - - #[test] - fn load_channel_should_support_uppercase() -> anyhow::Result<()> { - let content = r#" - nodes: [] - channels: - - Output@/pixel-pattern-0 -> Pixels@/opc-output-0 - "#; - - let result = Project::load(content)?; - - assert_eq!( - result.channels[0], - Channel { - from_path: "/pixel-pattern-0".into(), - from_channel: "Output".into(), - to_path: "/opc-output-0".into(), - to_channel: "Pixels".into() - } - ); - Ok(()) - } - - #[test] - fn load_properties() -> anyhow::Result<()> { - let mut expected = HashMap::new(); - expected.insert("value".to_string(), 0.5f64); - let content = r#" - nodes: - - type: fader - path: /fader-0 - config: {} - "#; - - let result = Project::load(content)?; - - assert_eq!(result.nodes.len(), 1); - assert_eq!( - result.nodes[0], - Node { - path: "/fader-0".into(), - config: mizer_nodes::Node::Fader(mizer_nodes::FaderNode::default()), - designer: Default::default(), - } - ); - Ok(()) - } - - #[test] - fn load_fixtures() -> anyhow::Result<()> { - let content = r#" - fixtures: - - id: 1 - name: My Fixture - fixture: fixture-definition-ref - output: output - channel: 1 - "#; - - let result = Project::load(content)?; - - assert_eq!(result.fixtures.len(), 1); - assert_eq!( - result.fixtures[0], - FixtureConfig { - id: 1, - name: "My Fixture".into(), - fixture: "fixture-definition-ref".into(), - channel: 1, - universe: None, - mode: None, - configuration: Default::default(), - } - ); - Ok(()) - } - - #[test] - fn load_fixtures_with_mode() -> anyhow::Result<()> { - let content = r#" - fixtures: - - id: 1 - name: My Fixture - fixture: another-fixture - channel: 5 - mode: 2-channel - "#; - - let result = Project::load(content)?; - - assert_eq!(result.fixtures.len(), 1); - assert_eq!( - result.fixtures[0], - FixtureConfig { - id: 1, - name: "My Fixture".into(), - fixture: "another-fixture".into(), - channel: 5, - universe: None, - mode: Some("2-channel".into()), - configuration: Default::default(), - } - ); - Ok(()) - } - - #[test] - fn load_fixtures_with_universe() -> anyhow::Result<()> { - let content = r#" - fixtures: - - id: 1 - name: My Fixture - fixture: another-fixture - channel: 5 - universe: 1 - "#; - - let result = Project::load(content)?; - - assert_eq!(result.fixtures.len(), 1); - assert_eq!( - result.fixtures[0], - FixtureConfig { - id: 1, - name: "My Fixture".into(), - fixture: "another-fixture".into(), - channel: 5, - universe: Some(1), - mode: None, - configuration: Default::default(), - } - ); - Ok(()) - } -} diff --git a/crates/projects/src/project_file/archive_file.rs b/crates/projects/src/project_file/archive_file.rs new file mode 100644 index 000000000..3500f9812 --- /dev/null +++ b/crates/projects/src/project_file/archive_file.rs @@ -0,0 +1,56 @@ +use std::io::{Cursor, Read, Write}; + +use zip::{ZipArchive, ZipWriter}; + +#[derive(Default)] +pub(crate) struct ProjectArchive(pub(crate) Vec); + +impl ProjectArchive { + pub fn new() -> Self { + Self::default() + } + + pub fn read(&self) -> anyhow::Result { + let archive = ZipArchive::new(Cursor::new(self.0.as_slice()))?; + + Ok(ProjectArchiveReader(archive)) + } + + pub fn write(&mut self) -> anyhow::Result { + let archive = if self.0.is_empty() { + ZipWriter::new(Cursor::new(&mut self.0)) + }else { + ZipWriter::new_append(Cursor::new(&mut self.0))? + }; + + Ok(ProjectArchiveWriter(archive)) + } +} + +pub(crate) struct ProjectArchiveReader<'a>(ZipArchive>); + +impl<'a> ProjectArchiveReader<'a> { + pub fn read_file(&mut self, file_name: &str) -> anyhow::Result { + Ok(self.0.by_name(file_name)?) + } +} + +pub(crate) struct ProjectArchiveWriter<'a>(ZipWriter>>); + +impl<'a> ProjectArchiveWriter<'a> { + pub fn write_file(&mut self, file_name: &str) -> anyhow::Result<()> { + self.0.start_file::<_, (), _>(file_name, Default::default())?; + + Ok(()) + } +} + +impl<'a> Write for ProjectArchiveWriter<'a> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.0.flush() + } +} diff --git a/crates/projects/src/project_file/mod.rs b/crates/projects/src/project_file/mod.rs new file mode 100644 index 000000000..0a2aa2f43 --- /dev/null +++ b/crates/projects/src/project_file/mod.rs @@ -0,0 +1,113 @@ +use std::fs::File; +use std::io::Read; +use std::path::Path; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use zip::unstable::{LittleEndianReadExt, LittleEndianWriteExt}; +pub(crate) use self::archive_file::ProjectArchive; + +mod archive_file; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProjectFileType { + Yaml, + Zip, +} + +impl TryFrom<&Path> for ProjectFileType { + type Error = anyhow::Error; + + fn try_from(value: &Path) -> Result { + match value.extension().and_then(|ext| ext.to_str()) { + Some("yaml") | Some("yml") => Ok(Self::Yaml), + Some("zip") => Ok(Self::Zip), + _ => anyhow::bail!("unsupported file type"), + } + } +} + +pub(crate) enum ProjectFile { + Yaml(Vec), + Archive(ProjectArchive), +} + +impl ProjectFile { + pub fn open(path: &Path) -> anyhow::Result { + let file_type = ProjectFileType::try_from(path)?; + let mut file = File::open(path)?; + let mut file_content = Vec::new(); + file.read_to_end(&mut file_content)?; + match file_type { + ProjectFileType::Yaml => { + Ok(Self::Yaml(file_content)) + } + ProjectFileType::Zip => { + Ok(Self::Archive(ProjectArchive(file_content))) + } + } + } + + #[profiling::function] + pub(crate) fn get_version(&self) -> anyhow::Result { + match self { + Self::Yaml(file) => { + let project_file: ProjectVersion = serde_yaml::from_slice(file)?; + + Ok(project_file.version) + } + Self::Archive(archive) => { + let mut reader = archive.read()?; + let mut file = reader.read_file("VERSION")?; + let version = file.read_u32_le()?; + + Ok(version) + } + } + } + + #[profiling::function] + pub(crate) fn write_version(&mut self, version: u32) -> anyhow::Result<()> { + match self { + Self::Yaml(content) => { + let mut project_file: ProjectVersionWithContent = serde_yaml::from_slice(content)?; + project_file.version = version; + *content = serde_yaml::to_vec(&project_file)?; + } + Self::Archive(archive) => { + let mut writer = archive.write()?; + writer.write_file("VERSION")?; + writer.write_u32_le(version)?; + } + } + + Ok(()) + } + + pub(crate) fn as_yaml(&mut self) -> Option<&mut Vec> { + match self { + Self::Yaml(content) => Some(content), + _ => None, + } + } + + pub(crate) fn as_archive(&mut self) -> Option<&mut ProjectArchive> { + match self { + Self::Archive(content) => Some(content), + _ => None, + } + }} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +struct ProjectVersion { + #[serde(default)] + pub version: u32, +} + + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +struct ProjectVersionWithContent { + #[serde(default)] + pub version: u32, + #[serde(flatten)] + content: IndexMap, +} diff --git a/crates/projects/src/sequencer.rs b/crates/projects/src/sequencer.rs deleted file mode 100644 index f8818c640..000000000 --- a/crates/projects/src/sequencer.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{Project, ProjectManager}; -use mizer_sequencer::Sequencer; - -impl ProjectManager for Sequencer { - fn load(&self, project: &Project) -> anyhow::Result<()> { - profiling::scope!("Sequencer::load"); - self.load_sequences(project.sequences.clone()); - - Ok(()) - } - - fn save(&self, project: &mut Project) { - profiling::scope!("Sequencer::save"); - project.sequences = self.sequences(); - } - - fn clear(&self) { - self.clear(); - } -} diff --git a/crates/projects/src/surfaces.rs b/crates/projects/src/surfaces.rs deleted file mode 100644 index bf432b95a..000000000 --- a/crates/projects/src/surfaces.rs +++ /dev/null @@ -1,18 +0,0 @@ -use mizer_surfaces::SurfaceRegistry; - -use crate::{Project, ProjectManagerMut}; - -impl ProjectManagerMut for SurfaceRegistry { - fn load(&mut self, project: &Project) -> anyhow::Result<()> { - self.add_surfaces(project.surfaces.clone()); - Ok(()) - } - - fn save(&self, project: &mut Project) { - project.surfaces = self.list_surfaces(); - } - - fn clear(&mut self) { - self.clear_surfaces() - } -} diff --git a/crates/projects/src/versioning/migration.rs b/crates/projects/src/versioning/migration.rs index 5808b8e6b..d225785dd 100644 --- a/crates/projects/src/versioning/migration.rs +++ b/crates/projects/src/versioning/migration.rs @@ -1,6 +1,6 @@ -use super::ObjectSafeProjectMigration; -use crate::versioning::migrations::ProjectFileMigration; -use crate::versioning::{get_version, write_version}; +use crate::project_file::ProjectFile; +use super::ProjectMigration; +use crate::versioning::migrations::{ProjectArchiveMigration, ProjectFileMigration}; #[macro_export] macro_rules! migrations { @@ -35,26 +35,76 @@ macro_rules! migrations { #[enum_dispatch::enum_dispatch(Migrations)] - pub(super) trait ObjectSafeProjectMigration { - fn version(&self) -> usize; + pub(super) trait ProjectMigration { + fn version(&self) -> u32; - fn migrate(&self, project: &mut String) -> anyhow::Result<()>; + fn migrate(&self, project: &mut ProjectFile) -> anyhow::Result<()>; } } } -impl ObjectSafeProjectMigration for T { - #[inline] - fn version(&self) -> usize { - T::VERSION +// These macros exist because we cannot use specialization to generate different implementations for the ProjectMigration trait + +#[macro_export] +macro_rules! yaml_migrations { + ($($variant:ident),*,) => { + $( + yaml_migration!($variant); + )* } +} - fn migrate(&self, project: &mut String) -> anyhow::Result<()> { - let version = get_version(project)?; - tracing::info!("Migrating project file from {} to {}", version, T::VERSION); - self.migrate(project)?; - write_version(project, T::VERSION)?; +#[macro_export] +macro_rules! yaml_migration { + ($variant:ident) => { + impl ProjectMigration for $variant { + #[inline] + fn version(&self) -> u32 { + <$variant as ProjectFileMigration>::VERSION + } + + fn migrate(&self, project_file: &mut ProjectFile) -> anyhow::Result<()> { + let version = project_file.get_version()?; + tracing::info!("Migrating project file from {} to {}", version, <$variant as ProjectFileMigration>::VERSION); + let file_content = project_file.as_yaml() + .ok_or_else(|| anyhow::anyhow!("Trying to apply yaml migration on new project file"))?; + ProjectFileMigration::migrate(self, file_content)?; + project_file.write_version(<$variant as ProjectFileMigration>::VERSION)?; + + Ok(()) + } + } + }; +} - Ok(()) +#[macro_export] +macro_rules! archive_migrations { + ($($variant:ident),*,) => { + $( + archive_migration!($variant); + )* } } + +#[macro_export] +macro_rules! archive_migration { + ($variant:ident) => { + impl ProjectMigration for $variant { + #[inline] + fn version(&self) -> u32 { + <$variant as ProjectArchiveMigration>::VERSION + } + + fn migrate(&self, project_file: &mut ProjectFile) -> anyhow::Result<()> { + let version = project_file.get_version()?; + tracing::info!("Migrating project file from {} to {}", version, <$variant as ProjectArchiveMigration>::VERSION); + let file_content = project_file.as_archive() + .ok_or_else(|| anyhow::anyhow!("Trying to apply archive migration on old project file"))?; + ProjectArchiveMigration::migrate(self, file_content)?; + project_file.write_version(<$variant as ProjectArchiveMigration>::VERSION)?; + + Ok(()) + } + } + }; +} diff --git a/crates/projects/src/versioning/migrations/adapt_fader_config.rs b/crates/projects/src/versioning/migrations/adapt_fader_config.rs index 95561b93b..9e463fff7 100644 --- a/crates/projects/src/versioning/migrations/adapt_fader_config.rs +++ b/crates/projects/src/versioning/migrations/adapt_fader_config.rs @@ -8,14 +8,14 @@ use crate::versioning::migrations::ProjectFileMigration; pub struct AdaptFaderConfig; impl ProjectFileMigration for AdaptFaderConfig { - const VERSION: usize = 5; + const VERSION: u32 = 5; - fn migrate(&self, project_file: &mut String) -> anyhow::Result<()> { + fn migrate(&self, project_file: &mut Vec) -> anyhow::Result<()> { profiling::scope!("AdaptFaderConfig::migrate"); - let project: ProjectConfig = serde_yaml::from_str(project_file)?; + let project: ProjectConfig = serde_yaml::from_slice(&project_file)?; let project: ProjectConfig = project.into(); - *project_file = serde_yaml::to_string(&project)?; + *project_file = serde_yaml::to_vec(&project)?; Ok(()) } diff --git a/crates/projects/src/versioning/migrations/archive_file.rs b/crates/projects/src/versioning/migrations/archive_file.rs new file mode 100644 index 000000000..f96cc92c1 --- /dev/null +++ b/crates/projects/src/versioning/migrations/archive_file.rs @@ -0,0 +1,67 @@ +use crate::Project; +use crate::project_file::{ProjectArchive, ProjectFile}; +use crate::versioning::ProjectMigration; + +const ARCHIVE_VERSION: u32 = 10; + +#[derive(Clone, Copy)] +pub struct ArchiveFile; + +impl ProjectMigration for ArchiveFile { + fn version(&self) -> u32 { + ARCHIVE_VERSION + } + + + fn migrate(&self, project_file: &mut ProjectFile) -> anyhow::Result<()> { + let file_content = project_file.as_yaml() + .ok_or_else(|| anyhow::anyhow!("Trying to convert non yaml project file to new version"))?; + let project: Project = serde_yaml::from_slice(&file_content)?; + + let archive = ProjectArchive::try_from(project)?; + *project_file = ProjectFile::Archive(archive); + + Ok(()) + } +} + +impl TryFrom for ProjectArchive { + type Error = anyhow::Error; + + fn try_from(project: Project) -> Result { + let mut archive = ProjectArchive::default(); + { + let mut writer = archive.write()?; + writer.write_file("playback.json")?; + serde_json::to_writer(&mut writer, &project.playback)?; + writer.write_file("fixtures/patch.json")?; + serde_json::to_writer(&mut writer, &project.fixtures)?; + writer.write_file("fixtures/groups.json")?; + serde_json::to_writer(&mut writer, &project.groups)?; + writer.write_file("fixtures/presets.json")?; + serde_json::to_writer(&mut writer, &project.presets)?; + writer.write_file("sequencer/sequences.json")?; + serde_json::to_writer(&mut writer, &project.sequences)?; + writer.write_file("effects/effects.json")?; + serde_json::to_writer(&mut writer, &project.effects)?; + writer.write_file("timecodes/timecodes.json")?; + serde_json::to_writer(&mut writer, &project.timecodes)?; + writer.write_file("pipeline/nodes.json")?; + serde_json::to_writer(&mut writer, &project.nodes)?; + writer.write_file("pipeline/channels.json")?; + serde_json::to_writer(&mut writer, &project.channels)?; + writer.write_file("layouts/layouts.json")?; + serde_json::to_writer(&mut writer, &project.layouts)?; + writer.write_file("plans/plans.json")?; + serde_json::to_writer(&mut writer, &project.plans)?; + writer.write_file("surfaces/surfaces.json")?; + serde_json::to_writer(&mut writer, &project.surfaces)?; + writer.write_file("connections/connections.json")?; + serde_json::to_writer(&mut writer, &project.connections)?; + writer.write_file("media/media.json")?; + serde_json::to_writer(&mut writer, &project.media)?; + } + + Ok(archive) + } +} diff --git a/crates/projects/src/versioning/migrations/migrate_position_presets.rs b/crates/projects/src/versioning/migrations/migrate_position_presets.rs index cae7ad69a..5f4f46e5f 100644 --- a/crates/projects/src/versioning/migrations/migrate_position_presets.rs +++ b/crates/projects/src/versioning/migrations/migrate_position_presets.rs @@ -8,14 +8,14 @@ use crate::versioning::migrations::ProjectFileMigration; pub struct MigratePositionPresets; impl ProjectFileMigration for MigratePositionPresets { - const VERSION: usize = 4; + const VERSION: u32 = 4; - fn migrate(&self, project_file: &mut String) -> anyhow::Result<()> { + fn migrate(&self, project_file: &mut Vec) -> anyhow::Result<()> { profiling::scope!("MigratePositionPresets::migrate"); - let project: ProjectConfig = serde_yaml::from_str(project_file)?; + let project: ProjectConfig = serde_yaml::from_slice(&project_file)?; let project: ProjectConfig = project.into(); - *project_file = serde_yaml::to_string(&project)?; + *project_file = serde_yaml::to_vec(&project)?; Ok(()) } diff --git a/crates/projects/src/versioning/migrations/mod.rs b/crates/projects/src/versioning/migrations/mod.rs index 6adbae72e..e6de6dcdf 100644 --- a/crates/projects/src/versioning/migrations/mod.rs +++ b/crates/projects/src/versioning/migrations/mod.rs @@ -3,15 +3,24 @@ pub use migrate_position_presets::*; pub use rename_ports::*; pub use rework_layout_controls_to_not_use_nodes::*; pub use rework_midi_config::*; +pub use archive_file::*; +use crate::project_file::ProjectArchive; mod adapt_fader_config; mod migrate_position_presets; mod rename_ports; mod rework_layout_controls_to_not_use_nodes; mod rework_midi_config; +mod archive_file; pub trait ProjectFileMigration: Clone + Copy { - const VERSION: usize; + const VERSION: u32; - fn migrate(&self, project_file: &mut String) -> anyhow::Result<()>; + fn migrate(&self, project_file: &mut Vec) -> anyhow::Result<()>; +} + +pub trait ProjectArchiveMigration: Clone + Copy { + const VERSION: u32; + + fn migrate(&self, project_archive: &mut ProjectArchive) -> anyhow::Result<()>; } diff --git a/crates/projects/src/versioning/migrations/rename_ports.rs b/crates/projects/src/versioning/migrations/rename_ports.rs index 33d4b1f05..8daa09143 100644 --- a/crates/projects/src/versioning/migrations/rename_ports.rs +++ b/crates/projects/src/versioning/migrations/rename_ports.rs @@ -9,11 +9,11 @@ use serde_yaml::Value; pub struct RenamePorts; impl ProjectFileMigration for RenamePorts { - const VERSION: usize = 1; + const VERSION: u32 = 1; - fn migrate(&self, project_file: &mut String) -> anyhow::Result<()> { + fn migrate(&self, project_file: &mut Vec) -> anyhow::Result<()> { profiling::scope!("RenamePorts::migrate"); - let mut project: ProjectConfig = serde_yaml::from_str(project_file)?; + let mut project: ProjectConfig = serde_yaml::from_slice(&project_file)?; project.rename_input(NodeType::Select, "channel", "Channel"); project.rename_input(NodeType::Select, "input", "Inputs"); @@ -77,13 +77,13 @@ impl ProjectFileMigration for RenamePorts { project.rename_output(NodeType::IldaFile, "frames", "Frames"); project.rename_input(NodeType::Laser, "input", "Frames"); - *project_file = serde_yaml::to_string(&project)?; + *project_file = serde_yaml::to_vec(&project)?; Ok(()) } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] struct ProjectConfig { #[serde(default)] pub nodes: Vec, @@ -131,7 +131,7 @@ impl ProjectConfig { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] struct Node { #[serde(rename = "type")] node_type: NodeType, diff --git a/crates/projects/src/versioning/migrations/rework_layout_controls_to_not_use_nodes.rs b/crates/projects/src/versioning/migrations/rework_layout_controls_to_not_use_nodes.rs index f8bbc77fc..2e355b628 100644 --- a/crates/projects/src/versioning/migrations/rework_layout_controls_to_not_use_nodes.rs +++ b/crates/projects/src/versioning/migrations/rework_layout_controls_to_not_use_nodes.rs @@ -9,14 +9,14 @@ use crate::versioning::migrations::ProjectFileMigration; pub struct ReworkLayoutControlsToNotUseNodes; impl ProjectFileMigration for ReworkLayoutControlsToNotUseNodes { - const VERSION: usize = 3; + const VERSION: u32 = 3; - fn migrate(&self, project_file: &mut String) -> anyhow::Result<()> { + fn migrate(&self, project_file: &mut Vec) -> anyhow::Result<()> { profiling::scope!("ReworkLayoutControlsToNotUseNodes::migrate"); - let project: ProjectConfig = serde_yaml::from_str(project_file)?; + let project: ProjectConfig = serde_yaml::from_slice(&project_file)?; let project: ProjectConfig = project.into(); - *project_file = serde_yaml::to_string(&project)?; + *project_file = serde_yaml::to_vec(&project)?; Ok(()) } diff --git a/crates/projects/src/versioning/migrations/rework_midi_config.rs b/crates/projects/src/versioning/migrations/rework_midi_config.rs index 6aff78d24..8e8112cbc 100644 --- a/crates/projects/src/versioning/migrations/rework_midi_config.rs +++ b/crates/projects/src/versioning/migrations/rework_midi_config.rs @@ -8,14 +8,14 @@ use crate::versioning::migrations::ProjectFileMigration; pub struct ReworkMidiConfig; impl ProjectFileMigration for ReworkMidiConfig { - const VERSION: usize = 2; + const VERSION: u32 = 2; - fn migrate(&self, project_file: &mut String) -> anyhow::Result<()> { + fn migrate(&self, project_file: &mut Vec) -> anyhow::Result<()> { profiling::scope!("ReworkMidiConfig::migrate"); - let project: ProjectConfig = serde_yaml::from_str(project_file)?; + let project: ProjectConfig = serde_yaml::from_slice(&project_file)?; let project: ProjectConfig = project.into(); - *project_file = serde_yaml::to_string(&project)?; + *project_file = serde_yaml::to_vec(&project)?; Ok(()) } diff --git a/crates/projects/src/versioning/mod.rs b/crates/projects/src/versioning/mod.rs index 81c727220..14dba71c0 100644 --- a/crates/projects/src/versioning/mod.rs +++ b/crates/projects/src/versioning/mod.rs @@ -1,6 +1,5 @@ use enum_iterator::all; -use indexmap::IndexMap; -use serde::{Deserialize, Serialize}; +use crate::project_file::ProjectFile; use self::migrations::*; @@ -14,10 +13,19 @@ migrations! { ReworkLayoutControlsToNotUseNodes, MigratePositionPresets, AdaptFaderConfig, + ArchiveFile, +} + +yaml_migrations! { + RenamePorts, + ReworkMidiConfig, + ReworkLayoutControlsToNotUseNodes, + MigratePositionPresets, + AdaptFaderConfig, } impl Migrations { - pub fn latest_version() -> usize { + pub fn latest_version() -> u32 { all::() .map(|migration| migration.version()) .max() @@ -26,46 +34,16 @@ impl Migrations { } #[profiling::function] -pub fn migrate(project: &mut String) -> anyhow::Result<()> { - let version = get_version(project)?; +pub fn migrate(project_file: &mut ProjectFile) -> anyhow::Result<()> { + let version = project_file.get_version()?; let mut migrations = all::() .filter(|migration| migration.version() > version) .collect::>(); migrations.sort_by_key(|migration| migration.version()); for migration in migrations { - migration.migrate(project)?; + migration.migrate(project_file)?; } Ok(()) } - -#[profiling::function] -fn get_version(file: &str) -> anyhow::Result { - let project_file: ProjectVersion = serde_yaml::from_str(file)?; - - Ok(project_file.version) -} - -#[profiling::function] -fn write_version(file: &mut String, version: usize) -> anyhow::Result<()> { - let mut project_file: ProjectVersionWithContent = serde_yaml::from_str(file)?; - project_file.version = version; - *file = serde_yaml::to_string(&project_file)?; - - Ok(()) -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -struct ProjectVersion { - #[serde(default)] - pub version: usize, -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -struct ProjectVersionWithContent { - #[serde(default)] - pub version: usize, - #[serde(flatten)] - content: IndexMap, -} diff --git a/crates/runtime/module/Cargo.toml b/crates/runtime/module/Cargo.toml index a3cc2c5f6..d66706827 100644 --- a/crates/runtime/module/Cargo.toml +++ b/crates/runtime/module/Cargo.toml @@ -10,6 +10,7 @@ mizer-processing = { path = "../processing" } tracing = "0.1" mizer-settings = { path = "../settings"} mizer-status-bus = { path = "../../runtime/status" } +serde = "1" [dev-dependencies] test-case = "3.3" diff --git a/crates/runtime/module/src/lib.rs b/crates/runtime/module/src/lib.rs index d63c9f43d..c09b94806 100644 --- a/crates/runtime/module/src/lib.rs +++ b/crates/runtime/module/src/lib.rs @@ -1,6 +1,8 @@ use std::fmt::Display; use std::future::Future; use std::time::Duration; +use serde::de::DeserializeOwned; +use serde::Serialize; pub use mizer_processing::*; pub use mizer_settings::*; @@ -10,6 +12,28 @@ pub use crate::api_injector::ApiInjector; mod api_injector; +pub trait ProjectHandler { + fn get_name(&self) -> &'static str; + + fn new_project(&mut self, context: &mut impl ProjectHandlerContext) -> anyhow::Result<()>; + fn load_project(&mut self, context: &mut impl LoadProjectContext) -> anyhow::Result<()>; + fn save_project(&self, context: &mut impl SaveProjectContext) -> anyhow::Result<()>; +} + +pub trait ProjectHandlerContext { + fn try_get(&self) -> Option<&T>; + fn try_get_mut(&mut self) -> Option<&mut T>; + fn report_issue(&mut self, issue: impl Into); +} + +pub trait LoadProjectContext: ProjectHandlerContext { + fn read_file(&self, filename: &str) -> anyhow::Result; +} + +pub trait SaveProjectContext: ProjectHandlerContext { + fn write_file(&mut self, filename: &str, content: T) -> anyhow::Result<()>; +} + pub trait Module: Sized + Display { const IS_REQUIRED: bool; @@ -100,4 +124,6 @@ pub trait ModuleContext { fn spawn(&self, future: F) where ::Output: Send; fn status_handle(&self) -> StatusHandle; + + fn add_project_handler(&mut self, handler: impl ProjectHandler + 'static); }