diff --git a/crates/engine/src/commands/prop.rs b/crates/engine/src/commands/prop.rs index 8246d441..29da9c1e 100644 --- a/crates/engine/src/commands/prop.rs +++ b/crates/engine/src/commands/prop.rs @@ -1176,7 +1176,27 @@ fn velocity(s: &mut Session, p: &Value) -> Result { } fn convert_expr_to_keys(s: &mut Session, p: &Value) -> Result { - 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 { 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(); diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index 07446187..13fa0364 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -1318,6 +1318,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); diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index 71d3843f..d2456c3e 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -1242,3 +1242,65 @@ fn light_options_accept_the_after_effects_casts_shadows_name() { ); assert_ne!(o.result, json!("null"), "{:?}", o.result); } + +/// 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)); +}