Settings: fit tools move the target comp's playhead, not the active one

show_span now checks and moves the playhead of the comp the layer was
made in, through a new Session::set_time_of that set_time uses for the
active comp, so time sync and the text editor boundary stay the same.
This commit is contained in:
nazar-gavrylyk
2026-11-11 08:47:25 +01:00
parent abdf4c369a
commit 34086899ad
5 changed files with 46 additions and 16 deletions
+5 -5
View File
@@ -31,12 +31,12 @@ 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 the playhead to the span's start so the new layer shows. The layer's timing is kept.
pub(crate) fn show_span(s: &mut Session, span: Option<(Tick, Tick)>) {
/// move `comp`'s playhead to the span's start so the new layer shows. The layer's timing is kept.
pub(crate) fn show_span(s: &mut Session, comp: ItemId, span: Option<(Tick, Tick)>) {
if let Some((start, end)) = span
&& !(start..end).contains(&s.time())
&& !(start..end).contains(&s.time_of(comp))
{
s.set_time(start);
s.set_time_of(comp, start);
}
}
@@ -244,7 +244,7 @@ pub(super) fn new_text(s: &mut Session, p: &Value) -> Result<Value> {
Ok(id)
})?;
if edit {
show_span(s, span);
show_span(s, cid, span);
}
Ok(json!({"layer": id.0}))
}
+1 -1
View File
@@ -364,7 +364,7 @@ fn new_shape(s: &mut Session, p: &Value) -> Result<Value> {
Ok((lid, group))
})?;
if target.is_none() {
super::layer::show_span(s, span);
super::layer::show_span(s, cid, span);
}
Ok(json!({"layer": lid.0, "group": group}))
}
+1 -1
View File
@@ -302,7 +302,7 @@ fn new_path(s: &mut Session, p: &Value) -> Result<Value> {
Ok((lid, group, path_uid))
})?;
if target.is_none() {
super::layer::show_span(s, span);
super::layer::show_span(s, cid, span);
}
Ok(json!({"layer": lid.0, "group": group, "path": path_uid}))
}
+14 -9
View File
@@ -114,18 +114,23 @@ impl Session {
/// editor is open, so playback with no text editor does no text copying.
pub fn set_time(&mut self, t: Tick) {
if let Some(c) = self.state.active_comp {
let t = self.project.comp(c).map_or(t, |comp| comp_frame_at(comp, t));
let edited = commands::text_edit::active_editor_snapshot(self);
self.state.times.insert(c, t);
if self.prefs.general.sync_time_related_items {
// Direct call: this `set_time` owns the editor snapshot for both time changes,
// so a synced move does not take a second snapshot or start a second epoch.
self.sync_related_times_inner(c, t);
}
commands::text_edit::after_external_change(self, edited.as_ref());
self.set_time_of(c, t);
}
}
/// Move `comp`'s current time, as [`Session::set_time`] does for the active comp.
pub fn set_time_of(&mut self, c: ItemId, t: Tick) {
let t = self.project.comp(c).map_or(t, |comp| comp_frame_at(comp, t));
let edited = commands::text_edit::active_editor_snapshot(self);
self.state.times.insert(c, t);
if self.prefs.general.sync_time_related_items {
// Direct call: this `set_time_of` owns the editor snapshot for both time changes,
// so a synced move does not take a second snapshot or start a second epoch.
self.sync_related_times_inner(c, t);
}
commands::text_edit::after_external_change(self, edited.as_ref());
}
/// Settings ▸ General ▸ Synchronize Time of All Related Items: moving the current time of
/// `comp` moves the current time of the comps nested in it (through their precomp layers'
/// timing) and of the comps it is nested in, so every related viewer shows the same moment.
+25
View File
@@ -424,6 +424,31 @@ fn new_layers_fit_the_work_area_when_asked() {
assert_eq!(span(&s), (7.0, 7.0, 10.0), "footage and comps keep their own timing");
}
/// A tool command aimed at another comp moves that comp's playhead into its work area and
/// leaves the active comp's playhead where it was.
#[test]
fn fitted_layers_in_another_comp_move_that_comps_playhead() {
let mut s = session_with_comp();
let active = s.active_comp_id().unwrap();
s.execute("prefs.set", json!({"key": "general.fitNewLayersToWorkArea", "value": true})).unwrap();
let other = effectcraft_project::ItemId(s.execute("comp.new", json!({"name": "Other", "duration": 10, "open": false})).unwrap()["comp"].as_u64().unwrap());
s.execute("comp.workArea", json!({"comp": other.0, "start": 2, "end": 5})).unwrap();
s.execute("comp.workArea", json!({"start": 8, "end": 9})).unwrap();
s.set_time(effectcraft_time::Tick::from_seconds_f64(7.0));
for (cmd, p) in [
("layer.newText", json!({"comp": other.0, "text": "a", "edit": true})),
("shape.newShape", json!({"comp": other.0, "kind": "ellipse"})),
("shape.newPath", json!({"comp": other.0, "vertices": [[0, 0], [10, 0], [10, 10]]})),
] {
s.set_time_of(other, effectcraft_time::Tick::from_seconds_f64(7.0));
s.state.selected_layers.clear();
s.state.text_edit = None;
s.execute(cmd, p).unwrap();
assert_eq!(s.time_of(other).seconds().round(), 2.0, "{cmd}: the target comp's playhead");
assert_eq!(s.time_of(active).seconds().round(), 7.0, "{cmd}: the active comp's playhead");
}
}
#[test]
fn default_spatial_interpolation_linear() {
let mut s = session_with_comp();