Scripting: Google Earth Studio camera scripts key the camera (fixes #1077)

CameraLayer gets the transform and camera-options shortcuts (position, xRotation, zoom...) that AVLayer already had, and Convert Expression to Keyframes run without a property converts the selected ones, as app.executeCommand does it.
This commit is contained in:
Marco Arnulfo committed 2026-10-12 22:02:59 +02:00
1 parent 470ea7a3af
commit a179714108
3 files changed
+91 -1

No files matched your search

+21 -1
View File
@@ -1144,7 +1144,27 @@ fn velocity(s: &mut Session, p: &Value) -> Result<Value> {
}
fn convert_expr_to_keys(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid, uid) = prop_ref(s, p, "prop.convertExpressionToKeyframes")?;
const CMD: &str = "prop.convertExpressionToKeyframes";
// Without a property (the menu item, a script's `app.executeCommand`) it converts the selected ones.
if p.get("path").is_none() && p.get("prop").is_none() && p.get("layer").is_none() {
let cid = comp_id(s, p)?;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let sel: Vec<(LayerId, Uid)> =
selected_leaf_props(s).into_iter().filter(|(l, u)| comp.layer(*l).and_then(|l| l.props.find(*u)).is_some_and(Property::has_expression)).collect();
if sel.is_empty() {
return Err(bad(CMD, "no `path` given and no selected property has an expression"));
}
let mut n = 0;
for (lid, uid) in sel {
n += convert_one_expr(s, cid, lid, uid)?.as_u64().unwrap_or(0);
}
return Ok(json!(n));
}
let (cid, lid, uid) = prop_ref(s, p, CMD)?;
convert_one_expr(s, cid, lid, uid)
}
fn convert_one_expr(s: &mut Session, cid: ItemId, lid: LayerId, uid: Uid) -> Result<Value> {
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone();
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?.clone();
let pr = layer.props.find(uid).ok_or_else(|| bad("prop.convertExpressionToKeyframes", "no property"))?.clone();
+8
View File
@@ -1304,6 +1304,14 @@ Object.defineProperty(ShapeLayer.prototype, "source", { get: function () { retur
function CameraLayer(comp, id) { return this.__init(comp, id, "CameraLayer"); }
CameraLayer.prototype = Object.create(Layer.prototype);
CameraLayer.prototype.constructor = CameraLayer;
// Camera scripts (Google Earth Studio exports among them) key `cameraLayer.position`,
// `.xRotation` and `.zoom` directly, as on AVLayer: return the live Property objects.
["pointOfInterest", "position", "orientation", "xRotation", "yRotation", "zRotation"].forEach(function (name) {
Object.defineProperty(CameraLayer.prototype, name, { get: function () { return this.transform[name]; } });
});
["zoom", "depthOfField", "focusDistance", "aperture", "blurLevel"].forEach(function (name) {
Object.defineProperty(CameraLayer.prototype, name, { get: function () { return this.cameraOptions[name]; } });
});
function LightLayer(comp, id) { return this.__init(comp, id, "LightLayer"); }
LightLayer.prototype = Object.create(Layer.prototype);
+62
View File
@@ -1010,3 +1010,65 @@ fn scripted_text_range_selector_resolves_and_is_the_only_selector() {
);
assert_eq!(o.result, json!([1, "ADBE Text Selector", 50, 100, "ADBE Text Percent Offset"]));
}
/// A Google Earth Studio camera export: transform and camera shortcuts on the camera layer, keys
/// set through them, and Convert Expression to Keyframes run on the selected property (#1077).
#[test]
fn earth_studio_style_script_keys_the_camera() {
let mut s = session();
let o = ok(
&mut s,
r#"
var comp = app.project.items.addComp("Orbit", 1920, 1080, 1, 1, 10);
comp.openInViewer();
var cam = comp.layers.addCamera("Camera", ["960", "540"]);
cam.locked = true;
var pos = cam.position, rx = cam.xRotation, ry = cam.yRotation, rz = cam.zRotation, zoom = cam.zoom;
cam.autoOrient = AutoOrientType.NO_AUTO_ORIENT;
var ints = comp.layers.addNull();
ints.name = "ints";
ints.threeDLayer = true;
ints.position.setValuesAtTimes([0, 0.5], [[100, 200, 300], [110, 220, 330]]);
rx.setValuesAtTimes([0, 0.5], [181, 182]);
ry.setValuesAtTimes([0, 0.5], [39, 38]);
rz.setValuesAtTimes([0, 0.5], [87, 86]);
pos.expression = 'thisComp.layer("ints").transform.position';
var id = app.findMenuCommandId("Convert Expression to Keyframes");
if (id && pos.canSetExpression && pos.expressionEnabled) {
pos.selected = true;
app.executeCommand(id);
pos.selected = false;
ints.remove();
}
zoom.setValueAtTime(0, 1500);
zoom.setValueAtTime(0.5, 1600);
[pos.numKeys, rx.numKeys, ry.numKeys, rz.numKeys, zoom.numKeys, comp.numLayers,
cam.pointOfInterest.matchName, cam.orientation.matchName, cam.depthOfField.matchName,
cam.focusDistance.matchName, cam.aperture.matchName, cam.blurLevel.matchName]
"#,
);
assert_eq!(
o.result,
json!([
10,
2,
2,
2,
2,
1,
"ADBE Anchor Point",
"ADBE Orientation",
"ADBE Camera Depth of Field",
"ADBE Camera Focus Distance",
"ADBE Camera Aperture",
"ADBE Camera Blur Level"
])
);
let (_, c) = comp_named(&s, "Orbit");
let cam = layer(c, "Camera");
let p = cam.props.prop("transform/position").unwrap();
assert_eq!(p.keys[5].value, KV::Vec3([110.0, 220.0, 330.0]));
assert!(!p.expr.as_ref().unwrap().enabled);
assert_eq!(cam.props.prop("transform/rotationX").unwrap().keys[1].value, KV::Scalar(182.0));
assert_eq!(cam.props.prop("cameraOptions/zoom").unwrap().keys[1].value, KV::Scalar(1600.0));
}