mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 13:37:37 -06:00
Merge main into fix/1063-text-animator-stacked
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
This commit is contained in:
@@ -10,6 +10,18 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::{Outcome, run_code};
|
||||
|
||||
mod callable;
|
||||
mod ease_presets;
|
||||
mod evalfile;
|
||||
mod include;
|
||||
mod panels;
|
||||
#[cfg(not(windows))]
|
||||
mod services;
|
||||
mod tasks;
|
||||
mod ui;
|
||||
mod ui_more;
|
||||
mod ui_mouse;
|
||||
|
||||
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);
|
||||
@@ -126,6 +138,65 @@ fn fill_parameters_use_documented_match_names() {
|
||||
assert_eq!(o.result, json!("Color|All Masks|ADBE Fill-0007"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stroke_parameters_use_after_effects_match_names() {
|
||||
// #1405: `ADBE Stroke-0002` is Stroke's Color in After Effects, not All Masks.
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"var c = app.project.items.addComp("S", 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 Stroke"), out = [];
|
||||
for (var i = 1; i <= 11; i++) out.push(fx.property("ADBE Stroke-" + ("000" + i).slice(-4)).name);
|
||||
out.push(fx.property("Color").matchName); out"#,
|
||||
);
|
||||
assert_eq!(
|
||||
o.result,
|
||||
json!([
|
||||
"Path",
|
||||
"Color",
|
||||
"Brush Size",
|
||||
"Brush Hardness",
|
||||
"Opacity",
|
||||
"Spacing",
|
||||
"Paint Style",
|
||||
"Start",
|
||||
"End",
|
||||
"All Masks",
|
||||
"Stroke Sequentially",
|
||||
"ADBE Stroke-0002"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hue_saturation_parameters_use_after_effects_match_names() {
|
||||
// #1405: `ADBE HUE SATURATION-0004` is Master Hue in After Effects. The per-channel controls,
|
||||
// which After Effects doesn't expose, are numbered after its parameters so none takes an AE number.
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"var c = app.project.items.addComp("H", 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 HUE SATURATION"), out = [];
|
||||
var n = [2, 4, 5, 6, 7, 8, 9, 10];
|
||||
for (var i = 0; i < n.length; i++) out.push(fx.property("ADBE HUE SATURATION-" + ("000" + n[i]).slice(-4)).name);
|
||||
out.push(fx.property("Red Hue").matchName); out"#,
|
||||
);
|
||||
assert_eq!(
|
||||
o.result,
|
||||
json!([
|
||||
"Channel Control",
|
||||
"Master Hue",
|
||||
"Master Saturation",
|
||||
"Master Lightness",
|
||||
"Colorize",
|
||||
"Colorize Hue",
|
||||
"Colorize Saturation",
|
||||
"Colorize Lightness",
|
||||
"ADBE HUE SATURATION-0011"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_steps_without_a_group() {
|
||||
let mut s = session();
|
||||
@@ -199,6 +270,44 @@ fn an_undefined_name_mentioned_earlier_reports_its_use_in_the_reading_function()
|
||||
assert_eq!(e.line, Some(6), "{e:?}");
|
||||
}
|
||||
|
||||
/// Scripts reach Wiggle Paths' properties, Spatial Phase included, by their match names.
|
||||
#[test]
|
||||
fn wiggle_paths_properties_by_match_name() {
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"
|
||||
var c = app.project.items.addComp("W", 200, 200, 1, 2, 30);
|
||||
var sh = c.layers.addShape();
|
||||
var w = sh.property("ADBE Root Vectors Group").addProperty("ADBE Vector Filter - Roughen");
|
||||
w.property("ADBE Vector Spatial Phase").setValue(45);
|
||||
var names = [];
|
||||
for (var i = 1; i <= w.numProperties; i++) names.push(w.property(i).matchName);
|
||||
[w.matchName, w.property("ADBE Vector Spatial Phase").name, names]
|
||||
"#,
|
||||
);
|
||||
assert_eq!(
|
||||
o.result,
|
||||
json!([
|
||||
"ADBE Vector Filter - Roughen",
|
||||
"Spatial Phase",
|
||||
[
|
||||
"ADBE Vector Roughen Size",
|
||||
"ADBE Vector Roughen Detail",
|
||||
"ADBE Vector Roughen Points",
|
||||
"ADBE Vector Temporal Freq",
|
||||
"ADBE Vector Correlation",
|
||||
"ADBE Vector Temporal Phase",
|
||||
"ADBE Vector Spatial Phase",
|
||||
"ADBE Vector Random Seed"
|
||||
]
|
||||
])
|
||||
);
|
||||
let (_, c) = comp_named(&s, "W");
|
||||
let sh = c.layers.iter().find(|l| l.props.sub("contents").is_some()).unwrap();
|
||||
assert_eq!(sh.props.prop("contents/wiggle/spatialPhase").unwrap().value, KV::Scalar(45.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_names_and_lookups() {
|
||||
let mut s = session();
|
||||
@@ -385,6 +494,38 @@ fn layers_items_and_markers() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folder_paths_trim_trailing_separators() {
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"
|
||||
var paths = ['', '/', '///', 'a/b/', 'a\\b\\/', 'a/b', 'a/\n'];
|
||||
var result = paths.map(function (path) {
|
||||
var folder = new Folder(path);
|
||||
return [folder.name, folder.path, __join(path, 'child')];
|
||||
});
|
||||
// A long run followed by a non-separator used to cause quadratic regex backtracking.
|
||||
var longPath = Array(20001).join('/') + 'x';
|
||||
result.push([new Folder(longPath).name, new Folder(longPath).path.length, __join(longPath, 'child').length]);
|
||||
result
|
||||
"#,
|
||||
);
|
||||
assert_eq!(
|
||||
o.result,
|
||||
json!([
|
||||
["", "", "/child"],
|
||||
["", "", "/child"],
|
||||
["", "", "/child"],
|
||||
["b", "a", "a/b/child"],
|
||||
["b", "a", "a\\b/child"],
|
||||
["b", "a", "a/b/child"],
|
||||
["\n", "a", "a/\n/child"],
|
||||
["x", 19999, 20007]
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_gate_for_files() {
|
||||
let dir = std::env::temp_dir().join(format!("ec-script-gate-{}", std::process::id()));
|
||||
@@ -552,6 +693,26 @@ fn engine_entry_points() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// `app.project.framesCountType` / `displayStartFrame`: Project Settings ▸ Frame Count (#1807).
|
||||
#[test]
|
||||
fn frames_count_type_sets_the_project_frame_count() {
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
"var p = app.project; var a = [p.framesCountType === FramesCountType.FC_START_0, p.displayStartFrame]; p.framesCountType = FramesCountType.FC_START_1; a.concat([p.framesCountType === FramesCountType.FC_START_1, p.displayStartFrame]).join('|')",
|
||||
);
|
||||
assert_eq!(o.result, json!("true|0|true|1"));
|
||||
// After Effects' enum values, so scripts that store the number keep working.
|
||||
let o = ok(&mut s, "[FramesCountType.FC_START_0, FramesCountType.FC_START_1, FramesCountType.FC_TIMECODE_CONVERSION].join('|')");
|
||||
assert_eq!(o.result, json!("2612|2613|2614"));
|
||||
assert_eq!(s.project.settings.frame_start, 1);
|
||||
ok(&mut s, "app.project.displayStartFrame = 0");
|
||||
assert_eq!(s.project.settings.frame_start, 0);
|
||||
let o = run_code(&mut s, "app.project.framesCountType = FramesCountType.FC_TIMECODE_CONVERSION", "test.jsx");
|
||||
assert!(o.error.is_some_and(|e| e.message.contains("Timecode Conversion")));
|
||||
assert_eq!(s.project.settings.frame_start, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_keeps_variables() {
|
||||
let mut s = session();
|
||||
@@ -904,6 +1065,37 @@ fn footage_proxies_sequences_and_conform_rate() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #813: `AVLayer.replaceSource` changes the source item and keeps the layer's name, timing and
|
||||
/// properties; a folder is not a source.
|
||||
#[test]
|
||||
fn av_layer_replace_source_keeps_the_layer() {
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"
|
||||
var main = app.project.items.addComp("Main", 100, 100, 1, 5, 25);
|
||||
var layer = main.layers.addSolid([1, 0, 0], "Old", 100, 100, 1);
|
||||
layer.transform.position.setValue([10, 20]);
|
||||
layer.startTime = 1;
|
||||
var nested = app.project.items.addComp("Nested", 200, 100, 1, 3, 25);
|
||||
layer.replaceSource(nested, false);
|
||||
[layer.source.name, layer.source.width, layer.source.height, layer.source instanceof CompItem,
|
||||
layer.name, layer.transform.position.value[0], layer.startTime, main.numLayers]
|
||||
"#,
|
||||
);
|
||||
assert_eq!(o.result, json!(["Nested", 200, 100, true, "Old", 10, 1, 1]));
|
||||
let bad = run_code(
|
||||
&mut s,
|
||||
r#"
|
||||
var main = app.project.items.addComp("Other", 100, 100, 1, 2, 25);
|
||||
var layer = main.layers.addSolid([1, 0, 0], "Layer", 100, 100, 1);
|
||||
layer.replaceSource(app.project.items.addFolder("F"), false);
|
||||
"#,
|
||||
"replace-source.jsx",
|
||||
);
|
||||
assert!(bad.error.as_ref().is_some_and(|e| e.message.contains("folder")), "{bad:?}");
|
||||
}
|
||||
|
||||
/// A flat green footage source, so `sampleImage` on a footage layer has known pixels.
|
||||
struct Flat;
|
||||
impl effectcraft_engine::render::FootageSource for Flat {
|
||||
@@ -1326,6 +1518,50 @@ fn key_interpolation_and_ease_apply_outside_the_active_comp() {
|
||||
assert!(v[3].as_f64().is_some_and(|x| (x - -44.5).abs() > 1.0), "{v}");
|
||||
}
|
||||
|
||||
/// #1531: a key the user selected in the property's comp stays selected, and only the named key
|
||||
/// changes (on the old code the change went to the selected key instead).
|
||||
/// The active comp's own key selection is untouched.
|
||||
#[test]
|
||||
fn a_per_key_method_in_another_comp_changes_only_its_key_and_keeps_both_selections() {
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"
|
||||
var C = app.project.items.addComp('C', 100, 100, 1, 10, 20);
|
||||
var p = C.layers.addNull().transform.rotation;
|
||||
p.setValuesAtTimes([0, 1, 2], [0, 90, 180]);
|
||||
var A = app.project.items.addComp('A', 100, 100, 1, 10, 20);
|
||||
A.openInViewer();
|
||||
var q = A.layers.addNull().transform.opacity;
|
||||
q.setValuesAtTimes([0, 1], [0, 100]);
|
||||
app.run('edit.deselectAll', {});
|
||||
q.setSelectedAtKey(2, true);
|
||||
// the user selected key 3 of C earlier: C remembers it while A is active
|
||||
p.setSelectedAtKey(3, true);
|
||||
p.setInterpolationTypeAtKey(1, KeyframeInterpolationType.HOLD);
|
||||
p.setTemporalEaseAtKey(2, [new KeyframeEase(0, 80)]);
|
||||
var hold = KeyframeInterpolationType.HOLD;
|
||||
var out = [p.keyOutInterpolationType(1) == hold, p.keyOutInterpolationType(3) == hold, p.keyInInterpolationType(3) == hold,
|
||||
p.keyInTemporalEase(2)[0].influence, p.keyInTemporalEase(3)[0].influence,
|
||||
q.keySelected(1), q.keySelected(2)];
|
||||
p.removeKey(1);
|
||||
out.push(p.numKeys, p.keyTime(1), p.keyValue(1), q.keySelected(2), app.project.activeItem.name);
|
||||
// C's remembered selection is live again once C is active: still the user's key (now 2)
|
||||
C.openInViewer();
|
||||
out.push(p.keySelected(1), p.keySelected(2));
|
||||
out;
|
||||
"#,
|
||||
);
|
||||
let v = o.result.as_array().unwrap();
|
||||
let ease = |i: usize| v[i].as_f64().unwrap();
|
||||
assert_eq!(&v[..3], &[json!(true), json!(false), json!(false)], "only key 1 holds: {v:?}");
|
||||
assert!((ease(3) - 80.0).abs() < 1e-6, "key 2 eased: {v:?}");
|
||||
assert!((ease(4) - 80.0).abs() > 1e-6, "key 3 untouched: {v:?}");
|
||||
assert_eq!(&v[5..7], &[json!(false), json!(true)], "A's key selection kept: {v:?}");
|
||||
assert_eq!(&v[7..12], &[json!(2), json!(1), json!(90), json!(true), json!("A")], "key 1 removed, A still active: {v:?}");
|
||||
assert_eq!(&v[12..], &[json!(false), json!(true)], "C's key selection kept, the target key not added: {v:?}");
|
||||
}
|
||||
|
||||
/// #808: sourcePointToComp and compPointToSource follow the layer's transform.
|
||||
#[test]
|
||||
fn source_point_conversions_follow_the_layer_transform() {
|
||||
@@ -1503,3 +1739,68 @@ fn scripted_text_animator_property_is_the_one_asked_for() {
|
||||
);
|
||||
assert_eq!(o.result, json!(["Tracking Amount", 20, "Tracking Amount", "ADBE Text Tracking Amount", 2]));
|
||||
}
|
||||
|
||||
/// #1936: an array whose length doesn't match the property's dimension is an error in After
|
||||
/// Effects, not a value to truncate or broadcast. Two values for a 3-D property stay legal.
|
||||
#[test]
|
||||
fn set_value_rejects_an_array_of_the_wrong_dimension() {
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"
|
||||
var c = app.project.items.addComp("Dims", 640, 360, 1, 2, 30);
|
||||
var l = c.layers.addSolid([1, 0, 0], "s", 640, 360, 1);
|
||||
var ramp = l.property("ADBE Effect Parade").addProperty("ADBE Ramp").property("ADBE Ramp-0001");
|
||||
function attempt(f) { try { f(); return "no error"; } catch (e) { return e.message; } }
|
||||
var before = "" + ramp.value;
|
||||
var tooMany = attempt(function () { ramp.setValue([1, 2, 3]); });
|
||||
var tooFew = attempt(function () { ramp.setValue([1]); });
|
||||
var untouched = "" + ramp.value;
|
||||
ramp.setValue([7, 8]);
|
||||
var keyed = attempt(function () { ramp.setValueAtTime(0, [1, 2, 3, 4]); });
|
||||
// A 3-D property still takes two values, as it does in After Effects: z stays 0.
|
||||
l.property("Position").setValue([120, 34]);
|
||||
[tooMany, tooFew, untouched === before, "" + ramp.value, keyed, "" + l.property("Position").value]
|
||||
"#,
|
||||
);
|
||||
assert_eq!(
|
||||
o.result,
|
||||
json!([
|
||||
"Array is not of the correct dimension",
|
||||
"Array is not of the correct dimension",
|
||||
true,
|
||||
"7,8",
|
||||
"Array is not of the correct dimension",
|
||||
"120,34,0"
|
||||
])
|
||||
);
|
||||
let (_, c) = comp_named(&s, "Dims");
|
||||
assert_eq!(layer(c, "s").props.prop("transform/position").unwrap().value, KV::Vec3([120.0, 34.0, 0.0]));
|
||||
}
|
||||
|
||||
/// #1942: the scripting width and height setters resize the frame only. Re-anchoring the old frame
|
||||
/// in the new one, which moves every unparented layer, belongs to the Composition Settings dialog.
|
||||
#[test]
|
||||
fn setting_comp_width_and_height_leaves_the_layers_where_they_are() {
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"
|
||||
var c = app.project.items.addComp("w", 400, 200, 1, 1, 25);
|
||||
var l = c.layers.addSolid([1, 1, 1], "s", 50, 50, 1, 1);
|
||||
var pos = l.property("ADBE Transform Group").property("ADBE Position");
|
||||
pos.setValue([100, 100]);
|
||||
var k = c.layers.addSolid([1, 1, 1], "k", 50, 50, 1, 1);
|
||||
var kpos = k.property("ADBE Transform Group").property("ADBE Position");
|
||||
kpos.setValueAtTime(0, [10, 20]);
|
||||
kpos.setValueAtTime(1, [30, 40]);
|
||||
var before = "" + pos.value;
|
||||
c.width = 800;
|
||||
c.height = 400;
|
||||
[before, "" + pos.value, "" + kpos.keyValue(1), "" + kpos.keyValue(2), c.width, c.height]
|
||||
"#,
|
||||
);
|
||||
assert_eq!(o.result, json!(["100,100,0", "100,100,0", "10,20,0", "30,40,0", 800, 400]));
|
||||
let (_, c) = comp_named(&s, "w");
|
||||
assert_eq!(layer(c, "s").props.prop("transform/position").unwrap().value, KV::Vec3([100.0, 100.0, 0.0]));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user