mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-07 18:03:48 +00:00
Composition Settings ▸ Advanced has both options (they were missing from the model, the
renderer and the dialog):
- Preserve frame rate: nested in another comp, the comp shows only its own frames
(`EvalCtx::nested_time` floors the precomp layer's source time to the nested comp's frame
rate; used for the nested frame, its proxy, aux channels, collapsed transformations and the
viewer's internal wireframes), and the Render Queue renders it at its own rate whatever the
Frame Rate override (`RenderSettings::rate`).
- Preserve resolution: nested in a comp rendered at Half / Third / Quarter, it renders at full
size and the buffer's scale places it.
- `comp.settings {preserveFrameRate, preserveResolution}`, `comp.info` reports them, scripts
read and set `CompItem.preserveNestedFrameRate` / `preserveNestedResolution`, Pre-compose
copies them, and the dialog registers `dialog.comp.preserveFrameRate` /
`dialog.comp.preserveResolution`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
505 lines
22 KiB
Rust
505 lines
22 KiB
Rust
//! End-to-end scripts against a headless session: build with the object model, read back through
|
|
//! the engine.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use effectcraft_engine::Session;
|
|
use effectcraft_engine::keyframe::Value as KV;
|
|
use effectcraft_engine::project::{Comp, ItemId, Layer};
|
|
use serde_json::{Value, 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
|
|
}
|
|
|
|
fn comp_named<'a>(s: &'a Session, name: &str) -> (ItemId, &'a Comp) {
|
|
let it = s.project.items.values().find(|i| i.name == name && i.as_comp().is_some()).unwrap_or_else(|| panic!("no comp {name}"));
|
|
(it.id, it.as_comp().unwrap())
|
|
}
|
|
|
|
fn layer<'a>(c: &'a Comp, name: &str) -> &'a Layer {
|
|
c.layers.iter().find(|l| l.name == name).unwrap_or_else(|| panic!("no layer {name}"))
|
|
}
|
|
|
|
#[test]
|
|
fn builds_a_comp_end_to_end() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
app.beginUndoGroup("Build");
|
|
var comp = app.project.items.addComp("Main", 640, 360, 1, 4, 25);
|
|
var bg = comp.layers.addSolid([0.2, 0.4, 0.6], "BG", 640, 360, 1);
|
|
var txt = comp.layers.addText("Hello");
|
|
txt.name = "Title";
|
|
var pos = txt.property("ADBE Transform Group").property("ADBE Position");
|
|
pos.setValueAtTime(0, [100, 180]);
|
|
pos.setValueAtTime(2, [540, 180]);
|
|
txt.transform.opacity.expression = "50 + 25";
|
|
var blur = bg.Effects.addProperty("ADBE Gaussian Blur 2");
|
|
blur.property("Blurriness").setValue(12);
|
|
var nul = comp.layers.addNull();
|
|
txt.setParentWithJump(nul);
|
|
bg.blendingMode = BlendingMode.MULTIPLY;
|
|
var rq = app.project.renderQueue.items.add(comp);
|
|
rq.outputModule(1).file = new File("/tmp/effectcraft-script-test/out.mp4");
|
|
app.endUndoGroup();
|
|
writeLn("layers: " + comp.numLayers);
|
|
[comp.numLayers, pos.numKeys, pos.keyValue(2)[0], txt.transform.opacity.value, comp.layer(1).name]
|
|
"#,
|
|
);
|
|
assert_eq!(o.output, vec!["layers: 3"]);
|
|
assert_eq!(o.result, json!([3, 2, 540, 75, "Null 1"]));
|
|
let (cid, c) = comp_named(&s, "Main");
|
|
assert_eq!((c.width, c.height), (640, 360));
|
|
assert_eq!(c.duration.seconds(), 4.0);
|
|
// AE adds new layers at the top.
|
|
let names: Vec<&str> = c.layers.iter().map(|l| l.name.as_str()).collect();
|
|
assert_eq!(names, ["Null 1", "Title", "BG"]);
|
|
let title = layer(c, "Title");
|
|
let p = title.props.prop("transform/position").unwrap();
|
|
assert_eq!(p.keys.len(), 2);
|
|
assert_eq!(p.keys[1].value, KV::Vec3([540.0, 180.0, 0.0]));
|
|
assert_eq!(title.props.prop("transform/opacity").unwrap().expr.as_ref().unwrap().text, "50 + 25");
|
|
assert_eq!(title.parent, Some(layer(c, "Null 1").id));
|
|
let bg = layer(c, "BG");
|
|
assert_eq!(bg.blend_mode, effectcraft_engine::color::BlendMode::Multiply);
|
|
let fx = bg.props.group("effects/#1").unwrap();
|
|
assert_eq!(fx.match_id, "ec.blur.gaussian");
|
|
assert_eq!(bg.props.prop("effects/#1/blurriness").unwrap().value, KV::Scalar(12.0));
|
|
assert_eq!(s.project.render_queue.len(), 1);
|
|
assert_eq!(s.project.render_queue[0].comp, cid);
|
|
// One undo step for the whole group.
|
|
assert_eq!(s.history.undo.len(), 1);
|
|
assert_eq!(s.history.undo[0].0, "Build");
|
|
assert!(s.undo());
|
|
assert!(s.project.items.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn fill_parameters_use_documented_match_names() {
|
|
// M13.15: `ADBE Fill-0002` is Fill's Color (All Masks is -0007), not the second row.
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"var c = app.project.items.addComp("F", 100, 100, 1, 1, 12); var l = c.layers.addSolid([1,1,1], "S", 100, 100, 1);
|
|
var fx = l.property("ADBE Effect Parade").addProperty("ADBE Fill");
|
|
[fx.property("ADBE Fill-0002").name, fx.property("ADBE Fill-0007").name, fx.property(2).matchName].join("|")"#,
|
|
);
|
|
assert_eq!(o.result, json!("Color|All Masks|ADBE Fill-0007"));
|
|
}
|
|
|
|
#[test]
|
|
fn undo_steps_without_a_group() {
|
|
let mut s = session();
|
|
ok(&mut s, r#"var c = app.project.items.addComp("A", 100, 100, 1, 1, 24); c.layers.addNull(); c.layers.addNull();"#);
|
|
assert_eq!(s.history.undo.len(), 3);
|
|
// An unbalanced group is closed when the script ends.
|
|
ok(&mut s, r#"app.beginUndoGroup("Two"); var c = app.project.item(1); c.layers.addNull(); c.layers.addNull();"#);
|
|
assert_eq!(s.history.undo.len(), 4);
|
|
assert_eq!(s.history.undo[3].0, "Two");
|
|
// Nested groups fold into the outermost one.
|
|
ok(
|
|
&mut s,
|
|
r#"app.beginUndoGroup("Outer"); app.beginUndoGroup("Inner"); app.project.item(1).layers.addNull(); app.endUndoGroup(); app.project.item(1).layers.addNull(); app.endUndoGroup();"#,
|
|
);
|
|
assert_eq!(s.history.undo.len(), 5);
|
|
assert_eq!(s.history.undo[4].0, "Outer");
|
|
s.undo();
|
|
assert_eq!(s.project.comp(ItemId(1)).unwrap().layers.len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn errors_report_line_and_column() {
|
|
let mut s = session();
|
|
let o = run_code(&mut s, "var a = 1;\nvar b = 2;\nfoo.bar();\n", "broken.jsx");
|
|
let e = o.error.expect("error");
|
|
assert_eq!(e.line, Some(3), "{e:?}");
|
|
assert!(e.message.contains("foo"), "{e:?}");
|
|
assert_eq!(e.file, "broken.jsx");
|
|
let o = run_code(&mut s, "var a = 1;\n\nvar = ;\n", "syntax.jsx");
|
|
let e = o.error.expect("syntax error");
|
|
assert_eq!(e.line, Some(3), "{e:?}");
|
|
let o = run_code(&mut s, "writeLn('before');\nthrow new Error('custom failure');\n", "throw.jsx");
|
|
let e = o.error.expect("thrown");
|
|
assert_eq!(e.message, "custom failure");
|
|
assert_eq!(e.line, Some(2), "{e:?}");
|
|
assert_eq!(o.output, vec!["before"]);
|
|
// Errors from the object model point at the user's line.
|
|
let o = run_code(&mut s, "var c = app.project.items.addComp('C', 10, 10, 1, 1, 24);\n\nc.layer(5);\n", "range.jsx");
|
|
let e = o.error.expect("range error");
|
|
assert!(e.message.contains("out of range"), "{e:?}");
|
|
assert_eq!(e.line, Some(3), "{e:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn match_names_and_lookups() {
|
|
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 tr = l.property("ADBE Transform Group");
|
|
var fx = l.property("ADBE Effect Parade").addProperty("Gaussian Blur");
|
|
var m = l.property("ADBE Mask Parade").addProperty("ADBE Mask Atom");
|
|
var sh = c.layers.addShape();
|
|
var grp = sh.property("ADBE Root Vectors Group").addProperty("ADBE Vector Group");
|
|
var rect = grp.property("ADBE Vectors Group").addProperty("ADBE Vector Shape - Rect");
|
|
var fill = grp.property("Contents").addProperty("ADBE Vector Graphic - Fill");
|
|
fill.property("ADBE Vector Fill Color").setValue([1, 0, 0]);
|
|
rect.property("ADBE Vector Rect Size").setValue([50, 60]);
|
|
[
|
|
l.matchName, tr.matchName, tr.property("ADBE Position").matchName, tr.property("Anchor Point").matchName,
|
|
tr.property(1).name, tr.anchorPoint.matchName, l.transform.position.propertyValueType == PropertyValueType.ThreeD_SPATIAL,
|
|
fx.matchName, fx.property(1).matchName, fx.property("ADBE Gaussian Blur 2-0001").name, m.matchName,
|
|
m.property("ADBE Mask Shape").name, sh.matchName, grp.matchName, rect.matchName, fill.matchName,
|
|
tr.propertyType == PropertyType.NAMED_GROUP, l.Effects.propertyType == PropertyType.INDEXED_GROUP,
|
|
tr.position.parentProperty.matchName, tr.position.propertyDepth, l.property("Nope") === null,
|
|
fx.isEffect, m.isMask, l.Effects.numProperties
|
|
]
|
|
"#,
|
|
);
|
|
assert_eq!(
|
|
o.result,
|
|
json!([
|
|
"ADBE AV Layer",
|
|
"ADBE Transform Group",
|
|
"ADBE Position",
|
|
"ADBE Anchor Point",
|
|
"Anchor Point",
|
|
"ADBE Anchor Point",
|
|
true,
|
|
"ADBE Gaussian Blur 2",
|
|
"ADBE Gaussian Blur 2-0001",
|
|
"Blurriness",
|
|
"ADBE Mask Atom",
|
|
"Mask Path",
|
|
"ADBE Vector Layer",
|
|
"ADBE Vector Group",
|
|
"ADBE Vector Shape - Rect",
|
|
"ADBE Vector Graphic - Fill",
|
|
true,
|
|
true,
|
|
"ADBE Transform Group",
|
|
2,
|
|
true,
|
|
true,
|
|
true,
|
|
1
|
|
])
|
|
);
|
|
let (_, c) = comp_named(&s, "M");
|
|
let sh = c.layers.iter().find(|l| l.props.sub("contents").is_some()).unwrap();
|
|
assert_eq!(sh.props.prop("contents/#1/contents/rect/size").unwrap().value, KV::Vec2([50.0, 60.0]));
|
|
assert_eq!(sh.props.prop("contents/#1/contents/fill/color").unwrap().value, KV::Color([1.0, 0.0, 0.0, 1.0]));
|
|
}
|
|
|
|
#[test]
|
|
fn keyframes_eases_and_interpolation() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var c = app.project.items.addComp("K", 100, 100, 1, 5, 25);
|
|
var l = c.layers.addNull();
|
|
var r = l.transform.rotation;
|
|
r.setValuesAtTimes([0, 1, 2], [0, 90, 180]);
|
|
r.setInterpolationTypeAtKey(2, KeyframeInterpolationType.HOLD, KeyframeInterpolationType.LINEAR);
|
|
r.setTemporalEaseAtKey(3, [new KeyframeEase(0, 75)]);
|
|
var idx = r.addKey(1.5);
|
|
r.removeKey(1);
|
|
var out = [r.numKeys, idx, r.keyTime(1), r.keyInInterpolationType(1) == KeyframeInterpolationType.HOLD,
|
|
r.keyOutInterpolationType(1) == KeyframeInterpolationType.LINEAR, r.keyInTemporalEase(3)[0].influence,
|
|
r.valueAtTime(1, false), r.isTimeVarying, r.nearestKeyIndex(1.9)];
|
|
l.startTime = 1;
|
|
out.push(r.keyTime(1));
|
|
out
|
|
"#,
|
|
);
|
|
let v = o.result.as_array().unwrap();
|
|
assert_eq!(v[0], json!(3));
|
|
assert_eq!(v[1], json!(3));
|
|
assert_eq!(v[2], json!(1));
|
|
assert_eq!(v[3], json!(true));
|
|
assert_eq!(v[4], json!(true));
|
|
assert!((v[5].as_f64().unwrap() - 75.0).abs() < 1e-6, "{v:?}");
|
|
assert_eq!(v[6], json!(90));
|
|
assert_eq!(v[7], json!(true));
|
|
assert_eq!(v[8], json!(3));
|
|
// Moving the layer moves its keys (layer time): key 1 now sits at 2 s comp time.
|
|
assert_eq!(v[9], json!(2));
|
|
}
|
|
|
|
#[test]
|
|
fn text_documents() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var c = app.project.items.addComp("T", 400, 200, 1, 2, 30);
|
|
var t = c.layers.addText("abc");
|
|
var st = t.property("ADBE Text Properties").property("ADBE Text Document");
|
|
var doc = st.value;
|
|
doc.text = "Changed";
|
|
doc.fontSize = 40;
|
|
doc.fillColor = [1, 0, 0];
|
|
doc.justification = ParagraphJustification.CENTER_JUSTIFY;
|
|
doc.applyStroke = true;
|
|
doc.strokeWidth = 3;
|
|
st.setValue(doc);
|
|
var d2 = t.text.sourceText.value;
|
|
[d2.text, d2.fontSize, d2.fillColor, d2.justification == ParagraphJustification.CENTER_JUSTIFY, d2.strokeWidth, t instanceof TextLayer, t.name]
|
|
"#,
|
|
);
|
|
assert_eq!(o.result, json!(["Changed", 40, [1, 0, 0], true, 3, true, "abc"]));
|
|
let (_, c) = comp_named(&s, "T");
|
|
let KV::Text(d) = &c.layers[0].props.prop("text/sourceText").unwrap().value else { panic!() };
|
|
assert_eq!(d.text, "Changed");
|
|
assert_eq!(d.style_at(0).size, 40.0);
|
|
}
|
|
|
|
#[test]
|
|
fn layers_items_and_markers() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var f = app.project.items.addFolder("Comps");
|
|
var c = f.items.addComp("Inner", 100, 100, 1, 3, 30);
|
|
var main = app.project.items.addComp("Outer", 200, 200, 1, 6, 30);
|
|
var nested = main.layers.add(c);
|
|
var a = main.layers.addSolid([1, 0, 0], "A", 50, 50, 1, 2);
|
|
var b = main.layers.addSolid([0, 1, 0], "B", 50, 50, 1);
|
|
b.moveAfter(nested);
|
|
a.enabled = false; a.solo = true; a.locked = true; a.shy = true;
|
|
a.threeDLayer = true;
|
|
var cam = main.layers.addCamera("Cam", [100, 100]);
|
|
var lt = main.layers.addLight("Key", [100, 100]);
|
|
lt.lightType = LightType.SPOT;
|
|
var d = a.duplicate();
|
|
d.name = "A copy";
|
|
var mv = new MarkerValue("hello");
|
|
mv.duration = 0.5;
|
|
main.markerProperty.setValueAtTime(1, mv);
|
|
b.marker.setValueAtTime(0.5, new MarkerValue("layer marker"));
|
|
b.inPoint = 1; b.outPoint = 3;
|
|
var pc = main.layers.precompose([b.index], "Pre", true);
|
|
[c.parentFolder.name, nested.source.name, a.outPoint, a.enabled, a.solo, a.locked, a.shy, a.threeDLayer,
|
|
cam instanceof CameraLayer, lt.lightType == LightType.SPOT, main.markerProperty.numKeys, main.markerProperty.keyValue(1).comment,
|
|
main.markerProperty.keyValue(1).duration, pc.name, pc instanceof CompItem, main.layer("A copy").index, app.project.numItems,
|
|
main.layers.byName("Pre") !== null, f.numItems]
|
|
"#,
|
|
);
|
|
assert_eq!(o.result, json!(["Comps", "Inner", 2, false, true, true, true, true, true, true, 1, "hello", 0.5, "Pre", true, 3, 7, true, 1]));
|
|
let (_, c) = comp_named(&s, "Outer");
|
|
assert_eq!(c.markers.len(), 1);
|
|
assert!(
|
|
c.layers
|
|
.iter()
|
|
.any(|l| l.name == "Key"
|
|
&& matches!(l.source, effectcraft_engine::project::LayerSource::Light { kind: effectcraft_engine::project::LightKind::Spot }))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn security_gate_for_files() {
|
|
let dir = std::env::temp_dir().join(format!("ec-script-gate-{}", std::process::id()));
|
|
let proj = dir.join("proj");
|
|
std::fs::create_dir_all(&proj).unwrap();
|
|
std::fs::write(proj.join("data.txt"), "inside").unwrap();
|
|
std::fs::write(dir.join("secret.txt"), "outside").unwrap();
|
|
let mut s = session();
|
|
s.path = Some(proj.join("p.ecproj").to_string_lossy().to_string());
|
|
let q = |p: &std::path::Path| serde_json::to_string(&p.to_string_lossy()).unwrap();
|
|
// Reading inside the project folder works.
|
|
let o = ok(&mut s, &format!("var f = new File({}); f.open('r'); var t = f.read(); f.close(); t", q(&proj.join("data.txt"))));
|
|
assert_eq!(o.result, json!("inside"));
|
|
// Outside it (also via `..`) is refused.
|
|
for path in [dir.join("secret.txt"), proj.join("../secret.txt")] {
|
|
let o = run_code(&mut s, &format!("var f = new File({}); f.open('r');", q(&path)), "gate.jsx");
|
|
let e = o.error.expect("blocked");
|
|
assert!(e.message.contains("Allow Scripts to Write Files"), "{e:?}");
|
|
}
|
|
// Writing is refused anywhere, the network too.
|
|
let o = run_code(&mut s, &format!("var f = new File({}); f.open('w'); f.write('x'); f.close();", q(&proj.join("out.txt"))), "gate.jsx");
|
|
assert!(o.error.is_some());
|
|
assert!(!proj.join("out.txt").exists());
|
|
assert!(run_code(&mut s, "new Socket()", "net.jsx").error.is_some());
|
|
assert!(run_code(&mut s, "system.callSystem('ls')", "sys.jsx").error.is_some());
|
|
// With the preference on, scripts can write and read anywhere.
|
|
s.prefs.scripting.allow_scripts_write_files = true;
|
|
ok(&mut s, &format!("var f = new File({}); f.open('w'); f.writeln('written'); f.close();", q(&dir.join("w.txt"))));
|
|
assert_eq!(std::fs::read_to_string(dir.join("w.txt")).unwrap(), "written\n");
|
|
let o = ok(&mut s, &format!("var f = new File({}); f.open('r'); f.readln()", q(&dir.join("secret.txt"))));
|
|
assert_eq!(o.result, json!("outside"));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn engine_entry_points() {
|
|
let mut s = session();
|
|
// script.run reports errors in the result, not as a command failure.
|
|
let r = s.execute("script.run", json!({"code": "writeLn('hi'); 1 + 2"})).unwrap();
|
|
assert_eq!(r["ok"], json!(true));
|
|
assert_eq!(r["result"], json!(3));
|
|
assert_eq!(r["output"], json!("hi"));
|
|
let r = s.execute("script.run", json!({"code": "\nnope()"})).unwrap();
|
|
assert_eq!(r["ok"], json!(false));
|
|
assert_eq!(r["error"]["line"], json!(2));
|
|
// Scripts can't run scripts.
|
|
let r = s.execute("script.run", json!({"code": "app.run('script.run', {code: '1'})"})).unwrap();
|
|
assert!(r["error"]["message"].as_str().unwrap().contains("scripts can't run scripts"), "{r}");
|
|
// File ▸ Scripts ▸ Run Script File… runs .jsx as JavaScript.
|
|
let dir = std::env::temp_dir().join(format!("ec-script-file-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let f = dir.join("make.jsx");
|
|
std::fs::write(&f, "app.project.items.addComp('From File', 320, 240, 1, 2, 24);").unwrap();
|
|
s.execute("file.runScript", json!({"path": f.to_string_lossy()})).unwrap();
|
|
assert!(s.project.items.values().any(|i| i.name == "From File"));
|
|
std::fs::write(&f, "\n\nbroken(;").unwrap();
|
|
let e = s.execute("file.runScript", json!({"path": f.to_string_lossy()})).unwrap_err().to_string();
|
|
assert!(e.contains("make.jsx (line 3)"), "{e}");
|
|
// JSON command scripts still work.
|
|
let j = dir.join("steps.json");
|
|
std::fs::write(&j, r#"[{"command": "comp.new", "params": {"name": "From Steps"}}]"#).unwrap();
|
|
s.execute("file.runScript", json!({"path": j.to_string_lossy()})).unwrap();
|
|
assert!(s.project.items.values().any(|i| i.name == "From Steps"));
|
|
// Menu commands by name.
|
|
let o = ok(&mut s, "app.findMenuCommandId('New Project') > 0 && app.findMenuCommandId('No Such Thing') === 0");
|
|
assert_eq!(o.result, json!(true));
|
|
let o = ok(&mut s, "app.executeCommand(app.findMenuCommandId('New Project')); app.project.numItems");
|
|
assert_eq!(o.result, json!(0));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn console_keeps_variables() {
|
|
let mut s = session();
|
|
let req = |code: &'static str| effectcraft_engine::ScriptRequest { code, name: "console", console: true };
|
|
let r = crate::run(&mut s, &req("var counter = 41;"));
|
|
assert!(r.error.is_none(), "{r:?}");
|
|
let r = crate::run(&mut s, &req("counter + 1"));
|
|
assert_eq!(r.result, json!(42));
|
|
let _ = Value::Null;
|
|
}
|
|
|
|
#[test]
|
|
fn schedule_task_runs_after_the_script() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
"app.scheduleTask('writeLn(\"later\")', 100, false); var id = app.scheduleTask('writeLn(\"never\")', 10, false); app.cancelTask(id); writeLn('now');",
|
|
);
|
|
assert_eq!(o.output, vec!["now", "later"]);
|
|
}
|
|
|
|
#[test]
|
|
fn layer_switches_timing_and_comp_settings() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var c = app.project.items.addComp("S", 320, 240, 1, 5, 30);
|
|
var matte = c.layers.addText("M");
|
|
var fill = c.layers.addSolid([1, 1, 1], "Fill", 320, 240, 1);
|
|
fill.moveToEnd();
|
|
fill.setTrackMatte(matte, TrackMatteType.LUMA);
|
|
var t1 = fill.trackMatteType == TrackMatteType.LUMA && fill.hasTrackMatte && fill.trackMatteLayer.name == "M";
|
|
fill.stretch = 200;
|
|
fill.label = 3;
|
|
c.width = 400;
|
|
c.duration = 8;
|
|
c.bgColor = [0.1, 0.2, 0.3];
|
|
c.time = 2;
|
|
c.workAreaStart = 1;
|
|
c.workAreaDuration = 3;
|
|
var extra = c.layers.addNull();
|
|
extra.remove();
|
|
fill.selected = true;
|
|
var sel = c.selectedLayers.length;
|
|
fill.transform.opacity.expression = "thisLayer.nope(";
|
|
var err = fill.transform.opacity.expressionError;
|
|
fill.transform.opacity.expressionEnabled = false;
|
|
var copy = c.duplicate();
|
|
var f = app.project.items.addFolder("Bin");
|
|
copy.parentFolder = f;
|
|
[t1, fill.stretch, fill.label, c.width, c.duration, c.bgColor[2], c.time, c.workAreaStart, c.workAreaDuration,
|
|
c.numLayers, sel, err.length > 0, fill.transform.opacity.expressionEnabled, copy.name, f.numItems, copy.parentFolder.name]
|
|
"#,
|
|
);
|
|
let v = o.result;
|
|
assert_eq!(v[0], json!(true), "{v}");
|
|
assert_eq!(v[1], json!(200));
|
|
assert_eq!(v[2], json!(3));
|
|
assert_eq!(v[3], json!(400));
|
|
assert_eq!(v[4], json!(8));
|
|
assert!((v[5].as_f64().unwrap() - 0.3).abs() < 1e-6);
|
|
assert_eq!(v[6], json!(2));
|
|
assert_eq!(v[7], json!(1));
|
|
assert_eq!(v[8], json!(3));
|
|
assert_eq!(v[9], json!(2));
|
|
assert_eq!(v[10], json!(1));
|
|
assert_eq!(v[11], json!(true));
|
|
assert_eq!(v[12], json!(false));
|
|
assert_eq!(v[13], json!("S 2"));
|
|
assert_eq!(v[14], json!(1));
|
|
assert_eq!(v[15], json!("Bin"));
|
|
// Removing the copy through the object model.
|
|
let o = ok(&mut s, "var n = app.project.numItems; app.project.item(1).remove(); n - app.project.numItems");
|
|
assert_eq!(o.result, json!(1));
|
|
}
|
|
|
|
#[test]
|
|
fn motion_graphics_template_hooks_add_mirrors_and_controllers() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var comp = app.project.items.addComp("Card", 64, 64, 1, 2, 30);
|
|
var bg = comp.layers.addSolid([1, 1, 1], "BG", 64, 64, 1);
|
|
var op = bg.transform.opacity;
|
|
var r = [op.canAddToMotionGraphicsTemplate(comp), op.addToMotionGraphicsTemplateAs(comp, "Fade")];
|
|
// Again: a mirror of the same property (a controller of its own).
|
|
r.push(op.addToMotionGraphicsTemplate(comp));
|
|
r.push(comp.motionGraphicsTemplateControllerCount);
|
|
r.push(comp.getMotionGraphicsTemplateControllerName(1), comp.getMotionGraphicsTemplateControllerName(2));
|
|
comp.setMotionGraphicsControllerName(2, "Fade Again");
|
|
r.push(comp.getMotionGraphicsTemplateControllerName(2));
|
|
r
|
|
"#,
|
|
);
|
|
assert_eq!(o.result, json!([true, true, true, 2, "Fade", "Fade", "Fade Again"]));
|
|
let (_, c) = comp_named(&s, "Card");
|
|
let eg = c.essential.as_ref().unwrap();
|
|
assert!(matches!(eg.controls[1].kind, effectcraft_engine::project::essential::EgKind::Mirror { of } if of == eg.controls[0].id));
|
|
}
|
|
|
|
#[test]
|
|
fn comp_preserves_nested_frame_rate_and_resolution() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var comp = app.project.items.addComp("Nested", 320, 180, 1, 2, 12);
|
|
var before = [comp.preserveNestedFrameRate, comp.preserveNestedResolution];
|
|
comp.preserveNestedFrameRate = true;
|
|
comp.preserveNestedResolution = true;
|
|
before.concat([comp.preserveNestedFrameRate, comp.preserveNestedResolution]).join(",");
|
|
"#,
|
|
);
|
|
assert_eq!(o.result, json!("false,false,true,true"));
|
|
let (_, c) = comp_named(&s, "Nested");
|
|
assert!(c.preserve_frame_rate && c.preserve_resolution);
|
|
}
|