@pinepaper.studio/mcp-server 1.6.33 → 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 +102 -1
- 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/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/tools/batch-macros.d.ts +20 -1
- package/dist/tools/batch-macros.d.ts.map +1 -1
- package/dist/tools/batch-macros.js +146 -7
- package/dist/tools/batch-macros.js.map +1 -1
- package/dist/tools/compact-descriptions.js +1 -1
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +87 -68
- 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.d.ts.map +1 -1
- package/dist/tools/handlers.js +24 -10
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +8 -1
- package/dist/tools/lane.d.ts.map +1 -1
- package/dist/tools/lane.js +54 -13
- package/dist/tools/lane.js.map +1 -1
- package/dist/tools/minimal-descriptions.js +1 -1
- package/dist/tools/minimal-descriptions.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/tools/toolkits.d.ts +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +7 -2
- package/dist/tools/toolkits.js.map +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 +298 -69
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +74 -30
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +13 -8
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -124,18 +124,29 @@ ${itemVarName}.${propertyName} = (function() {
|
|
|
124
124
|
};
|
|
125
125
|
})();`;
|
|
126
126
|
}
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
// A LINEAR RAMP FITS THE ITEM, top to bottom, as the engine's own app.create does (FIREFLY31). This was a fixed
|
|
128
|
+
// 100 px diagonal at the requested position. Leaving the ends out is not enough here: this assigns straight to
|
|
129
|
+
// Paper, where a gradient with no ends is zero-length. An end the caller gave is kept as given.
|
|
130
|
+
const fx = position.x, fy = position.y;
|
|
131
|
+
const o = origin ? `[${origin[0]}, ${origin[1]}]`
|
|
132
|
+
: destination ? `[${destination[0]}, __gb ? __gb.y : ${fy - 50}]`
|
|
133
|
+
: `(__gb ? [__gb.x + __gb.width / 2, __gb.y] : [${fx}, ${fy - 50}])`;
|
|
134
|
+
const d = destination ? `[${destination[0]}, ${destination[1]}]`
|
|
135
|
+
: origin ? `[${origin[0]}, __gb ? __gb.y + __gb.height : ${fy + 50}]`
|
|
136
|
+
: `(__gb ? [__gb.x + __gb.width / 2, __gb.y + __gb.height] : [${fx}, ${fy + 50}])`;
|
|
130
137
|
return `
|
|
131
|
-
${itemVarName}.${propertyName} = {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
138
|
+
${itemVarName}.${propertyName} = (function() {
|
|
139
|
+
const __b = ${itemVarName} && ${itemVarName}.bounds;
|
|
140
|
+
const __gb = __b && isFinite(__b.x) && isFinite(__b.y) && __b.width > 0 && __b.height > 0 ? __b : null;
|
|
141
|
+
return {
|
|
142
|
+
gradient: {
|
|
143
|
+
stops: [${stopsCode}],
|
|
144
|
+
radial: false
|
|
145
|
+
},
|
|
146
|
+
origin: ${o},
|
|
147
|
+
destination: ${d}
|
|
148
|
+
};
|
|
149
|
+
})();`;
|
|
139
150
|
}
|
|
140
151
|
/**
|
|
141
152
|
* Generate shadow code for an item
|
|
@@ -363,7 +374,10 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
363
374
|
createScale.scaleY = num(properties.scaleY);
|
|
364
375
|
const hasCreateScale = Object.keys(createScale).length > 0;
|
|
365
376
|
const SCALE_KEYS = ['scale', 'scaleX', 'scaleY'];
|
|
366
|
-
|
|
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'
|
|
367
381
|
? (!CREATE_KNOWN_KEYS.has(k) && !textOnly(k) && !DIRECTION_KEYS.includes(k))
|
|
368
382
|
|| (k === 'letterSpacing' && letterSpacingCss(properties[k]) === undefined)
|
|
369
383
|
: !CREATE_KNOWN_KEYS.has(k) || textOnly(k));
|
|
@@ -411,6 +425,10 @@ const item = app.create(${jsLiteral(itemType)}, ${JSON.stringify(params, null, 2
|
|
|
411
425
|
if (opacity !== undefined) {
|
|
412
426
|
code += `\nitem.opacity = ${opacity};`;
|
|
413
427
|
}
|
|
428
|
+
if (createVisible !== undefined) {
|
|
429
|
+
code += `
|
|
430
|
+
if (item) item.visible = ${createVisible};`;
|
|
431
|
+
}
|
|
414
432
|
if (hasCreateScale) {
|
|
415
433
|
code += `
|
|
416
434
|
const __scale = (function(it) {
|
|
@@ -1547,6 +1565,31 @@ export function animationOptionWarning(options) {
|
|
|
1547
1565
|
return unreadKeysWarning('animationOptions', ANIMATION_OPTION_KEYS, options);
|
|
1548
1566
|
}
|
|
1549
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;`;
|
|
1550
1593
|
/**
|
|
1551
1594
|
* A visibility reaction reads params.visible and nothing else: absent means show. `value`, which the other set_*
|
|
1552
1595
|
* reactions take, was ignored, so {value: false} SHOWED the item and a hide did nothing (issue #29 O4).
|
|
@@ -1576,14 +1619,18 @@ String(JSON.stringify(sourceId)) +
|
|
|
1576
1619
|
String(JSON.stringify(targetId)) +
|
|
1577
1620
|
"].filter(function(id) { return !app._isKnownRelationEndpoint(norm(id)); });\n if (bad.length) {\n return { success: false, relationType: " +
|
|
1578
1621
|
String(jsLiteral(relationType)) +
|
|
1579
|
-
", 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(" +
|
|
1580
1627
|
String(jsLiteral(sourceId)) +
|
|
1581
1628
|
", " +
|
|
1582
1629
|
String(jsLiteral(targetId)) +
|
|
1583
1630
|
", " +
|
|
1584
1631
|
String(jsLiteral(relationType)) +
|
|
1585
|
-
", " +
|
|
1586
|
-
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();
|
|
1587
1634
|
}
|
|
1588
1635
|
/**
|
|
1589
1636
|
* The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
|
|
@@ -1721,6 +1768,11 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1721
1768
|
const f = o.fade;
|
|
1722
1769
|
if (f && typeof f === 'object' && !Array.isArray(f))
|
|
1723
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
|
+
}
|
|
1724
1776
|
};
|
|
1725
1777
|
scalarRanges('spec', out, [...SCALAR_ONLY, ...SPEC_ONLY]);
|
|
1726
1778
|
bursts.forEach((b, i) => { if (b && typeof b === 'object')
|
|
@@ -2305,6 +2357,29 @@ function generateExecuteGeneratorCode(generatorName, params, region) {
|
|
|
2305
2357
|
* at all and draws full-canvas). So the item is checked first and refused by
|
|
2306
2358
|
* name, and the result says whether this studio clips to shapes.
|
|
2307
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
|
+
}
|
|
2308
2383
|
function emitGeneratorRun(generatorName, paramsJson, region) {
|
|
2309
2384
|
const options = JSON.stringify(region ? { region } : {});
|
|
2310
2385
|
const itemId = region && typeof region.shape === 'object' ? region.shape.itemId : null;
|
|
@@ -3602,7 +3677,7 @@ String(JSON.stringify(validated.presetId)) +
|
|
|
3602
3677
|
if (validated.targetId)
|
|
3603
3678
|
return code;
|
|
3604
3679
|
return code
|
|
3605
|
-
.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,`)
|
|
3606
3681
|
.replace(`relation between ${validated.sourceId} and ${validated.sourceId}.`, `relation on ${validated.sourceId} (no target).`);
|
|
3607
3682
|
}
|
|
3608
3683
|
/**
|
|
@@ -4297,15 +4372,17 @@ String(jsLiteral(source)) +
|
|
|
4297
4372
|
String(jsLiteral(target)) +
|
|
4298
4373
|
"];\n if (sourceId && targetId) {\n const params = " +
|
|
4299
4374
|
String(paramsStr) +
|
|
4300
|
-
";\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, " +
|
|
4301
4378
|
String(jsLiteral(type)) +
|
|
4302
|
-
", params)) {\n results.relations.push({ source: " +
|
|
4379
|
+
", params)) {\n results.relations.push(Object.assign({ source: " +
|
|
4303
4380
|
String(jsLiteral(source)) +
|
|
4304
4381
|
", target: " +
|
|
4305
4382
|
String(jsLiteral(target)) +
|
|
4306
4383
|
", type: " +
|
|
4307
4384
|
String(jsLiteral(type)) +
|
|
4308
|
-
" });\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" +
|
|
4309
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"));
|
|
4310
4387
|
}
|
|
4311
4388
|
// Apply animations
|
|
@@ -5308,6 +5385,45 @@ const __members = app.itemRegistry.getAll()
|
|
|
5308
5385
|
if (!__members.length) throw new Error('no item is named like ' + ${jsLiteral(op.sourceId)} + ': name items with properties.name');`
|
|
5309
5386
|
: `const __members = [{ id: ${srcRef} }];
|
|
5310
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
|
+
}
|
|
5311
5427
|
return ("\n" +
|
|
5312
5428
|
String(stepGuard) +
|
|
5313
5429
|
String(members) +
|
|
@@ -5349,14 +5465,18 @@ String(noTarget ? 'null' : tgtRef) +
|
|
|
5349
5465
|
String(noTarget ? 'null' : 'app.getItemById(targetId)') +
|
|
5350
5466
|
";\nif (!source) throw new Error('Source not found: ' + sourceId);\n" +
|
|
5351
5467
|
String(noTarget ? '' : "if (!target) throw new Error('Target not found: ' + targetId);\n") +
|
|
5352
|
-
"const
|
|
5353
|
-
String(JSON.stringify(String(op.relationType))) +
|
|
5354
|
-
", " +
|
|
5468
|
+
"const __relOpts = " +
|
|
5355
5469
|
String(relOpts) +
|
|
5356
|
-
"
|
|
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 = " +
|
|
5357
5475
|
String(relationRefusalReasonJs(jsLiteral(op.relationType))) +
|
|
5358
5476
|
";\n return { success: false, relationId: false, sourceId, targetId, relationType: " +
|
|
5359
|
-
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");
|
|
5360
5480
|
case 'delete':
|
|
5361
5481
|
const delRef = batchRef(op.itemId);
|
|
5362
5482
|
return `
|
|
@@ -5586,7 +5706,10 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5586
5706
|
}
|
|
5587
5707
|
generateAgentExport(input) {
|
|
5588
5708
|
const validated = AgentExportInputSchema.parse(input);
|
|
5589
|
-
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, motionBlur, ad: adOpts, frames, startTime } = validated;
|
|
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;
|
|
5711
|
+
// Every video is deterministic unless the caller opts out (owner, 2026-11-07), so every receipt says whether its file repeats.
|
|
5712
|
+
const deterministic = askedDeterministic ?? (format === 'mp4' || format === 'webm');
|
|
5590
5713
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
5591
5714
|
const videoEncodeOpts = {};
|
|
5592
5715
|
// A RANGE of the timeline (FxTool #250): frames for t in [startTime, startTime + duration), each exactly the frame
|
|
@@ -5911,34 +6034,42 @@ String(stillTime === undefined ? ` if (['png', 'jpg', 'webp', 'svg', 'pdf'].ind
|
|
|
5911
6034
|
` : '') +
|
|
5912
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 " +
|
|
5913
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," +
|
|
5914
|
-
" and put the playhead back afterwards.\n const __prevT = typeof app.playbackTime === 'number' ? app.playbackTime : 0;\n
|
|
5915
|
-
"
|
|
5916
|
-
|
|
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 = " +
|
|
5917
6041
|
String(region.outputWidth ?? Math.round(region.width)) +
|
|
5918
6042
|
", rh = " +
|
|
5919
6043
|
String(region.outputHeight ?? Math.round(region.height)) +
|
|
5920
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" +
|
|
5921
6045
|
"ckTime : 0));\n const regionUrl = app.renderRegionToDataURL(new paper.Rectangle(" +
|
|
5922
|
-
String(
|
|
6046
|
+
String(regionSrc.x) +
|
|
5923
6047
|
", " +
|
|
5924
|
-
String(
|
|
6048
|
+
String(regionSrc.y) +
|
|
5925
6049
|
", " +
|
|
5926
|
-
String(
|
|
6050
|
+
String(regionSrc.width) +
|
|
5927
6051
|
", " +
|
|
5928
|
-
String(
|
|
6052
|
+
String(regionSrc.height) +
|
|
5929
6053
|
"), rw, rh" +
|
|
5930
6054
|
String(region.excludeForeign === false ? '' : ', { excludeForeign: true }') +
|
|
5931
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 = " +
|
|
5932
|
-
String(
|
|
6056
|
+
String(regionSrc.x) +
|
|
5933
6057
|
", __ry = " +
|
|
5934
|
-
String(
|
|
6058
|
+
String(regionSrc.y) +
|
|
5935
6059
|
", __rr = " +
|
|
5936
|
-
String(
|
|
6060
|
+
String(regionSrc.x + regionSrc.width) +
|
|
5937
6061
|
", __rb = " +
|
|
5938
|
-
String(
|
|
6062
|
+
String(regionSrc.y + regionSrc.height) +
|
|
5939
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" +
|
|
5940
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: " +
|
|
5941
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.')}` : '') +
|
|
5942
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 " +
|
|
5943
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;"
|
|
5944
6075
|
|
|
@@ -5962,6 +6093,11 @@ String(JSON.stringify({ x: region.x, y: region.y, width: region.width, height: r
|
|
|
5962
6093
|
|
|
5963
6094
|
|
|
5964
6095
|
|
|
6096
|
+
|
|
6097
|
+
|
|
6098
|
+
|
|
6099
|
+
|
|
6100
|
+
|
|
5965
6101
|
|
|
5966
6102
|
|
|
5967
6103
|
|
|
@@ -6000,9 +6136,9 @@ String(encodeOptsJs ? `
|
|
|
6000
6136
|
// Broadcast / bitrate. A store that rebuilds its options without
|
|
6001
6137
|
// them is caught by the video report below, not assumed.
|
|
6002
6138
|
${encodeOptsJs.slice(2)},` : '') +
|
|
6003
|
-
"\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 =
|
|
6004
|
-
"
|
|
6005
|
-
"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 = " +
|
|
6006
6142
|
String(INLINE_MAX_BYTES) +
|
|
6007
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 " +
|
|
6008
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 " +
|
|
@@ -6021,21 +6157,42 @@ String(gifLoop !== undefined ? `// A LOOP COUNT (8.30): _quickExportVideo rebuil
|
|
|
6021
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 " +
|
|
6022
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 > " +
|
|
6023
6159
|
String(maxBytes) +
|
|
6024
|
-
") {\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 > " +
|
|
6025
6162
|
String(maxBytes) +
|
|
6026
6163
|
"; k++) {\n const f = Math.sqrt(" +
|
|
6027
6164
|
String(maxBytes) +
|
|
6028
|
-
" /
|
|
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 }" +
|
|
6029
6167
|
String(gifLoop !== undefined ? `, { loop: ${JSON.stringify(gifLoop)} }` : '') +
|
|
6030
|
-
"));\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: " +
|
|
6031
6169
|
String(maxBytes) +
|
|
6032
6170
|
", met: !!(result.success && result.size <= " +
|
|
6033
6171
|
String(maxBytes) +
|
|
6034
|
-
"), 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 > " +
|
|
6035
6173
|
String(maxBytes) +
|
|
6036
|
-
") {\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 ' + " +
|
|
6037
6175
|
String(maxBytes) +
|
|
6038
|
-
" + '-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
|
+
|
|
6039
6196
|
|
|
6040
6197
|
|
|
6041
6198
|
|
|
@@ -6162,7 +6319,8 @@ String(pdfOpts.searchableText !== false) +
|
|
|
6162
6319
|
|
|
6163
6320
|
|
|
6164
6321
|
) : '') +
|
|
6165
|
-
"\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: " +
|
|
6166
6324
|
String(pdfOpts?.dpi ?? 'settings.dpi') +
|
|
6167
6325
|
",\n download: false," +
|
|
6168
6326
|
String(pdfOpts?.searchableText === false ? '\n searchableText: false,' : '') +
|
|
@@ -6171,18 +6329,20 @@ String(pdfOpts?.orientation ? `\n orientation: ${JSON.stringify(pdfOp
|
|
|
6171
6329
|
String(pdfOpts?.bleed !== undefined ? `\n includeBleed: ${pdfOpts.bleed > 0}, bleed: ${pdfOpts.bleed},` : '') +
|
|
6172
6330
|
String(pdfOpts?.trimMarks !== undefined ? `\n trimMarks: ${pdfOpts.trimMarks},` : '') +
|
|
6173
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 " +
|
|
6174
|
-
" // 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 } : {})," +
|
|
6175
|
-
String(pdfOpts?.paperFormat && pdfOpts.paperFormat !== 'custom' &&
|
|
6176
|
-
// Bleed
|
|
6177
|
-
//
|
|
6178
|
-
//
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
"
|
|
6183
|
-
"
|
|
6184
|
-
"
|
|
6185
|
-
"
|
|
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 * " +
|
|
6186
6346
|
String(videoDuration) +
|
|
6187
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" +
|
|
6188
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" +
|
|
@@ -6248,10 +6408,10 @@ String(videoDuration) +
|
|
|
6248
6408
|
".264 needs even dimensions, so\n // 1200x675 came out 1200x676 — and platform specs are exact-pixel. The\n // result's dimensions become what the file IS, and fidelity says why.\n const __vd = __vr && __vr.dimensions;\n if (result && result.success && __vd && Array.isArray(__vd.requested) && Array.isArray(__vd.output)\n && (__vd.requested[0] !== __vd.output[0] || __vd.requested[1] !== __vd.output[1])) {\n result.dimensions = { width: __vd.output[0], height: __vd.output[1] };\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'dimensions_rounded',\n message: 'the file is ' + __vd.outpu" +
|
|
6249
6409
|
"t[0] + 'x' + __vd.output[1] + ', not the requested ' + __vd.requested[0] + 'x' + __vd.requested[1] + ': H.264 needs even dimensions, so an odd side was rounded up by 1 px (the extra row / column is background). For an exact-pixel spec, use an even canvas size, or crop the extra line when re-encoding (ffmpeg -vf crop=' + __vd.requested[0] + ':' + __vd.requested[1] + ':0:0).' }]);\n if (result.fidelity.note) delete result.fidelity.note;\n }\n" +
|
|
6250
6410
|
String(deterministic ? (" // DETERMINISTIC: the container timestamps are pinned either way; the PIXELS\n // are only identical run to run on the software encoder, because the\n // browser may pick a different hardware backend each time (FxTool\n // e34ae03d: 92/255 between backends, 2/255 run to run on hardware). The\n // engine requests software and reports what it got.\n // Where the studio says outright whether the file repeats (bitExact, FxTool\n // PR #31: it test-encodes and keeps whichever encoder does — often the\n // HARDWARE one on a Mac, since Chrome's software H.264 is not repeatable at\n // some sizes), that answer decides; the encoder name is only the fallback\n // for studios that do not r" +
|
|
6251
|
-
"eport it.\n if (result && result.success) {\n const __hw = __vr && __vr.hardwareAcceleration;\n const __exact = __vr && typeof __vr.bitExact === 'boolean' ? __vr.bitExact : null;\n if (__exact === false) {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',\n ...(__vr && __vr.cause ? { cause: __vr.cause } : {}),\n message: '
|
|
6252
|
-
"
|
|
6253
|
-
"
|
|
6254
|
-
|
|
6411
|
+
"eport it.\n if (result && result.success) {\n const __hw = __vr && __vr.hardwareAcceleration;\n const __exact = __vr && typeof __vr.bitExact === 'boolean' ? __vr.bitExact : null;\n if (__exact === false) {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',\n ...(__vr && __vr.determinism && __vr.determinism.cause ? { cause: __vr.determinism.cause } : {}),\n message: 'this file will not repeat byte for byte: no encoder on this machine repeats at this size (the studio test-encoded and checked' + (__vr && __vr.determinism && __vr.determinism.cause === " +
|
|
6412
|
+
"'h264-threaded' ? ': the software H.264 encoder varies run to run at this size on this many CPUs' : '') + '). What still holds: the frames fed to the encoder are identical (compare frameDigest, not sha256), and the container is identical apart from the encoded samples. Do not cache this file by hash. For a byte-identical file, export WebM, or export_png_sequence (pinepaper_interchange) and encode the frames with your own FFmpeg, which the server does not provide.' }]);\n if (result.fidelity.note) delete result.fidelity.note;\n } else if (__exact === null && __hw !== 'prefer-software') {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = " +
|
|
6413
|
+
"(result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',\n message: __hw\n ? 'this file may not repeat byte for byte: the encoder ran with hardwareAcceleration ' + JSON.stringify(__hw) + ': the container is pinned, the pixels may still differ slightly between runs (hardware encoders vary). Do not cache this file by hash.'\n : 'deterministic pinned the container timestamps, but this studio does not report which encoder backend ran, so the pixels may differ slightly between runs if the browser switches between hardware and software encoding. Do not rely on byte-identical files from this studio.' }]);\n if (result.fidelity.note) delete result.fi" +
|
|
6414
|
+
"delity.note;\n }\n }\n"
|
|
6255
6415
|
|
|
6256
6416
|
|
|
6257
6417
|
|
|
@@ -6333,7 +6493,8 @@ String((() => {
|
|
|
6333
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).`,
|
|
6334
6494
|
})};\n`;
|
|
6335
6495
|
})()) +
|
|
6336
|
-
"\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(" +
|
|
6337
6498
|
String(JSON.stringify(format)) +
|
|
6338
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: " +
|
|
6339
6500
|
String(format) +
|
|
@@ -7383,6 +7544,57 @@ String(jsLiteral(format)) +
|
|
|
7383
7544
|
|
|
7384
7545
|
|
|
7385
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
|
+
|
|
7386
7598
|
|
|
7387
7599
|
|
|
7388
7600
|
|
|
@@ -9495,7 +9707,7 @@ String(name ? `\n name: ${JSON.stringify(name)},` : '') +
|
|
|
9495
9707
|
"\n bounds: { x: b.x, y: b.y, width: b.width, height: b.height }" +
|
|
9496
9708
|
String(mask ? `,\n mask: ${jsLiteral(mask)}` : '') +
|
|
9497
9709
|
String(hasShadow ? ',\n shadow: __shadow' : '') +
|
|
9498
|
-
String(loop !== undefined ? ",\n playback: (raster && raster.data && raster.data.animatedImage && raster.data.animatedImage.loop !== undefined) ? { applied: true, plays: raster.data.animatedImage.loop } : { applied: false, warning: 'loop was not applied: this image does not animate in this studio. Animated GIF and
|
|
9710
|
+
String(loop !== undefined ? ",\n playback: (raster && raster.data && raster.data.animatedImage && raster.data.animatedImage.loop !== undefined) ? { applied: true, plays: raster.data.animatedImage.loop } : { applied: false, warning: 'loop was not applied: this image does not animate in this studio. Animated GIF, WebP and APNG do.' }" : '') +
|
|
9499
9711
|
String(hasLook ? ',\n look: __look' : '') +
|
|
9500
9712
|
",\n // Stored smaller than its source (the studio's cap): said, with the\n // way to keep every pixel. The studio reports native size where it can.\n ...((entry && entry.nativeWidth && entry.width && entry.nativeWidth > entry.width)\n ? { stored: { width: entry.width, height: entry.height }, native: { width: entry.nativeWidth, height: entry.nativeHeight },\n note: 'the studio stored this image at ' + entry.width + 'x' + entry.height + ' (its source is ' + entry.nativeWidth + 'x' + entry.nativeHeight + '). Pass maxEdge: \"native\" to keep every pixel.' }\n : {})\n };\n } catch (e) {\n return { error: 'Failed to import image: ' + e.message };\n " +
|
|
9501
9713
|
" }\n})();\n"
|
|
@@ -16335,11 +16547,28 @@ function generateContraptionCode(input) {
|
|
|
16335
16547
|
...(input.elseTrigger ? { elseTrigger: input.elseTrigger } : {}),
|
|
16336
16548
|
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
16337
16549
|
};
|
|
16338
|
-
return
|
|
16339
|
-
|
|
16340
|
-
const
|
|
16341
|
-
|
|
16342
|
-
|
|
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
|
+
);
|
|
16343
16572
|
}
|
|
16344
16573
|
case 'remove_contraption':
|
|
16345
16574
|
return `${refuse('removeContraption')}
|