@pinepaper.studio/mcp-server 1.6.14 → 1.6.16
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.de.md +6 -1
- package/README.es.md +6 -1
- package/README.fr.md +6 -1
- package/README.hi.md +6 -1
- package/README.ja.md +6 -1
- package/README.ko.md +6 -1
- package/README.md +130 -6
- package/README.pt-BR.md +6 -1
- package/README.zh-CN.md +6 -1
- package/assets/brand/pinepaper-lockup-horizontal-white.svg +1 -0
- package/assets/brand/pinepaper-lockup-horizontal.svg +1 -0
- package/assets/edit-whenever.svg +1 -1
- package/dist/browser/puppeteer-controller.js +2 -2
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +107 -9
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +1 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +59 -12
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/camera.d.ts.map +1 -1
- package/dist/tools/handlers/camera.js +11 -0
- package/dist/tools/handlers/camera.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +324 -0
- package/dist/tools/handlers/generate.d.ts.map +1 -0
- package/dist/tools/handlers/generate.js +264 -0
- package/dist/tools/handlers/generate.js.map +1 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +39 -4
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +3 -0
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.d.ts.map +1 -1
- package/dist/tools/shape-params.js +20 -3
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +3 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +15 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +339 -29
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +532 -82
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +26 -10
- package/dist/types/schemas.js.map +1 -1
- package/dist/vendor/design/design-style.js +1 -1
- package/icon.png +0 -0
- package/icon.svg +1 -0
- package/manifest.json +15 -2
- package/package.json +3 -1
|
@@ -263,8 +263,16 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
263
263
|
// the handful this emitter consumes itself are added. Reported back, not
|
|
264
264
|
// refused: the item is still what the caller asked for in every other way,
|
|
265
265
|
// and a refusal would cost a re-issue over one typo.
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
//
|
|
267
|
+
// The generated allowlist says what create() READS, for any type; the text
|
|
268
|
+
// keys it reads apply only to text (the engine gates them on PointText). So
|
|
269
|
+
// on another type they are reported whatever the list says. `direction` is
|
|
270
|
+
// not among them: an arc reads it as its sweep.
|
|
271
|
+
const textOnly = (k) => TEXT_STYLE_KEYS.includes(k) || k === 'fit' || k === 'textDirection' || k === 'dir'
|
|
272
|
+
|| TABULAR_KEYS.includes(k) || k === 'strokePosition';
|
|
273
|
+
const ignored = Object.keys(properties).filter((k) => itemType === 'text'
|
|
274
|
+
? !CREATE_KNOWN_KEYS.has(k) && !textOnly(k) && !DIRECTION_KEYS.includes(k)
|
|
275
|
+
: !CREATE_KNOWN_KEYS.has(k) || textOnly(k));
|
|
268
276
|
// Build the code
|
|
269
277
|
let code = `
|
|
270
278
|
// Create ${itemType} item
|
|
@@ -909,8 +917,15 @@ if (_flagged && _flagged.item && _flagged.item.data) Object.assign(_flagged.item
|
|
|
909
917
|
+ modIgnored.filter((k) => MODIFY_DID_YOU_MEAN[k]).map((k) => ` Did you mean ${MODIFY_DID_YOU_MEAN[k]} (not ${k})?`).join(''))}`;
|
|
910
918
|
code += `
|
|
911
919
|
app.historyManager.saveState();
|
|
920
|
+
${properties.content !== undefined ? `
|
|
921
|
+
// CONTENT UNDER A CONTENT TRACK (round 10, 10.13): the keyframes set the text
|
|
922
|
+
// on every frame, so a direct edit is overwritten at once — it answered
|
|
923
|
+
// success and changed nothing visible.
|
|
924
|
+
const __ctItem = app.getItemById('${itemId}');
|
|
925
|
+
const __ctKeys = (__ctItem && __ctItem.data && Array.isArray(__ctItem.data.keyframes))
|
|
926
|
+
? __ctItem.data.keyframes.filter(function(k) { return k && k.properties && Object.prototype.hasOwnProperty.call(k.properties, 'content'); }).length : 0;` : ''}
|
|
912
927
|
${modDirection ? emitDirectionCheck(`app.getItemById('${itemId}')`, modDirection) : ''}${modTabular ? emitTabularCheck(`app.getItemById('${itemId}')`) : ''}${modStrokePos ? emitStrokePositionCheck(`app.getItemById('${itemId}')`, modStrokePos) : ''}
|
|
913
|
-
return { success: true, itemId: '${itemId}'${modFit ? ', textFit: __textFit' : ''}${modDirection ? ', direction: __direction' : ''}${modTabular ? ', tabularFigures: __tabular' : ''}${modStrokePos ? ', strokePosition: __strokePos' : ''}${affine ? `, affine: ${JSON.stringify({ skewX, skewY, matrix, note: 'applied to the current shape: calling again compounds it. To undo, apply the inverse (negative skew, or the inverse matrix).' })}` : ''}${modLifetime ? ', lifetime: __lifetime' : ''}${modFont ? ', ...(__font ? { font: __font } : {})' : ''}${modWarning} };`;
|
|
928
|
+
return { success: true, itemId: '${itemId}'${properties.content !== undefined ? `, ...(__ctKeys ? { contentTrackWarning: 'this text\\'s content is keyframed (' + __ctKeys + ' key(s)), so the track sets it on every frame and this edit does not show. Change the content keys with pinepaper_keyframe_animate (append: true replaces same-time keys), or animate another property.' } : {})` : ''}${modFit ? ', textFit: __textFit' : ''}${modDirection ? ', direction: __direction' : ''}${modTabular ? ', tabularFigures: __tabular' : ''}${modStrokePos ? ', strokePosition: __strokePos' : ''}${affine ? `, affine: ${JSON.stringify({ skewX, skewY, matrix, note: 'applied to the current shape: calling again compounds it. To undo, apply the inverse (negative skew, or the inverse matrix).' })}` : ''}${modLifetime ? ', lifetime: __lifetime' : ''}${modFont ? ', ...(__font ? { font: __font } : {})' : ''}${modWarning} };`;
|
|
914
929
|
// Wrapped, because the body now RETURNS early on a miss rather than falling
|
|
915
930
|
// through to an unconditional success. Async only when a font must load
|
|
916
931
|
// before modifyItem re-measures the text.
|
|
@@ -1227,7 +1242,7 @@ if (!_target) {
|
|
|
1227
1242
|
return { success: false, error: ${JSON.stringify(itemId)} + ' is not on the canvas, so ' + ${JSON.stringify(what)} + ' would have done nothing. Check the id with pinepaper_get_items — create returns the registry id, not the Paper id.' };
|
|
1228
1243
|
}`.trim();
|
|
1229
1244
|
}
|
|
1230
|
-
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits) {
|
|
1245
|
+
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false) {
|
|
1231
1246
|
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
1232
1247
|
const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
|
|
1233
1248
|
const opts = { duration: calculatedDuration, loop };
|
|
@@ -1239,6 +1254,10 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1239
1254
|
opts.clipOutPoint = clipOutPoint;
|
|
1240
1255
|
if (timeUnits !== undefined)
|
|
1241
1256
|
opts.timeUnits = timeUnits;
|
|
1257
|
+
// addAnimation SETS the item's track; mode 'merge' adds to it (FxTool
|
|
1258
|
+
// 2475333b). An engine without merge ignores the option and replaces.
|
|
1259
|
+
if (append)
|
|
1260
|
+
opts.mode = 'merge';
|
|
1242
1261
|
// A FADE ON AN AUDIO ITEM WAS ACCEPTED AND NEVER HEARD. addAnimation keyframes
|
|
1243
1262
|
// the audio item's sentinel, and the mix reads one static audioGain per clip,
|
|
1244
1263
|
// so {audioGain: 0→1} answered success, duration 15, and exported at a flat
|
|
@@ -1277,7 +1296,10 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1277
1296
|
const gaps = keyframes
|
|
1278
1297
|
.map((k, i) => ({ time: k.time, missing: allProps.filter((p) => !propSets[i].includes(p)) }))
|
|
1279
1298
|
.filter((g) => g.missing.length);
|
|
1280
|
-
|
|
1299
|
+
// With append the gaps that matter are the MERGED track's (an earlier key
|
|
1300
|
+
// with no scale beside a new key with one), so that check runs in the page
|
|
1301
|
+
// on the result instead (below).
|
|
1302
|
+
const gapNote = !append && gaps.length && keyframes.length > 1
|
|
1281
1303
|
? `, propertyGaps: ${JSON.stringify(gaps.slice(0, 10))}, gapWarning: ${JSON.stringify(`${gaps.length} keyframe(s) leave out a property other keys animate (${[...new Set(gaps.flatMap((g) => g.missing))].join(', ')}). The engine can break that property's track at such a key; repeat the value on every key — hold it by repeating the previous value.`)}`
|
|
1282
1304
|
: '';
|
|
1283
1305
|
const first = [...keyframes].sort((a, b) => a.time - b.time)[0];
|
|
@@ -1294,8 +1316,25 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1294
1316
|
// Apply keyframe animation to ${itemId}
|
|
1295
1317
|
(function() {
|
|
1296
1318
|
${requireItem(itemId, 'the animation')}${audioLevelGuard}
|
|
1319
|
+
// The track as it was, so the result can say what happened to it: merged
|
|
1320
|
+
// (every earlier key still there) or replaced. That is the evidence of
|
|
1321
|
+
// merge support — an older engine takes the option and replaces anyway.
|
|
1322
|
+
const __ki = _target && (_target.item || _target);
|
|
1323
|
+
const __prev = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];
|
|
1297
1324
|
app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
|
|
1298
|
-
|
|
1325
|
+
const __now = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];
|
|
1326
|
+
const __kept = __prev.every(function(t) { return __now.indexOf(t) >= 0; });${append ? `
|
|
1327
|
+
// Property gaps across the merged track (the same check a single call makes
|
|
1328
|
+
// on its own keys at generation time).
|
|
1329
|
+
const __all = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes : [];
|
|
1330
|
+
const __props = __all.reduce(function(acc, k) { Object.keys((k && k.properties) || {}).forEach(function(p) { if (acc.indexOf(p) < 0) acc.push(p); }); return acc; }, []);
|
|
1331
|
+
const __gaps = __all.length > 1 ? __all.map(function(k) { const have = Object.keys((k && k.properties) || {}); return { time: k && k.time, missing: __props.filter(function(p) { return have.indexOf(p) < 0; }) }; }).filter(function(g) { return g.missing.length; }) : [];` : ''}
|
|
1332
|
+
const __track = !__prev.length ? null
|
|
1333
|
+
: ${append ? `(__kept
|
|
1334
|
+
? { appended: true, previousKeys: __prev.length, keys: __now.length }
|
|
1335
|
+
: { appended: false, replaced: true, previousKeys: __prev.length, warning: 'append was asked for, but this studio replaced the existing track (' + __prev.length + ' key(s)) instead of adding to it: it has no merge mode. Put every key in one keyframe_animate call.' })`
|
|
1336
|
+
: `{ replaced: true, previousKeys: __prev.length, note: 'the item already had a keyframe track (' + __prev.length + ' key(s)); this call replaced it. To add keys to it instead, pass append: true.' }`};
|
|
1337
|
+
return { success: true, itemId: '${itemId}', duration: ${calculatedDuration}, loop: ${loop}${append ? ', append: true' : ''}, ...(__track ? { track: __track } : {})${append ? `, ...(__gaps.length ? { propertyGaps: __gaps.slice(0, 10), gapWarning: __gaps.length + ' key(s) of the merged track leave out a property other keys animate (' + __gaps.reduce(function(a, g) { g.missing.forEach(function(p) { if (a.indexOf(p) < 0) a.push(p); }); return a; }, []).join(', ') + '). The engine can break that property at such a key; give each key the full set — hold a value by repeating it.' } : {})` : ''}${timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : ''}${clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : ''}${clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : ''}${easingNote}${loopNote}${gapNote} };
|
|
1299
1338
|
})();
|
|
1300
1339
|
`.trim();
|
|
1301
1340
|
}
|
|
@@ -2275,7 +2314,7 @@ export class PinePaperCodeGenerator {
|
|
|
2275
2314
|
*/
|
|
2276
2315
|
generateKeyframeAnimate(input) {
|
|
2277
2316
|
const validated = KeyframeAnimateInputSchema.parse(input);
|
|
2278
|
-
return generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint, validated.timeUnits);
|
|
2317
|
+
return generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint, validated.timeUnits, validated.append);
|
|
2279
2318
|
}
|
|
2280
2319
|
/**
|
|
2281
2320
|
* Generate code for importing / retargeting a motion-capture clip
|
|
@@ -3937,7 +3976,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3937
3976
|
}
|
|
3938
3977
|
generateAgentExport(input) {
|
|
3939
3978
|
const validated = AgentExportInputSchema.parse(input);
|
|
3940
|
-
const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, ad: adOpts } = validated;
|
|
3979
|
+
const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, alphaQuantizer, deterministic, ad: adOpts } = validated;
|
|
3941
3980
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
3942
3981
|
const videoEncodeOpts = {};
|
|
3943
3982
|
if (broadcast)
|
|
@@ -3955,6 +3994,17 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3955
3994
|
videoEncodeOpts.minBitrate = minBitrate;
|
|
3956
3995
|
if (bitrateMode !== undefined)
|
|
3957
3996
|
videoEncodeOpts.bitrateMode = bitrateMode;
|
|
3997
|
+
// Byte-identical repeats (FxTool 3b335567): the container timestamps are
|
|
3998
|
+
// the only bytes that varied, and this pins them.
|
|
3999
|
+
if (deterministic)
|
|
4000
|
+
videoEncodeOpts.deterministic = true;
|
|
4001
|
+
// A transparent WebM (FxTool 1833b397) rides the store route too.
|
|
4002
|
+
const webmAlpha = format === 'webm' && transparent === true;
|
|
4003
|
+
if (webmAlpha) {
|
|
4004
|
+
videoEncodeOpts.transparent = true;
|
|
4005
|
+
if (alphaQuantizer !== undefined)
|
|
4006
|
+
videoEncodeOpts.alphaQuantizer = alphaQuantizer;
|
|
4007
|
+
}
|
|
3958
4008
|
const encodeOptsJs = Object.keys(videoEncodeOpts).length ? `, ...${JSON.stringify(videoEncodeOpts)}` : '';
|
|
3959
4009
|
const qualityLevel = quality || 'standard';
|
|
3960
4010
|
const videoDuration = duration ?? 5;
|
|
@@ -4224,7 +4274,7 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4224
4274
|
// generator or pattern backdrop never trips it.
|
|
4225
4275
|
function alphaLoss(fmt) {
|
|
4226
4276
|
try {
|
|
4227
|
-
if (['mp4', 'webm', '
|
|
4277
|
+
if (['mp4', 'jpg'${format === 'webm' && transparent === true ? '' : ", 'webm'"}${format === 'apng' && transparent === false ? ", 'apng'" : ''}].indexOf(fmt) === -1) return [];
|
|
4228
4278
|
const bg = app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor;
|
|
4229
4279
|
const hasColor = !!bg && !/^(transparent|rgba\\([^)]*,\\s*0\\))$/i.test(bg);
|
|
4230
4280
|
const hasBgItems = !!(app.patternGroup && app.patternGroup.children && app.patternGroup.children.length);
|
|
@@ -4443,9 +4493,6 @@ ${stillTime !== undefined ? `
|
|
|
4443
4493
|
// this render, a top-level item that reaches in but is centred outside —
|
|
4444
4494
|
// a neighbouring card's overflowing headline. Older studios ignore the
|
|
4445
4495
|
// fourth argument, and leave lastRegionExcluded unset.
|
|
4446
|
-
// @engine-surface-exempt lastRegionExcluded — a report property, not a
|
|
4447
|
-
// method, so it cannot be typeof-probed; read only through Array.isArray.
|
|
4448
|
-
// Branch-only until 78ec5916 reaches origin/main.
|
|
4449
4496
|
if ('lastRegionExcluded' in app) app.lastRegionExcluded = null;
|
|
4450
4497
|
const regionUrl = app.renderRegionToDataURL(new paper.Rectangle(${region.x}, ${region.y}, ${region.width}, ${region.height}), rw, rh${region.excludeForeign === false ? '' : ', { excludeForeign: true }'});
|
|
4451
4498
|
const __excluded = Array.isArray(app.lastRegionExcluded) ? app.lastRegionExcluded.filter(function(id) { return id; }) : null;
|
|
@@ -4808,7 +4855,7 @@ ${stillTime !== undefined ? `
|
|
|
4808
4855
|
// the per-page reports are summed into result.pdf.
|
|
4809
4856
|
const __wantLayer = ${pdfOpts.searchableText !== false};
|
|
4810
4857
|
const __layer = __wantLayer && typeof app.exportEngine.addTextLayer === 'function';
|
|
4811
|
-
const __tl = { written: 0, skipped: 0, reasons: [] };
|
|
4858
|
+
const __tl = { written: 0, skipped: 0, reasons: [], fonts: null };
|
|
4812
4859
|
try {
|
|
4813
4860
|
for (const spec of pageSpecs) {
|
|
4814
4861
|
const id = spec.id;
|
|
@@ -4820,8 +4867,29 @@ ${stillTime !== undefined ? `
|
|
|
4820
4867
|
const orient = wMM > hMM ? 'landscape' : 'portrait';
|
|
4821
4868
|
const png = app.captureFrameDataURL(Math.max(1, targetDpi / dpi));
|
|
4822
4869
|
const jpg = await toJpeg(png);
|
|
4870
|
+
const __first = !doc;
|
|
4823
4871
|
if (!doc) doc = new lib.jsPDF({ orientation: orient, unit: 'mm', format: [wMM, hMM] });
|
|
4824
4872
|
else doc.addPage([wMM, hMM], orient);
|
|
4873
|
+
// The scene's fonts (FxTool dbe8441d). Once per document on an
|
|
4874
|
+
// engine whose repeat call re-registers over page 1's face; per
|
|
4875
|
+
// page (this page's scene) where the engine caches faces on the
|
|
4876
|
+
// document — it leaves doc.__ppFontCache after the first call
|
|
4877
|
+
// (3b9499f2), which is the evidence, not a version guess.
|
|
4878
|
+
if (__layer && typeof app.exportEngine.embedTextFonts === 'function' && (__first || doc.__ppFontCache)) {
|
|
4879
|
+
try {
|
|
4880
|
+
const fr = await app.exportEngine.embedTextFonts(doc);
|
|
4881
|
+
if (!__tl.fonts) __tl.fonts = fr;
|
|
4882
|
+
else if (fr) {
|
|
4883
|
+
const seen = {};
|
|
4884
|
+
__tl.fonts.families = (__tl.fonts.families || []).concat(fr.families || []).filter(function(f) {
|
|
4885
|
+
const k = f && (f.family + '|' + f.weight + '|' + f.italic);
|
|
4886
|
+
if (!k || seen[k]) return false; seen[k] = true; return true;
|
|
4887
|
+
});
|
|
4888
|
+
if (typeof fr.embedded === 'number') __tl.fonts.embedded = fr.embedded; // cumulative for the document
|
|
4889
|
+
if (typeof fr.hiddenTextItems === 'number') __tl.fonts.hiddenTextItems = (__tl.fonts.hiddenTextItems || 0) + fr.hiddenTextItems;
|
|
4890
|
+
}
|
|
4891
|
+
} catch (e) { if (!__tl.fonts) __tl.fonts = { embedded: 0, reason: e && e.message }; }
|
|
4892
|
+
}
|
|
4825
4893
|
if (jpg) doc.addImage(jpg, 'JPEG', 0, 0, wMM, hMM); else doc.addImage(png, 'PNG', 0, 0, wMM, hMM);
|
|
4826
4894
|
if (__layer) {
|
|
4827
4895
|
try {
|
|
@@ -4842,6 +4910,7 @@ ${stillTime !== undefined ? `
|
|
|
4842
4910
|
pdf: !__wantLayer ? { searchableText: false, reason: 'off' }
|
|
4843
4911
|
: !__layer ? { searchableText: false, reason: 'this studio adds no text layer to multi-page PDFs' }
|
|
4844
4912
|
: Object.assign({ searchableText: __tl.written > 0 || __tl.reasons.length < pagesOut.length, linesWritten: __tl.written, linesSkipped: __tl.skipped },
|
|
4913
|
+
__tl.fonts ? { fonts: __tl.fonts } : {},
|
|
4845
4914
|
__tl.reasons.length ? { pagesWithoutLayer: __tl.reasons } : {},
|
|
4846
4915
|
__tl.written === 0 && __tl.reasons.length === pagesOut.length ? { reason: __tl.reasons.join('; ') } : {}) };
|
|
4847
4916
|
if (!sized) result.warning = 'every page used the current canvas size: saved scenes do not record theirs. To give a page its own size, pass pages as [{sceneId, width, height}, …].';
|
|
@@ -4934,6 +5003,18 @@ ${stillTime !== undefined ? `
|
|
|
4934
5003
|
|
|
4935
5004
|
if (result && result.success) Object.assign(result, fidelity(format));
|
|
4936
5005
|
if (result && result.success && __fit) result.platformFit = __fit;
|
|
5006
|
+
// A LANDSCAPE CANVAS FOR A PORTRAIT PLATFORM (or the reverse) is not an
|
|
5007
|
+
// aspect nuance but the wrong deliverable: tiktok on a 16:9 canvas came out
|
|
5008
|
+
// 1920x1080 with only a note (round 10, 10.7). Said where a caller looks.
|
|
5009
|
+
if (result && result.success && __fit) {
|
|
5010
|
+
const __pw = __fit.preset.split('x').map(Number), __uw = __fit.used.split('x').map(Number);
|
|
5011
|
+
if ((__pw[0] > __pw[1]) !== (__uw[0] > __uw[1]) && __pw[0] !== __pw[1]) {
|
|
5012
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5013
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'platform_orientation_mismatch',
|
|
5014
|
+
message: platform + ' is ' + (__pw[0] > __pw[1] ? 'landscape' : 'portrait') + ' (' + __fit.preset + '), but the canvas is ' + (__uw[0] > __uw[1] ? 'landscape' : 'portrait') + ', so the file is ' + __fit.used + '. For a ' + platform + ' deliverable, resize the canvas to ' + __fit.preset + ' (pinepaper_set_canvas_size) and re-lay the scene for it; the export does not reframe a scene.' }]);
|
|
5015
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
4937
5018
|
// AUDIO TIMING, WHERE THE STUDIO REPORTS IT (6.27). An MP4's AAC track used
|
|
4938
5019
|
// to start ~44 ms late and run past the picture — a 6.000 s bumper with sound
|
|
4939
5020
|
// then failed its cap. The engine now reports the track; it is surfaced as
|
|
@@ -4959,6 +5040,10 @@ ${stillTime !== undefined ? `
|
|
|
4959
5040
|
const __ff = (__pr.fonts && Array.isArray(__pr.fonts.families)) ? __pr.fonts.families.filter(function(f) { return f && f.reason; }) : [];
|
|
4960
5041
|
const __fnames = __ff.slice(0, 4).map(function(f) { return f.family + ' (' + f.reason + ')'; }).join('; ');
|
|
4961
5042
|
if (__pr.searchableText && __pr.linesSkipped > 0) __pw.push({ code: 'pdf_text_lines_skipped', message: __pr.linesSkipped + ' text line(s) are not in the searchable layer — no font in the PDF covers their characters — so they are drawn but cannot be selected, searched or read by a screen reader.' + (__fnames ? ' Fonts that could not be embedded: ' + __fnames + '.' : '') });
|
|
5043
|
+
// Text hidden at the render moment (a lifetime window, visible: false,
|
|
5044
|
+
// opacity 0) is in neither the picture nor the layer (FxTool 3b9499f2).
|
|
5045
|
+
const __hid = __pr.fonts && typeof __pr.fonts.hiddenTextItems === 'number' ? __pr.fonts.hiddenTextItems : 0;
|
|
5046
|
+
if (__hid > 0) __pw.push({ code: 'pdf_text_hidden', message: __hid + ' text item(s) were hidden when the PDF was rendered (outside their bornAt / ttl window, visible: false, or opacity 0), so they are not in it. Export at a time they show (time), or change their lifetime.' });
|
|
4962
5047
|
if (__ff.length && !(__pr.linesSkipped > 0)) __pw.push({ code: 'pdf_font_not_embedded', message: 'these fonts could not be embedded, so their text uses a fallback font in the searchable layer: ' + __fnames + '.' });
|
|
4963
5048
|
if (__pr.searchableText && __pr.pagesWithoutLayer) __pw.push({ code: 'pdf_no_text_layer', message: 'some pages have no searchable text layer — ' + __pr.pagesWithoutLayer.join('; ') });
|
|
4964
5049
|
if (!__pr.searchableText && __pr.reason && __pr.reason !== 'off') __pw.push({ code: 'pdf_no_text_layer', message: 'no searchable text layer was written (' + __pr.reason + '): the text is drawn but cannot be selected, searched or read by a screen reader.' });
|
|
@@ -4969,7 +5054,21 @@ ${stillTime !== undefined ? `
|
|
|
4969
5054
|
}
|
|
4970
5055
|
}
|
|
4971
5056
|
const __vr = __vx && __vx.lastVideoReport;
|
|
4972
|
-
if (result && result.success && __vr && typeof __vr === 'object') result.video = __vr;${
|
|
5057
|
+
if (result && result.success && __vr && typeof __vr === 'object') result.video = __vr;${webmAlpha ? `
|
|
5058
|
+
// A TRANSPARENT WEBM is confirmed from the encoder's own record: an alpha
|
|
5059
|
+
// stream with frames in it. A studio without WebM alpha writes an opaque
|
|
5060
|
+
// file that looks like it worked.
|
|
5061
|
+
if (result && result.success) {
|
|
5062
|
+
const __al = __vr && __vr.alpha;
|
|
5063
|
+
const __aw = [];
|
|
5064
|
+
if (!(__al && __al.frames > 0)) __aw.push({ code: 'alpha_not_applied', message: 'transparent was asked for, but this studio wrote no alpha stream: the WebM is opaque (transparent areas black). Export apng, or a png sequence, for transparency here.' });
|
|
5065
|
+
else if (__al.keyframeMismatches > 0) __aw.push({ code: 'alpha_keyframe_mismatch', message: __al.keyframeMismatches + ' frame(s) had colour and alpha keyframes out of step; some players may show those frames without transparency.' });
|
|
5066
|
+
if (__aw.length) {
|
|
5067
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5068
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat(__aw);
|
|
5069
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5070
|
+
}
|
|
5071
|
+
}` : ''}${headroomDefaulted ? `
|
|
4973
5072
|
// The applied value is the engine's own report where it gives one
|
|
4974
5073
|
// (5adfcf1c: lastVideoReport.headroom), so the result and the report agree.
|
|
4975
5074
|
if (result && result.success) result.broadcastHeadroom = { value: (__vr && typeof __vr.headroom === 'number') ? __vr.headroom : 12, defaulted: true, note: 'broadcast defaults to 12 luma codes of headroom, which keeps encoder ringing legal; pass broadcastHeadroom: 0 for the full range.' };` : ''}
|
|
@@ -4985,7 +5084,34 @@ ${stillTime !== undefined ? `
|
|
|
4985
5084
|
message: 'the file is ' + __vd.output[0] + 'x' + __vd.output[1] + ', not the requested ' + __vd.requested[0] + 'x' + __vd.requested[1] + ': H.264 needs even dimensions, so an odd side was rounded up by 1 px (the extra row / column is background). For an exact-pixel spec, use an even canvas size, or crop the extra line when re-encoding (ffmpeg -vf crop=' + __vd.requested[0] + ':' + __vd.requested[1] + ':0:0).' }]);
|
|
4986
5085
|
if (result.fidelity.note) delete result.fidelity.note;
|
|
4987
5086
|
}
|
|
4988
|
-
//
|
|
5087
|
+
${deterministic ? ` // DETERMINISTIC: the container timestamps are pinned either way; the PIXELS
|
|
5088
|
+
// are only identical run to run on the software encoder, because the
|
|
5089
|
+
// browser may pick a different hardware backend each time (FxTool
|
|
5090
|
+
// e34ae03d: 92/255 between backends, 2/255 run to run on hardware). The
|
|
5091
|
+
// engine requests software and reports what it got.
|
|
5092
|
+
// Where the studio says outright whether the file repeats (bitExact, FxTool
|
|
5093
|
+
// PR #31: it test-encodes and keeps whichever encoder does — often the
|
|
5094
|
+
// HARDWARE one on a Mac, since Chrome's software H.264 is not repeatable at
|
|
5095
|
+
// some sizes), that answer decides; the encoder name is only the fallback
|
|
5096
|
+
// for studios that do not report it.
|
|
5097
|
+
if (result && result.success) {
|
|
5098
|
+
const __hw = __vr && __vr.hardwareAcceleration;
|
|
5099
|
+
const __exact = __vr && typeof __vr.bitExact === 'boolean' ? __vr.bitExact : null;
|
|
5100
|
+
if (__exact === false) {
|
|
5101
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5102
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',
|
|
5103
|
+
message: 'deterministic was asked for, but no encoder on this machine repeats byte for byte at this size (the studio test-encoded and checked): the pixels may differ slightly between runs. Do not cache this file by hash; a different frame size may repeat.' }]);
|
|
5104
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5105
|
+
} else if (__exact === null && __hw !== 'prefer-software') {
|
|
5106
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5107
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',
|
|
5108
|
+
message: __hw
|
|
5109
|
+
? 'deterministic was asked for, but the encoder ran with hardwareAcceleration ' + JSON.stringify(__hw) + ': the container is pinned, the pixels may still differ slightly between runs (hardware encoders vary). Do not cache this file by hash.'
|
|
5110
|
+
: 'deterministic pinned the container timestamps, but this studio does not report which encoder backend ran, so the pixels may differ slightly between runs if the browser switches between hardware and software encoding. Do not rely on byte-identical files from this studio.' }]);
|
|
5111
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
` : ''} // LUMA OUTSIDE 16-235 (8.35, FxTool 4f1ec3ae). The broadcast encode is
|
|
4989
5115
|
// decoded back and measured: H.264 ringing at hard edges puts a fraction of
|
|
4990
5116
|
// a percent of samples out of range, which QC rejects. broadcastHeadroom
|
|
4991
5117
|
// cuts it but never to zero, so a legaliser pass is the requirement.
|
|
@@ -7649,6 +7775,12 @@ ${loads}
|
|
|
7649
7775
|
// Anything else (asset id, data:, URL) passes through untouched.
|
|
7650
7776
|
// @engine-methods analyzeAudio animateToBeat
|
|
7651
7777
|
const resolveSource = ` let __src = ${JSON.stringify(input.source || '')};
|
|
7778
|
+
// A large source was staged beside the code (see the handler); read it back.
|
|
7779
|
+
if (__src.indexOf('__ppStage:') === 0) {
|
|
7780
|
+
const __staged = window.__ppStage && window.__ppStage[__src.slice('__ppStage:'.length)];
|
|
7781
|
+
if (!__staged) return { success: false, error: 'the staged audio did not reach the page — try again, or upload it with pinepaper_media upload_audio and pass the returned id.' };
|
|
7782
|
+
__src = __staged;
|
|
7783
|
+
}
|
|
7652
7784
|
const __A = (typeof window !== 'undefined') && window.PinePaperAgent;
|
|
7653
7785
|
const __m = (__src && __A && typeof __A.listMedia === 'function')
|
|
7654
7786
|
? __A.listMedia().find(function(x) { return x.id === __src || x.registryId === __src; })
|
|
@@ -7672,6 +7804,11 @@ ${resolveSource}
|
|
|
7672
7804
|
if (r && typeof r === 'object' && 'ok' in r) {
|
|
7673
7805
|
return { success: r.ok !== false, ...r };
|
|
7674
7806
|
}
|
|
7807
|
+
// Nothing back is not a result: it was reported as success with an empty
|
|
7808
|
+
// body (gap 5.5), which reads as "analysed, no beats".
|
|
7809
|
+
if (r === undefined || r === null) {
|
|
7810
|
+
return { success: false, error: '${method} returned nothing, so there is no result to report. If the source is large, upload it with pinepaper_media upload_audio and pass the returned id.' };
|
|
7811
|
+
}
|
|
7675
7812
|
return { success: true, result: r };
|
|
7676
7813
|
})();`.trim();
|
|
7677
7814
|
if (input.action === 'analyze') {
|
|
@@ -8195,7 +8332,12 @@ ${resolve}
|
|
|
8195
8332
|
// engine mints a NUMBER. Returning the number meant the id could not be
|
|
8196
8333
|
// round-tripped: pass it back to add/remove and zod rejects it before the
|
|
8197
8334
|
// call is made.
|
|
8198
|
-
|
|
8335
|
+
// THE REGISTRY ID, not Paper's. createPrecomp returns the group, whose
|
|
8336
|
+
// .id is Paper's numeric id ("49"), which no tool accepts back ("precomp:
|
|
8337
|
+
// no such item(s): 49", round 10, 10.11). The registry id — what
|
|
8338
|
+
// listPrecomps and every other tool use — is on the group's data.
|
|
8339
|
+
const _d = (precomp && precomp.data) || {};
|
|
8340
|
+
const _pid = _d.registryId || _d.id || (precomp && precomp.id !== undefined ? precomp.id : precomp);
|
|
8199
8341
|
return { success: true, action: 'create', precompId: String(_pid), itemCount: items.length, name: ${JSON.stringify(input.name || '')} };
|
|
8200
8342
|
})();`.trim();
|
|
8201
8343
|
}
|
|
@@ -8206,7 +8348,11 @@ ${resolve}
|
|
|
8206
8348
|
// addToPrecomp answers falsy when the precomp or the item is not found, and
|
|
8207
8349
|
// this reported success regardless — the same silent-success shape as an
|
|
8208
8350
|
// unknown itemId elsewhere.
|
|
8209
|
-
|
|
8351
|
+
// The engine takes the ITEM, not its id (it clones it); an id string made
|
|
8352
|
+
// add throw and could never have worked. Resolved here, and a miss named.
|
|
8353
|
+
const _item = app.getItemById(${JSON.stringify(input.itemId || '')});
|
|
8354
|
+
if (!_item) return { success: false, action: 'add', error: 'no item ' + ${JSON.stringify(JSON.stringify(input.itemId || ''))} + ' on the canvas — check the id with pinepaper_get_items.' };
|
|
8355
|
+
const _ok = app.addToPrecomp(${JSON.stringify(input.precompId || '')}, _item);
|
|
8210
8356
|
if (_ok === false || _ok === null || _ok === undefined) {
|
|
8211
8357
|
return { success: false, action: 'add', error: 'nothing was added — check the precompId (create returns it as a string) and the itemId with pinepaper_get_items.' };
|
|
8212
8358
|
}
|
|
@@ -8216,7 +8362,14 @@ ${resolve}
|
|
|
8216
8362
|
return `
|
|
8217
8363
|
// Remove item from precomp
|
|
8218
8364
|
(function() {
|
|
8219
|
-
|
|
8365
|
+
// The item, not its id (the engine checks item.parent); and its answer is
|
|
8366
|
+
// read — null meant nothing was removed, and this said success regardless.
|
|
8367
|
+
const _item = app.getItemById(${JSON.stringify(input.itemId || '')});
|
|
8368
|
+
if (!_item) return { success: false, action: 'remove', error: 'no item ' + ${JSON.stringify(JSON.stringify(input.itemId || ''))} + ' on the canvas — check the id with pinepaper_get_items.' };
|
|
8369
|
+
const _r = app.removeFromPrecomp(${JSON.stringify(input.precompId || '')}, _item);
|
|
8370
|
+
if (_r === null || _r === undefined || _r === false) {
|
|
8371
|
+
return { success: false, action: 'remove', error: 'nothing was removed — the item is not in that precomp, or the precompId is not a precomp (create returns it; list shows them).' };
|
|
8372
|
+
}
|
|
8220
8373
|
return { success: true, action: 'remove', precompId: ${JSON.stringify(input.precompId || '')}, itemId: ${JSON.stringify(input.itemId || '')} };
|
|
8221
8374
|
})();`.trim();
|
|
8222
8375
|
default:
|
|
@@ -9613,7 +9766,7 @@ ${checks.includes('contrast') ? ` const all = (app.itemRegistry && typeof app.i
|
|
|
9613
9766
|
// Upload video from URL
|
|
9614
9767
|
(async function() {
|
|
9615
9768
|
${guard}
|
|
9616
|
-
const info = await A.uploadVideo(${srcExpr(input.url, 'video')}, ${opts});
|
|
9769
|
+
const info = await A.uploadVideo(${srcExpr(input.url, 'video')}, ${input.atPlayhead ? `Object.assign(${opts}, { timeOffset: Number(app.playbackTime) || 0 })` : opts});
|
|
9617
9770
|
return { success: true, action: 'upload_video', media: info };
|
|
9618
9771
|
})();`.trim();
|
|
9619
9772
|
}
|
|
@@ -9628,7 +9781,7 @@ ${guard}
|
|
|
9628
9781
|
// Upload audio from URL
|
|
9629
9782
|
(async function() {
|
|
9630
9783
|
${guard}
|
|
9631
|
-
const info = await A.uploadAudio(${srcExpr(input.url, 'audio')}, ${opts});${input.volume !== undefined ? `
|
|
9784
|
+
const info = await A.uploadAudio(${srcExpr(input.url, 'audio')}, ${input.atPlayhead ? `Object.assign(${opts}, { timeOffset: Number(app.playbackTime) || 0 })` : opts});${input.volume !== undefined ? `
|
|
9632
9785
|
// THE LEVEL IS STORED UNDER ONE KEY AND EXPORTED FROM ANOTHER. The upload
|
|
9633
9786
|
// records volume as \`gain\` on the registry entry; the video exporter's mix
|
|
9634
9787
|
// reads \`audioGain\`. So a bed uploaded at 0.25 exported at unity (measured:
|
|
@@ -9677,10 +9830,37 @@ ${guard}
|
|
|
9677
9830
|
if (typeof A.setMediaClip !== 'function') { return { success: false, error: 'setMediaClip unavailable — update FxTool' }; }
|
|
9678
9831
|
${resolveMedia}
|
|
9679
9832
|
if (!__m) return __miss;
|
|
9680
|
-
|
|
9833
|
+
${input.timeOffset !== undefined ? ` // Moving the clip is the 4th argument, which older studios' setMediaClip
|
|
9834
|
+
// does not take (it dropped it silently): its declared arity is the tell.
|
|
9835
|
+
if (A.setMediaClip.length < 4) {
|
|
9836
|
+
return { success: false, action: 'set_clip', id: __mid, error: 'this studio can re-trim a clip but not move it (setMediaClip takes no start time here) — nothing was changed. Update the studio, or remove and re-upload the clip with timeOffset.' };
|
|
9837
|
+
}
|
|
9838
|
+
` : ''} const ok = A.setMediaClip(__mid, ${input.inPoint}, ${input.outPoint}${input.timeOffset !== undefined ? `, ${input.timeOffset}` : ''});
|
|
9681
9839
|
return ok
|
|
9682
|
-
? { success: true, action: 'set_clip', id: __mid, registryId: __m.registryId, inPoint: ${input.inPoint}, outPoint: ${input.outPoint} }
|
|
9840
|
+
? { success: true, action: 'set_clip', id: __mid, registryId: __m.registryId, inPoint: ${input.inPoint}, outPoint: ${input.outPoint}${input.timeOffset !== undefined ? `, timeOffset: ${input.timeOffset}` : ''} }
|
|
9683
9841
|
: { success: false, action: 'set_clip', id: __mid, error: 'the studio refused the clip window ${input.inPoint}–${input.outPoint} for ' + __mid + '.' };
|
|
9842
|
+
})();`.trim();
|
|
9843
|
+
// Split and transitions (FxTool, verified on main): the engine's own
|
|
9844
|
+
// refusals come back as {ok:false, reason} and are errors here.
|
|
9845
|
+
case 'split':
|
|
9846
|
+
return `
|
|
9847
|
+
// Split a media clip at canvas time ${input.at}
|
|
9848
|
+
(async function() {
|
|
9849
|
+
${guard}
|
|
9850
|
+
if (typeof A.splitMedia !== 'function') { return { success: false, action: 'split', error: 'this studio is too old to split a clip (no splitMedia) — nothing was changed.' }; }
|
|
9851
|
+
const r = await A.splitMedia(${JSON.stringify(input.id)}, ${input.at});
|
|
9852
|
+
if (!r || r.ok === false) return { success: false, action: 'split', error: (r && r.reason) || 'the studio refused the split' };
|
|
9853
|
+
return Object.assign({ success: true, action: 'split' }, r);
|
|
9854
|
+
})();`.trim();
|
|
9855
|
+
case 'add_transition':
|
|
9856
|
+
return `
|
|
9857
|
+
// ${input.transition?.type ?? 'crossfade'} between ${input.fromItemId} and ${input.toItemId}
|
|
9858
|
+
(function() {
|
|
9859
|
+
${guard}
|
|
9860
|
+
if (typeof A.addTransition !== 'function') { return { success: false, action: 'add_transition', error: 'this studio is too old for clip transitions (no addTransition) — nothing was changed.' }; }
|
|
9861
|
+
const r = A.addTransition(${JSON.stringify(input.fromItemId)}, ${JSON.stringify(input.toItemId)}, ${JSON.stringify(input.transition ?? {})});
|
|
9862
|
+
if (!r || r.ok === false) return { success: false, action: 'add_transition', error: (r && r.reason) || 'the studio refused the transition' };
|
|
9863
|
+
return Object.assign({ success: true, action: 'add_transition' }, r);
|
|
9684
9864
|
})();`.trim();
|
|
9685
9865
|
// ── Video-editing actions (v1.6.4) — these live on `app` (PinePaper
|
|
9686
9866
|
// facades), not the agent media API, so they carry their own guards.
|
|
@@ -9690,15 +9870,18 @@ ${resolveMedia}
|
|
|
9690
9870
|
return `
|
|
9691
9871
|
// Remap a clip's time — speed ramps, freeze frames, reverse (null clears)
|
|
9692
9872
|
(function() {
|
|
9873
|
+
// A media id (vraster_ / araster_, what upload returns) is resolved to the
|
|
9874
|
+
// registry id these app facades take (round 10, 10.12).
|
|
9875
|
+
const __reg = function(r) { const __A = window.PinePaperAgent; const __m = (__A && typeof __A.listMedia === 'function') ? __A.listMedia().find(function(x) { return x.id === r; }) : null; return (__m && __m.registryId) || r; };
|
|
9693
9876
|
if (typeof app.setTimeRemap !== 'function') { return { success: false, error: 'app.setTimeRemap unavailable — update FxTool to a time-remap-capable build' }; }
|
|
9694
|
-
const r = app.setTimeRemap(${S(input.id)}, ${S(input.remapTrack ?? null)});
|
|
9877
|
+
const r = app.setTimeRemap(__reg(${S(input.id)}), ${S(input.remapTrack ?? null)});
|
|
9695
9878
|
if (!(r && r.ok)) return { success: false, error: (r && r.error) || 'remap failed' };
|
|
9696
9879
|
// WHAT THE REMAP CANNOT DO, READ BACK FROM WHAT WAS STORED. The clip is on
|
|
9697
9880
|
// the timeline only for its trimmed window, and past that it is HIDDEN
|
|
9698
9881
|
// whatever the curve says — a remap to 6.5 s on a 5 s clip cut to black at
|
|
9699
9882
|
// 5.0 s. Source values outside [inPoint, outPoint] are clamped, so that part
|
|
9700
9883
|
// of the curve is a hold. Both answered success before.
|
|
9701
|
-
const __rid = typeof app._resolveId === 'function' ? app._resolveId(${S(input.id)}) : ${S(input.id)};
|
|
9884
|
+
const __rid = typeof app._resolveId === 'function' ? app._resolveId(__reg(${S(input.id)})) : ${S(input.id)};
|
|
9702
9885
|
const __e = __rid && app.itemRegistry && app.itemRegistry.get(__rid);
|
|
9703
9886
|
const __p = (__e && __e.properties) || {};
|
|
9704
9887
|
const __tr = Array.isArray(__p.timeRemap) ? __p.timeRemap : [];
|
|
@@ -9726,15 +9909,18 @@ ${resolveMedia}
|
|
|
9726
9909
|
return `
|
|
9727
9910
|
// Speed ramp: consecutive {duration, speed} segments compiled to a remap curve
|
|
9728
9911
|
(function() {
|
|
9912
|
+
// A media id (vraster_ / araster_, what upload returns) is resolved to the
|
|
9913
|
+
// registry id these app facades take (round 10, 10.12).
|
|
9914
|
+
const __reg = function(r) { const __A = window.PinePaperAgent; const __m = (__A && typeof __A.listMedia === 'function') ? __A.listMedia().find(function(x) { return x.id === r; }) : null; return (__m && __m.registryId) || r; };
|
|
9729
9915
|
if (typeof app.speedRamp !== 'function') { return { success: false, error: 'app.speedRamp unavailable — update FxTool to a time-remap-capable build' }; }
|
|
9730
|
-
const r = app.speedRamp(${S(input.id)}, ${S(input.segments)});
|
|
9916
|
+
const r = app.speedRamp(__reg(${S(input.id)}), ${S(input.segments)});
|
|
9731
9917
|
if (!(r && r.ok)) return { success: false, error: (r && r.error) || 'speed ramp failed' };
|
|
9732
9918
|
// WHAT THE REMAP CANNOT DO, READ BACK FROM WHAT WAS STORED. The clip is on
|
|
9733
9919
|
// the timeline only for its trimmed window, and past that it is HIDDEN
|
|
9734
9920
|
// whatever the curve says — a remap to 6.5 s on a 5 s clip cut to black at
|
|
9735
9921
|
// 5.0 s. Source values outside [inPoint, outPoint] are clamped, so that part
|
|
9736
9922
|
// of the curve is a hold. Both answered success before.
|
|
9737
|
-
const __rid = typeof app._resolveId === 'function' ? app._resolveId(${S(input.id)}) : ${S(input.id)};
|
|
9923
|
+
const __rid = typeof app._resolveId === 'function' ? app._resolveId(__reg(${S(input.id)})) : ${S(input.id)};
|
|
9738
9924
|
const __e = __rid && app.itemRegistry && app.itemRegistry.get(__rid);
|
|
9739
9925
|
const __p = (__e && __e.properties) || {};
|
|
9740
9926
|
const __tr = Array.isArray(__p.timeRemap) ? __p.timeRemap : [];
|
|
@@ -9770,8 +9956,11 @@ ${resolveMedia}
|
|
|
9770
9956
|
return `
|
|
9771
9957
|
// Match cut: align the SUBJECT across the cut (on-device detection)
|
|
9772
9958
|
(async function() {
|
|
9959
|
+
// A media id (vraster_ / araster_, what upload returns) is resolved to the
|
|
9960
|
+
// registry id these app facades take (round 10, 10.12).
|
|
9961
|
+
const __reg = function(r) { const __A = window.PinePaperAgent; const __m = (__A && typeof __A.listMedia === 'function') ? __A.listMedia().find(function(x) { return x.id === r; }) : null; return (__m && __m.registryId) || r; };
|
|
9773
9962
|
if (typeof app.matchCut !== 'function') { return { success: false, error: 'app.matchCut unavailable — update FxTool to a match-cut-capable build' }; }
|
|
9774
|
-
const r = await app.matchCut(${S(input.fromItemId)}, ${S(input.toItemId)}, ${opts});
|
|
9963
|
+
const r = await app.matchCut(__reg(${S(input.fromItemId)}), __reg(${S(input.toItemId)}), ${opts});
|
|
9775
9964
|
if (r && r.needsConsent) { return { success: false, needsConsent: true, cost: r.cost, error: 'Detection model download needs consent — re-call with consent: true' }; }
|
|
9776
9965
|
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'match cut failed' }; }
|
|
9777
9966
|
return { success: true, action: 'match_cut', aspectMismatch: r.aspectMismatch || false };
|
|
@@ -9789,9 +9978,12 @@ ${resolveMedia}
|
|
|
9789
9978
|
return `
|
|
9790
9979
|
// Track matte: the matte item's ${input.channel || 'luma'} drives the target's alpha${input.live ? ' (LIVE — re-cuts as the matte animates)' : ''}
|
|
9791
9980
|
(async function() {
|
|
9981
|
+
// A media id (vraster_ / araster_, what upload returns) is resolved to the
|
|
9982
|
+
// registry id these app facades take (round 10, 10.12).
|
|
9983
|
+
const __reg = function(r) { const __A = window.PinePaperAgent; const __m = (__A && typeof __A.listMedia === 'function') ? __A.listMedia().find(function(x) { return x.id === r; }) : null; return (__m && __m.registryId) || r; };
|
|
9792
9984
|
if (typeof app.applyTrackMatte !== 'function') { return { success: false, error: 'app.applyTrackMatte unavailable — update FxTool to a track-matte-capable build' }; }
|
|
9793
9985
|
try {
|
|
9794
|
-
await app.applyTrackMatte(${S(input.id)}, ${S(input.matteItemId)}, ${opts});
|
|
9986
|
+
await app.applyTrackMatte(__reg(${S(input.id)}), __reg(${S(input.matteItemId)}), ${opts});
|
|
9795
9987
|
return { success: true, action: 'apply_track_matte', id: ${S(input.id)}, matte: ${S(input.matteItemId)}, live: ${S(!!input.live)} };
|
|
9796
9988
|
} catch (e) {
|
|
9797
9989
|
// The facade throws on a missing raster/matte rather than returning {ok:false}.
|
|
@@ -9804,8 +9996,11 @@ ${resolveMedia}
|
|
|
9804
9996
|
return `
|
|
9805
9997
|
// Stop a live matte, leaving the last cut in place
|
|
9806
9998
|
(function() {
|
|
9999
|
+
// A media id (vraster_ / araster_, what upload returns) is resolved to the
|
|
10000
|
+
// registry id these app facades take (round 10, 10.12).
|
|
10001
|
+
const __reg = function(r) { const __A = window.PinePaperAgent; const __m = (__A && typeof __A.listMedia === 'function') ? __A.listMedia().find(function(x) { return x.id === r; }) : null; return (__m && __m.registryId) || r; };
|
|
9807
10002
|
if (typeof app.stopLiveMatte !== 'function') { return { success: false, error: 'app.stopLiveMatte unavailable — update FxTool' }; }
|
|
9808
|
-
const r = app.stopLiveMatte(${S(input.id)});
|
|
10003
|
+
const r = app.stopLiveMatte(__reg(${S(input.id)}));
|
|
9809
10004
|
return r && r.ok ? { success: true, action: 'stop_live_matte' } : { success: false, error: (r && r.error) || 'no live matte on that item' };
|
|
9810
10005
|
})();`.trim();
|
|
9811
10006
|
}
|
|
@@ -11824,4 +12019,119 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
11824
12019
|
}
|
|
11825
12020
|
// Export singleton instance
|
|
11826
12021
|
export const codeGenerator = new PinePaperCodeGenerator();
|
|
12022
|
+
/**
|
|
12023
|
+
* WORDS ARE VECTOR (cloud: every generated plate is text-free; the words come
|
|
12024
|
+
* back as textLayers). Lays them out as real PinePaper text over a placed
|
|
12025
|
+
* plate, by role, inside title-safe (5%), sized from the canvas height:
|
|
12026
|
+
* headline top-left (Anton by default, fitted to the safe width), subhead
|
|
12027
|
+
* under it, caption / cta stacked up from the bottom-left, label top-right.
|
|
12028
|
+
* Each text is checked against the plate's average colour under it (WCAG:
|
|
12029
|
+
* 4.5:1, 3:1 for large text) and gets the lightest dark scrim that passes
|
|
12030
|
+
* when it falls short. A body for pinepaper_execute_custom_code (async).
|
|
12031
|
+
*/
|
|
12032
|
+
export function emitTextLayersPlacement(layers, plateRef) {
|
|
12033
|
+
const ROLE_FONT = { headline: 'Anton' };
|
|
12034
|
+
const families = [...new Set(layers.map((l) => l.font || ROLE_FONT[l.role ?? ''] || 'Inter'))];
|
|
12035
|
+
const fontBlocks = families.map((f) => `{${emitEnsureFont(f)}
|
|
12036
|
+
if (__font) fonts.push(__font);
|
|
12037
|
+
}`).join('\n');
|
|
12038
|
+
return `
|
|
12039
|
+
const fonts = [];
|
|
12040
|
+
${fontBlocks}
|
|
12041
|
+
const plate = (function(r) {
|
|
12042
|
+
let it = app.getItemById(r);
|
|
12043
|
+
if (!it && window.PinePaperAgent && typeof window.PinePaperAgent.listMedia === 'function') {
|
|
12044
|
+
const m = window.PinePaperAgent.listMedia().find(function(x) { return x.id === r || x.registryId === r; });
|
|
12045
|
+
if (m) it = app.getItemById(m.registryId);
|
|
12046
|
+
}
|
|
12047
|
+
return it;
|
|
12048
|
+
})(${JSON.stringify(plateRef)});
|
|
12049
|
+
const cs = app.getCanvasSize();
|
|
12050
|
+
const W = cs.width, H = cs.height, mx = Math.round(W * 0.05), my = Math.round(H * 0.05);
|
|
12051
|
+
const LAYERS = ${JSON.stringify(layers)};
|
|
12052
|
+
const SPEC = {
|
|
12053
|
+
headline: { size: 0.075, font: 'Anton', where: 'top' },
|
|
12054
|
+
subhead: { size: 0.036, font: 'Inter', where: 'top' },
|
|
12055
|
+
caption: { size: 0.028, font: 'Inter', where: 'bottom' },
|
|
12056
|
+
cta: { size: 0.032, font: 'Inter', where: 'bottom', bold: true },
|
|
12057
|
+
label: { size: 0.022, font: 'Inter', where: 'topright' },
|
|
12058
|
+
};
|
|
12059
|
+
const lin = function(v) { return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
12060
|
+
const make = function(l, spec, x, y, anchor) {
|
|
12061
|
+
const size = Math.max(8, Math.round(H * spec.size));
|
|
12062
|
+
const it = app.create('text', { content: l.content, x: x, y: y, fontSize: size, fontFamily: l.font || spec.font, color: '#ffffff', anchor: anchor });
|
|
12063
|
+
if (!it) return null;
|
|
12064
|
+
if (spec.bold) it.fontWeight = 'bold';
|
|
12065
|
+
if (typeof app.fitText === 'function') {
|
|
12066
|
+
try { app.fitText(it, { maxWidth: W - 2 * mx, maxFontSize: size, minFontSize: Math.max(8, Math.round(size * 0.4)), hold: anchor.indexOf('bottom') === 0 ? 'bottom' : 'top' }); } catch (_) { /* keeps its size */ }
|
|
12067
|
+
}
|
|
12068
|
+
return it;
|
|
12069
|
+
};
|
|
12070
|
+
const contrast = function(it, spec) {
|
|
12071
|
+
if (!plate || typeof plate.getAverageColor !== 'function') return { checked: false, note: 'the plate is not an image the studio can sample (a video plate): contrast was not checked.' };
|
|
12072
|
+
// The BRIGHT end of what is under the text, not its mean: a caption across
|
|
12073
|
+
// bright reflections on a dark road averaged to 15:1 and was unreadable in
|
|
12074
|
+
// the bright part. An 8 x 3 grid of cells, each averaged, judged at the
|
|
12075
|
+
// 90th percentile of luminance.
|
|
12076
|
+
const b0 = it.bounds, lums = [];
|
|
12077
|
+
for (let gy = 0; gy < 3; gy++) {
|
|
12078
|
+
for (let gx = 0; gx < 8; gx++) {
|
|
12079
|
+
const cw = b0.width / 8, ch = b0.height / 3;
|
|
12080
|
+
const cc = plate.getAverageColor(new paper.Rectangle(b0.left + gx * cw, b0.top + gy * ch, cw, ch));
|
|
12081
|
+
if (cc) lums.push(0.2126 * lin(cc.red) + 0.7152 * lin(cc.green) + 0.0722 * lin(cc.blue));
|
|
12082
|
+
}
|
|
12083
|
+
}
|
|
12084
|
+
if (!lums.length) return { checked: false };
|
|
12085
|
+
lums.sort(function(x, y) { return x - y; });
|
|
12086
|
+
const L = lums[Math.floor(0.9 * (lums.length - 1))];
|
|
12087
|
+
const ratio = 1.05 / (L + 0.05);
|
|
12088
|
+
const large = it.fontSize >= 24 || (spec.bold && it.fontSize >= 18.66);
|
|
12089
|
+
const required = large ? 3 : 4.5;
|
|
12090
|
+
const out = { checked: true, ratio: Math.round(ratio * 100) / 100, required: required, approximate: 'against the bright end (90th percentile of 24 sampled cells) of the plate behind the text' };
|
|
12091
|
+
if (ratio >= required) return out;
|
|
12092
|
+
// The lightest black scrim that reaches the requirement: under it the
|
|
12093
|
+
// background luminance is L * (1 - a).
|
|
12094
|
+
const a = Math.min(0.85, Math.max(0.2, 1 - (1.05 / required - 0.05) / L));
|
|
12095
|
+
const pad = Math.round(it.fontSize * 0.35);
|
|
12096
|
+
const b = it.bounds;
|
|
12097
|
+
const s = app.create('rectangle', { x: b.left - pad, y: b.top - pad, width: b.width + 2 * pad, height: b.height + 2 * pad, color: '#000000', anchor: 'top-left' });
|
|
12098
|
+
if (s) { s.opacity = a; if (typeof s.insertBelow === 'function') s.insertBelow(it); }
|
|
12099
|
+
out.scrim = { itemId: s && s.data && s.data.registryId, opacity: Math.round(a * 100) / 100 };
|
|
12100
|
+
out.ratioAfter = Math.round(1.05 / (L * (1 - a) + 0.05) * 100) / 100;
|
|
12101
|
+
return out;
|
|
12102
|
+
};
|
|
12103
|
+
const role = function(l) { return SPEC[l.role] ? l.role : 'caption'; };
|
|
12104
|
+
const placed = [];
|
|
12105
|
+
let top = my;
|
|
12106
|
+
LAYERS.filter(function(l) { return SPEC[role(l)].where === 'top'; }).forEach(function(l) {
|
|
12107
|
+
const spec = SPEC[role(l)];
|
|
12108
|
+
const it = make(l, spec, mx, top, 'top-left');
|
|
12109
|
+
if (!it) return;
|
|
12110
|
+
top = it.bounds.bottom + Math.round(H * 0.015);
|
|
12111
|
+
placed.push({ it: it, l: l, spec: spec });
|
|
12112
|
+
});
|
|
12113
|
+
let bottom = H - my;
|
|
12114
|
+
LAYERS.filter(function(l) { return SPEC[role(l)].where === 'bottom'; }).slice().reverse().forEach(function(l) {
|
|
12115
|
+
const spec = SPEC[role(l)];
|
|
12116
|
+
const it = make(l, spec, mx, bottom, 'bottom-left');
|
|
12117
|
+
if (!it) return;
|
|
12118
|
+
bottom = it.bounds.top - Math.round(H * 0.012);
|
|
12119
|
+
placed.unshift({ it: it, l: l, spec: spec });
|
|
12120
|
+
});
|
|
12121
|
+
let rtop = my;
|
|
12122
|
+
LAYERS.filter(function(l) { return SPEC[role(l)].where === 'topright'; }).forEach(function(l) {
|
|
12123
|
+
const spec = SPEC[role(l)];
|
|
12124
|
+
const it = make(l, spec, W - mx, rtop, 'top-right');
|
|
12125
|
+
if (!it) return;
|
|
12126
|
+
rtop = it.bounds.bottom + Math.round(H * 0.01);
|
|
12127
|
+
placed.push({ it: it, l: l, spec: spec });
|
|
12128
|
+
});
|
|
12129
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
12130
|
+
return {
|
|
12131
|
+
textItems: placed.map(function(p) {
|
|
12132
|
+
return { itemId: p.it.data && p.it.data.registryId, role: role(p.l), content: p.l.content, fontFamily: p.it.fontFamily, fontSize: Math.round(p.it.fontSize), contrast: contrast(p.it, p.spec) };
|
|
12133
|
+
}),
|
|
12134
|
+
fonts: fonts,
|
|
12135
|
+
};`;
|
|
12136
|
+
}
|
|
11827
12137
|
//# sourceMappingURL=code-generator.js.map
|