@pinepaper.studio/mcp-server 1.6.32 → 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 +53 -0
- package/dist/agent/types.d.ts +3 -3
- package/dist/build-info.json +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +51 -38
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +5 -3
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +11 -3
- package/dist/tools/engine-lists.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 +10 -4
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +59 -11
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.js +2 -2
- package/dist/tools/lane.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 +4 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +113 -34
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +31 -6
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +11 -3
- package/dist/types/schemas.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';
|
|
@@ -1483,10 +1483,10 @@ function customOptionWarningJs(relationType, params, key, injected = []) {
|
|
|
1483
1483
|
return `, ...(function () { try {
|
|
1484
1484
|
var cr = typeof window !== 'undefined' && window.__ppCustomRelations && window.__ppCustomRelations[${jsLiteral(relationType)}];
|
|
1485
1485
|
if (!cr || !Array.isArray(cr.reads)) return {};
|
|
1486
|
-
var skip = ${JSON.stringify(injected)};
|
|
1486
|
+
var skip = ${JSON.stringify([...injected, ...RELATION_LOOP_PARAMS])};
|
|
1487
1487
|
var un = ${JSON.stringify(keys)}.filter(function (k) { return cr.reads.indexOf(k) < 0 && skip.indexOf(k) < 0; });
|
|
1488
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(', ') + '.' };
|
|
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
1490
|
} catch (e) { return {}; } })()`;
|
|
1491
1491
|
}
|
|
1492
1492
|
export function relationOptionWarning(relationType, params, injected = []) {
|
|
@@ -1508,10 +1508,25 @@ function equationFlipNote(relationType, params) {
|
|
|
1508
1508
|
return null;
|
|
1509
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.';
|
|
1510
1510
|
}
|
|
1511
|
-
|
|
1511
|
+
/** What a built-in relation reads: its rule's params, and what the relation loop reads on every relation. */
|
|
1512
|
+
function relationReads(relationType) {
|
|
1512
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);
|
|
1513
1518
|
return keys ? unreadKeysWarning(String(relationType), keys, params, injected) : null;
|
|
1514
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
|
+
}
|
|
1515
1530
|
/** The keys of `params` outside `keys`, named with a "did you mean" and the list that is read; null when all are read. */
|
|
1516
1531
|
function unreadKeysWarning(reader, keys, params, injected = []) {
|
|
1517
1532
|
if (!params || typeof params !== 'object')
|
|
@@ -1568,7 +1583,7 @@ String(jsLiteral(targetId)) +
|
|
|
1568
1583
|
", " +
|
|
1569
1584
|
String(jsLiteral(relationType)) +
|
|
1570
1585
|
", " +
|
|
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((
|
|
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();
|
|
1572
1587
|
}
|
|
1573
1588
|
/**
|
|
1574
1589
|
* The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
|
|
@@ -1644,9 +1659,18 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1644
1659
|
for (const k of ['velocities', 'offsets'])
|
|
1645
1660
|
if (out[k] !== undefined)
|
|
1646
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];
|
|
1647
1666
|
return out;
|
|
1648
1667
|
};
|
|
1649
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
|
+
}
|
|
1650
1674
|
if (Array.isArray(spec.bursts))
|
|
1651
1675
|
out.bursts = spec.bursts.map((b) => (b && typeof b === 'object' ? fix(b) : b));
|
|
1652
1676
|
// The studio's default count per burst (ParticleEmitter DEFAULT_LOOK).
|
|
@@ -2072,6 +2096,24 @@ function unanimatedTextKeysJs(keys) {
|
|
|
2072
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; });
|
|
2073
2097
|
})()`;
|
|
2074
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.'`;
|
|
2075
2117
|
/** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
|
|
2076
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.'`;
|
|
2077
2119
|
/**
|
|
@@ -2097,12 +2139,6 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
|
|
|
2097
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.`)}`
|
|
2098
2140
|
: '';
|
|
2099
2141
|
const first = [...keyframes].sort((a, b) => a.time - b.time)[0];
|
|
2100
|
-
// loop:true repeats in the editor; an export of the measured build played
|
|
2101
|
-
// the keyframes ONCE and held (round 7, 2.24). Said where it is set, with
|
|
2102
|
-
// the two ways that do export, until the engine's export clock wraps them.
|
|
2103
|
-
const loopNote = loop
|
|
2104
|
-
? `, 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.')}`
|
|
2105
|
-
: '';
|
|
2106
2142
|
// SIZEKEY-PAIR (mcp-cloud #28): on a studio before FxTool #262, x (or
|
|
2107
2143
|
// position) keyed beside fontSize on one text item drifted in an export: the
|
|
2108
2144
|
// size was applied after the position, and a text resizes around its anchor.
|
|
@@ -2114,7 +2150,7 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
|
|
|
2114
2150
|
const easingNote = easingAt !== 'departure' && first?.easing && first.easing !== 'linear'
|
|
2115
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).`)}`
|
|
2116
2152
|
: '';
|
|
2117
|
-
return `${easingNote}${sizekeyPairNote}${
|
|
2153
|
+
return `${easingNote}${sizekeyPairNote}${gapNote}`;
|
|
2118
2154
|
}
|
|
2119
2155
|
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
|
|
2120
2156
|
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
@@ -2172,9 +2208,12 @@ String(JSON.stringify(levelKeys)) +
|
|
|
2172
2208
|
const textKeyCheck = textKeysJs ? `
|
|
2173
2209
|
const __ignored = ${textKeysJs};
|
|
2174
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});`;
|
|
2175
2214
|
const easingAtCheck = easingAt !== undefined ? `
|
|
2176
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.');` : '';
|
|
2177
|
-
const runtimeWarning =
|
|
2216
|
+
const runtimeWarning = `, ...(${textKeysJs ? '__ignored.length || ' : ''}__filterIgnored.length ? { ignoredProperties: ${textKeysJs ? '__ignored.concat(__filterIgnored)' : '__filterIgnored'} } : {}), ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
|
|
2178
2217
|
const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `${ID}`;
|
|
2179
2218
|
return ("\n// Apply keyframe animation to " +
|
|
2180
2219
|
String(ID) +
|
|
@@ -2190,6 +2229,7 @@ String(keyframesJson) +
|
|
|
2190
2229
|
", __opts);\n const __warn = [];" +
|
|
2191
2230
|
String(easingAtCheck) +
|
|
2192
2231
|
String(textKeyCheck) +
|
|
2232
|
+
String(filterKeyCheck) +
|
|
2193
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; });" +
|
|
2194
2234
|
String(append ? `
|
|
2195
2235
|
// Property gaps across the merged track (the same check a single call makes
|
|
@@ -2209,8 +2249,7 @@ String(calculatedDuration) +
|
|
|
2209
2249
|
", loop: " +
|
|
2210
2250
|
String(loop) +
|
|
2211
2251
|
String(append ? ', append: true' : '') +
|
|
2212
|
-
", ...(__track ? { track: __track } : {})" +
|
|
2213
|
-
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();
|
|
2214
2253
|
}
|
|
2215
2254
|
/**
|
|
2216
2255
|
* Template for executing generators
|
|
@@ -3554,7 +3593,7 @@ String(JSON.stringify(validated.presetId)) +
|
|
|
3554
3593
|
const code = stepNoTarget
|
|
3555
3594
|
? `(function() {
|
|
3556
3595
|
if (!${STEP_ACTIVE_TARGETLESS_JS}) return { success: false, relationType: 'on_event_step_active', error: ${jsLiteral(stepNoTarget)} };
|
|
3557
|
-
|
|
3596
|
+
${asReturningBody(built)}
|
|
3558
3597
|
})();`
|
|
3559
3598
|
: built;
|
|
3560
3599
|
// NO TARGET (event reactions like on_event_play_sound, self-relations): the
|
|
@@ -4234,7 +4273,7 @@ if (typeof _gen === 'function') {
|
|
|
4234
4273
|
})();`);
|
|
4235
4274
|
}
|
|
4236
4275
|
// Establish relations
|
|
4237
|
-
for (const relation of relations) {
|
|
4276
|
+
for (const [relationIndex, relation] of relations.entries()) {
|
|
4238
4277
|
const { source, target, type, params = {} } = relation;
|
|
4239
4278
|
const paramsStr = JSON.stringify(params);
|
|
4240
4279
|
const specProblem = relationSpecProblem(type, params, target, 'no-page');
|
|
@@ -4264,7 +4303,10 @@ String(jsLiteral(type)) +
|
|
|
4264
4303
|
String(jsLiteral(source)) +
|
|
4265
4304
|
", target: " +
|
|
4266
4305
|
String(jsLiteral(target)) +
|
|
4267
|
-
", 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"));
|
|
4268
4310
|
}
|
|
4269
4311
|
// Apply animations
|
|
4270
4312
|
for (const animation of animations) {
|
|
@@ -5234,7 +5276,7 @@ const targetId = ${animItemRef};
|
|
|
5234
5276
|
const item = app.getItemById(targetId);
|
|
5235
5277
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
5236
5278
|
app.animate(item, { animationType: ${JSON.stringify(String(op.animationType))}, ...${animOpts} });
|
|
5237
|
-
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)}` : ''; })()} };
|
|
5238
5280
|
`;
|
|
5239
5281
|
case 'relation':
|
|
5240
5282
|
const srcRef = batchRef(op.sourceId);
|
|
@@ -5295,7 +5337,7 @@ String(type) +
|
|
|
5295
5337
|
", sourceIds: __failed, error: " +
|
|
5296
5338
|
String(op.update === true ? '__why' : `'the studio refused the ' + ${type} + ' relation for ' + __failed.join(', ') + ' — nothing was bound' + (__why ? ': ' + __why : '.')`) +
|
|
5297
5339
|
" };\n}\nreturn { success: true, relationType: " +
|
|
5298
|
-
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");
|
|
5299
5341
|
}
|
|
5300
5342
|
return ("\n" +
|
|
5301
5343
|
String(stepGuard) +
|
|
@@ -5314,7 +5356,7 @@ String(relOpts) +
|
|
|
5314
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 = " +
|
|
5315
5357
|
String(relationRefusalReasonJs(jsLiteral(op.relationType))) +
|
|
5316
5358
|
";\n return { success: false, relationId: false, sourceId, targetId, relationType: " +
|
|
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((
|
|
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");
|
|
5318
5360
|
case 'delete':
|
|
5319
5361
|
const delRef = batchRef(op.itemId);
|
|
5320
5362
|
return `
|
|
@@ -5402,8 +5444,9 @@ return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget}
|
|
|
5402
5444
|
kfExtra.push(`timeUnits: ${jsLiteral(kfUnits)}`);
|
|
5403
5445
|
const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
|
|
5404
5446
|
const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
|
|
5405
|
-
const
|
|
5406
|
-
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(' ') } : {})`;
|
|
5407
5450
|
return ("\nconst targetId = " +
|
|
5408
5451
|
String(kfItemRef) +
|
|
5409
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, " +
|
|
@@ -6294,9 +6337,14 @@ String((() => {
|
|
|
6294
6337
|
String(JSON.stringify(format)) +
|
|
6295
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: " +
|
|
6296
6339
|
String(format) +
|
|
6297
|
-
" 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 //
|
|
6298
|
-
|
|
6299
|
-
"
|
|
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(" +
|
|
6300
6348
|
String(JSON.stringify(format)) +
|
|
6301
6349
|
") === -1) {\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'relight_not_applied', message: 'a relight rig is set, but ' + " +
|
|
6302
6350
|
String(jsLiteral(format)) +
|
|
@@ -7343,6 +7391,12 @@ String(jsLiteral(format)) +
|
|
|
7343
7391
|
|
|
7344
7392
|
|
|
7345
7393
|
|
|
7394
|
+
|
|
7395
|
+
|
|
7396
|
+
|
|
7397
|
+
|
|
7398
|
+
|
|
7399
|
+
|
|
7346
7400
|
|
|
7347
7401
|
|
|
7348
7402
|
|
|
@@ -9353,7 +9407,9 @@ String(jsLiteral(templateId)) +
|
|
|
9353
9407
|
).trim();
|
|
9354
9408
|
}
|
|
9355
9409
|
generateImportImage(input) {
|
|
9356
|
-
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;
|
|
9357
9413
|
// IMPORTSHADOW: the shadow keys, applied through the engine's own modify
|
|
9358
9414
|
// door once the image is placed (and masked) — the route modify_item takes.
|
|
9359
9415
|
const shadow = {};
|
|
@@ -9375,6 +9431,7 @@ String(jsLiteral(templateId)) +
|
|
|
9375
9431
|
maxWidth !== undefined ? `maxWidth: ${maxWidth}` : '',
|
|
9376
9432
|
maxHeight !== undefined ? `maxHeight: ${maxHeight}` : '',
|
|
9377
9433
|
nativeSize ? 'nativeSize: true' : '',
|
|
9434
|
+
loop !== undefined ? `loop: ${JSON.stringify(loop)}` : '',
|
|
9378
9435
|
].filter(Boolean);
|
|
9379
9436
|
const optsLiteral = optParts.length ? `{ ${optParts.join(', ')} }` : '{}';
|
|
9380
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" +
|
|
@@ -9406,6 +9463,16 @@ String(mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask b
|
|
|
9406
9463
|
if (app.itemRegistry && typeof app.itemRegistry.rebind === 'function') app.itemRegistry.rebind(itemId, masked);
|
|
9407
9464
|
placed = masked;
|
|
9408
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
|
+
` : '') +
|
|
9409
9476
|
String(hasShadow ? ` // The shadow goes on what is DRAWN, after a mask has replaced the raster.
|
|
9410
9477
|
let __shadow = null;
|
|
9411
9478
|
{
|
|
@@ -9428,6 +9495,8 @@ String(name ? `\n name: ${JSON.stringify(name)},` : '') +
|
|
|
9428
9495
|
"\n bounds: { x: b.x, y: b.y, width: b.width, height: b.height }" +
|
|
9429
9496
|
String(mask ? `,\n mask: ${jsLiteral(mask)}` : '') +
|
|
9430
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' : '') +
|
|
9431
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 " +
|
|
9432
9501
|
" }\n})();\n"
|
|
9433
9502
|
|
|
@@ -9519,6 +9588,12 @@ String(hasShadow ? ',\n shadow: __shadow' : '') +
|
|
|
9519
9588
|
|
|
9520
9589
|
|
|
9521
9590
|
|
|
9591
|
+
|
|
9592
|
+
|
|
9593
|
+
|
|
9594
|
+
|
|
9595
|
+
|
|
9596
|
+
|
|
9522
9597
|
|
|
9523
9598
|
|
|
9524
9599
|
|
|
@@ -16238,10 +16313,8 @@ String(op) +
|
|
|
16238
16313
|
}
|
|
16239
16314
|
/**
|
|
16240
16315
|
* 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
|
|
16242
|
-
*
|
|
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.
|
|
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.
|
|
16245
16318
|
*/
|
|
16246
16319
|
function generateContraptionCode(input) {
|
|
16247
16320
|
const refuse = (method) => `if (typeof app.${method} !== 'function') {
|
|
@@ -16252,7 +16325,7 @@ function generateContraptionCode(input) {
|
|
|
16252
16325
|
const body = () => {
|
|
16253
16326
|
switch (input.action) {
|
|
16254
16327
|
case 'add_contraption': {
|
|
16255
|
-
const targets = [...new Set((input.actions ?? []).flatMap((a) => (
|
|
16328
|
+
const targets = [...new Set((input.actions ?? []).flatMap((a) => ('targetId' in a ? [a.targetId] : [])))];
|
|
16256
16329
|
const config = {
|
|
16257
16330
|
...(input.contraptionId ? { id: input.contraptionId } : {}),
|
|
16258
16331
|
sourceEvent: input.sourceEvent,
|
|
@@ -16266,14 +16339,20 @@ function generateContraptionCode(input) {
|
|
|
16266
16339
|
const contraptionId = app.addContraption(${jsLiteral(config)});
|
|
16267
16340
|
const missing = ${jsLiteral(targets)}.filter((id) => !app.getItemById(id));
|
|
16268
16341
|
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'" : ''} };`;
|
|
16342
|
+
...(missing.length ? { warning: 'no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does' } : {}), };`;
|
|
16271
16343
|
}
|
|
16272
16344
|
case 'remove_contraption':
|
|
16273
16345
|
return `${refuse('removeContraption')}
|
|
16274
16346
|
const removed = app.removeContraption(${jsLiteral(input.contraptionId)});
|
|
16275
16347
|
return removed ? { success: true, action: 'remove_contraption', contraptionId: ${jsLiteral(input.contraptionId)} }
|
|
16276
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}) };`;
|
|
16277
16356
|
case 'list_contraptions':
|
|
16278
16357
|
return `${refuse('listContraptions')}
|
|
16279
16358
|
return { success: true, action: 'list_contraptions', contraptions: app.listContraptions() };`;
|