mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-12 21:32:52 +00:00
Merge remote-tracking branch 'upstream/main' into fix/3087-aep-effect-placeholders
# Conflicts: # crates/expr/src/host.rs
This commit is contained in:
10 files changed
+472
-12
No files matched your search
+49
-10
@@ -9,6 +9,7 @@
|
||||
|
||||
use effectcraft_geom::{Mat3, Mat4};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ae_order::{self, AeChild};
|
||||
use effectcraft_project::{Comp, GroupKind, ItemId, ItemKind, Layer, LayerId, LayerSource, Node, PropGroup, Property};
|
||||
use effectcraft_render::EvalCtx;
|
||||
use effectcraft_time::Tick;
|
||||
@@ -272,6 +273,19 @@ fn node_resp(path: String, n: &Node, index: usize) -> Resp {
|
||||
])
|
||||
}
|
||||
|
||||
/// A top-level slot the layer has no node for, as group data: `@marker` (the markers, built by the
|
||||
/// prelude) or `@absent` (an empty group, as After Effects shows an unused slot).
|
||||
fn slot_resp(path: &str, s: &ae_order::AeSlot, index: usize) -> Resp {
|
||||
Resp::List(vec![
|
||||
Resp::Str(path.into()),
|
||||
Resp::Bool(true),
|
||||
Resp::Str(s.name.into()),
|
||||
Resp::Str(s.match_name.into()),
|
||||
Resp::Num(0.0),
|
||||
Resp::Num(index as f64),
|
||||
])
|
||||
}
|
||||
|
||||
fn find_child<'g>(g: &'g PropGroup, key: &Key) -> Option<&'g Node> {
|
||||
match key {
|
||||
Key::Index(i) => usize::try_from(*i).ok().filter(|i| *i >= 1).and_then(|i| g.children.get(i - 1)),
|
||||
@@ -351,7 +365,36 @@ impl<'a> Resolver<'a> {
|
||||
let uid = segs.get(keep - 1)?.strip_prefix('@')?.parse::<u64>().ok()?;
|
||||
let index = parent.children.iter().position(|c| c.uid() == uid)?;
|
||||
let n = parent.children.get(index)?;
|
||||
Some(node_resp(group_path, n, index + 1))
|
||||
// A top-level group's number is After Effects' (see `ae_order`).
|
||||
let number = if keep == 1 { ae_order::ae_index(l, n.match_id()) } else { None }.unwrap_or(index + 1);
|
||||
Some(node_resp(group_path, n, number))
|
||||
}
|
||||
|
||||
/// A child of a layer (`group` empty) or of one of its groups, by name or 1-based number.
|
||||
fn child(&self, comp: u64, layer: u64, group: &str, key: &Key) -> Option<Resp> {
|
||||
let l = self.layer(comp, layer)?;
|
||||
// A layer's numbers are After Effects' (Marker is 1, unused groups keep their slot), and so
|
||||
// are its match names (`thisLayer("ADBE Effect Parade")`, as scripts such as Karton write).
|
||||
let slot = match key {
|
||||
Key::Index(i) => usize::try_from(*i).ok(),
|
||||
Key::Name(name) => ae_order::ae_index_of_match_name(l, name),
|
||||
};
|
||||
if group.is_empty()
|
||||
&& ae_order::ae_slots(&l.source).is_some()
|
||||
&& let Some(i) = slot
|
||||
{
|
||||
return Some(match ae_order::ae_child(l, i)? {
|
||||
AeChild::Node(n) => node_resp(format!("@{}", n.uid()), n, i),
|
||||
AeChild::Marker => slot_resp("@marker", &ae_order::MARKER, 1),
|
||||
AeChild::Absent(s) => slot_resp("@absent", s, i),
|
||||
});
|
||||
}
|
||||
let g = if group.is_empty() { &l.props } else { l.props.group(group)? };
|
||||
let n = find_child(g, key)?;
|
||||
let index = if group.is_empty() { ae_order::ae_index(l, n.match_id()) } else { None }
|
||||
.unwrap_or_else(|| g.children.iter().position(|c| c.uid() == n.uid()).unwrap_or(0) + 1);
|
||||
let path = if group.is_empty() { format!("@{}", n.uid()) } else { format!("{group}/@{}", n.uid()) };
|
||||
Some(node_resp(path, n, index))
|
||||
}
|
||||
|
||||
fn try_resolve(&self, req: &Req) -> Option<Resp> {
|
||||
@@ -406,14 +449,7 @@ impl<'a> Resolver<'a> {
|
||||
Resp::Bool(l.switches.video),
|
||||
])
|
||||
}
|
||||
Req::Child { comp, layer, group, key } => {
|
||||
let l = self.layer(*comp, *layer)?;
|
||||
let g = if group.is_empty() { &l.props } else { l.props.group(group)? };
|
||||
let n = find_child(g, key)?;
|
||||
let index = g.children.iter().position(|c| c.uid() == n.uid()).unwrap_or(0) + 1;
|
||||
let path = if group.is_empty() { format!("@{}", n.uid()) } else { format!("{group}/@{}", n.uid()) };
|
||||
node_resp(path, n, index)
|
||||
}
|
||||
Req::Child { comp, layer, group, key } => return self.child(*comp, *layer, group, key),
|
||||
Req::Ancestor { comp, layer, path, up } => return self.ancestor(*comp, *layer, path, *up),
|
||||
Req::PropInfo { comp, layer, path } => {
|
||||
let (l, p) = self.prop(*comp, *layer, path)?;
|
||||
@@ -426,7 +462,10 @@ impl<'a> Resolver<'a> {
|
||||
Some(g) => l.props.group(g)?,
|
||||
None => &l.props,
|
||||
};
|
||||
g.children.iter().position(|n| format!("@{}", n.uid()) == last).map(|i| i + 1)
|
||||
let i = g.children.iter().position(|n| format!("@{}", n.uid()) == last)?;
|
||||
// A top-level property (an enabled Time Remap) has After Effects' number.
|
||||
let top = if parent.is_none() { g.children.get(i).and_then(|n| ae_order::ae_index(l, n.match_id())) } else { None };
|
||||
Some(top.unwrap_or(i + 1))
|
||||
});
|
||||
Resp::List(vec![
|
||||
Resp::Str(p.name.clone()),
|
||||
|
||||
@@ -495,6 +495,8 @@ function __mod(a, d) { return ((a % d) + d) % d; }
|
||||
function __child(c, l, group, key) {
|
||||
var info = __h(__CHILD, c, l, group, key);
|
||||
if (info === null) return undefined;
|
||||
// `thisLayer(1)` is the layer's markers, as in After Effects (see `ae_order`).
|
||||
if (group === '' && info[0] === '@marker') return new Markers(c, l);
|
||||
return info[1] ? __mkGroup(c, l, info) : __mkProp(c, l, info[0]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2046,6 +2046,7 @@ fn an_expression_cant_change_the_prelude_for_the_next() {
|
||||
}
|
||||
}
|
||||
|
||||
mod ae_layer_order;
|
||||
mod property_group;
|
||||
|
||||
/// Timing harness for the prelude's cost on the evaluation path. Ignored: it is a measurement, not
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//! A layer's top-level numbers are After Effects' (`project::ae_order`): Marker is 1, and slots the
|
||||
//! layer does not use (Time Remap, Motion Trackers, Layer Styles…) keep their place. Rigs that read
|
||||
//! a palette by number, such as `comp("Palette").layer(1)(4)(n)(1)` on a shape layer, failed
|
||||
//! silently before: `(4)` reached Transform, the expression's `try` fell back to the stored value and
|
||||
//! every linked colour rendered near-black.
|
||||
|
||||
use super::*;
|
||||
use effectcraft_project::Property;
|
||||
|
||||
/// The fixture with a Slider Control (value 7) on layer "Shape".
|
||||
fn with_shape_slider() -> Fx {
|
||||
let mut f = fx();
|
||||
let spec = effectcraft_effects::find("ec.control.slider").unwrap();
|
||||
let mut slider = group_push(&mut f.p, |ids| effectcraft_effects::instantiate(spec, ids, "Swatch 1", [20.0, 20.0]));
|
||||
slider.prop_mut("slider").unwrap().value = Value::Scalar(7.0);
|
||||
f.layer_mut(f.shape).props.sub_mut("effects").unwrap().children.push(slider.into());
|
||||
f
|
||||
}
|
||||
|
||||
fn yes(f: &Fx, cond: &str) -> f64 {
|
||||
f.num(&format!("({cond}) ? 1 : 0"), 0.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effects_by_number_on_a_shape_layer_is_four() {
|
||||
let f = with_shape_slider();
|
||||
// The Ray Dynamic Color lookup: layer(1)(4)(swatch)(1).
|
||||
assert_close!(f.num("thisComp.layer('Shape')(4)(1)(1)", 0.0), 7.0);
|
||||
assert_close!(f.num("try { thisComp.layer('Shape')(4)(1)(1) } catch (e) { -1 }", 0.0), 7.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effects_by_number_on_a_solid_is_five() {
|
||||
let f = fx();
|
||||
assert_close!(f.num("thisComp.layer('A')(5)(1)(1)", 0.0), 42.0);
|
||||
assert_close!(f.num("thisComp.layer('A')(5)('Slider Control')('Slider')", 0.0), 42.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_names_follow_after_effects() {
|
||||
let f = fx();
|
||||
assert_close!(yes(&f, "thisComp.layer('A')(4).name == 'Masks'"), 1.0);
|
||||
assert_close!(yes(&f, "thisComp.layer('A')(5).name == 'Effects'"), 1.0);
|
||||
assert_close!(yes(&f, "thisComp.layer('A')(6).name == 'Transform'"), 1.0);
|
||||
assert_close!(yes(&f, "thisComp.layer('Shape')(2).name == 'Contents'"), 1.0);
|
||||
assert_close!(yes(&f, "thisComp.layer('Shape')(5).name == 'Transform'"), 1.0);
|
||||
assert_close!(yes(&f, "thisComp.layer('Title')(2).name == 'Text'"), 1.0);
|
||||
assert_close!(yes(&f, "thisComp.layer('Title')(4).name == 'Effects'"), 1.0);
|
||||
assert_close!(f.num("thisComp.layer('A')(6)('Position')[0]", 0.0), 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_is_the_markers() {
|
||||
let f = fx();
|
||||
assert_close!(f.num("thisComp.layer('A')(1).numKeys", 0.0), 2.0);
|
||||
assert_close!(f.num("thisComp.layer('A')(1).key(2).time", 0.0), 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unused_slot_is_an_empty_group() {
|
||||
let f = fx();
|
||||
// Layer Styles (7) and Data (12) exist in After Effects before they are used.
|
||||
assert_close!(yes(&f, "thisComp.layer('A')(7).name == 'Layer Styles'"), 1.0);
|
||||
assert_close!(f.num("thisComp.layer('A')(12).numProperties", 0.0), 0.0);
|
||||
assert_close!(f.num("try { thisComp.layer('A')(12)(1); 1 } catch (e) { -1 }", 0.0), -1.0);
|
||||
// Past the last slot there is nothing.
|
||||
assert_close!(f.num("try { thisComp.layer('A')(16); 1 } catch (e) { -1 }", 0.0), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_index_of_a_top_level_group_is_after_effects() {
|
||||
let f = fx();
|
||||
assert_close!(f.num("thisComp.layer('A').effect('Slider Control').propertyGroup(1).propertyIndex", 0.0), 5.0);
|
||||
assert_close!(f.num("thisComp.layer('A')('Transform').propertyIndex", 0.0), 6.0);
|
||||
assert_close!(f.num("thisComp.layer('Shape')('Contents').propertyIndex", 0.0), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_groups_by_after_effects_match_name() {
|
||||
// Karton's expressions: thisLayer("ADBE Effect Parade")("…").
|
||||
let f = with_shape_slider();
|
||||
assert_close!(f.num("thisComp.layer('A')('ADBE Effect Parade')('Slider Control')('Slider')", 0.0), 42.0);
|
||||
assert_close!(f.num("thisComp.layer('Shape')('ADBE Effect Parade')(1)(1)", 0.0), 7.0);
|
||||
assert_close!(f.num("thisComp.layer('A')('ADBE Transform Group')('Position')[0]", 0.0), 50.0);
|
||||
assert_close!(f.num("thisComp.layer('A')('ADBE Marker').numKeys", 0.0), 2.0);
|
||||
assert_close!(f.num("thisComp.layer('A')('ADBE Effect Parade').propertyIndex", 0.0), 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_enabled_time_remap_is_number_two() {
|
||||
// Enabled Time Remap is a property at the top of the tree; After Effects numbers it 2.
|
||||
let mut f = fx();
|
||||
let uid = f.p.next_id;
|
||||
f.p.next_id += 1;
|
||||
f.layer_mut(f.a).props.children.insert(0, Property::new(uid, "timeRemap", "Time Remap", Value::Scalar(0.0)).into());
|
||||
assert_close!(f.num("thisComp.layer('A')('Time Remap').propertyIndex", 0.0), 2.0);
|
||||
assert_close!(f.num("thisComp.layer('A')(2).propertyIndex", 0.0), 2.0);
|
||||
}
|
||||
Reference in new issue
Block a user