@pinepaper.studio/mcp-server 1.6.36 → 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 (48) hide show
  1. package/README.md +95 -1
  2. package/dist/agent/types.d.ts +12 -12
  3. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  4. package/dist/browser/puppeteer-controller.js +3 -1
  5. package/dist/browser/puppeteer-controller.js.map +1 -1
  6. package/dist/build-info.json +2 -2
  7. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  8. package/dist/ontology/upstream-vocabulary.js +2 -2
  9. package/dist/tools/definitions.d.ts.map +1 -1
  10. package/dist/tools/definitions.js +70 -61
  11. package/dist/tools/definitions.js.map +1 -1
  12. package/dist/tools/engine-lists.d.ts +30 -3
  13. package/dist/tools/engine-lists.d.ts.map +1 -1
  14. package/dist/tools/engine-lists.js +346 -4
  15. package/dist/tools/engine-lists.js.map +1 -1
  16. package/dist/tools/engine-methods.d.ts.map +1 -1
  17. package/dist/tools/engine-methods.js +1 -0
  18. package/dist/tools/engine-methods.js.map +1 -1
  19. package/dist/tools/engine-surface.d.ts +1 -1
  20. package/dist/tools/engine-surface.d.ts.map +1 -1
  21. package/dist/tools/engine-surface.js +19 -6
  22. package/dist/tools/engine-surface.js.map +1 -1
  23. package/dist/tools/export-file-receipt.d.ts +33 -0
  24. package/dist/tools/export-file-receipt.d.ts.map +1 -1
  25. package/dist/tools/export-file-receipt.js +49 -0
  26. package/dist/tools/export-file-receipt.js.map +1 -1
  27. package/dist/tools/frozen-output.d.ts +33 -0
  28. package/dist/tools/frozen-output.d.ts.map +1 -0
  29. package/dist/tools/frozen-output.js +73 -0
  30. package/dist/tools/frozen-output.js.map +1 -0
  31. package/dist/tools/handlers.d.ts +8 -0
  32. package/dist/tools/handlers.d.ts.map +1 -1
  33. package/dist/tools/handlers.js +107 -14
  34. package/dist/tools/handlers.js.map +1 -1
  35. package/dist/tools/shape-params.d.ts +2 -2
  36. package/dist/tools/shape-params.js +2 -2
  37. package/dist/tools/stick-vocabulary.d.ts +1 -1
  38. package/dist/tools/stick-vocabulary.js +1 -1
  39. package/dist/types/code-generator.d.ts +11 -1
  40. package/dist/types/code-generator.d.ts.map +1 -1
  41. package/dist/types/code-generator.js +365 -188
  42. package/dist/types/code-generator.js.map +1 -1
  43. package/dist/types/schemas.d.ts +77 -267
  44. package/dist/types/schemas.d.ts.map +1 -1
  45. package/dist/types/schemas.js +15 -136
  46. package/dist/types/schemas.js.map +1 -1
  47. package/manifest.json +1 -1
  48. package/package.json +1 -1
@@ -1578,15 +1578,19 @@ const EVENT_PULSING_RELATIONS = new Set(['on_complete', 'at_beat']);
1578
1578
  function eventNameResolveJs(relationType, paramsVar) {
1579
1579
  if (!EVENT_PULSING_RELATIONS.has(String(relationType)))
1580
1580
  return 'const __ev = {};';
1581
- return `const __ev = {};
1582
- (function(p) {
1583
- if (!p || typeof p.event !== 'string' || !p.event || app.getItemById(p.event)) return;
1584
- const ids = (app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : [])
1585
- .filter(function(e) { return e && e.item && e.item.data && e.item.data.isEvent && e.item.data.eventName === p.event; })
1586
- .map(function(e) { return e.id || e.itemId; });
1587
- if (ids.length === 1) { __ev.eventResolvedFrom = p.event; __ev.eventResolvedTo = ids[0]; p.event = ids[0]; }
1588
- 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.';
1589
- })(${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
+ );
1590
1594
  }
1591
1595
  /** Spread into a reply after its own fields: the resolve fields, with an event warning joined onto any warning. */
1592
1596
  const EV_MERGE_JS = (replyVar, key = 'warning') => `Object.keys(__ev).forEach(function(k) { if (k !== 'warning') ${replyVar}[k] = __ev[k]; });
@@ -1746,6 +1750,17 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
1746
1750
  if (own.count !== undefined || own.velocities !== undefined)
1747
1751
  mismatch(`bursts[${i}]`, own.count ?? out.count, own.velocities ?? out.velocities);
1748
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
+ }
1749
1764
  // A RANGE WHERE THE STUDIO READS ONE NUMBER is not drawn as a range (1.6.24
