@pinepaper.studio/mcp-server 1.6.32 → 1.6.33

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 (46) hide show
  1. package/README.md +53 -0
  2. package/dist/agent/types.d.ts +3 -3
  3. package/dist/build-info.json +2 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +3 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/ontology/upstream-vocabulary.d.ts +1 -1
  8. package/dist/ontology/upstream-vocabulary.js +1 -1
  9. package/dist/tools/definitions.d.ts.map +1 -1
  10. package/dist/tools/definitions.js +51 -38
  11. package/dist/tools/definitions.js.map +1 -1
  12. package/dist/tools/engine-lists.d.ts +5 -3
  13. package/dist/tools/engine-lists.d.ts.map +1 -1
  14. package/dist/tools/engine-lists.js +11 -3
  15. package/dist/tools/engine-lists.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 +10 -4
  19. package/dist/tools/engine-surface.js.map +1 -1
  20. package/dist/tools/handlers.d.ts.map +1 -1
  21. package/dist/tools/handlers.js +59 -11
  22. package/dist/tools/handlers.js.map +1 -1
  23. package/dist/tools/lane.js +2 -2
  24. package/dist/tools/lane.js.map +1 -1
  25. package/dist/tools/reply-fields.d.ts +15 -0
  26. package/dist/tools/reply-fields.d.ts.map +1 -0
  27. package/dist/tools/reply-fields.js +117 -0
  28. package/dist/tools/reply-fields.js.map +1 -0
  29. package/dist/tools/scene-cache-export.d.ts.map +1 -1
  30. package/dist/tools/scene-cache-export.js +2 -1
  31. package/dist/tools/scene-cache-export.js.map +1 -1
  32. package/dist/tools/shape-params.d.ts +2 -2
  33. package/dist/tools/shape-params.d.ts.map +1 -1
  34. package/dist/tools/shape-params.js +4 -2
  35. package/dist/tools/shape-params.js.map +1 -1
  36. package/dist/tools/stick-vocabulary.d.ts +1 -1
  37. package/dist/tools/stick-vocabulary.js +1 -1
  38. package/dist/types/code-generator.d.ts.map +1 -1
  39. package/dist/types/code-generator.js +113 -34
  40. package/dist/types/code-generator.js.map +1 -1
  41. package/dist/types/schemas.d.ts +31 -6
  42. package/dist/types/schemas.d.ts.map +1 -1
  43. package/dist/types/schemas.js +11 -3
  44. package/dist/types/schemas.js.map +1 -1
  45. package/manifest.json +1 -1
  46. package/package.json +1 -1
@@ -7,7 +7,7 @@
7
7
  import { compileChoreography } from '../tools/choreograph.js';
8
8
  import { compileBatchTimelines, emitTimelineOpBody } from '../tools/timeline.js';
9
9
  import { normalizeSvgSize } from '../utils/svg-size.js';
10
- import { WORLD3D_COLOR_PATHS, PATTERN_KINDS, EXPRESSION_FUNCTION_ARITY, EXPRESSION_CONSTANTS, EXPRESSION_REJECTED, RELATION_PARAM_KEYS, ANIMATION_OPTION_KEYS } from '../tools/engine-lists.js';
10
+ import { WORLD3D_COLOR_PATHS, PATTERN_KINDS, EXPRESSION_FUNCTION_ARITY, EXPRESSION_CONSTANTS, EXPRESSION_REJECTED, RELATION_PARAM_KEYS, RELATION_LOOP_PARAMS, ANIMATION_OPTION_KEYS, FILTER_TYPES, FILTER_PARAM_RANGES } from '../tools/engine-lists.js';
11
11
  import { expressionGrammarText } from '../tools/expression-grammar.js';
12
12
  import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
