M13.11: essential graphics mirrored and linked properties

Adding a property already in the Essential Graphics panel adds a mirror
(EgKind::Mirror: own name and place, one value in the comp and in every
instance; editing, reverting or pushing either acts on both). A property
control can also drive further properties of the comp (links follow the
main property both ways and take instance values). essential.addMirror,
essential.linkProperty, essential.unlinkProperty; panel badges and menu;
ExtendScript addToMotionGraphicsTemplate(As), canAddToMotionGraphicsTemplate,
motionGraphicsTemplateName, controller count/name and
exportAsMotionGraphicsTemplate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-03 09:38:19 -04:00
co-authored by Claude Opus 5.5
parent bb24b931a6
commit d774d11231
6 changed files with 628 additions and 55 deletions
+49 -3
View File
@@ -805,8 +805,22 @@ Property.prototype.property = function () { return null; };
};
P.keyLabel = function () { return 0; };
P.setLabelAtKey = function () {};
P.addToMotionGraphicsTemplate = function () { return false; };
P.canAddToMotionGraphicsTemplate = function () { return false; };
// Essential Graphics: `comp` must be the comp holding the property. A property already in
// the panel is added again as a mirror (both controls edit one value).
P.canAddToMotionGraphicsTemplate = function (comp) {
var c = comp && comp.__id !== undefined ? comp.__id : this.__layer.__comp;
if (c !== this.__layer.__comp) return false;
var sup = __call("essential.list", { comp: c, supported: true }).supported || [], uid = this.__uid;
for (var i = 0; i < sup.length; i++) if (sup[i].uid === uid) return true;
return false;
};
P.addToMotionGraphicsTemplateAs = function (comp, name) {
if (!this.canAddToMotionGraphicsTemplate(comp)) return false;
var p = this.__ref();
if (name !== undefined) p.name = String(name);
return __call("essential.addProperty", p).controls.length > 0;
};
P.addToMotionGraphicsTemplate = function (comp) { return this.addToMotionGraphicsTemplateAs(comp); };
})();
// Layer / composition markers (`layer.marker`, `comp.markerProperty`): keyframes are markers.
@@ -1352,7 +1366,39 @@ CompItem.prototype.constructor = CompItem;
};
P.openInViewer = function () { __call("comp.open", { comp: this.__id }); return null; };
P.duplicate = function () { return __item(__call("project.duplicate", { items: [this.__id] }).items[0]); };
P.exportAsMotionGraphicsTemplate = function () { return false; };
// Essential Graphics template: name, controllers (property, mirror and media controls in
// panel order, 1-based) and export.
P.__egControllers = function () {
var out = [];
(function go(v) {
for (var i = 0; i < v.length; i++) {
if (v[i].kind === "group") go(v[i].children || []);
else if (v[i].kind !== "comment") out.push(v[i]);
}
})(__call("essential.list", { comp: this.__id }).controls || []);
return out;
};
def("motionGraphicsTemplateName", function () { return __call("essential.list", { comp: this.__id }).name; }, function (v) {
__call("essential.setName", { comp: this.__id, name: String(v) });
});
def("motionGraphicsTemplateControllerCount", function () { return this.__egControllers().length; });
P.getMotionGraphicsTemplateControllerName = function (i) {
var c = this.__egControllers()[__num(i, "index") - 1];
if (!c) throw __err("controller index out of range");
return c.name;
};
P.setMotionGraphicsControllerName = function (i, name) {
var c = this.__egControllers()[__num(i, "index") - 1];
if (!c) throw __err("controller index out of range");
__call("essential.rename", { comp: this.__id, control: c.id, name: String(name) });
return String(name);
};
P.exportAsMotionGraphicsTemplate = function (overwrite, path) {
if (path === undefined) return false;
var p = path && path.fsName !== undefined ? path.fsName : String(path);
if (!overwrite && new File(p).exists) return false;
try { __call("essential.exportTemplate", { comp: this.__id, path: p }); return true; } catch (e) { return false; }
};
})();
function FootageItem(id) {
+38
View File
@@ -446,3 +446,41 @@ fn layer_switches_timing_and_comp_settings() {
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 dir = std::env::temp_dir().join(format!("effectcraft-eg-script-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("card.ectemplate");
let o = ok(
&mut s,
&format!(
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.
r.push(op.addToMotionGraphicsTemplate(comp));
r.push(comp.motionGraphicsTemplateControllerCount);
r.push(comp.getMotionGraphicsTemplateControllerName(1), comp.getMotionGraphicsTemplateControllerName(2));
comp.setMotionGraphicsControllerName(2, "Fade Again");
comp.motionGraphicsTemplateName = "Lower Card";
r.push(comp.getMotionGraphicsTemplateControllerName(2), comp.motionGraphicsTemplateName);
// A property of another comp can't be added.
var other = app.project.items.addComp("Other", 64, 64, 1, 2, 30);
r.push(op.canAddToMotionGraphicsTemplate(other));
r.push(comp.exportAsMotionGraphicsTemplate(true, new File("{}")));
r
"#,
path.to_string_lossy()
),
);
assert_eq!(o.result, json!([true, true, true, 2, "Fade", "Fade", "Fade Again", "Lower Card", false, true]));
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));
assert!(path.exists());
let _ = std::fs::remove_dir_all(&dir);
}