1750
1765
  // probe: count [a, b] → 0 on older studios, the middle on FxTool #228). Only
1751
1766
  // the keys that take one number on EVERY build are named here. gravity, drag,
@@ -2205,6 +2220,58 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
2205
2220
  : '';
2206
2221
  return `${easingNote}${sizekeyPairNote}${gapNote}`;
2207
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
+ }
2208
2275
  function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
2209
2276
  const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
2210
2277
  const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
@@ -2216,8 +2283,10 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
2216
2283
  opts.clipInPoint = clipInPoint;
2217
2284
  if (clipOutPoint !== undefined)
2218
2285
  opts.clipOutPoint = clipOutPoint;
2219
- if (timeUnits !== undefined)
2220
- 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';
2221
2290
  // addAnimation SETS the item's track; mode 'merge' adds to it (FxTool
2222
2291
  // 2475333b). An engine without merge ignores the option and replaces.
2223
2292
  if (append)
@@ -2267,29 +2336,47 @@ String(JSON.stringify(levelKeys)) +
2267
2336
  const easingAtCheck = easingAt !== undefined ? `
2268
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.');` : '';
2269
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
+ : '';
2270
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 });
2271
2346
  return ("\n// Apply keyframe animation to " +
2272
2347
  String(ID) +
2273
2348
  "\n(function() {\n " +
2274
2349
  String(requireItem(itemId, 'the animation')) +
2275
2350
  String(audioLevelGuard) +
2276
- "\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 = " +
2277
2353
  String(JSON.stringify(opts)) +
2278
- ";\n app.addAnimation(" +
2354
+ ";\n" +
2355
+ String(guard.before) +
2356
+ "\n app.addAnimation(" +
2279
2357
  String(addAnimTarget) +
2280
2358
  ", " +
2281
2359
  String(keyframesJson) +
2282
2360
  ", __opts);\n const __warn = [];" +
2361
+ String(unitsNote) +
2362
+ "\n" +
2363
+ String(guard.after) +
2283
2364
  String(easingAtCheck) +
2284
2365
  String(textKeyCheck) +
2285
2366
  String(filterKeyCheck) +
2286
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; });" +
2287
- String(append ? `
2288
- // Property gaps across the merged track (the same check a single call makes
2289
- // on its own keys at generation time).
2290
- const __all = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes : [];
2291
- 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; }, []);
2292
- 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
+ ) : '') +
2293
2380
  "\n const __track = !__prev.length ? null\n : " +
2294
2381
  String(append ? `(__kept
2295
2382
  ? { appended: true, previousKeys: __prev.length, keys: __now.length }
@@ -2302,7 +2389,12 @@ String(calculatedDuration) +
2302
2389
  ", loop: " +
2303
2390
  String(loop) +
2304
2391
  String(append ? ', append: true' : '') +
2305
- ", ...(__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();
2306
2398
  }
2307
2399
  /**
2308
2400
  * Template for executing generators
@@ -2393,15 +2485,26 @@ String(itemId ? `
2393
2485
  if (!__si) return { success: false, error: ${JSON.stringify(`region.shape names no item "${itemId}" — nothing was drawn. List items with pinepaper_get_items.`)} };
2394
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.' };
2395
2487
  }` : '') +
2396
- "\n await app.executeGenerator(" +
2397
- String(jsLiteral(generatorName)) +
2398
- ", " +
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 = " +
2399
2489
  String(paramsJson) +
2400
- ", " +
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, " +
2401
2495
  String(options) +
2402
2496
  ");" +
2403
2497
  String(shaped ? `
2404
- 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
+ );
2405
2508
  }
2406
2509
  /**
2407
2510
  * Template for applying effects
@@ -5105,7 +5208,29 @@ String(oneLine(operations.length)) +
5105
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 " +
5106
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" +
5107
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" +
5108
- "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"
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
+
5109
5234
 
5110
5235
 
5111
5236
 
@@ -5183,39 +5308,14 @@ String(oneLine(operations.length)) +
5183
5308
  const opItem = [o.itemId, o.targetId, o.sourceId, o.id].find((v) => typeof v === 'string') ?? null;
5184
5309
  const opItemJs = typeof opItem === 'string' && /^\$\d+$/.test(opItem) ? `__refName(${opItem.slice(1)})` : JSON.stringify(opItem);
5185
5310
  const opStep = typeof o._step === 'string' ? `op ${index}: ${o._step}` : `op ${index} (${op.type})`;
5186
- code += ("\n // Operation " +
5187
- String(oneLine(index)) +
5188
- ": " +
5189
- String(oneLine(op.type)) +
5190
- "\n try {\n const result" +
5191
- String(index) +
5192
- " = await (async () => {\n " +
5193
- String(opCode) +
5194
- "\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" +
5195
- String(index) +
5196
- " = _opFailure(result" +
5197
- String(index) +
5198
- ");\n if (failed" +
5199
- String(index) +
5200
- ") {\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 " +
5201
- 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} });
5202
- success = false;`) +
5203
- "\n } else {\n results.push({ index: " +
5204
- String(index) +
5205
- ", success: true, result: result" +
5206
- String(index) +
5207
- " });\n " +
5208
- String(claimsItemSlot ? `if (result${index} && result${index}.itemId) {
5209
- itemIds.push(result${index}.itemId);
5210
- }` : '') +
5211
- "\n }\n } catch (opError) {\n const __r" +
5212
- String(index) +
5213
- " = _why(opError, 'operation " +
5214
- String(index) +
5215
- " (" +
5216
- String(op.type) +
5217
- ") threw with no message');\n results.push({ index: " +
5218
- 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
+ `;
5219
5319
  });
5220
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: " +
5221
5321
  String(operations.length) +
@@ -5585,17 +5685,27 @@ return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget}
5585
5685
  if (op[key] !== undefined && Number.isFinite(v))
5586
5686
  kfExtra.push(`${key}: ${v}`);
5587
5687
  }
5588
- const kfUnits = op.timeUnits;
5589
- if (kfUnits === 'seconds' || kfUnits === 'ms')
5590
- 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
+ : '';
5591
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 });
5592
5700
  const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
5593
5701
  const kfFilterKeysJs = unanimatedFilterKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
5594
5702
  const kfTextCheck = `\nconst _kfIgnored = ${kfTextKeysJs || '[]'};\nconst _kfFilterIgnored = ${kfFilterKeysJs};`;
5595
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(' ') } : {})`;
5596
5704
  return ("\nconst targetId = " +
5597
5705
  String(kfItemRef) +
5598
- ";\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, " +
5599
5709
  String(kfJson) +
5600
5710
  ", { duration: " +
5601
5711
  String(kfDuration) +
@@ -5603,8 +5713,7 @@ String(kfDuration) +
5603
5713
  String(kfLoop) +
5604
5714
  String(kfAppend ? ", mode: 'merge'" : '') +
5605
5715
  String(kfExtraStr) +
5606
- " });" +
5607
- 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");
5608
5717
  }
5609
5718
  case 'apply_mask': {
5610
5719
  const maskItemRef = batchRef(op.itemId);
@@ -6465,7 +6574,10 @@ String(deterministic ? (" // DETERMINISTIC: the container timestamps are pinned
6465
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" +
6466
6575
  "x ' + __vr.luma.max + ', broadcastHeadroom ' + (typeof __vr.headroom === 'number' ? __vr.headroom : " +
6467
6576
  String(JSON.stringify(broadcastHeadroom ?? 12)) +
6468
- ") + '). 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" +
6469
6581
  String(Object.keys(videoEncodeOpts).length ? (" if (result && result.success) {\n const __asked = " +
6470
6582
  String(JSON.stringify(videoEncodeOpts)) +
6471
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" +
@@ -7635,9 +7747,6 @@ String(jsLiteral(format)) +
7635
7747
 
7636
7748
 
7637
7749
 
7638
-
7639
-
7640
-
7641
7750
 
7642
7751
 
7643
7752
 
@@ -11727,15 +11836,23 @@ String(warmArg) +
11727
11836
  if (input.steps !== undefined)
11728
11837
  params.steps = input.steps;
11729
11838
  const itemIdStr = JSON.stringify(input.itemId);
11730
- return `
11731
- // Apply deformation preset
11732
- (function() {
11733
- ${guard}
11734
- const entry = app.itemRegistry.get(${itemIdStr});
11735
- if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
11736
- app.deformPresets.apply(entry.item, ${jsLiteral(input.preset)}, ${JSON.stringify(params)});
11737
- return { success: true, action: 'apply', itemId: ${itemIdStr}, preset: ${jsLiteral(input.preset)} };
11738
- })();`.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();
11739
11856
  }
11740
11857
  case 'trigger': {
11741
11858
  if (!input.itemId)
@@ -12737,17 +12854,28 @@ String(idExpr) +
12737
12854
  }
12738
12855
  if (input.action === 'ungroup') {
12739
12856
  const idExpr = JSON.stringify(input.groupId || '');
12740
- return `
12741
- // Ungroup ${oneLine(input.groupId || '')}
12742
- (function() {
12743
- if (!app.groupManager) return { success: false, error: 'GroupManager not available — update FxTool' };
12744
- const gid = ${idExpr};
12745
- const group = app.getItemById(gid) || (app.groupManager.getGroupById ? app.groupManager.getGroupById(gid) : null);
12746
- if (!group) return { success: false, error: 'Group not found: ' + gid };
12747
- app.groupManager.ungroupAll(group);
12748
- if (app.historyManager) app.historyManager.saveState();
12749
- return { success: true, ungrouped: true, groupId: gid };
12750
- })();`.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();
12751
12879
  }
12752
12880
  const idsArrayExpr = JSON.stringify(input.itemIds ?? []);
12753
12881
  const nameExpr = input.groupName !== undefined ? JSON.stringify(input.groupName) : 'undefined';
@@ -13818,9 +13946,9 @@ String(S(input.beats)) +
13818
13946
  String(id) +
13819
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(" +
13820
13948
  String(id) +
13821
- ", __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" +
13822
- "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" +
13823
- "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: " +
13824
13952
  String(id) +
13825
13953
  ", beats: " +
13826
13954
  String(input.beats.length) +
@@ -14062,63 +14190,52 @@ String(sound ? ` // A sound on each morph (D68's placement, with its MP4-lead n
14062
14190
  * Character tool aliases (pinepaper_create_character, pinepaper_character_action, pinepaper_character_pose)
14063
14191
  * Routing on-device / local model calls cleanly to the engine.
14064
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
+ */
14065
14200
  generateCreateCharacter(input) {
14066
14201
  const S = (v) => JSON.stringify(v);
14067
- const charId = S(input.characterId || input.name || `char_${Date.now()}`);
14068
- const posX = typeof input.x === 'number' ? input.x : 400;
14069
- const posY = typeof input.y === 'number' ? input.y : 300;
14070
- const scale = typeof input.scale === 'number' ? input.scale : 1;
14071
- const name = S(input.name || input.characterId || 'Character');
14072
- const base = input.base ? S(input.base) : 'null';
14073
- const preset = input.preset || input.concept ? S(input.preset || input.concept) : 'null';
14074
- return ("\n// Create Character\n(async function() {\n const charId = " +
14075
- String(charId) +
14076
- ";\n const posX = " +
14077
- String(posX) +
14078
- ";\n const posY = " +
14079
- String(posY) +
14080
- ";\n const scale = " +
14081
- String(scale) +
14082
- ";\n const name = " +
14083
- String(name) +
14084
- ";\n const base = " +
14085
- String(base) +
14086
- ";\n const preset = " +
14087
- String(preset) +
14088
- ";\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" +
14089
- "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" +
14090
- ": '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})();"
14091
-
14092
-
14093
-
14094
-
14095
-
14096
-
14097
-
14098
-
14099
-
14100
-
14101
-
14102
-
14103
-
14104
-
14105
-
14106
-
14107
-
14108
-
14109
-
14110
-
14111
-
14112
-
14113
-
14114
-
14115
-
14116
-
14117
-
14118
-
14119
-
14120
-
14121
-
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})();"
14122
14239
  ).trim();
