@pinepaper.studio/mcp-server 1.6.34 → 1.6.36
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.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +84 -6
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +4 -0
- package/dist/browser/puppeteer-controller.js.map +1 -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/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 +113 -67
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/effect-class.d.ts.map +1 -1
- package/dist/tools/effect-class.js +4 -0
- package/dist/tools/effect-class.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 +4 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +19 -8
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/beats.d.ts +1 -1
- package/dist/tools/handlers.d.ts +7 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +43 -3
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +1 -0
- 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.map +1 -1
- package/dist/tools/toolkits.js +2 -1
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +39 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +366 -63
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +100 -33
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +34 -12
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/asset-paths.js +1 -1
- package/dist/utils/asset-paths.js.map +1 -1
- package/manifest.json +5 -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) {
|
|
@@ -495,13 +502,14 @@ const __scale = (function(it) {
|
|
|
495
502
|
if (lineIgnoresPosition) {
|
|
496
503
|
warnings.push('a line runs from `from` to `to` in canvas coordinates, so position did not move it: position is where the line is now. Give the endpoints where the line goes.');
|
|
497
504
|
}
|
|
498
|
-
code += ("\nconst itemId = item.data.registryId;\napp.historyManager.saveState();\n\n// Return item info\n({ itemId, type: " +
|
|
505
|
+
code += ("\nconst itemId = item.data.registryId;\n// Debounced where the studio has it: a full snapshot per create made one-call-per-item building O(items) a call.\napp.historyManager[app.historyManager.saveStateDebounced ? 'saveStateDebounced' : 'saveState']();\n\n// Return item info\n({ itemId, type: " +
|
|
499
506
|
String(jsLiteral(itemType)) +
|
|
500
507
|
String(outlineOnly ? `, fill: ${JSON.stringify(OUTLINE_NOTE)}` : '') +
|
|
501
508
|
String(textFit ? ', textFit: __textFit' : '') +
|
|
502
509
|
String(textDirection ? ', direction: __direction' : '') +
|
|
503
510
|
String(textTabular ? ', tabularFigures: __tabular' : '') +
|
|
504
|
-
String(textTypography.length ? ', typography: __typography' : '') +
|
|
511
|
+
String(textTypography.length ? ', typography: __typography' : '') +
|
|
512
|
+
String(createFillOpacity ? ', fillOpacity: __fillOpacity' : '') + String(textStrokePos ? ', strokePosition: __strokePos' : '') + String(hasCreateScale ? ', scale: __scale' : '') + ", position: " + String(reportActualPosition ? `(item.position ? { x: item.position.x, y: item.position.y } : null)` : `{ x: ${position.x}, y: ${position.y} }`) + String(hasLifetime ? ', lifetime: __lifetime' : '') + String(loadsFont ? ', ...(__font ? { font: __font } : {})' : '') + String(ignored.length > 0
|
|
505
513
|
? `, ignoredProperties: ${JSON.stringify(ignored)}` : '') + String(warnings.length > 0 ? `, warning: ${JSON.stringify(warnings.join(' '))}` : '') + " });");
|
|
506
514
|
// Loading a font is async, and it has to happen BEFORE create() measures the
|
|
507
515
|
// text — so only this case becomes an async IIFE; every other create stays a
|
|
@@ -1558,6 +1566,31 @@ export function animationOptionWarning(options) {
|
|
|
1558
1566
|
return unreadKeysWarning('animationOptions', ANIMATION_OPTION_KEYS, options);
|
|
1559
1567
|
}
|
|
1560
1568
|
const VISIBILITY_REACTION = /^on_(event|enter|exit)_set_visibility$/;
|
|
1569
|
+
/** The relations that PULSE params.event (FxTool js/relations/triggers.js dispatchTrigger). */
|
|
1570
|
+
const EVENT_PULSING_RELATIONS = new Set(['on_complete', 'at_beat']);
|
|
1571
|
+
/**
|
|
1572
|
+
* ONCOMP34: AN EVENT'S NAME IS NOT ITS CHANNEL, here as in DBLEXP33. on_complete / at_beat pulse params.event
|
|
1573
|
+
* verbatim, and a reaction listens on its source's id, so {event: 'done'} (the name of event item_4) pulsed a
|
|
1574
|
+
* channel nothing listens on: bound, stored, no warning, never seen. A name exactly one event holds, and that no
|
|
1575
|
+
* item has as its id, is put back as that event's id before the bind; a name two events share is named instead.
|
|
1576
|
+
* Statements, run in the page; `paramsVar` is mutated in place and `__ev` holds the reply fields.
|
|
1577
|
+
*/
|
|
1578
|
+
function eventNameResolveJs(relationType, paramsVar) {
|
|
1579
|
+
if (!EVENT_PULSING_RELATIONS.has(String(relationType)))
|
|
1580
|
+
return 'const __ev = {};';
|
|
1581
|
+
return `const __ev = {};
|
|
1582
|
+
(function(p) {
|
|
1583
|
+
if (!p || typeof p.event !== 'string' || !p.event || app.getItemById(p.event)) return;
|
|
1584
|
+
const ids = (app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : [])
|
|
1585
|
+
.filter(function(e) { return e && e.item && e.item.data && e.item.data.isEvent && e.item.data.eventName === p.event; })
|
|
1586
|
+
.map(function(e) { return e.id || e.itemId; });
|
|
1587
|
+
if (ids.length === 1) { __ev.eventResolvedFrom = p.event; __ev.eventResolvedTo = ids[0]; p.event = ids[0]; }
|
|
1588
|
+
else if (ids.length > 1) __ev.warning = ids.length + ' events are named ' + JSON.stringify(p.event) + ' (' + ids.join(', ') + '), so params.event cannot say which one: it pulses the literal name, which no reaction bound to an event hears. Pass one event id as params.event.';
|
|
1589
|
+
})(${paramsVar});`;
|
|
1590
|
+
}
|
|
1591
|
+
/** Spread into a reply after its own fields: the resolve fields, with an event warning joined onto any warning. */
|
|
1592
|
+
const EV_MERGE_JS = (replyVar, key = 'warning') => `Object.keys(__ev).forEach(function(k) { if (k !== 'warning') ${replyVar}[k] = __ev[k]; });
|
|
1593
|
+
if (__ev.warning) ${replyVar}.${key} = ${replyVar}.${key} ? ${replyVar}.${key} + ' ' + __ev.warning : __ev.warning;`;
|
|
1561
1594
|
/**
|
|
1562
1595
|
* A visibility reaction reads params.visible and nothing else: absent means show. `value`, which the other set_*
|
|
1563
1596
|
* reactions take, was ignored, so {value: false} SHOWED the item and a hide did nothing (issue #29 O4).
|
|
@@ -1587,14 +1620,18 @@ String(JSON.stringify(sourceId)) +
|
|
|
1587
1620
|
String(JSON.stringify(targetId)) +
|
|
1588
1621
|
"].filter(function(id) { return !app._isKnownRelationEndpoint(norm(id)); });\n if (bad.length) {\n return { success: false, relationType: " +
|
|
1589
1622
|
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
|
|
1623
|
+
", 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 = " +
|
|
1624
|
+
String(JSON.stringify(params, null, 2)) +
|
|
1625
|
+
";\n " +
|
|
1626
|
+
String(eventNameResolveJs(relationType, '__params')) +
|
|
1627
|
+
"\n const success = app.addRelation(" +
|
|
1591
1628
|
String(jsLiteral(sourceId)) +
|
|
1592
1629
|
", " +
|
|
1593
1630
|
String(jsLiteral(targetId)) +
|
|
1594
1631
|
", " +
|
|
1595
1632
|
String(jsLiteral(relationType)) +
|
|
1596
|
-
", " +
|
|
1597
|
-
String(
|
|
1633
|
+
", __params);\n if (!success) {\n const __why = " +
|
|
1634
|
+
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
1635
|
}
|
|
1599
1636
|
/**
|
|
1600
1637
|
* The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
|
|
@@ -1732,6 +1769,11 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1732
1769
|
const f = o.fade;
|
|
1733
1770
|
if (f && typeof f === 'object' && !Array.isArray(f))
|
|
1734
1771
|
scalarRanges(`${where}.fade`, f, ['in', 'out']);
|
|
1772
|
+
// fade takes 'none', 'linear' (or true / false) or { in, out }. Any other word is not read, so the
|
|
1773
|
+
// particles keep the default linear fade over their life: fade 'out' looked like an option (EMITFADE34).
|
|
1774
|
+
if (typeof f === 'string' && f !== 'none' && f !== 'linear') {
|
|
1775
|
+
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).`);
|
|
1776
|
+
}
|
|
1735
1777
|
};
|
|
1736
1778
|
scalarRanges('spec', out, [...SCALAR_ONLY, ...SPEC_ONLY]);
|
|
1737
1779
|
bursts.forEach((b, i) => { if (b && typeof b === 'object')
|
|
@@ -2316,6 +2358,29 @@ function generateExecuteGeneratorCode(generatorName, params, region) {
|
|
|
2316
2358
|
* at all and draws full-canvas). So the item is checked first and refused by
|
|
2317
2359
|
* name, and the result says whether this studio clips to shapes.
|
|
2318
2360
|
*/
|
|
2361
|
+
/** Relations the studio declares ONCE over a set (multiInstance: false in js/relations/collection.js). */
|
|
2362
|
+
export const COLLECTION_RELATIONS = new Set(['phase_wave', 'ring_forming', 'flock']);
|
|
2363
|
+
/**
|
|
2364
|
+
* The part of an export region that is rendered: the region itself, or, when
|
|
2365
|
+
* outputWidth / outputHeight have another aspect, the largest box of that aspect
|
|
2366
|
+
* centred in it (a cover crop). Never larger than the region.
|
|
2367
|
+
*/
|
|
2368
|
+
export function regionCoverCrop(r) {
|
|
2369
|
+
const outW = r.outputWidth ?? Math.round(r.width);
|
|
2370
|
+
const outH = r.outputHeight ?? Math.round(r.height);
|
|
2371
|
+
const want = outW / outH, have = r.width / r.height;
|
|
2372
|
+
// Under half a source pixel of difference on either side is the same box.
|
|
2373
|
+
if (!(outW > 0 && outH > 0) || Math.abs(r.width - r.height * want) < 0.5 && Math.abs(r.height - r.width / want) < 0.5) {
|
|
2374
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height, cropped: false };
|
|
2375
|
+
}
|
|
2376
|
+
const round = (v) => Math.round(v * 1000) / 1000;
|
|
2377
|
+
if (want > have) {
|
|
2378
|
+
const h = r.width / want;
|
|
2379
|
+
return { x: r.x, y: round(r.y + (r.height - h) / 2), width: r.width, height: round(h), cropped: true };
|
|
2380
|
+
}
|
|
2381
|
+
const w = r.height * want;
|
|
2382
|
+
return { x: round(r.x + (r.width - w) / 2), y: r.y, width: round(w), height: r.height, cropped: true };
|
|
2383
|
+
}
|
|
2319
2384
|
function emitGeneratorRun(generatorName, paramsJson, region) {
|
|
2320
2385
|
const options = JSON.stringify(region ? { region } : {});
|
|
2321
2386
|
const itemId = region && typeof region.shape === 'object' ? region.shape.itemId : null;
|
|
@@ -3108,6 +3173,15 @@ function asReturningBody(snippet) {
|
|
|
3108
3173
|
* out, and the batch saves once at the end, which is what batchCreate was
|
|
3109
3174
|
* for. An item that fails is reported by index; the rest are still created.
|
|
3110
3175
|
*/
|
|
3176
|
+
/**
|
|
3177
|
+
* A create inside a batch skips the engine's per-item select, snapshot and redraw (`_bulkCreate`, the template
|
|
3178
|
+
* loader's path); the batch saves and draws once at the end. A governed run gets this from the engine already, but a
|
|
3179
|
+
* batch over the governor's size limit runs ungoverned, and there every create redrew the whole scene: 400 creates
|
|
3180
|
+
* on 2,000 items spent 2 of 5 s in full redraws.
|
|
3181
|
+
*/
|
|
3182
|
+
function asBulkCreate(code) {
|
|
3183
|
+
return code.replace(/(const item = app\.create\((?:\x22[^\x22\n]*\x22|\x27[^\x27\n]*\x27), )\{/g, '$1{ _bulkCreate: true, ');
|
|
3184
|
+
}
|
|
3111
3185
|
function generateBatchCreateCode(items) {
|
|
3112
3186
|
const blocks = items.map((it, i) => {
|
|
3113
3187
|
const { x, y, position, ...props } = (it.params || {});
|
|
@@ -3115,8 +3189,8 @@ function generateBatchCreateCode(items) {
|
|
|
3115
3189
|
: (position && typeof position === 'object') ? { x: Number(position.x), y: Number(position.y) }
|
|
3116
3190
|
: { x: typeof x === 'number' ? x : 400, y: typeof y === 'number' ? y : 300 };
|
|
3117
3191
|
const given = position !== undefined || typeof x === 'number' || typeof y === 'number';
|
|
3118
|
-
const body = asReturningBody(generateCreateItemCode(it.type, p, props, undefined, given))
|
|
3119
|
-
.replace(/app\.historyManager\.saveState\(\);/g, '');
|
|
3192
|
+
const body = asBulkCreate(asReturningBody(generateCreateItemCode(it.type, p, props, undefined, given)))
|
|
3193
|
+
.replace(/app\.historyManager(?:\.saveState\(\)|\[app\.historyManager\.saveStateDebounced \? 'saveStateDebounced' : 'saveState'\]\(\));/g, '');
|
|
3120
3194
|
return `
|
|
3121
3195
|
try {
|
|
3122
3196
|
const r${i} = await (async function() {
|
|
@@ -3133,6 +3207,8 @@ ${body}
|
|
|
3133
3207
|
const errors = [];
|
|
3134
3208
|
${blocks.join('\n')}
|
|
3135
3209
|
if (app.historyManager) app.historyManager.saveState();
|
|
3210
|
+
// Bulk creates skipped their redraws: one for the batch.
|
|
3211
|
+
try { if (typeof paper !== 'undefined' && paper.view) paper.view.draw(); } catch (_) { /* the frame loop draws */ }
|
|
3136
3212
|
return { success: errors.length === 0, items: results, count: results.length, errors: errors };
|
|
3137
3213
|
})();`.trim();
|
|
3138
3214
|
}
|
|
@@ -3613,7 +3689,7 @@ String(JSON.stringify(validated.presetId)) +
|
|
|
3613
3689
|
if (validated.targetId)
|
|
3614
3690
|
return code;
|
|
3615
3691
|
return code
|
|
3616
|
-
.replace(`
|
|
3692
|
+
.replace(`const __reply = { success: true, sourceId: ${jsLiteral(validated.sourceId)}, targetId: ${jsLiteral(validated.sourceId)},`, `const __reply = { success: true, sourceId: ${jsLiteral(validated.sourceId)}, targetId: null,`)
|
|
3617
3693
|
.replace(`relation between ${validated.sourceId} and ${validated.sourceId}.`, `relation on ${validated.sourceId} (no target).`);
|
|
3618
3694
|
}
|
|
3619
3695
|
/**
|
|
@@ -4308,15 +4384,17 @@ String(jsLiteral(source)) +
|
|
|
4308
4384
|
String(jsLiteral(target)) +
|
|
4309
4385
|
"];\n if (sourceId && targetId) {\n const params = " +
|
|
4310
4386
|
String(paramsStr) +
|
|
4311
|
-
";\n
|
|
4387
|
+
";\n " +
|
|
4388
|
+
String(eventNameResolveJs(type, 'params')) +
|
|
4389
|
+
"\n // A refused relation is not listed as made (owner rule 1).\n if (app.addRelation(sourceId, targetId, " +
|
|
4312
4390
|
String(jsLiteral(type)) +
|
|
4313
|
-
", params)) {\n results.relations.push({ source: " +
|
|
4391
|
+
", params)) {\n results.relations.push(Object.assign({ source: " +
|
|
4314
4392
|
String(jsLiteral(source)) +
|
|
4315
4393
|
", target: " +
|
|
4316
4394
|
String(jsLiteral(target)) +
|
|
4317
4395
|
", type: " +
|
|
4318
4396
|
String(jsLiteral(type)) +
|
|
4319
|
-
" });\n const __o = { _: 0" +
|
|
4397
|
+
" }, __ev.eventResolvedTo ? { eventResolvedFrom: __ev.eventResolvedFrom, eventResolvedTo: __ev.eventResolvedTo } : {}));\n if (__ev.warning) (results.optionWarnings = results.optionWarnings || []).push(__ev.warning);\n const __o = { _: 0" +
|
|
4320
4398
|
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
4399
|
}
|
|
4322
4400
|
// Apply animations
|
|
@@ -5026,7 +5104,19 @@ String(oneLine(operations.length)) +
|
|
|
5026
5104
|
"t): create and group ops take a slot each, in op\n // order (O2, mcp-server#29: a group is an item every later op addresses, and it had no slot). The\n // previous batch's ids are kept only to name the right id when a $N reaches\n // past what this batch has made: picking it silently shifted every $N in a\n // batch that also creates (B115 follow-up).\n const itemIds = [];\n const __prevBatchIds = ((typeof window !== 'undefined' && window.__pinepaper_batch_item_ids) || []).slice();\n const __ref = function (n) {\n if (itemIds[n] !== undefined) return itemIds[n];\n const prev = __prevBatchIds[n];\n throw new Error('$' + n + ' is item number ' + n + ' THIS batch made (create " +
|
|
5027
5105
|
"and group ops are numbered, in op order, from $0), and it has made ' + itemIds.length + ' so far.'\n + (prev ? ' In the previous batch $' + n + ' was ' + prev + ' — pass that id instead.' : ' Put the ops that make it first, or pass the item id.'));\n };\n // A failure names the item, not the \"$1\" that addressed it; the ref as written when the batch never made it.\n const __refName = function (n) { return itemIds[n] !== undefined ? itemIds[n] : '$' + n; };\n const results = [];\n let success = true;\n // A failure's words, never \"undefined\" for a thrown non-Error.\n const _why = (e, fb) => (e && typeof e.message === 'string' && e.message.trim()) ? e.message\n : (typeof e " +
|
|
5028
5106
|
"=== 'string' && e.trim()) ? e : fb;\n // THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything\n // failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the\n // result. A studio without lintScene says nothing.\n const __ppLintNow = function () {\n try {\n if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];\n const r = app.lintScene({ warningsOnly: true });\n return r && Array.isArray(r.warnings) ? r.warnings : [];\n } catch (e) {\n return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could no" +
|
|
5029
|
-
"t run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];\n }\n };\n // Every failed op as { step, itemId, reason }, beside the per-op results.\n const _failures = () => results.filter((r) => !r.success).map((r) => ({ step: r.step, itemId: r.itemId, reason: r.reason }));\n\n
|
|
5107
|
+
"t run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];\n }\n };\n // Every failed op as { step, itemId, reason }, beside the per-op results.\n const _failures = () => results.filter((r) => !r.success).map((r) => ({ step: r.step, itemId: r.itemId, reason: r.reason }));\n // ONE UNDO SNAPSHOT FOR THE BATCH, NOT ONE PER OP. Each create saved a full-project snapshot\n // (exportJSON), so a batch on a big scene cost O(items) per op: 200 creates on 2,000 items took\n // 14 s against 0.76 s for the bare creates. History batch mode skips the per-op saves; the batch\n // saves once at the end, as it always did.\n const __hm = app.historyManager && app.his" +
|
|
5108
|
+
"toryManager.beginBatch ? app.historyManager : null;\n if (__hm) __hm.beginBatch();\n let __hmOpen = !!__hm;\n const __hmEnd = () => {\n if (__hmOpen) { __hmOpen = false; __hm.endBatch(false); }\n // Bulk creates skipped their redraws: one for the batch.\n try { if (typeof paper !== 'undefined' && paper.view) paper.view.draw(); } catch (_) { /* the frame loop draws */ }\n };\n\n try {\n"
|
|
5109
|
+
|
|
5110
|
+
|
|
5111
|
+
|
|
5112
|
+
|
|
5113
|
+
|
|
5114
|
+
|
|
5115
|
+
|
|
5116
|
+
|
|
5117
|
+
|
|
5118
|
+
|
|
5119
|
+
|
|
5030
5120
|
|
|
5031
5121
|
|
|
5032
5122
|
|
|
@@ -5127,10 +5217,10 @@ String(op.type) +
|
|
|
5127
5217
|
") threw with no message');\n results.push({ index: " +
|
|
5128
5218
|
String(index) + ", success: false, step: " + String(JSON.stringify(opStep)) + ", itemId: (opError && typeof opError.itemId === 'string') ? opError.itemId : " + String(opItemJs) + ", reason: __r" + String(index) + ", error: __r" + String(index) + " });\n " + String(isAtomic ? 'throw opError;' : 'success = false;') + "\n }\n");
|
|
5129
5219
|
});
|
|
5130
|
-
code += ("\n // Save state\n if (app.historyManager) app.historyManager.saveState();\n\n const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };\n // Name WHICH ops failed on the result itself. A caller reading a failed\n // 60-op batch should not have to scan sixty entries to learn that op 41\n // could not find a preset.\n const _failed = results.filter((r) => !r.success);\n // A batch that created nothing keeps the previous batch's ids on record.\n if (typeof window !== 'undefined' && itemIds.length) {\n window.__pinepaper_batch_item_ids = itemIds;\n }\n return { success, itemIds, results, operationCount: " +
|
|
5220
|
+
code += ("\n // Save state\n __hmEnd();\n if (app.historyManager) app.historyManager.saveState();\n\n const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };\n // Name WHICH ops failed on the result itself. A caller reading a failed\n // 60-op batch should not have to scan sixty entries to learn that op 41\n // could not find a preset.\n const _failed = results.filter((r) => !r.success);\n // A batch that created nothing keeps the previous batch's ids on record.\n if (typeof window !== 'undefined' && itemIds.length) {\n window.__pinepaper_batch_item_ids = itemIds;\n }\n return { success, itemIds, results, operationCount: " +
|
|
5131
5221
|
String(operations.length) +
|
|
5132
|
-
",\n failedCount: _failed.length,\n ...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; '), failures: _failures() } : {}),\n canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };\n } catch (e) {\n // A batch that created nothing keeps the previous batch's ids on record.\n if (typeof window !== 'undefined' && itemIds.length) {\n window.__pinepaper_batch_item_ids = itemIds;\n }\n const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };\n const __fails = _failures();\n
|
|
5133
|
-
"_why(e, 'the batch failed with no message');\n return { success: false, error: __reason, itemIds, results, operationCount: " +
|
|
5222
|
+
",\n failedCount: _failed.length,\n ...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; '), failures: _failures() } : {}),\n canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };\n } catch (e) {\n __hmEnd();\n // A batch that created nothing keeps the previous batch's ids on record.\n if (typeof window !== 'undefined' && itemIds.length) {\n window.__pinepaper_batch_item_ids = itemIds;\n }\n const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };\n const __fails = _failures();\n c" +
|
|
5223
|
+
"onst __reason = _why(e, 'the batch failed with no message');\n return { success: false, error: __reason, itemIds, results, operationCount: " +
|
|
5134
5224
|
String(operations.length) +
|
|
5135
5225
|
",\n failures: __fails.length ? __fails : [{ step: 'batch', itemId: null, reason: __reason }],\n canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };\n }\n})();\n"
|
|
5136
5226
|
|
|
@@ -5154,6 +5244,8 @@ String(operations.length) +
|
|
|
5154
5244
|
|
|
5155
5245
|
|
|
5156
5246
|
|
|
5247
|
+
|
|
5248
|
+
|
|
5157
5249
|
|
|
5158
5250
|
|
|
5159
5251
|
|
|
@@ -5208,7 +5300,7 @@ String(operations.length) +
|
|
|
5208
5300
|
const createProps = withRadiusAxes(opProps);
|
|
5209
5301
|
const opBrand = { ...opProps };
|
|
5210
5302
|
delete opProps.noBrand;
|
|
5211
|
-
const returning = asReturningBody(withBrandDefaults(generateCreateItemCode(op.itemType, pos, { ...opProps }, undefined, op.position !== undefined || typeof anyOp.x === 'number' || typeof anyOp.y === 'number'), op.itemType, opBrand));
|
|
5303
|
+
const returning = asBulkCreate(asReturningBody(withBrandDefaults(generateCreateItemCode(op.itemType, pos, { ...opProps }, undefined, op.position !== undefined || typeof anyOp.x === 'number' || typeof anyOp.y === 'number'), op.itemType, opBrand)));
|
|
5212
5304
|
let createCode = `${hasItemSetters(opSetters) ? `\n${itemSettersPreflightJs(opSetters)}` : ''}
|
|
5213
5305
|
const __created = await (async function() {
|
|
5214
5306
|
${returning}
|
|
@@ -5319,6 +5411,45 @@ const __members = app.itemRegistry.getAll()
|
|
|
5319
5411
|
if (!__members.length) throw new Error('no item is named like ' + ${jsLiteral(op.sourceId)} + ': name items with properties.name');`
|
|
5320
5412
|
: `const __members = [{ id: ${srcRef} }];
|
|
5321
5413
|
if (!app.getItemById(__members[0].id)) throw new Error('Source not found: ' + __members[0].id);`;
|
|
5414
|
+
// A COLLECTION RULE IS DECLARED ONCE OVER THE SET (PHASEWAVE32, RINGFORM33). phase_wave, ring_forming
|
|
5415
|
+
// and flock take their members from params.members (or a group's children) and space them by rank in
|
|
5416
|
+
// that list; one relation per item gave each a set of one, so every disc took slot 0 (all at phase 0,
|
|
5417
|
+
// all on the angle-0 point of the ring). Bound once, on the first match, with the matches as members.
|
|
5418
|
+
const rel = String(op.relationType);
|
|
5419
|
+
const relParams = (op.relationOptions ?? {});
|
|
5420
|
+
if (glob && op.update !== true && COLLECTION_RELATIONS.has(rel)) {
|
|
5421
|
+
// ring_forming centres on the source's own bounds by default, and the source is a member that
|
|
5422
|
+
// moves: the centre is fixed at the members' centre as bound, unless a target or center was given.
|
|
5423
|
+
const ringCentre = rel === 'ring_forming' && noTarget && relParams.center === undefined;
|
|
5424
|
+
return ("\n" +
|
|
5425
|
+
String(members) +
|
|
5426
|
+
"\nconst targetId = " +
|
|
5427
|
+
String(noTarget ? 'null' : tgtRef) +
|
|
5428
|
+
";\nconst target = " +
|
|
5429
|
+
String(noTarget ? 'null' : 'app.getItemById(targetId)') +
|
|
5430
|
+
";\n" +
|
|
5431
|
+
String(noTarget ? '' : "if (!target) throw new Error('Target not found: ' + targetId);\n") +
|
|
5432
|
+
"const __ids = __members.map((m) => m.id);\nconst __p = Object.assign({}, " +
|
|
5433
|
+
String(relOpts) +
|
|
5434
|
+
String(relParams.members === undefined ? ', { members: { ids: __ids } }' : '') +
|
|
5435
|
+
");\n" +
|
|
5436
|
+
String(ringCentre ? `{
|
|
5437
|
+
let __b = null;
|
|
5438
|
+
__ids.forEach((id) => { const it = app.getItemById(id); if (it && it.bounds) __b = __b ? __b.unite(it.bounds) : it.bounds.clone(); });
|
|
5439
|
+
if (__b) __p.center = { x: __b.center.x, y: __b.center.y };
|
|
5440
|
+
}
|
|
5441
|
+
` : '') +
|
|
5442
|
+
"const __src = app.getItemById(__ids[0]);\nconst __existing = app.relationRegistry && app.relationRegistry.getAssociations\n && app.relationRegistry.getAssociations(__ids[0], " +
|
|
5443
|
+
String(type) +
|
|
5444
|
+
").some((a) => a.target === targetId || (targetId === null && a.target == null));\nconst ok = (__existing && typeof app.modifyRelationParams === 'function')\n ? (app.modifyRelationParams(__ids[0], targetId, " +
|
|
5445
|
+
String(type) +
|
|
5446
|
+
", __p) || (targetId === null && app.modifyRelationParams(__ids[0], undefined, " +
|
|
5447
|
+
String(type) +
|
|
5448
|
+
", __p)))\n : app.addRelation(__src, target, " +
|
|
5449
|
+
String(type) +
|
|
5450
|
+
", __p);\nif (!ok) {\n const __why = " +
|
|
5451
|
+
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");
|
|
5452
|
+
}
|
|
5322
5453
|
return ("\n" +
|
|
5323
5454
|
String(stepGuard) +
|
|
5324
5455
|
String(members) +
|
|
@@ -5360,14 +5491,18 @@ String(noTarget ? 'null' : tgtRef) +
|
|
|
5360
5491
|
String(noTarget ? 'null' : 'app.getItemById(targetId)') +
|
|
5361
5492
|
";\nif (!source) throw new Error('Source not found: ' + sourceId);\n" +
|
|
5362
5493
|
String(noTarget ? '' : "if (!target) throw new Error('Target not found: ' + targetId);\n") +
|
|
5363
|
-
"const
|
|
5364
|
-
String(JSON.stringify(String(op.relationType))) +
|
|
5365
|
-
", " +
|
|
5494
|
+
"const __relOpts = " +
|
|
5366
5495
|
String(relOpts) +
|
|
5367
|
-
"
|
|
5496
|
+
";\n" +
|
|
5497
|
+
String(eventNameResolveJs(op.relationType, '__relOpts')) +
|
|
5498
|
+
"\nconst relationId = app.addRelation(source, target, " +
|
|
5499
|
+
String(JSON.stringify(String(op.relationType))) +
|
|
5500
|
+
", __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
5501
|
String(relationRefusalReasonJs(jsLiteral(op.relationType))) +
|
|
5369
5502
|
";\n return { success: false, relationId: false, sourceId, targetId, relationType: " +
|
|
5370
|
-
String(JSON.stringify(op.relationType)) +
|
|
5503
|
+
String(JSON.stringify(op.relationType)) +
|
|
5504
|
+
",\n error: 'the studio refused the ' + " +
|
|
5505
|
+
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
5506
|
case 'delete':
|
|
5372
5507
|
const delRef = batchRef(op.itemId);
|
|
5373
5508
|
return `
|
|
@@ -5598,6 +5733,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5598
5733
|
generateAgentExport(input) {
|
|
5599
5734
|
const validated = AgentExportInputSchema.parse(input);
|
|
5600
5735
|
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;
|
|
5736
|
+
const regionSrc = region ? regionCoverCrop(region) : undefined;
|
|
5601
5737
|
// Every video is deterministic unless the caller opts out (owner, 2026-11-07), so every receipt says whether its file repeats.
|
|
5602
5738
|
const deterministic = askedDeterministic ?? (format === 'mp4' || format === 'webm');
|
|
5603
5739
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
@@ -5924,34 +6060,42 @@ String(stillTime === undefined ? ` if (['png', 'jpg', 'webp', 'svg', 'pdf'].ind
|
|
|
5924
6060
|
` : '') +
|
|
5925
6061
|
" // 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
6062
|
" }\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
|
-
|
|
6063
|
+
" 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" +
|
|
6064
|
+
"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" +
|
|
6065
|
+
"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 " +
|
|
6066
|
+
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
6067
|
String(region.outputWidth ?? Math.round(region.width)) +
|
|
5931
6068
|
", rh = " +
|
|
5932
6069
|
String(region.outputHeight ?? Math.round(region.height)) +
|
|
5933
6070
|
";\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
6071
|
"ckTime : 0));\n const regionUrl = app.renderRegionToDataURL(new paper.Rectangle(" +
|
|
5935
|
-
String(
|
|
6072
|
+
String(regionSrc.x) +
|
|
5936
6073
|
", " +
|
|
5937
|
-
String(
|
|
6074
|
+
String(regionSrc.y) +
|
|
5938
6075
|
", " +
|
|
5939
|
-
String(
|
|
6076
|
+
String(regionSrc.width) +
|
|
5940
6077
|
", " +
|
|
5941
|
-
String(
|
|
6078
|
+
String(regionSrc.height) +
|
|
5942
6079
|
"), rw, rh" +
|
|
5943
6080
|
String(region.excludeForeign === false ? '' : ', { excludeForeign: true }') +
|
|
5944
6081
|
");\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(
|
|
6082
|
+
String(regionSrc.x) +
|
|
5946
6083
|
", __ry = " +
|
|
5947
|
-
String(
|
|
6084
|
+
String(regionSrc.y) +
|
|
5948
6085
|
", __rr = " +
|
|
5949
|
-
String(
|
|
6086
|
+
String(regionSrc.x + regionSrc.width) +
|
|
5950
6087
|
", __rb = " +
|
|
5951
|
-
String(
|
|
6088
|
+
String(regionSrc.y + regionSrc.height) +
|
|
5952
6089
|
";\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
6090
|
" 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
6091
|
String(JSON.stringify({ x: region.x, y: region.y, width: region.width, height: region.height })) +
|
|
6092
|
+
String(regionSrc.cropped ? `,
|
|
6093
|
+
// A DIFFERENT OUTPUT ASPECT IS A CROP (REGION34). The studio grew the box to
|
|
6094
|
+
// the output's aspect instead, drawing content from outside the region and
|
|
6095
|
+
// black past the canvas edge. The server now trims the region about its
|
|
6096
|
+
// centre to the output's aspect, so the studio is only asked to scale.
|
|
6097
|
+
croppedTo: ${JSON.stringify({ x: regionSrc.x, y: regionSrc.y, width: regionSrc.width, height: regionSrc.height })},
|
|
6098
|
+
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
6099
|
" };\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
6100
|
" 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
6101
|
|
|
@@ -5975,6 +6119,11 @@ String(JSON.stringify({ x: region.x, y: region.y, width: region.width, height: r
|
|
|
5975
6119
|
|
|
5976
6120
|
|
|
5977
6121
|
|
|
6122
|
+
|
|
6123
|
+
|
|
6124
|
+
|
|
6125
|
+
|
|
6126
|
+
|
|
5978
6127
|
|
|
5979
6128
|
|
|
5980
6129
|
|
|
@@ -6013,9 +6162,9 @@ String(encodeOptsJs ? `
|
|
|
6013
6162
|
// Broadcast / bitrate. A store that rebuilds its options without
|
|
6014
6163
|
// them is caught by the video report below, not assumed.
|
|
6015
6164
|
${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 = " +
|
|
6165
|
+
"\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" +
|
|
6166
|
+
"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 " +
|
|
6167
|
+
" 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
6168
|
String(INLINE_MAX_BYTES) +
|
|
6020
6169
|
";\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
6170
|
"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 +6183,42 @@ String(gifLoop !== undefined ? `// A LOOP COUNT (8.30): _quickExportVideo rebuil
|
|
|
6034
6183
|
"\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
6184
|
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
6185
|
String(maxBytes) +
|
|
6037
|
-
") {\n
|
|
6186
|
+
") {\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" +
|
|
6187
|
+
"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
6188
|
String(maxBytes) +
|
|
6039
6189
|
"; k++) {\n const f = Math.sqrt(" +
|
|
6040
6190
|
String(maxBytes) +
|
|
6041
|
-
" /
|
|
6191
|
+
" / 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:" +
|
|
6192
|
+
" w, height: h }" +
|
|
6042
6193
|
String(gifLoop !== undefined ? `, { loop: ${JSON.stringify(gifLoop)} }` : '') +
|
|
6043
|
-
"));\n if (!again) break;\n
|
|
6194
|
+
"));\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
6195
|
String(maxBytes) +
|
|
6045
6196
|
", met: !!(result.success && result.size <= " +
|
|
6046
6197
|
String(maxBytes) +
|
|
6047
|
-
"), attempts: attempts };\n if (result.success && result.size > " +
|
|
6198
|
+
"), 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
6199
|
String(maxBytes) +
|
|
6049
|
-
") {\n result.warning = 'still ' + result.size + ' bytes
|
|
6200
|
+
") {\n result.warning = 'still ' + result.size + ' bytes at ' + bestDims.width + 'x' + bestDims.height + ', the smallest of ' + attempts.length + ' tries — over the ' + " +
|
|
6050
6201
|
String(maxBytes) +
|
|
6051
|
-
" + '-byte budget. Lower fps, shorten duration, or use quality \"draft\".';\n }
|
|
6202
|
+
" + '-byte budget. Lower fps, shorten duration, or use quality \"draft\".';\n }\n }"
|
|
6203
|
+
|
|
6204
|
+
|
|
6205
|
+
|
|
6206
|
+
|
|
6207
|
+
|
|
6208
|
+
|
|
6209
|
+
|
|
6210
|
+
|
|
6211
|
+
|
|
6212
|
+
|
|
6213
|
+
|
|
6214
|
+
|
|
6215
|
+
|
|
6216
|
+
|
|
6217
|
+
|
|
6218
|
+
|
|
6219
|
+
|
|
6220
|
+
|
|
6221
|
+
|
|
6052
6222
|
|
|
6053
6223
|
|
|
6054
6224
|
|
|
@@ -6175,7 +6345,8 @@ String(pdfOpts.searchableText !== false) +
|
|
|
6175
6345
|
|
|
6176
6346
|
|
|
6177
6347
|
) : '') +
|
|
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
|
|
6348
|
+
"\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" +
|
|
6349
|
+
"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
6350
|
String(pdfOpts?.dpi ?? 'settings.dpi') +
|
|
6180
6351
|
",\n download: false," +
|
|
6181
6352
|
String(pdfOpts?.searchableText === false ? '\n searchableText: false,' : '') +
|
|
@@ -6184,18 +6355,20 @@ String(pdfOpts?.orientation ? `\n orientation: ${JSON.stringify(pdfOp
|
|
|
6184
6355
|
String(pdfOpts?.bleed !== undefined ? `\n includeBleed: ${pdfOpts.bleed > 0}, bleed: ${pdfOpts.bleed},` : '') +
|
|
6185
6356
|
String(pdfOpts?.trimMarks !== undefined ? `\n trimMarks: ${pdfOpts.trimMarks},` : '') +
|
|
6186
6357
|
"\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
|
-
"
|
|
6358
|
+
" // 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 } : {})," +
|
|
6359
|
+
String(pdfOpts?.paperFormat && pdfOpts.paperFormat !== 'custom' && pdfOpts.trimMarks ? `
|
|
6360
|
+
// TRIM MARKS ON A NAMED SHEET. Bleed is applied there (page = sheet + 2 x bleed, TrimBox = sheet), but
|
|
6361
|
+
// a studio before FxTool #347 draws no marks on a named sheet. Said unless the file's own record
|
|
6362
|
+
// says it drew them.
|
|
6363
|
+
...((pdfOut && pdfOut.print && pdfOut.print.trimMarksDrawn) || (__pee.lastPdfReport && __pee.lastPdfReport.trimMarksDrawn) ? {} : {
|
|
6364
|
+
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).`)}],
|
|
6365
|
+
}),` : '') +
|
|
6366
|
+
"\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" +
|
|
6367
|
+
"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() {" +
|
|
6368
|
+
"\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, " +
|
|
6369
|
+
"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 &" +
|
|
6370
|
+
"& 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" +
|
|
6371
|
+
" 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
6372
|
String(videoDuration) +
|
|
6200
6373
|
" * 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
6374
|
"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 +6519,8 @@ String((() => {
|
|
|
6346
6519
|
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
6520
|
})};\n`;
|
|
6348
6521
|
})()) +
|
|
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
|
|
6522
|
+
"\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" +
|
|
6523
|
+
" moved.\n if (result && result.success && typeof app.screenSpaceItems === 'function' && ['svg', 'lottie', 'dotlottie'].indexOf(" +
|
|
6350
6524
|
String(JSON.stringify(format)) +
|
|
6351
6525
|
") !== -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
6526
|
String(format) +
|
|
@@ -7396,6 +7570,57 @@ String(jsLiteral(format)) +
|
|
|
7396
7570
|
|
|
7397
7571
|
|
|
7398
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
|
+
|
|
7598
|
+
|
|
7599
|
+
|
|
7600
|
+
|
|
7601
|
+
|
|
7602
|
+
|
|
7603
|
+
|
|
7604
|
+
|
|
7605
|
+
|
|
7606
|
+
|
|
7607
|
+
|
|
7608
|
+
|
|
7609
|
+
|
|
7610
|
+
|
|
7611
|
+
|
|
7612
|
+
|
|
7613
|
+
|
|
7614
|
+
|
|
7615
|
+
|
|
7616
|
+
|
|
7617
|
+
|
|
7618
|
+
|
|
7619
|
+
|
|
7620
|
+
|
|
7621
|
+
|
|
7622
|
+
|
|
7623
|
+
|
|
7399
7624
|
|
|
7400
7625
|
|
|
7401
7626
|
|
|
@@ -15162,6 +15387,66 @@ String(S(report)) +
|
|
|
15162
15387
|
* reassembled into it. Naming them individually is the point: the kit's
|
|
15163
15388
|
* options were discoverable only by reading FxTool's source.
|
|
15164
15389
|
*/
|
|
15390
|
+
/**
|
|
15391
|
+
* pinepaper_instances — a swarm of up to a million marks as one item. Every field passes through to the
|
|
15392
|
+
* engine, which validates it and names a bad one; the reply carries describeInstances, so a subsampled or
|
|
15393
|
+
* capped swarm says so (drawn vs count) in the call that made it.
|
|
15394
|
+
*/
|
|
15395
|
+
generateInstances(input) {
|
|
15396
|
+
const S = (v) => JSON.stringify(v);
|
|
15397
|
+
const { action, itemId, name, ...rest } = input;
|
|
15398
|
+
const spec = {};
|
|
15399
|
+
for (const [k, v] of Object.entries(rest))
|
|
15400
|
+
if (v !== undefined)
|
|
15401
|
+
spec[k] = v;
|
|
15402
|
+
const refuse = (m) => `if (typeof app.${m} !== 'function') { return { success: false, error: 'this studio has no instances (app.${m}): update PinePaper Studio' }; }`;
|
|
15403
|
+
const why = `(app.lastReceipt && Array.isArray(app.lastReceipt.warnings) && app.lastReceipt.warnings.map(function (w) { return w && (w.message || w.reason); }).filter(Boolean).join(' ')) || ''`;
|
|
15404
|
+
switch (action) {
|
|
15405
|
+
case 'rules':
|
|
15406
|
+
return `
|
|
15407
|
+
// Instances: the rules, layouts and shapes this studio has
|
|
15408
|
+
(function() {
|
|
15409
|
+
// @engine-methods instanceRules
|
|
15410
|
+
${refuse('instanceRules')}
|
|
15411
|
+
return Object.assign({ success: true }, app.instanceRules());
|
|
15412
|
+
})();`.trim();
|
|
15413
|
+
case 'describe':
|
|
15414
|
+
return `
|
|
15415
|
+
// Instances: describe ${oneLine(itemId ?? '')}
|
|
15416
|
+
(function() {
|
|
15417
|
+
// @engine-methods describeInstances
|
|
15418
|
+
${refuse('describeInstances')}
|
|
15419
|
+
const d = app.describeInstances(${S(itemId)});
|
|
15420
|
+
if (!d || d.ok === false) { return { success: false, error: (d && (d.error || d.reason)) || 'no swarm has the id ' + ${S(itemId)} }; }
|
|
15421
|
+
return Object.assign({ success: true, itemId: ${S(itemId)} }, d);
|
|
15422
|
+
})();`.trim();
|
|
15423
|
+
case 'modify':
|
|
15424
|
+
return `
|
|
15425
|
+
// Instances: modify ${oneLine(itemId ?? '')}
|
|
15426
|
+
(function() {
|
|
15427
|
+
// @engine-methods setInstances describeInstances
|
|
15428
|
+
${refuse('setInstances')}
|
|
15429
|
+
const r = app.setInstances(${S(itemId)}, ${S(spec)});
|
|
15430
|
+
if (!r || r.ok === false) { return { success: false, error: (r && (r.error || r.reason)) || ${why} || 'the swarm was not changed' }; }
|
|
15431
|
+
const d = app.describeInstances(${S(itemId)}) || {};
|
|
15432
|
+
return { success: true, itemId: ${S(itemId)}, spec: r.spec, count: d.count, drawn: d.drawn, subsampled: d.subsampled, path: d.path, determinism: d.determinism };
|
|
15433
|
+
})();`.trim();
|
|
15434
|
+
case 'create':
|
|
15435
|
+
default:
|
|
15436
|
+
return `
|
|
15437
|
+
// Instances: create a swarm${spec.count ? ` of ${oneLine(String(spec.count))}` : ''}
|
|
15438
|
+
(function() {
|
|
15439
|
+
// @engine-methods describeInstances
|
|
15440
|
+
${refuse('describeInstances')}
|
|
15441
|
+
const item = app.create('instances', ${S(spec)});
|
|
15442
|
+
if (!item) { return { success: false, error: ${why} || 'the studio made no swarm from that spec' }; }
|
|
15443
|
+
const itemId = item.data && (item.data.registryId || item.data.id);${name ? `
|
|
15444
|
+
try { item.name = ${S(name)}; } catch (_) {}` : ''}
|
|
15445
|
+
const d = app.describeInstances(itemId) || {};
|
|
15446
|
+
return { success: true, itemId: itemId, count: d.count, drawn: d.drawn, subsampled: d.subsampled, path: d.path, rule: d.rule, determinism: d.determinism };
|
|
15447
|
+
})();`.trim();
|
|
15448
|
+
}
|
|
15449
|
+
}
|
|
15165
15450
|
generateStick(input) {
|
|
15166
15451
|
const S = (v) => JSON.stringify(v);
|
|
15167
15452
|
const { action, ...rest } = input;
|
|
@@ -15211,7 +15496,7 @@ String(S(report)) +
|
|
|
15211
15496
|
if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool (the stick kit is vendored from mcp-cloud)' }; }
|
|
15212
15497
|
const r = await app.${fn}(${S(opts)});
|
|
15213
15498
|
if (!r || r.ok === false) { return { success: false, error: (r && (r.reason || r.error)) || 'the stick kit produced nothing' }; }
|
|
15214
|
-
return { success: true, ...r };
|
|
15499
|
+
return { success: true, ...r${action === 'figure' ? ", anchor: app.stickFigureAnchorsFeet === true ? 'feet' : 'hip'" : ''} };
|
|
15215
15500
|
})();`.trim();
|
|
15216
15501
|
}
|
|
15217
15502
|
/**
|
|
@@ -15566,7 +15851,8 @@ String(S(opts)) +
|
|
|
15566
15851
|
).trim();
|
|
15567
15852
|
}
|
|
15568
15853
|
case 'create': {
|
|
15569
|
-
|
|
15854
|
+
// visual: false means no drawing options. The chip is editor-only, and no export draws it.
|
|
15855
|
+
const v = (input.visual || {});
|
|
15570
15856
|
const { startTime, duration, ...visualOnly } = v;
|
|
15571
15857
|
const placement = (startTime !== undefined || duration !== undefined)
|
|
15572
15858
|
? { ...(startTime !== undefined ? { startTime } : {}), ...(duration !== undefined ? { duration } : {}) }
|
|
@@ -16348,11 +16634,28 @@ function generateContraptionCode(input) {
|
|
|
16348
16634
|
...(input.elseTrigger ? { elseTrigger: input.elseTrigger } : {}),
|
|
16349
16635
|
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
16350
16636
|
};
|
|
16351
|
-
return
|
|
16352
|
-
|
|
16353
|
-
const
|
|
16354
|
-
|
|
16355
|
-
|
|
16637
|
+
return ("" +
|
|
16638
|
+
String(refuse('addContraption')) +
|
|
16639
|
+
"\n const contraptionId = app.addContraption(" +
|
|
16640
|
+
String(jsLiteral(config)) +
|
|
16641
|
+
");\n const missing = " +
|
|
16642
|
+
String(jsLiteral(targets)) +
|
|
16643
|
+
".filter((id) => !app.getItemById(id));\n // AN EVENT'S NAME IS NOT ITS CHANNEL (DBLEXP33). A timed event and a pulse by eventId both fire the id create\n // returned; a rule on the event's name ran only when that literal name was pulsed, so it stayed at fireCount 0\n // with no word said. Named here, with the id to use.\n const __src = " +
|
|
16644
|
+
String(jsLiteral(input.sourceEvent ?? '')) +
|
|
16645
|
+
";\n const __named = (app.itemRegistry && app.itemRegistry.getAll ? app.itemRegistry.getAll() : [])\n .filter((e) => e && e.item && e.item.data && e.item.data.isEvent && e.item.data.eventName === __src && (e.id || e.itemId) !== __src)\n .map((e) => e.id || e.itemId);\n // A studio that resolves a name held by one event to its id (FxTool #346) stores the id: read the rule back\n // rather than trust a flag, so the warning is true on every build.\n const __stored = typeof app.listContraptions === 'function'\n ? (app.listContraptions() || []).find((c) => c && c.id === contraptionId) : null;\n const __resolved = !!(__stored && __named.indexOf(__stored.sourceEvent) >= 0);\n const " +
|
|
16646
|
+
"__warnings = [];\n if (missing.length) __warnings.push('no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does');\n if (__named.length > 1 && !__resolved) __warnings.push(__named.length + ' events are named ' + JSON.stringify(__src) + ' (' + __named.join(', ') + '), so the rule cannot tell which one: it runs only when the literal name is pulsed. Pass one eventId as sourceEvent.');\n else if (__named.length && !__resolved) __warnings.push('sourceEvent ' + JSON.stringify(__src) + ' is the NAME of event ' + __named[0] + ': its timer and a pulse of its id fire ' + __named[0] + ', not the name, so this rule runs only when the literal name ' + JSON.strin" +
|
|
16647
|
+
"gify(__src) + ' is pulsed. Use sourceEvent ' + JSON.stringify(__named[0]) + ' to run it when that event fires.');\n return { success: true, action: 'add_contraption', contraptionId,\n ...(__resolved ? { sourceEventResolvedTo: __stored.sourceEvent } : __named.length === 1 ? { eventIdForName: __named[0] } : {}),\n ...(__warnings.length ? { warning: __warnings.join(' ') } : {}), };"
|
|
16648
|
+
|
|
16649
|
+
|
|
16650
|
+
|
|
16651
|
+
|
|
16652
|
+
|
|
16653
|
+
|
|
16654
|
+
|
|
16655
|
+
|
|
16656
|
+
|
|
16657
|
+
|
|
16658
|
+
);
|
|
16356
16659
|
}
|
|
16357
16660
|
case 'remove_contraption':
|
|
16358
16661
|
return `${refuse('removeContraption')}
|