@pinepaper.studio/mcp-server 1.6.31 → 1.6.32
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 +79 -1
- package/SECURITY.md +1 -1
- package/dist/agent/types.d.ts +10 -10
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +42 -17
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +12 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +32 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.d.ts +4 -0
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +22 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +18 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/batch-op-names.d.ts +6 -0
- package/dist/tools/batch-op-names.d.ts.map +1 -1
- package/dist/tools/batch-op-names.js +40 -2
- package/dist/tools/batch-op-names.js.map +1 -1
- package/dist/tools/choreograph.d.ts +8 -0
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +9 -7
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +108 -45
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/effect-class.d.ts.map +1 -1
- package/dist/tools/effect-class.js +7 -0
- package/dist/tools/effect-class.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +5 -5
- package/dist/tools/engine-lists.js +5 -5
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +3 -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 +20 -4
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +2 -2
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +38 -7
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +1 -1
- package/dist/tools/lane.d.ts.map +1 -1
- package/dist/tools/lane.js +1 -1
- 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.d.ts.map +1 -1
- package/dist/tools/shape-params.js +8 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +4 -4
- package/dist/tools/stick-vocabulary.d.ts.map +1 -1
- package/dist/tools/stick-vocabulary.js +15 -4
- package/dist/tools/stick-vocabulary.js.map +1 -1
- package/dist/types/code-generator.d.ts +8 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +241 -45
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +246 -105
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +69 -5
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/env.d.ts +13 -41
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/env.js +25 -43
- package/dist/utils/env.js.map +1 -1
- package/dist/utils/exit-when-client-gone.d.ts +10 -0
- package/dist/utils/exit-when-client-gone.d.ts.map +1 -0
- package/dist/utils/exit-when-client-gone.js +21 -0
- package/dist/utils/exit-when-client-gone.js.map +1 -0
- package/dist/vendor/design/design-style.d.ts +2 -2
- package/dist/vendor/design/design-style.d.ts.map +1 -1
- package/dist/vendor/design/design-style.js +5 -2
- package/dist/vendor/design/design-style.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -16,6 +16,16 @@ import { jsLiteral } from '../utils/js-literal.js';
|
|
|
16
16
|
/** A value spliced into a generated `//` comment, on one line: a line break in caller text would end the comment and run the rest as code. */
|
|
17
17
|
/** How many relations pinepaper_lint_scene lists back; past it the result says how many there are. */
|
|
18
18
|
const RELATIONS_READBACK_CAP = 200;
|
|
19
|
+
/**
|
|
20
|
+
* A relation's parameters as a caller may read them: the engine's own rule for what leaves a relation
|
|
21
|
+
* (RelationRegistry.stripTransientParams, its save boundary), applied in the page because that is where the value is.
|
|
22
|
+
*
|
|
23
|
+
* The engine keeps its working state in the stored params: _itemRegistry (written on the first frame), _initialized,
|
|
24
|
+
* _startTime, _affectedItems, listeners as functions. Read back raw, a reply carried the registry, so it failed the page
|
|
25
|
+
* bridge's structured clone and took the walker's path, where the whole reply was truncated to fit. Sent back in an update
|
|
26
|
+
* op, the same keys overwrite live state: the engine fills those slots only when empty, so a stale copy stays for good.
|
|
27
|
+
*/
|
|
28
|
+
const RELATION_PUBLIC_PARAMS_JS = "((p) => Object.fromEntries(Object.entries(p || {}).filter(([k, v]) => k.charAt(0) !== '_' && typeof v !== 'function')))";
|
|
19
29
|
function oneLine(v) {
|
|
20
30
|
return String(v).replace(/[\r\n\u2028\u2029]+/g, ' ');
|
|
21
31
|
}
|
|
@@ -182,12 +192,13 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
182
192
|
if (properties.content === undefined && properties.text !== undefined) {
|
|
183
193
|
properties = { ...properties, content: properties.text };
|
|
184
194
|
}
|
|
195
|
+
// The alias replaces the key: left beside justification, the engine's receipt
|
|
196
|
+
// reported textAlign 'justify' as refused while it had been applied.
|
|
185
197
|
if (properties.justification === undefined) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
properties = { ...properties, justification: properties.align };
|
|
198
|
+
const alignKey = properties.textAlign !== undefined ? 'textAlign' : properties.align !== undefined ? 'align' : null;
|
|
199
|
+
if (alignKey) {
|
|
200
|
+
const { [alignKey]: alignValue, ...rest } = properties;
|
|
201
|
+
properties = { ...rest, justification: cssAlignToJustification(alignValue) };
|
|
191
202
|
}
|
|
192
203
|
}
|
|
193
204
|
// Extract special properties that need separate handling
|
|
@@ -218,6 +229,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
218
229
|
textDirection.resolved = baseDirection(createProps.content);
|
|
219
230
|
}
|
|
220
231
|
const textTabular = itemType === 'text' && wantsTabular(createProps) === true;
|
|
232
|
+
const textTypography = itemType === 'text' ? typographyRequest(createProps) : [];
|
|
221
233
|
const createFillOpacity = createProps.fillOpacity !== undefined;
|
|
222
234
|
const textStrokePos = itemType === 'text' ? takeStrokePosition(createProps, false) : null;
|
|
223
235
|
const params = {
|
|
@@ -422,6 +434,8 @@ const __scale = (function(it) {
|
|
|
422
434
|
code += emitDirectionCheck('item', textDirection);
|
|
423
435
|
if (textTabular)
|
|
424
436
|
code += emitTabularCheck('item');
|
|
437
|
+
if (textTypography.length)
|
|
438
|
+
code += emitTypographyCheck('item', textTypography);
|
|
425
439
|
if (createFillOpacity)
|
|
426
440
|
code += emitFillOpacityCheck('item');
|
|
427
441
|
if (textStrokePos)
|
|
@@ -476,7 +490,7 @@ String(outlineOnly ? `, fill: ${JSON.stringify(OUTLINE_NOTE)}` : '') +
|
|
|
476
490
|
String(textFit ? ', textFit: __textFit' : '') +
|
|
477
491
|
String(textDirection ? ', direction: __direction' : '') +
|
|
478
492
|
String(textTabular ? ', tabularFigures: __tabular' : '') +
|
|
479
|
-
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
|
|
493
|
+
String(textTypography.length ? ', typography: __typography' : '') + 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
|
|
480
494
|
? `, ignoredProperties: ${JSON.stringify(ignored)}` : '') + String(warnings.length > 0 ? `, warning: ${JSON.stringify(warnings.join(' '))}` : '') + " });");
|
|
481
495
|
// Loading a font is async, and it has to happen BEFORE create() measures the
|
|
482
496
|
// text — so only this case becomes an async IIFE; every other create stays a
|
|
@@ -743,6 +757,50 @@ function anchorHold(anchor) {
|
|
|
743
757
|
const a = String(anchor ?? '').toLowerCase();
|
|
744
758
|
return /^top/.test(a) ? 'top' : /^bottom/.test(a) ? 'bottom' : undefined;
|
|
745
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* wordSpacing, textTransform, textDecoration and justify (FxTool #289, WORDSPC).
|
|
762
|
+
* The engine stores them on item.data and its text layer draws them; a studio
|
|
763
|
+
* before #289 has no such layer and the keys are lost. These keys are on the
|
|
764
|
+
* current engine's read list (shape-params.ts), so the unread-key report no
|
|
765
|
+
* longer names them; the reply instead reads item.data, so an older studio is
|
|
766
|
+
* still named rather than reported as done.
|
|
767
|
+
*/
|
|
768
|
+
/**
|
|
769
|
+
* textAlign / align, the CSS names, as Paper's justification: start / end read
|
|
770
|
+
* left / right and centre reads center, as the engine's own textAlign door does.
|
|
771
|
+
* Anything else passes through for the engine to judge (and report).
|
|
772
|
+
*/
|
|
773
|
+
function cssAlignToJustification(v) {
|
|
774
|
+
if (typeof v !== 'string')
|
|
775
|
+
return v;
|
|
776
|
+
const k = v.trim().toLowerCase();
|
|
777
|
+
return { start: 'left', end: 'right', centre: 'center', left: 'left', center: 'center', right: 'right', justify: 'justify' }[k] ?? v;
|
|
778
|
+
}
|
|
779
|
+
const TYPOGRAPHY_KEYS = ['wordSpacing', 'textTransform', 'textDecoration'];
|
|
780
|
+
function typographyRequest(props) {
|
|
781
|
+
const keys = TYPOGRAPHY_KEYS.filter((k) => props[k] !== undefined);
|
|
782
|
+
const align = props.justification ?? props.textAlign ?? props.align;
|
|
783
|
+
if (typeof align === 'string' && align.trim().toLowerCase() === 'justify')
|
|
784
|
+
keys.push('justify');
|
|
785
|
+
return keys;
|
|
786
|
+
}
|
|
787
|
+
function emitTypographyCheck(itemExpr, keys) {
|
|
788
|
+
return ("\nconst __typography = (function(it, keys) {\n if (!it || it.className !== 'PointText') return { applied: false, note: keys.join(', ') + ' apply to text items only.' };\n const d = it.data || {};\n if (typeof app._applyTextTypography !== 'function') return { applied: false, ignored: keys, note: 'this studio does not draw ' + keys.join(', ') + ' (it needs an engine with text typography): the text is drawn without ' + (keys.length > 1 ? 'them' : 'it') + '. Set the case in the content itself; for an underline, a line item under the text.' };\n const out = { applied: true };\n const unset = [];\n for (const k of keys) {\n const v = k === 'justify' ? (d.justify === true ? true : undefi" +
|
|
789
|
+
"ned) : d[k];\n out[k] = v === undefined ? null : v;\n if (v === undefined) unset.push(k);\n }\n if (unset.length) out.note = unset.join(', ') + (unset.length > 1 ? ' are' : ' is') + ' not set on the item now: a clearing value (none / normal / 0) removes it, and a value the studio cannot draw is refused by name in the engine receipt.';\n return out;\n})(" +
|
|
790
|
+
String(itemExpr) +
|
|
791
|
+
", " +
|
|
792
|
+
String(JSON.stringify(keys)) +
|
|
793
|
+
");"
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
);
|
|
803
|
+
}
|
|
746
804
|
function emitTextFit(itemExpr, box, holdFallback = 'undefined') {
|
|
747
805
|
return `
|
|
748
806
|
if (typeof app.fitText === 'function') {
|
|
@@ -978,12 +1036,12 @@ function generateModifyItemCode(itemId, properties, dataFlags) {
|
|
|
978
1036
|
const aliased = new Set();
|
|
979
1037
|
if (restProperties.justification === undefined) {
|
|
980
1038
|
if (restProperties.textAlign !== undefined) {
|
|
981
|
-
restProperties.justification = restProperties.textAlign;
|
|
1039
|
+
restProperties.justification = cssAlignToJustification(restProperties.textAlign);
|
|
982
1040
|
delete restProperties.textAlign;
|
|
983
1041
|
aliased.add('textAlign');
|
|
984
1042
|
}
|
|
985
1043
|
else if (restProperties.align !== undefined) {
|
|
986
|
-
restProperties.justification = restProperties.align;
|
|
1044
|
+
restProperties.justification = cssAlignToJustification(restProperties.align);
|
|
987
1045
|
delete restProperties.align;
|
|
988
1046
|
aliased.add('align');
|
|
989
1047
|
}
|
|
@@ -1001,6 +1059,7 @@ function generateModifyItemCode(itemId, properties, dataFlags) {
|
|
|
1001
1059
|
delete restProperties.fit;
|
|
1002
1060
|
const modDirection = takeDirection(restProperties);
|
|
1003
1061
|
const modTabular = wantsTabular(restProperties) === true;
|
|
1062
|
+
const modTypography = typographyRequest(restProperties);
|
|
1004
1063
|
const modFillOpacity = restProperties.fillOpacity !== undefined;
|
|
1005
1064
|
const modStrokePos = takeStrokePosition(restProperties, true);
|
|
1006
1065
|
// modifyItem(id, changes), NOT select() + modify().
|
|
@@ -1145,12 +1204,12 @@ const __ctKeys = (__ctItem && __ctItem.data && Array.isArray(__ctItem.data.keyfr
|
|
|
1145
1204
|
"\n" +
|
|
1146
1205
|
String(modDirection ? emitDirectionCheck(`app.getItemById(${ID})`, modDirection) : '') +
|
|
1147
1206
|
String(modTabular ? emitTabularCheck(`app.getItemById(${ID})`) : '') +
|
|
1207
|
+
String(modTypography.length ? emitTypographyCheck(`app.getItemById(${ID})`, modTypography) : '') +
|
|
1148
1208
|
String(modFillOpacity ? emitFillOpacityCheck(`app.getItemById(${ID})`) : '') +
|
|
1149
1209
|
String(modStrokePos ? emitStrokePositionCheck(`app.getItemById(${ID})`, modStrokePos) : '') +
|
|
1150
1210
|
"\nreturn { success: true, itemId: " +
|
|
1151
1211
|
String(ID) +
|
|
1152
|
-
String(properties.content !== undefined ? `, ...(__ctKeys ? { contentTrackWarning: 'this text\\'s content is keyframed (' + __ctKeys + ' key(s)), so the track sets it on every frame and this edit does not show. Change the content keys with pinepaper_keyframe_animate (append: true replaces same-time keys), or animate another property.' } : {})` : '') +
|
|
1153
|
-
String(modFit ? ', textFit: __textFit' : '') + String(modDirection ? ', direction: __direction' : '') + String(modTabular ? ', tabularFigures: __tabular' : '') + String(modFillOpacity ? ', fillOpacity: __fillOpacity' : '') + String(modStrokePos ? ', strokePosition: __strokePos' : '') + String(affine ? `, affine: ${jsLiteral({ skewX, skewY, matrix, note: 'applied to the current shape: calling again compounds it. To undo, apply the inverse (negative skew, or the inverse matrix).' })}` : '') + String(modLifetime ? ', lifetime: __lifetime' : '') + String(modFont ? ', ...(__font ? { font: __font } : {})' : '') + String(modWarning) + " };");
|
|
1212
|
+
String(properties.content !== undefined ? `, ...(__ctKeys ? { contentTrackWarning: 'this text\\'s content is keyframed (' + __ctKeys + ' key(s)), so the track sets it on every frame and this edit does not show. Change the content keys with pinepaper_keyframe_animate (append: true replaces same-time keys), or animate another property.' } : {})` : '') + String(modFit ? ', textFit: __textFit' : '') + String(modDirection ? ', direction: __direction' : '') + String(modTabular ? ', tabularFigures: __tabular' : '') + String(modTypography.length ? ', typography: __typography' : '') + String(modFillOpacity ? ', fillOpacity: __fillOpacity' : '') + String(modStrokePos ? ', strokePosition: __strokePos' : '') + String(affine ? `, affine: ${jsLiteral({ skewX, skewY, matrix, note: 'applied to the current shape: calling again compounds it. To undo, apply the inverse (negative skew, or the inverse matrix).' })}` : '') + String(modLifetime ? ', lifetime: __lifetime' : '') + String(modFont ? ', ...(__font ? { font: __font } : {})' : '') + String(modWarning) + " };");
|
|
1154
1213
|
// Wrapped, because the body now RETURNS early on a miss rather than falling
|
|
1155
1214
|
// through to an unconditional success. Async only when a font must load
|
|
1156
1215
|
// before modifyItem re-measures the text.
|
|
@@ -1391,6 +1450,45 @@ function editDistance(a, b) {
|
|
|
1391
1450
|
}
|
|
1392
1451
|
return row[b.length];
|
|
1393
1452
|
}
|
|
1453
|
+
/**
|
|
1454
|
+
* The params keys a custom relation reads, worked out when it is registered: what it declares, every `params.X` its
|
|
1455
|
+
* compute / apply source reads, and the index / count a name pattern adds. null when the source uses params any other
|
|
1456
|
+
* way (params[k], destructuring, handing params on), because then the list could not be trusted and a warning could
|
|
1457
|
+
* name a key that works; null means no check, never a wrong one (RELWARN27: a bogus option on a built-in relation was
|
|
1458
|
+
* named, the same option on a custom one was stored in silence).
|
|
1459
|
+
*/
|
|
1460
|
+
export function customRelationReads(params, computeFunction, applyFunction) {
|
|
1461
|
+
const src = `${String(computeFunction ?? '')}\n${String(applyFunction ?? '')}`.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '');
|
|
1462
|
+
const bare = [...src.matchAll(/\bparams\b(?!\s*\??\.\s*[A-Za-z_$])/g)];
|
|
1463
|
+
if (bare.length)
|
|
1464
|
+
return null;
|
|
1465
|
+
const keys = new Set(['index', 'count']);
|
|
1466
|
+
if (params && typeof params === 'object')
|
|
1467
|
+
Object.keys(params).forEach((k) => keys.add(k));
|
|
1468
|
+
for (const m of src.matchAll(/\bparams\s*\??\.\s*([A-Za-z_$][\w$]*)/g))
|
|
1469
|
+
keys.add(m[1]);
|
|
1470
|
+
return [...keys].sort();
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* A relation's options checked against a CUSTOM relation's read list, in the page (the rule was registered there, maybe
|
|
1474
|
+
* in an earlier call). A fragment for a result object: `, ...{ key: warning }` or nothing. Built-ins are checked here
|
|
1475
|
+
* already (relationOptionWarning), so only a type without a built-in list is asked about.
|
|
1476
|
+
*/
|
|
1477
|
+
function customOptionWarningJs(relationType, params, key, injected = []) {
|
|
1478
|
+
if (typeof relationType !== 'string' || RELATION_PARAM_KEYS[relationType] || !params || typeof params !== 'object')
|
|
1479
|
+
return '';
|
|
1480
|
+
const keys = Object.keys(params);
|
|
1481
|
+
if (!keys.length)
|
|
1482
|
+
return '';
|
|
1483
|
+
return `, ...(function () { try {
|
|
1484
|
+
var cr = typeof window !== 'undefined' && window.__ppCustomRelations && window.__ppCustomRelations[${jsLiteral(relationType)}];
|
|
1485
|
+
if (!cr || !Array.isArray(cr.reads)) return {};
|
|
1486
|
+
var skip = ${JSON.stringify(injected)};
|
|
1487
|
+
var un = ${JSON.stringify(keys)}.filter(function (k) { return cr.reads.indexOf(k) < 0 && skip.indexOf(k) < 0; });
|
|
1488
|
+
if (!un.length) return {};
|
|
1489
|
+
return { ${key}: ${jsLiteral(relationType)} + ' does not read ' + un.join(', ') + ', so ' + (un.length > 1 ? 'they were' : 'it was') + ' ignored. ' + ${jsLiteral(relationType)} + ' reads: ' + cr.reads.join(', ') + '.' };
|
|
1490
|
+
} catch (e) { return {}; } })()`;
|
|
1491
|
+
}
|
|
1394
1492
|
export function relationOptionWarning(relationType, params, injected = []) {
|
|
1395
1493
|
const flip = equationFlipNote(relationType, params);
|
|
1396
1494
|
const unread = unreadOptionWarning(relationType, params, injected);
|
|
@@ -1470,7 +1568,7 @@ String(jsLiteral(targetId)) +
|
|
|
1470
1568
|
", " +
|
|
1471
1569
|
String(jsLiteral(relationType)) +
|
|
1472
1570
|
", " +
|
|
1473
|
-
String(JSON.stringify(params, null, 2)) + ");\n if (!success) {\n const __why = " + 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 return { success: true, sourceId: " + String(jsLiteral(sourceId)) + ", targetId: " + String(jsLiteral(targetId)) + ", relationType: " + String(jsLiteral(relationType)) + String((() => { const w = relationOptionWarning(relationType, params); return w ? `, warning: ${jsLiteral(w)}` : ''; })()) + " };\n})();\n").trim();
|
|
1571
|
+
String(JSON.stringify(params, null, 2)) + ");\n if (!success) {\n const __why = " + 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 return { success: true, sourceId: " + String(jsLiteral(sourceId)) + ", targetId: " + String(jsLiteral(targetId)) + ", relationType: " + String(jsLiteral(relationType)) + String((() => { const w = relationOptionWarning(relationType, params); return w ? `, warning: ${jsLiteral(w)}` : ''; })()) + String(customOptionWarningJs(relationType, params, 'warning')) + " };\n})();\n").trim();
|
|
1474
1572
|
}
|
|
1475
1573
|
/**
|
|
1476
1574
|
* The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
|
|
@@ -1802,7 +1900,7 @@ const formatted = relations.map(r => ({
|
|
|
1802
1900
|
// this tool even though it was wired correctly on the canvas.
|
|
1803
1901
|
targetId: r.target ?? r.targetId ?? null,
|
|
1804
1902
|
relationType: r.relation || r.type,
|
|
1805
|
-
params: r.params
|
|
1903
|
+
params: ${RELATION_PUBLIC_PARAMS_JS}(r.params)
|
|
1806
1904
|
}));
|
|
1807
1905
|
|
|
1808
1906
|
({ relations: formatted });
|
|
@@ -1822,7 +1920,7 @@ const relations = results.map(r => ({
|
|
|
1822
1920
|
// the filter string labelled every edge 'unknown' on an unfiltered query —
|
|
1823
1921
|
// which is exactly the query you make when exploring an unfamiliar graph.
|
|
1824
1922
|
relationType: r.relation || ${relationType ? jsLiteral(relationType) : "'unknown'"},
|
|
1825
|
-
params: r.params
|
|
1923
|
+
params: ${RELATION_PUBLIC_PARAMS_JS}(r.params)
|
|
1826
1924
|
}));
|
|
1827
1925
|
|
|
1828
1926
|
({ relations });
|
|
@@ -2246,6 +2344,29 @@ function withRadiusAxes(props) {
|
|
|
2246
2344
|
out.height = radiusY * 2;
|
|
2247
2345
|
return out;
|
|
2248
2346
|
}
|
|
2347
|
+
/**
|
|
2348
|
+
* One get_items entry. What create applied is read back here, so an agent can check it: the name a patch addresses
|
|
2349
|
+
* the item by, and scale, rotation and opacity when they are not the default (CROWSCALE30: a create-time scale
|
|
2350
|
+
* rendered exactly and get_items did not show it; PATCHIMG29: an import's name was not shown either). Defaults are
|
|
2351
|
+
* left out, so a plain item's entry stays as small as before.
|
|
2352
|
+
*/
|
|
2353
|
+
const GET_ITEMS_ENTRY_JS = ("entry => {\n const it = entry.item || {};\n const out = {\n id: entry.id || entry.itemId,\n type: entry.type,\n position: it.position ? { x: it.position.x, y: it.position.y } : null,\n hasAnimation: !!((it.data?.animationType && it.data.animationType !== 'none') || (Array.isArray(it.data?.keyframes) && it.data.keyframes.length > 0)),\n relations: app.getRelations(entry.id || entry.itemId).map(r => r.relation || r.type)\n };\n const name = it.name || entry.name || (it.data && it.data.name);\n if (name) out.name = name;\n const r2 = (v) => Math.round(v * 1000) / 1000;\n if (it.scaling && (Math.abs(it.scaling.x - 1) > 1e-6 || Math.abs(it.scaling.y - 1) > 1e-6)) out.scale" +
|
|
2354
|
+
" = { x: r2(it.scaling.x), y: r2(it.scaling.y) };\n if (typeof it.rotation === 'number' && Math.abs(it.rotation) > 1e-6) out.rotation = r2(it.rotation);\n if (typeof it.opacity === 'number' && it.opacity !== 1) out.opacity = r2(it.opacity);\n return out;\n}"
|
|
2355
|
+
|
|
2356
|
+
|
|
2357
|
+
|
|
2358
|
+
|
|
2359
|
+
|
|
2360
|
+
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
|
|
2364
|
+
|
|
2365
|
+
|
|
2366
|
+
|
|
2367
|
+
|
|
2368
|
+
|
|
2369
|
+
);
|
|
2249
2370
|
function generateGetItemsCode(filter) {
|
|
2250
2371
|
// A filter OBJECT is not the same as a filter. `{}` is already handled, but
|
|
2251
2372
|
// so is `{ type: undefined }` and anything whose keys this function does not
|
|
@@ -2270,13 +2391,7 @@ function generateGetItemsCode(filter) {
|
|
|
2270
2391
|
return `
|
|
2271
2392
|
// Get all items
|
|
2272
2393
|
const entries = app.itemRegistry.getAll();
|
|
2273
|
-
const items = entries.map(
|
|
2274
|
-
id: entry.id || entry.itemId,
|
|
2275
|
-
type: entry.type,
|
|
2276
|
-
position: entry.item.position ? { x: entry.item.position.x, y: entry.item.position.y } : null,
|
|
2277
|
-
hasAnimation: !!((entry.item.data?.animationType && entry.item.data.animationType !== 'none') || (Array.isArray(entry.item.data?.keyframes) && entry.item.data.keyframes.length > 0)),
|
|
2278
|
-
relations: app.getRelations(entry.id || entry.itemId).map(r => r.relation || r.type)
|
|
2279
|
-
}));
|
|
2394
|
+
const items = entries.map(${GET_ITEMS_ENTRY_JS});
|
|
2280
2395
|
|
|
2281
2396
|
({ items, count: items.length });
|
|
2282
2397
|
`.trim();
|
|
@@ -2285,13 +2400,7 @@ const items = entries.map(entry => ({
|
|
|
2285
2400
|
// Get items with filter
|
|
2286
2401
|
const entries = app.itemRegistry.getAll();
|
|
2287
2402
|
const filtered = entries.filter(entry => ${conditions.join(' && ')});
|
|
2288
|
-
const items = filtered.map(
|
|
2289
|
-
id: entry.id || entry.itemId,
|
|
2290
|
-
type: entry.type,
|
|
2291
|
-
position: entry.item.position ? { x: entry.item.position.x, y: entry.item.position.y } : null,
|
|
2292
|
-
hasAnimation: !!((entry.item.data?.animationType && entry.item.data.animationType !== 'none') || (Array.isArray(entry.item.data?.keyframes) && entry.item.data.keyframes.length > 0)),
|
|
2293
|
-
relations: app.getRelations(entry.id || entry.itemId).map(r => r.relation || r.type)
|
|
2294
|
-
}));
|
|
2403
|
+
const items = filtered.map(${GET_ITEMS_ENTRY_JS});
|
|
2295
2404
|
|
|
2296
2405
|
({ items, count: items.length });
|
|
2297
2406
|
`.trim();
|
|
@@ -2542,7 +2651,12 @@ function generateClearCanvasCode() {
|
|
|
2542
2651
|
String(MEDIA_PURGE) +
|
|
2543
2652
|
"\n\nif (typeof app.clearCanvas === 'function') {\n app.clearCanvas();\n} else {\n // Fallback for older builds without clearCanvas().\n if (app.textItemGroup) app.textItemGroup.removeChildren();\n if (app.patternGroup) app.patternGroup.removeChildren();\n if (app.itemRegistry) app.itemRegistry.clear();\n if (app.relationRegistry) app.relationRegistry.clear();\n}\n" +
|
|
2544
2653
|
String(ID_COUNTER_RESET) +
|
|
2545
|
-
"\n\nif (app.historyManager) app.historyManager.saveState();\n\nconst remaining = app.itemRegistry ? app.itemRegistry.getAll().length : 0;\n\n({ success: true, removedCount: Math.max(0, before - remaining), remainingItems: remaining, mediaRemoved: __mediaRemoved });\n"
|
|
2654
|
+
"\n// A rule outlives app.clearCanvas, and would go on running in the next scene against its item ids.\nlet __rulesRemoved = 0;\nif (typeof app.listContraptions === 'function' && typeof app.removeContraption === 'function') {\n app.listContraptions().map((c) => c.id).forEach((id) => { if (app.removeContraption(id)) __rulesRemoved++; });\n}\n\nif (app.historyManager) app.historyManager.saveState();\n\nconst remaining = app.itemRegistry ? app.itemRegistry.getAll().length : 0;\n\n({ success: true, ...(__rulesRemoved ? { contraptionsRemoved: __rulesRemoved } : {}), removedCount: Math.max(0, before - remaining), remainingItems: remaining, mediaRemoved: __mediaRemoved });\n"
|
|
2655
|
+
|
|
2656
|
+
|
|
2657
|
+
|
|
2658
|
+
|
|
2659
|
+
|
|
2546
2660
|
|
|
2547
2661
|
|
|
2548
2662
|
|
|
@@ -5181,7 +5295,7 @@ String(type) +
|
|
|
5181
5295
|
", sourceIds: __failed, error: " +
|
|
5182
5296
|
String(op.update === true ? '__why' : `'the studio refused the ' + ${type} + ' relation for ' + __failed.join(', ') + ' — nothing was bound' + (__why ? ': ' + __why : '.')`) +
|
|
5183
5297
|
" };\n}\nreturn { success: true, relationType: " +
|
|
5184
|
-
String(type) + ", " + String(op.update === true ? 'updated' : 'bound') + ": __done, targetId" + String(glob ? ', count: __members.length' : '') + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions, ['index', 'count', 'pattern']); return w ? `, optionWarning: ${jsLiteral(w)}` : ''; })()) + ", ...(__failed.length ? { failed: __failed, warning: __failed.length + ' of ' + __members.length + ' were not " + String(op.update === true ? 'changed' : 'bound') + ": ' + __failed.join(', ') } : {}) };\n");
|
|
5298
|
+
String(type) + ", " + String(op.update === true ? 'updated' : 'bound') + ": __done, targetId" + String(glob ? ', count: __members.length' : '') + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions, ['index', 'count', 'pattern']); return w ? `, optionWarning: ${jsLiteral(w)}` : ''; })()) + String(customOptionWarningJs(op.relationType, op.relationOptions, 'optionWarning', ['index', 'count', 'pattern'])) + ", ...(__failed.length ? { failed: __failed, warning: __failed.length + ' of ' + __members.length + ' were not " + String(op.update === true ? 'changed' : 'bound') + ": ' + __failed.join(', ') } : {}) };\n");
|
|
5185
5299
|
}
|
|
5186
5300
|
return ("\n" +
|
|
5187
5301
|
String(stepGuard) +
|
|
@@ -5200,7 +5314,7 @@ String(relOpts) +
|
|
|
5200
5314
|
");\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 = " +
|
|
5201
5315
|
String(relationRefusalReasonJs(jsLiteral(op.relationType))) +
|
|
5202
5316
|
";\n return { success: false, relationId: false, sourceId, targetId, relationType: " +
|
|
5203
|
-
String(JSON.stringify(op.relationType)) + ",\n error: 'the studio refused the ' + " + String(JSON.stringify(op.relationType)) + " + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };\n}\nreturn { relationId, sourceId, targetId, relationType: " + String(JSON.stringify(String(op.relationType))) + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()) + " };\n");
|
|
5317
|
+
String(JSON.stringify(op.relationType)) + ",\n error: 'the studio refused the ' + " + String(JSON.stringify(op.relationType)) + " + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };\n}\nreturn { relationId, sourceId, targetId, relationType: " + String(JSON.stringify(String(op.relationType))) + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()) + String(customOptionWarningJs(op.relationType, op.relationOptions, 'warning')) + " };\n");
|
|
5204
5318
|
case 'delete':
|
|
5205
5319
|
const delRef = batchRef(op.itemId);
|
|
5206
5320
|
return `
|
|
@@ -5429,7 +5543,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5429
5543
|
}
|
|
5430
5544
|
generateAgentExport(input) {
|
|
5431
5545
|
const validated = AgentExportInputSchema.parse(input);
|
|
5432
|
-
const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, alphaQuantizer, deterministic, motionBlur, ad: adOpts, frames, startTime } = validated;
|
|
5546
|
+
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;
|
|
5433
5547
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
5434
5548
|
const videoEncodeOpts = {};
|
|
5435
5549
|
// A RANGE of the timeline (FxTool #250): frames for t in [startTime, startTime + duration), each exactly the frame
|
|
@@ -5451,8 +5565,13 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5451
5565
|
videoEncodeOpts.minBitrate = minBitrate;
|
|
5452
5566
|
if (bitrateMode !== undefined)
|
|
5453
5567
|
videoEncodeOpts.bitrateMode = bitrateMode;
|
|
5454
|
-
//
|
|
5455
|
-
//
|
|
5568
|
+
// B45 (FxTool #288): 'aac' makes the studio refuse an MP4 whose soundtrack would be Opus (code
|
|
5569
|
+
// audio_codec_unavailable) rather than write a file WhatsApp rejects. 'any' is the engine's default, so not sent.
|
|
5570
|
+
if (audioCodec === 'aac')
|
|
5571
|
+
videoEncodeOpts.audioCodec = 'aac';
|
|
5572
|
+
// Determinism (B31, the contract signed off in FxTool #290): identical encoder
|
|
5573
|
+
// input and a container identical apart from the encoded samples; the file
|
|
5574
|
+
// itself repeats only where the studio reports bitExact: true.
|
|
5456
5575
|
if (deterministic)
|
|
5457
5576
|
videoEncodeOpts.deterministic = true;
|
|
5458
5577
|
// Motion blur (FxTool #55): the engine forwards it on every mp4/webm path
|
|
@@ -6086,9 +6205,10 @@ String(videoDuration) +
|
|
|
6086
6205
|
".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" +
|
|
6087
6206
|
"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" +
|
|
6088
6207
|
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" +
|
|
6089
|
-
"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 message: 'deterministic was asked for, but no encoder on this machine repeats byte for byte at this size (the studio test-encoded and checked
|
|
6090
|
-
"result.fidelity.note;\n } else if (__exact === null && __hw !== 'prefer-software') {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',\n message: __hw\n ? 'deterministic was asked for, but the encoder ran with hardwareAcceleration ' + JSON.
|
|
6091
|
-
"
|
|
6208
|
+
"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: 'deterministic was asked for, but no encoder on this machine repeats byte for byte at this size (the studio test-encoded and checked' + (__vr && __vr.cause === 'h264-threaded' ? ': the software H.264 encoder is multi-threaded and " +
|
|
6209
|
+
"takes no fixed quantizer' : '') + '). 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; export WebM for a byte-identical file.' }]);\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 = (result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',\n message: __hw\n ? 'deterministic was asked for, but the encoder ran with hardwareAcceleration ' + JSON.str" +
|
|
6210
|
+
"ingify(__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.fidelity.note;\n }\n }\n"
|
|
6211
|
+
|
|
6092
6212
|
|
|
6093
6213
|
|
|
6094
6214
|
|
|
@@ -7227,6 +7347,7 @@ String(jsLiteral(format)) +
|
|
|
7227
7347
|
|
|
7228
7348
|
|
|
7229
7349
|
|
|
7350
|
+
|
|
7230
7351
|
|
|
7231
7352
|
|
|
7232
7353
|
).trim();
|
|
@@ -8840,7 +8961,7 @@ String(validated.priority || 0) +
|
|
|
8840
8961
|
"\n });\n // The definition as given, kept on the page so a later call can read it back (pinepaper_lint_scene\n // customRelations): the engine keeps the compiled rule, not the text an agent would edit.\n window.__ppCustomRelations = window.__ppCustomRelations || {};\n window.__ppCustomRelations[" +
|
|
8841
8962
|
String(jsLiteral(validated.name)) +
|
|
8842
8963
|
"] = " +
|
|
8843
|
-
String(jsLiteral({ name: validated.name, ...(validated.description ? { description: validated.description } : {}), params: validated.params ?? {}, computeFunction: validated.computeFunction, applyFunction: validated.applyFunction })) +
|
|
8964
|
+
String(jsLiteral({ name: validated.name, ...(validated.description ? { description: validated.description } : {}), params: validated.params ?? {}, computeFunction: validated.computeFunction, applyFunction: validated.applyFunction, reads: customRelationReads(validated.params, validated.computeFunction, validated.applyFunction) })) +
|
|
8844
8965
|
";\n return { success: true, relationName: " +
|
|
8845
8966
|
String(jsLiteral(validated.name)) +
|
|
8846
8967
|
", message: 'Custom relation registered successfully' };\n } catch (error) {\n return { success: false, error: error.message };\n }\n})();\n"
|
|
@@ -12761,16 +12882,20 @@ String(diagJson) +
|
|
|
12761
12882
|
String(optsJson) +
|
|
12762
12883
|
");\n // THE RELATIONS AS BOUND, so a later agent can find the one to edit without exporting the scene to read it:\n // from, to, type and params, capped. And the custom relation definitions registered on this page, as given.\n const __rels = [];\n let __relCount = 0;\n try {\n const __rr = app.relationRegistry;\n const __active = __rr && typeof __rr.queryActive === 'function' ? __rr.queryActive() : [];\n __active.forEach((e) => (e.associations || []).forEach((a) => {\n __relCount++;\n if (__rels.length >= " +
|
|
12763
12884
|
String(RELATIONS_READBACK_CAP) +
|
|
12764
|
-
") return;\n const __name = e.item && e.item.data && e.item.data.name;\n __rels.push({ from: e.itemId, ...(__name ? { fromName: __name } : {}), to: a.target === undefined ? null : a.target, type: a.relation, params:
|
|
12765
|
-
|
|
12885
|
+
") return;\n const __name = e.item && e.item.data && e.item.data.name;\n __rels.push({ from: e.itemId, ...(__name ? { fromName: __name } : {}), to: a.target === undefined ? null : a.target, type: a.relation, params: " +
|
|
12886
|
+
String(RELATION_PUBLIC_PARAMS_JS) +
|
|
12887
|
+
"(a.params) });\n }));\n } catch (e) { /* the relations are an addition; the lint stands without them */ }\n // CUSTOM RELATIONS THIS SCENE USES (#43): one registered before a clear_canvas stayed in the read-back of the scene\n // after it. Those in use are listed; the rest are named in registeredUnused. Their source (computeFunction /\n // applyFunction) only with withSource: true, since an edit of params never needs it.\n const __usedTypes = new Set();\n try { const __rr2 = app.relationRegistry; (__rr2 && typeof __rr2.queryActive === 'function' ? __rr2.queryActive() : []).forEach((e) => (e.associations || []).forEach((a) => __usedTypes.add(a.relation))); } catch (e) { /* none kno" +
|
|
12888
|
+
"wn */ }\n const __allCustom = (typeof window !== 'undefined' && window.__ppCustomRelations) ? Object.values(window.__ppCustomRelations) : [];\n const __strip = (d) => { if (" +
|
|
12766
12889
|
String(input.withSource === true ? 'true' : 'false') +
|
|
12767
12890
|
") return d; const o = Object.assign({}, d); delete o.computeFunction; delete o.applyFunction; return o; };\n const __custom = __allCustom.filter((d) => __usedTypes.has(d.name)).map(__strip);\n const __unusedCustom = __allCustom.filter((d) => !__usedTypes.has(d.name)).map((d) => d.name);\n // HOW TO CHANGE IT (#43): an editing agent needs the op, not the schemas. Each relation comes with the batch op that\n // changes it; a group bound by a name pattern (\"dot-*\", recorded in each member's params) comes once, with the ops\n // that add and remove a member; a custom motion with the call that redefines it.\n const __edit = { hint: 'To change this scene, send the ops below (your values " +
|
|
12768
|
-
"in relationOptions) in ONE pinepaper_agent_batch_execute call; a custom motion changes by sending its pinepaper_register_custom_relation call again.', groups: [], relations: [], customRelations: [] };\n try {\n const __byPat = new Map();\n __rels.forEach((r) => {\n const pat = r.params && r.params.pattern;\n if (typeof pat === 'string') {\n const k = pat + '|' + r.type + '|' + r.to;\n if (!__byPat.has(k)) __byPat.set(k, { pattern: pat, type: r.type, to: r.to, members: [] });\n __byPat.get(k).members.push(r);\n return;\n }\n __edit.relations.push({ from: r.from, ...(r.fromName ? { fromName: r.
|
|
12769
|
-
"type,\n change: { type: 'relation', sourceId: r.fromName || r.from, ...(r.to != null ? { targetId: r.to } : {}), relationType: r.type, update: true, relationOptions: r.params } });\n });\n __byPat.forEach((g) => {\n const base = Object.assign({}, g.members[0].params); delete base.index; delete base.count; delete base.pattern;\n const names = g.members.map((m) => m.fromName).filter(Boolean).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));\n const prefix = g.pattern.split('*')[0];\n const nums = names.map((n) => Number(n.slice(prefix.length))).filter((n) => Number.isFinite(n));\n const next =
|
|
12770
|
-
"nums) + 1 : g.members.length + 1);\n const firstEntry = app.itemRegistry.get(g.members[0].from);\n const bind = { type: 'relation', sourceId: g.pattern, ...(g.to != null ? { targetId: g.to } : {}), relationType: g.type, relationOptions: base };\n __edit.groups.push({ pattern: g.pattern, relationType: g.type, members: g.members.length, ...(names.length ? { first: names[0], last: names[names.length - 1] } : {}), relationOptions: base,\n change: Object.assign({}, bind, { update: true }),\n addMember: [{ type: 'create', itemType: (firstEntry && firstEntry.type) || 'circle', position: [0, 0], properties: { name: next } },
|
|
12771
|
-
"
|
|
12891
|
+
"in relationOptions) in ONE pinepaper_agent_batch_execute call; a custom motion changes by sending its pinepaper_register_custom_relation call again (withSource: true gives the complete call).', groups: [], relations: [], customRelations: [] };\n try {\n const __byPat = new Map();\n __rels.forEach((r) => {\n const pat = r.params && r.params.pattern;\n if (typeof pat === 'string') {\n const k = pat + '|' + r.type + '|' + r.to;\n if (!__byPat.has(k)) __byPat.set(k, { pattern: pat, type: r.type, to: r.to, members: [] });\n __byPat.get(k).members.push(r);\n return;\n }\n __edit.relations.push({ from: r.from, ...(r.fromName ? { fromName: r.f" +
|
|
12892
|
+
"romName } : {}), to: r.to, relationType: r.type,\n change: { type: 'relation', sourceId: r.fromName || r.from, ...(r.to != null ? { targetId: r.to } : {}), relationType: r.type, update: true, relationOptions: r.params } });\n });\n __byPat.forEach((g) => {\n const base = Object.assign({}, g.members[0].params); delete base.index; delete base.count; delete base.pattern;\n const names = g.members.map((m) => m.fromName).filter(Boolean).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));\n const prefix = g.pattern.split('*')[0];\n const nums = names.map((n) => Number(n.slice(prefix.length))).filter((n) => Number.isFinite(n));\n const next = pr" +
|
|
12893
|
+
"efix + (nums.length ? Math.max.apply(null, nums) + 1 : g.members.length + 1);\n const firstEntry = app.itemRegistry.get(g.members[0].from);\n const bind = { type: 'relation', sourceId: g.pattern, ...(g.to != null ? { targetId: g.to } : {}), relationType: g.type, relationOptions: base };\n __edit.groups.push({ pattern: g.pattern, relationType: g.type, members: g.members.length, ...(names.length ? { first: names[0], last: names[names.length - 1] } : {}), relationOptions: base,\n change: Object.assign({}, bind, { update: true }),\n addMember: [{ type: 'create', itemType: (firstEntry && firstEntry.type) || 'circle', position: [0, 0], properties: { name: next } }, b" +
|
|
12894
|
+
"ind],\n ...(names.length ? { removeMember: [{ type: 'delete', itemId: names[names.length - 1] }, bind] } : {}) });\n });\n // The register call requires its source, so without withSource there is no call to offer: an op that can only be\n // refused is not one to send. reads is the server's own working-out from that source, not an argument of the call.\n __custom.forEach((d) => {\n const args = Object.assign({}, d); delete args.reads;\n __edit.customRelations.push(" +
|
|
12895
|
+
String(input.withSource === true ? "{ name: d.name, change: { tool: 'pinepaper_register_custom_relation', args } }" : "args") +
|
|
12896
|
+
");\n });\n } catch (e) { /* the read-back stands without the edit ops */ }\n if (!" +
|
|
12772
12897
|
String(input.withSource === true ? 'true' : 'false') +
|
|
12773
|
-
" && __custom.length) __edit.customHint = 'custom
|
|
12898
|
+
" && __custom.length) __edit.customHint = 'custom relations are listed without their source, and the call that redefines one needs it: pass withSource: true to get each complete pinepaper_register_custom_relation call.';\n // A GROUP IS LISTED ONCE (#43): members bound by a name pattern repeated the same relation once per item in\n // `relations`; they are in edit.groups, and counted here.\n const __loose = __rels.filter((r) => !(r.params && typeof r.params.pattern === 'string'));\n const __grouped = __rels.length - __loose.length;\n const __editOut = __edit.groups.length || __edit.relations.length || __edit.customRelations.length ? { edit: __edit } : {};\n if (" +
|
|
12774
12899
|
String(input.only === 'edit' ? 'true' : 'false') +
|
|
12775
12900
|
") {\n return Object.assign({ success: true }, __editOut.edit ? __editOut : { edit: { hint: 'nothing to change: this scene binds no relations.' } },\n __unusedCustom.length ? { registeredUnused: __unusedCustom } : {});\n }\n // warnings: what the engine's own rules found (TEXT_CLIPS_FRAME: a centre-anchored text running past the\n // frame edge). Passed through when the studio reports them; an older studio has none to pass.\n return { success: true, density: result.density, suggestions: result.suggestions,\n ...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}),\n relations: __loose, ...(__grouped ? { groupedRelations: __grouped" +
|
|
12776
12901
|
", groupedNote: 'relations bound by a name pattern are listed once per group in edit.groups' } : {}),\n ...(__relCount > __rels.length ? { relationsShown: __rels.length, relationCount: __relCount } : {}),\n ...(__custom.length ? { customRelations: __custom } : {}),\n ...(__unusedCustom.length ? { registeredUnused: __unusedCustom } : {}),\n ...__editOut };\n})();"
|
|
@@ -12836,6 +12961,7 @@ String(input.only === 'edit' ? 'true' : 'false') +
|
|
|
12836
12961
|
|
|
12837
12962
|
|
|
12838
12963
|
|
|
12964
|
+
|
|
12839
12965
|
|
|
12840
12966
|
|
|
12841
12967
|
).trim();
|
|
@@ -13385,7 +13511,7 @@ String(id) +
|
|
|
13385
13511
|
String(id) +
|
|
13386
13512
|
", beats: " +
|
|
13387
13513
|
String(input.beats.length) +
|
|
13388
|
-
", keyframes: __r.keyframes.length, duration: __r.end, final: __r.final };\n" +
|
|
13514
|
+
", keyframes: __r.keyframes.length, duration: __r.end, final: __r.final, ...(__r.defaultsUsed ? { defaultsUsed: __r.defaultsUsed } : {}) };\n" +
|
|
13389
13515
|
String(input.sound ? (" // SOUND ON THE ACTION (D68): one placed sound per moment the motion makes —\n // pop, launch, contact, shake — loud as the moment is strong. A sound that\n // does not place is reported; it never undoes the motion.\n const __map = Object.assign({ pop: 'pop', whoosh: 'whoosh', contact: 'tom', shake: 'hat' }, " +
|
|
13390
13516
|
String(S(typeof input.sound === 'object' ? Object.fromEntries(Object.entries(input.sound).filter(([k]) => k !== 'gain')) : {})) +
|
|
13391
13517
|
");\n const __gain = " +
|
|
@@ -16094,6 +16220,8 @@ String(op) +
|
|
|
16094
16220
|
return { success: true, action: 'create', eventId${input.at !== undefined ? `, at: ${JSON.stringify(input.at)}` : ''} };
|
|
16095
16221
|
})();`.trim();
|
|
16096
16222
|
}
|
|
16223
|
+
if (input.action !== 'pulse')
|
|
16224
|
+
return generateContraptionCode(input);
|
|
16097
16225
|
// pulse — a runtime fire, not a scene edit (no history save)
|
|
16098
16226
|
const eventIdJson = JSON.stringify(input.eventId);
|
|
16099
16227
|
const payloadJson = JSON.stringify(input.payload ?? null);
|
|
@@ -16108,6 +16236,74 @@ String(op) +
|
|
|
16108
16236
|
})();`.trim();
|
|
16109
16237
|
}
|
|
16110
16238
|
}
|
|
16239
|
+
/**
|
|
16240
|
+
* Rules, variables and bound text (the studio's contraption engine). Each action asks for the method it calls, so a
|
|
16241
|
+
* studio without it refuses by name instead of a TypeError. The engine's animateItem, playSound and custom actions are
|
|
16242
|
+
* not offered: animateItem goes through app.modify, which acts on the selection and not on its target; playSound does
|
|
16243
|
+
* not call app.playSound the way that method is written; custom needs a function, which JSON cannot carry. All three
|
|
16244
|
+
* answer ok regardless, so a rule using them would report success for nothing.
|
|
16245
|
+
*/
|
|
16246
|
+
function generateContraptionCode(input) {
|
|
16247
|
+
const refuse = (method) => `if (typeof app.${method} !== 'function') {
|
|
16248
|
+
return { success: false, action: ${jsLiteral(input.action)}, error: 'this studio has no contraption engine (app.${method}): update PinePaper Studio' };
|
|
16249
|
+
}`;
|
|
16250
|
+
const scope = jsLiteral(input.scope ?? 'default');
|
|
16251
|
+
const key = jsLiteral(input.key ?? '');
|
|
16252
|
+
const body = () => {
|
|
16253
|
+
switch (input.action) {
|
|
16254
|
+
case 'add_contraption': {
|
|
16255
|
+
const targets = [...new Set((input.actions ?? []).flatMap((a) => (a.type === 'modifyItem' ? [a.targetId] : [])))];
|
|
16256
|
+
const config = {
|
|
16257
|
+
...(input.contraptionId ? { id: input.contraptionId } : {}),
|
|
16258
|
+
sourceEvent: input.sourceEvent,
|
|
16259
|
+
actions: input.actions,
|
|
16260
|
+
...(input.condition ? { condition: input.condition } : {}),
|
|
16261
|
+
...(input.thenTrigger ? { thenTrigger: input.thenTrigger } : {}),
|
|
16262
|
+
...(input.elseTrigger ? { elseTrigger: input.elseTrigger } : {}),
|
|
16263
|
+
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
16264
|
+
};
|
|
16265
|
+
return `${refuse('addContraption')}
|
|
16266
|
+
const contraptionId = app.addContraption(${jsLiteral(config)});
|
|
16267
|
+
const missing = ${jsLiteral(targets)}.filter((id) => !app.getItemById(id));
|
|
16268
|
+
return { success: true, action: 'add_contraption', contraptionId,
|
|
16269
|
+
...(missing.length ? { warning: 'no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does' } : {}),
|
|
16270
|
+
${targets.length ? "note: 'changes a rule makes to an item stay: nothing rewinds them, so another export starts from the changed scene'" : ''} };`;
|
|
16271
|
+
}
|
|
16272
|
+
case 'remove_contraption':
|
|
16273
|
+
return `${refuse('removeContraption')}
|
|
16274
|
+
const removed = app.removeContraption(${jsLiteral(input.contraptionId)});
|
|
16275
|
+
return removed ? { success: true, action: 'remove_contraption', contraptionId: ${jsLiteral(input.contraptionId)} }
|
|
16276
|
+
: { success: false, action: 'remove_contraption', error: 'no rule has the id ' + ${jsLiteral(input.contraptionId)} };`;
|
|
16277
|
+
case 'list_contraptions':
|
|
16278
|
+
return `${refuse('listContraptions')}
|
|
16279
|
+
return { success: true, action: 'list_contraptions', contraptions: app.listContraptions() };`;
|
|
16280
|
+
case 'set_variable':
|
|
16281
|
+
return `${refuse('setVariable')}
|
|
16282
|
+
app.setVariable(${key}, ${jsLiteral(input.value)}, ${scope});
|
|
16283
|
+
return { success: true, action: 'set_variable', key: ${key}, value: app.getVariable(${key}, null, ${scope}) };`;
|
|
16284
|
+
case 'get_variable':
|
|
16285
|
+
return `${refuse('getVariable')}
|
|
16286
|
+
return { success: true, action: 'get_variable', key: ${key}, value: app.getVariable(${key}, ${jsLiteral(input.fallback ?? null)}, ${scope}) };`;
|
|
16287
|
+
case 'increment_variable':
|
|
16288
|
+
return `${refuse('incrementVariable')}
|
|
16289
|
+
return { success: true, action: 'increment_variable', key: ${key}, value: app.incrementVariable(${key}, ${input.by ?? 1}, ${scope}) };`;
|
|
16290
|
+
default: {
|
|
16291
|
+
const itemId = jsLiteral(input.itemId);
|
|
16292
|
+
return `${refuse('bindTextToState')}
|
|
16293
|
+
const item = app.getItemById(${itemId});
|
|
16294
|
+
if (!item) return { success: false, action: 'bind_text', error: 'Item not found: ' + ${itemId} };
|
|
16295
|
+
if (item.content === undefined) return { success: false, action: 'bind_text', error: ${itemId} + ' is not a text item, so it has no text to bind' };
|
|
16296
|
+
app.bindTextToState(${itemId}, ${jsLiteral(input.template)}, ${scope});
|
|
16297
|
+
return { success: true, action: 'bind_text', itemId: ${itemId}, text: item.content };`;
|
|
16298
|
+
}
|
|
16299
|
+
}
|
|
16300
|
+
};
|
|
16301
|
+
return `
|
|
16302
|
+
// Contraption engine: ${input.action}
|
|
16303
|
+
(function() {
|
|
16304
|
+
${body()}
|
|
16305
|
+
})();`.trim();
|
|
16306
|
+
}
|
|
16111
16307
|
// Export singleton instance
|
|
16112
16308
|
export const codeGenerator = new PinePaperCodeGenerator();
|
|
16113
16309
|
/**
|