14123
14240
  }
14124
14241
  generateCharacterAction(input) {
@@ -14132,12 +14249,10 @@ String(S(targetId)) +
14132
14249
  String(S(act)) +
14133
14250
  ";\n const speed = " +
14134
14251
  String(speed) +
14135
- ";\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" +
14136
- "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" +
14137
- "obble') });\n }\n\n if (app._scheduleRepaint) app._scheduleRepaint();\n return { success: true, characterId: targetId || itemId, action: act };\n})();"
14138
-
14139
-
14140
-
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})();"
14141
14256
 
14142
14257
 
14143
14258
 
@@ -14301,14 +14416,19 @@ String(target) +
14301
14416
  ? `, ...((function(w) { return w.length ? { warnings: w } : {}; })(${S(fixed)}.concat(${checks.length ? `__capWarnings(${stored})` : '[]'})))`
14302
14417
  : '';
14303
14418
  if (input.action === 'set') {
14304
- return `
14305
- // Emitter: change a spec
14306
- (function() {
14307
- if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }${capFn}
14308
- const r = app.setEmitter(${S(input.itemId)}, ${S(input.spec)});
14309
- if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the emitter change was refused' }; }
14310
- return { success: true, action: 'set', itemId: ${S(input.itemId)}, spec: r.spec${capJs('r.spec')} };
14311
- })();`.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();
14312
14432
  }
