Stabilize control surface and external bridge v1
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
use crate::{
|
||||
control::{
|
||||
CatalogSnapshot, CatalogSource, CreativeSnapshotSummary, GroupSummary, HostCommandError,
|
||||
PanelTarget, PresetSummary, SceneTransitionStyle,
|
||||
OutputBackendMode, PanelTarget, PresetSummary, SceneTransitionStyle,
|
||||
},
|
||||
scene::{PatternRegistry, SceneRuntime},
|
||||
};
|
||||
use infinity_config::{PanelPosition, ProjectConfig};
|
||||
use infinity_config::{ColorOrder, DriverKind, LedDirection, PanelPosition, ProjectConfig};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
@@ -59,16 +59,64 @@ impl Default for PersistedGlobalState {
|
||||
Self {
|
||||
blackout: false,
|
||||
master_brightness: 0.20,
|
||||
transition_duration_ms: 150,
|
||||
transition_duration_ms: 2_000,
|
||||
transition_style: SceneTransitionStyle::Crossfade,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_fps() -> u16 {
|
||||
40
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PersistedNodeState {
|
||||
pub node_id: String,
|
||||
pub reserved_ip: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PersistedPanelState {
|
||||
pub target: PanelTarget,
|
||||
pub physical_output_name: String,
|
||||
pub driver_kind: DriverKind,
|
||||
pub driver_reference: String,
|
||||
pub led_count: u16,
|
||||
pub direction: LedDirection,
|
||||
pub color_order: ColorOrder,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PersistedTechnicalState {
|
||||
pub backend_mode: OutputBackendMode,
|
||||
pub output_enabled: bool,
|
||||
#[serde(default = "default_output_fps")]
|
||||
pub output_fps: u16,
|
||||
#[serde(default)]
|
||||
pub nodes: Vec<PersistedNodeState>,
|
||||
#[serde(default)]
|
||||
pub panels: Vec<PersistedPanelState>,
|
||||
}
|
||||
|
||||
impl Default for PersistedTechnicalState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
backend_mode: OutputBackendMode::PreviewOnly,
|
||||
output_enabled: false,
|
||||
output_fps: default_output_fps(),
|
||||
nodes: Vec::new(),
|
||||
panels: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct PersistedRuntimeState {
|
||||
pub active_scene: Option<SceneRuntime>,
|
||||
pub global: PersistedGlobalState,
|
||||
#[serde(default)]
|
||||
pub technical: PersistedTechnicalState,
|
||||
pub user_presets: Vec<StoredPreset>,
|
||||
pub user_groups: Vec<StoredGroup>,
|
||||
pub creative_snapshots: Vec<StoredCreativeSnapshot>,
|
||||
@@ -467,6 +515,36 @@ impl ShowStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_preset(&mut self, preset_id: &str) -> Result<(), HostCommandError> {
|
||||
if preset_id.trim().is_empty() {
|
||||
return Err(HostCommandError::new(
|
||||
"invalid_preset_id",
|
||||
"preset_id must not be empty",
|
||||
));
|
||||
}
|
||||
|
||||
let preset_index = self
|
||||
.presets
|
||||
.iter()
|
||||
.position(|preset| preset.preset_id == preset_id)
|
||||
.ok_or_else(|| {
|
||||
HostCommandError::new(
|
||||
"unknown_preset",
|
||||
format!("preset '{preset_id}' does not exist"),
|
||||
)
|
||||
})?;
|
||||
|
||||
if self.presets[preset_index].source != CatalogSource::RuntimeUser {
|
||||
return Err(HostCommandError::new(
|
||||
"preset_delete_forbidden",
|
||||
format!("preset '{preset_id}' is built-in and cannot be deleted"),
|
||||
));
|
||||
}
|
||||
|
||||
self.presets.remove(preset_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_creative_snapshot(
|
||||
&mut self,
|
||||
snapshot_id: &str,
|
||||
@@ -554,10 +632,12 @@ impl ShowStore {
|
||||
&self,
|
||||
active_scene: &SceneRuntime,
|
||||
global: PersistedGlobalState,
|
||||
technical: PersistedTechnicalState,
|
||||
) -> PersistedRuntimeState {
|
||||
PersistedRuntimeState {
|
||||
active_scene: Some(active_scene.clone()),
|
||||
global,
|
||||
technical,
|
||||
user_presets: self
|
||||
.presets
|
||||
.iter()
|
||||
@@ -663,6 +743,45 @@ mod tests {
|
||||
assert!(store.recall_creative_snapshot("variant_a").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_presets_can_be_deleted_but_builtins_cannot() {
|
||||
let registry = PatternRegistry::new();
|
||||
let mut store = ShowStore::from_project(&sample_project(), ®istry);
|
||||
let scene = registry.scene_for_pattern(
|
||||
"noise",
|
||||
None,
|
||||
Some("top_panels".to_string()),
|
||||
31,
|
||||
vec!["#AA8844".to_string()],
|
||||
false,
|
||||
);
|
||||
store
|
||||
.save_preset_from_scene(
|
||||
"runtime_delete_me",
|
||||
&scene,
|
||||
210,
|
||||
SceneTransitionStyle::Crossfade,
|
||||
false,
|
||||
)
|
||||
.expect("runtime preset save should succeed");
|
||||
store
|
||||
.delete_preset("runtime_delete_me")
|
||||
.expect("runtime preset delete should succeed");
|
||||
assert!(store.scene_from_preset_id("runtime_delete_me").is_none());
|
||||
|
||||
let built_in_preset_id = store
|
||||
.catalog(®istry)
|
||||
.presets
|
||||
.iter()
|
||||
.find(|preset| preset.source == CatalogSource::BuiltIn)
|
||||
.map(|preset| preset.preset_id.clone())
|
||||
.expect("sample project should contain built-in presets");
|
||||
let delete_error = store
|
||||
.delete_preset(&built_in_preset_id)
|
||||
.expect_err("built-in presets should not be deletable");
|
||||
assert_eq!(delete_error.code, "preset_delete_forbidden");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_state_storage_roundtrip_preserves_scene_and_library() {
|
||||
let registry = PatternRegistry::new();
|
||||
@@ -701,6 +820,7 @@ mod tests {
|
||||
transition_duration_ms: 220,
|
||||
transition_style: SceneTransitionStyle::Chase,
|
||||
},
|
||||
PersistedTechnicalState::default(),
|
||||
);
|
||||
storage.save(&runtime).expect("save should work");
|
||||
let loaded = storage.load().expect("load should work");
|
||||
|
||||
Reference in New Issue
Block a user