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/snap.rs | 113 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 rust/crates/timeline/src/snap.rs (limited to 'rust/crates/timeline/src/snap.rs') diff --git a/rust/crates/timeline/src/snap.rs b/rust/crates/timeline/src/snap.rs new file mode 100644 index 00000000..1133726f --- /dev/null +++ b/rust/crates/timeline/src/snap.rs @@ -0,0 +1,113 @@ +use std::collections::HashSet; + +use scene::SceneTracks; +use time::MediaTime; + +pub const BASE_TIMELINE_PIXELS_PER_SECOND: f64 = 50.0; +pub const TIMELINE_ZOOM_MIN: f64 = 0.1; +pub const TIMELINE_ZOOM_MAX: f64 = 100.0; +pub const DEFAULT_TIMELINE_SNAP_THRESHOLD_PX: f64 = 10.0; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SnapPointType { + ElementStart, + ElementEnd, + Playhead, + Bookmark, + Keyframe, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SnapPoint { + pub time: MediaTime, + pub kind: SnapPointType, + pub element_id: Option, + pub track_id: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SnapResult { + pub snapped_time: MediaTime, + pub snap_point: Option, + pub snap_distance: f64, +} + +pub fn resolve_timeline_snap( + target_time: MediaTime, + snap_points: &[SnapPoint], + max_snap_distance: f64, +) -> SnapResult { + let mut closest_snap_point: Option<&SnapPoint> = None; + let mut closest_distance = f64::INFINITY; + + for snap_point in snap_points { + let distance = (target_time - snap_point.time).as_ticks().unsigned_abs() as f64; + if distance <= max_snap_distance && distance < closest_distance { + closest_distance = distance; + closest_snap_point = Some(snap_point); + } + } + + SnapResult { + snapped_time: closest_snap_point + .map(|snap_point| snap_point.time) + .unwrap_or(target_time), + snap_point: closest_snap_point.cloned(), + snap_distance: closest_distance, + } +} + +pub fn get_timeline_snap_threshold_in_ticks( + zoom_level: f64, + snap_threshold_px: Option, +) -> f64 { + let threshold_px = snap_threshold_px.unwrap_or(DEFAULT_TIMELINE_SNAP_THRESHOLD_PX); + let pixels_per_second = BASE_TIMELINE_PIXELS_PER_SECOND * zoom_level; + (threshold_px / pixels_per_second) * time::TICKS_PER_SECOND as f64 +} + +pub fn build_timeline_snap_points( + sources: impl IntoIterator>, +) -> Vec { + sources.into_iter().flatten().collect() +} + +pub fn get_element_edge_snap_points( + tracks: &SceneTracks, + exclude_element_ids: Option<&HashSet<&str>>, +) -> Vec { + let mut snap_points = Vec::new(); + + for track in crate::placement::ordered_tracks(tracks) { + for element in track.elements() { + let base = element.base(); + if exclude_element_ids.is_some_and(|ids| ids.contains(base.id.as_str())) { + continue; + } + + snap_points.push(SnapPoint { + time: base.start_time, + kind: SnapPointType::ElementStart, + element_id: Some(base.id.clone()), + track_id: Some(track.id().to_string()), + }); + snap_points.push(SnapPoint { + time: base.start_time + base.duration, + kind: SnapPointType::ElementEnd, + element_id: Some(base.id.clone()), + track_id: Some(track.id().to_string()), + }); + } + } + + snap_points +} + +pub fn get_playhead_snap_points(playhead_time: MediaTime) -> Vec { + vec![SnapPoint { + time: playhead_time, + kind: SnapPointType::Playhead, + element_id: None, + track_id: None, + }] +} -- cgit v1.3.1