@pinepaper.studio/mcp-server 1.6.35 → 1.6.37

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 (67) 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 +139 -7
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/agent/types.d.ts +12 -12
  11. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  12. package/dist/browser/puppeteer-controller.js +7 -1
  13. package/dist/browser/puppeteer-controller.js.map +1 -1
  14. package/dist/build-info.json +2 -2
  15. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  16. package/dist/ontology/upstream-vocabulary.js +2 -2
  17. package/dist/tools/definitions.d.ts.map +1 -1
  18. package/dist/tools/definitions.js +141 -91
  19. package/dist/tools/definitions.js.map +1 -1
  20. package/dist/tools/effect-class.d.ts.map +1 -1
  21. package/dist/tools/effect-class.js +4 -0
  22. package/dist/tools/effect-class.js.map +1 -1
  23. package/dist/tools/engine-lists.d.ts +30 -3
  24. package/dist/tools/engine-lists.d.ts.map +1 -1
  25. package/dist/tools/engine-lists.js +346 -4
  26. package/dist/tools/engine-lists.js.map +1 -1
  27. package/dist/tools/engine-methods.d.ts.map +1 -1
  28. package/dist/tools/engine-methods.js +4 -0
  29. package/dist/tools/engine-methods.js.map +1 -1
  30. package/dist/tools/engine-surface.d.ts +1 -1
  31. package/dist/tools/engine-surface.d.ts.map +1 -1
  32. package/dist/tools/engine-surface.js +29 -7
  33. package/dist/tools/engine-surface.js.map +1 -1
  34. package/dist/tools/export-file-receipt.d.ts +33 -0
  35. package/dist/tools/export-file-receipt.d.ts.map +1 -1
  36. package/dist/tools/export-file-receipt.js +49 -0
  37. package/dist/tools/export-file-receipt.js.map +1 -1
  38. package/dist/tools/frozen-output.d.ts +33 -0
  39. package/dist/tools/frozen-output.d.ts.map +1 -0
  40. package/dist/tools/frozen-output.js +73 -0
  41. package/dist/tools/frozen-output.js.map +1 -0
  42. package/dist/tools/handlers.d.ts +15 -0
  43. package/dist/tools/handlers.d.ts.map +1 -1
  44. package/dist/tools/handlers.js +141 -15
  45. package/dist/tools/handlers.js.map +1 -1
  46. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  47. package/dist/tools/minimal-descriptions.js +1 -0
  48. package/dist/tools/minimal-descriptions.js.map +1 -1
  49. package/dist/tools/shape-params.d.ts +2 -2
  50. package/dist/tools/shape-params.js +2 -2
  51. package/dist/tools/stick-vocabulary.d.ts +1 -1
  52. package/dist/tools/stick-vocabulary.js +1 -1
  53. package/dist/tools/toolkits.d.ts.map +1 -1
  54. package/dist/tools/toolkits.js +2 -1
  55. package/dist/tools/toolkits.js.map +1 -1
  56. package/dist/types/code-generator.d.ts +18 -2
  57. package/dist/types/code-generator.d.ts.map +1 -1
  58. package/dist/types/code-generator.js +462 -198
  59. package/dist/types/code-generator.js.map +1 -1
  60. package/dist/types/schemas.d.ts +112 -270
  61. package/dist/types/schemas.d.ts.map +1 -1
  62. package/dist/types/schemas.js +46 -145
  63. package/dist/types/schemas.js.map +1 -1
  64. package/dist/utils/asset-paths.js +1 -1
  65. package/dist/utils/asset-paths.js.map +1 -1
  66. package/manifest.json +5 -1
  67. package/package.json +1 -1
