summaryrefslogtreecommitdiff
path: root/rust/crates/timeline/src/placement
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-28 19:41:16 +0200
committercashmere <cashmere@autistici.org>2026-07-28 19:41:16 +0200
commitf5a82152394fde4d8f740f664ab7737b82861618 (patch)
tree5a51b181abead1ce03d2dbfc12b77918d3be0e1b /rust/crates/timeline/src/placement
parente837e7504379e100ff3b56282577ec67bb88bec9 (diff)
downloadopencut-f5a82152394fde4d8f740f664ab7737b82861618.tar.gz
feat: GPUI desktop port foundation
- scene crate: full project/scene/element model, serde round-trip proven against the web SerializedProject shape - transfer crate: .ocp zip container, cross-compat with web fflate archives proven in both directions - storage crate: filesystem project store replacing IndexedDB, .ocp import/export - media crate: ffmpeg-next 8 decoder (frames, audio, thumbnails, waveform summaries) - playback crate: wall-clock anchored, frame-rounded playback clock - gpu crate: native texture readback; compositor renders headless offscreen on Vulkan (spike test) - flake devshell: rust toolchain, GPUI native deps, ffmpeg dev, bindgen clang args; GPUI hello-world builds - port plan in notes/gpui-port.md
Diffstat (limited to 'rust/crates/timeline/src/placement')
-rw-r--r--rust/crates/timeline/src/placement/apply.rs101
-rw-r--r--rust/crates/timeline/src/placement/compatibility.rs43
-rw-r--r--rust/crates/timeline/src/placement/insert_index.rs81
-rw-r--r--rust/crates/timeline/src/placement/main_track.rs44
-rw-r--r--rust/crates/timeline/src/placement/mod.rs104
-rw-r--r--rust/crates/timeline/src/placement/overlap.rs34
-rw-r--r--rust/crates/timeline/src/placement/resolve.rs223
-rw-r--r--rust/crates/timeline/src/placement/track_factory.rs62
8 files changed, 692 insertions, 0 deletions
diff --git a/rust/crates/timeline/src/placement/apply.rs b/rust/crates/timeline/src/placement/apply.rs
new file mode 100644
index 00000000..4e044a68
--- /dev/null
+++ b/rust/crates/timeline/src/placement/apply.rs
@@ -0,0 +1,101 @@
+use scene::{Element, SceneTracks, Track};
+
+use super::track_factory::build_empty_track;
+use super::{PlacementResult, ordered_tracks};
+use crate::track_element_update::update_track_in_scene_tracks;
+use crate::types::TrackType;
+
+pub struct PlacementApplied {
+ pub updated_tracks: SceneTracks,
+ pub target_track_id: String,
+}
+
+pub struct ApplyPlacementParams {
+ pub tracks: SceneTracks,
+ pub placement_result: PlacementResult,
+ pub elements: Vec<Element>,
+ pub new_track_id: String,
+ pub new_track_insert_index_override: Option<usize>,
+}
+
+pub fn apply_placement(params: &ApplyPlacementParams) -> Option<PlacementApplied> {
+ let tracks = &params.tracks;
+ match &params.placement_result {
+ PlacementResult::ExistingTrack { track_index, .. } => {
+ let ordered = ordered_tracks(tracks);
+ let target_track = ordered.get(*track_index)?;
+
+ let updated_tracks =
+ update_track_in_scene_tracks(tracks, target_track.id(), |track| {
+ let mut next = track.clone();
+ next.elements_mut().extend(params.elements.iter().cloned());
+ next
+ });
+
+ Some(PlacementApplied {
+ updated_tracks,
+ target_track_id: target_track.id().to_string(),
+ })
+ }
+ PlacementResult::NewTrack {
+ track_type,
+ insert_index,
+ ..
+ } => {
+ let new_track_id = params.new_track_id.clone();
+ let insert_index = params
+ .new_track_insert_index_override
+ .unwrap_or(*insert_index);
+ let new_track = build_empty_track(new_track_id.clone(), *track_type, None)
+ .with_elements(params.elements.clone());
+
+ let updated_tracks = if *track_type == TrackType::Audio {
+ SceneTracks {
+ audio: insert_into_audio_tracks(tracks, insert_index, new_track),
+ ..tracks.clone()
+ }
+ } else {
+ SceneTracks {
+ overlay: insert_into_overlay_tracks(tracks, insert_index, new_track),
+ ..tracks.clone()
+ }
+ };
+
+ Some(PlacementApplied {
+ updated_tracks,
+ target_track_id: new_track_id,
+ })
+ }
+ }
+}
+
+fn insert_into_overlay_tracks(
+ tracks: &SceneTracks,
+ insert_index: usize,
+ track: Track,
+) -> Vec<Track> {
+ let normalized = insert_index.min(tracks.overlay.len());
+ let mut next = tracks.overlay.clone();
+ next.insert(normalized, track);
+ next
+}
+
+fn insert_into_audio_tracks(tracks: &SceneTracks, insert_index: usize, track: Track) -> Vec<Track> {
+ let audio_index = insert_index
+ .saturating_sub(tracks.overlay.len() + 1)
+ .min(tracks.audio.len());
+ let mut next = tracks.audio.clone();
+ next.insert(audio_index, track);
+ next
+}
+
+trait WithElements {
+ fn with_elements(self, elements: Vec<Element>) -> Self;
+}
+
+impl WithElements for Track {
+ fn with_elements(mut self, elements: Vec<Element>) -> Self {
+ *self.elements_mut() = elements;
+ self
+ }
+}
diff --git a/rust/crates/timeline/src/placement/compatibility.rs b/rust/crates/timeline/src/placement/compatibility.rs
new file mode 100644
index 00000000..9512af49
--- /dev/null
+++ b/rust/crates/timeline/src/placement/compatibility.rs
@@ -0,0 +1,43 @@
+use crate::types::{ElementType, TrackType};
+
+pub fn get_track_type_for_element_type(element_type: ElementType) -> TrackType {
+ match element_type {
+ ElementType::Audio => TrackType::Audio,
+ ElementType::Text => TrackType::Text,
+ ElementType::Sticker | ElementType::Graphic => TrackType::Graphic,
+ ElementType::Effect => TrackType::Effect,
+ ElementType::Video | ElementType::Image => TrackType::Video,
+ }
+}
+
+pub fn can_element_go_on_track(element_type: ElementType, track_type: TrackType) -> bool {
+ get_track_type_for_element_type(element_type) == track_type
+}
+
+pub struct TrackCompatibility {
+ pub is_valid: bool,
+ pub error_message: Option<String>,
+}
+
+pub fn validate_element_track_compatibility(
+ element_type: ElementType,
+ track_type: TrackType,
+) -> TrackCompatibility {
+ let is_valid = can_element_go_on_track(element_type, track_type);
+
+ if !is_valid {
+ return TrackCompatibility {
+ is_valid: false,
+ error_message: Some(format!(
+ "{} elements cannot be placed on {} tracks",
+ element_type.as_str(),
+ track_type.as_str(),
+ )),
+ };
+ }
+
+ TrackCompatibility {
+ is_valid: true,
+ error_message: None,
+ }
+}
diff --git a/rust/crates/timeline/src/placement/insert_index.rs b/rust/crates/timeline/src/placement/insert_index.rs
new file mode 100644
index 00000000..b0feaec6
--- /dev/null
+++ b/rust/crates/timeline/src/placement/insert_index.rs
@@ -0,0 +1,81 @@
+use scene::SceneTracks;
+
+use super::InsertDirection;
+use crate::types::TrackType;
+
+pub fn get_default_insert_index_for_track(tracks: &SceneTracks, track_type: TrackType) -> usize {
+ if track_type == TrackType::Audio {
+ return tracks.overlay.len() + 1 + tracks.audio.len();
+ }
+
+ if track_type == TrackType::Effect {
+ return 0;
+ }
+
+ tracks.overlay.len()
+}
+
+pub fn get_highest_insert_index_for_track(tracks: &SceneTracks, track_type: TrackType) -> usize {
+ if track_type == TrackType::Audio {
+ return tracks.overlay.len() + 1;
+ }
+
+ 0
+}
+
+pub struct PreferredNewTrackPlacement {
+ pub insert_index: usize,
+ pub insert_position: Option<InsertDirection>,
+}
+
+pub fn resolve_preferred_new_track_placement(
+ tracks: &SceneTracks,
+ track_type: TrackType,
+ preferred_index: usize,
+ direction: InsertDirection,
+) -> PreferredNewTrackPlacement {
+ let track_count = tracks.overlay.len() + 1 + tracks.audio.len();
+ if track_count == 0 {
+ return PreferredNewTrackPlacement {
+ insert_index: 0,
+ insert_position: (track_type == TrackType::Audio)
+ .then_some(InsertDirection::Below),
+ };
+ }
+
+ let safe_preferred_index = preferred_index.min(track_count - 1);
+ let main_track_index = tracks.overlay.len();
+
+ if track_type == TrackType::Audio {
+ if safe_preferred_index <= main_track_index {
+ return PreferredNewTrackPlacement {
+ insert_index: main_track_index + 1,
+ insert_position: Some(InsertDirection::Below),
+ };
+ }
+
+ return PreferredNewTrackPlacement {
+ insert_index: match direction {
+ InsertDirection::Above => safe_preferred_index,
+ InsertDirection::Below => safe_preferred_index + 1,
+ },
+ insert_position: Some(direction),
+ };
+ }
+
+ let insert_index = match direction {
+ InsertDirection::Above => safe_preferred_index,
+ InsertDirection::Below => safe_preferred_index + 1,
+ };
+ if insert_index > main_track_index {
+ return PreferredNewTrackPlacement {
+ insert_index: main_track_index,
+ insert_position: Some(InsertDirection::Above),
+ };
+ }
+
+ PreferredNewTrackPlacement {
+ insert_index,
+ insert_position: Some(direction),
+ }
+}
diff --git a/rust/crates/timeline/src/placement/main_track.rs b/rust/crates/timeline/src/placement/main_track.rs
new file mode 100644
index 00000000..01d91d4f
--- /dev/null
+++ b/rust/crates/timeline/src/placement/main_track.rs
@@ -0,0 +1,44 @@
+use scene::{Element, SceneTracks, Track};
+use time::MediaTime;
+
+pub const MAIN_TRACK_NAME: &str = "Main Track";
+
+pub fn get_earliest_main_track_element<'a>(
+ main_track: &'a Track,
+ exclude_element_id: Option<&str>,
+) -> Option<&'a Element> {
+ main_track
+ .elements()
+ .iter()
+ .filter(|element| exclude_element_id != Some(element.base().id.as_str()))
+ .reduce(|earliest, element| {
+ if element.base().start_time < earliest.base().start_time {
+ element
+ } else {
+ earliest
+ }
+ })
+}
+
+pub fn enforce_main_track_start(
+ tracks: &SceneTracks,
+ target_track_id: &str,
+ requested_start_time: MediaTime,
+ exclude_element_id: Option<&str>,
+) -> MediaTime {
+ if tracks.main.id() != target_track_id {
+ return requested_start_time;
+ }
+
+ let Some(earliest_element) =
+ get_earliest_main_track_element(&tracks.main, exclude_element_id)
+ else {
+ return MediaTime::ZERO;
+ };
+
+ if requested_start_time <= earliest_element.base().start_time {
+ return MediaTime::ZERO;
+ }
+
+ requested_start_time
+}
diff --git a/rust/crates/timeline/src/placement/mod.rs b/rust/crates/timeline/src/placement/mod.rs
new file mode 100644
index 00000000..8ad5710e
--- /dev/null
+++ b/rust/crates/timeline/src/placement/mod.rs
@@ -0,0 +1,104 @@
+use scene::{Element, SceneTracks, Track};
+use time::MediaTime;
+
+use crate::types::{ElementType, TrackType};
+
+pub mod apply;
+pub mod compatibility;
+pub mod insert_index;
+pub mod main_track;
+pub mod overlap;
+pub mod resolve;
+pub mod track_factory;
+
+pub use apply::{PlacementApplied, apply_placement};
+pub use compatibility::{
+ can_element_go_on_track, get_track_type_for_element_type, validate_element_track_compatibility,
+};
+pub use insert_index::{
+ get_default_insert_index_for_track, get_highest_insert_index_for_track,
+ resolve_preferred_new_track_placement,
+};
+pub use main_track::{MAIN_TRACK_NAME, enforce_main_track_start, get_earliest_main_track_element};
+pub use overlap::can_place_time_spans_on_track;
+pub use resolve::resolve_track_placement;
+pub use track_factory::build_empty_track;
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct PlacementTimeSpan {
+ pub start_time: MediaTime,
+ pub duration: MediaTime,
+ pub exclude_element_id: Option<String>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum PlacementSubject {
+ ElementType(ElementType),
+ TrackType(TrackType),
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum InsertDirection {
+ Above,
+ Below,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum VerticalDragDirection {
+ Up,
+ Down,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum NewTrackPosition {
+ Highest,
+ Default,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum PlacementStrategy {
+ Explicit {
+ track_id: String,
+ },
+ FirstAvailable,
+ PreferIndex {
+ track_index: usize,
+ hover_direction: InsertDirection,
+ vertical_drag_direction: Option<VerticalDragDirection>,
+ create_new_track_only: bool,
+ },
+ AboveSource {
+ source_track_index: usize,
+ },
+ AlwaysNew {
+ position: NewTrackPosition,
+ },
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum PlacementResult {
+ ExistingTrack {
+ track_id: String,
+ track_index: usize,
+ track_type: TrackType,
+ adjusted_start_time: Option<MediaTime>,
+ },
+ NewTrack {
+ track_type: TrackType,
+ insert_index: usize,
+ insert_position: Option<InsertDirection>,
+ },
+}
+
+pub fn ordered_tracks(tracks: &SceneTracks) -> Vec<&Track> {
+ tracks
+ .overlay
+ .iter()
+ .chain(std::iter::once(&tracks.main))
+ .chain(tracks.audio.iter())
+ .collect()
+}
+
+pub fn track_elements(track: &Track) -> &[Element] {
+ track.elements()
+}
diff --git a/rust/crates/timeline/src/placement/overlap.rs b/rust/crates/timeline/src/placement/overlap.rs
new file mode 100644
index 00000000..98055e9a
--- /dev/null
+++ b/rust/crates/timeline/src/placement/overlap.rs
@@ -0,0 +1,34 @@
+use scene::Element;
+use time::MediaTime;
+
+use super::PlacementTimeSpan;
+
+fn would_element_overlap(
+ elements: &[Element],
+ start_time: MediaTime,
+ end_time: MediaTime,
+ exclude_element_id: Option<&str>,
+) -> bool {
+ elements.iter().any(|element| {
+ if exclude_element_id == Some(element.base().id.as_str()) {
+ return false;
+ }
+
+ let element_end = element.base().start_time + element.base().duration;
+ start_time < element_end && end_time > element.base().start_time
+ })
+}
+
+pub fn can_place_time_spans_on_track(
+ elements: &[Element],
+ time_spans: &[PlacementTimeSpan],
+) -> bool {
+ time_spans.iter().all(|span| {
+ !would_element_overlap(
+ elements,
+ span.start_time,
+ span.start_time + span.duration,
+ span.exclude_element_id.as_deref(),
+ )
+ })
+}
diff --git a/rust/crates/timeline/src/placement/resolve.rs b/rust/crates/timeline/src/placement/resolve.rs
new file mode 100644
index 00000000..e95bc93c
--- /dev/null
+++ b/rust/crates/timeline/src/placement/resolve.rs
@@ -0,0 +1,223 @@
+use scene::{SceneTracks, Track};
+use time::MediaTime;
+
+use super::compatibility::get_track_type_for_element_type;
+use super::insert_index::{
+ get_default_insert_index_for_track, get_highest_insert_index_for_track,
+ resolve_preferred_new_track_placement,
+};
+use super::main_track::enforce_main_track_start;
+use super::overlap::can_place_time_spans_on_track;
+use super::{
+ InsertDirection, NewTrackPosition, PlacementResult, PlacementStrategy, PlacementSubject,
+ PlacementTimeSpan, VerticalDragDirection, ordered_tracks,
+};
+use crate::types::TrackType;
+
+pub struct ResolveTrackPlacementParams {
+ pub tracks: SceneTracks,
+ pub subject: PlacementSubject,
+ pub time_spans: Vec<PlacementTimeSpan>,
+ pub strategy: PlacementStrategy,
+}
+
+fn build_existing_track_result(
+ track: &Track,
+ track_index: usize,
+ tracks: &SceneTracks,
+ time_spans: &[PlacementTimeSpan],
+) -> PlacementResult {
+ let first_span = time_spans.first();
+ let requested_start_time = first_span
+ .map(|span| span.start_time)
+ .unwrap_or(MediaTime::ZERO);
+ let adjusted_start_time = enforce_main_track_start(
+ tracks,
+ track.id(),
+ requested_start_time,
+ first_span.and_then(|span| span.exclude_element_id.as_deref()),
+ );
+
+ PlacementResult::ExistingTrack {
+ track_id: track.id().to_string(),
+ track_index,
+ track_type: TrackType::of_track(track),
+ adjusted_start_time: (adjusted_start_time != requested_start_time)
+ .then_some(adjusted_start_time),
+ }
+}
+
+fn build_new_track_result(
+ track_type: TrackType,
+ insert_index: usize,
+ insert_position: Option<InsertDirection>,
+) -> PlacementResult {
+ PlacementResult::NewTrack {
+ track_type,
+ insert_index,
+ insert_position,
+ }
+}
+
+fn find_first_available_track_index(
+ tracks: &[&Track],
+ track_type: TrackType,
+ time_spans: &[PlacementTimeSpan],
+) -> Option<usize> {
+ tracks.iter().position(|track| {
+ TrackType::of_track(track) == track_type
+ && can_place_time_spans_on_track(track.elements(), time_spans)
+ })
+}
+
+fn resolve_always_new_track(
+ tracks: &SceneTracks,
+ track_type: TrackType,
+ position: NewTrackPosition,
+) -> PlacementResult {
+ let insert_index = match position {
+ NewTrackPosition::Highest => get_highest_insert_index_for_track(tracks, track_type),
+ NewTrackPosition::Default => get_default_insert_index_for_track(tracks, track_type),
+ };
+
+ build_new_track_result(track_type, insert_index, None)
+}
+
+fn get_insert_direction(
+ hover_direction: InsertDirection,
+ vertical_drag_direction: Option<VerticalDragDirection>,
+) -> InsertDirection {
+ match vertical_drag_direction {
+ Some(VerticalDragDirection::Up) => InsertDirection::Above,
+ Some(VerticalDragDirection::Down) => InsertDirection::Below,
+ None => hover_direction,
+ }
+}
+
+pub fn resolve_track_placement(params: &ResolveTrackPlacementParams) -> Option<PlacementResult> {
+ let tracks = &params.tracks;
+ let ordered = ordered_tracks(tracks);
+ let track_type = match params.subject {
+ PlacementSubject::TrackType(track_type) => track_type,
+ PlacementSubject::ElementType(element_type) => {
+ get_track_type_for_element_type(element_type)
+ }
+ };
+ let time_spans = &params.time_spans;
+
+ match &params.strategy {
+ PlacementStrategy::Explicit { track_id } => {
+ let track_index = ordered.iter().position(|track| track.id() == track_id)?;
+ let track = ordered[track_index];
+ if TrackType::of_track(track) != track_type {
+ return None;
+ }
+
+ Some(build_existing_track_result(
+ track,
+ track_index,
+ tracks,
+ time_spans,
+ ))
+ }
+ PlacementStrategy::FirstAvailable => {
+ if let Some(existing_index) =
+ find_first_available_track_index(&ordered, track_type, time_spans)
+ {
+ return Some(build_existing_track_result(
+ ordered[existing_index],
+ existing_index,
+ tracks,
+ time_spans,
+ ));
+ }
+
+ Some(resolve_always_new_track(
+ tracks,
+ track_type,
+ NewTrackPosition::Highest,
+ ))
+ }
+ PlacementStrategy::PreferIndex {
+ track_index,
+ hover_direction,
+ vertical_drag_direction,
+ create_new_track_only,
+ } => {
+ let preferred_track = ordered.get(*track_index);
+ let is_preferred_compatible = preferred_track
+ .is_some_and(|track| TrackType::of_track(track) == track_type);
+ let can_use_existing = !create_new_track_only
+ && is_preferred_compatible
+ && preferred_track.is_some_and(|track| {
+ can_place_time_spans_on_track(track.elements(), time_spans)
+ });
+ if can_use_existing {
+ return Some(build_existing_track_result(
+ preferred_track?,
+ *track_index,
+ tracks,
+ time_spans,
+ ));
+ }
+
+ let direction = get_insert_direction(
+ *hover_direction,
+ if is_preferred_compatible {
+ None
+ } else {
+ *vertical_drag_direction
+ },
+ );
+ let placement = resolve_preferred_new_track_placement(
+ tracks,
+ track_type,
+ *track_index,
+ direction,
+ );
+ Some(build_new_track_result(
+ track_type,
+ placement.insert_index,
+ placement.insert_position,
+ ))
+ }
+ PlacementStrategy::AboveSource { source_track_index } => {
+ let above_track = source_track_index
+ .checked_sub(1)
+ .and_then(|above_index| ordered.get(above_index));
+ if let Some(above_track) = above_track
+ && TrackType::of_track(above_track) == track_type
+ && can_place_time_spans_on_track(above_track.elements(), time_spans)
+ {
+ return Some(build_existing_track_result(
+ above_track,
+ source_track_index - 1,
+ tracks,
+ time_spans,
+ ));
+ }
+
+ if let Some(first_available_index) =
+ find_first_available_track_index(&ordered, track_type, time_spans)
+ {
+ return Some(build_existing_track_result(
+ ordered[first_available_index],
+ first_available_index,
+ tracks,
+ time_spans,
+ ));
+ }
+
+ Some(build_new_track_result(
+ track_type,
+ get_highest_insert_index_for_track(tracks, track_type),
+ None,
+ ))
+ }
+ PlacementStrategy::AlwaysNew { position } => Some(resolve_always_new_track(
+ tracks,
+ track_type,
+ *position,
+ )),
+ }
+}
diff --git a/rust/crates/timeline/src/placement/track_factory.rs b/rust/crates/timeline/src/placement/track_factory.rs
new file mode 100644
index 00000000..0b86ecf2
--- /dev/null
+++ b/rust/crates/timeline/src/placement/track_factory.rs
@@ -0,0 +1,62 @@
+use scene::Track;
+
+use crate::types::TrackType;
+
+pub const DEFAULT_VIDEO_TRACK_NAME: &str = "Video track";
+pub const DEFAULT_TEXT_TRACK_NAME: &str = "Text track";
+pub const DEFAULT_AUDIO_TRACK_NAME: &str = "Audio track";
+pub const DEFAULT_GRAPHIC_TRACK_NAME: &str = "Graphic track";
+pub const DEFAULT_EFFECT_TRACK_NAME: &str = "Effect track";
+
+pub fn default_track_name(track_type: TrackType) -> &'static str {
+ match track_type {
+ TrackType::Video => DEFAULT_VIDEO_TRACK_NAME,
+ TrackType::Text => DEFAULT_TEXT_TRACK_NAME,
+ TrackType::Audio => DEFAULT_AUDIO_TRACK_NAME,
+ TrackType::Graphic => DEFAULT_GRAPHIC_TRACK_NAME,
+ TrackType::Effect => DEFAULT_EFFECT_TRACK_NAME,
+ }
+}
+
+pub fn build_empty_track(
+ id: impl Into<String>,
+ track_type: TrackType,
+ name: Option<String>,
+) -> Track {
+ let id = id.into();
+ let name = name.unwrap_or_else(|| default_track_name(track_type).to_string());
+
+ match track_type {
+ TrackType::Video => Track::Video {
+ id,
+ name,
+ elements: Vec::new(),
+ muted: false,
+ hidden: false,
+ },
+ TrackType::Text => Track::Text {
+ id,
+ name,
+ elements: Vec::new(),
+ hidden: false,
+ },
+ TrackType::Audio => Track::Audio {
+ id,
+ name,
+ elements: Vec::new(),
+ muted: false,
+ },
+ TrackType::Graphic => Track::Graphic {
+ id,
+ name,
+ elements: Vec::new(),
+ hidden: false,
+ },
+ TrackType::Effect => Track::Effect {
+ id,
+ name,
+ elements: Vec::new(),
+ hidden: false,
+ },
+ }
+}