@pinepaper.studio/mcp-server 1.6.31 → 1.6.33
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 +132 -1
- package/SECURITY.md +1 -1
- package/dist/agent/types.d.ts +13 -13
- 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 +5 -2
- 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 +139 -63
- 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 +7 -5
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +13 -5
- 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 +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 +26 -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 +92 -13
- 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 +3 -3
- 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/reply-fields.d.ts +15 -0
- package/dist/tools/reply-fields.d.ts.map +1 -0
- package/dist/tools/reply-fields.js +117 -0
- package/dist/tools/reply-fields.js.map +1 -0
- package/dist/tools/scene-cache-export.d.ts.map +1 -1
- package/dist/tools/scene-cache-export.js +2 -1
- package/dist/tools/scene-cache-export.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 +10 -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 +342 -67
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +277 -111
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +79 -7
- 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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { compileChoreography } from '../tools/choreograph.js';
|
|
8
8
|
import { compileBatchTimelines, emitTimelineOpBody } from '../tools/timeline.js';
|
|
9
9
|
import { normalizeSvgSize } from '../utils/svg-size.js';
|
|
10
|
-
import { WORLD3D_COLOR_PATHS, PATTERN_KINDS, EXPRESSION_FUNCTION_ARITY, EXPRESSION_CONSTANTS, EXPRESSION_REJECTED, RELATION_PARAM_KEYS, ANIMATION_OPTION_KEYS } from '../tools/engine-lists.js';
|
|
10
|
+
import { WORLD3D_COLOR_PATHS, PATTERN_KINDS, EXPRESSION_FUNCTION_ARITY, EXPRESSION_CONSTANTS, EXPRESSION_REJECTED, RELATION_PARAM_KEYS, RELATION_LOOP_PARAMS, ANIMATION_OPTION_KEYS, FILTER_TYPES, FILTER_PARAM_RANGES } from '../tools/engine-lists.js';
|
|
11
11
|
import { expressionGrammarText } from '../tools/expression-grammar.js';
|
|
12
12
|
import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
|
|
13
13
|
import { NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
|
|
@@ -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, ...RELATION_LOOP_PARAMS])};
|
|
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 { ignoredOptions: un, ${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);
|
|
@@ -1410,10 +1508,25 @@ function equationFlipNote(relationType, params) {
|
|
|
1410
1508
|
return null;
|
|
1411
1509
|
return 'params.equation is read with y UP (flipY defaults to true), so on screen the curve is mirrored about cy. If you wrote it in screen coordinates (y down), add flipY: false to the equation.';
|
|
1412
1510
|
}
|
|
1413
|
-
|
|
1511
|
+
/** What a built-in relation reads: its rule's params, and what the relation loop reads on every relation. */
|
|
1512
|
+
function relationReads(relationType) {
|
|
1414
1513
|
const keys = typeof relationType === 'string' ? RELATION_PARAM_KEYS[relationType] : undefined;
|
|
1514
|
+
return keys ? [...keys, ...RELATION_LOOP_PARAMS] : undefined;
|
|
1515
|
+
}
|
|
1516
|
+
function unreadOptionWarning(relationType, params, injected) {
|
|
1517
|
+
const keys = relationReads(relationType);
|
|
1415
1518
|
return keys ? unreadKeysWarning(String(relationType), keys, params, injected) : null;
|
|
1416
1519
|
}
|
|
1520
|
+
const unreadKeys = (keys, params, injected = []) => keys && params && typeof params === 'object' ? Object.keys(params).filter((k) => !keys.includes(k) && !injected.includes(k)) : [];
|
|
1521
|
+
/**
|
|
1522
|
+
* A relation reply's fields for its options: the unread keys as data (ignoredOptions, which the reply's
|
|
1523
|
+
* ignoredArguments is built from) beside the sentence. Built-ins are known here, a custom relation only in the page.
|
|
1524
|
+
*/
|
|
1525
|
+
function relationOptionFields(relationType, params, key, injected = []) {
|
|
1526
|
+
const unread = unreadKeys(relationReads(relationType), params, injected);
|
|
1527
|
+
const warning = relationOptionWarning(relationType, params, injected);
|
|
1528
|
+
return `${unread.length ? `, ignoredOptions: ${jsLiteral(unread)}` : ''}${warning ? `, ${key}: ${jsLiteral(warning)}` : ''}${customOptionWarningJs(relationType, params, key, injected)}`;
|
|
1529
|
+
}
|
|
1417
1530
|
/** The keys of `params` outside `keys`, named with a "did you mean" and the list that is read; null when all are read. */
|
|
1418
1531
|
function unreadKeysWarning(reader, keys, params, injected = []) {
|
|
1419
1532
|
if (!params || typeof params !== 'object')
|
|
@@ -1470,7 +1583,7 @@ String(jsLiteral(targetId)) +
|
|
|
1470
1583
|
", " +
|
|
1471
1584
|
String(jsLiteral(relationType)) +
|
|
1472
1585
|
", " +
|
|
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((
|
|
1586
|
+
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(relationOptionFields(relationType, params, 'warning')) + " };\n})();\n").trim();
|
|
1474
1587
|
}
|
|
1475
1588
|
/**
|
|
1476
1589
|
* The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
|
|
@@ -1546,9 +1659,18 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1546
1659
|
for (const k of ['velocities', 'offsets'])
|
|
1547
1660
|
if (out[k] !== undefined)
|
|
1548
1661
|
out[k] = pairs(out[k]);
|
|
1662
|
+
// A trail's single offset is one pair; the studio reads [x, y] and dropped {x, y} with nothing said (TRAILOFF31).
|
|
1663
|
+
const off = out.offset;
|
|
1664
|
+
if (off && typeof off === 'object' && !Array.isArray(off) && typeof off.x === 'number' && typeof off.y === 'number')
|
|
1665
|
+
out.offset = [off.x, off.y];
|
|
1549
1666
|
return out;
|
|
1550
1667
|
};
|
|
1551
1668
|
const out = fix(spec);
|
|
1669
|
+
// rate, start and end drive a trail. Beside bursts they drew nothing and said nothing (EMITCONT31).
|
|
1670
|
+
const trailOnly = ['rate', 'start', 'end'].filter((k) => spec[k] !== undefined);
|
|
1671
|
+
if (trailOnly.length && Array.isArray(spec.bursts) && spec.mode !== 'trail' && spec.target === undefined) {
|
|
1672
|
+
warnings.push(`${trailOnly.join(', ')} ${trailOnly.length > 1 ? 'are' : 'is'} not read by a burst emitter, so ${trailOnly.length > 1 ? 'they had' : 'it had'} no effect: only the bursts' own count is drawn. For particles over time, add bursts, or use mode 'trail' with a target.`);
|
|
1673
|
+
}
|
|
1552
1674
|
if (Array.isArray(spec.bursts))
|
|
1553
1675
|
out.bursts = spec.bursts.map((b) => (b && typeof b === 'object' ? fix(b) : b));
|
|
1554
1676
|
// The studio's default count per burst (ParticleEmitter DEFAULT_LOOK).
|
|
@@ -1802,7 +1924,7 @@ const formatted = relations.map(r => ({
|
|
|
1802
1924
|
// this tool even though it was wired correctly on the canvas.
|
|
1803
1925
|
targetId: r.target ?? r.targetId ?? null,
|
|
1804
1926
|
relationType: r.relation || r.type,
|
|
1805
|
-
params: r.params
|
|
1927
|
+
params: ${RELATION_PUBLIC_PARAMS_JS}(r.params)
|
|
1806
1928
|
}));
|
|
1807
1929
|
|
|
1808
1930
|
({ relations: formatted });
|
|
@@ -1822,7 +1944,7 @@ const relations = results.map(r => ({
|
|
|
1822
1944
|
// the filter string labelled every edge 'unknown' on an unfiltered query —
|
|
1823
1945
|
// which is exactly the query you make when exploring an unfamiliar graph.
|
|
1824
1946
|
relationType: r.relation || ${relationType ? jsLiteral(relationType) : "'unknown'"},
|
|
1825
|
-
params: r.params
|
|
1947
|
+
params: ${RELATION_PUBLIC_PARAMS_JS}(r.params)
|
|
1826
1948
|
}));
|
|
1827
1949
|
|
|
1828
1950
|
({ relations });
|
|
@@ -1974,6 +2096,24 @@ function unanimatedTextKeysJs(keys) {
|
|
|
1974
2096
|
return ${JSON.stringify(spec)}.filter(function(s) { return !s.names.some(function(n) { return __names.indexOf(n) >= 0; }); }).map(function(s) { return s.key; });
|
|
1975
2097
|
})()`;
|
|
1976
2098
|
}
|
|
2099
|
+
/**
|
|
2100
|
+
* An image filter's parameter as a key: keyframe_animate stores it, and a filter does not read a track, so the item
|
|
2101
|
+
* never changes with success:true (FILTKEY31). Asked of the same list, so a studio that starts animating one is believed.
|
|
2102
|
+
*/
|
|
2103
|
+
const FILTER_PARAM_KEYS = new Set([...Object.values(FILTER_PARAM_RANGES).flatMap((p) => Object.keys(p)), ...FILTER_TYPES].filter((k) => !['color', 'opacity', 'scale', 'x', 'y', 'rotation', 'size', 'radius', 'width', 'height', 'value', 'amount'].includes(k)));
|
|
2104
|
+
function unanimatedFilterKeysJs(keys) {
|
|
2105
|
+
const filterKeys = [...new Set(keys)].filter((k) => FILTER_PARAM_KEYS.has(k));
|
|
2106
|
+
if (!filterKeys.length)
|
|
2107
|
+
return '[]';
|
|
2108
|
+
return `(function() {
|
|
2109
|
+
let __l = null;
|
|
2110
|
+
try { __l = typeof app.listAnimatableProperties === 'function' ? app.listAnimatableProperties() : null; } catch (_) { __l = null; }
|
|
2111
|
+
if (!__l || !__l.properties) return [];
|
|
2112
|
+
const __names = __l.properties.map(function(p) { return p && p.name; });
|
|
2113
|
+
return ${JSON.stringify(filterKeys)}.filter(function(k) { return __names.indexOf(k) < 0; });
|
|
2114
|
+
})()`;
|
|
2115
|
+
}
|
|
2116
|
+
const UNANIMATED_FILTER_KEYS_SENTENCE = `' is an image filter setting and not an animatable property, so the key had no effect. Set a fixed value with pinepaper_image_filter or pinepaper_add_filter, and animate opacity or scale for a change over time.'`;
|
|
1977
2117
|
/** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
|
|
1978
2118
|
const UNANIMATED_TEXT_KEYS_SENTENCE = `' is not an animatable property in this studio (app.listAnimatableProperties does not list it), so it had no effect: a key animates only where that list names it. Here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX. For a skew, key skewX / skewY (degrees) where the studio lists them, else a transform matrix track.'`;
|
|
1979
2119
|
/**
|
|
@@ -1999,12 +2139,6 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
|
|
|
1999
2139
|
? `, propertyGaps: ${JSON.stringify(gaps.slice(0, 10))}, gapWarning: ${JSON.stringify(`${gaps.length} keyframe(s) leave out a property other keys animate (${[...new Set(gaps.flatMap((g) => g.missing))].join(', ')}). The engine can break that property's track at such a key; repeat the value on every key — hold it by repeating the previous value.`)}`
|
|
2000
2140
|
: '';
|
|
2001
2141
|
const first = [...keyframes].sort((a, b) => a.time - b.time)[0];
|
|
2002
|
-
// loop:true repeats in the editor; an export of the measured build played
|
|
2003
|
-
// the keyframes ONCE and held (round 7, 2.24). Said where it is set, with
|
|
2004
|
-
// the two ways that do export, until the engine's export clock wraps them.
|
|
2005
|
-
const loopNote = loop
|
|
2006
|
-
? `, loopNote: ${JSON.stringify('loop:true repeats in the editor, but has been measured playing ONCE in an exported video and then holding the last keyframe. For an export longer than one cycle, check a frame in the second cycle — if it holds, write the keyframes out per cycle, or use a loop preset (pinepaper_animate), which does cycle in export.')}`
|
|
2007
|
-
: '';
|
|
2008
2142
|
// SIZEKEY-PAIR (mcp-cloud #28): on a studio before FxTool #262, x (or
|
|
2009
2143
|
// position) keyed beside fontSize on one text item drifted in an export: the
|
|
2010
2144
|
// size was applied after the position, and a text resizes around its anchor.
|
|
@@ -2016,7 +2150,7 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
|
|
|
2016
2150
|
const easingNote = easingAt !== 'departure' && first?.easing && first.easing !== 'linear'
|
|
2017
2151
|
? `, note: ${JSON.stringify(`easing '${first.easing}' is on the first keyframe, where it has no effect by default: easing shapes the segment ARRIVING at a keyframe. Put it on the keyframe you are moving to, or pass easingAt: 'departure' to read every easing as the move LEAVING its key (current studio).`)}`
|
|
2018
2152
|
: '';
|
|
2019
|
-
return `${easingNote}${sizekeyPairNote}${
|
|
2153
|
+
return `${easingNote}${sizekeyPairNote}${gapNote}`;
|
|
2020
2154
|
}
|
|
2021
2155
|
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
|
|
2022
2156
|
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
@@ -2074,9 +2208,12 @@ String(JSON.stringify(levelKeys)) +
|
|
|
2074
2208
|
const textKeyCheck = textKeysJs ? `
|
|
2075
2209
|
const __ignored = ${textKeysJs};
|
|
2076
2210
|
if (__ignored.length) __warn.push(__ignored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE});` : '';
|
|
2211
|
+
const filterKeyCheck = `
|
|
2212
|
+
const __filterIgnored = ${unanimatedFilterKeysJs(keyframes.flatMap((k) => Object.keys(k.properties ?? {})))};
|
|
2213
|
+
if (__filterIgnored.length) __warn.push(__filterIgnored.join(', ') + ${UNANIMATED_FILTER_KEYS_SENTENCE});`;
|
|
2077
2214
|
const easingAtCheck = easingAt !== undefined ? `
|
|
2078
2215
|
if (__opts && Object.prototype.hasOwnProperty.call(__opts, 'easingAt')) __warn.push('easingAt ' + ${JSON.stringify(JSON.stringify(easingAt))} + ' was not applied: this studio predates it, so every easing was read as the move ARRIVING at its key. Put each easing on the key the move goes to instead.');` : '';
|
|
2079
|
-
const runtimeWarning =
|
|
2216
|
+
const runtimeWarning = `, ...(${textKeysJs ? '__ignored.length || ' : ''}__filterIgnored.length ? { ignoredProperties: ${textKeysJs ? '__ignored.concat(__filterIgnored)' : '__filterIgnored'} } : {}), ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
|
|
2080
2217
|
const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `${ID}`;
|
|
2081
2218
|
return ("\n// Apply keyframe animation to " +
|
|
2082
2219
|
String(ID) +
|
|
@@ -2092,6 +2229,7 @@ String(keyframesJson) +
|
|
|
2092
2229
|
", __opts);\n const __warn = [];" +
|
|
2093
2230
|
String(easingAtCheck) +
|
|
2094
2231
|
String(textKeyCheck) +
|
|
2232
|
+
String(filterKeyCheck) +
|
|
2095
2233
|
"\n const __now = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];\n const __kept = __prev.every(function(t) { return __now.indexOf(t) >= 0; });" +
|
|
2096
2234
|
String(append ? `
|
|
2097
2235
|
// Property gaps across the merged track (the same check a single call makes
|
|
@@ -2111,8 +2249,7 @@ String(calculatedDuration) +
|
|
|
2111
2249
|
", loop: " +
|
|
2112
2250
|
String(loop) +
|
|
2113
2251
|
String(append ? ', append: true' : '') +
|
|
2114
|
-
", ...(__track ? { track: __track } : {})" +
|
|
2115
|
-
String(append ? `, ...(__gaps.length ? { propertyGaps: __gaps.slice(0, 10), gapWarning: __gaps.length + ' key(s) of the merged track leave out a property other keys animate (' + __gaps.reduce(function(a, g) { g.missing.forEach(function(p) { if (a.indexOf(p) < 0) a.push(p); }); return a; }, []).join(', ') + '). The engine can break that property at such a key; give each key the full set — hold a value by repeating it.' } : {})` : '') + String(timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : '') + String(clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : '') + String(clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : '') + String(__notes) + String(runtimeWarning) + " };\n})();\n").trim();
|
|
2252
|
+
", ...(__track ? { track: __track } : {})" + String(append ? `, ...(__gaps.length ? { propertyGaps: __gaps.slice(0, 10), gapWarning: __gaps.length + ' key(s) of the merged track leave out a property other keys animate (' + __gaps.reduce(function(a, g) { g.missing.forEach(function(p) { if (a.indexOf(p) < 0) a.push(p); }); return a; }, []).join(', ') + '). The engine can break that property at such a key; give each key the full set — hold a value by repeating it.' } : {})` : '') + String(timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : '') + String(clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : '') + String(clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : '') + String(__notes) + String(runtimeWarning) + " };\n})();\n").trim();
|
|
2116
2253
|
}
|
|
2117
2254
|
/**
|
|
2118
2255
|
* Template for executing generators
|
|
@@ -2246,6 +2383,29 @@ function withRadiusAxes(props) {
|
|
|
2246
2383
|
out.height = radiusY * 2;
|
|
2247
2384
|
return out;
|
|
2248
2385
|
}
|
|
2386
|
+
/**
|
|
2387
|
+
* One get_items entry. What create applied is read back here, so an agent can check it: the name a patch addresses
|
|
2388
|
+
* the item by, and scale, rotation and opacity when they are not the default (CROWSCALE30: a create-time scale
|
|
2389
|
+
* rendered exactly and get_items did not show it; PATCHIMG29: an import's name was not shown either). Defaults are
|
|
2390
|
+
* left out, so a plain item's entry stays as small as before.
|
|
2391
|
+
*/
|
|
2392
|
+
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" +
|
|
2393
|
+
" = { 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}"
|
|
2394
|
+
|
|
2395
|
+
|
|
2396
|
+
|
|
2397
|
+
|
|
2398
|
+
|
|
2399
|
+
|
|
2400
|
+
|
|
2401
|
+
|
|
2402
|
+
|
|
2403
|
+
|
|
2404
|
+
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
);
|
|
2249
2409
|
function generateGetItemsCode(filter) {
|
|
2250
2410
|
// A filter OBJECT is not the same as a filter. `{}` is already handled, but
|
|
2251
2411
|
// so is `{ type: undefined }` and anything whose keys this function does not
|
|
@@ -2270,13 +2430,7 @@ function generateGetItemsCode(filter) {
|
|
|
2270
2430
|
return `
|
|
2271
2431
|
// Get all items
|
|
2272
2432
|
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
|
-
}));
|
|
2433
|
+
const items = entries.map(${GET_ITEMS_ENTRY_JS});
|
|
2280
2434
|
|
|
2281
2435
|
({ items, count: items.length });
|
|
2282
2436
|
`.trim();
|
|
@@ -2285,13 +2439,7 @@ const items = entries.map(entry => ({
|
|
|
2285
2439
|
// Get items with filter
|
|
2286
2440
|
const entries = app.itemRegistry.getAll();
|
|
2287
2441
|
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
|
-
}));
|
|
2442
|
+
const items = filtered.map(${GET_ITEMS_ENTRY_JS});
|
|
2295
2443
|
|
|
2296
2444
|
({ items, count: items.length });
|
|
2297
2445
|
`.trim();
|
|
@@ -2542,7 +2690,12 @@ function generateClearCanvasCode() {
|
|
|
2542
2690
|
String(MEDIA_PURGE) +
|
|
2543
2691
|
"\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
2692
|
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"
|
|
2693
|
+
"\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"
|
|
2694
|
+
|
|
2695
|
+
|
|
2696
|
+
|
|
2697
|
+
|
|
2698
|
+
|
|
2546
2699
|
|
|
2547
2700
|
|
|
2548
2701
|
|
|
@@ -3440,7 +3593,7 @@ String(JSON.stringify(validated.presetId)) +
|
|
|
3440
3593
|
const code = stepNoTarget
|
|
3441
3594
|
? `(function() {
|
|
3442
3595
|
if (!${STEP_ACTIVE_TARGETLESS_JS}) return { success: false, relationType: 'on_event_step_active', error: ${jsLiteral(stepNoTarget)} };
|
|
3443
|
-
|
|
3596
|
+
${asReturningBody(built)}
|
|
3444
3597
|
})();`
|
|
3445
3598
|
: built;
|
|
3446
3599
|
// NO TARGET (event reactions like on_event_play_sound, self-relations): the
|
|
@@ -4120,7 +4273,7 @@ if (typeof _gen === 'function') {
|
|
|
4120
4273
|
})();`);
|
|
4121
4274
|
}
|
|
4122
4275
|
// Establish relations
|
|
4123
|
-
for (const relation of relations) {
|
|
4276
|
+
for (const [relationIndex, relation] of relations.entries()) {
|
|
4124
4277
|
const { source, target, type, params = {} } = relation;
|
|
4125
4278
|
const paramsStr = JSON.stringify(params);
|
|
4126
4279
|
const specProblem = relationSpecProblem(type, params, target, 'no-page');
|
|
@@ -4150,7 +4303,10 @@ String(jsLiteral(type)) +
|
|
|
4150
4303
|
String(jsLiteral(source)) +
|
|
4151
4304
|
", target: " +
|
|
4152
4305
|
String(jsLiteral(target)) +
|
|
4153
|
-
", type: " +
|
|
4306
|
+
", type: " +
|
|
4307
|
+
String(jsLiteral(type)) +
|
|
4308
|
+
" });\n const __o = { _: 0" +
|
|
4309
|
+
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"));
|
|
4154
4310
|
}
|
|
4155
4311
|
// Apply animations
|
|
4156
4312
|
for (const animation of animations) {
|
|
@@ -5120,7 +5276,7 @@ const targetId = ${animItemRef};
|
|
|
5120
5276
|
const item = app.getItemById(targetId);
|
|
5121
5277
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
5122
5278
|
app.animate(item, { animationType: ${JSON.stringify(String(op.animationType))}, ...${animOpts} });
|
|
5123
|
-
return { itemId: targetId, animationType: ${JSON.stringify(String(op.animationType))}${(() => { const w = animationOptionWarning(op.animationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()} };
|
|
5279
|
+
return { itemId: targetId, animationType: ${JSON.stringify(String(op.animationType))}${(() => { const w = animationOptionWarning(op.animationOptions); return w ? `, ignoredOptions: ${jsLiteral(unreadKeys(ANIMATION_OPTION_KEYS, op.animationOptions))}, warning: ${jsLiteral(w)}` : ''; })()} };
|
|
5124
5280
|
`;
|
|
5125
5281
|
case 'relation':
|
|
5126
5282
|
const srcRef = batchRef(op.sourceId);
|
|
@@ -5181,7 +5337,7 @@ String(type) +
|
|
|
5181
5337
|
", sourceIds: __failed, error: " +
|
|
5182
5338
|
String(op.update === true ? '__why' : `'the studio refused the ' + ${type} + ' relation for ' + __failed.join(', ') + ' — nothing was bound' + (__why ? ': ' + __why : '.')`) +
|
|
5183
5339
|
" };\n}\nreturn { success: true, relationType: " +
|
|
5184
|
-
String(type) + ", " + String(op.update === true ? 'updated' : 'bound') + ": __done, targetId" + String(glob ? ', count: __members.length' : '') + String((
|
|
5340
|
+
String(type) + ", " + String(op.update === true ? 'updated' : 'bound') + ": __done, targetId" + String(glob ? ', count: __members.length' : '') + String(relationOptionFields(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
5341
|
}
|
|
5186
5342
|
return ("\n" +
|
|
5187
5343
|
String(stepGuard) +
|
|
@@ -5200,7 +5356,7 @@ String(relOpts) +
|
|
|
5200
5356
|
");\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
5357
|
String(relationRefusalReasonJs(jsLiteral(op.relationType))) +
|
|
5202
5358
|
";\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((
|
|
5359
|
+
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(relationOptionFields(op.relationType, op.relationOptions, 'warning')) + " };\n");
|
|
5204
5360
|
case 'delete':
|
|
5205
5361
|
const delRef = batchRef(op.itemId);
|
|
5206
5362
|
return `
|
|
@@ -5288,8 +5444,9 @@ return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget}
|
|
|
5288
5444
|
kfExtra.push(`timeUnits: ${jsLiteral(kfUnits)}`);
|
|
5289
5445
|
const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
|
|
5290
5446
|
const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
|
|
5291
|
-
const
|
|
5292
|
-
const
|
|
5447
|
+
const kfFilterKeysJs = unanimatedFilterKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
|
|
5448
|
+
const kfTextCheck = `\nconst _kfIgnored = ${kfTextKeysJs || '[]'};\nconst _kfFilterIgnored = ${kfFilterKeysJs};`;
|
|
5449
|
+
const kfIgnored = `, ...(_kfIgnored.length || _kfFilterIgnored.length ? { ignoredProperties: _kfIgnored.concat(_kfFilterIgnored), warning: [_kfIgnored.length ? _kfIgnored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE} : '', _kfFilterIgnored.length ? _kfFilterIgnored.join(', ') + ${UNANIMATED_FILTER_KEYS_SENTENCE} : ''].filter(Boolean).join(' ') } : {})`;
|
|
5293
5450
|
return ("\nconst targetId = " +
|
|
5294
5451
|
String(kfItemRef) +
|
|
5295
5452
|
";\nconst _target = (app.getItemById && app.getItemById(targetId))\n || (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(targetId));\nif (!_target) {\n return { success: false, itemId: targetId, error: 'Item not found: ' + targetId };\n}\nconst _kfItem = _target.item || _target;\nconst _prevKeys = (_kfItem.data && Array.isArray(_kfItem.data.keyframes)) ? _kfItem.data.keyframes.length : 0;\napp.addAnimation(targetId, " +
|
|
@@ -5429,7 +5586,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5429
5586
|
}
|
|
5430
5587
|
generateAgentExport(input) {
|
|
5431
5588
|
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;
|
|
5589
|
+
const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, audioCodec, transparent, alphaQuantizer, deterministic, motionBlur, ad: adOpts, frames, startTime } = validated;
|
|
5433
5590
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
5434
5591
|
const videoEncodeOpts = {};
|
|
5435
5592
|
// A RANGE of the timeline (FxTool #250): frames for t in [startTime, startTime + duration), each exactly the frame
|
|
@@ -5451,8 +5608,13 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5451
5608
|
videoEncodeOpts.minBitrate = minBitrate;
|
|
5452
5609
|
if (bitrateMode !== undefined)
|
|
5453
5610
|
videoEncodeOpts.bitrateMode = bitrateMode;
|
|
5454
|
-
//
|
|
5455
|
-
//
|
|
5611
|
+
// B45 (FxTool #288): 'aac' makes the studio refuse an MP4 whose soundtrack would be Opus (code
|
|
5612
|
+
// audio_codec_unavailable) rather than write a file WhatsApp rejects. 'any' is the engine's default, so not sent.
|
|
5613
|
+
if (audioCodec === 'aac')
|
|
5614
|
+
videoEncodeOpts.audioCodec = 'aac';
|
|
5615
|
+
// Determinism (B31, the contract signed off in FxTool #290): identical encoder
|
|
5616
|
+
// input and a container identical apart from the encoded samples; the file
|
|
5617
|
+
// itself repeats only where the studio reports bitExact: true.
|
|
5456
5618
|
if (deterministic)
|
|
5457
5619
|
videoEncodeOpts.deterministic = true;
|
|
5458
5620
|
// Motion blur (FxTool #55): the engine forwards it on every mp4/webm path
|
|
@@ -6086,9 +6248,10 @@ String(videoDuration) +
|
|
|
6086
6248
|
".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
6249
|
"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
6250
|
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
|
-
"
|
|
6251
|
+
"eport it.\n if (result && result.success) {\n const __hw = __vr && __vr.hardwareAcceleration;\n const __exact = __vr && typeof __vr.bitExact === 'boolean' ? __vr.bitExact : null;\n if (__exact === false) {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',\n ...(__vr && __vr.cause ? { cause: __vr.cause } : {}),\n message: '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 " +
|
|
6252
|
+
"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" +
|
|
6253
|
+
"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"
|
|
6254
|
+
|
|
6092
6255
|
|
|
6093
6256
|
|
|
6094
6257
|
|
|
@@ -6174,9 +6337,14 @@ String((() => {
|
|
|
6174
6337
|
String(JSON.stringify(format)) +
|
|
6175
6338
|
") !== -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: " +
|
|
6176
6339
|
String(format) +
|
|
6177
|
-
" has no camera frame, so they do not stay fixed under the camera move. Export mp4, webm, gif or png to keep them fixed.' }]);\n }\n } catch (_) { /* the export stands */ }\n }\n //
|
|
6178
|
-
|
|
6179
|
-
"
|
|
6340
|
+
" has no camera frame, so they do not stay fixed under the camera move. Export mp4, webm, gif or png to keep them fixed.' }]);\n }\n } catch (_) { /* the export stands */ }\n }\n // A SOUND PAST THE END (SNDTAIL31): placed, accepted, and not in the file. The video's length decides, as it should; it is said.\n if (result && result.success && ['mp4', 'webm'].indexOf(" +
|
|
6341
|
+
String(JSON.stringify(format)) +
|
|
6342
|
+
") !== -1 && app.itemRegistry && typeof app.itemRegistry.getAll === 'function') {\n try {\n const __end = (result.video && result.video.durationS) || " +
|
|
6343
|
+
String(JSON.stringify(duration ?? null)) +
|
|
6344
|
+
";\n const __late = __end ? app.itemRegistry.getAll().filter(function (e) { const d = (e.item || e).data; return d && d.isSoundChip && typeof d.clipStartTime === 'number' && d.clipStartTime >= __end; }) : [];\n if (__late.length) {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'sound_after_end', message: __late.length + ' sound(s) start at or after the end of this ' + __end + ' s export, so they are not in the file: ' + __late.map(function (e) { return (e.item || e).data.registryId + ' at ' + (e.item || e).data.clipStartTime + ' s'; }).slice(0, 10).join(', ') + '.' }]);\n }" +
|
|
6345
|
+
"\n } catch (_) { /* the export stands */ }\n }\n // WHICH STUDIO MADE THIS FILE (D92): a service worker can serve a stale\n // build right after a deploy, and a retest then judges old code. The\n // engine's own identity, version + build sha, rides on every export reply.\n try { if (result && typeof result === 'object' && app.buildId) result.studioBuild = app.buildId; } catch (_) { /* older studio */ }\n // A STALE STUDIO (D92): the file was made by an old build. Said in the\n // fidelity warnings the reply shows, not only as a field.\n try {\n const __bs = app.buildStatus ? app.buildStatus() : null;\n if (result && typeof result === 'object' && __bs) {\n result.liveB" +
|
|
6346
|
+
"uild = __bs.liveBuildId;\n if (__bs.stale) {\n result.stale = true;\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'stale_build', message: 'this studio is running an old build (' + __bs.buildId + '; the live build is ' + __bs.liveBuildId + '): this file was made by the old code, so it may differ from what the live build makes. pinepaper_clear_canvas (or pinepaper_agent_start_job) loads the live build; it empties the canvas, so rebuild the scene and export again if the difference matters.' }]);\n }\n }\n } catch (_) { /* older studio */ }\n // RELIGHT (FxTool D5): appl" +
|
|
6347
|
+
"ied to png / pdf / mp4 / webm, not gif / apng\n // or the vector formats; and it is what makes a video export slow.\n if (result && result.success && typeof app.getRelight === 'function') {\n try {\n const __rl = app.getRelight();\n if (__rl && __rl.enabled) {\n result.fidelity = result.fidelity || { warnings: [] };\n if (['png', 'jpg', 'webp', 'pdf', 'mp4', 'webm'].indexOf(" +
|
|
6180
6348
|
String(JSON.stringify(format)) +
|
|
6181
6349
|
") === -1) {\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'relight_not_applied', message: 'a relight rig is set, but ' + " +
|
|
6182
6350
|
String(jsLiteral(format)) +
|
|
@@ -7221,6 +7389,13 @@ String(jsLiteral(format)) +
|
|
|
7221
7389
|
|
|
7222
7390
|
|
|
7223
7391
|
|
|
7392
|
+
|
|
7393
|
+
|
|
7394
|
+
|
|
7395
|
+
|
|
7396
|
+
|
|
7397
|
+
|
|
7398
|
+
|
|
7224
7399
|
|
|
7225
7400
|
|
|
7226
7401
|
|
|
@@ -8840,7 +9015,7 @@ String(validated.priority || 0) +
|
|
|
8840
9015
|
"\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
9016
|
String(jsLiteral(validated.name)) +
|
|
8842
9017
|
"] = " +
|
|
8843
|
-
String(jsLiteral({ name: validated.name, ...(validated.description ? { description: validated.description } : {}), params: validated.params ?? {}, computeFunction: validated.computeFunction, applyFunction: validated.applyFunction })) +
|
|
9018
|
+
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
9019
|
";\n return { success: true, relationName: " +
|
|
8845
9020
|
String(jsLiteral(validated.name)) +
|
|
8846
9021
|
", message: 'Custom relation registered successfully' };\n } catch (error) {\n return { success: false, error: error.message };\n }\n})();\n"
|
|
@@ -9232,7 +9407,9 @@ String(jsLiteral(templateId)) +
|
|
|
9232
9407
|
).trim();
|
|
9233
9408
|
}
|
|
9234
9409
|
generateImportImage(input) {
|
|
9235
|
-
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset, name } = input;
|
|
9410
|
+
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset, name, opacity, blendMode, scale, loop } = input;
|
|
9411
|
+
const look = { ...(opacity !== undefined ? { opacity } : {}), ...(blendMode !== undefined ? { blendMode } : {}), ...(scale !== undefined ? { scale } : {}) };
|
|
9412
|
+
const hasLook = Object.keys(look).length > 0;
|
|
9236
9413
|
// IMPORTSHADOW: the shadow keys, applied through the engine's own modify
|
|
9237
9414
|
// door once the image is placed (and masked) — the route modify_item takes.
|
|
9238
9415
|
const shadow = {};
|
|
@@ -9254,6 +9431,7 @@ String(jsLiteral(templateId)) +
|
|
|
9254
9431
|
maxWidth !== undefined ? `maxWidth: ${maxWidth}` : '',
|
|
9255
9432
|
maxHeight !== undefined ? `maxHeight: ${maxHeight}` : '',
|
|
9256
9433
|
nativeSize ? 'nativeSize: true' : '',
|
|
9434
|
+
loop !== undefined ? `loop: ${JSON.stringify(loop)}` : '',
|
|
9257
9435
|
].filter(Boolean);
|
|
9258
9436
|
const optsLiteral = optParts.length ? `{ ${optParts.join(', ')} }` : '{}';
|
|
9259
9437
|
return ("\n// Import image from URL\n(async function() {\n if (!app.imageTools) {\n return { error: 'Image tools not available. Make sure PinePaper Studio is loaded.' };\n }\n try {\n // FETCH IT OURSELVES FIRST, to find out WHY when it fails.\n //\n // uploadFromURL sets img.src and rejects with a bare \"Failed to load image\n // from URL\" — an <img> onerror genuinely carries no reason, so that message\n // is the browser's limit rather than the engine being unhelpful. fetch()\n // has the reason: a status code, a CORS rejection, a refused connection, a\n // DNS failure. A caller cannot debug what the tool will not show them.\n //\n // The bytes then go in as a DA" +
|
|
@@ -9285,6 +9463,16 @@ String(mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask b
|
|
|
9285
9463
|
if (app.itemRegistry && typeof app.itemRegistry.rebind === 'function') app.itemRegistry.rebind(itemId, masked);
|
|
9286
9464
|
placed = masked;
|
|
9287
9465
|
` : '') +
|
|
9466
|
+
String(hasLook ? ` // opacity, blendMode, scale: read by modify, on what is drawn. They were not import arguments (OPACIMP31, BLENDIMP31, IMGSCALE31).
|
|
9467
|
+
let __look = null;
|
|
9468
|
+
{
|
|
9469
|
+
let __ok = false;
|
|
9470
|
+
try { __ok = app.modifyItem(itemId, ${JSON.stringify(look)}) !== false; } catch (_) { __ok = false; }
|
|
9471
|
+
__look = __ok
|
|
9472
|
+
? { applied: true, opacity: placed.opacity, blendMode: placed.blendMode }
|
|
9473
|
+
: { applied: false, warning: 'the image was imported but ' + ${jsLiteral(Object.keys(look).join(', '))} + ' was not applied: set it with pinepaper_modify_item on ' + itemId + '.' };
|
|
9474
|
+
}
|
|
9475
|
+
` : '') +
|
|
9288
9476
|
String(hasShadow ? ` // The shadow goes on what is DRAWN, after a mask has replaced the raster.
|
|
9289
9477
|
let __shadow = null;
|
|
9290
9478
|
{
|
|
@@ -9307,6 +9495,8 @@ String(name ? `\n name: ${JSON.stringify(name)},` : '') +
|
|
|
9307
9495
|
"\n bounds: { x: b.x, y: b.y, width: b.width, height: b.height }" +
|
|
9308
9496
|
String(mask ? `,\n mask: ${jsLiteral(mask)}` : '') +
|
|
9309
9497
|
String(hasShadow ? ',\n shadow: __shadow' : '') +
|
|
9498
|
+
String(loop !== undefined ? ",\n playback: (raster && raster.data && raster.data.animatedImage && raster.data.animatedImage.loop !== undefined) ? { applied: true, plays: raster.data.animatedImage.loop } : { applied: false, warning: 'loop was not applied: this image does not animate in this studio. Animated GIF and WebP do.' }" : '') +
|
|
9499
|
+
String(hasLook ? ',\n look: __look' : '') +
|
|
9310
9500
|
",\n // Stored smaller than its source (the studio's cap): said, with the\n // way to keep every pixel. The studio reports native size where it can.\n ...((entry && entry.nativeWidth && entry.width && entry.nativeWidth > entry.width)\n ? { stored: { width: entry.width, height: entry.height }, native: { width: entry.nativeWidth, height: entry.nativeHeight },\n note: 'the studio stored this image at ' + entry.width + 'x' + entry.height + ' (its source is ' + entry.nativeWidth + 'x' + entry.nativeHeight + '). Pass maxEdge: \"native\" to keep every pixel.' }\n : {})\n };\n } catch (e) {\n return { error: 'Failed to import image: ' + e.message };\n " +
|
|
9311
9501
|
" }\n})();\n"
|
|
9312
9502
|
|
|
@@ -9398,6 +9588,12 @@ String(hasShadow ? ',\n shadow: __shadow' : '') +
|
|
|
9398
9588
|
|
|
9399
9589
|
|
|
9400
9590
|
|
|
9591
|
+
|
|
9592
|
+
|
|
9593
|
+
|
|
9594
|
+
|
|
9595
|
+
|
|
9596
|
+
|
|
9401
9597
|
|
|
9402
9598
|
|
|
9403
9599
|
|
|
@@ -12761,16 +12957,20 @@ String(diagJson) +
|
|
|
12761
12957
|
String(optsJson) +
|
|
12762
12958
|
");\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
12959
|
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
|
-
|
|
12960
|
+
") 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: " +
|
|
12961
|
+
String(RELATION_PUBLIC_PARAMS_JS) +
|
|
12962
|
+
"(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" +
|
|
12963
|
+
"wn */ }\n const __allCustom = (typeof window !== 'undefined' && window.__ppCustomRelations) ? Object.values(window.__ppCustomRelations) : [];\n const __strip = (d) => { if (" +
|
|
12766
12964
|
String(input.withSource === true ? 'true' : 'false') +
|
|
12767
12965
|
") 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
|
-
"
|
|
12966
|
+
"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" +
|
|
12967
|
+
"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" +
|
|
12968
|
+
"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" +
|
|
12969
|
+
"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(" +
|
|
12970
|
+
String(input.withSource === true ? "{ name: d.name, change: { tool: 'pinepaper_register_custom_relation', args } }" : "args") +
|
|
12971
|
+
");\n });\n } catch (e) { /* the read-back stands without the edit ops */ }\n if (!" +
|
|
12772
12972
|
String(input.withSource === true ? 'true' : 'false') +
|
|
12773
|
-
" && __custom.length) __edit.customHint = 'custom
|
|
12973
|
+
" && __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
12974
|
String(input.only === 'edit' ? 'true' : 'false') +
|
|
12775
12975
|
") {\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
12976
|
", 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 +13036,7 @@ String(input.only === 'edit' ? 'true' : 'false') +
|
|
|
12836
13036
|
|
|
12837
13037
|
|
|
12838
13038
|
|
|
13039
|
+
|
|
12839
13040
|
|
|
12840
13041
|
|
|
12841
13042
|
).trim();
|
|
@@ -13385,7 +13586,7 @@ String(id) +
|
|
|
13385
13586
|
String(id) +
|
|
13386
13587
|
", beats: " +
|
|
13387
13588
|
String(input.beats.length) +
|
|
13388
|
-
", keyframes: __r.keyframes.length, duration: __r.end, final: __r.final };\n" +
|
|
13589
|
+
", keyframes: __r.keyframes.length, duration: __r.end, final: __r.final, ...(__r.defaultsUsed ? { defaultsUsed: __r.defaultsUsed } : {}) };\n" +
|
|
13389
13590
|
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
13591
|
String(S(typeof input.sound === 'object' ? Object.fromEntries(Object.entries(input.sound).filter(([k]) => k !== 'gain')) : {})) +
|
|
13391
13592
|
");\n const __gain = " +
|
|
@@ -16094,6 +16295,8 @@ String(op) +
|
|
|
16094
16295
|
return { success: true, action: 'create', eventId${input.at !== undefined ? `, at: ${JSON.stringify(input.at)}` : ''} };
|
|
16095
16296
|
})();`.trim();
|
|
16096
16297
|
}
|
|
16298
|
+
if (input.action !== 'pulse')
|
|
16299
|
+
return generateContraptionCode(input);
|
|
16097
16300
|
// pulse — a runtime fire, not a scene edit (no history save)
|
|
16098
16301
|
const eventIdJson = JSON.stringify(input.eventId);
|
|
16099
16302
|
const payloadJson = JSON.stringify(input.payload ?? null);
|
|
@@ -16108,6 +16311,78 @@ String(op) +
|
|
|
16108
16311
|
})();`.trim();
|
|
16109
16312
|
}
|
|
16110
16313
|
}
|
|
16314
|
+
/**
|
|
16315
|
+
* Rules, variables and bound text (the studio's contraption engine). Each action asks for the method it calls, so a
|
|
16316
|
+
* studio without it refuses by name instead of a TypeError. The engine's custom action is not offered: it needs a
|
|
16317
|
+
* function, which JSON cannot carry.
|
|
16318
|
+
*/
|
|
16319
|
+
function generateContraptionCode(input) {
|
|
16320
|
+
const refuse = (method) => `if (typeof app.${method} !== 'function') {
|
|
16321
|
+
return { success: false, action: ${jsLiteral(input.action)}, error: 'this studio has no contraption engine (app.${method}): update PinePaper Studio' };
|
|
16322
|
+
}`;
|
|
16323
|
+
const scope = jsLiteral(input.scope ?? 'default');
|
|
16324
|
+
const key = jsLiteral(input.key ?? '');
|
|
16325
|
+
const body = () => {
|
|
16326
|
+
switch (input.action) {
|
|
16327
|
+
case 'add_contraption': {
|
|
16328
|
+
const targets = [...new Set((input.actions ?? []).flatMap((a) => ('targetId' in a ? [a.targetId] : [])))];
|
|
16329
|
+
const config = {
|
|
16330
|
+
...(input.contraptionId ? { id: input.contraptionId } : {}),
|
|
16331
|
+
sourceEvent: input.sourceEvent,
|
|
16332
|
+
actions: input.actions,
|
|
16333
|
+
...(input.condition ? { condition: input.condition } : {}),
|
|
16334
|
+
...(input.thenTrigger ? { thenTrigger: input.thenTrigger } : {}),
|
|
16335
|
+
...(input.elseTrigger ? { elseTrigger: input.elseTrigger } : {}),
|
|
16336
|
+
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
16337
|
+
};
|
|
16338
|
+
return `${refuse('addContraption')}
|
|
16339
|
+
const contraptionId = app.addContraption(${jsLiteral(config)});
|
|
16340
|
+
const missing = ${jsLiteral(targets)}.filter((id) => !app.getItemById(id));
|
|
16341
|
+
return { success: true, action: 'add_contraption', contraptionId,
|
|
16342
|
+
...(missing.length ? { warning: 'no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does' } : {}), };`;
|
|
16343
|
+
}
|
|
16344
|
+
case 'remove_contraption':
|
|
16345
|
+
return `${refuse('removeContraption')}
|
|
16346
|
+
const removed = app.removeContraption(${jsLiteral(input.contraptionId)});
|
|
16347
|
+
return removed ? { success: true, action: 'remove_contraption', contraptionId: ${jsLiteral(input.contraptionId)} }
|
|
16348
|
+
: { success: false, action: 'remove_contraption', error: 'no rule has the id ' + ${jsLiteral(input.contraptionId)} };`;
|
|
16349
|
+
case 'clear_contraptions':
|
|
16350
|
+
return `${refuse('clearContraptions')}
|
|
16351
|
+
app.clearContraptions();
|
|
16352
|
+
return { success: true, action: 'clear_contraptions' };`;
|
|
16353
|
+
case 'clear_variables':
|
|
16354
|
+
return `${refuse('clearVariables')}
|
|
16355
|
+
return { success: true, action: 'clear_variables', scope: ${scope}, removed: app.clearVariables(${scope}) };`;
|
|
16356
|
+
case 'list_contraptions':
|
|
16357
|
+
return `${refuse('listContraptions')}
|
|
16358
|
+
return { success: true, action: 'list_contraptions', contraptions: app.listContraptions() };`;
|
|
16359
|
+
case 'set_variable':
|
|
16360
|
+
return `${refuse('setVariable')}
|
|
16361
|
+
app.setVariable(${key}, ${jsLiteral(input.value)}, ${scope});
|
|
16362
|
+
return { success: true, action: 'set_variable', key: ${key}, value: app.getVariable(${key}, null, ${scope}) };`;
|
|
16363
|
+
case 'get_variable':
|
|
16364
|
+
return `${refuse('getVariable')}
|
|
16365
|
+
return { success: true, action: 'get_variable', key: ${key}, value: app.getVariable(${key}, ${jsLiteral(input.fallback ?? null)}, ${scope}) };`;
|
|
16366
|
+
case 'increment_variable':
|
|
16367
|
+
return `${refuse('incrementVariable')}
|
|
16368
|
+
return { success: true, action: 'increment_variable', key: ${key}, value: app.incrementVariable(${key}, ${input.by ?? 1}, ${scope}) };`;
|
|
16369
|
+
default: {
|
|
16370
|
+
const itemId = jsLiteral(input.itemId);
|
|
16371
|
+
return `${refuse('bindTextToState')}
|
|
16372
|
+
const item = app.getItemById(${itemId});
|
|
16373
|
+
if (!item) return { success: false, action: 'bind_text', error: 'Item not found: ' + ${itemId} };
|
|
16374
|
+
if (item.content === undefined) return { success: false, action: 'bind_text', error: ${itemId} + ' is not a text item, so it has no text to bind' };
|
|
16375
|
+
app.bindTextToState(${itemId}, ${jsLiteral(input.template)}, ${scope});
|
|
16376
|
+
return { success: true, action: 'bind_text', itemId: ${itemId}, text: item.content };`;
|
|
16377
|
+
}
|
|
16378
|
+
}
|
|
16379
|
+
};
|
|
16380
|
+
return `
|
|
16381
|
+
// Contraption engine: ${input.action}
|
|
16382
|
+
(function() {
|
|
16383
|
+
${body()}
|
|
16384
|
+
})();`.trim();
|
|
16385
|
+
}
|
|
16111
16386
|
// Export singleton instance
|
|
16112
16387
|
export const codeGenerator = new PinePaperCodeGenerator();
|
|
16113
16388
|
/**
|