Merge remote-tracking branch 'upstream/main' into fix/3087-aep-effect-placeholders

# Conflicts:
#	crates/expr/src/host.rs
This commit is contained in:
arnav-hiwarkar committed 2026-11-12 13:42:21 +05:30
commit b762b4800d
10 files changed
+472 -12

No files matched your search

+49 -10
View File
@@ -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()),
+2
View File
@@ -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]);
}
+1
View File
@@ -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
+98
View File
@@ -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);
}
+139
View File
@@ -0,0 +1,139 @@
//! After Effects' numeric order of a layer's top-level property groups.
//!
//! In After Effects `thisLayer(4)` and `layer.property(4)` reach the same group on every layer of a
//! kind, whatever the layer holds: Marker is always 1, and the groups a layer has not used yet
//! (Time Remap, Motion Trackers, Layer Styles…) still take their slot. EffectCraft's tree holds only
//! the groups a layer has, without Marker, so its own positions differ and move as groups are
//! added. Rigs that address groups by number (`comp("Palette").layer(1)(4)(n)(1)`) read the wrong
//! group, or none, unless numbers go through this table.
//!
//! The order was observed in After Effects 26.3 by reading `layer.property(i).matchName` from a
//! script and `thisComp.layer(..)(i).name` from an expression on new layers of each kind; both
//! agree. 3D model and primitive layers have no table: their numbers stay EffectCraft's own.
use crate::{Layer, LayerSource, Node};
/// One top-level slot in After Effects' order.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AeSlot {
/// The group's match id in EffectCraft's tree, or `""` when EffectCraft has no such group.
pub match_id: &'static str,
/// The display name After Effects gives the slot.
pub name: &'static str,
/// After Effects' match name for the slot.
pub match_name: &'static str,
}
const fn slot(match_id: &'static str, name: &'static str, match_name: &'static str) -> AeSlot {
AeSlot { match_id, name, match_name }
}
/// The marker slot: number 1 on every layer. Markers are a field of [`Layer`], not a tree node.
pub const MARKER: AeSlot = slot("marker", "Marker", "ADBE Marker");
/// Footage, solid, adjustment, precomp and null layers.
const AV: &[AeSlot] = &[
MARKER,
slot("timeRemap", "Time Remap", "ADBE Time Remapping"),
slot("motionTrackers", "Motion Trackers", "ADBE MTrackers"),
slot("masks", "Masks", "ADBE Mask Parade"),
slot("effects", "Effects", "ADBE Effect Parade"),
slot("transform", "Transform", "ADBE Transform Group"),
slot("layerStyles", "Layer Styles", "ADBE Layer Styles"),
slot("", "Geometry Options", "ADBE Plane Options Group"),
slot("geometryOptions", "Geometry Options", "ADBE Extrsn Options Group"),
slot("materialOptions", "Material Options", "ADBE Material Options Group"),
slot("audio", "Audio", "ADBE Audio Group"),
slot("", "Data", "ADBE Data Group"),
slot("", "Essential Properties", "ADBE Layer Overrides"),
slot("", "Sets", "ADBE Layer Sets"),
slot("", "Source Options", "ADBE Source Options Group"),
];
const TEXT: &[AeSlot] = &[
MARKER,
slot("text", "Text", "ADBE Text Properties"),
slot("masks", "Masks", "ADBE Mask Parade"),
slot("effects", "Effects", "ADBE Effect Parade"),
slot("transform", "Transform", "ADBE Transform Group"),
slot("layerStyles", "Layer Styles", "ADBE Layer Styles"),
slot("geometryOptions", "Geometry Options", "ADBE Extrsn Options Group"),
slot("materialOptions", "Material Options", "ADBE Material Options Group"),
slot("audio", "Audio", "ADBE Audio Group"),
slot("", "Sets", "ADBE Layer Sets"),
];
const SHAPE: &[AeSlot] = &[
MARKER,
slot("contents", "Contents", "ADBE Root Vectors Group"),
slot("masks", "Masks", "ADBE Mask Parade"),
slot("effects", "Effects", "ADBE Effect Parade"),
slot("transform", "Transform", "ADBE Transform Group"),
slot("layerStyles", "Layer Styles", "ADBE Layer Styles"),
slot("geometryOptions", "Geometry Options", "ADBE Extrsn Options Group"),
slot("materialOptions", "Material Options", "ADBE Material Options Group"),
slot("audio", "Audio", "ADBE Audio Group"),
slot("", "Sets", "ADBE Layer Sets"),
];
const CAMERA: &[AeSlot] =
&[MARKER, slot("transform", "Transform", "ADBE Transform Group"), slot("cameraOptions", "Camera Options", "ADBE Camera Options Group")];
const LIGHT: &[AeSlot] = &[MARKER, slot("transform", "Transform", "ADBE Transform Group"), slot("lightOptions", "Light Options", "ADBE Light Options Group")];
/// After Effects' top-level slots for a layer of this kind, or `None` for 3D model and primitive
/// layers, which keep EffectCraft's own numbers.
pub fn ae_slots(source: &LayerSource) -> Option<&'static [AeSlot]> {
match source {
LayerSource::Footage { .. } | LayerSource::Comp { .. } | LayerSource::Solid { .. } | LayerSource::Null => Some(AV),
LayerSource::Text => Some(TEXT),
LayerSource::Shape => Some(SHAPE),
LayerSource::Camera => Some(CAMERA),
LayerSource::Light { .. } => Some(LIGHT),
LayerSource::Model { .. } | LayerSource::Primitive { .. } => None,
}
}
/// What After Effects' top-level number `index` (1-based) reaches on `layer`.
#[derive(Clone, Copy, Debug)]
pub enum AeChild<'a> {
/// The layer's markers (number 1).
Marker,
/// A group or property the layer has.
Node(&'a Node),
/// A slot After Effects always has and this layer does not (an empty group there).
Absent(&'static AeSlot),
}
/// The top-level node After Effects' number `index` (1-based) reaches, or `None` when the number is
/// past the last slot or the layer kind has no table ([`ae_slots`]).
pub fn ae_child(layer: &Layer, index: usize) -> Option<AeChild<'_>> {
let s = ae_slots(&layer.source)?.get(index.checked_sub(1)?)?;
if *s == MARKER {
return Some(AeChild::Marker);
}
if s.match_id.is_empty() {
return Some(AeChild::Absent(s));
}
Some(layer.props.children.iter().find(|n| n.match_id() == s.match_id).map_or(AeChild::Absent(s), AeChild::Node))
}
/// After Effects' 1-based number for the top-level node with this match id, when it has one.
pub fn ae_index(layer: &Layer, match_id: &str) -> Option<usize> {
if match_id.is_empty() {
return None;
}
ae_slots(&layer.source)?.iter().position(|s| s.match_id == match_id).map(|i| i + 1)
}
/// How many top-level properties After Effects counts on `layer` (`numProperties`), or `None` for
/// layer kinds without a table.
pub fn ae_count(layer: &Layer) -> Option<usize> {
ae_slots(&layer.source).map(<[AeSlot]>::len)
}
/// After Effects' 1-based number for the top-level slot with this After Effects match name
/// (`ADBE Effect Parade`), when the layer kind has one.
pub fn ae_index_of_match_name(layer: &Layer, match_name: &str) -> Option<usize> {
ae_slots(&layer.source)?.iter().position(|s| s.match_name == match_name).map(|i| i + 1)
}
+1
View File
@@ -6,6 +6,7 @@
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
pub mod ae_order;
pub mod build;
pub mod essential;
mod file_check;
+24 -2
View File
@@ -5,6 +5,7 @@
use effectcraft_engine::Session;
use effectcraft_engine::color::Label;
use effectcraft_keyframe::{Interp, TextDoc, Value as KV};
use effectcraft_project::ae_order::{self, AeChild};
use effectcraft_project::{Comp, GroupKind, ItemId, ItemKind, Layer, LayerId, LayerSource, Node, ParamUi, PropGroup};
use effectcraft_render::EvalCtx;
use effectcraft_time::Tick;
@@ -318,7 +319,8 @@ fn indexed_parade(g: &PropGroup) -> bool {
fn node_json(c: &Comp, l: &Layer, uid: u64) -> R {
if uid == 0 || uid == l.props.uid {
let n = l.props.children.len();
// After Effects' count (every slot, used or not); 3D model layers count their own groups.
let n = ae_order::ae_count(l).unwrap_or(l.props.children.len());
return Ok(json!({
"uid": l.props.uid, "kind": "group", "name": l.name, "matchName": matchnames::layer(&l.source), "matchId": "layer",
"index": c.index_of(l.id).unwrap_or(0), "depth": 0, "parent": null, "isLayer": true, "numProperties": n,
@@ -329,7 +331,8 @@ fn node_json(c: &Comp, l: &Layer, uid: u64) -> R {
let ch = chain(l, uid).ok_or("the property no longer exists (it was deleted)")?;
let node = *ch.last().ok_or("bad property")?;
let parent_group: &PropGroup = if ch.len() >= 2 { ch[ch.len() - 2].as_group().ok_or("bad tree")? } else { &l.props };
let index = parent_group.children.iter().position(|x| x.uid() == uid).map(|i| i + 1).unwrap_or(0);
let top = if ch.len() == 1 { ae_order::ae_index(l, node.match_id()) } else { None };
let index = top.unwrap_or_else(|| parent_group.children.iter().position(|x| x.uid() == uid).map(|i| i + 1).unwrap_or(0));
let parent_uid = if ch.len() >= 2 { ch[ch.len() - 2].uid() } else { l.props.uid };
let match_name = matchnames::node(&ch);
let instance_parent = indexed_parade(parent_group) && ch.len() >= 2 || (ch.len() == 2 && ch[0].match_id() == "contents");
@@ -475,6 +478,25 @@ fn child(s: &Session, a: &J) -> R {
let uid = u(a, "uid").unwrap_or(0);
let g: &PropGroup = if uid == 0 || uid == l.props.uid { &l.props } else { l.props.find_group(uid).ok_or("not a property group")? };
let key = a.get("key").cloned().unwrap_or(J::Null);
// A layer's numbers and match names are After Effects' (Marker is 1, unused groups keep their
// slot and resolve to the same empty group by number or by match name).
let slot = match &key {
J::Number(n) => Some(n.as_u64().and_then(|i| usize::try_from(i).ok())),
J::String(name) => ae_order::ae_index_of_match_name(l, name).map(Some),
_ => None,
};
if std::ptr::eq(g, &l.props)
&& ae_order::ae_slots(&l.source).is_some()
&& let Some(slot) = slot
{
let Some(i) = slot else { return Ok(J::Null) };
return match ae_order::ae_child(l, i) {
Some(AeChild::Node(n)) => node_json(c, l, n.uid()),
Some(AeChild::Marker) => Ok(json!({"marker": true})),
Some(AeChild::Absent(s)) => Ok(json!({"absent": true, "name": s.name, "matchName": s.match_name, "index": i})),
None => Ok(J::Null),
};
}
let found = match &key {
J::Number(n) => n.as_u64().and_then(|i| g.children.get((i as usize).checked_sub(1)?)),
J::String(k) => find_child(l, g, k, NESTED_CHILD_DEPTH),
+36
View File
@@ -653,10 +653,46 @@ TextDocument.prototype = {
function __propOf(layer, info) {
if (!info) return null;
if (info.isLayer) return layer;
if (info.marker) return layer.marker;
if (info.absent) return __absentGroup(layer, info);
if (info.kind === "group") return info.isMask ? new MaskPropertyGroup(layer, info.uid) : new PropertyGroup(layer, info.uid);
return new Property(layer, info.uid);
}
// A top-level slot After Effects always has and this layer does not use yet (Layer Styles before
// any style, Data, Sets…): an empty group, so loops over `numProperties` meet no null.
function __absentGroup(layer, info) {
var g = {
__kind: "PropertyGroup",
get name() { return info.name; },
get matchName() { return info.matchName; },
get propertyIndex() { return info.index; },
get propertyDepth() { return 1; },
get propertyType() { return PropertyType.NAMED_GROUP; },
get parentProperty() { return layer; },
get numProperties() { return 0; },
get enabled() { return true; },
get active() { return layer.enabled; },
get canSetEnabled() { return false; },
get isModified() { return false; },
get isEffect() { return false; },
get isMask() { return false; },
get elided() { return false; },
get selected() { return false; },
property: function () { return null; },
propertyGroup: function (n) { return n === undefined || n === 1 ? layer : null; },
canAddProperty: function () { return false; },
addProperty: function (n) { throw __err("Can not add \"" + n + "\" to \"" + info.name + "\""); },
toString: function () { return "[object PropertyGroup]"; },
};
// Callable like any group: `layer.property(7)(1)` is `.property(1)`, null.
var f = function (k) { return g.property(k); };
delete f.name;
delete f.length;
for (const k of Object.getOwnPropertyNames(g)) Object.defineProperty(f, k, Object.getOwnPropertyDescriptor(g, k));
return f;
}
function PropertyBase() {}
PropertyBase.prototype = {
__info: function () { return __get("node", { comp: this.__layer.__comp, layer: this.__layer.__id, uid: this.__uid }); },
+1
View File
@@ -10,6 +10,7 @@ use serde_json::{Value, json};
use crate::{Outcome, run_code};
mod ae_layer_order;
mod callable;
mod ease_presets;
mod evalfile;
+121
View File
@@ -0,0 +1,121 @@
//! `layer.property(n)` and the layer's `numProperties` use After Effects' top-level numbers
//! (`project::ae_order`): Marker is 1 and unused slots keep their place as empty groups, so a loop
//! over `numProperties` meets every slot and never a null.
use std::sync::Arc;
use effectcraft_engine::Session;
use serde_json::json;
use crate::{Outcome, run_code};
fn session() -> Session {
let mut s = Session { expr: Some(Arc::new(effectcraft_expr::Expressions)), expr_check: Some(effectcraft_expr::check_syntax), ..Default::default() };
crate::install(&mut s);
s
}
fn ok(s: &mut Session, code: &str) -> Outcome {
let o = run_code(s, code, "test.jsx");
assert!(o.error.is_none(), "script failed: {:?}\noutput: {:?}", o.error, o.output);
o
}
#[test]
fn layer_numbers_follow_after_effects() {
let mut s = session();
let o = ok(
&mut s,
r#"
var c = app.project.items.addComp("M", 200, 200, 1, 2, 30);
var solid = c.layers.addSolid([1, 1, 1], "S", 200, 200, 1);
solid.Effects.addProperty("ADBE Slider Control");
var shape = c.layers.addShape();
var text = c.layers.addText("t");
var cam = c.layers.addCamera("Cam", [100, 100]);
function names(l) { var o = []; for (var i = 1; i <= l.numProperties; i++) o.push(l.property(i).matchName); return o; }
[names(solid), names(shape), names(text), names(cam),
solid.property(5)(1)(1).value, solid.property(5).propertyIndex, solid.property("Effects").propertyIndex,
solid.property(1) === solid.marker || solid.property(1).matchName, solid.property(16)]
"#,
);
let av = [
"ADBE Marker",
"ADBE Time Remapping",
"ADBE MTrackers",
"ADBE Mask Parade",
"ADBE Effect Parade",
"ADBE Transform Group",
"ADBE Layer Styles",
"ADBE Plane Options Group",
"ADBE Extrsn Options Group",
"ADBE Material Options Group",
"ADBE Audio Group",
"ADBE Data Group",
"ADBE Layer Overrides",
"ADBE Layer Sets",
"ADBE Source Options Group",
];
let vector = |first: &str| {
json!([
"ADBE Marker",
first,
"ADBE Mask Parade",
"ADBE Effect Parade",
"ADBE Transform Group",
"ADBE Layer Styles",
"ADBE Extrsn Options Group",
"ADBE Material Options Group",
"ADBE Audio Group",
"ADBE Layer Sets"
])
};
let r = o.result.as_array().unwrap();
assert_eq!(r[0], json!(av), "solid");
assert_eq!(r[1], vector("ADBE Root Vectors Group"), "shape");
assert_eq!(r[2], vector("ADBE Text Properties"), "text");
assert_eq!(r[3], json!(["ADBE Marker", "ADBE Transform Group", "ADBE Camera Options Group"]), "camera");
assert_eq!(r[4], json!(0), "the slider's value through property(5)");
assert_eq!(r[5], json!(5));
assert_eq!(r[6], json!(5), "propertyIndex by name is After Effects' too");
assert_eq!(r[7], json!("ADBE Marker"));
assert_eq!(r[8], json!(null), "past the last slot");
}
#[test]
fn walking_every_property_meets_no_null() {
let mut s = session();
let o = ok(
&mut s,
r#"
var c = app.project.items.addComp("M", 200, 200, 1, 2, 30);
var l = c.layers.addShape();
var n = 0;
function walk(g) {
for (var i = 1; i <= g.numProperties; i++) {
var p = g.property(i);
if (p === null) throw new Error("null at " + i + " of " + g.name);
if (p.propertyType === PropertyType.PROPERTY) n++; else walk(p);
}
}
walk(l);
n > 0
"#,
);
assert_eq!(o.result, json!(true));
}
#[test]
fn an_unused_slot_is_the_same_by_number_and_match_name_and_callable() {
let mut s = session();
let o = ok(
&mut s,
r#"
var c = app.project.items.addComp("M", 200, 200, 1, 2, 30);
var l = c.layers.addSolid([1, 1, 1], "S", 200, 200, 1);
var byName = l.property("ADBE Layer Styles"), byNumber = l.property(7);
[byName !== null && byName.matchName, byName !== null && byName.numProperties, byNumber.propertyIndex, l.property(7)(1), l(12).name]
"#,
);
assert_eq!(o.result, json!(["ADBE Layer Styles", 0, 7, null, "Data"]));
}