mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Merge main into #2279; keep main's Scripts subfolders (RESERVED_FOLDERS) next to the Startup/Shutdown constants, merge docs rows
This commit is contained in:
@@ -90,6 +90,44 @@ pub fn stroke_dash(name: &str) -> Option<&'static str> {
|
||||
|
||||
const TEXT_ANIMATOR: &[(&str, &str)] = &[("selectors", "ADBE Text Selectors"), ("properties", "ADBE Text Animator Properties")];
|
||||
|
||||
/// Animator property ids and their stable After Effects scripting names. Shared by lookup,
|
||||
/// `.matchName`, and `addProperty` so the same property has one name in every direction.
|
||||
pub const TEXT_ANIMATOR_PROPERTIES: &[(&str, &str)] = &[
|
||||
("anchor", "ADBE Text Anchor Point 3D"),
|
||||
("position", "ADBE Text Position 3D"),
|
||||
("scale", "ADBE Text Scale 3D"),
|
||||
("skew", "ADBE Text Skew"),
|
||||
("rotation", "ADBE Text Rotation"),
|
||||
("opacity", "ADBE Text Opacity"),
|
||||
("fillColor", "ADBE Text Fill Color"),
|
||||
("fillHue", "ADBE Text Fill Hue"),
|
||||
("fillSaturation", "ADBE Text Fill Saturation"),
|
||||
("fillBrightness", "ADBE Text Fill Brightness"),
|
||||
("fillOpacity", "ADBE Text Fill Opacity"),
|
||||
("strokeColor", "ADBE Text Stroke Color"),
|
||||
("strokeHue", "ADBE Text Stroke Hue"),
|
||||
("strokeSaturation", "ADBE Text Stroke Saturation"),
|
||||
("strokeBrightness", "ADBE Text Stroke Brightness"),
|
||||
("strokeOpacity", "ADBE Text Stroke Opacity"),
|
||||
("strokeWidth", "ADBE Text Stroke Width"),
|
||||
("tracking", "ADBE Text Tracking Amount"),
|
||||
("lineAnchor", "ADBE Text Line Anchor"),
|
||||
("lineSpacing", "ADBE Text Line Spacing"),
|
||||
("characterOffset", "ADBE Text Character Offset"),
|
||||
("characterValue", "ADBE Text Character Replace"),
|
||||
("blur", "ADBE Text Blur"),
|
||||
];
|
||||
|
||||
/// Companion properties that exist below an animator but are not kinds `addProperty` accepts:
|
||||
/// they come with Tracking (Track Type), Skew (Skew Axis) and per-character 3D Rotation
|
||||
/// (X and Y Rotation). Lookup and `.matchName` know them; `addProperty` does not.
|
||||
pub const TEXT_ANIMATOR_COMPANIONS: &[(&str, &str)] = &[
|
||||
("trackingType", "ADBE Text Track Type"),
|
||||
("skewAxis", "ADBE Text Skew Axis"),
|
||||
("rotationX", "ADBE Text Rotation X"),
|
||||
("rotationY", "ADBE Text Rotation Y"),
|
||||
];
|
||||
|
||||
const TEXT_SELECTORS: &[(&str, &str)] =
|
||||
&[("rangeSelector", "ADBE Text Selector"), ("wigglySelector", "ADBE Text Wiggly Selector"), ("expressionSelector", "ADBE Text Expressible Selector")];
|
||||
|
||||
@@ -209,6 +247,14 @@ const SHAPE_PROPS: &[(&str, &str, &str)] = &[
|
||||
("twist", "angle", "ADBE Vector Twist Angle"),
|
||||
("twist", "center", "ADBE Vector Twist Center"),
|
||||
("merge", "mode", "ADBE Vector Merge Type"),
|
||||
("wiggle", "size", "ADBE Vector Roughen Size"),
|
||||
("wiggle", "detail", "ADBE Vector Roughen Detail"),
|
||||
("wiggle", "points", "ADBE Vector Roughen Points"),
|
||||
("wiggle", "speed", "ADBE Vector Temporal Freq"),
|
||||
("wiggle", "correlation", "ADBE Vector Correlation"),
|
||||
("wiggle", "phase", "ADBE Vector Temporal Phase"),
|
||||
("wiggle", "spatialPhase", "ADBE Vector Spatial Phase"),
|
||||
("wiggle", "seed", "ADBE Vector Random Seed"),
|
||||
];
|
||||
|
||||
/// Effects: our effect id → AE match name (the documented ones; the rest keep our id).
|
||||
@@ -479,6 +525,9 @@ pub fn node(chain: &[&Node]) -> String {
|
||||
"text" if pid == "animators" => "ADBE Text Animator".into(),
|
||||
"text" if pid == "selectors" => find(TEXT_SELECTORS, id).map(str::to_string).unwrap_or_else(own),
|
||||
"text" if pid == "rangeSelector" => find(TEXT_RANGE_SELECTOR, id).map(str::to_string).unwrap_or_else(own),
|
||||
"text" if pid == "properties" && chain.len() == 5 && chain.get(1).is_some_and(|n| n.match_id() == "animators") => {
|
||||
find(TEXT_ANIMATOR_PROPERTIES, id).or_else(|| find(TEXT_ANIMATOR_COMPANIONS, id)).map(str::to_string).unwrap_or_else(own)
|
||||
}
|
||||
"text" if grand.map(|g| g.match_id()) == Some("animators") => find(TEXT_ANIMATOR, id).map(str::to_string).unwrap_or_else(own),
|
||||
"cameraOptions" => find(CAMERA, id).map(str::to_string).unwrap_or_else(own),
|
||||
"lightOptions" => find(LIGHT, id).map(str::to_string).unwrap_or_else(own),
|
||||
@@ -529,6 +578,30 @@ mod tests {
|
||||
assert_eq!(shape_kind("nope"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_animator_property_names_are_canonical_and_scoped() {
|
||||
let n = |m: &str| Node::from(effectcraft_project::PropGroup::new(1, m, m));
|
||||
let (text, animators, animator, properties) = (n("text"), n("animators"), n("animator"), n("properties"));
|
||||
for (id, name) in TEXT_ANIMATOR_PROPERTIES {
|
||||
let prop = n(id);
|
||||
assert_eq!(node(&[&text, &animators, &animator, &properties, &prop]), *name);
|
||||
assert_eq!(node(&[&properties, &prop]), *id, "an unrelated Properties group keeps its own ids");
|
||||
}
|
||||
}
|
||||
|
||||
/// #1063: the companions an animator property brings (Track Type, Skew Axis, X / Y Rotation)
|
||||
/// have their After Effects names too, though `addProperty` does not accept them.
|
||||
#[test]
|
||||
fn text_animator_companions_have_after_effects_names() {
|
||||
let n = |m: &str| Node::from(effectcraft_project::PropGroup::new(1, m, m));
|
||||
let (text, animators, animator, properties) = (n("text"), n("animators"), n("animator"), n("properties"));
|
||||
for (id, name) in TEXT_ANIMATOR_COMPANIONS {
|
||||
let prop = n(id);
|
||||
assert_eq!(node(&[&text, &animators, &animator, &properties, &prop]), *name);
|
||||
assert!(!TEXT_ANIMATOR_PROPERTIES.iter().any(|(k, m)| k == id || m == name), "{id} is not an addProperty kind");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_range_selector_names() {
|
||||
let n = |m: &str| Node::from(effectcraft_project::PropGroup::new(1, m, m));
|
||||
|
||||
@@ -743,36 +743,12 @@ fn resolve_add(s: &Session, a: &J) -> R {
|
||||
/// A text animator property (`layer.addTextAnimatorProperty` key) from an After Effects match
|
||||
/// name or a display name.
|
||||
fn text_animator_property(name: &str) -> Option<&'static str> {
|
||||
const AE: &[(&str, &str)] = &[
|
||||
("ADBE Text Anchor Point 3D", "anchor"),
|
||||
("ADBE Text Position 3D", "position"),
|
||||
("ADBE Text Scale 3D", "scale"),
|
||||
("ADBE Text Skew", "skew"),
|
||||
("ADBE Text Rotation", "rotation"),
|
||||
("ADBE Text Opacity", "opacity"),
|
||||
("ADBE Text Fill Color", "fillColor"),
|
||||
("ADBE Text Fill Hue", "fillHue"),
|
||||
("ADBE Text Fill Saturation", "fillSaturation"),
|
||||
("ADBE Text Fill Brightness", "fillBrightness"),
|
||||
("ADBE Text Fill Opacity", "fillOpacity"),
|
||||
("ADBE Text Stroke Color", "strokeColor"),
|
||||
("ADBE Text Stroke Hue", "strokeHue"),
|
||||
("ADBE Text Stroke Saturation", "strokeSaturation"),
|
||||
("ADBE Text Stroke Brightness", "strokeBrightness"),
|
||||
("ADBE Text Stroke Opacity", "strokeOpacity"),
|
||||
("ADBE Text Stroke Width", "strokeWidth"),
|
||||
("ADBE Text Tracking Amount", "tracking"),
|
||||
("ADBE Text Line Anchor", "lineAnchor"),
|
||||
("ADBE Text Line Spacing", "lineSpacing"),
|
||||
("ADBE Text Character Offset", "characterOffset"),
|
||||
("ADBE Text Character Replace", "characterValue"),
|
||||
("ADBE Text Blur", "blur"),
|
||||
];
|
||||
if let Some((_, k)) = AE.iter().find(|(m, _)| m.eq_ignore_ascii_case(name)) {
|
||||
let table = matchnames::TEXT_ANIMATOR_PROPERTIES;
|
||||
if let Some((k, _)) = table.iter().find(|(_, m)| m.eq_ignore_ascii_case(name)) {
|
||||
return Some(k);
|
||||
}
|
||||
let n = loose(name);
|
||||
AE.iter().map(|(_, k)| *k).find(|k| loose(k) == n || (n == "anchorpoint" && *k == "anchor") || (n == "trackingamount" && *k == "tracking"))
|
||||
table.iter().map(|(k, _)| *k).find(|k| loose(k) == n || (n == "anchorpoint" && *k == "anchor") || (n == "trackingamount" && *k == "tracking"))
|
||||
}
|
||||
|
||||
/// `app.findMenuCommandId(name)`: 1-based index of the command whose label matches (menu items
|
||||
|
||||
@@ -189,7 +189,12 @@ BridgeTalk.pump = function () { return false; };
|
||||
|
||||
// ------------------------------------------------------------------ File / Folder
|
||||
|
||||
function __join(a, b) { return a.replace(/[\/\\]+$/, "") + "/" + b; }
|
||||
function __trimPathSeparators(path) {
|
||||
var end = path.length;
|
||||
while (end > 0 && (path.charAt(end - 1) === "/" || path.charAt(end - 1) === "\\")) end--;
|
||||
return path.slice(0, end);
|
||||
}
|
||||
function __join(a, b) { return __trimPathSeparators(a) + "/" + b; }
|
||||
|
||||
function File(path) {
|
||||
if (!(this instanceof File)) return new File(path);
|
||||
@@ -292,9 +297,9 @@ Folder.prototype = {
|
||||
get fsName() { return this.__path; },
|
||||
get fullName() { return this.__path; },
|
||||
get absoluteURI() { return this.__path; },
|
||||
get name() { return this.__path.replace(/[\/\\]+$/, "").split(/[\/\\]/).pop(); },
|
||||
get name() { return __trimPathSeparators(this.__path).split(/[\/\\]/).pop(); },
|
||||
get displayName() { return this.name; },
|
||||
get path() { var p = this.__path.replace(/[\/\\]+$/, "").split(/[\/\\]/); p.pop(); return p.join("/"); },
|
||||
get path() { var p = __trimPathSeparators(this.__path).split(/[\/\\]/); p.pop(); return p.join("/"); },
|
||||
get parent() { return new Folder(this.path); },
|
||||
get exists() { return JSON.parse(__file("isDir", this.__path)) === true; },
|
||||
toString: function () { return this.__path; },
|
||||
@@ -729,9 +734,8 @@ PropertyGroup.prototype.addProperty = function (name) {
|
||||
} else if (r.kind === "animator") {
|
||||
uid = __call("layer.addTextAnimator", { comp: L.__comp, layer: L.__id, properties: [] }).animator;
|
||||
} else if (r.kind === "animatorProperty") {
|
||||
var before = this.numProperties;
|
||||
__call("layer.addTextAnimatorProperty", { comp: L.__comp, layer: L.__id, animator: r.animator, property: r.property });
|
||||
return this.property(before + 1);
|
||||
var added = __call("layer.addTextAnimatorProperty", { comp: L.__comp, layer: L.__id, animator: r.animator, property: r.property });
|
||||
return this.property(added.index);
|
||||
} else if (r.kind === "selector") {
|
||||
var n0 = this.numProperties;
|
||||
__call("layer.addTextSelector", { comp: L.__comp, layer: L.__id, animator: r.animator, kind: r.selector });
|
||||
|
||||
@@ -55,7 +55,7 @@ mod imp {
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::*;
|
||||
use super::{J, err, json};
|
||||
|
||||
enum Sock {
|
||||
Stream { s: TcpStream, buf: Vec<u8>, eof: bool },
|
||||
@@ -394,19 +394,33 @@ mod tests {
|
||||
|
||||
/// Fill bounded socket buffers nonblocking, rather than assuming a 64 MiB write stalls.
|
||||
/// In particular, Windows may accept that entire write with its default send buffering.
|
||||
/// Writes must stay blocked for [`SETTLED`] before the buffers count as full: on macOS the
|
||||
/// accepted stream does not keep the listener's 4 KiB receive buffer (it gets about 114 KiB),
|
||||
/// so the first `WouldBlock` only means the 4 KiB send buffer is full, and the send buffer then
|
||||
/// drains into the peer's larger receive buffer.
|
||||
fn backpressure(id: u32) {
|
||||
const SETTLED: Duration = Duration::from_millis(200);
|
||||
let mut stream = super::imp::test_stream(id);
|
||||
socket2::SockRef::from(&stream).set_send_buffer_size(4096).unwrap();
|
||||
stream.set_nonblocking(true).unwrap();
|
||||
let start = Instant::now();
|
||||
let chunk = [b'x'; 4096];
|
||||
let mut sent = 0;
|
||||
let mut blocked_since = None;
|
||||
loop {
|
||||
assert!(sent < 16 * 1024 * 1024 && start.elapsed() < Duration::from_secs(5), "could not establish backpressure");
|
||||
match stream.write(&chunk) {
|
||||
Ok(0) => panic!("peer closed while establishing backpressure"),
|
||||
Ok(n) => sent += n,
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => break,
|
||||
Ok(n) => {
|
||||
sent += n;
|
||||
blocked_since = None;
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
if blocked_since.get_or_insert_with(Instant::now).elapsed() >= SETTLED {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
|
||||
Err(e) => panic!("establishing backpressure: {e}"),
|
||||
}
|
||||
|
||||
@@ -271,6 +271,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();
|
||||
@@ -457,6 +495,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()));
|
||||
@@ -1650,6 +1720,50 @@ fn layer_property_resolves_transform_and_source_text_shortcuts() {
|
||||
assert_eq!(layer(c, "s").props.prop("transform/opacity").unwrap().value, KV::Scalar(25.0));
|
||||
}
|
||||
|
||||
/// A text animator property added by its After Effects name is the one asked for, not its Tracking
|
||||
/// Type companion, and it can be found again by that name (#1063).
|
||||
#[test]
|
||||
fn scripted_text_animator_property_is_the_one_asked_for() {
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"
|
||||
var c = app.project.items.addComp("Track", 960, 300, 1, 3, 30);
|
||||
var t = c.layers.addText("MOVARA");
|
||||
var a = t.property("ADBE Text Properties").property("ADBE Text Animators").addProperty("ADBE Text Animator");
|
||||
var props = a.property("ADBE Text Animator Properties");
|
||||
var added = props.addProperty("ADBE Text Tracking Amount");
|
||||
added.setValue(20);
|
||||
[added.name, props.property("ADBE Text Tracking Amount").value, props.property("ADBE Text Tracking Amount").name,
|
||||
added.matchName, props.property(added.matchName).propertyIndex]
|
||||
"#,
|
||||
);
|
||||
assert_eq!(o.result, json!(["Tracking Amount", 20, "Tracking Amount", "ADBE Text Tracking Amount", 2]));
|
||||
}
|
||||
|
||||
/// The companion properties added with Tracking and Skew are found by their After Effects names,
|
||||
/// but cannot be added on their own (#1063).
|
||||
#[test]
|
||||
fn text_animator_companion_properties_resolve_by_name() {
|
||||
let mut s = session();
|
||||
let o = ok(
|
||||
&mut s,
|
||||
r#"
|
||||
var c = app.project.items.addComp("Track", 960, 300, 1, 3, 30);
|
||||
var t = c.layers.addText("MOVARA");
|
||||
var props = t.property("ADBE Text Properties").property("ADBE Text Animators").addProperty("ADBE Text Animator").property("ADBE Text Animator Properties");
|
||||
props.addProperty("ADBE Text Tracking Amount");
|
||||
props.addProperty("ADBE Text Skew");
|
||||
var type = props.property("ADBE Text Track Type");
|
||||
var axis = props.property("ADBE Text Skew Axis");
|
||||
var added = "";
|
||||
try { props.addProperty("ADBE Text Track Type"); added = "added"; } catch (e) { added = "refused"; }
|
||||
[type ? type.matchName : null, axis ? axis.matchName : null, added]
|
||||
"#,
|
||||
);
|
||||
assert_eq!(o.result, json!(["ADBE Text Track Type", "ADBE Text Skew Axis", "refused"]));
|
||||
}
|
||||
|
||||
/// #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]
|
||||
|
||||
@@ -608,3 +608,37 @@ fn presets_saved_by_the_old_core_panel_carry_over() {
|
||||
let win = open_panel(&mut s);
|
||||
assert_eq!(control(&mut s, win, "presets")["items"].as_array().unwrap().len(), 12);
|
||||
}
|
||||
|
||||
/// #2465: the Linear preset sets Linear interpolation on every side it applies to, including the
|
||||
/// unused first-in and last-out sides; editing its speeds makes a Bezier curve.
|
||||
#[test]
|
||||
fn the_linear_preset_sets_linear_interpolation_and_edited_speeds_give_bezier() {
|
||||
let store = Arc::new(MemoryConfig::default());
|
||||
let mut s = session(&store);
|
||||
let l = setup(&mut s);
|
||||
animate(&mut s, l, "transform/opacity", &[(0.0, json!(0)), (1.0, json!(100)), (3.0, json!(20))]);
|
||||
s.execute("prop.select", json!({"layer": l, "path": "transform/opacity"})).unwrap();
|
||||
let win = open_panel(&mut s);
|
||||
// Start from Bezier keys so Linear has something to restore.
|
||||
ok(
|
||||
&mut s,
|
||||
r#"var p = app.project.activeItem.layer(1).transform.opacity, K = KeyframeInterpolationType;
|
||||
for (var i = 1; i <= p.numKeys; i++) p.setInterpolationTypeAtKey(i, K.BEZIER, K.BEZIER);"#,
|
||||
);
|
||||
let op = prop(&s, l, "transform/opacity");
|
||||
for (i, k) in op.keys.iter().enumerate() {
|
||||
assert_eq!((k.in_interp, k.out_interp), (Interp::Bezier, Interp::Bezier), "initial key {i}");
|
||||
}
|
||||
let steps = s.history.undo.len();
|
||||
set(&mut s, win, "presets", json!("Linear"));
|
||||
assert_eq!(s.history.undo.len(), steps + 1, "one undo step");
|
||||
let op = prop(&s, l, "transform/opacity");
|
||||
for (i, k) in op.keys.iter().enumerate() {
|
||||
assert_eq!((k.in_interp, k.out_interp), (Interp::Linear, Interp::Linear), "key {i}");
|
||||
}
|
||||
// Edited speeds make a custom Bezier curve even though Linear is still the selected preset.
|
||||
select_keys(&mut s, l, &[("transform/opacity", 1.0)]);
|
||||
apply_typed(&mut s, win, "In-Out", [33.0, 2.0, 33.0, 2.0]);
|
||||
let k = &prop(&s, l, "transform/opacity").keys[1];
|
||||
assert_eq!((k.in_interp, k.out_interp), (Interp::Bezier, Interp::Bezier));
|
||||
}
|
||||
|
||||
@@ -86,3 +86,27 @@ fn open_panels_open_again_at_the_next_launch() {
|
||||
let mut s = session(&store);
|
||||
assert_eq!(s.execute("window.restoreScriptPanels", json!({})).unwrap(), json!({"opened": [], "failed": []}));
|
||||
}
|
||||
|
||||
/// A panel in a subfolder (Window ▸ Motion Kit ▸ Animate.jsx) is `Motion Kit/Animate.jsx`: it opens
|
||||
/// titled after its file and opens again at the next launch.
|
||||
#[test]
|
||||
fn a_panel_in_a_subfolder_opens_and_opens_again_at_the_next_launch() {
|
||||
let store = Arc::new(MemoryConfig::default());
|
||||
store.write(&format!("{PANELS_DIR}/Motion Kit/Animate.jsx"), PANEL).unwrap();
|
||||
let mut s = session(&store);
|
||||
let list = s.execute("file.scripts.list", json!({})).unwrap();
|
||||
assert!(list.as_array().unwrap().iter().any(|e| e["name"] == "Motion Kit/Animate.jsx" && e["panel"] == true), "{list}");
|
||||
let r = s.execute("window.scriptPanel", json!({"name": "Motion Kit/Animate.jsx"})).unwrap();
|
||||
let w = s.script_ui.window(r["window"].as_u64().unwrap() as u32).unwrap();
|
||||
assert_eq!((w.kind, w.title.as_str()), (WindowKind::Panel, "Animate"));
|
||||
assert_eq!(remembered(&store), json!({"open": ["Motion Kit/Animate.jsx"]}));
|
||||
// A saved name that leaves the folder, or goes two folders deep, fails and is forgotten.
|
||||
store.write(OPEN_PANELS_FILE, &json!({"open": ["../Top.jsx", "Motion Kit/Animate.jsx", "a/b/c.jsx"]}).to_string()).unwrap();
|
||||
drop(s);
|
||||
let mut s = session(&store);
|
||||
let r = s.execute("window.restoreScriptPanels", json!({})).unwrap();
|
||||
assert_eq!(r["opened"], json!(["Motion Kit/Animate.jsx"]));
|
||||
assert_eq!(r["failed"].as_array().unwrap().iter().map(|f| f["name"].as_str().unwrap()).collect::<Vec<_>>(), ["../Top.jsx", "a/b/c.jsx"]);
|
||||
assert!(panel_window(&s, "Motion Kit/Animate.jsx").is_some());
|
||||
assert_eq!(remembered(&store), json!({"open": ["Motion Kit/Animate.jsx"]}));
|
||||
}
|
||||
|
||||
@@ -222,7 +222,16 @@ fn position_now(s: &Session, layer: u64) -> [f64; 2] {
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let comp = s.project.comp(cid).unwrap();
|
||||
let l = comp.layer(effectcraft_engine::project::LayerId(layer)).unwrap();
|
||||
let ctx = effectcraft_engine::render::EvalCtx { project: &s.project, comp_id: cid, comp, time: s.time(), expr: s.expr.as_deref(), footage: None };
|
||||
let ctx = effectcraft_engine::render::EvalCtx {
|
||||
project: &s.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: s.time(),
|
||||
expr: s.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
let v = ctx.value(l, l.props.prop("transform/position").unwrap()).components();
|
||||
[v[0], v[1]]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user