mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 21:48:20 -06:00
Merge main into #1739; keep both footage re-exports (Cineon + VIDEO_EXTENSIONS) and main's gaps.md localization row with the PR's AVI row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -155,12 +155,7 @@ fn unsupported(project: &Project, comp: &Comp, l: &Layer, format: TimelineFormat
|
||||
return Some(format!("blend mode {}", l.blend_mode.label()));
|
||||
}
|
||||
// Premiere's Basic Motion in the interchange formats has one (uniform) scale.
|
||||
if let Some(s) = l.transform().and_then(|t| t.get("scale"))
|
||||
&& std::iter::once(&s.value).chain(s.keys.iter().map(|k| &k.value)).any(|v| {
|
||||
let a = v.as_vec3();
|
||||
(a[0] - a[1]).abs() > 1e-9
|
||||
})
|
||||
{
|
||||
if l.transform().and_then(|t| t.get("scale")).is_some_and(non_uniform_scale) {
|
||||
return Some("non-uniform scale".into());
|
||||
}
|
||||
if l.preserve_transparency {
|
||||
@@ -581,10 +576,7 @@ impl Exporter<'_> {
|
||||
}
|
||||
}
|
||||
if let Some(p) = tr.get("scale") {
|
||||
let uniform = std::iter::once(&p.value).chain(p.keys.iter().map(|k| &k.value)).all(|v| {
|
||||
let a = v.as_vec3();
|
||||
(a[0] - a[1]).abs() < 1e-9
|
||||
});
|
||||
let uniform = !non_uniform_scale(p);
|
||||
m.params.insert("scale".into(), param_out(p, &to_clip, |v| fp::ParamValue::Float(v.as_vec3()[1])));
|
||||
if !uniform {
|
||||
m.params.insert("scale_width".into(), param_out(p, &to_clip, |v| fp::ParamValue::Float(v.as_vec3()[0])));
|
||||
@@ -636,6 +628,16 @@ fn differs(v: &Value, def: [f64; 2]) -> bool {
|
||||
(a[0] - def[0]).abs() > 1e-6 || (a[1] - def[1]).abs() > 1e-6
|
||||
}
|
||||
|
||||
/// Whether a scale property is non-uniform at some point: active key values, or the static
|
||||
/// value when every key is muted (muted keys are not exported, #1894).
|
||||
fn non_uniform_scale(p: &Property) -> bool {
|
||||
let non_uniform = |v: &Value| {
|
||||
let a = v.as_vec3();
|
||||
(a[0] - a[1]).abs() > 1e-9
|
||||
};
|
||||
if p.keys.iter().any(|k| !k.muted) { p.keys.iter().filter(|k| !k.muted).any(|k| non_uniform(&k.value)) } else { non_uniform(&p.value) }
|
||||
}
|
||||
|
||||
/// The clip's `id` effect, or a new instance of it (None if the registry has no such effect).
|
||||
fn effect_or_new(ti: &fp::TrackItem, id: &str) -> Option<fp::EffectInstance> {
|
||||
ti.effect(id).cloned().or_else(|| fp::find_effect(id).map(|e| e.instance()))
|
||||
@@ -651,7 +653,9 @@ fn set_effect(ti: &mut fp::TrackItem, e: fp::EffectInstance) {
|
||||
/// A property → a FilmCraft parameter (layer-time keys → clip-time keys).
|
||||
fn param_out(p: &Property, to_clip: &impl Fn(Tick) -> filmcraft_time::Tick, conv: impl Fn(&Value) -> fp::ParamValue) -> fp::Param {
|
||||
let mut out = fp::Param::new(conv(&p.value));
|
||||
for k in &p.keys {
|
||||
// XML timelines have no mute state: leave muted keys out, as the Lottie exporter does, so
|
||||
// the exported animation matches what the renderer shows (#1894).
|
||||
for k in p.keys.iter().filter(|k| !k.muted) {
|
||||
let mut fk = fp::Keyframe::new(to_clip(k.time), conv(&k.value));
|
||||
fk.interp = match k.out_interp {
|
||||
Interp::Linear => fp::Interpolation::Linear,
|
||||
|
||||
@@ -415,7 +415,7 @@ fn ae_project() -> (Project, ItemId) {
|
||||
let mut ls = effectcraft_project::build::layer(&mut p, &c, "tone", LayerSource::Footage { item: s }, (0, 0), None);
|
||||
ls.out_point = t(3.0);
|
||||
ls.props.prop_mut("audio/levels").unwrap().value = Value::Vec2([-3.0, -3.0]);
|
||||
c.layers = vec![lt, lp, la, ls];
|
||||
c.layers = vec![lt, lp, la, ls].into();
|
||||
let cid = comp_item(&mut p, "Main", c);
|
||||
(p, cid)
|
||||
}
|
||||
@@ -484,6 +484,59 @@ fn tr_orig(l: &Layer) -> &effectcraft_project::Property {
|
||||
l.transform().unwrap().get("position").unwrap()
|
||||
}
|
||||
|
||||
/// #1894: XML timelines have no mute state, so a muted key must not be written as a keyframe
|
||||
/// (the export would otherwise animate through it).
|
||||
#[test]
|
||||
fn export_skips_muted_keyframes() {
|
||||
let mut p = Project::default();
|
||||
let a = footage_item(&mut p, "/media/a.mov");
|
||||
let mut c = Comp::new(W, H, FrameRate::new(25, 1), t(4.0));
|
||||
let mut l = effectcraft_project::build::layer(&mut p, &c, "clip", LayerSource::Footage { item: a }, (W, H), None);
|
||||
{
|
||||
let pos = l.transform_mut().unwrap().get_mut("position").unwrap();
|
||||
pos.keys = vec![
|
||||
Keyframe::new(t(0.0), Value::Vec3([100.0, 120.0, 0.0])),
|
||||
Keyframe::new(t(1.0), Value::Vec3([500.0, 240.0, 0.0])),
|
||||
Keyframe::new(t(2.0), Value::Vec3([300.0, 60.0, 0.0])),
|
||||
];
|
||||
pos.keys[1].muted = true;
|
||||
}
|
||||
c.layers.push(l);
|
||||
let cid = comp_item(&mut p, "Main", c);
|
||||
let out = export(&p, cid, &ExportOptions { format: TimelineFormat::Fcp7Xml, ..Default::default() }, &HashMap::new()).unwrap();
|
||||
let (q, r) = import_doc(&out.bytes, "Main.xml", &mut probe);
|
||||
let main = r.comps.iter().copied().find(|c| q.item(*c).unwrap().name == "Main").expect("Main");
|
||||
let pos = layer(q.comp(main).unwrap(), "clip").transform().unwrap().get("position").unwrap();
|
||||
assert_eq!(pos.keys.len(), 2, "the muted key was exported");
|
||||
assert_eq!(pos.keys[0].value.as_vec3()[0], 100.0);
|
||||
assert_eq!(pos.keys[1].value.as_vec3()[0], 300.0);
|
||||
}
|
||||
|
||||
/// #1894: a muted non-uniform scale key must not force a uniform layer to pre-render; only the
|
||||
/// active keys matter, and the static value when every key is muted.
|
||||
#[test]
|
||||
fn muted_non_uniform_scale_does_not_force_prerendering() {
|
||||
let mut p = Project::default();
|
||||
let a = footage_item(&mut p, "/media/a.mov");
|
||||
let mut c = Comp::new(W, H, FrameRate::new(25, 1), t(2.0));
|
||||
let mut l = effectcraft_project::build::layer(&mut p, &c, "clip", LayerSource::Footage { item: a }, (W, H), None);
|
||||
{
|
||||
let scale = l.transform_mut().unwrap().get_mut("scale").unwrap();
|
||||
scale.keys = vec![Keyframe::new(t(0.0), Value::Vec3([100.0, 100.0, 100.0])), Keyframe::new(t(1.0), Value::Vec3([50.0, 200.0, 100.0]))];
|
||||
scale.keys[1].muted = true;
|
||||
}
|
||||
let lid = l.id;
|
||||
c.layers.push(l);
|
||||
let cid = comp_item(&mut p, "Main", c);
|
||||
assert_eq!(prerender_reason(&p, cid, lid, &ExportOptions::default()), None);
|
||||
// With every key muted the static value drives evaluation, so a non-uniform static scale
|
||||
// still takes the flattened path.
|
||||
let scale = p.comp_mut(cid).unwrap().layer_mut(lid).unwrap().transform_mut().unwrap().get_mut("scale").unwrap();
|
||||
scale.keys[0].muted = true;
|
||||
scale.value = Value::Vec3([50.0, 200.0, 100.0]);
|
||||
assert_eq!(prerender_reason(&p, cid, lid, &ExportOptions::default()).as_deref(), Some("non-uniform scale"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_without_prerender_leaves_text_out() {
|
||||
let (p, cid) = ae_project();
|
||||
|
||||
Reference in New Issue
Block a user