mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 12:37:08 -06:00
Merge main into M13.5 long-tail polish
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -146,7 +146,15 @@ fn card(p: &mut Project, comp: &Comp, folder: ItemId, name: &str, a: &str, b: &s
|
||||
&mut l,
|
||||
"ec.generate.grid",
|
||||
[w, h],
|
||||
&[("width", Value::Scalar(w)), ("height", Value::Scalar(h)), ("border", Value::Scalar(10.0)), ("opacity", Value::Scalar(35.0))],
|
||||
&[
|
||||
("sizeFrom", Value::Enum(2)),
|
||||
("width", Value::Scalar(w)),
|
||||
("height", Value::Scalar(h)),
|
||||
("border", Value::Scalar(10.0)),
|
||||
("opacity", Value::Scalar(35.0)),
|
||||
// Normal: the grid over the gradient.
|
||||
("blendingMode", Value::Enum(1)),
|
||||
],
|
||||
);
|
||||
l.switches.three_d = true;
|
||||
set(&mut l, "transform/position", Value::Vec3(pos));
|
||||
@@ -174,6 +182,7 @@ fn showcase_3d(p: &mut Project, solids: ItemId) -> Comp {
|
||||
("border", Value::Scalar(3.0)),
|
||||
("color", Value::Color(hex("#6F7BB8"))),
|
||||
("opacity", Value::Scalar(45.0)),
|
||||
("blendingMode", Value::Enum(1)),
|
||||
],
|
||||
);
|
||||
floor.switches.three_d = true;
|
||||
|
||||
@@ -635,6 +635,7 @@ impl Session {
|
||||
self.roto_job = None;
|
||||
self.roto_pending.clear();
|
||||
p.fix_next_id();
|
||||
upgrade_effects(&mut p);
|
||||
self.project = Arc::new(p);
|
||||
self.history = History::default();
|
||||
self.state = EditorState { snapping: true, view_layout: 1, ..Default::default() };
|
||||
@@ -648,6 +649,35 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring effect instances saved by earlier versions up to date with the effect registry
|
||||
/// (renamed / regrouped / added parameters, reordered popups; see
|
||||
/// [`effectcraft_effects::migrate`]).
|
||||
pub fn upgrade_effects(p: &mut Project) {
|
||||
let mut sizes = std::collections::HashMap::new();
|
||||
for (cid, c) in p.comps() {
|
||||
for l in &c.layers {
|
||||
let (w, h) = effectcraft_render::source_size(p, l);
|
||||
sizes.insert((*cid, l.id), if w == 0 { [c.width as f64, c.height as f64] } else { [w as f64, h as f64] });
|
||||
}
|
||||
}
|
||||
let mut next = p.next_id;
|
||||
for (cid, it) in p.items.iter_mut() {
|
||||
let effectcraft_project::ItemKind::Comp(c) = &mut it.kind else { continue };
|
||||
for l in &mut std::sync::Arc::make_mut(c).layers {
|
||||
let size = sizes.get(&(*cid, l.id)).copied().unwrap_or([100.0, 100.0]);
|
||||
let Some(fx) = l.props.sub_mut("effects") else { continue };
|
||||
for n in &mut fx.children {
|
||||
let effectcraft_project::Node::Group(g) = n else { continue };
|
||||
let effectcraft_project::GroupKind::Effect { effect } = &g.kind else { continue };
|
||||
if let Some(spec) = effectcraft_effects::find(effect) {
|
||||
effectcraft_effects::migrate::upgrade_instance(spec, g, &mut effectcraft_project::build::Ids(&mut next), size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p.next_id = next;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod rq_tests;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -352,7 +352,8 @@ fn add_effect(proj: &mut Project, l: &mut Layer, effect: &str, comp: &Comp, para
|
||||
let mut ids = Ids(&mut proj.next_id);
|
||||
let mut g = effectcraft_effects::instantiate(spec, &mut ids, spec.name, [comp.width as f64, comp.height as f64]);
|
||||
for (k, v) in params {
|
||||
if let Some(p) = g.get_mut(k) {
|
||||
// Paths reach parameters in twirl-down groups (`master/exposure`).
|
||||
if let Some(p) = g.prop_mut(k) {
|
||||
p.value = v.clone();
|
||||
}
|
||||
}
|
||||
@@ -364,9 +365,13 @@ fn add_effect(proj: &mut Project, l: &mut Layer, effect: &str, comp: &Comp, para
|
||||
fn add_adjustment_effect(proj: &mut Project, l: &mut Layer, adj: &Adjustment, comp: &Comp, warnings: &mut Vec<String>, name: &str) {
|
||||
match adj {
|
||||
Adjustment::Invert => add_effect(proj, l, "Invert", comp, &[]),
|
||||
Adjustment::BrightnessContrast { brightness, contrast, .. } => {
|
||||
add_effect(proj, l, "Brightness & Contrast", comp, &[("brightness", Value::Scalar(*brightness)), ("contrast", Value::Scalar(*contrast))])
|
||||
}
|
||||
Adjustment::BrightnessContrast { brightness, contrast, legacy } => add_effect(
|
||||
proj,
|
||||
l,
|
||||
"Brightness & Contrast",
|
||||
comp,
|
||||
&[("brightness", Value::Scalar(*brightness)), ("contrast", Value::Scalar(*contrast)), ("useLegacy", Value::Bool(*legacy))],
|
||||
),
|
||||
Adjustment::HueSaturation { hue, saturation, lightness, colorize } => {
|
||||
let mut p = vec![("hue", Value::Scalar(*hue)), ("saturation", Value::Scalar(*saturation)), ("lightness", Value::Scalar(*lightness))];
|
||||
if *colorize {
|
||||
@@ -397,7 +402,7 @@ fn add_adjustment_effect(proj: &mut Project, l: &mut Layer, adj: &Adjustment, co
|
||||
l,
|
||||
"Exposure",
|
||||
comp,
|
||||
&[("exposure", Value::Scalar(*exposure)), ("offset", Value::Scalar(*offset)), ("gamma", Value::Scalar(*gamma))],
|
||||
&[("master/exposure", Value::Scalar(*exposure)), ("master/offset", Value::Scalar(*offset)), ("master/gamma", Value::Scalar(*gamma))],
|
||||
),
|
||||
Adjustment::Other(k) => warnings.push(format!("{name}: adjustment `{k}` is not converted (layer kept as an empty adjustment layer)")),
|
||||
}
|
||||
|
||||
@@ -152,3 +152,108 @@ fn layer_params_get_a_source_companion() {
|
||||
assert!(matches!(src.ui, effectcraft_project::ParamUi::Hidden));
|
||||
}
|
||||
}
|
||||
|
||||
/// Projects saved before parameters were renamed, regrouped, added or had their popups
|
||||
/// reordered open with their instances brought up to date (`effectcraft_effects::migrate`).
|
||||
#[test]
|
||||
fn opening_an_old_project_upgrades_effect_instances() {
|
||||
use effectcraft_effects::migrate::PARAM_ID_ALIASES;
|
||||
use effectcraft_project::{Node, ParamUi, PropGroup};
|
||||
let (mut s, a, _) = comp_with_two_solids();
|
||||
apply(&mut s, a, "ec.blur.gaussian");
|
||||
// One instance of every effect with renamed parameter ids.
|
||||
let mut aliased: Vec<&str> = PARAM_ID_ALIASES.iter().map(|(e, _, _)| *e).collect();
|
||||
aliased.dedup();
|
||||
for e in &aliased {
|
||||
apply(&mut s, a, e);
|
||||
}
|
||||
// Rewrite the saved instances the way an older version wrote them.
|
||||
fn take(g: &mut PropGroup, path: &str) -> Option<effectcraft_project::Property> {
|
||||
match path.split_once('/') {
|
||||
None => {
|
||||
let i = g.children.iter().position(|c| matches!(c, Node::Prop(p) if p.match_id == path))?;
|
||||
match g.children.remove(i) {
|
||||
Node::Prop(p) => Some(p),
|
||||
Node::Group(_) => None,
|
||||
}
|
||||
}
|
||||
Some((first, rest)) => take(g.sub_mut(first)?, rest),
|
||||
}
|
||||
}
|
||||
let mut old = (*s.project).clone();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
{
|
||||
let comp = std::sync::Arc::make_mut(match &mut old.items.get_mut(&cid).unwrap().kind {
|
||||
effectcraft_project::ItemKind::Comp(c) => c,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
let l = comp.layers.iter_mut().find(|l| l.id == LayerId(a)).unwrap();
|
||||
for n in &mut l.props.sub_mut("effects").unwrap().children {
|
||||
let Node::Group(g) = n else { continue };
|
||||
if g.match_id == "ec.blur.gaussian" {
|
||||
// Repeat Edge Pixels did not exist yet, Blurriness had another name, the popup
|
||||
// listed its options in reverse.
|
||||
take(g, "repeatEdge").unwrap();
|
||||
g.get_mut("blurriness").unwrap().name = "Old Blurriness".into();
|
||||
let d = g.get_mut("dimensions").unwrap();
|
||||
let ParamUi::Popup { options } = &d.ui else { panic!("dimensions is a popup") };
|
||||
d.ui = ParamUi::Popup { options: options.iter().rev().cloned().collect() };
|
||||
d.value = KV::Enum(0);
|
||||
}
|
||||
for (e, old_id, new_id) in PARAM_ID_ALIASES {
|
||||
if g.match_id == *e {
|
||||
let mut p = take(g, new_id).unwrap_or_else(|| panic!("{e}: no {new_id}"));
|
||||
// Put it back at its old path (which may be inside a group).
|
||||
let (path, leaf) = old_id.rsplit_once('/').map(|(p, l)| (Some(p), l)).unwrap_or((None, old_id));
|
||||
p.match_id = leaf.to_string();
|
||||
let mut at: &mut PropGroup = g;
|
||||
for seg in path.into_iter().flat_map(|p| p.split('/')) {
|
||||
if at.sub(seg).is_none() {
|
||||
at.children.push(PropGroup::new(1_000_000 + at.children.len() as u64, seg, seg).into());
|
||||
}
|
||||
at = at.sub_mut(seg).unwrap();
|
||||
}
|
||||
at.children.push(p.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("ec-upgrade-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("old.ecproj").to_string_lossy().to_string();
|
||||
std::fs::write(&path, old.to_json()).unwrap();
|
||||
let mut s2 = Session::default();
|
||||
s2.execute("file.open", json!({"path": path})).unwrap();
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
let l = s2.project.comp(cid).unwrap().layer(LayerId(a)).unwrap().clone();
|
||||
let fx = l.effects().unwrap();
|
||||
let spec = effectcraft_effects::find("ec.blur.gaussian").unwrap();
|
||||
let sp = |id: &str| spec.params.iter().find(|p| p.id == id).unwrap();
|
||||
let blur = fx.groups().find(|g| g.match_id == "ec.blur.gaussian").unwrap();
|
||||
assert_eq!(blur.get("blurriness").unwrap().name, sp("blurriness").name);
|
||||
assert!(blur.get("repeatEdge").is_some(), "missing parameter added");
|
||||
let dims = blur.get("dimensions").unwrap();
|
||||
let ParamUi::Popup { options } = &sp("dimensions").ui else { panic!() };
|
||||
assert_eq!(dims.value, KV::Enum(options.len() as u32 - 1), "popup value remapped by label");
|
||||
assert_eq!(dims.ui, sp("dimensions").ui);
|
||||
for (e, old_id, new_id) in PARAM_ID_ALIASES {
|
||||
let g = fx.groups().find(|g| g.match_id == *e).unwrap();
|
||||
let at = |id: &str| {
|
||||
let (path, leaf) = id.rsplit_once('/').map(|(p, l)| (Some(p), l)).unwrap_or((None, id));
|
||||
let mut node = Some(g);
|
||||
for seg in path.into_iter().flat_map(|p| p.split('/')) {
|
||||
node = node.and_then(|n| n.sub(seg));
|
||||
}
|
||||
node.and_then(|n| n.get(leaf)).is_some()
|
||||
};
|
||||
assert!(!at(old_id), "{e}: {old_id} still present");
|
||||
let (path, leaf) = new_id.rsplit_once('/').map(|(p, l)| (Some(p), l)).unwrap_or((None, new_id));
|
||||
let mut node = Some(g);
|
||||
for seg in path.into_iter().flat_map(|p| p.split('/')) {
|
||||
node = node.and_then(|n| n.sub(seg));
|
||||
}
|
||||
assert!(node.and_then(|n| n.get(leaf)).is_some(), "{e}: {old_id} not moved to {new_id}");
|
||||
}
|
||||
// The upgraded project renders.
|
||||
let _ = s2.render(cid, s2.time(), Default::default());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user