13
13
  import { NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
@@ -1483,10 +1483,10 @@ function customOptionWarningJs(relationType, params, key, injected = []) {
1483
1483
  return `, ...(function () { try {
1484
1484
  var cr = typeof window !== 'undefined' && window.__ppCustomRelations && window.__ppCustomRelations[${jsLiteral(relationType)}];
1485
1485
  if (!cr || !Array.isArray(cr.reads)) return {};
1486
- var skip = ${JSON.stringify(injected)};
1486
+ var skip = ${JSON.stringify([...injected, ...RELATION_LOOP_PARAMS])};
1487
1487
  var un = ${JSON.stringify(keys)}.filter(function (k) { return cr.reads.indexOf(k) < 0 && skip.indexOf(k) < 0; });
1488
1488
  if (!un.length) return {};
1489
- return { ${key}: ${jsLiteral(relationType)} + ' does not read ' + un.join(', ') + ', so ' + (un.length > 1 ? 'they were' : 'it was') + ' ignored. ' + ${jsLiteral(relationType)} + ' reads: ' + cr.reads.join(', ') + '.' };
1489
+ return { ignoredOptions: un, ${key}: ${jsLiteral(relationType)} + ' does not read ' + un.join(', ') + ', so ' + (un.length > 1 ? 'they were' : 'it was') + ' ignored. ' + ${jsLiteral(relationType)} + ' reads: ' + cr.reads.join(', ') + '.' };
1490
1490
  } catch (e) { return {}; } })()`;
1491
1491
  }
1492
1492
  export function relationOptionWarning(relationType, params, injected = []) {
@@ -1508,10 +1508,25 @@ function equationFlipNote(relationType, params) {
1508
1508
  return null;
1509
1509
  return 'params.equation is read with y UP (flipY defaults to true), so on screen the curve is mirrored about cy. If you wrote it in screen coordinates (y down), add flipY: false to the equation.';
1510
1510
  }
1511
- function unreadOptionWarning(relationType, params, injected) {
1511
+ /** What a built-in relation reads: its rule's params, and what the relation loop reads on every relation. */
1512
+ function relationReads(relationType) {
1512
1513
  const keys = typeof relationType === 'string' ? RELATION_PARAM_KEYS[relationType] : undefined;
1514
+ return keys ? [...keys, ...RELATION_LOOP_PARAMS] : undefined;
1515
+ }
1516
+ function unreadOptionWarning(relationType, params, injected) {
1517
+ const keys = relationReads(relationType);
1513
1518
  return keys ? unreadKeysWarning(String(relationType), keys, params, injected) : null;
1514
1519
  }
1520
+ const unreadKeys = (keys, params, injected = []) => keys && params && typeof params === 'object' ? Object.keys(params).filter((k) => !keys.includes(k) && !injected.includes(k)) : [];
1521
+ /**
1522
+ * A relation reply's fields for its options: the unread keys as data (ignoredOptions, which the reply's
1523
+ * ignoredArguments is built from) beside the sentence. Built-ins are known here, a custom relation only in the page.
1524
+ */
1525
+ function relationOptionFields(relationType, params, key, injected = []) {
1526
+ const unread = unreadKeys(relationReads(relationType), params, injected);
1527
+ const warning = relationOptionWarning(relationType, params, injected);
1528
+ return `${unread.length ? `, ignoredOptions: ${jsLiteral(unread)}` : ''}${warning ? `, ${key}: ${jsLiteral(warning)}` : ''}${customOptionWarningJs(relationType, params, key, injected)}`;
1529
+ }
1515
1530
  /** The keys of `params` outside `keys`, named with a "did you mean" and the list that is read; null when all are read. */
1516
1531
  function unreadKeysWarning(reader, keys, params, injected = []) {
1517
1532
  if (!params || typeof params !== 'object')
@@ -1568,7 +1583,7 @@ String(jsLiteral(targetId)) +
1568
1583
  ", " +
1569
1584
  String(jsLiteral(relationType)) +
1570
1585
  ", " +
1571
- String(JSON.stringify(params, null, 2)) + ");\n if (!success) {\n const __why = " + String(relationRefusalReasonJs(jsLiteral(relationType))) + ";\n return { success: false, relationType: " + String(jsLiteral(relationType)) + ", error: 'the studio refused the ' + " + String(jsLiteral(relationType)) + " + ' relation between ' + " + String(jsLiteral(sourceId)) + " + ' and ' + " + String(jsLiteral(targetId)) + " + (__why ? ': ' + __why : '.') };\n }\n app.historyManager.saveState();\n return { success: true, sourceId: " + String(jsLiteral(sourceId)) + ", targetId: " + String(jsLiteral(targetId)) + ", relationType: " + String(jsLiteral(relationType)) + String((() => { const w = relationOptionWarning(relationType, params); return w ? `, warning: ${jsLiteral(w)}` : ''; })()) + String(customOptionWarningJs(relationType, params, 'warning')) + " };\n})();\n").trim();
1586
+ String(JSON.stringify(params, null, 2)) + ");\n if (!success) {\n const __why = " + String(relationRefusalReasonJs(jsLiteral(relationType))) + ";\n return { success: false, relationType: " + String(jsLiteral(relationType)) + ", error: 'the studio refused the ' + " + String(jsLiteral(relationType)) + " + ' relation between ' + " + String(jsLiteral(sourceId)) + " + ' and ' + " + String(jsLiteral(targetId)) + " + (__why ? ': ' + __why : '.') };\n }\n app.historyManager.saveState();\n return { success: true, sourceId: " + String(jsLiteral(sourceId)) + ", targetId: " + String(jsLiteral(targetId)) + ", relationType: " + String(jsLiteral(relationType)) + String(relationOptionFields(relationType, params, 'warning')) + " };\n})();\n").trim();
1572
1587
  }
1573
1588
  /**
1574
1589
  * The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
@@ -1644,9 +1659,18 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
1644
1659
  for (const k of ['velocities', 'offsets'])
1645
1660
  if (out[k] !== undefined)
1646
1661
  out[k] = pairs(out[k]);
1662
+ // A trail's single offset is one pair; the studio reads [x, y] and dropped {x, y} with nothing said (TRAILOFF31).
1663
+ const off = out.offset;
1664
+ if (off && typeof off === 'object' && !Array.isArray(off) && typeof off.x === 'number' && typeof off.y === 'number')
1665
+ out.offset = [off.x, off.y];
1647
1666
  return out;
1648
1667
  };
1649
1668
  const out = fix(spec);
1669
+ // rate, start and end drive a trail. Beside bursts they drew nothing and said nothing (EMITCONT31).
1670
+ const trailOnly = ['rate', 'start', 'end'].filter((k) => spec[k] !== undefined);
1671
+ if (trailOnly.length && Array.isArray(spec.bursts) && spec.mode !== 'trail' && spec.target === undefined) {
1672
+ warnings.push(`${trailOnly.join(', ')} ${trailOnly.length > 1 ? 'are' : 'is'} not read by a burst emitter, so ${trailOnly.length > 1 ? 'they had' : 'it had'} no effect: only the bursts' own count is drawn. For particles over time, add bursts, or use mode 'trail' with a target.`);
1673
+ }
1650
1674
  if (Array.isArray(spec.bursts))
1651
1675
  out.bursts = spec.bursts.map((b) => (b && typeof b === 'object' ? fix(b) : b));
1652
1676
  // The studio's default count per burst (ParticleEmitter DEFAULT_LOOK).
@@ -2072,6 +2096,24 @@ function unanimatedTextKeysJs(keys) {
2072
2096
  return ${JSON.stringify(spec)}.filter(function(s) { return !s.names.some(function(n) { return __names.indexOf(n) >= 0; }); }).map(function(s) { return s.key; });
2073
2097
  })()`;
2074
2098
  }
2099
+ /**
2100
+ * An image filter's parameter as a key: keyframe_animate stores it, and a filter does not read a track, so the item
2101
+ * never changes with success:true (FILTKEY31). Asked of the same list, so a studio that starts animating one is believed.
2102
+ */
2103
+ const FILTER_PARAM_KEYS = new Set([...Object.values(FILTER_PARAM_RANGES).flatMap((p) => Object.keys(p)), ...FILTER_TYPES].filter((k) => !['color', 'opacity', 'scale', 'x', 'y', 'rotation', 'size', 'radius', 'width', 'height', 'value', 'amount'].includes(k)));
2104
+ function unanimatedFilterKeysJs(keys) {
2105
+ const filterKeys = [...new Set(keys)].filter((k) => FILTER_PARAM_KEYS.has(k));
2106
+ if (!filterKeys.length)
2107
+ return '[]';
2108
+ return `(function() {
2109
+ let __l = null;
2110
+ try { __l = typeof app.listAnimatableProperties === 'function' ? app.listAnimatableProperties() : null; } catch (_) { __l = null; }
2111
+ if (!__l || !__l.properties) return [];
2112
+ const __names = __l.properties.map(function(p) { return p && p.name; });
2113
+ return ${JSON.stringify(filterKeys)}.filter(function(k) { return __names.indexOf(k) < 0; });
2114
+ })()`;
2115
+ }
2116
+ const UNANIMATED_FILTER_KEYS_SENTENCE = `' is an image filter setting and not an animatable property, so the key had no effect. Set a fixed value with pinepaper_image_filter or pinepaper_add_filter, and animate opacity or scale for a change over time.'`;
2075
2117
  /** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
2076
2118
  const UNANIMATED_TEXT_KEYS_SENTENCE = `' is not an animatable property in this studio (app.listAnimatableProperties does not list it), so it had no effect: a key animates only where that list names it. Here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX. For a skew, key skewX / skewY (degrees) where the studio lists them, else a transform matrix track.'`;
2077
2119
  /**
@@ -2097,12 +2139,6 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
2097
2139
  ? `, 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.`)}`
2098
2140
  : '';
2099
2141
  const first = [...keyframes].sort((a, b) => a.time - b.time)[0];
2100
- // loop:true repeats in the editor; an export of the measured build played
2101
- // the keyframes ONCE and held (round 7, 2.24). Said where it is set, with
2102
- // the two ways that do export, until the engine's export clock wraps them.
2103
- const loopNote = loop
2104
- ? `, loopNote: ${JSON.stringify('loop:true repeats in the editor, but has been measured playing ONCE in an exported video and then holding the last keyframe. For an export longer than one cycle, check a frame in the second cycle — if it holds, write the keyframes out per cycle, or use a loop preset (pinepaper_animate), which does cycle in export.')}`
2105
- : '';
2106
2142
  // SIZEKEY-PAIR (mcp-cloud #28): on a studio before FxTool #262, x (or
2107
2143
  // position) keyed beside fontSize on one text item drifted in an export: the
2108
2144
  // size was applied after the position, and a text resizes around its anchor.
@@ -2114,7 +2150,7 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
2114
2150
  const easingNote = easingAt !== 'departure' && first?.easing && first.easing !== 'linear'
2115
2151
  ? `, note: ${JSON.stringify(`easing '${first.easing}' is on the first keyframe, where it has no effect by default: easing shapes the segment ARRIVING at a keyframe. Put it on the keyframe you are moving to, or pass easingAt: 'departure' to read every easing as the move LEAVING its key (current studio).`)}`
2116
2152
  : '';
2117
- return `${easingNote}${sizekeyPairNote}${loopNote}${gapNote}`;
2153
+ return `${easingNote}${sizekeyPairNote}${gapNote}`;
2118
2154
  }
2119
2155
  function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
2120
2156
  const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
@@ -2172,9 +2208,12 @@ String(JSON.stringify(levelKeys)) +
2172
2208
  const textKeyCheck = textKeysJs ? `
2173
2209
  const __ignored = ${textKeysJs};
2174
2210
  if (__ignored.length) __warn.push(__ignored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE});` : '';
2211
+ const filterKeyCheck = `
2212
+ const __filterIgnored = ${unanimatedFilterKeysJs(keyframes.flatMap((k) => Object.keys(k.properties ?? {})))};
2213
+ if (__filterIgnored.length) __warn.push(__filterIgnored.join(', ') + ${UNANIMATED_FILTER_KEYS_SENTENCE});`;
2175
2214
  const easingAtCheck = easingAt !== undefined ? `
2176
2215
  if (__opts && Object.prototype.hasOwnProperty.call(__opts, 'easingAt')) __warn.push('easingAt ' + ${JSON.stringify(JSON.stringify(easingAt))} + ' was not applied: this studio predates it, so every easing was read as the move ARRIVING at its key. Put each easing on the key the move goes to instead.');` : '';
2177
- const runtimeWarning = `${textKeysJs ? ', ...(__ignored.length ? { ignoredProperties: __ignored } : {})' : ''}, ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
2216
+ const runtimeWarning = `, ...(${textKeysJs ? '__ignored.length || ' : ''}__filterIgnored.length ? { ignoredProperties: ${textKeysJs ? '__ignored.concat(__filterIgnored)' : '__filterIgnored'} } : {}), ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
2178
2217
  const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `${ID}`;
2179
2218
  return ("\n// Apply keyframe animation to " +
2180
2219
  String(ID) +
@@ -2190,6 +2229,7 @@ String(keyframesJson) +
2190
2229
  ", __opts);\n const __warn = [];" +
2191
2230
  String(easingAtCheck) +
2192
2231
  String(textKeyCheck) +
2232
+ String(filterKeyCheck) +
2193
2233
  "\n const __now = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];\n const __kept = __prev.every(function(t) { return __now.indexOf(t) >= 0; });" +
2194
2234
  String(append ? `
2195
2235
  // Property gaps across the merged track (the same check a single call makes
@@ -2209,8 +2249,7 @@ String(calculatedDuration) +
2209
2249
  ", loop: " +
2210
2250
  String(loop) +
2211
2251
  String(append ? ', append: true' : '') +
2212
- ", ...(__track ? { track: __track } : {})" +
2213
- String(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.' } : {})` : '') + String(timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : '') + String(clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : '') + String(clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : '') + String(__notes) + String(runtimeWarning) + " };\n})();\n").trim();
2252
+ ", ...(__track ? { track: __track } : {})" + String(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.' } : {})` : '') + String(timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : '') + String(clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : '') + String(clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : '') + String(__notes) + String(runtimeWarning) + " };\n})();\n").trim();
2214
2253
  }
2215
2254
  /**
2216
2255
  * Template for executing generators
@@ -3554,7 +3593,7 @@ String(JSON.stringify(validated.presetId)) +
3554
3593
  const code = stepNoTarget
3555
3594
  ? `(function() {
3556
3595
  if (!${STEP_ACTIVE_TARGETLESS_JS}) return { success: false, relationType: 'on_event_step_active', error: ${jsLiteral(stepNoTarget)} };
3557
- return ${built.trim().replace(/;$/, '')};
3596
+ ${asReturningBody(built)}
3558
3597
  })();`
3559
3598
  : built;
3560
3599
  // NO TARGET (event reactions like on_event_play_sound, self-relations): the
@@ -4234,7 +4273,7 @@ if (typeof _gen === 'function') {
4234
4273
  })();`);
4235
4274
  }
4236
4275
  // Establish relations
4237
- for (const relation of relations) {
4276
+ for (const [relationIndex, relation] of relations.entries()) {
4238
4277
  const { source, target, type, params = {} } = relation;
4239
4278
  const paramsStr = JSON.stringify(params);
4240
4279
  const specProblem = relationSpecProblem(type, params, target, 'no-page');
@@ -4264,7 +4303,10 @@ String(jsLiteral(type)) +
4264
4303
  String(jsLiteral(source)) +
4265
4304
  ", target: " +
4266
4305
  String(jsLiteral(target)) +
4267
- ", type: " + String(jsLiteral(type)) + " });\n } else {\n const __why = " + String(relationRefusalReasonJs(jsLiteral(type))) + ";\n (results.relationsRefused = results.relationsRefused || []).push({ source: " + String(jsLiteral(source)) + ", target: " + String(jsLiteral(target)) + ", type: " + String(jsLiteral(type)) + ", reason: __why || 'the studio refused it' });\n results.success = false;\n results.error = 'the studio refused ' + results.relationsRefused.length + ' relation(s): ' + results.relationsRefused.map(function(r) { return r.type + ' ' + r.source + ' -> ' + r.target + ' (' + r.reason + ')'; }).join('; ') + '. The items were created.';\n }\n }\n})();\n"));
4306
+ ", type: " +
4307
+ String(jsLiteral(type)) +
4308
+ " });\n const __o = { _: 0" +
4309
+ String(relationOptionFields(type, params, 'warning')) + " };\n if (__o.warning) (results.optionWarnings = results.optionWarnings || []).push(__o.warning);\n if (__o.ignoredOptions) (results.ignoredOptions = results.ignoredOptions || []).push.apply(results.ignoredOptions, __o.ignoredOptions.map(function(k) { return 'relations[" + String(relationIndex) + "].params.' + k; }));\n } else {\n const __why = " + String(relationRefusalReasonJs(jsLiteral(type))) + ";\n (results.relationsRefused = results.relationsRefused || []).push({ source: " + String(jsLiteral(source)) + ", target: " + String(jsLiteral(target)) + ", type: " + String(jsLiteral(type)) + ", reason: __why || 'the studio refused it' });\n results.success = false;\n results.error = 'the studio refused ' + results.relationsRefused.length + ' relation(s): ' + results.relationsRefused.map(function(r) { return r.type + ' ' + r.source + ' -> ' + r.target + ' (' + r.reason + ')'; }).join('; ') + '. The items were created.';\n }\n }\n})();\n"));
4268
4310
  }
4269
4311
  // Apply animations
4270
4312
  for (const animation of animations) {
@@ -5234,7 +5276,7 @@ const targetId = ${animItemRef};
5234
5276
  const item = app.getItemById(targetId);
5235
5277
  if (!item) throw new Error('Item not found: ' + targetId);
5236
5278
  app.animate(item, { animationType: ${JSON.stringify(String(op.animationType))}, ...${animOpts} });
5237
- return { itemId: targetId, animationType: ${JSON.stringify(String(op.animationType))}${(() => { const w = animationOptionWarning(op.animationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()} };
5279
+ return { itemId: targetId, animationType: ${JSON.stringify(String(op.animationType))}${(() => { const w = animationOptionWarning(op.animationOptions); return w ? `, ignoredOptions: ${jsLiteral(unreadKeys(ANIMATION_OPTION_KEYS, op.animationOptions))}, warning: ${jsLiteral(w)}` : ''; })()} };
5238
5280
  `;
5239
5281
  case 'relation':
5240
5282
  const srcRef = batchRef(op.sourceId);
@@ -5295,7 +5337,7 @@ String(type) +
5295
5337
  ", sourceIds: __failed, error: " +
5296
5338
  String(op.update === true ? '__why' : `'the studio refused the ' + ${type} + ' relation for ' + __failed.join(', ') + ' — nothing was bound' + (__why ? ': ' + __why : '.')`) +
5297
5339
  " };\n}\nreturn { success: true, relationType: " +
5298
- String(type) + ", " + String(op.update === true ? 'updated' : 'bound') + ": __done, targetId" + String(glob ? ', count: __members.length' : '') + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions, ['index', 'count', 'pattern']); return w ? `, optionWarning: ${jsLiteral(w)}` : ''; })()) + String(customOptionWarningJs(op.relationType, op.relationOptions, 'optionWarning', ['index', 'count', 'pattern'])) + ", ...(__failed.length ? { failed: __failed, warning: __failed.length + ' of ' + __members.length + ' were not " + String(op.update === true ? 'changed' : 'bound') + ": ' + __failed.join(', ') } : {}) };\n");
5340
+ String(type) + ", " + String(op.update === true ? 'updated' : 'bound') + ": __done, targetId" + String(glob ? ', count: __members.length' : '') + String(relationOptionFields(op.relationType, op.relationOptions, 'optionWarning', ['index', 'count', 'pattern'])) + ", ...(__failed.length ? { failed: __failed, warning: __failed.length + ' of ' + __members.length + ' were not " + String(op.update === true ? 'changed' : 'bound') + ": ' + __failed.join(', ') } : {}) };\n");
5299
5341
  }
5300
5342
  return ("\n" +
5301
5343
  String(stepGuard) +
@@ -5314,7 +5356,7 @@ String(relOpts) +
5314
5356
  ");\n// NOT BOUND IS NOT A SUCCESS (owner rule 1, B119). addRelation answers false\n// when it refuses — spring_follow \"not loaded yet\" — and this op returned\n// { relationId: false } with nothing marking it failed, so the batch said\n// success over a relation that does not exist.\nif (!relationId) {\n const __why = " +
5315
5357
  String(relationRefusalReasonJs(jsLiteral(op.relationType))) +
5316
5358
  ";\n return { success: false, relationId: false, sourceId, targetId, relationType: " +
5317
- String(JSON.stringify(op.relationType)) + ",\n error: 'the studio refused the ' + " + String(JSON.stringify(op.relationType)) + " + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };\n}\nreturn { relationId, sourceId, targetId, relationType: " + String(JSON.stringify(String(op.relationType))) + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()) + String(customOptionWarningJs(op.relationType, op.relationOptions, 'warning')) + " };\n");
5359
+ String(JSON.stringify(op.relationType)) + ",\n error: 'the studio refused the ' + " + String(JSON.stringify(op.relationType)) + " + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };\n}\nreturn { relationId, sourceId, targetId, relationType: " + String(JSON.stringify(String(op.relationType))) + String(relationOptionFields(op.relationType, op.relationOptions, 'warning')) + " };\n");
5318
5360
  case 'delete':
5319
5361
  const delRef = batchRef(op.itemId);
5320
5362
  return `
@@ -5402,8 +5444,9 @@ return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget}
5402
5444
  kfExtra.push(`timeUnits: ${jsLiteral(kfUnits)}`);
5403
5445
  const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
5404
5446
  const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
5405
- const kfTextCheck = kfTextKeysJs ? `\nconst _kfIgnored = ${kfTextKeysJs};` : '';
5406
- const kfIgnored = kfTextKeysJs ? `, ...(_kfIgnored.length ? { ignoredProperties: _kfIgnored, warning: _kfIgnored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE} } : {})` : '';
5447
+ const kfFilterKeysJs = unanimatedFilterKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
5448
+ const kfTextCheck = `\nconst _kfIgnored = ${kfTextKeysJs || '[]'};\nconst _kfFilterIgnored = ${kfFilterKeysJs};`;
5449
+ const kfIgnored = `, ...(_kfIgnored.length || _kfFilterIgnored.length ? { ignoredProperties: _kfIgnored.concat(_kfFilterIgnored), warning: [_kfIgnored.length ? _kfIgnored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE} : '', _kfFilterIgnored.length ? _kfFilterIgnored.join(', ') + ${UNANIMATED_FILTER_KEYS_SENTENCE} : ''].filter(Boolean).join(' ') } : {})`;
5407
5450
  return ("\nconst targetId = " +
5408
5451
  String(kfItemRef) +
5409
5452
  ";\nconst _target = (app.getItemById && app.getItemById(targetId))\n || (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(targetId));\nif (!_target) {\n return { success: false, itemId: targetId, error: 'Item not found: ' + targetId };\n}\nconst _kfItem = _target.item || _target;\nconst _prevKeys = (_kfItem.data && Array.isArray(_kfItem.data.keyframes)) ? _kfItem.data.keyframes.length : 0;\napp.addAnimation(targetId, " +
@@ -6294,9 +6337,14 @@ String((() => {
6294
6337
  String(JSON.stringify(format)) +
6295
6338
  ") !== -1) {\n try {\n const __hud = app.screenSpaceItems() || [];\n if (__hud.length) {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'screen_space_ignored', message: __hud.length + ' screen-space (HUD) item(s) are written at their frame coordinates in world space: " +
6296
6339
  String(format) +
6297
- " has no camera frame, so they do not stay fixed under the camera move. Export mp4, webm, gif or png to keep them fixed.' }]);\n }\n } catch (_) { /* the export stands */ }\n }\n // WHICH STUDIO MADE THIS FILE (D92): a service worker can serve a stale\n // build right after a deploy, and a retest then judges old code. The\n // engine's own identity, version + build sha, rides on every export reply.\n try { if (result && typeof result === 'object' && app.buildId) result.studioBuild = app.buildId; } catch (_) { /* older studio */ }\n // A STALE STUDIO (D92): the file was made by an old build. Said in the\n // fidelity warnings the reply shows, not only as a field.\n try {\n " +
6298
- " const __bs = app.buildStatus ? app.buildStatus() : null;\n if (result && typeof result === 'object' && __bs) {\n result.liveBuild = __bs.liveBuildId;\n if (__bs.stale) {\n result.stale = true;\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'stale_build', message: 'this studio is running an old build (' + __bs.buildId + '; the live build is ' + __bs.liveBuildId + '): this file was made by the old code, so it may differ from what the live build makes. pinepaper_clear_canvas (or pinepaper_agent_start_job) loads the live build; it empties the canvas, so rebuild the scene a" +
6299
- "nd export again if the difference matters.' }]);\n }\n }\n } catch (_) { /* older studio */ }\n // RELIGHT (FxTool D5): applied to png / pdf / mp4 / webm, not gif / apng\n // or the vector formats; and it is what makes a video export slow.\n if (result && result.success && typeof app.getRelight === 'function') {\n try {\n const __rl = app.getRelight();\n if (__rl && __rl.enabled) {\n result.fidelity = result.fidelity || { warnings: [] };\n if (['png', 'jpg', 'webp', 'pdf', 'mp4', 'webm'].indexOf(" +
6340
+ " has no camera frame, so they do not stay fixed under the camera move. Export mp4, webm, gif or png to keep them fixed.' }]);\n }\n } catch (_) { /* the export stands */ }\n }\n // A SOUND PAST THE END (SNDTAIL31): placed, accepted, and not in the file. The video's length decides, as it should; it is said.\n if (result && result.success && ['mp4', 'webm'].indexOf(" +
6341
+ String(JSON.stringify(format)) +
6342
+ ") !== -1 && app.itemRegistry && typeof app.itemRegistry.getAll === 'function') {\n try {\n const __end = (result.video && result.video.durationS) || " +
6343
+ String(JSON.stringify(duration ?? null)) +
6344
+ ";\n const __late = __end ? app.itemRegistry.getAll().filter(function (e) { const d = (e.item || e).data; return d && d.isSoundChip && typeof d.clipStartTime === 'number' && d.clipStartTime >= __end; }) : [];\n if (__late.length) {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'sound_after_end', message: __late.length + ' sound(s) start at or after the end of this ' + __end + ' s export, so they are not in the file: ' + __late.map(function (e) { return (e.item || e).data.registryId + ' at ' + (e.item || e).data.clipStartTime + ' s'; }).slice(0, 10).join(', ') + '.' }]);\n }" +
6345
+ "\n } catch (_) { /* the export stands */ }\n }\n // WHICH STUDIO MADE THIS FILE (D92): a service worker can serve a stale\n // build right after a deploy, and a retest then judges old code. The\n // engine's own identity, version + build sha, rides on every export reply.\n try { if (result && typeof result === 'object' && app.buildId) result.studioBuild = app.buildId; } catch (_) { /* older studio */ }\n // A STALE STUDIO (D92): the file was made by an old build. Said in the\n // fidelity warnings the reply shows, not only as a field.\n try {\n const __bs = app.buildStatus ? app.buildStatus() : null;\n if (result && typeof result === 'object' && __bs) {\n result.liveB" +
6346
+ "uild = __bs.liveBuildId;\n if (__bs.stale) {\n result.stale = true;\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'stale_build', message: 'this studio is running an old build (' + __bs.buildId + '; the live build is ' + __bs.liveBuildId + '): this file was made by the old code, so it may differ from what the live build makes. pinepaper_clear_canvas (or pinepaper_agent_start_job) loads the live build; it empties the canvas, so rebuild the scene and export again if the difference matters.' }]);\n }\n }\n } catch (_) { /* older studio */ }\n // RELIGHT (FxTool D5): appl" +
6347
+ "ied to png / pdf / mp4 / webm, not gif / apng\n // or the vector formats; and it is what makes a video export slow.\n if (result && result.success && typeof app.getRelight === 'function') {\n try {\n const __rl = app.getRelight();\n if (__rl && __rl.enabled) {\n result.fidelity = result.fidelity || { warnings: [] };\n if (['png', 'jpg', 'webp', 'pdf', 'mp4', 'webm'].indexOf(" +
6300
6348
  String(JSON.stringify(format)) +
