diff --git a/crates/engine/src/cmd/layer/create.rs b/crates/engine/src/cmd/layer/create.rs index 0b5116e95..c46222e7e 100644 --- a/crates/engine/src/cmd/layer/create.rs +++ b/crates/engine/src/cmd/layer/create.rs @@ -31,12 +31,19 @@ pub(crate) fn fit_span(l: &mut Layer, span: Option<(Tick, Tick)>) { } /// Tools draw on the layer they create: when it was fitted to a span the playhead is outside, -/// move `comp`'s playhead to the span's start so the new layer shows. The layer's timing is kept. +/// move `comp`'s playhead to the first frame in the span so the new layer shows. The layer's timing +/// is kept. The work area need not sit on frames (a typed start, a frame-rate change), so the +/// frame is rounded up from the start, not to the nearest; a span holding no frame moves nothing. pub(crate) fn show_span(s: &mut Session, comp: ItemId, span: Option<(Tick, Tick)>) { + let Some(fr) = s.project.comp(comp).map(|c| c.frame_rate) else { return }; if let Some((start, end)) = span && !(start..end).contains(&s.time_of(comp)) { - s.set_time_of(comp, start); + let first = fr.snap(start); + let first = if first < start { fr.tick_of(fr.frame_at(start).saturating_add(1)) } else { first }; + if first < end { + s.set_time_of(comp, first); + } } } diff --git a/crates/engine/src/tests/settings.rs b/crates/engine/src/tests/settings.rs index 556135fff..29792efc0 100644 --- a/crates/engine/src/tests/settings.rs +++ b/crates/engine/src/tests/settings.rs @@ -449,6 +449,27 @@ fn fitted_layers_in_another_comp_move_that_comps_playhead() { } } +/// A work area off the frame grid (set at 25 fps, then the comp went to 30 fps): the playhead +/// goes to the first frame inside it, not the nearest frame before its start. +#[test] +fn fitted_layers_off_the_frame_grid_move_the_playhead_inside_them() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Grid", "duration": 2, "frameRate": 25})).unwrap(); + s.execute("prefs.set", json!({"key": "general.fitNewLayersToWorkArea", "value": true})).unwrap(); + s.execute("comp.workArea", json!({"start": 0.04, "end": 0.08})).unwrap(); + s.execute("comp.settings", json!({"frameRate": 30})).unwrap(); + for (cmd, p) in [("layer.newText", json!({"text": "a", "edit": true})), ("shape.newPath", json!({"vertices": [[0, 0], [10, 0], [10, 10]]}))] { + s.set_time(effectcraft_time::Tick::from_seconds_f64(1.0)); + s.state.selected_layers.clear(); + s.state.text_edit = None; + s.execute(cmd, p).unwrap(); + let l = &s.active_comp().unwrap().layers[0]; + let t = s.time(); + assert!(l.in_point <= t && t < l.out_point, "{cmd}: {t:?} in {:?}..{:?}", l.in_point, l.out_point); + assert_eq!(t, effectcraft_time::Tick::from_seconds_f64(2.0 / 30.0), "{cmd}: the first frame inside"); + } +} + #[test] fn default_spatial_interpolation_linear() { let mut s = session_with_comp();