diff options
Diffstat (limited to 'rust/crates/commands')
32 files changed, 1682 insertions, 0 deletions
diff --git a/rust/crates/commands/Cargo.toml b/rust/crates/commands/Cargo.toml new file mode 100644 index 00000000..ef5bc895 --- /dev/null +++ b/rust/crates/commands/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "commands" +version = "0.1.0" +edition = "2024" + +[dependencies] +animation = { version = "0.1.0", path = "../animation" } +chrono = { version = "0.4", features = ["serde"] } +scene = { version = "0.1.0", path = "../scene" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2" +time = { version = "0.1.0", path = "../time" } +timeline = { version = "0.1.0", path = "../timeline" } +uuid = { version = "1", features = ["v4"] } diff --git a/rust/crates/commands/src/batch_command.rs b/rust/crates/commands/src/batch_command.rs new file mode 100644 index 00000000..bd928d33 --- /dev/null +++ b/rust/crates/commands/src/batch_command.rs @@ -0,0 +1,61 @@ +use scene::Project; + +use crate::command::{Command, CommandError, CommandResult}; + +pub struct BatchCommand { + commands: Vec<Box<dyn Command>>, +} + +impl BatchCommand { + pub fn new(commands: Vec<Box<dyn Command>>) -> Self { + Self { commands } + } + + pub fn len(&self) -> usize { + self.commands.len() + } + + pub fn is_empty(&self) -> bool { + self.commands.is_empty() + } +} + +impl Command for BatchCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let mut latest_selection_result = None; + + for command in &mut self.commands { + let result = command.execute(project)?; + if result.as_ref().and_then(|r| r.selection.as_ref()).is_some() { + latest_selection_result = result; + } + } + + Ok(latest_selection_result) + } + + fn undo(&mut self, project: &mut Project) { + for command in self.commands.iter_mut().rev() { + command.undo(project); + } + } + + fn redo( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let mut latest_selection_result = None; + + for command in &mut self.commands { + let result = command.redo(project)?; + if result.as_ref().and_then(|r| r.selection.as_ref()).is_some() { + latest_selection_result = result; + } + } + + Ok(latest_selection_result) + } +} diff --git a/rust/crates/commands/src/bookmarks.rs b/rust/crates/commands/src/bookmarks.rs new file mode 100644 index 00000000..79a73015 --- /dev/null +++ b/rust/crates/commands/src/bookmarks.rs @@ -0,0 +1,94 @@ +use scene::Bookmark; +use time::{FrameRate, MediaTime}; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct BookmarkPatch { + pub note: Option<Option<String>>, + pub color: Option<Option<String>>, + pub duration: Option<Option<MediaTime>>, +} + +pub fn find_bookmark_index(bookmarks: &[Bookmark], frame_time: MediaTime) -> Option<usize> { + bookmarks.iter().position(|bookmark| bookmark.time == frame_time) +} + +pub fn is_bookmark_at_time(bookmarks: &[Bookmark], frame_time: MediaTime) -> bool { + find_bookmark_index(bookmarks, frame_time).is_some() +} + +pub fn toggle_bookmark_in_array( + bookmarks: &[Bookmark], + frame_time: MediaTime, +) -> Vec<Bookmark> { + if let Some(index) = find_bookmark_index(bookmarks, frame_time) { + return bookmarks + .iter() + .enumerate() + .filter(|(bookmark_index, _)| *bookmark_index != index) + .map(|(_, bookmark)| bookmark.clone()) + .collect(); + } + + let mut next = bookmarks.to_vec(); + next.push(Bookmark { + time: frame_time, + note: None, + color: None, + duration: None, + }); + next.sort_by_key(|bookmark| bookmark.time); + next +} + +pub fn remove_bookmark_from_array( + bookmarks: &[Bookmark], + frame_time: MediaTime, +) -> Vec<Bookmark> { + bookmarks + .iter() + .filter(|bookmark| bookmark.time != frame_time) + .cloned() + .collect() +} + +pub fn update_bookmark_in_array( + bookmarks: &[Bookmark], + frame_time: MediaTime, + updates: &BookmarkPatch, +) -> Vec<Bookmark> { + let Some(index) = find_bookmark_index(bookmarks, frame_time) else { + return bookmarks.to_vec(); + }; + + let mut next = bookmarks.to_vec(); + let bookmark = &mut next[index]; + if let Some(note) = &updates.note { + bookmark.note = note.clone(); + } + if let Some(color) = &updates.color { + bookmark.color = color.clone(); + } + if let Some(duration) = &updates.duration { + bookmark.duration = *duration; + } + next +} + +pub fn move_bookmark_in_array( + bookmarks: &[Bookmark], + from_time: MediaTime, + to_time: MediaTime, +) -> Vec<Bookmark> { + let Some(index) = find_bookmark_index(bookmarks, from_time) else { + return bookmarks.to_vec(); + }; + + let mut next = bookmarks.to_vec(); + next[index].time = to_time; + next.sort_by_key(|bookmark| bookmark.time); + next +} + +pub fn get_frame_time(time: MediaTime, fps: FrameRate) -> MediaTime { + time.round_to_frame(fps).unwrap_or(time) +} diff --git a/rust/crates/commands/src/command.rs b/rust/crates/commands/src/command.rs new file mode 100644 index 00000000..c13468e7 --- /dev/null +++ b/rust/crates/commands/src/command.rs @@ -0,0 +1,46 @@ +use scene::{ElementRef, Project}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum CommandError { + #[error("source track or element not found")] + SourceNotFound, + #[error("target track not found")] + TargetTrackNotFound, + #[error("{0}")] + IncompatiblePlacement(String), +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct SelectionPatch { + pub selected_elements: Vec<ElementRef>, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct CommandResult { + pub selection: Option<SelectionPatch>, +} + +impl CommandResult { + pub fn element_selection(selected_elements: Vec<ElementRef>) -> Self { + Self { + selection: Some(SelectionPatch { selected_elements }), + } + } +} + +pub trait Command { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError>; + + fn undo(&mut self, project: &mut Project); + + fn redo( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + self.execute(project) + } +} diff --git a/rust/crates/commands/src/history.rs b/rust/crates/commands/src/history.rs new file mode 100644 index 00000000..f8792373 --- /dev/null +++ b/rust/crates/commands/src/history.rs @@ -0,0 +1,68 @@ +use scene::Project; + +use crate::command::{Command, CommandError, CommandResult}; + +#[derive(Default)] +pub struct CommandHistory { + history: Vec<Box<dyn Command>>, + redo_stack: Vec<Box<dyn Command>>, +} + +impl CommandHistory { + pub fn new() -> Self { + Self::default() + } + + pub fn execute( + &mut self, + mut command: Box<dyn Command>, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let result = command.execute(project)?; + self.history.push(command); + self.redo_stack.clear(); + Ok(result) + } + + pub fn push(&mut self, command: Box<dyn Command>) { + self.history.push(command); + self.redo_stack.clear(); + } + + pub fn undo(&mut self, project: &mut Project) { + let Some(mut command) = self.history.pop() else { + return; + }; + command.undo(project); + self.redo_stack.push(command); + } + + pub fn redo( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(mut command) = self.redo_stack.pop() else { + return Ok(None); + }; + let result = command.redo(project)?; + self.history.push(command); + Ok(result) + } + + pub fn can_undo(&self) -> bool { + !self.history.is_empty() + } + + pub fn can_redo(&self) -> bool { + !self.redo_stack.is_empty() + } + + pub fn len(&self) -> usize { + self.history.len() + } + + pub fn clear(&mut self) { + self.history.clear(); + self.redo_stack.clear(); + } +} diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs new file mode 100644 index 00000000..3fede6bb --- /dev/null +++ b/rust/crates/commands/src/lib.rs @@ -0,0 +1,18 @@ +pub mod batch_command; +pub mod bookmarks; +pub mod command; +pub mod history; +pub mod preview_tracker; +pub mod project; +pub mod project_ext; +pub mod scene; +pub mod timeline; +pub mod tracks; + +pub use batch_command::BatchCommand; +pub use command::{Command, CommandError, CommandResult, SelectionPatch}; +pub use history::CommandHistory; +pub use preview_tracker::PreviewTracker; +pub use project::*; +pub use scene::*; +pub use timeline::*; diff --git a/rust/crates/commands/src/preview_tracker.rs b/rust/crates/commands/src/preview_tracker.rs new file mode 100644 index 00000000..4eb7484d --- /dev/null +++ b/rust/crates/commands/src/preview_tracker.rs @@ -0,0 +1,28 @@ +#[derive(Clone, Debug, Default)] +pub struct PreviewTracker<T> { + snapshot: Option<T>, +} + +impl<T: Clone> PreviewTracker<T> { + pub fn new() -> Self { + Self { snapshot: None } + } + + pub fn begin(&mut self, state: &T) { + if self.snapshot.is_none() { + self.snapshot = Some(state.clone()); + } + } + + pub fn is_active(&self) -> bool { + self.snapshot.is_some() + } + + pub fn snapshot(&self) -> Option<&T> { + self.snapshot.as_ref() + } + + pub fn end(&mut self) -> Option<T> { + self.snapshot.take() + } +} diff --git a/rust/crates/commands/src/project/mod.rs b/rust/crates/commands/src/project/mod.rs new file mode 100644 index 00000000..28e9c92c --- /dev/null +++ b/rust/crates/commands/src/project/mod.rs @@ -0,0 +1,3 @@ +pub mod update_project_settings; + +pub use update_project_settings::{ProjectSettingsPatch, UpdateProjectSettingsCommand}; diff --git a/rust/crates/commands/src/project/update_project_settings.rs b/rust/crates/commands/src/project/update_project_settings.rs new file mode 100644 index 00000000..a0f6e99c --- /dev/null +++ b/rust/crates/commands/src/project/update_project_settings.rs @@ -0,0 +1,77 @@ +use scene::{Background, CanvasSize, CanvasSizeMode, Project, ProjectSettings}; +use time::FrameRate; + +use crate::command::{Command, CommandError, CommandResult}; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ProjectSettingsPatch { + pub fps: Option<FrameRate>, + pub canvas_size: Option<CanvasSize>, + pub canvas_size_mode: Option<Option<CanvasSizeMode>>, + pub last_custom_canvas_size: Option<Option<CanvasSize>>, + pub original_canvas_size: Option<Option<CanvasSize>>, + pub background: Option<Background>, +} + +impl ProjectSettingsPatch { + pub fn apply_to(&self, settings: &mut ProjectSettings) { + if let Some(fps) = self.fps { + settings.fps = fps; + } + if let Some(canvas_size) = self.canvas_size { + settings.canvas_size = canvas_size; + } + if let Some(canvas_size_mode) = &self.canvas_size_mode { + settings.canvas_size_mode = *canvas_size_mode; + } + if let Some(last_custom_canvas_size) = &self.last_custom_canvas_size { + settings.last_custom_canvas_size = *last_custom_canvas_size; + } + if let Some(original_canvas_size) = &self.original_canvas_size { + settings.original_canvas_size = *original_canvas_size; + } + if let Some(background) = &self.background { + settings.background = background.clone(); + } + } +} + +pub struct UpdateProjectSettingsCommand { + updates: ProjectSettingsPatch, + saved_settings: Option<ProjectSettings>, + saved_updated_at: Option<chrono::DateTime<chrono::Utc>>, +} + +impl UpdateProjectSettingsCommand { + pub fn new(updates: ProjectSettingsPatch) -> Self { + Self { + updates, + saved_settings: None, + saved_updated_at: None, + } + } +} + +impl Command for UpdateProjectSettingsCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + self.saved_settings = Some(project.settings.clone()); + self.saved_updated_at = Some(project.metadata.updated_at); + + self.updates.apply_to(&mut project.settings); + project.metadata.updated_at = chrono::Utc::now(); + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let (Some(saved_settings), Some(saved_updated_at)) = + (&self.saved_settings, self.saved_updated_at) + else { + return; + }; + project.settings = saved_settings.clone(); + project.metadata.updated_at = saved_updated_at; + } +} diff --git a/rust/crates/commands/src/project_ext.rs b/rust/crates/commands/src/project_ext.rs new file mode 100644 index 00000000..18262fbd --- /dev/null +++ b/rust/crates/commands/src/project_ext.rs @@ -0,0 +1,20 @@ +use scene::{Project, Scene}; + +pub fn active_scene(project: &Project) -> Option<&Scene> { + project + .scenes + .iter() + .find(|scene| scene.id == project.current_scene_id) + .or_else(|| project.scenes.iter().find(|scene| scene.is_main)) + .or_else(|| project.scenes.first()) +} + +pub fn active_scene_mut(project: &mut Project) -> Option<&mut Scene> { + let index = project + .scenes + .iter() + .position(|scene| scene.id == project.current_scene_id) + .or_else(|| project.scenes.iter().position(|scene| scene.is_main)) + .or(if project.scenes.is_empty() { None } else { Some(0) })?; + project.scenes.get_mut(index) +} diff --git a/rust/crates/commands/src/scene/create_scene.rs b/rust/crates/commands/src/scene/create_scene.rs new file mode 100644 index 00000000..1f2de02b --- /dev/null +++ b/rust/crates/commands/src/scene/create_scene.rs @@ -0,0 +1,46 @@ +use scene::{Project, Scene}; + +use crate::command::{Command, CommandError, CommandResult}; + +pub struct CreateSceneCommand { + name: String, + is_main: bool, + saved_scenes: Option<Vec<Scene>>, + created_scene_id: Option<String>, +} + +impl CreateSceneCommand { + pub fn new(name: impl Into<String>, is_main: bool) -> Self { + Self { + name: name.into(), + is_main, + saved_scenes: None, + created_scene_id: None, + } + } + + pub fn scene_id(&self) -> Option<&str> { + self.created_scene_id.as_deref() + } +} + +impl Command for CreateSceneCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + self.saved_scenes = Some(project.scenes.clone()); + + let created = timeline::scenes::build_default_scene(&self.name, self.is_main); + self.created_scene_id = Some(created.id.clone()); + project.scenes.push(created); + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_scenes else { + return; + }; + project.scenes = saved.clone(); + } +} diff --git a/rust/crates/commands/src/scene/delete_scene.rs b/rust/crates/commands/src/scene/delete_scene.rs new file mode 100644 index 00000000..61763dfd --- /dev/null +++ b/rust/crates/commands/src/scene/delete_scene.rs @@ -0,0 +1,61 @@ +use scene::{Project, Scene}; + +use crate::command::{Command, CommandError, CommandResult}; + +pub struct DeleteSceneCommand { + scene_id: String, + saved_scenes: Option<Vec<Scene>>, + saved_current_scene_id: Option<String>, +} + +impl DeleteSceneCommand { + pub fn new(scene_id: impl Into<String>) -> Self { + Self { + scene_id: scene_id.into(), + saved_scenes: None, + saved_current_scene_id: None, + } + } +} + +impl Command for DeleteSceneCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(deleted) = project.scenes.iter().find(|scene| scene.id == self.scene_id) + else { + return Ok(None); + }; + if deleted.is_main { + return Ok(None); + } + + self.saved_scenes = Some(project.scenes.clone()); + self.saved_current_scene_id = Some(project.current_scene_id.clone()); + + project.scenes.retain(|scene| scene.id != self.scene_id); + + if project.current_scene_id == self.scene_id { + let fallback_id = project + .scenes + .iter() + .find(|scene| scene.is_main) + .map(|scene| scene.id.clone()); + if let Some(fallback_id) = fallback_id { + project.current_scene_id = fallback_id; + } + } + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let (Some(saved_scenes), Some(saved_current_scene_id)) = + (&self.saved_scenes, &self.saved_current_scene_id) + else { + return; + }; + project.scenes = saved_scenes.clone(); + project.current_scene_id = saved_current_scene_id.clone(); + } +} diff --git a/rust/crates/commands/src/scene/mod.rs b/rust/crates/commands/src/scene/mod.rs new file mode 100644 index 00000000..10d687c1 --- /dev/null +++ b/rust/crates/commands/src/scene/mod.rs @@ -0,0 +1,15 @@ +pub mod create_scene; +pub mod delete_scene; +pub mod move_bookmark; +pub mod remove_bookmark; +pub mod rename_scene; +pub mod toggle_bookmark; +pub mod update_bookmark; + +pub use create_scene::CreateSceneCommand; +pub use delete_scene::DeleteSceneCommand; +pub use move_bookmark::MoveBookmarkCommand; +pub use remove_bookmark::RemoveBookmarkCommand; +pub use rename_scene::RenameSceneCommand; +pub use toggle_bookmark::ToggleBookmarkCommand; +pub use update_bookmark::UpdateBookmarkCommand; diff --git a/rust/crates/commands/src/scene/move_bookmark.rs b/rust/crates/commands/src/scene/move_bookmark.rs new file mode 100644 index 00000000..dddef11f --- /dev/null +++ b/rust/crates/commands/src/scene/move_bookmark.rs @@ -0,0 +1,50 @@ +use scene::Project; +use time::MediaTime; + +use crate::bookmarks::{get_frame_time, move_bookmark_in_array}; +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +pub struct MoveBookmarkCommand { + from_time: MediaTime, + to_time: MediaTime, + saved_bookmarks: Option<Vec<scene::Bookmark>>, +} + +impl MoveBookmarkCommand { + pub fn new(from_time: MediaTime, to_time: MediaTime) -> Self { + Self { + from_time, + to_time, + saved_bookmarks: None, + } + } +} + +impl Command for MoveBookmarkCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let fps = project.settings.fps; + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_bookmarks = Some(scene.bookmarks.clone()); + let from_frame_time = get_frame_time(self.from_time, fps); + let to_frame_time = get_frame_time(self.to_time, fps); + scene.bookmarks = + move_bookmark_in_array(&scene.bookmarks, from_frame_time, to_frame_time); + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_bookmarks else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.bookmarks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/scene/remove_bookmark.rs b/rust/crates/commands/src/scene/remove_bookmark.rs new file mode 100644 index 00000000..f8fe0c5a --- /dev/null +++ b/rust/crates/commands/src/scene/remove_bookmark.rs @@ -0,0 +1,54 @@ +use scene::Project; +use time::MediaTime; + +use crate::bookmarks::{get_frame_time, remove_bookmark_from_array}; +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +pub struct RemoveBookmarkCommand { + time: MediaTime, + frame_time: MediaTime, + saved_bookmarks: Option<Vec<scene::Bookmark>>, +} + +impl RemoveBookmarkCommand { + pub fn new(time: MediaTime) -> Self { + Self { + time, + frame_time: MediaTime::ZERO, + saved_bookmarks: None, + } + } +} + +impl Command for RemoveBookmarkCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let fps = project.settings.fps; + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_bookmarks = Some(scene.bookmarks.clone()); + self.frame_time = get_frame_time(self.time, fps); + + let updated = remove_bookmark_from_array(&scene.bookmarks, self.frame_time); + if updated.len() == scene.bookmarks.len() { + return Ok(None); + } + + scene.bookmarks = updated; + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_bookmarks else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.bookmarks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/scene/rename_scene.rs b/rust/crates/commands/src/scene/rename_scene.rs new file mode 100644 index 00000000..4876fb9b --- /dev/null +++ b/rust/crates/commands/src/scene/rename_scene.rs @@ -0,0 +1,58 @@ +use scene::Project; + +use crate::command::{Command, CommandError, CommandResult}; + +pub struct RenameSceneCommand { + scene_id: String, + new_name: String, + saved_name: Option<String>, + saved_updated_at: Option<chrono::DateTime<chrono::Utc>>, +} + +impl RenameSceneCommand { + pub fn new(scene_id: impl Into<String>, new_name: impl Into<String>) -> Self { + Self { + scene_id: scene_id.into(), + new_name: new_name.into(), + saved_name: None, + saved_updated_at: None, + } + } +} + +impl Command for RenameSceneCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = project + .scenes + .iter_mut() + .find(|scene| scene.id == self.scene_id) + else { + return Ok(None); + }; + + self.saved_name = Some(scene.name.clone()); + self.saved_updated_at = Some(scene.updated_at); + scene.name = self.new_name.clone(); + scene.updated_at = chrono::Utc::now(); + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let (Some(saved_name), Some(saved_updated_at)) = + (&self.saved_name, self.saved_updated_at) + else { + return; + }; + if let Some(scene) = project + .scenes + .iter_mut() + .find(|scene| scene.id == self.scene_id) + { + scene.name = saved_name.clone(); + scene.updated_at = saved_updated_at; + } + } +} diff --git a/rust/crates/commands/src/scene/toggle_bookmark.rs b/rust/crates/commands/src/scene/toggle_bookmark.rs new file mode 100644 index 00000000..054e1c78 --- /dev/null +++ b/rust/crates/commands/src/scene/toggle_bookmark.rs @@ -0,0 +1,48 @@ +use scene::Project; + +use crate::bookmarks::{get_frame_time, toggle_bookmark_in_array}; +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use time::MediaTime; + +pub struct ToggleBookmarkCommand { + time: MediaTime, + frame_time: MediaTime, + saved_bookmarks: Option<Vec<scene::Bookmark>>, +} + +impl ToggleBookmarkCommand { + pub fn new(time: MediaTime) -> Self { + Self { + time, + frame_time: MediaTime::ZERO, + saved_bookmarks: None, + } + } +} + +impl Command for ToggleBookmarkCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let fps = project.settings.fps; + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_bookmarks = Some(scene.bookmarks.clone()); + self.frame_time = get_frame_time(self.time, fps); + scene.bookmarks = toggle_bookmark_in_array(&scene.bookmarks, self.frame_time); + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_bookmarks else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.bookmarks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/scene/update_bookmark.rs b/rust/crates/commands/src/scene/update_bookmark.rs new file mode 100644 index 00000000..f2c9b94e --- /dev/null +++ b/rust/crates/commands/src/scene/update_bookmark.rs @@ -0,0 +1,49 @@ +use scene::Project; +use time::MediaTime; + +use crate::bookmarks::{BookmarkPatch, get_frame_time, update_bookmark_in_array}; +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +pub struct UpdateBookmarkCommand { + time: MediaTime, + updates: BookmarkPatch, + saved_bookmarks: Option<Vec<scene::Bookmark>>, +} + +impl UpdateBookmarkCommand { + pub fn new(time: MediaTime, updates: BookmarkPatch) -> Self { + Self { + time, + updates, + saved_bookmarks: None, + } + } +} + +impl Command for UpdateBookmarkCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let fps = project.settings.fps; + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_bookmarks = Some(scene.bookmarks.clone()); + let frame_time = get_frame_time(self.time, fps); + scene.bookmarks = + update_bookmark_in_array(&scene.bookmarks, frame_time, &self.updates); + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_bookmarks else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.bookmarks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/element/delete_elements.rs b/rust/crates/commands/src/timeline/element/delete_elements.rs new file mode 100644 index 00000000..bdcb5b6d --- /dev/null +++ b/rust/crates/commands/src/timeline/element/delete_elements.rs @@ -0,0 +1,64 @@ +use scene::{ElementRef, Project, SceneTracks, Track}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +fn remove_track_elements(track: &mut Track, elements: &[ElementRef]) { + if !elements.iter().any(|target| target.track_id == track.id()) { + return; + } + track.elements_mut().retain(|element| { + !elements + .iter() + .any(|target| target.track_id == track.id() && target.element_id == element.base().id) + }); +} + +fn remove_elements_from_tracks(tracks: &mut SceneTracks, elements: &[ElementRef]) { + for track in &mut tracks.overlay { + remove_track_elements(track, elements); + } + remove_track_elements(&mut tracks.main, elements); + for track in &mut tracks.audio { + remove_track_elements(track, elements); + } +} + +pub struct DeleteElementsCommand { + elements: Vec<ElementRef>, + saved_state: Option<SceneTracks>, +} + +impl DeleteElementsCommand { + pub fn new(elements: Vec<ElementRef>) -> Self { + Self { + elements, + saved_state: None, + } + } +} + +impl Command for DeleteElementsCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + remove_elements_from_tracks(&mut scene.tracks, &self.elements); + + Ok(Some(CommandResult::element_selection(Vec::new()))) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/element/mod.rs b/rust/crates/commands/src/timeline/element/mod.rs new file mode 100644 index 00000000..2ba806e1 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/mod.rs @@ -0,0 +1,3 @@ +pub mod delete_elements; + +pub use delete_elements::DeleteElementsCommand; diff --git a/rust/crates/commands/src/timeline/element/move_elements.rs b/rust/crates/commands/src/timeline/element/move_elements.rs new file mode 100644 index 00000000..fc6425d1 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/move_elements.rs @@ -0,0 +1,145 @@ +use scene::{Element, Project, SceneTracks, Track}; +use time::MediaTime; +use timeline::TrackType; +use timeline::placement::build_empty_track; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::{all_tracks_mut, find_track}; + +#[derive(Clone, Debug, PartialEq)] +pub struct PlannedTrackCreation { + pub id: String, + pub track_type: TrackType, + pub index: usize, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PlannedElementMove { + pub source_track_id: String, + pub target_track_id: String, + pub element_id: String, + pub new_start_time: MediaTime, +} + +fn insert_track_at_display_index( + tracks: &mut SceneTracks, + track: Track, + insert_index: usize, +) { + if matches!(track, Track::Audio { .. }) { + let audio_insert_index = insert_index + .saturating_sub(tracks.overlay.len() + 1) + .min(tracks.audio.len()); + tracks.audio.insert(audio_insert_index, track); + return; + } + + let overlay_insert_index = insert_index.min(tracks.overlay.len()); + tracks.overlay.insert(overlay_insert_index, track); +} + +fn validate_compatibility(element: &Element, track: &Track) -> Result<(), CommandError> { + timeline::placement::validate_element_track_compatibility(element, track) + .map_err(CommandError::IncompatiblePlacement) +} + +pub struct MoveElementCommand { + moves: Vec<PlannedElementMove>, + create_tracks: Vec<PlannedTrackCreation>, + saved_state: Option<SceneTracks>, +} + +impl MoveElementCommand { + pub fn new(moves: Vec<PlannedElementMove>, create_tracks: Vec<PlannedTrackCreation>) -> Self { + Self { + moves, + create_tracks, + saved_state: None, + } + } +} + +impl Command for MoveElementCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + let saved_state = scene.tracks.clone(); + self.saved_state = Some(saved_state.clone()); + + let mut create_tracks = self.create_tracks.clone(); + create_tracks.sort_by_key(|creation| creation.index); + for creation in &create_tracks { + let track = build_empty_track(&creation.id, creation.track_type, None); + insert_track_at_display_index(&mut scene.tracks, track, creation.index); + } + + let mut moved_elements = Vec::new(); + for planned_move in &self.moves { + let source_track = find_track(&saved_state, &planned_move.source_track_id); + let source_element = source_track.and_then(|track| { + track + .elements() + .iter() + .find(|element| element.base().id == planned_move.element_id) + }); + let Some(source_element) = source_element else { + return Err(CommandError::SourceNotFound); + }; + + let Some(target_track) = find_track(&scene.tracks, &planned_move.target_track_id) + else { + return Err(CommandError::TargetTrackNotFound); + }; + + validate_compatibility(source_element, target_track)?; + + let mut moved = source_element.clone(); + moved.base_mut().start_time = planned_move.new_start_time; + moved_elements.push((planned_move.target_track_id.clone(), moved)); + } + + let moved_ids: Vec<&str> = self + .moves + .iter() + .map(|planned_move| planned_move.element_id.as_str()) + .collect(); + + for track in all_tracks_mut(&mut scene.tracks) { + let track_id = track.id().to_string(); + let mut elements = std::mem::take(track.elements_mut()); + elements.retain(|element| !moved_ids.contains(&element.base().id.as_str())); + elements.extend( + moved_elements + .iter() + .filter(|(target_track_id, _)| *target_track_id == track_id) + .map(|(_, element)| element.clone()), + ); + *track.elements_mut() = elements; + } + + Ok(Some(CommandResult::element_selection( + self.moves + .iter() + .map(|planned_move| scene::ElementRef { + track_id: planned_move.target_track_id.clone(), + element_id: planned_move.element_id.clone(), + }) + .collect(), + ))) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/element/shift_split_remainder.rs b/rust/crates/commands/src/timeline/element/shift_split_remainder.rs new file mode 100644 index 00000000..be2f78e4 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/shift_split_remainder.rs @@ -0,0 +1,58 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use scene::{ElementRef, Project}; +use time::MediaTime; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::timeline::element::move_elements::{MoveElementCommand, PlannedElementMove}; + +pub struct ShiftSplitRemainderCommand { + right_side: Rc<RefCell<Vec<ElementRef>>>, + new_start_time: MediaTime, + inner: Option<MoveElementCommand>, +} + +impl ShiftSplitRemainderCommand { + pub fn new(right_side: Rc<RefCell<Vec<ElementRef>>>, new_start_time: MediaTime) -> Self { + Self { + right_side, + new_start_time, + inner: None, + } + } +} + +impl Command for ShiftSplitRemainderCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let right_side = self.right_side.borrow().clone(); + if right_side.is_empty() { + return Ok(None); + } + + let mut inner = MoveElementCommand::new( + right_side + .iter() + .map(|element| PlannedElementMove { + source_track_id: element.track_id.clone(), + target_track_id: element.track_id.clone(), + element_id: element.element_id.clone(), + new_start_time: self.new_start_time, + }) + .collect(), + Vec::new(), + ); + let result = inner.execute(project)?; + self.inner = Some(inner); + Ok(result) + } + + fn undo(&mut self, project: &mut Project) { + if let Some(inner) = &mut self.inner { + inner.undo(project); + } + } +} diff --git a/rust/crates/commands/src/timeline/element/split_elements.rs b/rust/crates/commands/src/timeline/element/split_elements.rs new file mode 100644 index 00000000..0da90792 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/split_elements.rs @@ -0,0 +1,225 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use scene::{AudioElement, Element, ElementRef, Project, RetimeConfig, SceneTracks}; +use time::MediaTime; +use uuid::Uuid; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::all_tracks_mut; + +const MIN_RETIME_RATE: f64 = 0.01; +const MAX_RETIME_RATE: f64 = 5.0; +const DEFAULT_RETIME_RATE: f64 = 1.0; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum RetainSide { + #[default] + Both, + Left, + Right, +} + +fn clamp_retime_rate(rate: f64) -> f64 { + if !rate.is_finite() || rate <= 0.0 { + return DEFAULT_RETIME_RATE; + } + rate.clamp(MIN_RETIME_RATE, MAX_RETIME_RATE) +} + +fn source_span_at_clip_time(clip_time: MediaTime, retime: Option<RetimeConfig>) -> f64 { + let rate = clamp_retime_rate(retime.map(|config| config.rate).unwrap_or(1.0)); + (clip_time.as_ticks() as f64 * rate).max(0.0) +} + +fn round_to_ticks(time: f64) -> MediaTime { + MediaTime::from_ticks(time.round() as i64) +} + +fn retime_of(element: &Element) -> Option<RetimeConfig> { + match element { + Element::Video(video) => video.retime, + Element::Audio(AudioElement::Upload(upload)) => upload.retime, + Element::Audio(AudioElement::Library(library)) => library.retime, + _ => None, + } +} + +pub struct SplitElementsCommand { + elements: Vec<ElementRef>, + split_time: MediaTime, + retain_side: RetainSide, + saved_state: Option<SceneTracks>, + right_side: Rc<RefCell<Vec<ElementRef>>>, +} + +impl SplitElementsCommand { + pub fn new(elements: Vec<ElementRef>, split_time: MediaTime, retain_side: RetainSide) -> Self { + Self { + elements, + split_time, + retain_side, + saved_state: None, + right_side: Rc::new(RefCell::new(Vec::new())), + } + } + + pub fn right_side_handle(&self) -> Rc<RefCell<Vec<ElementRef>>> { + Rc::clone(&self.right_side) + } + + pub fn right_side_elements(&self) -> Vec<ElementRef> { + self.right_side.borrow().clone() + } + + fn split_element( + &self, + track_id: &str, + element: Element, + right_side: &mut Vec<ElementRef>, + ) -> Vec<Element> { + let start = element.base().start_time; + let end = start + element.base().duration; + + if self.split_time <= start || self.split_time >= end { + return vec![element]; + } + + let relative_time = self.split_time - start; + let left_visible_duration = relative_time; + let right_visible_duration = element.base().duration - relative_time; + let retime = retime_of(&element); + + let left_source_span = round_to_ticks(source_span_at_clip_time( + left_visible_duration, + retime, + )); + let total_source_span = + round_to_ticks(source_span_at_clip_time(element.base().duration, retime)); + let right_source_span = total_source_span - left_source_span; + + let (left_animations, right_animations) = animation::split_animations_at_time_with_options( + element.base().animations.as_ref(), + relative_time, + true, + ); + + let left_trim_end = element.base().trim_end + right_source_span; + let right_trim_start = element.base().trim_start + left_source_span; + + match self.retain_side { + RetainSide::Left => { + let mut left = element; + { + let base = left.base_mut(); + base.duration = left_visible_duration; + base.trim_end = left_trim_end; + base.name = format!("{} (left)", base.name); + base.animations = left_animations; + } + vec![left] + } + RetainSide::Right => { + let mut right = element; + let new_id = Uuid::new_v4().to_string(); + right_side.push(ElementRef { + track_id: track_id.to_string(), + element_id: new_id.clone(), + }); + { + let base = right.base_mut(); + base.id = new_id; + base.start_time = self.split_time; + base.duration = right_visible_duration; + base.trim_start = right_trim_start; + base.name = format!("{} (right)", base.name); + base.animations = right_animations; + } + vec![right] + } + RetainSide::Both => { + let mut left = element; + let mut right = left.clone(); + let right_id = Uuid::new_v4().to_string(); + right_side.push(ElementRef { + track_id: track_id.to_string(), + element_id: right_id.clone(), + }); + { + let base = left.base_mut(); + base.duration = left_visible_duration; + base.trim_end = left_trim_end; + base.name = format!("{} (left)", base.name); + base.animations = left_animations; + } + { + let base = right.base_mut(); + base.id = right_id; + base.start_time = self.split_time; + base.duration = right_visible_duration; + base.trim_start = right_trim_start; + base.name = format!("{} (right)", base.name); + base.animations = right_animations; + } + vec![left, right] + } + } + } +} + +impl Command for SplitElementsCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + self.right_side.borrow_mut().clear(); + + for track in all_tracks_mut(&mut scene.tracks) { + let targets: Vec<&str> = self + .elements + .iter() + .filter(|target| target.track_id == track.id()) + .map(|target| target.element_id.as_str()) + .collect(); + if targets.is_empty() { + continue; + } + + let track_id = track.id().to_string(); + let previous = std::mem::take(track.elements_mut()); + let mut next = Vec::with_capacity(previous.len()); + { + let mut right_side = self.right_side.borrow_mut(); + for element in previous { + if !targets.contains(&element.base().id.as_str()) { + next.push(element); + continue; + } + next.extend(self.split_element(&track_id, element, &mut right_side)); + } + } + *track.elements_mut() = next; + } + + let right_side = self.right_side.borrow(); + if right_side.is_empty() { + return Ok(None); + } + Ok(Some(CommandResult::element_selection(right_side.clone()))) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/element/update_elements.rs b/rust/crates/commands/src/timeline/element/update_elements.rs new file mode 100644 index 00000000..f5931bb7 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/update_elements.rs @@ -0,0 +1,72 @@ +use scene::{ElementRef, Project, SceneTracks}; +use timeline::update_pipeline::{ElementPatch, ElementUpdateContext, apply_element_update}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::find_track_mut; + +#[derive(Clone, Debug, PartialEq)] +pub struct ElementUpdate { + pub track_id: String, + pub element_id: String, + pub patch: ElementPatch, +} + +pub struct UpdateElementsCommand { + updates: Vec<ElementUpdate>, + saved_state: Option<SceneTracks>, +} + +impl UpdateElementsCommand { + pub fn new(updates: Vec<ElementUpdate>) -> Self { + Self { + updates, + saved_state: None, + } + } +} + +impl Command for UpdateElementsCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + + for update in &self.updates { + let context_tracks = scene.tracks.clone(); + let context = ElementUpdateContext { + tracks: &context_tracks, + track_id: &update.track_id, + }; + let Some(track) = find_track_mut(&mut scene.tracks, &update.track_id) else { + continue; + }; + let Some(element) = track + .elements_mut() + .iter_mut() + .find(|element| element.base().id == update.element_id) + else { + continue; + }; + + let current = element.clone(); + *element = apply_element_update(current, update.patch.clone(), &context); + } + + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/mod.rs b/rust/crates/commands/src/timeline/mod.rs new file mode 100644 index 00000000..532885ce --- /dev/null +++ b/rust/crates/commands/src/timeline/mod.rs @@ -0,0 +1,5 @@ +pub mod element; +pub mod track; +pub mod tracks_snapshot; + +pub use tracks_snapshot::TracksSnapshotCommand; diff --git a/rust/crates/commands/src/timeline/track/add_track.rs b/rust/crates/commands/src/timeline/track/add_track.rs new file mode 100644 index 00000000..d6e9bbae --- /dev/null +++ b/rust/crates/commands/src/timeline/track/add_track.rs @@ -0,0 +1,69 @@ +use scene::{Project, SceneTracks}; +use timeline::placement::{build_empty_track, get_default_insert_index_for_track}; +use timeline::TrackType; +use uuid::Uuid; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +pub struct AddTrackCommand { + track_type: TrackType, + index: Option<usize>, + track_id: String, + saved_state: Option<SceneTracks>, +} + +impl AddTrackCommand { + pub fn new(track_type: TrackType, index: Option<usize>) -> Self { + Self { + track_type, + index, + track_id: Uuid::new_v4().to_string(), + saved_state: None, + } + } + + pub fn track_id(&self) -> &str { + &self.track_id + } +} + +impl Command for AddTrackCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + + let insert_index = self.index.unwrap_or_else(|| { + get_default_insert_index_for_track(&scene.tracks, self.track_type) + }); + + let new_track = build_empty_track(&self.track_id, self.track_type, None); + if self.track_type == TrackType::Audio { + let audio_insert_index = insert_index + .saturating_sub(scene.tracks.overlay.len() + 1); + scene + .tracks + .audio + .insert(audio_insert_index.min(scene.tracks.audio.len()), new_track); + } else { + let overlay_insert_index = insert_index.min(scene.tracks.overlay.len()); + scene.tracks.overlay.insert(overlay_insert_index, new_track); + } + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/track/mod.rs b/rust/crates/commands/src/timeline/track/mod.rs new file mode 100644 index 00000000..c133a12d --- /dev/null +++ b/rust/crates/commands/src/timeline/track/mod.rs @@ -0,0 +1,9 @@ +pub mod add_track; +pub mod remove_track; +pub mod toggle_track_mute; +pub mod toggle_track_visibility; + +pub use add_track::AddTrackCommand; +pub use remove_track::RemoveTrackCommand; +pub use toggle_track_mute::ToggleTrackMuteCommand; +pub use toggle_track_visibility::ToggleTrackVisibilityCommand; diff --git a/rust/crates/commands/src/timeline/track/remove_track.rs b/rust/crates/commands/src/timeline/track/remove_track.rs new file mode 100644 index 00000000..5a79b4ae --- /dev/null +++ b/rust/crates/commands/src/timeline/track/remove_track.rs @@ -0,0 +1,43 @@ +use scene::{Project, SceneTracks}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +pub struct RemoveTrackCommand { + track_id: String, + saved_state: Option<SceneTracks>, +} + +impl RemoveTrackCommand { + pub fn new(track_id: impl Into<String>) -> Self { + Self { + track_id: track_id.into(), + saved_state: None, + } + } +} + +impl Command for RemoveTrackCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + scene.tracks.overlay.retain(|track| track.id() != self.track_id); + scene.tracks.audio.retain(|track| track.id() != self.track_id); + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/track/toggle_track_mute.rs b/rust/crates/commands/src/timeline/track/toggle_track_mute.rs new file mode 100644 index 00000000..afaaf3f3 --- /dev/null +++ b/rust/crates/commands/src/timeline/track/toggle_track_mute.rs @@ -0,0 +1,49 @@ +use scene::{Project, SceneTracks, Track}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::find_track_mut; + +pub struct ToggleTrackMuteCommand { + track_id: String, + saved_state: Option<SceneTracks>, +} + +impl ToggleTrackMuteCommand { + pub fn new(track_id: impl Into<String>) -> Self { + Self { + track_id: track_id.into(), + saved_state: None, + } + } +} + +impl Command for ToggleTrackMuteCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + + match find_track_mut(&mut scene.tracks, &self.track_id) { + Some(Track::Video { muted, .. }) | Some(Track::Audio { muted, .. }) => { + *muted = !*muted; + } + _ => {} + } + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/track/toggle_track_visibility.rs b/rust/crates/commands/src/timeline/track/toggle_track_visibility.rs new file mode 100644 index 00000000..79000a73 --- /dev/null +++ b/rust/crates/commands/src/timeline/track/toggle_track_visibility.rs @@ -0,0 +1,54 @@ +use scene::{Project, SceneTracks, Track}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::find_track_mut; + +pub struct ToggleTrackVisibilityCommand { + track_id: String, + saved_state: Option<SceneTracks>, +} + +impl ToggleTrackVisibilityCommand { + pub fn new(track_id: impl Into<String>) -> Self { + Self { + track_id: track_id.into(), + saved_state: None, + } + } +} + +impl Command for ToggleTrackVisibilityCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + + match find_track_mut(&mut scene.tracks, &self.track_id) { + Some( + Track::Video { hidden, .. } + | Track::Text { hidden, .. } + | Track::Graphic { hidden, .. } + | Track::Effect { hidden, .. }, + ) => { + *hidden = !*hidden; + } + _ => {} + } + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/tracks_snapshot.rs b/rust/crates/commands/src/timeline/tracks_snapshot.rs new file mode 100644 index 00000000..5108e123 --- /dev/null +++ b/rust/crates/commands/src/timeline/tracks_snapshot.rs @@ -0,0 +1,33 @@ +use scene::{Project, SceneTracks}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +pub struct TracksSnapshotCommand { + before: SceneTracks, + after: SceneTracks, +} + +impl TracksSnapshotCommand { + pub fn new(before: SceneTracks, after: SceneTracks) -> Self { + Self { before, after } + } +} + +impl Command for TracksSnapshotCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + if let Some(scene) = active_scene_mut(project) { + scene.tracks = self.after.clone(); + } + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + if let Some(scene) = active_scene_mut(project) { + scene.tracks = self.before.clone(); + } + } +} diff --git a/rust/crates/commands/src/tracks.rs b/rust/crates/commands/src/tracks.rs new file mode 100644 index 00000000..332a5e6b --- /dev/null +++ b/rust/crates/commands/src/tracks.rs @@ -0,0 +1,42 @@ +use scene::{SceneTracks, Track}; + +pub fn find_track<'a>(tracks: &'a SceneTracks, track_id: &str) -> Option<&'a Track> { + if tracks.main.id() == track_id { + return Some(&tracks.main); + } + tracks + .overlay + .iter() + .find(|track| track.id() == track_id) + .or_else(|| tracks.audio.iter().find(|track| track.id() == track_id)) +} + +pub fn find_track_mut<'a>( + tracks: &'a mut SceneTracks, + track_id: &str, +) -> Option<&'a mut Track> { + if tracks.main.id() == track_id { + return Some(&mut tracks.main); + } + tracks + .overlay + .iter_mut() + .find(|track| track.id() == track_id) + .or_else(|| tracks.audio.iter_mut().find(|track| track.id() == track_id)) +} + +pub fn all_tracks(tracks: &SceneTracks) -> impl Iterator<Item = &Track> { + tracks + .overlay + .iter() + .chain(std::iter::once(&tracks.main)) + .chain(tracks.audio.iter()) +} + +pub fn all_tracks_mut(tracks: &mut SceneTracks) -> impl Iterator<Item = &mut Track> { + tracks + .overlay + .iter_mut() + .chain(std::iter::once(&mut tracks.main)) + .chain(tracks.audio.iter_mut()) +} |
