@pinepaper.studio/mcp-server 1.6.26 → 1.6.28
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 +155 -2
- package/SECURITY.md +4 -3
- package/dist/agent/types.d.ts +3 -3
- package/dist/browser/puppeteer-controller.d.ts +11 -0
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +38 -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 +6 -3
- package/dist/index.js.map +1 -1
- package/dist/ontology/hints.d.ts +3 -1
- package/dist/ontology/hints.d.ts.map +1 -1
- package/dist/ontology/hints.js +4 -2
- package/dist/ontology/hints.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +2 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +7 -0
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +58 -28
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +20 -3
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +156 -3
- package/dist/tools/engine-lists.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +2 -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 +19 -4
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/export-file-receipt.d.ts +64 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -1
- package/dist/tools/export-file-receipt.js +91 -13
- package/dist/tools/export-file-receipt.js.map +1 -1
- package/dist/tools/expression-grammar.d.ts +10 -0
- package/dist/tools/expression-grammar.d.ts.map +1 -0
- package/dist/tools/expression-grammar.js +33 -0
- package/dist/tools/expression-grammar.js.map +1 -0
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +58 -4
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/generate.js +2 -2
- package/dist/tools/handlers/generate.js.map +1 -1
- package/dist/tools/handlers.d.ts +5 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +74 -2
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +10 -5
- package/dist/tools/lane.d.ts.map +1 -1
- package/dist/tools/lane.js +49 -8
- package/dist/tools/lane.js.map +1 -1
- package/dist/tools/scene-cache-export.d.ts +52 -0
- package/dist/tools/scene-cache-export.d.ts.map +1 -0
- package/dist/tools/scene-cache-export.js +231 -0
- package/dist/tools/scene-cache-export.js.map +1 -0
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.js +2 -2
- 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 +8 -2
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +1 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +536 -255
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +238 -201
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +14 -24
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/asset-paths.d.ts.map +1 -1
- package/dist/utils/asset-paths.js +11 -0
- package/dist/utils/asset-paths.js.map +1 -1
- package/dist/utils/capabilities.d.ts +15 -0
- package/dist/utils/capabilities.d.ts.map +1 -0
- package/dist/utils/capabilities.js +63 -0
- package/dist/utils/capabilities.js.map +1 -0
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/env.js +2 -1
- package/dist/utils/env.js.map +1 -1
- package/manifest.json +18 -2
- package/package.json +1 -1
|
@@ -7,11 +7,18 @@
|
|
|
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 } from '../tools/engine-lists.js';
|
|
10
|
+
import { WORLD3D_COLOR_PATHS, PATTERN_KINDS, EXPRESSION_FUNCTION_ARITY, EXPRESSION_CONSTANTS, EXPRESSION_REJECTED, RELATION_PARAM_KEYS } from '../tools/engine-lists.js';
|
|
11
|
+
import { expressionGrammarText } from '../tools/expression-grammar.js';
|
|
11
12
|
import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
|
|
12
13
|
import { NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
|
|
13
14
|
import { CREATE_KNOWN_KEYS } from '../tools/create-known-keys.js';
|
|
14
15
|
import { jsLiteral } from '../utils/js-literal.js';
|
|
16
|
+
/** A value spliced into a generated `//` comment, on one line: a line break in caller text would end the comment and run the rest as code. */
|
|
17
|
+
/** How many relations pinepaper_lint_scene lists back; past it the result says how many there are. */
|
|
18
|
+
const RELATIONS_READBACK_CAP = 200;
|
|
19
|
+
function oneLine(v) {
|
|
20
|
+
return String(v).replace(/[\r\n\u2028\u2029]+/g, ' ');
|
|
21
|
+
}
|
|
15
22
|
import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
|
|
16
23
|
import { expandBatchMacros } from '../tools/batch-macros.js';
|
|
17
24
|
import { CreateItemInputSchema, ScreenSpaceSchema, StepTimingSchema, CounterSchema, ImportMotionCaptureInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
|
|
@@ -78,7 +85,7 @@ function normalizeGradientSpec(v, key) {
|
|
|
78
85
|
*/
|
|
79
86
|
function generateGradientCode(gradient, itemVarName, position, propertyName = 'fillColor') {
|
|
80
87
|
const { type, stops, origin, destination } = gradient;
|
|
81
|
-
const stopsCode = stops.map(s => `[
|
|
88
|
+
const stopsCode = stops.map(s => `[${jsLiteral(s.color)}, ${s.offset}]`).join(', ');
|
|
82
89
|
if (type === 'radial') {
|
|
83
90
|
// A RADIAL CORE BELONGS ON THE ITEM, NOT ON THE NUMBER ASKED FOR (17
|
|
84
91
|
// RADIALOFFSET). The origin was the REQUESTED position and the radius a
|
|
@@ -126,7 +133,7 @@ ${itemVarName}.${propertyName} = {
|
|
|
126
133
|
function generateShadowCode(itemVarName, shadowColor, shadowBlur, shadowOffset) {
|
|
127
134
|
const lines = [];
|
|
128
135
|
if (shadowColor) {
|
|
129
|
-
lines.push(`${itemVarName}.shadowColor =
|
|
136
|
+
lines.push(`${itemVarName}.shadowColor = ${jsLiteral(shadowColor)};`);
|
|
130
137
|
}
|
|
131
138
|
if (shadowBlur !== undefined) {
|
|
132
139
|
lines.push(`${itemVarName}.shadowBlur = ${shadowBlur};`);
|
|
@@ -140,7 +147,7 @@ function generateShadowCode(itemVarName, shadowColor, shadowBlur, shadowOffset)
|
|
|
140
147
|
* Generate blend mode code for an item
|
|
141
148
|
*/
|
|
142
149
|
function generateBlendModeCode(itemVarName, blendMode) {
|
|
143
|
-
return `${itemVarName}.blendMode =
|
|
150
|
+
return `${itemVarName}.blendMode = ${jsLiteral(blendMode)};`;
|
|
144
151
|
}
|
|
145
152
|
// =============================================================================
|
|
146
153
|
// CODE TEMPLATES
|
|
@@ -155,7 +162,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
155
162
|
// break those later calls: refused here, by name, before anything is made.
|
|
156
163
|
if (typeof properties.id === 'string' && UNSAFE_ID_CHARS.test(properties.id)) {
|
|
157
164
|
return `
|
|
158
|
-
// Create ${itemType} — refused: an id later calls could not address
|
|
165
|
+
// Create ${oneLine(itemType)} — refused: an id later calls could not address
|
|
159
166
|
({ success: false, error: ${jsLiteral(`properties.id ${JSON.stringify(properties.id)} has a quote, backslash or line break, so later calls could not address the item. Nothing was made. Use letters, digits, spaces, - and _ (e.g. "bobs-hat").`)} });`.trim();
|
|
160
167
|
}
|
|
161
168
|
// Colour objects are gradient specs: normalised, or refused by name.
|
|
@@ -165,7 +172,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
165
172
|
const g = normalizeGradientSpec(v, key);
|
|
166
173
|
if ('error' in g) {
|
|
167
174
|
return `
|
|
168
|
-
// Create ${itemType} — refused: an unreadable gradient would draw nothing
|
|
175
|
+
// Create ${oneLine(itemType)} — refused: an unreadable gradient would draw nothing
|
|
169
176
|
({ success: false, error: ${JSON.stringify(g.error)} });`.trim();
|
|
170
177
|
}
|
|
171
178
|
properties = { ...properties, [key]: g.gradient };
|
|
@@ -274,7 +281,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
274
281
|
if (params.opacity != null)
|
|
275
282
|
plate.opacity = params.opacity;
|
|
276
283
|
return `
|
|
277
|
-
// ${itemType} — a render-time surface: a plate tagged with what to draw over it.
|
|
284
|
+
// ${oneLine(itemType)} — a render-time surface: a plate tagged with what to draw over it.
|
|
278
285
|
const item = app.create('rectangle', ${JSON.stringify(plate, null, 2)});
|
|
279
286
|
if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.data.renderParams = ${JSON.stringify(params)}; }
|
|
280
287
|
// The snippet's value is its last statement's, which was the assignment above:
|
|
@@ -284,7 +291,7 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
284
291
|
// and its value is lost.
|
|
285
292
|
((item && item.data)
|
|
286
293
|
? { itemId: item.data.registryId, type: ${JSON.stringify(itemType)}, position: { x: ${position.x}, y: ${position.y} } }
|
|
287
|
-
: { success: false, error: 'the plate for this ${itemType} was not created.' });`;
|
|
294
|
+
: { success: false, error: 'the plate for this ' + ${jsLiteral(itemType)} + ' was not created.' });`;
|
|
288
295
|
}
|
|
289
296
|
// AN UNKNOWN ANCHOR IS INVISIBLE OVER MCP.
|
|
290
297
|
//
|
|
@@ -299,7 +306,7 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
299
306
|
const normalized = String(anchorGiven).toLowerCase().replace(/[\s_]+/g, '-');
|
|
300
307
|
if (!ANCHORS.includes(normalized)) {
|
|
301
308
|
return `
|
|
302
|
-
// Create ${itemType} — refused, rather than silently centred
|
|
309
|
+
// Create ${oneLine(itemType)} — refused, rather than silently centred
|
|
303
310
|
({ success: false, error: ${JSON.stringify(`anchor ${JSON.stringify(String(anchorGiven))} is not one of ${ANCHORS.slice(0, 5).join(', ')}. `
|
|
304
311
|
+ 'The engine would leave x/y as the CENTRE and say so only in a console this tool cannot read, '
|
|
305
312
|
+ 'so the item would land half its own size away with nothing to explain it.')} });`.trim();
|
|
@@ -331,8 +338,8 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
331
338
|
: !CREATE_KNOWN_KEYS.has(k) || textOnly(k));
|
|
332
339
|
// Build the code
|
|
333
340
|
let code = `
|
|
334
|
-
// Create ${itemType} item
|
|
335
|
-
const item = app.create(
|
|
341
|
+
// Create ${oneLine(itemType)} item
|
|
342
|
+
const item = app.create(${jsLiteral(itemType)}, ${JSON.stringify(params, null, 2)});`;
|
|
336
343
|
// Add gradient support for colors
|
|
337
344
|
if (color && isGradient(color)) {
|
|
338
345
|
code += generateGradientCode(color, 'item', position, 'fillColor');
|
|
@@ -413,7 +420,7 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
|
|
|
413
420
|
opts.countdownTarget = countdownTarget;
|
|
414
421
|
if (countdownEndText)
|
|
415
422
|
opts.countdownEndText = countdownEndText;
|
|
416
|
-
code += `\nif (app.setDynamicContent) app.setDynamicContent(item,
|
|
423
|
+
code += `\nif (app.setDynamicContent) app.setDynamicContent(item, ${jsLiteral(contentType)}, ${JSON.stringify(opts)});`;
|
|
417
424
|
}
|
|
418
425
|
const loadsFont = itemType === 'text' && typeof properties.fontFamily === 'string' && properties.fontFamily.trim() !== '';
|
|
419
426
|
// A LINE WITH `from` IS DRAWN WHERE from AND to SAY (issue #29 O8). They are
|
|
@@ -435,7 +442,7 @@ const itemId = item.data.registryId;
|
|
|
435
442
|
app.historyManager.saveState();
|
|
436
443
|
|
|
437
444
|
// Return item info
|
|
438
|
-
({ itemId, type:
|
|
445
|
+
({ itemId, type: ${jsLiteral(itemType)}${outlineOnly ? `, fill: ${JSON.stringify(OUTLINE_NOTE)}` : ''}${textFit ? ', textFit: __textFit' : ''}${textDirection ? ', direction: __direction' : ''}${textTabular ? ', tabularFigures: __tabular' : ''}${createFillOpacity ? ', fillOpacity: __fillOpacity' : ''}${textStrokePos ? ', strokePosition: __strokePos' : ''}, position: ${reportActualPosition ? `(item.position ? { x: item.position.x, y: item.position.y } : null)` : `{ x: ${position.x}, y: ${position.y} }`}${hasLifetime ? ', lifetime: __lifetime' : ''}${loadsFont ? ', ...(__font ? { font: __font } : {})' : ''}${ignored.length > 0
|
|
439
446
|
? `, ignoredProperties: ${JSON.stringify(ignored)}` : ''}${warnings.length > 0 ? `, warning: ${JSON.stringify(warnings.join(' '))}` : ''} });`;
|
|
440
447
|
// Loading a font is async, and it has to happen BEFORE create() measures the
|
|
441
448
|
// text — so only this case becomes an async IIFE; every other create stays a
|
|
@@ -1003,7 +1010,7 @@ if (entry && entry.item && app.removeDynamicContent) app.removeDynamicContent(en
|
|
|
1003
1010
|
opts.countdownEndText = countdownEndText;
|
|
1004
1011
|
code += `
|
|
1005
1012
|
const entry = app.itemRegistry.get(${ID});
|
|
1006
|
-
if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.item,
|
|
1013
|
+
if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.item, ${jsLiteral(contentType)}, ${jsLiteral(opts)});`;
|
|
1007
1014
|
}
|
|
1008
1015
|
}
|
|
1009
1016
|
// AN AUDIO ITEM'S LEVEL IS NOT AN ITEM PROPERTY. modifyItem applies changes
|
|
@@ -1221,6 +1228,8 @@ export function relationSpecProblem(relationType, params) {
|
|
|
1221
1228
|
const p = (params && typeof params === 'object' ? params : {});
|
|
1222
1229
|
if (relationType === 'on_event_fire_if')
|
|
1223
1230
|
return fireIfProblem(p);
|
|
1231
|
+
if (relationType === 'time_expression' && typeof p.expression === 'string')
|
|
1232
|
+
return expressionProblem(p.expression);
|
|
1224
1233
|
if (typeof relationType === 'string' && VISIBILITY_REACTION.test(relationType))
|
|
1225
1234
|
return visibilityProblem(relationType, p);
|
|
1226
1235
|
if (relationType === 'on_event_add_relation' && (p.type === 'on_event_fire_if' || (typeof p.type === 'string' && VISIBILITY_REACTION.test(p.type)))) {
|
|
@@ -1230,6 +1239,129 @@ export function relationSpecProblem(relationType, params) {
|
|
|
1230
1239
|
}
|
|
1231
1240
|
return null;
|
|
1232
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* A time_expression formula outside the engine's grammar parses to nothing, and the relation then does nothing with
|
|
1244
|
+
* success reported (#39: Math.sin(...) from JavaScript habit, no motion). Names are checked against the grammar the
|
|
1245
|
+
* engine publishes (engine-lists, generated from expr-to-ir.js); syntax is the engine's to judge.
|
|
1246
|
+
*/
|
|
1247
|
+
function expressionProblem(expr) {
|
|
1248
|
+
const allowed = new Set([...Object.keys(EXPRESSION_FUNCTION_ARITY), ...EXPRESSION_CONSTANTS, 't', 'v']);
|
|
1249
|
+
const grammar = expressionGrammarText();
|
|
1250
|
+
for (let i = 0; i < expr.length;) {
|
|
1251
|
+
const c = expr[i];
|
|
1252
|
+
if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(expr[i + 1] ?? ''))) {
|
|
1253
|
+
i++;
|
|
1254
|
+
while (i < expr.length && /[0-9.]/.test(expr[i]))
|
|
1255
|
+
i++;
|
|
1256
|
+
if (/[eE]/.test(expr[i] ?? '') && /[0-9+-]/.test(expr[i + 1] ?? '')) {
|
|
1257
|
+
i += 2;
|
|
1258
|
+
while (i < expr.length && /[0-9]/.test(expr[i]))
|
|
1259
|
+
i++;
|
|
1260
|
+
}
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
1264
|
+
let j = i + 1;
|
|
1265
|
+
while (j < expr.length && /[A-Za-z0-9_]/.test(expr[j]))
|
|
1266
|
+
j++;
|
|
1267
|
+
const name = expr.slice(i, j);
|
|
1268
|
+
if (expr[j] === '.') {
|
|
1269
|
+
const member = /^[A-Za-z_][A-Za-z0-9_]*/.exec(expr.slice(j + 1))?.[0] ?? '';
|
|
1270
|
+
return `expression ${JSON.stringify(expr)} uses ${name}.${member}, and the formula is not JavaScript: it would parse to nothing and the item would not move. Write ${member || 'the function'}(…) without ${name}. The grammar: ${grammar}.`;
|
|
1271
|
+
}
|
|
1272
|
+
if (EXPRESSION_REJECTED.includes(name)) {
|
|
1273
|
+
return `expression ${JSON.stringify(expr)} uses ${name}(), which the engine refuses: a formula must give the same value at the same time in every export. Use noise(t) for a varying value that repeats.`;
|
|
1274
|
+
}
|
|
1275
|
+
const arity = EXPRESSION_FUNCTION_ARITY[name];
|
|
1276
|
+
if (arity !== undefined) {
|
|
1277
|
+
// Count the call's top-level arguments: a wrong count parses to nothing as well (min(1, 2, 3)).
|
|
1278
|
+
let k = j;
|
|
1279
|
+
while (expr[k] === ' ')
|
|
1280
|
+
k++;
|
|
1281
|
+
if (expr[k] === '(') {
|
|
1282
|
+
let depth = 0, args = 0, any = false;
|
|
1283
|
+
for (let q = k; q < expr.length; q++) {
|
|
1284
|
+
const ch = expr[q];
|
|
1285
|
+
if (ch === '(') {
|
|
1286
|
+
depth++;
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (ch === ')') {
|
|
1290
|
+
depth--;
|
|
1291
|
+
if (depth === 0)
|
|
1292
|
+
break;
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
if (depth === 1 && ch === ',')
|
|
1296
|
+
args++;
|
|
1297
|
+
if (depth >= 1 && ch.trim())
|
|
1298
|
+
any = true;
|
|
1299
|
+
}
|
|
1300
|
+
const given = any ? args + 1 : 0;
|
|
1301
|
+
if (given !== arity)
|
|
1302
|
+
return `expression ${JSON.stringify(expr)} calls ${name} with ${given} argument${given === 1 ? '' : 's'}; it takes ${arity}, and any other count parses to nothing, so the item would not move.`;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (!allowed.has(name)) {
|
|
1306
|
+
const near = [...allowed].find((a) => a.toLowerCase() === name.toLowerCase());
|
|
1307
|
+
return `expression ${JSON.stringify(expr)} names ${name}, which the formula grammar does not have, so it would parse to nothing and the item would not move.${near ? ` Did you mean ${near}?` : ''} The grammar: ${grammar}.`;
|
|
1308
|
+
}
|
|
1309
|
+
i = j;
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
i++;
|
|
1313
|
+
}
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* A relationOptions key the rule never reads, said instead of accepted in silence. The list is the engine's own
|
|
1318
|
+
* (RELATION_PARAM_KEYS: what each rule declares or reads); a rule without one (custom, event reactions) gets no
|
|
1319
|
+
* warning. `injected` are keys the tool added itself (a name pattern's index and count).
|
|
1320
|
+
*/
|
|
1321
|
+
/** Levenshtein distance, for a "did you mean" on a misspelt key. */
|
|
1322
|
+
function editDistance(a, b) {
|
|
1323
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
1324
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1325
|
+
let prev = row[0];
|
|
1326
|
+
row[0] = i;
|
|
1327
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1328
|
+
const t = row[j];
|
|
1329
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1330
|
+
prev = t;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return row[b.length];
|
|
1334
|
+
}
|
|
1335
|
+
export function relationOptionWarning(relationType, params, injected = []) {
|
|
1336
|
+
const flip = equationFlipNote(relationType, params);
|
|
1337
|
+
const unread = unreadOptionWarning(relationType, params, injected);
|
|
1338
|
+
return unread && flip ? `${unread} ${flip}` : unread ?? flip;
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* An equation route is in math coordinates, y up, unless flipY: false. Authored
|
|
1342
|
+
* as screen coordinates it rode the curve mirrored about cy, every centroid
|
|
1343
|
+
* within a pixel of the mirror and up to 300 px off the curve as written, with
|
|
1344
|
+
* nothing said (ORBDUR26, ASYMORB27). Said when the caller left flipY to the default.
|
|
1345
|
+
*/
|
|
1346
|
+
function equationFlipNote(relationType, params) {
|
|
1347
|
+
if (relationType !== 'moves_along_path' || !params || typeof params !== 'object')
|
|
1348
|
+
return null;
|
|
1349
|
+
const eq = params.equation;
|
|
1350
|
+
if (!eq || typeof eq !== 'object' || eq.flipY !== undefined)
|
|
1351
|
+
return null;
|
|
1352
|
+
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.';
|
|
1353
|
+
}
|
|
1354
|
+
function unreadOptionWarning(relationType, params, injected) {
|
|
1355
|
+
const keys = typeof relationType === 'string' ? RELATION_PARAM_KEYS[relationType] : undefined;
|
|
1356
|
+
if (!keys || !params || typeof params !== 'object')
|
|
1357
|
+
return null;
|
|
1358
|
+
const unknown = Object.keys(params).filter((k) => !keys.includes(k) && !injected.includes(k));
|
|
1359
|
+
if (!unknown.length)
|
|
1360
|
+
return null;
|
|
1361
|
+
const norm = (x) => x.replace(/_/g, '').toLowerCase();
|
|
1362
|
+
const hint = unknown.map((k) => keys.find((x) => norm(x) === norm(k)) ?? keys.find((x) => editDistance(norm(x), norm(k)) <= (k.length > 5 ? 2 : 1))).map((x, i) => (x ? `${unknown[i]} → ${x}?` : '')).filter(Boolean);
|
|
1363
|
+
return `${relationType} does not read ${unknown.join(', ')}, so ${unknown.length > 1 ? 'they were' : 'it was'} ignored${hint.length ? ` (${hint.join('; ')})` : ''}. ${relationType} reads: ${keys.join(', ')}.`;
|
|
1364
|
+
}
|
|
1233
1365
|
const VISIBILITY_REACTION = /^on_(event|enter|exit)_set_visibility$/;
|
|
1234
1366
|
/**
|
|
1235
1367
|
* A visibility reaction reads params.visible and nothing else: absent means show. `value`, which the other set_*
|
|
@@ -1249,23 +1381,23 @@ function generateAddRelationCode(sourceId, targetId, relationType, params) {
|
|
|
1249
1381
|
// an item, the camera, a cursor target — is asked first here, after the same
|
|
1250
1382
|
// '#id' forgiveness the engine applies.
|
|
1251
1383
|
return `
|
|
1252
|
-
// Add ${relationType} relation: ${sourceId} -> ${targetId}
|
|
1384
|
+
// Add ${oneLine(relationType)} relation: ${oneLine(sourceId)} -> ${oneLine(targetId)}
|
|
1253
1385
|
(function() {
|
|
1254
1386
|
const norm = function(id) { return typeof app._resolveRelationEndpoint === 'function' ? app._resolveRelationEndpoint(id) : id; };
|
|
1255
1387
|
if (typeof app._isKnownRelationEndpoint === 'function') {
|
|
1256
1388
|
const bad = [${JSON.stringify(sourceId)}, ${JSON.stringify(targetId)}].filter(function(id) { return !app._isKnownRelationEndpoint(norm(id)); });
|
|
1257
1389
|
if (bad.length) {
|
|
1258
|
-
return { success: false, relationType:
|
|
1390
|
+
return { success: false, relationType: ${jsLiteral(relationType)}, error: bad.map(function(id) { return JSON.stringify(id); }).join(' and ')
|
|
1259
1391
|
+ ' ' + (bad.length > 1 ? 'are not items' : 'is not an item') + ' on the canvas, so the relation would be stored and skipped every frame. Check the id (pinepaper_get_items lists them).' };
|
|
1260
1392
|
}
|
|
1261
1393
|
}
|
|
1262
|
-
const success = app.addRelation(
|
|
1394
|
+
const success = app.addRelation(${jsLiteral(sourceId)}, ${jsLiteral(targetId)}, ${jsLiteral(relationType)}, ${JSON.stringify(params, null, 2)});
|
|
1263
1395
|
if (!success) {
|
|
1264
1396
|
const __why = ${relationRefusalReasonJs(jsLiteral(relationType))};
|
|
1265
|
-
return { success: false, relationType:
|
|
1397
|
+
return { success: false, relationType: ${jsLiteral(relationType)}, error: 'the studio refused the ' + ${jsLiteral(relationType)} + ' relation between ' + ${jsLiteral(sourceId)} + ' and ' + ${jsLiteral(targetId)} + (__why ? ': ' + __why : '.') };
|
|
1266
1398
|
}
|
|
1267
1399
|
app.historyManager.saveState();
|
|
1268
|
-
return { success: true, sourceId:
|
|
1400
|
+
return { success: true, sourceId: ${jsLiteral(sourceId)}, targetId: ${jsLiteral(targetId)}, relationType: ${jsLiteral(relationType)}${(() => { const w = relationOptionWarning(relationType, params); return w ? `, warning: ${jsLiteral(w)}` : ''; })()} };
|
|
1269
1401
|
})();
|
|
1270
1402
|
`.trim();
|
|
1271
1403
|
}
|
|
@@ -1423,7 +1555,7 @@ function generateMovesAlongPathCode(sourceId, targetId, raw) {
|
|
|
1423
1555
|
const hasDuration = typeof duration === 'number' && duration > 0;
|
|
1424
1556
|
const hasDelay = typeof delay === 'number' && delay > 0;
|
|
1425
1557
|
return `
|
|
1426
|
-
// moves_along_path: ${sourceId}${ref ? ` along ${ref}` : ''}
|
|
1558
|
+
// moves_along_path: ${oneLine(sourceId)}${ref ? ` along ${oneLine(ref)}` : ''}
|
|
1427
1559
|
(function() {
|
|
1428
1560
|
const params = ${JSON.stringify(params)};
|
|
1429
1561
|
const hasPoints = Array.isArray(params.path) && params.path.length >= 2;
|
|
@@ -1537,14 +1669,14 @@ function aliasGeneratorParams(name, params) {
|
|
|
1537
1669
|
* Template for removing relations
|
|
1538
1670
|
*/
|
|
1539
1671
|
function generateRemoveRelationCode(sourceId, targetId, relationType) {
|
|
1540
|
-
const relationArg = relationType ?
|
|
1672
|
+
const relationArg = relationType ? jsLiteral(relationType) : 'undefined';
|
|
1541
1673
|
return `
|
|
1542
|
-
// Remove relation: ${sourceId} -> ${targetId}${relationType ? ` (${relationType})` : ''}
|
|
1543
|
-
app.removeRelation(
|
|
1674
|
+
// Remove relation: ${oneLine(sourceId)} -> ${oneLine(targetId)}${relationType ? ` (${oneLine(relationType)})` : ''}
|
|
1675
|
+
app.removeRelation(${jsLiteral(sourceId)}, ${jsLiteral(targetId)}, ${relationArg});
|
|
1544
1676
|
app.historyManager.saveState();
|
|
1545
1677
|
|
|
1546
1678
|
// Return success
|
|
1547
|
-
({ success: true, sourceId:
|
|
1679
|
+
({ success: true, sourceId: ${jsLiteral(sourceId)}, targetId: ${jsLiteral(targetId)} });
|
|
1548
1680
|
`.trim();
|
|
1549
1681
|
}
|
|
1550
1682
|
/**
|
|
@@ -1553,7 +1685,7 @@ app.historyManager.saveState();
|
|
|
1553
1685
|
function generateQueryRelationsCode(itemId, relationType, direction = 'outgoing') {
|
|
1554
1686
|
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1555
1687
|
if (direction === 'outgoing') {
|
|
1556
|
-
const typeArg = relationType ?
|
|
1688
|
+
const typeArg = relationType ? jsLiteral(relationType) : 'undefined';
|
|
1557
1689
|
return `
|
|
1558
1690
|
// Query outgoing relations for ${ID}
|
|
1559
1691
|
const relations = app.getRelations(${ID}, ${typeArg});
|
|
@@ -1574,7 +1706,7 @@ const formatted = relations.map(r => ({
|
|
|
1574
1706
|
`.trim();
|
|
1575
1707
|
}
|
|
1576
1708
|
else {
|
|
1577
|
-
const typeArg = relationType ?
|
|
1709
|
+
const typeArg = relationType ? jsLiteral(relationType) : 'undefined';
|
|
1578
1710
|
return `
|
|
1579
1711
|
// Query incoming relations for ${ID}
|
|
1580
1712
|
const results = app.queryByRelationTarget(${ID}, ${typeArg});
|
|
@@ -1586,7 +1718,7 @@ const relations = results.map(r => ({
|
|
|
1586
1718
|
// queryByTarget returns the real relation name on each result. Hardcoding
|
|
1587
1719
|
// the filter string labelled every edge 'unknown' on an unfiltered query —
|
|
1588
1720
|
// which is exactly the query you make when exploring an unfamiliar graph.
|
|
1589
|
-
relationType: r.relation || ${relationType ?
|
|
1721
|
+
relationType: r.relation || ${relationType ? jsLiteral(relationType) : "'unknown'"},
|
|
1590
1722
|
params: r.params || {}
|
|
1591
1723
|
}));
|
|
1592
1724
|
|
|
@@ -1597,18 +1729,26 @@ const relations = results.map(r => ({
|
|
|
1597
1729
|
/**
|
|
1598
1730
|
* Template for simple animation
|
|
1599
1731
|
*/
|
|
1600
|
-
function generateAnimateCode(itemId, animationType, speed, intensity, delay) {
|
|
1732
|
+
function generateAnimateCode(itemId, animationType, speed, intensity, delay, waveform, origin) {
|
|
1601
1733
|
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1602
1734
|
const extra = (intensity !== undefined ? `,\n animationIntensity: ${intensity}` : '') +
|
|
1603
|
-
(delay !== undefined ? `,\n animationDelay: ${delay}` : '')
|
|
1735
|
+
(delay !== undefined ? `,\n animationDelay: ${delay}` : '') +
|
|
1736
|
+
(waveform !== undefined ? `,\n animationWaveform: ${jsLiteral(waveform)}` : '') +
|
|
1737
|
+
(origin !== undefined ? `,\n animationOrigin: ${jsLiteral(origin)}` : '');
|
|
1738
|
+
// A studio from before field motions (FxTool #255) drops waveform and origin
|
|
1739
|
+
// without a word, and the wave runs as sine from the centre. Read them back.
|
|
1740
|
+
const optionCheck = waveform === undefined && origin === undefined ? '' : `
|
|
1741
|
+
const __unset = [${waveform !== undefined ? `['waveform', 'animationWaveform', ${jsLiteral(waveform)}]` : ''}${waveform !== undefined && origin !== undefined ? ', ' : ''}${origin !== undefined ? `['origin', 'animationOrigin', ${jsLiteral(origin)}]` : ''}]
|
|
1742
|
+
.filter(function (o) { return !item.data || item.data[o[1]] !== o[2]; }).map(function (o) { return o[0]; });
|
|
1743
|
+
if (__unset.length) out.warning = __unset.join(' and ') + ' did not take: this studio predates field-motion options (or refused the value), so the wave runs as sine from the centre. Update the studio.';`;
|
|
1604
1744
|
return `
|
|
1605
|
-
// Apply ${animationType} animation to ${ID}
|
|
1745
|
+
// Apply ${oneLine(animationType)} animation to ${ID}
|
|
1606
1746
|
const item = app.getItemById(${ID});
|
|
1607
1747
|
if (!item) {
|
|
1608
1748
|
throw new Error('Item not found: ' + ${ID});
|
|
1609
1749
|
}
|
|
1610
1750
|
app.animate(item, {
|
|
1611
|
-
animationType:
|
|
1751
|
+
animationType: ${jsLiteral(animationType)},
|
|
1612
1752
|
animationSpeed: ${speed}${extra}
|
|
1613
1753
|
});
|
|
1614
1754
|
|
|
@@ -1627,7 +1767,8 @@ app.animate(item, {
|
|
|
1627
1767
|
known: r.known,
|
|
1628
1768
|
};
|
|
1629
1769
|
}
|
|
1630
|
-
|
|
1770
|
+
const out = { success: true, itemId: ${ID}, animationType: ${jsLiteral(animationType)} };${optionCheck}
|
|
1771
|
+
return out;
|
|
1631
1772
|
})();
|
|
1632
1773
|
`.trim();
|
|
1633
1774
|
}
|
|
@@ -1703,6 +1844,12 @@ const TEXT_KEY_ENGINE_NAMES = {
|
|
|
1703
1844
|
textAlign: ['textAlign', 'justification'],
|
|
1704
1845
|
lineHeight: ['lineHeight', 'leading'],
|
|
1705
1846
|
letterSpacing: ['letterSpacing'],
|
|
1847
|
+
// Text style keys no studio has animated so far. Keyed, they were stored and
|
|
1848
|
+
// drew nothing, with success:true (WORDSPC26). Checked against the same list,
|
|
1849
|
+
// so a studio that learns one is believed and the warning stops.
|
|
1850
|
+
wordSpacing: ['wordSpacing'],
|
|
1851
|
+
textDecoration: ['textDecoration'],
|
|
1852
|
+
textTransform: ['textTransform'],
|
|
1706
1853
|
};
|
|
1707
1854
|
/** A JS expression (run in the page) → the keys of `keys` this studio does not animate. '' when none are text keys. */
|
|
1708
1855
|
function unanimatedTextKeysJs(keys) {
|
|
@@ -1717,7 +1864,7 @@ function unanimatedTextKeysJs(keys) {
|
|
|
1717
1864
|
})()`;
|
|
1718
1865
|
}
|
|
1719
1866
|
/** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
|
|
1720
|
-
const UNANIMATED_TEXT_KEYS_SENTENCE = `' is not an animatable property in this studio (app.listAnimatableProperties does not list it), so it had no effect
|
|
1867
|
+
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 text key animates only where that list names it. Here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX.'`;
|
|
1721
1868
|
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
|
|
1722
1869
|
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1723
1870
|
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
@@ -1868,7 +2015,7 @@ function generateExecuteGeneratorCode(generatorName, params, region) {
|
|
|
1868
2015
|
? ` in region ${region.width}×${region.height} @ (${region.x},${region.y})`
|
|
1869
2016
|
: ` in the shape of ${region.shape.itemId}`;
|
|
1870
2017
|
return `
|
|
1871
|
-
// Execute ${generatorName} generator${where}
|
|
2018
|
+
// Execute ${oneLine(generatorName)} generator${oneLine(where)}
|
|
1872
2019
|
(async function() {${emitGeneratorRun(generatorName, JSON.stringify(params, null, 2), region)}
|
|
1873
2020
|
})();
|
|
1874
2021
|
`.trim();
|
|
@@ -1895,9 +2042,9 @@ function emitGeneratorRun(generatorName, paramsJson, region) {
|
|
|
1895
2042
|
if (!__si) return { success: false, error: ${JSON.stringify(`region.shape names no item "${itemId}" — nothing was drawn. List items with pinepaper_get_items.`)} };
|
|
1896
2043
|
if (__si.className !== 'Path' && __si.className !== 'CompoundPath') return { success: false, error: ${JSON.stringify(`region.shape item "${itemId}" is a `)} + __si.className + ', and a clip needs a path (a shape, not text, a group or an image) — nothing was drawn.' };
|
|
1897
2044
|
}` : ''}
|
|
1898
|
-
await app.executeGenerator(
|
|
2045
|
+
await app.executeGenerator(${jsLiteral(generatorName)}, ${paramsJson}, ${options});${shaped ? `
|
|
1899
2046
|
const __shapes = !!(app.generatorRegistry && typeof app.generatorRegistry._regionClipPath === 'function');` : ''}
|
|
1900
|
-
return { success: true, generator:
|
|
2047
|
+
return { success: true, generator: ${jsLiteral(generatorName)}${region ? `, region: ${JSON.stringify(region)}` : ''}${shaped ? `, ...(__shapes ? {} : { regionShapeApplied: false, warning: ${JSON.stringify(`this studio does not clip generator regions to a shape, so ${typeof region.shape === 'object' ? 'the region had no box and the generator drew across the whole canvas' : `the ${region.shape} was drawn as a rectangle`}. It needs an engine with region shapes.`)} })` : ''} };`;
|
|
1901
2048
|
}
|
|
1902
2049
|
/**
|
|
1903
2050
|
* Template for applying effects
|
|
@@ -1905,14 +2052,14 @@ function emitGeneratorRun(generatorName, paramsJson, region) {
|
|
|
1905
2052
|
function generateApplyEffectCode(itemId, effectType, params) {
|
|
1906
2053
|
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1907
2054
|
return `
|
|
1908
|
-
// Apply ${effectType} effect to ${ID}
|
|
2055
|
+
// Apply ${oneLine(effectType)} effect to ${ID}
|
|
1909
2056
|
const item = app.getItemById(${ID});
|
|
1910
2057
|
if (!item) {
|
|
1911
2058
|
throw new Error('Item not found: ' + ${ID});
|
|
1912
2059
|
}
|
|
1913
|
-
app.applyEffect(item,
|
|
2060
|
+
app.applyEffect(item, ${jsLiteral(effectType)}, ${JSON.stringify(params, null, 2)});
|
|
1914
2061
|
|
|
1915
|
-
({ success: true, itemId: ${ID}, effectType:
|
|
2062
|
+
({ success: true, itemId: ${ID}, effectType: ${jsLiteral(effectType)} });
|
|
1916
2063
|
`.trim();
|
|
1917
2064
|
}
|
|
1918
2065
|
/**
|
|
@@ -1964,9 +2111,9 @@ function generateGetItemsCode(filter) {
|
|
|
1964
2111
|
// conditions, not on the keys.
|
|
1965
2112
|
const conditions = [];
|
|
1966
2113
|
if (filter?.type)
|
|
1967
|
-
conditions.push(`entry.type ===
|
|
2114
|
+
conditions.push(`entry.type === ${jsLiteral(filter.type)}`);
|
|
1968
2115
|
if (filter?.source)
|
|
1969
|
-
conditions.push(`entry.source ===
|
|
2116
|
+
conditions.push(`entry.source === ${jsLiteral(filter.source)}`);
|
|
1970
2117
|
if (filter?.hasAnimation !== undefined) {
|
|
1971
2118
|
// 'none' is what the engine stores on a still item, and !!'none' is true
|
|
1972
2119
|
// (gate H5): animated means a real loop animation OR keyframes.
|
|
@@ -2163,8 +2310,8 @@ app.setPlaybackTime(${time || 0});
|
|
|
2163
2310
|
function generateSetBackgroundColorCode(color) {
|
|
2164
2311
|
return `
|
|
2165
2312
|
// Set background color
|
|
2166
|
-
app.setBackgroundColor(
|
|
2167
|
-
({ success: true, color:
|
|
2313
|
+
app.setBackgroundColor(${jsLiteral(color)});
|
|
2314
|
+
({ success: true, color: ${jsLiteral(color)} });
|
|
2168
2315
|
`.trim();
|
|
2169
2316
|
}
|
|
2170
2317
|
/**
|
|
@@ -2671,7 +2818,7 @@ ${body}
|
|
|
2671
2818
|
} catch (e) { errors.push({ index: ${i}, error: (e && e.message) || String(e) }); }`;
|
|
2672
2819
|
});
|
|
2673
2820
|
return `
|
|
2674
|
-
// Batch create ${items.length} items — create_item for each, one history save
|
|
2821
|
+
// Batch create ${oneLine(items.length)} items — create_item for each, one history save
|
|
2675
2822
|
(async function() {
|
|
2676
2823
|
const results = [];
|
|
2677
2824
|
const errors = [];
|
|
@@ -2704,7 +2851,7 @@ function generateBatchModifyCode(modifications) {
|
|
|
2704
2851
|
} catch (e) { skipped.push({ itemId: ${JSON.stringify(m.itemId)}, reason: (e && e.message) || String(e) }); }`;
|
|
2705
2852
|
});
|
|
2706
2853
|
return `
|
|
2707
|
-
// Batch modify ${modifications.length} items — modify_item for each, one history save
|
|
2854
|
+
// Batch modify ${oneLine(modifications.length)} items — modify_item for each, one history save
|
|
2708
2855
|
(async function() {
|
|
2709
2856
|
let count = 0;
|
|
2710
2857
|
const skipped = [];
|
|
@@ -2726,11 +2873,11 @@ ${blocks.join('\n')}
|
|
|
2726
2873
|
*/
|
|
2727
2874
|
function generateCreateGridCode(cols, rows, strokeColor, strokeWidth, animated, waveSpeed, waveAmplitude) {
|
|
2728
2875
|
return `
|
|
2729
|
-
// Create grid with ${cols} columns and ${rows} rows
|
|
2876
|
+
// Create grid with ${oneLine(cols)} columns and ${oneLine(rows)} rows
|
|
2730
2877
|
const grid = app.createGrid({
|
|
2731
2878
|
cols: ${cols},
|
|
2732
2879
|
rows: ${rows},
|
|
2733
|
-
strokeColor:
|
|
2880
|
+
strokeColor: ${jsLiteral(strokeColor)},
|
|
2734
2881
|
strokeWidth: ${strokeWidth},
|
|
2735
2882
|
animated: ${animated},
|
|
2736
2883
|
waveSpeed: ${waveSpeed},
|
|
@@ -3119,7 +3266,7 @@ export class PinePaperCodeGenerator {
|
|
|
3119
3266
|
// than read as a single string, which would render "[object Object]".
|
|
3120
3267
|
if (validated.presetId) {
|
|
3121
3268
|
return `
|
|
3122
|
-
// Adopt relation preset
|
|
3269
|
+
// Adopt relation preset ${jsLiteral(validated.presetId)} on ${jsLiteral(validated.sourceId)}
|
|
3123
3270
|
(async function() {
|
|
3124
3271
|
if (typeof app.applyRelationPreset !== 'function') { return { success: false, error: 'app.applyRelationPreset unavailable — update FxTool' }; }
|
|
3125
3272
|
const r = await app.applyRelationPreset(${JSON.stringify(validated.sourceId)}, ${JSON.stringify(validated.presetId)}, ${JSON.stringify(validated.presetValues ?? {})});
|
|
@@ -3149,7 +3296,7 @@ export class PinePaperCodeGenerator {
|
|
|
3149
3296
|
if (validated.targetId)
|
|
3150
3297
|
return code;
|
|
3151
3298
|
return code
|
|
3152
|
-
.replace(`return { success: true, sourceId:
|
|
3299
|
+
.replace(`return { success: true, sourceId: ${jsLiteral(validated.sourceId)}, targetId: ${jsLiteral(validated.sourceId)},`, `return { success: true, sourceId: ${jsLiteral(validated.sourceId)}, targetId: null,`)
|
|
3153
3300
|
.replace(`relation between ${validated.sourceId} and ${validated.sourceId}.`, `relation on ${validated.sourceId} (no target).`);
|
|
3154
3301
|
}
|
|
3155
3302
|
/**
|
|
@@ -3169,7 +3316,7 @@ export class PinePaperCodeGenerator {
|
|
|
3169
3316
|
*/
|
|
3170
3317
|
generateAnimate(input) {
|
|
3171
3318
|
const validated = AnimateItemInputSchema.parse(input);
|
|
3172
|
-
return generateAnimateCode(validated.itemId, validated.animationType, validated.speed, validated.intensity, validated.delay);
|
|
3319
|
+
return generateAnimateCode(validated.itemId, validated.animationType, validated.speed, validated.intensity, validated.delay, validated.waveform, validated.origin);
|
|
3173
3320
|
}
|
|
3174
3321
|
/**
|
|
3175
3322
|
* Generate code for keyframe animation
|
|
@@ -3313,7 +3460,7 @@ ${asReturningBody(code)}
|
|
|
3313
3460
|
: `{ item: app.importSVG(${varName}), changes: [] }`;
|
|
3314
3461
|
if (url) {
|
|
3315
3462
|
return `
|
|
3316
|
-
// Import SVG from URL${source === 'figma' ? ' (Figma-normalised)' : ''}
|
|
3463
|
+
// Import SVG from URL${oneLine(source === 'figma' ? ' (Figma-normalised)' : '')}
|
|
3317
3464
|
// WRAPPED, because these were bare top-level statements with an await. That
|
|
3318
3465
|
// parses under the governor, which builds an AsyncFunction, and is a SYNTAX
|
|
3319
3466
|
// ERROR on the eval fallback — so importing an SVG by URL never ran at all on
|
|
@@ -3322,12 +3469,12 @@ ${asReturningBody(code)}
|
|
|
3322
3469
|
(async function() {
|
|
3323
3470
|
let response;
|
|
3324
3471
|
try {
|
|
3325
|
-
response = await fetch(
|
|
3472
|
+
response = await fetch(${jsLiteral(url)});
|
|
3326
3473
|
} catch (netErr) {
|
|
3327
|
-
return { success: false, error: 'could not reach ${url} — ' + ((netErr && netErr.message) || 'network request failed') };
|
|
3474
|
+
return { success: false, error: 'could not reach ' + ${jsLiteral(url)} + ' — ' + ((netErr && netErr.message) || 'network request failed') };
|
|
3328
3475
|
}
|
|
3329
3476
|
if (!response.ok) {
|
|
3330
|
-
return { success: false, error: 'the server refused ${url} — HTTP ' + response.status + ' ' + (response.statusText || ''), status: response.status };
|
|
3477
|
+
return { success: false, error: 'the server refused ' + ${jsLiteral(url)} + ' — HTTP ' + response.status + ' ' + (response.statusText || ''), status: response.status };
|
|
3331
3478
|
}
|
|
3332
3479
|
let svgText = await response.text();
|
|
3333
3480
|
${INLINE_REMOTE_IMAGES}
|
|
@@ -3336,7 +3483,7 @@ ${INLINE_REMOTE_IMAGES}
|
|
|
3336
3483
|
const _r = ${importExpr('svgText')};
|
|
3337
3484
|
const imported = _r.item;
|
|
3338
3485
|
if (!imported) {
|
|
3339
|
-
return { success: false, error: 'the response from ${url} did not parse as SVG. Check the URL serves the file itself rather than a page around it.' };
|
|
3486
|
+
return { success: false, error: 'the response from ' + ${jsLiteral(url)} + ' did not parse as SVG. Check the URL serves the file itself rather than a page around it.' };
|
|
3340
3487
|
}
|
|
3341
3488
|
imported.position = new paper.Point(${position.x}, ${position.y});
|
|
3342
3489
|
imported.scale(${scale});
|
|
@@ -3360,7 +3507,7 @@ ${INLINE_REMOTE_IMAGES}
|
|
|
3360
3507
|
// Escape the SVG string for embedding in code
|
|
3361
3508
|
const escapedSvg = svgString.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
3362
3509
|
return `
|
|
3363
|
-
// Import SVG string${source === 'figma' ? ' (Figma-normalised)' : ''}
|
|
3510
|
+
// Import SVG string${oneLine(source === 'figma' ? ' (Figma-normalised)' : '')}
|
|
3364
3511
|
(async function() {
|
|
3365
3512
|
let svgString = \`${escapedSvg}\`;
|
|
3366
3513
|
${INLINE_REMOTE_IMAGES}
|
|
@@ -3542,9 +3689,9 @@ throw new Error('Either svgString or url must be provided');
|
|
|
3542
3689
|
*/
|
|
3543
3690
|
generateAddFilter(filterType, params = {}) {
|
|
3544
3691
|
return `
|
|
3545
|
-
// Add ${filterType} filter
|
|
3546
|
-
const filterId = app.filterSystem.addFilter(
|
|
3547
|
-
({ success: true, filterId, filterType:
|
|
3692
|
+
// Add ${oneLine(filterType)} filter
|
|
3693
|
+
const filterId = app.filterSystem.addFilter(${jsLiteral(filterType)}, ${JSON.stringify(params, null, 2)});
|
|
3694
|
+
({ success: true, filterId, filterType: ${jsLiteral(filterType)} });
|
|
3548
3695
|
`.trim();
|
|
3549
3696
|
}
|
|
3550
3697
|
/**
|
|
@@ -3573,7 +3720,7 @@ const filterId = app.filterSystem.addFilter('${filterType}', ${JSON.stringify(pa
|
|
|
3573
3720
|
return `
|
|
3574
3721
|
// Create glossy 3D sphere effect
|
|
3575
3722
|
const sphereGroup = new paper.Group({ parent: app.textItemGroup });
|
|
3576
|
-
const baseColor = new paper.Color(
|
|
3723
|
+
const baseColor = new paper.Color(${jsLiteral(baseColor)});
|
|
3577
3724
|
|
|
3578
3725
|
${castShadow ? `// Shadow underneath
|
|
3579
3726
|
const shadow = new paper.Path.Ellipse({
|
|
@@ -3753,7 +3900,7 @@ paper.project.activeLayer.children.forEach(c => {
|
|
|
3753
3900
|
const bg = new paper.Path.Rectangle({
|
|
3754
3901
|
point: [0, 0],
|
|
3755
3902
|
size: [paper.view.size.width, paper.view.size.height],
|
|
3756
|
-
fillColor:
|
|
3903
|
+
fillColor: ${jsLiteral(backgroundColor)},
|
|
3757
3904
|
name: 'background'
|
|
3758
3905
|
});
|
|
3759
3906
|
bg.sendToBack();
|
|
@@ -3763,7 +3910,7 @@ bg.sendToBack();
|
|
|
3763
3910
|
if (backgroundGenerator) {
|
|
3764
3911
|
codeParts.push(`
|
|
3765
3912
|
// Execute background generator
|
|
3766
|
-
const _gen = app.generatorRegistry?.get(
|
|
3913
|
+
const _gen = app.generatorRegistry?.get(${jsLiteral(backgroundGenerator)});
|
|
3767
3914
|
if (typeof _gen === 'function') {
|
|
3768
3915
|
_gen();
|
|
3769
3916
|
} else if (_gen && typeof _gen.generate === 'function') {
|
|
@@ -3795,20 +3942,20 @@ if (typeof _gen === 'function') {
|
|
|
3795
3942
|
...properties,
|
|
3796
3943
|
};
|
|
3797
3944
|
codeParts.push(`
|
|
3798
|
-
// Create item: ${name}
|
|
3945
|
+
// Create item: ${oneLine(name)}
|
|
3799
3946
|
(function() {
|
|
3800
|
-
const item = app.create(
|
|
3947
|
+
const item = app.create(${jsLiteral(itemType)}, ${JSON.stringify(sceneParams)});
|
|
3801
3948
|
if (!item) {
|
|
3802
|
-
results.items.push({ name:
|
|
3949
|
+
results.items.push({ name: ${jsLiteral(name)}, error: 'app.create returned nothing for type ' + ${jsLiteral(itemType)} + ' — check the type and its properties.' });
|
|
3803
3950
|
return;
|
|
3804
3951
|
}
|
|
3805
3952
|
// create() registers and mints the id; only fall back for an older studio.
|
|
3806
3953
|
const itemId = (item.data && item.data.id)
|
|
3807
3954
|
? item.data.id
|
|
3808
|
-
: app.registerItem(item,
|
|
3955
|
+
: app.registerItem(item, ${jsLiteral(itemType)}, { source: 'mcp-scene' });
|
|
3809
3956
|
if (item.bringToFront) item.bringToFront();
|
|
3810
|
-
nameToId[
|
|
3811
|
-
results.items.push({ name:
|
|
3957
|
+
nameToId[${jsLiteral(name)}] = itemId;
|
|
3958
|
+
results.items.push({ name: ${jsLiteral(name)}, itemId, type: ${jsLiteral(itemType)} });
|
|
3812
3959
|
})();`);
|
|
3813
3960
|
}
|
|
3814
3961
|
// Establish relations
|
|
@@ -3818,25 +3965,25 @@ if (typeof _gen === 'function') {
|
|
|
3818
3965
|
const specProblem = relationSpecProblem(type, params);
|
|
3819
3966
|
if (specProblem) {
|
|
3820
3967
|
codeParts.push(`
|
|
3821
|
-
// Refused relation: ${source} ${type} ${target}
|
|
3968
|
+
// Refused relation: ${oneLine(source)} ${oneLine(type)} ${oneLine(target)}
|
|
3822
3969
|
(results.relationsRefused = results.relationsRefused || []).push(${JSON.stringify({ source, target, type, reason: specProblem })});
|
|
3823
3970
|
results.success = false;
|
|
3824
3971
|
results.error = '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.';`);
|
|
3825
3972
|
continue;
|
|
3826
3973
|
}
|
|
3827
3974
|
codeParts.push(`
|
|
3828
|
-
// Add relation: ${source} ${type} ${target}
|
|
3975
|
+
// Add relation: ${oneLine(source)} ${oneLine(type)} ${oneLine(target)}
|
|
3829
3976
|
(function() {
|
|
3830
|
-
const sourceId = nameToId[
|
|
3831
|
-
const targetId = nameToId[
|
|
3977
|
+
const sourceId = nameToId[${jsLiteral(source)}];
|
|
3978
|
+
const targetId = nameToId[${jsLiteral(target)}];
|
|
3832
3979
|
if (sourceId && targetId) {
|
|
3833
3980
|
const params = ${paramsStr};
|
|
3834
3981
|
// A refused relation is not listed as made (owner rule 1).
|
|
3835
|
-
if (app.addRelation(sourceId, targetId,
|
|
3836
|
-
results.relations.push({ source:
|
|
3982
|
+
if (app.addRelation(sourceId, targetId, ${jsLiteral(type)}, params)) {
|
|
3983
|
+
results.relations.push({ source: ${jsLiteral(source)}, target: ${jsLiteral(target)}, type: ${jsLiteral(type)} });
|
|
3837
3984
|
} else {
|
|
3838
3985
|
const __why = ${relationRefusalReasonJs(jsLiteral(type))};
|
|
3839
|
-
(results.relationsRefused = results.relationsRefused || []).push({ source:
|
|
3986
|
+
(results.relationsRefused = results.relationsRefused || []).push({ source: ${jsLiteral(source)}, target: ${jsLiteral(target)}, type: ${jsLiteral(type)}, reason: __why || 'the studio refused it' });
|
|
3840
3987
|
results.success = false;
|
|
3841
3988
|
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.';
|
|
3842
3989
|
}
|
|
@@ -3853,9 +4000,9 @@ results.error = 'refused ' + results.relationsRefused.length + ' relation(s): '
|
|
|
3853
4000
|
...(startTime !== undefined ? { animationDelay: startTime } : {}),
|
|
3854
4001
|
});
|
|
3855
4002
|
codeParts.push(`
|
|
3856
|
-
// Apply animation: ${type} to ${target}
|
|
4003
|
+
// Apply animation: ${oneLine(type)} to ${oneLine(target)}
|
|
3857
4004
|
(function() {
|
|
3858
|
-
const targetId = nameToId[
|
|
4005
|
+
const targetId = nameToId[${jsLiteral(target)}];
|
|
3859
4006
|
if (targetId) {
|
|
3860
4007
|
const targetItem = app.itemRegistry.get(targetId);
|
|
3861
4008
|
if (targetItem) {
|
|
@@ -3865,8 +4012,8 @@ results.error = 'refused ' + results.relationsRefused.length + ' relation(s): '
|
|
|
3865
4012
|
// loops with no end, so there is no duration to map and none is invented
|
|
3866
4013
|
// — staggering the STARTS is what makes a scene read as choreographed
|
|
3867
4014
|
// rather than everything moving at once from frame zero.
|
|
3868
|
-
app.animate(targetItem, { animationType:
|
|
3869
|
-
results.animations.push({ target:
|
|
4015
|
+
app.animate(targetItem, { animationType: ${jsLiteral(type)}, ...params });
|
|
4016
|
+
results.animations.push({ target: ${jsLiteral(target)}, type: ${jsLiteral(type)}${startTime !== undefined ? `, startTime: ${startTime}` : ''} });
|
|
3870
4017
|
}
|
|
3871
4018
|
}
|
|
3872
4019
|
})();
|
|
@@ -3900,7 +4047,7 @@ return results;
|
|
|
3900
4047
|
// ending in \ escapes the closing quote; 'x\\'; evil()' breaks out).
|
|
3901
4048
|
const labelStr = label ? JSON.stringify(label) : 'null';
|
|
3902
4049
|
return `
|
|
3903
|
-
// Create diagram shape: ${shapeType}
|
|
4050
|
+
// Create diagram shape: ${oneLine(shapeType)}
|
|
3904
4051
|
// POSITIONAL: createShape(shapeType, config). The type was being passed
|
|
3905
4052
|
// INSIDE the config object, where nothing reads it.
|
|
3906
4053
|
const shapeConfig = {
|
|
@@ -3911,12 +4058,12 @@ const shapeConfig = {
|
|
|
3911
4058
|
style: ${styleStr}
|
|
3912
4059
|
};
|
|
3913
4060
|
|
|
3914
|
-
const shape = app.diagramSystem.createShape(
|
|
3915
|
-
if (!shape) throw new Error('createShape returned nothing for shapeType
|
|
4061
|
+
const shape = app.diagramSystem.createShape(${jsLiteral(shapeType)}, shapeConfig);
|
|
4062
|
+
if (!shape) throw new Error('createShape returned nothing for shapeType ' + ${jsLiteral(shapeType)} + ' — call pinepaper_get_diagram_shapes for the registered types.');
|
|
3916
4063
|
const itemId = shape.data?.registryId || shape.id;
|
|
3917
4064
|
app.historyManager.saveState();
|
|
3918
4065
|
|
|
3919
|
-
({ itemId, shapeType:
|
|
4066
|
+
({ itemId, shapeType: ${jsLiteral(shapeType)}, position: { x: ${position.x}, y: ${position.y} } });
|
|
3920
4067
|
`.trim();
|
|
3921
4068
|
}
|
|
3922
4069
|
/**
|
|
@@ -3948,12 +4095,12 @@ app.historyManager.saveState();
|
|
|
3948
4095
|
if (label)
|
|
3949
4096
|
config.label = label;
|
|
3950
4097
|
return `
|
|
3951
|
-
// Connect items: ${sourceItemId} -> ${targetItemId}
|
|
3952
|
-
const sourceItem = app.getItemById(
|
|
3953
|
-
const targetItem = app.getItemById(
|
|
4098
|
+
// Connect items: ${oneLine(sourceItemId)} -> ${oneLine(targetItemId)}
|
|
4099
|
+
const sourceItem = app.getItemById(${jsLiteral(sourceItemId)});
|
|
4100
|
+
const targetItem = app.getItemById(${jsLiteral(targetItemId)});
|
|
3954
4101
|
|
|
3955
|
-
if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}
|
|
3956
|
-
if (!targetItem) throw new Error('Target item not found: ${targetItemId}
|
|
4102
|
+
if (!sourceItem) throw new Error('Source item not found: ' + ${jsLiteral(sourceItemId)});
|
|
4103
|
+
if (!targetItem) throw new Error('Target item not found: ' + ${jsLiteral(targetItemId)});
|
|
3957
4104
|
|
|
3958
4105
|
const config = ${JSON.stringify(config, null, 2)};
|
|
3959
4106
|
const connector = app.diagramSystem.connect(sourceItem, targetItem, config);
|
|
@@ -3971,8 +4118,8 @@ app.historyManager.saveState();
|
|
|
3971
4118
|
// mismatch is REPORTED rather than left for a later call to trip over.
|
|
3972
4119
|
({
|
|
3973
4120
|
connectorId,
|
|
3974
|
-
sourceItemId:
|
|
3975
|
-
targetItemId:
|
|
4121
|
+
sourceItemId: ${jsLiteral(sourceItemId)},
|
|
4122
|
+
targetItemId: ${jsLiteral(targetItemId)},
|
|
3976
4123
|
...(${JSON.stringify(id ?? null)} && connectorId !== ${JSON.stringify(id ?? null)}
|
|
3977
4124
|
? { requestedId: ${JSON.stringify(id ?? null)},
|
|
3978
4125
|
idHonoured: false,
|
|
@@ -3994,12 +4141,12 @@ app.historyManager.saveState();
|
|
|
3994
4141
|
const merged = id ? { ...(config ?? {}), id } : config;
|
|
3995
4142
|
const configStr = merged ? JSON.stringify(merged, null, 2) : '{}';
|
|
3996
4143
|
return `
|
|
3997
|
-
// Connect ports: ${sourceItemId}:${sourcePort} -> ${targetItemId}:${targetPort}
|
|
3998
|
-
const sourceItem = app.getItemById(
|
|
3999
|
-
const targetItem = app.getItemById(
|
|
4144
|
+
// Connect ports: ${oneLine(sourceItemId)}:${oneLine(sourcePort)} -> ${oneLine(targetItemId)}:${oneLine(targetPort)}
|
|
4145
|
+
const sourceItem = app.getItemById(${jsLiteral(sourceItemId)});
|
|
4146
|
+
const targetItem = app.getItemById(${jsLiteral(targetItemId)});
|
|
4000
4147
|
|
|
4001
|
-
if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}
|
|
4002
|
-
if (!targetItem) throw new Error('Target item not found: ${targetItemId}
|
|
4148
|
+
if (!sourceItem) throw new Error('Source item not found: ' + ${jsLiteral(sourceItemId)});
|
|
4149
|
+
if (!targetItem) throw new Error('Target item not found: ' + ${jsLiteral(targetItemId)});
|
|
4003
4150
|
|
|
4004
4151
|
const config = ${configStr};
|
|
4005
4152
|
// connectPorts(sourcePort, targetPort, config) takes PORT OBJECTS (gate C8,
|
|
@@ -4024,7 +4171,7 @@ if (!connector) { return { success: false, error: 'the diagram system did not cr
|
|
|
4024
4171
|
const connectorId = connector.id || connector.data?.registryId;
|
|
4025
4172
|
app.historyManager.saveState();
|
|
4026
4173
|
|
|
4027
|
-
({ connectorId, sourceItemId:
|
|
4174
|
+
({ connectorId, sourceItemId: ${jsLiteral(sourceItemId)}, sourcePort: ${jsLiteral(sourcePort)}, targetItemId: ${jsLiteral(targetItemId)}, targetPort: ${jsLiteral(targetPort)} });
|
|
4028
4175
|
`.trim();
|
|
4029
4176
|
}
|
|
4030
4177
|
/**
|
|
@@ -4042,22 +4189,22 @@ app.historyManager.saveState();
|
|
|
4042
4189
|
if (ports !== undefined || count !== undefined) {
|
|
4043
4190
|
const named = [ports !== undefined && 'ports', count !== undefined && 'count'].filter(Boolean).join(' and ');
|
|
4044
4191
|
throw new Error(`add_ports cannot honour ${named}: this engine lays out a whole port SET by type `
|
|
4045
|
-
+ `(portType
|
|
4192
|
+
+ `(portType ${jsLiteral(portType)}), with no per-port placement or count. Drop ${named}, or place `
|
|
4046
4193
|
+ 'individual connection points as items and join them with pinepaper_connect.');
|
|
4047
4194
|
}
|
|
4048
4195
|
return `
|
|
4049
|
-
// Add ports to item: ${itemId}
|
|
4050
|
-
const item = app.getItemById(
|
|
4051
|
-
if (!item) throw new Error('Item not found: ${itemId}
|
|
4196
|
+
// Add ports to item: ${oneLine(itemId)}
|
|
4197
|
+
const item = app.getItemById(${jsLiteral(itemId)});
|
|
4198
|
+
if (!item) throw new Error('Item not found: ' + ${jsLiteral(itemId)});
|
|
4052
4199
|
|
|
4053
4200
|
// The second argument is a TYPE STRING, not a config object. Passing an
|
|
4054
4201
|
// object made the engine iterate it as a list — "t.forEach is not a function".
|
|
4055
|
-
const result = app.diagramSystem.addPorts(item,
|
|
4202
|
+
const result = app.diagramSystem.addPorts(item, ${jsLiteral(portType)});
|
|
4056
4203
|
app.historyManager.saveState();
|
|
4057
4204
|
|
|
4058
4205
|
// addPorts returns the ports it made; older builds returned a count object.
|
|
4059
4206
|
const _added = Array.isArray(result) ? result.length : (result?.portsAdded ?? 0);
|
|
4060
|
-
({ itemId:
|
|
4207
|
+
({ itemId: ${jsLiteral(itemId)}, portsAdded: _added, portType: ${jsLiteral(portType)} });
|
|
4061
4208
|
`.trim();
|
|
4062
4209
|
}
|
|
4063
4210
|
/**
|
|
@@ -4069,7 +4216,7 @@ const _added = Array.isArray(result) ? result.length : (result?.portsAdded ?? 0)
|
|
|
4069
4216
|
const itemIdsStr = itemIds ? JSON.stringify(itemIds) : 'null';
|
|
4070
4217
|
const optionsStr = options ? JSON.stringify(options, null, 2) : '{}';
|
|
4071
4218
|
return `
|
|
4072
|
-
// Apply auto-layout: ${layoutType}
|
|
4219
|
+
// Apply auto-layout: ${oneLine(layoutType)}
|
|
4073
4220
|
// Wrapped: applyLayout is async, and a bare top-level await only works on the
|
|
4074
4221
|
// governed path. A paren-led IIFE also keeps the trailing-expression capture.
|
|
4075
4222
|
(async function() {
|
|
@@ -4085,10 +4232,10 @@ const items = itemIds
|
|
|
4085
4232
|
: (app.itemRegistry?.getAll?.() ?? []);
|
|
4086
4233
|
if (!items.length) throw new Error('auto_layout found no items to lay out' + (itemIds ? ' for the given itemIds' : ' on the canvas'));
|
|
4087
4234
|
|
|
4088
|
-
await app.diagramSystem.applyLayout(items,
|
|
4235
|
+
await app.diagramSystem.applyLayout(items, ${jsLiteral(layoutType)}, options);
|
|
4089
4236
|
app.historyManager.saveState();
|
|
4090
4237
|
|
|
4091
|
-
return { layoutType:
|
|
4238
|
+
return { layoutType: ${jsLiteral(layoutType)}, itemsAffected: items.length, success: true };
|
|
4092
4239
|
})();
|
|
4093
4240
|
`.trim();
|
|
4094
4241
|
}
|
|
@@ -4098,7 +4245,7 @@ return { layoutType: '${layoutType}', itemsAffected: items.length, success: true
|
|
|
4098
4245
|
generateGetDiagramShapes(input) {
|
|
4099
4246
|
const validated = GetDiagramShapesInputSchema.parse(input);
|
|
4100
4247
|
const { category } = validated;
|
|
4101
|
-
const categoryFilter = category ?
|
|
4248
|
+
const categoryFilter = category ? jsLiteral(category) : 'null';
|
|
4102
4249
|
return `
|
|
4103
4250
|
// Get available diagram shapes
|
|
4104
4251
|
const category = ${categoryFilter};
|
|
@@ -4117,7 +4264,7 @@ const shapes = category ? _lib.getByCategory(category) : _lib.getAll();
|
|
|
4117
4264
|
const validated = UpdateConnectorInputSchema.parse(input);
|
|
4118
4265
|
const { connectorId, style, label, labelPosition } = validated;
|
|
4119
4266
|
return `
|
|
4120
|
-
// Update connector: ${connectorId}
|
|
4267
|
+
// Update connector: ${oneLine(connectorId)}
|
|
4121
4268
|
(function() {
|
|
4122
4269
|
// CONNECTORS LIVE IN THE DIAGRAM SYSTEM, NOT THE ITEM REGISTRY (gate D1,
|
|
4123
4270
|
// 1.6.19): getItemById never resolves the id connect returned.
|
|
@@ -4169,7 +4316,7 @@ const shapes = category ? _lib.getByCategory(category) : _lib.getAll();
|
|
|
4169
4316
|
const validated = RemoveConnectorInputSchema.parse(input);
|
|
4170
4317
|
const { connectorId } = validated;
|
|
4171
4318
|
return `
|
|
4172
|
-
// Remove connector: ${connectorId}
|
|
4319
|
+
// Remove connector: ${oneLine(connectorId)}
|
|
4173
4320
|
// Resolved through the diagram system (gate D1, 1.6.19), as update_connector is.
|
|
4174
4321
|
const __cid = ${JSON.stringify(connectorId)};
|
|
4175
4322
|
const connector = (app.diagramSystem && app.diagramSystem.getConnector(__cid)) || app.getItemById(__cid);
|
|
@@ -4178,7 +4325,7 @@ if (!connector) throw new Error('Connector not found: ' + __cid);
|
|
|
4178
4325
|
app.diagramSystem.connectorManager.removeConnector(connector);
|
|
4179
4326
|
app.historyManager.saveState();
|
|
4180
4327
|
|
|
4181
|
-
({ connectorId:
|
|
4328
|
+
({ connectorId: ${jsLiteral(connectorId)}, removed: true });
|
|
4182
4329
|
`.trim();
|
|
4183
4330
|
}
|
|
4184
4331
|
/**
|
|
@@ -4207,17 +4354,17 @@ const isActive = app.diagramSystem.toggle();
|
|
|
4207
4354
|
({ action: 'toggle', active: isActive });
|
|
4208
4355
|
`.trim();
|
|
4209
4356
|
case 'setMode':
|
|
4210
|
-
const modeStr = mode ?
|
|
4211
|
-
const shapeStr = shapeType ? `,
|
|
4357
|
+
const modeStr = mode ? jsLiteral(mode) : "'select'";
|
|
4358
|
+
const shapeStr = shapeType ? `, ${jsLiteral(shapeType)}` : '';
|
|
4212
4359
|
return `
|
|
4213
4360
|
// Set diagram tool mode
|
|
4214
4361
|
app.diagramSystem.setMode(${modeStr}${shapeStr});
|
|
4215
|
-
({ action: 'setMode', mode: ${modeStr}${shapeType ? `, shapeType:
|
|
4362
|
+
({ action: 'setMode', mode: ${modeStr}${shapeType ? `, shapeType: ${jsLiteral(shapeType)}` : ''} });
|
|
4216
4363
|
`.trim();
|
|
4217
4364
|
default:
|
|
4218
4365
|
return `
|
|
4219
4366
|
// Unknown diagram mode action
|
|
4220
|
-
throw new Error('Unknown diagram mode action: ${action}
|
|
4367
|
+
throw new Error('Unknown diagram mode action: ' + ${jsLiteral(action)});
|
|
4221
4368
|
`.trim();
|
|
4222
4369
|
}
|
|
4223
4370
|
}
|
|
@@ -4239,7 +4386,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4239
4386
|
(async function() {
|
|
4240
4387
|
const jobOptions = {
|
|
4241
4388
|
name: ${nameStr},
|
|
4242
|
-
screenshotPolicy:
|
|
4389
|
+
screenshotPolicy: ${jsLiteral(policyStr)},
|
|
4243
4390
|
agentMode: true
|
|
4244
4391
|
};
|
|
4245
4392
|
`;
|
|
@@ -4259,7 +4406,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4259
4406
|
if (canvasPreset) {
|
|
4260
4407
|
code += `
|
|
4261
4408
|
// Set canvas size to preset (platform name → engine preset key)
|
|
4262
|
-
app.setCanvasSize(
|
|
4409
|
+
app.setCanvasSize(${jsLiteral(canvasPresetFor(canvasPreset))});
|
|
4263
4410
|
// Allow canvas resize to take effect
|
|
4264
4411
|
await new Promise(r => setTimeout(r, 50));
|
|
4265
4412
|
`;
|
|
@@ -4303,14 +4450,14 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4303
4450
|
`;
|
|
4304
4451
|
}
|
|
4305
4452
|
code += `
|
|
4306
|
-
// Return job context with canvas size${wantOntology ? ' + initial ontology' : ''}
|
|
4453
|
+
// Return job context with canvas size${oneLine(wantOntology ? ' + initial ontology' : '')}
|
|
4307
4454
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
4308
4455
|
return {
|
|
4309
4456
|
success: true,
|
|
4310
4457
|
jobStarted: true,
|
|
4311
4458
|
name: ${nameStr},
|
|
4312
|
-
screenshotPolicy:
|
|
4313
|
-
canvasPreset: ${canvasPreset ?
|
|
4459
|
+
screenshotPolicy: ${jsLiteral(policyStr)},
|
|
4460
|
+
canvasPreset: ${canvasPreset ? jsLiteral(canvasPreset) : 'null'},
|
|
4314
4461
|
canvasCleared: ${shouldClear},
|
|
4315
4462
|
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }${canvasPreset ? '' : `,
|
|
4316
4463
|
...(_boundedFrom ? { note: 'the canvas was unbounded (it exported the artwork, not the board); it is now fixed at ' + (_cs.width || 800) + 'x' + (_cs.height || 600) + ', so stills and video both export that size.' } : {})`}${wantOntology ? `,
|
|
@@ -4510,7 +4657,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4510
4657
|
if (canvasPreset) {
|
|
4511
4658
|
code += `
|
|
4512
4659
|
// Set canvas to preset (platform name → engine preset key)
|
|
4513
|
-
app.setCanvasSize(
|
|
4660
|
+
app.setCanvasSize(${jsLiteral(canvasPresetFor(canvasPreset))});
|
|
4514
4661
|
// Allow canvas resize to take effect
|
|
4515
4662
|
await new Promise(r => setTimeout(r, 50));
|
|
4516
4663
|
`;
|
|
@@ -4518,14 +4665,14 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4518
4665
|
if (backgroundColor && !preserveBackground) {
|
|
4519
4666
|
code += `
|
|
4520
4667
|
// Set background color
|
|
4521
|
-
app.setBackgroundColor(
|
|
4668
|
+
app.setBackgroundColor(${jsLiteral(backgroundColor)});
|
|
4522
4669
|
`;
|
|
4523
4670
|
}
|
|
4524
4671
|
code += `
|
|
4525
4672
|
// Save state
|
|
4526
4673
|
if (app.historyManager) app.historyManager.saveState();
|
|
4527
4674
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
4528
|
-
return { success: true, reset: true, canvasPreset: ${canvasPreset ?
|
|
4675
|
+
return { success: true, reset: true, canvasPreset: ${canvasPreset ? jsLiteral(canvasPreset) : 'null'}, canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
4529
4676
|
})();
|
|
4530
4677
|
`;
|
|
4531
4678
|
return code.trim();
|
|
@@ -4546,7 +4693,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4546
4693
|
// anything runs; a direct caller gets the op's own failure instead.
|
|
4547
4694
|
const timelines = compileBatchTimelines(operations);
|
|
4548
4695
|
let code = `
|
|
4549
|
-
// Batch execute ${operations.length} operations
|
|
4696
|
+
// Batch execute ${oneLine(operations.length)} operations
|
|
4550
4697
|
(async function() {
|
|
4551
4698
|
// An op's own verdict. Deliberately narrow, matching the server-side rule:
|
|
4552
4699
|
// only an explicit success:false or ok:false is a failure, because most ops
|
|
@@ -4619,7 +4766,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4619
4766
|
const opItemJs = typeof opItem === 'string' && /^\$\d+$/.test(opItem) ? `__refName(${opItem.slice(1)})` : JSON.stringify(opItem);
|
|
4620
4767
|
const opStep = typeof o._step === 'string' ? `op ${index}: ${o._step}` : `op ${index} (${op.type})`;
|
|
4621
4768
|
code += `
|
|
4622
|
-
// Operation ${index}: ${op.type}
|
|
4769
|
+
// Operation ${oneLine(index)}: ${oneLine(op.type)}
|
|
4623
4770
|
try {
|
|
4624
4771
|
const result${index} = await (async () => {
|
|
4625
4772
|
${opCode}
|
|
@@ -4765,7 +4912,24 @@ return __created;
|
|
|
4765
4912
|
// The emitter embeds the id as a literal; a batch ref ($N) is only known
|
|
4766
4913
|
// at run time, so it is generated with a placeholder and the runtime
|
|
4767
4914
|
// variable put in its place.
|
|
4768
|
-
|
|
4915
|
+
// A TOP-LEVEL position moves the item, as it places one on create (#42: it reported modified:true and the
|
|
4916
|
+
// item stayed put; only properties were read). properties.x / y / position, when given, win.
|
|
4917
|
+
const modProps = { ...(op.properties || {}) };
|
|
4918
|
+
const pos = op.position;
|
|
4919
|
+
const hasOwn = modProps.x !== undefined || modProps.y !== undefined || modProps.position !== undefined;
|
|
4920
|
+
if (pos !== undefined && !hasOwn) {
|
|
4921
|
+
const [px, py] = Array.isArray(pos) ? pos : [pos.x, pos.y];
|
|
4922
|
+
if (typeof px === 'number')
|
|
4923
|
+
modProps.x = px;
|
|
4924
|
+
if (typeof py === 'number')
|
|
4925
|
+
modProps.y = py;
|
|
4926
|
+
}
|
|
4927
|
+
if (!Object.keys(modProps).length) {
|
|
4928
|
+
return `
|
|
4929
|
+
return { success: false, itemId: ${itemRef}, error: 'modify changes nothing: pass properties ({"x": 700}, {"fillColor": "#f00"}) or a top-level position.' };
|
|
4930
|
+
`;
|
|
4931
|
+
}
|
|
4932
|
+
const single = generateModifyItemCode('__MID__', modProps).trim()
|
|
4769
4933
|
.replace(/"__MID__"/g, '__mid')
|
|
4770
4934
|
.replace(/__MID__/g, "' + __mid + '")
|
|
4771
4935
|
.replace(/;\s*$/, '');
|
|
@@ -4803,14 +4967,55 @@ return { itemId: targetId, animationType: ${JSON.stringify(String(op.animationTy
|
|
|
4803
4967
|
return `
|
|
4804
4968
|
throw new Error(${JSON.stringify(String(op.relationType) + ': ' + fireIf)});
|
|
4805
4969
|
`;
|
|
4970
|
+
// No targetId is a relation on the item alone (the engine takes a null target, as staggered_with does); a
|
|
4971
|
+
// custom relation whose compute ignores the target needed a stand-in before.
|
|
4972
|
+
const noTarget = op.targetId === undefined || op.targetId === null || op.targetId === '';
|
|
4973
|
+
// A NAME PATTERN AND AN UPDATE (mcp-server#37). sourceId "dot-*" binds every item named like it, in name
|
|
4974
|
+
// order (dot-2 before dot-10), each with index and count added to its params, so "12 → 20 dots" is
|
|
4975
|
+
// adding members. update: true changes the params of the relation already bound instead of adding one.
|
|
4976
|
+
const glob = typeof op.sourceId === 'string' && op.sourceId.includes('*') && !op.sourceId.startsWith('$');
|
|
4977
|
+
if (glob || op.update === true) {
|
|
4978
|
+
const type = jsLiteral(String(op.relationType));
|
|
4979
|
+
const members = glob
|
|
4980
|
+
? `const __re = new RegExp(${jsLiteral('^' + String(op.sourceId).split('*').map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$')});
|
|
4981
|
+
const __members = app.itemRegistry.getAll()
|
|
4982
|
+
.map((e) => ({ id: e.id || e.itemId, name: (e.item && ((e.item.data && e.item.data.name) || e.item.name)) || '' }))
|
|
4983
|
+
.filter((m) => __re.test(m.name))
|
|
4984
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
|
|
4985
|
+
if (!__members.length) throw new Error('no item is named like ' + ${jsLiteral(op.sourceId)} + ': name items with properties.name');`
|
|
4986
|
+
: `const __members = [{ id: ${srcRef} }];
|
|
4987
|
+
if (!app.getItemById(__members[0].id)) throw new Error('Source not found: ' + __members[0].id);`;
|
|
4988
|
+
return `
|
|
4989
|
+
${members}
|
|
4990
|
+
const targetId = ${noTarget ? 'null' : tgtRef};
|
|
4991
|
+
const target = ${noTarget ? 'null' : 'app.getItemById(targetId)'};
|
|
4992
|
+
${noTarget ? '' : "if (!target) throw new Error('Target not found: ' + targetId);\n"}${op.update === true ? "if (typeof app.modifyRelationParams !== 'function') throw new Error('this studio cannot change a bound relation (app.modifyRelationParams): update PinePaper Studio');\n" : ''}const __done = [], __failed = [];
|
|
4993
|
+
__members.forEach((m, i) => {
|
|
4994
|
+
const __p = Object.assign({}, ${relOpts}${glob ? `, { index: i, count: __members.length, pattern: ${jsLiteral(op.sourceId)} }` : ''});
|
|
4995
|
+
const ok = ${op.update === true
|
|
4996
|
+
? `app.modifyRelationParams(m.id, targetId, ${type}, __p) || (targetId === null && app.modifyRelationParams(m.id, undefined, ${type}, __p))`
|
|
4997
|
+
: glob
|
|
4998
|
+
// Already bound (a re-run after adding members): its params change, so index and count follow the new set.
|
|
4999
|
+
? `((app.relationRegistry && app.relationRegistry.getAssociations(m.id, ${type}).some((a) => a.target === targetId || (targetId === null && a.target == null)) && typeof app.modifyRelationParams === 'function')
|
|
5000
|
+
? (app.modifyRelationParams(m.id, targetId, ${type}, __p) || (targetId === null && app.modifyRelationParams(m.id, undefined, ${type}, __p)))
|
|
5001
|
+
: app.addRelation(app.getItemById(m.id), target, ${type}, __p))`
|
|
5002
|
+
: `app.addRelation(app.getItemById(m.id), target, ${type}, __p)`};
|
|
5003
|
+
(ok ? __done : __failed).push(m.id);
|
|
5004
|
+
});
|
|
5005
|
+
if (!__done.length) {
|
|
5006
|
+
const __why = ${op.update === true ? `'no ' + ${type} + ' relation is bound from ' + __failed.join(', ') + (targetId === null ? '' : ' to ' + targetId) + ' to change: bind it first (a relation op without update)'` : relationRefusalReasonJs(type)};
|
|
5007
|
+
return { success: false, relationType: ${type}, sourceIds: __failed, error: ${op.update === true ? '__why' : `'the studio refused the ' + ${type} + ' relation for ' + __failed.join(', ') + ' — nothing was bound' + (__why ? ': ' + __why : '.')`} };
|
|
5008
|
+
}
|
|
5009
|
+
return { success: true, relationType: ${type}, ${op.update === true ? 'updated' : 'bound'}: __done, targetId${glob ? ', count: __members.length' : ''}${(() => { const w = relationOptionWarning(op.relationType, op.relationOptions, ['index', 'count', 'pattern']); return w ? `, optionWarning: ${jsLiteral(w)}` : ''; })()}, ...(__failed.length ? { failed: __failed, warning: __failed.length + ' of ' + __members.length + ' were not ${op.update === true ? 'changed' : 'bound'}: ' + __failed.join(', ') } : {}) };
|
|
5010
|
+
`;
|
|
5011
|
+
}
|
|
4806
5012
|
return `
|
|
4807
5013
|
const sourceId = ${srcRef};
|
|
4808
|
-
const targetId = ${tgtRef};
|
|
5014
|
+
const targetId = ${noTarget ? 'null' : tgtRef};
|
|
4809
5015
|
const source = app.getItemById(sourceId);
|
|
4810
|
-
const target = app.getItemById(targetId);
|
|
5016
|
+
const target = ${noTarget ? 'null' : 'app.getItemById(targetId)'};
|
|
4811
5017
|
if (!source) throw new Error('Source not found: ' + sourceId);
|
|
4812
|
-
if (!target) throw new Error('Target not found: ' + targetId);
|
|
4813
|
-
const relationId = app.addRelation(source, target, ${JSON.stringify(String(op.relationType))}, ${relOpts});
|
|
5018
|
+
${noTarget ? '' : "if (!target) throw new Error('Target not found: ' + targetId);\n"}const relationId = app.addRelation(source, target, ${JSON.stringify(String(op.relationType))}, ${relOpts});
|
|
4814
5019
|
// NOT BOUND IS NOT A SUCCESS (owner rule 1, B119). addRelation answers false
|
|
4815
5020
|
// when it refuses — spring_follow "not loaded yet" — and this op returned
|
|
4816
5021
|
// { relationId: false } with nothing marking it failed, so the batch said
|
|
@@ -4820,7 +5025,7 @@ if (!relationId) {
|
|
|
4820
5025
|
return { success: false, relationId: false, sourceId, targetId, relationType: ${JSON.stringify(op.relationType)},
|
|
4821
5026
|
error: 'the studio refused the ' + ${JSON.stringify(op.relationType)} + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };
|
|
4822
5027
|
}
|
|
4823
|
-
return { relationId, sourceId, targetId, relationType: ${JSON.stringify(String(op.relationType))} };
|
|
5028
|
+
return { relationId, sourceId, targetId, relationType: ${JSON.stringify(String(op.relationType))}${(() => { const w = relationOptionWarning(op.relationType, op.relationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()} };
|
|
4824
5029
|
`;
|
|
4825
5030
|
case 'delete':
|
|
4826
5031
|
const delRef = batchRef(op.itemId);
|
|
@@ -4876,7 +5081,7 @@ return { success: true, width: (_cs && _cs.width) || ${w}, height: (_cs && _cs.h
|
|
|
4876
5081
|
const foldTarget = mergePlan?.foldedInto.get(index);
|
|
4877
5082
|
if (foldTarget !== undefined) {
|
|
4878
5083
|
return `
|
|
4879
|
-
// Keyframes merged into operation ${foldTarget} (same target). A keyframe track
|
|
5084
|
+
// Keyframes merged into operation ${oneLine(foldTarget)} (same target). A keyframe track
|
|
4880
5085
|
// is replaced wholesale, so emitting both here would silently drop one.
|
|
4881
5086
|
return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget} };
|
|
4882
5087
|
`;
|
|
@@ -4906,7 +5111,7 @@ return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget}
|
|
|
4906
5111
|
}
|
|
4907
5112
|
const kfUnits = op.timeUnits;
|
|
4908
5113
|
if (kfUnits === 'seconds' || kfUnits === 'ms')
|
|
4909
|
-
kfExtra.push(`timeUnits:
|
|
5114
|
+
kfExtra.push(`timeUnits: ${jsLiteral(kfUnits)}`);
|
|
4910
5115
|
const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
|
|
4911
5116
|
const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
|
|
4912
5117
|
const kfTextCheck = kfTextKeysJs ? `\nconst _kfIgnored = ${kfTextKeysJs};` : '';
|
|
@@ -4933,12 +5138,12 @@ const targetId = ${maskItemRef};
|
|
|
4933
5138
|
const item = app.getItemById(targetId);
|
|
4934
5139
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
4935
5140
|
if (!app.applyAnimatedMask) throw new Error('Mask system not available');
|
|
4936
|
-
const maskedGroup = app.applyAnimatedMask(item,
|
|
5141
|
+
const maskedGroup = app.applyAnimatedMask(item, ${jsLiteral(preset)}, ${maskOpts});
|
|
4937
5142
|
const maskRej = maskedGroup && maskedGroup.data && maskedGroup.data.maskAnimationRejected;
|
|
4938
5143
|
if (maskRej) {
|
|
4939
5144
|
return { success: false, itemId: targetId, error: 'Masked, but animation preset "' + maskRej.requested + '" is unknown — it will NOT animate.', requested: maskRej.requested, known: maskRej.known };
|
|
4940
5145
|
}
|
|
4941
|
-
return { success: !!maskedGroup, itemId: targetId, preset:
|
|
5146
|
+
return { success: !!maskedGroup, itemId: targetId, preset: ${jsLiteral(preset)} };
|
|
4942
5147
|
`;
|
|
4943
5148
|
}
|
|
4944
5149
|
case 'apply_effect': {
|
|
@@ -4952,12 +5157,12 @@ if (!item) throw new Error('Item not found: ' + targetId);
|
|
|
4952
5157
|
// shape, so an effect the engine refused — an unknown preset, a facade that
|
|
4953
5158
|
// is not loaded — was recorded as applied. The batch cannot report what the
|
|
4954
5159
|
// op never tells it.
|
|
4955
|
-
const _eff = app.applyEffect(item,
|
|
5160
|
+
const _eff = app.applyEffect(item, ${jsLiteral(op.effectType || 'sparkle')}, ${effParams});
|
|
4956
5161
|
if (_eff && (_eff.success === false || _eff.ok === false)) {
|
|
4957
|
-
return { success: false, itemId: targetId, effectType:
|
|
5162
|
+
return { success: false, itemId: targetId, effectType: ${jsLiteral(op.effectType || 'sparkle')},
|
|
4958
5163
|
error: _eff.error || _eff.reason || 'the effect was refused without a reason' };
|
|
4959
5164
|
}
|
|
4960
|
-
return { itemId: targetId, effectType:
|
|
5165
|
+
return { itemId: targetId, effectType: ${jsLiteral(op.effectType || 'sparkle')} };
|
|
4961
5166
|
`;
|
|
4962
5167
|
}
|
|
4963
5168
|
case 'play_timeline': {
|
|
@@ -5005,7 +5210,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5005
5210
|
if (typeof length === 'number')
|
|
5006
5211
|
range.length = length;
|
|
5007
5212
|
return `
|
|
5008
|
-
// Read export chunk at ${offset}
|
|
5213
|
+
// Read export chunk at ${oneLine(offset)}
|
|
5009
5214
|
(async function() {
|
|
5010
5215
|
if (!app.exportEngine || typeof app.exportEngine.readExport !== 'function') {
|
|
5011
5216
|
return { ok: false, reason: 'app.exportEngine.readExport unavailable — update FxTool' };
|
|
@@ -5050,9 +5255,13 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5050
5255
|
}
|
|
5051
5256
|
generateAgentExport(input) {
|
|
5052
5257
|
const validated = AgentExportInputSchema.parse(input);
|
|
5053
|
-
const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, alphaQuantizer, deterministic, motionBlur, ad: adOpts, frames } = validated;
|
|
5258
|
+
const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, alphaQuantizer, deterministic, motionBlur, ad: adOpts, frames, startTime } = validated;
|
|
5054
5259
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
5055
5260
|
const videoEncodeOpts = {};
|
|
5261
|
+
// A RANGE of the timeline (FxTool #250): frames for t in [startTime, startTime + duration), each exactly the frame
|
|
5262
|
+
// a full export has at that time (the engine renders the frames before startTime without encoding them).
|
|
5263
|
+
if (startTime !== undefined)
|
|
5264
|
+
videoEncodeOpts.startTime = startTime;
|
|
5056
5265
|
if (broadcast)
|
|
5057
5266
|
videoEncodeOpts.broadcast = true;
|
|
5058
5267
|
// Headroom defaults to 12 with broadcast (retest of caa9560): on a
|
|
@@ -5172,10 +5381,10 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5172
5381
|
if (exportFormat === 'srt' || exportFormat === 'vtt' || exportFormat === 'scc') {
|
|
5173
5382
|
const fmt = exportFormat;
|
|
5174
5383
|
return `
|
|
5175
|
-
// Export: captions (${fmt}) from text items with a lifetime
|
|
5384
|
+
// Export: captions (${oneLine(fmt)}) from text items with a lifetime
|
|
5176
5385
|
(function() {
|
|
5177
5386
|
if (!app.itemRegistry || typeof app.itemRegistry.getAll !== 'function') {
|
|
5178
|
-
return { success: false, format:
|
|
5387
|
+
return { success: false, format: ${jsLiteral(fmt)}, error: 'this studio cannot list items (app.itemRegistry.getAll) — update PinePaper Studio.' };
|
|
5179
5388
|
}
|
|
5180
5389
|
const texts = app.itemRegistry.getAll().filter(function(e) {
|
|
5181
5390
|
const it = e && e.item;
|
|
@@ -5183,7 +5392,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
5183
5392
|
});
|
|
5184
5393
|
const timed = texts.filter(function(e) { return e.item.data && typeof e.item.data.bornAt === 'number'; });
|
|
5185
5394
|
if (!timed.length) {
|
|
5186
|
-
return { success: false, format:
|
|
5395
|
+
return { success: false, format: ${jsLiteral(fmt)}, error: 'no captions to export: no text item has a lifetime. Stage each caption with properties.bornAt / ttl (seconds) on create_item or modify_item, then export ${fmt}.', untimedText: texts.length };
|
|
5187
5396
|
}
|
|
5188
5397
|
const end = ${videoDuration};
|
|
5189
5398
|
const ts = function(t, sep) {
|
|
@@ -5203,7 +5412,7 @@ ${fmt === 'scc' ? ` return { success: true, format: 'scc', sccCues: cues, cues:
|
|
|
5203
5412
|
return ${fmt === 'srt' ? `(i + 1) + '\\n' + ts(c.start, ',') + ' --> ' + ts(c.stop, ',')` : `ts(c.start, '.') + ' --> ' + ts(c.stop, '.')`} + '\\n' + c.text;
|
|
5204
5413
|
}).join('\\n\\n') + '\\n';
|
|
5205
5414
|
const text = ${fmt === 'vtt' ? `'WEBVTT\\n\\n' + body` : 'body'};
|
|
5206
|
-
return { success: true, format:
|
|
5415
|
+
return { success: true, format: ${jsLiteral(fmt)}, data: text, mimeType: ${jsLiteral(fmt === 'srt' ? 'application/x-subrip' : 'text/vtt')}, size: text.length, cues: cues.length, untimedText: texts.length - timed.length };
|
|
5207
5416
|
})();`.trim();
|
|
5208
5417
|
}
|
|
5209
5418
|
if (exportFormat === 'wav') {
|
|
@@ -5253,7 +5462,7 @@ ${fmt === 'scc' ? ` return { success: true, format: 'scc', sccCues: cues, cues:
|
|
|
5253
5462
|
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
|
|
5254
5463
|
}
|
|
5255
5464
|
return {
|
|
5256
|
-
success: true, format: 'wav', platform:
|
|
5465
|
+
success: true, format: 'wav', platform: ${jsLiteral(platform)},
|
|
5257
5466
|
data: 'data:audio/wav;base64,' + btoa(bin),
|
|
5258
5467
|
size: bytes.length,
|
|
5259
5468
|
duration: r.duration, sampleRate: r.sampleRate, sounds: r.sounds, placed: r.placed,
|
|
@@ -5264,12 +5473,14 @@ ${fmt === 'scc' ? ` return { success: true, format: 'scc', sccCues: cues, cues:
|
|
|
5264
5473
|
})();`.trim();
|
|
5265
5474
|
}
|
|
5266
5475
|
return `
|
|
5267
|
-
// Smart export for ${platform}
|
|
5476
|
+
// Smart export for ${oneLine(platform)}
|
|
5268
5477
|
(async function() {${ENSURE_EXPORT_ENGINE}
|
|
5269
|
-
const platform =
|
|
5270
|
-
const format =
|
|
5271
|
-
|
|
5272
|
-
|
|
5478
|
+
const platform = ${jsLiteral(platform)};
|
|
5479
|
+
const format = ${jsLiteral(exportFormat)};${startTime !== undefined ? `
|
|
5480
|
+
if (format !== 'mp4' && format !== 'webm') return { success: false, format, error: 'startTime exports a range of the timeline, which only mp4 and webm can: ' + format + ' would start from 0.' };
|
|
5481
|
+
if (!(app.exportEngine && app.exportEngine.supportsTimeRange === true)) return { success: false, format, error: 'this studio cannot export a range of the timeline (startTime): update PinePaper Studio, or export from 0.' };` : ''}
|
|
5482
|
+
const quality = ${jsLiteral(qualityLevel)};
|
|
5483
|
+
const framing = ${jsLiteral(framing)};
|
|
5273
5484
|
// An explicit fps wins over the tier's. The tiers bundle frame rate with
|
|
5274
5485
|
// compression (draft 15 / standard 30 / high 60), so "higher quality" also
|
|
5275
5486
|
// doubled the frame count and the render time — a surprise the schema used
|
|
@@ -6438,7 +6649,7 @@ ${(() => {
|
|
|
6438
6649
|
if (__bs.stale) {
|
|
6439
6650
|
result.stale = true;
|
|
6440
6651
|
result.fidelity = result.fidelity || { warnings: [] };
|
|
6441
|
-
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
|
|
6652
|
+
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.' }]);
|
|
6442
6653
|
}
|
|
6443
6654
|
}
|
|
6444
6655
|
} catch (_) { /* older studio */ }
|
|
@@ -6450,7 +6661,7 @@ ${(() => {
|
|
|
6450
6661
|
if (__rl && __rl.enabled) {
|
|
6451
6662
|
result.fidelity = result.fidelity || { warnings: [] };
|
|
6452
6663
|
if (['png', 'jpg', 'webp', 'pdf', 'mp4', 'webm'].indexOf(${JSON.stringify(format)}) === -1) {
|
|
6453
|
-
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'relight_not_applied', message: 'a relight rig is set, but ${format} is not relit. Export png, pdf, mp4 or webm to keep the lighting.' }]);
|
|
6664
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'relight_not_applied', message: 'a relight rig is set, but ' + ${jsLiteral(format)} + ' is not relit. Export png, pdf, mp4 or webm to keep the lighting.' }]);
|
|
6454
6665
|
} else {
|
|
6455
6666
|
result.relight = { applied: true, lights: (__rl.lights || []).length };
|
|
6456
6667
|
}
|
|
@@ -6681,7 +6892,7 @@ ${(() => {
|
|
|
6681
6892
|
const validated = AnimateLetterCollageInputSchema.parse(input);
|
|
6682
6893
|
const { collageId, animationType, staggerDelay = 0.1, animationSpeed = 1, } = validated;
|
|
6683
6894
|
return `
|
|
6684
|
-
// Animate letter collage: ${collageId}
|
|
6895
|
+
// Animate letter collage: ${oneLine(collageId)}
|
|
6685
6896
|
(function() {
|
|
6686
6897
|
const collageId = ${JSON.stringify(collageId)};
|
|
6687
6898
|
const animationType = ${JSON.stringify(animationType)};
|
|
@@ -6778,10 +6989,10 @@ ${(() => {
|
|
|
6778
6989
|
}
|
|
6779
6990
|
|
|
6780
6991
|
try {
|
|
6781
|
-
const result = await app.mapSystem.loadMap(
|
|
6992
|
+
const result = await app.mapSystem.loadMap(${jsLiteral(mapId)}, ${optionsStr});
|
|
6782
6993
|
return {
|
|
6783
6994
|
success: true,
|
|
6784
|
-
mapId: result.mapId ||
|
|
6995
|
+
mapId: result.mapId || ${jsLiteral(mapId)},
|
|
6785
6996
|
// regionPaths is a Map: .length was undefined, so every load reported
|
|
6786
6997
|
// regions: 0 (gate D3). size, with the feature count as a fallback.
|
|
6787
6998
|
regions: (result.regions && (result.regions.size ?? result.regions.length)) || (result.geoData && result.geoData.features ? result.geoData.features.length : 0),
|
|
@@ -7198,7 +7409,7 @@ ${(() => {
|
|
|
7198
7409
|
}
|
|
7199
7410
|
|
|
7200
7411
|
try {
|
|
7201
|
-
const result = await app.mapSystem.importCustomMap(
|
|
7412
|
+
const result = await app.mapSystem.importCustomMap(${jsLiteral(validated.url)}, ${optionsStr});
|
|
7202
7413
|
// A summary, not the engine's return: that carries the Paper group and
|
|
7203
7414
|
// the region Map, which serialised into a raw Paper.js dump (gate N1).
|
|
7204
7415
|
const __r = result && result.regions;
|
|
@@ -7316,7 +7527,7 @@ ${(() => {
|
|
|
7316
7527
|
loop: ${validated.loop !== false},
|
|
7317
7528
|
colors: ${colorsStr},${validated.waveSpeed !== undefined ? `
|
|
7318
7529
|
waveSpeed: ${validated.waveSpeed},` : ''}
|
|
7319
|
-
waveDirection:
|
|
7530
|
+
waveDirection: ${jsLiteral(validated.waveDirection || 'horizontal')}
|
|
7320
7531
|
});
|
|
7321
7532
|
return { success: true, animatedRegions: result?.animatedRegions || [], totalRegions: result?.totalRegions || 0 };
|
|
7322
7533
|
} catch (error) {
|
|
@@ -7388,7 +7599,7 @@ ${(() => {
|
|
|
7388
7599
|
includeSelected: ${validated.includeSelected !== false},
|
|
7389
7600
|
includeColors: ${validated.includeColors !== false},
|
|
7390
7601
|
download: ${validated.download === true},
|
|
7391
|
-
filename:
|
|
7602
|
+
filename: ${jsLiteral(validated.filename || 'map-regions.csv')}
|
|
7392
7603
|
});
|
|
7393
7604
|
return { success: true, csv: result?.csv || '', regionCount: result?.regionCount || 0 };
|
|
7394
7605
|
} catch (error) {
|
|
@@ -7645,7 +7856,7 @@ ${(() => {
|
|
|
7645
7856
|
|
|
7646
7857
|
const item = (app.getItemById && app.getItemById(${JSON.stringify(itemId)})) || (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(${JSON.stringify(itemId)}) && (app.itemRegistry.get(${JSON.stringify(itemId)}).item || app.itemRegistry.get(${JSON.stringify(itemId)})));
|
|
7647
7858
|
if (!item) {
|
|
7648
|
-
return { success: false, error: 'Item ${itemId} not found' };
|
|
7859
|
+
return { success: false, error: 'Item ' + ${jsLiteral(itemId)} + ' not found' };
|
|
7649
7860
|
}
|
|
7650
7861
|
|
|
7651
7862
|
try {
|
|
@@ -7674,7 +7885,7 @@ ${(() => {
|
|
|
7674
7885
|
|
|
7675
7886
|
const item = (app.getItemById && app.getItemById(${JSON.stringify(itemId)})) || (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(${JSON.stringify(itemId)}) && (app.itemRegistry.get(${JSON.stringify(itemId)}).item || app.itemRegistry.get(${JSON.stringify(itemId)})));
|
|
7676
7887
|
if (!item) {
|
|
7677
|
-
return { success: false, error: 'Item ${itemId} not found' };
|
|
7888
|
+
return { success: false, error: 'Item ' + ${jsLiteral(itemId)} + ' not found' };
|
|
7678
7889
|
}
|
|
7679
7890
|
|
|
7680
7891
|
try {
|
|
@@ -8047,10 +8258,10 @@ ${(() => {
|
|
|
8047
8258
|
const paramsStr = validated.params ? JSON.stringify(validated.params) : '{}';
|
|
8048
8259
|
const templatesStr = validated.templates ? JSON.stringify(validated.templates) : '[]';
|
|
8049
8260
|
return `
|
|
8050
|
-
// Register custom relation: ${validated.name}
|
|
8261
|
+
// Register custom relation: ${oneLine(validated.name)}
|
|
8051
8262
|
(function() {
|
|
8052
8263
|
try {
|
|
8053
|
-
app.registerRelationRule(
|
|
8264
|
+
app.registerRelationRule(${jsLiteral(validated.name)}, {
|
|
8054
8265
|
description: ${JSON.stringify(validated.description || '')},
|
|
8055
8266
|
params: ${paramsStr},
|
|
8056
8267
|
compute: (ctx) => {
|
|
@@ -8064,7 +8275,11 @@ ${(() => {
|
|
|
8064
8275
|
continuous: ${validated.continuous !== false},
|
|
8065
8276
|
priority: ${validated.priority || 0}
|
|
8066
8277
|
});
|
|
8067
|
-
|
|
8278
|
+
// The definition as given, kept on the page so a later call can read it back (pinepaper_lint_scene
|
|
8279
|
+
// customRelations): the engine keeps the compiled rule, not the text an agent would edit.
|
|
8280
|
+
window.__ppCustomRelations = window.__ppCustomRelations || {};
|
|
8281
|
+
window.__ppCustomRelations[${jsLiteral(validated.name)}] = ${jsLiteral({ name: validated.name, ...(validated.description ? { description: validated.description } : {}), params: validated.params ?? {}, computeFunction: validated.computeFunction, applyFunction: validated.applyFunction })};
|
|
8282
|
+
return { success: true, relationName: ${jsLiteral(validated.name)}, message: 'Custom relation registered successfully' };
|
|
8068
8283
|
} catch (error) {
|
|
8069
8284
|
return { success: false, error: error.message };
|
|
8070
8285
|
}
|
|
@@ -8095,7 +8310,7 @@ ${(() => {
|
|
|
8095
8310
|
// __ppValue with the scene lint beside it, and a throw comes back as { step, itemId, reason }, never a bare
|
|
8096
8311
|
// message (and never "undefined" for a thrown non-Error).
|
|
8097
8312
|
return `
|
|
8098
|
-
// ${description}
|
|
8313
|
+
// ${oneLine(description)}
|
|
8099
8314
|
(async function() {
|
|
8100
8315
|
// THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything
|
|
8101
8316
|
// failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the
|
|
@@ -8151,21 +8366,21 @@ ${(() => {
|
|
|
8151
8366
|
// Per-preset options. Custom keyframes are an animated-mask extension —
|
|
8152
8367
|
// when supplied, fall through to maskingSystem.applyCustomMask instead.
|
|
8153
8368
|
return `
|
|
8154
|
-
// Apply animated mask to ${validated.itemId}
|
|
8369
|
+
// Apply animated mask to ${oneLine(validated.itemId)}
|
|
8155
8370
|
(function() {
|
|
8156
8371
|
if (!app.applyAnimatedMask) {
|
|
8157
8372
|
return { success: false, error: 'Mask system not available' };
|
|
8158
8373
|
}
|
|
8159
|
-
const item = app.getItemById(
|
|
8160
|
-
if (!item) return { success: false, error: 'Item not found: ${validated.itemId}
|
|
8374
|
+
const item = app.getItemById(${jsLiteral(validated.itemId)});
|
|
8375
|
+
if (!item) return { success: false, error: 'Item not found: ' + ${jsLiteral(validated.itemId)} };
|
|
8161
8376
|
|
|
8162
8377
|
try {
|
|
8163
8378
|
const mergedOpts = Object.assign({}, ${optionsStr}, ${maskOptionsStr});
|
|
8164
8379
|
let maskedGroup;
|
|
8165
8380
|
if (${keyframesStr} && app.maskingSystem && app.maskingSystem.applyCustomMask) {
|
|
8166
|
-
maskedGroup = app.maskingSystem.applyCustomMask(item,
|
|
8381
|
+
maskedGroup = app.maskingSystem.applyCustomMask(item, ${jsLiteral(customShape)}, ${keyframesStr}, mergedOpts);
|
|
8167
8382
|
} else {
|
|
8168
|
-
maskedGroup = app.applyAnimatedMask(item,
|
|
8383
|
+
maskedGroup = app.applyAnimatedMask(item, ${jsLiteral(preset)}, mergedOpts);
|
|
8169
8384
|
}
|
|
8170
8385
|
// A mask with an unknown ANIMATION preset still masks — the item just never
|
|
8171
8386
|
// animates — so !!maskedGroup is true either way. The engine records which.
|
|
@@ -8173,13 +8388,13 @@ ${(() => {
|
|
|
8173
8388
|
if (rej) {
|
|
8174
8389
|
return {
|
|
8175
8390
|
success: false,
|
|
8176
|
-
itemId:
|
|
8391
|
+
itemId: ${jsLiteral(validated.itemId)},
|
|
8177
8392
|
error: 'Masked, but the animation preset "' + rej.requested + '" is unknown — it will NOT animate.',
|
|
8178
8393
|
requested: rej.requested,
|
|
8179
8394
|
known: rej.known,
|
|
8180
8395
|
};
|
|
8181
8396
|
}
|
|
8182
|
-
return { success: !!maskedGroup, itemId:
|
|
8397
|
+
return { success: !!maskedGroup, itemId: ${jsLiteral(validated.itemId)}, preset: ${jsLiteral(preset)} };
|
|
8183
8398
|
} catch (error) {
|
|
8184
8399
|
return { success: false, error: error.message };
|
|
8185
8400
|
}
|
|
@@ -8191,17 +8406,17 @@ ${(() => {
|
|
|
8191
8406
|
const keyframesStr = JSON.stringify(validated.keyframes);
|
|
8192
8407
|
const maskOptionsStr = validated.maskOptions ? JSON.stringify(validated.maskOptions) : '{}';
|
|
8193
8408
|
return `
|
|
8194
|
-
// Apply custom mask to ${validated.itemId}
|
|
8409
|
+
// Apply custom mask to ${oneLine(validated.itemId)}
|
|
8195
8410
|
(function() {
|
|
8196
8411
|
if (!app.maskingSystem || !app.maskingSystem.applyCustomMask) {
|
|
8197
8412
|
return { success: false, error: 'Mask system not available' };
|
|
8198
8413
|
}
|
|
8199
|
-
const item = app.getItemById(
|
|
8200
|
-
if (!item) return { success: false, error: 'Item not found: ${validated.itemId}
|
|
8414
|
+
const item = app.getItemById(${jsLiteral(validated.itemId)});
|
|
8415
|
+
if (!item) return { success: false, error: 'Item not found: ' + ${jsLiteral(validated.itemId)} };
|
|
8201
8416
|
|
|
8202
8417
|
try {
|
|
8203
|
-
const maskedGroup = app.maskingSystem.applyCustomMask(item,
|
|
8204
|
-
return { success: !!maskedGroup, itemId:
|
|
8418
|
+
const maskedGroup = app.maskingSystem.applyCustomMask(item, ${jsLiteral(validated.maskType)}, ${keyframesStr}, ${maskOptionsStr});
|
|
8419
|
+
return { success: !!maskedGroup, itemId: ${jsLiteral(validated.itemId)}, maskType: ${jsLiteral(validated.maskType)} };
|
|
8205
8420
|
} catch (error) {
|
|
8206
8421
|
return { success: false, error: error.message };
|
|
8207
8422
|
}
|
|
@@ -8211,20 +8426,20 @@ ${(() => {
|
|
|
8211
8426
|
generateRemoveMask(input) {
|
|
8212
8427
|
const validated = RemoveMaskInputSchema.parse(input);
|
|
8213
8428
|
return `
|
|
8214
|
-
// Remove mask from ${validated.itemId}
|
|
8429
|
+
// Remove mask from ${oneLine(validated.itemId)}
|
|
8215
8430
|
(function() {
|
|
8216
8431
|
if (!app.removeMask) {
|
|
8217
8432
|
return { success: false, error: 'Mask system not available' };
|
|
8218
8433
|
}
|
|
8219
8434
|
// The item id may point at either the masked group OR the original item;
|
|
8220
8435
|
// app.removeMask expects the masked group, so resolve and walk up if needed.
|
|
8221
|
-
const item = app.getItemById(
|
|
8222
|
-
if (!item) return { success: false, error: 'Item not found: ${validated.itemId}
|
|
8436
|
+
const item = app.getItemById(${jsLiteral(validated.itemId)});
|
|
8437
|
+
if (!item) return { success: false, error: 'Item not found: ' + ${jsLiteral(validated.itemId)} };
|
|
8223
8438
|
const maskedGroup = (item.data && item.data.isMaskedGroup) ? item : (item.parent && item.parent.data && item.parent.data.isMaskedGroup ? item.parent : item);
|
|
8224
8439
|
|
|
8225
8440
|
try {
|
|
8226
8441
|
app.removeMask(maskedGroup);
|
|
8227
|
-
return { success: true, itemId:
|
|
8442
|
+
return { success: true, itemId: ${jsLiteral(validated.itemId)}, message: 'Mask removed' };
|
|
8228
8443
|
} catch (error) {
|
|
8229
8444
|
return { success: false, error: error.message };
|
|
8230
8445
|
}
|
|
@@ -8253,7 +8468,7 @@ ${(() => {
|
|
|
8253
8468
|
return { success: false, error: 'app.listAnimatableProperties unavailable — update the studio. The keyframe tool lists the properties it accepts in its own description meanwhile.' };
|
|
8254
8469
|
}
|
|
8255
8470
|
const item = app.getItemById(${JSON.stringify(itemId)});
|
|
8256
|
-
if (!item) { return { success: false, error: 'no item ${itemId}
|
|
8471
|
+
if (!item) { return { success: false, error: 'no item ' + ${jsLiteral(itemId)} }; }
|
|
8257
8472
|
const r = app.listAnimatableProperties(item);
|
|
8258
8473
|
return { success: true, ...r };
|
|
8259
8474
|
})();`.trim();
|
|
@@ -8351,7 +8566,7 @@ ${(() => {
|
|
|
8351
8566
|
generateApplyTemplate(input) {
|
|
8352
8567
|
const { templateId, category, listOnly } = input;
|
|
8353
8568
|
if (listOnly || !templateId) {
|
|
8354
|
-
const categoryFilter = category ?
|
|
8569
|
+
const categoryFilter = category ? jsLiteral(category) : 'null';
|
|
8355
8570
|
return `
|
|
8356
8571
|
// List available templates
|
|
8357
8572
|
(async function() {
|
|
@@ -8379,7 +8594,7 @@ ${(() => {
|
|
|
8379
8594
|
`.trim();
|
|
8380
8595
|
}
|
|
8381
8596
|
return `
|
|
8382
|
-
// Apply template: ${templateId}
|
|
8597
|
+
// Apply template: ${oneLine(templateId)}
|
|
8383
8598
|
(async function() {
|
|
8384
8599
|
if (!app.templateManager) {
|
|
8385
8600
|
return { error: 'Template manager not available. Make sure PinePaper Studio is loaded.' };
|
|
@@ -8426,7 +8641,7 @@ ${(() => {
|
|
|
8426
8641
|
let fidelity = {};
|
|
8427
8642
|
try {
|
|
8428
8643
|
const tpl = (app.templateManager.getAllTemplates() || [])
|
|
8429
|
-
.find(function(t) { return t && (t.id ===
|
|
8644
|
+
.find(function(t) { return t && (t.id === ${jsLiteral(templateId)} || t.templateId === ${jsLiteral(templateId)}); });
|
|
8430
8645
|
const items = tpl && tpl.data && Array.isArray(tpl.data.items) ? tpl.data.items : null;
|
|
8431
8646
|
const declared = items ? items.filter(function(i) { return i && i.type === 'group' && i.paperJSON; }).length : 0;
|
|
8432
8647
|
if (declared > 0 && app.itemRegistry && typeof app.itemRegistry.getAll === 'function') {
|
|
@@ -8447,7 +8662,7 @@ ${(() => {
|
|
|
8447
8662
|
}
|
|
8448
8663
|
} catch (e) { /* a check that cannot run must not fail the load */ }
|
|
8449
8664
|
|
|
8450
|
-
return { success: true, templateId:
|
|
8665
|
+
return { success: true, templateId: ${jsLiteral(templateId)}, message: 'Template applied successfully. Canvas has been replaced with template content.', ...fidelity };
|
|
8451
8666
|
} catch (e) {
|
|
8452
8667
|
return { error: 'Failed to apply template: ' + e.message };
|
|
8453
8668
|
}
|
|
@@ -8497,7 +8712,7 @@ ${(() => {
|
|
|
8497
8712
|
// The bytes then go in as a DATA URL, which img.src loads without touching
|
|
8498
8713
|
// the network again — so a host that allows fetch but blocks the image
|
|
8499
8714
|
// load (CORS on <img>, a tainted-canvas refusal) now works too.
|
|
8500
|
-
let src =
|
|
8715
|
+
let src = ${jsLiteral(url)};
|
|
8501
8716
|
// A STAGED KEY, not the bytes. The handler writes anything over ~64KB to
|
|
8502
8717
|
// window.__ppStage and passes the key instead, because the governor's
|
|
8503
8718
|
// loop-guard transform bails on a code string that large and takes the
|
|
@@ -8559,7 +8774,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
8559
8774
|
// refused instead of answered with the raster's bounds.
|
|
8560
8775
|
const masked = app.imageTools.applyMask(raster, ${JSON.stringify(mask)});
|
|
8561
8776
|
if (!masked) {
|
|
8562
|
-
return { success: false, itemId: itemId, error: 'the ${mask} mask was not applied — the image may not be a raster yet. The unmasked image is on the canvas as ' + itemId + '.' };
|
|
8777
|
+
return { success: false, itemId: itemId, error: 'the ' + ${jsLiteral(mask)} + ' mask was not applied — the image may not be a raster yet. The unmasked image is on the canvas as ' + itemId + '.' };
|
|
8563
8778
|
}
|
|
8564
8779
|
if (app.itemRegistry && typeof app.itemRegistry.rebind === 'function') app.itemRegistry.rebind(itemId, masked);
|
|
8565
8780
|
placed = masked;
|
|
@@ -8586,7 +8801,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
8586
8801
|
success: true,
|
|
8587
8802
|
itemId: itemId,
|
|
8588
8803
|
message: 'Image imported and placed on canvas.',${name ? `\n name: ${JSON.stringify(name)},` : ''}
|
|
8589
|
-
bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask:
|
|
8804
|
+
bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: ${jsLiteral(mask)}` : ''}${hasShadow ? ',\n shadow: __shadow' : ''},
|
|
8590
8805
|
// Stored smaller than its source (the studio's cap): said, with the
|
|
8591
8806
|
// way to keep every pixel. The studio reports native size where it can.
|
|
8592
8807
|
...((entry && entry.nativeWidth && entry.width && entry.nativeWidth > entry.width)
|
|
@@ -8680,7 +8895,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
8680
8895
|
(function() {
|
|
8681
8896
|
${guard}
|
|
8682
8897
|
const scene = app.sceneManager.getScene(${JSON.stringify(input.sceneId)});
|
|
8683
|
-
if (!scene) return { error: 'Scene not found: ${input.sceneId}
|
|
8898
|
+
if (!scene) return { error: 'Scene not found: ' + ${jsLiteral(input.sceneId)} };
|
|
8684
8899
|
return { success: true, action: 'info', scene: scene };
|
|
8685
8900
|
})();
|
|
8686
8901
|
`.trim();
|
|
@@ -8960,8 +9175,17 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
|
|
|
8960
9175
|
}
|
|
8961
9176
|
case 'modify': {
|
|
8962
9177
|
const ch = { ...(v.params ? { params: v.params } : {}), ...(v.members ? { members: v.members } : {}), ...(v.window !== undefined ? { window: v.window } : {}) };
|
|
9178
|
+
// A pattern made with members as a plain array is stored as { refs }, a
|
|
9179
|
+
// shape the engine's own re-check does not accept, so a params-only
|
|
9180
|
+
// modify of it failed with "members is …" (PATIMG24). The stored refs
|
|
9181
|
+
// are handed back as the array they came from.
|
|
9182
|
+
const keep = v.members ? '' : `
|
|
9183
|
+
const __cur = typeof app.getPattern === 'function' ? app.getPattern(${S(v.id)}) : null;
|
|
9184
|
+
const __m = __cur && __cur.spec && __cur.spec.members;
|
|
9185
|
+
if (__m && Array.isArray(__m.refs)) __ch.members = __m.refs.slice();`;
|
|
8963
9186
|
body = `${need('modifyPattern')}
|
|
8964
|
-
const
|
|
9187
|
+
const __ch = ${S(ch)};${keep}
|
|
9188
|
+
const r = app.modifyPattern(${S(v.id)}, __ch);
|
|
8965
9189
|
return Object.assign({ success: !!(r && r.ok) }, r || {}, app.lastReceipt ? { receipt: app.lastReceipt } : {});`;
|
|
8966
9190
|
break;
|
|
8967
9191
|
}
|
|
@@ -9144,7 +9368,7 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
|
|
|
9144
9368
|
selected.push(id);
|
|
9145
9369
|
}
|
|
9146
9370
|
}
|
|
9147
|
-
return { success: true, action: 'select', mode:
|
|
9371
|
+
return { success: true, action: 'select', mode: ${jsLiteral(mode)}, selected };
|
|
9148
9372
|
})();`.trim();
|
|
9149
9373
|
}
|
|
9150
9374
|
case 'select_all':
|
|
@@ -9201,7 +9425,7 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
|
|
|
9201
9425
|
*/
|
|
9202
9426
|
_facadeCall(method, args, label) {
|
|
9203
9427
|
return `
|
|
9204
|
-
// ${label}
|
|
9428
|
+
// ${oneLine(label)}
|
|
9205
9429
|
(async function() {
|
|
9206
9430
|
if (typeof app.${method} !== 'function') {
|
|
9207
9431
|
return { success: false, error: 'app.${method}() unavailable — update PinePaper Studio to a build that has it.' };
|
|
@@ -9223,7 +9447,7 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
|
|
|
9223
9447
|
// studios, and a brand must not cross between them.
|
|
9224
9448
|
if (input.action === 'get' || input.action === 'clear') {
|
|
9225
9449
|
return `
|
|
9226
|
-
// Brand kit: ${input.action} the active brand
|
|
9450
|
+
// Brand kit: ${oneLine(input.action)} the active brand
|
|
9227
9451
|
(function() {
|
|
9228
9452
|
const b = (typeof window !== 'undefined' && window.__ppActiveBrand) || null;
|
|
9229
9453
|
${input.action === 'clear' ? 'if (typeof window !== "undefined") window.__ppActiveBrand = null;' : ''}
|
|
@@ -9480,7 +9704,7 @@ ${loads}
|
|
|
9480
9704
|
__src = raster.url;
|
|
9481
9705
|
}`;
|
|
9482
9706
|
const call = (method, args, label) => `
|
|
9483
|
-
// ${label}
|
|
9707
|
+
// ${oneLine(label)}
|
|
9484
9708
|
(async function() {
|
|
9485
9709
|
if (typeof app.${method} !== 'function') {
|
|
9486
9710
|
return { success: false, error: 'app.${method}() unavailable — update PinePaper Studio to a build that has it.' };
|
|
@@ -9624,7 +9848,7 @@ ${resolveSource}
|
|
|
9624
9848
|
case 'fit': {
|
|
9625
9849
|
const mode = input.mode || 'contain';
|
|
9626
9850
|
return `
|
|
9627
|
-
// Fit item to the export frame (${mode})
|
|
9851
|
+
// Fit item to the export frame (${oneLine(mode)})
|
|
9628
9852
|
(function() {
|
|
9629
9853
|
// Route through the agent facade, not app.fitToFrame directly: an agent that
|
|
9630
9854
|
// just called upload_video holds a MEDIA id (vraster_…), which is a different
|
|
@@ -9667,7 +9891,7 @@ ${resolveSource}
|
|
|
9667
9891
|
case 'flip': {
|
|
9668
9892
|
const dir = input.direction || 'horizontal';
|
|
9669
9893
|
return `
|
|
9670
|
-
// Flip item ${dir}ly
|
|
9894
|
+
// Flip item ${oneLine(dir)}ly
|
|
9671
9895
|
(function() {
|
|
9672
9896
|
const entry = app.itemRegistry.get(${ID});
|
|
9673
9897
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
|
|
@@ -9688,7 +9912,7 @@ ${resolveSource}
|
|
|
9688
9912
|
if (needsSibling) {
|
|
9689
9913
|
const siblingExpr = order === 'moveUp' ? 'entry.item.nextSibling' : 'entry.item.previousSibling';
|
|
9690
9914
|
return `
|
|
9691
|
-
// Reorder item: ${order}
|
|
9915
|
+
// Reorder item: ${oneLine(order)}
|
|
9692
9916
|
(function() {
|
|
9693
9917
|
const entry = app.itemRegistry.get(${ID});
|
|
9694
9918
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
|
|
@@ -9698,7 +9922,7 @@ ${resolveSource}
|
|
|
9698
9922
|
})();`.trim();
|
|
9699
9923
|
}
|
|
9700
9924
|
return `
|
|
9701
|
-
// Reorder item: ${order}
|
|
9925
|
+
// Reorder item: ${oneLine(order)}
|
|
9702
9926
|
(function() {
|
|
9703
9927
|
const entry = app.itemRegistry.get(${ID});
|
|
9704
9928
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
|
|
@@ -9788,7 +10012,7 @@ ${resolve}
|
|
|
9788
10012
|
// otherwise; a falsy answer is a refusal it did not raise.
|
|
9789
10013
|
if (r === false || r === null) {
|
|
9790
10014
|
return { success: false, action: 'apply', itemId: ${JSON.stringify(input.itemId)},
|
|
9791
|
-
error: 'the filter did not apply — ${input.filterName || ''} may not be available on this studio.' };
|
|
10015
|
+
error: 'the filter did not apply — ' + ${jsLiteral(input.filterName || '')} + ' may not be available on this studio.' };
|
|
9792
10016
|
}
|
|
9793
10017
|
return { success: true, action: 'apply', itemId: ${JSON.stringify(input.itemId)}, filter: ${JSON.stringify(input.filterName || '')} };
|
|
9794
10018
|
} catch (e) {
|
|
@@ -10143,7 +10367,7 @@ ${resolve}
|
|
|
10143
10367
|
// Get item details by ID
|
|
10144
10368
|
(function() {
|
|
10145
10369
|
const entry = app.itemRegistry.get(${JSON.stringify(input.itemId || '')});
|
|
10146
|
-
if (!entry || !entry.item) return { error: 'Item not found: ${input.itemId}
|
|
10370
|
+
if (!entry || !entry.item) return { error: 'Item not found: ' + ${jsLiteral(input.itemId)} };
|
|
10147
10371
|
const item = entry.item;
|
|
10148
10372
|
return {
|
|
10149
10373
|
success: true, action: 'get_by_id',
|
|
@@ -10443,8 +10667,8 @@ ${resolve}
|
|
|
10443
10667
|
${guard}
|
|
10444
10668
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
10445
10669
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
10446
|
-
app.deformPresets.apply(entry.item,
|
|
10447
|
-
return { success: true, action: 'apply', itemId: ${itemIdStr}, preset:
|
|
10670
|
+
app.deformPresets.apply(entry.item, ${jsLiteral(input.preset)}, ${JSON.stringify(params)});
|
|
10671
|
+
return { success: true, action: 'apply', itemId: ${itemIdStr}, preset: ${jsLiteral(input.preset)} };
|
|
10448
10672
|
})();`.trim();
|
|
10449
10673
|
}
|
|
10450
10674
|
case 'trigger': {
|
|
@@ -10464,8 +10688,8 @@ ${resolve}
|
|
|
10464
10688
|
${guard}
|
|
10465
10689
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
10466
10690
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
10467
|
-
app.deformPresets.trigger(entry.item,
|
|
10468
|
-
return { success: true, action: 'trigger', itemId: ${itemIdStr}, preset:
|
|
10691
|
+
app.deformPresets.trigger(entry.item, ${jsLiteral(input.preset)}, ${JSON.stringify(params)});
|
|
10692
|
+
return { success: true, action: 'trigger', itemId: ${itemIdStr}, preset: ${jsLiteral(input.preset)} };
|
|
10469
10693
|
})();`.trim();
|
|
10470
10694
|
}
|
|
10471
10695
|
case 'remove': {
|
|
@@ -10718,8 +10942,8 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10718
10942
|
// Export sprite sheet
|
|
10719
10943
|
(async function() {
|
|
10720
10944
|
${guard}
|
|
10721
|
-
const result = await app.exportSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, { format:
|
|
10722
|
-
return { success: true, action: 'export', format:
|
|
10945
|
+
const result = await app.exportSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, { format: ${jsLiteral(input.format || 'png')}, download: true, includeMetadata: true });
|
|
10946
|
+
return { success: true, action: 'export', format: ${jsLiteral(input.format || 'png')} };
|
|
10723
10947
|
})();`.trim();
|
|
10724
10948
|
default:
|
|
10725
10949
|
return `(function() { return { error: 'Unknown sprite sheet action: ${input.action}' }; })();`;
|
|
@@ -10740,8 +10964,8 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10740
10964
|
${guard}
|
|
10741
10965
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
10742
10966
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
10743
|
-
const behaviorId = app.interactionSystem.addContinuousBehavior(entry.item,
|
|
10744
|
-
return { success: true, action: 'add_behavior', itemId: ${itemIdStr}, behaviorType:
|
|
10967
|
+
const behaviorId = app.interactionSystem.addContinuousBehavior(entry.item, ${jsLiteral(input.behaviorType)}, ${params});
|
|
10968
|
+
return { success: true, action: 'add_behavior', itemId: ${itemIdStr}, behaviorType: ${jsLiteral(input.behaviorType)}, behaviorId };
|
|
10745
10969
|
})();`.trim();
|
|
10746
10970
|
}
|
|
10747
10971
|
case 'remove_behavior': {
|
|
@@ -10798,8 +11022,8 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10798
11022
|
// Trigger interaction action
|
|
10799
11023
|
(function() {
|
|
10800
11024
|
${guard}
|
|
10801
|
-
app.interactionSystem.executeAction(
|
|
10802
|
-
return { success: true, action: 'trigger_action', actionType:
|
|
11025
|
+
app.interactionSystem.executeAction(${jsLiteral(input.actionType)}, ${params});
|
|
11026
|
+
return { success: true, action: 'trigger_action', actionType: ${jsLiteral(input.actionType)} };
|
|
10803
11027
|
})();`.trim();
|
|
10804
11028
|
}
|
|
10805
11029
|
case 'get_state':
|
|
@@ -11226,14 +11450,14 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
11226
11450
|
}[input.action];
|
|
11227
11451
|
const id = JSON.stringify(input.itemId);
|
|
11228
11452
|
return `
|
|
11229
|
-
// Arrange ${input.itemId} → ${input.action}
|
|
11453
|
+
// Arrange ${oneLine(input.itemId)} → ${oneLine(input.action)}
|
|
11230
11454
|
(function() {
|
|
11231
11455
|
const item = app.getItemById(${id});
|
|
11232
11456
|
if (!item) return { success: false, error: 'Item not found: ' + ${id} };
|
|
11233
11457
|
if (typeof app.${method} !== 'function') return { success: false, error: 'app.${method} unavailable — update FxTool' };
|
|
11234
11458
|
app.select(${id});
|
|
11235
11459
|
app.${method}();
|
|
11236
|
-
return { success: true, itemId: ${id}, action:
|
|
11460
|
+
return { success: true, itemId: ${id}, action: ${jsLiteral(input.action)} };
|
|
11237
11461
|
})();`.trim();
|
|
11238
11462
|
}
|
|
11239
11463
|
/**
|
|
@@ -11266,7 +11490,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
11266
11490
|
...(input.queries !== undefined ? { queries: input.queries } : {}),
|
|
11267
11491
|
});
|
|
11268
11492
|
return `
|
|
11269
|
-
// Detect objects${input.queries !== undefined ? ' (open-vocabulary / OWL-ViT)' : ''}${input.asNodes ? ' (asNodes — relational compositing)' : ''}
|
|
11493
|
+
// Detect objects${oneLine(input.queries !== undefined ? ' (open-vocabulary / OWL-ViT)' : '')}${oneLine(input.asNodes ? ' (asNodes — relational compositing)' : '')}
|
|
11270
11494
|
(async function() {
|
|
11271
11495
|
if (typeof app.detectObjects !== 'function') {
|
|
11272
11496
|
return { success: false, error: 'app.detectObjects unavailable — update FxTool to a build that exposes on-device object detection on app' };
|
|
@@ -11296,7 +11520,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
11296
11520
|
...(input.id !== undefined ? { id: input.id } : {}),
|
|
11297
11521
|
};
|
|
11298
11522
|
return `
|
|
11299
|
-
// Styled scene${input.style ? ` (${input.style.replace(/[\r\n]/g, ' ')})` : ''}
|
|
11523
|
+
// Styled scene${input.style ? ` (${oneLine(input.style.replace(/[\r\n]/g, ' '))})` : ''}
|
|
11300
11524
|
(async function() {
|
|
11301
11525
|
if (typeof app.styledScene !== 'function') { return { success: false, error: 'app.styledScene unavailable — update FxTool' }; }
|
|
11302
11526
|
const r = await app.styledScene(${JSON.stringify(input.spec ?? {})}, ${JSON.stringify(opts)});
|
|
@@ -11316,7 +11540,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
11316
11540
|
...(input.threshold !== undefined ? { threshold: input.threshold } : {}),
|
|
11317
11541
|
});
|
|
11318
11542
|
return `
|
|
11319
|
-
// Extract object${input.label ? `
|
|
11543
|
+
// Extract object${input.label ? ` ${jsLiteral(input.label)}` : ''} from the image
|
|
11320
11544
|
(async function() {
|
|
11321
11545
|
if (typeof app.extractObject !== 'function') {
|
|
11322
11546
|
return { success: false, error: 'app.extractObject unavailable — update FxTool to a build that exposes on-device object detection on app' };
|
|
@@ -11406,7 +11630,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
11406
11630
|
if (input.action === 'break_apart') {
|
|
11407
11631
|
const idExpr = JSON.stringify(input.itemId || '');
|
|
11408
11632
|
return `
|
|
11409
|
-
// Break apart ${input.itemId || ''} into movable parts
|
|
11633
|
+
// Break apart ${oneLine(input.itemId || '')} into movable parts
|
|
11410
11634
|
(function() {
|
|
11411
11635
|
if (typeof app.breakApart !== 'function') return { success: false, error: 'app.breakApart unavailable — update FxTool to a build with SVG/group segmentation' };
|
|
11412
11636
|
const target = app.getItemById(${idExpr});
|
|
@@ -11421,7 +11645,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
11421
11645
|
if (input.action === 'ungroup') {
|
|
11422
11646
|
const idExpr = JSON.stringify(input.groupId || '');
|
|
11423
11647
|
return `
|
|
11424
|
-
// Ungroup ${input.groupId || ''}
|
|
11648
|
+
// Ungroup ${oneLine(input.groupId || '')}
|
|
11425
11649
|
(function() {
|
|
11426
11650
|
if (!app.groupManager) return { success: false, error: 'GroupManager not available — update FxTool' };
|
|
11427
11651
|
const gid = ${idExpr};
|
|
@@ -11435,7 +11659,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
11435
11659
|
const idsArrayExpr = JSON.stringify(input.itemIds ?? []);
|
|
11436
11660
|
const nameExpr = input.groupName !== undefined ? JSON.stringify(input.groupName) : 'undefined';
|
|
11437
11661
|
return `
|
|
11438
|
-
// Group ${(input.itemIds || []).length} items${input.groupName ? ` as
|
|
11662
|
+
// Group ${oneLine((input.itemIds || []).length)} items${input.groupName ? ` as ${jsLiteral(input.groupName)}` : ''}
|
|
11439
11663
|
(function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
|
|
11440
11664
|
})();`.trim();
|
|
11441
11665
|
}
|
|
@@ -11485,7 +11709,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
11485
11709
|
const duration = input.duration ?? 5;
|
|
11486
11710
|
const n = Math.min(1200, Math.max(3, Math.round(duration * fps)));
|
|
11487
11711
|
return `
|
|
11488
|
-
// Accessibility measurements (${checks.join(', ')})
|
|
11712
|
+
// Accessibility measurements (${oneLine(checks.join(', '))})
|
|
11489
11713
|
(async function() {
|
|
11490
11714
|
const out = { success: true, fps: ${fps}, duration: ${duration} };
|
|
11491
11715
|
const rgba = function(c) {
|
|
@@ -11642,7 +11866,7 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11642
11866
|
const mime = sheet.format === 'png' ? 'image/png' : 'image/jpeg';
|
|
11643
11867
|
const S = (v) => JSON.stringify(v);
|
|
11644
11868
|
return `
|
|
11645
|
-
// Contact sheet of ${n} frame(s)
|
|
11869
|
+
// Contact sheet of ${oneLine(n)} frame(s)
|
|
11646
11870
|
(async function() {
|
|
11647
11871
|
if (typeof app.captureFramesAt !== 'function') {
|
|
11648
11872
|
return { success: false, error: 'app.captureFramesAt unavailable — update FxTool to a build with the deterministic capture entrypoint' };
|
|
@@ -11718,7 +11942,7 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11718
11942
|
// tool call that screenshots next wants that to have happened.
|
|
11719
11943
|
const engineOptsJson = JSON.stringify({ canvas: input.canvas, defaultGeometry: input.defaultGeometry, awaitImages: true });
|
|
11720
11944
|
return `
|
|
11721
|
-
// Instantiate ontology → scene (${ops.filter((o) => o.op === 'create').length} items, ${ops.filter((o) => o.op === 'addRelation').length} relations)
|
|
11945
|
+
// Instantiate ontology → scene (${oneLine(ops.filter((o) => o.op === 'create').length)} items, ${oneLine(ops.filter((o) => o.op === 'addRelation').length)} relations)
|
|
11722
11946
|
(async function() {
|
|
11723
11947
|
// Engine door first: the studio's own OntologyCompiler reads every node facet
|
|
11724
11948
|
// (geometry, paint, text, the keyframe track, masks) and builds through the
|
|
@@ -11803,10 +12027,60 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11803
12027
|
return { success: false, error: 'app.lintScene unavailable — update FxTool to a build with the relational linter (S12-E2)' };
|
|
11804
12028
|
}
|
|
11805
12029
|
const result = app.lintScene(${optsJson});
|
|
12030
|
+
// THE RELATIONS AS BOUND, so a later agent can find the one to edit without exporting the scene to read it:
|
|
12031
|
+
// from, to, type and params, capped. And the custom relation definitions registered on this page, as given.
|
|
12032
|
+
const __rels = [];
|
|
12033
|
+
let __relCount = 0;
|
|
12034
|
+
try {
|
|
12035
|
+
const __rr = app.relationRegistry;
|
|
12036
|
+
const __active = __rr && typeof __rr.queryActive === 'function' ? __rr.queryActive() : [];
|
|
12037
|
+
__active.forEach((e) => (e.associations || []).forEach((a) => {
|
|
12038
|
+
__relCount++;
|
|
12039
|
+
if (__rels.length >= ${RELATIONS_READBACK_CAP}) return;
|
|
12040
|
+
const __name = e.item && e.item.data && e.item.data.name;
|
|
12041
|
+
__rels.push({ from: e.itemId, ...(__name ? { fromName: __name } : {}), to: a.target === undefined ? null : a.target, type: a.relation, params: a.params || {} });
|
|
12042
|
+
}));
|
|
12043
|
+
} catch (e) { /* the relations are an addition; the lint stands without them */ }
|
|
12044
|
+
const __custom = (typeof window !== 'undefined' && window.__ppCustomRelations) ? Object.values(window.__ppCustomRelations) : [];
|
|
12045
|
+
// HOW TO CHANGE IT (#43): an editing agent needs the op, not the schemas. Each relation comes with the batch op that
|
|
12046
|
+
// changes it; a group bound by a name pattern ("dot-*", recorded in each member's params) comes once, with the ops
|
|
12047
|
+
// that add and remove a member; a custom motion with the call that redefines it.
|
|
12048
|
+
const __edit = { hint: 'To change this scene, send the ops below (your values in relationOptions) in ONE pinepaper_agent_batch_execute call; a custom motion changes by sending its pinepaper_register_custom_relation call again.', groups: [], relations: [], customRelations: [] };
|
|
12049
|
+
try {
|
|
12050
|
+
const __byPat = new Map();
|
|
12051
|
+
__rels.forEach((r) => {
|
|
12052
|
+
const pat = r.params && r.params.pattern;
|
|
12053
|
+
if (typeof pat === 'string') {
|
|
12054
|
+
const k = pat + '|' + r.type + '|' + r.to;
|
|
12055
|
+
if (!__byPat.has(k)) __byPat.set(k, { pattern: pat, type: r.type, to: r.to, members: [] });
|
|
12056
|
+
__byPat.get(k).members.push(r);
|
|
12057
|
+
return;
|
|
12058
|
+
}
|
|
12059
|
+
__edit.relations.push({ from: r.from, ...(r.fromName ? { fromName: r.fromName } : {}), to: r.to, relationType: r.type,
|
|
12060
|
+
change: { type: 'relation', sourceId: r.fromName || r.from, ...(r.to != null ? { targetId: r.to } : {}), relationType: r.type, update: true, relationOptions: r.params } });
|
|
12061
|
+
});
|
|
12062
|
+
__byPat.forEach((g) => {
|
|
12063
|
+
const base = Object.assign({}, g.members[0].params); delete base.index; delete base.count; delete base.pattern;
|
|
12064
|
+
const names = g.members.map((m) => m.fromName).filter(Boolean).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
12065
|
+
const prefix = g.pattern.split('*')[0];
|
|
12066
|
+
const nums = names.map((n) => Number(n.slice(prefix.length))).filter((n) => Number.isFinite(n));
|
|
12067
|
+
const next = prefix + (nums.length ? Math.max.apply(null, nums) + 1 : g.members.length + 1);
|
|
12068
|
+
const firstEntry = app.itemRegistry.get(g.members[0].from);
|
|
12069
|
+
const bind = { type: 'relation', sourceId: g.pattern, ...(g.to != null ? { targetId: g.to } : {}), relationType: g.type, relationOptions: base };
|
|
12070
|
+
__edit.groups.push({ pattern: g.pattern, relationType: g.type, members: g.members.length, ...(names.length ? { first: names[0], last: names[names.length - 1] } : {}), relationOptions: base,
|
|
12071
|
+
change: Object.assign({}, bind, { update: true }),
|
|
12072
|
+
addMember: [{ type: 'create', itemType: (firstEntry && firstEntry.type) || 'circle', position: [0, 0], properties: { name: next } }, bind],
|
|
12073
|
+
...(names.length ? { removeMember: [{ type: 'delete', itemId: names[names.length - 1] }, bind] } : {}) });
|
|
12074
|
+
});
|
|
12075
|
+
__custom.forEach((d) => __edit.customRelations.push({ name: d.name, change: { tool: 'pinepaper_register_custom_relation', args: d } }));
|
|
12076
|
+
} catch (e) { /* the read-back stands without the edit ops */ }
|
|
11806
12077
|
// warnings: what the engine's own rules found (TEXT_CLIPS_FRAME: a centre-anchored text running past the
|
|
11807
12078
|
// frame edge). Passed through when the studio reports them; an older studio has none to pass.
|
|
11808
12079
|
return { success: true, density: result.density, suggestions: result.suggestions,
|
|
11809
|
-
...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {})
|
|
12080
|
+
...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}),
|
|
12081
|
+
relations: __rels, ...(__relCount > __rels.length ? { relationsShown: __rels.length, relationCount: __relCount } : {}),
|
|
12082
|
+
...(__custom.length ? { customRelations: __custom } : {}),
|
|
12083
|
+
...(__edit.groups.length || __edit.relations.length || __edit.customRelations.length ? { edit: __edit } : {}) };
|
|
11810
12084
|
})();`.trim();
|
|
11811
12085
|
}
|
|
11812
12086
|
/**
|
|
@@ -11837,11 +12111,11 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11837
12111
|
if (u.indexOf('data:') !== 0) return u;
|
|
11838
12112
|
const comma = u.indexOf(',');
|
|
11839
12113
|
const meta = u.slice(5, comma);
|
|
11840
|
-
const mime = meta.split(';')[0] ||
|
|
12114
|
+
const mime = meta.split(';')[0] || ${jsLiteral(kind === 'audio' ? 'audio/mpeg' : 'video/mp4')};
|
|
11841
12115
|
const body = u.slice(comma + 1);
|
|
11842
12116
|
const bin = /;base64/i.test(meta) ? atob(body) : decodeURIComponent(body);
|
|
11843
12117
|
const bytes = Uint8Array.from(bin, function(c) { return c.charCodeAt(0); });
|
|
11844
|
-
const ext = (mime.split('/')[1] ||
|
|
12118
|
+
const ext = (mime.split('/')[1] || ${jsLiteral(kind === 'audio' ? 'mp3' : 'mp4')}).replace(/[^a-z0-9]/gi, '');
|
|
11845
12119
|
return new File([bytes], '${kind}.' + ext, { type: mime });
|
|
11846
12120
|
})(${JSON.stringify(url ?? '')})`;
|
|
11847
12121
|
// TWO ID SPACES, AND THE CALLER HOLDS THE WRONG ONE.
|
|
@@ -11950,7 +12224,7 @@ ${input.timeOffset !== undefined ? ` // Moving the clip is the 4th argument, wh
|
|
|
11950
12224
|
// refusals come back as {ok:false, reason} and are errors here.
|
|
11951
12225
|
case 'split':
|
|
11952
12226
|
return `
|
|
11953
|
-
// Split a media clip at canvas time ${input.at}
|
|
12227
|
+
// Split a media clip at canvas time ${oneLine(input.at)}
|
|
11954
12228
|
(async function() {
|
|
11955
12229
|
${guard}
|
|
11956
12230
|
if (typeof A.splitMedia !== 'function') { return { success: false, action: 'split', error: 'this studio is too old to split a clip (no splitMedia) — nothing was changed.' }; }
|
|
@@ -11960,7 +12234,7 @@ ${guard}
|
|
|
11960
12234
|
})();`.trim();
|
|
11961
12235
|
case 'add_transition':
|
|
11962
12236
|
return `
|
|
11963
|
-
// ${input.transition?.type ?? 'crossfade'} between ${input.fromItemId} and ${input.toItemId}
|
|
12237
|
+
// ${oneLine(input.transition?.type ?? 'crossfade')} between ${oneLine(input.fromItemId)} and ${oneLine(input.toItemId)}
|
|
11964
12238
|
(function() {
|
|
11965
12239
|
${guard}
|
|
11966
12240
|
if (typeof A.addTransition !== 'function') { return { success: false, action: 'add_transition', error: 'this studio is too old for clip transitions (no addTransition) — nothing was changed.' }; }
|
|
@@ -12082,7 +12356,7 @@ ${guard}
|
|
|
12082
12356
|
...(input.live !== undefined ? { live: input.live } : {}),
|
|
12083
12357
|
});
|
|
12084
12358
|
return `
|
|
12085
|
-
// Track matte: the matte item's ${input.channel || 'luma'} drives the target's alpha${input.live ? ' (LIVE — re-cuts as the matte animates)' : ''}
|
|
12359
|
+
// Track matte: the matte item's ${oneLine(input.channel || 'luma')} drives the target's alpha${oneLine(input.live ? ' (LIVE — re-cuts as the matte animates)' : '')}
|
|
12086
12360
|
(async function() {
|
|
12087
12361
|
// A media id (vraster_ / araster_, what upload returns) is resolved to the
|
|
12088
12362
|
// registry id these app facades take (round 10, 10.12).
|
|
@@ -12167,7 +12441,7 @@ ${guard}
|
|
|
12167
12441
|
...(input.fontSize !== undefined ? { fontSize: input.fontSize } : {}),
|
|
12168
12442
|
});
|
|
12169
12443
|
return `
|
|
12170
|
-
// Display text style: ${input.styleKey} — stacked-layer title, id adopted from the text
|
|
12444
|
+
// Display text style: ${oneLine(input.styleKey)} — stacked-layer title, id adopted from the text
|
|
12171
12445
|
(async function() {
|
|
12172
12446
|
if (typeof app.applyTextStyle !== 'function') { return { success: false, error: 'app.applyTextStyle unavailable — update FxTool to a text-styles build' }; }
|
|
12173
12447
|
// LOAD THE FACES FIRST. The decorative stylesheet is fetched on pointerenter
|
|
@@ -12318,7 +12592,7 @@ ${guard}
|
|
|
12318
12592
|
const S = (v) => JSON.stringify(v);
|
|
12319
12593
|
const id = S(input.itemId);
|
|
12320
12594
|
return `
|
|
12321
|
-
// Choreograph ${input.beats.length} beat(s) for ${input.itemId}
|
|
12595
|
+
// Choreograph ${oneLine(input.beats.length)} beat(s) for ${oneLine(input.itemId)}
|
|
12322
12596
|
(function() {
|
|
12323
12597
|
const __it = (app.getItemById && app.getItemById(${id}))
|
|
12324
12598
|
|| (app.itemRegistry && app.itemRegistry.get && (app.itemRegistry.get(${id}) || {}).item);
|
|
@@ -12426,7 +12700,7 @@ ${input.sound ? ` // SOUND ON THE ACTION (D68): one placed sound per moment the
|
|
|
12426
12700
|
const cam = input.camera === true ? { fill: 0.45 } : (input.camera || null);
|
|
12427
12701
|
const sound = input.sound === true ? 'whoosh' : (input.sound || null);
|
|
12428
12702
|
return `
|
|
12429
|
-
// Morph sequence: ${states.length} state(s)
|
|
12703
|
+
// Morph sequence: ${oneLine(states.length)} state(s)
|
|
12430
12704
|
(function() {
|
|
12431
12705
|
const src = app.getItemById && app.getItemById(${S(input.itemId)});
|
|
12432
12706
|
if (!src) { return { success: false, error: ${S(input.itemId)} + ' is not on the canvas.' }; }
|
|
@@ -12903,7 +13177,7 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
|
|
|
12903
13177
|
}
|
|
12904
13178
|
})();
|
|
12905
13179
|
return `
|
|
12906
|
-
// Shader graph: ${input.action}
|
|
13180
|
+
// Shader graph: ${oneLine(input.action)}
|
|
12907
13181
|
(async function() {
|
|
12908
13182
|
${head}
|
|
12909
13183
|
${body}
|
|
@@ -12924,7 +13198,7 @@ ${body}
|
|
|
12924
13198
|
// or brush-only medium with the reason. The Paper item it returns is not
|
|
12925
13199
|
// passed back (it cannot cross the bridge); its id is.
|
|
12926
13200
|
return `
|
|
12927
|
-
// Medium: apply ${input.medium}
|
|
13201
|
+
// Medium: apply ${oneLine(input.medium)}
|
|
12928
13202
|
(async function() {
|
|
12929
13203
|
if (typeof app.applyMedium !== 'function') { return { success: false, error: 'app.applyMedium unavailable — update FxTool' }; }
|
|
12930
13204
|
const r = await app.applyMedium(${S(input.itemId)}, ${S(input.medium)}, ${S(input.options ?? {})});
|
|
@@ -13118,7 +13392,7 @@ ${body}
|
|
|
13118
13392
|
...(input.keepSource !== undefined ? { keepSource: input.keepSource } : {}),
|
|
13119
13393
|
});
|
|
13120
13394
|
return `
|
|
13121
|
-
// Text effect: ${input.effect} — the text becomes one animated item PER CHARACTER
|
|
13395
|
+
// Text effect: ${oneLine(input.effect)} — the text becomes one animated item PER CHARACTER
|
|
13122
13396
|
(async function() {
|
|
13123
13397
|
if (typeof app.applyTextEffect !== 'function') { return { success: false, error: 'app.applyTextEffect unavailable — update FxTool to a text-effects build' }; }
|
|
13124
13398
|
const r = await app.applyTextEffect(${S(input.itemId)}, ${S(input.effect)}, ${opts});
|
|
@@ -13190,7 +13464,7 @@ ${body}
|
|
|
13190
13464
|
...(input.keepSource !== undefined ? { keepSource: input.keepSource } : {}),
|
|
13191
13465
|
});
|
|
13192
13466
|
return `
|
|
13193
|
-
// Shatter ${input.itemId} into tiles (inert until animated — that is the point)
|
|
13467
|
+
// Shatter ${oneLine(input.itemId)} into tiles (inert until animated — that is the point)
|
|
13194
13468
|
(function() {
|
|
13195
13469
|
if (typeof app.shatterImage !== 'function') { return { success: false, error: 'app.shatterImage unavailable — update FxTool to a shatter-capable build' }; }
|
|
13196
13470
|
const r = app.shatterImage(${S(input.itemId)}, ${opts});
|
|
@@ -13325,7 +13599,10 @@ ${body}
|
|
|
13325
13599
|
const needWorld = ` if (!app._world3d) { return { success: false, error: "no 3D world — call pinepaper_world3d create first" }; }`;
|
|
13326
13600
|
switch (input.action) {
|
|
13327
13601
|
case 'create': {
|
|
13328
|
-
|
|
13602
|
+
if (input.control === true && input.character === false) {
|
|
13603
|
+
return `({ success: false, error: ${jsLiteral('control: true walks the character, and character: false leaves it out. Nothing was made. Drop one of the two.')} });`;
|
|
13604
|
+
}
|
|
13605
|
+
const opts = S({ ...(input.character !== undefined ? { character: input.character } : {}), ...(input.control === true ? { control: true } : {}) });
|
|
13329
13606
|
return `
|
|
13330
13607
|
// World3D: create — terrain, sky, shadows, walkable character, under the Paper canvas
|
|
13331
13608
|
(async function() {
|
|
@@ -13338,7 +13615,11 @@ ${body}
|
|
|
13338
13615
|
// params is WORLD_SCHEMA: an ARRAY of {path, kind, doc}. Object.keys on it
|
|
13339
13616
|
// returned "0", "1", "2"… — the indexes, not a single usable key.
|
|
13340
13617
|
const keys = !d ? [] : Array.isArray(d.params) ? d.params.map(function (e) { return e && e.path; }).filter(Boolean) : Object.keys(d.params || d);
|
|
13341
|
-
|
|
13618
|
+
// What the world reports, not what was asked: an engine without the option takes the world and ignores control.
|
|
13619
|
+
const result = { success: true, action: 'create', spec: ${S(input.spec ?? 'forest')}, configurableKeys: keys };${input.control === true ? `
|
|
13620
|
+
result.control = !!(app._world3d && app._world3d.controlEnabled);
|
|
13621
|
+
if (!result.control) result.warning = 'control was asked for and this studio did not turn it on: the character cannot be walked with the keyboard. Update PinePaper Studio.';` : ''}
|
|
13622
|
+
return result;
|
|
13342
13623
|
})();`.trim();
|
|
13343
13624
|
}
|
|
13344
13625
|
case 'describe':
|
|
@@ -13368,7 +13649,7 @@ ${needWorld}
|
|
|
13368
13649
|
...(input.live !== undefined ? { live: input.live } : {}),
|
|
13369
13650
|
});
|
|
13370
13651
|
return `
|
|
13371
|
-
// World3D: put an actor on the stage${input.live ? ' (live sprite — a rigged character PERFORMS, not a photograph of itself)' : ''}
|
|
13652
|
+
// World3D: put an actor on the stage${oneLine(input.live ? ' (live sprite — a rigged character PERFORMS, not a photograph of itself)' : '')}
|
|
13372
13653
|
(function() {
|
|
13373
13654
|
if (typeof app.addWorldActor !== 'function') { return { success: false, error: 'app.addWorldActor unavailable — update FxTool' }; }
|
|
13374
13655
|
${needWorld}
|
|
@@ -13471,7 +13752,7 @@ ${needWorld}
|
|
|
13471
13752
|
mesh.uniforms = uniforms;
|
|
13472
13753
|
}
|
|
13473
13754
|
return `
|
|
13474
|
-
// World3D: ${input.action} — a canvas path becomes real geometry
|
|
13755
|
+
// World3D: ${oneLine(input.action)} — a canvas path becomes real geometry
|
|
13475
13756
|
(async function() {
|
|
13476
13757
|
// @engine-methods extrudeToMesh latheToMesh importOBJToWorld importGLTFToWorld
|
|
13477
13758
|
if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool to a world3d-capable build' }; }
|
|
@@ -13578,7 +13859,7 @@ ${needWorld}
|
|
|
13578
13859
|
case 'import_gltf': {
|
|
13579
13860
|
const fn = input.action === 'import_obj' ? 'importOBJToWorld' : 'importGLTFToWorld';
|
|
13580
13861
|
return `
|
|
13581
|
-
// World3D: ${input.action}
|
|
13862
|
+
// World3D: ${oneLine(input.action)}
|
|
13582
13863
|
(async function() {
|
|
13583
13864
|
// @engine-methods extrudeToMesh latheToMesh importOBJToWorld importGLTFToWorld
|
|
13584
13865
|
if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool to a world3d-capable build' }; }
|
|
@@ -13780,7 +14061,7 @@ ${needWorld}
|
|
|
13780
14061
|
generateDesignCompose(scene, ops, style, sizeWarning, report = {}) {
|
|
13781
14062
|
const S = (v) => JSON.stringify(v);
|
|
13782
14063
|
return `
|
|
13783
|
-
// Design: compose a ${style} scene — ${ops.length} items, laid out server-side
|
|
14064
|
+
// Design: compose a ${oneLine(style)} scene — ${oneLine(ops.length)} items, laid out server-side
|
|
13784
14065
|
(function() {
|
|
13785
14066
|
if (typeof app.create !== 'function') { return { success: false, error: 'app.create unavailable — update FxTool' }; }
|
|
13786
14067
|
const size = ${S({ width: scene.width, height: scene.height })};
|
|
@@ -13860,7 +14141,7 @@ ${needWorld}
|
|
|
13860
14141
|
}
|
|
13861
14142
|
const fn = action === 'figure' ? 'stickFigure' : 'stickSet';
|
|
13862
14143
|
return `
|
|
13863
|
-
// Stick: ${action}
|
|
14144
|
+
// Stick: ${oneLine(action)}
|
|
13864
14145
|
(async function() {
|
|
13865
14146
|
// @engine-methods stickFigure stickSet
|
|
13866
14147
|
if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool (the stick kit is vendored from mcp-cloud)' }; }
|
|
@@ -13954,7 +14235,7 @@ ${needWorld}
|
|
|
13954
14235
|
case 'export_dotlottie': {
|
|
13955
14236
|
const fn = input.action === 'export_lottie' ? 'exportLottie' : 'exportDotLottie';
|
|
13956
14237
|
return `
|
|
13957
|
-
// Interchange: ${fn}
|
|
14238
|
+
// Interchange: ${oneLine(fn)}
|
|
13958
14239
|
(async function() {
|
|
13959
14240
|
${guard(fn)}
|
|
13960
14241
|
${TO_DATA_URL_JS}
|
|
@@ -14071,7 +14352,7 @@ ${guard('exportPNGSequence')}
|
|
|
14071
14352
|
const S = (v) => JSON.stringify(v);
|
|
14072
14353
|
const guard = (fn) => ` if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool' }; }`;
|
|
14073
14354
|
const simple = (fn, args, key, label) => key === 'played' ? `
|
|
14074
|
-
// Sound: ${label}
|
|
14355
|
+
// Sound: ${oneLine(label)}
|
|
14075
14356
|
(function() {
|
|
14076
14357
|
${guard(fn)}
|
|
14077
14358
|
// A play that returns no voice played nothing, and reported success (gate
|
|
@@ -14080,7 +14361,7 @@ ${guard(fn)}
|
|
|
14080
14361
|
if (__v == null) { return { success: false, error: 'nothing played: the studio returned no voice (it may have no audio output). Placed sounds (create, sequence) and render_soundtrack work without one.' }; }
|
|
14081
14362
|
return { success: true, played: __v };
|
|
14082
14363
|
})();`.trim() : `
|
|
14083
|
-
// Sound: ${label}
|
|
14364
|
+
// Sound: ${oneLine(label)}
|
|
14084
14365
|
(function() {
|
|
14085
14366
|
${guard(fn)}
|
|
14086
14367
|
return { success: true, ${key}: app.${fn}(${args}) };
|
|
@@ -14102,7 +14383,7 @@ ${guard(fn)}
|
|
|
14102
14383
|
: simple('playSoundFromText', S(input.text), 'played', 'a described sound');
|
|
14103
14384
|
case 'from_text':
|
|
14104
14385
|
return `
|
|
14105
|
-
// Sound: resolve
|
|
14386
|
+
// Sound: resolve ${jsLiteral(input.text)} to a spec WITHOUT playing it
|
|
14106
14387
|
(function() {
|
|
14107
14388
|
${guard('buildSoundFromText')}
|
|
14108
14389
|
const spec = app.buildSoundFromText(${S(input.text)});
|
|
@@ -14145,7 +14426,7 @@ ${guard('buildSoundFromText')}
|
|
|
14145
14426
|
// checking the method a caller needs. It extracted only
|
|
14146
14427
|
// renderSoundtrackWav until this line existed.
|
|
14147
14428
|
return `
|
|
14148
|
-
// Sound: define ${input.name} at runtime
|
|
14429
|
+
// Sound: define ${oneLine(input.name)} at runtime
|
|
14149
14430
|
(function() {
|
|
14150
14431
|
${guard(fn)}
|
|
14151
14432
|
var name;
|
|
@@ -14329,7 +14610,7 @@ ${guard('timbreFromPath')}
|
|
|
14329
14610
|
opts[k] = input[k];
|
|
14330
14611
|
}
|
|
14331
14612
|
return `
|
|
14332
|
-
// Motion: ${input.motion} on ${input.itemIds?.length ? `${input.itemIds.length} items` : input.itemId}
|
|
14613
|
+
// Motion: ${oneLine(input.motion)} on ${input.itemIds?.length ? `${oneLine(input.itemIds.length)} items` : input.itemId}
|
|
14333
14614
|
(async function() {
|
|
14334
14615
|
if (typeof app.animateItems !== 'function') { return { success: false, error: 'app.animateItems unavailable — update FxTool' }; }
|
|
14335
14616
|
const r = await app.animateItems(${target}, ${S(opts)});
|
|
@@ -14367,7 +14648,7 @@ ${guard('timbreFromPath')}
|
|
|
14367
14648
|
switch (input.action) {
|
|
14368
14649
|
case 'boolean':
|
|
14369
14650
|
return `
|
|
14370
|
-
// Path boolean: ${input.op} — CONSUMES its operands and leaves one result
|
|
14651
|
+
// Path boolean: ${oneLine(input.op)} — CONSUMES its operands and leaves one result
|
|
14371
14652
|
(function() {
|
|
14372
14653
|
${guard('booleanOp')}${pass(`app.booleanOp(${S(input.itemIds)}, ${S(input.op)})`)}
|
|
14373
14654
|
})();`.trim();
|
|
@@ -14391,7 +14672,7 @@ ${guard('togglePathClosed')}${pass(`app.togglePathClosed(${S(input.itemId)})`)}
|
|
|
14391
14672
|
})();`.trim();
|
|
14392
14673
|
case 'pattern':
|
|
14393
14674
|
return `
|
|
14394
|
-
// Pattern: repeat the item — ${input.kind}
|
|
14675
|
+
// Pattern: repeat the item — ${oneLine(input.kind)}
|
|
14395
14676
|
(function() {
|
|
14396
14677
|
${guard('patternFrom')}${pass(`app.patternFrom(${S(input.itemId)}, ${S(input.kind)}, ${S(input.pattern ?? {})})`)}
|
|
14397
14678
|
})();`.trim();
|
|
@@ -14427,7 +14708,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
14427
14708
|
generateCropImage(input) {
|
|
14428
14709
|
const opts = input.aspectRatio ? JSON.stringify({ aspectRatio: input.aspectRatio }) : '{}';
|
|
14429
14710
|
return `
|
|
14430
|
-
// Crop image ${input.itemId}
|
|
14711
|
+
// Crop image ${oneLine(input.itemId)}
|
|
14431
14712
|
(async function() {
|
|
14432
14713
|
if (typeof app.cropImage !== 'function') { return { success: false, error: 'app.cropImage unavailable — update FxTool to a build with one-shot image ops' }; }
|
|
14433
14714
|
const result = await app.cropImage(${JSON.stringify(input.itemId)}, ${JSON.stringify(input.rect)}, ${opts});
|
|
@@ -14446,7 +14727,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
14446
14727
|
...(input.smoothing !== undefined ? { smoothing: input.smoothing } : {}),
|
|
14447
14728
|
});
|
|
14448
14729
|
return `
|
|
14449
|
-
// Chroma-key image ${input.itemId}
|
|
14730
|
+
// Chroma-key image ${oneLine(input.itemId)}
|
|
14450
14731
|
(async function() {
|
|
14451
14732
|
if (typeof app.applyChromaKey !== 'function') { return { success: false, error: 'app.applyChromaKey unavailable — update FxTool to a build with one-shot image ops' }; }
|
|
14452
14733
|
const result = await app.applyChromaKey(${JSON.stringify(input.itemId)}, ${opts});
|
|
@@ -14862,7 +15143,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
14862
15143
|
const argsJson = JSON.stringify(input.args ?? []);
|
|
14863
15144
|
const createAsJson = input.createAs ? JSON.stringify(input.createAs) : 'null';
|
|
14864
15145
|
return `
|
|
14865
|
-
// Geometry construction: ${input.operation}
|
|
15146
|
+
// Geometry construction: ${oneLine(input.operation)}
|
|
14866
15147
|
(function() {
|
|
14867
15148
|
const g = app.geometry;
|
|
14868
15149
|
if (!g || typeof g[${op}] !== 'function') {
|
|
@@ -14924,7 +15205,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
14924
15205
|
const optsJson = JSON.stringify(input);
|
|
14925
15206
|
const kindJson = JSON.stringify(input.kind);
|
|
14926
15207
|
return `
|
|
14927
|
-
// Equation-driven path: ${input.kind}${input.preset ? ' ' + input.preset : ''}
|
|
15208
|
+
// Equation-driven path: ${oneLine(input.kind)}${oneLine(input.preset ? ' ' + input.preset : '')}
|
|
14928
15209
|
(function() {
|
|
14929
15210
|
if (typeof app.createEquationPath !== 'function') {
|
|
14930
15211
|
return { success: false, error: 'app.createEquationPath unavailable — update FxTool to a build with the Expression IR equation-path API (S10 B5)' };
|
|
@@ -14948,7 +15229,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
14948
15229
|
const nameJson = JSON.stringify(input.name);
|
|
14949
15230
|
const optsJson = JSON.stringify({ payloadType: input.payloadType, x: input.x, y: input.y, ...(input.at !== undefined ? { at: input.at } : {}) });
|
|
14950
15231
|
return `
|
|
14951
|
-
// Create event: ${input.name}
|
|
15232
|
+
// Create event: ${oneLine(input.name)}
|
|
14952
15233
|
(function() {
|
|
14953
15234
|
if (typeof app.createEvent !== 'function') {
|
|
14954
15235
|
return { success: false, error: 'app.createEvent unavailable — update FxTool to a build with the event system (S11)' };
|
|
@@ -14963,7 +15244,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
14963
15244
|
const eventIdJson = JSON.stringify(input.eventId);
|
|
14964
15245
|
const payloadJson = JSON.stringify(input.payload ?? null);
|
|
14965
15246
|
return `
|
|
14966
|
-
// Pulse event: ${input.eventId}
|
|
15247
|
+
// Pulse event: ${oneLine(input.eventId)}
|
|
14967
15248
|
(function() {
|
|
14968
15249
|
if (typeof app.pulseEvent !== 'function') {
|
|
14969
15250
|
return { success: false, error: 'app.pulseEvent unavailable — update FxTool to a build with the event system (S11)' };
|
|
@@ -15108,7 +15389,7 @@ return {
|
|
|
15108
15389
|
*/
|
|
15109
15390
|
export function generateAutoKeyCode(itemId, props, atTime, easing) {
|
|
15110
15391
|
return `
|
|
15111
|
-
// Auto-key ${itemId} at ${atTime}s
|
|
15392
|
+
// Auto-key ${oneLine(itemId)} at ${oneLine(atTime)}s
|
|
15112
15393
|
(function() {
|
|
15113
15394
|
const item = app.getItemById(${JSON.stringify(itemId)});
|
|
15114
15395
|
if (!item) return { success: false, error: ${JSON.stringify(`no item "${itemId}" on the canvas — check the id with pinepaper_get_items.`)} };
|
|
@@ -15166,7 +15447,7 @@ export function generateAutoKeyCode(itemId, props, atTime, easing) {
|
|
|
15166
15447
|
*/
|
|
15167
15448
|
export function generateQueryMutationsCode(itemId, property, range, fps = 30) {
|
|
15168
15449
|
return `
|
|
15169
|
-
// Keyframe series for ${itemId}${property ? '.' + property : ''}
|
|
15450
|
+
// Keyframe series for ${oneLine(itemId)}${oneLine(property ? '.' + property : '')}
|
|
15170
15451
|
(function() {
|
|
15171
15452
|
const item = app.getItemById(${JSON.stringify(itemId)});
|
|
15172
15453
|
if (!item) return { success: false, error: ${JSON.stringify(`no item "${itemId}" on the canvas.`)} };
|