mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
#1714: reopen each comp at its saved current time
Layer times were already saved and read back as exact ticks; the values in the issue are 34f, 315f and 3033f at 59.94 fps. The viewer was black because the playhead wasn't saved: a reopened comp started at 0, before its layers. The project file's view (`activeComp`, `openComps`) now also records each comp's current time (`compTimes`), restored on open within the comp's frames. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8f02f52a44
commit
b9172fec2f
@@ -344,6 +344,11 @@ fn one_view() -> u8 {
|
||||
1
|
||||
}
|
||||
|
||||
/// `t` as a current time of `comp`: a frame within its 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))
|
||||
}
|
||||
|
||||
/// What Copy with Property Links / Copy Expression Only put on the clipboard.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum LinkClip {
|
||||
@@ -860,11 +865,7 @@ impl Session {
|
||||
|
||||
pub fn set_time(&mut self, t: Tick) {
|
||||
if let Some(c) = self.state.active_comp {
|
||||
let comp = self.project.comp(c);
|
||||
let t = match comp {
|
||||
Some(comp) => comp.frame_rate.snap_nearest(t.clamp(Tick::ZERO, comp.duration - comp.frame_duration())),
|
||||
None => t,
|
||||
};
|
||||
let t = self.project.comp(c).map_or(t, |comp| comp_frame_at(comp, t));
|
||||
self.state.times.insert(c, t);
|
||||
if self.prefs.general.sync_time_related_items {
|
||||
self.sync_related_times(c, t);
|
||||
@@ -985,13 +986,21 @@ impl Session {
|
||||
|
||||
/// The open compositions, as a project file records them ([`effectcraft_project::SavedView`]).
|
||||
pub fn saved_view(&self) -> effectcraft_project::SavedView {
|
||||
effectcraft_project::SavedView { active_comp: self.state.active_comp, open_comps: self.state.open_comps.clone() }
|
||||
let comp_times = self.state.times.iter().filter(|(id, _)| self.project.comp(**id).is_some()).map(|(id, t)| (*id, *t)).collect();
|
||||
effectcraft_project::SavedView { active_comp: self.state.active_comp, open_comps: self.state.open_comps.clone(), comp_times }
|
||||
}
|
||||
|
||||
/// Reopen the compositions a project file was saved with (#748). Ids that no longer name a
|
||||
/// Reopen the compositions a project file was saved with (#748), each at its saved current
|
||||
/// time (#1714). Ids that no longer name a
|
||||
/// comp are skipped; with none left the project stays on the comp [`Session::replace_project`]
|
||||
/// opened (its first), as files from before the view was saved do.
|
||||
pub fn restore_view(&mut self, view: &effectcraft_project::SavedView) {
|
||||
for (id, t) in &view.comp_times {
|
||||
if let Some(comp) = self.project.comp(*id) {
|
||||
let t = comp_frame_at(comp, *t);
|
||||
self.state.times.insert(*id, t);
|
||||
}
|
||||
}
|
||||
let mut open: Vec<ItemId> = Vec::new();
|
||||
for id in view.open_comps.iter().copied().filter(|id| self.project.comp(*id).is_some()) {
|
||||
if !open.contains(&id) {
|
||||
|
||||
@@ -686,3 +686,41 @@ fn reopening_a_project_restores_its_active_comp() {
|
||||
o.insert("openComps".into(), open);
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer times are saved as exact ticks and read back unchanged, and each comp reopens at the
|
||||
/// current time it was saved at, not at 0, where a layer starting later shows nothing (#1714).
|
||||
#[test]
|
||||
fn reopening_a_project_restores_layer_times_and_current_time() {
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
let dir = std::env::temp_dir().join(format!("ec-test-1714-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("times.ecproj").to_string_lossy().to_string();
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "C", "width": 108, "height": 192, "duration": 60})).unwrap();
|
||||
s.execute("layer.newSolid", json!({"name": "crown"})).unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let comp = std::sync::Arc::make_mut(&mut s.project).comp_mut(cid).unwrap();
|
||||
comp.frame_rate = FrameRate::FPS_59_94;
|
||||
// The layer from the issue: 34f, 315f and 3033f at 59.94 fps.
|
||||
let l = comp.layers.first_mut().unwrap();
|
||||
(l.start_time, l.in_point, l.out_point) = (Tick(144_086_342_400), Tick(1_334_917_584_000), Tick(12_853_349_308_800));
|
||||
s.execute("time.set", json!({"time": 6.0})).unwrap();
|
||||
let t = s.time();
|
||||
s.execute("file.saveAs", json!({"path": path})).unwrap();
|
||||
|
||||
let mut s2 = Session::default();
|
||||
s2.execute("file.open", json!({"path": path})).unwrap();
|
||||
let l = s2.project.comp(cid).unwrap().layers.first().unwrap();
|
||||
assert_eq!((l.start_time, l.in_point, l.out_point), (Tick(144_086_342_400), Tick(1_334_917_584_000), Tick(12_853_349_308_800)));
|
||||
assert_eq!(s2.time(), t);
|
||||
assert!(l.in_point <= s2.time() && s2.time() < l.out_point, "the layer shows at the reopened time");
|
||||
assert!(!s2.is_dirty());
|
||||
|
||||
// A saved time past the comp's end (a hand-edited file) lands on its last frame.
|
||||
let text = std::fs::read_to_string(&path).unwrap().replace(&format!("\"{}\": {}", cid.0, t.0), &format!("\"{}\": {}", cid.0, i64::MAX));
|
||||
std::fs::write(&path, text).unwrap();
|
||||
let mut s3 = Session::default();
|
||||
s3.execute("file.open", json!({"path": path})).unwrap();
|
||||
let c = s3.project.comp(cid).unwrap();
|
||||
assert_eq!(s3.time(), c.frame_rate.snap_nearest(c.duration - c.frame_duration()));
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ pub struct SavedView {
|
||||
/// The open comps (Timeline tabs), in tab order.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub open_comps: Vec<ItemId>,
|
||||
/// Each comp's current time, so a project reopens on the frame it was saved at rather than
|
||||
/// at 0, where its layers may not have started yet (#1714).
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub comp_times: BTreeMap<ItemId, Tick>,
|
||||
}
|
||||
|
||||
/// The view a project file was saved with ([`SavedView`]); empty for older files. Values that
|
||||
@@ -73,6 +77,8 @@ pub fn saved_view(text: &str) -> SavedView {
|
||||
active_comp: Option<serde_json::Value>,
|
||||
#[serde(rename = "openComps", default)]
|
||||
open_comps: Option<serde_json::Value>,
|
||||
#[serde(rename = "compTimes", default)]
|
||||
comp_times: Option<serde_json::Value>,
|
||||
}
|
||||
let Ok(m) = serde_json::from_str::<Meta>(text) else { return SavedView::default() };
|
||||
SavedView {
|
||||
@@ -82,6 +88,11 @@ pub fn saved_view(text: &str) -> SavedView {
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map_or_else(Vec::new, |a| a.iter().filter_map(serde_json::Value::as_u64).map(ItemId).collect()),
|
||||
comp_times: m
|
||||
.comp_times
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.map_or_else(BTreeMap::new, |o| o.iter().filter_map(|(k, v)| Some((ItemId(k.parse().ok()?), Tick(v.as_i64()?)))).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,14 +173,14 @@ fn project_files_name_their_version_and_always_read_back() {
|
||||
#[test]
|
||||
fn project_files_remember_the_open_comps() {
|
||||
let (p, cid) = project_with_layer();
|
||||
let view = crate::SavedView { active_comp: Some(cid), open_comps: vec![cid] };
|
||||
let view = crate::SavedView { active_comp: Some(cid), open_comps: vec![cid], comp_times: [(cid, effectcraft_time::Tick(4_237_833_600))].into() };
|
||||
let text = p.to_file_json_with_view(&view).unwrap();
|
||||
assert_eq!(crate::saved_view(&text), view);
|
||||
assert_eq!(Project::from_json(&text).unwrap(), p, "the view is not part of the project");
|
||||
// Files from before #748 have no view; a garbled one is ignored, not an error.
|
||||
assert_eq!(crate::saved_view(&p.to_file_json().unwrap()), crate::SavedView::default());
|
||||
let odd = text.replacen(&format!("\"activeComp\": {}", cid.0), "\"activeComp\": \"Main\"", 1);
|
||||
assert_eq!(crate::saved_view(&odd), crate::SavedView { active_comp: None, open_comps: vec![cid] });
|
||||
assert_eq!(crate::saved_view(&odd), crate::SavedView { active_comp: None, ..view });
|
||||
}
|
||||
|
||||
/// The Project panel's "used N times": layers in every comp that use the item directly, a precomp
|
||||
|
||||
Reference in New Issue
Block a user