@pinepaper.studio/mcp-server 1.6.15 → 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.
Files changed (50) hide show
  1. package/README.de.md +1 -1
  2. package/README.es.md +1 -1
  3. package/README.fr.md +1 -1
  4. package/README.hi.md +1 -1
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +64 -4
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/build-info.json +2 -2
  11. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  12. package/dist/ontology/upstream-vocabulary.js +2 -2
  13. package/dist/tools/definitions.d.ts.map +1 -1
  14. package/dist/tools/definitions.js +105 -8
  15. package/dist/tools/definitions.js.map +1 -1
  16. package/dist/tools/engine-surface.d.ts +1 -1
  17. package/dist/tools/engine-surface.d.ts.map +1 -1
  18. package/dist/tools/engine-surface.js +30 -9
  19. package/dist/tools/engine-surface.js.map +1 -1
  20. package/dist/tools/handlers/camera.d.ts.map +1 -1
  21. package/dist/tools/handlers/camera.js +11 -0
  22. package/dist/tools/handlers/camera.js.map +1 -1
  23. package/dist/tools/handlers/generate.d.ts +324 -0
  24. package/dist/tools/handlers/generate.d.ts.map +1 -0
  25. package/dist/tools/handlers/generate.js +264 -0
  26. package/dist/tools/handlers/generate.js.map +1 -0
  27. package/dist/tools/handlers.d.ts.map +1 -1
  28. package/dist/tools/handlers.js +39 -4
  29. package/dist/tools/handlers.js.map +1 -1
  30. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  31. package/dist/tools/minimal-descriptions.js +3 -0
  32. package/dist/tools/minimal-descriptions.js.map +1 -1
  33. package/dist/tools/shape-params.d.ts +2 -2
  34. package/dist/tools/shape-params.js +2 -2
  35. package/dist/tools/stick-vocabulary.d.ts +1 -1
  36. package/dist/tools/stick-vocabulary.js +1 -1
  37. package/dist/tools/toolkits.d.ts.map +1 -1
  38. package/dist/tools/toolkits.js +3 -0
  39. package/dist/tools/toolkits.js.map +1 -1
  40. package/dist/types/code-generator.d.ts +15 -0
  41. package/dist/types/code-generator.d.ts.map +1 -1
  42. package/dist/types/code-generator.js +279 -21
  43. package/dist/types/code-generator.js.map +1 -1
  44. package/dist/types/schemas.d.ts +527 -82
  45. package/dist/types/schemas.d.ts.map +1 -1
  46. package/dist/types/schemas.js +19 -7
  47. package/dist/types/schemas.js.map +1 -1
  48. package/dist/vendor/design/design-style.js +1 -1
  49. package/manifest.json +13 -1
  50. package/package.json +1 -1
@@ -917,8 +917,15 @@ if (_flagged && _flagged.item && _flagged.item.data) Object.assign(_flagged.item
917
917
  + modIgnored.filter((k) => MODIFY_DID_YOU_MEAN[k]).map((k) => ` Did you mean ${MODIFY_DID_YOU_MEAN[k]} (not ${k})?`).join(''))}`;
918
918
  code += `
919
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;` : ''}
920
927
  ${modDirection ? emitDirectionCheck(`app.getItemById('${itemId}')`, modDirection) : ''}${modTabular ? emitTabularCheck(`app.getItemById('${itemId}')`) : ''}${modStrokePos ? emitStrokePositionCheck(`app.getItemById('${itemId}')`, modStrokePos) : ''}
921
- 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} };`;
922
929
  // Wrapped, because the body now RETURNS early on a miss rather than falling
923
930
  // through to an unconditional success. Async only when a font must load
924
931
  // before modifyItem re-measures the text.
