From f5a82152394fde4d8f740f664ab7737b82861618 Mon Sep 17 00:00:00 2001 From: cashmere Date: Tue, 28 Jul 2026 19:41:16 +0200 Subject: 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 --- rust/crates/timeline/src/update_pipeline.rs | 158 ++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 rust/crates/timeline/src/update_pipeline.rs (limited to 'rust/crates/timeline/src/update_pipeline.rs') diff --git a/rust/crates/timeline/src/update_pipeline.rs b/rust/crates/timeline/src/update_pipeline.rs new file mode 100644 index 00000000..83b03d85 --- /dev/null +++ b/rust/crates/timeline/src/update_pipeline.rs @@ -0,0 +1,158 @@ +use std::collections::HashSet; + +use scene::{Element, ElementAnimations, ParamValues, RetimeConfig, SceneTracks}; +use time::MediaTime; + +use crate::retime::{ + clamp_retime_rate, get_source_span_at_clip_time, get_timeline_duration_for_source_span, + round_media_time, +}; + +#[derive(Clone, Debug, Default)] +pub struct ElementPatch { + pub name: Option, + pub duration: Option, + pub start_time: Option, + pub trim_start: Option, + pub trim_end: Option, + pub source_duration: Option>, + pub animations: Option>, + pub params: Option, + pub retime: Option>, +} + +pub struct ElementUpdateContext<'a> { + pub tracks: &'a SceneTracks, + pub track_id: &'a str, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +enum UpdateField { + Retime, + Duration, + StartTime, +} + +pub fn apply_element_update( + element: &Element, + patch: &ElementPatch, + context: &ElementUpdateContext, +) -> Element { + let mut next_element = merge_patch(element, patch); + let mut changed_fields: HashSet = HashSet::new(); + if patch.retime.is_some() { + changed_fields.insert(UpdateField::Retime); + } + if patch.duration.is_some() { + changed_fields.insert(UpdateField::Duration); + } + if patch.start_time.is_some() { + changed_fields.insert(UpdateField::StartTime); + } + + if changed_fields.contains(&UpdateField::Retime) + && let Some(patch_retime) = &patch.retime + && crate::is_retimable_element(&next_element) + { + let next_retime = patch_retime.map(|retime| RetimeConfig { + rate: clamp_retime_rate(retime.rate), + maintain_pitch: retime.maintain_pitch, + }); + + let source_duration = get_source_duration(element); + let visible_source_span = (source_duration + - next_element.base().trim_start.as_ticks() as f64 + - next_element.base().trim_end.as_ticks() as f64) + .max(0.0); + let next_duration = round_media_time(get_timeline_duration_for_source_span( + visible_source_span, + next_retime.as_ref(), + )); + + crate::set_element_retime(&mut next_element, next_retime); + next_element.base_mut().duration = next_duration; + changed_fields.insert(UpdateField::Duration); + } + + if changed_fields.contains(&UpdateField::Duration) { + next_element.base_mut().animations = animation::clamp_animations_to_duration( + next_element.base().animations.as_ref(), + next_element.base().duration, + ); + } + + if changed_fields.contains(&UpdateField::StartTime) { + let requested_start_time = next_element.base().start_time.max(MediaTime::ZERO); + if context.track_id != context.tracks.main.id() { + next_element.base_mut().start_time = requested_start_time; + } else { + let earliest = context + .tracks + .main + .elements() + .iter() + .filter(|candidate| candidate.base().id != next_element.base().id) + .reduce(|earliest, candidate| { + if candidate.base().start_time < earliest.base().start_time { + candidate + } else { + earliest + } + }); + + next_element.base_mut().start_time = match earliest { + Some(earliest) if requested_start_time > earliest.base().start_time => { + requested_start_time + } + _ => MediaTime::ZERO, + }; + } + } + + next_element +} + +fn merge_patch(element: &Element, patch: &ElementPatch) -> Element { + let mut next = element.clone(); + let base = next.base_mut(); + if let Some(name) = &patch.name { + base.name = name.clone(); + } + if let Some(duration) = patch.duration { + base.duration = duration; + } + if let Some(start_time) = patch.start_time { + base.start_time = start_time; + } + if let Some(trim_start) = patch.trim_start { + base.trim_start = trim_start; + } + if let Some(trim_end) = patch.trim_end { + base.trim_end = trim_end; + } + if let Some(source_duration) = patch.source_duration { + base.source_duration = source_duration; + } + if let Some(animations) = &patch.animations { + base.animations = animations.clone(); + } + if let Some(params) = &patch.params { + base.params.extend(params.clone()); + } + if let Some(retime) = &patch.retime { + crate::set_element_retime(&mut next, *retime); + } + next +} + +fn get_source_duration(element: &Element) -> f64 { + let base = element.base(); + if let Some(source_duration) = base.source_duration { + return source_duration.as_ticks() as f64; + } + + let retime = crate::element_retime(element); + base.trim_start.as_ticks() as f64 + + get_source_span_at_clip_time(base.duration.as_ticks() as f64, retime.as_ref()) + + base.trim_end.as_ticks() as f64 +} -- cgit v1.3.1