@pinepaper.studio/mcp-server 1.6.32 → 1.6.34
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 +115 -1
- 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 +6 -5
- package/dist/index.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/tools/batch-macros.d.ts +20 -1
- package/dist/tools/batch-macros.d.ts.map +1 -1
- package/dist/tools/batch-macros.js +146 -7
- package/dist/tools/batch-macros.js.map +1 -1
- package/dist/tools/compact-descriptions.js +1 -1
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +82 -55
- 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 +11 -5
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +71 -16
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +8 -1
- package/dist/tools/lane.d.ts.map +1 -1
- package/dist/tools/lane.js +56 -15
- 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 +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/tools/toolkits.d.ts +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +7 -2
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +142 -50
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +40 -6
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +20 -7
- 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';
|
|
@@ -124,18 +124,29 @@ ${itemVarName}.${propertyName} = (function() {
|
|
|
124
124
|
};
|
|
125
125
|
})();`;
|
|
126
126
|
}
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
// A LINEAR RAMP FITS THE ITEM, top to bottom, as the engine's own app.create does (FIREFLY31). This was a fixed
|
|
128
|
+
// 100 px diagonal at the requested position. Leaving the ends out is not enough here: this assigns straight to
|
|
129
|
+
// Paper, where a gradient with no ends is zero-length. An end the caller gave is kept as given.
|
|
130
|
+
const fx = position.x, fy = position.y;
|
|
131
|
+
const o = origin ? `[${origin[0]}, ${origin[1]}]`
|
|
132
|
+
: destination ? `[${destination[0]}, __gb ? __gb.y : ${fy - 50}]`
|
|
133
|
+
: `(__gb ? [__gb.x + __gb.width / 2, __gb.y] : [${fx}, ${fy - 50}])`;
|
|
134
|
+
const d = destination ? `[${destination[0]}, ${destination[1]}]`
|
|
135
|
+
: origin ? `[${origin[0]}, __gb ? __gb.y + __gb.height : ${fy + 50}]`
|
|
136
|
+
: `(__gb ? [__gb.x + __gb.width / 2, __gb.y + __gb.height] : [${fx}, ${fy + 50}])`;
|
|
130
137
|
return `
|
|
131
|
-
${itemVarName}.${propertyName} = {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
138
|
+
${itemVarName}.${propertyName} = (function() {
|
|
139
|
+
const __b = ${itemVarName} && ${itemVarName}.bounds;
|
|
140
|
+
const __gb = __b && isFinite(__b.x) && isFinite(__b.y) && __b.width > 0 && __b.height > 0 ? __b : null;
|
|
141
|
+
return {
|
|
142
|
+
gradient: {
|
|
143
|
+
stops: [${stopsCode}],
|
|
144
|
+
radial: false
|
|
145
|
+
},
|
|
146
|
+
origin: ${o},
|
|
147
|
+
destination: ${d}
|
|
148
|
+
};
|
|
149
|
+
})();`;
|
|
139
150
|
}
|
|
140
151
|
/**
|
|
141
152
|
* Generate shadow code for an item
|
|
@@ -1483,10 +1494,10 @@ function customOptionWarningJs(relationType, params, key, injected = []) {
|
|
|
1483
1494
|
return `, ...(function () { try {
|
|
1484
1495
|
var cr = typeof window !== 'undefined' && window.__ppCustomRelations && window.__ppCustomRelations[${jsLiteral(relationType)}];
|
|
1485
1496
|
if (!cr || !Array.isArray(cr.reads)) return {};
|
|
1486
|
-
var skip = ${JSON.stringify(injected)};
|
|
1497
|
+
var skip = ${JSON.stringify([...injected, ...RELATION_LOOP_PARAMS])};
|
|
1487
1498
|
var un = ${JSON.stringify(keys)}.filter(function (k) { return cr.reads.indexOf(k) < 0 && skip.indexOf(k) < 0; });
|
|
1488
1499
|
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(', ') + '.' };
|
|
1500
|
+
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
1501
|
} catch (e) { return {}; } })()`;
|
|
1491
1502
|
}
|
|
1492
1503
|
export function relationOptionWarning(relationType, params, injected = []) {
|
|
@@ -1508,10 +1519,25 @@ function equationFlipNote(relationType, params) {
|
|
|
1508
1519
|
return null;
|
|
1509
1520
|
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
1521
|
}
|
|
1511
|
-
|
|
1522
|
+
/** What a built-in relation reads: its rule's params, and what the relation loop reads on every relation. */
|
|
1523
|
+
function relationReads(relationType) {
|
|
1512
1524
|
const keys = typeof relationType === 'string' ? RELATION_PARAM_KEYS[relationType] : undefined;
|
|
1525
|
+
return keys ? [...keys, ...RELATION_LOOP_PARAMS] : undefined;
|
|
1526
|
+
}
|
|
1527
|
+
function unreadOptionWarning(relationType, params, injected) {
|
|
1528
|
+
const keys = relationReads(relationType);
|
|
1513
1529
|
return keys ? unreadKeysWarning(String(relationType), keys, params, injected) : null;
|
|
1514
1530
|
}
|
|
1531
|
+
const unreadKeys = (keys, params, injected = []) => keys && params && typeof params === 'object' ? Object.keys(params).filter((k) => !keys.includes(k) && !injected.includes(k)) : [];
|
|
1532
|
+
/**
|
|
1533
|
+
* A relation reply's fields for its options: the unread keys as data (ignoredOptions, which the reply's
|
|
1534
|
+
* ignoredArguments is built from) beside the sentence. Built-ins are known here, a custom relation only in the page.
|
|
1535
|
+
*/
|
|
1536
|
+
function relationOptionFields(relationType, params, key, injected = []) {
|
|
1537
|
+
const unread = unreadKeys(relationReads(relationType), params, injected);
|
|
1538
|
+
const warning = relationOptionWarning(relationType, params, injected);
|
|
1539
|
+
return `${unread.length ? `, ignoredOptions: ${jsLiteral(unread)}` : ''}${warning ? `, ${key}: ${jsLiteral(warning)}` : ''}${customOptionWarningJs(relationType, params, key, injected)}`;
|
|
1540
|
+
}
|
|
1515
1541
|
/** The keys of `params` outside `keys`, named with a "did you mean" and the list that is read; null when all are read. */
|
|
1516
1542
|
function unreadKeysWarning(reader, keys, params, injected = []) {
|
|
1517
1543
|
if (!params || typeof params !== 'object')
|
|
@@ -1568,7 +1594,7 @@ String(jsLiteral(targetId)) +
|
|
|
1568
1594
|
", " +
|
|
1569
1595
|
String(jsLiteral(relationType)) +
|
|
1570
1596
|
", " +
|
|
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((
|
|
1597
|
+
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
1598
|
}
|
|
1573
1599
|
/**
|
|
1574
1600
|
* The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
|
|
@@ -1644,9 +1670,18 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1644
1670
|
for (const k of ['velocities', 'offsets'])
|
|
1645
1671
|
if (out[k] !== undefined)
|
|
1646
1672
|
out[k] = pairs(out[k]);
|
|
1673
|
+
// A trail's single offset is one pair; the studio reads [x, y] and dropped {x, y} with nothing said (TRAILOFF31).
|
|
1674
|
+
const off = out.offset;
|
|
1675
|
+
if (off && typeof off === 'object' && !Array.isArray(off) && typeof off.x === 'number' && typeof off.y === 'number')
|
|
1676
|
+
out.offset = [off.x, off.y];
|
|
1647
1677
|
return out;
|
|
1648
1678
|
};
|
|
1649
1679
|
const out = fix(spec);
|
|
1680
|
+
// rate, start and end drive a trail. Beside bursts they drew nothing and said nothing (EMITCONT31).
|
|
1681
|
+
const trailOnly = ['rate', 'start', 'end'].filter((k) => spec[k] !== undefined);
|
|
1682
|
+
if (trailOnly.length && Array.isArray(spec.bursts) && spec.mode !== 'trail' && spec.target === undefined) {
|
|
1683
|
+
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.`);
|
|
1684
|
+
}
|
|
1650
1685
|
if (Array.isArray(spec.bursts))
|
|
1651
1686
|
out.bursts = spec.bursts.map((b) => (b && typeof b === 'object' ? fix(b) : b));
|
|
1652
1687
|
// The studio's default count per burst (ParticleEmitter DEFAULT_LOOK).
|
|
@@ -2072,6 +2107,24 @@ function unanimatedTextKeysJs(keys) {
|
|
|
2072
2107
|
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
2108
|
})()`;
|
|
2074
2109
|
}
|
|
2110
|
+
/**
|
|
2111
|
+
* An image filter's parameter as a key: keyframe_animate stores it, and a filter does not read a track, so the item
|
|
2112
|
+
* never changes with success:true (FILTKEY31). Asked of the same list, so a studio that starts animating one is believed.
|
|
2113
|
+
*/
|
|
2114
|
+
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)));
|
|
2115
|
+
function unanimatedFilterKeysJs(keys) {
|
|
2116
|
+
const filterKeys = [...new Set(keys)].filter((k) => FILTER_PARAM_KEYS.has(k));
|
|
2117
|
+
if (!filterKeys.length)
|
|
2118
|
+
return '[]';
|
|
2119
|
+
return `(function() {
|
|
2120
|
+
let __l = null;
|
|
2121
|
+
try { __l = typeof app.listAnimatableProperties === 'function' ? app.listAnimatableProperties() : null; } catch (_) { __l = null; }
|
|
2122
|
+
if (!__l || !__l.properties) return [];
|
|
2123
|
+
const __names = __l.properties.map(function(p) { return p && p.name; });
|
|
2124
|
+
return ${JSON.stringify(filterKeys)}.filter(function(k) { return __names.indexOf(k) < 0; });
|
|
2125
|
+
})()`;
|
|
2126
|
+
}
|
|
2127
|
+
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
2128
|
/** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
|
|
2076
2129
|
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
2130
|
/**
|
|
@@ -2097,12 +2150,6 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
|
|
|
2097
2150
|
? `, 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
2151
|
: '';
|
|
2099
2152
|
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
2153
|
// SIZEKEY-PAIR (mcp-cloud #28): on a studio before FxTool #262, x (or
|
|
2107
2154
|
// position) keyed beside fontSize on one text item drifted in an export: the
|
|
2108
2155
|
// size was applied after the position, and a text resizes around its anchor.
|
|
@@ -2114,7 +2161,7 @@ function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt
|
|
|
2114
2161
|
const easingNote = easingAt !== 'departure' && first?.easing && first.easing !== 'linear'
|
|
2115
2162
|
? `, 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
2163
|
: '';
|
|
2117
|
-
return `${easingNote}${sizekeyPairNote}${
|
|
2164
|
+
return `${easingNote}${sizekeyPairNote}${gapNote}`;
|
|
2118
2165
|
}
|
|
2119
2166
|
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
|
|
2120
2167
|
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
@@ -2172,9 +2219,12 @@ String(JSON.stringify(levelKeys)) +
|
|
|
2172
2219
|
const textKeyCheck = textKeysJs ? `
|
|
2173
2220
|
const __ignored = ${textKeysJs};
|
|
2174
2221
|
if (__ignored.length) __warn.push(__ignored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE});` : '';
|
|
2222
|
+
const filterKeyCheck = `
|
|
2223
|
+
const __filterIgnored = ${unanimatedFilterKeysJs(keyframes.flatMap((k) => Object.keys(k.properties ?? {})))};
|
|
2224
|
+
if (__filterIgnored.length) __warn.push(__filterIgnored.join(', ') + ${UNANIMATED_FILTER_KEYS_SENTENCE});`;
|
|
2175
2225
|
const easingAtCheck = easingAt !== undefined ? `
|
|
2176
2226
|
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 =
|
|
2227
|
+
const runtimeWarning = `, ...(${textKeysJs ? '__ignored.length || ' : ''}__filterIgnored.length ? { ignoredProperties: ${textKeysJs ? '__ignored.concat(__filterIgnored)' : '__filterIgnored'} } : {}), ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
|
|
2178
2228
|
const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `${ID}`;
|
|
2179
2229
|
return ("\n// Apply keyframe animation to " +
|
|
2180
2230
|
String(ID) +
|
|
@@ -2190,6 +2240,7 @@ String(keyframesJson) +
|
|
|
2190
2240
|
", __opts);\n const __warn = [];" +
|
|
2191
2241
|
String(easingAtCheck) +
|
|
2192
2242
|
String(textKeyCheck) +
|
|
2243
|
+
String(filterKeyCheck) +
|
|
2193
2244
|
"\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
2245
|
String(append ? `
|
|
2195
2246
|
// Property gaps across the merged track (the same check a single call makes
|
|
@@ -2209,8 +2260,7 @@ String(calculatedDuration) +
|
|
|
2209
2260
|
", loop: " +
|
|
2210
2261
|
String(loop) +
|
|
2211
2262
|
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();
|
|
2263
|
+
", ...(__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
2264
|
}
|
|
2215
2265
|
/**
|
|
2216
2266
|
* Template for executing generators
|
|
@@ -3554,7 +3604,7 @@ String(JSON.stringify(validated.presetId)) +
|
|
|
3554
3604
|
const code = stepNoTarget
|
|
3555
3605
|
? `(function() {
|
|
3556
3606
|
if (!${STEP_ACTIVE_TARGETLESS_JS}) return { success: false, relationType: 'on_event_step_active', error: ${jsLiteral(stepNoTarget)} };
|
|
3557
|
-
|
|
3607
|
+
${asReturningBody(built)}
|
|
3558
3608
|
})();`
|
|
3559
3609
|
: built;
|
|
3560
3610
|
// NO TARGET (event reactions like on_event_play_sound, self-relations): the
|
|
@@ -4234,7 +4284,7 @@ if (typeof _gen === 'function') {
|
|
|
4234
4284
|
})();`);
|
|
4235
4285
|
}
|
|
4236
4286
|
// Establish relations
|
|
4237
|
-
for (const relation of relations) {
|
|
4287
|
+
for (const [relationIndex, relation] of relations.entries()) {
|
|
4238
4288
|
const { source, target, type, params = {} } = relation;
|
|
4239
4289
|
const paramsStr = JSON.stringify(params);
|
|
4240
4290
|
const specProblem = relationSpecProblem(type, params, target, 'no-page');
|
|
@@ -4264,7 +4314,10 @@ String(jsLiteral(type)) +
|
|
|
4264
4314
|
String(jsLiteral(source)) +
|
|
4265
4315
|
", target: " +
|
|
4266
4316
|
String(jsLiteral(target)) +
|
|
4267
|
-
", type: " +
|
|
4317
|
+
", type: " +
|
|
4318
|
+
String(jsLiteral(type)) +
|
|
4319
|
+
" });\n const __o = { _: 0" +
|
|
4320
|
+
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
4321
|
}
|
|
4269
4322
|
// Apply animations
|
|
4270
4323
|
for (const animation of animations) {
|
|
@@ -5234,7 +5287,7 @@ const targetId = ${animItemRef};
|
|
|
5234
5287
|
const item = app.getItemById(targetId);
|
|
5235
5288
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
5236
5289
|
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)}` : ''; })()} };
|
|
5290
|
+
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
5291
|
`;
|
|
5239
5292
|
case 'relation':
|
|
5240
5293
|
const srcRef = batchRef(op.sourceId);
|
|
@@ -5295,7 +5348,7 @@ String(type) +
|
|
|
5295
5348
|
", sourceIds: __failed, error: " +
|
|
5296
5349
|
String(op.update === true ? '__why' : `'the studio refused the ' + ${type} + ' relation for ' + __failed.join(', ') + ' — nothing was bound' + (__why ? ': ' + __why : '.')`) +
|
|
5297
5350
|
" };\n}\nreturn { success: true, relationType: " +
|
|
5298
|
-
String(type) + ", " + String(op.update === true ? 'updated' : 'bound') + ": __done, targetId" + String(glob ? ', count: __members.length' : '') + String((
|
|
5351
|
+
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
5352
|
}
|
|
5300
5353
|
return ("\n" +
|
|
5301
5354
|
String(stepGuard) +
|
|
@@ -5314,7 +5367,7 @@ String(relOpts) +
|
|
|
5314
5367
|
");\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
5368
|
String(relationRefusalReasonJs(jsLiteral(op.relationType))) +
|
|
5316
5369
|
";\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((
|
|
5370
|
+
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
5371
|
case 'delete':
|
|
5319
5372
|
const delRef = batchRef(op.itemId);
|
|
5320
5373
|
return `
|
|
@@ -5402,8 +5455,9 @@ return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget}
|
|
|
5402
5455
|
kfExtra.push(`timeUnits: ${jsLiteral(kfUnits)}`);
|
|
5403
5456
|
const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
|
|
5404
5457
|
const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
|
|
5405
|
-
const
|
|
5406
|
-
const
|
|
5458
|
+
const kfFilterKeysJs = unanimatedFilterKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
|
|
5459
|
+
const kfTextCheck = `\nconst _kfIgnored = ${kfTextKeysJs || '[]'};\nconst _kfFilterIgnored = ${kfFilterKeysJs};`;
|
|
5460
|
+
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
5461
|
return ("\nconst targetId = " +
|
|
5408
5462
|
String(kfItemRef) +
|
|
5409
5463
|
";\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, " +
|
|
@@ -5543,7 +5597,9 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5543
5597
|
}
|
|
5544
5598
|
generateAgentExport(input) {
|
|
5545
5599
|
const validated = AgentExportInputSchema.parse(input);
|
|
5546
|
-
const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, audioCodec, transparent, alphaQuantizer, deterministic, motionBlur, ad: adOpts, frames, startTime } = validated;
|
|
5600
|
+
const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, audioCodec, transparent, alphaQuantizer, deterministic: askedDeterministic, motionBlur, ad: adOpts, frames, startTime } = validated;
|
|
5601
|
+
// Every video is deterministic unless the caller opts out (owner, 2026-11-07), so every receipt says whether its file repeats.
|
|
5602
|
+
const deterministic = askedDeterministic ?? (format === 'mp4' || format === 'webm');
|
|
5547
5603
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
5548
5604
|
const videoEncodeOpts = {};
|
|
5549
5605
|
// A RANGE of the timeline (FxTool #250): frames for t in [startTime, startTime + duration), each exactly the frame
|
|
@@ -6205,10 +6261,10 @@ String(videoDuration) +
|
|
|
6205
6261
|
".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" +
|
|
6206
6262
|
"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" +
|
|
6207
6263
|
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" +
|
|
6208
|
-
"eport it.\n if (result && result.success) {\n const __hw = __vr && __vr.hardwareAcceleration;\n const __exact = __vr && typeof __vr.bitExact === 'boolean' ? __vr.bitExact : null;\n if (__exact === false) {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',\n ...(__vr && __vr.cause ? { cause: __vr.cause } : {}),\n message: '
|
|
6209
|
-
"
|
|
6210
|
-
"
|
|
6211
|
-
|
|
6264
|
+
"eport it.\n if (result && result.success) {\n const __hw = __vr && __vr.hardwareAcceleration;\n const __exact = __vr && typeof __vr.bitExact === 'boolean' ? __vr.bitExact : null;\n if (__exact === false) {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',\n ...(__vr && __vr.determinism && __vr.determinism.cause ? { cause: __vr.determinism.cause } : {}),\n message: 'this file will not repeat byte for byte: no encoder on this machine repeats at this size (the studio test-encoded and checked' + (__vr && __vr.determinism && __vr.determinism.cause === " +
|
|
6265
|
+
"'h264-threaded' ? ': the software H.264 encoder varies run to run at this size on this many CPUs' : '') + '). What still holds: the frames fed to the encoder are identical (compare frameDigest, not sha256), and the container is identical apart from the encoded samples. Do not cache this file by hash. For a byte-identical file, export WebM, or export_png_sequence (pinepaper_interchange) and encode the frames with your own FFmpeg, which the server does not provide.' }]);\n if (result.fidelity.note) delete result.fidelity.note;\n } else if (__exact === null && __hw !== 'prefer-software') {\n result.fidelity = result.fidelity || { warnings: [] };\n result.fidelity.warnings = " +
|
|
6266
|
+
"(result.fidelity.warnings || []).concat([{ code: 'determinism_not_pinned',\n message: __hw\n ? 'this file may not repeat byte for byte: the encoder ran with hardwareAcceleration ' + JSON.stringify(__hw) + ': the container is pinned, the pixels may still differ slightly between runs (hardware encoders vary). Do not cache this file by hash.'\n : 'deterministic pinned the container timestamps, but this studio does not report which encoder backend ran, so the pixels may differ slightly between runs if the browser switches between hardware and software encoding. Do not rely on byte-identical files from this studio.' }]);\n if (result.fidelity.note) delete result.fi" +
|
|
6267
|
+
"delity.note;\n }\n }\n"
|
|
6212
6268
|
|
|
6213
6269
|
|
|
6214
6270
|
|
|
@@ -6294,9 +6350,14 @@ String((() => {
|
|
|
6294
6350
|
String(JSON.stringify(format)) +
|
|
6295
6351
|
") !== -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
6352
|
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
|
-
"
|
|
6353
|
+
" 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(" +
|
|
6354
|
+
String(JSON.stringify(format)) +
|
|
6355
|
+
") !== -1 && app.itemRegistry && typeof app.itemRegistry.getAll === 'function') {\n try {\n const __end = (result.video && result.video.durationS) || " +
|
|
6356
|
+
String(JSON.stringify(duration ?? null)) +
|
|
6357
|
+
";\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 }" +
|
|
6358
|
+
"\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" +
|
|
6359
|
+
"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" +
|
|
6360
|
+
"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
6361
|
String(JSON.stringify(format)) +
|
|
6301
6362
|
") === -1) {\n result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'relight_not_applied', message: 'a relight rig is set, but ' + " +
|
|
6302
6363
|
String(jsLiteral(format)) +
|
|
@@ -7343,6 +7404,12 @@ String(jsLiteral(format)) +
|
|
|
7343
7404
|
|
|
7344
7405
|
|
|
7345
7406
|
|
|
7407
|
+
|
|
7408
|
+
|
|
7409
|
+
|
|
7410
|
+
|
|
7411
|
+
|
|
7412
|
+
|
|
7346
7413
|
|
|
7347
7414
|
|
|
7348
7415
|
|
|
@@ -9353,7 +9420,9 @@ String(jsLiteral(templateId)) +
|
|
|
9353
9420
|
).trim();
|
|
9354
9421
|
}
|
|
9355
9422
|
generateImportImage(input) {
|
|
9356
|
-
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset, name } = input;
|
|
9423
|
+
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset, name, opacity, blendMode, scale, loop } = input;
|
|
9424
|
+
const look = { ...(opacity !== undefined ? { opacity } : {}), ...(blendMode !== undefined ? { blendMode } : {}), ...(scale !== undefined ? { scale } : {}) };
|
|
9425
|
+
const hasLook = Object.keys(look).length > 0;
|
|
9357
9426
|
// IMPORTSHADOW: the shadow keys, applied through the engine's own modify
|
|
9358
9427
|
// door once the image is placed (and masked) — the route modify_item takes.
|
|
9359
9428
|
const shadow = {};
|
|
@@ -9375,6 +9444,7 @@ String(jsLiteral(templateId)) +
|
|
|
9375
9444
|
maxWidth !== undefined ? `maxWidth: ${maxWidth}` : '',
|
|
9376
9445
|
maxHeight !== undefined ? `maxHeight: ${maxHeight}` : '',
|
|
9377
9446
|
nativeSize ? 'nativeSize: true' : '',
|
|
9447
|
+
loop !== undefined ? `loop: ${JSON.stringify(loop)}` : '',
|
|
9378
9448
|
].filter(Boolean);
|
|
9379
9449
|
const optsLiteral = optParts.length ? `{ ${optParts.join(', ')} }` : '{}';
|
|
9380
9450
|
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 +9476,16 @@ String(mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask b
|
|
|
9406
9476
|
if (app.itemRegistry && typeof app.itemRegistry.rebind === 'function') app.itemRegistry.rebind(itemId, masked);
|
|
9407
9477
|
placed = masked;
|
|
9408
9478
|
` : '') +
|
|
9479
|
+
String(hasLook ? ` // opacity, blendMode, scale: read by modify, on what is drawn. They were not import arguments (OPACIMP31, BLENDIMP31, IMGSCALE31).
|
|
9480
|
+
let __look = null;
|
|
9481
|
+
{
|
|
9482
|
+
let __ok = false;
|
|
9483
|
+
try { __ok = app.modifyItem(itemId, ${JSON.stringify(look)}) !== false; } catch (_) { __ok = false; }
|
|
9484
|
+
__look = __ok
|
|
9485
|
+
? { applied: true, opacity: placed.opacity, blendMode: placed.blendMode }
|
|
9486
|
+
: { applied: false, warning: 'the image was imported but ' + ${jsLiteral(Object.keys(look).join(', '))} + ' was not applied: set it with pinepaper_modify_item on ' + itemId + '.' };
|
|
9487
|
+
}
|
|
9488
|
+
` : '') +
|
|
9409
9489
|
String(hasShadow ? ` // The shadow goes on what is DRAWN, after a mask has replaced the raster.
|
|
9410
9490
|
let __shadow = null;
|
|
9411
9491
|
{
|
|
@@ -9428,6 +9508,8 @@ String(name ? `\n name: ${JSON.stringify(name)},` : '') +
|
|
|
9428
9508
|
"\n bounds: { x: b.x, y: b.y, width: b.width, height: b.height }" +
|
|
9429
9509
|
String(mask ? `,\n mask: ${jsLiteral(mask)}` : '') +
|
|
9430
9510
|
String(hasShadow ? ',\n shadow: __shadow' : '') +
|
|
9511
|
+
String(loop !== undefined ? ",\n playback: (raster && raster.data && raster.data.animatedImage && raster.data.animatedImage.loop !== undefined) ? { applied: true, plays: raster.data.animatedImage.loop } : { applied: false, warning: 'loop was not applied: this image does not animate in this studio. Animated GIF, WebP and APNG do.' }" : '') +
|
|
9512
|
+
String(hasLook ? ',\n look: __look' : '') +
|
|
9431
9513
|
",\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
9514
|
" }\n})();\n"
|
|
9433
9515
|
|
|
@@ -9519,6 +9601,12 @@ String(hasShadow ? ',\n shadow: __shadow' : '') +
|
|
|
9519
9601
|
|
|
9520
9602
|
|
|
9521
9603
|
|
|
9604
|
+
|
|
9605
|
+
|
|
9606
|
+
|
|
9607
|
+
|
|
9608
|
+
|
|
9609
|
+
|
|
9522
9610
|
|
|
9523
9611
|
|
|
9524
9612
|
|
|
@@ -16238,10 +16326,8 @@ String(op) +
|
|
|
16238
16326
|
}
|
|
16239
16327
|
/**
|
|
16240
16328
|
* 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.
|
|
16329
|
+
* studio without it refuses by name instead of a TypeError. The engine's custom action is not offered: it needs a
|
|
16330
|
+
* function, which JSON cannot carry.
|
|
16245
16331
|
*/
|
|
16246
16332
|
function generateContraptionCode(input) {
|
|
16247
16333
|
const refuse = (method) => `if (typeof app.${method} !== 'function') {
|
|
@@ -16252,7 +16338,7 @@ function generateContraptionCode(input) {
|
|
|
16252
16338
|
const body = () => {
|
|
16253
16339
|
switch (input.action) {
|
|
16254
16340
|
case 'add_contraption': {
|
|
16255
|
-
const targets = [...new Set((input.actions ?? []).flatMap((a) => (
|
|
16341
|
+
const targets = [...new Set((input.actions ?? []).flatMap((a) => ('targetId' in a ? [a.targetId] : [])))];
|
|
16256
16342
|
const config = {
|
|
16257
16343
|
...(input.contraptionId ? { id: input.contraptionId } : {}),
|
|
16258
16344
|
sourceEvent: input.sourceEvent,
|
|
@@ -16266,14 +16352,20 @@ function generateContraptionCode(input) {
|
|
|
16266
16352
|
const contraptionId = app.addContraption(${jsLiteral(config)});
|
|
16267
16353
|
const missing = ${jsLiteral(targets)}.filter((id) => !app.getItemById(id));
|
|
16268
16354
|
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'" : ''} };`;
|
|
16355
|
+
...(missing.length ? { warning: 'no item has the id ' + missing.join(', ') + ' yet: the rule does nothing to it until one does' } : {}), };`;
|
|
16271
16356
|
}
|
|
16272
16357
|
case 'remove_contraption':
|
|
16273
16358
|
return `${refuse('removeContraption')}
|
|
16274
16359
|
const removed = app.removeContraption(${jsLiteral(input.contraptionId)});
|
|
16275
16360
|
return removed ? { success: true, action: 'remove_contraption', contraptionId: ${jsLiteral(input.contraptionId)} }
|
|
16276
16361
|
: { success: false, action: 'remove_contraption', error: 'no rule has the id ' + ${jsLiteral(input.contraptionId)} };`;
|
|
16362
|
+
case 'clear_contraptions':
|
|
16363
|
+
return `${refuse('clearContraptions')}
|
|
16364
|
+
app.clearContraptions();
|
|
16365
|
+
return { success: true, action: 'clear_contraptions' };`;
|
|
16366
|
+
case 'clear_variables':
|
|
16367
|
+
return `${refuse('clearVariables')}
|
|
16368
|
+
return { success: true, action: 'clear_variables', scope: ${scope}, removed: app.clearVariables(${scope}) };`;
|
|
16277
16369
|
case 'list_contraptions':
|
|
16278
16370
|
return `${refuse('listContraptions')}
|
|
16279
16371
|
return { success: true, action: 'list_contraptions', contraptions: app.listContraptions() };`;
|