@@ -502,13 +502,14 @@ const __scale = (function(it) {
502
502
  if (lineIgnoresPosition) {
503
503
  warnings.push('a line runs from `from` to `to` in canvas coordinates, so position did not move it: position is where the line is now. Give the endpoints where the line goes.');
504
504
  }
505
- code += ("\nconst itemId = item.data.registryId;\napp.historyManager.saveState();\n\n// Return item info\n({ itemId, type: " +
505
+ code += ("\nconst itemId = item.data.registryId;\n// Debounced where the studio has it: a full snapshot per create made one-call-per-item building O(items) a call.\napp.historyManager[app.historyManager.saveStateDebounced ? 'saveStateDebounced' : 'saveState']();\n\n// Return item info\n({ itemId, type: " +
506
506
  String(jsLiteral(itemType)) +
507
507
  String(outlineOnly ? `, fill: ${JSON.stringify(OUTLINE_NOTE)}` : '') +
508
508
  String(textFit ? ', textFit: __textFit' : '') +
509
509
  String(textDirection ? ', direction: __direction' : '') +
510
510
  String(textTabular ? ', tabularFigures: __tabular' : '') +
511
- String(textTypography.length ? ', typography: __typography' : '') + String(createFillOpacity ? ', fillOpacity: __fillOpacity' : '') + String(textStrokePos ? ', strokePosition: __strokePos' : '') + String(hasCreateScale ? ', scale: __scale' : '') + ", position: " + String(reportActualPosition ? `(item.position ? { x: item.position.x, y: item.position.y } : null)` : `{ x: ${position.x}, y: ${position.y} }`) + String(hasLifetime ? ', lifetime: __lifetime' : '') + String(loadsFont ? ', ...(__font ? { font: __font } : {})' : '') + String(ignored.length > 0
511
+ String(textTypography.length ? ', typography: __typography' : '') +
512
+ String(createFillOpacity ? ', fillOpacity: __fillOpacity' : '') + String(textStrokePos ? ', strokePosition: __strokePos' : '') + String(hasCreateScale ? ', scale: __scale' : '') + ", position: " + String(reportActualPosition ? `(item.position ? { x: item.position.x, y: item.position.y } : null)` : `{ x: ${position.x}, y: ${position.y} }`) + String(hasLifetime ? ', lifetime: __lifetime' : '') + String(loadsFont ? ', ...(__font ? { font: __font } : {})' : '') + String(ignored.length > 0
512
513
  ? `, ignoredProperties: ${JSON.stringify(ignored)}` : '') + String(warnings.length > 0 ? `, warning: ${JSON.stringify(warnings.join(' '))}` : '') + " });");
513
514
  // Loading a font is async, and it has to happen BEFORE create() measures the
514
515
  // text — so only this case becomes an async IIFE; every other create stays a
@@ -1577,15 +1578,19 @@ const EVENT_PULSING_RELATIONS = new Set(['on_complete', 'at_beat']);
1577
1578
  function eventNameResolveJs(relationType, paramsVar) {
1578
1579
  if (!EVENT_PULSING_RELATIONS.has(String(relationType)))
1579
1580
  return 'const __ev = {};';
1580
- return `const __ev = {};
1581
- (function(p) {
1582
- if (!p || typeof p.event !== 'string' || !p.event || app.getItemById(p.event)) return;
1583
- const ids = (app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : [])
1584
- .filter(function(e) { return e && e.item && e.item.data && e.item.data.isEvent && e.item.data.eventName === p.event; })
1585
- .map(function(e) { return e.id || e.itemId; });
1586
- if (ids.length === 1) { __ev.eventResolvedFrom = p.event; __ev.eventResolvedTo = ids[0]; p.event = ids[0]; }
1587
- else if (ids.length > 1) __ev.warning = ids.length + ' events are named ' + JSON.stringify(p.event) + ' (' + ids.join(', ') + '), so params.event cannot say which one: it pulses the literal name, which no reaction bound to an event hears. Pass one event id as params.event.';
1588
- })(${paramsVar});`;
1581
+ return ("const __ev = {};\n (function(p) {\n if (!p || typeof p.event !== 'string' || !p.event) return;\n // An id, not a name: getItemById also finds an item by its NAME, so a hit alone said 'done' was an id and the\n // name was never resolved (ONCOMP35: no eventResolvedTo in eight sessions). Only an exact registry id counts.\n const __hit = app.getItemById(p.event);\n if (__hit && __hit.data && (__hit.data.registryId === p.event || __hit.data.id === p.event)) return;\n const ids = (app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : [])\n .filter(function(e) { return e && e.item && e.item.data && e.item.data.isEvent && e.item.data.eventName === p.eve" +
1582
+ "nt; })\n .map(function(e) { return e.id || e.itemId; });\n if (ids.length === 1) { __ev.eventResolvedFrom = p.event; __ev.eventResolvedTo = ids[0]; p.event = ids[0]; }\n else if (ids.length > 1) __ev.warning = ids.length + ' events are named ' + JSON.stringify(p.event) + ' (' + ids.join(', ') + '), so params.event cannot say which one: it pulses the literal name, which no reaction bound to an event hears. Pass one event id as params.event.';\n })(" +
1583
+ String(paramsVar) +
1584
+ ");"
1585
+
1586
+
1587
+
1588
+
1589
+
1590
+
1591
+
1592
+
1593
+ );
1589
1594
  }
1590
1595
  /** Spread into a reply after its own fields: the resolve fields, with an event warning joined onto any warning. */
1591
1596
  const EV_MERGE_JS = (replyVar, key = 'warning') => `Object.keys(__ev).forEach(function(k) { if (k !== 'warning') ${replyVar}[k] = __ev[k]; });
@@ -1745,6 +1750,17 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
1745
1750
  if (own.count !== undefined || own.velocities !== undefined)
1746
1751
  mismatch(`bursts[${i}]`, own.count ?? out.count, own.velocities ?? out.velocities);
1747
1752
  });
1753
+ // SPAWN IS A BOX ON THE CANVAS AXES (ParticleEmitter: x += (u - 0.5) * spawn[0], y += (u - 0.5) * spawn[1]), not a
1754
+ // range and not along the burst's angle. spawn [0, 200] beside angle 0 drew a vertical column of particles that
1755
+ // then flew right (SPAWNAX36). Said once, where a spawn box meets an aimed angle.
1756
+ const fullCircle = (a) => a === undefined || (Array.isArray(a) && Number(a[1]) - Number(a[0]) >= 360);
1757
+ const aimed = [{ where: 'spec', own: out }, ...bursts.map((b, i) => ({ where: `bursts[${i}]`, own: (b && typeof b === 'object' ? b : {}) }))]
1758
+ .find(({ own }) => (own.spawn ?? out.spawn) !== undefined && !fullCircle(own.angle ?? out.angle));
1759
+ if (aimed) {
1760
+ const sp = aimed.own.spawn ?? out.spawn;
1761
+ const box = Array.isArray(sp) ? `${sp[0]} wide and ${sp[1]} tall` : `${sp} by ${sp}`;
1762
+ warnings.push(`${aimed.where}: spawn is a box on the canvas axes centred on the burst point (here ${box}), not a range and not turned to the burst's angle: particles start anywhere in it and fly along angle. To spread them across the direction of travel, size the box on that axis (spawn [w, 0] spreads along x, [0, h] along y).`);
1763
+ }
1748
1764
  // A RANGE WHERE THE STUDIO READS ONE NUMBER is not drawn as a range (1.6.24
1749
1765
  // probe: count [a, b] → 0 on older studios, the middle on FxTool #228). Only
1750
1766
  // the keys that take one number on EVERY build are named here. gravity, drag,
@@ -2204,6 +2220,58 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
2204
2220
  : '';
2205
2221
  return `${easingNote}${sizekeyPairNote}${gapNote}`;
2206
2222
  }
2223
+ /**
2224
+ * READ BACK THE KEY TIMES (LONGKEY36). A studio that rescales keyframe times (one that reads any time over 100 as
2225
+ * milliseconds, whatever timeUnits says) stored a 120 s track as 0.12 s, and the export froze at its end state from
2226
+ * the first half second while every reply said success. After the write, each asked time must be on the stored
2227
+ * track. If it is not: a track that spans 100 s or less is rewritten from 0 with its first key as the clip's
2228
+ * timeOffset (which the studio keeps), so it plays at the asked times; otherwise the earlier track is put back and
2229
+ * the call fails with the times asked and stored. A studio that keeps the times passes on the first read.
2230
+ *
2231
+ * `before` goes just before the addAnimation call, `after` just after it (it may `return` a failure), and `field`
2232
+ * is spread into the success reply.
2233
+ */
2234
+ function keyTimeGuard(a) {
2235
+ const scale = a.timeUnits === 'ms' ? 0.001 : 1;
2236
+ const asked = a.keyframes.map((k) => (Number(k.time) || 0) * scale).sort((x, y) => x - y);
2237
+ const first = asked.length ? asked[0] : 0;
2238
+ const end = asked.length ? asked[asked.length - 1] : 0;
2239
+ const canShift = !a.append && !a.timeOffsetGiven && first > 0 && end - first <= 100;
2240
+ const shiftedKfs = canShift ? normalizeKeyframePositions(a.keyframes.map((k) => ({ ...k, time: (Number(k.time) || 0) * scale - first }))) : [];
2241
+ const shiftedOpts = canShift ? { ...a.opts, timeUnits: 'seconds', timeOffset: first, duration: Math.max(end - first, (Number(a.duration) || 0) * scale - first) } : {};
2242
+ const advice = end - first > 100
2243
+ ? 'This studio cannot hold a keyframe track whose keys span more than 100 s. Split the motion: key the first part on this item and the rest on a copy that starts later (timeOffset), or keep each track within 100 s.'
2244
+ : a.append
2245
+ ? 'This studio cannot hold a key past 100 s in a merged track. Key the late part on its own track with timeOffset instead of appending.'
2246
+ : 'Keep each track within 100 s, or start it later with timeOffset.';
2247
+ const it = a.item;
2248
+ const before = `
2249
+ // The whole earlier track, so a write the studio got wrong can be undone.
2250
+ const __prevKfs = (${it} && ${it}.data && Array.isArray(${it}.data.keyframes)) ? ${it}.data.keyframes.map(function(k) { return Object.assign({}, k, { properties: Object.assign({}, k && k.properties) }); }) : [];`;
2251
+ const after = ("\n const __asked = " +
2252
+ String(JSON.stringify(asked)) +
2253
+ ";\n const __storedTimes = function() { return (" +
2254
+ String(it) +
2255
+ " && " +
2256
+ String(it) +
2257
+ ".data && Array.isArray(" +
2258
+ String(it) +
2259
+ ".data.keyframes)) ? " +
2260
+ String(it) +
2261
+ ".data.keyframes.map(function(k) { return k && k.time; }) : []; };\n const __landed = function(shift) { const st = __storedTimes(); return __asked.every(function(t) { return st.some(function(x) { return Math.abs(x + shift - t) < 1e-6; }); }); };\n let __shifted = null;\n // Only a track the studio wrote and this can read is checked; one stored elsewhere is not judged.\n if (" +
2262
+ String(it) +
2263
+ " && __storedTimes().length && !__landed(0)) {\n const __wrong = __storedTimes();\n " +
2264
+ String(canShift ? `app.addAnimation(${a.target}, ${JSON.stringify(shiftedKfs)}, ${JSON.stringify(shiftedOpts)});
2265
+ if (__landed(${first}) && Math.abs((Number(${it}.data.timeOffset) || 0) - ${first}) < 1e-6) {
2266
+ __shifted = { by: ${first}, note: 'this studio cannot hold a key past 100 s in one track, so the keys were written from 0 and the track starts at ${first} s (timeOffset). It plays at the times asked.' };
2267
+ } else ` : '') +
2268
+ "{\n if (__prevKfs.length) app.addAnimation(" +
2269
+ String(a.target) +
2270
+ ", __prevKfs, { timeUnits: 'seconds', loop: " +
2271
+ String(a.loop === true) +
2272
+ " });\n else if (typeof app.clearKeyframes === 'function') app.clearKeyframes(" + String(it) + ");\n return { success: false, itemId: " + String(a.target) + ", code: 'keyframe_times_not_kept',\n error: 'the studio did not keep the key times: asked ' + __asked[0] + '..' + __asked[__asked.length - 1] + ' s, it stored ' + __wrong.slice(0, 3).join(', ') + (__wrong.length > 3 ? ', ...' : '') + ' s. ' + " + String(JSON.stringify(advice)) + " + (__prevKfs.length ? ' The earlier track was put back.' : ' The track was removed.'),\n askedTimes: __asked.slice(0, 20), storedTimes: __wrong.slice(0, 20) };\n }\n }");
2273
+ return { before, after, field: ', ...(__shifted ? { shifted: __shifted } : {})' };
2274
+ }
2207
2275
  function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
2208
2276
  const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
2209
2277
  const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
@@ -2215,8 +2283,10 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
2215
2283
  opts.clipInPoint = clipInPoint;
2216
2284
  if (clipOutPoint !== undefined)
2217
2285
  opts.clipOutPoint = clipOutPoint;
2218
- if (timeUnits !== undefined)
2219
- opts.timeUnits = timeUnits;
2286
+ // TIMES ARE SECONDS UNLESS THE CALLER SAYS ms. Left unset, the studio guesses (TimeUnits.js): any time over 100
2287
+ // made it read EVERY time as milliseconds, so a 120 s track ran in 0.12 s and the export showed its end state from
2288
+ // the first half second, reported as a success (GRIEF). The tool documents seconds, so the server says seconds.
2289
+ opts.timeUnits = timeUnits ?? 'seconds';
2220
2290
  // addAnimation SETS the item's track; mode 'merge' adds to it (FxTool
2221
2291
  // 2475333b). An engine without merge ignores the option and replaces.
2222
2292
  if (append)
@@ -2266,29 +2336,47 @@ String(JSON.stringify(levelKeys)) +
2266
2336
  const easingAtCheck = easingAt !== undefined ? `
2267
2337
  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.');` : '';
2268
2338
  const runtimeWarning = `, ...(${textKeysJs ? '__ignored.length || ' : ''}__filterIgnored.length ? { ignoredProperties: ${textKeysJs ? '__ignored.concat(__filterIgnored)' : '__filterIgnored'} } : {}), ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
2339
+ // Said where the old guess would have read this track differently: a time over 100 with no unit given.
2340
+ const __lastTime = Math.max(0, ...keyframes.map((k) => Number(k.time) || 0), Number(duration) || 0);
2341
+ const unitsNote = timeUnits === undefined && __lastTime > 100
2342
+ ? `\n __warn.push(${JSON.stringify(`times are read as seconds: the track ends at ${__lastTime} s (${(__lastTime / 60).toFixed(1)} min). If you meant milliseconds, pass timeUnits: 'ms'.`)});`
2343
+ : '';
2269
2344
  const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `${ID}`;
2345
+ const guard = keyTimeGuard({ target: addAnimTarget, item: '__ki', keyframes, timeUnits, append, timeOffsetGiven: timeOffset !== undefined, loop, opts, duration: calculatedDuration });
2270
2346
  return ("\n// Apply keyframe animation to " +
2271
2347
  String(ID) +
2272
2348
  "\n(function() {\n " +
2273
2349
  String(requireItem(itemId, 'the animation')) +
2274
2350
  String(audioLevelGuard) +
2275
- "\n // The track as it was, so the result can say what happened to it: merged\n // (every earlier key still there) or replaced. That is the evidence of\n // merge support — an older engine takes the option and replaces anyway.\n const __ki = _target && (_target.item || _target);\n const __prev = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];\n const __opts = " +
2351
+ "\n // The track as it was, so the result can say what happened to it: merged\n // (every earlier key still there) or replaced. That is the evidence of\n // merge support — an older engine takes the option and replaces anyway.\n const __ki = _target && (_target.item || _target);\n const __prev = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];\n // SQUASH35: a merge whose new key lands on an existing key's time takes the new key's easing, in silence. The\n // earlier keys' easings, by time, so a collision that changed one can be named.\n const __prevEase = (__ki && __ki.data && Array.isArray(__ki.data.keyf" +
2352
+ "rames)) ? __ki.data.keyframes.map(function(k) { return [k && k.time, (k && k.easing) || 'linear']; }) : [];\n const __opts = " +
2276
2353
  String(JSON.stringify(opts)) +
2277
- ";\n app.addAnimation(" +
2354
+ ";\n" +
2355
+ String(guard.before) +
2356
+ "\n app.addAnimation(" +
2278
2357
  String(addAnimTarget) +
2279
2358
  ", " +
2280
2359
  String(keyframesJson) +
2281
2360
  ", __opts);\n const __warn = [];" +
2361
+ String(unitsNote) +
2362
+ "\n" +
2363
+ String(guard.after) +
2282
2364
  String(easingAtCheck) +
2283
2365
  String(textKeyCheck) +
2284
2366
  String(filterKeyCheck) +
2285
2367
  "\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; });" +
2286
- String(append ? `
2287
- // Property gaps across the merged track (the same check a single call makes
2288
- // on its own keys at generation time).
2289
- const __all = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes : [];
2290
- 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; }, []);
2291
- 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; }) : [];` : '') +
2368
+ String(append ? ("\n // Property gaps across the merged track (the same check a single call makes\n // on its own keys at generation time).\n const __all = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes : [];\n 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; }, []);\n const __collide = __prevEase.map(function(pe) {\n const k = __all.find(function(n) { return n && n.time === pe[0]; });\n const ne = k ? (k.easing || 'linear') : null;\n return (ne && ne !== pe[1]) ? { time: pe[0], was: pe[1], now: ne } : null;\n }).filter(Boolean);\n if (__collide.le" +
2369
+ "ngth) __warn.push('append: ' + __collide.length + ' new key(s) landed on the time of an existing key and replaced its easing (' + __collide.slice(0, 5).map(function(c) { return 't=' + c.time + ' ' + c.was + ' -> ' + c.now; }).join(', ') + '). Pass the easing you want on the colliding key, or move the new key off that time.');\n 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; }) : [];"
2370
+
2371
+
2372
+
2373
+
2374
+
2375
+
2376
+
2377
+
2378
+
2379
+ ) : '') +
2292
2380
  "\n const __track = !__prev.length ? null\n : " +
2293
2381
  String(append ? `(__kept
2294
2382
  ? { appended: true, previousKeys: __prev.length, keys: __now.length }
@@ -2301,7 +2389,12 @@ String(calculatedDuration) +
2301
2389
  ", loop: " +
2302
2390
  String(loop) +
2303
2391
  String(append ? ', append: true' : '') +
2304
- ", ...(__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();
2392
+ String(guard.field) +
2393
+ ", ...(__track ? { track: __track } : {})" +
2394
+ 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.' } : {})` : '') +
2395
+ String(timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : '') +
2396
+ String(clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : '') +
2397
+ String(clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : '') + String(__notes) + String(runtimeWarning) + " };\n})();\n").trim();
2305
2398
  }
2306
2399
  /**
2307
2400
  * Template for executing generators
@@ -2392,15 +2485,26 @@ String(itemId ? `
2392
2485
  if (!__si) return { success: false, error: ${JSON.stringify(`region.shape names no item "${itemId}" — nothing was drawn. List items with pinepaper_get_items.`)} };
2393
2486
  if (__si.className !== 'Path' && __si.className !== 'CompoundPath') return { success: false, error: ${JSON.stringify(`region.shape item "${itemId}" is a `)} + __si.className + ', and a clip needs a path (a shape, not text, a group or an image) — nothing was drawn.' };
2394
2487
  }` : '') +
2395
- "\n await app.executeGenerator(" +
2396
- String(jsLiteral(generatorName)) +
2397
- ", " +
2488
+ "\n // FOREGROUND GENERATORS DEFAULT TO EDITABLE PARTS (owner, 2026-11-14). The studio files ui, infographics and\n // captions generators as foreground; with interactive on, each part (a bubble, a button, a label) is its own item,\n // which is what a phone frame or a chat thread is for, and what pinepaper_group ungroup releases. The studio's\n // own default is false for every generator, so unless the caller says, a foreground one runs with true.\n const __gp = " +
2398
2489
  String(paramsJson) +
2399
- ", " +
2490
+ ";\n let __interactiveDefault = false;\n if (__gp && __gp.interactive === undefined && app.generatorRegistry && app.generatorRegistry.get) {\n const __gd = app.generatorRegistry.get(" +
2491
+ String(jsLiteral(generatorName)) +
2492
+ ");\n const __gc = __gd && __gd.category;\n if (__gc === 'ui' || __gc === 'infographics' || __gc === 'captions') { __gp.interactive = true; __interactiveDefault = true; }\n }\n await app.executeGenerator(" +
2493
+ String(jsLiteral(generatorName)) +
2494
+ ", __gp, " +
2400
2495
  String(options) +
2401
2496
  ");" +
2402
2497
  String(shaped ? `
2403
- const __shapes = !!(app.generatorRegistry && typeof app.generatorRegistry._regionClipPath === 'function');` : '') + "\n return { success: true, generator: " + String(jsLiteral(generatorName)) + String(region ? `, region: ${JSON.stringify(region)}` : '') + String(shaped ? `, ...(__shapes ? {} : { regionShapeApplied: false, warning: ${JSON.stringify(`this studio does not clip generator regions to a shape, so ${typeof region.shape === 'object' ? 'the region had no box and the generator drew across the whole canvas' : `the ${region.shape} was drawn as a rectangle`}. It needs an engine with region shapes.`)} })` : '') + " };");
2498
+ const __shapes = !!(app.generatorRegistry && typeof app.generatorRegistry._regionClipPath === 'function');` : '') +
2499
+ "\n return { success: true, generator: " +
2500
+ String(jsLiteral(generatorName)) +
2501
+ ", ...(__interactiveDefault ? { interactive: true, interactiveNote: 'a foreground generator: its parts are editable items (interactive: true by default for ui, infographics and captions generators). Pass params.interactive: false for one flattened composition.' } : {})" +
2502
+ String(region ? `, region: ${JSON.stringify(region)}` : '') +
2503
+ String(shaped ? `, ...(__shapes ? {} : { regionShapeApplied: false, warning: ${JSON.stringify(`this studio does not clip generator regions to a shape, so ${typeof region.shape === 'object' ? 'the region had no box and the generator drew across the whole canvas' : `the ${region.shape} was drawn as a rectangle`}. It needs an engine with region shapes.`)} })` : '') +
2504
+ " };"
2505
+
2506
+
2507
+ );
2404
2508
  }
2405
2509
  /**
2406
2510
  * Template for applying effects
@@ -3172,6 +3276,15 @@ function asReturningBody(snippet) {
3172
3276
  * out, and the batch saves once at the end, which is what batchCreate was
3173
3277
  * for. An item that fails is reported by index; the rest are still created.
3174
3278
  */