6301
6349
  ") === -1) {\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'relight_not_applied', message: 'a relight rig is set, but ' + " +
6302
6350
  String(jsLiteral(format)) +
@@ -7343,6 +7391,12 @@ String(jsLiteral(format)) +
7343
7391
 
7344
7392
 
7345
7393
 
7394
+
7395
+
7396
+
7397
+
7398
+
7399
+
7346
7400
 
7347
7401
 
7348
7402
 
@@ -9353,7 +9407,9 @@ String(jsLiteral(templateId)) +
9353
9407
  ).trim();
9354
9408
  }
9355
9409
  generateImportImage(input) {
9356
- const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset, name } = input;
9410
+ const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset, name, opacity, blendMode, scale, loop } = input;
9411
+ const look = { ...(opacity !== undefined ? { opacity } : {}), ...(blendMode !== undefined ? { blendMode } : {}), ...(scale !== undefined ? { scale } : {}) };
9412
+ const hasLook = Object.keys(look).length > 0;
9357
9413
  // IMPORTSHADOW: the shadow keys, applied through the engine's own modify
9358
9414
  // door once the image is placed (and masked) — the route modify_item takes.
9359
9415
  const shadow = {};
@@ -9375,6 +9431,7 @@ String(jsLiteral(templateId)) +
9375
9431
  maxWidth !== undefined ? `maxWidth: ${maxWidth}` : '',
