@engine-room/after-effects-mcp 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/bin/server.js +275 -53
- package/package.json +1 -1
- package/panel/CSXS/manifest.xml +2 -2
- package/panel/client/framecache.js +98 -0
- package/panel/client/main.js +240 -15
- package/panel/client/mogrt.js +328 -0
- package/panel/client/pngcodec.js +402 -0
- package/panel/jsx/bundle.jsx +1187 -157
- package/panel/package.json +1 -1
package/panel/jsx/bundle.jsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Auto-generated bundle. Do not edit directly — edit files in packages/jsx/.
|
|
2
|
-
// Generated 2026-08-12T07:48:29.949Z
|
|
3
2
|
|
|
4
3
|
// ===== core.jsx =====
|
|
5
4
|
|
|
@@ -70,10 +69,29 @@ function __newJobId() {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
// ---------- Undo wrapper ----------
|
|
72
|
+
// __UNDO_OPEN tracks only the group *dispatch* opened. Nothing else may set it:
|
|
73
|
+
// it is what lets withoutUndoGroup() reopen exactly what it closed, and leave
|
|
74
|
+
// undo state alone entirely when we never opened a group in the first place.
|
|
75
|
+
var __UNDO_OPEN = false;
|
|
76
|
+
|
|
73
77
|
function withUndo(name, fn) {
|
|
74
78
|
app.beginUndoGroup(name || "AE MCP");
|
|
79
|
+
__UNDO_OPEN = true;
|
|
75
80
|
try { return fn(); }
|
|
76
|
-
finally { app.endUndoGroup(); }
|
|
81
|
+
finally { __UNDO_OPEN = false; app.endUndoGroup(); }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Run fn with the dispatcher's undo group closed, then reopen it.
|
|
85
|
+
// After Effects refuses a handful of operations while an undo group is open —
|
|
86
|
+
// copyToComp on a layer with a parent or a linked expression is the one that
|
|
87
|
+
// bites (issue #30). No-op when no group of ours is open, so it is always safe
|
|
88
|
+
// to call. The work inside becomes its own undo step, separate from the rest.
|
|
89
|
+
function withoutUndoGroup(fn) {
|
|
90
|
+
if (!__UNDO_OPEN) return fn();
|
|
91
|
+
app.endUndoGroup();
|
|
92
|
+
__UNDO_OPEN = false;
|
|
93
|
+
try { return fn(); }
|
|
94
|
+
finally { app.beginUndoGroup("AE MCP: continue"); __UNDO_OPEN = true; }
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
// ---------- Error helper ----------
|
|
@@ -97,7 +115,13 @@ function dispatch(payloadJson) {
|
|
|
97
115
|
try {
|
|
98
116
|
var handler = OPS[op];
|
|
99
117
|
var meta = handler.__meta || {};
|
|
100
|
-
|
|
118
|
+
// __meta.noUndo is a boolean for handlers that never want a group, or a
|
|
119
|
+
// predicate over args for ops where the caller decides per call (run_jsx).
|
|
120
|
+
// Keeping the predicate on the handler is what stops one op's opt-out from
|
|
121
|
+
// leaking into the next: dispatch never remembers anything between calls.
|
|
122
|
+
var skipUndo = meta.noUndo;
|
|
123
|
+
if (typeof skipUndo === "function") skipUndo = skipUndo(args);
|
|
124
|
+
if (skipUndo) {
|
|
101
125
|
return { ok: true, result: handler(args) };
|
|
102
126
|
}
|
|
103
127
|
var result = withUndo(meta.undoName || ("AE MCP: " + op), function () { return handler(args); });
|
|
@@ -109,6 +133,8 @@ function dispatch(payloadJson) {
|
|
|
109
133
|
|
|
110
134
|
// Helper for handlers that don't want an undo group (read-only ops, job continuations).
|
|
111
135
|
function noUndo(fn) { fn.__meta = { noUndo: true }; return fn; }
|
|
136
|
+
// Same, but the handler decides per call from its own args.
|
|
137
|
+
function noUndoWhen(pred, fn) { fn.__meta = { noUndo: pred }; return fn; }
|
|
112
138
|
function undoNamed(name, fn) { fn.__meta = { undoName: name }; return fn; }
|
|
113
139
|
|
|
114
140
|
|
|
@@ -165,27 +191,42 @@ function colorOrDefault(c, fallback) {
|
|
|
165
191
|
|
|
166
192
|
// comps.jsx — composition ops.
|
|
167
193
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
};
|
|
194
|
+
// The section filter every read op shares. `sections` is the caller's `include`
|
|
195
|
+
// array: null/undefined means "all of them", which is what every caller written
|
|
196
|
+
// before `include` existed passes. An empty array means the identifying core
|
|
197
|
+
// only. Defined here because comps.jsx is the first module in the bundle that
|
|
198
|
+
// needs it; layers.jsx and explore.jsx use the same one.
|
|
199
|
+
function __wantsSection(sections, name) {
|
|
200
|
+
if (!sections) return true;
|
|
201
|
+
for (var i = 0; i < sections.length; i++) if (sections[i] === name) return true;
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function __compSummary(c, sections) {
|
|
206
|
+
// id and name are the map an agent orients with, so they are never optional.
|
|
207
|
+
var out = { id: c.id, name: c.name };
|
|
208
|
+
if (__wantsSection(sections, "size")) {
|
|
209
|
+
out.width = c.width;
|
|
210
|
+
out.height = c.height;
|
|
211
|
+
out.pixelAspect = c.pixelAspect;
|
|
212
|
+
}
|
|
213
|
+
if (__wantsSection(sections, "timing")) {
|
|
214
|
+
out.duration = c.duration;
|
|
215
|
+
out.frameRate = c.frameRate;
|
|
216
|
+
out.workAreaStart = c.workAreaStart;
|
|
217
|
+
out.workAreaDuration = c.workAreaDuration;
|
|
218
|
+
}
|
|
219
|
+
if (__wantsSection(sections, "bg")) out.bgColor = [c.bgColor[0], c.bgColor[1], c.bgColor[2]];
|
|
220
|
+
if (__wantsSection(sections, "counts")) out.numLayers = c.numLayers;
|
|
221
|
+
return out;
|
|
182
222
|
}
|
|
183
223
|
|
|
184
224
|
OPS.list_comps = noUndo(function (args) {
|
|
225
|
+
var sections = (args && args.include) ? args.include : null;
|
|
185
226
|
var out = [];
|
|
186
227
|
for (var i = 1; i <= app.project.numItems; i++) {
|
|
187
228
|
var it = app.project.item(i);
|
|
188
|
-
if (it instanceof CompItem) out.push(__compSummary(it));
|
|
229
|
+
if (it instanceof CompItem) out.push(__compSummary(it, sections));
|
|
189
230
|
}
|
|
190
231
|
return out;
|
|
191
232
|
});
|
|
@@ -272,32 +313,40 @@ function __layerKind(l) {
|
|
|
272
313
|
return "unknown";
|
|
273
314
|
}
|
|
274
315
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
316
|
+
// `sections` is the caller's `include` array (see __wantsSection in comps.jsx).
|
|
317
|
+
// Null means every section, so every existing caller is unaffected; the core —
|
|
318
|
+
// the id/index/name/type map an agent orients with — is always present.
|
|
319
|
+
function __layerSummary(l, sections) {
|
|
320
|
+
var out = {
|
|
278
321
|
id: l.id,
|
|
279
322
|
index: l.index,
|
|
280
323
|
name: l.name,
|
|
281
|
-
enabled: l.enabled,
|
|
282
|
-
solo: l.solo,
|
|
283
|
-
locked: l.locked,
|
|
284
|
-
shy: l.shy,
|
|
285
|
-
threeDLayer: l.threeDLayer,
|
|
286
|
-
label: l.label,
|
|
287
|
-
inPoint: l.inPoint,
|
|
288
|
-
outPoint: l.outPoint,
|
|
289
|
-
startTime: l.startTime,
|
|
290
|
-
stretch: l.stretch,
|
|
291
324
|
sourceType: __layerKind(l),
|
|
292
|
-
parent: parent,
|
|
293
|
-
blendingMode: l.blendingMode,
|
|
294
325
|
};
|
|
326
|
+
if (__wantsSection(sections, "flags")) {
|
|
327
|
+
out.enabled = l.enabled;
|
|
328
|
+
out.solo = l.solo;
|
|
329
|
+
out.locked = l.locked;
|
|
330
|
+
out.shy = l.shy;
|
|
331
|
+
out.threeDLayer = l.threeDLayer;
|
|
332
|
+
out.label = l.label;
|
|
333
|
+
out.blendingMode = l.blendingMode;
|
|
334
|
+
}
|
|
335
|
+
if (__wantsSection(sections, "timing")) {
|
|
336
|
+
out.inPoint = l.inPoint;
|
|
337
|
+
out.outPoint = l.outPoint;
|
|
338
|
+
out.startTime = l.startTime;
|
|
339
|
+
out.stretch = l.stretch;
|
|
340
|
+
}
|
|
341
|
+
if (__wantsSection(sections, "parent")) out.parent = l.parent ? l.parent.id : null;
|
|
342
|
+
return out;
|
|
295
343
|
}
|
|
296
344
|
|
|
297
345
|
OPS.list_layers = noUndo(function (args) {
|
|
298
346
|
var c = getCompById(args.compId);
|
|
347
|
+
var sections = (args && args.include) ? args.include : null;
|
|
299
348
|
var out = [];
|
|
300
|
-
for (var i = 1; i <= c.numLayers; i++) out.push(__layerSummary(c.layer(i)));
|
|
349
|
+
for (var i = 1; i <= c.numLayers; i++) out.push(__layerSummary(c.layer(i), sections));
|
|
301
350
|
return out;
|
|
302
351
|
});
|
|
303
352
|
|
|
@@ -305,27 +354,35 @@ OPS.create_text_layer = function (args) {
|
|
|
305
354
|
var c = getCompById(args.compId);
|
|
306
355
|
var l = c.layers.addText(args.text || "");
|
|
307
356
|
if (args.name) l.name = args.name;
|
|
308
|
-
//
|
|
309
|
-
|
|
357
|
+
// `anchorAlign` is paragraph justification, not geometry. Offsetting the
|
|
358
|
+
// anchor to the measured bbox edge (what this did before) looks right at
|
|
359
|
+
// creation and is wrong the moment the Source Text changes — retyped, driven
|
|
360
|
+
// by an expression, or edited through Essential Graphics in Premiere — because
|
|
361
|
+
// the anchor stays baked for the old string and the layout jumps. Justifying
|
|
362
|
+
// instead keeps the alignment live and leaves the anchor at the origin, which
|
|
363
|
+
// is also what sourceRectAtTime()-driven backgrounds expect.
|
|
364
|
+
//
|
|
365
|
+
// Tracking is normalised for a different reason: addText() inherits the
|
|
366
|
+
// workspace's Character panel, so an untouched layer arrives with whatever
|
|
367
|
+
// that was last left on (-20 is common) and the same call renders differently
|
|
368
|
+
// on two machines. `tracking` sets it explicitly; omitting it means 0.
|
|
369
|
+
//
|
|
370
|
+
// 'none' opts out of all of it and leaves AE's raw defaults alone.
|
|
371
|
+
var align = args.anchorAlign === undefined ? "left" : args.anchorAlign;
|
|
372
|
+
var wantJustify = align !== "none" && __JUSTIFICATION[align] !== undefined;
|
|
373
|
+
var wantTracking = args.tracking !== undefined || align !== "none";
|
|
374
|
+
if (args.font || args.size || args.color || wantJustify || wantTracking) {
|
|
310
375
|
var srcText = l.property("Source Text");
|
|
311
376
|
var td = srcText.value;
|
|
312
377
|
if (args.font) td.font = args.font;
|
|
313
378
|
if (args.size) td.fontSize = args.size;
|
|
314
379
|
if (args.color) { td.applyFill = true; td.fillColor = [args.color[0], args.color[1], args.color[2]]; }
|
|
380
|
+
if (wantTracking) td.tracking = (args.tracking !== undefined ? args.tracking : 0);
|
|
381
|
+
if (wantJustify) td.justification = __JUSTIFICATION[align];
|
|
315
382
|
srcText.setValue(td);
|
|
316
383
|
}
|
|
317
|
-
// AE's addText() puts the anchor at the bbox center, which surprises agents
|
|
318
|
-
// who expect position to mean the left edge. Default to 'left' so position
|
|
319
|
-
// semantically matches the visible start of the text. 'none' opts out.
|
|
320
|
-
var align = args.anchorAlign === undefined ? "left" : args.anchorAlign;
|
|
321
384
|
if (align !== "none") {
|
|
322
|
-
|
|
323
|
-
var __rect = l.sourceRectAtTime(c.time, false);
|
|
324
|
-
var __ax = __rect.left;
|
|
325
|
-
if (align === "center") __ax = __rect.left + __rect.width / 2;
|
|
326
|
-
else if (align === "right") __ax = __rect.left + __rect.width;
|
|
327
|
-
l.property("Transform").property("Anchor Point").setValue([__ax, 0, 0]);
|
|
328
|
-
} catch (__e) {}
|
|
385
|
+
l.property("Transform").property("Anchor Point").setValue([0, 0, 0]);
|
|
329
386
|
}
|
|
330
387
|
if (args.position) {
|
|
331
388
|
var p = args.position;
|
|
@@ -443,15 +500,328 @@ OPS.set_layer = function (args) {
|
|
|
443
500
|
return __layerSummary(l);
|
|
444
501
|
};
|
|
445
502
|
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// Parenting that does not move the layer
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// AE compensates a child's transform when you assign layer.parent, but within
|
|
507
|
+
// one script evaluation it reads the parent from stale data if that parent was
|
|
508
|
+
// itself re-parented earlier in the same evaluation: the compensation runs
|
|
509
|
+
// against the pre-move parent and the child lands at W - parentWorld twice
|
|
510
|
+
// over, and a parent whose scale is not 100 leaves children at 333% (issue
|
|
511
|
+
// #28). run_batch and run_jsx are one evaluation, so this is their normal case.
|
|
512
|
+
//
|
|
513
|
+
// So we do not trust it. We compute the child's world transform ourselves from
|
|
514
|
+
// property values before the assignment, compute it again after, and correct
|
|
515
|
+
// the local transform when the two disagree. When AE gets it right the two
|
|
516
|
+
// agree and nothing is written at all — that no-op is what makes
|
|
517
|
+
// preserveTransform safe to default to true.
|
|
518
|
+
//
|
|
519
|
+
// The matrices are ours because AE's scripting DOM has no toComp/toWorld —
|
|
520
|
+
// those exist only in the expression language. Limits, all reported in the
|
|
521
|
+
// result rather than worked around:
|
|
522
|
+
// * 2D only. AE's 3D chain (orientation plus three rotations, composed in
|
|
523
|
+
// AE's own order) is not reimplementable here with any confidence, so a 3D
|
|
524
|
+
// layer, camera or light anywhere in either chain skips the correction.
|
|
525
|
+
// * One time. Everything is measured at the comp's current time. A keyframed
|
|
526
|
+
// or expression-driven ancestor makes the world transform time-varying, and
|
|
527
|
+
// no static correction is right at every frame.
|
|
528
|
+
// * Position only when AE is provably wrong. See __PARENT_POS_MODEL below.
|
|
529
|
+
|
|
530
|
+
// Well below anything visible, well above double-precision noise through a
|
|
531
|
+
// chain of matrix products. Nothing is written inside these — the point is that
|
|
532
|
+
// a correct compensation is left exactly as AE wrote it.
|
|
533
|
+
var __PT_TOL_POS = 1e-3; // comp pixels
|
|
534
|
+
var __PT_TOL_SCALE = 1e-3; // percentage points
|
|
535
|
+
var __PT_TOL_ROT = 1e-3; // degrees
|
|
536
|
+
|
|
537
|
+
// A child's Position is measured in its parent's space, and there are two
|
|
538
|
+
// readings of where that space starts: at the parent's layer-space origin (so
|
|
539
|
+
// Position is a point pushed through the parent's own matrix) or at the
|
|
540
|
+
// parent's anchor point. They differ by exactly the parent's anchor, mapped up
|
|
541
|
+
// the chain, and coincide whenever every anchor above is [0,0] — which covers
|
|
542
|
+
// nulls, shapes and point text, i.e. most rigs. Rather than bet on one, we
|
|
543
|
+
// compute both, and only rewrite Position when AE's own answer matches
|
|
544
|
+
// *neither* — that is the only case where AE is provably wrong, and it is the
|
|
545
|
+
// bug. When AE matches one of them we learn which for the rest of the session.
|
|
546
|
+
var __PARENT_POS_MODEL = null;
|
|
547
|
+
|
|
548
|
+
// Duck-typed rather than `instanceof Array` so it holds for anything indexable
|
|
549
|
+
// AE hands back, and a bare number promotes to a vector.
|
|
550
|
+
function __ptV2(v) {
|
|
551
|
+
if (v === null || v === undefined) return [0, 0];
|
|
552
|
+
if (typeof v === "number") return [v, 0];
|
|
553
|
+
return [v[0] || 0, v[1] || 0];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// 2D affine as [a, b, c, d, tx, ty]: x' = a*x + c*y + tx, y' = b*x + d*y + ty
|
|
557
|
+
function __ptMId() { return [1, 0, 0, 1, 0, 0]; }
|
|
558
|
+
function __ptMMul(m, n) { // apply n first, then m
|
|
559
|
+
return [
|
|
560
|
+
m[0] * n[0] + m[2] * n[1],
|
|
561
|
+
m[1] * n[0] + m[3] * n[1],
|
|
562
|
+
m[0] * n[2] + m[2] * n[3],
|
|
563
|
+
m[1] * n[2] + m[3] * n[3],
|
|
564
|
+
m[0] * n[4] + m[2] * n[5] + m[4],
|
|
565
|
+
m[1] * n[4] + m[3] * n[5] + m[5]
|
|
566
|
+
];
|
|
567
|
+
}
|
|
568
|
+
function __ptMPoint(m, p) { return [m[0] * p[0] + m[2] * p[1] + m[4], m[1] * p[0] + m[3] * p[1] + m[5]]; }
|
|
569
|
+
function __ptMVec(m, p) { return [m[0] * p[0] + m[2] * p[1], m[1] * p[0] + m[3] * p[1]]; }
|
|
570
|
+
function __ptMLinear(m) { return [m[0], m[1], m[2], m[3], 0, 0]; }
|
|
571
|
+
function __ptMInvert(m) {
|
|
572
|
+
var det = m[0] * m[3] - m[1] * m[2];
|
|
573
|
+
if (!isFinite(det) || Math.abs(det) < 1e-12) return null; // a scale of 0 somewhere
|
|
574
|
+
var a = m[3] / det, b = -m[1] / det, cc = -m[2] / det, d = m[0] / det;
|
|
575
|
+
return [a, b, cc, d, -(a * m[4] + cc * m[5]), -(b * m[4] + d * m[5])];
|
|
576
|
+
}
|
|
577
|
+
function __ptNear(a, b, tol) {
|
|
578
|
+
if (!a || !b) return false;
|
|
579
|
+
return Math.abs(a[0] - b[0]) < tol && Math.abs(a[1] - b[1]) < tol;
|
|
580
|
+
}
|
|
581
|
+
function __ptWrapDeg(d) { while (d > 180) d -= 360; while (d <= -180) d += 360; return d; }
|
|
582
|
+
function __ptR4(n) { return Math.round(n * 10000) / 10000; } // for prose only; reported deltas keep full precision
|
|
583
|
+
|
|
584
|
+
function __ptProps(l) {
|
|
585
|
+
var tr = l.property("Transform");
|
|
586
|
+
return {
|
|
587
|
+
position: tr.property("Position"),
|
|
588
|
+
anchor: tr.property("Anchor Point"),
|
|
589
|
+
scale: tr.property("Scale"),
|
|
590
|
+
rotation: tr.property("Rotation")
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function __ptAnimated(p) {
|
|
595
|
+
var list = [p.position, p.anchor, p.scale, p.rotation];
|
|
596
|
+
for (var i = 0; i < list.length; i++) {
|
|
597
|
+
if (!list[i]) continue;
|
|
598
|
+
if (list[i].numKeys > 0) return true;
|
|
599
|
+
try { if (list[i].expressionEnabled) return true; } catch (e) {}
|
|
600
|
+
}
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// T(position) · R(rotation) · S(scale/100) · T(-anchor). This is the same
|
|
605
|
+
// matrix the layer uses for its own points, which is what lets a child's
|
|
606
|
+
// Position be fed straight through its parent's copy of it.
|
|
607
|
+
function __ptLocalMatrix(l, t) {
|
|
608
|
+
var p = __ptProps(l);
|
|
609
|
+
var pos = __ptV2(p.position.valueAtTime(t, false));
|
|
610
|
+
var anc = __ptV2(p.anchor.valueAtTime(t, false));
|
|
611
|
+
var sc = __ptV2(p.scale.valueAtTime(t, false));
|
|
612
|
+
var rad = p.rotation.valueAtTime(t, false) * Math.PI / 180;
|
|
613
|
+
var cos = Math.cos(rad), sin = Math.sin(rad);
|
|
614
|
+
var rs = [cos * sc[0] / 100, sin * sc[0] / 100, -sin * sc[1] / 100, cos * sc[1] / 100, 0, 0];
|
|
615
|
+
return __ptMMul(__ptMMul([1, 0, 0, 1, pos[0], pos[1]], rs), [1, 0, 0, 1, -anc[0], -anc[1]]);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Walks up the parent chain. `m` is the layer's full world matrix; `wb` is its
|
|
619
|
+
// world anchor position under the anchor-relative reading of Position.
|
|
620
|
+
function __ptChain(l, t, depth) {
|
|
621
|
+
if (!l) return { ok: true, m: __ptMId(), wb: [0, 0], animated: false };
|
|
622
|
+
if (depth > 32) return { ok: false, reason: "parent chain deeper than 32 layers" };
|
|
623
|
+
if (!(l instanceof AVLayer)) return { ok: false, reason: 'a camera or light in the chain ("' + l.name + '") has no 2D transform' };
|
|
624
|
+
if (l.threeDLayer) return { ok: false, reason: 'a 3D layer in the chain ("' + l.name + '")' };
|
|
625
|
+
var up = __ptChain(l.parent, t, depth + 1);
|
|
626
|
+
if (!up.ok) return up;
|
|
627
|
+
var p = __ptProps(l);
|
|
628
|
+
var off = __ptMVec(up.m, __ptV2(p.position.valueAtTime(t, false)));
|
|
629
|
+
return {
|
|
630
|
+
ok: true,
|
|
631
|
+
m: __ptMMul(up.m, __ptLocalMatrix(l, t)),
|
|
632
|
+
wb: [up.wb[0] + off[0], up.wb[1] + off[1]],
|
|
633
|
+
animated: up.animated || __ptAnimated(p)
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Applies fn to a property's static value, or to every keyframe. AE's own
|
|
638
|
+
// compensation rewrites every key, so ours does too: the correction is a
|
|
639
|
+
// constant change of frame, not a change of animation.
|
|
640
|
+
function __ptAdjust(prop, fn) {
|
|
641
|
+
if (prop.numKeys > 0) {
|
|
642
|
+
for (var i = 1; i <= prop.numKeys; i++) prop.setValueAtKey(i, fn(prop.keyValue(i)));
|
|
643
|
+
return prop.numKeys;
|
|
644
|
+
}
|
|
645
|
+
prop.setValue(fn(prop.value));
|
|
646
|
+
return 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function __ptExprDriven(prop) {
|
|
650
|
+
try { return !!prop.expressionEnabled; } catch (e) { return false; }
|
|
651
|
+
}
|
|
652
|
+
|
|
446
653
|
OPS.parent_layer = function (args) {
|
|
447
654
|
var c = getCompById(args.compId);
|
|
448
655
|
var l = getLayerById(c, args.layerId);
|
|
449
|
-
|
|
450
|
-
|
|
656
|
+
var newParent = null;
|
|
657
|
+
if (args.parentLayerId !== null && args.parentLayerId !== undefined) {
|
|
658
|
+
newParent = getLayerById(c, args.parentLayerId);
|
|
659
|
+
}
|
|
660
|
+
var preserve = args.preserveTransform !== false;
|
|
661
|
+
|
|
662
|
+
if (!preserve) {
|
|
663
|
+
l.parent = newParent;
|
|
664
|
+
var plain = __layerSummary(l);
|
|
665
|
+
plain.preserveTransform = false;
|
|
666
|
+
return plain;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
var t = c.time;
|
|
670
|
+
var notes = [];
|
|
671
|
+
var before = __ptChain(l, t, 0);
|
|
672
|
+
var oldParent = __ptChain(l.parent, t, 0);
|
|
673
|
+
var posChildBefore = __ptV2(__ptProps(l).position.valueAtTime(t, false));
|
|
674
|
+
|
|
675
|
+
// The parenting itself always happens, whether or not we can check it.
|
|
676
|
+
l.parent = newParent;
|
|
677
|
+
|
|
678
|
+
var out = __layerSummary(l);
|
|
679
|
+
out.preserveTransform = true;
|
|
680
|
+
var corr = {
|
|
681
|
+
applied: false,
|
|
682
|
+
atTime: t,
|
|
683
|
+
positionDelta: null,
|
|
684
|
+
scaleDelta: null,
|
|
685
|
+
rotationDelta: null,
|
|
686
|
+
keysAdjusted: 0,
|
|
687
|
+
positionModel: null,
|
|
688
|
+
timeVarying: false,
|
|
689
|
+
notes: notes
|
|
690
|
+
};
|
|
691
|
+
out.correction = corr;
|
|
692
|
+
|
|
693
|
+
if (!before.ok || !oldParent.ok) {
|
|
694
|
+
notes.push("not corrected: " + (before.reason || oldParent.reason));
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
var after = __ptChain(newParent, t, 0);
|
|
698
|
+
if (!after.ok) {
|
|
699
|
+
notes.push("not corrected: " + after.reason);
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
702
|
+
var invAfter = __ptMInvert(after.m);
|
|
703
|
+
if (!invAfter) {
|
|
704
|
+
notes.push("not corrected: the new parent chain has a scale of 0 and cannot be inverted");
|
|
705
|
+
return out;
|
|
706
|
+
}
|
|
707
|
+
corr.timeVarying = !!(before.animated || after.animated);
|
|
708
|
+
if (corr.timeVarying) {
|
|
709
|
+
notes.push("a transform in the chain is keyframed or expression-driven, so the world transform varies over time; corrected at comp time " + t);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
var props = __ptProps(l);
|
|
713
|
+
// The child's world anchor position before the assignment, under each of the
|
|
714
|
+
// two readings of what a child's Position means in its parent's space.
|
|
715
|
+
var waAnchorRel = __ptV2(before.wb);
|
|
716
|
+
var waLayerSpace = __ptMPoint(oldParent.m, posChildBefore);
|
|
717
|
+
|
|
718
|
+
// --- rotation and scale -------------------------------------------------
|
|
719
|
+
// The linear part of a world matrix is the product of R·S up the chain, which
|
|
720
|
+
// is the same under both readings of Position. This half is model-free.
|
|
721
|
+
//
|
|
722
|
+
// desired = inv(Lin(new parent chain)) · Lin(child's world before), i.e. the
|
|
723
|
+
// child's own R·S that reproduces the world it had. Split it back into a
|
|
724
|
+
// rotation and an axis-aligned scale: column 0 is R·(sx,0) and column 1 is
|
|
725
|
+
// R·(0,sy), so |column 0| is |sx| and its angle is the rotation. A mirrored
|
|
726
|
+
// layer can be written either as a negative sx or as a negative sy plus 180
|
|
727
|
+
// degrees, so we keep whichever sign the layer already carries rather than
|
|
728
|
+
// rewriting an equivalent transform for no reason.
|
|
729
|
+
var desired = __ptMMul(__ptMLinear(invAfter), __ptMLinear(before.m));
|
|
730
|
+
var scNow = __ptV2(props.scale.valueAtTime(t, false));
|
|
731
|
+
var col0x = desired[0], col0y = desired[1];
|
|
732
|
+
var negX = (scNow[0] < 0);
|
|
733
|
+
if (negX) { col0x = -col0x; col0y = -col0y; }
|
|
734
|
+
var sxNew = Math.sqrt(col0x * col0x + col0y * col0y);
|
|
735
|
+
if (negX) sxNew = -sxNew;
|
|
736
|
+
var theta = Math.atan2(col0y, col0x);
|
|
737
|
+
var cs = Math.cos(theta), sn = Math.sin(theta);
|
|
738
|
+
var skew = cs * desired[2] + sn * desired[3];
|
|
739
|
+
var syNew = -sn * desired[2] + cs * desired[3];
|
|
740
|
+
if (Math.abs(skew) > 1e-6) {
|
|
741
|
+
notes.push("the new parent shears the layer (non-uniform scale under rotation); position/scale/rotation cannot express it exactly, residual skew " + __ptR4(skew));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
var rotNow = props.rotation.valueAtTime(t, false);
|
|
745
|
+
var rotDelta = __ptWrapDeg(theta * 180 / Math.PI - rotNow);
|
|
746
|
+
if (Math.abs(rotDelta) > __PT_TOL_ROT) {
|
|
747
|
+
if (__ptExprDriven(props.rotation)) {
|
|
748
|
+
notes.push("rotation is expression-driven; the " + __ptR4(rotDelta) + "deg correction was not written");
|
|
749
|
+
} else {
|
|
750
|
+
corr.keysAdjusted += __ptAdjust(props.rotation, function (v) { return v + rotDelta; });
|
|
751
|
+
corr.rotationDelta = rotDelta;
|
|
752
|
+
corr.applied = true;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
var scTarget = [sxNew * 100, syNew * 100];
|
|
757
|
+
var dsx = scTarget[0] - scNow[0], dsy = scTarget[1] - scNow[1];
|
|
758
|
+
if (Math.abs(dsx) > __PT_TOL_SCALE || Math.abs(dsy) > __PT_TOL_SCALE) {
|
|
759
|
+
if (__ptExprDriven(props.scale)) {
|
|
760
|
+
notes.push("scale is expression-driven; the correction to [" + __ptR4(scTarget[0]) + ", " + __ptR4(scTarget[1]) + "] was not written");
|
|
761
|
+
} else if (Math.abs(scNow[0]) < 1e-9 || Math.abs(scNow[1]) < 1e-9) {
|
|
762
|
+
notes.push("scale is 0 on an axis, so it cannot be scaled back to [" + __ptR4(scTarget[0]) + ", " + __ptR4(scTarget[1]) + "]");
|
|
763
|
+
} else {
|
|
764
|
+
// Multiplicative, so a keyframed scale keeps the shape of its animation.
|
|
765
|
+
var fx = scTarget[0] / scNow[0], fy = scTarget[1] / scNow[1];
|
|
766
|
+
corr.keysAdjusted += __ptAdjust(props.scale, function (v) {
|
|
767
|
+
var o = [];
|
|
768
|
+
for (var i = 0; i < v.length; i++) {
|
|
769
|
+
var f = fy;
|
|
770
|
+
if (i === 0) f = fx;
|
|
771
|
+
o.push(v[i] * f);
|
|
772
|
+
}
|
|
773
|
+
return o;
|
|
774
|
+
});
|
|
775
|
+
corr.scaleDelta = [dsx, dsy];
|
|
776
|
+
corr.applied = true;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// --- position -----------------------------------------------------------
|
|
781
|
+
// Recomputed after the linear correction, since the child's own R·S does not
|
|
782
|
+
// affect where its anchor lands but AE may have rewritten Position too.
|
|
783
|
+
var pAfter = __ptV2(props.position.valueAtTime(t, false));
|
|
784
|
+
var candLayerSpace = __ptMPoint(invAfter, waLayerSpace);
|
|
785
|
+
var candAnchorRel = __ptMVec(invAfter, [waAnchorRel[0] - after.wb[0], waAnchorRel[1] - after.wb[1]]);
|
|
786
|
+
var hitLayerSpace = __ptNear(pAfter, candLayerSpace, __PT_TOL_POS);
|
|
787
|
+
var hitAnchorRel = __ptNear(pAfter, candAnchorRel, __PT_TOL_POS);
|
|
788
|
+
var ambiguous = !__ptNear(candLayerSpace, candAnchorRel, __PT_TOL_POS);
|
|
789
|
+
|
|
790
|
+
if (hitLayerSpace || hitAnchorRel) {
|
|
791
|
+
// AE placed it where one of the two readings says it belongs — nothing to
|
|
792
|
+
// fix. Learn the reading when the two candidates actually disagree.
|
|
793
|
+
if (ambiguous && hitLayerSpace !== hitAnchorRel) {
|
|
794
|
+
__PARENT_POS_MODEL = hitLayerSpace ? "layer-space" : "anchor-relative";
|
|
795
|
+
}
|
|
796
|
+
corr.positionModel = __PARENT_POS_MODEL;
|
|
451
797
|
} else {
|
|
452
|
-
|
|
798
|
+
var model = __PARENT_POS_MODEL || "layer-space";
|
|
799
|
+
var target = candLayerSpace;
|
|
800
|
+
if (model === "anchor-relative") target = candAnchorRel;
|
|
801
|
+
corr.positionModel = model;
|
|
802
|
+
if (ambiguous && !__PARENT_POS_MODEL) {
|
|
803
|
+
notes.push("the parent chain has a non-zero anchor point and this session has not yet seen AE agree with either reading of a child's Position, so the layer-space reading was assumed; the two differ by [" + __ptR4(candLayerSpace[0] - candAnchorRel[0]) + ", " + __ptR4(candLayerSpace[1] - candAnchorRel[1]) + "]");
|
|
804
|
+
}
|
|
805
|
+
var dx = target[0] - pAfter[0], dy = target[1] - pAfter[1];
|
|
806
|
+
if (__ptExprDriven(props.position)) {
|
|
807
|
+
notes.push("position is expression-driven; the [" + __ptR4(dx) + ", " + __ptR4(dy) + "] correction was not written");
|
|
808
|
+
} else {
|
|
809
|
+
corr.keysAdjusted += __ptAdjust(props.position, function (v) {
|
|
810
|
+
var o = [];
|
|
811
|
+
for (var i = 0; i < v.length; i++) {
|
|
812
|
+
var d = 0;
|
|
813
|
+
if (i === 0) d = dx;
|
|
814
|
+
else if (i === 1) d = dy;
|
|
815
|
+
o.push(v[i] + d);
|
|
816
|
+
}
|
|
817
|
+
return o;
|
|
818
|
+
});
|
|
819
|
+
corr.positionDelta = [dx, dy];
|
|
820
|
+
corr.applied = true;
|
|
821
|
+
}
|
|
453
822
|
}
|
|
454
|
-
|
|
823
|
+
|
|
824
|
+
return out;
|
|
455
825
|
};
|
|
456
826
|
|
|
457
827
|
OPS.reorder_layer = function (args) {
|
|
@@ -746,28 +1116,57 @@ OPS.set_effect_enabled = function (args) {
|
|
|
746
1116
|
return { ok: true };
|
|
747
1117
|
};
|
|
748
1118
|
|
|
749
|
-
|
|
1119
|
+
// Enumerating app.effects is very slow — around 250 entries in AE 26.3, slow
|
|
1120
|
+
// enough that a hand-rolled loop over it in run_jsx blocks the panel's socket
|
|
1121
|
+
// past the server's timeout and presents as a dead bridge (issue #26). The
|
|
1122
|
+
// table only changes when a plugin is installed, which needs an AE restart,
|
|
1123
|
+
// which reloads this bundle — so caching it for the life of the session is
|
|
1124
|
+
// exact, not merely close enough. Refreshing is available for the case where a
|
|
1125
|
+
// user swears they just installed something.
|
|
1126
|
+
var __availableEffects = null;
|
|
1127
|
+
|
|
1128
|
+
function __enumerateEffects() {
|
|
750
1129
|
// app.effects is an array of {displayName, matchName, category, version} on modern AE.
|
|
751
|
-
var
|
|
1130
|
+
var fx;
|
|
752
1131
|
try {
|
|
753
|
-
|
|
754
|
-
for (var i = 0; i < fx.length; i++) {
|
|
755
|
-
out.push({ displayName: fx[i].displayName, matchName: fx[i].matchName, category: fx[i].category });
|
|
756
|
-
}
|
|
1132
|
+
fx = app.effects;
|
|
757
1133
|
} catch (e) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1134
|
+
fx = null;
|
|
1135
|
+
}
|
|
1136
|
+
if (!fx || fx.length === undefined) {
|
|
1137
|
+
// No silent curated substitute here. Handing back eight effects that look
|
|
1138
|
+
// like the whole list makes "not installed" indistinguishable from "not
|
|
1139
|
+
// enumerated", and an agent then rules out an effect that is right there.
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
"Cannot read app.effects on this After Effects build, so the installed-effect list is unavailable. " +
|
|
1142
|
+
"Add effects by matchName directly — add_effect fails immediately and clearly on a wrong one. " +
|
|
1143
|
+
"Common matchNames: ADBE Gaussian Blur 2 (Gaussian Blur), ADBE Box Blur2 (Fast Box Blur), " +
|
|
1144
|
+
"ADBE Glo2 (Glow), ADBE Drop Shadow, ADBE CurvesCustom (Curves), ADBE Easy Levels2 (Levels), " +
|
|
1145
|
+
"ADBE Fill, CC Light Sweep."
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
var out = [];
|
|
1149
|
+
for (var i = 0; i < fx.length; i++) {
|
|
1150
|
+
out.push({ displayName: fx[i].displayName, matchName: fx[i].matchName, category: fx[i].category });
|
|
769
1151
|
}
|
|
770
1152
|
return out;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
OPS.list_available_effects = noUndo(function (args) {
|
|
1156
|
+
if (!args) args = {};
|
|
1157
|
+
// Only a successful enumeration is cached; a throw leaves the cache empty so
|
|
1158
|
+
// the next call retries rather than repeating a stale failure.
|
|
1159
|
+
if (args.refresh || !__availableEffects) __availableEffects = __enumerateEffects();
|
|
1160
|
+
if (!args.filter) return __availableEffects;
|
|
1161
|
+
|
|
1162
|
+
var q = String(args.filter).toLowerCase();
|
|
1163
|
+
var hits = [];
|
|
1164
|
+
for (var i = 0; i < __availableEffects.length; i++) {
|
|
1165
|
+
var entry = __availableEffects[i];
|
|
1166
|
+
var hay = String(entry.displayName) + " " + String(entry.matchName) + " " + String(entry.category);
|
|
1167
|
+
if (hay.toLowerCase().indexOf(q) !== -1) hits.push(entry);
|
|
1168
|
+
}
|
|
1169
|
+
return hits;
|
|
771
1170
|
});
|
|
772
1171
|
|
|
773
1172
|
|
|
@@ -775,6 +1174,16 @@ OPS.list_available_effects = noUndo(function (args) {
|
|
|
775
1174
|
|
|
776
1175
|
// text.jsx — text layer styling.
|
|
777
1176
|
|
|
1177
|
+
// Shared with create_text_layer, which implements `anchorAlign` as live
|
|
1178
|
+
// paragraph justification rather than a one-time anchor offset. One map so the
|
|
1179
|
+
// two cannot drift.
|
|
1180
|
+
var __JUSTIFICATION = {
|
|
1181
|
+
left: ParagraphJustification.LEFT_JUSTIFY,
|
|
1182
|
+
center: ParagraphJustification.CENTER_JUSTIFY,
|
|
1183
|
+
right: ParagraphJustification.RIGHT_JUSTIFY,
|
|
1184
|
+
full: ParagraphJustification.FULL_JUSTIFY_LASTLINE_LEFT,
|
|
1185
|
+
};
|
|
1186
|
+
|
|
778
1187
|
OPS.set_text = function (args) {
|
|
779
1188
|
var c = getCompById(args.compId);
|
|
780
1189
|
var l = getLayerById(c, args.layerId);
|
|
@@ -790,13 +1199,7 @@ OPS.set_text = function (args) {
|
|
|
790
1199
|
if (args.tracking !== undefined) td.tracking = args.tracking;
|
|
791
1200
|
if (args.leading !== undefined) td.leading = args.leading;
|
|
792
1201
|
if (args.justification !== undefined) {
|
|
793
|
-
|
|
794
|
-
left: ParagraphJustification.LEFT_JUSTIFY,
|
|
795
|
-
center: ParagraphJustification.CENTER_JUSTIFY,
|
|
796
|
-
right: ParagraphJustification.RIGHT_JUSTIFY,
|
|
797
|
-
full: ParagraphJustification.FULL_JUSTIFY_LASTLINE_LEFT,
|
|
798
|
-
};
|
|
799
|
-
if (jmap[args.justification]) td.justification = jmap[args.justification];
|
|
1202
|
+
if (__JUSTIFICATION[args.justification]) td.justification = __JUSTIFICATION[args.justification];
|
|
800
1203
|
}
|
|
801
1204
|
if (args.applyFill !== undefined) td.applyFill = args.applyFill;
|
|
802
1205
|
if (args.applyStroke !== undefined) td.applyStroke = args.applyStroke;
|
|
@@ -975,6 +1378,42 @@ OPS.add_shape_content = function (args) {
|
|
|
975
1378
|
}
|
|
976
1379
|
|
|
977
1380
|
var node = parent.addProperty(match);
|
|
1381
|
+
|
|
1382
|
+
// Render order. addProperty always appends to the END of the group, and in a
|
|
1383
|
+
// shape layer index 1 renders in FRONT — so an appended node lands at the back
|
|
1384
|
+
// and the first thing added is the thing on top. That is the opposite of the
|
|
1385
|
+
// layer stack, and building back-to-front silently produces a solid slab.
|
|
1386
|
+
//
|
|
1387
|
+
// "back" is therefore what already happened and needs no move. "front" is the
|
|
1388
|
+
// only case that reorders, and moveTo is the only primitive AE offers for it —
|
|
1389
|
+
// see the note in the tool description about nested renders. Do it here, on a
|
|
1390
|
+
// node with nothing in it yet, so a failure costs an empty node and nothing
|
|
1391
|
+
// else, and re-fetch afterwards: a structural change invalidates references
|
|
1392
|
+
// into the group (the same trap that makes a Fill reference go stale when a
|
|
1393
|
+
// Stroke is added beside it).
|
|
1394
|
+
if (args.zOrder === "front" && node.propertyIndex !== 1) {
|
|
1395
|
+
try {
|
|
1396
|
+
node.moveTo(1);
|
|
1397
|
+
} catch (eMove) {
|
|
1398
|
+
try { node.remove(); } catch (eRm) {}
|
|
1399
|
+
throw new Error(
|
|
1400
|
+
"add_shape_content created the '" + type + "' node but could not move it to the front of the " +
|
|
1401
|
+
"render stack: " + eMove.message + ". The node was removed, so nothing changed."
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
node = parent.property(1);
|
|
1405
|
+
// Cheap identity check on the re-fetch. If moveTo ever lands the node
|
|
1406
|
+
// somewhere other than index 1, setting properties on whatever is there
|
|
1407
|
+
// would corrupt a node the caller never asked about.
|
|
1408
|
+
if (!node || node.matchName !== match) {
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
"add_shape_content moved the '" + type + "' node towards the front of the render stack but did not " +
|
|
1411
|
+
"find it at index 1 afterwards. No properties were set on it. Read the layer back with get_layer_full " +
|
|
1412
|
+
"before retrying, and add contents front-to-back instead of using zOrder."
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
978
1417
|
var applied = [];
|
|
979
1418
|
var failed = [];
|
|
980
1419
|
|
|
@@ -1153,6 +1592,27 @@ function __clampDownsample(v) {
|
|
|
1153
1592
|
return n > 8 ? 8 : n;
|
|
1154
1593
|
}
|
|
1155
1594
|
|
|
1595
|
+
// The long edge we aim a screenshot at. ~1280px is still legible for checking
|
|
1596
|
+
// type and layout, and costs roughly 1.2k image tokens instead of the 11k a
|
|
1597
|
+
// full-resolution 4K frame costs.
|
|
1598
|
+
var __SCREENSHOT_TARGET_PX = 1280;
|
|
1599
|
+
|
|
1600
|
+
// The correct downsample was always derivable from the comp, and an agent that
|
|
1601
|
+
// forgot it got a full-resolution 4K frame — the most expensive accident
|
|
1602
|
+
// available through these tools. So derive it, and let an explicit value win.
|
|
1603
|
+
// 1080p -> 2 (960px), 4K -> 3 (1280px), and anything already small -> 1.
|
|
1604
|
+
function __autoDownsample(comp) {
|
|
1605
|
+
var longEdge = comp.width > comp.height ? comp.width : comp.height;
|
|
1606
|
+
var n = Math.ceil(longEdge / __SCREENSHOT_TARGET_PX);
|
|
1607
|
+
if (!(n > 1)) return 1;
|
|
1608
|
+
return n > 8 ? 8 : n;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function __resolveDownsample(comp, requested) {
|
|
1612
|
+
if (requested === undefined || requested === null) return __autoDownsample(comp);
|
|
1613
|
+
return __clampDownsample(requested);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1156
1616
|
// saveFrameToPng honours the comp's resolutionFactor, so AE can render the
|
|
1157
1617
|
// reduced frame directly instead of writing full size and resampling
|
|
1158
1618
|
// afterwards. That is faster (a quarter of the pixels at factor 2) and needs no
|
|
@@ -1176,7 +1636,7 @@ function __saveFrameAt(comp, time, file, factor) {
|
|
|
1176
1636
|
OPS.screenshot_frame = noUndo(function (args) {
|
|
1177
1637
|
var c = getCompById(args.compId);
|
|
1178
1638
|
var t = (args.time !== undefined && args.time !== null) ? args.time : c.time;
|
|
1179
|
-
var ds =
|
|
1639
|
+
var ds = __resolveDownsample(c, args.downsample);
|
|
1180
1640
|
var path = __tmpPngPath();
|
|
1181
1641
|
var f = new File(path);
|
|
1182
1642
|
// saveFrameToPng is async-ish; the panel polls the file's existence/size.
|
|
@@ -1201,7 +1661,7 @@ OPS.screenshot_layer = noUndo(function (args) {
|
|
|
1201
1661
|
ll.solo = false;
|
|
1202
1662
|
}
|
|
1203
1663
|
l.solo = true;
|
|
1204
|
-
var ds =
|
|
1664
|
+
var ds = __resolveDownsample(c, args.downsample);
|
|
1205
1665
|
var path = __tmpPngPath();
|
|
1206
1666
|
var f = new File(path);
|
|
1207
1667
|
try {
|
|
@@ -1222,6 +1682,399 @@ OPS.screenshot_layer = noUndo(function (args) {
|
|
|
1222
1682
|
});
|
|
1223
1683
|
|
|
1224
1684
|
|
|
1685
|
+
// ===== footage.jsx =====
|
|
1686
|
+
|
|
1687
|
+
// footage.jsx — import a file into the project, and refuse to hand back an
|
|
1688
|
+
// asset that After Effects imported wrongly.
|
|
1689
|
+
//
|
|
1690
|
+
// Import exists here for one reason: it is the only place a viewBox check has
|
|
1691
|
+
// to live. AE's SVG importer fabricates pixel dimensions for an SVG whose
|
|
1692
|
+
// viewBox uses a very large coordinate space, and then rasterizes nothing — no
|
|
1693
|
+
// error at any stage, a footage item that looks healthy in the project panel,
|
|
1694
|
+
// and an empty frame wherever it is placed (issue #33). The agent doing the
|
|
1695
|
+
// import is the only thing in the loop that knows an import happened, so a
|
|
1696
|
+
// guide can tell it what to compare; a tool can compare it.
|
|
1697
|
+
//
|
|
1698
|
+
// The check is the one from that report: the aspect ratio the SVG asks for
|
|
1699
|
+
// against the aspect ratio AE produced. It needs the file path and the
|
|
1700
|
+
// resulting item together, which is exactly and only what an import op has.
|
|
1701
|
+
|
|
1702
|
+
var __SVG_ASPECT_TOLERANCE = 0.02; // 2% — comfortably past rounding, far short of the 3x that a broken import produces.
|
|
1703
|
+
var __SVG_SNIFF_BYTES = 16384;
|
|
1704
|
+
|
|
1705
|
+
function __lowerExt(path) {
|
|
1706
|
+
var dot = String(path).lastIndexOf(".");
|
|
1707
|
+
if (dot < 0) return "";
|
|
1708
|
+
return String(path).substring(dot + 1).toLowerCase();
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/** First `max` bytes of a file as text, or null if it cannot be read. */
|
|
1712
|
+
function __readHead(file, max) {
|
|
1713
|
+
var text = null;
|
|
1714
|
+
try {
|
|
1715
|
+
file.encoding = "UTF-8";
|
|
1716
|
+
if (!file.open("r")) return null;
|
|
1717
|
+
try { text = file.read(max); } finally { file.close(); }
|
|
1718
|
+
} catch (e) { return null; }
|
|
1719
|
+
return text;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* viewBox / width / height off the root <svg> element. Deliberately a regex
|
|
1724
|
+
* rather than a parser: we need four numbers out of the first tag, not a DOM,
|
|
1725
|
+
* and ExtendScript has no XML reader that is worth the failure modes.
|
|
1726
|
+
*/
|
|
1727
|
+
function __parseSvgViewBox(text) {
|
|
1728
|
+
if (!text) return null;
|
|
1729
|
+
var m = text.match(/\bviewBox\s*=\s*["']([^"']+)["']/);
|
|
1730
|
+
if (!m) return null;
|
|
1731
|
+
var parts = m[1].replace(/,/g, " ").replace(/^\s+|\s+$/g, "").split(/\s+/);
|
|
1732
|
+
if (parts.length < 4) return null;
|
|
1733
|
+
var w = parseFloat(parts[2]);
|
|
1734
|
+
var h = parseFloat(parts[3]);
|
|
1735
|
+
if (!isFinite(w) || !isFinite(h) || w <= 0 || h <= 0) return null;
|
|
1736
|
+
return { minX: parseFloat(parts[0]), minY: parseFloat(parts[1]), width: w, height: h, raw: m[1] };
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* Compare what the SVG asked for against what AE produced.
|
|
1741
|
+
*
|
|
1742
|
+
* Aspect ratio rather than absolute size on purpose: AE is entitled to pick the
|
|
1743
|
+
* pixel dimensions for a vector file with no width/height, and does so
|
|
1744
|
+
* sensibly for ordinary SVGs. What it is not entitled to do is change the
|
|
1745
|
+
* shape, and in the broken case it does — the report's example asks for
|
|
1746
|
+
* 278050x333334 (0.83) and gets 15906x5654 (2.81).
|
|
1747
|
+
*/
|
|
1748
|
+
function __validateSvgImport(item, file) {
|
|
1749
|
+
var vb = __parseSvgViewBox(__readHead(file, __SVG_SNIFF_BYTES));
|
|
1750
|
+
if (!vb) return { ok: true, checked: false, reason: "no viewBox on the root <svg> element; nothing to compare against" };
|
|
1751
|
+
|
|
1752
|
+
var w = item.width, h = item.height;
|
|
1753
|
+
if (!(w > 0) || !(h > 0)) {
|
|
1754
|
+
return {
|
|
1755
|
+
ok: false, checked: true, viewBox: vb.raw, itemWidth: w, itemHeight: h,
|
|
1756
|
+
reason: "After Effects imported this SVG with dimensions " + w + "x" + h + "."
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
var expected = vb.width / vb.height;
|
|
1761
|
+
var actual = w / h;
|
|
1762
|
+
var drift = Math.abs(actual - expected) / expected;
|
|
1763
|
+
var out = {
|
|
1764
|
+
checked: true,
|
|
1765
|
+
viewBox: vb.raw,
|
|
1766
|
+
expectedAspect: Math.round(expected * 10000) / 10000,
|
|
1767
|
+
actualAspect: Math.round(actual * 10000) / 10000,
|
|
1768
|
+
itemWidth: w,
|
|
1769
|
+
itemHeight: h
|
|
1770
|
+
};
|
|
1771
|
+
if (drift <= __SVG_ASPECT_TOLERANCE) { out.ok = true; return out; }
|
|
1772
|
+
out.ok = false;
|
|
1773
|
+
out.reason =
|
|
1774
|
+
"After Effects imported this SVG as " + w + "x" + h + " (aspect " + out.actualAspect + "), but its " +
|
|
1775
|
+
"viewBox \"" + vb.raw + "\" asks for aspect " + out.expectedAspect + ". AE fabricates dimensions for an SVG " +
|
|
1776
|
+
"with a very large viewBox coordinate space and then rasterizes nothing, so this item renders empty " +
|
|
1777
|
+
"wherever it is placed, with no error.";
|
|
1778
|
+
return out;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/** The advice is the same whichever way the item failed, so it is written once. */
|
|
1782
|
+
function __brokenSvgAdvice() {
|
|
1783
|
+
return (
|
|
1784
|
+
" Workarounds: for a simple flat SVG, read its path data and rebuild it as a shape layer with " +
|
|
1785
|
+
"set_shape_path, scaling the coordinates down to a sane space (divide by 333.334 for a 1000px " +
|
|
1786
|
+
"version) and setting ADBE Vector Fill Rule to 2 if the SVG says fill-rule=\"evenodd\" — that is " +
|
|
1787
|
+
"pixel-accurate. For a complex one, normalise the viewBox to a small range or rasterize to PNG " +
|
|
1788
|
+
"outside AE, then import that. Pass force:true to keep the item anyway."
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
OPS.import_footage = function (args) {
|
|
1793
|
+
var path = args && args.path;
|
|
1794
|
+
if (typeof path !== "string" || path.length === 0) throw new Error("path is required");
|
|
1795
|
+
|
|
1796
|
+
var file = new File(path);
|
|
1797
|
+
if (!file.exists) throw new Error("No file at " + path);
|
|
1798
|
+
|
|
1799
|
+
var opts = new ImportOptions(file);
|
|
1800
|
+
if (args.sequence === true) {
|
|
1801
|
+
if (!opts.canImportAs(ImportAsType.FOOTAGE)) throw new Error("Cannot import " + path + " as footage, so it cannot be a sequence either");
|
|
1802
|
+
opts.importAs = ImportAsType.FOOTAGE;
|
|
1803
|
+
opts.sequence = true;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
var item = app.project.importFile(opts);
|
|
1807
|
+
if (!item) throw new Error("After Effects returned no item for " + path);
|
|
1808
|
+
|
|
1809
|
+
if (typeof args.name === "string" && args.name.length > 0) item.name = args.name;
|
|
1810
|
+
|
|
1811
|
+
var result = {
|
|
1812
|
+
itemId: item.id,
|
|
1813
|
+
name: item.name,
|
|
1814
|
+
path: path,
|
|
1815
|
+
width: item.width,
|
|
1816
|
+
height: item.height,
|
|
1817
|
+
duration: item.duration,
|
|
1818
|
+
frameRate: (item.frameRate !== undefined) ? item.frameRate : null,
|
|
1819
|
+
isStill: !!item.footageMissing ? null : (item.duration === 0),
|
|
1820
|
+
footageMissing: !!item.footageMissing
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
if (__lowerExt(path) !== "svg") return result;
|
|
1824
|
+
|
|
1825
|
+
var v = __validateSvgImport(item, file);
|
|
1826
|
+
result.validation = v;
|
|
1827
|
+
if (v.ok) return result;
|
|
1828
|
+
|
|
1829
|
+
// A silently empty asset is the failure this whole op exists to prevent, so
|
|
1830
|
+
// it is not something to return with a warning attached and hope is read.
|
|
1831
|
+
// Remove what we created and say why — the same all-or-nothing stance
|
|
1832
|
+
// add_shape_content takes when a key will not resolve.
|
|
1833
|
+
if (args.force === true) {
|
|
1834
|
+
result.warning = v.reason + __brokenSvgAdvice();
|
|
1835
|
+
return result;
|
|
1836
|
+
}
|
|
1837
|
+
var name = item.name;
|
|
1838
|
+
try { item.remove(); result.removed = true; }
|
|
1839
|
+
catch (e) { result.removed = false; }
|
|
1840
|
+
throw new Error(
|
|
1841
|
+
v.reason + " The item (\"" + name + "\") has been removed from the project so nothing places it by mistake." +
|
|
1842
|
+
__brokenSvgAdvice()
|
|
1843
|
+
);
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
OPS.create_footage_layer = function (args) {
|
|
1847
|
+
var comp = getCompById(args.compId);
|
|
1848
|
+
var item = app.project.itemByID(args.itemId);
|
|
1849
|
+
if (!item) throw new Error("No project item with id " + args.itemId);
|
|
1850
|
+
if (item instanceof FolderItem) throw new Error("Item " + args.itemId + " (\"" + item.name + "\") is a folder, not footage");
|
|
1851
|
+
|
|
1852
|
+
var layer = comp.layers.add(item);
|
|
1853
|
+
if (typeof args.name === "string" && args.name.length > 0) layer.name = args.name;
|
|
1854
|
+
if (args.position !== undefined && args.position !== null) {
|
|
1855
|
+
layer.property("Transform").property("Position").setValue(args.position);
|
|
1856
|
+
}
|
|
1857
|
+
if (args.startTime !== undefined && args.startTime !== null) layer.startTime = args.startTime;
|
|
1858
|
+
|
|
1859
|
+
return {
|
|
1860
|
+
layerId: layer.id,
|
|
1861
|
+
index: layer.index,
|
|
1862
|
+
name: layer.name,
|
|
1863
|
+
compId: comp.id,
|
|
1864
|
+
itemId: item.id,
|
|
1865
|
+
inPoint: layer.inPoint,
|
|
1866
|
+
outPoint: layer.outPoint
|
|
1867
|
+
};
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
// ===== mogrt.jsx =====
|
|
1872
|
+
|
|
1873
|
+
// mogrt.jsx — export a comp as a Motion Graphics template without the three
|
|
1874
|
+
// modal dialogs that make a scripted export look like a hung bridge (issue #23).
|
|
1875
|
+
//
|
|
1876
|
+
// Measured against AE 26.3 rather than assumed, because the mechanism was
|
|
1877
|
+
// filed as untested:
|
|
1878
|
+
//
|
|
1879
|
+
// * WITHOUT app.beginSuppressDialogs(), a comp using a non-Adobe font raises
|
|
1880
|
+
// "The following 1 fonts were not synced from Adobe … Click OK to continue"
|
|
1881
|
+
// and the export blocks. ExtendScript is single-threaded, so the panel
|
|
1882
|
+
// cannot service its socket while that dialog is up: the call sat past 60s
|
|
1883
|
+
// and no file was written until someone clicked OK.
|
|
1884
|
+
// * WITH it, the same export returned in a couple of seconds and produced a
|
|
1885
|
+
// valid .mogrt. So suppression is the thing that fixes it, and it is on by
|
|
1886
|
+
// default here.
|
|
1887
|
+
//
|
|
1888
|
+
// The other two dialogs are handled by construction. `app.project.save()`
|
|
1889
|
+
// immediately before the export removes the "project needs to be saved" prompt
|
|
1890
|
+
// deterministically — and the export itself dirties the project, so saving once
|
|
1891
|
+
// at the start of a session is not enough, it has to be per export. The
|
|
1892
|
+
// "undo group mismatch" warning is a consequence of running a non-undoable
|
|
1893
|
+
// export inside dispatch()'s undo group, so this op opts out of the group
|
|
1894
|
+
// entirely via noUndo.
|
|
1895
|
+
//
|
|
1896
|
+
// Two traps that are not in the report, both found by measurement here:
|
|
1897
|
+
//
|
|
1898
|
+
// * The output filename comes from `comp.motionGraphicsTemplateName`, NOT
|
|
1899
|
+
// from the comp name, and it defaults to the literal "Untitled" for a
|
|
1900
|
+
// template assembled by script. Left alone, every export from every comp in
|
|
1901
|
+
// a project writes Untitled.mogrt over the last one.
|
|
1902
|
+
// * The export invalidates every object reference held across it, including
|
|
1903
|
+
// `app.project`. See the re-fetch below.
|
|
1904
|
+
|
|
1905
|
+
/** Distinct fonts used by the text layers in a comp, following nested comps. */
|
|
1906
|
+
function __collectFonts(comp, depth, seenComps, fonts) {
|
|
1907
|
+
if (depth > 4) return;
|
|
1908
|
+
for (var c = 0; c < seenComps.length; c++) { if (seenComps[c] === comp.id) return; }
|
|
1909
|
+
seenComps.push(comp.id);
|
|
1910
|
+
|
|
1911
|
+
for (var i = 1; i <= comp.numLayers; i++) {
|
|
1912
|
+
var layer = comp.layer(i);
|
|
1913
|
+
try {
|
|
1914
|
+
if (layer instanceof TextLayer) {
|
|
1915
|
+
var doc = layer.property("Source Text").value;
|
|
1916
|
+
var f = doc.font;
|
|
1917
|
+
if (f) {
|
|
1918
|
+
var have = false;
|
|
1919
|
+
for (var k = 0; k < fonts.length; k++) { if (fonts[k] === f) { have = true; break; } }
|
|
1920
|
+
if (!have) fonts.push(f);
|
|
1921
|
+
}
|
|
1922
|
+
} else if (layer.source && (layer.source instanceof CompItem)) {
|
|
1923
|
+
__collectFonts(layer.source, depth + 1, seenComps, fonts);
|
|
1924
|
+
}
|
|
1925
|
+
} catch (e) {}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function __joinPath(dir, leaf) {
|
|
1930
|
+
var d = String(dir);
|
|
1931
|
+
if (d.charAt(d.length - 1) === "/" || d.charAt(d.length - 1) === "\\") d = d.substring(0, d.length - 1);
|
|
1932
|
+
return d + "/" + leaf;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/** AE writes <motionGraphicsTemplateName>.mogrt into the folder it is given. */
|
|
1936
|
+
function __mogrtFileName(name) {
|
|
1937
|
+
return String(name) + ".mogrt";
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
OPS.export_mogrt = noUndo(function (args) {
|
|
1941
|
+
var comp = getCompById(args.compId);
|
|
1942
|
+
|
|
1943
|
+
// The save prompt is the first dialog, and the only way to remove it without
|
|
1944
|
+
// guessing is to have somewhere to save to. A project that has never been
|
|
1945
|
+
// saved has no file and no folder — the user has to do that once by hand,
|
|
1946
|
+
// which is worth saying rather than raising a dialog they did not expect.
|
|
1947
|
+
if (!app.project.file) {
|
|
1948
|
+
throw new Error(
|
|
1949
|
+
"This After Effects project has never been saved, so the export cannot save it first and " +
|
|
1950
|
+
"After Effects would raise a modal 'save the project?' dialog that freezes this connection " +
|
|
1951
|
+
"until someone clicks it. Ask the user to save the project once, then call export_mogrt again."
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
var templateName = (typeof args.name === "string" && args.name.length > 0) ? args.name : null;
|
|
1956
|
+
var previousTemplateName = null;
|
|
1957
|
+
try { previousTemplateName = comp.motionGraphicsTemplateName; } catch (e) {}
|
|
1958
|
+
if (!templateName) {
|
|
1959
|
+
// AE's own default is "Untitled" for a template built by script, which
|
|
1960
|
+
// silently collides with every other comp in the project. The comp name is
|
|
1961
|
+
// what the user would have typed. A name they *did* type is left alone.
|
|
1962
|
+
templateName = (!previousTemplateName || previousTemplateName === "Untitled") ? comp.name : previousTemplateName;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
var destDir = (typeof args.destDir === "string" && args.destDir.length > 0)
|
|
1966
|
+
? args.destDir
|
|
1967
|
+
: app.project.file.parent.fsName;
|
|
1968
|
+
var folder = new Folder(destDir);
|
|
1969
|
+
var createdDir = false;
|
|
1970
|
+
if (!folder.exists) {
|
|
1971
|
+
if (!folder.create()) throw new Error("Could not create the destination folder " + destDir);
|
|
1972
|
+
createdDir = true;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
var outPath = __joinPath(folder.fsName, __mogrtFileName(templateName));
|
|
1976
|
+
var outFile = new File(outPath);
|
|
1977
|
+
var existed = outFile.exists;
|
|
1978
|
+
if (existed && args.overwrite !== true) {
|
|
1979
|
+
throw new Error(
|
|
1980
|
+
"A template already exists at " + outPath + ". Pass overwrite: true to replace it, or a " +
|
|
1981
|
+
"different `name`."
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
var fonts = [];
|
|
1986
|
+
try { __collectFonts(comp, 0, [], fonts); } catch (eF) {}
|
|
1987
|
+
|
|
1988
|
+
if (templateName !== previousTemplateName) comp.motionGraphicsTemplateName = templateName;
|
|
1989
|
+
|
|
1990
|
+
// Saving is what removes the save prompt, and it has to happen per export:
|
|
1991
|
+
// exporting dirties the project, so a project saved before the first export
|
|
1992
|
+
// is dirty again before the second.
|
|
1993
|
+
app.project.save();
|
|
1994
|
+
|
|
1995
|
+
// Everything the result needs, read *before* the export. See below.
|
|
1996
|
+
var compId = comp.id;
|
|
1997
|
+
var compName = comp.name;
|
|
1998
|
+
|
|
1999
|
+
var suppress = args.suppressDialogs !== false;
|
|
2000
|
+
var suppressed = false;
|
|
2001
|
+
var exported;
|
|
2002
|
+
try {
|
|
2003
|
+
if (suppress) { app.beginSuppressDialogs(); suppressed = true; }
|
|
2004
|
+
exported = comp.exportAsMotionGraphicsTemplate(true, folder.fsName);
|
|
2005
|
+
} finally {
|
|
2006
|
+
// Unconditional: leaving dialogs suppressed would silence every warning in
|
|
2007
|
+
// the rest of the user's session, including ones they need to see.
|
|
2008
|
+
if (suppressed) { try { app.endSuppressDialogs(false); } catch (eS) {} }
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// exportAsMotionGraphicsTemplate invalidates every object reference held
|
|
2012
|
+
// across it — the CompItem, and `app.project` itself. Measured, not assumed:
|
|
2013
|
+
// after a successful export, `comp.name` and a captured `app.project.file`
|
|
2014
|
+
// both throw "Object is invalid", while a fresh `app.project.itemByID(id)`
|
|
2015
|
+
// returns a working comp. Reading through a stale handle here would report a
|
|
2016
|
+
// failure for an export that had already written a valid file, which is the
|
|
2017
|
+
// same lie as swallowing an error, just pointing the other way.
|
|
2018
|
+
comp = getCompById(compId);
|
|
2019
|
+
|
|
2020
|
+
// exportAsMotionGraphicsTemplate returns a boolean, and a false is the whole
|
|
2021
|
+
// failure report AE offers. Check the file too — a truthy return with nothing
|
|
2022
|
+
// on disk is exactly the kind of success-for-work-that-did-not-happen this
|
|
2023
|
+
// codebase refuses to pass on.
|
|
2024
|
+
var written = new File(outPath);
|
|
2025
|
+
if (!written.exists) {
|
|
2026
|
+
throw new Error(
|
|
2027
|
+
"After Effects reported " + String(exported) + " for the export but no file appeared at " + outPath +
|
|
2028
|
+
". If suppressDialogs was false, a modal dialog in After Effects may have cancelled it."
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
var bytes = written.length;
|
|
2032
|
+
if (!(bytes > 0)) throw new Error("The exported template at " + outPath + " is empty (0 bytes).");
|
|
2033
|
+
|
|
2034
|
+
var result = {
|
|
2035
|
+
ok: true,
|
|
2036
|
+
path: outPath,
|
|
2037
|
+
bytes: bytes,
|
|
2038
|
+
name: templateName,
|
|
2039
|
+
compId: compId,
|
|
2040
|
+
compName: compName,
|
|
2041
|
+
replaced: existed,
|
|
2042
|
+
createdDir: createdDir,
|
|
2043
|
+
projectSaved: app.project.file.fsName,
|
|
2044
|
+
dialogsSuppressed: suppress,
|
|
2045
|
+
controllerCount: comp.motionGraphicsTemplateControllerCount,
|
|
2046
|
+
fonts: fonts
|
|
2047
|
+
};
|
|
2048
|
+
if (!suppress) {
|
|
2049
|
+
result.warning =
|
|
2050
|
+
"Dialogs were not suppressed. If this call took a long time, a modal font warning was waiting " +
|
|
2051
|
+
"in After Effects.";
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// The thumbnail. AE has no scriptable poster time — CompItem.posterTime does
|
|
2055
|
+
// not exist — and the export ignores comp.time, so thumb.png inside the
|
|
2056
|
+
// .mogrt is whatever AE decided, usually black. Render the requested frame
|
|
2057
|
+
// here and let the panel put it into the archive: ExtendScript can write a
|
|
2058
|
+
// PNG but cannot rewrite a zip, and the panel is already the layer that
|
|
2059
|
+
// post-processes files AE has just written.
|
|
2060
|
+
if (args.posterTime !== undefined && args.posterTime !== null) {
|
|
2061
|
+
var t = args.posterTime;
|
|
2062
|
+
if (t < 0 || t > comp.duration) {
|
|
2063
|
+
throw new Error(
|
|
2064
|
+
"posterTime " + t + " is outside the comp's 0.." + comp.duration + "s. The template was still " +
|
|
2065
|
+
"exported to " + outPath + " with After Effects' own thumbnail."
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
var posterPath = __tmpPngPath();
|
|
2069
|
+
__saveFrameAt(comp, t, new File(posterPath), 1);
|
|
2070
|
+
result.posterPngPath = posterPath;
|
|
2071
|
+
result.posterTime = t;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
return result;
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
|
|
1225
2078
|
// ===== style.jsx =====
|
|
1226
2079
|
|
|
1227
2080
|
// style.jsx — the project's house style, read from and written to a plain
|
|
@@ -1440,7 +2293,46 @@ OPS._get_job = noUndo(function (args) {
|
|
|
1440
2293
|
|
|
1441
2294
|
// explore.jsx — rich one-shot inspection. The whole reason this MCP exists.
|
|
1442
2295
|
|
|
1443
|
-
|
|
2296
|
+
// Which keyframes survive a cap, or null when none need to go. Keeping the
|
|
2297
|
+
// first few and the last few rather than a prefix means the shape of the
|
|
2298
|
+
// animation — where it starts, where it ends — is still readable.
|
|
2299
|
+
function __keyframeWindow(total, max) {
|
|
2300
|
+
if (!(max > 0) || total <= max) return null;
|
|
2301
|
+
var head = Math.ceil(max / 2);
|
|
2302
|
+
return { head: head, tail: max - head, total: total, omitted: total - max };
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// Truncation is never silent: an agent that cannot see what was dropped will
|
|
2306
|
+
// read a partial answer as the whole one.
|
|
2307
|
+
function __truncationNote(w) {
|
|
2308
|
+
return "first " + w.head + " and last " + w.tail + " of " + w.total + " keyframes; " +
|
|
2309
|
+
w.omitted + " omitted — raise maxKeyframes, or read them all with get_keyframes";
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
function __serializeKeyframe(p, k) {
|
|
2313
|
+
var entry = { index: k, time: p.keyTime(k), value: p.keyValue(k) };
|
|
2314
|
+
try {
|
|
2315
|
+
entry["in"] = String(p.keyInInterpolationType(k));
|
|
2316
|
+
entry["out"] = String(p.keyOutInterpolationType(k));
|
|
2317
|
+
} catch (e1) {}
|
|
2318
|
+
try {
|
|
2319
|
+
var inE = p.keyInTemporalEase(k);
|
|
2320
|
+
var outE = p.keyOutTemporalEase(k);
|
|
2321
|
+
entry.easeIn = { influence: inE[0].influence, speed: inE[0].speed };
|
|
2322
|
+
entry.easeOut = { influence: outE[0].influence, speed: outE[0].speed };
|
|
2323
|
+
} catch (e2) {}
|
|
2324
|
+
if (p.isSpatial) {
|
|
2325
|
+
try {
|
|
2326
|
+
entry.inTangent = p.keyInSpatialTangent(k);
|
|
2327
|
+
entry.outTangent = p.keyOutSpatialTangent(k);
|
|
2328
|
+
} catch (e3) {}
|
|
2329
|
+
}
|
|
2330
|
+
return entry;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// `opts.maxKeyframes` bounds the response; 0 or absent means every keyframe,
|
|
2334
|
+
// which is what every caller written before the cap existed gets.
|
|
2335
|
+
function __serializeProperty(p, opts) {
|
|
1444
2336
|
var out = {
|
|
1445
2337
|
name: p.name,
|
|
1446
2338
|
matchName: p.matchName,
|
|
@@ -1451,49 +2343,66 @@ function __serializeProperty(p, deep) {
|
|
|
1451
2343
|
try { out.value = p.value; } catch (e) {}
|
|
1452
2344
|
if (p.canSetExpression && p.expression) out.expression = p.expression;
|
|
1453
2345
|
if (p.numKeys > 0) {
|
|
2346
|
+
var total = p.numKeys;
|
|
2347
|
+
var w = __keyframeWindow(total, (opts && opts.maxKeyframes) ? opts.maxKeyframes : 0);
|
|
1454
2348
|
out.keyframes = [];
|
|
1455
|
-
for (var k = 1; k <=
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
var outE = p.keyOutTemporalEase(k);
|
|
1464
|
-
entry.easeIn = { influence: inE[0].influence, speed: inE[0].speed };
|
|
1465
|
-
entry.easeOut = { influence: outE[0].influence, speed: outE[0].speed };
|
|
1466
|
-
} catch (e2) {}
|
|
1467
|
-
if (p.isSpatial) {
|
|
1468
|
-
try {
|
|
1469
|
-
entry.inTangent = p.keyInSpatialTangent(k);
|
|
1470
|
-
entry.outTangent = p.keyOutSpatialTangent(k);
|
|
1471
|
-
} catch (e3) {}
|
|
1472
|
-
}
|
|
1473
|
-
out.keyframes.push(entry);
|
|
2349
|
+
for (var k = 1; k <= total; k++) {
|
|
2350
|
+
if (w && k > w.head && k <= total - w.tail) continue;
|
|
2351
|
+
out.keyframes.push(__serializeKeyframe(p, k));
|
|
2352
|
+
}
|
|
2353
|
+
if (w) {
|
|
2354
|
+
out.keyframeCount = w.total;
|
|
2355
|
+
out.keyframesOmitted = w.omitted;
|
|
2356
|
+
out.keyframesTruncated = __truncationNote(w);
|
|
1474
2357
|
}
|
|
1475
2358
|
}
|
|
1476
2359
|
return out;
|
|
1477
2360
|
}
|
|
1478
2361
|
|
|
1479
|
-
function __serializeTransformGroup(tg) {
|
|
2362
|
+
function __serializeTransformGroup(tg, opts) {
|
|
1480
2363
|
var out = {};
|
|
1481
2364
|
for (var i = 1; i <= tg.numProperties; i++) {
|
|
1482
2365
|
var p = tg.property(i);
|
|
1483
|
-
out[p.name] = __serializeProperty(p);
|
|
2366
|
+
out[p.name] = __serializeProperty(p, opts);
|
|
1484
2367
|
}
|
|
1485
2368
|
return out;
|
|
1486
2369
|
}
|
|
1487
2370
|
|
|
1488
|
-
|
|
2371
|
+
// The cap is applied after the fact rather than inside __serializeEffect,
|
|
2372
|
+
// which effects.jsx also uses for list_effects and add_effect — those return
|
|
2373
|
+
// one effect and have no size problem to solve.
|
|
2374
|
+
function __capEffectKeyframes(effects, max) {
|
|
2375
|
+
if (!(max > 0)) return effects;
|
|
2376
|
+
for (var i = 0; i < effects.length; i++) {
|
|
2377
|
+
var params = effects[i].params;
|
|
2378
|
+
for (var j = 0; j < params.length; j++) {
|
|
2379
|
+
var keys = params[j].keyframes;
|
|
2380
|
+
if (!keys) continue;
|
|
2381
|
+
var w = __keyframeWindow(keys.length, max);
|
|
2382
|
+
if (!w) continue;
|
|
2383
|
+
var kept = [];
|
|
2384
|
+
for (var k = 0; k < w.total; k++) {
|
|
2385
|
+
if (k >= w.head && k < w.total - w.tail) continue;
|
|
2386
|
+
kept.push(keys[k]);
|
|
2387
|
+
}
|
|
2388
|
+
params[j].keyframes = kept;
|
|
2389
|
+
params[j].keyframeCount = w.total;
|
|
2390
|
+
params[j].keyframesOmitted = w.omitted;
|
|
2391
|
+
params[j].keyframesTruncated = __truncationNote(w);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
return effects;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
function __serializeEffects(layer, opts) {
|
|
1489
2398
|
var fx = layer.property("Effects");
|
|
1490
2399
|
if (!fx || fx.numProperties === 0) return [];
|
|
1491
2400
|
var arr = [];
|
|
1492
2401
|
for (var i = 1; i <= fx.numProperties; i++) arr.push(__serializeEffect(fx.property(i)));
|
|
1493
|
-
return arr;
|
|
2402
|
+
return __capEffectKeyframes(arr, (opts && opts.maxKeyframes) ? opts.maxKeyframes : 0);
|
|
1494
2403
|
}
|
|
1495
2404
|
|
|
1496
|
-
function __serializeMasks(layer) {
|
|
2405
|
+
function __serializeMasks(layer, opts) {
|
|
1497
2406
|
var masks = layer.property("Masks");
|
|
1498
2407
|
if (!masks || masks.numProperties === 0) return [];
|
|
1499
2408
|
var out = [];
|
|
@@ -1505,10 +2414,10 @@ function __serializeMasks(layer) {
|
|
|
1505
2414
|
mode: String(m.maskMode),
|
|
1506
2415
|
inverted: m.inverted,
|
|
1507
2416
|
};
|
|
1508
|
-
try { entry.shape = __serializeProperty(m.property("ADBE Mask Shape")); } catch (e1) {}
|
|
1509
|
-
try { entry.opacity = __serializeProperty(m.property("ADBE Mask Opacity")); } catch (e2) {}
|
|
1510
|
-
try { entry.expansion = __serializeProperty(m.property("ADBE Mask Offset")); } catch (e3) {}
|
|
1511
|
-
try { entry.feather = __serializeProperty(m.property("ADBE Mask Feather")); } catch (e4) {}
|
|
2417
|
+
try { entry.shape = __serializeProperty(m.property("ADBE Mask Shape"), opts); } catch (e1) {}
|
|
2418
|
+
try { entry.opacity = __serializeProperty(m.property("ADBE Mask Opacity"), opts); } catch (e2) {}
|
|
2419
|
+
try { entry.expansion = __serializeProperty(m.property("ADBE Mask Offset"), opts); } catch (e3) {}
|
|
2420
|
+
try { entry.feather = __serializeProperty(m.property("ADBE Mask Feather"), opts); } catch (e4) {}
|
|
1512
2421
|
out.push(entry);
|
|
1513
2422
|
}
|
|
1514
2423
|
return out;
|
|
@@ -1563,6 +2472,9 @@ function __serializeShapeContents(group, depth) {
|
|
|
1563
2472
|
var entry = { name: p.name, matchName: p.matchName, index: i };
|
|
1564
2473
|
if (p.propertyType === PropertyType.NAMED_GROUP || p.propertyType === PropertyType.INDEXED_GROUP) {
|
|
1565
2474
|
if (depth > 0) entry.children = __serializeShapeContents(p, depth - 1);
|
|
2475
|
+
// Say where the walk stopped. A group that simply has no `children` key
|
|
2476
|
+
// reads as empty, which for a deep shape tree is a lie.
|
|
2477
|
+
else if (p.numProperties > 0) entry.childrenOmitted = p.numProperties;
|
|
1566
2478
|
} else {
|
|
1567
2479
|
try { entry.value = p.value; } catch (e) {}
|
|
1568
2480
|
}
|
|
@@ -1574,6 +2486,11 @@ function __serializeShapeContents(group, depth) {
|
|
|
1574
2486
|
OPS.get_layer_full = noUndo(function (args) {
|
|
1575
2487
|
var c = getCompById(args.compId);
|
|
1576
2488
|
var l = getLayerById(c, args.layerId);
|
|
2489
|
+
// `include` bounds the response by section, `maxKeyframes` and `shapeDepth`
|
|
2490
|
+
// bound the two things that make a single layer weigh 250KB. All three are
|
|
2491
|
+
// absent by default, and absent means "everything", exactly as before.
|
|
2492
|
+
var want = (args && args.include) ? args.include : null;
|
|
2493
|
+
var opts = { maxKeyframes: (args && args.maxKeyframes > 0) ? args.maxKeyframes : 0 };
|
|
1577
2494
|
var out = {
|
|
1578
2495
|
id: l.id,
|
|
1579
2496
|
index: l.index,
|
|
@@ -1592,44 +2509,69 @@ OPS.get_layer_full = noUndo(function (args) {
|
|
|
1592
2509
|
preserveTransparency: l.preserveTransparency,
|
|
1593
2510
|
parent: l.parent ? { layerId: l.parent.id, name: l.parent.name } : null,
|
|
1594
2511
|
sourceType: __layerKind(l),
|
|
1595
|
-
transform: __serializeTransformGroup(l.property("Transform")),
|
|
1596
|
-
effects: __serializeEffects(l),
|
|
1597
|
-
masks: __serializeMasks(l),
|
|
1598
|
-
markers: __serializeMarkers(l),
|
|
1599
2512
|
};
|
|
2513
|
+
if (__wantsSection(want, "transform")) out.transform = __serializeTransformGroup(l.property("Transform"), opts);
|
|
2514
|
+
if (__wantsSection(want, "effects")) out.effects = __serializeEffects(l, opts);
|
|
2515
|
+
if (__wantsSection(want, "masks")) out.masks = __serializeMasks(l, opts);
|
|
2516
|
+
if (__wantsSection(want, "markers")) out.markers = __serializeMarkers(l);
|
|
1600
2517
|
// Visual bounds at the comp's current time — cheap to fetch and removes a
|
|
1601
2518
|
// class of "I need to screenshot to know where this renders" round-trips.
|
|
1602
2519
|
// Coordinates are in the layer's local space (origin at the Anchor Point).
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
2520
|
+
if (__wantsSection(want, "bounds")) {
|
|
2521
|
+
try {
|
|
2522
|
+
var __rect = l.sourceRectAtTime(c.time, false);
|
|
2523
|
+
out.sourceRect = { left: __rect.left, top: __rect.top, width: __rect.width, height: __rect.height, time: c.time };
|
|
2524
|
+
} catch (__e) {}
|
|
2525
|
+
}
|
|
2526
|
+
if (l instanceof TextLayer && __wantsSection(want, "text")) out.text = __serializeText(l);
|
|
2527
|
+
if (l instanceof ShapeLayer && __wantsSection(want, "shape")) {
|
|
2528
|
+
var depth = (args && args.shapeDepth !== undefined && args.shapeDepth !== null) ? args.shapeDepth : 4;
|
|
2529
|
+
try { out.shape = { depth: depth, contents: __serializeShapeContents(l.property("Contents"), depth) }; }
|
|
1610
2530
|
catch (e) {}
|
|
1611
2531
|
}
|
|
1612
|
-
if (
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2532
|
+
if (__wantsSection(want, "source")) {
|
|
2533
|
+
if (l.source && l.source instanceof CompItem) {
|
|
2534
|
+
out.precomp = { compId: l.source.id, compName: l.source.name };
|
|
2535
|
+
if (args.includeChildren) {
|
|
2536
|
+
out.children = [];
|
|
2537
|
+
for (var i = 1; i <= l.source.numLayers; i++) out.children.push(__layerSummary(l.source.layer(i)));
|
|
2538
|
+
}
|
|
2539
|
+
} else if (l.source && l.source instanceof FootageItem) {
|
|
2540
|
+
var src = l.source;
|
|
2541
|
+
out.footage = {
|
|
2542
|
+
itemId: src.id,
|
|
2543
|
+
name: src.name,
|
|
2544
|
+
hasAlpha: src.hasAlpha,
|
|
2545
|
+
duration: src.duration,
|
|
2546
|
+
width: src.width,
|
|
2547
|
+
height: src.height,
|
|
2548
|
+
};
|
|
2549
|
+
try { if (src.file) out.footage.path = src.file.fsName; } catch (e2) {}
|
|
1617
2550
|
}
|
|
1618
|
-
} else if (l.source && l.source instanceof FootageItem) {
|
|
1619
|
-
var src = l.source;
|
|
1620
|
-
out.footage = {
|
|
1621
|
-
itemId: src.id,
|
|
1622
|
-
name: src.name,
|
|
1623
|
-
hasAlpha: src.hasAlpha,
|
|
1624
|
-
duration: src.duration,
|
|
1625
|
-
width: src.width,
|
|
1626
|
-
height: src.height,
|
|
1627
|
-
};
|
|
1628
|
-
try { if (src.file) out.footage.path = src.file.fsName; } catch (e2) {}
|
|
1629
2551
|
}
|
|
2552
|
+
// Echo the scoping back, so a bounded answer is never read as a full one.
|
|
2553
|
+
if (want) out.included = want;
|
|
1630
2554
|
return out;
|
|
1631
2555
|
});
|
|
1632
2556
|
|
|
2557
|
+
// Project item kind. An if/else chain, not a chained ternary: this build of
|
|
2558
|
+
// ExtendScript parses `a ? x : b ? y : z` left-associatively, so the first
|
|
2559
|
+
// truthy branch became the next condition and every item fell through to
|
|
2560
|
+
// "folder" (issues #21/#22). tests/unit/jsx-ternary.mjs keeps it that way.
|
|
2561
|
+
// "solid" mirrors __layerKind in layers.jsx so an item and a layer that share a
|
|
2562
|
+
// source describe it with the same word.
|
|
2563
|
+
function __itemKind(it) {
|
|
2564
|
+
if (it instanceof CompItem) return "comp";
|
|
2565
|
+
if (it instanceof FolderItem) return "folder";
|
|
2566
|
+
if (it instanceof FootageItem) {
|
|
2567
|
+
try {
|
|
2568
|
+
if (it.mainSource && it.mainSource.color !== undefined) return "solid";
|
|
2569
|
+
} catch (e) {}
|
|
2570
|
+
return "footage";
|
|
2571
|
+
}
|
|
2572
|
+
return "unknown";
|
|
2573
|
+
}
|
|
2574
|
+
|
|
1633
2575
|
OPS.get_project_summary = noUndo(function (args) {
|
|
1634
2576
|
var p = app.project;
|
|
1635
2577
|
var items = [];
|
|
@@ -1638,7 +2580,7 @@ OPS.get_project_summary = noUndo(function (args) {
|
|
|
1638
2580
|
items.push({
|
|
1639
2581
|
id: it.id,
|
|
1640
2582
|
name: it.name,
|
|
1641
|
-
type: (it
|
|
2583
|
+
type: __itemKind(it),
|
|
1642
2584
|
});
|
|
1643
2585
|
}
|
|
1644
2586
|
return {
|
|
@@ -1690,29 +2632,117 @@ OPS.find_layers = noUndo(function (args) {
|
|
|
1690
2632
|
|
|
1691
2633
|
// raw.jsx — escape hatch. Eval arbitrary ExtendScript and return the value.
|
|
1692
2634
|
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2635
|
+
// ---------- Result serialization ----------
|
|
2636
|
+
// The returned value has to survive JSON.stringify in the panel. Anything that
|
|
2637
|
+
// cannot be represented is replaced *in place* by a short marker string, never
|
|
2638
|
+
// dropped: the old coercion kept scalars only, so {done:[...], skipped:[...]}
|
|
2639
|
+
// came back as {} — indistinguishable from "the script did nothing" while its
|
|
2640
|
+
// mutations had already landed, which invites re-running a mutating script
|
|
2641
|
+
// (issue #31). An empty result must mean the script returned nothing.
|
|
2642
|
+
//
|
|
2643
|
+
// The walk is deliberately opt-in: only arrays and plain objects are recursed
|
|
2644
|
+
// into. Live AE objects (Layer, Property, CompItem …) have huge and partly
|
|
2645
|
+
// throwing property graphs, so they degrade to "[AVLayer \"name\"]" rather than
|
|
2646
|
+
// being walked.
|
|
2647
|
+
|
|
2648
|
+
var __RJ_MAX_DEPTH = 12;
|
|
2649
|
+
var __RJ_MAX_NODES = 50000;
|
|
2650
|
+
|
|
2651
|
+
function __rjIsPlainObject(v) {
|
|
2652
|
+
try { if (v.constructor === Object) return true; } catch (e) {}
|
|
2653
|
+
try { if (v.reflect && v.reflect.name === "Object") return true; } catch (e2) {}
|
|
2654
|
+
return false;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
function __rjTypeName(v) {
|
|
2658
|
+
var n = null;
|
|
2659
|
+
try { if (v.reflect && v.reflect.name) n = String(v.reflect.name); } catch (e) {}
|
|
2660
|
+
if (!n) {
|
|
2661
|
+
try {
|
|
2662
|
+
n = String(v);
|
|
2663
|
+
if (n.substring(0, 8) === "[object ") n = n.substring(8, n.length - 1);
|
|
2664
|
+
} catch (e2) { n = "object"; }
|
|
2665
|
+
}
|
|
2666
|
+
if (n.length > 48) n = n.substring(0, 48);
|
|
2667
|
+
return n;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// Short, identifiable stand-in for a value we refuse to walk. The name and id
|
|
2671
|
+
// are worth the two guarded reads: they are what turns "something was here"
|
|
2672
|
+
// into a handle the caller can pass to get_comp or get_layer_full.
|
|
2673
|
+
function __rjMarker(v) {
|
|
2674
|
+
var extra = "";
|
|
2675
|
+
try {
|
|
2676
|
+
if (typeof v.name === "string" && v.name.length > 0 && v.name.length < 64) extra = ' "' + v.name + '"';
|
|
2677
|
+
} catch (e) {}
|
|
2678
|
+
try {
|
|
2679
|
+
if (typeof v.id === "number") extra += " #" + v.id;
|
|
2680
|
+
} catch (e2) {}
|
|
2681
|
+
return "[" + __rjTypeName(v) + extra + "]";
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
// `stack` holds the ancestors currently being walked, so a repeated reference
|
|
2685
|
+
// that is not a cycle still serializes. ES3 has no Set — a linear scan over a
|
|
2686
|
+
// depth-limited stack is cheap enough.
|
|
2687
|
+
function __rjSerialize(v, depth, stack, budget) {
|
|
2688
|
+
if (v === null) return null;
|
|
2689
|
+
var t = typeof v;
|
|
2690
|
+
if (t === "undefined") return "[undefined]";
|
|
2691
|
+
if (t === "boolean" || t === "string") return v;
|
|
2692
|
+
if (t === "number") {
|
|
2693
|
+
if (isNaN(v)) return "[NaN]";
|
|
2694
|
+
if (!isFinite(v)) return v > 0 ? "[Infinity]" : "[-Infinity]";
|
|
2695
|
+
return v;
|
|
2696
|
+
}
|
|
2697
|
+
if (t === "function") return "[function]";
|
|
2698
|
+
if (t !== "object") return "[" + t + "]";
|
|
2699
|
+
if (v instanceof Date) return String(v);
|
|
2700
|
+
|
|
2701
|
+
budget.n += 1;
|
|
2702
|
+
if (budget.n > __RJ_MAX_NODES) return "[truncated: node limit]";
|
|
2703
|
+
|
|
2704
|
+
var isArray = (v instanceof Array);
|
|
2705
|
+
if (!isArray && !__rjIsPlainObject(v)) return __rjMarker(v);
|
|
2706
|
+
|
|
2707
|
+
for (var s = 0; s < stack.length; s++) { if (stack[s] === v) return "[circular]"; }
|
|
2708
|
+
if (depth >= __RJ_MAX_DEPTH) return "[max depth]";
|
|
2709
|
+
|
|
2710
|
+
stack.push(v);
|
|
2711
|
+
var out;
|
|
1707
2712
|
try {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
2713
|
+
if (isArray) {
|
|
2714
|
+
out = [];
|
|
2715
|
+
for (var i = 0; i < v.length; i++) out.push(__rjSerialize(v[i], depth + 1, stack, budget));
|
|
2716
|
+
} else {
|
|
2717
|
+
out = {};
|
|
2718
|
+
for (var k in v) {
|
|
2719
|
+
if (!v.hasOwnProperty(k)) continue;
|
|
2720
|
+
var val;
|
|
2721
|
+
try { val = v[k]; }
|
|
2722
|
+
catch (eg) { out[k] = "[threw: " + (eg && eg.message ? eg.message : String(eg)) + "]"; continue; }
|
|
2723
|
+
out[k] = __rjSerialize(val, depth + 1, stack, budget);
|
|
1714
2724
|
}
|
|
1715
2725
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
}
|
|
2726
|
+
} finally {
|
|
2727
|
+
stack.pop();
|
|
2728
|
+
}
|
|
2729
|
+
return out;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
function __rjResult(result) {
|
|
2733
|
+
// A script with no `return` returns undefined at the top level; that is
|
|
2734
|
+
// genuinely "nothing", not an unserializable value.
|
|
2735
|
+
if (typeof result === "undefined") return null;
|
|
2736
|
+
return __rjSerialize(result, 0, [], { n: 0 });
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// undoGroup:false is a per-call opt-out, read by dispatch() through the
|
|
2740
|
+
// predicate form of __meta.noUndo. It exists because AE refuses copyToComp for
|
|
2741
|
+
// a layer with a parent or a linked expression while an undo group is open,
|
|
2742
|
+
// which is exactly the layer worth copying (issue #30).
|
|
2743
|
+
OPS.run_jsx = noUndoWhen(function (args) { return args.undoGroup === false; }, function (args) {
|
|
2744
|
+
var code = args.code || "";
|
|
2745
|
+
// We wrap in a function so `return` works.
|
|
2746
|
+
var wrapper = "(function(){ " + code + " })()";
|
|
2747
|
+
return __rjResult(eval(wrapper));
|
|
2748
|
+
});
|