mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-12 21:32:52 +00:00
#3456: keep Warp Stabilizer and 3D Camera Tracker analyses saved before 0.23
Their keys were compared only with today's signature, so the first edit of a project saved by 0.22 (even adding it to the render queue, as effectcraft-cli render does) cleared both analyses and queued them again, and lost them when the footage was offline. One check, warp::key_matches, now accepts a key made from today's signature or from the one 0.22 wrote for the same frames, and keeps it. Warp Stabilizer, the 3D Camera Tracker and Roto Brush all use it. The regression test opens a project saved by the released 0.22.0 CLI.
This commit is contained in:
7 files changed
+1700
-13
No files matched your search
@@ -134,6 +134,7 @@ configuration from these Apache-2.0 projects:
|
||||
| Asset | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| `crates/engine/src/tests/fixtures/object-matte-0.22.ecproj` | lisn0 | Original work, synthetic project saved by EffectCraft 0.22.0 | MIT OR Apache-2.0 |
|
||||
| `crates/engine/src/tests/fixtures/tracked-0.22.ecproj` | arnav-hiwarkar | Original work, synthetic project saved by EffectCraft 0.22.0 | MIT OR Apache-2.0 |
|
||||
|
||||
## First-party brand marks
|
||||
|
||||
|
||||
@@ -124,8 +124,17 @@ fn str_of(g: &PropGroup, m: &str) -> String {
|
||||
|
||||
/// What a tracker's tracks are made from: the layer's frames and Detailed Analysis.
|
||||
fn tracks_key(project: &Project, cid: ItemId, l: &Layer, g: &PropGroup) -> String {
|
||||
tracks_key_of(signature(project, cid, l), g)
|
||||
}
|
||||
|
||||
fn tracks_key_of(signature: String, g: &PropGroup) -> String {
|
||||
let detailed = effectcraft_effects::warp_stab::param(&static_params(g), "advanced/detailedAnalysis").is_some_and(Value::as_bool);
|
||||
format!("{}{}", signature(project, cid, l), if detailed { "+detailed" } else { "" })
|
||||
format!("{signature}{}", if detailed { "+detailed" } else { "" })
|
||||
}
|
||||
|
||||
/// Whether the tracker's saved tracks are made from the layer's current frames and settings.
|
||||
fn tracks_match(project: &Project, cid: ItemId, l: &Layer, g: &PropGroup) -> bool {
|
||||
crate::warp::key_matches(project, cid, l, &str_of(g, TRACKS_KEY), |s| tracks_key_of(s, g))
|
||||
}
|
||||
|
||||
/// Parameters of an instance from static values (the tracker's settings are not animated).
|
||||
@@ -165,7 +174,7 @@ pub(crate) fn invalidate(before: &Project, after: &mut Project) -> Vec<(ItemId,
|
||||
let old_layer = before.comp(*cid).and_then(|bc| bc.layer(l.id));
|
||||
for g in fx.groups().filter(|g| is_tracker(g)) {
|
||||
let tracked = !str_of(g, TRACKS).is_empty();
|
||||
if tracked && str_of(g, TRACKS_KEY) != tracks_key(after, *cid, l, g) {
|
||||
if tracked && !tracks_match(after, *cid, l, g) {
|
||||
clears.push((*cid, l.id, g.uid, true));
|
||||
todo.push((*cid, l.id, g.uid));
|
||||
} else if tracked && str_of(g, SOLVE_KEY) != ct::solve_key(&ct::settings(&static_params(g))) {
|
||||
|
||||
@@ -222,12 +222,6 @@ pub fn input_key(p: &Project, comp: ItemId, layer: &Layer, uid: Uid) -> String {
|
||||
input_key_with_signature(crate::warp::signature(p, comp, layer), layer, uid)
|
||||
}
|
||||
|
||||
/// Recognize the input key serialized before 0.23 without re-keying its frozen matte.
|
||||
fn input_key_before_placed_page(p: &Project, comp: ItemId, layer: &Layer, uid: Uid) -> Option<String> {
|
||||
let signature = crate::warp::signature_before_placed_page(p, comp, layer)?;
|
||||
Some(input_key_with_signature(signature, layer, uid))
|
||||
}
|
||||
|
||||
fn input_key_with_signature(mut s: String, layer: &Layer, uid: Uid) -> String {
|
||||
if let Some(m) = layer.masks() {
|
||||
s.push_str(&format!("{m:?}"));
|
||||
@@ -258,7 +252,7 @@ pub(crate) fn sync(before: &Project, after: &mut Project) -> Vec<(ItemId, LayerI
|
||||
let old = before.comp(*cid).and_then(|bc| bc.layer(l.id)).and_then(|ol| ol.props.find_group(g.uid));
|
||||
let saved_key = str_of(g, INPUT_KEY);
|
||||
// Keep a matching old key: the frozen cache itself also hashes that key.
|
||||
if saved_key != key && input_key_before_placed_page(after, *cid, l, g.uid).as_deref() != Some(saved_key.as_str()) {
|
||||
if !crate::warp::key_matches(after, *cid, l, &saved_key, |s| input_key_with_signature(s, l, g.uid)) {
|
||||
set.push((*cid, l.id, g.uid, key));
|
||||
todo.push((*cid, l.id, g.uid));
|
||||
} else if old.is_none_or(|o| guard_of(o) != guard_of(g)) {
|
||||
|
||||
@@ -122,9 +122,16 @@ pub fn signature(p: &Project, comp: ItemId, layer: &Layer) -> String {
|
||||
signature_with_source(p, comp, layer, &src)
|
||||
}
|
||||
|
||||
/// Whether `saved`, a key an analysis of `layer` was saved with, is for the layer's current frames.
|
||||
/// `key` makes such a key from a signature. A key made from the signature an earlier release wrote
|
||||
/// for the same frames matches too: keep it rather than rewrite it (analysing writes today's).
|
||||
pub(crate) fn key_matches(p: &Project, comp: ItemId, layer: &Layer, saved: &str, key: impl Fn(String) -> String) -> bool {
|
||||
key(signature(p, comp, layer)) == saved || signature_before_placed_page(p, comp, layer).is_some_and(|s| key(s) == saved)
|
||||
}
|
||||
|
||||
/// Before `Footage::placed_page` was added in 0.23, the persisted signature hashed the
|
||||
/// same Debug representation without that last field. Only ordinary footage can match it.
|
||||
pub(crate) fn signature_before_placed_page(p: &Project, comp: ItemId, layer: &Layer) -> Option<String> {
|
||||
/// same footage text without that last field. Only ordinary footage can match it.
|
||||
fn signature_before_placed_page(p: &Project, comp: ItemId, layer: &Layer) -> Option<String> {
|
||||
let LayerSource::Footage { item } = &layer.source else { return None };
|
||||
let ItemKind::Footage(footage) = &p.item(*item)?.kind else { return None };
|
||||
if footage.placed_page {
|
||||
@@ -256,11 +263,10 @@ pub(crate) fn invalidate(before: &Project, after: &mut Project) -> Vec<(ItemId,
|
||||
if !fx.groups().any(is_warp) {
|
||||
continue;
|
||||
}
|
||||
let sig = signature(after, *cid, l);
|
||||
let old_layer = before.comp(*cid).and_then(|bc| bc.layer(l.id));
|
||||
for g in fx.groups().filter(|g| is_warp(g)) {
|
||||
let analysed = !str_of(g, ANALYSIS).is_empty();
|
||||
if analysed && str_of(g, ANALYSIS_KEY) != sig {
|
||||
if analysed && !key_matches(after, *cid, l, &str_of(g, ANALYSIS_KEY), |s| s) {
|
||||
clear.push((*cid, l.id, g.uid));
|
||||
todo.push((*cid, l.id, g.uid));
|
||||
} else if !analysed && old_layer.and_then(|ol| ol.props.find_group(g.uid)).is_none() {
|
||||
|
||||
+1593
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
||||
asset: tracked-0.22.ecproj
|
||||
title: One clip with an analysed Warp Stabilizer and 3D Camera Tracker from EffectCraft 0.22.0
|
||||
author: arnav-hiwarkar
|
||||
source: original work
|
||||
license: MIT OR Apache-2.0
|
||||
license-file: ../../../../../LICENSE-MIT
|
||||
added: 2026-11-12 by arnav-hiwarkar
|
||||
notes: Saved by the released 0.22.0 CLI after importing a generated 160x120, 12-frame shaking crop of an ffmpeg testsrc2 pattern, applying both effects and analysing them. The source path is a generic test path; the video is not needed by the tests, which only check that the saved analyses are kept or cleared.
|
||||
@@ -1,6 +1,82 @@
|
||||
//! Warp Stabilizer and 3D Camera Tracker analyses saved by an earlier release keep matching
|
||||
//! their footage after an edit that doesn't change it (#3456), and a real change still clears them.
|
||||
|
||||
use effectcraft_effects::camera_tracker::{TRACKS, TRACKS_KEY};
|
||||
use effectcraft_effects::warp_stab::{ANALYSIS, ANALYSIS_KEY};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::{ItemId, LayerId, Project};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
const FOOTAGE: ItemId = ItemId(1);
|
||||
const COMP: ItemId = ItemId(2);
|
||||
const CLIP: LayerId = LayerId(3);
|
||||
const WARP: u64 = 26;
|
||||
const TRACKER: u64 = 54;
|
||||
|
||||
/// The analysis (as its length and a hash: it's long) and key of the Warp Stabilizer and the
|
||||
/// 3D Camera Tracker.
|
||||
fn saved(s: &Session) -> [(usize, u64, String); 2] {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let layer = s.project.comp(COMP).unwrap().layer(CLIP).unwrap();
|
||||
let get = |uid: u64, data: &str, key: &str| {
|
||||
let params = crate::camera_track::static_params(layer.props.find_group(uid).unwrap());
|
||||
let text = |id: &str| match params.get(id) {
|
||||
Some(Value::Str(s)) => s.clone(),
|
||||
other => panic!("{id}: {other:?}"),
|
||||
};
|
||||
let analysis = text(data);
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
analysis.hash(&mut h);
|
||||
(analysis.len(), h.finish(), text(key))
|
||||
};
|
||||
[get(WARP, ANALYSIS, ANALYSIS_KEY), get(TRACKER, TRACKS, TRACKS_KEY)]
|
||||
}
|
||||
|
||||
fn open(json: &str) -> Session {
|
||||
let mut s = Session::default();
|
||||
s.replace_project(Project::from_json(json).unwrap(), None);
|
||||
s.open_comp(COMP);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyses_saved_by_0_22_survive_edits_that_keep_their_frames() {
|
||||
// Saved by the released 0.22.0 CLI after analysing both effects; its footage is offline here.
|
||||
let mut s = open(include_str!("fixtures/tracked-0.22.ecproj"));
|
||||
let before = saved(&s);
|
||||
let layer = s.project.comp(COMP).unwrap().layer(CLIP).unwrap();
|
||||
for (len, _, key) in &before {
|
||||
assert!(*len > 0);
|
||||
// The key predates Footage::placed_page, so it isn't today's signature.
|
||||
assert_ne!(key, &crate::warp::signature(&s.project, COMP, layer));
|
||||
}
|
||||
|
||||
// Adding to the render queue edits the project, as `effectcraft-cli render` does.
|
||||
s.execute_checked("renderQueue.add", json!({"comp": COMP.0, "format": "png", "output": "frame.png"})).unwrap();
|
||||
assert_eq!(saved(&s), before);
|
||||
s.execute_checked("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(saved(&s), before);
|
||||
|
||||
// Saved and reopened (by this version), the old keys are kept and still match.
|
||||
let mut s = open(&s.project.to_json());
|
||||
s.execute_checked("renderQueue.add", json!({"comp": COMP.0, "format": "png", "output": "frame.png"})).unwrap();
|
||||
assert_eq!(saved(&s), before);
|
||||
|
||||
// Other frames, from other timing or a reinterpreted footage item: both analyses are
|
||||
// cleared, and undo brings them back.
|
||||
let cleared = |s: &Session| saved(s).iter().all(|(len, _, key)| *len == 0 && key.is_empty());
|
||||
s.execute_checked("layer.timing", json!({"layer": CLIP.0, "out": 0.5})).unwrap();
|
||||
assert!(cleared(&s));
|
||||
s.execute_checked("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(saved(&s), before);
|
||||
s.execute_checked("file.interpretFootage", json!({"items": [FOOTAGE.0], "invertAlpha": true})).unwrap();
|
||||
assert!(cleared(&s));
|
||||
s.execute_checked("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(saved(&s), before);
|
||||
}
|
||||
|
||||
/// The footage text analysis keys are hashed from, as 0.23 saved it. A new `Footage` field must
|
||||
/// not change it (or every saved analysis is dropped again).
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user