@engine-room/after-effects-mcp 0.3.1 → 0.5.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 +10 -8
- package/bin/server.js +2684 -527
- package/package.json +1 -1
- package/panel/CSXS/manifest.xml +2 -2
- package/panel/client/contactsheet.js +347 -0
- package/panel/client/framereader.js +248 -0
- package/panel/client/main.js +619 -112
- package/panel/client/pngcodec.js +122 -0
- package/panel/jsx/bundle.jsx +3299 -241
- package/panel/package.json +1 -1
package/panel/jsx/bundle.jsx
CHANGED
|
@@ -69,13 +69,33 @@ function __newJobId() {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// ---------- Undo wrapper ----------
|
|
72
|
-
// __UNDO_OPEN tracks
|
|
73
|
-
//
|
|
74
|
-
//
|
|
72
|
+
// __UNDO_OPEN tracks whether a group *this bundle* opened is currently open.
|
|
73
|
+
// withUndo() is the only thing that may set it — dispatch and run_batch both
|
|
74
|
+
// group through there — because it is what lets withoutUndoGroup() reopen
|
|
75
|
+
// exactly what it closed, and leave undo state alone entirely when we never
|
|
76
|
+
// opened a group in the first place.
|
|
75
77
|
var __UNDO_OPEN = false;
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
// Every undo group this bundle opens is counted here, and __beginUndoGroup is
|
|
80
|
+
// the only place allowed to call app.beginUndoGroup. run_batch reports the
|
|
81
|
+
// number of undo steps it cost as the *delta* of this counter, so the number an
|
|
82
|
+
// agent repeats to the user as "press Cmd-Z N times" is measured rather than
|
|
83
|
+
// predicted — a chunk that threw still opened its group, and a batched op that
|
|
84
|
+
// called withoutUndoGroup() really did split itself into two steps.
|
|
85
|
+
var __UNDO_GROUPS = 0;
|
|
86
|
+
|
|
87
|
+
function __beginUndoGroup(name) {
|
|
88
|
+
__UNDO_GROUPS += 1;
|
|
78
89
|
app.beginUndoGroup(name || "AE MCP");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function __undoGroupsOpened() { return __UNDO_GROUPS; }
|
|
93
|
+
|
|
94
|
+
// An undo group must open and close inside ONE evalScript call. After Effects
|
|
95
|
+
// discards one that spans two — see the note in CLAUDE.md; measured, not
|
|
96
|
+
// inferred. Everything that groups goes through here for that reason.
|
|
97
|
+
function withUndo(name, fn) {
|
|
98
|
+
__beginUndoGroup(name || "AE MCP");
|
|
79
99
|
__UNDO_OPEN = true;
|
|
80
100
|
try { return fn(); }
|
|
81
101
|
finally { __UNDO_OPEN = false; app.endUndoGroup(); }
|
|
@@ -91,15 +111,28 @@ function withoutUndoGroup(fn) {
|
|
|
91
111
|
app.endUndoGroup();
|
|
92
112
|
__UNDO_OPEN = false;
|
|
93
113
|
try { return fn(); }
|
|
94
|
-
finally {
|
|
114
|
+
finally { __beginUndoGroup("AE MCP: continue"); __UNDO_OPEN = true; }
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
// ---------- Error helper ----------
|
|
118
|
+
// A handler that can say more about *where* it failed than "an error happened"
|
|
119
|
+
// attaches that on `aeDetail` and it is copied onto the result verbatim —
|
|
120
|
+
// run_jsx maps After Effects' line number back onto the script the caller
|
|
121
|
+
// actually submitted (issue #46). The bag is free-form here on purpose; the
|
|
122
|
+
// panel forwards a named list of fields, which is where the contract is kept.
|
|
98
123
|
function __mkError(e) {
|
|
99
124
|
var msg = e && e.message ? String(e.message) : String(e);
|
|
100
125
|
var stack = e && e.stack ? String(e.stack) : "";
|
|
101
126
|
var line = e && typeof e.line !== "undefined" ? e.line : null;
|
|
102
|
-
|
|
127
|
+
var out = { ok: false, error: msg, stack: stack, line: line };
|
|
128
|
+
var detail = null;
|
|
129
|
+
try { if (e && e.aeDetail) detail = e.aeDetail; } catch (ed) {}
|
|
130
|
+
if (detail) {
|
|
131
|
+
for (var k in detail) {
|
|
132
|
+
if (detail.hasOwnProperty(k)) out[k] = detail[k];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
103
136
|
}
|
|
104
137
|
|
|
105
138
|
// ---------- Main dispatch ----------
|
|
@@ -262,10 +295,221 @@ OPS.set_comp = function (args) {
|
|
|
262
295
|
return __compSummary(c);
|
|
263
296
|
};
|
|
264
297
|
|
|
298
|
+
// A solid layer's source is a FootageItem in the project's Solids folder, and
|
|
299
|
+
// removing the comp removes the layer, not the item (issue #83). The solids
|
|
300
|
+
// this comp used are collected from ITS OWN layers before the removal and
|
|
301
|
+
// nothing else — deleting comp A must never reach a solid that only some other
|
|
302
|
+
// comp uses — and after it, `usedIn` decides: an item some other comp still
|
|
303
|
+
// places is kept and reported with the comps using it. The helpers live in
|
|
304
|
+
// footage.jsx beside purge_unused_footage, the project-wide sweep.
|
|
265
305
|
OPS.delete_comp = function (args) {
|
|
266
306
|
var c = getCompById(args.compId);
|
|
307
|
+
var compId = c.id;
|
|
308
|
+
var compName = c.name;
|
|
309
|
+
var purge = (args.purgeUnusedSolids === true);
|
|
310
|
+
|
|
311
|
+
var solids = __solidItemsOf(c);
|
|
312
|
+
|
|
267
313
|
c.remove();
|
|
268
|
-
|
|
314
|
+
|
|
315
|
+
var out = { ok: true, compId: compId, name: compName };
|
|
316
|
+
var kept = [];
|
|
317
|
+
var toRemove = [];
|
|
318
|
+
var gone = [];
|
|
319
|
+
for (var i = 0; i < solids.length; i++) {
|
|
320
|
+
// Re-fetched by id after the removal rather than held across it — the
|
|
321
|
+
// handle staleness this repo has measured elsewhere is not worth risking
|
|
322
|
+
// for one lookup per solid.
|
|
323
|
+
var item = app.project.itemByID(solids[i].id);
|
|
324
|
+
if (!item) { gone.push(solids[i]); continue; }
|
|
325
|
+
var uses = __usedInList(item);
|
|
326
|
+
if (uses.length > 0) {
|
|
327
|
+
kept.push({ id: item.id, name: item.name, usedIn: uses });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
toRemove.push({ item: item, id: item.id, name: item.name, kind: "solid" });
|
|
331
|
+
}
|
|
332
|
+
if (gone.length > 0) out.solidsAlreadyGone = gone;
|
|
333
|
+
|
|
334
|
+
if (!purge) {
|
|
335
|
+
out.unusedSolidsLeft = toRemove.length;
|
|
336
|
+
if (toRemove.length > 0) {
|
|
337
|
+
out.note = toRemove.length + " solid item(s) this comp used are now used by nothing and remain in the project's Solids folder. " +
|
|
338
|
+
"Pass purgeUnusedSolids:true to remove them with the comp, or purge_unused_footage to sweep the whole project.";
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
var r = __removeItems(toRemove);
|
|
344
|
+
out.removedSolids = r.removed;
|
|
345
|
+
out.keptSolids = kept;
|
|
346
|
+
if (r.failed) {
|
|
347
|
+
var msg = "delete_comp removed comp \"" + compName + "\" (id " + compId + "), then failed purging its solids at \"" +
|
|
348
|
+
r.failed.name + "\" (id " + r.failed.id + "): " + r.error + ".";
|
|
349
|
+
if (r.removed.length > 0) msg += " Removed before it: " + __nameList(r.removed, 20) + ".";
|
|
350
|
+
else msg += " No solid had been removed yet.";
|
|
351
|
+
if (r.notAttempted.length > 0) msg += " Not attempted: " + __nameList(r.notAttempted, 20) + ".";
|
|
352
|
+
msg += " The comp is gone, so do not call delete_comp for it again. One Undo in After Effects restores the comp and " +
|
|
353
|
+
"these solids together; purge_unused_footage removes whatever is still left.";
|
|
354
|
+
throw new Error(msg);
|
|
355
|
+
}
|
|
356
|
+
if (kept.length > 0) {
|
|
357
|
+
out.note = kept.length + " solid item(s) were kept because another comp still uses them; see keptSolids.usedIn.";
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// duplicate_comp
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// There was duplicate_layer, create_comp and delete_comp but no way to copy a
|
|
366
|
+
// comp, so every rig workflow detoured through run_jsx and CompItem.duplicate()
|
|
367
|
+
// (issue #54). Two things that detour never got right:
|
|
368
|
+
//
|
|
369
|
+
// * AE's own Duplicate is SHALLOW. The copy's precomp layers point at the
|
|
370
|
+
// same source comps as the original, so "make a variant of this rig" and
|
|
371
|
+
// then editing the variant edits the original too. `deep:true` duplicates
|
|
372
|
+
// the nested comps as well and re-points the copy's layers at them, which
|
|
373
|
+
// is the entire value of the flag.
|
|
374
|
+
// * The same nested comp usually appears on several layers. Duplicating per
|
|
375
|
+
// layer fans out one copy per reference; __dupNested keeps a map from
|
|
376
|
+
// original id to its copy and reuses it, and registers the copy *before*
|
|
377
|
+
// recursing so a cycle terminates instead of recursing for ever.
|
|
378
|
+
|
|
379
|
+
// AE happily allows two project items with the same name, which makes a
|
|
380
|
+
// deep-duplicated rig unreadable in the project panel. Appending a counter is
|
|
381
|
+
// the smaller evil, and the chosen name is reported either way.
|
|
382
|
+
function __dupNameTaken(name) {
|
|
383
|
+
for (var i = 1; i <= app.project.numItems; i++) {
|
|
384
|
+
if (app.project.item(i).name === name) return true;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function __dupUniqueName(base) {
|
|
390
|
+
if (!__dupNameTaken(base)) return base;
|
|
391
|
+
for (var n = 2; n < 1000; n++) {
|
|
392
|
+
var candidate = base + " " + n;
|
|
393
|
+
if (!__dupNameTaken(candidate)) return candidate;
|
|
394
|
+
}
|
|
395
|
+
return base;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Depth is a backstop, not the cycle guard — `seen` is. AE refuses to nest a
|
|
399
|
+
// comp inside itself, but nothing here should recurse for ever if a future
|
|
400
|
+
// build ever allows it.
|
|
401
|
+
var __DUP_MAX_DEPTH = 32;
|
|
402
|
+
|
|
403
|
+
function __dupNested(src, opts, depth) {
|
|
404
|
+
var key = "C" + src.id;
|
|
405
|
+
if (opts.seen.hasOwnProperty(key)) return opts.seen[key];
|
|
406
|
+
if (depth > __DUP_MAX_DEPTH) {
|
|
407
|
+
throw new Error("nested comps are more than " + __DUP_MAX_DEPTH + " deep below the comp being duplicated");
|
|
408
|
+
}
|
|
409
|
+
var srcId = src.id;
|
|
410
|
+
var srcName = src.name;
|
|
411
|
+
var dup = src.duplicate();
|
|
412
|
+
opts.seen[key] = dup;
|
|
413
|
+
if (opts.nameSuffix) dup.name = __dupUniqueName(srcName + opts.nameSuffix);
|
|
414
|
+
opts.created.push({ fromCompId: srcId, fromName: srcName, compId: dup.id, name: dup.name });
|
|
415
|
+
__dupRepoint(dup, opts, depth);
|
|
416
|
+
return dup;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Re-point every precomp layer of a freshly duplicated comp at the duplicate of
|
|
420
|
+
// its source rather than the original. Layers whose source is footage, and
|
|
421
|
+
// layers with no source at all, are left alone.
|
|
422
|
+
function __dupRepoint(comp, opts, depth) {
|
|
423
|
+
for (var i = 1; i <= comp.numLayers; i++) {
|
|
424
|
+
var l = comp.layer(i);
|
|
425
|
+
if (!(l instanceof AVLayer)) continue;
|
|
426
|
+
var srcItem = null;
|
|
427
|
+
try { srcItem = l.source; } catch (e) { continue; }
|
|
428
|
+
if (!srcItem || !(srcItem instanceof CompItem)) continue;
|
|
429
|
+
var replacement = __dupNested(srcItem, opts, depth + 1);
|
|
430
|
+
// fixExpressions:false — the layer keeps its name and its own properties,
|
|
431
|
+
// so there is nothing for AE to rewrite, and letting it rewrite expressions
|
|
432
|
+
// on a rig is a change nobody asked for.
|
|
433
|
+
l.replaceSource(replacement, false);
|
|
434
|
+
opts.repointed += 1;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// A `folderId` argument resolved to a FolderItem, or a thrown error naming the
|
|
439
|
+
// id and what it actually is. Shared by duplicate_comp and purge_unused_footage.
|
|
440
|
+
function __folderArg(id) {
|
|
441
|
+
var f = app.project.itemByID(id);
|
|
442
|
+
if (!f) throw new Error("No project item with id " + id + " to use as folderId");
|
|
443
|
+
if (!(f instanceof FolderItem)) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
"folderId " + id + ' ("' + f.name + '") is a ' + __itemKind(f) +
|
|
446
|
+
", not a project folder. Pass the id of a folder from get_project_summary, or omit folderId."
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return f;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
OPS.duplicate_comp = function (args) {
|
|
453
|
+
var src = getCompById(args.compId);
|
|
454
|
+
var folder = null;
|
|
455
|
+
if (args.folderId !== undefined && args.folderId !== null) folder = __folderArg(args.folderId);
|
|
456
|
+
|
|
457
|
+
var opts = { seen: {}, created: [], repointed: 0, nameSuffix: null };
|
|
458
|
+
if (args.nameSuffix) opts.nameSuffix = args.nameSuffix;
|
|
459
|
+
|
|
460
|
+
// Captured as primitives before the duplicate. Some AE calls invalidate every
|
|
461
|
+
// handle held across them (exportAsMotionGraphicsTemplate is the measured
|
|
462
|
+
// one), so nothing below reads `src` again.
|
|
463
|
+
var srcId = src.id;
|
|
464
|
+
var srcName = src.name;
|
|
465
|
+
var dup = src.duplicate();
|
|
466
|
+
var newId = dup.id;
|
|
467
|
+
if (args.name) dup.name = args.name;
|
|
468
|
+
|
|
469
|
+
if (args.deep) {
|
|
470
|
+
opts.seen["C" + srcId] = dup;
|
|
471
|
+
try {
|
|
472
|
+
__dupRepoint(dup, opts, 1);
|
|
473
|
+
} catch (e) {
|
|
474
|
+
// The copy and any nested copies made before the failure are real and
|
|
475
|
+
// nothing rolled them back. Reporting {ok:true} over a half-built rig, or
|
|
476
|
+
// an error that does not name what exists, are the same class of lie.
|
|
477
|
+
var madeIds = [];
|
|
478
|
+
madeIds.push(String(newId));
|
|
479
|
+
for (var m = 0; m < opts.created.length; m++) madeIds.push(String(opts.created[m].compId));
|
|
480
|
+
throw new Error(
|
|
481
|
+
"duplicate_comp deep failed part-way: " + e.message +
|
|
482
|
+
". These comps were created and still exist: ids " + madeIds.join(", ") +
|
|
483
|
+
". Undo once in After Effects to back the whole thing out, or delete_comp them."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Re-fetch by id rather than trusting the handle held across the duplication.
|
|
489
|
+
var made = app.project.itemByID(newId);
|
|
490
|
+
if (!made) throw new Error("duplicate_comp created comp " + newId + " but it could not be read back");
|
|
491
|
+
if (folder) made.parentFolder = folder;
|
|
492
|
+
|
|
493
|
+
var out = __compSummary(made);
|
|
494
|
+
out.fromCompId = srcId;
|
|
495
|
+
out.fromName = srcName;
|
|
496
|
+
out.deep = !!args.deep;
|
|
497
|
+
if (folder) {
|
|
498
|
+
out.folderId = folder.id;
|
|
499
|
+
out.folderName = folder.name;
|
|
500
|
+
}
|
|
501
|
+
if (args.deep) {
|
|
502
|
+
out.nestedDuplicated = opts.created;
|
|
503
|
+
out.nestedCount = opts.created.length;
|
|
504
|
+
out.layersRepointed = opts.repointed;
|
|
505
|
+
if (opts.created.length === 0) {
|
|
506
|
+
out.note = "deep:true had nothing to do - this comp has no precomp layers.";
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
out.note = "Shallow copy: its precomp layers still point at the SAME nested comps as the original, " +
|
|
510
|
+
"so editing one of those edits both. Pass deep:true to duplicate the nested comps too.";
|
|
511
|
+
}
|
|
512
|
+
return out;
|
|
269
513
|
};
|
|
270
514
|
|
|
271
515
|
OPS.set_active_comp = function (args) {
|
|
@@ -371,15 +615,33 @@ OPS.create_text_layer = function (args) {
|
|
|
371
615
|
var align = args.anchorAlign === undefined ? "left" : args.anchorAlign;
|
|
372
616
|
var wantJustify = align !== "none" && __JUSTIFICATION[align] !== undefined;
|
|
373
617
|
var wantTracking = args.tracking !== undefined || align !== "none";
|
|
618
|
+
var verified = null;
|
|
374
619
|
if (args.font || args.size || args.color || wantJustify || wantTracking) {
|
|
375
620
|
var srcText = l.property("Source Text");
|
|
376
621
|
var td = srcText.value;
|
|
622
|
+
// The same TextDocument round trip set_text does, so the same read-back
|
|
623
|
+
// (issue #93 / #94, see __verifyJustification in text.jsx): the requested
|
|
624
|
+
// justification if there is one, otherwise whatever addText() gave the
|
|
625
|
+
// layer — 'none' promised to leave it alone, and that is checked too.
|
|
626
|
+
var justificationWanted = wantJustify ? __JUSTIFICATION[align] : td.justification;
|
|
377
627
|
if (args.font) td.font = args.font;
|
|
378
628
|
if (args.size) td.fontSize = args.size;
|
|
379
629
|
if (args.color) { td.applyFill = true; td.fillColor = [args.color[0], args.color[1], args.color[2]]; }
|
|
380
630
|
if (wantTracking) td.tracking = (args.tracking !== undefined ? args.tracking : 0);
|
|
381
|
-
if (wantJustify) td.justification =
|
|
631
|
+
if (wantJustify) td.justification = justificationWanted;
|
|
382
632
|
srcText.setValue(td);
|
|
633
|
+
try {
|
|
634
|
+
verified = __verifyJustification(srcText, justificationWanted);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
// A throw here would leave a layer the caller has no id for, and the
|
|
637
|
+
// natural next move — call again — makes two. Same rule as
|
|
638
|
+
// add_shape_content: take the node back, then say so. The message is
|
|
639
|
+
// amended on the same Error object so nothing about it is lost.
|
|
640
|
+
try { l.remove(); } catch (e2) {}
|
|
641
|
+
e.message = String(e.message) + " The new layer was removed, so nothing was created; " +
|
|
642
|
+
"create it with anchorAlign:\"none\" and set the alignment afterwards with set_text.";
|
|
643
|
+
throw e;
|
|
644
|
+
}
|
|
383
645
|
}
|
|
384
646
|
if (align !== "none") {
|
|
385
647
|
l.property("Transform").property("Anchor Point").setValue([0, 0, 0]);
|
|
@@ -388,7 +650,12 @@ OPS.create_text_layer = function (args) {
|
|
|
388
650
|
var p = args.position;
|
|
389
651
|
l.property("Transform").property("Position").setValue(p.length === 3 ? p : [p[0], p[1]]);
|
|
390
652
|
}
|
|
391
|
-
|
|
653
|
+
var out = __layerSummary(l);
|
|
654
|
+
if (verified) {
|
|
655
|
+
out.justification = __justificationName(verified.justification);
|
|
656
|
+
out.justificationReasserted = verified.reasserted;
|
|
657
|
+
}
|
|
658
|
+
return out;
|
|
392
659
|
};
|
|
393
660
|
|
|
394
661
|
OPS.create_solid_layer = function (args) {
|
|
@@ -415,12 +682,59 @@ OPS.create_adjustment_layer = function (args) {
|
|
|
415
682
|
return __layerSummary(l);
|
|
416
683
|
};
|
|
417
684
|
|
|
685
|
+
// Where a new shape layer's origin goes (issue #51).
|
|
686
|
+
//
|
|
687
|
+
// AE's addShape() leaves Position at the comp centre with the Anchor Point at
|
|
688
|
+
// [0,0], so layer space is offset from comp space by half a frame. Every path
|
|
689
|
+
// this toolset can write — set_shape_path vertices, add_shape_content vertices,
|
|
690
|
+
// a rect/ellipse `position` — is expressed in *layer* space, and nothing in the
|
|
691
|
+
// response says where that space starts. An agent authoring in comp pixels gets
|
|
692
|
+
// the whole drawing shifted by (width/2, height/2), and the check that would
|
|
693
|
+
// catch it is a downsampled screenshot.
|
|
694
|
+
//
|
|
695
|
+
// Default [0,0] makes layer space and comp space the same space for a fresh
|
|
696
|
+
// shape layer, which is the only arrangement in which a vertex list means what
|
|
697
|
+
// it says. "center" is AE's own spawn point, kept as one word so the old
|
|
698
|
+
// behaviour is still one argument away.
|
|
699
|
+
function __shapeSpawnPosition(comp, position) {
|
|
700
|
+
if (position === undefined || position === null) return [0, 0];
|
|
701
|
+
if (position === "center") return [comp.width / 2, comp.height / 2];
|
|
702
|
+
if (position instanceof Array && position.length >= 2) {
|
|
703
|
+
if (position.length === 3) return [position[0], position[1], position[2]];
|
|
704
|
+
return [position[0], position[1]];
|
|
705
|
+
}
|
|
706
|
+
throw new Error(
|
|
707
|
+
"create_shape_layer: position must be [x,y], [x,y,z] or the string \"center\" (After Effects' own " +
|
|
708
|
+
"spawn point, the comp centre). Got: " + String(position)
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
418
712
|
OPS.create_shape_layer = function (args) {
|
|
419
713
|
var c = getCompById(args.compId);
|
|
714
|
+
var pos = __shapeSpawnPosition(c, args.position);
|
|
420
715
|
var l = c.layers.addShape();
|
|
421
716
|
if (args.name) l.name = args.name;
|
|
717
|
+
try {
|
|
718
|
+
l.property("Transform").property("Position").setValue(pos);
|
|
719
|
+
} catch (ePos) {
|
|
720
|
+
// The shape that can still get here is three components on a 2D layer, which
|
|
721
|
+
// AE refuses. Leaving an empty shape layer in the timeline behind a thrown
|
|
722
|
+
// error is the half-built failure add_shape_content refuses to produce.
|
|
723
|
+
try { l.remove(); } catch (eRm) {}
|
|
724
|
+
throw new Error(
|
|
725
|
+
"create_shape_layer could not set position to [" + pos.join(", ") + "]: " + ePos.message +
|
|
726
|
+
". (A three-component position needs a 3D layer; create it 2D and set threeDLayer with set_layer first.) " +
|
|
727
|
+
"The empty layer was removed, so nothing changed."
|
|
728
|
+
);
|
|
729
|
+
}
|
|
422
730
|
// shapes payload kept loose for v1 — the agent can use add_shape_content for detail
|
|
423
|
-
|
|
731
|
+
var out = __layerSummary(l);
|
|
732
|
+
// Read both back rather than echoing what we asked for: together they are the
|
|
733
|
+
// coordinate space every subsequent path on this layer is measured in, and a
|
|
734
|
+
// caller should never have to render a frame to find out what it is.
|
|
735
|
+
out.position = l.property("Transform").property("Position").value;
|
|
736
|
+
out.anchorPoint = l.property("Transform").property("Anchor Point").value;
|
|
737
|
+
return out;
|
|
424
738
|
};
|
|
425
739
|
|
|
426
740
|
OPS.create_precomp_layer = function (args) {
|
|
@@ -824,165 +1138,894 @@ OPS.parent_layer = function (args) {
|
|
|
824
1138
|
return out;
|
|
825
1139
|
};
|
|
826
1140
|
|
|
1141
|
+
// Layers do not reorder with moveTo. `moveTo` is a PropertyBase method for
|
|
1142
|
+
// re-ranking a property inside an indexed group, and calling it on a Layer
|
|
1143
|
+
// throws "parent is not an INDEXED_GROUP" — which is every call this op has
|
|
1144
|
+
// ever served (issue #70). AE's layer-level primitives are moveBefore,
|
|
1145
|
+
// moveAfter, moveToBeginning and moveToEnd, and all four take a *layer*, never
|
|
1146
|
+
// an index. (The one legitimate moveTo in this codebase is the shape-property
|
|
1147
|
+
// one in shapes.jsx; it is on a PropertyBase and stays.)
|
|
1148
|
+
//
|
|
1149
|
+
// Two ways in, because reorder is the op that invalidates indexes:
|
|
1150
|
+
//
|
|
1151
|
+
// * `beforeLayerId` / `afterLayerId` — place this layer relative to another
|
|
1152
|
+
// one by id. Nothing about the answer depends on the stack not having moved
|
|
1153
|
+
// since the caller last read it, which is why these are the preferred form.
|
|
1154
|
+
// * `toIndex` — the index the layer ends up at, counting from the front.
|
|
1155
|
+
// Stated as the *landing* index because "the index it displaces" and "the
|
|
1156
|
+
// index it lands on" differ by one when moving down the stack and the
|
|
1157
|
+
// caller cannot tell which one a tool meant.
|
|
1158
|
+
//
|
|
1159
|
+
// The two directions need different primitives, and swapping them is off by one
|
|
1160
|
+
// with no error. Moving up (toIndex < from), the layer currently at the target
|
|
1161
|
+
// is pushed down, so moveBefore lands exactly on it. Moving down, that layer
|
|
1162
|
+
// shifts up by one as this one leaves, so moveAfter is what lands on the target.
|
|
827
1163
|
OPS.reorder_layer = function (args) {
|
|
828
1164
|
var c = getCompById(args.compId);
|
|
829
1165
|
var l = getLayerById(c, args.layerId);
|
|
830
|
-
l.
|
|
831
|
-
|
|
1166
|
+
var from = l.index;
|
|
1167
|
+
var n = c.numLayers;
|
|
1168
|
+
var to;
|
|
1169
|
+
|
|
1170
|
+
if (args.beforeLayerId !== undefined && args.beforeLayerId !== null) {
|
|
1171
|
+
var b = getLayerById(c, args.beforeLayerId);
|
|
1172
|
+
if (b.id === l.id) throw new Error("reorder_layer: beforeLayerId is the layer being moved (" + l.id + ").");
|
|
1173
|
+
l.moveBefore(b);
|
|
1174
|
+
} else if (args.afterLayerId !== undefined && args.afterLayerId !== null) {
|
|
1175
|
+
var a = getLayerById(c, args.afterLayerId);
|
|
1176
|
+
if (a.id === l.id) throw new Error("reorder_layer: afterLayerId is the layer being moved (" + l.id + ").");
|
|
1177
|
+
l.moveAfter(a);
|
|
1178
|
+
} else if (args.toIndex !== undefined && args.toIndex !== null) {
|
|
1179
|
+
to = Math.round(args.toIndex);
|
|
1180
|
+
if (to < 1) to = 1;
|
|
1181
|
+
if (to > n) to = n;
|
|
1182
|
+
if (to === from) {
|
|
1183
|
+
// Nothing to do. Saying so beats an AE call that is a no-op anyway.
|
|
1184
|
+
} else if (to === 1) {
|
|
1185
|
+
l.moveToBeginning();
|
|
1186
|
+
} else if (to === n) {
|
|
1187
|
+
l.moveToEnd();
|
|
1188
|
+
} else if (to < from) {
|
|
1189
|
+
l.moveBefore(c.layer(to));
|
|
1190
|
+
} else {
|
|
1191
|
+
l.moveAfter(c.layer(to));
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
throw new Error("reorder_layer: pass one of toIndex, beforeLayerId or afterLayerId.");
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Report both ends of the move. The landing index is read back off the layer
|
|
1198
|
+
// rather than assumed, so a result can never claim a position AE did not
|
|
1199
|
+
// give it — and `movedFrom === index` is how a caller sees a no-op.
|
|
1200
|
+
var out = __layerSummary(l);
|
|
1201
|
+
out.movedFrom = from;
|
|
1202
|
+
return out;
|
|
832
1203
|
};
|
|
833
1204
|
|
|
834
1205
|
|
|
835
|
-
// =====
|
|
1206
|
+
// ===== snapshot.jsx =====
|
|
836
1207
|
|
|
837
|
-
//
|
|
1208
|
+
// snapshot.jsx — a cheap structural fingerprint of a comp, and the diff
|
|
1209
|
+
// between two of them.
|
|
1210
|
+
//
|
|
1211
|
+
// Verifying a write today means reading the comp back and comparing by eye,
|
|
1212
|
+
// and every one of those reads is re-sent on every later request for the rest
|
|
1213
|
+
// of the session. A fingerprint is the other end of that trade: it records
|
|
1214
|
+
// what an agent actually checks after a write — which layers exist, what they
|
|
1215
|
+
// are called, where they sit in time, what they are parented to, how many
|
|
1216
|
+
// keyframes/expressions/effects they carry — and nothing else. The diff of two
|
|
1217
|
+
// of them is a few dozen tokens for "3 layers added: ids 512-514; layer 498
|
|
1218
|
+
// Opacity keys 0 -> 4" (issue #52).
|
|
1219
|
+
//
|
|
1220
|
+
// Cheapness is the whole point, so the walk stops at the layer's own Transform
|
|
1221
|
+
// group plus four named properties. It never opens an effect's parameters or a
|
|
1222
|
+
// shape layer's Contents — those are what make get_layer_full expensive, and a
|
|
1223
|
+
// fingerprint that costs as much as the read it replaces is worth nothing.
|
|
1224
|
+
//
|
|
1225
|
+
// That has a cost of its own, and per the "a scoped read must say what it left
|
|
1226
|
+
// out" rule it is stated rather than left to be discovered: a diff can only
|
|
1227
|
+
// report a field it records. __DIFF_COVERS travels with every diff for exactly
|
|
1228
|
+
// that reason — "no differences" must never be read as "identical".
|
|
1229
|
+
|
|
1230
|
+
var __DIFF_COVERS = "Compares recorded fields only - not property values, expression text, " +
|
|
1231
|
+
"effect parameters, masks or shape contents. \"No differences\" means none of the recorded fields moved, " +
|
|
1232
|
+
"not that the two states render identically.";
|
|
1233
|
+
|
|
1234
|
+
var __FP_COVERS = "A snapshot records, per layer: id, name, index, type, inPoint, outPoint, startTime, " +
|
|
1235
|
+
"parentId, enabled, keyframe counts per Transform property (plus Marker, Time Remap and Source Text), " +
|
|
1236
|
+
"expression count and effect count. Per comp: name, size, duration, frame rate, work area and markers. " +
|
|
1237
|
+
"It does NOT record property values, expression text, effect parameters, mask shapes or shape contents, " +
|
|
1238
|
+
"because reading those costs as much as the read this replaces.";
|
|
1239
|
+
|
|
1240
|
+
// Times are floats out of AE and are compared, not displayed, so they are
|
|
1241
|
+
// rounded once here rather than at every comparison site.
|
|
1242
|
+
var __FP_PRECISION = 1000000;
|
|
1243
|
+
|
|
1244
|
+
function __fpRound(n) {
|
|
1245
|
+
if (typeof n !== "number") return n;
|
|
1246
|
+
if (!isFinite(n)) return null;
|
|
1247
|
+
return Math.round(n * __FP_PRECISION) / __FP_PRECISION;
|
|
1248
|
+
}
|
|
838
1249
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
847
|
-
|
|
1250
|
+
// Properties worth a fingerprint that do not live under Transform. Each is one
|
|
1251
|
+
// guarded lookup; a layer that has none of them pays four null checks.
|
|
1252
|
+
var __FP_EXTRA_PROPS = ["Marker", "Time Remap", "Source Text", "Audio Levels"];
|
|
1253
|
+
|
|
1254
|
+
function __fpCountProperty(p, out) {
|
|
1255
|
+
if (!p) return;
|
|
1256
|
+
var keys = 0;
|
|
1257
|
+
try { keys = p.numKeys; } catch (e) { return; }
|
|
1258
|
+
if (typeof keys !== "number") return;
|
|
1259
|
+
if (keys > 0) out.keyCounts[p.name] = keys;
|
|
1260
|
+
try {
|
|
1261
|
+
if (p.canSetExpression && p.expression) out.expressionCount += 1;
|
|
1262
|
+
} catch (e2) {}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function __fpLayer(l) {
|
|
1266
|
+
var out = {
|
|
1267
|
+
id: l.id,
|
|
1268
|
+
name: l.name,
|
|
1269
|
+
index: l.index,
|
|
1270
|
+
type: __layerKind(l),
|
|
1271
|
+
inPoint: __fpRound(l.inPoint),
|
|
1272
|
+
outPoint: __fpRound(l.outPoint),
|
|
1273
|
+
startTime: __fpRound(l.startTime),
|
|
1274
|
+
parentId: null,
|
|
1275
|
+
enabled: l.enabled,
|
|
1276
|
+
keyCounts: {},
|
|
1277
|
+
expressionCount: 0,
|
|
1278
|
+
effectCount: 0
|
|
1279
|
+
};
|
|
1280
|
+
try { if (l.parent) out.parentId = l.parent.id; } catch (e0) {}
|
|
1281
|
+
|
|
1282
|
+
var tr = null;
|
|
1283
|
+
try { tr = l.property("Transform"); } catch (e1) {}
|
|
1284
|
+
if (tr) {
|
|
1285
|
+
for (var i = 1; i <= tr.numProperties; i++) __fpCountProperty(tr.property(i), out);
|
|
848
1286
|
}
|
|
1287
|
+
for (var j = 0; j < __FP_EXTRA_PROPS.length; j++) {
|
|
1288
|
+
var extra = null;
|
|
1289
|
+
try { extra = l.property(__FP_EXTRA_PROPS[j]); } catch (e2) {}
|
|
1290
|
+
__fpCountProperty(extra, out);
|
|
1291
|
+
}
|
|
1292
|
+
// Effects are counted, never walked. The parameter tree is the expensive
|
|
1293
|
+
// part and an agent checking "did my effect land" only needs the count plus
|
|
1294
|
+
// list_effects when it did not.
|
|
1295
|
+
try {
|
|
1296
|
+
var fx = l.property("Effects");
|
|
1297
|
+
if (fx) out.effectCount = fx.numProperties;
|
|
1298
|
+
} catch (e3) {}
|
|
1299
|
+
return out;
|
|
849
1300
|
}
|
|
850
1301
|
|
|
851
|
-
|
|
852
|
-
var
|
|
853
|
-
var
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
var
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
var rotProp = l.threeDLayer ? tr.property("Z Rotation") : tr.property("Rotation");
|
|
862
|
-
__setOrKeyAtTime(rotProp, p.rotation, t, kf);
|
|
1302
|
+
function __fpCompMarkers(c) {
|
|
1303
|
+
var out = [];
|
|
1304
|
+
var mp = null;
|
|
1305
|
+
try { mp = c.markerProperty; } catch (e) { return out; }
|
|
1306
|
+
if (!mp) return out;
|
|
1307
|
+
var n = 0;
|
|
1308
|
+
try { n = mp.numKeys; } catch (e2) { return out; }
|
|
1309
|
+
for (var i = 1; i <= n; i++) {
|
|
1310
|
+
var mv = mp.keyValue(i);
|
|
1311
|
+
out.push(String(__fpRound(mp.keyTime(i))) + "|" + String(mv.comment) + "|" + String(__fpRound(mv.duration)));
|
|
863
1312
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
if (p.orientation !== undefined && l.threeDLayer) __setOrKeyAtTime(tr.property("Orientation"), p.orientation, t, kf);
|
|
867
|
-
if (p.xRotation !== undefined && l.threeDLayer) __setOrKeyAtTime(tr.property("X Rotation"), p.xRotation, t, kf);
|
|
868
|
-
if (p.yRotation !== undefined && l.threeDLayer) __setOrKeyAtTime(tr.property("Y Rotation"), p.yRotation, t, kf);
|
|
869
|
-
if (p.zRotation !== undefined && l.threeDLayer) __setOrKeyAtTime(tr.property("Z Rotation"), p.zRotation, t, kf);
|
|
870
|
-
return { ok: true };
|
|
871
|
-
};
|
|
1313
|
+
return out;
|
|
1314
|
+
}
|
|
872
1315
|
|
|
1316
|
+
function __compFingerprint(compId) {
|
|
1317
|
+
var c = getCompById(compId);
|
|
1318
|
+
var fp = {
|
|
1319
|
+
compId: c.id,
|
|
1320
|
+
name: c.name,
|
|
1321
|
+
width: c.width,
|
|
1322
|
+
height: c.height,
|
|
1323
|
+
duration: __fpRound(c.duration),
|
|
1324
|
+
frameRate: __fpRound(c.frameRate),
|
|
1325
|
+
workAreaStart: __fpRound(c.workAreaStart),
|
|
1326
|
+
workAreaDuration: __fpRound(c.workAreaDuration),
|
|
1327
|
+
numLayers: c.numLayers,
|
|
1328
|
+
markers: __fpCompMarkers(c),
|
|
1329
|
+
layers: []
|
|
1330
|
+
};
|
|
1331
|
+
for (var i = 1; i <= c.numLayers; i++) fp.layers.push(__fpLayer(c.layer(i)));
|
|
1332
|
+
return fp;
|
|
1333
|
+
}
|
|
873
1334
|
|
|
874
|
-
//
|
|
1335
|
+
// ---------------------------------------------------------------------------
|
|
1336
|
+
// The diff
|
|
1337
|
+
// ---------------------------------------------------------------------------
|
|
1338
|
+
// Pure: two fingerprints in, one object out, no AE. That is what lets it be
|
|
1339
|
+
// tested against synthetic input (tests/unit/comp-snapshot.mjs) — there is no
|
|
1340
|
+
// ExtendScript runtime on a runner and this is the part that has to be right.
|
|
1341
|
+
|
|
1342
|
+
// Below a frame at any sane frame rate, above the float noise a round-trip
|
|
1343
|
+
// through JSON leaves behind. Without it a comp re-read after a frame-rate
|
|
1344
|
+
// change reports every layer "retimed" by 1e-15.
|
|
1345
|
+
var __DIFF_EPS = 0.000001;
|
|
1346
|
+
|
|
1347
|
+
function __diffMoved(a, b) {
|
|
1348
|
+
if (typeof a !== "number") return a !== b;
|
|
1349
|
+
if (typeof b !== "number") return true;
|
|
1350
|
+
return Math.abs(a - b) > __DIFF_EPS;
|
|
1351
|
+
}
|
|
875
1352
|
|
|
876
|
-
//
|
|
1353
|
+
// ES3 has no Map. The "L" prefix keeps a numeric id away from anything already
|
|
1354
|
+
// on Object.prototype.
|
|
1355
|
+
function __fpById(fp) {
|
|
1356
|
+
var m = {};
|
|
1357
|
+
for (var i = 0; i < fp.layers.length; i++) m["L" + fp.layers[i].id] = fp.layers[i];
|
|
1358
|
+
return m;
|
|
1359
|
+
}
|
|
877
1360
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1361
|
+
function __diffKeyCounts(a, b) {
|
|
1362
|
+
var out = null;
|
|
1363
|
+
var seen = {};
|
|
1364
|
+
var k;
|
|
1365
|
+
for (k in a) {
|
|
1366
|
+
if (!a.hasOwnProperty(k)) continue;
|
|
1367
|
+
seen[k] = true;
|
|
1368
|
+
var bv = 0;
|
|
1369
|
+
if (b.hasOwnProperty(k)) bv = b[k];
|
|
1370
|
+
if (a[k] !== bv) {
|
|
1371
|
+
if (!out) out = {};
|
|
1372
|
+
out[k] = { from: a[k], to: bv };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
for (k in b) {
|
|
1376
|
+
if (!b.hasOwnProperty(k)) continue;
|
|
1377
|
+
if (seen[k]) continue;
|
|
1378
|
+
if (!out) out = {};
|
|
1379
|
+
out[k] = { from: 0, to: b[k] };
|
|
1380
|
+
}
|
|
1381
|
+
return out;
|
|
1382
|
+
}
|
|
883
1383
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1384
|
+
var __DIFF_EXACT_FIELDS = ["name", "type", "enabled"];
|
|
1385
|
+
var __DIFF_TIME_FIELDS = ["inPoint", "outPoint", "startTime"];
|
|
1386
|
+
var __DIFF_COUNT_FIELDS = ["expressionCount", "effectCount"];
|
|
1387
|
+
|
|
1388
|
+
// `index` is deliberately not compared here: inserting one layer shifts every
|
|
1389
|
+
// index below it, which would report twenty changed layers for one addition.
|
|
1390
|
+
// Relative order is compared separately, in __diffReordered.
|
|
1391
|
+
function __diffLayer(a, b) {
|
|
1392
|
+
var ch = {};
|
|
1393
|
+
var n = 0;
|
|
1394
|
+
var i, k;
|
|
1395
|
+
for (i = 0; i < __DIFF_EXACT_FIELDS.length; i++) {
|
|
1396
|
+
k = __DIFF_EXACT_FIELDS[i];
|
|
1397
|
+
if (a[k] !== b[k]) { ch[k] = { from: a[k], to: b[k] }; n += 1; }
|
|
888
1398
|
}
|
|
889
|
-
|
|
1399
|
+
for (i = 0; i < __DIFF_TIME_FIELDS.length; i++) {
|
|
1400
|
+
k = __DIFF_TIME_FIELDS[i];
|
|
1401
|
+
if (__diffMoved(a[k], b[k])) { ch[k] = { from: a[k], to: b[k] }; n += 1; }
|
|
1402
|
+
}
|
|
1403
|
+
for (i = 0; i < __DIFF_COUNT_FIELDS.length; i++) {
|
|
1404
|
+
k = __DIFF_COUNT_FIELDS[i];
|
|
1405
|
+
if (a[k] !== b[k]) { ch[k] = { from: a[k], to: b[k] }; n += 1; }
|
|
1406
|
+
}
|
|
1407
|
+
if (a.parentId !== b.parentId) { ch.parentId = { from: a.parentId, to: b.parentId }; n += 1; }
|
|
1408
|
+
var kc = __diffKeyCounts(a.keyCounts || {}, b.keyCounts || {});
|
|
1409
|
+
if (kc) { ch.keyCounts = kc; n += 1; }
|
|
1410
|
+
if (n === 0) return null;
|
|
1411
|
+
return { id: b.id, name: b.name, changes: ch };
|
|
890
1412
|
}
|
|
891
1413
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
dim = (prop.value && prop.value.length) ? prop.value.length : 1;
|
|
907
|
-
}
|
|
908
|
-
var inEase = interp.easeIn || { influence: 33, speed: 0 };
|
|
909
|
-
var outEase = interp.easeOut || { influence: 33, speed: 0 };
|
|
910
|
-
var inArr = []; var outArr = [];
|
|
911
|
-
for (var d = 0; d < dim; d++) {
|
|
912
|
-
inArr.push(new KeyframeEase(inEase.speed, inEase.influence));
|
|
913
|
-
outArr.push(new KeyframeEase(outEase.speed, outEase.influence));
|
|
914
|
-
}
|
|
915
|
-
prop.setTemporalEaseAtKey(keyIndex, inArr, outArr);
|
|
1414
|
+
// Only layers present on both sides, and only when their order relative to one
|
|
1415
|
+
// another actually changed. A layer added or removed elsewhere in the stack is
|
|
1416
|
+
// not a reorder of anything.
|
|
1417
|
+
function __diffReordered(before, after, aMap, bMap) {
|
|
1418
|
+
var seqA = [];
|
|
1419
|
+
var seqB = [];
|
|
1420
|
+
var i;
|
|
1421
|
+
for (i = 0; i < before.layers.length; i++) {
|
|
1422
|
+
var idA = before.layers[i].id;
|
|
1423
|
+
if (bMap.hasOwnProperty("L" + idA)) seqA.push(idA);
|
|
1424
|
+
}
|
|
1425
|
+
for (i = 0; i < after.layers.length; i++) {
|
|
1426
|
+
var idB = after.layers[i].id;
|
|
1427
|
+
if (aMap.hasOwnProperty("L" + idB)) seqB.push(idB);
|
|
916
1428
|
}
|
|
1429
|
+
if (seqA.length !== seqB.length) return null;
|
|
1430
|
+
var same = true;
|
|
1431
|
+
for (i = 0; i < seqA.length; i++) {
|
|
1432
|
+
if (seqA[i] !== seqB[i]) { same = false; break; }
|
|
1433
|
+
}
|
|
1434
|
+
if (same) return null;
|
|
1435
|
+
var posA = {};
|
|
1436
|
+
for (i = 0; i < seqA.length; i++) posA["L" + seqA[i]] = i;
|
|
1437
|
+
var moved = [];
|
|
1438
|
+
for (i = 0; i < seqB.length; i++) {
|
|
1439
|
+
var id = seqB[i];
|
|
1440
|
+
if (posA["L" + id] === i) continue;
|
|
1441
|
+
moved.push({
|
|
1442
|
+
id: id,
|
|
1443
|
+
name: bMap["L" + id].name,
|
|
1444
|
+
fromIndex: aMap["L" + id].index,
|
|
1445
|
+
toIndex: bMap["L" + id].index
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
if (moved.length === 0) return null;
|
|
1449
|
+
return moved;
|
|
917
1450
|
}
|
|
918
1451
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
var
|
|
922
|
-
|
|
923
|
-
prop.setValueAtTime(args.time, args.value);
|
|
924
|
-
if (args.interpolation) {
|
|
925
|
-
var idx = __findKeyIndexAtTime(prop, args.time);
|
|
926
|
-
if (idx > 0) __applyInterpolationToKey(prop, idx, args.interpolation);
|
|
1452
|
+
function __diffMarkers(a, b) {
|
|
1453
|
+
if (a.length !== b.length) return { from: a.length, to: b.length };
|
|
1454
|
+
for (var i = 0; i < a.length; i++) {
|
|
1455
|
+
if (a[i] !== b[i]) return { count: a.length, edited: true };
|
|
927
1456
|
}
|
|
928
|
-
return
|
|
929
|
-
}
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
930
1459
|
|
|
931
|
-
|
|
932
|
-
var
|
|
933
|
-
var
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
};
|
|
1460
|
+
function __diffCompFields(a, b) {
|
|
1461
|
+
var ch = {};
|
|
1462
|
+
var n = 0;
|
|
1463
|
+
if (a.name !== b.name) { ch.name = { from: a.name, to: b.name }; n += 1; }
|
|
1464
|
+
if (a.width !== b.width || a.height !== b.height) {
|
|
1465
|
+
ch.size = { from: [a.width, a.height], to: [b.width, b.height] };
|
|
1466
|
+
n += 1;
|
|
1467
|
+
}
|
|
1468
|
+
if (__diffMoved(a.duration, b.duration)) { ch.duration = { from: a.duration, to: b.duration }; n += 1; }
|
|
1469
|
+
if (__diffMoved(a.frameRate, b.frameRate)) { ch.frameRate = { from: a.frameRate, to: b.frameRate }; n += 1; }
|
|
1470
|
+
if (__diffMoved(a.workAreaStart, b.workAreaStart) || __diffMoved(a.workAreaDuration, b.workAreaDuration)) {
|
|
1471
|
+
ch.workArea = {
|
|
1472
|
+
from: [a.workAreaStart, a.workAreaDuration],
|
|
1473
|
+
to: [b.workAreaStart, b.workAreaDuration]
|
|
1474
|
+
};
|
|
1475
|
+
n += 1;
|
|
1476
|
+
}
|
|
1477
|
+
var mk = __diffMarkers(a.markers || [], b.markers || []);
|
|
1478
|
+
if (mk) { ch.markers = mk; n += 1; }
|
|
1479
|
+
if (n === 0) return null;
|
|
1480
|
+
return ch;
|
|
1481
|
+
}
|
|
940
1482
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1483
|
+
// ---------- summary prose ----------
|
|
1484
|
+
|
|
1485
|
+
function __diffPlural(n) {
|
|
1486
|
+
if (n === 1) return "";
|
|
1487
|
+
return "s";
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// "512-514" for a run, "512, 517" otherwise. Ids come out of AE in creation
|
|
1491
|
+
// order, so a batch of new layers is nearly always consecutive and this is the
|
|
1492
|
+
// difference between a readable line and a wall of numbers.
|
|
1493
|
+
function __diffIdList(ids) {
|
|
1494
|
+
if (!ids.length) return "";
|
|
1495
|
+
var sorted = [];
|
|
1496
|
+
for (var i = 0; i < ids.length; i++) sorted.push(ids[i]);
|
|
1497
|
+
sorted.sort(function (x, y) { return x - y; });
|
|
1498
|
+
var parts = [];
|
|
1499
|
+
var start = sorted[0];
|
|
1500
|
+
var prev = sorted[0];
|
|
1501
|
+
for (var j = 1; j <= sorted.length; j++) {
|
|
1502
|
+
var cur = sorted[j];
|
|
1503
|
+
var breaks = true;
|
|
1504
|
+
if (j < sorted.length && cur === prev + 1) breaks = false;
|
|
1505
|
+
if (breaks) {
|
|
1506
|
+
if (start === prev) parts.push(String(start));
|
|
1507
|
+
else if (prev === start + 1) parts.push(String(start) + ", " + String(prev));
|
|
1508
|
+
else parts.push(String(start) + "-" + String(prev));
|
|
1509
|
+
start = cur;
|
|
960
1510
|
}
|
|
961
|
-
|
|
962
|
-
index: i,
|
|
963
|
-
time: prop.keyTime(i),
|
|
964
|
-
value: prop.keyValue(i),
|
|
965
|
-
interpolation: {
|
|
966
|
-
"in": __invInterp(prop.keyInInterpolationType(i)),
|
|
967
|
-
"out": __invInterp(prop.keyOutInterpolationType(i)),
|
|
968
|
-
},
|
|
969
|
-
ease: ease,
|
|
970
|
-
tangents: tangents,
|
|
971
|
-
});
|
|
1511
|
+
prev = cur;
|
|
972
1512
|
}
|
|
973
|
-
return
|
|
974
|
-
}
|
|
1513
|
+
return parts.join(", ");
|
|
1514
|
+
}
|
|
975
1515
|
|
|
976
|
-
function
|
|
977
|
-
if (
|
|
978
|
-
|
|
979
|
-
if (t === KeyframeInterpolationType.HOLD) return "hold";
|
|
980
|
-
return "unknown";
|
|
1516
|
+
function __diffParentLabel(v) {
|
|
1517
|
+
if (v === null || v === undefined) return "none";
|
|
1518
|
+
return String(v);
|
|
981
1519
|
}
|
|
982
1520
|
|
|
983
|
-
|
|
984
|
-
var
|
|
985
|
-
var
|
|
1521
|
+
function __diffLayerPhrases(entry) {
|
|
1522
|
+
var label = "layer " + entry.id;
|
|
1523
|
+
var c = entry.changes;
|
|
1524
|
+
var out = [];
|
|
1525
|
+
if (c.name) out.push(label + ' renamed "' + c.name.from + '" -> "' + c.name.to + '"');
|
|
1526
|
+
if (c.enabled) {
|
|
1527
|
+
if (c.enabled.to) out.push(label + " enabled");
|
|
1528
|
+
else out.push(label + " disabled");
|
|
1529
|
+
}
|
|
1530
|
+
var times = [];
|
|
1531
|
+
if (c.inPoint) times.push("in " + c.inPoint.from + " -> " + c.inPoint.to);
|
|
1532
|
+
if (c.outPoint) times.push("out " + c.outPoint.from + " -> " + c.outPoint.to);
|
|
1533
|
+
if (c.startTime) times.push("start " + c.startTime.from + " -> " + c.startTime.to);
|
|
1534
|
+
if (times.length) out.push(label + " retimed (" + times.join(", ") + ")");
|
|
1535
|
+
if (c.parentId) {
|
|
1536
|
+
out.push(label + " re-parented " + __diffParentLabel(c.parentId.from) + " -> " + __diffParentLabel(c.parentId.to));
|
|
1537
|
+
}
|
|
1538
|
+
if (c.keyCounts) {
|
|
1539
|
+
for (var k in c.keyCounts) {
|
|
1540
|
+
if (!c.keyCounts.hasOwnProperty(k)) continue;
|
|
1541
|
+
out.push(label + " " + k + " keys " + c.keyCounts[k].from + " -> " + c.keyCounts[k].to);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (c.effectCount) out.push(label + " effects " + c.effectCount.from + " -> " + c.effectCount.to);
|
|
1545
|
+
if (c.expressionCount) {
|
|
1546
|
+
out.push(label + " expressions " + c.expressionCount.from + " -> " + c.expressionCount.to);
|
|
1547
|
+
}
|
|
1548
|
+
if (c.type) out.push(label + " type " + c.type.from + " -> " + c.type.to);
|
|
1549
|
+
return out;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function __diffCompPhrases(ch) {
|
|
1553
|
+
var out = [];
|
|
1554
|
+
if (ch.name) out.push('comp renamed "' + ch.name.from + '" -> "' + ch.name.to + '"');
|
|
1555
|
+
if (ch.size) {
|
|
1556
|
+
out.push("comp resized " + ch.size.from[0] + "x" + ch.size.from[1] + " -> " + ch.size.to[0] + "x" + ch.size.to[1]);
|
|
1557
|
+
}
|
|
1558
|
+
if (ch.duration) out.push("comp duration " + ch.duration.from + " -> " + ch.duration.to);
|
|
1559
|
+
if (ch.frameRate) out.push("comp frame rate " + ch.frameRate.from + " -> " + ch.frameRate.to);
|
|
1560
|
+
if (ch.workArea) out.push("comp work area moved");
|
|
1561
|
+
if (ch.markers) {
|
|
1562
|
+
if (ch.markers.edited) out.push("comp markers edited");
|
|
1563
|
+
else out.push("comp markers " + ch.markers.from + " -> " + ch.markers.to);
|
|
1564
|
+
}
|
|
1565
|
+
return out;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// One line. Past this many clauses the rest is counted rather than spelled out
|
|
1569
|
+
// — the structured fields carry everything, and a summary nobody reads to the
|
|
1570
|
+
// end is worse than a short one that names where to look.
|
|
1571
|
+
var __DIFF_SUMMARY_CLAUSES = 8;
|
|
1572
|
+
|
|
1573
|
+
function __diffSummary(d) {
|
|
1574
|
+
var parts = [];
|
|
1575
|
+
var i;
|
|
1576
|
+
if (d.added) {
|
|
1577
|
+
var addedIds = [];
|
|
1578
|
+
for (i = 0; i < d.added.length; i++) addedIds.push(d.added[i].id);
|
|
1579
|
+
parts.push(d.added.length + " layer" + __diffPlural(d.added.length) + " added: ids " + __diffIdList(addedIds));
|
|
1580
|
+
}
|
|
1581
|
+
if (d.removed) {
|
|
1582
|
+
var removedIds = [];
|
|
1583
|
+
for (i = 0; i < d.removed.length; i++) removedIds.push(d.removed[i].id);
|
|
1584
|
+
parts.push(d.removed.length + " layer" + __diffPlural(d.removed.length) + " removed: ids " + __diffIdList(removedIds));
|
|
1585
|
+
}
|
|
1586
|
+
if (d.reordered) {
|
|
1587
|
+
parts.push(d.reordered.length + " layer" + __diffPlural(d.reordered.length) + " moved in the stack");
|
|
1588
|
+
}
|
|
1589
|
+
if (d.changed) {
|
|
1590
|
+
for (i = 0; i < d.changed.length; i++) {
|
|
1591
|
+
var phrases = __diffLayerPhrases(d.changed[i]);
|
|
1592
|
+
for (var p = 0; p < phrases.length; p++) parts.push(phrases[p]);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (d.comp) {
|
|
1596
|
+
var compPhrases = __diffCompPhrases(d.comp);
|
|
1597
|
+
for (i = 0; i < compPhrases.length; i++) parts.push(compPhrases[i]);
|
|
1598
|
+
}
|
|
1599
|
+
if (parts.length === 0) {
|
|
1600
|
+
return "No differences in the recorded fields. Property values, expression text, effect parameters " +
|
|
1601
|
+
"and shape contents are not compared, so this is not a claim that nothing changed at all.";
|
|
1602
|
+
}
|
|
1603
|
+
if (parts.length > __DIFF_SUMMARY_CLAUSES) {
|
|
1604
|
+
var extra = parts.length - __DIFF_SUMMARY_CLAUSES;
|
|
1605
|
+
parts = parts.slice(0, __DIFF_SUMMARY_CLAUSES);
|
|
1606
|
+
parts.push("and " + extra + " more change" + __diffPlural(extra) + " (see the fields below)");
|
|
1607
|
+
}
|
|
1608
|
+
return parts.join("; ");
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// Two fingerprints -> only what moved. Unchanged layers are counted, never
|
|
1612
|
+
// listed: listing them is the cost this whole thing exists to avoid.
|
|
1613
|
+
function __diffFingerprints(before, after) {
|
|
1614
|
+
var aMap = __fpById(before);
|
|
1615
|
+
var bMap = __fpById(after);
|
|
1616
|
+
var added = [];
|
|
1617
|
+
var removed = [];
|
|
1618
|
+
var changed = [];
|
|
1619
|
+
var unchanged = 0;
|
|
1620
|
+
var i, l;
|
|
1621
|
+
for (i = 0; i < after.layers.length; i++) {
|
|
1622
|
+
l = after.layers[i];
|
|
1623
|
+
var prev = aMap["L" + l.id];
|
|
1624
|
+
if (!prev) {
|
|
1625
|
+
added.push({ id: l.id, name: l.name, index: l.index, type: l.type });
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
var d = __diffLayer(prev, l);
|
|
1629
|
+
if (d) changed.push(d);
|
|
1630
|
+
else unchanged += 1;
|
|
1631
|
+
}
|
|
1632
|
+
for (i = 0; i < before.layers.length; i++) {
|
|
1633
|
+
l = before.layers[i];
|
|
1634
|
+
if (bMap.hasOwnProperty("L" + l.id)) continue;
|
|
1635
|
+
removed.push({ id: l.id, name: l.name, index: l.index, type: l.type });
|
|
1636
|
+
}
|
|
1637
|
+
var reordered = __diffReordered(before, after, aMap, bMap);
|
|
1638
|
+
var comp = __diffCompFields(before, after);
|
|
1639
|
+
|
|
1640
|
+
var out = { compId: after.compId, compName: after.name };
|
|
1641
|
+
if (added.length) out.added = added;
|
|
1642
|
+
if (removed.length) out.removed = removed;
|
|
1643
|
+
if (changed.length) out.changed = changed;
|
|
1644
|
+
if (reordered) out.reordered = reordered;
|
|
1645
|
+
if (comp) out.comp = comp;
|
|
1646
|
+
out.unchangedLayers = unchanged;
|
|
1647
|
+
var count = added.length + removed.length + changed.length;
|
|
1648
|
+
if (reordered) count += reordered.length;
|
|
1649
|
+
if (comp) count += 1;
|
|
1650
|
+
out.changeCount = count;
|
|
1651
|
+
out.summary = __diffSummary(out);
|
|
1652
|
+
out.covers = __DIFF_COVERS;
|
|
1653
|
+
return out;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// ---------------------------------------------------------------------------
|
|
1657
|
+
// Ops
|
|
1658
|
+
// ---------------------------------------------------------------------------
|
|
1659
|
+
// Both are internal: the tools an agent sees are snapshot_comp and diff_comp,
|
|
1660
|
+
// and those are half server-resident. Only the panel can read After Effects,
|
|
1661
|
+
// and only the server can remember anything between calls — so the server
|
|
1662
|
+
// keeps the fingerprint and these two gather it. See snapshots/store.ts.
|
|
1663
|
+
|
|
1664
|
+
OPS._comp_fingerprint = noUndo(function (args) {
|
|
1665
|
+
return __compFingerprint(args.compId);
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
OPS._comp_diff = noUndo(function (args) {
|
|
1669
|
+
var before = args.since;
|
|
1670
|
+
if (!before || !before.layers) {
|
|
1671
|
+
throw new Error("_comp_diff needs a stored fingerprint in `since` (the MCP server supplies it from the snapshot id)");
|
|
1672
|
+
}
|
|
1673
|
+
var compId = args.compId;
|
|
1674
|
+
if (compId === undefined || compId === null) compId = before.compId;
|
|
1675
|
+
var after = __compFingerprint(compId);
|
|
1676
|
+
return { diff: __diffFingerprints(before, after), fingerprint: after };
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
// ---------------------------------------------------------------------------
|
|
1680
|
+
// diff:true on the write ops
|
|
1681
|
+
// ---------------------------------------------------------------------------
|
|
1682
|
+
// The before-fingerprint has to be taken inside the same bridge call as the
|
|
1683
|
+
// write, or it is not a before at all: a separate snapshot_comp is a second
|
|
1684
|
+
// round-trip during which anything can happen, and the agent has to remember to
|
|
1685
|
+
// make it. run_jsx and run_batch therefore fingerprint, run, fingerprint again,
|
|
1686
|
+
// and diff — one call, one answer.
|
|
1687
|
+
|
|
1688
|
+
function __diffPushUnique(list, v) {
|
|
1689
|
+
for (var i = 0; i < list.length; i++) {
|
|
1690
|
+
if (list[i] === v) return;
|
|
1691
|
+
}
|
|
1692
|
+
list.push(v);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// Explicit diffCompId wins. Otherwise a batch names its own comps in its ops,
|
|
1696
|
+
// and a script gets the comp the user is looking at. When none of the three
|
|
1697
|
+
// yields anything the diff is refused with a reason rather than quietly
|
|
1698
|
+
// skipped — a missing diff that looks like "nothing changed" is the failure
|
|
1699
|
+
// this whole feature exists to prevent.
|
|
1700
|
+
function __diffCompIds(args, ops) {
|
|
1701
|
+
var ids = [];
|
|
1702
|
+
if (args && args.diffCompId !== undefined && args.diffCompId !== null) {
|
|
1703
|
+
__diffPushUnique(ids, args.diffCompId);
|
|
1704
|
+
return ids;
|
|
1705
|
+
}
|
|
1706
|
+
if (ops) {
|
|
1707
|
+
for (var i = 0; i < ops.length; i++) {
|
|
1708
|
+
var a = ops[i].args;
|
|
1709
|
+
if (a && typeof a.compId === "number") __diffPushUnique(ids, a.compId);
|
|
1710
|
+
}
|
|
1711
|
+
if (ids.length > 0) return ids;
|
|
1712
|
+
}
|
|
1713
|
+
var active = null;
|
|
1714
|
+
try { active = app.project.activeItem; } catch (e) {}
|
|
1715
|
+
if (active && active instanceof CompItem) __diffPushUnique(ids, active.id);
|
|
1716
|
+
return ids;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function __diffStart(args, ops) {
|
|
1720
|
+
if (!args || !args.diff) return null;
|
|
1721
|
+
var ids = __diffCompIds(args, ops);
|
|
1722
|
+
if (ids.length === 0) {
|
|
1723
|
+
return {
|
|
1724
|
+
unavailable: true,
|
|
1725
|
+
reason: "no compId appeared in this call and no composition is open in the viewer — pass diffCompId to name the comp to fingerprint"
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
var state = { ids: ids, before: [], unavailable: false };
|
|
1729
|
+
for (var i = 0; i < ids.length; i++) {
|
|
1730
|
+
try {
|
|
1731
|
+
state.before.push(__compFingerprint(ids[i]));
|
|
1732
|
+
} catch (e) {
|
|
1733
|
+
return { unavailable: true, reason: "comp " + ids[i] + " could not be fingerprinted before the call: " + e.message };
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return state;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function __diffFinish(state) {
|
|
1740
|
+
if (!state) return null;
|
|
1741
|
+
if (state.unavailable) {
|
|
1742
|
+
return {
|
|
1743
|
+
unavailable: true,
|
|
1744
|
+
reason: state.reason,
|
|
1745
|
+
summary: "No diff was taken: " + state.reason + ". The call itself is unaffected."
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
var comps = [];
|
|
1749
|
+
var total = 0;
|
|
1750
|
+
for (var i = 0; i < state.before.length; i++) {
|
|
1751
|
+
var b = state.before[i];
|
|
1752
|
+
var after = null;
|
|
1753
|
+
try {
|
|
1754
|
+
after = __compFingerprint(b.compId);
|
|
1755
|
+
} catch (e) {
|
|
1756
|
+
comps.push({
|
|
1757
|
+
compId: b.compId,
|
|
1758
|
+
compName: b.name,
|
|
1759
|
+
gone: true,
|
|
1760
|
+
changeCount: 1,
|
|
1761
|
+
summary: "comp " + b.compId + ' ("' + b.name + '") can no longer be read: ' + e.message
|
|
1762
|
+
});
|
|
1763
|
+
total += 1;
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
var d = __diffFingerprints(b, after);
|
|
1767
|
+
comps.push(d);
|
|
1768
|
+
total += d.changeCount;
|
|
1769
|
+
}
|
|
1770
|
+
if (comps.length === 1) return comps[0];
|
|
1771
|
+
var parts = [];
|
|
1772
|
+
for (var j = 0; j < comps.length; j++) parts.push("comp " + comps[j].compId + ": " + comps[j].summary);
|
|
1773
|
+
return { comps: comps, changeCount: total, summary: parts.join(" | "), covers: __DIFF_COVERS };
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// A script or batch that threw still changed whatever it changed before it
|
|
1777
|
+
// stopped, and nothing rolls back. Finding that stop point by reading the comp
|
|
1778
|
+
// back is one of the three cases issue #52 was opened for, so the diff is put
|
|
1779
|
+
// where the agent will actually see it: on the error. The error object itself
|
|
1780
|
+
// is mutated rather than replaced, so `line` and `stack` survive for the
|
|
1781
|
+
// caller's own reporting.
|
|
1782
|
+
function __diffAnnotateError(e, state) {
|
|
1783
|
+
if (!state) return;
|
|
1784
|
+
var d = null;
|
|
1785
|
+
try { d = __diffFinish(state); } catch (e2) { return; }
|
|
1786
|
+
if (!d || !d.summary) return;
|
|
1787
|
+
try {
|
|
1788
|
+
e.message = String(e.message) + " || Changed before it stopped: " + d.summary +
|
|
1789
|
+
" (nothing rolls back - read the state back rather than re-running)";
|
|
1790
|
+
} catch (e3) {}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// run_jsx returns whatever the script returned, which may be a number or a
|
|
1794
|
+
// string with nowhere to hang a diff on. With diff:true it is enveloped
|
|
1795
|
+
// instead. The null envelope __rjResult already builds is the right shape
|
|
1796
|
+
// already, so that one is extended rather than nested inside a second one.
|
|
1797
|
+
function __rjWithDiff(out, diff, undoGroupName) {
|
|
1798
|
+
if (out && typeof out === "object" && !(out instanceof Array) &&
|
|
1799
|
+
out.ok === true && out.returned === null && out.note) {
|
|
1800
|
+
out.diff = diff;
|
|
1801
|
+
return out;
|
|
1802
|
+
}
|
|
1803
|
+
return { ok: true, returned: out, undoGroup: undoGroupName, diff: diff };
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
|
|
1807
|
+
// ===== transforms.jsx =====
|
|
1808
|
+
|
|
1809
|
+
// transforms.jsx — fast-path for setting common transform properties.
|
|
1810
|
+
|
|
1811
|
+
function __setOrKeyAtTime(prop, value, time, keyframe) {
|
|
1812
|
+
if (keyframe && time !== undefined && time !== null) {
|
|
1813
|
+
prop.setValueAtTime(time, value);
|
|
1814
|
+
} else if (time !== undefined && time !== null && !keyframe) {
|
|
1815
|
+
// Set value at time only meaningful if property has keyframes; else set static.
|
|
1816
|
+
if (prop.numKeys > 0) prop.setValueAtTime(time, value);
|
|
1817
|
+
else prop.setValue(value);
|
|
1818
|
+
} else {
|
|
1819
|
+
prop.setValue(value);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
OPS.set_transform = function (args) {
|
|
1824
|
+
var c = getCompById(args.compId);
|
|
1825
|
+
var l = getLayerById(c, args.layerId);
|
|
1826
|
+
var t = args.time;
|
|
1827
|
+
var kf = !!args.keyframe;
|
|
1828
|
+
var tr = l.property("Transform");
|
|
1829
|
+
var p = args.properties || {};
|
|
1830
|
+
if (p.position !== undefined) __setOrKeyAtTime(tr.property("Position"), p.position, t, kf);
|
|
1831
|
+
if (p.scale !== undefined) __setOrKeyAtTime(tr.property("Scale"), p.scale, t, kf);
|
|
1832
|
+
if (p.rotation !== undefined) {
|
|
1833
|
+
var rotProp = l.threeDLayer ? tr.property("Z Rotation") : tr.property("Rotation");
|
|
1834
|
+
__setOrKeyAtTime(rotProp, p.rotation, t, kf);
|
|
1835
|
+
}
|
|
1836
|
+
if (p.anchorPoint !== undefined) __setOrKeyAtTime(tr.property("Anchor Point"), p.anchorPoint, t, kf);
|
|
1837
|
+
if (p.opacity !== undefined) __setOrKeyAtTime(tr.property("Opacity"), p.opacity, t, kf);
|
|
1838
|
+
if (p.orientation !== undefined && l.threeDLayer) __setOrKeyAtTime(tr.property("Orientation"), p.orientation, t, kf);
|
|
1839
|
+
if (p.xRotation !== undefined && l.threeDLayer) __setOrKeyAtTime(tr.property("X Rotation"), p.xRotation, t, kf);
|
|
1840
|
+
if (p.yRotation !== undefined && l.threeDLayer) __setOrKeyAtTime(tr.property("Y Rotation"), p.yRotation, t, kf);
|
|
1841
|
+
if (p.zRotation !== undefined && l.threeDLayer) __setOrKeyAtTime(tr.property("Z Rotation"), p.zRotation, t, kf);
|
|
1842
|
+
return { ok: true };
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
// ===== keyframes.jsx =====
|
|
1847
|
+
|
|
1848
|
+
// keyframes.jsx — generic keyframe ops.
|
|
1849
|
+
|
|
1850
|
+
var __INTERP_MAP = {
|
|
1851
|
+
linear: KeyframeInterpolationType.LINEAR,
|
|
1852
|
+
bezier: KeyframeInterpolationType.BEZIER,
|
|
1853
|
+
hold: KeyframeInterpolationType.HOLD,
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
function __findKeyIndexAtTime(prop, time, eps) {
|
|
1857
|
+
eps = eps || 0.001;
|
|
1858
|
+
for (var i = 1; i <= prop.numKeys; i++) {
|
|
1859
|
+
if (Math.abs(prop.keyTime(i) - time) < eps) return i;
|
|
1860
|
+
}
|
|
1861
|
+
return -1;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// ---------- ease array sizing (issue #50) ----------
|
|
1865
|
+
//
|
|
1866
|
+
// setTemporalEaseAtKey wants an array of KeyframeEase whose length belongs to
|
|
1867
|
+
// the *property*, and it is not derivable from the value: a 2D layer Scale
|
|
1868
|
+
// wants 3, a shape Ellipse Size wants 2 while its value reads [w,h], Opacity
|
|
1869
|
+
// and a slider want 1, and a spatial Position wants 1 whether the layer is 2D
|
|
1870
|
+
// or 3D because the ease runs along the motion path. Get it wrong and AE throws
|
|
1871
|
+
// "parameter 2" and says nothing else — no property name, no expected count.
|
|
1872
|
+
//
|
|
1873
|
+
// So: derive the likely count from the property, then try the others. The
|
|
1874
|
+
// derivation is the fast path; the retry is the safety net, because the Ellipse
|
|
1875
|
+
// Size case is precisely the one no table gets right from the outside. Whatever
|
|
1876
|
+
// AE accepted is reported back, so the answer for a given property stops being
|
|
1877
|
+
// folklore and becomes something a caller can read off a result.
|
|
1878
|
+
var __EASE_ARITIES = [1, 2, 3, 4];
|
|
1879
|
+
|
|
1880
|
+
/** The count to try first, from the property rather than from the value passed in. */
|
|
1881
|
+
function __easePropertyArity(prop) {
|
|
1882
|
+
// Spatial first and unconditionally: a 3D Position is ThreeD_SPATIAL and
|
|
1883
|
+
// still takes one entry, so the value type must not get a say here.
|
|
1884
|
+
if (prop.isSpatial) return 1;
|
|
1885
|
+
var vt = null;
|
|
1886
|
+
try { vt = prop.propertyValueType; } catch (eType) {}
|
|
1887
|
+
if (vt !== null && vt !== undefined && typeof PropertyValueType !== "undefined") {
|
|
1888
|
+
if (vt === PropertyValueType.OneD) return 1;
|
|
1889
|
+
if (vt === PropertyValueType.TwoD) return 2;
|
|
1890
|
+
if (vt === PropertyValueType.TwoD_SPATIAL) return 1;
|
|
1891
|
+
if (vt === PropertyValueType.ThreeD) return 3;
|
|
1892
|
+
if (vt === PropertyValueType.ThreeD_SPATIAL) return 1;
|
|
1893
|
+
if (vt === PropertyValueType.COLOR) return 4;
|
|
1894
|
+
}
|
|
1895
|
+
var v = null;
|
|
1896
|
+
try { v = prop.value; } catch (eVal) {}
|
|
1897
|
+
if (v && v.length) return v.length;
|
|
1898
|
+
return 1;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/** Derived count first, then every other plausible one, no repeats. */
|
|
1902
|
+
function __easeArityCandidates(prop) {
|
|
1903
|
+
var first = __easePropertyArity(prop);
|
|
1904
|
+
var out = [first];
|
|
1905
|
+
for (var i = 0; i < __EASE_ARITIES.length; i++) {
|
|
1906
|
+
if (__EASE_ARITIES[i] !== first) out.push(__EASE_ARITIES[i]);
|
|
1907
|
+
}
|
|
1908
|
+
return out;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function __easeArray(ease, n) {
|
|
1912
|
+
var arr = [];
|
|
1913
|
+
for (var i = 0; i < n; i++) arr.push(new KeyframeEase(ease.speed, ease.influence));
|
|
1914
|
+
return arr;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* Apply one {influence, speed} pair to every dimension of `prop` at `keyIndex`,
|
|
1919
|
+
* and return the number of KeyframeEase entries After Effects accepted.
|
|
1920
|
+
*
|
|
1921
|
+
* Throws only when no count works, naming every one it tried. An ease that
|
|
1922
|
+
* quietly failed to land is invisible until someone watches the render, which
|
|
1923
|
+
* is the same class of lie as a swallowed error.
|
|
1924
|
+
*/
|
|
1925
|
+
function __applyTemporalEase(prop, keyIndex, easeIn, easeOut) {
|
|
1926
|
+
var inEase = easeIn || { influence: 33, speed: 0 };
|
|
1927
|
+
var outEase = easeOut || { influence: 33, speed: 0 };
|
|
1928
|
+
var candidates = __easeArityCandidates(prop);
|
|
1929
|
+
var lastMessage = "";
|
|
1930
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
1931
|
+
var n = candidates[i];
|
|
1932
|
+
try {
|
|
1933
|
+
prop.setTemporalEaseAtKey(keyIndex, __easeArray(inEase, n), __easeArray(outEase, n));
|
|
1934
|
+
return n;
|
|
1935
|
+
} catch (e) {
|
|
1936
|
+
lastMessage = (e && e.message) ? String(e.message) : String(e);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
var label = "the property";
|
|
1940
|
+
try { label = "'" + prop.name + "'"; } catch (eName) {}
|
|
1941
|
+
throw new Error(
|
|
1942
|
+
"Could not set the temporal ease on " + label + " at key " + keyIndex + ": After Effects rejected ease " +
|
|
1943
|
+
"arrays of " + candidates.join(", ") + " entries. Last error from AE: " + lastMessage
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/** Returns the ease arity that was used, or null when no ease was requested. */
|
|
1948
|
+
function __applyInterpolationToKey(prop, keyIndex, interp) {
|
|
1949
|
+
if (!interp) return null;
|
|
1950
|
+
var inT = interp["in"] && __INTERP_MAP[interp["in"]] ? __INTERP_MAP[interp["in"]] : prop.keyInInterpolationType(keyIndex);
|
|
1951
|
+
var outT = interp["out"] && __INTERP_MAP[interp["out"]] ? __INTERP_MAP[interp["out"]] : prop.keyOutInterpolationType(keyIndex);
|
|
1952
|
+
prop.setInterpolationTypeAtKey(keyIndex, inT, outT);
|
|
1953
|
+
if (interp.easeIn || interp.easeOut) {
|
|
1954
|
+
return __applyTemporalEase(prop, keyIndex, interp.easeIn, interp.easeOut);
|
|
1955
|
+
}
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
OPS.add_keyframe = function (args) {
|
|
1960
|
+
var c = getCompById(args.compId);
|
|
1961
|
+
var l = getLayerById(c, args.layerId);
|
|
1962
|
+
var prop = walkProperty(l, args.propertyPath);
|
|
1963
|
+
prop.setValueAtTime(args.time, args.value);
|
|
1964
|
+
var easeDimensions = null;
|
|
1965
|
+
if (args.interpolation) {
|
|
1966
|
+
var idx = __findKeyIndexAtTime(prop, args.time);
|
|
1967
|
+
if (idx > 0) easeDimensions = __applyInterpolationToKey(prop, idx, args.interpolation);
|
|
1968
|
+
}
|
|
1969
|
+
var out = { ok: true, keyIndex: __findKeyIndexAtTime(prop, args.time) };
|
|
1970
|
+
if (easeDimensions !== null) out.easeDimensions = easeDimensions;
|
|
1971
|
+
return out;
|
|
1972
|
+
};
|
|
1973
|
+
|
|
1974
|
+
OPS.remove_keyframe = function (args) {
|
|
1975
|
+
var c = getCompById(args.compId);
|
|
1976
|
+
var l = getLayerById(c, args.layerId);
|
|
1977
|
+
var prop = walkProperty(l, args.propertyPath);
|
|
1978
|
+
var idx = __findKeyIndexAtTime(prop, args.time);
|
|
1979
|
+
if (idx < 1) throw new Error("No keyframe at time " + args.time);
|
|
1980
|
+
prop.removeKey(idx);
|
|
1981
|
+
return { ok: true };
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
OPS.get_keyframes = noUndo(function (args) {
|
|
1985
|
+
var c = getCompById(args.compId);
|
|
1986
|
+
var l = getLayerById(c, args.layerId);
|
|
1987
|
+
var prop = walkProperty(l, args.propertyPath);
|
|
1988
|
+
var keys = [];
|
|
1989
|
+
for (var i = 1; i <= prop.numKeys; i++) {
|
|
1990
|
+
var ease = null;
|
|
1991
|
+
try {
|
|
1992
|
+
var inE = prop.keyInTemporalEase(i);
|
|
1993
|
+
var outE = prop.keyOutTemporalEase(i);
|
|
1994
|
+
ease = {
|
|
1995
|
+
easeIn: { influence: inE[0].influence, speed: inE[0].speed },
|
|
1996
|
+
easeOut: { influence: outE[0].influence, speed: outE[0].speed },
|
|
1997
|
+
};
|
|
1998
|
+
} catch (e) {}
|
|
1999
|
+
var tangents = null;
|
|
2000
|
+
if (prop.isSpatial) {
|
|
2001
|
+
try { tangents = { inTangent: prop.keyInSpatialTangent(i), outTangent: prop.keyOutSpatialTangent(i) }; }
|
|
2002
|
+
catch (e2) {}
|
|
2003
|
+
}
|
|
2004
|
+
keys.push({
|
|
2005
|
+
index: i,
|
|
2006
|
+
time: prop.keyTime(i),
|
|
2007
|
+
value: prop.keyValue(i),
|
|
2008
|
+
interpolation: {
|
|
2009
|
+
"in": __invInterp(prop.keyInInterpolationType(i)),
|
|
2010
|
+
"out": __invInterp(prop.keyOutInterpolationType(i)),
|
|
2011
|
+
},
|
|
2012
|
+
ease: ease,
|
|
2013
|
+
tangents: tangents,
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
return keys;
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
function __invInterp(t) {
|
|
2020
|
+
if (t === KeyframeInterpolationType.LINEAR) return "linear";
|
|
2021
|
+
if (t === KeyframeInterpolationType.BEZIER) return "bezier";
|
|
2022
|
+
if (t === KeyframeInterpolationType.HOLD) return "hold";
|
|
2023
|
+
return "unknown";
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
OPS.set_interpolation = function (args) {
|
|
2027
|
+
var c = getCompById(args.compId);
|
|
2028
|
+
var l = getLayerById(c, args.layerId);
|
|
986
2029
|
var prop = walkProperty(l, args.propertyPath);
|
|
987
2030
|
__applyInterpolationToKey(prop, args.keyIndex, { "in": args["in"], "out": args["out"] });
|
|
988
2031
|
return { ok: true };
|
|
@@ -992,8 +2035,13 @@ OPS.set_temporal_ease = function (args) {
|
|
|
992
2035
|
var c = getCompById(args.compId);
|
|
993
2036
|
var l = getLayerById(c, args.layerId);
|
|
994
2037
|
var prop = walkProperty(l, args.propertyPath);
|
|
995
|
-
|
|
996
|
-
|
|
2038
|
+
// A call with neither ease would previously return {ok:true} having done
|
|
2039
|
+
// nothing at all, which reads as "the ease is set" to whoever asked for it.
|
|
2040
|
+
if (!args.easeIn && !args.easeOut) {
|
|
2041
|
+
throw new Error("set_temporal_ease needs easeIn, easeOut or both — nothing was changed.");
|
|
2042
|
+
}
|
|
2043
|
+
var n = __applyTemporalEase(prop, args.keyIndex, args.easeIn, args.easeOut);
|
|
2044
|
+
return { ok: true, easeDimensions: n };
|
|
997
2045
|
};
|
|
998
2046
|
|
|
999
2047
|
OPS.set_spatial_tangents = function (args) {
|
|
@@ -1010,11 +2058,54 @@ OPS.set_spatial_tangents = function (args) {
|
|
|
1010
2058
|
|
|
1011
2059
|
// expressions.jsx — get/set/toggle/clear expressions on any property.
|
|
1012
2060
|
|
|
2061
|
+
// After Effects reports a broken expression on `Property.expressionError` and
|
|
2062
|
+
// nowhere a script can otherwise see: assigning `.expression` succeeds whatever
|
|
2063
|
+
// the text says, and the failure is a warning banner in the UI (issue #97).
|
|
2064
|
+
// The field is filled in when the expression is *evaluated*, which a bare
|
|
2065
|
+
// assignment does not necessarily do — so the value is read first, inside a
|
|
2066
|
+
// try, purely to force one evaluation. Whether AE would populate the field
|
|
2067
|
+
// without that nudge cannot be settled offline; forcing it costs one evaluation
|
|
2068
|
+
// and makes the answer independent of the question. `.value` throws on a
|
|
2069
|
+
// property that has none (a group), which is also not an expression error.
|
|
2070
|
+
function __expressionError(prop) {
|
|
2071
|
+
try { var forced = prop.value; } catch (e) {}
|
|
2072
|
+
var err = "";
|
|
2073
|
+
try { err = prop.expressionError; } catch (e2) { err = ""; }
|
|
2074
|
+
return err ? String(err) : "";
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function __propertyPathLabel(path) {
|
|
2078
|
+
var parts = [];
|
|
2079
|
+
for (var i = 0; i < path.length; i++) parts.push(String(path[i]));
|
|
2080
|
+
return parts.join(" > ");
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// Throws if the expression on `prop` does not evaluate, naming the property,
|
|
2084
|
+
// AE's own message and both ways forward. The expression stays written — the
|
|
2085
|
+
// message says so — because nothing here rolls back and a caller fixing a
|
|
2086
|
+
// typo wants the property it was aimed at, not a cleared one.
|
|
2087
|
+
function __assertExpressionEvaluates(prop, path, verb) {
|
|
2088
|
+
var err = __expressionError(prop);
|
|
2089
|
+
if (!err) return;
|
|
2090
|
+
var enabledNow;
|
|
2091
|
+
try { enabledNow = !!prop.expressionEnabled; } catch (e) { enabledNow = null; }
|
|
2092
|
+
throw new Error(
|
|
2093
|
+
"Expression on " + __propertyPathLabel(path) + " was " + verb +
|
|
2094
|
+
" but After Effects cannot evaluate it: " + err +
|
|
2095
|
+
(enabledNow === false ? " (After Effects has disabled it.)" : "") +
|
|
2096
|
+
" Fix the expression and call set_expression again, or clear_expression to remove it."
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
1013
2100
|
OPS.get_expression = noUndo(function (args) {
|
|
1014
2101
|
var c = getCompById(args.compId);
|
|
1015
2102
|
var l = getLayerById(c, args.layerId);
|
|
1016
2103
|
var prop = walkProperty(l, args.propertyPath);
|
|
1017
|
-
return {
|
|
2104
|
+
return {
|
|
2105
|
+
expression: prop.expression || "",
|
|
2106
|
+
enabled: !!prop.expressionEnabled,
|
|
2107
|
+
expressionError: __expressionError(prop),
|
|
2108
|
+
};
|
|
1018
2109
|
});
|
|
1019
2110
|
|
|
1020
2111
|
OPS.set_expression = function (args) {
|
|
@@ -1023,6 +2114,7 @@ OPS.set_expression = function (args) {
|
|
|
1023
2114
|
var prop = walkProperty(l, args.propertyPath);
|
|
1024
2115
|
prop.expression = args.expression || "";
|
|
1025
2116
|
prop.expressionEnabled = true;
|
|
2117
|
+
__assertExpressionEvaluates(prop, args.propertyPath, "written");
|
|
1026
2118
|
return { ok: true };
|
|
1027
2119
|
};
|
|
1028
2120
|
|
|
@@ -1031,6 +2123,9 @@ OPS.toggle_expression = function (args) {
|
|
|
1031
2123
|
var l = getLayerById(c, args.layerId);
|
|
1032
2124
|
var prop = walkProperty(l, args.propertyPath);
|
|
1033
2125
|
prop.expressionEnabled = !!args.enabled;
|
|
2126
|
+
// Disabling cannot fail this way; enabling can surface an error that has
|
|
2127
|
+
// been sitting on the property since it was disabled.
|
|
2128
|
+
if (args.enabled) __assertExpressionEvaluates(prop, args.propertyPath, "enabled");
|
|
1034
2129
|
return { ok: true };
|
|
1035
2130
|
};
|
|
1036
2131
|
|
|
@@ -1184,12 +2279,63 @@ var __JUSTIFICATION = {
|
|
|
1184
2279
|
full: ParagraphJustification.FULL_JUSTIFY_LASTLINE_LEFT,
|
|
1185
2280
|
};
|
|
1186
2281
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
2282
|
+
// The name for an enum value, for results and errors. AE has more FULL_JUSTIFY
|
|
2283
|
+
// variants than the map offers, so a value the map does not know comes back as
|
|
2284
|
+
// its number rather than as the nearest name.
|
|
2285
|
+
function __justificationName(v) {
|
|
2286
|
+
for (var k in __JUSTIFICATION) {
|
|
2287
|
+
if (__JUSTIFICATION.hasOwnProperty(k) && __JUSTIFICATION[k] === v) return k;
|
|
2288
|
+
}
|
|
2289
|
+
return String(v);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// Issue #93: on AE 26.3 the TextDocument round trip — read `.value`, change
|
|
2293
|
+
// `.text`, `setValue()` — has come back with justification reset to CENTER
|
|
2294
|
+
// even though the document that went in still said LEFT; issue #94 reports the
|
|
2295
|
+
// setter storing RIGHT when handed CENTER from a script. Nothing raises in
|
|
2296
|
+
// either case and the mechanism is unknown, so the only guard that can be built
|
|
2297
|
+
// blind is the one this repo asks for anyway: read the property back and
|
|
2298
|
+
// compare with what should be there. A second write of the justification alone
|
|
2299
|
+
// has been seen to land (`set_text({justification})` on its own always did), so
|
|
2300
|
+
// one re-assert is tried before giving up. If that also disagrees, the caller
|
|
2301
|
+
// is told what was expected and what the layer actually shows — a text layer
|
|
2302
|
+
// that silently re-centres is the failure this codebase refuses everywhere.
|
|
2303
|
+
//
|
|
2304
|
+
// `expected` is an enum value: the requested one, or the one read before the
|
|
2305
|
+
// mutation. Returns {justification, reasserted}; throws when it cannot deliver.
|
|
2306
|
+
function __verifyJustification(src, expected) {
|
|
2307
|
+
var actual = src.value.justification;
|
|
2308
|
+
if (actual === expected) return { justification: actual, reasserted: false };
|
|
2309
|
+
var again = src.value;
|
|
2310
|
+
again.justification = expected;
|
|
2311
|
+
src.setValue(again);
|
|
2312
|
+
actual = src.value.justification;
|
|
2313
|
+
if (actual === expected) return { justification: actual, reasserted: true };
|
|
2314
|
+
throw new Error(
|
|
2315
|
+
"Source Text justification did not land: expected " + __justificationName(expected) +
|
|
2316
|
+
" (" + String(expected) + "), the layer reads " + __justificationName(actual) +
|
|
2317
|
+
" (" + String(actual) + ") after a second write. The other fields were written. " +
|
|
2318
|
+
"Read the layer back with get_layer_full include:[\"text\"] before relying on its alignment."
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
OPS.set_text = function (args) {
|
|
2323
|
+
var c = getCompById(args.compId);
|
|
2324
|
+
var l = getLayerById(c, args.layerId);
|
|
1190
2325
|
if (!(l instanceof TextLayer)) throw new Error("Layer is not a TextLayer");
|
|
1191
2326
|
var src = l.property("Source Text");
|
|
1192
2327
|
var td = src.value;
|
|
2328
|
+
// Captured before anything is touched: "undefined fields unchanged" is a
|
|
2329
|
+
// claim about this value, and it is what the read-back is checked against
|
|
2330
|
+
// when no justification was asked for.
|
|
2331
|
+
var justificationBefore = td.justification;
|
|
2332
|
+
var justificationWanted = justificationBefore;
|
|
2333
|
+
if (args.justification !== undefined) {
|
|
2334
|
+
if (__JUSTIFICATION[args.justification] === undefined) {
|
|
2335
|
+
throw new Error("Unknown justification '" + String(args.justification) + "'; expected one of left, center, right, full. Nothing was changed.");
|
|
2336
|
+
}
|
|
2337
|
+
justificationWanted = __JUSTIFICATION[args.justification];
|
|
2338
|
+
}
|
|
1193
2339
|
if (args.text !== undefined) td.text = args.text;
|
|
1194
2340
|
if (args.font !== undefined) td.font = args.font;
|
|
1195
2341
|
if (args.size !== undefined) td.fontSize = args.size;
|
|
@@ -1198,9 +2344,7 @@ OPS.set_text = function (args) {
|
|
|
1198
2344
|
if (args.strokeWidth !== undefined) td.strokeWidth = args.strokeWidth;
|
|
1199
2345
|
if (args.tracking !== undefined) td.tracking = args.tracking;
|
|
1200
2346
|
if (args.leading !== undefined) td.leading = args.leading;
|
|
1201
|
-
if (args.justification !== undefined)
|
|
1202
|
-
if (__JUSTIFICATION[args.justification]) td.justification = __JUSTIFICATION[args.justification];
|
|
1203
|
-
}
|
|
2347
|
+
if (args.justification !== undefined) td.justification = justificationWanted;
|
|
1204
2348
|
if (args.applyFill !== undefined) td.applyFill = args.applyFill;
|
|
1205
2349
|
if (args.applyStroke !== undefined) td.applyStroke = args.applyStroke;
|
|
1206
2350
|
if (args.fauxBold !== undefined) td.fauxBold = args.fauxBold;
|
|
@@ -1209,7 +2353,12 @@ OPS.set_text = function (args) {
|
|
|
1209
2353
|
if (args.smallCaps !== undefined) td.smallCaps = args.smallCaps;
|
|
1210
2354
|
if (args.baselineShift !== undefined) td.baselineShift = args.baselineShift;
|
|
1211
2355
|
src.setValue(td);
|
|
1212
|
-
|
|
2356
|
+
var verified = __verifyJustification(src, justificationWanted);
|
|
2357
|
+
return {
|
|
2358
|
+
ok: true,
|
|
2359
|
+
justification: __justificationName(verified.justification),
|
|
2360
|
+
justificationReasserted: verified.reasserted,
|
|
2361
|
+
};
|
|
1213
2362
|
};
|
|
1214
2363
|
|
|
1215
2364
|
OPS.add_text_animator = function (args) {
|
|
@@ -1600,10 +2749,26 @@ var __SCREENSHOT_TARGET_PX = 1280;
|
|
|
1600
2749
|
// The correct downsample was always derivable from the comp, and an agent that
|
|
1601
2750
|
// forgot it got a full-resolution 4K frame — the most expensive accident
|
|
1602
2751
|
// available through these tools. So derive it, and let an explicit value win.
|
|
2752
|
+
function __autoDownsampleFor(comp, targetPx) {
|
|
2753
|
+
var longEdge = comp.width > comp.height ? comp.width : comp.height;
|
|
2754
|
+
var n = Math.ceil(longEdge / targetPx);
|
|
2755
|
+
if (!(n > 1)) return 1;
|
|
2756
|
+
return n > 8 ? 8 : n;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
1603
2759
|
// 1080p -> 2 (960px), 4K -> 3 (1280px), and anything already small -> 1.
|
|
1604
2760
|
function __autoDownsample(comp) {
|
|
1605
|
-
|
|
1606
|
-
|
|
2761
|
+
return __autoDownsampleFor(comp, __SCREENSHOT_TARGET_PX);
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
// A contact sheet of N frames has to cost about what one frame costs, so each
|
|
2765
|
+
// tile gets 1/sqrt(N) of the single-frame long edge — N tiles of 1/N the area
|
|
2766
|
+
// each. Expressed as a multiple of what this comp's *single* frame would have
|
|
2767
|
+
// used rather than as its own target, for two reasons: it cannot drift from
|
|
2768
|
+
// __autoDownsample, and the factor is an integer, so deriving from the target
|
|
2769
|
+
// instead would let the rounding leave a 1080p sheet at nearly twice the budget.
|
|
2770
|
+
function __tileDownsample(comp, count) {
|
|
2771
|
+
var n = Math.ceil(__autoDownsample(comp) * Math.sqrt(count));
|
|
1607
2772
|
if (!(n > 1)) return 1;
|
|
1608
2773
|
return n > 8 ? 8 : n;
|
|
1609
2774
|
}
|
|
@@ -1617,24 +2782,68 @@ function __resolveDownsample(comp, requested) {
|
|
|
1617
2782
|
// reduced frame directly instead of writing full size and resampling
|
|
1618
2783
|
// afterwards. That is faster (a quarter of the pixels at factor 2) and needs no
|
|
1619
2784
|
// external image tool, which is what makes it work off macOS.
|
|
2785
|
+
//
|
|
2786
|
+
// Factor 1 is *set*, not skipped. Skipping it rendered at whatever the user had
|
|
2787
|
+
// left the viewer on, so a comp parked at Quarter answered `downsample: 1` with
|
|
2788
|
+
// a quarter-size frame and `downsample: 2` came back **larger** than
|
|
2789
|
+
// `downsample: 1` (issue #72). Designers leave heavy comps at Quarter or Third
|
|
2790
|
+
// as a matter of course, so that was the common case rather than a corner. The
|
|
2791
|
+
// panel reads the real dimensions out of the PNG's IHDR, which means the
|
|
2792
|
+
// response stayed honest while the *picture* was not the one asked for — the
|
|
2793
|
+
// worse of the two failures, because an agent that can see a frame believes it.
|
|
2794
|
+
// Every factor reaching this function now names the resolution it renders at.
|
|
1620
2795
|
function __saveFrameAt(comp, time, file, factor) {
|
|
1621
|
-
|
|
1622
|
-
comp.saveFrameToPng(time, file);
|
|
1623
|
-
return;
|
|
1624
|
-
}
|
|
2796
|
+
var f = (factor > 1) ? Math.round(factor) : 1;
|
|
1625
2797
|
var previous = comp.resolutionFactor;
|
|
1626
2798
|
try {
|
|
1627
|
-
comp.resolutionFactor = [
|
|
2799
|
+
comp.resolutionFactor = [f, f];
|
|
1628
2800
|
comp.saveFrameToPng(time, file);
|
|
1629
2801
|
} finally {
|
|
1630
2802
|
// Restore unconditionally — a failed render must never leave the user
|
|
1631
|
-
// looking at a half-resolution comp.
|
|
2803
|
+
// looking at a half-resolution comp. This covers factor 1 too: the render
|
|
2804
|
+
// happens at Full and the viewer goes back to Quarter afterwards.
|
|
1632
2805
|
comp.resolutionFactor = previous;
|
|
1633
2806
|
}
|
|
1634
2807
|
}
|
|
1635
2808
|
|
|
2809
|
+
// Several times in one call. Every requested time gets an entry, in order,
|
|
2810
|
+
// whether or not it rendered — the panel draws a marked block for the ones that
|
|
2811
|
+
// did not, so the sheet it composes still lines up with the times that were
|
|
2812
|
+
// asked for. Dropping a failed tile would silently renumber the rest, which is
|
|
2813
|
+
// the same class of lie as swallowing an error.
|
|
2814
|
+
function __contactSheetFrames(comp, args) {
|
|
2815
|
+
var times = args.times;
|
|
2816
|
+
var ds;
|
|
2817
|
+
if (args.downsample === undefined || args.downsample === null) {
|
|
2818
|
+
ds = __tileDownsample(comp, times.length);
|
|
2819
|
+
} else {
|
|
2820
|
+
ds = __clampDownsample(args.downsample);
|
|
2821
|
+
}
|
|
2822
|
+
var tiles = [];
|
|
2823
|
+
for (var i = 0; i < times.length; i++) {
|
|
2824
|
+
var entry = { time: times[i], downsample: ds };
|
|
2825
|
+
try {
|
|
2826
|
+
var p = __tmpPngPath();
|
|
2827
|
+
__saveFrameAt(comp, times[i], new File(p), ds);
|
|
2828
|
+
entry.path = p;
|
|
2829
|
+
} catch (e) {
|
|
2830
|
+
// One time that will not render must not cost the other five.
|
|
2831
|
+
entry.error = String(e && e.message ? e.message : e);
|
|
2832
|
+
}
|
|
2833
|
+
tiles.push(entry);
|
|
2834
|
+
}
|
|
2835
|
+
return {
|
|
2836
|
+
contactSheet: true,
|
|
2837
|
+
tiles: tiles,
|
|
2838
|
+
downsample: ds,
|
|
2839
|
+
width: comp.width, height: comp.height,
|
|
2840
|
+
times: times, compId: comp.id
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
|
|
1636
2844
|
OPS.screenshot_frame = noUndo(function (args) {
|
|
1637
2845
|
var c = getCompById(args.compId);
|
|
2846
|
+
if (args.times && args.times.length) return __contactSheetFrames(c, args);
|
|
1638
2847
|
var t = (args.time !== undefined && args.time !== null) ? args.time : c.time;
|
|
1639
2848
|
var ds = __resolveDownsample(c, args.downsample);
|
|
1640
2849
|
var path = __tmpPngPath();
|
|
@@ -1789,22 +2998,53 @@ function __brokenSvgAdvice() {
|
|
|
1789
2998
|
);
|
|
1790
2999
|
}
|
|
1791
3000
|
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
3001
|
+
/**
|
|
3002
|
+
* The bare import — ImportOptions, importFile, and the two ways AE can decline.
|
|
3003
|
+
* Kept as its own function because it is the only import in the codebase and
|
|
3004
|
+
* anything else that needs a file in the project (audio.jsx) must go through
|
|
3005
|
+
* exactly this, not a second copy that drifts.
|
|
3006
|
+
*/
|
|
3007
|
+
function __importFile(file, path, sequence) {
|
|
1799
3008
|
var opts = new ImportOptions(file);
|
|
1800
|
-
if (
|
|
3009
|
+
if (sequence === true) {
|
|
1801
3010
|
if (!opts.canImportAs(ImportAsType.FOOTAGE)) throw new Error("Cannot import " + path + " as footage, so it cannot be a sequence either");
|
|
1802
3011
|
opts.importAs = ImportAsType.FOOTAGE;
|
|
1803
3012
|
opts.sequence = true;
|
|
1804
3013
|
}
|
|
1805
|
-
|
|
1806
3014
|
var item = app.project.importFile(opts);
|
|
1807
3015
|
if (!item) throw new Error("After Effects returned no item for " + path);
|
|
3016
|
+
return item;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
/**
|
|
3020
|
+
* fsName -> project item, for every item in the project that came from a file.
|
|
3021
|
+
* One pass: the caller with N paths to resolve would otherwise walk the project
|
|
3022
|
+
* N times, and a project with a few hundred items makes that visible.
|
|
3023
|
+
*/
|
|
3024
|
+
function __itemPathMap() {
|
|
3025
|
+
var map = {};
|
|
3026
|
+
var proj = app.project;
|
|
3027
|
+
for (var i = 1; i <= proj.numItems; i++) {
|
|
3028
|
+
var it = proj.item(i);
|
|
3029
|
+
var key = null;
|
|
3030
|
+
try {
|
|
3031
|
+
if (it.mainSource && it.mainSource.file) key = String(it.mainSource.file.fsName);
|
|
3032
|
+
} catch (e) {}
|
|
3033
|
+
// First one wins: two items on the same file are a duplicate import, and
|
|
3034
|
+
// reusing the earlier is what the user would have done by hand.
|
|
3035
|
+
if (key && !map.hasOwnProperty(key)) map[key] = it;
|
|
3036
|
+
}
|
|
3037
|
+
return map;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
OPS.import_footage = function (args) {
|
|
3041
|
+
var path = args && args.path;
|
|
3042
|
+
if (typeof path !== "string" || path.length === 0) throw new Error("path is required");
|
|
3043
|
+
|
|
3044
|
+
var file = new File(path);
|
|
3045
|
+
if (!file.exists) throw new Error("No file at " + path);
|
|
3046
|
+
|
|
3047
|
+
var item = __importFile(file, path, args.sequence);
|
|
1808
3048
|
|
|
1809
3049
|
if (typeof args.name === "string" && args.name.length > 0) item.name = args.name;
|
|
1810
3050
|
|
|
@@ -1843,29 +3083,985 @@ OPS.import_footage = function (args) {
|
|
|
1843
3083
|
);
|
|
1844
3084
|
};
|
|
1845
3085
|
|
|
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");
|
|
3086
|
+
OPS.create_footage_layer = function (args) {
|
|
3087
|
+
var comp = getCompById(args.compId);
|
|
3088
|
+
var item = app.project.itemByID(args.itemId);
|
|
3089
|
+
if (!item) throw new Error("No project item with id " + args.itemId);
|
|
3090
|
+
if (item instanceof FolderItem) throw new Error("Item " + args.itemId + " (\"" + item.name + "\") is a folder, not footage");
|
|
3091
|
+
|
|
3092
|
+
var layer = comp.layers.add(item);
|
|
3093
|
+
if (typeof args.name === "string" && args.name.length > 0) layer.name = args.name;
|
|
3094
|
+
if (args.position !== undefined && args.position !== null) {
|
|
3095
|
+
layer.property("Transform").property("Position").setValue(args.position);
|
|
3096
|
+
}
|
|
3097
|
+
if (args.startTime !== undefined && args.startTime !== null) layer.startTime = args.startTime;
|
|
3098
|
+
|
|
3099
|
+
return {
|
|
3100
|
+
layerId: layer.id,
|
|
3101
|
+
index: layer.index,
|
|
3102
|
+
name: layer.name,
|
|
3103
|
+
compId: comp.id,
|
|
3104
|
+
itemId: item.id,
|
|
3105
|
+
inPoint: layer.inPoint,
|
|
3106
|
+
outPoint: layer.outPoint
|
|
3107
|
+
};
|
|
3108
|
+
};
|
|
3109
|
+
|
|
3110
|
+
// ---------------------------------------------------------------------------
|
|
3111
|
+
// Unused footage, and the solids delete_comp leaves behind (issue #83)
|
|
3112
|
+
// ---------------------------------------------------------------------------
|
|
3113
|
+
// A solid layer's source is a FootageItem in the project's Solids folder, not
|
|
3114
|
+
// something the comp owns. Deleting the comp (or the layer) deletes the layer
|
|
3115
|
+
// and leaves the item: nothing in AE's DOM ties the two lifetimes together and
|
|
3116
|
+
// nothing warns, so a session that builds and discards comps grows the project
|
|
3117
|
+
// bin without bound — a verification pass had to sweep 1,863 of them out by
|
|
3118
|
+
// name. Once orphaned they are indistinguishable from solids still in use
|
|
3119
|
+
// unless something reads `usedIn`, which is what everything below does.
|
|
3120
|
+
//
|
|
3121
|
+
// Two ops share these helpers. `delete_comp` (comps.jsx) can purge the solids
|
|
3122
|
+
// the deleted comp referenced — only those, and only the ones nothing else
|
|
3123
|
+
// still uses — and `purge_unused_footage` is the project-wide sweep: AE's own
|
|
3124
|
+
// Remove Unused Footage, with a dryRun and a report of what went.
|
|
3125
|
+
|
|
3126
|
+
/** The item behind a solid or adjustment layer: a FootageItem whose source is a SolidSource. */
|
|
3127
|
+
function __isSolidItem(it) {
|
|
3128
|
+
if (!(it instanceof FootageItem)) return false;
|
|
3129
|
+
var src = null;
|
|
3130
|
+
try { src = it.mainSource; } catch (e) { return false; }
|
|
3131
|
+
if (!src) return false;
|
|
3132
|
+
return (src instanceof SolidSource);
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/** "solid" | "placeholder" | "footage". Only ever called on a FootageItem — a comp or folder is never footage. */
|
|
3136
|
+
function __footageKind(it) {
|
|
3137
|
+
if (__isSolidItem(it)) return "solid";
|
|
3138
|
+
try {
|
|
3139
|
+
if (typeof PlaceholderSource !== "undefined" && it.mainSource instanceof PlaceholderSource) return "placeholder";
|
|
3140
|
+
} catch (e) {}
|
|
3141
|
+
return "footage";
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
/**
|
|
3145
|
+
* The comps an item is placed in, as {id, name}. Read from After Effects on
|
|
3146
|
+
* every call rather than inferred from anything this bundle remembers — the
|
|
3147
|
+
* whole safety of a purge is that the answer is AE's, at the moment of asking.
|
|
3148
|
+
*/
|
|
3149
|
+
function __usedInList(it) {
|
|
3150
|
+
var out = [];
|
|
3151
|
+
var comps = null;
|
|
3152
|
+
try { comps = it.usedIn; } catch (e) { comps = null; }
|
|
3153
|
+
if (!comps) return out;
|
|
3154
|
+
for (var i = 0; i < comps.length; i++) {
|
|
3155
|
+
var c = comps[i];
|
|
3156
|
+
if (!c) continue;
|
|
3157
|
+
out.push({ id: c.id, name: c.name });
|
|
3158
|
+
}
|
|
3159
|
+
return out;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
/** True when `it` sits in `folder` or in any folder nested under it. */
|
|
3163
|
+
function __isInFolder(it, folder) {
|
|
3164
|
+
var cur = null;
|
|
3165
|
+
try { cur = it.parentFolder; } catch (e) { return false; }
|
|
3166
|
+
for (var depth = 0; cur && depth < 64; depth++) {
|
|
3167
|
+
if (cur.id === folder.id) return true;
|
|
3168
|
+
var next = null;
|
|
3169
|
+
try { next = cur.parentFolder; } catch (e2) { next = null; }
|
|
3170
|
+
cur = next;
|
|
3171
|
+
}
|
|
3172
|
+
return false;
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
/** The distinct solid items this comp's own layers point at, as {id, name}. Nothing from any other comp. */
|
|
3176
|
+
function __solidItemsOf(comp) {
|
|
3177
|
+
var out = [];
|
|
3178
|
+
var seen = {};
|
|
3179
|
+
for (var i = 1; i <= comp.numLayers; i++) {
|
|
3180
|
+
var l = comp.layer(i);
|
|
3181
|
+
if (!(l instanceof AVLayer)) continue;
|
|
3182
|
+
var src = null;
|
|
3183
|
+
try { src = l.source; } catch (e) { continue; }
|
|
3184
|
+
if (!src || !__isSolidItem(src)) continue;
|
|
3185
|
+
var key = "I" + src.id;
|
|
3186
|
+
if (seen.hasOwnProperty(key)) continue;
|
|
3187
|
+
seen[key] = true;
|
|
3188
|
+
out.push({ id: src.id, name: src.name });
|
|
3189
|
+
}
|
|
3190
|
+
return out;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
function __itemReport(entry) {
|
|
3194
|
+
var r = { id: entry.id, name: entry.name, kind: entry.kind };
|
|
3195
|
+
if (entry.footageMissing) r.footageMissing = true;
|
|
3196
|
+
return r;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
function __nameList(reports, cap) {
|
|
3200
|
+
var names = [];
|
|
3201
|
+
for (var i = 0; i < reports.length && i < cap; i++) names.push("\"" + reports[i].name + "\" (id " + reports[i].id + ")");
|
|
3202
|
+
var s = names.join(", ");
|
|
3203
|
+
if (reports.length > cap) s += " and " + (reports.length - cap) + " more";
|
|
3204
|
+
return s;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
/**
|
|
3208
|
+
* Remove a list of project items. Entries are {item, id, name, kind, index?}.
|
|
3209
|
+
*
|
|
3210
|
+
* Highest project index first when indices are known, so no removal shifts an
|
|
3211
|
+
* item still to be visited — the entries were collected in one pass over
|
|
3212
|
+
* app.project.item(i) and removed in a second, never removed mid-walk. It stops
|
|
3213
|
+
* at the first failure: nothing rolls back, so the caller is handed what went
|
|
3214
|
+
* before the failure and what was never attempted, and reports that, rather
|
|
3215
|
+
* than a sweep that claims to have finished.
|
|
3216
|
+
*/
|
|
3217
|
+
function __removeItems(entries) {
|
|
3218
|
+
var ordered = entries.slice(0);
|
|
3219
|
+
ordered.sort(function (a, b) { return (b.index || 0) - (a.index || 0); });
|
|
3220
|
+
var removed = [];
|
|
3221
|
+
for (var i = 0; i < ordered.length; i++) {
|
|
3222
|
+
var entry = ordered[i];
|
|
3223
|
+
try {
|
|
3224
|
+
entry.item.remove();
|
|
3225
|
+
} catch (e) {
|
|
3226
|
+
var pending = [];
|
|
3227
|
+
for (var j = i + 1; j < ordered.length; j++) pending.push(__itemReport(ordered[j]));
|
|
3228
|
+
return {
|
|
3229
|
+
removed: removed,
|
|
3230
|
+
failed: __itemReport(entry),
|
|
3231
|
+
error: (e && e.message) ? String(e.message) : String(e),
|
|
3232
|
+
notAttempted: pending
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
removed.push(__itemReport(entry));
|
|
3236
|
+
}
|
|
3237
|
+
return { removed: removed, failed: null, error: null, notAttempted: [] };
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
OPS.purge_unused_footage = noUndoWhen(
|
|
3241
|
+
// A dry run is not an undo step. A sweep that changed nothing appearing in
|
|
3242
|
+
// the user's undo history would make "this changed nothing" false in the one
|
|
3243
|
+
// place they can see it — the same stance place_audio_cues takes.
|
|
3244
|
+
function (args) { return !!(args && args.dryRun === true); },
|
|
3245
|
+
function (args) {
|
|
3246
|
+
args = args || {};
|
|
3247
|
+
var solidsOnly = (args.solidsOnly !== false);
|
|
3248
|
+
var dryRun = (args.dryRun === true);
|
|
3249
|
+
var folder = null;
|
|
3250
|
+
if (args.folderId !== undefined && args.folderId !== null) folder = __folderArg(args.folderId);
|
|
3251
|
+
var what = "footage item";
|
|
3252
|
+
if (solidsOnly) what = "solid";
|
|
3253
|
+
|
|
3254
|
+
// One pass to decide, a second to remove. Removing inside the walk would
|
|
3255
|
+
// shift every index above the removed item under the loop's feet.
|
|
3256
|
+
var candidates = [];
|
|
3257
|
+
var scanned = 0;
|
|
3258
|
+
var inUse = 0;
|
|
3259
|
+
var proj = app.project;
|
|
3260
|
+
for (var i = 1; i <= proj.numItems; i++) {
|
|
3261
|
+
var it = proj.item(i);
|
|
3262
|
+
// A comp is an AVItem with a usedIn of its own, and a nested comp nothing
|
|
3263
|
+
// uses any more is exactly what a naive sweep would delete. Only footage.
|
|
3264
|
+
if (!(it instanceof FootageItem)) continue;
|
|
3265
|
+
if (solidsOnly && !__isSolidItem(it)) continue;
|
|
3266
|
+
if (folder && !__isInFolder(it, folder)) continue;
|
|
3267
|
+
scanned += 1;
|
|
3268
|
+
if (__usedInList(it).length > 0) { inUse += 1; continue; }
|
|
3269
|
+
var missing = false;
|
|
3270
|
+
try { missing = !!it.footageMissing; } catch (eMissing) {}
|
|
3271
|
+
candidates.push({ item: it, id: it.id, name: it.name, kind: __footageKind(it), index: i, footageMissing: missing });
|
|
3272
|
+
}
|
|
3273
|
+
// Sorted here, once, so a dry run lists items in exactly the order a real
|
|
3274
|
+
// run removes them — the plan has to be the truth about order too.
|
|
3275
|
+
candidates.sort(function (a, b) { return b.index - a.index; });
|
|
3276
|
+
|
|
3277
|
+
var out = { solidsOnly: solidsOnly, scanned: scanned, inUse: inUse };
|
|
3278
|
+
var where = "in the project";
|
|
3279
|
+
if (folder) {
|
|
3280
|
+
out.folderId = folder.id;
|
|
3281
|
+
out.folderName = folder.name;
|
|
3282
|
+
where = "in folder \"" + folder.name + "\"";
|
|
3283
|
+
}
|
|
3284
|
+
var nothingWhy = "every " + what + " " + where + " is still used in a comp";
|
|
3285
|
+
if (scanned === 0) nothingWhy = "no " + what + "s " + where;
|
|
3286
|
+
|
|
3287
|
+
if (dryRun) {
|
|
3288
|
+
var would = [];
|
|
3289
|
+
for (var w = 0; w < candidates.length; w++) would.push(__itemReport(candidates[w]));
|
|
3290
|
+
out.dryRun = true;
|
|
3291
|
+
out.wouldRemove = would;
|
|
3292
|
+
out.wouldRemoveCount = would.length;
|
|
3293
|
+
if (would.length === 0) {
|
|
3294
|
+
out.note = "Nothing to remove: " + nothingWhy + ". Nothing was changed and this call is not an undo step.";
|
|
3295
|
+
} else {
|
|
3296
|
+
out.note = "Nothing was removed and this call is not an undo step. Call again without dryRun to remove these " +
|
|
3297
|
+
would.length + " " + what + (would.length === 1 ? "" : "s") + " " + where + ".";
|
|
3298
|
+
}
|
|
3299
|
+
return out;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
var r = __removeItems(candidates);
|
|
3303
|
+
out.removed = r.removed;
|
|
3304
|
+
out.removedCount = r.removed.length;
|
|
3305
|
+
if (r.failed) {
|
|
3306
|
+
var msg = "purge_unused_footage stopped at \"" + r.failed.name + "\" (id " + r.failed.id + ", " + r.failed.kind + "): " + r.error + ".";
|
|
3307
|
+
if (r.removed.length > 0) {
|
|
3308
|
+
msg += " " + r.removed.length + " item(s) were removed before it: " + __nameList(r.removed, 20) + ".";
|
|
3309
|
+
} else {
|
|
3310
|
+
msg += " Nothing had been removed yet.";
|
|
3311
|
+
}
|
|
3312
|
+
msg += " " + r.notAttempted.length + " unused item(s) were not attempted." +
|
|
3313
|
+
" Nothing rolls back; one Undo in After Effects restores what this call removed. Run again, or with dryRun:true, to see what is left.";
|
|
3314
|
+
throw new Error(msg);
|
|
3315
|
+
}
|
|
3316
|
+
if (r.removed.length === 0) {
|
|
3317
|
+
out.note = "Nothing to remove: " + nothingWhy + ".";
|
|
3318
|
+
} else {
|
|
3319
|
+
out.note = "Removed " + r.removed.length + " unused " + what + (r.removed.length === 1 ? "" : "s") + " " + where + ", as one undo step.";
|
|
3320
|
+
}
|
|
3321
|
+
return out;
|
|
3322
|
+
}
|
|
3323
|
+
);
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
// ===== audio.jsx =====
|
|
3327
|
+
|
|
3328
|
+
// audio.jsx — place a list of sound effects into a comp in one pass.
|
|
3329
|
+
//
|
|
3330
|
+
// Scoring a scene is 40-90 layers: import or reuse the file, add a layer, set
|
|
3331
|
+
// its start time, set its level in dB, name it, sometimes trim it. Done through
|
|
3332
|
+
// the general tools that is dozens of round trips; done through run_jsx it is a
|
|
3333
|
+
// hand-written loop that has to know the one thing nothing tells you —
|
|
3334
|
+
// `layer.property("ADBE Audio Levels")` returns **null** on an audio layer,
|
|
3335
|
+
// because Audio Levels lives under the layer's "Audio" group and the only
|
|
3336
|
+
// reliable handle is the `layer.audioLevels` shortcut (issue #48).
|
|
3337
|
+
//
|
|
3338
|
+
// Two properties carry the design:
|
|
3339
|
+
//
|
|
3340
|
+
// - Nothing is created until every cue has been checked. A run that dies on
|
|
3341
|
+
// cue 30 of 90 leaves 29 sound effects in someone's timeline and an error
|
|
3342
|
+
// that does not say which ones — the same half-built failure
|
|
3343
|
+
// add_shape_content refuses to produce. So: plan first with no side
|
|
3344
|
+
// effects, throw naming the offending cue indices, and if a creation still
|
|
3345
|
+
// fails, remove everything this call made before rethrowing.
|
|
3346
|
+
// - A file named by several cues is imported once. Repeated imports of one
|
|
3347
|
+
// .wav are the normal shape of a cue list ("whoosh" nine times), and each
|
|
3348
|
+
// one would otherwise add another project item. Anything already in the
|
|
3349
|
+
// project from that path is reused rather than imported a second time.
|
|
3350
|
+
//
|
|
3351
|
+
// Three per-cue options (issue #85) — `loop`, `fadeIn`/`fadeOut`, `stretch` —
|
|
3352
|
+
// are the patterns every scoring pass used to re-implement in a run_jsx
|
|
3353
|
+
// follow-up. Each one moves something After Effects then quietly resets, so
|
|
3354
|
+
// the order inside __placeAudioCue is load-bearing; see the comment there.
|
|
3355
|
+
|
|
3356
|
+
var __MAX_AUDIO_CUES = 200;
|
|
3357
|
+
var __CUE_TIME_EPS = 1e-6;
|
|
3358
|
+
|
|
3359
|
+
// The level a fade starts from and ends at when the caller gives none. -48 dB
|
|
3360
|
+
// is the bottom of the range After Effects' own Audio Levels slider offers, and
|
|
3361
|
+
// 1/256 of the recorded amplitude — near-silence for a fade to begin from while
|
|
3362
|
+
// staying inside the range a user can drag the property through by hand.
|
|
3363
|
+
var __DEFAULT_FADE_FLOOR_DB = -48;
|
|
3364
|
+
|
|
3365
|
+
// AE's layer label colours. Users can rename them in preferences, but the tool
|
|
3366
|
+
// takes an index and the indices do not move, so the names are accepted as a
|
|
3367
|
+
// convenience and translated here. 0 is "None".
|
|
3368
|
+
var __LABEL_NAMES = {
|
|
3369
|
+
none: 0, red: 1, yellow: 2, aqua: 3, pink: 4, lavender: 5, peach: 6,
|
|
3370
|
+
seafoam: 7, blue: 8, green: 9, purple: 10, orange: 11, brown: 12,
|
|
3371
|
+
fuchsia: 13, cyan: 14, sandstone: 15, darkgreen: 16
|
|
3372
|
+
};
|
|
3373
|
+
|
|
3374
|
+
function __labelKey(s) {
|
|
3375
|
+
return String(s).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
function __labelNameList() {
|
|
3379
|
+
var names = [];
|
|
3380
|
+
for (var k in __LABEL_NAMES) {
|
|
3381
|
+
if (__LABEL_NAMES.hasOwnProperty(k)) names.push(k);
|
|
3382
|
+
}
|
|
3383
|
+
return names.join(", ");
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
/** 0..16, or a thrown error naming everything it would have taken. */
|
|
3387
|
+
function __resolveLabel(label) {
|
|
3388
|
+
if (typeof label === "number") {
|
|
3389
|
+
if (label !== Math.floor(label) || label < 0 || label > 16) {
|
|
3390
|
+
throw new Error("label must be a whole number 0-16 or a colour name (" + __labelNameList() + "); got " + label);
|
|
3391
|
+
}
|
|
3392
|
+
return label;
|
|
3393
|
+
}
|
|
3394
|
+
var key = __labelKey(label);
|
|
3395
|
+
if (__LABEL_NAMES.hasOwnProperty(key)) return __LABEL_NAMES[key];
|
|
3396
|
+
throw new Error("unknown label \"" + label + "\" — use 0-16 or one of: " + __labelNameList());
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
function __basename(p) {
|
|
3400
|
+
var s = String(p).replace(/\\/g, "/");
|
|
3401
|
+
var slash = s.lastIndexOf("/");
|
|
3402
|
+
if (slash >= 0) s = s.substring(slash + 1);
|
|
3403
|
+
return s;
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
function __stripExtension(name) {
|
|
3407
|
+
var s = String(name);
|
|
3408
|
+
var dot = s.lastIndexOf(".");
|
|
3409
|
+
if (dot > 0) return s.substring(0, dot);
|
|
3410
|
+
return s;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
function __isFiniteNumber(v) {
|
|
3414
|
+
return typeof v === "number" && isFinite(v);
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
/**
|
|
3418
|
+
* The Audio Levels property of a layer.
|
|
3419
|
+
*
|
|
3420
|
+
* `layer.audioLevels` is the shortcut that works. `layer.property("ADBE Audio
|
|
3421
|
+
* Levels")` returns null on an audio layer — the property sits inside the
|
|
3422
|
+
* layer's "Audio" group, not on the layer — and a null there is what silently
|
|
3423
|
+
* turns a scripted level into no level at all. The group walk is only a
|
|
3424
|
+
* fallback for a layer whose shortcut is somehow absent; if neither answers,
|
|
3425
|
+
* the caller is told rather than left with an unset level.
|
|
3426
|
+
*/
|
|
3427
|
+
function __audioLevelsProperty(layer) {
|
|
3428
|
+
var p = null;
|
|
3429
|
+
try { p = layer.audioLevels; } catch (e) {}
|
|
3430
|
+
if (p) return p;
|
|
3431
|
+
try {
|
|
3432
|
+
var g = layer.property("Audio");
|
|
3433
|
+
if (g) p = g.property("Audio Levels");
|
|
3434
|
+
} catch (e2) {}
|
|
3435
|
+
return p;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
/**
|
|
3439
|
+
* The Time Remap property of a layer that has remapping enabled. Same shape of
|
|
3440
|
+
* trap as Audio Levels: `layer.property("ADBE Time Remap")` is null on an
|
|
3441
|
+
* audio layer even after `timeRemapEnabled = true`, and the shortcut
|
|
3442
|
+
* `layer.timeRemap` is the handle that answers. The match-name lookup is kept
|
|
3443
|
+
* only as a fallback; a null from both is reported, never worked around.
|
|
3444
|
+
*/
|
|
3445
|
+
function __timeRemapProperty(layer) {
|
|
3446
|
+
var p = null;
|
|
3447
|
+
try { p = layer.timeRemap; } catch (e) {}
|
|
3448
|
+
if (p) return p;
|
|
3449
|
+
try { p = layer.property("ADBE Time Remapping"); } catch (e2) {}
|
|
3450
|
+
return p;
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
/** null when the item can carry an audio cue, else the reason it cannot. */
|
|
3454
|
+
function __audioItemProblem(item) {
|
|
3455
|
+
if (item instanceof FolderItem) return "\"" + item.name + "\" is a folder, not footage";
|
|
3456
|
+
if (item instanceof CompItem) return "\"" + item.name + "\" is a comp — use create_precomp_layer for that";
|
|
3457
|
+
if (item.footageMissing) return "\"" + item.name + "\" is offline; the file it points at is missing";
|
|
3458
|
+
// Explicitly `=== false`, not `!item.hasAudio`: an item that does not report
|
|
3459
|
+
// the flag at all must not be refused on the strength of a missing property.
|
|
3460
|
+
if (item.hasAudio === false) return "\"" + item.name + "\" has no audio track";
|
|
3461
|
+
return null;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
function __sourceReport(item, path) {
|
|
3465
|
+
return { itemId: item.id, name: item.name, path: path };
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
/** The duration of a project item, or null when it does not say. */
|
|
3469
|
+
function __itemDuration(item) {
|
|
3470
|
+
if (!item) return null;
|
|
3471
|
+
var d = null;
|
|
3472
|
+
try { d = item.duration; } catch (e) {}
|
|
3473
|
+
if (__isFiniteNumber(d)) return d;
|
|
3474
|
+
return null;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
/**
|
|
3478
|
+
* Where a planned cue's layer will start and end, in comp time, given what is
|
|
3479
|
+
* known about its source. `end` is null when it cannot be known yet — an
|
|
3480
|
+
* unlooped cue on a file that has not been imported, with no outPoint to cap
|
|
3481
|
+
* it — and the caller decides what to do with that.
|
|
3482
|
+
*/
|
|
3483
|
+
function __cueExtent(p, itemDuration) {
|
|
3484
|
+
var start = p.time;
|
|
3485
|
+
if (p.inPoint !== null) start = p.inPoint;
|
|
3486
|
+
var end = null;
|
|
3487
|
+
if (p.loop) {
|
|
3488
|
+
// A loop has no natural end, so its end is the one the caller gave or the
|
|
3489
|
+
// comp's, whichever the planner settled on.
|
|
3490
|
+
end = p.loopEnd;
|
|
3491
|
+
} else {
|
|
3492
|
+
var natural = null;
|
|
3493
|
+
if (__isFiniteNumber(itemDuration)) natural = p.time + itemDuration * p.stretchFactor;
|
|
3494
|
+
end = p.outPoint;
|
|
3495
|
+
// AE clamps an out point to what the source can supply, so a requested
|
|
3496
|
+
// outPoint past the file's end is not where the layer will end.
|
|
3497
|
+
if (end === null || (natural !== null && natural < end)) end = natural;
|
|
3498
|
+
}
|
|
3499
|
+
return { start: start, end: end };
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
function __fmtSeconds(n) {
|
|
3503
|
+
// Rounded for prose only; nothing downstream reads these numbers back.
|
|
3504
|
+
return String(Math.round(n * 1000) / 1000) + "s";
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
/**
|
|
3508
|
+
* null when the cue's fades fit inside its extent; a reason string when they
|
|
3509
|
+
* do not; and the string "unknown" when the extent cannot be computed yet.
|
|
3510
|
+
* Pure — used at plan time and again after the imports, on the same plan.
|
|
3511
|
+
*/
|
|
3512
|
+
function __fadeFitProblem(p, extent) {
|
|
3513
|
+
var total = p.fadeIn + p.fadeOut;
|
|
3514
|
+
if (total <= 0 && !p.loop) return null;
|
|
3515
|
+
if (extent.end === null) {
|
|
3516
|
+
if (total <= 0) return null;
|
|
3517
|
+
return "unknown";
|
|
3518
|
+
}
|
|
3519
|
+
var dur = extent.end - extent.start;
|
|
3520
|
+
var span = " (from " + __fmtSeconds(extent.start) + " to " + __fmtSeconds(extent.end) + ")";
|
|
3521
|
+
if (dur <= __CUE_TIME_EPS) {
|
|
3522
|
+
if (p.loop) {
|
|
3523
|
+
var why = "the comp's end, since no outPoint was given";
|
|
3524
|
+
if (p.outPoint !== null) why = "its outPoint";
|
|
3525
|
+
return "inPoint " + __fmtSeconds(extent.start) + " is not before the end of the loop at " + __fmtSeconds(extent.end) + " (" + why + ")";
|
|
3526
|
+
}
|
|
3527
|
+
return "inPoint " + __fmtSeconds(extent.start) + " is past where the cue ends" + span;
|
|
3528
|
+
}
|
|
3529
|
+
if (total <= 0) return null;
|
|
3530
|
+
if (total > dur + __CUE_TIME_EPS) {
|
|
3531
|
+
var what;
|
|
3532
|
+
if (p.fadeIn > 0 && p.fadeOut > 0) {
|
|
3533
|
+
what = "fadeIn " + __fmtSeconds(p.fadeIn) + " + fadeOut " + __fmtSeconds(p.fadeOut) + " = " + __fmtSeconds(total);
|
|
3534
|
+
} else if (p.fadeIn > 0) {
|
|
3535
|
+
what = "fadeIn " + __fmtSeconds(p.fadeIn);
|
|
3536
|
+
} else {
|
|
3537
|
+
what = "fadeOut " + __fmtSeconds(p.fadeOut);
|
|
3538
|
+
}
|
|
3539
|
+
var tail = "";
|
|
3540
|
+
if (!p.loop && p.outPoint === null) tail = " — the file's own length, with no outPoint to extend it and no loop";
|
|
3541
|
+
return what + " is longer than the cue, which runs " + __fmtSeconds(dur) + span + tail;
|
|
3542
|
+
}
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
/**
|
|
3547
|
+
* Turn the cue list into a plan, touching nothing. Every problem found is
|
|
3548
|
+
* collected with the index of the cue that caused it rather than thrown on the
|
|
3549
|
+
* spot, so one call reports all of them instead of one per round trip.
|
|
3550
|
+
*/
|
|
3551
|
+
function __planAudioCues(comp, args) {
|
|
3552
|
+
var cues = args.cues;
|
|
3553
|
+
if (!(cues instanceof Array) || cues.length === 0) {
|
|
3554
|
+
throw new Error("place_audio_cues needs a non-empty `cues` array.");
|
|
3555
|
+
}
|
|
3556
|
+
if (cues.length > __MAX_AUDIO_CUES) {
|
|
3557
|
+
throw new Error(
|
|
3558
|
+
"place_audio_cues was given " + cues.length + " cues and the limit is " + __MAX_AUDIO_CUES + " per call. " +
|
|
3559
|
+
"ExtendScript is single-threaded, so one long run freezes After Effects' interface for its whole duration. " +
|
|
3560
|
+
"Split the list into calls of " + __MAX_AUDIO_CUES + " or fewer."
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
var prefix = "SFX_";
|
|
3565
|
+
if (typeof args.namePrefix === "string") prefix = args.namePrefix;
|
|
3566
|
+
|
|
3567
|
+
// One floor for the whole list: it is a property of the mix, not of a cue.
|
|
3568
|
+
var floorDb = __DEFAULT_FADE_FLOOR_DB;
|
|
3569
|
+
if (args.fadeFloorDb !== undefined && args.fadeFloorDb !== null) {
|
|
3570
|
+
if (!__isFiniteNumber(args.fadeFloorDb)) {
|
|
3571
|
+
throw new Error("fadeFloorDb must be a number of decibels (the default is " + __DEFAULT_FADE_FLOOR_DB + "); got " + String(args.fadeFloorDb));
|
|
3572
|
+
}
|
|
3573
|
+
floorDb = args.fadeFloorDb;
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
var byPath = __itemPathMap();
|
|
3577
|
+
var problems = [];
|
|
3578
|
+
var planned = [];
|
|
3579
|
+
var toImport = [];
|
|
3580
|
+
var toImportSeen = {};
|
|
3581
|
+
|
|
3582
|
+
for (var i = 0; i < cues.length; i++) {
|
|
3583
|
+
var cue = cues[i];
|
|
3584
|
+
if (!cue) { problems.push({ cue: i, reason: "is empty" }); continue; }
|
|
3585
|
+
|
|
3586
|
+
var hasId = (cue.footageId !== undefined && cue.footageId !== null);
|
|
3587
|
+
var hasPath = (typeof cue.path === "string" && cue.path.length > 0);
|
|
3588
|
+
if (hasId && hasPath) { problems.push({ cue: i, reason: "has both footageId and path — give exactly one" }); continue; }
|
|
3589
|
+
if (!hasId && !hasPath) { problems.push({ cue: i, reason: "has neither footageId nor path — give exactly one" }); continue; }
|
|
3590
|
+
|
|
3591
|
+
var time = cue.time;
|
|
3592
|
+
if (!__isFiniteNumber(time)) {
|
|
3593
|
+
problems.push({ cue: i, reason: "time must be a number of seconds; got " + String(time) });
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3596
|
+
if (time < -__CUE_TIME_EPS || time > comp.duration + __CUE_TIME_EPS) {
|
|
3597
|
+
problems.push({ cue: i, reason: "time " + time + "s is outside the comp, which runs 0 to " + comp.duration + "s" });
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
var levelDb = 0;
|
|
3602
|
+
if (cue.levelDb !== undefined && cue.levelDb !== null) {
|
|
3603
|
+
if (!__isFiniteNumber(cue.levelDb)) {
|
|
3604
|
+
problems.push({ cue: i, reason: "levelDb must be a number of decibels (0 is unedited); got " + String(cue.levelDb) });
|
|
3605
|
+
continue;
|
|
3606
|
+
}
|
|
3607
|
+
levelDb = cue.levelDb;
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
var inPoint = null;
|
|
3611
|
+
var outPoint = null;
|
|
3612
|
+
var trimBad = false;
|
|
3613
|
+
if (cue.inPoint !== undefined && cue.inPoint !== null) {
|
|
3614
|
+
if (!__isFiniteNumber(cue.inPoint)) {
|
|
3615
|
+
problems.push({ cue: i, reason: "inPoint must be a comp time in seconds; got " + String(cue.inPoint) });
|
|
3616
|
+
trimBad = true;
|
|
3617
|
+
} else {
|
|
3618
|
+
inPoint = cue.inPoint;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
if (!trimBad && cue.outPoint !== undefined && cue.outPoint !== null) {
|
|
3622
|
+
if (!__isFiniteNumber(cue.outPoint)) {
|
|
3623
|
+
problems.push({ cue: i, reason: "outPoint must be a comp time in seconds; got " + String(cue.outPoint) });
|
|
3624
|
+
trimBad = true;
|
|
3625
|
+
} else {
|
|
3626
|
+
outPoint = cue.outPoint;
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
if (trimBad) continue;
|
|
3630
|
+
// in/out are absolute comp times, like everywhere else in these tools, so
|
|
3631
|
+
// they are measured against `time` rather than against the file.
|
|
3632
|
+
if (inPoint !== null && inPoint < time - __CUE_TIME_EPS) {
|
|
3633
|
+
problems.push({ cue: i, reason: "inPoint " + inPoint + "s is before the cue's own time " + time + "s; both are comp times" });
|
|
3634
|
+
continue;
|
|
3635
|
+
}
|
|
3636
|
+
var trimStart = time;
|
|
3637
|
+
if (inPoint !== null) trimStart = inPoint;
|
|
3638
|
+
if (outPoint !== null && outPoint <= trimStart + __CUE_TIME_EPS) {
|
|
3639
|
+
problems.push({ cue: i, reason: "outPoint " + outPoint + "s is not after the cue starts at " + trimStart + "s" });
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
var label = null;
|
|
3644
|
+
if (cue.label !== undefined && cue.label !== null) {
|
|
3645
|
+
try {
|
|
3646
|
+
label = __resolveLabel(cue.label);
|
|
3647
|
+
} catch (eLabel) {
|
|
3648
|
+
problems.push({ cue: i, reason: eLabel.message });
|
|
3649
|
+
continue;
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// The #85 options. Type-checked here, before the source is resolved, so a
|
|
3654
|
+
// cue with a bad stretch AND a missing file reports the first of the two
|
|
3655
|
+
// it hits — every cue still gets a line, which is what matters.
|
|
3656
|
+
var loop = false;
|
|
3657
|
+
if (cue.loop !== undefined && cue.loop !== null) {
|
|
3658
|
+
if (cue.loop !== true && cue.loop !== false) {
|
|
3659
|
+
problems.push({ cue: i, reason: "loop must be true or false; got " + String(cue.loop) });
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
loop = cue.loop;
|
|
3663
|
+
}
|
|
3664
|
+
var stretch = null;
|
|
3665
|
+
if (cue.stretch !== undefined && cue.stretch !== null) {
|
|
3666
|
+
if (!__isFiniteNumber(cue.stretch) || cue.stretch <= 0) {
|
|
3667
|
+
problems.push({ cue: i, reason: "stretch must be a percentage greater than 0 (100 is unchanged, 200 is half speed); got " + String(cue.stretch) });
|
|
3668
|
+
continue;
|
|
3669
|
+
}
|
|
3670
|
+
stretch = cue.stretch;
|
|
3671
|
+
}
|
|
3672
|
+
var fadeIn = 0;
|
|
3673
|
+
var fadeOut = 0;
|
|
3674
|
+
var fadeBad = false;
|
|
3675
|
+
if (cue.fadeIn !== undefined && cue.fadeIn !== null) {
|
|
3676
|
+
if (!__isFiniteNumber(cue.fadeIn) || cue.fadeIn < 0) {
|
|
3677
|
+
problems.push({ cue: i, reason: "fadeIn must be a number of seconds, 0 or more; got " + String(cue.fadeIn) });
|
|
3678
|
+
fadeBad = true;
|
|
3679
|
+
} else {
|
|
3680
|
+
fadeIn = cue.fadeIn;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
if (!fadeBad && cue.fadeOut !== undefined && cue.fadeOut !== null) {
|
|
3684
|
+
if (!__isFiniteNumber(cue.fadeOut) || cue.fadeOut < 0) {
|
|
3685
|
+
problems.push({ cue: i, reason: "fadeOut must be a number of seconds, 0 or more; got " + String(cue.fadeOut) });
|
|
3686
|
+
fadeBad = true;
|
|
3687
|
+
} else {
|
|
3688
|
+
fadeOut = cue.fadeOut;
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
if (fadeBad) continue;
|
|
3692
|
+
if ((fadeIn > 0 || fadeOut > 0) && floorDb >= levelDb) {
|
|
3693
|
+
problems.push({
|
|
3694
|
+
cue: i,
|
|
3695
|
+
reason: "fadeFloorDb " + floorDb + " is not below the cue's level " + levelDb + " dB, so its fade would go nowhere — lower the floor or raise the level"
|
|
3696
|
+
});
|
|
3697
|
+
continue;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
// Resolve the source. A footageId names an item that must already be
|
|
3701
|
+
// usable; a path is either something the project already holds or an
|
|
3702
|
+
// import this call will do exactly once.
|
|
3703
|
+
var source = null;
|
|
3704
|
+
var defaultName = null;
|
|
3705
|
+
if (hasId) {
|
|
3706
|
+
var item = app.project.itemByID(cue.footageId);
|
|
3707
|
+
if (!item) { problems.push({ cue: i, reason: "no project item with id " + cue.footageId }); continue; }
|
|
3708
|
+
var why = __audioItemProblem(item);
|
|
3709
|
+
if (why) { problems.push({ cue: i, reason: why }); continue; }
|
|
3710
|
+
source = { kind: "item", item: item, fsName: null, path: null };
|
|
3711
|
+
defaultName = item.name;
|
|
3712
|
+
} else {
|
|
3713
|
+
var file = new File(cue.path);
|
|
3714
|
+
if (!file.exists) { problems.push({ cue: i, reason: "no file at " + cue.path }); continue; }
|
|
3715
|
+
var fsName = String(file.fsName);
|
|
3716
|
+
var existing = null;
|
|
3717
|
+
if (byPath.hasOwnProperty(fsName)) existing = byPath[fsName];
|
|
3718
|
+
if (existing) {
|
|
3719
|
+
var whyExisting = __audioItemProblem(existing);
|
|
3720
|
+
if (whyExisting) { problems.push({ cue: i, reason: whyExisting + " (already in the project from " + cue.path + ")" }); continue; }
|
|
3721
|
+
source = { kind: "reused", item: existing, fsName: fsName, path: cue.path };
|
|
3722
|
+
} else {
|
|
3723
|
+
if (!toImportSeen.hasOwnProperty(fsName)) {
|
|
3724
|
+
toImportSeen[fsName] = true;
|
|
3725
|
+
toImport.push({ fsName: fsName, path: cue.path, file: file });
|
|
3726
|
+
}
|
|
3727
|
+
source = { kind: "import", item: null, fsName: fsName, path: cue.path };
|
|
3728
|
+
}
|
|
3729
|
+
// The caller named a path, so the path's basename is the honest default
|
|
3730
|
+
// even when the project item it resolves to was renamed by hand.
|
|
3731
|
+
defaultName = __basename(cue.path);
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
var name = prefix + __stripExtension(defaultName);
|
|
3735
|
+
if (typeof cue.name === "string" && cue.name.length > 0) name = cue.name;
|
|
3736
|
+
|
|
3737
|
+
var stretchFactor = 1;
|
|
3738
|
+
if (stretch !== null) stretchFactor = stretch / 100;
|
|
3739
|
+
// A looped cue needs an end, because a loop has none of its own.
|
|
3740
|
+
var loopEnd = null;
|
|
3741
|
+
if (loop) {
|
|
3742
|
+
loopEnd = comp.duration;
|
|
3743
|
+
if (outPoint !== null) loopEnd = outPoint;
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
var p = {
|
|
3747
|
+
index: i, name: name, time: time, levelDb: levelDb,
|
|
3748
|
+
inPoint: inPoint, outPoint: outPoint, label: label, source: source,
|
|
3749
|
+
loop: loop, loopEnd: loopEnd, stretch: stretch, stretchFactor: stretchFactor,
|
|
3750
|
+
fadeIn: fadeIn, fadeOut: fadeOut,
|
|
3751
|
+
// true when the fade could not be measured against the file yet
|
|
3752
|
+
fadeUnchecked: false
|
|
3753
|
+
};
|
|
3754
|
+
|
|
3755
|
+
// Does the cue fit? Known now for a loop (its end is chosen here), for a
|
|
3756
|
+
// footageId or reused item (After Effects reports its duration) and for a
|
|
3757
|
+
// capped outPoint; not yet for an unlooped cue on a file still to import.
|
|
3758
|
+
var fit = __fadeFitProblem(p, __cueExtent(p, __itemDuration(source.item)));
|
|
3759
|
+
if (fit === "unknown") {
|
|
3760
|
+
p.fadeUnchecked = true;
|
|
3761
|
+
} else if (fit !== null) {
|
|
3762
|
+
problems.push({ cue: i, reason: fit });
|
|
3763
|
+
continue;
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
planned.push(p);
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
return { prefix: prefix, planned: planned, problems: problems, toImport: toImport, floorDb: floorDb };
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
function __audioProblemLines(problems) {
|
|
3773
|
+
var lines = [];
|
|
3774
|
+
for (var i = 0; i < problems.length; i++) {
|
|
3775
|
+
lines.push("cue " + problems[i].cue + ": " + problems[i].reason);
|
|
3776
|
+
}
|
|
3777
|
+
return lines.join("; ");
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
function __audioProblemMessage(problems, total) {
|
|
3781
|
+
return (
|
|
3782
|
+
"place_audio_cues placed nothing — " + problems.length + " of " + total + " cues cannot be placed. " +
|
|
3783
|
+
__audioProblemLines(problems) + ". Every cue is checked before anything is created, so the comp and project are " +
|
|
3784
|
+
"untouched. Fix these and call again; dryRun:true checks a list without placing it."
|
|
3785
|
+
);
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
/**
|
|
3789
|
+
* Counts and the two lists a caller cannot work out alone — what would be
|
|
3790
|
+
* imported and what is already there — plus the failing cues. Never the
|
|
3791
|
+
* resolved list: a 7-cue "all fine" used to cost ~1.5k tokens of echo (#88).
|
|
3792
|
+
*/
|
|
3793
|
+
function __audioDryRunReport(comp, plan, total) {
|
|
3794
|
+
var wouldImport = [];
|
|
3795
|
+
for (var j = 0; j < plan.toImport.length; j++) wouldImport.push(plan.toImport[j].path);
|
|
3796
|
+
|
|
3797
|
+
var wouldReuse = [];
|
|
3798
|
+
var reuseSeen = {};
|
|
3799
|
+
var looped = 0;
|
|
3800
|
+
var faded = 0;
|
|
3801
|
+
var stretched = 0;
|
|
3802
|
+
var fadesUnchecked = 0;
|
|
3803
|
+
for (var i = 0; i < plan.planned.length; i++) {
|
|
3804
|
+
var p = plan.planned[i];
|
|
3805
|
+
if (p.source.item && !reuseSeen.hasOwnProperty(String(p.source.item.id))) {
|
|
3806
|
+
reuseSeen[String(p.source.item.id)] = true;
|
|
3807
|
+
var r = { itemId: p.source.item.id, name: p.source.item.name };
|
|
3808
|
+
if (p.source.path) r.path = p.source.path;
|
|
3809
|
+
wouldReuse.push(r);
|
|
3810
|
+
}
|
|
3811
|
+
if (p.loop) looped++;
|
|
3812
|
+
if (p.fadeIn > 0 || p.fadeOut > 0) faded++;
|
|
3813
|
+
if (p.stretch !== null) stretched++;
|
|
3814
|
+
if (p.fadeUnchecked) fadesUnchecked++;
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
var out = {
|
|
3818
|
+
dryRun: true,
|
|
3819
|
+
ok: plan.problems.length === 0,
|
|
3820
|
+
compId: comp.id,
|
|
3821
|
+
compName: comp.name,
|
|
3822
|
+
cueCount: total,
|
|
3823
|
+
wouldPlace: plan.planned.length,
|
|
3824
|
+
wouldImport: wouldImport,
|
|
3825
|
+
wouldReuse: wouldReuse,
|
|
3826
|
+
problems: plan.problems,
|
|
3827
|
+
note: "Nothing was imported, created or changed, and this call is not an undo step."
|
|
3828
|
+
};
|
|
3829
|
+
if (looped > 0) out.looped = looped;
|
|
3830
|
+
if (faded > 0) out.faded = faded;
|
|
3831
|
+
if (stretched > 0) out.stretched = stretched;
|
|
3832
|
+
if (wouldImport.length > 0) {
|
|
3833
|
+
var fadeNote = "";
|
|
3834
|
+
if (fadesUnchecked > 0) {
|
|
3835
|
+
fadeNote = " — and, for " + fadesUnchecked + " cue(s) with a fade, whether the fade fits the file's length —";
|
|
3836
|
+
}
|
|
3837
|
+
out.unverified =
|
|
3838
|
+
wouldImport.length + " of these files are not in the project yet. They exist on disk, but whether each " +
|
|
3839
|
+
"carries an audio track" + fadeNote + " is only knowable once After Effects has imported it; a real run checks " +
|
|
3840
|
+
"that and refuses the whole call if one does not.";
|
|
3841
|
+
}
|
|
3842
|
+
return out;
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
/** Undo everything this call made, newest first. Layers before items: an item still in use cannot go. */
|
|
3846
|
+
function __rollbackAudioCues(layers, items) {
|
|
3847
|
+
for (var i = layers.length - 1; i >= 0; i--) {
|
|
3848
|
+
try { layers[i].remove(); } catch (e) {}
|
|
3849
|
+
}
|
|
3850
|
+
for (var j = items.length - 1; j >= 0; j--) {
|
|
3851
|
+
try { items[j].remove(); } catch (e2) {}
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
/**
|
|
3856
|
+
* The Time Remap expression that loops a cue. It is the pattern measured in
|
|
3857
|
+
* issue #85 — comp time since the layer began, wrapped at the file's length —
|
|
3858
|
+
* with two substitutions: `startTime` rather than `inPoint`, so trimming the
|
|
3859
|
+
* in point hides the front of the file the way it does on every other layer
|
|
3860
|
+
* instead of delaying it; and the source's duration read live off the layer,
|
|
3861
|
+
* so a relinked file keeps looping. An expression cannot read a layer's
|
|
3862
|
+
* stretch, so a stretched cue has its factor baked in, and that constant goes
|
|
3863
|
+
* stale if the stretch is changed by hand afterwards.
|
|
3864
|
+
*/
|
|
3865
|
+
function __loopExpression(p) {
|
|
3866
|
+
var elapsed = "(time - startTime)";
|
|
3867
|
+
if (p.stretch !== null && p.stretch !== 100) elapsed = "((time - startTime) * 100 / " + p.stretch + ")";
|
|
3868
|
+
return elapsed + " % thisLayer.source.duration";
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
/**
|
|
3872
|
+
* `created` is the rollback list, and the layer joins it the instant it exists
|
|
3873
|
+
* rather than once it is fully configured. A cue that dies between add() and
|
|
3874
|
+
* the last setValue is exactly the case rollback is for, and a layer that had
|
|
3875
|
+
* not been registered yet would be the one thing left behind.
|
|
3876
|
+
*
|
|
3877
|
+
* The order after that is not free to change. Each of the #85 options moves
|
|
3878
|
+
* something After Effects then resets: `stretch` moves startTime, so startTime
|
|
3879
|
+
* is set after it; enabling time remap resets the out point, so the extent is
|
|
3880
|
+
* set after that; and the fade keys sit at the in/out the layer actually has,
|
|
3881
|
+
* so they go last. Out point before in point throughout — extending first is
|
|
3882
|
+
* what lets an in point past the file's natural end land on a looped layer.
|
|
3883
|
+
*/
|
|
3884
|
+
function __placeAudioCue(comp, p, item, created, floorDb) {
|
|
3885
|
+
var layer = comp.layers.add(item);
|
|
3886
|
+
created.push(layer);
|
|
3887
|
+
layer.name = p.name;
|
|
3888
|
+
if (p.stretch !== null) layer.stretch = p.stretch;
|
|
3889
|
+
// startTime first: it slides the whole layer and would drag any trim with it.
|
|
3890
|
+
layer.startTime = p.time;
|
|
3891
|
+
var levels = __audioLevelsProperty(layer);
|
|
3892
|
+
if (!levels) {
|
|
3893
|
+
throw new Error(
|
|
3894
|
+
"the layer created for \"" + item.name + "\" has no Audio Levels property, so its level could not be set"
|
|
3895
|
+
);
|
|
3896
|
+
}
|
|
3897
|
+
if (p.label !== null) layer.label = p.label;
|
|
3898
|
+
|
|
3899
|
+
if (p.loop) {
|
|
3900
|
+
// `=== false`, as with hasAudio: a layer that does not answer the question
|
|
3901
|
+
// is not refused on the strength of a missing property.
|
|
3902
|
+
if (layer.canSetTimeRemapEnabled === false) {
|
|
3903
|
+
throw new Error(
|
|
3904
|
+
"After Effects reports that time remapping cannot be enabled on the layer created for \"" + item.name + "\", so it cannot loop"
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
layer.timeRemapEnabled = true;
|
|
3908
|
+
var remap = __timeRemapProperty(layer);
|
|
3909
|
+
if (!remap) {
|
|
3910
|
+
throw new Error(
|
|
3911
|
+
"time remapping was enabled on the layer created for \"" + item.name + "\" but its Time Remap property could not be found, so the loop expression could not be set"
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3914
|
+
// The two keyframes AE created when remapping was enabled stay exactly
|
|
3915
|
+
// where they are. Removing them hides the property, and the next write to
|
|
3916
|
+
// it throws (issue #86); the expression overrides them anyway.
|
|
3917
|
+
remap.expression = __loopExpression(p);
|
|
3918
|
+
layer.outPoint = p.loopEnd;
|
|
3919
|
+
if (p.inPoint !== null) layer.inPoint = p.inPoint;
|
|
3920
|
+
} else {
|
|
3921
|
+
if (p.outPoint !== null) layer.outPoint = p.outPoint;
|
|
3922
|
+
if (p.inPoint !== null) layer.inPoint = p.inPoint;
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// AE's Audio Levels is itself in decibels, one entry per channel.
|
|
3926
|
+
var level = [p.levelDb, p.levelDb];
|
|
3927
|
+
if (p.fadeIn > 0 || p.fadeOut > 0) {
|
|
3928
|
+
var inP = layer.inPoint;
|
|
3929
|
+
var outP = layer.outPoint;
|
|
3930
|
+
var dur = outP - inP;
|
|
3931
|
+
// The planner measured the fade against what it could know; this is the
|
|
3932
|
+
// extent After Effects actually gave the layer, and the last line.
|
|
3933
|
+
if (p.fadeIn + p.fadeOut > dur + __CUE_TIME_EPS) {
|
|
3934
|
+
throw new Error(
|
|
3935
|
+
"its fades (" + __fmtSeconds(p.fadeIn) + " in, " + __fmtSeconds(p.fadeOut) + " out) are longer than the " +
|
|
3936
|
+
__fmtSeconds(dur) + " After Effects gave the layer (" + __fmtSeconds(inP) + " to " + __fmtSeconds(outP) + ")"
|
|
3937
|
+
);
|
|
3938
|
+
}
|
|
3939
|
+
var floor = [floorDb, floorDb];
|
|
3940
|
+
if (p.fadeIn > 0) {
|
|
3941
|
+
levels.setValueAtTime(inP, floor);
|
|
3942
|
+
levels.setValueAtTime(inP + p.fadeIn, level);
|
|
3943
|
+
}
|
|
3944
|
+
if (p.fadeOut > 0) {
|
|
3945
|
+
levels.setValueAtTime(outP - p.fadeOut, level);
|
|
3946
|
+
levels.setValueAtTime(outP, floor);
|
|
3947
|
+
}
|
|
3948
|
+
} else {
|
|
3949
|
+
levels.setValue(level);
|
|
3950
|
+
}
|
|
3951
|
+
return layer;
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
/**
|
|
3955
|
+
* What the caller gets back per cue: the id it has to carry forward, the name,
|
|
3956
|
+
* the time, and — only where an option could have changed it — what After
|
|
3957
|
+
* Effects actually did. Never an echo of the request.
|
|
3958
|
+
*/
|
|
3959
|
+
function __placedEntry(layer, p) {
|
|
3960
|
+
var entry = { layerId: layer.id, name: layer.name, time: p.time };
|
|
3961
|
+
// Read the trim back: AE clamps an in/out point to what the source can
|
|
3962
|
+
// actually supply, and the caller should see what it got.
|
|
3963
|
+
if (p.inPoint !== null) entry.inPoint = layer.inPoint;
|
|
3964
|
+
if (p.outPoint !== null || p.loop || p.stretch !== null) entry.outPoint = layer.outPoint;
|
|
3965
|
+
if (p.loop) entry.looped = true;
|
|
3966
|
+
if (p.stretch !== null) entry.stretch = layer.stretch;
|
|
3967
|
+
if (p.fadeIn > 0) entry.fadeIn = p.fadeIn;
|
|
3968
|
+
if (p.fadeOut > 0) entry.fadeOut = p.fadeOut;
|
|
3969
|
+
return entry;
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
OPS.place_audio_cues = noUndoWhen(
|
|
3973
|
+
// dryRun is not an undo step either. A plan that quietly appeared in the
|
|
3974
|
+
// user's undo history would make "this changed nothing" false in the one
|
|
3975
|
+
// place they can see it.
|
|
3976
|
+
function (args) { return !!(args && args.dryRun === true); },
|
|
3977
|
+
function (args) {
|
|
3978
|
+
var comp = getCompById(args.compId);
|
|
3979
|
+
var total = 0;
|
|
3980
|
+
if (args.cues instanceof Array) total = args.cues.length;
|
|
3981
|
+
var plan = __planAudioCues(comp, args);
|
|
3982
|
+
|
|
3983
|
+
if (args.dryRun === true) return __audioDryRunReport(comp, plan, total);
|
|
3984
|
+
if (plan.problems.length > 0) throw new Error(__audioProblemMessage(plan.problems, total));
|
|
3985
|
+
|
|
3986
|
+
var createdLayers = [];
|
|
3987
|
+
var importedItems = [];
|
|
3988
|
+
var importedReport = [];
|
|
3989
|
+
var reusedReport = [];
|
|
3990
|
+
var reusedSeen = {};
|
|
3991
|
+
var placed = [];
|
|
3992
|
+
var anyFade = false;
|
|
3993
|
+
|
|
3994
|
+
try {
|
|
3995
|
+
// One import per distinct file, before any layer exists, so a bad file
|
|
3996
|
+
// costs nothing but the import itself.
|
|
3997
|
+
var imported = {};
|
|
3998
|
+
for (var i = 0; i < plan.toImport.length; i++) {
|
|
3999
|
+
var spec = plan.toImport[i];
|
|
4000
|
+
var newItem = __importFile(spec.file, spec.path, false);
|
|
4001
|
+
importedItems.push(newItem);
|
|
4002
|
+
var why = __audioItemProblem(newItem);
|
|
4003
|
+
if (why) throw new Error("imported " + spec.path + " and then found that " + why);
|
|
4004
|
+
imported[spec.fsName] = newItem;
|
|
4005
|
+
importedReport.push(__sourceReport(newItem, spec.path));
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
// The fades the planner could not measure — on files it had not seen —
|
|
4009
|
+
// are measured now, with every duration known and no layer yet made, so
|
|
4010
|
+
// a misfit still names every offending cue at once and costs only the
|
|
4011
|
+
// imports, which the rollback takes back.
|
|
4012
|
+
var lateProblems = [];
|
|
4013
|
+
for (var f = 0; f < plan.planned.length; f++) {
|
|
4014
|
+
var pf = plan.planned[f];
|
|
4015
|
+
if (!pf.fadeUnchecked) continue;
|
|
4016
|
+
var fit = __fadeFitProblem(pf, __cueExtent(pf, __itemDuration(imported[pf.source.fsName])));
|
|
4017
|
+
if (fit === "unknown") {
|
|
4018
|
+
fit = "After Effects did not report a duration for the imported " + pf.source.path + ", so its fade cannot be checked";
|
|
4019
|
+
}
|
|
4020
|
+
if (fit !== null) lateProblems.push({ cue: pf.index, reason: fit });
|
|
4021
|
+
}
|
|
4022
|
+
if (lateProblems.length > 0) {
|
|
4023
|
+
throw new Error(
|
|
4024
|
+
"the fades on " + lateProblems.length + " cue(s) do not fit the files once imported — " + __audioProblemLines(lateProblems)
|
|
4025
|
+
);
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
for (var k = 0; k < plan.planned.length; k++) {
|
|
4029
|
+
var p = plan.planned[k];
|
|
4030
|
+
var item = p.source.item;
|
|
4031
|
+
if (!item) item = imported[p.source.fsName];
|
|
4032
|
+
if (p.source.kind !== "import" && !reusedSeen.hasOwnProperty(String(item.id))) {
|
|
4033
|
+
reusedSeen[String(item.id)] = true;
|
|
4034
|
+
reusedReport.push(__sourceReport(item, p.source.path));
|
|
4035
|
+
}
|
|
4036
|
+
var layer;
|
|
4037
|
+
try {
|
|
4038
|
+
layer = __placeAudioCue(comp, p, item, createdLayers, plan.floorDb);
|
|
4039
|
+
} catch (eCue) {
|
|
4040
|
+
throw new Error("cue " + p.index + " (\"" + p.name + "\" at " + p.time + "s): " + eCue.message);
|
|
4041
|
+
}
|
|
4042
|
+
if (p.fadeIn > 0 || p.fadeOut > 0) anyFade = true;
|
|
4043
|
+
placed.push(__placedEntry(layer, p));
|
|
4044
|
+
}
|
|
4045
|
+
} catch (e) {
|
|
4046
|
+
__rollbackAudioCues(createdLayers, importedItems);
|
|
4047
|
+
throw new Error(
|
|
4048
|
+
"place_audio_cues failed on " + ((e && e.message) ? e.message : String(e)) +
|
|
4049
|
+
". The " + createdLayers.length + " layer(s) and " + importedItems.length +
|
|
4050
|
+
" import(s) it had already made were removed, so the comp and project are as they were."
|
|
4051
|
+
);
|
|
4052
|
+
}
|
|
1851
4053
|
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
4054
|
+
var out = {
|
|
4055
|
+
compId: comp.id,
|
|
4056
|
+
placed: placed,
|
|
4057
|
+
count: placed.length,
|
|
4058
|
+
sources: { imported: importedReport, reused: reusedReport },
|
|
4059
|
+
levelUnit: "dB"
|
|
4060
|
+
};
|
|
4061
|
+
if (anyFade) out.fadeFloorDb = plan.floorDb;
|
|
4062
|
+
return out;
|
|
1856
4063
|
}
|
|
1857
|
-
|
|
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
|
-
};
|
|
4064
|
+
);
|
|
1869
4065
|
|
|
1870
4066
|
|
|
1871
4067
|
// ===== mogrt.jsx =====
|
|
@@ -1901,6 +4097,19 @@ OPS.create_footage_layer = function (args) {
|
|
|
1901
4097
|
// a project writes Untitled.mogrt over the last one.
|
|
1902
4098
|
// * The export invalidates every object reference held across it, including
|
|
1903
4099
|
// `app.project`. See the re-fetch below.
|
|
4100
|
+
//
|
|
4101
|
+
// And one thing suppression costs, which is the whole of issue #71.
|
|
4102
|
+
// exportAsMotionGraphicsTemplate reports a boolean and nothing else — AE never
|
|
4103
|
+
// says *why* it declined — so once dialogs are suppressed a refusal and a
|
|
4104
|
+
// success-that-wrote-nothing look identical from a script. That silence is
|
|
4105
|
+
// evidence of nothing in particular, and it must not be read as "a dialog
|
|
4106
|
+
// blocked us": under beginSuppressDialogs() a blocking dialog is the one cause
|
|
4107
|
+
// ruled out by construction, because suppressing them is exactly what that call
|
|
4108
|
+
// did. A comp with an empty Essential Graphics panel is not exportable, and it
|
|
4109
|
+
// used to come back blaming a dialog that did not exist and offering a remedy
|
|
4110
|
+
// (retry unsuppressed and click it) that led nowhere. So: check every
|
|
4111
|
+
// precondition that can be checked *before* the export and name it, and when
|
|
4112
|
+
// the export still fails, report the cause as unknown rather than as a dialog.
|
|
1904
4113
|
|
|
1905
4114
|
/** Distinct fonts used by the text layers in a comp, following nested comps. */
|
|
1906
4115
|
function __collectFonts(comp, depth, seenComps, fonts) {
|
|
@@ -1937,6 +4146,106 @@ function __mogrtFileName(name) {
|
|
|
1937
4146
|
return String(name) + ".mogrt";
|
|
1938
4147
|
}
|
|
1939
4148
|
|
|
4149
|
+
/** Trailing separators off, so two spellings of the same folder compare equal. */
|
|
4150
|
+
function __trimTrailingSeparator(p) {
|
|
4151
|
+
var s = String(p);
|
|
4152
|
+
while (s.length > 1) {
|
|
4153
|
+
var last = s.charAt(s.length - 1);
|
|
4154
|
+
if (last !== "/" && last !== "\\") break;
|
|
4155
|
+
s = s.substring(0, s.length - 1);
|
|
4156
|
+
}
|
|
4157
|
+
return s;
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
function __isBlank(s) {
|
|
4161
|
+
var t = String(s);
|
|
4162
|
+
for (var i = 0; i < t.length; i++) {
|
|
4163
|
+
var ch = t.charAt(i);
|
|
4164
|
+
if (ch !== " " && ch !== "\t" && ch !== "\n" && ch !== "\r") return false;
|
|
4165
|
+
}
|
|
4166
|
+
return true;
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
/**
|
|
4170
|
+
* How many controllers the comp's Essential Graphics panel holds, or null when
|
|
4171
|
+
* the host will not say.
|
|
4172
|
+
*
|
|
4173
|
+
* null is not zero. A read that throws means this AE cannot be asked, and
|
|
4174
|
+
* refusing an export on the strength of a question that was never answered
|
|
4175
|
+
* would be its own confidently wrong diagnosis. Only an explicit 0 refuses.
|
|
4176
|
+
*/
|
|
4177
|
+
function __controllerCount(comp) {
|
|
4178
|
+
var n = null;
|
|
4179
|
+
try { n = comp.motionGraphicsTemplateControllerCount; } catch (e) { return null; }
|
|
4180
|
+
if (typeof n !== "number") return null;
|
|
4181
|
+
return n;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
/**
|
|
4185
|
+
* Whether After Effects can actually write into the destination folder.
|
|
4186
|
+
*
|
|
4187
|
+
* ExtendScript has no permission API, so the only reliable test is to write.
|
|
4188
|
+
* A folder that exists and refuses writes is another cause the export reports
|
|
4189
|
+
* as nothing at all, and it is worth naming before spending the export rather
|
|
4190
|
+
* than after. The probe is removed on every path; it never outlives the call.
|
|
4191
|
+
*/
|
|
4192
|
+
function __folderIsWritable(folder) {
|
|
4193
|
+
var probe = new File(__joinPath(folder.fsName, ".ae-mcp-write-probe"));
|
|
4194
|
+
var opened = false;
|
|
4195
|
+
try {
|
|
4196
|
+
opened = probe.open("w");
|
|
4197
|
+
if (opened) probe.write("");
|
|
4198
|
+
} catch (eW) {
|
|
4199
|
+
opened = false;
|
|
4200
|
+
}
|
|
4201
|
+
try { probe.close(); } catch (eC) {}
|
|
4202
|
+
try { if (probe.exists) probe.remove(); } catch (eR) {}
|
|
4203
|
+
return opened === true;
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
/**
|
|
4207
|
+
* The failure message for an export that wrote nothing.
|
|
4208
|
+
*
|
|
4209
|
+
* Built rather than fixed, because the only honest message names what was
|
|
4210
|
+
* actually established and stops there. `suppressed` is the hinge: with dialogs
|
|
4211
|
+
* suppressed a modal cannot be the cause and must not be mentioned as one
|
|
4212
|
+
* (issue #71); without suppression it is the first thing to look at. Everything
|
|
4213
|
+
* after that is unknown, and says so — AE's own UI is the only place the real
|
|
4214
|
+
* reason exists, which is why the diagnostic step is to run the export by hand.
|
|
4215
|
+
*/
|
|
4216
|
+
function __exportFailureMessage(info) {
|
|
4217
|
+
var lines = [];
|
|
4218
|
+
lines.push(
|
|
4219
|
+
"After Effects did not write a template. It returned " + String(info.exported) +
|
|
4220
|
+
" and there is no file at " + info.outPath + "."
|
|
4221
|
+
);
|
|
4222
|
+
lines.push(
|
|
4223
|
+
"Ruled out before the export ran: the project is saved (" + info.projectPath + "); the destination " +
|
|
4224
|
+
"folder exists and accepts writes; the template name resolves to that folder; and the comp has " +
|
|
4225
|
+
info.controllerText + "."
|
|
4226
|
+
);
|
|
4227
|
+
if (!info.suppressed) {
|
|
4228
|
+
lines.push(
|
|
4229
|
+
"Dialogs were NOT suppressed for this call, so a modal dialog waiting in After Effects is the " +
|
|
4230
|
+
"likeliest cause — switch to After Effects and look for one. Retry with the default " +
|
|
4231
|
+
"suppressDialogs:true once it is cleared."
|
|
4232
|
+
);
|
|
4233
|
+
} else {
|
|
4234
|
+
lines.push(
|
|
4235
|
+
"Dialogs were suppressed, so a modal dialog cannot be what stopped this — do not go looking for " +
|
|
4236
|
+
"one to click. After Effects reported no reason at all, and there is no reason for a script to " +
|
|
4237
|
+
"read, so the cause is unknown."
|
|
4238
|
+
);
|
|
4239
|
+
lines.push(
|
|
4240
|
+
"To find out what After Effects would have said, ask the user to export the same comp by hand: " +
|
|
4241
|
+
"Window > Essential Graphics, select the comp, then Export Motion Graphics Template. After " +
|
|
4242
|
+
"Effects shows its own error there. Running this tool again with suppressDialogs:false shows the " +
|
|
4243
|
+
"same dialog, but it will freeze this connection until someone clicks it."
|
|
4244
|
+
);
|
|
4245
|
+
}
|
|
4246
|
+
return lines.join(" ");
|
|
4247
|
+
}
|
|
4248
|
+
|
|
1940
4249
|
OPS.export_mogrt = noUndo(function (args) {
|
|
1941
4250
|
var comp = getCompById(args.compId);
|
|
1942
4251
|
|
|
@@ -1952,6 +4261,29 @@ OPS.export_mogrt = noUndo(function (args) {
|
|
|
1952
4261
|
);
|
|
1953
4262
|
}
|
|
1954
4263
|
|
|
4264
|
+
// Every precondition that can be established from a script runs here, before
|
|
4265
|
+
// the project is touched at all: the template is not renamed, the project is
|
|
4266
|
+
// not saved and no export is attempted until they all pass. A refusal below
|
|
4267
|
+
// therefore costs the user nothing and names its own remedy — which is the
|
|
4268
|
+
// whole point, since a failure *after* the export names nothing.
|
|
4269
|
+
|
|
4270
|
+
// The Essential Graphics panel has to have something in it. AE will not build
|
|
4271
|
+
// a template with no controllers, and under suppression it declines without a
|
|
4272
|
+
// word — which is what made this look like a dialog (issue #71). An explicit
|
|
4273
|
+
// 0 is the whole diagnosis; a count the host refuses to give is not.
|
|
4274
|
+
var preControllerCount = __controllerCount(comp);
|
|
4275
|
+
if (preControllerCount === 0) {
|
|
4276
|
+
throw new Error(
|
|
4277
|
+
"This comp has 0 Essential Graphics controllers, and After Effects cannot export a template " +
|
|
4278
|
+
"from a comp with an empty Essential Graphics panel. It refuses without writing a file and " +
|
|
4279
|
+
"without reporting anything a script can read, so nothing was exported and nothing in the " +
|
|
4280
|
+
"project was changed. To fix it, put at least one property into the panel: in After Effects " +
|
|
4281
|
+
"open Window > Essential Graphics, pick this comp in the panel's dropdown, then drag a layer " +
|
|
4282
|
+
"property into it — a text layer's Source Text, a colour, a slider, a position. Then call " +
|
|
4283
|
+
"export_mogrt again."
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
|
|
1955
4287
|
var templateName = (typeof args.name === "string" && args.name.length > 0) ? args.name : null;
|
|
1956
4288
|
var previousTemplateName = null;
|
|
1957
4289
|
try { previousTemplateName = comp.motionGraphicsTemplateName; } catch (e) {}
|
|
@@ -1959,7 +4291,16 @@ OPS.export_mogrt = noUndo(function (args) {
|
|
|
1959
4291
|
// AE's own default is "Untitled" for a template built by script, which
|
|
1960
4292
|
// silently collides with every other comp in the project. The comp name is
|
|
1961
4293
|
// what the user would have typed. A name they *did* type is left alone.
|
|
1962
|
-
|
|
4294
|
+
if (!previousTemplateName || previousTemplateName === "Untitled") {
|
|
4295
|
+
templateName = comp.name;
|
|
4296
|
+
} else {
|
|
4297
|
+
templateName = previousTemplateName;
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
if (__isBlank(templateName)) {
|
|
4301
|
+
throw new Error(
|
|
4302
|
+
"The template name is empty, and it becomes the .mogrt filename. Pass a `name`."
|
|
4303
|
+
);
|
|
1963
4304
|
}
|
|
1964
4305
|
|
|
1965
4306
|
var destDir = (typeof args.destDir === "string" && args.destDir.length > 0)
|
|
@@ -1974,6 +4315,22 @@ OPS.export_mogrt = noUndo(function (args) {
|
|
|
1974
4315
|
|
|
1975
4316
|
var outPath = __joinPath(folder.fsName, __mogrtFileName(templateName));
|
|
1976
4317
|
var outFile = new File(outPath);
|
|
4318
|
+
|
|
4319
|
+
// The name becomes a filename, so a separator in it sends the write somewhere
|
|
4320
|
+
// else — or nowhere, silently. Measured rather than pattern-matched: ask the
|
|
4321
|
+
// File where it actually landed and compare. That works the same on both
|
|
4322
|
+
// platforms, which a list of illegal characters would not.
|
|
4323
|
+
var resolvedParent = null;
|
|
4324
|
+
try { resolvedParent = outFile.parent.fsName; } catch (eP) {}
|
|
4325
|
+
if (resolvedParent !== null &&
|
|
4326
|
+
__trimTrailingSeparator(resolvedParent) !== __trimTrailingSeparator(folder.fsName)) {
|
|
4327
|
+
throw new Error(
|
|
4328
|
+
"The template name \"" + templateName + "\" is used as the .mogrt filename, and it resolves to " +
|
|
4329
|
+
outPath + ", which is not inside " + folder.fsName + ". Pass a `name` with no slashes in it, and " +
|
|
4330
|
+
"a `destDir` if you want it written somewhere else."
|
|
4331
|
+
);
|
|
4332
|
+
}
|
|
4333
|
+
|
|
1977
4334
|
var existed = outFile.exists;
|
|
1978
4335
|
if (existed && args.overwrite !== true) {
|
|
1979
4336
|
throw new Error(
|
|
@@ -1982,6 +4339,17 @@ OPS.export_mogrt = noUndo(function (args) {
|
|
|
1982
4339
|
);
|
|
1983
4340
|
}
|
|
1984
4341
|
|
|
4342
|
+
// Last of the checkable preconditions, and the last thing that costs nothing
|
|
4343
|
+
// to get wrong. A folder that will not accept a file is one more cause the
|
|
4344
|
+
// export reports as silence.
|
|
4345
|
+
if (!__folderIsWritable(folder)) {
|
|
4346
|
+
throw new Error(
|
|
4347
|
+
"After Effects cannot write into " + folder.fsName + " — a test file could not be created there. " +
|
|
4348
|
+
"Nothing was exported. Pass a `destDir` the user can write to, or ask them to fix the folder's " +
|
|
4349
|
+
"permissions."
|
|
4350
|
+
);
|
|
4351
|
+
}
|
|
4352
|
+
|
|
1985
4353
|
var fonts = [];
|
|
1986
4354
|
try { __collectFonts(comp, 0, [], fonts); } catch (eF) {}
|
|
1987
4355
|
|
|
@@ -1995,6 +4363,7 @@ OPS.export_mogrt = noUndo(function (args) {
|
|
|
1995
4363
|
// Everything the result needs, read *before* the export. See below.
|
|
1996
4364
|
var compId = comp.id;
|
|
1997
4365
|
var compName = comp.name;
|
|
4366
|
+
var projectPath = app.project.file.fsName;
|
|
1998
4367
|
|
|
1999
4368
|
var suppress = args.suppressDialogs !== false;
|
|
2000
4369
|
var suppressed = false;
|
|
@@ -2023,10 +4392,21 @@ OPS.export_mogrt = noUndo(function (args) {
|
|
|
2023
4392
|
// codebase refuses to pass on.
|
|
2024
4393
|
var written = new File(outPath);
|
|
2025
4394
|
if (!written.exists) {
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
)
|
|
4395
|
+
var controllerText;
|
|
4396
|
+
if (preControllerCount === null) {
|
|
4397
|
+
controllerText = "controllers this version of After Effects would not count";
|
|
4398
|
+
} else if (preControllerCount === 1) {
|
|
4399
|
+
controllerText = "1 Essential Graphics controller";
|
|
4400
|
+
} else {
|
|
4401
|
+
controllerText = String(preControllerCount) + " Essential Graphics controllers";
|
|
4402
|
+
}
|
|
4403
|
+
throw new Error(__exportFailureMessage({
|
|
4404
|
+
exported: exported,
|
|
4405
|
+
outPath: outPath,
|
|
4406
|
+
projectPath: projectPath,
|
|
4407
|
+
suppressed: suppress,
|
|
4408
|
+
controllerText: controllerText
|
|
4409
|
+
}));
|
|
2030
4410
|
}
|
|
2031
4411
|
var bytes = written.length;
|
|
2032
4412
|
if (!(bytes > 0)) throw new Error("The exported template at " + outPath + " is empty (0 bytes).");
|
|
@@ -2042,7 +4422,9 @@ OPS.export_mogrt = noUndo(function (args) {
|
|
|
2042
4422
|
createdDir: createdDir,
|
|
2043
4423
|
projectSaved: app.project.file.fsName,
|
|
2044
4424
|
dialogsSuppressed: suppress,
|
|
2045
|
-
|
|
4425
|
+
// Re-read rather than echoed: the export is the only thing that can change
|
|
4426
|
+
// it, and a host that will not answer says null instead of a made-up 0.
|
|
4427
|
+
controllerCount: __controllerCount(comp),
|
|
2046
4428
|
fonts: fonts
|
|
2047
4429
|
};
|
|
2048
4430
|
if (!suppress) {
|
|
@@ -2175,6 +4557,11 @@ OPS.set_house_style = noUndo(function (args) {
|
|
|
2175
4557
|
}
|
|
2176
4558
|
|
|
2177
4559
|
file.encoding = "UTF-8";
|
|
4560
|
+
// ExtendScript's default lineFeed on macOS is "Macintosh", which turns every
|
|
4561
|
+
// "\n" into a bare "\r" on disk (measured on 26.3: a guide written through
|
|
4562
|
+
// this op read back fine, because File.read normalises, but opened as one
|
|
4563
|
+
// long line in a text editor). The file is meant to be hand-edited.
|
|
4564
|
+
file.lineFeed = "Unix";
|
|
2178
4565
|
if (!file.open("w")) throw new Error("Could not open " + file.fsName + " for writing");
|
|
2179
4566
|
try {
|
|
2180
4567
|
if (!file.write(content)) throw new Error("Write failed for " + file.fsName);
|
|
@@ -2196,32 +4583,66 @@ OPS.set_house_style = noUndo(function (args) {
|
|
|
2196
4583
|
|
|
2197
4584
|
// batch.jsx — execute many ops in one ExtendScript pass.
|
|
2198
4585
|
// For huge batches, registers a job and processes in chunks via _continue_job.
|
|
4586
|
+
//
|
|
4587
|
+
// UNDO GROUPING, which is the whole design constraint here (issue #69):
|
|
4588
|
+
// After Effects **discards** an undo group opened in one `evalScript` call and
|
|
4589
|
+
// closed in another. Measured, not inferred — a 600-op batch that opened its
|
|
4590
|
+
// group in `run_batch` and closed it from a later `_continue_job` produced ~600
|
|
4591
|
+
// undo steps, and AE's Edit menu read `Undo New Solid` rather than the group's
|
|
4592
|
+
// name. So a group only survives if it opens and closes inside one call, and
|
|
4593
|
+
// that gives the two paths below genuinely different answers:
|
|
4594
|
+
//
|
|
4595
|
+
// inline (<= 500 ops, or any size with singleUndo) — the whole batch runs in
|
|
4596
|
+
// one call, so one group round it is real. One undo step.
|
|
4597
|
+
// chunked (> 500 ops) — the batch spans N calls, so the most AE will keep is
|
|
4598
|
+
// one group per chunk. N undo steps, counted and reported, never claimed
|
|
4599
|
+
// to be one.
|
|
4600
|
+
|
|
4601
|
+
// The inline cutoff. Inline stays sub-second for typical create/keyframe ops up
|
|
4602
|
+
// to a few hundred; the async-job overhead (jobId envelope, polling, progress
|
|
4603
|
+
// notifications) is only worth it for genuinely long jobs.
|
|
4604
|
+
var __BATCH_INLINE_MAX = 500;
|
|
4605
|
+
// Chunk size for the async path. The panel reads this back off the envelope
|
|
4606
|
+
// rather than holding its own copy, so the two cannot drift.
|
|
4607
|
+
var __BATCH_CHUNK = 25;
|
|
4608
|
+
// The ceiling on singleUndo. Past this, one blocking ExtendScript call freezes
|
|
4609
|
+
// After Effects for long enough that the user will think it has hung, with no
|
|
4610
|
+
// progress events to say otherwise — so it is refused rather than attempted.
|
|
4611
|
+
var __BATCH_SINGLE_UNDO_MAX = 2000;
|
|
2199
4612
|
|
|
2200
4613
|
OPS.run_batch = function (args) {
|
|
2201
4614
|
var ops = args.ops || [];
|
|
2202
4615
|
var transactional = args.transactional !== false;
|
|
2203
4616
|
var name = args.undoGroupName || "AE MCP Batch";
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
//
|
|
2207
|
-
//
|
|
2208
|
-
if (ops.length
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
errors.push({ index: i, op: step.op, error: e.message });
|
|
2218
|
-
if (transactional) throw new Error("Batch failed at op[" + i + "] " + step.op + ": " + e.message);
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
return { results: results, errors: errors, total: ops.length };
|
|
4617
|
+
var singleUndo = args.singleUndo === true;
|
|
4618
|
+
|
|
4619
|
+
// Refused before anything else happens, including the diff fingerprint: a
|
|
4620
|
+
// call that is not going to run should cost nothing.
|
|
4621
|
+
if (singleUndo && ops.length > __BATCH_SINGLE_UNDO_MAX) {
|
|
4622
|
+
throw new Error(
|
|
4623
|
+
"singleUndo refuses " + ops.length + " ops (the limit is " + __BATCH_SINGLE_UNDO_MAX + "). " +
|
|
4624
|
+
"One undo step means the whole batch runs inside a single ExtendScript call, which " +
|
|
4625
|
+
"freezes After Effects' interface for the whole of it and reports no progress; at this " +
|
|
4626
|
+
"size the user would reasonably think it had hung. Either split the work into batches of " +
|
|
4627
|
+
__BATCH_SINGLE_UNDO_MAX + " or fewer, or drop singleUndo and accept one undo step per " +
|
|
4628
|
+
"chunk of " + __BATCH_CHUNK + " (the result says exactly how many)."
|
|
4629
|
+
);
|
|
2222
4630
|
}
|
|
4631
|
+
|
|
4632
|
+
// diff:true fingerprints the comps this batch names, before and after, inside
|
|
4633
|
+
// this one call — see snapshot.jsx. Null unless asked for.
|
|
4634
|
+
var diffState = __diffStart(args, ops);
|
|
4635
|
+
|
|
4636
|
+
if (ops.length <= __BATCH_INLINE_MAX || singleUndo) {
|
|
4637
|
+
return __batchInline(ops, transactional, name, diffState);
|
|
4638
|
+
}
|
|
4639
|
+
|
|
2223
4640
|
// Long batches: register job, return jobId; panel polls _continue_job.
|
|
4641
|
+
// No undo group is opened here on purpose. One opened now would be closed
|
|
4642
|
+
// from a different call and AE would throw it away, which is exactly how this
|
|
4643
|
+
// op spent three releases reporting one undo step and delivering hundreds.
|
|
2224
4644
|
var jobId = __newJobId();
|
|
4645
|
+
var estimate = Math.ceil(ops.length / __BATCH_CHUNK);
|
|
2225
4646
|
JOBS[jobId] = {
|
|
2226
4647
|
cursor: 0,
|
|
2227
4648
|
total: ops.length,
|
|
@@ -2231,48 +4652,212 @@ OPS.run_batch = function (args) {
|
|
|
2231
4652
|
cancelled: false,
|
|
2232
4653
|
transactional: transactional,
|
|
2233
4654
|
name: name,
|
|
4655
|
+
chunkSize: __BATCH_CHUNK,
|
|
4656
|
+
// Counted as groups are actually opened, never predicted. See __UNDO_GROUPS.
|
|
4657
|
+
undoSteps: 0,
|
|
4658
|
+
// Carried across the chunked continuations: the before-fingerprint has to
|
|
4659
|
+
// outlive the call that took it, or a long batch could never be diffed.
|
|
4660
|
+
diffState: diffState,
|
|
4661
|
+
};
|
|
4662
|
+
return {
|
|
4663
|
+
jobId: jobId,
|
|
4664
|
+
async: true,
|
|
4665
|
+
total: ops.length,
|
|
4666
|
+
chunkSize: __BATCH_CHUNK,
|
|
4667
|
+
// Labelled an estimate because it is one: a transactional failure ends the
|
|
4668
|
+
// batch early, and the exact count comes back with the final result.
|
|
4669
|
+
undoStepsEstimate: estimate,
|
|
4670
|
+
undoGroupName: name,
|
|
4671
|
+
note: "This batch runs in " + estimate + " chunks of " + __BATCH_CHUNK + " and will land as " +
|
|
4672
|
+
"about " + estimate + " undo steps, NOT one — After Effects discards an undo group that " +
|
|
4673
|
+
"spans two script calls, so a chunked batch can only group per chunk. The final result " +
|
|
4674
|
+
"reports the exact number. If the user needs a single Cmd-Z, re-send with singleUndo:true " +
|
|
4675
|
+
"(up to " + __BATCH_SINGLE_UNDO_MAX + " ops), which runs the whole batch in one blocking " +
|
|
4676
|
+
"call with no progress events.",
|
|
2234
4677
|
};
|
|
2235
|
-
// Wrap async run already inside an undoGroup chain. We open it now, the
|
|
2236
|
-
// continuations stay inside it until finalization.
|
|
2237
|
-
app.beginUndoGroup(name);
|
|
2238
|
-
JOBS[jobId].undoOpen = true;
|
|
2239
|
-
return { jobId: jobId, async: true, total: ops.length };
|
|
2240
4678
|
};
|
|
2241
|
-
// run_batch is allowed to manage its own undo
|
|
4679
|
+
// run_batch is allowed to manage its own undo: the inline path opens exactly one
|
|
4680
|
+
// group of its own, and the chunked path opens one per continuation.
|
|
2242
4681
|
OPS.run_batch.__meta = { noUndo: true };
|
|
2243
4682
|
|
|
4683
|
+
// The inline path: every op inside one undo group inside one evalScript, which
|
|
4684
|
+
// is the only shape After Effects keeps.
|
|
4685
|
+
//
|
|
4686
|
+
// withUndo() rather than a bare beginUndoGroup/endUndoGroup pair, for two
|
|
4687
|
+
// reasons: its `finally` closes the group when a transactional op throws, and it
|
|
4688
|
+
// is the only thing that keeps __UNDO_OPEN honest, so a batched op that calls
|
|
4689
|
+
// withoutUndoGroup() still works instead of silently doing nothing.
|
|
4690
|
+
function __batchInline(ops, transactional, name, diffState) {
|
|
4691
|
+
var results = [];
|
|
4692
|
+
var errors = [];
|
|
4693
|
+
var before = __undoGroupsOpened();
|
|
4694
|
+
// A run_batch listed as an op inside another run_batch would otherwise open a
|
|
4695
|
+
// second group inside the first, and AE's groups do not nest: the inner
|
|
4696
|
+
// endUndoGroup closes the outer one and the rest of the outer batch writes
|
|
4697
|
+
// ungrouped. Run inside the group that is already open instead, and say so —
|
|
4698
|
+
// an inner batch reporting "one undo step" of its own would be a second lie
|
|
4699
|
+
// in the same shape as the one this fixes.
|
|
4700
|
+
var nested = __UNDO_OPEN;
|
|
4701
|
+
// An empty batch opens nothing. AE records no undo step for a group that
|
|
4702
|
+
// changed nothing, so opening one would make `undoSteps: 1` an over-report on
|
|
4703
|
+
// the one call where the truth is unambiguous.
|
|
4704
|
+
var empty = ops.length === 0;
|
|
4705
|
+
var run = function () {
|
|
4706
|
+
for (var i = 0; i < ops.length; i++) {
|
|
4707
|
+
var step = ops[i];
|
|
4708
|
+
try {
|
|
4709
|
+
var handler = OPS[step.op];
|
|
4710
|
+
if (!handler) throw new Error("Unknown op: " + step.op);
|
|
4711
|
+
results.push(handler(step.args || {}));
|
|
4712
|
+
} catch (e) {
|
|
4713
|
+
errors.push({ index: i, op: step.op, error: e.message });
|
|
4714
|
+
if (transactional) {
|
|
4715
|
+
// Nothing rolls back, so the half-applied state is what the caller
|
|
4716
|
+
// has to reason about. The diff says where it stopped.
|
|
4717
|
+
var failure = new Error("Batch failed at op[" + i + "] " + step.op + ": " + e.message);
|
|
4718
|
+
__diffAnnotateError(failure, diffState);
|
|
4719
|
+
throw failure;
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
};
|
|
4724
|
+
try {
|
|
4725
|
+
if (nested || empty) run();
|
|
4726
|
+
else withUndo(name, run);
|
|
4727
|
+
} catch (e) {
|
|
4728
|
+
// The group is already closed by withUndo's finally. Say what it cost, so a
|
|
4729
|
+
// failed batch is as clear about its undo history as a successful one.
|
|
4730
|
+
try {
|
|
4731
|
+
e.message = String(e.message) + " || " +
|
|
4732
|
+
__batchUndoNote(__undoGroupsOpened() - before, name, nested, false);
|
|
4733
|
+
} catch (e2) {}
|
|
4734
|
+
throw e;
|
|
4735
|
+
}
|
|
4736
|
+
var out = {
|
|
4737
|
+
results: results,
|
|
4738
|
+
errors: errors,
|
|
4739
|
+
total: ops.length,
|
|
4740
|
+
undoSteps: __undoGroupsOpened() - before,
|
|
4741
|
+
undoGroupName: name,
|
|
4742
|
+
};
|
|
4743
|
+
if (nested) out.nested = true;
|
|
4744
|
+
out.note = __batchUndoNote(out.undoSteps, name, nested, false);
|
|
4745
|
+
if (diffState) out.diff = __diffFinish(diffState);
|
|
4746
|
+
return out;
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
// The one sentence every batch result carries about its own undo history.
|
|
4750
|
+
// Written for an agent that is about to tell a person how to undo the work.
|
|
4751
|
+
// `steps` is always the measured count, so this describes what happened rather
|
|
4752
|
+
// than what the path intended.
|
|
4753
|
+
function __batchUndoNote(steps, name, nested, chunked) {
|
|
4754
|
+
if (nested) {
|
|
4755
|
+
return "This batch ran inside an undo group that was already open (a run_batch nested in " +
|
|
4756
|
+
"another one), so it adds no undo step of its own — its work is part of the outer step.";
|
|
4757
|
+
}
|
|
4758
|
+
if (steps <= 0) {
|
|
4759
|
+
return "This batch opened no undo group, so it added no undo steps.";
|
|
4760
|
+
}
|
|
4761
|
+
if (steps === 1) {
|
|
4762
|
+
// A chunked batch reaches one step only by stopping after its first chunk —
|
|
4763
|
+
// cancelled, or failed in it — and that group is named "(1)", not the bare
|
|
4764
|
+
// name. Quoting the wrong one would send the user looking for a menu entry
|
|
4765
|
+
// that is not there.
|
|
4766
|
+
return 'This batch is one undo step ("' + name + (chunked ? " (1)" : "") +
|
|
4767
|
+
'"): a single Cmd-Z (Ctrl-Z on Windows) undoes all of it.';
|
|
4768
|
+
}
|
|
4769
|
+
if (chunked) {
|
|
4770
|
+
return "This batch landed as " + steps + " undo steps, NOT one — named \"" + name + " (1)\" " +
|
|
4771
|
+
'through "' + name + " (" + steps + ')". After Effects discards an undo group that spans ' +
|
|
4772
|
+
"two script calls, so a chunked batch can only group per chunk. Undoing all of it takes up " +
|
|
4773
|
+
"to " + steps + " presses of Cmd-Z (Ctrl-Z on Windows) — tell the user that number rather " +
|
|
4774
|
+
"than saying one. (A chunk whose ops changed nothing records no step, so that is the " +
|
|
4775
|
+
"ceiling.) singleUndo:true buys a single step back, at the cost of freezing AE's interface " +
|
|
4776
|
+
"for the whole batch and emitting no progress.";
|
|
4777
|
+
}
|
|
4778
|
+
return "This batch landed as " + steps + " undo steps rather than one: an op inside it closed " +
|
|
4779
|
+
"the batch's undo group and opened a new one (withoutUndoGroup — After Effects refuses " +
|
|
4780
|
+
"copyToComp on a parented or expression-linked layer while a group is open). Undoing all of " +
|
|
4781
|
+
"it takes up to " + steps + " presses of Cmd-Z (Ctrl-Z on Windows).";
|
|
4782
|
+
}
|
|
4783
|
+
|
|
2244
4784
|
// Continuation step. Returns one chunk's worth of progress.
|
|
4785
|
+
//
|
|
4786
|
+
// Cancellation, which used to need bookkeeping across calls and no longer does:
|
|
4787
|
+
// _cancel_job is its own evalScript and the panel serializes those, so a cancel
|
|
4788
|
+
// can only ever be observed *between* chunks. A chunk's group opens and closes
|
|
4789
|
+
// inside this one call, so there is never an open group for a cancel — or a
|
|
4790
|
+
// throw, or a dropped WebSocket — to leave behind. That is what the old
|
|
4791
|
+
// `j.undoOpen` flag was for, and it was tracking a group AE had already dropped.
|
|
2245
4792
|
OPS._continue_job = noUndo(function (args) {
|
|
2246
4793
|
var jobId = args.jobId;
|
|
2247
4794
|
var j = JOBS[jobId];
|
|
2248
4795
|
if (!j) throw new Error("No job: " + jobId);
|
|
2249
4796
|
if (j.cancelled) {
|
|
2250
|
-
|
|
2251
|
-
|
|
4797
|
+
var stopped = {
|
|
4798
|
+
done: true, cancelled: true, jobId: jobId,
|
|
4799
|
+
results: j.results, errors: j.errors,
|
|
4800
|
+
undoSteps: j.undoSteps, undoGroupName: j.name,
|
|
4801
|
+
note: __batchUndoNote(j.undoSteps, j.name, false, true),
|
|
4802
|
+
};
|
|
4803
|
+
if (j.diffState) stopped.diff = __diffFinish(j.diffState);
|
|
4804
|
+
return stopped;
|
|
2252
4805
|
}
|
|
2253
|
-
var chunkSize = args.chunkSize ||
|
|
4806
|
+
var chunkSize = args.chunkSize || j.chunkSize || __BATCH_CHUNK;
|
|
2254
4807
|
var endAt = Math.min(j.cursor + chunkSize, j.total);
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
if (
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
4808
|
+
var before = __undoGroupsOpened();
|
|
4809
|
+
var failure = null;
|
|
4810
|
+
// One group per chunk, opened and closed inside this call. The chunk number
|
|
4811
|
+
// is in the name so the Edit menu says which part of the batch a step is.
|
|
4812
|
+
withUndo(j.name + " (" + (j.undoSteps + 1) + ")", function () {
|
|
4813
|
+
for (; j.cursor < endAt; j.cursor++) {
|
|
4814
|
+
var step = j.ops[j.cursor];
|
|
4815
|
+
try {
|
|
4816
|
+
var handler = OPS[step.op];
|
|
4817
|
+
if (!handler) throw new Error("Unknown op: " + step.op);
|
|
4818
|
+
j.results.push(handler(step.args || {}));
|
|
4819
|
+
} catch (e) {
|
|
4820
|
+
j.errors.push({ index: j.cursor, op: step.op, error: e.message });
|
|
4821
|
+
if (j.transactional) {
|
|
4822
|
+
failure = { error: e.message, atIndex: j.cursor };
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
2268
4825
|
}
|
|
2269
4826
|
}
|
|
4827
|
+
});
|
|
4828
|
+
j.undoSteps += __undoGroupsOpened() - before;
|
|
4829
|
+
|
|
4830
|
+
if (failure) {
|
|
4831
|
+
// No rollback is attempted. The old code fired one `Undo` menu command
|
|
4832
|
+
// here, which undoes at most the last step even when it works at all —
|
|
4833
|
+
// menu commands depend on host focus and the active selection, neither of
|
|
4834
|
+
// which this bridge has. Reporting a rollback that did not happen is the
|
|
4835
|
+
// same class of lie as swallowing an error, so it says plainly what is on
|
|
4836
|
+
// disk and how many steps it took.
|
|
4837
|
+
var failed = {
|
|
4838
|
+
done: true, failed: true, jobId: jobId,
|
|
4839
|
+
error: failure.error, atIndex: failure.atIndex,
|
|
4840
|
+
results: j.results, errors: j.errors,
|
|
4841
|
+
rolledBack: false,
|
|
4842
|
+
undoSteps: j.undoSteps, undoGroupName: j.name,
|
|
4843
|
+
note: __batchUndoNote(j.undoSteps, j.name, false, true) +
|
|
4844
|
+
" Nothing was rolled back: the ops before op[" + failure.atIndex + "] are applied and " +
|
|
4845
|
+
"stay applied. Read the state back rather than re-running the batch.",
|
|
4846
|
+
};
|
|
4847
|
+
if (j.diffState) failed.diff = __diffFinish(j.diffState);
|
|
4848
|
+
return failed;
|
|
2270
4849
|
}
|
|
2271
4850
|
if (j.cursor >= j.total) {
|
|
2272
|
-
|
|
2273
|
-
|
|
4851
|
+
var done = {
|
|
4852
|
+
done: true, jobId: jobId,
|
|
4853
|
+
results: j.results, errors: j.errors, total: j.total,
|
|
4854
|
+
undoSteps: j.undoSteps, undoGroupName: j.name,
|
|
4855
|
+
note: __batchUndoNote(j.undoSteps, j.name, false, true),
|
|
4856
|
+
};
|
|
4857
|
+
if (j.diffState) done.diff = __diffFinish(j.diffState);
|
|
4858
|
+
return done;
|
|
2274
4859
|
}
|
|
2275
|
-
return { done: false, jobId: jobId, progress: j.cursor, total: j.total };
|
|
4860
|
+
return { done: false, jobId: jobId, progress: j.cursor, total: j.total, undoSteps: j.undoSteps };
|
|
2276
4861
|
});
|
|
2277
4862
|
|
|
2278
4863
|
OPS._cancel_job = noUndo(function (args) {
|
|
@@ -2285,7 +4870,10 @@ OPS._cancel_job = noUndo(function (args) {
|
|
|
2285
4870
|
OPS._get_job = noUndo(function (args) {
|
|
2286
4871
|
var j = JOBS[args.jobId];
|
|
2287
4872
|
if (!j) return null;
|
|
2288
|
-
return {
|
|
4873
|
+
return {
|
|
4874
|
+
cursor: j.cursor, total: j.total, cancelled: j.cancelled,
|
|
4875
|
+
errorCount: j.errors.length, undoSteps: j.undoSteps,
|
|
4876
|
+
};
|
|
2289
4877
|
});
|
|
2290
4878
|
|
|
2291
4879
|
|
|
@@ -2787,7 +5375,16 @@ OPS.get_project_summary = noUndo(function (args) {
|
|
|
2787
5375
|
};
|
|
2788
5376
|
});
|
|
2789
5377
|
|
|
5378
|
+
// A search, so the default is the opposite of list_layers: absent `include`
|
|
5379
|
+
// means the core alone — id/index/name/sourceType plus the comp — because the
|
|
5380
|
+
// reason to call this is to learn which layers exist and what to address them
|
|
5381
|
+
// by, not to read them (issue #87: nine matches cost ~2k tokens, re-sent on
|
|
5382
|
+
// every later request). `include: []` and omitting it are therefore the same
|
|
5383
|
+
// here, and the section names are list_layers' own, read through the same
|
|
5384
|
+
// __layerSummary. What was included is echoed, so a bounded answer is never
|
|
5385
|
+
// read as a full one.
|
|
2790
5386
|
OPS.find_layers = noUndo(function (args) {
|
|
5387
|
+
var sections = (args && args.include) ? args.include : [];
|
|
2791
5388
|
var out = [];
|
|
2792
5389
|
var comps = [];
|
|
2793
5390
|
if (args.compId) comps.push(getCompById(args.compId));
|
|
@@ -2814,16 +5411,125 @@ OPS.find_layers = noUndo(function (args) {
|
|
|
2814
5411
|
}
|
|
2815
5412
|
if (!hit) continue;
|
|
2816
5413
|
}
|
|
2817
|
-
var s = __layerSummary(l);
|
|
5414
|
+
var s = __layerSummary(l, sections);
|
|
2818
5415
|
s.compId = c.id;
|
|
2819
5416
|
s.compName = c.name;
|
|
2820
5417
|
out.push(s);
|
|
2821
5418
|
}
|
|
2822
5419
|
}
|
|
2823
|
-
return out;
|
|
5420
|
+
return { matches: out, count: out.length, compsSearched: comps.length, included: sections };
|
|
2824
5421
|
});
|
|
2825
5422
|
|
|
2826
5423
|
|
|
5424
|
+
// ===== helpers.jsx =====
|
|
5425
|
+
|
|
5426
|
+
// helpers.jsx — the scope a run_jsx script runs in.
|
|
5427
|
+
//
|
|
5428
|
+
// Everything in this file is a global function, which is what makes it visible
|
|
5429
|
+
// to `eval`d script bodies and to anything loaded through run_jsx's
|
|
5430
|
+
// `libraries`. It exists because the run_jsx description promised "helpers in
|
|
5431
|
+
// scope" and never said which, so every session re-derived the same four or
|
|
5432
|
+
// five functions — find-a-layer-by-id, ease-with-the-right-array-size, a shape
|
|
5433
|
+
// builder that zeros the position (issue #53). Each of these was written from
|
|
5434
|
+
// scratch in the transcript, at token cost, with a fresh chance of getting the
|
|
5435
|
+
// AE quirk wrong.
|
|
5436
|
+
//
|
|
5437
|
+
// The rule for adding one: it has to be something an agent would otherwise
|
|
5438
|
+
// write badly, not merely something it would write often. All four below wrap
|
|
5439
|
+
// a documented AE trap.
|
|
5440
|
+
//
|
|
5441
|
+
// These are listed by signature in the run_jsx tool description. If you change
|
|
5442
|
+
// one, change that too — it is the only place a caller ever sees them.
|
|
5443
|
+
|
|
5444
|
+
// getCompById by a shorter name, because that is the name agents guess.
|
|
5445
|
+
function compById(id) {
|
|
5446
|
+
return getCompById(id);
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
// getLayerById, but the comp may be given as an id. An agent holding
|
|
5450
|
+
// (compId, layerId) — the pair every tool returns — can use it directly.
|
|
5451
|
+
function layerById(comp, layerId) {
|
|
5452
|
+
var c = comp;
|
|
5453
|
+
if (typeof c === "number") c = getCompById(c);
|
|
5454
|
+
return getLayerById(c, layerId);
|
|
5455
|
+
}
|
|
5456
|
+
|
|
5457
|
+
// {influence, speed}, or a bare number read as influence. AE's own default
|
|
5458
|
+
// ease is 33% influence at zero speed, so that is what an omitted field gets.
|
|
5459
|
+
function __hEaseSpec(spec) {
|
|
5460
|
+
if (spec === null || spec === undefined) return { influence: 33, speed: 0 };
|
|
5461
|
+
if (typeof spec === "number") return { influence: spec, speed: 0 };
|
|
5462
|
+
var inf = 33;
|
|
5463
|
+
var spd = 0;
|
|
5464
|
+
if (typeof spec.influence === "number") inf = spec.influence;
|
|
5465
|
+
if (typeof spec.speed === "number") spd = spec.speed;
|
|
5466
|
+
return { influence: inf, speed: spd };
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
// ease(prop, keyIndex, easeIn, easeOut) — sizes its own ease array.
|
|
5470
|
+
//
|
|
5471
|
+
// setTemporalEaseAtKey wants one KeyframeEase per dimension and the count is
|
|
5472
|
+
// NOT derivable from the value: a spatial property takes exactly one whatever
|
|
5473
|
+
// its dimension (the ease runs along the motion path), a 2D layer's Scale takes
|
|
5474
|
+
// three, a shape's Ellipse Size takes two, Opacity and sliders take one. The
|
|
5475
|
+
// wrong count throws "parameter 2" and says nothing else (issue #50).
|
|
5476
|
+
//
|
|
5477
|
+
// The sizing lives in `__applyTemporalEase` in keyframes.jsx, which is what
|
|
5478
|
+
// `set_temporal_ease` and `add_keyframe` use. This helper is the same function
|
|
5479
|
+
// with a friendlier signature — deliberately not a second implementation, so a
|
|
5480
|
+
// script written through run_jsx and the same work done through the tools can
|
|
5481
|
+
// never disagree about what a property wanted.
|
|
5482
|
+
//
|
|
5483
|
+
// easeOut omitted means the same ease on both sides. Returns the number of
|
|
5484
|
+
// entries that worked, so a caller can see what the property actually wanted.
|
|
5485
|
+
function ease(prop, keyIndex, easeIn, easeOut) {
|
|
5486
|
+
var inSpec = __hEaseSpec(easeIn);
|
|
5487
|
+
var outSpec = __hEaseSpec(easeOut === undefined ? easeIn : easeOut);
|
|
5488
|
+
return __applyTemporalEase(prop, keyIndex, inSpec, outSpec);
|
|
5489
|
+
}
|
|
5490
|
+
|
|
5491
|
+
// addKeys(prop, [[time, value], ...]) — or [{time, value}, ...].
|
|
5492
|
+
// Returns the key index of each, in the order given, so the next call can ease
|
|
5493
|
+
// them without searching for them again.
|
|
5494
|
+
function addKeys(prop, pairs) {
|
|
5495
|
+
if (!pairs || !pairs.length) return [];
|
|
5496
|
+
var out = [];
|
|
5497
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
5498
|
+
var p = pairs[i];
|
|
5499
|
+
var t;
|
|
5500
|
+
var v;
|
|
5501
|
+
if (p instanceof Array) {
|
|
5502
|
+
t = p[0];
|
|
5503
|
+
v = p[1];
|
|
5504
|
+
} else {
|
|
5505
|
+
t = p.time;
|
|
5506
|
+
v = p.value;
|
|
5507
|
+
}
|
|
5508
|
+
prop.setValueAtTime(t, v);
|
|
5509
|
+
out.push(prop.nearestKeyIndex(t));
|
|
5510
|
+
}
|
|
5511
|
+
return out;
|
|
5512
|
+
}
|
|
5513
|
+
|
|
5514
|
+
// shape(comp, {name, position}) — a shape layer that lands at [0,0].
|
|
5515
|
+
//
|
|
5516
|
+
// AE spawns a scripted shape layer at the comp centre with its anchor at
|
|
5517
|
+
// (0,0), so paths authored in comp pixels come out offset by half a frame —
|
|
5518
|
+
// easy to miss on a downsampled screenshot (issue #51). Position [0,0] makes
|
|
5519
|
+
// the layer's coordinate space the comp's, which is what path vertices assume.
|
|
5520
|
+
function shape(comp, opts) {
|
|
5521
|
+
var c = comp;
|
|
5522
|
+
if (typeof c === "number") c = getCompById(c);
|
|
5523
|
+
var o = opts || {};
|
|
5524
|
+
var l = c.layers.addShape();
|
|
5525
|
+
if (o.name) l.name = o.name;
|
|
5526
|
+
var pos = o.position ? o.position : [0, 0];
|
|
5527
|
+
var value = (pos.length === 3) ? pos : [pos[0], pos[1]];
|
|
5528
|
+
l.property("Transform").property("Position").setValue(value);
|
|
5529
|
+
return l;
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5532
|
+
|
|
2827
5533
|
// ===== raw.jsx =====
|
|
2828
5534
|
|
|
2829
5535
|
// raw.jsx — escape hatch. Eval arbitrary ExtendScript and return the value.
|
|
@@ -2949,17 +5655,369 @@ function __rjResult(result, undoGroupName) {
|
|
|
2949
5655
|
};
|
|
2950
5656
|
}
|
|
2951
5657
|
|
|
5658
|
+
// ---------- Mapping a failure back onto the caller's own script ----------
|
|
5659
|
+
// A failed run_jsx reported After Effects' line number with nothing to measure
|
|
5660
|
+
// it against, and that number does not count from where the caller thinks it
|
|
5661
|
+
// does: the same "line 22" pointed at two different statements in consecutive
|
|
5662
|
+
// calls (issue #46). Nothing rolls back, so an agent that cannot locate the
|
|
5663
|
+
// throw has to read the whole project back to find out where the script stopped.
|
|
5664
|
+
//
|
|
5665
|
+
// Two halves, and the second is the one that actually helps. The caller's line
|
|
5666
|
+
// 1 sits a *counted* distance down the evaluated source: with no libraries the
|
|
5667
|
+
// prefix carries no newline and the distance is zero, and with libraries it is
|
|
5668
|
+
// however many lines of library text were inlined ahead of the script.
|
|
5669
|
+
// __rjBuildSource measures the preamble it actually built rather than asserting
|
|
5670
|
+
// a constant, so the number can never drift from the string it describes —
|
|
5671
|
+
// a hand-written constant beside a string is exactly how the two came apart the
|
|
5672
|
+
// first time. Then the mapped line is reported with its TEXT, which an agent
|
|
5673
|
+
// can act on without trusting any numbering at all.
|
|
5674
|
+
//
|
|
5675
|
+
// The mapping refuses to guess: a line that falls outside the submitted source
|
|
5676
|
+
// is reported as unmappable, never clamped — unless it lands in a library that
|
|
5677
|
+
// was inlined ahead of it, which is a real file with real lines and is named.
|
|
5678
|
+
// A confident wrong line number is worse than none — it sends the reader to a
|
|
5679
|
+
// statement that did not fail.
|
|
5680
|
+
var __RJ_WRAP_PREFIX = "(function(){ ";
|
|
5681
|
+
// The closer sits on its own line. Appended to the caller's last line, a script
|
|
5682
|
+
// ending in a `//` comment commented out its own `})()` and failed to parse for
|
|
5683
|
+
// a reason nothing in the error mentioned.
|
|
5684
|
+
var __RJ_WRAP_SUFFIX = "\n})()";
|
|
5685
|
+
|
|
5686
|
+
// No regex literal: tests/unit/jsx-ternary.mjs strips string and comment
|
|
5687
|
+
// literals from these sources and does not understand regex literals, so one
|
|
5688
|
+
// here could desynchronise its scan of this file. (audio.jsx and footage.jsx do
|
|
5689
|
+
// carry a few; they survive only because their contents happen to pair up.)
|
|
5690
|
+
function __rjLines(s) {
|
|
5691
|
+
return String(s).split("\r\n").join("\n").split("\r").join("\n").split("\n");
|
|
5692
|
+
}
|
|
5693
|
+
|
|
5694
|
+
// The preamble of a call with no libraries: the bare wrapper opener, which
|
|
5695
|
+
// carries no newline, so zero. __rjBuildSource recomputes this per call once
|
|
5696
|
+
// libraries are in it; this is the default __rjSourceInfo falls back on when a
|
|
5697
|
+
// caller hands it no layout.
|
|
5698
|
+
var __RJ_PREAMBLE_LINES = __rjLines(__RJ_WRAP_PREFIX).length - 1;
|
|
5699
|
+
|
|
5700
|
+
function __rjTrim(s) {
|
|
5701
|
+
var t = String(s);
|
|
5702
|
+
var a = 0;
|
|
5703
|
+
var b = t.length;
|
|
5704
|
+
while (a < b && (t.charAt(a) === " " || t.charAt(a) === "\t")) a++;
|
|
5705
|
+
while (b > a && (t.charAt(b - 1) === " " || t.charAt(b - 1) === "\t")) b--;
|
|
5706
|
+
return t.substring(a, b);
|
|
5707
|
+
}
|
|
5708
|
+
|
|
5709
|
+
function __rjClip(s, max) {
|
|
5710
|
+
var t = String(s);
|
|
5711
|
+
if (t.length <= max) return t;
|
|
5712
|
+
return t.substring(0, max) + " ...";
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
// What After Effects reported, mapped onto the source the caller submitted.
|
|
5716
|
+
// Nothing here is fabricated: when the number does not land inside the script,
|
|
5717
|
+
// sourceLine stays null and the server says the number could not be mapped.
|
|
5718
|
+
//
|
|
5719
|
+
// `layout` is what __rjBuildSource returned for this call — how many lines of
|
|
5720
|
+
// preamble sit ahead of the caller's line 1, and where each inlined library
|
|
5721
|
+
// landed in the evaluated source. Omitted, it means the bare wrapper: no
|
|
5722
|
+
// libraries and a zero-line preamble, which is every call without `libraries`.
|
|
5723
|
+
function __rjSourceInfo(e, code, scriptPath, layout) {
|
|
5724
|
+
var lay = layout ? layout : { preambleLines: __RJ_PREAMBLE_LINES, segments: [] };
|
|
5725
|
+
var preamble = lay.preambleLines;
|
|
5726
|
+
var segs = lay.segments ? lay.segments : [];
|
|
5727
|
+
var lines = __rjLines(code);
|
|
5728
|
+
var info = {
|
|
5729
|
+
lineCount: lines.length,
|
|
5730
|
+
rawLine: null,
|
|
5731
|
+
sourceLine: null,
|
|
5732
|
+
sourceText: null
|
|
5733
|
+
};
|
|
5734
|
+
if (scriptPath) info.sourceName = String(scriptPath);
|
|
5735
|
+
|
|
5736
|
+
var raw = null;
|
|
5737
|
+
try {
|
|
5738
|
+
if (e && typeof e.line === "number" && isFinite(e.line)) raw = e.line;
|
|
5739
|
+
} catch (e1) {}
|
|
5740
|
+
info.rawLine = raw;
|
|
5741
|
+
|
|
5742
|
+
// Which line of the *evaluated source* — the wrapper, the inlined libraries
|
|
5743
|
+
// and the caller's script together — the failure sits on. After Effects' own
|
|
5744
|
+
// `line` already counts from there, which is what makes it usable at all.
|
|
5745
|
+
var wrapperLine = null;
|
|
5746
|
+
|
|
5747
|
+
// ExtendScript's Error also carries `source` (the text the error was raised
|
|
5748
|
+
// in) with `start`/`end` character offsets into it, and the documentation
|
|
5749
|
+
// presents those as the better answer, because they need no assumption about
|
|
5750
|
+
// what `line` counts from. On After Effects 2026 they are not offsets at all.
|
|
5751
|
+
//
|
|
5752
|
+
// Probed inside AE, catching from a four-line script that throws on line 4:
|
|
5753
|
+
//
|
|
5754
|
+
// eval("(function(){ var a=1;\nvar b=2;\nvar c=3;\nnope.boom();\n})()")
|
|
5755
|
+
// caught -> { "line": 4, "start": 0, "end": 0, "srcLen": 57 }
|
|
5756
|
+
//
|
|
5757
|
+
// `line` is already correct. `start` and `end` came back 0 on every error
|
|
5758
|
+
// measured, however far into the source it was raised. Read as an offset, a
|
|
5759
|
+
// zero start puts *every* failure on line 1 and prints line 1's text — which
|
|
5760
|
+
// is exactly what issue #46 still did after it was reported fixed, with the
|
|
5761
|
+
// true number demoted to the parenthetical afterwards.
|
|
5762
|
+
//
|
|
5763
|
+
// So the branch survives only for offsets that could actually be real: 0/0 is
|
|
5764
|
+
// After Effects declining to say, not After Effects pointing at the first
|
|
5765
|
+
// character. Do not restore this from the documentation.
|
|
5766
|
+
try {
|
|
5767
|
+
var src = null;
|
|
5768
|
+
if (e && typeof e.source === "string") src = e.source;
|
|
5769
|
+
var start = null;
|
|
5770
|
+
if (e && typeof e.start === "number" && isFinite(e.start)) start = e.start;
|
|
5771
|
+
var end = null;
|
|
5772
|
+
if (e && typeof e.end === "number" && isFinite(e.end)) end = e.end;
|
|
5773
|
+
var offsetsUsable = false;
|
|
5774
|
+
if (start !== null && start > 0) offsetsUsable = true;
|
|
5775
|
+
if (start === 0 && end !== null && end > 0) offsetsUsable = true;
|
|
5776
|
+
if (offsetsUsable && src !== null &&
|
|
5777
|
+
src.substring(0, __RJ_WRAP_PREFIX.length) === __RJ_WRAP_PREFIX) {
|
|
5778
|
+
wrapperLine = __rjLines(src.substring(0, start)).length;
|
|
5779
|
+
}
|
|
5780
|
+
} catch (e2) {}
|
|
5781
|
+
if (wrapperLine === null) wrapperLine = raw;
|
|
5782
|
+
if (wrapperLine === null) return info;
|
|
5783
|
+
|
|
5784
|
+
var mapped = wrapperLine - preamble;
|
|
5785
|
+
if (mapped >= 1 && mapped <= lines.length) {
|
|
5786
|
+
info.sourceLine = mapped;
|
|
5787
|
+
info.sourceText = __rjClip(__rjTrim(lines[mapped - 1]), 200);
|
|
5788
|
+
return info;
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
// Not the caller's script. A library inlined ahead of it is a real file with
|
|
5792
|
+
// real lines, so name it and point into it rather than reporting the number
|
|
5793
|
+
// as unmappable: that file is where the reader has to look, and naming the
|
|
5794
|
+
// script instead would send them to a line that did not fail.
|
|
5795
|
+
for (var s = 0; s < segs.length; s++) {
|
|
5796
|
+
var seg = segs[s];
|
|
5797
|
+
var count = seg.lines.length - 1;
|
|
5798
|
+
var within = wrapperLine - seg.firstLine + 1;
|
|
5799
|
+
if (within >= 1 && within <= count) {
|
|
5800
|
+
info.sourceName = seg.path;
|
|
5801
|
+
info.lineCount = count;
|
|
5802
|
+
info.sourceLine = within;
|
|
5803
|
+
info.sourceText = __rjClip(__rjTrim(seg.lines[within - 1]), 200);
|
|
5804
|
+
return info;
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
return info;
|
|
5808
|
+
}
|
|
5809
|
+
|
|
5810
|
+
// Rethrow with the mapping attached. dispatch()'s catch copies whatever sits on
|
|
5811
|
+
// `aeDetail` onto the error result; the panel forwards the fields it names and
|
|
5812
|
+
// the server prints the line's text. A plain object rather than an Error
|
|
5813
|
+
// because ExtendScript will not reliably let us write `line` on one, and
|
|
5814
|
+
// __mkError only ever reads message/stack/line.
|
|
5815
|
+
function __rjThrowWithSource(e, code, scriptPath, layout) {
|
|
5816
|
+
var info = __rjSourceInfo(e, code, scriptPath, layout);
|
|
5817
|
+
var msg = "";
|
|
5818
|
+
try { if (e && e.message) msg = String(e.message); } catch (e1) {}
|
|
5819
|
+
if (!msg) {
|
|
5820
|
+
try { msg = String(e); } catch (e2) { msg = "ExtendScript error"; }
|
|
5821
|
+
}
|
|
5822
|
+
var stack = "";
|
|
5823
|
+
try { if (e && e.stack) stack = String(e.stack); } catch (e3) {}
|
|
5824
|
+
throw { message: msg, stack: stack, line: info.rawLine, aeDetail: info };
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5827
|
+
// ---------- Helper libraries ----------
|
|
5828
|
+
// A library's source is inlined into the SAME eval as the caller's script,
|
|
5829
|
+
// ahead of it. That is not the obvious design, and it is the only one that
|
|
5830
|
+
// works.
|
|
5831
|
+
//
|
|
5832
|
+
// The first version used $.evalFile, on the documented premise that it
|
|
5833
|
+
// evaluates at global scope — load once, call for the rest of the After Effects
|
|
5834
|
+
// session (issue #53). Probed inside AE 2026, calling $.evalFile from the body
|
|
5835
|
+
// of an eval'd script, on a library declaring `function rig2()` and
|
|
5836
|
+
// `var RIGVAR = 3`:
|
|
5837
|
+
//
|
|
5838
|
+
// {"exists":true, "typeofRig2_local":"function", "typeofRIGVAR_local":"number",
|
|
5839
|
+
// "globalRig2":"undefined", "viaGlobal":null}
|
|
5840
|
+
// // and on the next run_jsx call, in the same AE session:
|
|
5841
|
+
// {"typeofRig2":"undefined", "viaGlobal":"undefined"}
|
|
5842
|
+
//
|
|
5843
|
+
// $.evalFile evaluates into the *calling function's* scope, exactly as eval
|
|
5844
|
+
// does. Everything a library defined therefore lived inside the loader and was
|
|
5845
|
+
// gone before the wrapper ran, so `libraries` answered "Function rig is
|
|
5846
|
+
// undefined" every time, for every library, in every session. The per-session
|
|
5847
|
+
// cache keyed on a content hash has gone with it: nothing was ever left loaded
|
|
5848
|
+
// to reuse, so the only work it ever skipped was work whose result had already
|
|
5849
|
+
// been discarded.
|
|
5850
|
+
//
|
|
5851
|
+
// One eval means one scope: a library's `function helper(){}` is a declaration
|
|
5852
|
+
// in the same function body as the script, so the script can call it. Two
|
|
5853
|
+
// consequences, both handled here rather than left to surprise someone:
|
|
5854
|
+
//
|
|
5855
|
+
// * A library is re-evaluated on every call. That is the price of the scoping
|
|
5856
|
+
// After Effects actually has. Keep libraries to declarations, not to work.
|
|
5857
|
+
// * The library text shifts the caller's line 1 down, which is precisely the
|
|
5858
|
+
// failure issue #46 was about. __rjBuildSource *counts* the preamble it
|
|
5859
|
+
// built rather than asserting a constant, and __rjSourceInfo subtracts that
|
|
5860
|
+
// count — so a caller's line 1 is line 1 of their own script whether they
|
|
5861
|
+
// passed libraries or not.
|
|
5862
|
+
|
|
5863
|
+
// No regex literal, for the same reason __rjLines has none.
|
|
5864
|
+
function __rjIsBlank(s) {
|
|
5865
|
+
var t = String(s);
|
|
5866
|
+
for (var i = 0; i < t.length; i++) {
|
|
5867
|
+
var c = t.charAt(i);
|
|
5868
|
+
if (c !== " " && c !== "\t" && c !== "\n" && c !== "\r") return false;
|
|
5869
|
+
}
|
|
5870
|
+
return true;
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
// Every library ends in a newline before the next one — or the caller's script
|
|
5874
|
+
// — follows it. Appended directly, a library whose last line is a `//` comment
|
|
5875
|
+
// would comment out whatever came after it: the same trap __RJ_WRAP_SUFFIX
|
|
5876
|
+
// exists for at the other end of the wrapper.
|
|
5877
|
+
function __rjEndWithNewline(s) {
|
|
5878
|
+
var t = String(s);
|
|
5879
|
+
if (t.length === 0) return "\n";
|
|
5880
|
+
var last = t.charAt(t.length - 1);
|
|
5881
|
+
if (last === "\n" || last === "\r") return t;
|
|
5882
|
+
return t + "\n";
|
|
5883
|
+
}
|
|
5884
|
+
|
|
5885
|
+
// The server reads library files and sends {path, text}, exactly as it does for
|
|
5886
|
+
// scriptPath — one place reads files, and its errors can name the path it was
|
|
5887
|
+
// given. Arriving with a path and no text means the call did not come through
|
|
5888
|
+
// the run_jsx tool: a run_batch step, whose args are never validated, or a
|
|
5889
|
+
// hand-rolled POST /op.
|
|
5890
|
+
function __rjLibrarySource(lib) {
|
|
5891
|
+
var p = null;
|
|
5892
|
+
var text = null;
|
|
5893
|
+
if (typeof lib === "string") {
|
|
5894
|
+
p = lib;
|
|
5895
|
+
} else if (lib) {
|
|
5896
|
+
p = lib.path;
|
|
5897
|
+
if (typeof lib.text === "string") text = lib.text;
|
|
5898
|
+
}
|
|
5899
|
+
if (!p) throw new Error("run_jsx: a libraries entry has no path.");
|
|
5900
|
+
if (text === null) {
|
|
5901
|
+
throw new Error(
|
|
5902
|
+
"run_jsx library \"" + p + "\" arrived with no source text. The server reads library files " +
|
|
5903
|
+
"and substitutes their contents, so this call did not go through the run_jsx tool — " +
|
|
5904
|
+
"run_batch steps and direct /op posts must pass {path, text} themselves."
|
|
5905
|
+
);
|
|
5906
|
+
}
|
|
5907
|
+
if (__rjIsBlank(text)) throw new Error("run_jsx library is empty: " + p);
|
|
5908
|
+
return { path: String(p), text: __rjEndWithNewline(text) };
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5911
|
+
// Parse each library on its own before any of it reaches the shared eval.
|
|
5912
|
+
// Inlining means a library that does not parse takes the whole wrapper with it,
|
|
5913
|
+
// and the line After Effects reports for a syntax error is wherever its parser
|
|
5914
|
+
// gave up — frequently inside the caller's script, which would blame the wrong
|
|
5915
|
+
// file for someone else's missing brace. An uncalled function expression forces
|
|
5916
|
+
// a full parse of exactly this library and nothing else, so the failure names
|
|
5917
|
+
// it and counts from its own line 1.
|
|
5918
|
+
function __rjCheckLibraryParses(lib) {
|
|
5919
|
+
try {
|
|
5920
|
+
eval("(function(){ " + lib.text + "})");
|
|
5921
|
+
} catch (e) {
|
|
5922
|
+
var m = "";
|
|
5923
|
+
try { m = (e && e.message) ? String(e.message) : String(e); } catch (e1) { m = "unknown error"; }
|
|
5924
|
+
var ln = null;
|
|
5925
|
+
try { if (e && typeof e.line === "number" && isFinite(e.line)) ln = e.line; } catch (e2) {}
|
|
5926
|
+
var libLines = __rjLines(lib.text);
|
|
5927
|
+
var count = libLines.length - 1;
|
|
5928
|
+
var detail = {
|
|
5929
|
+
lineCount: count,
|
|
5930
|
+
rawLine: ln,
|
|
5931
|
+
sourceLine: null,
|
|
5932
|
+
sourceText: null,
|
|
5933
|
+
sourceName: lib.path
|
|
5934
|
+
};
|
|
5935
|
+
if (ln !== null && ln >= 1 && ln <= count) {
|
|
5936
|
+
detail.sourceLine = ln;
|
|
5937
|
+
detail.sourceText = __rjClip(__rjTrim(libLines[ln - 1]), 200);
|
|
5938
|
+
}
|
|
5939
|
+
throw {
|
|
5940
|
+
message: "run_jsx library failed to parse: " + lib.path + " — " + m,
|
|
5941
|
+
stack: "",
|
|
5942
|
+
line: ln,
|
|
5943
|
+
aeDetail: detail
|
|
5944
|
+
};
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
|
|
5948
|
+
// The evaluated source for one call, plus the map that turns any line of it
|
|
5949
|
+
// back into a line of a file the caller knows about.
|
|
5950
|
+
//
|
|
5951
|
+
// `preambleLines` is COUNTED from the text that precedes the script rather than
|
|
5952
|
+
// assumed. That is the invariant issue #46 turned on, and it now has to hold
|
|
5953
|
+
// for a preamble whose length changes from call to call: it can never drift
|
|
5954
|
+
// from the string it describes, however many libraries there are.
|
|
5955
|
+
function __rjBuildSource(code, libraries) {
|
|
5956
|
+
var prefix = __RJ_WRAP_PREFIX;
|
|
5957
|
+
var segments = [];
|
|
5958
|
+
if (libraries && libraries.length) {
|
|
5959
|
+
for (var i = 0; i < libraries.length; i++) {
|
|
5960
|
+
var lib = __rjLibrarySource(libraries[i]);
|
|
5961
|
+
__rjCheckLibraryParses(lib);
|
|
5962
|
+
segments.push({
|
|
5963
|
+
path: lib.path,
|
|
5964
|
+
// The wrapper opener carries no newline, so the first library's line 1
|
|
5965
|
+
// shares line 1 of the evaluated source with it.
|
|
5966
|
+
firstLine: __rjLines(prefix).length,
|
|
5967
|
+
lines: __rjLines(lib.text)
|
|
5968
|
+
});
|
|
5969
|
+
prefix = prefix + lib.text;
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
return {
|
|
5973
|
+
wrapper: prefix + code + __RJ_WRAP_SUFFIX,
|
|
5974
|
+
preambleLines: __rjLines(prefix).length - 1,
|
|
5975
|
+
segments: segments
|
|
5976
|
+
};
|
|
5977
|
+
}
|
|
5978
|
+
|
|
2952
5979
|
// undoGroup:false is a per-call opt-out, read by dispatch() through the
|
|
2953
5980
|
// predicate form of __meta.noUndo. It exists because AE refuses copyToComp for
|
|
2954
5981
|
// a layer with a parent or a linked expression while an undo group is open,
|
|
2955
5982
|
// which is exactly the layer worth copying (issue #30).
|
|
2956
5983
|
OPS.run_jsx = noUndoWhen(function (args) { return args.undoGroup === false; }, function (args) {
|
|
2957
5984
|
var code = args.code || "";
|
|
2958
|
-
//
|
|
2959
|
-
|
|
5985
|
+
// scriptPath is resolved to code by the *server* before the payload is built.
|
|
5986
|
+
// Arriving here with a path and no code means it came in some other way — a
|
|
5987
|
+
// run_batch step, whose args are never validated, or a hand-rolled POST /op.
|
|
5988
|
+
// Running an empty script would return the "completed with no return value"
|
|
5989
|
+
// envelope, which is a success result for a file nobody read.
|
|
5990
|
+
if (!code && args.scriptPath) {
|
|
5991
|
+
throw new Error(
|
|
5992
|
+
"run_jsx received scriptPath \"" + args.scriptPath + "\" with no code. The server reads that " +
|
|
5993
|
+
"file and substitutes it, so this path did not go through the run_jsx tool — run_batch steps " +
|
|
5994
|
+
"and direct /op posts must pass `code` themselves."
|
|
5995
|
+
);
|
|
5996
|
+
}
|
|
5997
|
+
if (!code) throw new Error("run_jsx needs `code` — an empty script would report success for nothing.");
|
|
5998
|
+
// We wrap in a function so `return` works, with any libraries inlined ahead
|
|
5999
|
+
// of the script so they share its scope. Assembled and parse-checked before
|
|
6000
|
+
// anything runs, and outside the try below: a library that fails to parse is
|
|
6001
|
+
// not a line in the caller's script and must not be reported as one.
|
|
6002
|
+
var built = __rjBuildSource(code, args.libraries);
|
|
2960
6003
|
// Which undo step to look for in AE if the script has to be backed out. The
|
|
2961
6004
|
// name mirrors dispatch()'s default ("AE MCP: " + op); false means the caller
|
|
2962
6005
|
// asked for no group and the changes landed as whatever steps AE recorded.
|
|
2963
6006
|
var undoGroupName = (args.undoGroup === false) ? false : "AE MCP: run_jsx";
|
|
2964
|
-
|
|
6007
|
+
// diff:true fingerprints the comp before and after, inside this one call —
|
|
6008
|
+
// see snapshot.jsx. Null unless asked for, so the ordinary path is untouched.
|
|
6009
|
+
var __d = __diffStart(args, null);
|
|
6010
|
+
var __value;
|
|
6011
|
+
try {
|
|
6012
|
+
__value = eval(built.wrapper);
|
|
6013
|
+
} catch (e) {
|
|
6014
|
+
// Annotate before mapping the line: __rjThrowWithSource reads e.message,
|
|
6015
|
+
// so the diff note has to be on it by then, and the source mapping is what
|
|
6016
|
+
// makes the reported line the caller's own (#46).
|
|
6017
|
+
__diffAnnotateError(e, __d);
|
|
6018
|
+
__rjThrowWithSource(e, code, args.scriptPath, built);
|
|
6019
|
+
}
|
|
6020
|
+
var __out = __rjResult(__value, undoGroupName);
|
|
6021
|
+
if (__d) return __rjWithDiff(__out, __diffFinish(__d), undoGroupName);
|
|
6022
|
+
return __out;
|
|
2965
6023
|
});
|