summaryrefslogtreecommitdiff
path: root/rust/crates/commands/src/timeline
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-28 20:14:51 +0200
committercashmere <cashmere@autistici.org>2026-07-28 20:14:51 +0200
commit32438ce25f69de99d2523b98237f62cfa90890d7 (patch)
tree3a712c347dec0889bdea3328b4c817c2d9392b27 /rust/crates/commands/src/timeline
parentf5a82152394fde4d8f740f664ab7737b82861618 (diff)
downloadopencut-32438ce25f69de99d2523b98237f62cfa90890d7.tar.gz
feat: port domain crates to Rust (session 1 complete)
All green: cargo test --workspace (~300 tests, 0 failures). - timeline (45): placement, resize/ripple, split, snap, retime, audio separation, scene helpers - animation (73): bezier solver, channel sampling, keyframe ops, split/clamp/clone, param definitions - commands (9): Command trait, BatchCommand, history, 19 concrete commands, freeze-frame port 1:1 - audio (22): audible collection, range mixing, mastering limiter, rms/peak buckets - text (19): cosmic-text layout/measure/raster, backgrounds, decorations - graphics (52): full CSS gradient parser, tiny-skia raster, 4 shape definitions - effect-defs (16): blur definition, registry, pass resolution - mask-defs (19): all 9 mask renderers via tiny-skia - renderer (6): node model + scene builder port - desktop: GPUI projects screen wired to ProjectStore - scene: factory (web defaults: v32, Main Track, 1920x1080, 30fps)
Diffstat (limited to 'rust/crates/commands/src/timeline')
-rw-r--r--rust/crates/commands/src/timeline/element/delete_elements.rs5
-rw-r--r--rust/crates/commands/src/timeline/element/duplicate_elements.rs131
-rw-r--r--rust/crates/commands/src/timeline/element/insert_element.rs195
-rw-r--r--rust/crates/commands/src/timeline/element/mod.rs12
-rw-r--r--rust/crates/commands/src/timeline/element/move_elements.rs14
-rw-r--r--rust/crates/commands/src/timeline/element/split_elements.rs170
-rw-r--r--rust/crates/commands/src/timeline/element/update_elements.rs7
-rw-r--r--rust/crates/commands/src/timeline/mod.rs2
-rw-r--r--rust/crates/commands/src/timeline/track/add_track.rs2
9 files changed, 368 insertions, 170 deletions
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);