14313
14433
  return `
14314
14434
  // Emitter: create
@@ -15347,7 +15467,7 @@ ${needWorld}
15347
15467
  * already decided by a pure function, so nothing is left for the page to
15348
15468
  * work out.
15349
15469
  */
15350
- generateDesignCompose(scene, ops, style, sizeWarning, report = {}) {
15470
+ generateDesignCompose(scene, ops, style, sizeWarning, report = {}, fitTo) {
15351
15471
  const S = (v) => JSON.stringify(v);
15352
15472
  return ("\n// Design: compose a " +
15353
15473
  String(oneLine(style)) +
@@ -15355,17 +15475,36 @@ String(oneLine(style)) +
15355
15475
  String(oneLine(ops.length)) +
15356
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 = " +
15357
15477
  String(S({ width: scene.width, height: scene.height })) +
15358
- ";\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" +
15359
- "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 " +
15360
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') +
15361
15484
  "\n const ops = " +
15362
15485
  String(S(ops)) +
15363
- ";\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: " +
15364
15501
  String(S(style)) +
15365
- ", 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' + (" +
15366
15505
  String(S(sizeWarning ?? '')) +
15367
15506
  " ? '' : '; pass width and height to compose at your canvas size') } : {}),\n ...(" +
15368
- String(S(sizeWarning ?? '')) +
15507
+ String(S(fitTo ? '' : (sizeWarning ?? ''))) +
15369
15508
  " ? { warning: " +
15370
15509
  String(S(sizeWarning ?? '')) +
15371
15510
  " } : {}), ..." +
@@ -15378,6 +15517,15 @@ String(S(report)) +
15378
15517
 
15379
15518
 
15380
15519
 
15520
+
15521
+
15522
+
15523
+
15524
+
15525
+
15526
+
15527
+
15528
+
15381
15529
  ).trim();
15382
15530
  }
15383
15531
  /**
@@ -16245,9 +16393,15 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
16245
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" }; }
16246
16394
  return { success: true, action: 'load_pose', poseId: ${S(input.poseId)} };`);