3279
+ /**
3280
+ * A create inside a batch skips the engine's per-item select, snapshot and redraw (`_bulkCreate`, the template
3281
+ * loader's path); the batch saves and draws once at the end. A governed run gets this from the engine already, but a
3282
+ * batch over the governor's size limit runs ungoverned, and there every create redrew the whole scene: 400 creates
3283
+ * on 2,000 items spent 2 of 5 s in full redraws.
3284
+ */
3285
+ function asBulkCreate(code) {
3286
+ return code.replace(/(const item = app\.create\((?:\x22[^\x22\n]*\x22|\x27[^\x27\n]*\x27), )\{/g, '$1{ _bulkCreate: true, ');
3287
+ }
3175
3288
  function generateBatchCreateCode(items) {
3176
3289
  const blocks = items.map((it, i) => {
3177
3290
  const { x, y, position, ...props } = (it.params || {});
@@ -3179,8 +3292,8 @@ function generateBatchCreateCode(items) {
3179
3292
  : (position && typeof position === 'object') ? { x: Number(position.x), y: Number(position.y) }
3180
3293
  : { x: typeof x === 'number' ? x : 400, y: typeof y === 'number' ? y : 300 };
3181
3294
  const given = position !== undefined || typeof x === 'number' || typeof y === 'number';
3182
- const body = asReturningBody(generateCreateItemCode(it.type, p, props, undefined, given))
3183
- .replace(/app\.historyManager\.saveState\(\);/g, '');
3295
+ const body = asBulkCreate(asReturningBody(generateCreateItemCode(it.type, p, props, undefined, given)))
3296
+ .replace(/app\.historyManager(?:\.saveState\(\)|\[app\.historyManager\.saveStateDebounced \? 'saveStateDebounced' : 'saveState'\]\(\));/g, '');
3184
3297
  return `
3185
3298
  try {
3186
3299
  const r${i} = await (async function() {
@@ -3197,6 +3310,8 @@ ${body}
3197
3310
  const errors = [];
3198
3311
  ${blocks.join('\n')}
3199
3312
  if (app.historyManager) app.historyManager.saveState();
3313
+ // Bulk creates skipped their redraws: one for the batch.
3314
+ try { if (typeof paper !== 'undefined' && paper.view) paper.view.draw(); } catch (_) { /* the frame loop draws */ }
3200
3315
  return { success: errors.length === 0, items: results, count: results.length, errors: errors };
3201
3316
  })();`.trim();
3202
3317
  }
@@ -5092,7 +5207,41 @@ String(oneLine(operations.length)) +
5092
5207
  "t): create and group ops take a slot each, in op\n // order (O2, mcp-server#29: a group is an item every later op addresses, and it had no slot). The\n // previous batch's ids are kept only to name the right id when a $N reaches\n // past what this batch has made: picking it silently shifted every $N in a\n // batch that also creates (B115 follow-up).\n const itemIds = [];\n const __prevBatchIds = ((typeof window !== 'undefined' && window.__pinepaper_batch_item_ids) || []).slice();\n const __ref = function (n) {\n if (itemIds[n] !== undefined) return itemIds[n];\n const prev = __prevBatchIds[n];\n throw new Error('$' + n + ' is item number ' + n + ' THIS batch made (create " +
5093
5208
  "and group ops are numbered, in op order, from $0), and it has made ' + itemIds.length + ' so far.'\n + (prev ? ' In the previous batch $' + n + ' was ' + prev + ' — pass that id instead.' : ' Put the ops that make it first, or pass the item id.'));\n };\n // A failure names the item, not the \"$1\" that addressed it; the ref as written when the batch never made it.\n const __refName = function (n) { return itemIds[n] !== undefined ? itemIds[n] : '$' + n; };\n const results = [];\n let success = true;\n // A failure's words, never \"undefined\" for a thrown non-Error.\n const _why = (e, fb) => (e && typeof e.message === 'string' && e.message.trim()) ? e.message\n : (typeof e " +
5094
5209
  "=== 'string' && e.trim()) ? e : fb;\n // THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything\n // failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the\n // result. A studio without lintScene says nothing.\n const __ppLintNow = function () {\n try {\n if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];\n const r = app.lintScene({ warningsOnly: true });\n return r && Array.isArray(r.warnings) ? r.warnings : [];\n } catch (e) {\n return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could no" +
5095
- "t run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];\n }\n };\n // Every failed op as { step, itemId, reason }, beside the per-op results.\n const _failures = () => results.filter((r) => !r.success).map((r) => ({ step: r.step, itemId: r.itemId, reason: r.reason }));\n\n try {\n"
5210
+ "t run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];\n }\n };\n // Every failed op as { step, itemId, reason }, beside the per-op results.\n const _failures = () => results.filter((r) => !r.success).map((r) => ({ step: r.step, itemId: r.itemId, reason: r.reason }));\n // ONE UNDO SNAPSHOT FOR THE BATCH, NOT ONE PER OP. Each create saved a full-project snapshot\n // (exportJSON), so a batch on a big scene cost O(items) per op: 200 creates on 2,000 items took\n // 14 s against 0.76 s for the bare creates. History batch mode skips the per-op saves; the batch\n // saves once at the end, as it always did.\n const __hm = app.historyManager && app.his" +
5211
+ "toryManager.beginBatch ? app.historyManager : null;\n if (__hm) __hm.beginBatch();\n let __hmOpen = !!__hm;\n const __hmEnd = () => {\n if (__hmOpen) { __hmOpen = false; __hm.endBatch(false); }\n // Bulk creates skipped their redraws: one for the batch.\n try { if (typeof paper !== 'undefined' && paper.view) paper.view.draw(); } catch (_) { /* the frame loop draws */ }\n };\n\n try {\n // ONE WRAPPER FOR EVERY OP.\n // Not throwing is not succeeding: an op that RETURNS { success: false } (a guard that found a facade missing, a\n // preset the engine does not have) raises nothing, and was recorded as a success. Atomic: throw WITHOUT\n // recording and let the catch record " +
5212
+ "it once (doing both put two entries in for one failed op).\n const __op = async (index, type, step, opItem, claims, fn) => {\n try {\n const result = await fn();\n const failed = _opFailure(result);\n if (failed) {\n const itemId = (result && typeof result.itemId === 'string') ? result.itemId : opItem();\n " +
5213
+ String(isAtomic ? "const e = new Error(failed); e.itemId = itemId; throw e;" : "results.push({ index, success: false, step, itemId, reason: failed, error: failed, result }); success = false;") +
5214
+ "\n } else {\n results.push({ index, success: true, result });\n if (claims && result && result.itemId) itemIds.push(result.itemId);\n }\n } catch (opError) {\n const r = _why(opError, 'operation ' + index + ' (' + type + ') threw with no message');\n results.push({ index, success: false, step, itemId: (opError && typeof opError.itemId === 'string') ? opError.itemId : opItem(), reason: r, error: r });\n " +
5215
+ String(isAtomic ? 'throw opError;' : 'success = false;') +
5216
+ "\n }\n };\n\n"
5217
+
5218
+
5219
+
5220
+
5221
+
5222
+
5223
+
5224
+
5225
+
5226
+
5227
+
5228
+
5229
+
5230
+
5231
+
5232
+
5233
+
5234
+
5235
+
5236
+
5237
+
5238
+
5239
+
5240
+
5241
+
5242
+
5243
+
5244
+
5096
5245
 
5097
5246
 
5098
5247
 
@@ -5159,44 +5308,19 @@ String(oneLine(operations.length)) +
5159
5308
  const opItem = [o.itemId, o.targetId, o.sourceId, o.id].find((v) => typeof v === 'string') ?? null;
5160
5309
  const opItemJs = typeof opItem === 'string' && /^\$\d+$/.test(opItem) ? `__refName(${opItem.slice(1)})` : JSON.stringify(opItem);
5161
5310
  const opStep = typeof o._step === 'string' ? `op ${index}: ${o._step}` : `op ${index} (${op.type})`;
5162
- code += ("\n // Operation " +
5163
- String(oneLine(index)) +
5164
- ": " +
5165
- String(oneLine(op.type)) +
5166
- "\n try {\n const result" +
5167
- String(index) +
5168
- " = await (async () => {\n " +
5169
- String(opCode) +
5170
- "\n })();\n // NOT THROWING IS NOT SUCCEEDING. An op that RETURNS { success: false }\n // — a guard that found a facade missing, a preset the engine does not\n // have, a precondition unmet — raises nothing, so this recorded it as a\n // success and the batch reported success over it. Measured on\n // applyAnimatedMask(item, 'star'): the per-op error was perfectly good\n // and the batch still said everything worked.\n const failed" +
5171
- String(index) +
5172
- " = _opFailure(result" +
5173
- String(index) +
5174
- ");\n if (failed" +
5175
- String(index) +
5176
- ") {\n // ATOMIC: throw WITHOUT recording, and let the catch below record it\n // once. Doing both put two entries in for one failed op.\n " +
5177
- String(isAtomic ? `const __e${index} = new Error(failed${index}); __e${index}.itemId = (result${index} && typeof result${index}.itemId === 'string') ? result${index}.itemId : ${opItemJs}; throw __e${index};` : `results.push({ index: ${index}, success: false, step: ${JSON.stringify(opStep)}, itemId: (result${index} && typeof result${index}.itemId === 'string') ? result${index}.itemId : ${opItemJs}, reason: failed${index}, error: failed${index}, result: result${index} });
5178
- success = false;`) +
5179
- "\n } else {\n results.push({ index: " +
5180
- String(index) +
5181
- ", success: true, result: result" +
5182
- String(index) +
5183
- " });\n " +
5184
- String(claimsItemSlot ? `if (result${index} && result${index}.itemId) {
5185
- itemIds.push(result${index}.itemId);
5186
- }` : '') +
5187
- "\n }\n } catch (opError) {\n const __r" +
5188
- String(index) +
5189
- " = _why(opError, 'operation " +
5190
- String(index) +
5191
- " (" +
5192
- String(op.type) +
5193
- ") threw with no message');\n results.push({ index: " +
5194
- String(index) + ", success: false, step: " + String(JSON.stringify(opStep)) + ", itemId: (opError && typeof opError.itemId === 'string') ? opError.itemId : " + String(opItemJs) + ", reason: __r" + String(index) + ", error: __r" + String(index) + " });\n " + String(isAtomic ? 'throw opError;' : 'success = false;') + "\n }\n");
5311
+ // The wrapper is the shared __op helper (emitted once, below the history batch), not a copy per op: the
5312
+ // copied try/catch and its comments were ~1.5 KB of every op, 4.8 MB of code for 2,000 creates.
5313
+ code += `
5314
+ // Operation ${oneLine(index)}: ${oneLine(op.type)}
5315
+ await __op(${index}, ${JSON.stringify(op.type)}, ${JSON.stringify(opStep)}, () => ${opItemJs}, ${claimsItemSlot}, async () => {
5316
+ ${opCode}
5317
+ });
5318
+ `;
5195
5319
  });
5196
- code += ("\n // Save state\n if (app.historyManager) app.historyManager.saveState();\n\n const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };\n // Name WHICH ops failed on the result itself. A caller reading a failed\n // 60-op batch should not have to scan sixty entries to learn that op 41\n // could not find a preset.\n const _failed = results.filter((r) => !r.success);\n // A batch that created nothing keeps the previous batch's ids on record.\n if (typeof window !== 'undefined' && itemIds.length) {\n window.__pinepaper_batch_item_ids = itemIds;\n }\n return { success, itemIds, results, operationCount: " +
5320
+ code += ("\n // Save state\n __hmEnd();\n if (app.historyManager) app.historyManager.saveState();\n\n const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };\n // Name WHICH ops failed on the result itself. A caller reading a failed\n // 60-op batch should not have to scan sixty entries to learn that op 41\n // could not find a preset.\n const _failed = results.filter((r) => !r.success);\n // A batch that created nothing keeps the previous batch's ids on record.\n if (typeof window !== 'undefined' && itemIds.length) {\n window.__pinepaper_batch_item_ids = itemIds;\n }\n return { success, itemIds, results, operationCount: " +
5197
5321
  String(operations.length) +
5198
- ",\n failedCount: _failed.length,\n ...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; '), failures: _failures() } : {}),\n canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };\n } catch (e) {\n // A batch that created nothing keeps the previous batch's ids on record.\n if (typeof window !== 'undefined' && itemIds.length) {\n window.__pinepaper_batch_item_ids = itemIds;\n }\n const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };\n const __fails = _failures();\n const __reason = " +
5199
- "_why(e, 'the batch failed with no message');\n return { success: false, error: __reason, itemIds, results, operationCount: " +
5322
+ ",\n failedCount: _failed.length,\n ...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; '), failures: _failures() } : {}),\n canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };\n } catch (e) {\n __hmEnd();\n // A batch that created nothing keeps the previous batch's ids on record.\n if (typeof window !== 'undefined' && itemIds.length) {\n window.__pinepaper_batch_item_ids = itemIds;\n }\n const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };\n const __fails = _failures();\n c" +
5323
+ "onst __reason = _why(e, 'the batch failed with no message');\n return { success: false, error: __reason, itemIds, results, operationCount: " +
5200
5324
  String(operations.length) +
5201
5325
  ",\n failures: __fails.length ? __fails : [{ step: 'batch', itemId: null, reason: __reason }],\n canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };\n }\n})();\n"
5202
5326
 
@@ -5220,6 +5344,8 @@ String(operations.length) +
5220
5344
 
5221
5345
 
5222
5346
 
5347
+
5348
+
5223
5349
 
5224
5350
 
5225
5351
 
@@ -5274,7 +5400,7 @@ String(operations.length) +
5274
5400
  const createProps = withRadiusAxes(opProps);
5275
5401
  const opBrand = { ...opProps };
5276
5402
  delete opProps.noBrand;
5277
- const returning = asReturningBody(withBrandDefaults(generateCreateItemCode(op.itemType, pos, { ...opProps }, undefined, op.position !== undefined || typeof anyOp.x === 'number' || typeof anyOp.y === 'number'), op.itemType, opBrand));
5403
+ const returning = asBulkCreate(asReturningBody(withBrandDefaults(generateCreateItemCode(op.itemType, pos, { ...opProps }, undefined, op.position !== undefined || typeof anyOp.x === 'number' || typeof anyOp.y === 'number'), op.itemType, opBrand)));
5278
5404
  let createCode = `${hasItemSetters(opSetters) ? `\n${itemSettersPreflightJs(opSetters)}` : ''}
5279
5405
  const __created = await (async function() {
5280
5406
  ${returning}
@@ -5559,17 +5685,27 @@ return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget}
5559
5685
  if (op[key] !== undefined && Number.isFinite(v))
5560
5686
  kfExtra.push(`${key}: ${v}`);
5561
5687
  }
5562
- const kfUnits = op.timeUnits;
5563
- if (kfUnits === 'seconds' || kfUnits === 'ms')
5564
- kfExtra.push(`timeUnits: ${jsLiteral(kfUnits)}`);
5688
+ // Seconds unless the op says ms: the studio's own guess read every time as ms once one passed 100 (GRIEF).
5689
+ const kfUnitsGiven = op.timeUnits === 'seconds' || op.timeUnits === 'ms';
5690
+ const kfUnits = kfUnitsGiven ? op.timeUnits : 'seconds';
5691
+ kfExtra.push(`timeUnits: ${jsLiteral(kfUnits)}`);
5692
+ const kfLast = Math.max(0, ...(kfSource || []).map((k) => Number(k && k.time) || 0), Number(kfDuration) || 0);
5693
+ const kfUnitsNote = !kfUnitsGiven && kfLast > 100
5694
+ ? `, timeUnitsNote: ${JSON.stringify(`times are read as seconds: the track ends at ${kfLast} s (${(kfLast / 60).toFixed(1)} min). If you meant milliseconds, pass timeUnits: 'ms'.`)}`
5695
+ : '';
5565
5696
  const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
5697
+ const kfGuard = keyTimeGuard({ target: 'targetId', item: '_kfItem', keyframes: (kfSource || []), timeUnits: kfUnits,
5698
+ append: kfAppend, timeOffsetGiven: op.timeOffset !== undefined, loop: kfLoop,
5699
+ opts: { duration: kfDuration, loop: kfLoop, ...(kfAppend ? { mode: 'merge' } : {}) }, duration: kfDuration });
5566
5700
  const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
5567
5701
  const kfFilterKeysJs = unanimatedFilterKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
5568
5702
  const kfTextCheck = `\nconst _kfIgnored = ${kfTextKeysJs || '[]'};\nconst _kfFilterIgnored = ${kfFilterKeysJs};`;
5569
5703
  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(' ') } : {})`;
5570
5704
  return ("\nconst targetId = " +
5571
5705
  String(kfItemRef) +
5572
- ";\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, " +
5706
+ ";\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;\n" +
5707
+ String(kfGuard.before) +
5708
+ "\napp.addAnimation(targetId, " +
5573
5709
  String(kfJson) +
5574
5710
  ", { duration: " +
5575
5711
  String(kfDuration) +
@@ -5577,8 +5713,7 @@ String(kfDuration) +
5577
5713
  String(kfLoop) +
5578
5714
  String(kfAppend ? ", mode: 'merge'" : '') +
5579
5715
  String(kfExtraStr) +
5580
- " });" +
5581
- String(kfTextCheck) + "\nreturn { success: true, itemId: targetId, duration: " + String(kfDuration) + ", loop: " + String(kfLoop) + String(kfAppend ? ', append: true' : '') + String(keyframeResultNotes(kfSource, { loop: kfLoop === true, append: kfAppend })) + String(kfAppend ? '' : `, ...(_prevKeys ? { track: { replaced: true, previousKeys: _prevKeys, note: 'the item already had a keyframe track; this op replaced it. To add keys to it instead, pass append: true.' } } : {})`) + String(kfIgnored) + " };\n");
5716
+ " });" + String(kfGuard.after) + String(kfTextCheck) + "\nreturn { success: true, itemId: targetId, duration: " + String(kfDuration) + ", loop: " + String(kfLoop) + String(kfAppend ? ', append: true' : '') + String(kfGuard.field) + String(keyframeResultNotes(kfSource, { loop: kfLoop === true, append: kfAppend })) + String(kfAppend ? '' : `, ...(_prevKeys ? { track: { replaced: true, previousKeys: _prevKeys, note: 'the item already had a keyframe track; this op replaced it. To add keys to it instead, pass append: true.' } } : {})`) + String(kfUnitsNote) + String(kfIgnored) + " };\n");
5582
5717
  }
5583
5718
  case 'apply_mask': {
5584
5719
  const maskItemRef = batchRef(op.itemId);
@@ -6439,7 +6574,10 @@ String(deterministic ? (" // DETERMINISTIC: the container timestamps are pinned
6439
6574
  " // LUMA OUTSIDE 16-235 (8.35, FxTool 4f1ec3ae). The broadcast encode is\n // decoded back and measured: H.264 ringing at hard edges puts a fraction of\n // a percent of samples out of range, which QC rejects. broadcastHeadroom\n // cuts it but never to zero, so a legaliser pass is the requirement.\n if (result && result.success && __vr && __vr.luma && __vr.luma.outOfRange > 0 && result.fidelity) {\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'luma_out_of_range',\n message: 'broadcast QC will reject this as delivered: ' + ((__vr.luma.outOfRangeFraction || 0) * 100).toFixed(3) + '% of decoded luma is outside 16-235 (min ' + __vr.luma.min + ', ma" +
6440
6575
  "x ' + __vr.luma.max + ', broadcastHeadroom ' + (typeof __vr.headroom === 'number' ? __vr.headroom : " +
6441
6576
  String(JSON.stringify(broadcastHeadroom ?? 12)) +
6442
- ") + '). First re-export with a larger broadcastHeadroom (12 measured clean on hard white-on-black edges; up to 40). Otherwise a legaliser pass is REQUIRED before delivery — clip into a NARROWER band and re-encode at high quality, because a plain 16-235 clip followed by a re-encode brings the edge overshoot back: ffmpeg -i in.mp4 -vf \"lutyuv=y=clip(val\\\\,28\\\\,223):u=clip(val\\\\,16\\\\,240):v=clip(val\\\\,16\\\\,240)\" -c:v libx264 -crf 18 -color_range tv -colorspace bt709 -color_primaries bt709 -color_trc bt709 -c:a copy out.mp4 (measured 20-229). Verify the luma range after re-encoding.' }]);\n if (result.fidelity.note) delete result.fidelity.note;\n }\n" +
6577
+ ") + '). ' + (function () { const __h = typeof __vr.headroom === 'number' ? __vr.headroom : " +
6578
+ String(JSON.stringify(broadcastHeadroom ?? 12)) +
6579
+ "; return __h < 40 ? 'First re-export with broadcastHeadroom ' + Math.min(40, __h + 12) + ' (above the ' + __h + ' this used; 40 is the most): each step leaves fewer samples out of range and narrows the luma range of the picture, so check the result. ' : 'broadcastHeadroom is already at its most (40). '; })() + 'Otherwise a legaliser pass is REQUIRED before delivery — clip into a NARROWER band and re-encode at high quality, because a plain 16-235 clip followed by a re-encode brings the edge overshoot back: ffmpeg -i in.mp4 -vf \"lutyuv=y=clip(val\\\\,28\\\\,223):u=clip(val\\\\,16\\\\,240):v=clip(val\\\\,16\\\\,240)\" -c:v libx264 -crf 18 -color_range tv -colorspace bt709 -color_primaries bt" +
6580
+ "709 -color_trc bt709 -c:a copy out.mp4 (measured 20-229). Verify the luma range after re-encoding.' }]);\n if (result.fidelity.note) delete result.fidelity.note;\n }\n" +
6443
6581
  String(Object.keys(videoEncodeOpts).length ? (" if (result && result.success) {\n const __asked = " +
6444
6582
  String(JSON.stringify(videoEncodeOpts)) +
6445
6583
  ";\n const __vw = [];\n // The engine joins its warnings into one string (bitrate floor, then\n // luma), so each is raised from its own numbers, not from that text.\n // The studio's own measured bitrate_not_honoured, where it sends one,\n // replaces this estimate.\n const __engineBitrate = !!(__vr && Array.isArray(__vr.warnings) && __vr.warnings.some(function(w) { return w && w.code === 'bitrate_not_honoured'; }));\n if (!__engineBitrate && __vr && typeof __vr.achievedBitrate === 'number' && typeof __vr.bitrateFloor === 'number' && __vr.achievedBitrate < __vr.bitrateFloor * 0.95) {\n __vw.push({ code: 'bitrate_below_floor', message: 'achieved ' + (__vr.achievedB" +
@@ -7609,9 +7747,6 @@ String(jsLiteral(format)) +
7609
7747
 
7610
7748
 
7611
7749
 
7612
-
7613
-
7614
-
7615
7750
 
7616
7751
 
7617
7752
 
@@ -11701,15 +11836,23 @@ String(warmArg) +
11701
11836
  if (input.steps !== undefined)
11702
11837
  params.steps = input.steps;
11703
11838
  const itemIdStr = JSON.stringify(input.itemId);
11704
- return `
11705
- // Apply deformation preset
11706
- (function() {
11707
- ${guard}
11708
- const entry = app.itemRegistry.get(${itemIdStr});
11709
- if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
11710
- app.deformPresets.apply(entry.item, ${jsLiteral(input.preset)}, ${JSON.stringify(params)});
11711
- return { success: true, action: 'apply', itemId: ${itemIdStr}, preset: ${jsLiteral(input.preset)} };
11712
- })();`.trim();
11839
+ return ("\n// Apply deformation preset\n(function() {\n " +
11840
+ String(guard) +
11841
+ "\n const entry = app.itemRegistry.get(" +
11842
+ String(itemIdStr) +
11843
+ ");\n if (!entry || !entry.item) return { error: 'Item not found: ' + " +
11844
+ String(itemIdStr) +
11845
+ " };\n // The engine refuses by name (deform-unknown-preset, deform-no-item, deform-no-geometry), in its return and in the\n // receipt. The return was ignored, so a refused deform said success.\n const __r = app.deformPresets.apply(entry.item, " +
11846
+ String(jsLiteral(input.preset)) +
11847
+ ", " +
11848
+ String(JSON.stringify(params)) +
11849
+ ");\n const __rw = (app.lastReceipt && Array.isArray(app.lastReceipt.warnings) ? app.lastReceipt.warnings : [])\n .filter(function (w) { return w && /^deform-/.test(String(w.kind || w.code || '')); });\n if (__r === false || (__r && (__r.ok === false || __r.success === false)) || __rw.length) {\n const __why = (__r && (__r.reason || __r.error)) || __rw.map(function (w) { return w.message || w.reason || w.kind || w.code; }).join(' ') || 'the studio did not apply the deform';\n return { success: false, action: 'apply', itemId: " +
11850
+ String(itemIdStr) +
11851
+ ", preset: " +
11852
+ String(jsLiteral(input.preset)) +
11853
+ ", error: __why, ...(__rw.length ? { code: __rw[0].kind || __rw[0].code } : {}) };\n }\n return { success: true, action: 'apply', itemId: " +
11854
+ String(itemIdStr) +
11855
+ ", preset: " + String(jsLiteral(input.preset)) + " };\n})();").trim();
11713
11856
  }
11714
11857
  case 'trigger': {
11715
11858
  if (!input.itemId)
@@ -12711,17 +12854,28 @@ String(idExpr) +
12711
12854
  }
12712
12855
  if (input.action === 'ungroup') {
12713
12856
  const idExpr = JSON.stringify(input.groupId || '');
12714
- return `
12715
- // Ungroup ${oneLine(input.groupId || '')}
12716
- (function() {
12717
- if (!app.groupManager) return { success: false, error: 'GroupManager not available — update FxTool' };
12718
- const gid = ${idExpr};
12719
- const group = app.getItemById(gid) || (app.groupManager.getGroupById ? app.groupManager.getGroupById(gid) : null);
12720
- if (!group) return { success: false, error: 'Group not found: ' + gid };
12721
- app.groupManager.ungroupAll(group);
12722
- if (app.historyManager) app.historyManager.saveState();
12723
- return { success: true, ungrouped: true, groupId: gid };
12724
- })();`.trim();
12857
+ return ("\n// Ungroup " +
12858
+ String(oneLine(input.groupId || '')) +
12859
+ "\n(function() {\n if (!app.groupManager) return { success: false, error: 'GroupManager not available — update FxTool' };\n const gid = " +
12860
+ String(idExpr) +
12861
+ ";\n const group = app.getItemById(gid) || (app.groupManager.getGroupById ? app.groupManager.getGroupById(gid) : null);\n if (!group) return { success: false, error: 'Group not found: ' + gid };\n // A FOREGROUND GENERATOR'S OUTPUT (a chat thread, a phone frame) is a generator composition, not a user group:\n // the studio releases it into ordinary items through releaseParts (what Ungroup / app.ungroupSelection does).\n // The group manager's ungroup does not know its parts.\n const __gd = group.data || {};\n if ((__gd.generatorParts === true || (__gd.generatorPart && __gd.generatorPart !== 'text')) && app.generatorRegistry && app.generatorRegistry.releaseParts) {\n const r = app." +
12862
+ "generatorRegistry.releaseParts(group);\n if (!r || !r.ok) return { success: false, error: (r && r.reason) || 'the studio did not release the generator composition' };\n if (app.historyManager) app.historyManager.saveState();\n const ids = (r.items || []).map(function (it) { return it && it.data && (it.data.registryId || it.data.id); }).filter(Boolean);\n return { success: true, ungrouped: true, groupId: gid, generator: true, itemIds: ids, count: ids.length };\n }\n app.groupManager.ungroupAll(group);\n if (app.historyManager) app.historyManager.saveState();\n return { success: true, ungrouped: true, groupId: gid };\n})();"
12863
+
12864
+
12865
+
12866
+
12867
+
12868
+
12869
+
12870
+
12871
+
12872
+
12873
+
12874
+
12875
+
12876
+
12877
+
12878
+ ).trim();
12725
12879
  }
12726
12880
  const idsArrayExpr = JSON.stringify(input.itemIds ?? []);
12727
12881
  const nameExpr = input.groupName !== undefined ? JSON.stringify(input.groupName) : 'undefined';
@@ -13792,9 +13946,9 @@ String(S(input.beats)) +
13792
13946
  String(id) +
13793
13947
  ", error: __r.error }; }\n const __prev = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes : [];\n const __prevTimes = __prev.map(function (k) { return k && k.time; });\n const __ours = ['x', 'y', 'scaleX', 'scaleY', 'rotation', 'opacity'];\n const __other = __prev.reduce(function (acc, k) { Object.keys((k && k.properties) || {}).forEach(function (p) { if (__ours.indexOf(p) < 0 && acc.indexOf(p) < 0) acc.push(p); }); return acc; }, []);\n app.addAnimation(" +
13794
13948
  String(id) +
13795
- ", __r.keyframes, { duration: Math.max(__r.end, __prev.reduce(function (m, k) { return Math.max(m, (k && k.time) || 0); }, 0)), loop: false, mode: 'merge' });\n const __now = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes.map(function (k) { return k && k.time; }) : [];\n const __warnings = __anchorWarnings.concat(__r.warnings || []);\n if (__prevTimes.length && !__prevTimes.every(function (t) { return __now.indexOf(t) >= 0; })) {\n __warnings.push('this studio replaced the existing track of the actor (' + __prevTimes.length + ' key(s)) instead of merging: put the other keys in after choreographing.');\n }\n if (__other.length) {\n __warnings.push('the act" +
13796
- "or already animates ' + __other.join(', ') + '; the beat keys leave those out, which the engine can break at the beat keys. Re-key ' + __other.join(', ') + ' with every key carrying them, after choreographing.');\n }\n if (__r.final && !__r.final.onScreen) __warnings.push('after the last beat the actor is off screen at [' + __r.final.x + ', ' + __r.final.y + '].');\n if (__fr.unbounded && __warnings.some(function (w) { return /off screen/.test(w); })) __warnings.push('the canvas is unbounded, so it has no fixed frame: on-screen was checked against its nominal ' + __fr.width + 'x' + __fr.height + ' artboard. Set a canvas size or preset to fix the frame.');\n const __res = { success: tru" +
13797
- "e, itemId: " +
13949
+ ", __r.keyframes, { duration: Math.max(__r.end, __prev.reduce(function (m, k) { return Math.max(m, (k && k.time) || 0); }, 0)), loop: false, mode: 'merge', timeUnits: 'seconds' });\n const __now = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes.map(function (k) { return k && k.time; }) : [];\n const __warnings = __anchorWarnings.concat(__r.warnings || []);\n if (__prevTimes.length && !__prevTimes.every(function (t) { return __now.indexOf(t) >= 0; })) {\n __warnings.push('this studio replaced the existing track of the actor (' + __prevTimes.length + ' key(s)) instead of merging: put the other keys in after choreographing.');\n }\n if (__other.length) {\n __" +
13950
+ "warnings.push('the actor already animates ' + __other.join(', ') + '; the beat keys leave those out, which the engine can break at the beat keys. Re-key ' + __other.join(', ') + ' with every key carrying them, after choreographing.');\n }\n if (__r.final && !__r.final.onScreen) __warnings.push('after the last beat the actor is off screen at [' + __r.final.x + ', ' + __r.final.y + '].');\n if (__fr.unbounded && __warnings.some(function (w) { return /off screen/.test(w); })) __warnings.push('the canvas is unbounded, so it has no fixed frame: on-screen was checked against its nominal ' + __fr.width + 'x' + __fr.height + ' artboard. Set a canvas size or preset to fix the frame.');\n const " +
13951
+ "__res = { success: true, itemId: " +
13798
13952
  String(id) +
13799
13953
  ", beats: " +
13800
13954
  String(input.beats.length) +
@@ -14036,63 +14190,52 @@ String(sound ? ` // A sound on each morph (D68's placement, with its MP4-lead n
14036
14190
  * Character tool aliases (pinepaper_create_character, pinepaper_character_action, pinepaper_character_pose)
14037
14191
  * Routing on-device / local model calls cleanly to the engine.
14038
14192
  */
14193
+ /**
14194
+ * pinepaper_create_character's last door: no layered bundle, and no concept the design graph can draw. It used to
14195
+ * draw a purple placeholder circle and say success. It now makes a RIGGED character (app.createCharacter), the
14196
+ * owner's rule for anything that should move: a base named outright, or one mapped from the request (a pigeon is
14197
+ * a bird, a kitten a cat, a hero a person). A request that maps to no rigged base is refused with the five, never
14198
+ * fitted onto the nearest rig in silence (a dog on the cat rig is a product call, not this function's).
14199
+ */
14039
14200
  generateCreateCharacter(input) {
14040
14201
  const S = (v) => JSON.stringify(v);
14041
- const charId = S(input.characterId || input.name || `char_${Date.now()}`);
14042
- const posX = typeof input.x === 'number' ? input.x : 400;
14043
- const posY = typeof input.y === 'number' ? input.y : 300;
14044
- const scale = typeof input.scale === 'number' ? input.scale : 1;
14045
- const name = S(input.name || input.characterId || 'Character');
14046
- const base = input.base ? S(input.base) : 'null';
14047
- const preset = input.preset || input.concept ? S(input.preset || input.concept) : 'null';
14048
- return ("\n// Create Character\n(async function() {\n const charId = " +
14049
- String(charId) +
14050
- ";\n const posX = " +
14051
- String(posX) +
14052
- ";\n const posY = " +
14053
- String(posY) +
14054
- ";\n const scale = " +
14055
- String(scale) +
14056
- ";\n const name = " +
14057
- String(name) +
14058
- ";\n const base = " +
14059
- String(base) +
14060
- ";\n const preset = " +
14061
- String(preset) +
14062
- ";\n\n // 1. If original character base is specified and app.createCharacter exists\n if (base && typeof app.createCharacter === 'function') {\n const r = await app.createCharacter({ base: base, at: { x: posX, y: posY }, scale: scale });\n if (r && r.ok !== false) {\n return { success: true, characterId: charId, itemId: r.id, parts: r.parts };\n }\n }\n\n // 2. Try app.create\n let groupId = null;\n if (typeof app.create === 'function') {\n const group = app.create('circle', { x: posX, y: posY, radius: 35 * scale, color: '#7C3AED' });\n if (group) {\n group.data = group.data || {};\n group.data.ppType = 'pp:Character';\n group.data.characterId = char" +
14063
- "Id;\n group.name = name;\n groupId = group.id || (group.data && group.data.id) || charId;\n }\n } else {\n const p = typeof paper !== 'undefined' ? paper : ((typeof window !== 'undefined' ? window : globalThis).paper);\n if (p && p.Path) {\n const body = new p.Path.Circle({ center: [posX, posY], radius: 35 * scale, fillColor: '#7C3AED' });\n const head = new p.Path.Circle({ center: [posX, posY - 45 * scale], radius: 20 * scale, fillColor: '#EC4899' });\n const group = new p.Group([body, head]);\n group.name = name;\n if (app.textItemGroup) group.parent = app.textItemGroup;\n group.data = { isUserGroup: true, isLayeredCharacter: true, ppType" +
14064
- ": 'pp:Character', characterId: charId };\n const reg = app.itemRegistry;\n groupId = reg ? reg.register(group, 'group', { source: 'mcp_create_character' }, 'mcp') : charId;\n group.data.id = groupId;\n }\n }\n\n if (app._scheduleRepaint) app._scheduleRepaint();\n if (app.historyManager) app.historyManager.saveState();\n return { success: true, characterId: charId, itemId: groupId };\n})();"
14065
-
14066
-
14067
-
14068
-
14069
-
14070
-
14071
-
14072
-
14073
-
14074
-
14075
-
14076
-
14077
-
14078
-
14079
-
14080
-
14081
-
14082
-
14083
-
14084
-
14085
-
14086
-
14087
-
14088
-
14089
-
14090
-
14091
-
14092
-
14093
-
14094
-
14095
-
14202
+ const asked = String(input.base || input.preset || input.concept || '').trim();
14203
+ const word = asked.replace(/^pp:/i, '').toLowerCase();
14204
+ const MAP = [
14205
+ [/^(person|people|human|character|child|kid|hero|heroine|man|woman|boy|girl|figure|adult|player)$/, 'person'],
14206
+ [/^(robot|bot|android|mech|droid|cyborg)$/, 'robot'],
14207
+ [/^(cat|kitten|kitty|feline)$/, 'cat'],
14208
+ [/^(bird|pigeon|crow|raven|sparrow|parrot|robin|owl|seagull|gull|finch|duck|chick)$/, 'bird'],
14209
+ [/^(blob|slime|jelly)$/, 'blob'],
14210
+ ];
14211
+ // An explicit base goes to the studio as written (it refuses an unknown one with its list); a concept is mapped.
14212
+ const base = input.base ? String(input.base) : (!word ? 'person' : (MAP.find(([re]) => re.test(word)) || [null, null])[1]);
14213
+ if (!base) {
14214
+ return `(function() { return { success: false, error: ${S(`no rigged character fits "${asked}": the rigged bases are person, robot, cat, bird and blob. Pass base: one of those (with variant for its look), or for a figure that does not need to move, a concept pinepaper_query_ontology lists.`)} }; })();`;
14215
+ }
14216
+ const spec = { base };
14217
+ if (typeof input.x === 'number')
14218
+ spec.x = input.x;
14219
+ if (typeof input.y === 'number')
14220
+ spec.y = input.y;
14221
+ if (typeof input.scale === 'number')
14222
+ spec.scale = input.scale;
14223
+ if (input.variant)
14224
+ spec.variant = input.variant;
14225
+ if (input.name)
14226
+ spec.name = input.name;
14227
+ const mapped = input.base ? null : (word ? { from: asked, to: base } : null);
14228
+ return ("\n// Create a rigged character (base " +
14229
+ String(oneLine(base)) +
14230
+ ")\n(async function() {\n // @engine-methods createCharacter\n if (typeof app.createCharacter !== 'function') { return { success: false, error: 'this studio has no rigged characters (app.createCharacter): update PinePaper Studio' }; }\n const r = await app.createCharacter(" +
14231
+ String(S(spec)) +
14232
+ ");\n if (!r || r.ok === false) { return { success: false, error: (r && Array.isArray(r.errors) && r.errors.join('; ')) || (r && r.reason) || 'the studio made no character from that spec' }; }\n if (app.historyManager) app.historyManager.saveState();\n return { success: true, characterId: " +
14233
+ String(S(input.characterId || input.name || null)) +
14234
+ " || r.id, itemId: r.id, rigId: r.rigId || null, base: r.base || " +
14235
+ String(S(base)) +
14236
+ ", parts: r.parts" +
14237
+ String(mapped ? `, baseFrom: ${S(mapped)}` : '') +
14238
+ ",\n ...(r.rigId ? {} : { warning: 'the character was drawn but its rig failed, so it cannot walk, pose or act: treat it as still artwork' }),\n note: 'y is where the feet stand' };\n})();"
14096
14239
  ).trim();
14097
14240
  }
14098
14241
  generateCharacterAction(input) {
@@ -14106,12 +14249,10 @@ String(S(targetId)) +
14106
14249
  String(S(act)) +
14107
14250
  ";\n const speed = " +
14108
14251
  String(speed) +
14109
- ";\n\n const charItem = targetId\n ? (app.getItemById(targetId) || (app.getAllItems && app.getAllItems().find(i => (i.data && i.data.characterId === targetId) || i.name === targetId)))\n : (app.selectedItems && app.selectedItems[0]);\n\n if (!charItem) {\n return { success: false, error: 'Character not found: ' + targetId };\n }\n\n const itemId = charItem.id || (charItem.data && charItem.data.id) || targetId;\n const skelId = (charItem.data && charItem.data.characterId) ? 'character_' + charItem.data.characterId : null;\n\n if (act === 'walk' || act === 'run' || act === 'locomotion') {\n if (typeof app.addRelation === 'function') {\n app.addRelation(itemId, null, 'lo" +
14110
- "comotion', {\n skeletonId: skelId,\n speed: speed,\n cycleDistance: 150,\n });\n }\n } else if (app.riggingSystem) {\n const rs = app.riggingSystem;\n if (skelId && rs.skeletons && rs.skeletons.has(skelId)) {\n if (typeof rs.applyBoneAngles === 'function') {\n rs.applyBoneAngles(skelId, { spine: 0.1, left_arm: -0.5, right_arm: 0.5 });\n if (typeof rs.solveFK === 'function') rs.solveFK(skelId);\n }\n }\n }\n\n if (typeof app.animate === 'function' && (act === 'wave' || act === 'dance' || act === 'jump' || act === 'idle')) {\n app.animate(charItem, { animationType: act === 'jump' ? 'bounce' : (act === 'idle' ? 'breathe' : 'w" +
14111
- "obble') });\n }\n\n if (app._scheduleRepaint) app._scheduleRepaint();\n return { success: true, characterId: targetId || itemId, action: act };\n})();"
14112
-
14113
-
14114
-
14252
+ ";\n\n const charItem = targetId\n ? (app.getItemById(targetId) || (app.getAllItems && app.getAllItems().find(i => (i.data && i.data.characterId === targetId) || i.name === targetId)))\n : (app.selectedItems && app.selectedItems[0]);\n\n if (!charItem) {\n return { success: false, error: 'Character not found: ' + targetId };\n }\n\n const itemId = charItem.id || (charItem.data && charItem.data.id) || targetId;\n const skelId = (charItem.data && charItem.data.characterId) ? 'character_' + charItem.data.characterId : null;\n\n // WHAT WAS APPLIED, NOT ASSUMED. A concept character (pinepaper_character) has parts and keyframes but no\n // skeleton, so the skeleton branch did not" +
14253
+ "hing and the call still said success.\n const rs = app.riggingSystem;\n const hasSkeleton = !!(skelId && rs && rs.skeletons && rs.skeletons.has(skelId));\n const applied = [];\n if (act === 'walk' || act === 'run' || act === 'locomotion') {\n if (typeof app.addRelation === 'function' && app.addRelation(itemId, null, 'locomotion', { skeletonId: skelId, speed: speed, cycleDistance: 150 })) applied.push('locomotion relation');\n } else if (hasSkeleton && typeof rs.applyBoneAngles === 'function') {\n rs.applyBoneAngles(skelId, { spine: 0.1, left_arm: -0.5, right_arm: 0.5 });\n if (typeof rs.solveFK === 'function') rs.solveFK(skelId);\n applied.push('bone angles');\n }\n\n if" +
14254
+ " (typeof app.animate === 'function' && (act === 'wave' || act === 'dance' || act === 'jump' || act === 'idle')) {\n app.animate(charItem, { animationType: act === 'jump' ? 'bounce' : (act === 'idle' ? 'breathe' : 'wobble') });\n applied.push('whole-figure ' + (act === 'jump' ? 'bounce' : (act === 'idle' ? 'breathe' : 'wobble')));\n }\n\n if (app._scheduleRepaint) app._scheduleRepaint();\n const note = hasSkeleton ? null : 'this character has no skeleton (a concept character is parts and keyframes), so bone actions and rigging calls on it act on nothing' + (applied.length ? '; what ran: ' + applied.join(', ') : '') + '. For a figure that walks, poses and acts, make it with pinepape" +
14255
+ "r_original_character (rigged); pinepaper_character directs a concept through its channels (beats).';\n if (!applied.length) return { success: false, characterId: targetId || itemId, action: act, error: note || ('nothing was applied for action ' + act) };\n return { success: true, characterId: targetId || itemId, action: act, applied: applied, skeleton: hasSkeleton, ...(note ? { warning: note } : {}) };\n})();"
14115
14256
 
14116
14257
 
14117
14258
 
@@ -14275,14 +14416,19 @@ String(target) +
14275
14416
  ? `, ...((function(w) { return w.length ? { warnings: w } : {}; })(${S(fixed)}.concat(${checks.length ? `__capWarnings(${stored})` : '[]'})))`
14276
14417
  : '';
14277
14418
  if (input.action === 'set') {
14278
- return `
14279
- // Emitter: change a spec
14280
- (function() {
14281
- if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }${capFn}
14282
- const r = app.setEmitter(${S(input.itemId)}, ${S(input.spec)});
14283
- if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the emitter change was refused' }; }
14284
- return { success: true, action: 'set', itemId: ${S(input.itemId)}, spec: r.spec${capJs('r.spec')} };
14285
- })();`.trim();
14419
+ return ("\n// Emitter: change a spec\n(function() {\n if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }" +
14420
+ String(capFn) +
14421
+ "\n // set merges the spec's TOP-LEVEL keys; an array it names (bursts) replaces the old one whole (EMSETBURST36).\n const __b0 = (function () { try { const it = app.getItemById(" +
14422
+ String(S(input.itemId)) +
14423
+ "); const b = it && it.data && it.data.emitter && it.data.emitter.bursts; return Array.isArray(b) ? b.length : null; } catch (_) { return null; } })();\n const r = app.setEmitter(" +
14424
+ String(S(input.itemId)) +
14425
+ ", " +
14426
+ String(S(input.spec)) +
14427
+ ");\n if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the emitter change was refused' }; }\n const __b1 = r.spec && Array.isArray(r.spec.bursts) ? r.spec.bursts.length : null;\n return { success: true, action: 'set', itemId: " +
14428
+ String(S(input.itemId)) +
14429
+ ", spec: r.spec" +
14430
+ String(capJs('r.spec')) + String(Array.isArray(input.spec?.bursts) ? `,
14431
+ burstsReplaced: { before: __b0, after: __b1, note: 'bursts replaces the whole array: the emitter now has only the bursts this call passed. To add one, pass the existing bursts with it.' }` : '') + " };\n})();").trim();
14286
14432
  }
14287
14433
  return `
14288
14434
  // Emitter: create
@@ -15321,7 +15467,7 @@ ${needWorld}
15321
15467
  * already decided by a pure function, so nothing is left for the page to
15322
15468
  * work out.
15323
15469
  */
15324
- generateDesignCompose(scene, ops, style, sizeWarning, report = {}) {
15470
+ generateDesignCompose(scene, ops, style, sizeWarning, report = {}, fitTo) {
15325
15471
  const S = (v) => JSON.stringify(v);
15326
15472
  return ("\n// Design: compose a " +
15327
15473
  String(oneLine(style)) +
@@ -15329,17 +15475,36 @@ String(oneLine(style)) +
15329
15475
  String(oneLine(ops.length)) +
15330
15476
  " items, laid out server-side\n(function() {\n if (typeof app.create !== 'function') { return { success: false, error: 'app.create unavailable — update FxTool' }; }\n const size = " +
15331
15477
  String(S({ width: scene.width, height: scene.height })) +
15332
- ";\n // The layout was computed for this size, so the canvas is set to it — and\n // the result SAYS so: a 1080x1080 canvas became 1280x720 silently (gate C).\n const __before = app.getCanvasSize ? app.getCanvasSize() : null;\n if (typeof app.setCanvasSize === 'function') { try { app.setCanvasSize({ width: size.width, height: size.height }); } catch (_) { /* a fixed canvas is not a failure */ } }\n const __after = app.getCanvasSize ? app.getCanvasSize() : null;\n const canvasResized = __before && __after && (__before.width !== __after.width || __before.height !== __after.height)\n ? { from: { width: __before.width, height: __before.height }, to: { width: __after.width, height: __af" +
15333
- "ter.height } } : null;\n " +
15478
+ ";\n // The layout was computed for this size, so the canvas is set to it — and\n // the result SAYS so: a 1080x1080 canvas became 1280x720 silently (gate C).\n const __before = app.getCanvasSize ? app.getCanvasSize() : null;\n " +
15479
+ String(fitTo ? `// FIT, NOT RESIZE (RISO-RESIZE): this style lays out only at its own size, and the canvas is the caller's, so the
15480
+ // canvas is kept and the composition is scaled to fit it, centred, below.
15481
+ const __fit = ${S(fitTo)};` : `if (typeof app.setCanvasSize === 'function') { try { app.setCanvasSize({ width: size.width, height: size.height }); } catch (_) { /* a fixed canvas is not a failure */ } }`) +
15482
+ "\n const __after = app.getCanvasSize ? app.getCanvasSize() : null;\n const canvasResized = __before && __after && (__before.width !== __after.width || __before.height !== __after.height)\n ? { from: { width: __before.width, height: __before.height }, to: { width: __after.width, height: __after.height } } : null;\n " +
15334
15483
  String(scene.backgroundHex ? `if (typeof app.setBackgroundColor === 'function') { try { app.setBackgroundColor(${S(scene.backgroundHex)}); } catch (_) { /* ditto */ } }` : '// this style paints no background of its own') +
15335
15484
  "\n const ops = " +
15336
15485
  String(S(ops)) +
15337
- ";\n const ids = [];\n const failed = [];\n for (const op of ops) {\n const { type, note, name, ...props } = op;\n try {\n const item = app.create(type, props);\n if (item) { ids.push(item.data && item.data.id); } else { failed.push({ name: name, type: type, reason: 'create returned nothing' }); }\n } catch (e) { failed.push({ name: name, type: type, reason: (e && e.message) || String(e) }); }\n }\n if (app.historyManager) app.historyManager.saveState();\n // A partial composition is reported as one. Half a poster that claims\n // success is worse than a poster that says which four elements are missing.\n return { success: failed.length === 0, style: " +
15486
+ ";\n const ids = [];\n const failed = [];\n for (const op of ops) {\n const { type, note, name, ...props } = op;\n try {\n const item = app.create(type, props);\n if (item) { ids.push(item.data && item.data.id); } else { failed.push({ name: name, type: type, reason: 'create returned nothing' }); }\n } catch (e) { failed.push({ name: name, type: type, reason: (e && e.message) || String(e) }); }\n }\n" +
15487
+ String(fitTo ? (" const __k = Math.min(__fit.width / size.width, __fit.height / size.height);\n const __dx = (__fit.width - size.width * __k) / 2, __dy = (__fit.height - size.height * __k) / 2;\n let __fitted = 0;\n for (const id of ids) {\n const it = id && app.getItemById ? app.getItemById(id) : null;\n if (it && typeof it.scale === 'function' && typeof it.translate === 'function' && typeof paper !== 'undefined') {\n it.scale(__k, new paper.Point(0, 0)); it.translate(new paper.Point(__dx, __dy)); __fitted++;\n }\n }\n const __fitReport = { canvas: __fit.width + 'x' + __fit.height, layout: size.width + 'x' + size.height, scale: Math.round(__k * 1000) / 1000, itemsFitted: __fitted,\n " +
15488
+ " note: 'this style lays out only at ' + size.width + 'x' + size.height + ', so it was drawn at that size and scaled to fit your ' + __fit.width + 'x' + __fit.height + ' canvas, centred; the canvas was kept. Pass width and height for the style at its own size (the canvas is then resized).' };\n if (typeof paper !== 'undefined' && paper.view) paper.view.draw();\n"
15489
+
15490
+
15491
+
15492
+
15493
+
15494
+
15495
+
15496
+
15497
+
15498
+
15499
+ ) : '') +
15500
+ " if (app.historyManager) app.historyManager.saveState();\n // A partial composition is reported as one. Half a poster that claims\n // success is worse than a poster that says which four elements are missing.\n return { success: failed.length === 0, style: " +
15338
15501
  String(S(style)) +
15339
- ", itemIds: ids, itemCount: ids.length, failed: failed,\n ...(canvasResized ? { canvasResized: canvasResized, note: 'the canvas was resized to the composition' + (" +
15502
+ ", itemIds: ids, itemCount: ids.length, failed: failed," +
15503
+ String(fitTo ? ' fittedToCanvas: __fitReport,' : '') +
15504
+ "\n ...(canvasResized ? { canvasResized: canvasResized, note: 'the canvas was resized to the composition' + (" +
15340
15505
  String(S(sizeWarning ?? '')) +
15341
15506
  " ? '' : '; pass width and height to compose at your canvas size') } : {}),\n ...(" +
15342
- String(S(sizeWarning ?? '')) +
15507
+ String(S(fitTo ? '' : (sizeWarning ?? ''))) +
15343
15508
  " ? { warning: " +
15344
15509
  String(S(sizeWarning ?? '')) +
15345
15510
  " } : {}), ..." +
@@ -15352,6 +15517,15 @@ String(S(report)) +
15352
15517
 
15353
15518
 
15354
15519
 
15520
+
15521
+
15522
+
15523
+
15524
+
15525
+
15526
+
15527
+
15528
+
15355
15529
  ).trim();
15356
15530
  }
15357
15531
  /**
@@ -15361,6 +15535,66 @@ String(S(report)) +
15361
15535
  * reassembled into it. Naming them individually is the point: the kit's
15362
15536
  * options were discoverable only by reading FxTool's source.
15363
15537
  */
15538
+ /**
15539
+ * pinepaper_instances — a swarm of up to a million marks as one item. Every field passes through to the
15540
+ * engine, which validates it and names a bad one; the reply carries describeInstances, so a subsampled or
15541
+ * capped swarm says so (drawn vs count) in the call that made it.
15542
+ */
15543
+ generateInstances(input) {
15544
+ const S = (v) => JSON.stringify(v);
15545
+ const { action, itemId, name, ...rest } = input;
15546
+ const spec = {};
15547
+ for (const [k, v] of Object.entries(rest))
15548
+ if (v !== undefined)
15549
+ spec[k] = v;
15550
+ const refuse = (m) => `if (typeof app.${m} !== 'function') { return { success: false, error: 'this studio has no instances (app.${m}): update PinePaper Studio' }; }`;
15551
+ const why = `(app.lastReceipt && Array.isArray(app.lastReceipt.warnings) && app.lastReceipt.warnings.map(function (w) { return w && (w.message || w.reason); }).filter(Boolean).join(' ')) || ''`;
15552
+ switch (action) {
15553
+ case 'rules':
15554
+ return `
15555
+ // Instances: the rules, layouts and shapes this studio has
15556
+ (function() {
15557
+ // @engine-methods instanceRules
15558
+ ${refuse('instanceRules')}
15559
+ return Object.assign({ success: true }, app.instanceRules());
15560
+ })();`.trim();
15561
+ case 'describe':
15562
+ return `
15563
+ // Instances: describe ${oneLine(itemId ?? '')}
15564
+ (function() {
15565
+ // @engine-methods describeInstances
15566
+ ${refuse('describeInstances')}
15567
+ const d = app.describeInstances(${S(itemId)});
15568
+ if (!d || d.ok === false) { return { success: false, error: (d && (d.error || d.reason)) || 'no swarm has the id ' + ${S(itemId)} }; }
15569
+ return Object.assign({ success: true, itemId: ${S(itemId)} }, d);
15570
+ })();`.trim();
15571
+ case 'modify':
15572
+ return `
15573
+ // Instances: modify ${oneLine(itemId ?? '')}
15574
+ (function() {
15575
+ // @engine-methods setInstances describeInstances
15576
+ ${refuse('setInstances')}
15577
+ const r = app.setInstances(${S(itemId)}, ${S(spec)});
15578
+ if (!r || r.ok === false) { return { success: false, error: (r && (r.error || r.reason)) || ${why} || 'the swarm was not changed' }; }
15579
+ const d = app.describeInstances(${S(itemId)}) || {};
15580
+ return { success: true, itemId: ${S(itemId)}, spec: r.spec, count: d.count, drawn: d.drawn, subsampled: d.subsampled, path: d.path, determinism: d.determinism };
15581
+ })();`.trim();
15582
+ case 'create':
15583
+ default:
15584
+ return `
15585
+ // Instances: create a swarm${spec.count ? ` of ${oneLine(String(spec.count))}` : ''}
15586
+ (function() {
15587
+ // @engine-methods describeInstances
15588
+ ${refuse('describeInstances')}
15589
+ const item = app.create('instances', ${S(spec)});
15590
+ if (!item) { return { success: false, error: ${why} || 'the studio made no swarm from that spec' }; }
15591
+ const itemId = item.data && (item.data.registryId || item.data.id);${name ? `
15592
+ try { item.name = ${S(name)}; } catch (_) {}` : ''}
15593
+ const d = app.describeInstances(itemId) || {};
15594
+ return { success: true, itemId: itemId, count: d.count, drawn: d.drawn, subsampled: d.subsampled, path: d.path, rule: d.rule, determinism: d.determinism };
15595
+ })();`.trim();
15596
+ }
15597
+ }
15364
15598
  generateStick(input) {
15365
15599
  const S = (v) => JSON.stringify(v);
15366
15600
  const { action, ...rest } = input;
@@ -15410,7 +15644,7 @@ String(S(report)) +
15410
15644
  if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool (the stick kit is vendored from mcp-cloud)' }; }
15411
15645
  const r = await app.${fn}(${S(opts)});
15412
15646
  if (!r || r.ok === false) { return { success: false, error: (r && (r.reason || r.error)) || 'the stick kit produced nothing' }; }
15413
- return { success: true, ...r };
15647
+ return { success: true, ...r${action === 'figure' ? ", anchor: app.stickFigureAnchorsFeet === true ? 'feet' : 'hip'" : ''} };
15414
15648
  })();`.trim();
15415
15649
  }
15416
15650
  /**
@@ -15765,7 +15999,8 @@ String(S(opts)) +
15765
15999
  ).trim();
15766
16000
  }
15767
16001
  case 'create': {
15768
- const v = (input.visual ?? {});
16002
+ // visual: false means no drawing options. The chip is editor-only, and no export draws it.
16003
+ const v = (input.visual || {});
15769
16004
  const { startTime, duration, ...visualOnly } = v;
15770
16005
  const placement = (startTime !== undefined || duration !== undefined)
15771
16006
  ? { ...(startTime !== undefined ? { startTime } : {}), ...(duration !== undefined ? { duration } : {}) }
@@ -16158,9 +16393,15 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
16158
16393
  if (!ok) { return { success: false, action: 'load_pose', error: "no such pose, or the skeleton it was saved against is gone. list_poses for what is still loadable" }; }
16159
16394
  return { success: true, action: 'load_pose', poseId: ${S(input.poseId)} };`);
16160
16395
  case 'interpolate_poses':
16161
- return wrap('Rigging: interpolate poses', ` const ok = R.interpolatePoses(${S(input.poseIdA)}, ${S(input.poseIdB)}, ${input.t ?? 0.5});
16162
- if (!ok) { return { success: false, action: 'interpolate_poses', error: "one of the two poses does not exist, or they belong to DIFFERENT skeletons — blending across rigs is not defined. Call list_poses to see which skeleton each belongs to" }; }
16163
- return { success: true, action: 'interpolate_poses', t: ${input.t ?? 0.5} };`);
16396
+ return wrap('Rigging: interpolate poses', (" // interpolatePoses RETURNS the blended angles and changes nothing; they were discarded and the call said success.\n const angles = R.interpolatePoses(" +
16397
+ String(S(input.poseIdA)) +
16398
+ ", " +
16399
+ String(S(input.poseIdB)) +
16400
+ ", " +
16401
+ String(input.t ?? 0.5) +
16402
+ ");\n if (!angles) { return { success: false, action: 'interpolate_poses', error: \"one of the two poses does not exist, or they belong to DIFFERENT skeletons — blending across rigs is not defined. Call list_poses to see which skeleton each belongs to\" }; }\n const __pose = R.getPose(" +
16403
+ String(S(input.poseIdA)) +
16404
+ ");\n const skeletonId = __pose && __pose.skeletonId;\n if (!skeletonId || typeof R.applyBoneAngles !== 'function') { return { success: false, action: 'interpolate_poses', boneAngles: angles, error: 'the blend was computed but could not be applied: this studio does not say which skeleton pose ' + " + String(S(input.poseIdA)) + " + ' belongs to. Apply boneAngles yourself, or key them with add_pose_keyframe.' }; }\n R.applyBoneAngles(skeletonId, angles);\n if (app.historyManager) app.historyManager.saveState();\n return { success: true, action: 'interpolate_poses', t: " + String(input.t ?? 0.5) + ", skeletonId: skeletonId, applied: true, boneAngles: angles };"));
16164
16405
  case 'play_pose_sequence': {
16165
16406
  const opts = S({
16166
16407
  ...(input.loop !== undefined ? { loop: input.loop } : {}),
@@ -16222,15 +16463,21 @@ String(S(input.transitionName)) +
16222
16463
  // saves. It is not a procedural generator, it is a pose player, and
16223
16464
  // saying "needs a skeleton" when the skeleton is right there sends an
16224
16465
  // agent looking in the wrong place.
16225
- auto_walk: "at least 2 saved walk poses (walk_00…, or walk_contact_L / walk_passing_L / walk_contact_R / walk_passing_R). Run load_pose_library with libraryName 'humanoid' (or 'quadruped') first — it saves exactly those",
16466
+ auto_walk: "at least 2 saved walk poses (walk_00…, or walk_contact_L / walk_passing_L / walk_contact_R / walk_passing_R). A person or robot rig: run load_pose_library with libraryName 'humanoid' (a cat: 'quadruped') first, which saves exactly those. A rig with no walk cycle (a bird, a fish) moves another way: apply_pose_transition 'bird_flap' or 'fish_swim' (they loop), or move_root to carry it",
16226
16467
  auto_breath: 'a bone named spine, chest, upper_spine or body',
16227
16468
  auto_idle: 'a bone named head, hip, upper_hub or spine',
16228
- auto_jump: 'a skeleton, and saved jump poses if you want a posed jump rather than a root arc',
16469
+ auto_jump: "saved jump poses (idle, jump_anticipation, jump_apex, jump_land; load_pose_library 'humanoid' saves them). A bird has none: apply_pose_transition 'bird_flap' instead",
16229
16470
  };
16230
- return wrap(`Rigging: ${input.action}`, ` const res = R.${method}(${S(input.skeletonId)}, ${S(input.options ?? {})});
16231
- if (res && app.historyManager) app.historyManager.saveState();
16232
- if (!res) { return { success: false, action: ${S(input.action)}, error: ${S(`refused — needs ${needs[input.action]}.`)} }; }
16233
- return { success: true, action: ${S(input.action)}, result: res };`);
16471
+ return wrap(`Rigging: ${input.action}`, (" // Through the app's door where it has one: that is where the studio writes the rig-aware receipt (auto-* kinds).\n const res = app." +
16472
+ String(method) +
16473
+ " ? app." +
16474
+ String(method) +
16475
+ "(" +
16476
+ String(S(input.skeletonId)) +
16477
+ ", " +
16478
+ String(S(input.options ?? {})) +
16479
+ ") : R." +
16480
+ String(method) + "(" + String(S(input.skeletonId)) + ", " + String(S(input.options ?? {})) + ");\n // A refusal is null on older studios and { ok: false, reason, kind } on newer ones (rig-aware): both are refusals.\n const __refused = !res || res.ok === false;\n if (!__refused && app.historyManager) app.historyManager.saveState();\n // The studio's own reason first, where it gives one (rig-aware: it knows the rig); this list otherwise.\n const __why = (app.lastReceipt && Array.isArray(app.lastReceipt.warnings) ? app.lastReceipt.warnings : [])\n .filter(function (w) { return w && /^auto-/.test(String(w.kind || w.code || '')); }).map(function (w) { return w.message || w.reason; }).filter(Boolean).join(' ');\n if (__refused) { return { success: false, action: " + String(S(input.action)) + ", error: (res && res.reason) || __why || " + String(S(`refused — needs ${needs[input.action]}.`)) + ", ...(res && res.kind ? { code: res.kind } : {}) }; }\n return { success: true, action: " + String(S(input.action)) + ", result: res };"));
16234
16481
  }
16235
16482
  case 'move_root':
16236
16483
  return wrap('Rigging: root locomotion track', ` const ok = R.moveRoot(${S(input.skeletonId)}, ${S(input.keyframes ?? [])}, ${S(input.options ?? {})});
@@ -16512,15 +16759,28 @@ String(op) +
16512
16759
  // pulse — a runtime fire, not a scene edit (no history save)
16513
16760
  const eventIdJson = JSON.stringify(input.eventId);
16514
16761
  const payloadJson = JSON.stringify(input.payload ?? null);
16515
- return `
16516
- // Pulse event: ${oneLine(input.eventId)}
16517
- (function() {
16518
- if (typeof app.pulseEvent !== 'function') {
16519
- return { success: false, error: 'app.pulseEvent unavailable — update FxTool to a build with the event system (S11)' };
16520
- }
16521
- app.pulseEvent(${eventIdJson}, ${payloadJson});
16522
- return { success: true, action: 'pulse', eventId: ${eventIdJson} };
16523
- })();`.trim();
16762
+ return ("\n// Pulse event: " +
16763
+ String(oneLine(input.eventId)) +
16764
+ "\n(function() {\n if (typeof app.pulseEvent !== 'function') {\n return { success: false, error: 'app.pulseEvent unavailable — update FxTool to a build with the event system (S11)' };\n }\n // A PULSE NOTHING HEARS IS NOT A SUCCESS (tester, 1.6.36: a pulse of an id no event has returned success:true).\n // An event's channel is its id; a literal name is a channel only where a rule or relation listens on it.\n const __id = " +
16765
+ String(eventIdJson) +
16766
+ ";\n const __all = app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : [];\n const __isEvent = __all.some(function(e) { return e && e.item && e.item.data && e.item.data.isEvent && (e.id || e.itemId) === __id; });\n const __heard = (app.listContraptions && (app.listContraptions() || []).some(function(c) { return c && c.sourceEvent === __id; }))\n || !!(app.relationRegistry && app.relationRegistry.getAssociations && (app.relationRegistry.getAssociations(__id) || []).length);\n if (!__isEvent && !__heard) {\n const __named = __all.filter(function(e) { return e && e.item && e.item.data && e.item.data.isEvent && e.item.data.eventName === __id; }).map(function(e) {" +
16767
+ " return e.id || e.itemId; });\n return { success: false, action: 'pulse', eventId: __id, code: 'event_not_found',\n error: 'no event has the id ' + JSON.stringify(__id) + ' and no rule or relation listens on it, so the pulse would reach nothing; nothing was fired.'\n + (__named.length ? ' ' + JSON.stringify(__id) + ' is the name of event ' + __named.join(', ') + ': pulse its id.' : ' Create the event first (action: create), or pulse the id it returned.') };\n }\n app.pulseEvent(__id, " +
16768
+ String(payloadJson) +
16769
+ ");\n return { success: true, action: 'pulse', eventId: __id };\n})();"
16770
+
16771
+
16772
+
16773
+
16774
+
16775
+
16776
+
16777
+
16778
+
16779
+
16780
+
16781
+
16782
+
16783
+ ).trim();
16524
16784
  }
16525
16785
  }
16526
16786
  /**
@@ -16556,8 +16816,12 @@ String(jsLiteral(targets)) +
16556
16816
  ".filter((id) => !app.getItemById(id));\n // AN EVENT'S NAME IS NOT ITS CHANNEL (DBLEXP33). A timed event and a pulse by eventId both fire the id create\n // returned; a rule on the event's name ran only when that literal name was pulsed, so it stayed at fireCount 0\n // with no word said. Named here, with the id to use.\n const __src = " +
16557
16817
  String(jsLiteral(input.sourceEvent ?? '')) +
16558
16818
  ";\n const __named = (app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : [])\n .filter((e) => e && e.item && e.item.data && e.item.data.isEvent && e.item.data.eventName === __src && (e.id || e.itemId) !== __src)\n .map((e) => e.id || e.itemId);\n // A studio that resolves a name held by one event to its id (FxTool #346) stores the id: read the rule back\n // rather than trust a flag, so the warning is true on every build.\n const __stored = typeof app.listContraptions === 'function'\n ? (app.listContraptions() || []).find((c) => c && c.id === contraptionId) : null;\n const __resolved = !!(__stored && __named.indexOf(__stored.sourceEvent) >= 0);\n const " +
16559
- "__warnings = [];\n if (missing.length) __warnings.push('no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does');\n if (__named.length > 1 && !__resolved) __warnings.push(__named.length + ' events are named ' + JSON.stringify(__src) + ' (' + __named.join(', ') + '), so the rule cannot tell which one: it runs only when the literal name is pulsed. Pass one eventId as sourceEvent.');\n else if (__named.length && !__resolved) __warnings.push('sourceEvent ' + JSON.stringify(__src) + ' is the NAME of event ' + __named[0] + ': its timer and a pulse of its id fire ' + __named[0] + ', not the name, so this rule runs only when the literal name ' + JSON.strin" +
16560
- "gify(__src) + ' is pulsed. Use sourceEvent ' + JSON.stringify(__named[0]) + ' to run it when that event fires.');\n return { success: true, action: 'add_contraption', contraptionId,\n ...(__resolved ? { sourceEventResolvedTo: __stored.sourceEvent } : __named.length === 1 ? { eventIdForName: __named[0] } : {}),\n ...(__warnings.length ? { warning: __warnings.join(' ') } : {}), };"
16819
+ "__warnings = [];\n if (missing.length) __warnings.push('no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does');\n // A source no event has, by id or by name (tester, 1.6.36: accepted with no word). A literal channel is allowed:\n // the rule runs when that exact string is pulsed.\n const __srcIsEvent = (app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : []).some((e) => e && e.item && e.item.data && e.item.data.isEvent && (e.id || e.itemId) === __src);\n if (__src && !__srcIsEvent && !__named.length) __warnings.push('no event has the id or name ' + JSON.stringify(__src) + ' yet, so nothing fires this rule until an event wi" +
16820
+ "th that id exists or that exact string is pulsed.');\n if (__named.length > 1 && !__resolved) __warnings.push(__named.length + ' events are named ' + JSON.stringify(__src) + ' (' + __named.join(', ') + '), so the rule cannot tell which one: it runs only when the literal name is pulsed. Pass one eventId as sourceEvent.');\n else if (__named.length && !__resolved) __warnings.push('sourceEvent ' + JSON.stringify(__src) + ' is the NAME of event ' + __named[0] + ': its timer and a pulse of its id fire ' + __named[0] + ', not the name, so this rule runs only when the literal name ' + JSON.stringify(__src) + ' is pulsed. Use sourceEvent ' + JSON.stringify(__named[0]) + ' to run it when that eve" +
16821
+ "nt fires.');\n return { success: true, action: 'add_contraption', contraptionId,\n ...(__resolved ? { sourceEventResolvedTo: __stored.sourceEvent } : __named.length === 1 ? { eventIdForName: __named[0] } : {}),\n ...(__warnings.length ? { warning: __warnings.join(' ') } : {}), };"
16822
+
16823
+
16824
+
16561
16825
 
16562
16826
 
16563
16827
 
@@ -16765,7 +17029,7 @@ String(JSON.stringify(props)) +
16765
17029
  String(JSON.stringify(easing ?? null)) +
16766
17030
  "); });\n const keys = Object.keys(byTime).map(function(t) { return byTime[t]; }).sort(function(a, b) { return a.time - b.time; });\n const beforeTimes = track.map(function(k) { return k.time; });\n app.addAnimation(" +
16767
17031
  String(JSON.stringify(itemId)) +
16768
- ", keys, { mode: 'merge' });\n const after = (item.data && Array.isArray(item.data.keyframes)) ? item.data.keyframes : [];\n const kept = beforeTimes.every(function(t) { return after.some(function(k) { return k.time === t; }); });\n if (!kept) return { success: false, error: 'this studio replaced the track instead of merging into it (no merge mode): the earlier keys are gone. Put every key in one keyframe_animate call.', atTime: AT };\n if (app.historyManager) app.historyManager.saveState();\n return { success: true, itemId: " +
17032
+ ", keys, { mode: 'merge', timeUnits: 'seconds' });\n const after = (item.data && Array.isArray(item.data.keyframes)) ? item.data.keyframes : [];\n const kept = beforeTimes.every(function(t) { return after.some(function(k) { return k.time === t; }); });\n if (!kept) return { success: false, error: 'this studio replaced the track instead of merging into it (no merge mode): the earlier keys are gone. Put every key in one keyframe_animate call.', atTime: AT };\n if (app.historyManager) app.historyManager.saveState();\n return { success: true, itemId: " +
16769
17033
  String(JSON.stringify(itemId)) +
16770
17034
  ", atTime: AT, autoKeyed: true, changes: changes, keys: after.length,\n note: 'written as keyframes (a temporal edit), not a static change; pinepaper_query_mutations shows the series.' };\n})();"
16771
17035