@@ -1235,7 +1242,7 @@ if (!_target) {
1235
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.' };
1236
1243
  }`.trim();
1237
1244
  }
1238
- 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) {
1239
1246
  const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
1240
1247
  const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
1241
1248
  const opts = { duration: calculatedDuration, loop };
@@ -1247,6 +1254,10 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
1247
1254
  opts.clipOutPoint = clipOutPoint;
1248
1255
  if (timeUnits !== undefined)
1249
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';
1250
1261
  // A FADE ON AN AUDIO ITEM WAS ACCEPTED AND NEVER HEARD. addAnimation keyframes
1251
1262
  // the audio item's sentinel, and the mix reads one static audioGain per clip,
1252
1263
  // so {audioGain: 0→1} answered success, duration 15, and exported at a flat
@@ -1285,7 +1296,10 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
1285
1296
  const gaps = keyframes
1286
1297
  .map((k, i) => ({ time: k.time, missing: allProps.filter((p) => !propSets[i].includes(p)) }))
1287
1298
  .filter((g) => g.missing.length);
1288
- const gapNote = gaps.length && keyframes.length > 1
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
1289
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.`)}`
1290
1304
  : '';
1291
1305
  const first = [...keyframes].sort((a, b) => a.time - b.time)[0];
@@ -1302,8 +1316,25 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
1302
1316
  // Apply keyframe animation to ${itemId}
1303
1317
  (function() {
1304
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; }) : [];
1305
1324
  app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
1306
- return { success: true, itemId: '${itemId}', duration: ${calculatedDuration}, loop: ${loop}${timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : ''}${clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : ''}${clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : ''}${easingNote}${loopNote}${gapNote} };
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} };
1307
1338
  })();
1308
1339
  `.trim();
1309
1340
  }
@@ -2283,7 +2314,7 @@ export class PinePaperCodeGenerator {
2283
2314
  */
2284
2315
  generateKeyframeAnimate(input) {
2285
2316
  const validated = KeyframeAnimateInputSchema.parse(input);
2286
- 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);
2287
2318
  }
