Time: keep a composition's final partial frame reachable

A frame-rate change can leave the duration off the frame grid. The
current-time clamp used duration minus one frame, which put the last
reachable frame before a final partial frame, so a layer fitted to a
work area inside it never got the playhead. Clamp to the last frame
that starts before the end instead; on-grid durations are unchanged.
This commit is contained in:
nazar-gavrylyk
2026-11-11 09:55:54 +01:00
parent 1991ad5d56
commit c344ff8c23
2 changed files with 25 additions and 2 deletions
+5 -2
View File
@@ -1,8 +1,11 @@
use super::{Comp, CompSelection, Event, ItemId, LayerId, Result, Session, Tick, Value, commands};
/// `t` as a current time of `comp`: a frame within its duration.
/// `t` as a current time of `comp`: a frame within its duration. The last frame is the last one
/// that starts before the end, so a final partial frame (a duration off the frame grid after a
/// frame-rate change) stays reachable; on the grid it is `duration - frame_duration`.
fn comp_frame_at(comp: &Comp, t: Tick) -> Tick {
comp.frame_rate.snap_nearest(t.min(comp.duration - comp.frame_duration()).max(Tick::ZERO))
let last = comp.frame_rate.snap(comp.duration - Tick(1)).max(Tick::ZERO);
comp.frame_rate.snap_nearest(t.max(Tick::ZERO)).min(last)
}
impl Session {
+20
View File
@@ -470,6 +470,26 @@ fn fitted_layers_off_the_frame_grid_move_the_playhead_inside_them() {
}
}
/// A duration off the frame grid (2.01 s set at 100 fps, then 24 fps) keeps its final partial
/// frame reachable: a layer fitted to a work area inside it gets the playhead on that frame.
#[test]
fn fitted_layers_in_the_final_partial_frame_get_the_playhead() {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Tail", "duration": 2.01, "frameRate": 100})).unwrap();
s.execute("prefs.set", json!({"key": "general.fitNewLayersToWorkArea", "value": true})).unwrap();
s.execute("comp.workArea", json!({"start": 2.0, "end": 2.01})).unwrap();
s.execute("comp.settings", json!({"frameRate": 24})).unwrap();
s.set_time(effectcraft_time::Tick::ZERO);
s.execute("shape.newPath", json!({"vertices": [[0, 0], [10, 0], [10, 10]]})).unwrap();
let l = &s.active_comp().unwrap().layers[0];
let t = s.time();
assert!(l.in_point <= t && t < l.out_point, "{t:?} in {:?}..{:?}", l.in_point, l.out_point);
assert_eq!(t, effectcraft_time::Tick::from_seconds_f64(2.0), "frame 48 of 24 fps");
// The playhead can't go past that last frame.
s.set_time(effectcraft_time::Tick::from_seconds_f64(5.0));
assert_eq!(s.time(), effectcraft_time::Tick::from_seconds_f64(2.0));
}
#[test]
fn default_spatial_interpolation_linear() {
let mut s = session_with_comp();