16247
16395
  case 'interpolate_poses':
16248
- return wrap('Rigging: interpolate poses', ` const ok = R.interpolatePoses(${S(input.poseIdA)}, ${S(input.poseIdB)}, ${input.t ?? 0.5});
16249
- 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" }; }
16250
- 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 };"));
16251
16405
  case 'play_pose_sequence': {
16252
16406
  const opts = S({
16253
16407
  ...(input.loop !== undefined ? { loop: input.loop } : {}),
@@ -16309,15 +16463,21 @@ String(S(input.transitionName)) +
16309
16463
  // saves. It is not a procedural generator, it is a pose player, and
16310
16464
  // saying "needs a skeleton" when the skeleton is right there sends an
16311
16465
  // agent looking in the wrong place.
16312
- 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",
16313
16467
  auto_breath: 'a bone named spine, chest, upper_spine or body',
16314
16468
  auto_idle: 'a bone named head, hip, upper_hub or spine',
16315
- 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",
16316
16470
  };
16317
- return wrap(`Rigging: ${input.action}`, ` const res = R.${method}(${S(input.skeletonId)}, ${S(input.options ?? {})});
16318
- if (res && app.historyManager) app.historyManager.saveState();
16319
- if (!res) { return { success: false, action: ${S(input.action)}, error: ${S(`refused — needs ${needs[input.action]}.`)} }; }
16320
- 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 };"));
16321
16481
  }
