mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-12 21:32:52 +00:00
#3456: write the footage in analysis signatures field by field
The Warp Stabilizer, 3D Camera Tracker and Roto Brush signatures hashed the footage item's derived Debug text, so every new Footage field changed every signature and dropped every saved analysis (placed_page did in 0.23). Write the same text from a named list of fields instead: a new field no longer compiles until it is decided there. Pin the text in a test.
This commit is contained in:
3 files changed
+115
-5
No files matched your search
@@ -17,7 +17,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use effectcraft_effects::warp_stab::{self, ANALYSIS, ANALYSIS_KEY};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::{GroupKind, ItemId, ItemKind, Layer, LayerId, LayerSource, Project, PropGroup, Uid};
|
||||
use effectcraft_project::{Footage, GroupKind, ItemId, ItemKind, Layer, LayerId, LayerSource, Project, PropGroup, Uid};
|
||||
use effectcraft_raster::Image;
|
||||
use effectcraft_render::{EvalCtx, ExprHost, FootageSource, LayerCache, Renderer};
|
||||
use effectcraft_time::Tick;
|
||||
@@ -112,7 +112,7 @@ fn fnv(s: &str) -> u64 {
|
||||
pub fn signature(p: &Project, comp: ItemId, layer: &Layer) -> String {
|
||||
let src = match &layer.source {
|
||||
LayerSource::Footage { item } | LayerSource::Solid { item } | LayerSource::Comp { item } => match p.item(*item).map(|i| &i.kind) {
|
||||
Some(ItemKind::Footage(f)) => format!("{f:?}"),
|
||||
Some(ItemKind::Footage(f)) => footage_text(f, true),
|
||||
Some(ItemKind::Solid(s)) => format!("{s:?}"),
|
||||
Some(ItemKind::Comp(c)) => format!("{}x{}@{:?}/{:?}", c.width, c.height, c.frame_rate, c.duration),
|
||||
_ => String::new(),
|
||||
@@ -127,9 +127,96 @@ pub fn signature(p: &Project, comp: ItemId, layer: &Layer) -> String {
|
||||
pub(crate) 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 };
|
||||
let src = format!("{footage:?}");
|
||||
let old = src.strip_suffix(", placed_page: false }")?;
|
||||
Some(signature_with_source(p, comp, layer, &format!("{old} }}")))
|
||||
if footage.placed_page {
|
||||
return None;
|
||||
}
|
||||
Some(signature_with_source(p, comp, layer, &footage_text(footage, false)))
|
||||
}
|
||||
|
||||
/// A footage item in a signature: its fields as `Debug` wrote them in 0.23, the text saved
|
||||
/// analysis keys were hashed from (without `placed_page` before 0.23). It is written out here so
|
||||
/// that a new `Footage` field doesn't change every signature and drop every saved analysis.
|
||||
pub(crate) fn footage_text(f: &Footage, placed_page: bool) -> String {
|
||||
format!("{:?}", FootageText { f, placed_page })
|
||||
}
|
||||
|
||||
struct FootageText<'a> {
|
||||
f: &'a Footage,
|
||||
placed_page: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FootageText<'_> {
|
||||
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// Every field is named, so a new one doesn't compile until it is decided here. A new field
|
||||
// that changes the frames goes last, written only when it isn't its default, so keys saved
|
||||
// before it still match. A new field that doesn't change the frames stays out.
|
||||
let Footage {
|
||||
path,
|
||||
kind,
|
||||
width,
|
||||
height,
|
||||
pixel_aspect,
|
||||
frame_rate,
|
||||
native_rate,
|
||||
duration,
|
||||
has_video,
|
||||
has_audio,
|
||||
alpha,
|
||||
premul_color,
|
||||
loop_count,
|
||||
codec,
|
||||
missing,
|
||||
sequence,
|
||||
color_profile,
|
||||
layer,
|
||||
fields,
|
||||
invert_alpha,
|
||||
linear_light,
|
||||
preserve_rgb,
|
||||
cineon,
|
||||
data,
|
||||
page,
|
||||
alphabetical,
|
||||
start_timecode,
|
||||
file_timecode,
|
||||
markers,
|
||||
placed_page,
|
||||
} = self.f;
|
||||
let mut d = out.debug_struct("Footage");
|
||||
d.field("path", path)
|
||||
.field("kind", kind)
|
||||
.field("width", width)
|
||||
.field("height", height)
|
||||
.field("pixel_aspect", pixel_aspect)
|
||||
.field("frame_rate", frame_rate)
|
||||
.field("native_rate", native_rate)
|
||||
.field("duration", duration)
|
||||
.field("has_video", has_video)
|
||||
.field("has_audio", has_audio)
|
||||
.field("alpha", alpha)
|
||||
.field("premul_color", premul_color)
|
||||
.field("loop_count", loop_count)
|
||||
.field("codec", codec)
|
||||
.field("missing", missing)
|
||||
.field("sequence", sequence)
|
||||
.field("color_profile", color_profile)
|
||||
.field("layer", layer)
|
||||
.field("fields", fields)
|
||||
.field("invert_alpha", invert_alpha)
|
||||
.field("linear_light", linear_light)
|
||||
.field("preserve_rgb", preserve_rgb)
|
||||
.field("cineon", cineon)
|
||||
.field("data", data)
|
||||
.field("page", page)
|
||||
.field("alphabetical", alphabetical)
|
||||
.field("start_timecode", start_timecode)
|
||||
.field("file_timecode", file_timecode)
|
||||
.field("markers", markers);
|
||||
if self.placed_page {
|
||||
d.field("placed_page", placed_page);
|
||||
}
|
||||
d.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn signature_with_source(p: &Project, comp: ItemId, layer: &Layer, src: &str) -> String {
|
||||
|
||||
@@ -27,6 +27,7 @@ mod key_metadata_order;
|
||||
mod key_time;
|
||||
mod keyframe_text;
|
||||
mod keylight;
|
||||
mod legacy_analysis_keys;
|
||||
mod limits;
|
||||
mod lottie;
|
||||
mod m137;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
//! 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.
|
||||
|
||||
/// 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]
|
||||
fn footage_signature_text_is_pinned() {
|
||||
use effectcraft_project::{Footage, FootageKind};
|
||||
let f = Footage {
|
||||
path: "clip.mp4".into(),
|
||||
kind: FootageKind::Video,
|
||||
width: 160,
|
||||
height: 120,
|
||||
invert_alpha: true,
|
||||
page: 2,
|
||||
placed_page: true,
|
||||
..Footage::default()
|
||||
};
|
||||
let fields = r#"path: "clip.mp4", kind: Video, width: 160, height: 120, pixel_aspect: 1.0, frame_rate: FrameRate { num: 30, den: 1 }, native_rate: None, duration: Tick(0), has_video: false, has_audio: false, alpha: Straight, premul_color: [0.0, 0.0, 0.0], loop_count: 1, codec: "", missing: false, sequence: [], color_profile: None, layer: None, fields: Off, invert_alpha: true, linear_light: false, preserve_rgb: false, cineon: CineonSettings { log_conversion: false, converted_black: 0.0, converted_white: 1023.0, black: 95.0, white: 685.0, gamma: 1.7, rolloff: 20.0, units: TenBit }, data: None, page: 2, alphabetical: false, start_timecode: None, file_timecode: None, markers: []"#;
|
||||
assert_eq!(crate::warp::footage_text(&f, true), format!("Footage {{ {fields}, placed_page: true }}"));
|
||||
assert_eq!(crate::warp::footage_text(&f, false), format!("Footage {{ {fields} }}"));
|
||||
}
|
||||
Reference in new issue
Block a user