diff options
Diffstat (limited to 'rust/crates/commands/src')
10 files changed, 375 insertions, 171 deletions
diff --git a/rust/crates/commands/src/scene/create_scene.rs b/rust/crates/commands/src/scene/create_scene.rs index 1f2de02b..b109e0ce 100644 --- a/rust/crates/commands/src/scene/create_scene.rs +++ b/rust/crates/commands/src/scene/create_scene.rs @@ -31,7 +31,13 @@ impl Command for CreateSceneCommand { ) -> Result<Option<CommandResult>, CommandError> { self.saved_scenes = Some(project.scenes.clone()); - let created = timeline::scenes::build_default_scene(&self.name, self.is_main); + let created = timeline::scenes::build_default_scene( + uuid::Uuid::new_v4().to_string(), + uuid::Uuid::new_v4().to_string(), + self.name.clone(), + self.is_main, + chrono::Utc::now(), + ); self.created_scene_id = Some(created.id.clone()); project.scenes.push(created); Ok(None) diff --git a/rust/crates/commands/src/timeline/element/delete_elements.rs b/rust/crates/commands/src/timeline/element/delete_elements.rs index bdcb5b6d..191ad4a8 100644 --- a/rust/crates/commands/src/timeline/element/delete_elements.rs +++ b/rust/crates/commands/src/timeline/element/delete_elements.rs @@ -4,13 +4,14 @@ 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()) { + let track_id = track.id().to_string(); + 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) + .any(|target| target.track_id == track_id && target.element_id == element.base().id) }); } diff --git a/rust/crates/commands/src/timeline/element/duplicate_elements.rs b/rust/crates/commands/src/timeline/element/duplicate_elements.rs new file mode 100644 index 00000000..a77436ce --- /dev/null +++ b/rust/crates/commands/src/timeline/element/duplicate_elements.rs @@ -0,0 +1,131 @@ +use scene::{Element, ElementRef, Project, SceneTracks}; +use timeline::TrackType; +use timeline::placement::apply::ApplyPlacementParams; +use timeline::placement::{ + NewTrackPosition, PlacementResult, PlacementStrategy, PlacementSubject, + ResolveTrackPlacementParams, apply_placement, ordered_tracks, resolve_track_placement, +}; +use uuid::Uuid; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +fn build_duplicate_element(element: &Element, id: String) -> Element { + let mut duplicate = element.clone(); + { + let base = duplicate.base_mut(); + base.id = id; + base.name = format!("{} (copy)", base.name); + base.animations = animation::clone_animations(base.animations.as_ref(), true); + } + duplicate +} + +pub struct DuplicateElementsCommand { + elements: Vec<ElementRef>, + duplicated: Vec<ElementRef>, + saved_state: Option<SceneTracks>, +} + +impl DuplicateElementsCommand { + pub fn new(elements: Vec<ElementRef>) -> Self { + Self { + elements, + duplicated: Vec::new(), + saved_state: None, + } + } + + pub fn duplicated_elements(&self) -> &[ElementRef] { + &self.duplicated + } +} + +impl Command for DuplicateElementsCommand { + 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.duplicated.clear(); + + let saved_state = scene.tracks.clone(); + let mut updated_tracks = scene.tracks.clone(); + + for track in ordered_tracks(&saved_state) { + let new_elements: Vec<Element> = track + .elements() + .iter() + .filter(|element| { + self.elements.iter().any(|target| { + target.track_id == track.id() && target.element_id == element.base().id + }) + }) + .map(|element| build_duplicate_element(element, Uuid::new_v4().to_string())) + .collect(); + + if new_elements.is_empty() { + continue; + } + + let placement_result = resolve_track_placement(&ResolveTrackPlacementParams { + tracks: updated_tracks.clone(), + subject: PlacementSubject::TrackType(TrackType::of_track(track)), + time_spans: Vec::new(), + strategy: PlacementStrategy::AlwaysNew { + position: NewTrackPosition::Highest, + }, + }); + + let Some(placement_result @ PlacementResult::NewTrack { .. }) = placement_result + else { + continue; + }; + + let new_element_ids: Vec<String> = new_elements + .iter() + .map(|element| element.base().id.clone()) + .collect(); + + let Some(applied) = apply_placement(&ApplyPlacementParams { + tracks: updated_tracks.clone(), + placement_result, + elements: new_elements, + new_track_id: Uuid::new_v4().to_string(), + new_track_insert_index_override: None, + }) else { + continue; + }; + + updated_tracks = applied.updated_tracks; + self.duplicated.extend(new_element_ids.into_iter().map(|element_id| { + ElementRef { + track_id: applied.target_track_id.clone(), + element_id, + } + })); + } + + scene.tracks = updated_tracks; + + if self.duplicated.is_empty() { + return Ok(None); + } + Ok(Some(CommandResult::element_selection( + self.duplicated.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/insert_element.rs b/rust/crates/commands/src/timeline/element/insert_element.rs new file mode 100644 index 00000000..a2584c53 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/insert_element.rs @@ -0,0 +1,195 @@ +use scene::{AudioElement, Element, ElementRef, ParamValue, Project, SceneTracks}; +use timeline::placement::apply::ApplyPlacementParams; +use timeline::placement::{ + PlacementResult, PlacementStrategy, PlacementSubject, PlacementTimeSpan, + ResolveTrackPlacementParams, apply_placement, can_element_go_on_track, + resolve_track_placement, +}; +use timeline::{ElementType, TrackType}; +use uuid::Uuid; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +#[derive(Clone, Debug, PartialEq)] +pub enum InsertElementPlacement { + Explicit { + track_id: String, + }, + Auto { + track_type: Option<TrackType>, + insert_index: Option<usize>, + }, +} + +fn requires_media_id(element: &Element) -> bool { + matches!( + element, + Element::Video(_) | Element::Image(_) | Element::Audio(AudioElement::Upload(_)) + ) +} + +fn has_media_id_value(element: &Element) -> bool { + match element { + Element::Video(video) => !video.media_id.is_empty(), + Element::Image(image) => !image.media_id.is_empty(), + Element::Audio(AudioElement::Upload(upload)) => !upload.media_id.is_empty(), + _ => false, + } +} + +fn validate_element_basics(element: &Element) -> bool { + if requires_media_id(element) && !has_media_id_value(element) { + return false; + } + + match element { + Element::Audio(AudioElement::Library(library)) => !library.source_url.is_empty(), + Element::Sticker(sticker) => !sticker.sticker_id.is_empty(), + Element::Graphic(graphic) => !graphic.definition_id.is_empty(), + Element::Text(text) => matches!( + text.base.params.get("content"), + Some(ParamValue::String(content)) if !content.is_empty() + ), + Element::Effect(effect) => !effect.effect_type.is_empty(), + _ => true, + } +} + +pub struct InsertElementCommand { + element: Element, + placement: InsertElementPlacement, + element_id: String, + target_track_id: Option<String>, + saved_state: Option<SceneTracks>, +} + +impl InsertElementCommand { + pub fn new(element: Element, placement: InsertElementPlacement) -> Self { + Self { + element, + placement, + element_id: Uuid::new_v4().to_string(), + target_track_id: None, + saved_state: None, + } + } + + pub fn element_id(&self) -> &str { + &self.element_id + } + + pub fn track_id(&self) -> Option<&str> { + self.target_track_id.as_deref() + } + + fn apply_placement_result( + &self, + tracks: &SceneTracks, + element: &Element, + ) -> Option<(SceneTracks, String)> { + let element_type = ElementType::of_element(element); + + if let InsertElementPlacement::Auto { + track_type: Some(track_type), + .. + } = &self.placement + && !can_element_go_on_track(element_type, *track_type) + { + return None; + } + + let subject = match &self.placement { + InsertElementPlacement::Auto { + track_type: Some(track_type), + .. + } => PlacementSubject::TrackType(*track_type), + _ => PlacementSubject::ElementType(element_type), + }; + + let strategy = match &self.placement { + InsertElementPlacement::Explicit { track_id } => PlacementStrategy::Explicit { + track_id: track_id.clone(), + }, + InsertElementPlacement::Auto { .. } => PlacementStrategy::FirstAvailable, + }; + + let placement_result = resolve_track_placement(&ResolveTrackPlacementParams { + tracks: tracks.clone(), + subject, + time_spans: vec![PlacementTimeSpan { + start_time: element.base().start_time, + duration: element.base().duration, + exclude_element_id: None, + }], + strategy, + })?; + + let mut element_to_place = element.clone(); + if let PlacementResult::ExistingTrack { + adjusted_start_time: Some(adjusted), + .. + } = &placement_result + { + element_to_place.base_mut().start_time = *adjusted; + } + + let insert_index_override = match &self.placement { + InsertElementPlacement::Auto { insert_index, .. } => *insert_index, + InsertElementPlacement::Explicit { .. } => None, + }; + + let applied = apply_placement(&ApplyPlacementParams { + tracks: tracks.clone(), + placement_result, + elements: vec![element_to_place], + new_track_id: Uuid::new_v4().to_string(), + new_track_insert_index_override: insert_index_override, + })?; + + Some((applied.updated_tracks, applied.target_track_id)) + } +} + +impl Command for InsertElementCommand { + 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()); + + if !validate_element_basics(&self.element) { + return Ok(None); + } + + let mut new_element = self.element.clone(); + new_element.base_mut().id = self.element_id.clone(); + + let Some((updated_tracks, target_track_id)) = + self.apply_placement_result(&scene.tracks, &new_element) + else { + return Ok(None); + }; + + self.target_track_id = Some(target_track_id.clone()); + scene.tracks = updated_tracks; + + Ok(Some(CommandResult::element_selection(vec![ElementRef { + track_id: target_track_id, + element_id: self.element_id.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/mod.rs b/rust/crates/commands/src/timeline/element/mod.rs index 2ba806e1..a1d8de6e 100644 --- a/rust/crates/commands/src/timeline/element/mod.rs +++ b/rust/crates/commands/src/timeline/element/mod.rs @@ -1,3 +1,15 @@ pub mod delete_elements; +pub mod duplicate_elements; +pub mod insert_element; +pub mod move_elements; +pub mod shift_split_remainder; +pub mod split_elements; +pub mod update_elements; pub use delete_elements::DeleteElementsCommand; +pub use duplicate_elements::DuplicateElementsCommand; +pub use insert_element::{InsertElementCommand, InsertElementPlacement}; +pub use move_elements::{MoveElementCommand, PlannedElementMove, PlannedTrackCreation}; +pub use shift_split_remainder::ShiftSplitRemainderCommand; +pub use split_elements::{RetainSide, SplitElementsCommand}; +pub use update_elements::{ElementUpdate, UpdateElementsCommand}; diff --git a/rust/crates/commands/src/timeline/element/move_elements.rs b/rust/crates/commands/src/timeline/element/move_elements.rs index fc6425d1..b9324ab8 100644 --- a/rust/crates/commands/src/timeline/element/move_elements.rs +++ b/rust/crates/commands/src/timeline/element/move_elements.rs @@ -40,8 +40,16 @@ fn insert_track_at_display_index( } fn validate_compatibility(element: &Element, track: &Track) -> Result<(), CommandError> { - timeline::placement::validate_element_track_compatibility(element, track) - .map_err(CommandError::IncompatiblePlacement) + let compatibility = timeline::placement::validate_element_track_compatibility( + timeline::ElementType::of_element(element), + timeline::TrackType::of_track(track), + ); + if compatibility.is_valid { + return Ok(()); + } + Err(CommandError::IncompatiblePlacement( + compatibility.error_message.unwrap_or_default(), + )) } pub struct MoveElementCommand { @@ -75,7 +83,7 @@ impl Command for MoveElementCommand { 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); + let track = build_empty_track(creation.id.clone(), creation.track_type, None); insert_track_at_display_index(&mut scene.tracks, track, creation.index); } diff --git a/rust/crates/commands/src/timeline/element/split_elements.rs b/rust/crates/commands/src/timeline/element/split_elements.rs index 0da90792..98d292ab 100644 --- a/rust/crates/commands/src/timeline/element/split_elements.rs +++ b/rust/crates/commands/src/timeline/element/split_elements.rs @@ -1,50 +1,12 @@ use std::cell::RefCell; use std::rc::Rc; -use scene::{AudioElement, Element, ElementRef, Project, RetimeConfig, SceneTracks}; +use scene::{ElementRef, Project, SceneTracks}; use time::MediaTime; -use uuid::Uuid; +pub use timeline::split::RetainSide; 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>, @@ -72,100 +34,6 @@ impl SplitElementsCommand { 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 { @@ -178,34 +46,16 @@ impl Command for SplitElementsCommand { }; 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 result = timeline::split::split_elements( + &scene.tracks, + &self.elements, + self.split_time, + self.retain_side, + ); + scene.tracks = result.tracks; - 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; - } + *self.right_side.borrow_mut() = result.right_side_elements; let right_side = self.right_side.borrow(); if right_side.is_empty() { diff --git a/rust/crates/commands/src/timeline/element/update_elements.rs b/rust/crates/commands/src/timeline/element/update_elements.rs index f5931bb7..0ab21174 100644 --- a/rust/crates/commands/src/timeline/element/update_elements.rs +++ b/rust/crates/commands/src/timeline/element/update_elements.rs @@ -1,11 +1,11 @@ -use scene::{ElementRef, Project, SceneTracks}; +use scene::{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)] +#[derive(Clone, Debug)] pub struct ElementUpdate { pub track_id: String, pub element_id: String, @@ -54,8 +54,7 @@ impl Command for UpdateElementsCommand { continue; }; - let current = element.clone(); - *element = apply_element_update(current, update.patch.clone(), &context); + *element = apply_element_update(element, &update.patch, &context); } Ok(None) diff --git a/rust/crates/commands/src/timeline/mod.rs b/rust/crates/commands/src/timeline/mod.rs index 532885ce..b83d50b4 100644 --- a/rust/crates/commands/src/timeline/mod.rs +++ b/rust/crates/commands/src/timeline/mod.rs @@ -2,4 +2,6 @@ pub mod element; pub mod track; pub mod tracks_snapshot; +pub use element::*; +pub use track::*; 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 index d6e9bbae..4bf170c7 100644 --- a/rust/crates/commands/src/timeline/track/add_track.rs +++ b/rust/crates/commands/src/timeline/track/add_track.rs @@ -43,7 +43,7 @@ impl Command for AddTrackCommand { get_default_insert_index_for_track(&scene.tracks, self.track_type) }); - let new_track = build_empty_track(&self.track_id, self.track_type, None); + let new_track = build_empty_track(self.track_id.clone(), self.track_type, None); if self.track_type == TrackType::Audio { let audio_insert_index = insert_index .saturating_sub(scene.tracks.overlay.len() + 1); |
