@pinepaper.studio/mcp-server 1.6.23 → 1.6.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +84 -5
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SECURITY.md +46 -0
- package/dist/agent/types.d.ts +114 -106
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/browser/puppeteer-controller.js +2 -2
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/ontology/hints.d.ts.map +1 -1
- package/dist/ontology/hints.js +5 -1
- package/dist/ontology/hints.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +1 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/batch-macros.d.ts +27 -0
- package/dist/tools/batch-macros.d.ts.map +1 -0
- package/dist/tools/batch-macros.js +157 -0
- package/dist/tools/batch-macros.js.map +1 -0
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +2 -0
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/compact-descriptions.js +3 -3
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +87 -5
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +2 -2
- package/dist/tools/engine-lists.js +2 -2
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +1 -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 +52 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/export-file-check.d.ts +31 -0
- package/dist/tools/export-file-check.d.ts.map +1 -0
- package/dist/tools/export-file-check.js +103 -0
- package/dist/tools/export-file-check.js.map +1 -0
- package/dist/tools/export-file-receipt.d.ts +56 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -0
- package/dist/tools/export-file-receipt.js +151 -0
- package/dist/tools/export-file-receipt.js.map +1 -0
- package/dist/tools/handlers/beats.d.ts +7 -44
- package/dist/tools/handlers/beats.d.ts.map +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +31 -6
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +41 -270
- package/dist/tools/handlers/generate.d.ts.map +1 -1
- package/dist/tools/handlers.d.ts +67 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +304 -67
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +34 -0
- package/dist/tools/lane.d.ts.map +1 -0
- package/dist/tools/lane.js +210 -0
- package/dist/tools/lane.js.map +1 -0
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +3 -2
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/nested-ignored.d.ts +16 -0
- package/dist/tools/nested-ignored.d.ts.map +1 -0
- package/dist/tools/nested-ignored.js +83 -0
- package/dist/tools/nested-ignored.js.map +1 -0
- 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 +9 -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 +8 -4
- package/dist/tools/stick-vocabulary.js.map +1 -1
- package/dist/tools/timeline.d.ts +136 -0
- package/dist/tools/timeline.d.ts.map +1 -0
- package/dist/tools/timeline.js +1036 -0
- package/dist/tools/timeline.js.map +1 -0
- package/dist/tools/toolkits.d.ts +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +12 -2
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +30 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +277 -53
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +5276 -25048
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +78 -40
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +5 -1
- package/package.json +8 -7
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* This code is designed to run in the browser context where window.PinePaper is available.
|
|
6
6
|
*/
|
|
7
7
|
import { compileChoreography } from '../tools/choreograph.js';
|
|
8
|
+
import { compileBatchTimelines, emitTimelineOpBody } from '../tools/timeline.js';
|
|
8
9
|
import { normalizeSvgSize } from '../utils/svg-size.js';
|
|
9
10
|
import { WORLD3D_COLOR_PATHS } from '../tools/engine-lists.js';
|
|
10
11
|
import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
|
|
11
12
|
import { ACCEPTED_CREATE_PARAMS, NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
|
|
12
13
|
import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
|
|
14
|
+
import { expandBatchMacros } from '../tools/batch-macros.js';
|
|
13
15
|
import { CreateItemInputSchema, ScreenSpaceSchema, StepTimingSchema, CounterSchema, ImportMotionCaptureInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
|
|
14
16
|
// Diagram types
|
|
15
17
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
@@ -26,7 +28,7 @@ GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTou
|
|
|
26
28
|
// Custom relation/code types
|
|
27
29
|
RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
|
|
28
30
|
// Mask types
|
|
29
|
-
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, MaskTypeSchema, MaskPresetSchema, } from './schemas.js';
|
|
31
|
+
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, PatternInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, MaskTypeSchema, MaskPresetSchema, } from './schemas.js';
|
|
30
32
|
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
31
33
|
// =============================================================================
|
|
32
34
|
// HELPER FUNCTIONS
|
|
@@ -309,7 +311,8 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
309
311
|
const textOnly = (k) => TEXT_STYLE_KEYS.includes(k) || k === 'fit' || k === 'textDirection' || k === 'dir'
|
|
310
312
|
|| TABULAR_KEYS.includes(k) || k === 'strokePosition';
|
|
311
313
|
const ignored = Object.keys(properties).filter((k) => itemType === 'text'
|
|
312
|
-
? !CREATE_KNOWN_KEYS.has(k) && !textOnly(k) && !DIRECTION_KEYS.includes(k)
|
|
314
|
+
? (!CREATE_KNOWN_KEYS.has(k) && !textOnly(k) && !DIRECTION_KEYS.includes(k))
|
|
315
|
+
|| (k === 'letterSpacing' && letterSpacingCss(properties[k]) === undefined)
|
|
313
316
|
: !CREATE_KNOWN_KEYS.has(k) || textOnly(k));
|
|
314
317
|
// Build the code
|
|
315
318
|
let code = `
|
|
@@ -734,13 +737,26 @@ function authoringFrameJs(name) {
|
|
|
734
737
|
return (typeof app.exportFrameRect === 'function' && app.exportFrameRect()) || cs || { width: 1920, height: 1080 };
|
|
735
738
|
})();`;
|
|
736
739
|
}
|
|
737
|
-
const TEXT_STYLE_KEYS = ['fontWeight', 'fontStyle', 'leading', 'lineHeight', 'textAlign', 'align', 'text'];
|
|
740
|
+
const TEXT_STYLE_KEYS = ['fontWeight', 'fontStyle', 'leading', 'lineHeight', 'letterSpacing', 'textAlign', 'align', 'text'];
|
|
741
|
+
/**
|
|
742
|
+
* letterSpacing as the studio draws it, or undefined if it cannot be. create()
|
|
743
|
+
* and modifyItem() do not read it (B108); the studio draws the item's own
|
|
744
|
+
* letterSpacing, a CSS length, as its keyframes set it, and a number is px.
|
|
745
|
+
*/
|
|
746
|
+
function letterSpacingCss(v) {
|
|
747
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
748
|
+
return `${v}px`;
|
|
749
|
+
if (typeof v === 'string' && /^-?\d*\.?\d+(px|em)?$/.test(v.trim()))
|
|
750
|
+
return /\d$/.test(v.trim()) ? `${v.trim()}px` : v.trim();
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
738
753
|
function emitTextStyle(itemExpr, props) {
|
|
739
754
|
const weight = props.fontWeight;
|
|
740
755
|
const italic = props.fontStyle === 'italic' || props.fontStyle === 'oblique';
|
|
741
756
|
const lh = typeof props.lineHeight === 'number' ? props.lineHeight : undefined;
|
|
742
757
|
const leading = typeof props.leading === 'number' ? props.leading : undefined;
|
|
743
|
-
|
|
758
|
+
const ls = letterSpacingCss(props.letterSpacing);
|
|
759
|
+
if (weight === undefined && props.fontStyle === undefined && leading === undefined && lh === undefined && ls === undefined)
|
|
744
760
|
return '';
|
|
745
761
|
return `
|
|
746
762
|
(function(it) {
|
|
@@ -753,6 +769,7 @@ function emitTextStyle(itemExpr, props) {
|
|
|
753
769
|
: ''}
|
|
754
770
|
${leading !== undefined ? `it.leading = ${leading};` : ''}
|
|
755
771
|
${lh !== undefined ? `it.leading = ${lh} <= 4 ? ${lh} * it.fontSize : ${lh};` : ''}
|
|
772
|
+
${ls !== undefined ? `it.letterSpacing = ${JSON.stringify(ls)};` : ''}
|
|
756
773
|
const b1 = it.bounds;
|
|
757
774
|
it.position = it.position.add([ax - edge(b1), ay - b1.center.y]);
|
|
758
775
|
})(${itemExpr});`;
|
|
@@ -1059,6 +1076,8 @@ if (_flagged && _flagged.item && _flagged.item.data) Object.assign(_flagged.item
|
|
|
1059
1076
|
// AN UNREAD KEY HAD NO EFFECT, AND THE CALL SAID SUCCESS. {scaling: 8} on a
|
|
1060
1077
|
// raster answered success and left it 16 px; the engine reads `scale`. Same
|
|
1061
1078
|
// report create_item gives, against the engine's modify path (generated).
|
|
1079
|
+
if (textStyle && letterSpacingCss(properties.letterSpacing) !== undefined)
|
|
1080
|
+
aliased.add('letterSpacing');
|
|
1062
1081
|
const modIgnored = Object.keys(properties).filter((k) => !MODIFY_KNOWN_KEYS.has(k) && !aliased.has(k));
|
|
1063
1082
|
const modWarning = modIgnored.length === 0 ? '' : `, ignoredProperties: ${JSON.stringify(modIgnored)}, warning: ${JSON.stringify(`${modIgnored.join(', ')} ${modIgnored.length > 1 ? 'are' : 'is'} not read when modifying an item, so ${modIgnored.length > 1 ? 'they had' : 'it had'} no effect.`
|
|
1064
1083
|
+ modIgnored.filter((k) => MODIFY_DID_YOU_MEAN[k]).map((k) => ` Did you mean ${MODIFY_DID_YOU_MEAN[k]} (not ${k})?`).join(''))}`;
|
|
@@ -1191,6 +1210,61 @@ export function emitterCountWarnings(spec) {
|
|
|
1191
1210
|
? `${where} ${asked} is over the ${EMITTER_MAX_PER_BURST}-particle cap per burst of the current studio (${EMITTER_LEGACY_MAX_PER_BURST} on older studios), so it draws at most ${EMITTER_MAX_PER_BURST}. Split it into bursts at the same t for more.`
|
|
1192
1211
|
: `${where} ${asked} is over the ${EMITTER_LEGACY_MAX_PER_BURST}-particle cap per burst of studios before the ${EMITTER_MAX_PER_BURST} cap, and this studio did not report what it drew: on an older studio it draws ${EMITTER_LEGACY_MAX_PER_BURST}. Split it into bursts at the same t to be sure.`);
|
|
1193
1212
|
}
|
|
1213
|
+
/**
|
|
1214
|
+
* An emitter spec as the studio reads it, and what the reply says about it.
|
|
1215
|
+
*
|
|
1216
|
+
* The studio reads velocities and offsets as [x, y] pairs only and drops any
|
|
1217
|
+
* other entry, so velocities: [{x, y}] made particles with no velocity and no
|
|
1218
|
+
* word (VELOC23). Point objects are turned into pairs here. And a burst draws
|
|
1219
|
+
* `count` particles, the i-th taking velocities[i % length]: with fewer
|
|
1220
|
+
* velocities than particles the extras repeat a velocity and draw on top of an
|
|
1221
|
+
* earlier particle, and with more the extras go unused, both in silence
|
|
1222
|
+
* (VELOCMISMATCH23). That is said, not changed: cycling is the studio's rule.
|
|
1223
|
+
* `assumeDefaultCount` is false for a set, whose spec is only the keys changed.
|
|
1224
|
+
*/
|
|
1225
|
+
export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
1226
|
+
const warnings = [];
|
|
1227
|
+
if (!spec || typeof spec !== 'object')
|
|
1228
|
+
return { spec, warnings };
|
|
1229
|
+
const pairs = (v) => (Array.isArray(v)
|
|
1230
|
+
? v.map((p) => (p && typeof p === 'object' && !Array.isArray(p) && ('x' in p || 'y' in p)
|
|
1231
|
+
? [Number(p.x ?? 0), Number(p.y ?? 0)]
|
|
1232
|
+
: p))
|
|
1233
|
+
: v);
|
|
1234
|
+
const fix = (o) => {
|
|
1235
|
+
const out = { ...o };
|
|
1236
|
+
for (const k of ['velocities', 'offsets'])
|
|
1237
|
+
if (out[k] !== undefined)
|
|
1238
|
+
out[k] = pairs(out[k]);
|
|
1239
|
+
return out;
|
|
1240
|
+
};
|
|
1241
|
+
const out = fix(spec);
|
|
1242
|
+
if (Array.isArray(spec.bursts))
|
|
1243
|
+
out.bursts = spec.bursts.map((b) => (b && typeof b === 'object' ? fix(b) : b));
|
|
1244
|
+
// The studio's default count per burst (ParticleEmitter DEFAULT_LOOK).
|
|
1245
|
+
const DEFAULT_COUNT = 12;
|
|
1246
|
+
const mismatch = (where, count, velocities) => {
|
|
1247
|
+
if (!Array.isArray(velocities) || !velocities.length)
|
|
1248
|
+
return;
|
|
1249
|
+
const n = typeof count === 'number' && Number.isFinite(count) ? Math.round(count) : (assumeDefaultCount ? DEFAULT_COUNT : null);
|
|
1250
|
+
if (n === null || n === velocities.length)
|
|
1251
|
+
return;
|
|
1252
|
+
warnings.push(n > velocities.length
|
|
1253
|
+
? `${where}: count ${n} with ${velocities.length} velocities: particle i takes velocities[i % ${velocities.length}], so ${n - velocities.length} particles repeat an earlier particle's path and draw on top of it. Give one velocity per particle, or set count to ${velocities.length}.`
|
|
1254
|
+
: `${where}: count ${n} with ${velocities.length} velocities: only the first ${n} velocities are used. Set count to ${velocities.length} to draw them all.`);
|
|
1255
|
+
};
|
|
1256
|
+
// The spec's own pair once; a burst only where it sets either itself (it inherits the rest).
|
|
1257
|
+
const bursts = Array.isArray(out.bursts) ? out.bursts : [];
|
|
1258
|
+
const inherited = bursts.some((b) => !b || typeof b !== 'object' || (b.count === undefined && b.velocities === undefined));
|
|
1259
|
+
if (out.velocities !== undefined && (out.count !== undefined || inherited))
|
|
1260
|
+
mismatch('spec', out.count, out.velocities);
|
|
1261
|
+
bursts.forEach((b, i) => {
|
|
1262
|
+
const own = (b && typeof b === 'object' ? b : {});
|
|
1263
|
+
if (own.count !== undefined || own.velocities !== undefined)
|
|
1264
|
+
mismatch(`bursts[${i}]`, own.count ?? out.count, own.velocities ?? out.velocities);
|
|
1265
|
+
});
|
|
1266
|
+
return { spec: out, warnings };
|
|
1267
|
+
}
|
|
1194
1268
|
/**
|
|
1195
1269
|
* moves_along_path, from the parameters a caller actually writes.
|
|
1196
1270
|
*
|
|
@@ -2044,9 +2118,11 @@ const remaining = app.itemRegistry ? app.itemRegistry.getAll().length : 0;
|
|
|
2044
2118
|
*/
|
|
2045
2119
|
function generateExportSVGCode() {
|
|
2046
2120
|
return `
|
|
2047
|
-
// Export animated SVG
|
|
2121
|
+
// Export animated SVG. An export that produced nothing is a failure, named, not a success with an empty string.
|
|
2048
2122
|
const svgString = app.exportAnimatedSVG();
|
|
2049
|
-
(
|
|
2123
|
+
(typeof svgString !== 'string' || svgString.length === 0)
|
|
2124
|
+
? ({ success: false, step: 'export', error: 'export failed at the export step: exportAnimatedSVG returned ' + (svgString === '' ? 'an empty string' : (svgString === null ? 'null' : typeof svgString)) + ', so there is no SVG to return' })
|
|
2125
|
+
: ({ success: true, svgString, hasAnimations: svgString.includes('<animate') });
|
|
2050
2126
|
`.trim();
|
|
2051
2127
|
}
|
|
2052
2128
|
/**
|
|
@@ -2374,6 +2450,15 @@ ${itemSettersApplyJs(s, '__sid', '__res')}
|
|
|
2374
2450
|
return __res;
|
|
2375
2451
|
})();`;
|
|
2376
2452
|
}
|
|
2453
|
+
/**
|
|
2454
|
+
* A batch op's item reference as a JS expression: `$N` is the Nth item this
|
|
2455
|
+
* batch created, anything else is an id, quoted. Only digits follow the `$` —
|
|
2456
|
+
* the raw splice it replaces put `$1);…;(` straight into the snippet, and an id
|
|
2457
|
+
* with a quote in it broke the batch's syntax.
|
|
2458
|
+
*/
|
|
2459
|
+
export function batchRef(raw) {
|
|
2460
|
+
return typeof raw === 'string' && /^\$\d+$/.test(raw) ? `__ref(${raw.slice(1)})` : JSON.stringify(String(raw));
|
|
2461
|
+
}
|
|
2377
2462
|
function asReturningBody(snippet) {
|
|
2378
2463
|
const single = snippet.trim();
|
|
2379
2464
|
if (single.startsWith('(async function()'))
|
|
@@ -4265,11 +4350,16 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4265
4350
|
*/
|
|
4266
4351
|
generateAgentBatchExecute(input) {
|
|
4267
4352
|
const validated = AgentBatchExecuteInputSchema.parse(input);
|
|
4268
|
-
const {
|
|
4353
|
+
const { atomic } = validated;
|
|
4354
|
+
// ring / grid / orbit / stagger / patch become the batch's own ops (batch-macros.ts).
|
|
4355
|
+
const operations = expandBatchMacros(validated.operations);
|
|
4269
4356
|
const isAtomic = atomic !== false;
|
|
4270
4357
|
// Fold duplicate keyframe_animate targets so a later op can't silently
|
|
4271
4358
|
// clobber an earlier track (app.addAnimation replaces, never merges).
|
|
4272
4359
|
const mergePlan = planKeyframeMerges(operations);
|
|
4360
|
+
// Timeline ops compile here, on the server: the handler refuses a batch whose timeline does not compile before
|
|
4361
|
+
// anything runs; a direct caller gets the op's own failure instead.
|
|
4362
|
+
const timelines = compileBatchTimelines(operations);
|
|
4273
4363
|
let code = `
|
|
4274
4364
|
// Batch execute ${operations.length} operations
|
|
4275
4365
|
(async function() {
|
|
@@ -4298,11 +4388,32 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4298
4388
|
};
|
|
4299
4389
|
const results = [];
|
|
4300
4390
|
let success = true;
|
|
4391
|
+
// A failure's words, never "undefined" for a thrown non-Error.
|
|
4392
|
+
const _why = (e, fb) => (e && typeof e.message === 'string' && e.message.trim()) ? e.message
|
|
4393
|
+
: (typeof e === 'string' && e.trim()) ? e : fb;
|
|
4394
|
+
// THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything
|
|
4395
|
+
// failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the
|
|
4396
|
+
// result. A studio without lintScene says nothing.
|
|
4397
|
+
const __ppLintNow = function () {
|
|
4398
|
+
try {
|
|
4399
|
+
if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];
|
|
4400
|
+
const r = app.lintScene();
|
|
4401
|
+
return r && Array.isArray(r.warnings) ? r.warnings : [];
|
|
4402
|
+
} catch (e) {
|
|
4403
|
+
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
4404
|
+
}
|
|
4405
|
+
};
|
|
4406
|
+
// Every failed op as { step, itemId, reason }, beside the per-op results.
|
|
4407
|
+
const _failures = () => results.filter((r) => !r.success).map((r) => ({ step: r.step, itemId: r.itemId, reason: r.reason }));
|
|
4301
4408
|
|
|
4302
4409
|
try {
|
|
4303
4410
|
`;
|
|
4304
4411
|
operations.forEach((op, index) => {
|
|
4305
|
-
const opCode =
|
|
4412
|
+
const opCode = op.type === 'timeline'
|
|
4413
|
+
? (timelines.plans.has(index)
|
|
4414
|
+
? emitTimelineOpBody(timelines.plans.get(index), (t) => (t.startsWith('$') ? `__ref(${t.substring(1)})` : JSON.stringify(t)))
|
|
4415
|
+
: `return { success: false, error: ${JSON.stringify(timelines.errors.filter((e) => e.path.startsWith(`operations[${index}]`)).map((e) => `${e.path}: ${e.message}`).join('; ') || 'the timeline did not compile')} };`)
|
|
4416
|
+
: this.generateBatchOperationCode(op, index, mergePlan);
|
|
4306
4417
|
// $N variable references are documented as "items CREATED in earlier
|
|
4307
4418
|
// operations" — only `create` results may extend the itemIds array.
|
|
4308
4419
|
// animate/keyframe_animate/modify/delete also return { itemId } (the
|
|
@@ -4313,6 +4424,10 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4313
4424
|
// vehicles, 2026-07-30). `group` already follows this rule by
|
|
4314
4425
|
// returning groupId instead of itemId.
|
|
4315
4426
|
const claimsItemSlot = op.type === 'create';
|
|
4427
|
+
// The item this op acts on, for a failure's itemId: its own id field, else what its result names.
|
|
4428
|
+
const o = op;
|
|
4429
|
+
const opItem = [o.itemId, o.targetId, o.sourceId, o.id].find((v) => typeof v === 'string') ?? null;
|
|
4430
|
+
const opStep = typeof o._step === 'string' ? `op ${index}: ${o._step}` : `op ${index} (${op.type})`;
|
|
4316
4431
|
code += `
|
|
4317
4432
|
// Operation ${index}: ${op.type}
|
|
4318
4433
|
try {
|
|
@@ -4329,7 +4444,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4329
4444
|
if (failed${index}) {
|
|
4330
4445
|
// ATOMIC: throw WITHOUT recording, and let the catch below record it
|
|
4331
4446
|
// once. Doing both put two entries in for one failed op.
|
|
4332
|
-
${isAtomic ? `
|
|
4447
|
+
${isAtomic ? `const __e${index} = new Error(failed${index}); __e${index}.itemId = (result${index} && typeof result${index}.itemId === 'string') ? result${index}.itemId : ${JSON.stringify(opItem)}; throw __e${index};` : `results.push({ index: ${index}, success: false, step: ${JSON.stringify(opStep)}, itemId: (result${index} && typeof result${index}.itemId === 'string') ? result${index}.itemId : ${JSON.stringify(opItem)}, reason: failed${index}, error: failed${index}, result: result${index} });
|
|
4333
4448
|
success = false;`}
|
|
4334
4449
|
} else {
|
|
4335
4450
|
results.push({ index: ${index}, success: true, result: result${index} });
|
|
@@ -4338,7 +4453,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4338
4453
|
}` : ''}
|
|
4339
4454
|
}
|
|
4340
4455
|
} catch (opError) {
|
|
4341
|
-
|
|
4456
|
+
const __r${index} = _why(opError, 'operation ${index} (${op.type}) threw with no message');
|
|
4457
|
+
results.push({ index: ${index}, success: false, step: ${JSON.stringify(opStep)}, itemId: (opError && typeof opError.itemId === 'string') ? opError.itemId : ${JSON.stringify(opItem)}, reason: __r${index}, error: __r${index} });
|
|
4342
4458
|
${isAtomic ? 'throw opError;' : 'success = false;'}
|
|
4343
4459
|
}
|
|
4344
4460
|
`;
|
|
@@ -4358,15 +4474,19 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4358
4474
|
}
|
|
4359
4475
|
return { success, itemIds, results, operationCount: ${operations.length},
|
|
4360
4476
|
failedCount: _failed.length,
|
|
4361
|
-
...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; ') } : {}),
|
|
4362
|
-
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
4477
|
+
...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; '), failures: _failures() } : {}),
|
|
4478
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
4363
4479
|
} catch (e) {
|
|
4364
4480
|
// A batch that created nothing keeps the previous batch's ids on record.
|
|
4365
4481
|
if (typeof window !== 'undefined' && itemIds.length) {
|
|
4366
4482
|
window.__pinepaper_batch_item_ids = itemIds;
|
|
4367
4483
|
}
|
|
4368
4484
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
4369
|
-
|
|
4485
|
+
const __fails = _failures();
|
|
4486
|
+
const __reason = _why(e, 'the batch failed with no message');
|
|
4487
|
+
return { success: false, error: __reason, itemIds, results, operationCount: ${operations.length},
|
|
4488
|
+
failures: __fails.length ? __fails : [{ step: 'batch', itemId: null, reason: __reason }],
|
|
4489
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
4370
4490
|
}
|
|
4371
4491
|
})();
|
|
4372
4492
|
`;
|
|
@@ -4403,7 +4523,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4403
4523
|
'backgroundColor', 'generatorName', 'generatorParams', 'generatorRegion',
|
|
4404
4524
|
'width', 'height', 'preset', 'keyframes', 'duration', 'loop',
|
|
4405
4525
|
'maskPreset', 'maskType', 'maskOptions', 'effectType', 'effectParams',
|
|
4406
|
-
'action', 'time', 'append'
|
|
4526
|
+
'action', 'time', 'append', '_step'
|
|
4407
4527
|
]);
|
|
4408
4528
|
for (const [k, v] of Object.entries(anyOp)) {
|
|
4409
4529
|
if (!batchKeys.has(k) && v !== undefined && opProps[k] === undefined) {
|
|
@@ -4448,9 +4568,7 @@ return __created;
|
|
|
4448
4568
|
return createCode;
|
|
4449
4569
|
}
|
|
4450
4570
|
case 'modify': {
|
|
4451
|
-
const itemRef = op.itemId
|
|
4452
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4453
|
-
: `'${op.itemId}'`;
|
|
4571
|
+
const itemRef = batchRef(op.itemId);
|
|
4454
4572
|
// modify_item's own emitter, as create is create_item's (see above):
|
|
4455
4573
|
// the batch's modifyItem call missed text styles, font loading, audio
|
|
4456
4574
|
// level, no-fill, lifetimes, smoothing and the unread-property report.
|
|
@@ -4463,15 +4581,21 @@ return __created;
|
|
|
4463
4581
|
.replace(/;\s*$/, '');
|
|
4464
4582
|
return `
|
|
4465
4583
|
const __mid = ${itemRef};
|
|
4584
|
+
if (app.itemRegistry && app.itemRegistry.getItem && !app.itemRegistry.getItem(__mid) && typeof app.getItemsByName === 'function') {
|
|
4585
|
+
// A NAME HELD BY SEVERAL ITEMS is refused, not resolved: getItemById answers
|
|
4586
|
+
// the first holder, so a patch by name would change whichever came first.
|
|
4587
|
+
const __held = app.getItemsByName(__mid) || [];
|
|
4588
|
+
if (__held.length > 1) {
|
|
4589
|
+
return { success: false, itemId: __mid, error: __held.length + ' items are named ' + JSON.stringify(__mid) + ' (' + __held.map(function (h) { return (h && h.data && h.data.id) || '?'; }).join(', ') + '); pass one of their ids.' };
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4466
4592
|
const __r = await ${single};
|
|
4467
4593
|
if (__r && __r.success !== false) __r.modified = true;
|
|
4468
4594
|
return __r;
|
|
4469
4595
|
`;
|
|
4470
4596
|
}
|
|
4471
4597
|
case 'animate':
|
|
4472
|
-
const animItemRef = op.itemId
|
|
4473
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4474
|
-
: `'${op.itemId}'`;
|
|
4598
|
+
const animItemRef = batchRef(op.itemId);
|
|
4475
4599
|
const animOpts = JSON.stringify(op.animationOptions || {});
|
|
4476
4600
|
return `
|
|
4477
4601
|
const targetId = ${animItemRef};
|
|
@@ -4481,12 +4605,8 @@ app.animate(item, { animationType: '${op.animationType}', ...${animOpts} });
|
|
|
4481
4605
|
return { itemId: targetId, animationType: '${op.animationType}' };
|
|
4482
4606
|
`;
|
|
4483
4607
|
case 'relation':
|
|
4484
|
-
const srcRef = op.sourceId
|
|
4485
|
-
|
|
4486
|
-
: `'${op.sourceId}'`;
|
|
4487
|
-
const tgtRef = op.targetId?.startsWith('$')
|
|
4488
|
-
? `__ref(${op.targetId.substring(1)})`
|
|
4489
|
-
: `'${op.targetId}'`;
|
|
4608
|
+
const srcRef = batchRef(op.sourceId);
|
|
4609
|
+
const tgtRef = batchRef(op.targetId);
|
|
4490
4610
|
const relOpts = JSON.stringify(op.relationOptions || {});
|
|
4491
4611
|
return `
|
|
4492
4612
|
const sourceId = ${srcRef};
|
|
@@ -4508,9 +4628,7 @@ if (!relationId) {
|
|
|
4508
4628
|
return { relationId, sourceId, targetId, relationType: '${op.relationType}' };
|
|
4509
4629
|
`;
|
|
4510
4630
|
case 'delete':
|
|
4511
|
-
const delRef = op.itemId
|
|
4512
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4513
|
-
: `'${op.itemId}'`;
|
|
4631
|
+
const delRef = batchRef(op.itemId);
|
|
4514
4632
|
return `
|
|
4515
4633
|
const targetId = ${delRef};
|
|
4516
4634
|
const item = app.getItemById(targetId);
|
|
@@ -4521,7 +4639,7 @@ return { itemId: targetId, deleted: true };
|
|
|
4521
4639
|
`;
|
|
4522
4640
|
case 'group': {
|
|
4523
4641
|
// Resolve each ref ($N → itemIds[N]; literal → quoted id) into a JS array.
|
|
4524
|
-
const refs = (op.itemIds || []).map((ref) =>
|
|
4642
|
+
const refs = (op.itemIds || []).map((ref) => batchRef(ref));
|
|
4525
4643
|
const idsArrayExpr = `[${refs.join(', ')}]`;
|
|
4526
4644
|
const nameExpr = op.groupName !== undefined ? JSON.stringify(op.groupName) : 'undefined';
|
|
4527
4645
|
// Returns { groupId, ... } (no itemId) so it does NOT shift $N indices — like relation.
|
|
@@ -4533,8 +4651,8 @@ return (function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
|
|
|
4533
4651
|
case 'set_background':
|
|
4534
4652
|
const bgColor = op.backgroundColor || op.color || op.properties?.backgroundColor || op.properties?.color || '#000000';
|
|
4535
4653
|
return `
|
|
4536
|
-
app.setBackgroundColor(
|
|
4537
|
-
return { success: true, backgroundColor:
|
|
4654
|
+
app.setBackgroundColor(${JSON.stringify(String(bgColor))});
|
|
4655
|
+
return { success: true, backgroundColor: ${JSON.stringify(String(bgColor))} };
|
|
4538
4656
|
`;
|
|
4539
4657
|
case 'execute_generator':
|
|
4540
4658
|
const genName = op.generatorName || 'drawSunburst';
|
|
@@ -4565,14 +4683,12 @@ return { success: true, width: (_cs && _cs.width) || ${w}, height: (_cs && _cs.h
|
|
|
4565
4683
|
return `
|
|
4566
4684
|
// Keyframes merged into operation ${foldTarget} (same target). A keyframe track
|
|
4567
4685
|
// is replaced wholesale, so emitting both here would silently drop one.
|
|
4568
|
-
return { itemId: ${
|
|
4686
|
+
return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget} };
|
|
4569
4687
|
`;
|
|
4570
4688
|
}
|
|
4571
4689
|
const mergedPayload = mergePlan?.merged.get(index);
|
|
4572
4690
|
const rawItemId = String(op.itemId ?? op.id ?? op.properties?.itemId ?? op.properties?.id);
|
|
4573
|
-
const kfItemRef = rawItemId
|
|
4574
|
-
? `__ref(${rawItemId.substring(1)})`
|
|
4575
|
-
: `'${rawItemId}'`;
|
|
4691
|
+
const kfItemRef = batchRef(rawItemId);
|
|
4576
4692
|
const kfSource = mergedPayload
|
|
4577
4693
|
? mergedPayload.keyframes
|
|
4578
4694
|
: (op.keyframes || (op.properties && op.properties.keyframes) || (op.params && op.params.keyframes) || []);
|
|
@@ -4614,9 +4730,7 @@ return { success: true, itemId: targetId, duration: ${kfDuration}, loop: ${kfLoo
|
|
|
4614
4730
|
`;
|
|
4615
4731
|
}
|
|
4616
4732
|
case 'apply_mask': {
|
|
4617
|
-
const maskItemRef = op.itemId
|
|
4618
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4619
|
-
: `'${op.itemId}'`;
|
|
4733
|
+
const maskItemRef = batchRef(op.itemId);
|
|
4620
4734
|
const maskOpts = JSON.stringify(op.maskOptions || {});
|
|
4621
4735
|
const preset = op.maskPreset || op.maskType || 'wipeLeft';
|
|
4622
4736
|
return `
|
|
@@ -4633,9 +4747,7 @@ return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
|
|
|
4633
4747
|
`;
|
|
4634
4748
|
}
|
|
4635
4749
|
case 'apply_effect': {
|
|
4636
|
-
const effItemRef = op.itemId
|
|
4637
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4638
|
-
: `'${op.itemId}'`;
|
|
4750
|
+
const effItemRef = batchRef(op.itemId);
|
|
4639
4751
|
const effParams = JSON.stringify(op.effectParams || {});
|
|
4640
4752
|
return `
|
|
4641
4753
|
const targetId = ${effItemRef};
|
|
@@ -5120,6 +5232,19 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
5120
5232
|
// cannot be kept accurate — it breaks on any edit above it — so this repo
|
|
5121
5233
|
// does not use them. Camera animation still
|
|
5122
5234
|
// drives motion during export — framing only fixes the output frame.
|
|
5235
|
+
// THE SCENE LINT, BEFORE THE EXPORT (FxTool lintScene().warnings): what the file will show wrong without
|
|
5236
|
+
// anything failing — a centre-anchored text that runs out of the frame exports as "IGNAL" for "SIGNAL". Read from the
|
|
5237
|
+
// scene as it is, never a refusal; a studio without the rule says nothing, a lint that throws says so.
|
|
5238
|
+
const __lint = (function () {
|
|
5239
|
+
if (typeof app.lintScene !== 'function') return null;
|
|
5240
|
+
try {
|
|
5241
|
+
const r = app.lintScene();
|
|
5242
|
+
return r && Array.isArray(r.warnings) ? r.warnings : null;
|
|
5243
|
+
} catch (e) {
|
|
5244
|
+
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run before the export: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
5245
|
+
}
|
|
5246
|
+
})();
|
|
5247
|
+
|
|
5123
5248
|
let cameraDims = null;
|
|
5124
5249
|
if (framing === 'camera') {
|
|
5125
5250
|
if (!['gif', 'apng', 'mp4', 'webm'].includes(format)) {
|
|
@@ -5668,7 +5793,7 @@ ${stillTime !== undefined ? `
|
|
|
5668
5793
|
if (typeof fr.embedded === 'number') __tl.fonts.embedded = fr.embedded; // cumulative for the document
|
|
5669
5794
|
if (typeof fr.hiddenTextItems === 'number') __tl.fonts.hiddenTextItems = (__tl.fonts.hiddenTextItems || 0) + fr.hiddenTextItems;
|
|
5670
5795
|
}
|
|
5671
|
-
} catch (e) { if (!__tl.fonts) __tl.fonts = { embedded: 0, reason: e && e.message }; }
|
|
5796
|
+
} catch (e) { if (!__tl.fonts) __tl.fonts = { embedded: 0, reason: (e && typeof e.message === 'string' && e.message) || 'embedding the text fonts failed with no reason given' }; }
|
|
5672
5797
|
}
|
|
5673
5798
|
if (jpg) doc.addImage(jpg, 'JPEG', 0, 0, wMM, hMM); else doc.addImage(png, 'PNG', 0, 0, wMM, hMM);
|
|
5674
5799
|
if (__layer) {
|
|
@@ -5676,7 +5801,7 @@ ${stillTime !== undefined ? `
|
|
|
5676
5801
|
const tr = await app.exportEngine.addTextLayer(doc, { x: 0, y: 0, width: wMM, height: hMM });
|
|
5677
5802
|
if (tr && tr.ok !== false) { __tl.written += tr.linesWritten || 0; __tl.skipped += tr.linesSkipped || 0; }
|
|
5678
5803
|
else __tl.reasons.push(id + ': ' + ((tr && tr.reason) || 'refused'));
|
|
5679
|
-
} catch (e) { __tl.reasons.push(id + ': ' + e.message); }
|
|
5804
|
+
} catch (e) { __tl.reasons.push(id + ': ' + ((e && typeof e.message === 'string' && e.message) || 'the text layer failed with no reason given')); }
|
|
5680
5805
|
}
|
|
5681
5806
|
pagesOut.push({ sceneId: id, widthMM: Math.round(wMM * 10) / 10, heightMM: Math.round(hMM * 10) / 10 });
|
|
5682
5807
|
}
|
|
@@ -5739,7 +5864,7 @@ ${stillTime !== undefined ? `
|
|
|
5739
5864
|
result = { success: false, error: 'Unknown format: ' + format };
|
|
5740
5865
|
}
|
|
5741
5866
|
} catch (e) {
|
|
5742
|
-
result = { success: false, error: e.message, platform, format };
|
|
5867
|
+
result = { success: false, error: (e && typeof e.message === 'string' && e.message) || (typeof e === 'string' && e) || ('the ' + format + ' export threw with no message'), platform, format };
|
|
5743
5868
|
}
|
|
5744
5869
|
|
|
5745
5870
|
// One site, after every branch has set its result: a successful export says
|
|
@@ -5777,11 +5902,19 @@ ${stillTime !== undefined ? `
|
|
|
5777
5902
|
result = Object.assign({}, result, { format, data: encoded, mimeType: mime, size: Math.round(String(encoded).length * 0.75), encodeQuality: settings.compression });
|
|
5778
5903
|
}
|
|
5779
5904
|
} catch (e) {
|
|
5780
|
-
result = { success: false, platform, format, error: (e && e.message) ||
|
|
5905
|
+
result = { success: false, platform, format, error: (e && typeof e.message === 'string' && e.message) || (typeof e === 'string' && e) || ('re-encoding the still as ' + format + ' failed with no message') };
|
|
5781
5906
|
}
|
|
5782
5907
|
}
|
|
5783
5908
|
|
|
5784
5909
|
if (result && result.success) Object.assign(result, fidelity(format));
|
|
5910
|
+
if (result && result.success && __lint && __lint.length && !result.lintWarnings) {
|
|
5911
|
+
result.lintWarnings = __lint;
|
|
5912
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5913
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat(__lint.map(function (w) {
|
|
5914
|
+
return { code: String(w.code || 'lint'), message: String(w.message || w.code || 'scene lint warning'), ...(w.itemId ? { items: [w.itemId] } : {}) };
|
|
5915
|
+
}));
|
|
5916
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5917
|
+
}
|
|
5785
5918
|
if (result && result.success && __fit) result.platformFit = __fit;
|
|
5786
5919
|
${usesCanvasSize ? '' : ` // A PRESET SIZE IS NOT THE CANVAS SIZE, AND THAT IS SAID (21 PLATFORMOVERRIDE).
|
|
5787
5920
|
// A named platform renders the FILE at the preset's size; the canvas is not
|
|
@@ -7713,14 +7846,39 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7713
7846
|
// An async IIFE is still a `(`-led expression statement, so the governor
|
|
7714
7847
|
// captures its value, and executeCode's eval fallback already resolves a
|
|
7715
7848
|
// returned promise. Both paths await it; neither needed changing.
|
|
7849
|
+
//
|
|
7850
|
+
// The code runs inside its own async arrow so its `return` is still its value; the value comes back under
|
|
7851
|
+
// __ppValue with the scene lint beside it, and a throw comes back as { step, itemId, reason }, never a bare
|
|
7852
|
+
// message (and never "undefined" for a thrown non-Error).
|
|
7716
7853
|
return `
|
|
7717
7854
|
// ${description}
|
|
7718
7855
|
(async function() {
|
|
7856
|
+
// THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything
|
|
7857
|
+
// failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the
|
|
7858
|
+
// result. A studio without lintScene says nothing.
|
|
7859
|
+
const __ppLintNow = function () {
|
|
7860
|
+
try {
|
|
7861
|
+
if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];
|
|
7862
|
+
const r = app.lintScene();
|
|
7863
|
+
return r && Array.isArray(r.warnings) ? r.warnings : [];
|
|
7864
|
+
} catch (e) {
|
|
7865
|
+
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
7866
|
+
}
|
|
7867
|
+
};
|
|
7868
|
+
let __ppValue;
|
|
7719
7869
|
try {
|
|
7870
|
+
__ppValue = await (async () => {
|
|
7720
7871
|
${validated.code}
|
|
7872
|
+
})();
|
|
7721
7873
|
} catch (error) {
|
|
7722
|
-
|
|
7723
|
-
|
|
7874
|
+
const __ppReason = (error && typeof error.message === 'string' && error.message.trim()) ? error.message
|
|
7875
|
+
: (typeof error === 'string' && error.trim()) ? error : 'the custom code threw with no message';
|
|
7876
|
+
return { success: false, step: 'execute_custom_code', itemId: (error && typeof error.itemId === 'string') ? error.itemId : null,
|
|
7877
|
+
reason: __ppReason, error: __ppReason, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
7878
|
+
}
|
|
7879
|
+
// Only wrapped when the lint has something to say: otherwise the value is returned exactly as before.
|
|
7880
|
+
const __ppL = __ppLintNow();
|
|
7881
|
+
return __ppL.length ? { __ppValue: __ppValue, __ppLint: __ppL } : __ppValue;
|
|
7724
7882
|
})();
|
|
7725
7883
|
`.trim();
|
|
7726
7884
|
}
|
|
@@ -8528,6 +8686,53 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
|
|
|
8528
8686
|
};
|
|
8529
8687
|
})();
|
|
8530
8688
|
`.trim();
|
|
8689
|
+
}
|
|
8690
|
+
/**
|
|
8691
|
+
* pinepaper_pattern (AR-2 P2): one stored declaration moves many items (FxTool app.addPattern / modifyPattern /
|
|
8692
|
+
* removePattern / listPatterns / getPattern). No keyframes are written; the engine evaluates the node per frame.
|
|
8693
|
+
*/
|
|
8694
|
+
generatePattern(input) {
|
|
8695
|
+
const v = PatternInputSchema.parse(input);
|
|
8696
|
+
const S = (x) => JSON.stringify(x);
|
|
8697
|
+
const need = (fn) => `if (typeof app.${fn} !== 'function') return { success: false, error: 'app.${fn} unavailable — this studio predates patterns (FxTool AR-2 P1); update it' };`;
|
|
8698
|
+
const idNeeded = v.action !== 'list' && !v.id ? `return { success: false, error: '${v.action} needs the pattern id' };` : '';
|
|
8699
|
+
let body;
|
|
8700
|
+
switch (v.action) {
|
|
8701
|
+
case 'create': {
|
|
8702
|
+
const spec = { id: v.id, kind: v.kind, members: v.members, params: v.params || {}, ...(v.window ? { window: v.window } : {}) };
|
|
8703
|
+
body = `${need('addPattern')}
|
|
8704
|
+
if (!${S(v.kind ?? null)}) return { success: false, error: 'create needs kind: phase_wave or stagger' };
|
|
8705
|
+
if (!${S(v.members ?? null)}) return { success: false, error: 'create needs members: { names: [...] }, { ids: [...] }, { tag } or an array' };
|
|
8706
|
+
const r = app.addPattern(${S(spec)});
|
|
8707
|
+
return Object.assign({ success: !!(r && r.ok) }, r || {}, app.lastReceipt ? { receipt: app.lastReceipt } : {});`;
|
|
8708
|
+
break;
|
|
8709
|
+
}
|
|
8710
|
+
case 'modify': {
|
|
8711
|
+
const ch = { ...(v.params ? { params: v.params } : {}), ...(v.members ? { members: v.members } : {}), ...(v.window !== undefined ? { window: v.window } : {}) };
|
|
8712
|
+
body = `${need('modifyPattern')}
|
|
8713
|
+
const r = app.modifyPattern(${S(v.id)}, ${S(ch)});
|
|
8714
|
+
return Object.assign({ success: !!(r && r.ok) }, r || {}, app.lastReceipt ? { receipt: app.lastReceipt } : {});`;
|
|
8715
|
+
break;
|
|
8716
|
+
}
|
|
8717
|
+
case 'remove':
|
|
8718
|
+
body = `${need('removePattern')}
|
|
8719
|
+
const had = app.removePattern(${S(v.id)});
|
|
8720
|
+
return had ? { success: true, removed: ${S(v.id)} } : { success: false, error: 'no pattern ' + ${S(JSON.stringify(v.id ?? ''))} };`;
|
|
8721
|
+
break;
|
|
8722
|
+
case 'get':
|
|
8723
|
+
body = `${need('getPattern')}
|
|
8724
|
+
const p = app.getPattern(${S(v.id)});
|
|
8725
|
+
return p ? Object.assign({ success: true }, p) : { success: false, error: 'no pattern ' + ${S(JSON.stringify(v.id ?? ''))} };`;
|
|
8726
|
+
break;
|
|
8727
|
+
default:
|
|
8728
|
+
body = `${need('listPatterns')}
|
|
8729
|
+
const list = app.listPatterns();
|
|
8730
|
+
return { success: true, count: list.length, patterns: list };`;
|
|
8731
|
+
}
|
|
8732
|
+
return `(function() {
|
|
8733
|
+
${idNeeded}
|
|
8734
|
+
${body}
|
|
8735
|
+
})();`;
|
|
8531
8736
|
}
|
|
8532
8737
|
generateStagger(input) {
|
|
8533
8738
|
const validated = StaggerInputSchema.parse(input);
|
|
@@ -10368,6 +10573,9 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10368
10573
|
(async function() {
|
|
10369
10574
|
if (!app.exportEngine || !app.exportEngine.exportWidget) return { error: 'Widget export not available' };
|
|
10370
10575
|
const result = await app.exportEngine.exportWidget(${JSON.stringify(opts)});
|
|
10576
|
+
if (!result || typeof result.json !== 'string' || result.json.length === 0) {
|
|
10577
|
+
return { success: false, step: 'export', error: 'export failed at the export step: exportWidget returned no scene JSON' + (result && result.error ? ' (' + result.error + ')' : '') };
|
|
10578
|
+
}
|
|
10371
10579
|
// THE EMBED BOX WAS ALWAYS 16:9, AND IT IS NOT SELF-CONTAINED (round 9 GG,
|
|
10372
10580
|
// 8.32). The engine writes aspect-ratio: 16/9 and max-width: 800px whatever
|
|
10373
10581
|
// the scene, so a 300x250 ad sat letterboxed in a wide box. The box is sized
|
|
@@ -10403,6 +10611,9 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10403
10611
|
(async function() {
|
|
10404
10612
|
if (!app.exportEngine || !app.exportEngine.exportWidgetHTML) return { error: 'Widget HTML export not available' };
|
|
10405
10613
|
const result = await app.exportEngine.exportWidgetHTML(${JSON.stringify(opts)});
|
|
10614
|
+
if (!result || typeof result.html !== 'string' || result.html.length === 0) {
|
|
10615
|
+
return { success: false, step: 'export', error: 'export failed at the export step: exportWidgetHTML returned no page' + (result && result.error ? ' (' + result.error + ')' : '') };
|
|
10616
|
+
}
|
|
10406
10617
|
${input.lang || input.dir || input.alt ? `// lang / dir / alt on the engine's page (it writes <html lang="en"> and no
|
|
10407
10618
|
// text alternative). Applied as string edits on the one <html> tag and the
|
|
10408
10619
|
// first <canvas>; the result says which landed.
|
|
@@ -11325,7 +11536,10 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11325
11536
|
return { success: false, error: 'app.lintScene unavailable — update FxTool to a build with the relational linter (S12-E2)' };
|
|
11326
11537
|
}
|
|
11327
11538
|
const result = app.lintScene(${optsJson});
|
|
11328
|
-
|
|
11539
|
+
// warnings: what the engine's own rules found (TEXT_CLIPS_FRAME: a centre-anchored text running past the
|
|
11540
|
+
// frame edge). Passed through when the studio reports them; an older studio has none to pass.
|
|
11541
|
+
return { success: true, density: result.density, suggestions: result.suggestions,
|
|
11542
|
+
...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}) };
|
|
11329
11543
|
})();`.trim();
|
|
11330
11544
|
}
|
|
11331
11545
|
/**
|
|
@@ -12251,8 +12465,10 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
|
|
|
12251
12465
|
}
|
|
12252
12466
|
}
|
|
12253
12467
|
/** pinepaper_emitter (D29): create('emitter', spec) / setEmitter(id, patch). */
|
|
12254
|
-
generateEmitter(
|
|
12468
|
+
generateEmitter(raw) {
|
|
12255
12469
|
const S = (v) => JSON.stringify(v);
|
|
12470
|
+
const normalized = normalizeEmitterSpec(raw.spec, raw.action !== 'set');
|
|
12471
|
+
const input = { ...raw, spec: normalized.spec };
|
|
12256
12472
|
// A COUNT OVER THE CAP IS SAID (B144). The cap is 10000 a burst on the
|
|
12257
12473
|
// current studio and was 2000 before it, clamped in silence (2300 drew
|
|
12258
12474
|
// ~1940). Which applies is read from the spec the studio STORED — its
|
|
@@ -12274,7 +12490,10 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
|
|
|
12274
12490
|
}
|
|
12275
12491
|
return out;
|
|
12276
12492
|
}` : '';
|
|
12277
|
-
const
|
|
12493
|
+
const fixed = normalized.warnings;
|
|
12494
|
+
const capJs = (stored) => checks.length || fixed.length
|
|
12495
|
+
? `, ...((function(w) { return w.length ? { warnings: w } : {}; })(${S(fixed)}.concat(${checks.length ? `__capWarnings(${stored})` : '[]'})))`
|
|
12496
|
+
: '';
|
|
12278
12497
|
if (input.action === 'set') {
|
|
12279
12498
|
return `
|
|
12280
12499
|
// Emitter: change a spec
|
|
@@ -13562,10 +13781,15 @@ ${guard('exportPNGSequence')}
|
|
|
13562
13781
|
return Object.assign({ success: true, format: 'png-sequence', data: dataUrl, mimeType: out.type || 'application/zip', size: out.size }, __pngSeqDefaults);
|
|
13563
13782
|
}
|
|
13564
13783
|
if (Array.isArray(out)) {
|
|
13565
|
-
|
|
13784
|
+
// NOT A SAVED FILE, and said so in fields: file null, and why.
|
|
13785
|
+
return { success: true, format: 'png-sequence', frames: out.length, file: null,
|
|
13786
|
+
why: 'the frames stayed in the page: the studio returned them as a list of frames, not a file, so no file was written. Pass download: true to have the browser download them as a zip.',
|
|
13566
13787
|
note: 'the frames stayed in the page — pass download: true to write them out.' };
|
|
13567
13788
|
}
|
|
13568
|
-
|
|
13789
|
+
// download: true — the studio handed the zip to the BROWSER's download, so no bytes reached this server and no file
|
|
13790
|
+
// was written by it. Said in fields, so it cannot be read as a saved file.
|
|
13791
|
+
return { success: true, format: 'png-sequence', result: out, file: null,
|
|
13792
|
+
why: 'the studio handed the zip to the browser as a download (download: true): it went to the browser\\'s download folder, no bytes reached the MCP server, and the server wrote no file.' };
|
|
13569
13793
|
})();`.trim();
|
|
13570
13794
|
}
|
|
13571
13795
|
}
|