#890: the Timeline time field reads its displayed timecode back as the same frame at 29.97/59.94 fps

Problem: the Timeline shows non-drop timecode, but time.set (the current-time field) parsed
typed text as drop-frame at 29.97/59.94 fps, so committing the displayed time, or typing a
time, landed on another frame from minute 1 on (frame 1800 "0:01:00:00" -> 1798).

Fix: typed text is read as drop-frame only when it uses ';' fields; ':' and '.' follow the
displayed non-drop style. Regression test round-trips the display at both rates.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76 2026-10-11 19:00:25 +02:00
parent 67434e9038
commit 51501bbd21
2 changed files with 23 additions and 1 deletions

View File

@ -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)

View File

@ -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();