diff --git a/apps/desktop-gpui/src/editor_edits.rs b/apps/desktop-gpui/src/editor_edits.rs index 13631e4537f..56762e0e019 100644 --- a/apps/desktop-gpui/src/editor_edits.rs +++ b/apps/desktop-gpui/src/editor_edits.rs @@ -2558,6 +2558,7 @@ pub fn set_clip_segment_timescale( let old_duration = clip_timeline_duration(timeline); let old_offsets = editor_timeline::clip_timeline_offsets(timeline); + let holds = timeline.hold_windows(); let incoming_duration = timeline .effective_transition(index) .map_or(0.0, |transition| transition.duration); @@ -2571,9 +2572,10 @@ pub fn set_clip_segment_timescale( let old_next_boundary = old_offsets.get(index + 1).copied().unwrap_or(old_duration); let new_next_boundary = new_offsets.get(index + 1).copied().unwrap_or(new_duration); + let effective = |time: f64| time - held_time_before(&holds, time); let shift = |time: f64| { timeline_shift_after_clip_duration_change( - time, + effective(time), old_offsets[index], new_offsets[index], absolute_start, @@ -2598,14 +2600,26 @@ pub fn set_clip_segment_timescale( segment.start += shift(segment.start); segment.end += shift(segment.end); } - let bends = [old_offsets[index], absolute_start, old_next_boundary]; + // Track times include fullscreen-title holds, during which the clip clock + // stands still, so the remap also bends at every hold edge it spans. + let retimed = old_offsets[index]..=old_next_boundary; + let bends: Vec = [old_offsets[index], absolute_start, old_next_boundary] + .map(|bend| effective_to_output(&holds, bend)) + .into_iter() + .chain( + holds + .iter() + .flat_map(|&(start, end)| [start, end]) + .filter(|&edge| retimed.contains(&effective(edge))), + ) + .collect(); for segment in &mut timeline.mask_segments { let old_start = segment.start; let start_shift = shift(old_start); segment.start += start_shift; segment.end += shift(segment.end); let keyframes = &mut segment.keyframes; - let relative_bends = bends.map(|bend| bend - old_start); + let relative_bends: Vec = bends.iter().map(|bend| bend - old_start).collect(); split_keyframes_at( &mut keyframes.position, &relative_bends, @@ -4692,6 +4706,98 @@ mod tests { assert!(!set_clip_segment_timescale(timeline, 0, 2.0)); } + #[test] + fn clip_speed_changes_ripple_tracks_past_fullscreen_holds() { + let mut project = config(serde_json::json!({ + "timeline": { + "segments": [ + { "recordingSegment": 0, "timescale": 1.0, "start": 0.0, "end": 10.0 }, + { "recordingSegment": 1, "timescale": 1.0, "start": 0.0, "end": 10.0 } + ], + "zoomSegments": [ + { "start": 11.0, "end": 12.0, "amount": 1.5, "mode": "auto" }, + { "start": 20.0, "end": 21.0, "amount": 1.5, "mode": "auto" } + ], + "textSegments": [ + { "start": 0.0, "end": 3.0, "track": 0, "content": "intro", "layout": "fullscreen" }, + { "start": 23.0, "end": 25.0, "track": 0, "content": "outro", "layout": "fullscreen" } + ], + "maskSegments": [{ + "start": 11.0, "end": 17.0, "maskType": "sensitive", + "center": { "x": 0.5, "y": 0.5 }, "size": { "x": 0.2, "y": 0.2 }, + "keyframes": { "intensity": [ + { "time": 0.0, "value": 0.0 }, + { "time": 6.0, "value": 1.0 } + ] } + }] + } + })); + let timeline = project.timeline.as_mut().unwrap(); + + assert!(set_clip_segment_timescale(timeline, 1, 2.0)); + + let zooms: Vec<_> = timeline + .zoom_segments + .iter() + .map(|segment| (segment.start, segment.end)) + .collect(); + assert_eq!(zooms, [(11.0, 12.0), (16.5, 17.0)]); + let texts: Vec<_> = timeline + .text_segments + .iter() + .map(|segment| (segment.start, segment.end)) + .collect(); + assert_eq!(texts, [(0.0, 3.0), (18.0, 20.0)]); + let mask = &timeline.mask_segments[0]; + assert_eq!((mask.start, mask.end), (11.0, 15.0)); + let intensity: Vec<_> = mask + .keyframes + .intensity + .iter() + .map(|key| (key.time, key.value)) + .collect(); + assert_eq!(intensity, [(0.0, 0.0), (2.0, 1.0 / 3.0), (4.0, 1.0)]); + } + + #[test] + fn clip_speed_changes_split_mask_keyframes_at_hold_edges() { + let mut project = config(serde_json::json!({ + "timeline": { + "segments": [ + { "recordingSegment": 0, "timescale": 1.0, "start": 0.0, "end": 10.0 }, + { "recordingSegment": 1, "timescale": 1.0, "start": 0.0, "end": 10.0 } + ], + "zoomSegments": [], + "textSegments": [ + { "start": 14.0, "end": 16.0, "track": 0, "content": "title", "layout": "fullscreen" } + ], + "maskSegments": [{ + "start": 12.0, "end": 20.0, "maskType": "sensitive", + "center": { "x": 0.5, "y": 0.5 }, "size": { "x": 0.2, "y": 0.2 }, + "keyframes": { "intensity": [ + { "time": 0.0, "value": 0.0 }, + { "time": 8.0, "value": 1.0 } + ] } + }] + } + })); + let timeline = project.timeline.as_mut().unwrap(); + + assert!(set_clip_segment_timescale(timeline, 1, 2.0)); + + let text = &timeline.text_segments[0]; + assert_eq!((text.start, text.end), (12.0, 14.0)); + let mask = &timeline.mask_segments[0]; + assert_eq!((mask.start, mask.end), (11.0, 16.0)); + let intensity: Vec<_> = mask + .keyframes + .intensity + .iter() + .map(|key| (key.time, key.value)) + .collect(); + assert_eq!(intensity, [(0.0, 0.0), (1.0, 0.25), (3.0, 0.5), (5.0, 1.0)]); + } + #[test] fn clip_speed_changes_remap_mask_keyframes_in_absolute_time() { for (timescale, expected) in [