9376
9432
  maxHeight !== undefined ? `maxHeight: ${maxHeight}` : '',
9377
9433
  nativeSize ? 'nativeSize: true' : '',
9434
+ loop !== undefined ? `loop: ${JSON.stringify(loop)}` : '',
9378
9435
  ].filter(Boolean);
9379
9436
  const optsLiteral = optParts.length ? `{ ${optParts.join(', ')} }` : '{}';
9380
9437
  return ("\n// Import image from URL\n(async function() {\n if (!app.imageTools) {\n return { error: 'Image tools not available. Make sure PinePaper Studio is loaded.' };\n }\n try {\n // FETCH IT OURSELVES FIRST, to find out WHY when it fails.\n //\n // uploadFromURL sets img.src and rejects with a bare \"Failed to load image\n // from URL\" — an <img> onerror genuinely carries no reason, so that message\n // is the browser's limit rather than the engine being unhelpful. fetch()\n // has the reason: a status code, a CORS rejection, a refused connection, a\n // DNS failure. A caller cannot debug what the tool will not show them.\n //\n // The bytes then go in as a DA" +
@@ -9406,6 +9463,16 @@ String(mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask b
9406
9463
  if (app.itemRegistry && typeof app.itemRegistry.rebind === 'function') app.itemRegistry.rebind(itemId, masked);
9407
9464
  placed = masked;
9408
9465
  ` : '') +
9466
+ String(hasLook ? ` // opacity, blendMode, scale: read by modify, on what is drawn. They were not import arguments (OPACIMP31, BLENDIMP31, IMGSCALE31).
9467
+ let __look = null;
9468
+ {
9469
+ let __ok = false;
9470
+ try { __ok = app.modifyItem(itemId, ${JSON.stringify(look)}) !== false; } catch (_) { __ok = false; }
9471
+ __look = __ok
9472
+ ? { applied: true, opacity: placed.opacity, blendMode: placed.blendMode }
9473
+ : { applied: false, warning: 'the image was imported but ' + ${jsLiteral(Object.keys(look).join(', '))} + ' was not applied: set it with pinepaper_modify_item on ' + itemId + '.' };
9474
+ }
9475
+ ` : '') +
9409
9476
  String(hasShadow ? ` // The shadow goes on what is DRAWN, after a mask has replaced the raster.
9410
9477
  let __shadow = null;
9411
9478
  {
@@ -9428,6 +9495,8 @@ String(name ? `\n name: ${JSON.stringify(name)},` : '') +
9428
9495
  "\n bounds: { x: b.x, y: b.y, width: b.width, height: b.height }" +
9429
9496
  String(mask ? `,\n mask: ${jsLiteral(mask)}` : '') +
9430
9497
  String(hasShadow ? ',\n shadow: __shadow' : '') +
9498
+ String(loop !== undefined ? ",\n playback: (raster && raster.data && raster.data.animatedImage && raster.data.animatedImage.loop !== undefined) ? { applied: true, plays: raster.data.animatedImage.loop } : { applied: false, warning: 'loop was not applied: this image does not animate in this studio. Animated GIF and WebP do.' }" : '') +
9499
+ String(hasLook ? ',\n look: __look' : '') +
9431
9500
  ",\n // Stored smaller than its source (the studio's cap): said, with the\n // way to keep every pixel. The studio reports native size where it can.\n ...((entry && entry.nativeWidth && entry.width && entry.nativeWidth > entry.width)\n ? { stored: { width: entry.width, height: entry.height }, native: { width: entry.nativeWidth, height: entry.nativeHeight },\n note: 'the studio stored this image at ' + entry.width + 'x' + entry.height + ' (its source is ' + entry.nativeWidth + 'x' + entry.nativeHeight + '). Pass maxEdge: \"native\" to keep every pixel.' }\n : {})\n };\n } catch (e) {\n return { error: 'Failed to import image: ' + e.message };\n " +
9432
9501
  " }\n})();\n"
9433
9502
 
@@ -9519,6 +9588,12 @@ String(hasShadow ? ',\n shadow: __shadow' : '') +
9519
9588
 
9520
9589
 
9521
9590
 
9591
+
9592
+
9593
+
9594
+
9595
+
9596
+
9522
9597
 
9523
9598
 
9524
9599
 
@@ -16238,10 +16313,8 @@ String(op) +
16238
16313
  }
16239
16314
  /**
16240
16315
  * Rules, variables and bound text (the studio's contraption engine). Each action asks for the method it calls, so a
16241
- * studio without it refuses by name instead of a TypeError. The engine's animateItem, playSound and custom actions are
16242
- * not offered: animateItem goes through app.modify, which acts on the selection and not on its target; playSound does
16243
- * not call app.playSound the way that method is written; custom needs a function, which JSON cannot carry. All three
16244
- * answer ok regardless, so a rule using them would report success for nothing.
16316
+ * studio without it refuses by name instead of a TypeError. The engine's custom action is not offered: it needs a
16317
+ * function, which JSON cannot carry.
16245
16318
  */
16246
16319
  function generateContraptionCode(input) {
16247
16320
  const refuse = (method) => `if (typeof app.${method} !== 'function') {
@@ -16252,7 +16325,7 @@ function generateContraptionCode(input) {
16252
16325
  const body = () => {
16253
16326
  switch (input.action) {
16254
16327
  case 'add_contraption': {
16255
- const targets = [...new Set((input.actions ?? []).flatMap((a) => (a.type === 'modifyItem' ? [a.targetId] : [])))];
16328
+ const targets = [...new Set((input.actions ?? []).flatMap((a) => ('targetId' in a ? [a.targetId] : [])))];
16256
16329
  const config = {
16257
16330
  ...(input.contraptionId ? { id: input.contraptionId } : {}),
16258
16331
  sourceEvent: input.sourceEvent,
@@ -16266,14 +16339,20 @@ function generateContraptionCode(input) {
16266
16339
  const contraptionId = app.addContraption(${jsLiteral(config)});
16267
16340
  const missing = ${jsLiteral(targets)}.filter((id) => !app.getItemById(id));
16268
16341
  return { success: true, action: 'add_contraption', contraptionId,
16269
- ...(missing.length ? { warning: 'no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does' } : {}),
16270
- ${targets.length ? "note: 'changes a rule makes to an item stay: nothing rewinds them, so another export starts from the changed scene'" : ''} };`;
16342
+ ...(missing.length ? { warning: 'no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does' } : {}), };`;
16271
16343
  }
16272
16344
  case 'remove_contraption':
16273
16345
  return `${refuse('removeContraption')}
16274
16346
  const removed = app.removeContraption(${jsLiteral(input.contraptionId)});
16275
16347
  return removed ? { success: true, action: 'remove_contraption', contraptionId: ${jsLiteral(input.contraptionId)} }
16276
16348
  : { success: false, action: 'remove_contraption', error: 'no rule has the id ' + ${jsLiteral(input.contraptionId)} };`;
16349
+ case 'clear_contraptions':
16350
+ return `${refuse('clearContraptions')}
16351
+ app.clearContraptions();
16352
+ return { success: true, action: 'clear_contraptions' };`;
16353
+ case 'clear_variables':
16354
+ return `${refuse('clearVariables')}
16355
+ return { success: true, action: 'clear_variables', scope: ${scope}, removed: app.clearVariables(${scope}) };`;
16277
16356
  case 'list_contraptions':
16278
16357
  return `${refuse('listContraptions')}
16279
16358
  return { success: true, action: 'list_contraptions', contraptions: app.listContraptions() };`;