16322
16482
  case 'move_root':
16323
16483
  return wrap('Rigging: root locomotion track', ` const ok = R.moveRoot(${S(input.skeletonId)}, ${S(input.keyframes ?? [])}, ${S(input.options ?? {})});
@@ -16599,15 +16759,28 @@ String(op) +
16599
16759
  // pulse — a runtime fire, not a scene edit (no history save)
16600
16760
  const eventIdJson = JSON.stringify(input.eventId);
16601
16761
  const payloadJson = JSON.stringify(input.payload ?? null);
16602
- return `
16603
- // Pulse event: ${oneLine(input.eventId)}
16604
- (function() {
16605
- if (typeof app.pulseEvent !== 'function') {
16606
- return { success: false, error: 'app.pulseEvent unavailable — update FxTool to a build with the event system (S11)' };
16607
- }
16608
- app.pulseEvent(${eventIdJson}, ${payloadJson});
16609
- return { success: true, action: 'pulse', eventId: ${eventIdJson} };
16610
- })();`.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();
16611
16784
  }
16612
16785
  }
16613
16786
  /**
@@ -16643,8 +16816,12 @@ String(jsLiteral(targets)) +
16643
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 = " +
16644
16817
  String(jsLiteral(input.sourceEvent ?? '')) +
16645
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 " +
16646
- "__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" +
16647
- "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
+
16648
16825
 
16649
16826
 
16650
16827
 
@@ -16852,7 +17029,7 @@ String(JSON.stringify(props)) +
16852
17029
  String(JSON.stringify(easing ?? null)) +
16853
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(" +
16854
17031
  String(JSON.stringify(itemId)) +
16855
- ", 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: " +
16856
17033
  String(JSON.stringify(itemId)) +
16857
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})();"
16858
17035