@pinepaper.studio/mcp-server 1.6.24 → 1.6.25
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 +130 -14
- package/dist/browser/puppeteer-controller.d.ts +6 -0
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +97 -69
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/cli.js +7 -5
- package/dist/cli.js.map +1 -1
- package/dist/design/design-systems.js +2 -2
- package/dist/design/design-systems.js.map +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -11
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +5 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +86 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +159 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +20 -0
- package/dist/mcp/stdio.d.ts.map +1 -0
- package/dist/mcp/stdio.js +82 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/types.d.ts +598 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +140 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/metrics/performance-tracker.d.ts.map +1 -1
- package/dist/metrics/performance-tracker.js +4 -3
- package/dist/metrics/performance-tracker.js.map +1 -1
- package/dist/ontology/hints.d.ts.map +1 -1
- package/dist/ontology/hints.js +3 -1
- package/dist/ontology/hints.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.d.ts.map +1 -1
- package/dist/ontology/upstream-vocabulary.js +8 -2
- package/dist/ontology/upstream-vocabulary.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/tools/batch-macros.d.ts.map +1 -1
- package/dist/tools/batch-macros.js +2 -1
- package/dist/tools/batch-macros.js.map +1 -1
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +7 -6
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/craft-tools.d.ts +1 -1
- package/dist/tools/craft-tools.d.ts.map +1 -1
- package/dist/tools/create-known-keys.d.ts +2 -0
- package/dist/tools/create-known-keys.d.ts.map +1 -0
- package/dist/tools/create-known-keys.js +24 -0
- package/dist/tools/create-known-keys.js.map +1 -0
- package/dist/tools/definitions.d.ts +3 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +43 -22
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/effect-check.d.ts +1 -1
- package/dist/tools/effect-check.d.ts.map +1 -1
- package/dist/tools/effect-check.js +4 -0
- package/dist/tools/effect-check.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +4 -1
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +9 -1
- 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 +1 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +18 -3
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/export-file-receipt.d.ts +12 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -1
- package/dist/tools/export-file-receipt.js +21 -1
- package/dist/tools/export-file-receipt.js.map +1 -1
- package/dist/tools/handlers/beats.d.ts +1 -1
- package/dist/tools/handlers/beats.d.ts.map +1 -1
- package/dist/tools/handlers/camera.d.ts +1 -1
- package/dist/tools/handlers/camera.d.ts.map +1 -1
- package/dist/tools/handlers/export.d.ts +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +3 -2
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/font.d.ts +1 -1
- package/dist/tools/handlers/font.d.ts.map +1 -1
- package/dist/tools/handlers/font.js +8 -7
- package/dist/tools/handlers/font.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +1 -1
- package/dist/tools/handlers/generate.d.ts.map +1 -1
- package/dist/tools/handlers/generate.js +4 -3
- package/dist/tools/handlers/generate.js.map +1 -1
- package/dist/tools/handlers/maps.d.ts +1 -1
- package/dist/tools/handlers/maps.d.ts.map +1 -1
- package/dist/tools/handlers/ontology.d.ts +1 -1
- package/dist/tools/handlers/ontology.d.ts.map +1 -1
- package/dist/tools/handlers/tool-guide.d.ts +1 -1
- package/dist/tools/handlers/tool-guide.d.ts.map +1 -1
- package/dist/tools/handlers.d.ts +15 -3
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +71 -31
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +11 -6
- package/dist/tools/lane.d.ts.map +1 -1
- package/dist/tools/lane.js +46 -9
- package/dist/tools/lane.js.map +1 -1
- package/dist/tools/minimal-descriptions.js +1 -1
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/p5-compat/p5-helpers.d.ts +1 -1
- package/dist/tools/p5-compat/p5-helpers.js +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.d.ts.map +1 -1
- package/dist/tools/shape-params.js +8 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +2 -2
- package/dist/tools/stick-vocabulary.js +2 -2
- package/dist/tools/timeline.d.ts +2 -2
- package/dist/tools/timeline.d.ts.map +1 -1
- package/dist/tools/timeline.js +6 -1
- package/dist/tools/timeline.js.map +1 -1
- package/dist/tools/toolkits.d.ts +22 -7
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +102 -20
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +25 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +402 -157
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +52 -47
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +7 -5
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/asset-paths.d.ts.map +1 -1
- package/dist/utils/asset-paths.js +4 -4
- package/dist/utils/asset-paths.js.map +1 -1
- package/dist/utils/env.d.ts +9 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +11 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/export-dir.d.ts +36 -0
- package/dist/utils/export-dir.d.ts.map +1 -0
- package/dist/utils/export-dir.js +86 -0
- package/dist/utils/export-dir.js.map +1 -0
- package/dist/utils/js-literal.d.ts +4 -0
- package/dist/utils/js-literal.d.ts.map +1 -0
- package/dist/utils/js-literal.js +25 -0
- package/dist/utils/js-literal.js.map +1 -0
- package/dist/utils/mcp-log.d.ts +1 -1
- package/dist/utils/mcp-log.d.ts.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +5 -12
- package/dist/version.js.map +1 -1
- package/manifest.json +42 -63
- package/package.json +8 -6
|
@@ -7,9 +7,11 @@
|
|
|
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 } from '../tools/engine-lists.js';
|
|
10
|
+
import { WORLD3D_COLOR_PATHS, PATTERN_KINDS } from '../tools/engine-lists.js';
|
|
11
11
|
import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
|
|
12
|
-
import {
|
|
12
|
+
import { NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
|
|
13
|
+
import { CREATE_KNOWN_KEYS } from '../tools/create-known-keys.js';
|
|
14
|
+
import { jsLiteral } from '../utils/js-literal.js';
|
|
13
15
|
import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
|
|
14
16
|
import { expandBatchMacros } from '../tools/batch-macros.js';
|
|
15
17
|
import { CreateItemInputSchema, ScreenSpaceSchema, StepTimingSchema, CounterSchema, ImportMotionCaptureInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
|
|
@@ -146,7 +148,16 @@ function generateBlendModeCode(itemVarName, blendMode) {
|
|
|
146
148
|
/**
|
|
147
149
|
* Template for creating items with support for gradients, shadows, and blend modes
|
|
148
150
|
*/
|
|
151
|
+
const UNSAFE_ID_CHARS = /['"`\\\n\r\u2028\u2029]/;
|
|
149
152
|
function generateCreateItemCode(itemType, position, properties, dataFlags, positionGiven = true) {
|
|
153
|
+
// AN ID OF YOUR OWN IS SPLICED INTO LATER CALLS' CODE. Most tools now quote it,
|
|
154
|
+
// not all yet, so an id carrying a quote, a backslash or a line break would
|
|
155
|
+
// break those later calls: refused here, by name, before anything is made.
|
|
156
|
+
if (typeof properties.id === 'string' && UNSAFE_ID_CHARS.test(properties.id)) {
|
|
157
|
+
return `
|
|
158
|
+
// Create ${itemType} — refused: an id later calls could not address
|
|
159
|
+
({ 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
|
+
}
|
|
150
161
|
// Colour objects are gradient specs: normalised, or refused by name.
|
|
151
162
|
for (const key of ['color', 'fillColor', 'strokeColor']) {
|
|
152
163
|
const v = properties[key];
|
|
@@ -213,6 +224,10 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
213
224
|
// All three now mean no fill, applied after the item exists.
|
|
214
225
|
const noFill = (v) => v === null || (typeof v === 'string' && /^(transparent|none)$/i.test(v.trim()));
|
|
215
226
|
const clearFill = (fillColor !== undefined && noFill(fillColor)) || (fillColor === undefined && color !== undefined && noFill(color));
|
|
227
|
+
// A STROKE WITH NO FILL IS AN OUTLINE (issue #29 O7). A closed shape given
|
|
228
|
+
// only a strokeColor took the engine's default fill, so a ring came out as a
|
|
229
|
+
// blue disc with a red rim. Naming a stroke and no fill now means no fill.
|
|
230
|
+
const outlineOnly = isStrokeOnly(itemType, properties) && !clearFill;
|
|
216
231
|
// `color` ON A CLOSED PATH IS ITS FILL.
|
|
217
232
|
//
|
|
218
233
|
// The engine's path branch paints `color` as the STROKE whenever no fillColor
|
|
@@ -337,7 +352,7 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
|
|
|
337
352
|
if (blendMode) {
|
|
338
353
|
code += '\n' + generateBlendModeCode('item', blendMode);
|
|
339
354
|
}
|
|
340
|
-
if (clearFill)
|
|
355
|
+
if (clearFill || outlineOnly)
|
|
341
356
|
code += `\nitem.fillColor = null;`;
|
|
342
357
|
// DASHES, CAPS AND JOINS FOR EVERY SHAPE (round 7 Z, 1.54). create() applies
|
|
343
358
|
// them only in its path / line / pattern branches; the shared tail sets a
|
|
@@ -401,15 +416,27 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
|
|
|
401
416
|
code += `\nif (app.setDynamicContent) app.setDynamicContent(item, '${contentType}', ${JSON.stringify(opts)});`;
|
|
402
417
|
}
|
|
403
418
|
const loadsFont = itemType === 'text' && typeof properties.fontFamily === 'string' && properties.fontFamily.trim() !== '';
|
|
419
|
+
// A LINE WITH `from` IS DRAWN WHERE from AND to SAY (issue #29 O8). They are
|
|
420
|
+
// canvas coordinates, so position does not move it, and the result reported
|
|
421
|
+
// the position asked for while the line sat at the top-left: a line from
|
|
422
|
+
// [0, 0] to [200, 0] "created at (400, 300)". The result reads where it is.
|
|
423
|
+
const lineIgnoresPosition = itemType === 'line' && properties.from !== undefined && positionGiven;
|
|
424
|
+
const reportActualPosition = omitXY || lineIgnoresPosition;
|
|
425
|
+
const warnings = [];
|
|
426
|
+
if (ignored.length > 0) {
|
|
427
|
+
warnings.push(`${ignored.join(', ')} ${ignored.length > 1 ? 'are' : 'is'} not read when creating an item, so ${ignored.length > 1 ? 'they had' : 'it had'} no effect. `
|
|
428
|
+
+ 'Check the spelling against this item type\'s documented properties.');
|
|
429
|
+
}
|
|
430
|
+
if (lineIgnoresPosition) {
|
|
431
|
+
warnings.push('a line runs from `from` to `to` in canvas coordinates, so position did not move it: position is where the line is now. Give the endpoints where the line goes.');
|
|
432
|
+
}
|
|
404
433
|
code += `
|
|
405
434
|
const itemId = item.data.registryId;
|
|
406
435
|
app.historyManager.saveState();
|
|
407
436
|
|
|
408
437
|
// Return item info
|
|
409
|
-
({ itemId, type: '${itemType}'${textFit ? ', textFit: __textFit' : ''}${textDirection ? ', direction: __direction' : ''}${textTabular ? ', tabularFigures: __tabular' : ''}${createFillOpacity ? ', fillOpacity: __fillOpacity' : ''}${textStrokePos ? ', strokePosition: __strokePos' : ''}, position: ${
|
|
410
|
-
? `, ignoredProperties: ${JSON.stringify(ignored)}
|
|
411
|
-
+ 'Check the spelling against this item type\'s documented properties.')}`
|
|
412
|
-
: ''} });`;
|
|
438
|
+
({ itemId, type: '${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
|
+
? `, ignoredProperties: ${JSON.stringify(ignored)}` : ''}${warnings.length > 0 ? `, warning: ${JSON.stringify(warnings.join(' '))}` : ''} });`;
|
|
413
440
|
// Loading a font is async, and it has to happen BEFORE create() measures the
|
|
414
441
|
// text — so only this case becomes an async IIFE; every other create stays a
|
|
415
442
|
// plain snippet ending in its (-led result.
|
|
@@ -437,24 +464,6 @@ const MODIFY_DID_YOU_MEAN = {
|
|
|
437
464
|
scaling: 'scale', size: 'width / height', rotate: 'rotation', angle: 'rotation', shear: 'skewX / skewY', skew: 'skewX / skewY',
|
|
438
465
|
alpha: 'opacity', text: 'content', stroke_width: 'strokeWidth', lineWidth: 'strokeWidth',
|
|
439
466
|
};
|
|
440
|
-
/**
|
|
441
|
-
* Every `properties` key create_item can act on: what the engine's create()
|
|
442
|
-
* and normalizeParams() read (generated — see shape-params.ts), plus the keys
|
|
443
|
-
* generateCreateItemCode consumes itself before the engine sees them.
|
|
444
|
-
*/
|
|
445
|
-
const CREATE_KNOWN_KEYS = new Set([
|
|
446
|
-
...ACCEPTED_CREATE_PARAMS,
|
|
447
|
-
...NORMALIZE_PARAM_READS,
|
|
448
|
-
// withRadiusAxes, gradients / shadow / blend / opacity emitted here, and
|
|
449
|
-
// setDynamicContent for text.
|
|
450
|
-
'radiusX', 'radiusY', 'contentType', 'contentFormat', 'countdownTarget', 'countdownEndText',
|
|
451
|
-
// Lifetimes, on ANY item: this generator applies them through app.setLifetime
|
|
452
|
-
// (emitLifetime), whatever create() itself reads.
|
|
453
|
-
'bornAt', 'ttl',
|
|
454
|
-
// A solid fill's own alpha (FxTool 12 STROKEFILL); confirmed per call by
|
|
455
|
-
// emitFillOpacityCheck, since an older studio ignores it.
|
|
456
|
-
'fillOpacity',
|
|
457
|
-
]);
|
|
458
467
|
/**
|
|
459
468
|
* Load a font family before text is drawn with it, and say if it still is not there.
|
|
460
469
|
*
|
|
@@ -475,11 +484,11 @@ export function emitEnsureFont(family) {
|
|
|
475
484
|
return `
|
|
476
485
|
let __font = null;
|
|
477
486
|
if (typeof app.checkFont === 'function') {
|
|
478
|
-
const __v0 = app.checkFont(${
|
|
487
|
+
const __v0 = app.checkFont(${jsLiteral(family)});
|
|
479
488
|
if (__v0 && __v0.available === false) {
|
|
480
489
|
if (typeof app.ensureFontsLoaded === 'function') { try { await app.ensureFontsLoaded('all'); } catch (_) { /* reported below */ } }
|
|
481
|
-
try { if (typeof document !== 'undefined' && document.fonts) await document.fonts.load('16px ' + ${
|
|
482
|
-
let __v1 = app.checkFont(${
|
|
490
|
+
try { if (typeof document !== 'undefined' && document.fonts) await document.fonts.load('16px ' + ${jsLiteral(jsLiteral(primary))}); } catch (_) { /* reported below */ }
|
|
491
|
+
let __v1 = app.checkFont(${jsLiteral(family)});
|
|
483
492
|
let __via = 'studio';
|
|
484
493
|
// Not in the studio's own sheets (Monoton, a common neon face, is not):
|
|
485
494
|
// the page's CSP allows fonts.googleapis.com stylesheets and
|
|
@@ -487,21 +496,21 @@ if (typeof app.checkFont === 'function') {
|
|
|
487
496
|
if (!(__v1 && __v1.available === true) && typeof document !== 'undefined' && document.head) {
|
|
488
497
|
__via = 'google-fonts';
|
|
489
498
|
await new Promise(function(res) {
|
|
490
|
-
const id = 'pp-gf-' + ${
|
|
499
|
+
const id = 'pp-gf-' + ${jsLiteral(primary)}.replace(/[^a-z0-9]+/gi, '-');
|
|
491
500
|
if (document.getElementById(id)) return res();
|
|
492
501
|
const l = document.createElement('link');
|
|
493
502
|
l.id = id; l.rel = 'stylesheet';
|
|
494
|
-
l.href = 'https://fonts.googleapis.com/css2?family=' + encodeURIComponent(${
|
|
503
|
+
l.href = 'https://fonts.googleapis.com/css2?family=' + encodeURIComponent(${jsLiteral(primary)}).replace(/%20/g, '+') + '&display=swap';
|
|
495
504
|
l.onload = res; l.onerror = res; setTimeout(res, 5000);
|
|
496
505
|
document.head.appendChild(l);
|
|
497
506
|
});
|
|
498
|
-
try { if (document.fonts) await document.fonts.load('16px ' + ${
|
|
499
|
-
__v1 = app.checkFont(${
|
|
507
|
+
try { if (document.fonts) await document.fonts.load('16px ' + ${jsLiteral(jsLiteral(primary))}); } catch (_) { /* reported below */ }
|
|
508
|
+
__v1 = app.checkFont(${jsLiteral(family)});
|
|
500
509
|
}
|
|
501
510
|
__font = (__v1 && __v1.available === true)
|
|
502
|
-
? { family: ${
|
|
503
|
-
: { family: ${
|
|
504
|
-
warning: ${
|
|
511
|
+
? { family: ${jsLiteral(primary)}, available: true, loadedNow: true, via: __via }
|
|
512
|
+
: { family: ${jsLiteral(primary)}, available: false,
|
|
513
|
+
warning: ${jsLiteral(primary)} + ' could not be loaded, so this text is drawn in a fallback face. '
|
|
505
514
|
+ ((__v1 && __v1.reason) || '') + ' font list_available names the families this studio can load.' };
|
|
506
515
|
}
|
|
507
516
|
}`;
|
|
@@ -572,13 +581,13 @@ function takeDirection(props) {
|
|
|
572
581
|
function emitDirectionCheck(itemExpr, req) {
|
|
573
582
|
if ('error' in req)
|
|
574
583
|
return `
|
|
575
|
-
const __direction = ${
|
|
576
|
-
const v =
|
|
584
|
+
const __direction = ${jsLiteral({ applied: false, error: req.error })};`;
|
|
585
|
+
const v = jsLiteral(req.value);
|
|
577
586
|
return `
|
|
578
587
|
const __direction = (function(it) {
|
|
579
588
|
if (!it || it.className !== 'PointText') return { applied: false, note: 'direction applies to text items only.' };
|
|
580
589
|
return it.data && it.data.direction === ${v}
|
|
581
|
-
? { applied: true, value: ${v}${req.resolved ? `, resolved: ${
|
|
590
|
+
? { applied: true, value: ${v}${req.resolved ? `, resolved: ${jsLiteral(req.resolved)}` : ''} }
|
|
582
591
|
: { applied: false, note: 'this studio does not set a paragraph direction (it needs an engine with RTL text support): the text is laid out left-to-right.' };
|
|
583
592
|
})(${itemExpr});`;
|
|
584
593
|
}
|
|
@@ -678,7 +687,7 @@ function takeStrokePosition(props, allowNull) {
|
|
|
678
687
|
}
|
|
679
688
|
function emitStrokePositionCheck(itemExpr, req) {
|
|
680
689
|
if ('error' in req)
|
|
681
|
-
return `\nconst __strokePos = ${
|
|
690
|
+
return `\nconst __strokePos = ${jsLiteral({ applied: false, error: req.error })};`;
|
|
682
691
|
if (req.value === 'center')
|
|
683
692
|
return `\nconst __strokePos = { applied: true, value: 'center' };`;
|
|
684
693
|
return `
|
|
@@ -696,7 +705,7 @@ function anchorHold(anchor) {
|
|
|
696
705
|
function emitTextFit(itemExpr, box, holdFallback = 'undefined') {
|
|
697
706
|
return `
|
|
698
707
|
if (typeof app.fitText === 'function') {
|
|
699
|
-
const __fbox = ${
|
|
708
|
+
const __fbox = ${jsLiteral(box)};
|
|
700
709
|
if (__fbox.hold === undefined) { const __h = ${holdFallback}; if (__h) __fbox.hold = __h; }
|
|
701
710
|
const __fr = app.fitText(${itemExpr}, __fbox);
|
|
702
711
|
__textFit = __fr && __fr.ok !== false
|
|
@@ -765,11 +774,11 @@ function emitTextStyle(itemExpr, props) {
|
|
|
765
774
|
const edge = function(b) { return j === 'left' ? b.left : j === 'right' ? b.right : b.center.x; };
|
|
766
775
|
const b0 = it.bounds, ax = edge(b0), ay = b0.center.y;
|
|
767
776
|
${weight !== undefined || props.fontStyle !== undefined
|
|
768
|
-
? `it.fontWeight = ${
|
|
777
|
+
? `it.fontWeight = ${jsLiteral(`${italic ? 'italic ' : ''}${weight ?? 'normal'}`)};`
|
|
769
778
|
: ''}
|
|
770
779
|
${leading !== undefined ? `it.leading = ${leading};` : ''}
|
|
771
780
|
${lh !== undefined ? `it.leading = ${lh} <= 4 ? ${lh} * it.fontSize : ${lh};` : ''}
|
|
772
|
-
${ls !== undefined ? `it.letterSpacing = ${
|
|
781
|
+
${ls !== undefined ? `it.letterSpacing = ${jsLiteral(ls)};` : ''}
|
|
773
782
|
const b1 = it.bounds;
|
|
774
783
|
it.position = it.position.add([ax - edge(b1), ay - b1.center.y]);
|
|
775
784
|
})(${itemExpr});`;
|
|
@@ -918,6 +927,9 @@ function emitLifetime(itemExpr, bornAt, ttl) {
|
|
|
918
927
|
* Template for modifying items
|
|
919
928
|
*/
|
|
920
929
|
function generateModifyItemCode(itemId, properties, dataFlags) {
|
|
930
|
+
// The id as a quoted literal everywhere below: an id is caller text ("Bob's hat" is a valid properties.id), and a
|
|
931
|
+
// quote spliced raw ended the string and broke the whole call.
|
|
932
|
+
const ID = jsLiteral(itemId);
|
|
921
933
|
const { contentType, contentFormat, countdownTarget, countdownEndText, ...restProperties } = properties;
|
|
922
934
|
// Aliases for text items: text -> content (B66), textAlign / align -> justification (B89).
|
|
923
935
|
// A key consumed here was applied, so the unread-key report below must not
|
|
@@ -961,23 +973,23 @@ function generateModifyItemCode(itemId, properties, dataFlags) {
|
|
|
961
973
|
// The warning the engine logs on that path is console.warn, which production
|
|
962
974
|
// strips, so the failure was invisible twice over.
|
|
963
975
|
let code = `
|
|
964
|
-
// Modify item ${
|
|
976
|
+
// Modify item ${ID}
|
|
965
977
|
let _modified = true;`;
|
|
966
978
|
if (Object.keys(restProperties).length > 0) {
|
|
967
979
|
code += `
|
|
968
|
-
_modified = app.modifyItem(
|
|
969
|
-
if (!_modified) { return { success: false, error: 'no item ${
|
|
980
|
+
_modified = app.modifyItem(${ID}, ${jsLiteral(restProperties, 2)}) !== false;
|
|
981
|
+
if (!_modified) { return { success: false, error: 'no item ' + ${ID} + ' — nothing was modified' }; }`;
|
|
970
982
|
}
|
|
971
983
|
else {
|
|
972
984
|
code += `
|
|
973
|
-
if (!app.getItemById(
|
|
985
|
+
if (!app.getItemById(${ID})) { return { success: false, error: 'no item ' + ${ID} }; }`;
|
|
974
986
|
}
|
|
975
987
|
// Handle dynamic content type changes
|
|
976
988
|
if (contentType !== undefined) {
|
|
977
989
|
if (contentType === null || contentType === 'none' || contentType === '') {
|
|
978
990
|
// Remove dynamic content
|
|
979
991
|
code += `
|
|
980
|
-
const entry = app.itemRegistry.get(
|
|
992
|
+
const entry = app.itemRegistry.get(${ID});
|
|
981
993
|
if (entry && entry.item && app.removeDynamicContent) app.removeDynamicContent(entry.item);`;
|
|
982
994
|
}
|
|
983
995
|
else {
|
|
@@ -990,8 +1002,8 @@ if (entry && entry.item && app.removeDynamicContent) app.removeDynamicContent(en
|
|
|
990
1002
|
if (countdownEndText)
|
|
991
1003
|
opts.countdownEndText = countdownEndText;
|
|
992
1004
|
code += `
|
|
993
|
-
const entry = app.itemRegistry.get(
|
|
994
|
-
if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.item, '${contentType}', ${
|
|
1005
|
+
const entry = app.itemRegistry.get(${ID});
|
|
1006
|
+
if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.item, '${contentType}', ${jsLiteral(opts)});`;
|
|
995
1007
|
}
|
|
996
1008
|
}
|
|
997
1009
|
// AN AUDIO ITEM'S LEVEL IS NOT AN ITEM PROPERTY. modifyItem applies changes
|
|
@@ -1003,13 +1015,13 @@ if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.it
|
|
|
1003
1015
|
if (levelKey) {
|
|
1004
1016
|
const level = Math.max(0, Math.min(1, properties[levelKey]));
|
|
1005
1017
|
code += `
|
|
1006
|
-
const _ae = app.itemRegistry.get(
|
|
1018
|
+
const _ae = app.itemRegistry.get(${ID});
|
|
1007
1019
|
if (_ae && _ae.type === 'audio') {
|
|
1008
1020
|
_ae.properties = _ae.properties || {};
|
|
1009
1021
|
_ae.properties.audioGain = ${level};
|
|
1010
1022
|
_ae.properties.gain = ${level};
|
|
1011
1023
|
const _A = (typeof window !== 'undefined') && window.PinePaperAgent;
|
|
1012
|
-
const _m = _A && typeof _A.listMedia === 'function' ? _A.listMedia().find(function(x) { return x.registryId ===
|
|
1024
|
+
const _m = _A && typeof _A.listMedia === 'function' ? _A.listMedia().find(function(x) { return x.registryId === ${ID}; }) : null;
|
|
1013
1025
|
if (_m && app.audioLayer && typeof app.audioLayer.setVolume === 'function') app.audioLayer.setVolume(${level}, _m.id);
|
|
1014
1026
|
}`;
|
|
1015
1027
|
}
|
|
@@ -1017,17 +1029,17 @@ if (_ae && _ae.type === 'audio') {
|
|
|
1017
1029
|
// does not pass on; applied to a raster, or to the raster an image group holds.
|
|
1018
1030
|
if (typeof properties.smoothing === 'string') {
|
|
1019
1031
|
code += `
|
|
1020
|
-
const _sm = app.itemRegistry.get(
|
|
1032
|
+
const _sm = app.itemRegistry.get(${ID});
|
|
1021
1033
|
const _smR = _sm && _sm.item && (_sm.item.className === 'Raster' ? _sm.item
|
|
1022
1034
|
: (typeof _sm.item.getItem === 'function' ? _sm.item.getItem({ className: 'Raster' }) : null));
|
|
1023
|
-
if (_smR) _smR.smoothing = ${
|
|
1035
|
+
if (_smR) _smR.smoothing = ${jsLiteral(properties.smoothing)};`;
|
|
1024
1036
|
}
|
|
1025
1037
|
// No fill on modify too: null / 'transparent' / 'none' clear it outright,
|
|
1026
1038
|
// after modifyItem, which would otherwise read 'transparent' as black.
|
|
1027
1039
|
const fillNone = (v) => v === null || (typeof v === 'string' && /^(transparent|none)$/i.test(v.trim()));
|
|
1028
1040
|
if (fillNone(properties.fillColor) || (properties.fillColor === undefined && properties.color !== undefined && fillNone(properties.color))) {
|
|
1029
1041
|
code += `
|
|
1030
|
-
const _nf = app.itemRegistry.get(
|
|
1042
|
+
const _nf = app.itemRegistry.get(${ID});
|
|
1031
1043
|
if (_nf && _nf.item) _nf.item.fillColor = null;`;
|
|
1032
1044
|
}
|
|
1033
1045
|
// SKEW AND AFFINE, FOR MOCKUPS (round 9 HH). A box face or a phone screen in
|
|
@@ -1043,7 +1055,7 @@ if (_nf && _nf.item) _nf.item.fillColor = null;`;
|
|
|
1043
1055
|
const affine = skewX !== 0 || skewY !== 0 || matrix !== null;
|
|
1044
1056
|
if (affine) {
|
|
1045
1057
|
code += `
|
|
1046
|
-
const _af = app.itemRegistry.get(
|
|
1058
|
+
const _af = app.itemRegistry.get(${ID});
|
|
1047
1059
|
const _afi = _af && _af.item;
|
|
1048
1060
|
if (_afi) {
|
|
1049
1061
|
${skewX !== 0 || skewY !== 0 ? `_afi.shear(Math.tan(${skewX} * Math.PI / 180), Math.tan(${skewY} * Math.PI / 180), _afi.bounds.center);` : ''}
|
|
@@ -1054,23 +1066,23 @@ if (_afi) {
|
|
|
1054
1066
|
if (modFit) {
|
|
1055
1067
|
code += `
|
|
1056
1068
|
let __textFit = null;
|
|
1057
|
-
const _tf = app.itemRegistry.get(
|
|
1069
|
+
const _tf = app.itemRegistry.get(${ID});${emitTextFit('_tf && _tf.item', modFit, jsLiteral(anchorHold(properties.anchor ?? properties.origin) ?? null) + " || (_tf && _tf.item && _tf.item.data && _tf.item.data.fitBox && _tf.item.data.fitBox.hold)")}`;
|
|
1058
1070
|
}
|
|
1059
1071
|
const textStyle = emitTextStyle('_ts && _ts.item', properties);
|
|
1060
1072
|
if (textStyle) {
|
|
1061
1073
|
code += `
|
|
1062
|
-
const _ts = app.itemRegistry.get(
|
|
1074
|
+
const _ts = app.itemRegistry.get(${ID});${textStyle}`;
|
|
1063
1075
|
}
|
|
1064
1076
|
const modLifetime = properties.bornAt !== undefined || properties.ttl !== undefined;
|
|
1065
1077
|
if (modLifetime) {
|
|
1066
1078
|
code += `
|
|
1067
1079
|
let __lifetime = null;
|
|
1068
|
-
const _lt = app.itemRegistry.get(
|
|
1080
|
+
const _lt = app.itemRegistry.get(${ID});${emitLifetime('_lt && _lt.item', properties.bornAt, properties.ttl)}`;
|
|
1069
1081
|
}
|
|
1070
1082
|
if (dataFlags && Object.keys(dataFlags).length > 0) {
|
|
1071
1083
|
code += `
|
|
1072
|
-
const _flagged = app.itemRegistry.get(
|
|
1073
|
-
if (_flagged && _flagged.item && _flagged.item.data) Object.assign(_flagged.item.data, ${
|
|
1084
|
+
const _flagged = app.itemRegistry.get(${ID});
|
|
1085
|
+
if (_flagged && _flagged.item && _flagged.item.data) Object.assign(_flagged.item.data, ${jsLiteral(dataFlags)});`;
|
|
1074
1086
|
}
|
|
1075
1087
|
const modFont = typeof properties.fontFamily === 'string' && properties.fontFamily.trim() !== '';
|
|
1076
1088
|
// AN UNREAD KEY HAD NO EFFECT, AND THE CALL SAID SUCCESS. {scaling: 8} on a
|
|
@@ -1079,7 +1091,7 @@ if (_flagged && _flagged.item && _flagged.item.data) Object.assign(_flagged.item
|
|
|
1079
1091
|
if (textStyle && letterSpacingCss(properties.letterSpacing) !== undefined)
|
|
1080
1092
|
aliased.add('letterSpacing');
|
|
1081
1093
|
const modIgnored = Object.keys(properties).filter((k) => !MODIFY_KNOWN_KEYS.has(k) && !aliased.has(k));
|
|
1082
|
-
const modWarning = modIgnored.length === 0 ? '' : `, ignoredProperties: ${
|
|
1094
|
+
const modWarning = modIgnored.length === 0 ? '' : `, ignoredProperties: ${jsLiteral(modIgnored)}, warning: ${jsLiteral(`${modIgnored.join(', ')} ${modIgnored.length > 1 ? 'are' : 'is'} not read when modifying an item, so ${modIgnored.length > 1 ? 'they had' : 'it had'} no effect.`
|
|
1083
1095
|
+ modIgnored.filter((k) => MODIFY_DID_YOU_MEAN[k]).map((k) => ` Did you mean ${MODIFY_DID_YOU_MEAN[k]} (not ${k})?`).join(''))}`;
|
|
1084
1096
|
code += `
|
|
1085
1097
|
app.historyManager.saveState();
|
|
@@ -1087,11 +1099,11 @@ ${properties.content !== undefined ? `
|
|
|
1087
1099
|
// CONTENT UNDER A CONTENT TRACK (round 10, 10.13): the keyframes set the text
|
|
1088
1100
|
// on every frame, so a direct edit is overwritten at once — it answered
|
|
1089
1101
|
// success and changed nothing visible.
|
|
1090
|
-
const __ctItem = app.getItemById(
|
|
1102
|
+
const __ctItem = app.getItemById(${ID});
|
|
1091
1103
|
const __ctKeys = (__ctItem && __ctItem.data && Array.isArray(__ctItem.data.keyframes))
|
|
1092
1104
|
? __ctItem.data.keyframes.filter(function(k) { return k && k.properties && Object.prototype.hasOwnProperty.call(k.properties, 'content'); }).length : 0;` : ''}
|
|
1093
|
-
${modDirection ? emitDirectionCheck(`app.getItemById(
|
|
1094
|
-
return { success: true, itemId:
|
|
1105
|
+
${modDirection ? emitDirectionCheck(`app.getItemById(${ID})`, modDirection) : ''}${modTabular ? emitTabularCheck(`app.getItemById(${ID})`) : ''}${modFillOpacity ? emitFillOpacityCheck(`app.getItemById(${ID})`) : ''}${modStrokePos ? emitStrokePositionCheck(`app.getItemById(${ID})`, modStrokePos) : ''}
|
|
1106
|
+
return { success: true, itemId: ${ID}${properties.content !== undefined ? `, ...(__ctKeys ? { contentTrackWarning: 'this text\\'s content is keyframed (' + __ctKeys + ' key(s)), so the track sets it on every frame and this edit does not show. Change the content keys with pinepaper_keyframe_animate (append: true replaces same-time keys), or animate another property.' } : {})` : ''}${modFit ? ', textFit: __textFit' : ''}${modDirection ? ', direction: __direction' : ''}${modTabular ? ', tabularFigures: __tabular' : ''}${modFillOpacity ? ', fillOpacity: __fillOpacity' : ''}${modStrokePos ? ', strokePosition: __strokePos' : ''}${affine ? `, affine: ${jsLiteral({ skewX, skewY, matrix, note: 'applied to the current shape: calling again compounds it. To undo, apply the inverse (negative skew, or the inverse matrix).' })}` : ''}${modLifetime ? ', lifetime: __lifetime' : ''}${modFont ? ', ...(__font ? { font: __font } : {})' : ''}${modWarning} };`;
|
|
1095
1107
|
// Wrapped, because the body now RETURNS early on a miss rather than falling
|
|
1096
1108
|
// through to an unconditional success. Async only when a font must load
|
|
1097
1109
|
// before modifyItem re-measures the text.
|
|
@@ -1104,8 +1116,9 @@ return { success: true, itemId: '${itemId}'${properties.content !== undefined ?
|
|
|
1104
1116
|
* Template for deleting items
|
|
1105
1117
|
*/
|
|
1106
1118
|
function generateDeleteItemCode(itemId) {
|
|
1119
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1107
1120
|
return `
|
|
1108
|
-
// Delete item ${
|
|
1121
|
+
// Delete item ${ID}
|
|
1109
1122
|
//
|
|
1110
1123
|
// Delegate to app.deleteItem rather than hand-rolling the removal. The old
|
|
1111
1124
|
// inline version called \`app.itemRegistry.remove(...)\`, which does not exist —
|
|
@@ -1122,11 +1135,11 @@ function generateDeleteItemCode(itemId) {
|
|
|
1122
1135
|
if (typeof app.deleteItem !== 'function') {
|
|
1123
1136
|
return { success: false, error: 'app.deleteItem unavailable — update FxTool' };
|
|
1124
1137
|
}
|
|
1125
|
-
const ok = app.deleteItem(
|
|
1138
|
+
const ok = app.deleteItem(${ID});
|
|
1126
1139
|
if (!ok) {
|
|
1127
|
-
return { success: false, error: 'Item not found: ${
|
|
1140
|
+
return { success: false, error: 'Item not found: ' + ${ID} };
|
|
1128
1141
|
}
|
|
1129
|
-
return { success: true, itemId:
|
|
1142
|
+
return { success: true, itemId: ${ID} };
|
|
1130
1143
|
})();
|
|
1131
1144
|
`.trim();
|
|
1132
1145
|
}
|
|
@@ -1142,6 +1155,92 @@ function generateDeleteItemCode(itemId) {
|
|
|
1142
1155
|
export function relationRefusalReasonJs(typeJs) {
|
|
1143
1156
|
return `(function(){ var ws = (app && app._engineWarnings) || []; for (var i = ws.length - 1; i >= 0; i--) { var w = ws[i]; if (w && w.relation === ${typeJs} && typeof w.message === 'string') return w.message; } return null; })()`;
|
|
1144
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* on_event_fire_if FAILS SILENTLY ON A BAD SPEC (1.6.24 probe). The engine
|
|
1160
|
+
* evaluates params.predicate with the ExpressionIR interpreter, which is total:
|
|
1161
|
+
* a string such as "1 > 0" has no `op`, evaluates to 0 and takes the else
|
|
1162
|
+
* branch without a warning. And it pulses params.event, never the target, so a
|
|
1163
|
+
* relation with no event fires nothing at all. Both are refused here with the
|
|
1164
|
+
* shape that works. Only the predicate ops' own fields are checked; any other
|
|
1165
|
+
* op passes, so an op the engine adds later is not refused by a stale list.
|
|
1166
|
+
*/
|
|
1167
|
+
export function fireIfProblem(params) {
|
|
1168
|
+
const p = params ?? {};
|
|
1169
|
+
const example = 'e.g. predicate: { op: "cmp", cmp: "eq", a: { op: "graph", read: "data", item: "item_3", property: "score" }, b: { op: "const", value: 3 } }, event: "<event to fire when it holds>"';
|
|
1170
|
+
const walk = (n, path, depth) => {
|
|
1171
|
+
if (depth > 32)
|
|
1172
|
+
return `${path} nests deeper than 32 levels`;
|
|
1173
|
+
if (!n || typeof n !== 'object' || Array.isArray(n) || typeof n.op !== 'string') {
|
|
1174
|
+
return `${path} must be an ExpressionIR node (an object with an "op"), got ${JSON.stringify(n)}`;
|
|
1175
|
+
}
|
|
1176
|
+
const node = n;
|
|
1177
|
+
if (node.op === 'cmp') {
|
|
1178
|
+
if (!['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(node.cmp))
|
|
1179
|
+
return `${path}.cmp must be eq, neq, gt, gte, lt or lte, got ${JSON.stringify(node.cmp)}`;
|
|
1180
|
+
return walk(node.a, `${path}.a`, depth + 1) ?? walk(node.b, `${path}.b`, depth + 1);
|
|
1181
|
+
}
|
|
1182
|
+
if (node.op === 'logic') {
|
|
1183
|
+
if (!['and', 'or', 'not'].includes(node.logic))
|
|
1184
|
+
return `${path}.logic must be and, or or not, got ${JSON.stringify(node.logic)}`;
|
|
1185
|
+
if (!Array.isArray(node.args) || !node.args.length)
|
|
1186
|
+
return `${path}.args must be a non-empty array of nodes`;
|
|
1187
|
+
for (let i = 0; i < node.args.length; i++) {
|
|
1188
|
+
const bad = walk(node.args[i], `${path}.args[${i}]`, depth + 1);
|
|
1189
|
+
if (bad)
|
|
1190
|
+
return bad;
|
|
1191
|
+
}
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
if (node.op === 'graph') {
|
|
1195
|
+
if (!['data', 'active', 'visible'].includes(node.read))
|
|
1196
|
+
return `${path}.read must be data, active or visible, got ${JSON.stringify(node.read)}`;
|
|
1197
|
+
if (typeof node.item !== 'string' || !node.item)
|
|
1198
|
+
return `${path}.item must name the item to read`;
|
|
1199
|
+
if (node.read === 'data' && (typeof node.property !== 'string' || !node.property))
|
|
1200
|
+
return `${path}.property must name the item.data key to read`;
|
|
1201
|
+
}
|
|
1202
|
+
return null;
|
|
1203
|
+
};
|
|
1204
|
+
const bad = walk(p.predicate, 'params.predicate', 0);
|
|
1205
|
+
if (bad)
|
|
1206
|
+
return `${bad}. ${example}.`;
|
|
1207
|
+
const named = (v) => typeof v === 'string' && v.length > 0;
|
|
1208
|
+
if (!named(p.event) && !named(p.else)) {
|
|
1209
|
+
return 'params.event is missing, so nothing would fire: on_event_fire_if pulses params.event when the predicate holds (and params.else when it does not), never its target. '
|
|
1210
|
+
+ 'Make the event to fire with pinepaper_event and pass its id as params.event.';
|
|
1211
|
+
}
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* fireIfProblem for any relation a tool is about to add: an on_event_fire_if
|
|
1216
|
+
* itself, or one an on_event_add_relation will add when its event fires
|
|
1217
|
+
* (params.type / params.params). Every path that adds relations asks this, so
|
|
1218
|
+
* the check does not depend on which tool built the relation.
|
|
1219
|
+
*/
|
|
1220
|
+
export function relationSpecProblem(relationType, params) {
|
|
1221
|
+
const p = (params && typeof params === 'object' ? params : {});
|
|
1222
|
+
if (relationType === 'on_event_fire_if')
|
|
1223
|
+
return fireIfProblem(p);
|
|
1224
|
+
if (typeof relationType === 'string' && VISIBILITY_REACTION.test(relationType))
|
|
1225
|
+
return visibilityProblem(relationType, p);
|
|
1226
|
+
if (relationType === 'on_event_add_relation' && (p.type === 'on_event_fire_if' || (typeof p.type === 'string' && VISIBILITY_REACTION.test(p.type)))) {
|
|
1227
|
+
const innerParams = (p.params && typeof p.params === 'object' ? p.params : {});
|
|
1228
|
+
const inner = p.type === 'on_event_fire_if' ? fireIfProblem(innerParams) : visibilityProblem(p.type, innerParams);
|
|
1229
|
+
return inner ? `the ${String(p.type)} it adds: ${inner.replace(/params\./g, 'params.params.')}` : null;
|
|
1230
|
+
}
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
const VISIBILITY_REACTION = /^on_(event|enter|exit)_set_visibility$/;
|
|
1234
|
+
/**
|
|
1235
|
+
* A visibility reaction reads params.visible and nothing else: absent means show. `value`, which the other set_*
|
|
1236
|
+
* reactions take, was ignored, so {value: false} SHOWED the item and a hide did nothing (issue #29 O4).
|
|
1237
|
+
*/
|
|
1238
|
+
function visibilityProblem(relationType, p) {
|
|
1239
|
+
if (p.visible !== undefined || !('value' in p))
|
|
1240
|
+
return null;
|
|
1241
|
+
return `${relationType} reads params.visible (true shows, false hides; absent shows), not params.value, so this would `
|
|
1242
|
+
+ `${p.value === false || p.value === 0 || p.value === 'hidden' ? 'show the item instead of hiding it' : 'do nothing the value says'}. Pass params.visible: ${JSON.stringify(p.value === false || p.value === 0 || p.value === 'hidden' ? false : true)}.`;
|
|
1243
|
+
}
|
|
1145
1244
|
function generateAddRelationCode(sourceId, targetId, relationType, params) {
|
|
1146
1245
|
// AN UNKNOWN ENDPOINT IS REFUSED, NOT STORED (round 9 GG, 1.74). The engine
|
|
1147
1246
|
// stores a relation to an id it cannot resolve, returns true, and says "it
|
|
@@ -1162,7 +1261,7 @@ function generateAddRelationCode(sourceId, targetId, relationType, params) {
|
|
|
1162
1261
|
}
|
|
1163
1262
|
const success = app.addRelation('${sourceId}', '${targetId}', '${relationType}', ${JSON.stringify(params, null, 2)});
|
|
1164
1263
|
if (!success) {
|
|
1165
|
-
const __why = ${relationRefusalReasonJs(
|
|
1264
|
+
const __why = ${relationRefusalReasonJs(jsLiteral(relationType))};
|
|
1166
1265
|
return { success: false, relationType: '${relationType}', error: 'the studio refused the ${relationType} relation between ${sourceId} and ${targetId}' + (__why ? ': ' + __why : '.') };
|
|
1167
1266
|
}
|
|
1168
1267
|
app.historyManager.saveState();
|
|
@@ -1210,6 +1309,14 @@ export function emitterCountWarnings(spec) {
|
|
|
1210
1309
|
? `${where} ${asked} is over the ${EMITTER_MAX_PER_BURST}-particle cap per burst of the current studio (${EMITTER_LEGACY_MAX_PER_BURST} on older studios), so it draws at most ${EMITTER_MAX_PER_BURST}. Split it into bursts at the same t for more.`
|
|
1211
1310
|
: `${where} ${asked} is over the ${EMITTER_LEGACY_MAX_PER_BURST}-particle cap per burst of studios before the ${EMITTER_MAX_PER_BURST} cap, and this studio did not report what it drew: on an older studio it draws ${EMITTER_LEGACY_MAX_PER_BURST}. Split it into bursts at the same t to be sure.`);
|
|
1212
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* The scene lint every batch and custom-code call runs asks for warnings only.
|
|
1314
|
+
* Only lintScene().warnings (the text-clipping rule) is used, and a full
|
|
1315
|
+
* lintScene first runs RelationalLinter.analyze, a pairwise pass over every
|
|
1316
|
+
* item, whose result was thrown away. FxTool #220 added { warningsOnly: true },
|
|
1317
|
+
* which skips it and returns the same warnings; a studio before that ignores
|
|
1318
|
+
* the option and runs the full lint, as it always did.
|
|
1319
|
+
*/
|
|
1213
1320
|
/**
|
|
1214
1321
|
* An emitter spec as the studio reads it, and what the reply says about it.
|
|
1215
1322
|
*
|
|
@@ -1246,6 +1353,9 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1246
1353
|
const mismatch = (where, count, velocities) => {
|
|
1247
1354
|
if (!Array.isArray(velocities) || !velocities.length)
|
|
1248
1355
|
return;
|
|
1356
|
+
// A count given as a range is not read (said below); comparing it as the default 12 would be a false warning.
|
|
1357
|
+
if (Array.isArray(count))
|
|
1358
|
+
return;
|
|
1249
1359
|
const n = typeof count === 'number' && Number.isFinite(count) ? Math.round(count) : (assumeDefaultCount ? DEFAULT_COUNT : null);
|
|
1250
1360
|
if (n === null || n === velocities.length)
|
|
1251
1361
|
return;
|
|
@@ -1263,6 +1373,32 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1263
1373
|
if (own.count !== undefined || own.velocities !== undefined)
|
|
1264
1374
|
mismatch(`bursts[${i}]`, own.count ?? out.count, own.velocities ?? out.velocities);
|
|
1265
1375
|
});
|
|
1376
|
+
// A RANGE WHERE THE STUDIO READS ONE NUMBER is not drawn as a range (1.6.24
|
|
1377
|
+
// probe: gravity [100, 300] fell to 0, opacity [0.2, 1] to 1, no word). What
|
|
1378
|
+
// it draws instead depends on the studio build: older ones take num()'s
|
|
1379
|
+
// fallback (count [a, b] → 0, so nothing is drawn); FxTool #228 draws the
|
|
1380
|
+
// middle and says so (emitter-range-not-read). Only size, speed, angle,
|
|
1381
|
+
// spawn, lifetime and spin go through range(). The text says what holds on
|
|
1382
|
+
// both, so it is true whichever studio the server is pointed at.
|
|
1383
|
+
const SCALAR_ONLY = ['gravity', 'drag', 'friction', 'opacity', 'fadeIn', 'fadeOut', 'strokeWidth', 'jitter', 'jitterHz', 'aspect', 'sizeEnd', 'count'];
|
|
1384
|
+
const SPEC_ONLY = ['rate', 'start', 'end', 'seed'];
|
|
1385
|
+
const OUTCOME = {
|
|
1386
|
+
count: 'an older studio reads a range here as 0 and draws no particles',
|
|
1387
|
+
};
|
|
1388
|
+
const scalarRanges = (where, o, keys) => {
|
|
1389
|
+
for (const k of keys) {
|
|
1390
|
+
const v = o[k];
|
|
1391
|
+
if (Array.isArray(v) && v.length >= 2) {
|
|
1392
|
+
warnings.push(`${where}.${k} ${JSON.stringify(v)} is not drawn as a range: ${k} takes one number, so the studio draws one value for every particle (its default, or the middle of the range, depending on the build${OUTCOME[k] ? `; ${OUTCOME[k]}` : ''}). Pass the single value you want. Only size, speed, angle, spawn, lifetime and spin take a [min, max] range.`);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const f = o.fade;
|
|
1396
|
+
if (f && typeof f === 'object' && !Array.isArray(f))
|
|
1397
|
+
scalarRanges(`${where}.fade`, f, ['in', 'out']);
|
|
1398
|
+
};
|
|
1399
|
+
scalarRanges('spec', out, [...SCALAR_ONLY, ...SPEC_ONLY]);
|
|
1400
|
+
bursts.forEach((b, i) => { if (b && typeof b === 'object')
|
|
1401
|
+
scalarRanges(`bursts[${i}]`, b, SCALAR_ONLY); });
|
|
1266
1402
|
return { spec: out, warnings };
|
|
1267
1403
|
}
|
|
1268
1404
|
/**
|
|
@@ -1415,15 +1551,16 @@ app.historyManager.saveState();
|
|
|
1415
1551
|
* Template for querying relations
|
|
1416
1552
|
*/
|
|
1417
1553
|
function generateQueryRelationsCode(itemId, relationType, direction = 'outgoing') {
|
|
1554
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1418
1555
|
if (direction === 'outgoing') {
|
|
1419
1556
|
const typeArg = relationType ? `'${relationType}'` : 'undefined';
|
|
1420
1557
|
return `
|
|
1421
|
-
// Query outgoing relations for ${
|
|
1422
|
-
const relations = app.getRelations(
|
|
1558
|
+
// Query outgoing relations for ${ID}
|
|
1559
|
+
const relations = app.getRelations(${ID}, ${typeArg});
|
|
1423
1560
|
|
|
1424
1561
|
// Format results
|
|
1425
1562
|
const formatted = relations.map(r => ({
|
|
1426
|
-
sourceId: r.sourceId ||
|
|
1563
|
+
sourceId: r.sourceId || ${ID},
|
|
1427
1564
|
// RelationRegistry.getAssociations returns { relation, target, params } —
|
|
1428
1565
|
// there is no \`targetId\` on it. Reading the wrong field made EVERY edge come
|
|
1429
1566
|
// back with an undefined destination, so the graph was unreadable through
|
|
@@ -1439,13 +1576,13 @@ const formatted = relations.map(r => ({
|
|
|
1439
1576
|
else {
|
|
1440
1577
|
const typeArg = relationType ? `'${relationType}'` : 'undefined';
|
|
1441
1578
|
return `
|
|
1442
|
-
// Query incoming relations for ${
|
|
1443
|
-
const results = app.queryByRelationTarget(
|
|
1579
|
+
// Query incoming relations for ${ID}
|
|
1580
|
+
const results = app.queryByRelationTarget(${ID}, ${typeArg});
|
|
1444
1581
|
|
|
1445
1582
|
// Format results
|
|
1446
1583
|
const relations = results.map(r => ({
|
|
1447
1584
|
sourceId: r.itemId,
|
|
1448
|
-
targetId:
|
|
1585
|
+
targetId: ${ID},
|
|
1449
1586
|
// queryByTarget returns the real relation name on each result. Hardcoding
|
|
1450
1587
|
// the filter string labelled every edge 'unknown' on an unfiltered query —
|
|
1451
1588
|
// which is exactly the query you make when exploring an unfamiliar graph.
|
|
@@ -1461,13 +1598,14 @@ const relations = results.map(r => ({
|
|
|
1461
1598
|
* Template for simple animation
|
|
1462
1599
|
*/
|
|
1463
1600
|
function generateAnimateCode(itemId, animationType, speed, intensity, delay) {
|
|
1601
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1464
1602
|
const extra = (intensity !== undefined ? `,\n animationIntensity: ${intensity}` : '') +
|
|
1465
1603
|
(delay !== undefined ? `,\n animationDelay: ${delay}` : '');
|
|
1466
1604
|
return `
|
|
1467
|
-
// Apply ${animationType} animation to ${
|
|
1468
|
-
const item = app.getItemById(
|
|
1605
|
+
// Apply ${animationType} animation to ${ID}
|
|
1606
|
+
const item = app.getItemById(${ID});
|
|
1469
1607
|
if (!item) {
|
|
1470
|
-
throw new Error('Item not found: ${
|
|
1608
|
+
throw new Error('Item not found: ' + ${ID});
|
|
1471
1609
|
}
|
|
1472
1610
|
app.animate(item, {
|
|
1473
1611
|
animationType: '${animationType}',
|
|
@@ -1483,13 +1621,13 @@ app.animate(item, {
|
|
|
1483
1621
|
if (r) {
|
|
1484
1622
|
return {
|
|
1485
1623
|
success: false,
|
|
1486
|
-
itemId:
|
|
1624
|
+
itemId: ${ID},
|
|
1487
1625
|
error: 'Unknown animationType "' + r.requested + '" — nothing will move.',
|
|
1488
1626
|
requested: r.requested,
|
|
1489
1627
|
known: r.known,
|
|
1490
1628
|
};
|
|
1491
1629
|
}
|
|
1492
|
-
return { success: true, itemId:
|
|
1630
|
+
return { success: true, itemId: ${ID}, animationType: '${animationType}' };
|
|
1493
1631
|
})();
|
|
1494
1632
|
`.trim();
|
|
1495
1633
|
}
|
|
@@ -1539,10 +1677,11 @@ function normalizeKeyframePositions(keyframes) {
|
|
|
1539
1677
|
* "item_7 is not on the canvas" is actionable, silence is not.
|
|
1540
1678
|
*/
|
|
1541
1679
|
function requireItem(itemId, what) {
|
|
1680
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1542
1681
|
const isRef = typeof itemId === 'string' && itemId.startsWith('$');
|
|
1543
1682
|
const targetExpr = isRef
|
|
1544
|
-
? `((typeof window !== 'undefined' && window.__pinepaper_batch_item_ids && window.__pinepaper_batch_item_ids[${itemId.substring(1)}]) ||
|
|
1545
|
-
:
|
|
1683
|
+
? `((typeof window !== 'undefined' && window.__pinepaper_batch_item_ids && window.__pinepaper_batch_item_ids[${itemId.substring(1)}]) || ${ID})`
|
|
1684
|
+
: `${ID}`;
|
|
1546
1685
|
return `
|
|
1547
1686
|
const _targetId = ${targetExpr};
|
|
1548
1687
|
const _target = (app.getItemById && app.getItemById(_targetId))
|
|
@@ -1580,6 +1719,7 @@ function unanimatedTextKeysJs(keys) {
|
|
|
1580
1719
|
/** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
|
|
1581
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. The current studio animates fontStyle, textAlign, lineHeight and letterSpacing; here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX.'`;
|
|
1582
1721
|
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
|
|
1722
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1583
1723
|
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
1584
1724
|
const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
|
|
1585
1725
|
const opts = { duration: calculatedDuration, loop };
|
|
@@ -1661,9 +1801,9 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1661
1801
|
const easingAtCheck = easingAt !== undefined ? `
|
|
1662
1802
|
if (__opts && Object.prototype.hasOwnProperty.call(__opts, 'easingAt')) __warn.push('easingAt ' + ${JSON.stringify(JSON.stringify(easingAt))} + ' was not applied: this studio predates it, so every easing was read as the move ARRIVING at its key. Put each easing on the key the move goes to instead.');` : '';
|
|
1663
1803
|
const runtimeWarning = `${textKeysJs ? ', ...(__ignored.length ? { ignoredProperties: __ignored } : {})' : ''}, ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
|
|
1664
|
-
const addAnimTarget = itemId.startsWith('$') ? '_targetId' :
|
|
1804
|
+
const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `${ID}`;
|
|
1665
1805
|
return `
|
|
1666
|
-
// Apply keyframe animation to ${
|
|
1806
|
+
// Apply keyframe animation to ${ID}
|
|
1667
1807
|
(function() {
|
|
1668
1808
|
${requireItem(itemId, 'the animation')}${audioLevelGuard}
|
|
1669
1809
|
// The track as it was, so the result can say what happened to it: merged
|
|
@@ -1763,15 +1903,16 @@ function emitGeneratorRun(generatorName, paramsJson, region) {
|
|
|
1763
1903
|
* Template for applying effects
|
|
1764
1904
|
*/
|
|
1765
1905
|
function generateApplyEffectCode(itemId, effectType, params) {
|
|
1906
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1766
1907
|
return `
|
|
1767
|
-
// Apply ${effectType} effect to ${
|
|
1768
|
-
const item = app.getItemById(
|
|
1908
|
+
// Apply ${effectType} effect to ${ID}
|
|
1909
|
+
const item = app.getItemById(${ID});
|
|
1769
1910
|
if (!item) {
|
|
1770
|
-
throw new Error('Item not found: ${
|
|
1911
|
+
throw new Error('Item not found: ' + ${ID});
|
|
1771
1912
|
}
|
|
1772
1913
|
app.applyEffect(item, '${effectType}', ${JSON.stringify(params, null, 2)});
|
|
1773
1914
|
|
|
1774
|
-
({ success: true, itemId:
|
|
1915
|
+
({ success: true, itemId: ${ID}, effectType: '${effectType}' });
|
|
1775
1916
|
`.trim();
|
|
1776
1917
|
}
|
|
1777
1918
|
/**
|
|
@@ -2082,6 +2223,25 @@ const MEDIA_PURGE = `
|
|
|
2082
2223
|
if (__PA && typeof __PA.listMedia === 'function' && typeof __PA.removeMedia === 'function') {
|
|
2083
2224
|
__PA.listMedia().forEach(function(m) { try { if (__PA.removeMedia(m.id)) __mediaRemoved++; } catch (_) { /* keep clearing */ } });
|
|
2084
2225
|
}`;
|
|
2226
|
+
/**
|
|
2227
|
+
* THE ITEM-ID COUNTER STARTS AGAIN AFTER A CLEAR (O1, mcp-server#29). Run after clearCanvas.
|
|
2228
|
+
*
|
|
2229
|
+
* The studio's clearCanvas keeps uploaded clips, so it calls itemRegistry.clear with preserveTypes, and that branch
|
|
2230
|
+
* never resets nextId: a batch that made item_1 made item_2 when replayed after a clear. The rule: the next id is one
|
|
2231
|
+
* past the highest item_N still registered, so 1 on an empty canvas and never an id a kept clip holds. (An undo of
|
|
2232
|
+
* the clear restores the old items through rebuildFromCanvas, which moves nextId past them, as the studio's own
|
|
2233
|
+
* full clear already relies on.) The previous batch's $N ids named items that are gone, so they are forgotten too.
|
|
2234
|
+
*/
|
|
2235
|
+
const ID_COUNTER_RESET = `
|
|
2236
|
+
if (app.itemRegistry && typeof app.itemRegistry.nextId === 'number' && typeof app.itemRegistry.getAll === 'function') {
|
|
2237
|
+
let __maxLive = 0;
|
|
2238
|
+
app.itemRegistry.getAll().forEach(function (e) {
|
|
2239
|
+
const m = /^item_(\\d+)$/.exec((e && (e.id || e.itemId || (e.item && e.item.data && (e.item.data.registryId || e.item.data.id)))) || '');
|
|
2240
|
+
if (m) __maxLive = Math.max(__maxLive, Number(m[1]));
|
|
2241
|
+
});
|
|
2242
|
+
app.itemRegistry.nextId = __maxLive + 1;
|
|
2243
|
+
}
|
|
2244
|
+
if (typeof window !== 'undefined') window.__pinepaper_batch_item_ids = [];`;
|
|
2085
2245
|
/**
|
|
2086
2246
|
* Template for clearing all items from canvas
|
|
2087
2247
|
*/
|
|
@@ -2105,6 +2265,7 @@ if (typeof app.clearCanvas === 'function') {
|
|
|
2105
2265
|
if (app.itemRegistry) app.itemRegistry.clear();
|
|
2106
2266
|
if (app.relationRegistry) app.relationRegistry.clear();
|
|
2107
2267
|
}
|
|
2268
|
+
${ID_COUNTER_RESET}
|
|
2108
2269
|
|
|
2109
2270
|
if (app.historyManager) app.historyManager.saveState();
|
|
2110
2271
|
|
|
@@ -2355,13 +2516,23 @@ export function planKeyframeMerges(operations) {
|
|
|
2355
2516
|
* through modifyItem after create, so the engine's own bookkeeping runs.
|
|
2356
2517
|
*/
|
|
2357
2518
|
const BRAND_FILL_TYPES = new Set(['circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'pentagon', 'hexagon', 'diamond', 'arrow', 'heart']);
|
|
2519
|
+
/** A closed shape given a stroke colour and no fill of any spelling: drawn as an outline (issue #29 O7). */
|
|
2520
|
+
function isStrokeOnly(itemType, props) {
|
|
2521
|
+
if (!BRAND_FILL_TYPES.has(itemType))
|
|
2522
|
+
return false;
|
|
2523
|
+
if (props.strokeColor === undefined || props.strokeColor === null)
|
|
2524
|
+
return false;
|
|
2525
|
+
return ['color', 'fillColor', 'fill', 'gradient'].every((k) => props[k] === undefined);
|
|
2526
|
+
}
|
|
2527
|
+
const OUTLINE_NOTE = 'none: a strokeColor with no fill draws an outline. Pass fillColor to fill it.';
|
|
2358
2528
|
function withBrandDefaults(code, itemType, props) {
|
|
2359
2529
|
if (props.noBrand === true)
|
|
2360
2530
|
return code;
|
|
2361
2531
|
const isText = itemType === 'text';
|
|
2362
2532
|
if (!isText && !BRAND_FILL_TYPES.has(itemType))
|
|
2363
2533
|
return code;
|
|
2364
|
-
|
|
2534
|
+
// An outline is a colour choice: the brand does not fill it in.
|
|
2535
|
+
const hasColor = ['color', 'fillColor', 'fill'].some((k) => props[k] !== undefined) || isStrokeOnly(itemType, props);
|
|
2365
2536
|
const hasFont = props.fontFamily !== undefined;
|
|
2366
2537
|
if (hasColor && (!isText || hasFont))
|
|
2367
2538
|
return code;
|
|
@@ -2963,6 +3134,11 @@ export class PinePaperCodeGenerator {
|
|
|
2963
3134
|
return { success: true, presetId: ${JSON.stringify(validated.presetId)}, relationType: r.relationType, params: r.params, values: r.values };
|
|
2964
3135
|
})();`.trim();
|
|
2965
3136
|
}
|
|
3137
|
+
{
|
|
3138
|
+
const problem = relationSpecProblem(validated.relationType, validated.params);
|
|
3139
|
+
if (problem)
|
|
3140
|
+
return `(function() { return { success: false, relationType: ${jsLiteral(validated.relationType)}, error: ${jsLiteral(problem)} }; })();`;
|
|
3141
|
+
}
|
|
2966
3142
|
if (validated.relationType === 'moves_along_path') {
|
|
2967
3143
|
return generateMovesAlongPathCode(validated.sourceId, validated.targetId ?? undefined, (validated.params ?? {}));
|
|
2968
3144
|
}
|
|
@@ -3639,6 +3815,15 @@ if (typeof _gen === 'function') {
|
|
|
3639
3815
|
for (const relation of relations) {
|
|
3640
3816
|
const { source, target, type, params = {} } = relation;
|
|
3641
3817
|
const paramsStr = JSON.stringify(params);
|
|
3818
|
+
const specProblem = relationSpecProblem(type, params);
|
|
3819
|
+
if (specProblem) {
|
|
3820
|
+
codeParts.push(`
|
|
3821
|
+
// Refused relation: ${source} ${type} ${target}
|
|
3822
|
+
(results.relationsRefused = results.relationsRefused || []).push(${JSON.stringify({ source, target, type, reason: specProblem })});
|
|
3823
|
+
results.success = false;
|
|
3824
|
+
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
|
+
continue;
|
|
3826
|
+
}
|
|
3642
3827
|
codeParts.push(`
|
|
3643
3828
|
// Add relation: ${source} ${type} ${target}
|
|
3644
3829
|
(function() {
|
|
@@ -3650,7 +3835,7 @@ if (typeof _gen === 'function') {
|
|
|
3650
3835
|
if (app.addRelation(sourceId, targetId, '${type}', params)) {
|
|
3651
3836
|
results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
|
|
3652
3837
|
} else {
|
|
3653
|
-
const __why = ${relationRefusalReasonJs(
|
|
3838
|
+
const __why = ${relationRefusalReasonJs(jsLiteral(type))};
|
|
3654
3839
|
(results.relationsRefused = results.relationsRefused || []).push({ source: '${source}', target: '${target}', type: '${type}', reason: __why || 'the studio refused it' });
|
|
3655
3840
|
results.success = false;
|
|
3656
3841
|
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.';
|
|
@@ -4068,7 +4253,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4068
4253
|
if (app.patternGroup) app.patternGroup.removeChildren();
|
|
4069
4254
|
if (app.itemRegistry) app.itemRegistry.clear();
|
|
4070
4255
|
if (app.relationRegistry) app.relationRegistry.clear();
|
|
4071
|
-
}
|
|
4256
|
+
}${ID_COUNTER_RESET}
|
|
4072
4257
|
`;
|
|
4073
4258
|
}
|
|
4074
4259
|
if (canvasPreset) {
|
|
@@ -4311,7 +4496,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4311
4496
|
if (app.patternGroup) app.patternGroup.removeChildren();
|
|
4312
4497
|
if (app.itemRegistry) app.itemRegistry.clear();
|
|
4313
4498
|
if (app.relationRegistry) app.relationRegistry.clear();
|
|
4314
|
-
}
|
|
4499
|
+
}${ID_COUNTER_RESET}
|
|
4315
4500
|
`;
|
|
4316
4501
|
}
|
|
4317
4502
|
else {
|
|
@@ -4319,7 +4504,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4319
4504
|
// Clear items but preserve background
|
|
4320
4505
|
if (app.textItemGroup) app.textItemGroup.removeChildren();
|
|
4321
4506
|
if (app.itemRegistry) app.itemRegistry.clear();
|
|
4322
|
-
if (app.relationRegistry) app.relationRegistry.clear()
|
|
4507
|
+
if (app.relationRegistry) app.relationRegistry.clear();${ID_COUNTER_RESET}
|
|
4323
4508
|
`;
|
|
4324
4509
|
}
|
|
4325
4510
|
if (canvasPreset) {
|
|
@@ -4374,7 +4559,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4374
4559
|
|| (typeof v.reason === 'string' && v.reason)
|
|
4375
4560
|
|| 'the operation reported failure without naming a reason';
|
|
4376
4561
|
};
|
|
4377
|
-
// $N is the Nth item THIS batch
|
|
4562
|
+
// $N is the Nth item THIS batch made (the documented contract): create and group ops take a slot each, in op
|
|
4563
|
+
// order (O2, mcp-server#29: a group is an item every later op addresses, and it had no slot). The
|
|
4378
4564
|
// previous batch's ids are kept only to name the right id when a $N reaches
|
|
4379
4565
|
// past what this batch has made: picking it silently shifted every $N in a
|
|
4380
4566
|
// batch that also creates (B115 follow-up).
|
|
@@ -4383,9 +4569,11 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4383
4569
|
const __ref = function (n) {
|
|
4384
4570
|
if (itemIds[n] !== undefined) return itemIds[n];
|
|
4385
4571
|
const prev = __prevBatchIds[n];
|
|
4386
|
-
throw new Error('$' + n + ' is
|
|
4387
|
-
+ (prev ? ' In the previous batch $' + n + ' was ' + prev + ' — pass that id instead.' : ' Put the
|
|
4572
|
+
throw new Error('$' + n + ' is item number ' + n + ' THIS batch made (create and group ops are numbered, in op order, from $0), and it has made ' + itemIds.length + ' so far.'
|
|
4573
|
+
+ (prev ? ' In the previous batch $' + n + ' was ' + prev + ' — pass that id instead.' : ' Put the ops that make it first, or pass the item id.'));
|
|
4388
4574
|
};
|
|
4575
|
+
// A failure names the item, not the "$1" that addressed it; the ref as written when the batch never made it.
|
|
4576
|
+
const __refName = function (n) { return itemIds[n] !== undefined ? itemIds[n] : '$' + n; };
|
|
4389
4577
|
const results = [];
|
|
4390
4578
|
let success = true;
|
|
4391
4579
|
// A failure's words, never "undefined" for a thrown non-Error.
|
|
@@ -4397,7 +4585,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4397
4585
|
const __ppLintNow = function () {
|
|
4398
4586
|
try {
|
|
4399
4587
|
if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];
|
|
4400
|
-
const r = app.lintScene();
|
|
4588
|
+
const r = app.lintScene({ warningsOnly: true });
|
|
4401
4589
|
return r && Array.isArray(r.warnings) ? r.warnings : [];
|
|
4402
4590
|
} catch (e) {
|
|
4403
4591
|
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
@@ -4421,12 +4609,14 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4421
4609
|
// $N reference: a batch of [create ×5, keyframe_animate ×5, create ×5,
|
|
4422
4610
|
// keyframe_animate ×5] silently re-animated the FIRST five items and
|
|
4423
4611
|
// left the second five without keyframes (mis-oriented/mis-animated
|
|
4424
|
-
// vehicles, 2026-07-30). `group`
|
|
4425
|
-
//
|
|
4426
|
-
|
|
4612
|
+
// vehicles, 2026-07-30). A `group` MAKES an item (the group), so it
|
|
4613
|
+
// claims a slot too (O2, mcp-server#29): [create, create, group,
|
|
4614
|
+
// keyframe_animate "$2"] keys the group.
|
|
4615
|
+
const claimsItemSlot = op.type === 'create' || op.type === 'group';
|
|
4427
4616
|
// The item this op acts on, for a failure's itemId: its own id field, else what its result names.
|
|
4428
4617
|
const o = op;
|
|
4429
4618
|
const opItem = [o.itemId, o.targetId, o.sourceId, o.id].find((v) => typeof v === 'string') ?? null;
|
|
4619
|
+
const opItemJs = typeof opItem === 'string' && /^\$\d+$/.test(opItem) ? `__refName(${opItem.slice(1)})` : JSON.stringify(opItem);
|
|
4430
4620
|
const opStep = typeof o._step === 'string' ? `op ${index}: ${o._step}` : `op ${index} (${op.type})`;
|
|
4431
4621
|
code += `
|
|
4432
4622
|
// Operation ${index}: ${op.type}
|
|
@@ -4444,7 +4634,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4444
4634
|
if (failed${index}) {
|
|
4445
4635
|
// ATOMIC: throw WITHOUT recording, and let the catch below record it
|
|
4446
4636
|
// once. Doing both put two entries in for one failed op.
|
|
4447
|
-
${isAtomic ? `const __e${index} = new Error(failed${index}); __e${index}.itemId = (result${index} && typeof result${index}.itemId === 'string') ? result${index}.itemId : ${
|
|
4637
|
+
${isAtomic ? `const __e${index} = new Error(failed${index}); __e${index}.itemId = (result${index} && typeof result${index}.itemId === 'string') ? result${index}.itemId : ${opItemJs}; throw __e${index};` : `results.push({ index: ${index}, success: false, step: ${JSON.stringify(opStep)}, itemId: (result${index} && typeof result${index}.itemId === 'string') ? result${index}.itemId : ${opItemJs}, reason: failed${index}, error: failed${index}, result: result${index} });
|
|
4448
4638
|
success = false;`}
|
|
4449
4639
|
} else {
|
|
4450
4640
|
results.push({ index: ${index}, success: true, result: result${index} });
|
|
@@ -4454,7 +4644,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4454
4644
|
}
|
|
4455
4645
|
} catch (opError) {
|
|
4456
4646
|
const __r${index} = _why(opError, 'operation ${index} (${op.type}) threw with no message');
|
|
4457
|
-
results.push({ index: ${index}, success: false, step: ${JSON.stringify(opStep)}, itemId: (opError && typeof opError.itemId === 'string') ? opError.itemId : ${
|
|
4647
|
+
results.push({ index: ${index}, success: false, step: ${JSON.stringify(opStep)}, itemId: (opError && typeof opError.itemId === 'string') ? opError.itemId : ${opItemJs}, reason: __r${index}, error: __r${index} });
|
|
4458
4648
|
${isAtomic ? 'throw opError;' : 'success = false;'}
|
|
4459
4649
|
}
|
|
4460
4650
|
`;
|
|
@@ -4576,7 +4766,7 @@ return __created;
|
|
|
4576
4766
|
// at run time, so it is generated with a placeholder and the runtime
|
|
4577
4767
|
// variable put in its place.
|
|
4578
4768
|
const single = generateModifyItemCode('__MID__', (op.properties || {})).trim()
|
|
4579
|
-
.replace(/
|
|
4769
|
+
.replace(/"__MID__"/g, '__mid')
|
|
4580
4770
|
.replace(/__MID__/g, "' + __mid + '")
|
|
4581
4771
|
.replace(/;\s*$/, '');
|
|
4582
4772
|
return `
|
|
@@ -4601,13 +4791,18 @@ return __r;
|
|
|
4601
4791
|
const targetId = ${animItemRef};
|
|
4602
4792
|
const item = app.getItemById(targetId);
|
|
4603
4793
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
4604
|
-
app.animate(item, { animationType:
|
|
4605
|
-
return { itemId: targetId, animationType:
|
|
4794
|
+
app.animate(item, { animationType: ${JSON.stringify(String(op.animationType))}, ...${animOpts} });
|
|
4795
|
+
return { itemId: targetId, animationType: ${JSON.stringify(String(op.animationType))} };
|
|
4606
4796
|
`;
|
|
4607
4797
|
case 'relation':
|
|
4608
4798
|
const srcRef = batchRef(op.sourceId);
|
|
4609
4799
|
const tgtRef = batchRef(op.targetId);
|
|
4610
4800
|
const relOpts = JSON.stringify(op.relationOptions || {});
|
|
4801
|
+
const fireIf = relationSpecProblem(op.relationType, op.relationOptions);
|
|
4802
|
+
if (fireIf)
|
|
4803
|
+
return `
|
|
4804
|
+
throw new Error(${JSON.stringify(String(op.relationType) + ': ' + fireIf)});
|
|
4805
|
+
`;
|
|
4611
4806
|
return `
|
|
4612
4807
|
const sourceId = ${srcRef};
|
|
4613
4808
|
const targetId = ${tgtRef};
|
|
@@ -4615,17 +4810,17 @@ const source = app.getItemById(sourceId);
|
|
|
4615
4810
|
const target = app.getItemById(targetId);
|
|
4616
4811
|
if (!source) throw new Error('Source not found: ' + sourceId);
|
|
4617
4812
|
if (!target) throw new Error('Target not found: ' + targetId);
|
|
4618
|
-
const relationId = app.addRelation(source, target,
|
|
4813
|
+
const relationId = app.addRelation(source, target, ${JSON.stringify(String(op.relationType))}, ${relOpts});
|
|
4619
4814
|
// NOT BOUND IS NOT A SUCCESS (owner rule 1, B119). addRelation answers false
|
|
4620
4815
|
// when it refuses — spring_follow "not loaded yet" — and this op returned
|
|
4621
4816
|
// { relationId: false } with nothing marking it failed, so the batch said
|
|
4622
4817
|
// success over a relation that does not exist.
|
|
4623
4818
|
if (!relationId) {
|
|
4624
|
-
const __why = ${relationRefusalReasonJs(
|
|
4819
|
+
const __why = ${relationRefusalReasonJs(jsLiteral(op.relationType))};
|
|
4625
4820
|
return { success: false, relationId: false, sourceId, targetId, relationType: ${JSON.stringify(op.relationType)},
|
|
4626
4821
|
error: 'the studio refused the ' + ${JSON.stringify(op.relationType)} + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };
|
|
4627
4822
|
}
|
|
4628
|
-
return { relationId, sourceId, targetId, relationType:
|
|
4823
|
+
return { relationId, sourceId, targetId, relationType: ${JSON.stringify(String(op.relationType))} };
|
|
4629
4824
|
`;
|
|
4630
4825
|
case 'delete':
|
|
4631
4826
|
const delRef = batchRef(op.itemId);
|
|
@@ -4642,7 +4837,7 @@ return { itemId: targetId, deleted: true };
|
|
|
4642
4837
|
const refs = (op.itemIds || []).map((ref) => batchRef(ref));
|
|
4643
4838
|
const idsArrayExpr = `[${refs.join(', ')}]`;
|
|
4644
4839
|
const nameExpr = op.groupName !== undefined ? JSON.stringify(op.groupName) : 'undefined';
|
|
4645
|
-
// Returns { groupId,
|
|
4840
|
+
// Returns { groupId, itemId }: the group takes the next $N slot (O2), as a create does.
|
|
4646
4841
|
return `
|
|
4647
4842
|
return (function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
|
|
4648
4843
|
})();
|
|
@@ -5238,7 +5433,7 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
5238
5433
|
const __lint = (function () {
|
|
5239
5434
|
if (typeof app.lintScene !== 'function') return null;
|
|
5240
5435
|
try {
|
|
5241
|
-
const r = app.lintScene();
|
|
5436
|
+
const r = app.lintScene({ warningsOnly: true });
|
|
5242
5437
|
return r && Array.isArray(r.warnings) ? r.warnings : null;
|
|
5243
5438
|
} catch (e) {
|
|
5244
5439
|
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run before the export: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
@@ -5325,14 +5520,36 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
5325
5520
|
});
|
|
5326
5521
|
|
|
5327
5522
|
let result = { success: false, platform, format, quality, framing };
|
|
5328
|
-
// A STILL SHOWS
|
|
5523
|
+
// THE MOMENT A STILL SHOWS (issue #29 O6). With no time, a still of a scene
|
|
5524
|
+
// that is playing was whatever frame the live playhead had reached when the
|
|
5525
|
+
// call landed: an orbiting planet sat at a different angle on every run, and
|
|
5526
|
+
// nowhere near its place in the video's first frame. A playing scene's still
|
|
5527
|
+
// is now its first frame (time 0), and the result says so; a stopped
|
|
5528
|
+
// playhead is a moment someone chose, and is kept.
|
|
5529
|
+
let __stillT = ${stillTime !== undefined ? stillTime : 'null'};
|
|
5530
|
+
let __stillTimeNote = null;
|
|
5531
|
+
${stillTime === undefined ? ` if (['png', 'jpg', 'webp', 'svg', 'pdf'].indexOf(format) !== -1 && typeof app.setPlaybackTime === 'function') {
|
|
5532
|
+
const __tr = app.transport;
|
|
5533
|
+
let __playing = !!(__tr && __tr.isPlaying) || !!(app.isPlayingKeyframes && !app.isAnimationPaused);
|
|
5534
|
+
if (!__playing && typeof app.playbackTime === 'number') {
|
|
5535
|
+
// The flags are not the only clock: watch the playhead for two frames.
|
|
5536
|
+
const __t0 = app.playbackTime;
|
|
5537
|
+
await new Promise((r) => { if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => requestAnimationFrame(r)); else setTimeout(r, 40); });
|
|
5538
|
+
__playing = app.playbackTime !== __t0;
|
|
5539
|
+
}
|
|
5540
|
+
if (__playing) {
|
|
5541
|
+
__stillT = 0;
|
|
5542
|
+
__stillTimeNote = 'the scene was playing, so this still is its first frame (time 0), the frame a video starts on. Pass time for another moment.';
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
` : ''} // A STILL SHOWS WHAT IS ALIVE AT ITS MOMENT. Lifetimes that fall back to the
|
|
5329
5546
|
// MCP's frame callback are applied by the frame loop, and a seek does not run
|
|
5330
5547
|
// it — so a png at time 0.8 s drew nothing staged at 0.8 s, and an item past
|
|
5331
5548
|
// its ttl still showed (retest). The visibility pass runs here for stills.
|
|
5332
5549
|
if (['png', 'jpg', 'webp', 'svg', 'pdf'].indexOf(format) !== -1) {
|
|
5333
5550
|
const __L = globalThis.__ppMcp;
|
|
5334
5551
|
if (__L && __L.lifetimeApp === app && typeof __L.applyLifetimes === 'function') {
|
|
5335
|
-
try { __L.applyLifetimes(
|
|
5552
|
+
try { __L.applyLifetimes(__stillT !== null ? __stillT : (typeof app.playbackTime === 'number' ? app.playbackTime : 0)); } catch (_) { /* the still renders either way */ }
|
|
5336
5553
|
}
|
|
5337
5554
|
}
|
|
5338
5555
|
// The engine's report on the audio track of THIS export (FxTool b41860aa:
|
|
@@ -5341,18 +5558,19 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
5341
5558
|
const __vx = app.exportEngine && app.exportEngine.videoExporter;
|
|
5342
5559
|
if (__vx && 'lastAudioReport' in __vx) __vx.lastAudioReport = null;
|
|
5343
5560
|
if (__vx && 'lastVideoReport' in __vx) __vx.lastVideoReport = null;
|
|
5344
|
-
${stillTime !== undefined ? `
|
|
5345
5561
|
// A STILL AT A CHOSEN MOMENT (round 7 X, 1.57). Without this a png is
|
|
5346
5562
|
// whatever frame the playhead is on — two identical builds gave PNGs that
|
|
5347
5563
|
// differed in the animated streak while their MP4s were byte-identical.
|
|
5348
5564
|
// Seek, render, and put the playhead back afterwards.
|
|
5349
5565
|
const __prevT = typeof app.playbackTime === 'number' ? app.playbackTime : 0;
|
|
5350
|
-
if (
|
|
5351
|
-
|
|
5566
|
+
if (__stillT !== null) {
|
|
5567
|
+
if (typeof app.setPlaybackTime !== 'function') {
|
|
5568
|
+
return { success: false, platform, format, error: 'app.setPlaybackTime unavailable — this studio cannot render a still at a chosen time.' };
|
|
5569
|
+
}
|
|
5570
|
+
app.setPlaybackTime(__stillT);
|
|
5571
|
+
if (typeof paper !== 'undefined' && paper.view && typeof paper.view.update === 'function') paper.view.update();
|
|
5352
5572
|
}
|
|
5353
|
-
|
|
5354
|
-
if (typeof paper !== 'undefined' && paper.view && typeof paper.view.update === 'function') paper.view.update();
|
|
5355
|
-
` : ''}
|
|
5573
|
+
|
|
5356
5574
|
try {
|
|
5357
5575
|
// jpg / webp render as png, then re-encode below.
|
|
5358
5576
|
switch ((format === 'jpg' || format === 'webp') ? 'png' : format) {
|
|
@@ -5387,7 +5605,7 @@ ${stillTime !== undefined ? `
|
|
|
5387
5605
|
if ('lastRegionExcluded' in app) app.lastRegionExcluded = null;
|
|
5388
5606
|
// A video in the region is waited for at this time (round 12, 12.5):
|
|
5389
5607
|
// the render drew before the clip's seek landed.
|
|
5390
|
-
if (typeof app.awaitMediaAt === 'function') await app.awaitMediaAt(
|
|
5608
|
+
if (typeof app.awaitMediaAt === 'function') await app.awaitMediaAt(__stillT !== null ? __stillT : (typeof app.playbackTime === 'number' ? app.playbackTime : 0));
|
|
5391
5609
|
const regionUrl = app.renderRegionToDataURL(new paper.Rectangle(${region.x}, ${region.y}, ${region.width}, ${region.height}), rw, rh${region.excludeForeign === false ? '' : ', { excludeForeign: true }'});
|
|
5392
5610
|
const __excluded = Array.isArray(app.lastRegionExcluded) ? app.lastRegionExcluded.filter(function(id) { return id; }) : null;
|
|
5393
5611
|
// ITEMS THAT CROSS THE REGION'S EDGE (round 9 II, 1.78). In the sheet
|
|
@@ -5444,7 +5662,9 @@ ${stillTime !== undefined ? `
|
|
|
5444
5662
|
|| app.canvasSize
|
|
5445
5663
|
|| { width: dimensions.width, height: dimensions.height };
|
|
5446
5664
|
const pngScale = pngBoard.width > 0 ? dimensions.width / pngBoard.width : 1;
|
|
5447
|
-
|
|
5665
|
+
// The engine evaluates the scene at time itself (keyframes, relations, loops); without it, a playing
|
|
5666
|
+
// scene's still is left as the live frame.
|
|
5667
|
+
const png = await app.exportEngine.exportPNG({ scale: pngScale, ...(__stillT !== null ? { time: __stillT } : {}) });
|
|
5448
5668
|
const dataUrl = (png && png.dataUrl) ? png.dataUrl : png;
|
|
5449
5669
|
result = {
|
|
5450
5670
|
success: true,
|
|
@@ -6168,8 +6388,10 @@ ${(() => {
|
|
|
6168
6388
|
warning: `${frames} frames at ${Math.round(fps * 1000) / 1000} fps is ${actual.toFixed(3)} s — ${((actual - videoDuration) * 1000).toFixed(1)} ms over the ${videoDuration} s asked for. For a hard cap (e.g. a 6 s bumper) use duration ${fit.toFixed(4)} (${Math.floor(videoDuration * fps)} frames).`,
|
|
6169
6389
|
})};\n`;
|
|
6170
6390
|
})()}
|
|
6171
|
-
|
|
6172
|
-
|
|
6391
|
+
if (__stillT !== null) {
|
|
6392
|
+
try { app.setPlaybackTime(__prevT); } catch (_) { /* the still is already rendered */ }
|
|
6393
|
+
if (result && result.success) { result.time = __stillT; if (__stillTimeNote) result.stillTimeNote = __stillTimeNote; }
|
|
6394
|
+
}
|
|
6173
6395
|
// SCREEN-SPACE ITEMS (FxTool D20): SVG and Lottie write geometry, not the
|
|
6174
6396
|
// camera's frame map, so a HUD item is placed as if the camera had not moved.
|
|
6175
6397
|
if (result && result.success && typeof app.screenSpaceItems === 'function' && ['svg', 'lottie', 'dotlottie'].indexOf(${JSON.stringify(format)}) !== -1) {
|
|
@@ -6897,7 +7119,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6897
7119
|
// unchanged. The map itself cannot be moved without breaking geoToCanvas
|
|
6898
7120
|
// (it projects against the untransformed map, so markers would land in the
|
|
6899
7121
|
// wrong place). Until the engine has a map view transform, say so.
|
|
6900
|
-
return `(function() { return { success: false, error: ${
|
|
7122
|
+
return `(function() { return { success: false, error: ${jsLiteral(MAP_VIEW_REFUSAL('pan'))} }; })();`;
|
|
6901
7123
|
}
|
|
6902
7124
|
/**
|
|
6903
7125
|
* Generate code to zoom the map
|
|
@@ -6910,7 +7132,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6910
7132
|
if (validated.duration !== undefined)
|
|
6911
7133
|
options.duration = validated.duration;
|
|
6912
7134
|
// Refused for the same reason as pan (see generatePanMap).
|
|
6913
|
-
return `(function() { return { success: false, error: ${
|
|
7135
|
+
return `(function() { return { success: false, error: ${jsLiteral(MAP_VIEW_REFUSAL('zoom'))} }; })();`;
|
|
6914
7136
|
}
|
|
6915
7137
|
/**
|
|
6916
7138
|
* Generate code to export map configuration
|
|
@@ -7859,7 +8081,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7859
8081
|
const __ppLintNow = function () {
|
|
7860
8082
|
try {
|
|
7861
8083
|
if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];
|
|
7862
|
-
const r = app.lintScene();
|
|
8084
|
+
const r = app.lintScene({ warningsOnly: true });
|
|
7863
8085
|
return r && Array.isArray(r.warnings) ? r.warnings : [];
|
|
7864
8086
|
} catch (e) {
|
|
7865
8087
|
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
@@ -7877,8 +8099,10 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7877
8099
|
reason: __ppReason, error: __ppReason, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
7878
8100
|
}
|
|
7879
8101
|
// Only wrapped when the lint has something to say: otherwise the value is returned exactly as before.
|
|
8102
|
+
// __ppWrapped marks the wrapper itself: a code that returned undefined loses the __ppValue key on its way out
|
|
8103
|
+
// of the page (JSON drops undefined), and without the mark the server read the bare wrapper as the value, {}.
|
|
7880
8104
|
const __ppL = __ppLintNow();
|
|
7881
|
-
return __ppL.length ? { __ppValue: __ppValue, __ppLint: __ppL } : __ppValue;
|
|
8105
|
+
return __ppL.length ? { __ppWrapped: true, __ppValue: __ppValue, __ppLint: __ppL } : __ppValue;
|
|
7882
8106
|
})();
|
|
7883
8107
|
`.trim();
|
|
7884
8108
|
}
|
|
@@ -8209,7 +8433,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
8209
8433
|
`.trim();
|
|
8210
8434
|
}
|
|
8211
8435
|
generateImportImage(input) {
|
|
8212
|
-
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset } = input;
|
|
8436
|
+
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset, name } = input;
|
|
8213
8437
|
// IMPORTSHADOW: the shadow keys, applied through the engine's own modify
|
|
8214
8438
|
// door once the image is placed (and masked) — the route modify_item takes.
|
|
8215
8439
|
const shadow = {};
|
|
@@ -8327,6 +8551,11 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
8327
8551
|
? { applied: true, shadowColor: placed.shadowColor ? String(placed.shadowColor.toCSS ? placed.shadowColor.toCSS(true) : placed.shadowColor) : null, shadowBlur: placed.shadowBlur }
|
|
8328
8552
|
: { applied: false, warning: 'the image was imported but its shadow was not applied — set it with pinepaper_modify_item on ' + itemId + '.' };
|
|
8329
8553
|
}
|
|
8554
|
+
` : ''}${name ? ` // PATCHIMG24: an image had no name, so a batch patch (which addresses by
|
|
8555
|
+
// name) could not reach it. Set where the registry's name lookup reads it,
|
|
8556
|
+
// on what is drawn (the mask group when masked).
|
|
8557
|
+
placed.name = ${JSON.stringify(name)};
|
|
8558
|
+
if (placed.data) placed.data.name = ${JSON.stringify(name)};
|
|
8330
8559
|
` : ''} const b = placed.bounds;
|
|
8331
8560
|
if (!b || !(b.width > 0) || !(b.height > 0)) {
|
|
8332
8561
|
return { success: false, itemId: itemId, error: 'the image was placed with no size (' + (b ? b.width + 'x' + b.height : 'no bounds') + ') — it may not have loaded. Nothing visible was drawn.' };
|
|
@@ -8334,7 +8563,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
8334
8563
|
return {
|
|
8335
8564
|
success: true,
|
|
8336
8565
|
itemId: itemId,
|
|
8337
|
-
message: 'Image imported and placed on canvas.'
|
|
8566
|
+
message: 'Image imported and placed on canvas.',${name ? `\n name: ${JSON.stringify(name)},` : ''}
|
|
8338
8567
|
bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: '${mask}'` : ''}${hasShadow ? ',\n shadow: __shadow' : ''},
|
|
8339
8568
|
// Stored smaller than its source (the studio's cap): said, with the
|
|
8340
8569
|
// way to keep every pixel. The studio reports native size where it can.
|
|
@@ -8701,7 +8930,7 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
|
|
|
8701
8930
|
case 'create': {
|
|
8702
8931
|
const spec = { id: v.id, kind: v.kind, members: v.members, params: v.params || {}, ...(v.window ? { window: v.window } : {}) };
|
|
8703
8932
|
body = `${need('addPattern')}
|
|
8704
|
-
if (!${S(v.kind ?? null)}) return { success: false, error: 'create needs kind:
|
|
8933
|
+
if (!${S(v.kind ?? null)}) return { success: false, error: ${S('create needs kind: ' + PATTERN_KINDS.join(', '))} };
|
|
8705
8934
|
if (!${S(v.members ?? null)}) return { success: false, error: 'create needs members: { names: [...] }, { ids: [...] }, { tag } or an array' };
|
|
8706
8935
|
const r = app.addPattern(${S(spec)});
|
|
8707
8936
|
return Object.assign({ success: !!(r && r.ok) }, r || {}, app.lastReceipt ? { receipt: app.lastReceipt } : {});`;
|
|
@@ -9367,6 +9596,8 @@ ${resolveSource}
|
|
|
9367
9596
|
})();`.trim();
|
|
9368
9597
|
}
|
|
9369
9598
|
generateTransform(input) {
|
|
9599
|
+
// Every id is a quoted literal: an id with a quote in it was a syntax error, and an id is caller text.
|
|
9600
|
+
const ID = JSON.stringify(input.itemId || '');
|
|
9370
9601
|
switch (input.action) {
|
|
9371
9602
|
case 'fit': {
|
|
9372
9603
|
const mode = input.mode || 'contain';
|
|
@@ -9378,31 +9609,37 @@ ${resolveSource}
|
|
|
9378
9609
|
// id space from canvas item ids. PinePaperAgent.fitToFrame resolves either.
|
|
9379
9610
|
const A = (typeof window !== 'undefined') && window.PinePaperAgent;
|
|
9380
9611
|
if (A && typeof A.fitToFrame === 'function') {
|
|
9381
|
-
const ok = A.fitToFrame(${
|
|
9612
|
+
const ok = A.fitToFrame(${ID}, ${JSON.stringify(mode)});
|
|
9382
9613
|
return ok
|
|
9383
|
-
? { success: true, action: 'fit', itemId:
|
|
9384
|
-
: { success: false, error: 'Item not found or not fittable: ${
|
|
9614
|
+
? { success: true, action: 'fit', itemId: ${ID}, mode: ${JSON.stringify(mode)} }
|
|
9615
|
+
: { success: false, error: 'Item not found or not fittable: ' + ${ID} };
|
|
9385
9616
|
}
|
|
9386
9617
|
if (typeof app.fitToFrame !== 'function') {
|
|
9387
9618
|
return { success: false, error: 'fitToFrame unavailable — update FxTool' };
|
|
9388
9619
|
}
|
|
9389
|
-
const ok = app.fitToFrame(${
|
|
9620
|
+
const ok = app.fitToFrame(${ID}, ${JSON.stringify(mode)});
|
|
9390
9621
|
return ok
|
|
9391
|
-
? { success: true, action: 'fit', itemId:
|
|
9392
|
-
: { success: false, error: 'Item not found: ${
|
|
9622
|
+
? { success: true, action: 'fit', itemId: ${ID}, mode: ${JSON.stringify(mode)} }
|
|
9623
|
+
: { success: false, error: 'Item not found: ' + ${ID} };
|
|
9393
9624
|
})();`.trim();
|
|
9394
9625
|
}
|
|
9395
9626
|
case 'nudge': {
|
|
9396
|
-
const dx = input.dx ?? 0;
|
|
9397
|
-
const dy = input.dy ?? 0;
|
|
9627
|
+
const dx = Number(input.dx ?? 0);
|
|
9628
|
+
const dy = Number(input.dy ?? 0);
|
|
9629
|
+
// THROUGH modifyItem (issue #29 O8). Writing item.position directly
|
|
9630
|
+
// skipped the engine's bookkeeping: an item with a loop animation kept
|
|
9631
|
+
// its old home (data.originalPosition), and the change check, which
|
|
9632
|
+
// reads authored state, reported the move as no change.
|
|
9398
9633
|
return `
|
|
9399
9634
|
// Nudge item position
|
|
9400
9635
|
(function() {
|
|
9401
|
-
const entry = app.itemRegistry.get(${
|
|
9402
|
-
if (!entry || !entry.item) return { error: 'Item not found: ${
|
|
9403
|
-
entry.item.position.x
|
|
9404
|
-
entry.item.position.y
|
|
9405
|
-
|
|
9636
|
+
const entry = app.itemRegistry.get(${ID});
|
|
9637
|
+
if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
|
|
9638
|
+
const x = entry.item.position.x + ${dx};
|
|
9639
|
+
const y = entry.item.position.y + ${dy};
|
|
9640
|
+
if (typeof app.modifyItem === 'function') app.modifyItem(${ID}, { x: x, y: y });
|
|
9641
|
+
else { entry.item.position.x = x; entry.item.position.y = y; }
|
|
9642
|
+
return { success: true, action: 'nudge', itemId: ${ID}, dx: ${dx}, dy: ${dy}, position: { x: entry.item.position.x, y: entry.item.position.y } };
|
|
9406
9643
|
})();`.trim();
|
|
9407
9644
|
}
|
|
9408
9645
|
case 'flip': {
|
|
@@ -9410,10 +9647,10 @@ ${resolveSource}
|
|
|
9410
9647
|
return `
|
|
9411
9648
|
// Flip item ${dir}ly
|
|
9412
9649
|
(function() {
|
|
9413
|
-
const entry = app.itemRegistry.get(${
|
|
9414
|
-
if (!entry || !entry.item) return { error: 'Item not found: ${
|
|
9650
|
+
const entry = app.itemRegistry.get(${ID});
|
|
9651
|
+
if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
|
|
9415
9652
|
${dir === 'horizontal' ? 'entry.item.scale(-1, 1);' : 'entry.item.scale(1, -1);'}
|
|
9416
|
-
return { success: true, action: 'flip', itemId:
|
|
9653
|
+
return { success: true, action: 'flip', itemId: ${ID}, direction: ${JSON.stringify(dir)} };
|
|
9417
9654
|
})();`.trim();
|
|
9418
9655
|
}
|
|
9419
9656
|
case 'reorder': {
|
|
@@ -9431,20 +9668,20 @@ ${resolveSource}
|
|
|
9431
9668
|
return `
|
|
9432
9669
|
// Reorder item: ${order}
|
|
9433
9670
|
(function() {
|
|
9434
|
-
const entry = app.itemRegistry.get(${
|
|
9435
|
-
if (!entry || !entry.item) return { error: 'Item not found: ${
|
|
9671
|
+
const entry = app.itemRegistry.get(${ID});
|
|
9672
|
+
if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
|
|
9436
9673
|
const sibling = ${siblingExpr};
|
|
9437
9674
|
if (sibling) entry.item.${method}(sibling);
|
|
9438
|
-
return { success: true, action: 'reorder', itemId:
|
|
9675
|
+
return { success: true, action: 'reorder', itemId: ${ID}, order: ${JSON.stringify(order)} };
|
|
9439
9676
|
})();`.trim();
|
|
9440
9677
|
}
|
|
9441
9678
|
return `
|
|
9442
9679
|
// Reorder item: ${order}
|
|
9443
9680
|
(function() {
|
|
9444
|
-
const entry = app.itemRegistry.get(${
|
|
9445
|
-
if (!entry || !entry.item) return { error: 'Item not found: ${
|
|
9681
|
+
const entry = app.itemRegistry.get(${ID});
|
|
9682
|
+
if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
|
|
9446
9683
|
entry.item.${method}();
|
|
9447
|
-
return { success: true, action: 'reorder', itemId:
|
|
9684
|
+
return { success: true, action: 'reorder', itemId: ${ID}, order: ${JSON.stringify(order)} };
|
|
9448
9685
|
})();`.trim();
|
|
9449
9686
|
}
|
|
9450
9687
|
default:
|
|
@@ -10231,7 +10468,7 @@ ${resolve}
|
|
|
10231
10468
|
// HAND-DRAWN SKETCHY ANIMATION & JITTER
|
|
10232
10469
|
// ===========================================================================
|
|
10233
10470
|
generateHandDrawn(input) {
|
|
10234
|
-
const S =
|
|
10471
|
+
const S = jsLiteral;
|
|
10235
10472
|
// style carries no schema default: with one, `mode` (its alias) was never read.
|
|
10236
10473
|
const requested = input.style ?? input.mode ?? 'sketchy';
|
|
10237
10474
|
const style = requested === 'ink_reveal' ? 'reveal' : requested;
|
|
@@ -10620,7 +10857,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10620
10857
|
const __esc = function(v) { return String(v).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<'); };
|
|
10621
10858
|
let __html = String(result.html || '');
|
|
10622
10859
|
const __applied = [];
|
|
10623
|
-
${input.lang || input.dir ? `__html = __html.replace(/<html\\b[^>]*>/i, function() { __applied.push('html'); return '<html lang="' + __esc(${
|
|
10860
|
+
${input.lang || input.dir ? `__html = __html.replace(/<html\\b[^>]*>/i, function() { __applied.push('html'); return '<html lang="' + __esc(${jsLiteral(input.lang ?? 'en')}) + '"' + ${jsLiteral(input.dir ? ` dir="${input.dir}"` : '')} + '>'; });` : ''}
|
|
10624
10861
|
${input.alt ? `// The engine's widget draws its canvas from script, so there is no <canvas>
|
|
10625
10862
|
// tag to label (prod retest): its accessible name is div#w's aria-label,
|
|
10626
10863
|
// mirrored in the screen-reader text #w-desc. Both carry the alt; a literal
|
|
@@ -10649,7 +10886,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10649
10886
|
return `(function() { return { error: 'data is required for create' }; })();`;
|
|
10650
10887
|
const fields = resolveChartFields(input.chartType, input.data, input.options || {});
|
|
10651
10888
|
if ('error' in fields)
|
|
10652
|
-
return `(function() { return { success: false, error: ${
|
|
10889
|
+
return `(function() { return { success: false, error: ${jsLiteral(fields.error)} }; })();`;
|
|
10653
10890
|
const dataStr = JSON.stringify(input.data);
|
|
10654
10891
|
const optsStr = JSON.stringify(fields.options);
|
|
10655
10892
|
return `
|
|
@@ -11135,8 +11372,8 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
11135
11372
|
if (app.historyManager) app.historyManager.saveState();
|
|
11136
11373
|
const groupId = (group.data && (group.data.registryId || group.data.groupId)) || null;
|
|
11137
11374
|
// itemId too (round 8 BB, 1.70): every other create answers itemId, and a
|
|
11138
|
-
// group is an item every other tool addresses by that id. In a batch
|
|
11139
|
-
//
|
|
11375
|
+
// group is an item every other tool addresses by that id. In a batch it
|
|
11376
|
+
// claims the next $N slot, as a create does (O2).
|
|
11140
11377
|
return { success: true, groupId: groupId, itemId: groupId, groupName: group.data && group.data.groupName, itemCount: items.length };`;
|
|
11141
11378
|
}
|
|
11142
11379
|
/**
|
|
@@ -11443,6 +11680,14 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11443
11680
|
canvas: input.canvas,
|
|
11444
11681
|
defaultGeometry: input.defaultGeometry,
|
|
11445
11682
|
}).compile(input.doc);
|
|
11683
|
+
// A fire_if the doc declares is checked before anything is built: refused, the whole document is, with every reason.
|
|
11684
|
+
const specProblems = ops
|
|
11685
|
+
.filter((o) => o.op === 'addRelation')
|
|
11686
|
+
.map((o) => { const r = o; const why = relationSpecProblem(r.relation, r.params); return why ? `${r.from} -> ${r.to} (${r.relation}): ${why}` : null; })
|
|
11687
|
+
.filter((x) => !!x);
|
|
11688
|
+
if (specProblems.length) {
|
|
11689
|
+
return `(function() { return { success: false, error: ${jsLiteral('nothing was built: ' + specProblems.join(' | '))} }; })();`;
|
|
11690
|
+
}
|
|
11446
11691
|
const opsJson = JSON.stringify(ops, null, 2);
|
|
11447
11692
|
const diagJson = JSON.stringify(diagnostics);
|
|
11448
11693
|
const docJson = JSON.stringify(input.doc);
|
|
@@ -12466,7 +12711,7 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
|
|
|
12466
12711
|
}
|
|
12467
12712
|
/** pinepaper_emitter (D29): create('emitter', spec) / setEmitter(id, patch). */
|
|
12468
12713
|
generateEmitter(raw) {
|
|
12469
|
-
const S =
|
|
12714
|
+
const S = jsLiteral;
|
|
12470
12715
|
const normalized = normalizeEmitterSpec(raw.spec, raw.action !== 'set');
|
|
12471
12716
|
const input = { ...raw, spec: normalized.spec };
|
|
12472
12717
|
// A COUNT OVER THE CAP IS SAID (B144). The cap is 10000 a burst on the
|