@pinepaper.studio/mcp-server 1.6.34 → 1.6.35
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 +40 -0
- package/dist/build-info.json +2 -2
- package/dist/character/perform.d.ts +14 -0
- package/dist/character/perform.d.ts.map +1 -1
- package/dist/character/perform.js +29 -3
- package/dist/character/perform.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +40 -35
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +4 -4
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +13 -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 +8 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/beats.d.ts +1 -1
- package/dist/tools/handlers.js +9 -2
- 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 +32 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +268 -52
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +65 -30
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +3 -3
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -374,7 +374,10 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
374
374
|
createScale.scaleY = num(properties.scaleY);
|
|
375
375
|
const hasCreateScale = Object.keys(createScale).length > 0;
|
|
376
376
|
const SCALE_KEYS = ['scale', 'scaleX', 'scaleY'];
|
|
377
|
-
|
|
377
|
+
// VISIBLE AT CREATE. create() does not read visible, so a shape made hidden (a morph target, a reveal) was
|
|
378
|
+
// drawn; it was reported ignored. A boolean is applied to the item right after create.
|
|
379
|
+
const createVisible = typeof properties.visible === 'boolean' ? properties.visible : undefined;
|
|
380
|
+
const ignored = Object.keys(properties).filter((k) => !(k === 'visible' && createVisible !== undefined)).filter((k) => !(hasCreateScale && SCALE_KEYS.includes(k) && num(properties[k]) !== undefined)).filter((k) => itemType === 'text'
|
|
378
381
|
? (!CREATE_KNOWN_KEYS.has(k) && !textOnly(k) && !DIRECTION_KEYS.includes(k))
|
|
379
382
|
|| (k === 'letterSpacing' && letterSpacingCss(properties[k]) === undefined)
|
|
380
383
|
: !CREATE_KNOWN_KEYS.has(k) || textOnly(k));
|
|
@@ -422,6 +425,10 @@ const item = app.create(${jsLiteral(itemType)}, ${JSON.stringify(params, null, 2
|
|
|
422
425
|
if (opacity !== undefined) {
|
|
423
426
|
code += `\nitem.opacity = ${opacity};`;
|
|
424
427
|
}
|
|
428
|
+
if (createVisible !== undefined) {
|
|
429
|
+
code += `
|
|
430
|
+
if (item) item.visible = ${createVisible};`;
|
|
431
|
+
}
|
|
425
432
|
if (hasCreateScale) {
|
|
426
433
|
code += `
|
|
427
434
|
const __scale = (function(it) {
|
|
@@ -1558,6 +1565,31 @@ export function animationOptionWarning(options) {
|
|
|
1558
1565
|
return unreadKeysWarning('animationOptions', ANIMATION_OPTION_KEYS, options);
|
|
1559
1566
|
}
|
|
1560
1567
|
const VISIBILITY_REACTION = /^on_(event|enter|exit)_set_visibility$/;
|
|
1568
|
+
/** The relations that PULSE params.event (FxTool js/relations/triggers.js dispatchTrigger). */
|
|
1569
|
+
const EVENT_PULSING_RELATIONS = new Set(['on_complete', 'at_beat']);
|
|
1570
|
+
/**
|
|
1571
|
+
* ONCOMP34: AN EVENT'S NAME IS NOT ITS CHANNEL, here as in DBLEXP33. on_complete / at_beat pulse params.event
|
|
1572
|
+
* verbatim, and a reaction listens on its source's id, so {event: 'done'} (the name of event item_4) pulsed a
|
|
1573
|
+
* channel nothing listens on: bound, stored, no warning, never seen. A name exactly one event holds, and that no
|
|
1574
|
+
* item has as its id, is put back as that event's id before the bind; a name two events share is named instead.
|
|
1575
|
+
* Statements, run in the page; `paramsVar` is mutated in place and `__ev` holds the reply fields.
|
|
1576
|
+
*/
|
|
1577
|
+
function eventNameResolveJs(relationType, paramsVar) {
|
|
1578
|
+
if (!EVENT_PULSING_RELATIONS.has(String(relationType)))
|
|
1579
|
+
return 'const __ev = {};';
|
|
1580
|
+
return `const __ev = {};
|
|
1581
|
+
(function(p) {
|
|
1582
|
+
if (!p || typeof p.event !== 'string' || !p.event || app.getItemById(p.event)) return;
|
|
1583
|
+
const ids = (app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : [])
|
|
1584
|
+
.filter(function(e) { return e && e.item && e.item.data && e.item.data.isEvent && e.item.data.eventName === p.event; })
|
|
1585
|
+
.map(function(e) { return e.id || e.itemId; });
|
|
1586
|
+
if (ids.length === 1) { __ev.eventResolvedFrom = p.event; __ev.eventResolvedTo = ids[0]; p.event = ids[0]; }
|
|
1587
|
+
else if (ids.length > 1) __ev.warning = ids.length + ' events are named ' + JSON.stringify(p.event) + ' (' + ids.join(', ') + '), so params.event cannot say which one: it pulses the literal name, which no reaction bound to an event hears. Pass one event id as params.event.';
|
|
1588
|
+
})(${paramsVar});`;
|
|
1589
|
+
}
|
|
1590
|
+
/** Spread into a reply after its own fields: the resolve fields, with an event warning joined onto any warning. */
|
|
1591
|
+
const EV_MERGE_JS = (replyVar, key = 'warning') => `Object.keys(__ev).forEach(function(k) { if (k !== 'warning') ${replyVar}[k] = __ev[k]; });
|
|
1592
|
+
if (__ev.warning) ${replyVar}.${key} = ${replyVar}.${key} ? ${replyVar}.${key} + ' ' + __ev.warning : __ev.warning;`;
|
|
1561
1593
|
/**
|
|
1562
1594
|
* A visibility reaction reads params.visible and nothing else: absent means show. `value`, which the other set_*
|
|
1563
1595
|
* reactions take, was ignored, so {value: false} SHOWED the item and a hide did nothing (issue #29 O4).
|
|
@@ -1587,14 +1619,18 @@ String(JSON.stringify(sourceId)) +
|
|
|
1587
1619
|
String(JSON.stringify(targetId)) +
|
|
1588
1620
|
"].filter(function(id) { return !app._isKnownRelationEndpoint(norm(id)); });\n if (bad.length) {\n return { success: false, relationType: " +
|
|
1589
1621
|
String(jsLiteral(relationType)) +
|
|
1590
|
-
", error: bad.map(function(id) { return JSON.stringify(id); }).join(' and ')\n + ' ' + (bad.length > 1 ? 'are not items' : 'is not an item') + ' on the canvas, so the relation would be stored and skipped every frame. Check the id (pinepaper_get_items lists them).' };\n }\n }\n const
|
|
1622
|
+
", error: bad.map(function(id) { return JSON.stringify(id); }).join(' and ')\n + ' ' + (bad.length > 1 ? 'are not items' : 'is not an item') + ' on the canvas, so the relation would be stored and skipped every frame. Check the id (pinepaper_get_items lists them).' };\n }\n }\n const __params = " +
|
|
1623
|
+
String(JSON.stringify(params, null, 2)) +
|
|
1624
|
+
";\n " +
|
|
1625
|
+
String(eventNameResolveJs(relationType, '__params')) +
|
|
1626
|
+
"\n const success = app.addRelation(" +
|
|
1591
1627
|
String(jsLiteral(sourceId)) +
|
|
1592
1628
|
", " +
|
|
1593
1629
|
String(jsLiteral(targetId)) +
|
|
1594
1630
|
", " +
|
|
1595
1631
|
String(jsLiteral(relationType)) +
|
|
1596
|
-
", " +
|
|
1597
|
-
String(
|
|
1632
|
+
", __params);\n if (!success) {\n const __why = " +
|
|
1633
|
+
String(relationRefusalReasonJs(jsLiteral(relationType))) + ";\n return { success: false, relationType: " + String(jsLiteral(relationType)) + ", error: 'the studio refused the ' + " + String(jsLiteral(relationType)) + " + ' relation between ' + " + String(jsLiteral(sourceId)) + " + ' and ' + " + String(jsLiteral(targetId)) + " + (__why ? ': ' + __why : '.') };\n }\n app.historyManager.saveState();\n const __reply = { success: true, sourceId: " + String(jsLiteral(sourceId)) + ", targetId: " + String(jsLiteral(targetId)) + ", relationType: " + String(jsLiteral(relationType)) + String(relationOptionFields(relationType, params, 'warning')) + " };\n " + String(EV_MERGE_JS('__reply')) + "\n return __reply;\n})();\n").trim();
|
|
1598
1634
|
}
|
|
1599
1635
|
/**
|
|
1600
1636
|
* The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
|
|
@@ -1732,6 +1768,11 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1732
1768
|
const f = o.fade;
|
|
1733
1769
|
if (f && typeof f === 'object' && !Array.isArray(f))
|
|
1734
1770
|
scalarRanges(`${where}.fade`, f, ['in', 'out']);
|
|
1771
|
+
// fade takes 'none', 'linear' (or true / false) or { in, out }. Any other word is not read, so the
|
|
1772
|
+
// particles keep the default linear fade over their life: fade 'out' looked like an option (EMITFADE34).
|
|
1773
|
+
if (typeof f === 'string' && f !== 'none' && f !== 'linear') {
|
|
1774
|
+
warnings.push(`${where}.fade ${JSON.stringify(f)} is not a fade the studio reads, so the particles keep the default linear fade over their whole life. Use 'none', 'linear', or { in, out } in seconds (or fadeIn / fadeOut).`);
|
|
1775
|
+
}
|
|
1735
1776
|
};
|
|
1736
1777
|
scalarRanges('spec', out, [...SCALAR_ONLY, ...SPEC_ONLY]);
|
|
1737
1778
|
bursts.forEach((b, i) => { if (b && typeof b === 'object')
|
|
@@ -2316,6 +2357,29 @@ function generateExecuteGeneratorCode(generatorName, params, region) {
|
|
|
2316
2357
|
* at all and draws full-canvas). So the item is checked first and refused by
|
|
2317
2358
|
* name, and the result says whether this studio clips to shapes.
|
|
2318
2359
|
*/
|
|
2360
|
+
/** Relations the studio declares ONCE over a set (multiInstance: false in js/relations/collection.js). */
|
|
2361
|
+
export const COLLECTION_RELATIONS = new Set(['phase_wave', 'ring_forming', 'flock']);
|
|
2362
|
+
/**
|
|
2363
|
+
* The part of an export region that is rendered: the region itself, or, when
|
|
2364
|
+
* outputWidth / outputHeight have another aspect, the largest box of that aspect
|
|
2365
|
+
* centred in it (a cover crop). Never larger than the region.
|
|
2366
|
+
*/
|
|
2367
|
+
export function regionCoverCrop(r) {
|
|
2368
|
+
const outW = r.outputWidth ?? Math.round(r.width);
|
|
2369
|
+
const outH = r.outputHeight ?? Math.round(r.height);
|
|
2370
|
+
const want = outW / outH, have = r.width / r.height;
|
|
2371
|
+
// Under half a source pixel of difference on either side is the same box.
|
|
2372
|
+
if (!(outW > 0 && outH > 0) || Math.abs(r.width - r.height * want) < 0.5 && Math.abs(r.height - r.width / want) < 0.5) {
|
|
2373
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height, cropped: false };
|
|
2374
|
+
}
|
|
2375
|
+
const round = (v) => Math.round(v * 1000) / 1000;
|
|
2376
|
+
if (want > have) {
|
|
2377
|
+
const h = r.width / want;
|
|
2378
|
+
return { x: r.x, y: round(r.y + (r.height - h) / 2), width: r.width, height: round(h), cropped: true };
|
|
2379
|
+
}
|
|
2380
|
+
const w = r.height * want;
|
|
2381
|
+
return { x: round(r.x + (r.width - w) / 2), y: r.y, width: round(w), height: r.height, cropped: true };
|
|
2382
|
+
}
|
|
2319
2383
|
function emitGeneratorRun(generatorName, paramsJson, region) {
|
|
2320
2384
|
const options = JSON.stringify(region ? { region } : {});
|
|
2321
2385
|
const itemId = region && typeof region.shape === 'object' ? region.shape.itemId : null;
|
|
@@ -3613,7 +3677,7 @@ String(JSON.stringify(validated.presetId)) +
|
|
|
3613
3677
|
if (validated.targetId)
|
|
3614
3678
|
return code;
|
|
3615
3679
|
return code
|
|
3616
|
-
.replace(`
|
|
3680
|
+
.replace(`const __reply = { success: true, sourceId: ${jsLiteral(validated.sourceId)}, targetId: ${jsLiteral(validated.sourceId)},`, `const __reply = { success: true, sourceId: ${jsLiteral(validated.sourceId)}, targetId: null,`)
|
|
3617
3681
|
.replace(`relation between ${validated.sourceId} and ${validated.sourceId}.`, `relation on ${validated.sourceId} (no target).`);
|
|
3618
3682
|
}
|
|
3619
3683
|
/**
|
|
@@ -4308,15 +4372,17 @@ String(jsLiteral(source)) +
|
|
|
4308
4372
|
String(jsLiteral(target)) +
|
|
4309
4373
|
"];\n if (sourceId && targetId) {\n const params = " +
|
|
4310
4374
|
String(paramsStr) +
|
|
4311
|
-
";\n
|
|
4375
|
+
";\n " +
|
|
4376
|
+
String(eventNameResolveJs(type, 'params')) +
|
|
4377
|
+
"\n // A refused relation is not listed as made (owner rule 1).\n if (app.addRelation(sourceId, targetId, " +
|
|
4312
4378
|
String(jsLiteral(type)) +
|
|
4313
|
-
", params)) {\n results.relations.push({ source: " +
|
|
4379
|
+
", params)) {\n results.relations.push(Object.assign({ source: " +
|
|
4314
4380
|
String(jsLiteral(source)) +
|
|
4315
4381
|
", target: " +
|
|
4316
4382
|
String(jsLiteral(target)) +
|
|
4317
4383
|
", type: " +
|
|
4318
4384
|
String(jsLiteral(type)) +
|
|
4319
|
-
" });\n const __o = { _: 0" +
|
|
4385
|
+
" }, __ev.eventResolvedTo ? { eventResolvedFrom: __ev.eventResolvedFrom, eventResolvedTo: __ev.eventResolvedTo } : {}));\n if (__ev.warning) (results.optionWarnings = results.optionWarnings || []).push(__ev.warning);\n const __o = { _: 0" +
|
|
4320
4386
|
String(relationOptionFields(type, params, 'warning')) + " };\n if (__o.warning) (results.optionWarnings = results.optionWarnings || []).push(__o.warning);\n if (__o.ignoredOptions) (results.ignoredOptions = results.ignoredOptions || []).push.apply(results.ignoredOptions, __o.ignoredOptions.map(function(k) { return 'relations[" + String(relationIndex) + "].params.' + k; }));\n } else {\n const __why = " + String(relationRefusalReasonJs(jsLiteral(type))) + ";\n (results.relationsRefused = results.relationsRefused || []).push({ source: " + String(jsLiteral(source)) + ", target: " + String(jsLiteral(target)) + ", type: " + String(jsLiteral(type)) + ", reason: __why || 'the studio refused it' });\n results.success = false;\n results.error = 'the studio refused ' + results.relationsRefused.length + ' relation(s): ' + results.relationsRefused.map(function(r) { return r.type + ' ' + r.source + ' -> ' + r.target + ' (' + r.reason + ')'; }).join('; ') + '. The items were created.';\n }\n }\n})();\n"));
|
|
4321
4387
|
}
|
|
4322
4388
|
// Apply animations
|
|
@@ -5319,6 +5385,45 @@ const __members = app.itemRegistry.getAll()
|
|
|
5319
5385
|
if (!__members.length) throw new Error('no item is named like ' + ${jsLiteral(op.sourceId)} + ': name items with properties.name');`
|
|
5320
5386
|
: `const __members = [{ id: ${srcRef} }];
|
|
5321
5387
|
if (!app.getItemById(__members[0].id)) throw new Error('Source not found: ' + __members[0].id);`;
|
|
5388
|
+
// A COLLECTION RULE IS DECLARED ONCE OVER THE SET (PHASEWAVE32, RINGFORM33). phase_wave, ring_forming
|
|
5389
|
+
// and flock take their members from params.members (or a group's children) and space them by rank in
|
|
5390
|
+
// that list; one relation per item gave each a set of one, so every disc took slot 0 (all at phase 0,
|
|
5391
|
+
// all on the angle-0 point of the ring). Bound once, on the first match, with the matches as members.
|
|
5392
|
+
const rel = String(op.relationType);
|
|
5393
|
+
const relParams = (op.relationOptions ?? {});
|
|
5394
|
+
if (glob && op.update !== true && COLLECTION_RELATIONS.has(rel)) {
|
|
5395
|
+
// ring_forming centres on the source's own bounds by default, and the source is a member that
|
|
5396
|
+
// moves: the centre is fixed at the members' centre as bound, unless a target or center was given.
|
|
5397
|
+
const ringCentre = rel === 'ring_forming' && noTarget && relParams.center === undefined;
|
|
5398
|
+
return ("\n" +
|
|
5399
|
+
String(members) +
|
|
5400
|
+
"\nconst targetId = " +
|
|
5401
|
+
String(noTarget ? 'null' : tgtRef) +
|
|
5402
|
+
";\nconst target = " +
|
|
5403
|
+
String(noTarget ? 'null' : 'app.getItemById(targetId)') +
|
|
5404
|
+
";\n" +
|
|
5405
|
+
String(noTarget ? '' : "if (!target) throw new Error('Target not found: ' + targetId);\n") +
|
|
5406
|
+
"const __ids = __members.map((m) => m.id);\nconst __p = Object.assign({}, " +
|
|
5407
|
+
String(relOpts) +
|
|
5408
|
+
String(relParams.members === undefined ? ', { members: { ids: __ids } }' : '') +
|
|
5409
|
+
");\n" +
|
|
5410
|
+
String(ringCentre ? `{
|
|
5411
|
+
let __b = null;
|
|
5412
|
+
__ids.forEach((id) => { const it = app.getItemById(id); if (it && it.bounds) __b = __b ? __b.unite(it.bounds) : it.bounds.clone(); });
|
|
5413
|
+
if (__b) __p.center = { x: __b.center.x, y: __b.center.y };
|
|
5414
|
+
}
|
|
5415
|
+
` : '') +
|
|
5416
|
+
"const __src = app.getItemById(__ids[0]);\nconst __existing = app.relationRegistry && app.relationRegistry.getAssociations\n && app.relationRegistry.getAssociations(__ids[0], " +
|
|
5417
|
+
String(type) +
|
|
5418
|
+
").some((a) => a.target === targetId || (targetId === null && a.target == null));\nconst ok = (__existing && typeof app.modifyRelationParams === 'function')\n ? (app.modifyRelationParams(__ids[0], targetId, " +
|
|
5419
|
+
String(type) +
|
|
5420
|
+
", __p) || (targetId === null && app.modifyRelationParams(__ids[0], undefined, " +
|
|
5421
|
+
String(type) +
|
|
5422
|
+
", __p)))\n : app.addRelation(__src, target, " +
|
|
5423
|
+
String(type) +
|
|
5424
|
+
", __p);\nif (!ok) {\n const __why = " +
|
|
5425
|
+
String(relationRefusalReasonJs(type)) + ";\n return { success: false, relationType: " + String(type) + ", sourceIds: [__ids[0]], error: 'the studio refused the ' + " + String(type) + " + ' relation over ' + __ids.length + ' members — nothing was bound' + (__why ? ': ' + __why : '.') };\n}\nreturn { success: true, relationType: " + String(type) + ", bound: [__ids[0]], members: __ids, count: __ids.length, targetId" + String(ringCentre ? ', center: __p.center' : '') + String(relationOptionFields(op.relationType, op.relationOptions, 'optionWarning', ['members'])) + " };\n");
|
|
5426
|
+
}
|
|
5322
5427
|
return ("\n" +
|
|
5323
5428
|
String(stepGuard) +
|
|
5324
5429
|
String(members) +
|
|
@@ -5360,14 +5465,18 @@ String(noTarget ? 'null' : tgtRef) +
|
|
|
5360
5465
|
String(noTarget ? 'null' : 'app.getItemById(targetId)') +
|
|
5361
5466
|
";\nif (!source) throw new Error('Source not found: ' + sourceId);\n" +
|
|
5362
5467
|
String(noTarget ? '' : "if (!target) throw new Error('Target not found: ' + targetId);\n") +
|
|
5363
|
-
"const
|
|
5364
|
-
String(JSON.stringify(String(op.relationType))) +
|
|
5365
|
-
", " +
|
|
5468
|
+
"const __relOpts = " +
|
|
5366
5469
|
String(relOpts) +
|
|
5367
|
-
"
|
|
5470
|
+
";\n" +
|
|
5471
|
+
String(eventNameResolveJs(op.relationType, '__relOpts')) +
|
|
5472
|
+
"\nconst relationId = app.addRelation(source, target, " +
|
|
5473
|
+
String(JSON.stringify(String(op.relationType))) +
|
|
5474
|
+
", __relOpts);\n// NOT BOUND IS NOT A SUCCESS (owner rule 1, B119). addRelation answers false\n// when it refuses — spring_follow \"not loaded yet\" — and this op returned\n// { relationId: false } with nothing marking it failed, so the batch said\n// success over a relation that does not exist.\nif (!relationId) {\n const __why = " +
|
|
5368
5475
|
String(relationRefusalReasonJs(jsLiteral(op.relationType))) +
|
|
5369
5476
|
";\n return { success: false, relationId: false, sourceId, targetId, relationType: " +
|
|
5370
|
-
String(JSON.stringify(op.relationType)) +
|
|
5477
|
+
String(JSON.stringify(op.relationType)) +
|
|
5478
|
+
",\n error: 'the studio refused the ' + " +
|
|
5479
|
+
String(JSON.stringify(op.relationType)) + " + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };\n}\nconst __reply = { relationId, sourceId, targetId, relationType: " + String(JSON.stringify(String(op.relationType))) + String(relationOptionFields(op.relationType, op.relationOptions, 'warning')) + " };\n" + String(EV_MERGE_JS('__reply')) + "\nreturn __reply;\n");
|
|
5371
5480
|
case 'delete':
|
|
5372
5481
|
const delRef = batchRef(op.itemId);
|
|
5373
5482
|
return `
|
|
@@ -5598,6 +5707,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5598
5707
|
generateAgentExport(input) {
|
|
5599
5708
|
const validated = AgentExportInputSchema.parse(input);
|
|
5600
5709
|
const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, audioCodec, transparent, alphaQuantizer, deterministic: askedDeterministic, motionBlur, ad: adOpts, frames, startTime } = validated;
|
|
5710
|
+
const regionSrc = region ? regionCoverCrop(region) : undefined;
|
|
5601
5711
|
// Every video is deterministic unless the caller opts out (owner, 2026-11-07), so every receipt says whether its file repeats.
|
|
5602
5712
|
const deterministic = askedDeterministic ?? (format === 'mp4' || format === 'webm');
|
|
5603
5713
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
@@ -5924,34 +6034,42 @@ String(stillTime === undefined ? ` if (['png', 'jpg', 'webp', 'svg', 'pdf'].ind
|
|
|
5924
6034
|
` : '') +
|
|
5925
6035
|
" // A STILL SHOWS WHAT IS ALIVE AT ITS MOMENT. Lifetimes that fall back to the\n // MCP's frame callback are applied by the frame loop, and a seek does not run\n // it — so a png at time 0.8 s drew nothing staged at 0.8 s, and an item past\n // its ttl still showed (retest). The visibility pass runs here for stills.\n if (['png', 'jpg', 'webp', 'svg', 'pdf'].indexOf(format) !== -1) {\n const __L = globalThis.__ppMcp;\n if (__L && __L.lifetimeApp === app && typeof __L.applyLifetimes === 'function') {\n try { __L.applyLifetimes(__stillT !== null ? __stillT : (typeof app.playbackTime === 'number' ? app.playbackTime : 0)); } catch (_) { /* the still renders either way */ }\n " +
|
|
5926
6036
|
" }\n }\n // The engine's report on the audio track of THIS export (FxTool b41860aa:\n // encoder delay compensated, padding past the picture dropped). Cleared\n // first so a report left by an earlier export is never read as this one's.\n const __vx = app.exportEngine && app.exportEngine.videoExporter;\n if (__vx && 'lastAudioReport' in __vx) __vx.lastAudioReport = null;\n if (__vx && 'lastVideoReport' in __vx) __vx.lastVideoReport = null;\n // A STILL AT A CHOSEN MOMENT (round 7 X, 1.57). Without this a png is\n // whatever frame the playhead is on — two identical builds gave PNGs that\n // differed in the animated streak while their MP4s were byte-identical.\n // Seek, render," +
|
|
5927
|
-
" and put the playhead back afterwards.\n const __prevT = typeof app.playbackTime === 'number' ? app.playbackTime : 0;\n
|
|
5928
|
-
"
|
|
5929
|
-
|
|
6037
|
+
" and put the playhead back afterwards.\n const __prevT = typeof app.playbackTime === 'number' ? app.playbackTime : 0;\n // A PDF STILL HOLDS ITS MOMENT (PDFEXP34 a). exportPDF awaits its PDF library\n // and then captures the live view, so a playing timeline ran on past the seek\n // and the page showed about t=1.3 s whatever time asked for. The timeline is\n // paused for the PDF and resumed after it.\n let __pausedForStill = false;\n let __pdfEvaluated = false;\n if (__stillT !== null) {\n if (typeof app.setPlaybackTime !== 'function') {\n return { success: false, platform, format, error: 'app.setPlaybackTime unavailable — this studio cannot render a still at a chosen tim" +
|
|
6038
|
+
"e.' };\n }\n if (format === 'pdf' && app.isPlayingKeyframes && !app.isAnimationPaused) {\n try { app.pauseKeyframeTimeline(); __pausedForStill = true; } catch (_) { /* the seek below still applies */ }\n }\n app.setPlaybackTime(__stillT);\n if (typeof paper !== 'undefined' && paper.view && typeof paper.view.update === 'function') paper.view.update();\n }\n\n try {\n // jpg / webp render as png, then re-encode below.\n switch ((format === 'jpg' || format === 'webp') ? 'png' : format) {\n case 'svg':\n const svgString = app.exportAnimatedSVG ? app.exportAnimatedSVG() : app.exportSVGWithCSS();\n result = {\n success: true,\n pla" +
|
|
6039
|
+
"tform,\n format: 'svg',\n data: svgString,\n mimeType: 'image/svg+xml',\n size: new Blob([svgString]).size,\n hasAnimations: svgString.includes('<animate') || svgString.includes('@keyframes')\n };\n break;\n\n case 'png':\n " +
|
|
6040
|
+
String(region ? ("// A REGION, not the whole board — carousel slices, crops. renderRegionToDataURL\n // re-renders that world rect offscreen without touching the live view;\n // it was reachable only from custom code. A differing output aspect is\n // cropped here first (regionCoverCrop), so the engine only scales.\n if (typeof app.renderRegionToDataURL !== 'function') {\n result = { success: false, platform, format: 'png', error: 'app.renderRegionToDataURL unavailable — update PinePaper Studio to export a region.' };\n break;\n }\n {\n const rw = " +
|
|
5930
6041
|
String(region.outputWidth ?? Math.round(region.width)) +
|
|
5931
6042
|
", rh = " +
|
|
5932
6043
|
String(region.outputHeight ?? Math.round(region.height)) +
|
|
5933
6044
|
";\n // OWNERSHIP BY CENTRE (1.78, FxTool 78ec5916): excludeForeign hides, for\n // this render, a top-level item that reaches in but is centred outside —\n // a neighbouring card's overflowing headline. Older studios ignore the\n // fourth argument, and leave lastRegionExcluded unset.\n if ('lastRegionExcluded' in app) app.lastRegionExcluded = null;\n // A video in the region is waited for at this time (round 12, 12.5):\n // the render drew before the clip's seek landed.\n if (typeof app.awaitMediaAt === 'function') await app.awaitMediaAt(__stillT !== null ? __stillT : (typeof app.playbackTime === 'number' ? app.playba" +
|
|
5934
6045
|
"ckTime : 0));\n const regionUrl = app.renderRegionToDataURL(new paper.Rectangle(" +
|
|
5935
|
-
String(
|
|
6046
|
+
String(regionSrc.x) +
|
|
5936
6047
|
", " +
|
|
5937
|
-
String(
|
|
6048
|
+
String(regionSrc.y) +
|
|
5938
6049
|
", " +
|
|
5939
|
-
String(
|
|
6050
|
+
String(regionSrc.width) +
|
|
5940
6051
|
", " +
|
|
5941
|
-
String(
|
|
6052
|
+
String(regionSrc.height) +
|
|
5942
6053
|
"), rw, rh" +
|
|
5943
6054
|
String(region.excludeForeign === false ? '' : ', { excludeForeign: true }') +
|
|
5944
6055
|
");\n const __excluded = Array.isArray(app.lastRegionExcluded) ? app.lastRegionExcluded.filter(function(id) { return id; }) : null;\n // ITEMS THAT CROSS THE REGION'S EDGE (round 9 II, 1.78). In the sheet\n // recipe — one big canvas, one region per card — text that overflows\n // its cell spills into the next card's export (up to 5,826 foreign px),\n // silently. A region cannot know which card an item belongs to, so it\n // cannot clip per card; it can name every item that is only partly\n // inside it, which is exactly the overflow in either direction.\n const __rx = " +
|
|
5945
|
-
String(
|
|
6056
|
+
String(regionSrc.x) +
|
|
5946
6057
|
", __ry = " +
|
|
5947
|
-
String(
|
|
6058
|
+
String(regionSrc.y) +
|
|
5948
6059
|
", __rr = " +
|
|
5949
|
-
String(
|
|
6060
|
+
String(regionSrc.x + regionSrc.width) +
|
|
5950
6061
|
", __rb = " +
|
|
5951
|
-
String(
|
|
6062
|
+
String(regionSrc.y + regionSrc.height) +
|
|
5952
6063
|
";\n const crossing = [], coveringExcluded = [];\n if (app.itemRegistry && typeof app.itemRegistry.getAll === 'function') {\n app.itemRegistry.getAll().forEach(function(e) {\n const b = e && e.item && e.item.visible !== false && e.item.bounds;\n if (!b || !(b.width > 0)) return;\n const r = b.x + b.width, bt = b.y + b.height;\n const intersects = b.x < __rr && r > __rx && b.y < __rb && bt > __ry;\n const inside = b.x >= __rx - 0.5 && r <= __rr + 0.5 && b.y >= __ry - 0.5 && bt <= __rb + 0.5;\n const covers = b.x <= __rx && r >= __rr && b.y <= __ry && bt >= __rb; // a full-bleed backdrop" +
|
|
5953
6064
|
" is not overflow\n if (intersects && !inside && !covers && crossing.length < 20) crossing.push(e.itemId);\n if (covers && __excluded && (__excluded.indexOf(e.itemId) >= 0 || (e.item.data && __excluded.indexOf(e.item.data.id) >= 0))) coveringExcluded.push(e.itemId);\n });\n }\n result = { success: true, platform, format: 'png', data: regionUrl, mimeType: 'image/png',\n size: Math.round(String(regionUrl).length * 0.75), dimensions: { width: rw, height: rh },\n region: " +
|
|
5954
6065
|
String(JSON.stringify({ x: region.x, y: region.y, width: region.width, height: region.height })) +
|
|
6066
|
+
String(regionSrc.cropped ? `,
|
|
6067
|
+
// A DIFFERENT OUTPUT ASPECT IS A CROP (REGION34). The studio grew the box to
|
|
6068
|
+
// the output's aspect instead, drawing content from outside the region and
|
|
6069
|
+
// black past the canvas edge. The server now trims the region about its
|
|
6070
|
+
// centre to the output's aspect, so the studio is only asked to scale.
|
|
6071
|
+
croppedTo: ${JSON.stringify({ x: regionSrc.x, y: regionSrc.y, width: regionSrc.width, height: regionSrc.height })},
|
|
6072
|
+
cropNote: ${JSON.stringify('the output aspect differs from the region\'s, so the region was cropped about its centre to fit (croppedTo); nothing outside the region is drawn.')}` : '') +
|
|
5955
6073
|
" };\n if (__excluded && __excluded.length) {\n result.excludedItems = __excluded;\n result.excludedNote = 'centred outside this region, so treated as belonging to a neighbouring region and left out of this image.';\n }\n if (coveringExcluded.length) {\n result.warning = 'backdrop item(s) ' + coveringExcluded.join(', ') + ' cover this whole region but are centred outside it, so they were left out as foreign — this image has no background from them. Re-export with region.excludeForeign: false, or give each card its own backdrop.';\n }\n if (crossing.length) {\n result.crossingItems = crossing;\n " +
|
|
5956
6074
|
" result.warning = (result.warning ? result.warning + ' ' : '') + crossing.length + ' item(s) are only partly inside this region, so part of them is cut off here or spills into a neighbouring region: ' + crossing.join(', ') + '. In a sheet of cards this is text overflowing its card — shorten it, shrink it, or wrap it.';\n }\n }\n break;"
|
|
5957
6075
|
|
|
@@ -5975,6 +6093,11 @@ String(JSON.stringify({ x: region.x, y: region.y, width: region.width, height: r
|
|
|
5975
6093
|
|
|
5976
6094
|
|
|
5977
6095
|
|
|
6096
|
+
|
|
6097
|
+
|
|
6098
|
+
|
|
6099
|
+
|
|
6100
|
+
|
|
5978
6101
|
|
|
5979
6102
|
|
|
5980
6103
|
|
|
@@ -6013,9 +6136,9 @@ String(encodeOptsJs ? `
|
|
|
6013
6136
|
// Broadcast / bitrate. A store that rebuilds its options without
|
|
6014
6137
|
// them is caught by the video report below, not assumed.
|
|
6015
6138
|
${encodeOptsJs.slice(2)},` : '') +
|
|
6016
|
-
"\n });\n if (!stored || stored.ok === false) {\n result = { success: false, platform, format, error: (stored && stored.reason) || 'the export store refused the export without saying why' };\n } else {\n let chunkBytes = 4 * 1024 * 1024;\n try {\n const declared = app.exportEngine.constructor && app.exportEngine.constructor.EXPORT_CHUNK_BYTES;\n if (typeof declared === 'number' && declared > 0) chunkBytes =
|
|
6017
|
-
"
|
|
6018
|
-
"instead of a Blob and FileReader throws on it, and an export\n // past the inline ceiling used to die somewhere in the base64 rather\n // than say so. The ceiling sits above what this path could already do\n // (60 s at 8 Mbps is roughly 60 MB) so nothing that works today starts\n // refusing.\n const INLINE_MAX_BYTES = " +
|
|
6139
|
+
"\n });\n if (!stored || stored.ok === false) {\n result = { success: false, platform, format, error: (stored && stored.reason) || 'the export store refused the export without saying why',\n // The studio's code for a refusal (audio_codec_unavailable, B45), for a caller to branch on.\n ...(stored && typeof stored.code === 'string' ? { code: stored.code } : {}) };\n } else {\n let chunkBytes = 4 * 1024 * 1024;\n try {\n const declared = app.exportEngine.constructor && app.exportEngine.constructor.EXPORT_CHUNK_BYTES;\n if (typeof declared === 'number' && declared > 0) chunkBytes = d" +
|
|
6140
|
+
"eclared;\n } catch (_) { /* keep the default */ }\n result = {\n success: true, platform, framing,\n // Trust the container the store reports over the one asked for:\n // mp4 falls back to WebM without WebCodecs, and WebM bytes named\n // .mp4 fail in container-validating players.\n format: stored.format || format,\n retained: true,\n exportId: stored.exportId,\n mimeType: stored.mimeType || videoMimeType,\n size: stored.size,\n chunkBytes: chunkBytes,\n dimensions: cameraDims || dimensions,\n };\n }\n " +
|
|
6141
|
+
" break;\n }\n\n // NO EXPORT STORE ON THIS STUDIO, or gif. Both remaining failures are\n // named rather than left to blow up: a streamed export hands back a\n // marker instead of a Blob and FileReader throws on it, and an export\n // past the inline ceiling used to die somewhere in the base64 rather\n // than say so. The ceiling sits above what this path could already do\n // (60 s at 8 Mbps is roughly 60 MB) so nothing that works today starts\n // refusing.\n const INLINE_MAX_BYTES = " +
|
|
6019
6142
|
String(INLINE_MAX_BYTES) +
|
|
6020
6143
|
";\n const mb = (n) => (n / (1024 * 1024)).toFixed(1);\n const deliver = async (raw) => {\n // Normalise first: a {blob, …} record would otherwise be reported as\n // \"streamed to a file instead of returning bytes\", which is a true\n // sentence about the wrong problem.\n const blob = asBlob(raw) || raw;\n if (!blob || typeof blob.size !== 'number' || typeof blob.slice !== 'function') {\n return {\n success: false, platform, format,\n error: 'this export streamed to a file instead of returning bytes, and this studio has no app.exportEngine.exportToStore to page it back from — update FxTool for " +
|
|
6021
6144
|
"long-form export',\n };\n }\n if (blob.size > INLINE_MAX_BYTES) {\n return {\n success: false, platform, format, size: blob.size,\n error: 'export is ' + mb(blob.size) + ' MB, over the ' + mb(INLINE_MAX_BYTES)\n + ' MB inline ceiling, and this studio has no app.exportEngine.exportToStore to page it out of — update FxTool, or lower duration/quality',\n };\n }\n // The container the file IS: an encoder that cannot do H.264 at this\n // size writes VP9 WebM (round 12: mp4 at 12000 px came back WebM),\n // and naming it .mp4 breaks players. Said below in fidelity.\n " +
|
|
@@ -6034,21 +6157,42 @@ String(gifLoop !== undefined ? `// A LOOP COUNT (8.30): _quickExportVideo rebuil
|
|
|
6034
6157
|
"\n if (videoResult && videoResult.blob) {\n result = await deliver(videoResult.blob);\n } else {\n result = { success: false, error: format.toUpperCase() + ' export returned no data' };\n }\n " +
|
|
6035
6158
|
String(maxBytes !== undefined ? ("// A BYTE BUDGET (round 9 GG, 8.31 — email wants <= 1 MB). gif.js has no\n // colour-count knob, so size is the lever: GIF bytes scale roughly with\n // pixel count, so the frame is shrunk by sqrt(budget / size) with a\n // margin and re-encoded, at most twice. Every attempt is reported.\n if (format === 'gif' && result && result.success && result.size > " +
|
|
6036
6159
|
String(maxBytes) +
|
|
6037
|
-
") {\n
|
|
6160
|
+
") {\n // THE FILE'S OWN SIZE, from its header (GIFMAX34). The first GIF comes from _quickExportVideo, which\n // may not render at the canvas size, so shrinking \"the canvas\" could ask for a LARGER frame: a\n // 20 kB budget turned a 74 kB GIF into a 170 kB one. Each attempt is measured, a retry never asks for\n // more pixels than the file it replaces, and the smallest file wins.\n const __gifDims = async (b) => {\n try {\n const a = new Uint8Array(await (asBlob(b) || b).slice(0, 10).arrayBuffer());\n if (a[0] === 0x47 && a[1] === 0x49 && a[2] === 0x46) return { width: a[6] | (a[7] << 8), he" +
|
|
6161
|
+
"ight: a[8] | (a[9] << 8) };\n } catch (_) { /* the canvas size stands in */ }\n return null;\n };\n const __d0 = (videoResult && videoResult.blob && await __gifDims(videoResult.blob)) || { width: dimensions.width, height: dimensions.height };\n const attempts = [{ width: __d0.width, height: __d0.height, bytes: result.size }];\n let best = result, bestDims = __d0;\n let w = __d0.width, h = __d0.height;\n for (let k = 0; k < 2 && best.size > " +
|
|
6038
6162
|
String(maxBytes) +
|
|
6039
6163
|
"; k++) {\n const f = Math.sqrt(" +
|
|
6040
6164
|
String(maxBytes) +
|
|
6041
|
-
" /
|
|
6165
|
+
" / best.size) * 0.9;\n w = Math.max(16, Math.min(bestDims.width - 2, Math.round(bestDims.width * f / 2) * 2));\n h = Math.max(16, Math.min(bestDims.height - 2, Math.round(bestDims.height * f / 2) * 2));\n // videoExporter.export, not _quickExportVideo: the latter rebuilds\n // its settings without width / height, so a \"smaller\" retry\n // through it came out the same size.\n if (!app.exportEngine.videoExporter || typeof app.exportEngine.videoExporter.export !== 'function') break;\n const again = await app.exportEngine.videoExporter.export(Object.assign({}, baseVideoSettings, { format: 'gif', width:" +
|
|
6166
|
+
" w, height: h }" +
|
|
6042
6167
|
String(gifLoop !== undefined ? `, { loop: ${JSON.stringify(gifLoop)} }` : '') +
|
|
6043
|
-
"));\n if (!again) break;\n
|
|
6168
|
+
"));\n if (!again) break;\n const r2 = await deliver(again);\n const d2 = (await __gifDims(again)) || { width: w, height: h };\n attempts.push({ width: d2.width, height: d2.height, bytes: r2.size });\n if (r2.success && r2.size < best.size) { best = r2; bestDims = d2; }\n }\n result = best;\n result.budget = { maxBytes: " +
|
|
6044
6169
|
String(maxBytes) +
|
|
6045
6170
|
", met: !!(result.success && result.size <= " +
|
|
6046
6171
|
String(maxBytes) +
|
|
6047
|
-
"), attempts: attempts };\n if (result.success && result.size > " +
|
|
6172
|
+
"), attempts: attempts };\n // The file delivered is said by its own size, met or not (GIFMAX34: a missed budget kept the\n // requested dimensions in the reply).\n if (result.success) result.dimensions = { width: bestDims.width, height: bestDims.height };\n if (result.success && (bestDims.width !== dimensions.width || bestDims.height !== dimensions.height)) {\n result.downscaled = { from: { width: dimensions.width, height: dimensions.height }, to: { width: bestDims.width, height: bestDims.height }, reason: bestDims === __d0 ? 'studio' : 'maxBytes' };\n }\n if (result.success && result.size > " +
|
|
6048
6173
|
String(maxBytes) +
|
|
6049
|
-
") {\n result.warning = 'still ' + result.size + ' bytes
|
|
6174
|
+
") {\n result.warning = 'still ' + result.size + ' bytes at ' + bestDims.width + 'x' + bestDims.height + ', the smallest of ' + attempts.length + ' tries — over the ' + " +
|
|
6050
6175
|
String(maxBytes) +
|
|
6051
|
-
" + '-byte budget. Lower fps, shorten duration, or use quality \"draft\".';\n }
|
|
6176
|
+
" + '-byte budget. Lower fps, shorten duration, or use quality \"draft\".';\n }\n }"
|
|
6177
|
+
|
|
6178
|
+
|
|
6179
|
+
|
|
6180
|
+
|
|
6181
|
+
|
|
6182
|
+
|
|
6183
|
+
|
|
6184
|
+
|
|
6185
|
+
|
|
6186
|
+
|
|
6187
|
+
|
|
6188
|
+
|
|
6189
|
+
|
|
6190
|
+
|
|
6191
|
+
|
|
6192
|
+
|
|
6193
|
+
|
|
6194
|
+
|
|
6195
|
+
|
|
6052
6196
|
|
|
6053
6197
|
|
|
6054
6198
|
|
|
@@ -6175,7 +6319,8 @@ String(pdfOpts.searchableText !== false) +
|
|
|
6175
6319
|
|
|
6176
6320
|
|
|
6177
6321
|
) : '') +
|
|
6178
|
-
"\n if (app.exportEngine && app.exportEngine.exportPDF) {\n // Print options reach the engine (they were engine-only before), and\n // download:false, because the tool delivers the bytes itself.\n // lastPdfReport is a report PROPERTY (9917cfa5), read through the\n // engine object like the exporter's audio / video reports, and only\n // when present.\n const __pee = app.exportEngine;\n if ('lastPdfReport' in __pee) __pee.lastPdfReport = null;\n
|
|
6322
|
+
"\n if (app.exportEngine && app.exportEngine.exportPDF) {\n // Print options reach the engine (they were engine-only before), and\n // download:false, because the tool delivers the bytes itself.\n // lastPdfReport is a report PROPERTY (9917cfa5), read through the\n // engine object like the exporter's audio / video reports, and only\n // when present.\n const __pee = app.exportEngine;\n if ('lastPdfReport' in __pee) __pee.lastPdfReport = null;\n // exportPDF does not evaluate the scene at the playhead as exportPNG\n // does (relations, timed events), so the same engine step runs here,\n // and w" +
|
|
6323
|
+
"hat its timed events changed is put back after the page.\n if (__stillT !== null && typeof __pee._evaluateStillAt === 'function') {\n try { __pee._evaluateStillAt(__stillT); __pdfEvaluated = true; } catch (_) { /* the page renders either way */ }\n if (typeof paper !== 'undefined' && paper.view && typeof paper.view.update === 'function') paper.view.update();\n }\n const pdfOut = await app.exportEngine.exportPDF({\n dpi: " +
|
|
6179
6324
|
String(pdfOpts?.dpi ?? 'settings.dpi') +
|
|
6180
6325
|
",\n download: false," +
|
|
6181
6326
|
String(pdfOpts?.searchableText === false ? '\n searchableText: false,' : '') +
|
|
@@ -6184,18 +6329,20 @@ String(pdfOpts?.orientation ? `\n orientation: ${JSON.stringify(pdfOp
|
|
|
6184
6329
|
String(pdfOpts?.bleed !== undefined ? `\n includeBleed: ${pdfOpts.bleed > 0}, bleed: ${pdfOpts.bleed},` : '') +
|
|
6185
6330
|
String(pdfOpts?.trimMarks !== undefined ? `\n trimMarks: ${pdfOpts.trimMarks},` : '') +
|
|
6186
6331
|
"\n });\n const blob = asBlob(pdfOut);\n if (!blob) {\n const got = pdfOut === null ? 'null'\n : typeof pdfOut === 'object' ? 'an object with keys [' + Object.keys(pdfOut).join(', ') + ']'\n : typeof pdfOut;\n result = { success: false, platform, format: 'pdf',\n error: 'the PDF export did not produce a file: expected a Blob, got ' + got + '.' };\n break;\n }\n const dataUrl = await blobToDataUrl(blob);\n result = {\n success: true, platform, format: 'pdf', data: dataUrl,\n mimeType: 'application/pdf', size: blob.size, dpi: settings.dpi,\n " +
|
|
6187
|
-
" // The record carries the page geometry the engine chose; it is the\n // only place a caller can learn it.\n ...(pdfOut && pdfOut.width ? { dimensions: { width: pdfOut.width, height: pdfOut.height } } : {}),\n ...(pdfOut && pdfOut.format ? { paperFormat: pdfOut.format } : {})," +
|
|
6188
|
-
String(pdfOpts?.paperFormat && pdfOpts.paperFormat !== 'custom' &&
|
|
6189
|
-
// Bleed
|
|
6190
|
-
//
|
|
6191
|
-
//
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
"
|
|
6196
|
-
"
|
|
6197
|
-
"
|
|
6198
|
-
"
|
|
6332
|
+
" // The record carries the page geometry the engine chose; it is the\n // only place a caller can learn it.\n ...(pdfOut && pdfOut.width ? { dimensions: { width: pdfOut.width, height: pdfOut.height } } : {}),\n ...(pdfOut && pdfOut.format ? { paperFormat: pdfOut.format } : {}),\n // What the page carries for print (FxTool #347): marks drawn, bleed applied, page and trim boxes.\n ...(pdfOut && pdfOut.print && typeof pdfOut.print === 'object' ? { print: pdfOut.print } : {})," +
|
|
6333
|
+
String(pdfOpts?.paperFormat && pdfOpts.paperFormat !== 'custom' && pdfOpts.trimMarks ? `
|
|
6334
|
+
// TRIM MARKS ON A NAMED SHEET. Bleed is applied there (page = sheet + 2 x bleed, TrimBox = sheet), but
|
|
6335
|
+
// a studio before FxTool #347 draws no marks on a named sheet. Said unless the file's own record
|
|
6336
|
+
// says it drew them.
|
|
6337
|
+
...((pdfOut && pdfOut.print && pdfOut.print.trimMarksDrawn) || (__pee.lastPdfReport && __pee.lastPdfReport.trimMarksDrawn) ? {} : {
|
|
6338
|
+
warnings: [${JSON.stringify(`trimMarks were passed with paperFormat '${pdfOpts.paperFormat}', and this studio draws trim marks only with paperFormat 'custom' (the page is the canvas plus bleed), so this page has none. Any bleed was applied. For marks, pass paperFormat 'custom' on a canvas sized by a print preset (canvasPreset 'print-a4' etc., 300 dpi).`)}],
|
|
6339
|
+
}),` : '') +
|
|
6340
|
+
"\n };\n } else {\n result = { success: false, error: 'PDF export failed' };\n }\n break;\n\n default:\n result = { success: false, error: 'Unknown format: ' + format };\n }\n } catch (e) {\n result = { success: false, error: (e && typeof e.message === 'string' && e.message) || (typeof e === 'string' && e) || ('the ' + format + ' export threw with no message'), platform, format,\n ...(e && typeof e.code === 'string' ? { code: e.code } : {}) };\n }\n\n // One site, after every branch has set its result: a successful export says\n // what this scene lost to this format, rather than the caller finding out by\n // opening the fi" +
|
|
6341
|
+
"le. Only when there is something to say — see fidelity().\n // RE-ENCODE A STILL FOR A BYTE BUDGET. The png path rendered it (whole board\n // or region); the browser's own encoder makes jpg / webp at the tier's\n // compression. jpg has no alpha, so it is flattened onto the scene background\n // (white when there is none) rather than left to the encoder's black.\n if (result && result.success && (format === 'jpg' || format === 'webp') && typeof result.data === 'string') {\n const mime = format === 'jpg' ? 'image/jpeg' : 'image/webp';\n try {\n const encoded = await new Promise(function(resolve, reject) {\n const img = new Image();\n img.onload = function() {" +
|
|
6342
|
+
"\n const c = document.createElement('canvas');\n c.width = img.naturalWidth; c.height = img.naturalHeight;\n const x = c.getContext('2d');\n // The scene's own background when one is set — white text on a\n // transparent board vanished into a white flatten. White otherwise.\n if (mime === 'image/jpeg') {\n const bg = app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor;\n x.fillStyle = (bg && !/^(transparent|rgba\\([^)]*,\\s*0\\))$/i.test(bg)) ? bg : '#ffffff';\n x.fillRect(0, 0, c.width, c.height);\n }\n x.drawImage(img, 0, 0);\n resolve(c.toDataURL(mime, " +
|
|
6343
|
+
"settings.compression));\n };\n img.onerror = function() { reject(new Error('the rendered frame could not be decoded for re-encoding')); };\n img.src = result.data;\n });\n if (String(encoded).indexOf('data:' + mime) !== 0) {\n result = { success: false, platform, format, error: 'this browser cannot encode ' + format + ' (it returned ' + String(encoded).slice(5, 20) + '…).' };\n } else {\n result = Object.assign({}, result, { format, data: encoded, mimeType: mime, size: Math.round(String(encoded).length * 0.75), encodeQuality: settings.compression });\n }\n } catch (e) {\n result = { success: false, platform, format, error: (e &" +
|
|
6344
|
+
"& typeof e.message === 'string' && e.message) || (typeof e === 'string' && e) || ('re-encoding the still as ' + format + ' failed with no message') };\n }\n }\n\n if (result && result.success) Object.assign(result, fidelity(format));\n // MOTION FROM RELATIONS IS MOTION. The studio's check counts keyframe tracks\n // and simple animations only, so an orbit (or attached_to a moving item, a\n // generator, the camera) exported with animated:false while the frames moved,\n // and callers rebuilt the scene with keyframes. When it says false, the\n // frames decide: four captures across the export. The times are uneven so a\n // loop cannot land on whole periods and look still. After" +
|
|
6345
|
+
" the export, so it\n // cannot touch the file. A capture leaves the pose of the last time it drew,\n // so the samples end on 0 again: the canvas is left at rest, as found.\n if (result && result.success && ['mp4', 'webm', 'gif', 'apng'].indexOf(format) !== -1\n && result.fidelity && result.fidelity.checked && result.fidelity.checked.animated === false\n && typeof app.captureFramesAt === 'function') {\n try {\n const __mt = [0, 0.17, 0.43, 0.71].map((k) => Math.round(k * " +
|
|
6199
6346
|
String(videoDuration) +
|
|
6200
6347
|
" * 1000) / 1000);\n const __mcap = typeof app.captureFramesAtAsync === 'function' ? app.captureFramesAtAsync.bind(app) : app.captureFramesAt.bind(app);\n const __mf = await __mcap(__mt.concat([0]), { seed: 0, capture: (c) => (typeof app.captureFrameDataURL === 'function' ? app.captureFrameDataURL() : (c && c.toDataURL ? c.toDataURL() : '')) });\n const __mu = new Set((__mf || []).filter((u) => typeof u === 'string' && u.length > 0));\n if (__mu.size > 1) {\n result.fidelity.checked.animated = true;\n result.fidelity.checked.motion = 'frames differ across the export (sampled at ' + __mt.join(', ') + ' s): the motion comes from relations, generators or the c" +
|
|
6201
6348
|
"amera, not keyframe tracks';\n }\n } catch (e) { /* the studio's answer stands */ }\n }\n if (result && result.success && __lint && __lint.length && !result.lintWarnings) {\n result.lintWarnings = __lint;\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat(__lint.map(function (w) {\n return { code: String(w.code || 'lint'), message: String(w.message || w.code || 'scene lint warning'), ...(w.itemId ? { items: [w.itemId] } : {}) };\n }));\n if (result.fidelity.note) delete result.fidelity.note;\n }\n if (result && result.success && __fit) result.platformFit = __fit;\n" +
|
|
@@ -6346,7 +6493,8 @@ String((() => {
|
|
|
6346
6493
|
warning: `${frames} frames at ${Math.round(fps * 1000) / 1000} fps is ${actual.toFixed(3)} s — ${((actual - videoDuration) * 1000).toFixed(1)} ms over the ${videoDuration} s asked for. For a hard cap (e.g. a 6 s bumper) use duration ${fit.toFixed(4)} (${Math.floor(videoDuration * fps)} frames).`,
|
|
6347
6494
|
})};\n`;
|
|
6348
6495
|
})()) +
|
|
6349
|
-
"\n if (__stillT !== null) {\n try { app.setPlaybackTime(__prevT); } catch (_) { /* the still is already rendered */ }\n if (result && result.success) { result.time = __stillT; if (__stillTimeNote) result.stillTimeNote = __stillTimeNote; }\n }\n // SCREEN-SPACE ITEMS (FxTool D20): SVG and Lottie write geometry, not the\n // camera's frame map, so a HUD item is placed as if the camera had not
|
|
6496
|
+
"\n if (__pdfEvaluated && typeof app._restoreReactionJournal === 'function') {\n try { app._restoreReactionJournal(); } catch (_) { /* the page is already rendered */ }\n }\n if (__stillT !== null) {\n try { app.setPlaybackTime(__prevT); } catch (_) { /* the still is already rendered */ }\n if (__pausedForStill) { try { app.resumeKeyframeTimeline(); } catch (_) { /* the still is already rendered */ } }\n if (result && result.success) { result.time = __stillT; if (__stillTimeNote) result.stillTimeNote = __stillTimeNote; }\n }\n // SCREEN-SPACE ITEMS (FxTool D20): SVG and Lottie write geometry, not the\n // camera's frame map, so a HUD item is placed as if the camera had not" +
|
|
6497
|
+
" moved.\n if (result && result.success && typeof app.screenSpaceItems === 'function' && ['svg', 'lottie', 'dotlottie'].indexOf(" +
|
|
6350
6498
|
String(JSON.stringify(format)) +
|
|
6351
6499
|
") !== -1) {\n try {\n const __hud = app.screenSpaceItems() || [];\n if (__hud.length) {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'screen_space_ignored', message: __hud.length + ' screen-space (HUD) item(s) are written at their frame coordinates in world space: " +
|
|
6352
6500
|
String(format) +
|
|
@@ -7396,6 +7544,57 @@ String(jsLiteral(format)) +
|
|
|
7396
7544
|
|
|
7397
7545
|
|
|
7398
7546
|
|
|
7547
|
+
|
|
7548
|
+
|
|
7549
|
+
|
|
7550
|
+
|
|
7551
|
+
|
|
7552
|
+
|
|
7553
|
+
|
|
7554
|
+
|
|
7555
|
+
|
|
7556
|
+
|
|
7557
|
+
|
|
7558
|
+
|
|
7559
|
+
|
|
7560
|
+
|
|
7561
|
+
|
|
7562
|
+
|
|
7563
|
+
|
|
7564
|
+
|
|
7565
|
+
|
|
7566
|
+
|
|
7567
|
+
|
|
7568
|
+
|
|
7569
|
+
|
|
7570
|
+
|
|
7571
|
+
|
|
7572
|
+
|
|
7573
|
+
|
|
7574
|
+
|
|
7575
|
+
|
|
7576
|
+
|
|
7577
|
+
|
|
7578
|
+
|
|
7579
|
+
|
|
7580
|
+
|
|
7581
|
+
|
|
7582
|
+
|
|
7583
|
+
|
|
7584
|
+
|
|
7585
|
+
|
|
7586
|
+
|
|
7587
|
+
|
|
7588
|
+
|
|
7589
|
+
|
|
7590
|
+
|
|
7591
|
+
|
|
7592
|
+
|
|
7593
|
+
|
|
7594
|
+
|
|
7595
|
+
|
|
7596
|
+
|
|
7597
|
+
|
|
7399
7598
|
|
|
7400
7599
|
|
|
7401
7600
|
|
|
@@ -16348,11 +16547,28 @@ function generateContraptionCode(input) {
|
|
|
16348
16547
|
...(input.elseTrigger ? { elseTrigger: input.elseTrigger } : {}),
|
|
16349
16548
|
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
16350
16549
|
};
|
|
16351
|
-
return
|
|
16352
|
-
|
|
16353
|
-
const
|
|
16354
|
-
|
|
16355
|
-
|
|
16550
|
+
return ("" +
|
|
16551
|
+
String(refuse('addContraption')) +
|
|
16552
|
+
"\n const contraptionId = app.addContraption(" +
|
|
16553
|
+
String(jsLiteral(config)) +
|
|
16554
|
+
");\n const missing = " +
|
|
16555
|
+
String(jsLiteral(targets)) +
|
|
16556
|
+
".filter((id) => !app.getItemById(id));\n // AN EVENT'S NAME IS NOT ITS CHANNEL (DBLEXP33). A timed event and a pulse by eventId both fire the id create\n // returned; a rule on the event's name ran only when that literal name was pulsed, so it stayed at fireCount 0\n // with no word said. Named here, with the id to use.\n const __src = " +
|
|
16557
|
+
String(jsLiteral(input.sourceEvent ?? '')) +
|
|
16558
|
+
";\n const __named = (app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : [])\n .filter((e) => e && e.item && e.item.data && e.item.data.isEvent && e.item.data.eventName === __src && (e.id || e.itemId) !== __src)\n .map((e) => e.id || e.itemId);\n // A studio that resolves a name held by one event to its id (FxTool #346) stores the id: read the rule back\n // rather than trust a flag, so the warning is true on every build.\n const __stored = typeof app.listContraptions === 'function'\n ? (app.listContraptions() || []).find((c) => c && c.id === contraptionId) : null;\n const __resolved = !!(__stored && __named.indexOf(__stored.sourceEvent) >= 0);\n const " +
|
|
16559
|
+
"__warnings = [];\n if (missing.length) __warnings.push('no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does');\n if (__named.length > 1 && !__resolved) __warnings.push(__named.length + ' events are named ' + JSON.stringify(__src) + ' (' + __named.join(', ') + '), so the rule cannot tell which one: it runs only when the literal name is pulsed. Pass one eventId as sourceEvent.');\n else if (__named.length && !__resolved) __warnings.push('sourceEvent ' + JSON.stringify(__src) + ' is the NAME of event ' + __named[0] + ': its timer and a pulse of its id fire ' + __named[0] + ', not the name, so this rule runs only when the literal name ' + JSON.strin" +
|
|
16560
|
+
"gify(__src) + ' is pulsed. Use sourceEvent ' + JSON.stringify(__named[0]) + ' to run it when that event fires.');\n return { success: true, action: 'add_contraption', contraptionId,\n ...(__resolved ? { sourceEventResolvedTo: __stored.sourceEvent } : __named.length === 1 ? { eventIdForName: __named[0] } : {}),\n ...(__warnings.length ? { warning: __warnings.join(' ') } : {}), };"
|
|
16561
|
+
|
|
16562
|
+
|
|
16563
|
+
|
|
16564
|
+
|
|
16565
|
+
|
|
16566
|
+
|
|
16567
|
+
|
|
16568
|
+
|
|
16569
|
+
|
|
16570
|
+
|
|
16571
|
+
);
|
|
16356
16572
|
}
|
|
16357
16573
|
case 'remove_contraption':
|
|
16358
16574
|
return `${refuse('removeContraption')}
|