diff --git a/crates/aep/src/import/placeholder.rs b/crates/aep/src/import/placeholder.rs index 59b0865ac..a12ffa6c5 100644 --- a/crates/aep/src/import/placeholder.rs +++ b/crates/aep/src/import/placeholder.rs @@ -40,7 +40,9 @@ pub(crate) fn placeholder( refs: &mut Vec, col: &mut Collector, ) -> PropGroup { - let mut group = ids.group(fx.match_name(), shown); + // The effect id is the match name; the group's match id is one segment of a property path + // (a pseudo effect's `Pseudo/0.25` would be cut apart), so match paths find its parameters. + let mut group = ids.group(&segment(fx.match_name()), shown); group.kind = GroupKind::Effect { effect: fx.match_name().to_string() }; let here = format!("{}: '{}' effect '{shown}' ({})", cx.comp, cx.layer, fx.match_name()); for p in params.iter().filter(|p| !carries_nothing(p)) { @@ -126,8 +128,18 @@ fn popup_len(p: &EffectParam<'_, '_>) -> Option { /// third-party effect's `CC Cylinder-0003`), else `param` (a pseudo effect's /// `Pseudo/0.25-0001`: `/` and `.` separate path segments). Expressions also find it by its name. fn param_id(p: &EffectParam<'_, '_>) -> String { - let plain = !p.match_name.is_empty() && p.match_name.chars().all(|c| c.is_alphanumeric() || matches!(c, ' ' | '-' | '_')); - if plain { p.match_name.clone() } else { format!("param{}", p.index) } + if !p.match_name.is_empty() && p.match_name.chars().all(plain) { p.match_name.clone() } else { format!("param{}", p.index) } +} + +/// Whether `c` can stand in one segment of a property path (`/`, `.`, `#` and `@` mean more). +fn plain(c: char) -> bool { + c.is_alphanumeric() || matches!(c, ' ' | '-' | '_') +} + +/// `name` as one segment of a property path: other characters become `_` (`Pseudo/0.25` is +/// `Pseudo_0_25`). +fn segment(name: &str) -> String { + name.chars().map(|c| if plain(c) { c } else { '_' }).collect() } /// Sets `prop` from `p`: its expression, then its keys or its value (the record, else the `pard` diff --git a/crates/aep/tests/it/import/fx_inject.rs b/crates/aep/tests/it/import/fx_inject.rs index ae61794d3..535e82483 100644 --- a/crates/aep/tests/it/import/fx_inject.rs +++ b/crates/aep/tests/it/import/fx_inject.rs @@ -10,7 +10,7 @@ use effectcraft_aep::import::{ImportOptions, ImportResult, LossCategory, import}; use effectcraft_aep::{Body, Chunk, ParamKind, ParamValue, Project as AepProject, parse, write}; use effectcraft_keyframe::Value; -use effectcraft_project::{Project, PropGroup}; +use effectcraft_project::{GroupKind, Project, PropGroup}; const SYNTHETIC: &[u8] = include_bytes!("../../fixtures/synthetic.aep"); @@ -274,10 +274,15 @@ pub fn import_bytes(bytes: &[u8], effect: &str) -> (Option, ImportRes let mut project = Project::default(); let none = |_: &str| false; let r = import(&mut project, bytes, &ImportOptions { allow_losses: true, ..ImportOptions::new(&none) }).unwrap(); - let g = project - .comps() - .flat_map(|(_, c)| c.layers.iter()) - .find_map(|l| l.props.sub("effects").and_then(|fx| fx.children.iter().filter_map(|n| n.as_group()).find(|g| g.match_id == effect).cloned())); + let g = project.comps().flat_map(|(_, c)| c.layers.iter()).find_map(|l| { + l.props.sub("effects").and_then(|fx| { + fx.children + .iter() + .filter_map(|n| n.as_group()) + .find(|g| g.match_id == effect || matches!(&g.kind, GroupKind::Effect { effect: e } if e == effect)) + .cloned() + }) + }); (g, r) } diff --git a/crates/aep/tests/it/import/placeholders.rs b/crates/aep/tests/it/import/placeholders.rs index afa323065..679d508af 100644 --- a/crates/aep/tests/it/import/placeholders.rs +++ b/crates/aep/tests/it/import/placeholders.rs @@ -100,6 +100,13 @@ fn a_pseudo_effect_is_kept_with_its_parameter_names() { assert_eq!(g.prop("param1").map(|p| (p.name.as_str(), p.value.clone())), Some(("Speed", Value::Scalar(4.0)))); assert_eq!(g.prop("param2").map(|p| (p.name.as_str(), p.value.clone())), Some(("Mode", Value::Enum(1)))); assert_eq!(losses(&r, LossCategory::Effects, "Pseudo/0.25").len(), 1); + // The effect id stays the match name (Find Missing Effects, expressions); the group's match id + // is one path segment, so a match path (keyframe paste, pick whip) finds its parameters again. + assert_eq!(g.kind, GroupKind::Effect { effect: "Pseudo/0.25".into() }); + let speed = g.prop("param1").map(|p| p.uid); + let layer = PropGroup::new(0, "layer", "Layer").with(PropGroup::new(0, "effects", "Effects").with(g)); + let path = speed.and_then(|u| layer.match_path_of(u)); + assert_eq!(path.as_deref().and_then(|p| layer.prop(p)).map(|p| p.name.as_str()), Some("Speed"), "{path:?}"); } /// A popup whose definition is borrowed (the project copy) and has no value record: its default