diff --git a/crates/engine/src/commands/time.rs b/crates/engine/src/commands/time.rs index f98ea5ed..a2a1ffb4 100644 --- a/crates/engine/src/commands/time.rs +++ b/crates/engine/src/commands/time.rs @@ -39,7 +39,10 @@ pub fn parse_display_time(s: &Session, comp: &Comp, text: &str, current: i64) -> effectcraft_time::parse_feet_frames(text, pf, current + start).map(|f| f - start).map_err(|e| e.0) } None => { - let parsed = effectcraft_time::parse_timecode(text, fr, fr.supports_drop_frame(), current).map_err(|e| e.0)?; + // The Timeline shows non-drop timecode (`:`), so typed `:`/`.` text is read as non-drop + // too; only explicit `;` fields mean drop-frame (#890). + let drop_frame = fr.supports_drop_frame() && text.contains(';'); + let parsed = effectcraft_time::parse_timecode(text, fr, drop_frame, current).map_err(|e| e.0)?; let relative = text.trim().starts_with('+') || text.trim().starts_with('-'); if relative { Ok(parsed) diff --git a/crates/engine/src/tests_timeline.rs b/crates/engine/src/tests_timeline.rs index 6a74a23b..efa3bf5a 100644 --- a/crates/engine/src/tests_timeline.rs +++ b/crates/engine/src/tests_timeline.rs @@ -414,6 +414,25 @@ fn time_set_snaps_to_the_nearest_frame_and_ae_timecode() { assert_eq!(effectcraft_time::format_timecode_ae(75, fr, true), "0;00;02;15"); } +/// #890: at 29.97 / 59.94 fps the displayed (non-drop) timecode was read back as drop-frame, so +/// committing the current-time field moved the playhead to another frame from minute 1 on. +#[test] +fn time_set_round_trips_the_displayed_timecode_at_drop_frame_rates() { + for fps in [29.97, 59.94] { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "T", "width": 64, "height": 64, "frameRate": fps, "duration": 4000})).unwrap(); + for frame in [0_i64, 1799, 1800, 1801, 3600, 17982, 17983, 100_000] { + let r = s.execute("time.set", json!({"frame": frame})).unwrap(); + let display = r["display"].as_str().unwrap().to_string(); + let r = s.execute("time.set", json!({"timecode": display})).unwrap(); + assert_eq!(r["frame"], frame, "{fps} fps: displayed {display}"); + } + // Explicit drop-frame fields still mean drop-frame. + let r = s.execute("time.set", json!({"timecode": "0;01;00;02"})).unwrap(); + assert_eq!(r["frame"], if fps > 30.0 { 3598 } else { 1800 }); + } +} + #[test] fn time_set_keeps_the_frame_shown_in_the_current_time_field() { let mut s = Session::default();