2288
2319
  /**
2289
2320
  * Generate code for importing / retargeting a motion-capture clip
@@ -3945,7 +3976,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
3945
3976
  }
3946
3977
  generateAgentExport(input) {
3947
3978
  const validated = AgentExportInputSchema.parse(input);
3948
- const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, alphaQuantizer, 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;
3949
3980
  // Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
3950
3981
  const videoEncodeOpts = {};
3951
3982
  if (broadcast)
@@ -3963,6 +3994,10 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
3963
3994
  videoEncodeOpts.minBitrate = minBitrate;
3964
3995
  if (bitrateMode !== undefined)
3965
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;
3966
4001
  // A transparent WebM (FxTool 1833b397) rides the store route too.
3967
4002
  const webmAlpha = format === 'webm' && transparent === true;
3968
4003
  if (webmAlpha) {
@@ -4968,6 +5003,18 @@ ${stillTime !== undefined ? `
4968
5003
 
4969
5004
  if (result && result.success) Object.assign(result, fidelity(format));
4970
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
+ }
4971
5018
  // AUDIO TIMING, WHERE THE STUDIO REPORTS IT (6.27). An MP4's AAC track used
4972
5019
  // to start ~44 ms late and run past the picture — a 6.000 s bumper with sound
4973
5020
  // then failed its cap. The engine now reports the track; it is surfaced as
@@ -5037,7 +5084,34 @@ ${stillTime !== undefined ? `
5037
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).' }]);
5038
5085
  if (result.fidelity.note) delete result.fidelity.note;
5039
5086
  }
5040
- // LUMA OUTSIDE 16-235 (8.35, FxTool 4f1ec3ae). The broadcast encode is
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
5041
5115
  // decoded back and measured: H.264 ringing at hard edges puts a fraction of
5042
5116
  // a percent of samples out of range, which QC rejects. broadcastHeadroom
5043
5117
  // cuts it but never to zero, so a legaliser pass is the requirement.
@@ -7701,6 +7775,12 @@ ${loads}
7701
7775
  // Anything else (asset id, data:, URL) passes through untouched.
7702
7776
  // @engine-methods analyzeAudio animateToBeat
7703
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
+ }
7704
7784
  const __A = (typeof window !== 'undefined') && window.PinePaperAgent;
7705
7785
  const __m = (__src && __A && typeof __A.listMedia === 'function')
7706
7786
  ? __A.listMedia().find(function(x) { return x.id === __src || x.registryId === __src; })
@@ -7724,6 +7804,11 @@ ${resolveSource}
7724
7804
  if (r && typeof r === 'object' && 'ok' in r) {
7725
7805
  return { success: r.ok !== false, ...r };
7726
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
+ }
7727
7812
  return { success: true, result: r };
7728
7813
  })();`.trim();
7729
7814
  if (input.action === 'analyze') {
@@ -8247,7 +8332,12 @@ ${resolve}
8247
8332
  // engine mints a NUMBER. Returning the number meant the id could not be
8248
8333
  // round-tripped: pass it back to add/remove and zod rejects it before the
8249
8334
  // call is made.
8250
- const _pid = precomp && precomp.id !== undefined ? precomp.id : precomp;
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);
8251
8341
  return { success: true, action: 'create', precompId: String(_pid), itemCount: items.length, name: ${JSON.stringify(input.name || '')} };
8252
8342
  })();`.trim();
8253
8343
  }
@@ -8258,7 +8348,11 @@ ${resolve}
8258
8348
  // addToPrecomp answers falsy when the precomp or the item is not found, and
8259
8349
  // this reported success regardless — the same silent-success shape as an
8260
8350
  // unknown itemId elsewhere.
8261
- const _ok = app.addToPrecomp(${JSON.stringify(input.precompId || '')}, ${JSON.stringify(input.itemId || '')});
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);
8262
8356
  if (_ok === false || _ok === null || _ok === undefined) {
8263
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.' };
8264
8358
  }
@@ -8268,7 +8362,14 @@ ${resolve}
8268
8362
  return `
8269
8363
  // Remove item from precomp
8270
8364
  (function() {
8271
- app.removeFromPrecomp(${JSON.stringify(input.precompId || '')}, ${JSON.stringify(input.itemId || '')});
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
+ }
8272
8373
  return { success: true, action: 'remove', precompId: ${JSON.stringify(input.precompId || '')}, itemId: ${JSON.stringify(input.itemId || '')} };
8273
8374
  })();`.trim();
8274
8375
  default:
@@ -9665,7 +9766,7 @@ ${checks.includes('contrast') ? ` const all = (app.itemRegistry && typeof app.i
9665
9766
  // Upload video from URL
9666
9767
  (async function() {
9667
9768
  ${guard}
9668
- 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});
9669
9770
  return { success: true, action: 'upload_video', media: info };
9670
9771
  })();`.trim();
9671
9772
  }
@@ -9680,7 +9781,7 @@ ${guard}
9680
9781
  // Upload audio from URL
9681
9782
  (async function() {
9682
9783
  ${guard}
9683
- 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 ? `
9684
9785
  // THE LEVEL IS STORED UNDER ONE KEY AND EXPORTED FROM ANOTHER. The upload
9685
9786
  // records volume as \`gain\` on the registry entry; the video exporter's mix
9686
9787
  // reads \`audioGain\`. So a bed uploaded at 0.25 exported at unity (measured:
@@ -9729,10 +9830,37 @@ ${guard}
9729
9830
  if (typeof A.setMediaClip !== 'function') { return { success: false, error: 'setMediaClip unavailable — update FxTool' }; }
9730
9831
  ${resolveMedia}
9731
9832
  if (!__m) return __miss;
9732
- const ok = A.setMediaClip(__mid, ${input.inPoint}, ${input.outPoint});
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}` : ''});
9733
9839
  return ok
9734
- ? { 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}` : ''} }
9735
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);
9736
9864
  })();`.trim();
9737
9865
  // ── Video-editing actions (v1.6.4) — these live on `app` (PinePaper
9738
9866
  // facades), not the agent media API, so they carry their own guards.
@@ -9742,15 +9870,18 @@ ${resolveMedia}
9742
9870
  return `
9743
9871
  // Remap a clip's time — speed ramps, freeze frames, reverse (null clears)
9744
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; };
9745
9876
  if (typeof app.setTimeRemap !== 'function') { return { success: false, error: 'app.setTimeRemap unavailable — update FxTool to a time-remap-capable build' }; }
9746
- const r = app.setTimeRemap(${S(input.id)}, ${S(input.remapTrack ?? null)});
9877
+ const r = app.setTimeRemap(__reg(${S(input.id)}), ${S(input.remapTrack ?? null)});
9747
9878
  if (!(r && r.ok)) return { success: false, error: (r && r.error) || 'remap failed' };
9748
9879
  // WHAT THE REMAP CANNOT DO, READ BACK FROM WHAT WAS STORED. The clip is on
9749
9880
  // the timeline only for its trimmed window, and past that it is HIDDEN
9750
9881
  // whatever the curve says — a remap to 6.5 s on a 5 s clip cut to black at
9751
9882
  // 5.0 s. Source values outside [inPoint, outPoint] are clamped, so that part
9752
9883
  // of the curve is a hold. Both answered success before.
9753
- 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)};
9754
9885
  const __e = __rid && app.itemRegistry && app.itemRegistry.get(__rid);
9755
9886
  const __p = (__e && __e.properties) || {};
9756
9887
  const __tr = Array.isArray(__p.timeRemap) ? __p.timeRemap : [];
@@ -9778,15 +9909,18 @@ ${resolveMedia}
9778
9909
  return `
9779
9910
  // Speed ramp: consecutive {duration, speed} segments compiled to a remap curve
9780
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; };
9781
9915
  if (typeof app.speedRamp !== 'function') { return { success: false, error: 'app.speedRamp unavailable — update FxTool to a time-remap-capable build' }; }
9782
- const r = app.speedRamp(${S(input.id)}, ${S(input.segments)});
9916
+ const r = app.speedRamp(__reg(${S(input.id)}), ${S(input.segments)});
9783
9917
  if (!(r && r.ok)) return { success: false, error: (r && r.error) || 'speed ramp failed' };
9784
9918
  // WHAT THE REMAP CANNOT DO, READ BACK FROM WHAT WAS STORED. The clip is on
9785
9919
  // the timeline only for its trimmed window, and past that it is HIDDEN
9786
9920
  // whatever the curve says — a remap to 6.5 s on a 5 s clip cut to black at
9787
9921
  // 5.0 s. Source values outside [inPoint, outPoint] are clamped, so that part
9788
9922
  // of the curve is a hold. Both answered success before.
9789
- 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)};
9790
9924
  const __e = __rid && app.itemRegistry && app.itemRegistry.get(__rid);
9791
9925
  const __p = (__e && __e.properties) || {};
9792
9926
  const __tr = Array.isArray(__p.timeRemap) ? __p.timeRemap : [];
@@ -9822,8 +9956,11 @@ ${resolveMedia}
9822
9956
  return `
9823
9957
  // Match cut: align the SUBJECT across the cut (on-device detection)
9824
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; };
9825
9962
  if (typeof app.matchCut !== 'function') { return { success: false, error: 'app.matchCut unavailable — update FxTool to a match-cut-capable build' }; }
9826
- 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});
9827
9964
  if (r && r.needsConsent) { return { success: false, needsConsent: true, cost: r.cost, error: 'Detection model download needs consent — re-call with consent: true' }; }
9828
9965
  if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'match cut failed' }; }
9829
9966
  return { success: true, action: 'match_cut', aspectMismatch: r.aspectMismatch || false };
@@ -9841,9 +9978,12 @@ ${resolveMedia}
9841
9978
  return `
9842
9979
  // Track matte: the matte item's ${input.channel || 'luma'} drives the target's alpha${input.live ? ' (LIVE — re-cuts as the matte animates)' : ''}
9843
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; };
9844
9984
  if (typeof app.applyTrackMatte !== 'function') { return { success: false, error: 'app.applyTrackMatte unavailable — update FxTool to a track-matte-capable build' }; }
9845
9985
  try {
9846
- await app.applyTrackMatte(${S(input.id)}, ${S(input.matteItemId)}, ${opts});
9986
+ await app.applyTrackMatte(__reg(${S(input.id)}), __reg(${S(input.matteItemId)}), ${opts});
9847
9987
  return { success: true, action: 'apply_track_matte', id: ${S(input.id)}, matte: ${S(input.matteItemId)}, live: ${S(!!input.live)} };
9848
9988
  } catch (e) {
9849
9989
  // The facade throws on a missing raster/matte rather than returning {ok:false}.
@@ -9856,8 +9996,11 @@ ${resolveMedia}
9856
9996
  return `
9857
9997
  // Stop a live matte, leaving the last cut in place
9858
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; };
9859
10002
  if (typeof app.stopLiveMatte !== 'function') { return { success: false, error: 'app.stopLiveMatte unavailable — update FxTool' }; }
9860
- const r = app.stopLiveMatte(${S(input.id)});
10003
+ const r = app.stopLiveMatte(__reg(${S(input.id)}));
9861
10004
  return r && r.ok ? { success: true, action: 'stop_live_matte' } : { success: false, error: (r && r.error) || 'no live matte on that item' };
9862
10005
  })();`.trim();
9863
10006
  }
@@ -11876,4 +12019,119 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
11876
12019
  }
11877
12020
  // Export singleton instance
11878
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
+ }
11879
12137
  //# sourceMappingURL=code-generator.js.map