@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.
- package/README.md +95 -1
- package/dist/agent/types.d.ts +12 -12
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +3 -1
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +70 -61
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +30 -3
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +346 -4
- package/dist/tools/engine-lists.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +1 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +19 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/export-file-receipt.d.ts +33 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -1
- package/dist/tools/export-file-receipt.js +49 -0
- package/dist/tools/export-file-receipt.js.map +1 -1
- package/dist/tools/frozen-output.d.ts +33 -0
- package/dist/tools/frozen-output.d.ts.map +1 -0
- package/dist/tools/frozen-output.js +73 -0
- package/dist/tools/frozen-output.js.map +1 -0
- package/dist/tools/handlers.d.ts +8 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +107 -14
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.js +2 -2
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/types/code-generator.d.ts +11 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +365 -188
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +77 -267
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +15 -136
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +1 -1
- 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
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
2220
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
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
|
-
|
|
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
|
|
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');` : '') +
|
|
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
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
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
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
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;\
|
|
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
|
-
") + ').
|
|
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
|
-
|
|
11732
|
-
(
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
app.deformPresets.apply(entry.item,
|
|
11737
|
-
|
|
11738
|
-
|
|
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
|
-
|
|
12742
|
-
(function() {
|
|
12743
|
-
|
|
12744
|
-
const gid =
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
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
|
|
13822
|
-
"
|
|
13823
|
-
"
|
|
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
|
|
14068
|
-
const
|
|
14069
|
-
const
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
String(
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
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
|
|
14136
|
-
"
|
|
14137
|
-
"
|
|
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
|
-
|
|
14306
|
-
(function() {
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14311
|
-
|
|
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
|
|
15359
|
-
|
|
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
|
|
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
|
|
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',
|
|
16249
|
-
|
|
16250
|
-
|
|
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).
|
|
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:
|
|
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}`,
|
|
16318
|
-
|
|
16319
|
-
|
|
16320
|
-
|
|
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
|
-
|
|
16604
|
-
(function() {
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
}
|
|
16608
|
-
|
|
16609
|
-
return { success: true, action: 'pulse', eventId:
|
|
16610
|
-
|
|
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
|
|
16647
|
-
"
|
|
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
|
|