@pinepaper.studio/mcp-server 1.6.23 → 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.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +212 -17
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SECURITY.md +46 -0
- package/dist/agent/types.d.ts +114 -106
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/browser/puppeteer-controller.d.ts +6 -0
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +99 -71
- 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 +63 -12
- 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 +8 -2
- 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/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +1 -0
- package/dist/ontology/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 +27 -0
- package/dist/tools/batch-macros.d.ts.map +1 -0
- package/dist/tools/batch-macros.js +158 -0
- package/dist/tools/batch-macros.js.map +1 -0
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +2 -0
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +8 -7
- 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 +122 -19
- 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 +5 -2
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +10 -2
- package/dist/tools/engine-lists.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +2 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +67 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/export-file-check.d.ts +31 -0
- package/dist/tools/export-file-check.d.ts.map +1 -0
- package/dist/tools/export-file-check.js +103 -0
- package/dist/tools/export-file-check.js.map +1 -0
- package/dist/tools/export-file-receipt.d.ts +68 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -0
- package/dist/tools/export-file-receipt.js +171 -0
- package/dist/tools/export-file-receipt.js.map +1 -0
- package/dist/tools/handlers/beats.d.ts +8 -45
- 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 +34 -8
- 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 +42 -271
- 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 +82 -4
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +366 -89
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +39 -0
- package/dist/tools/lane.d.ts.map +1 -0
- package/dist/tools/lane.js +247 -0
- package/dist/tools/lane.js.map +1 -0
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +3 -2
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/nested-ignored.d.ts +16 -0
- package/dist/tools/nested-ignored.d.ts.map +1 -0
- package/dist/tools/nested-ignored.js +83 -0
- package/dist/tools/nested-ignored.js.map +1 -0
- package/dist/tools/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 +15 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +4 -4
- package/dist/tools/stick-vocabulary.d.ts.map +1 -1
- package/dist/tools/stick-vocabulary.js +8 -4
- package/dist/tools/stick-vocabulary.js.map +1 -1
- package/dist/tools/timeline.d.ts +136 -0
- package/dist/tools/timeline.d.ts.map +1 -0
- package/dist/tools/timeline.js +1041 -0
- package/dist/tools/timeline.js.map +1 -0
- package/dist/tools/toolkits.d.ts +22 -7
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +113 -21
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +55 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +671 -202
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +5281 -25048
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +81 -41
- 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 +46 -63
- package/package.json +15 -12
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
* This code is designed to run in the browser context where window.PinePaper is available.
|
|
6
6
|
*/
|
|
7
7
|
import { compileChoreography } from '../tools/choreograph.js';
|
|
8
|
+
import { compileBatchTimelines, emitTimelineOpBody } from '../tools/timeline.js';
|
|
8
9
|
import { normalizeSvgSize } from '../utils/svg-size.js';
|
|
9
|
-
import { WORLD3D_COLOR_PATHS } from '../tools/engine-lists.js';
|
|
10
|
+
import { WORLD3D_COLOR_PATHS, PATTERN_KINDS } from '../tools/engine-lists.js';
|
|
10
11
|
import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
|
|
11
|
-
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';
|
|
12
15
|
import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
|
|
16
|
+
import { expandBatchMacros } from '../tools/batch-macros.js';
|
|
13
17
|
import { CreateItemInputSchema, ScreenSpaceSchema, StepTimingSchema, CounterSchema, ImportMotionCaptureInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
|
|
14
18
|
// Diagram types
|
|
15
19
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
@@ -26,7 +30,7 @@ GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTou
|
|
|
26
30
|
// Custom relation/code types
|
|
27
31
|
RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
|
|
28
32
|
// Mask types
|
|
29
|
-
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, MaskTypeSchema, MaskPresetSchema, } from './schemas.js';
|
|
33
|
+
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, PatternInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, MaskTypeSchema, MaskPresetSchema, } from './schemas.js';
|
|
30
34
|
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
31
35
|
// =============================================================================
|
|
32
36
|
// HELPER FUNCTIONS
|
|
@@ -144,7 +148,16 @@ function generateBlendModeCode(itemVarName, blendMode) {
|
|
|
144
148
|
/**
|
|
145
149
|
* Template for creating items with support for gradients, shadows, and blend modes
|
|
146
150
|
*/
|
|
151
|
+
const UNSAFE_ID_CHARS = /['"`\\\n\r\u2028\u2029]/;
|
|
147
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
|
+
}
|
|
148
161
|
// Colour objects are gradient specs: normalised, or refused by name.
|
|
149
162
|
for (const key of ['color', 'fillColor', 'strokeColor']) {
|
|
150
163
|
const v = properties[key];
|
|
@@ -211,6 +224,10 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
211
224
|
// All three now mean no fill, applied after the item exists.
|
|
212
225
|
const noFill = (v) => v === null || (typeof v === 'string' && /^(transparent|none)$/i.test(v.trim()));
|
|
213
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;
|
|
214
231
|
// `color` ON A CLOSED PATH IS ITS FILL.
|
|
215
232
|
//
|
|
216
233
|
// The engine's path branch paints `color` as the STROKE whenever no fillColor
|
|
@@ -309,7 +326,8 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
309
326
|
const textOnly = (k) => TEXT_STYLE_KEYS.includes(k) || k === 'fit' || k === 'textDirection' || k === 'dir'
|
|
310
327
|
|| TABULAR_KEYS.includes(k) || k === 'strokePosition';
|
|
311
328
|
const ignored = Object.keys(properties).filter((k) => itemType === 'text'
|
|
312
|
-
? !CREATE_KNOWN_KEYS.has(k) && !textOnly(k) && !DIRECTION_KEYS.includes(k)
|
|
329
|
+
? (!CREATE_KNOWN_KEYS.has(k) && !textOnly(k) && !DIRECTION_KEYS.includes(k))
|
|
330
|
+
|| (k === 'letterSpacing' && letterSpacingCss(properties[k]) === undefined)
|
|
313
331
|
: !CREATE_KNOWN_KEYS.has(k) || textOnly(k));
|
|
314
332
|
// Build the code
|
|
315
333
|
let code = `
|
|
@@ -334,7 +352,7 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
|
|
|
334
352
|
if (blendMode) {
|
|
335
353
|
code += '\n' + generateBlendModeCode('item', blendMode);
|
|
336
354
|
}
|
|
337
|
-
if (clearFill)
|
|
355
|
+
if (clearFill || outlineOnly)
|
|
338
356
|
code += `\nitem.fillColor = null;`;
|
|
339
357
|
// DASHES, CAPS AND JOINS FOR EVERY SHAPE (round 7 Z, 1.54). create() applies
|
|
340
358
|
// them only in its path / line / pattern branches; the shared tail sets a
|
|
@@ -398,15 +416,27 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
|
|
|
398
416
|
code += `\nif (app.setDynamicContent) app.setDynamicContent(item, '${contentType}', ${JSON.stringify(opts)});`;
|
|
399
417
|
}
|
|
400
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
|
+
}
|
|
401
433
|
code += `
|
|
402
434
|
const itemId = item.data.registryId;
|
|
403
435
|
app.historyManager.saveState();
|
|
404
436
|
|
|
405
437
|
// Return item info
|
|
406
|
-
({ itemId, type: '${itemType}'${textFit ? ', textFit: __textFit' : ''}${textDirection ? ', direction: __direction' : ''}${textTabular ? ', tabularFigures: __tabular' : ''}${createFillOpacity ? ', fillOpacity: __fillOpacity' : ''}${textStrokePos ? ', strokePosition: __strokePos' : ''}, position: ${
|
|
407
|
-
? `, ignoredProperties: ${JSON.stringify(ignored)}
|
|
408
|
-
+ 'Check the spelling against this item type\'s documented properties.')}`
|
|
409
|
-
: ''} });`;
|
|
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(' '))}` : ''} });`;
|
|
410
440
|
// Loading a font is async, and it has to happen BEFORE create() measures the
|
|
411
441
|
// text — so only this case becomes an async IIFE; every other create stays a
|
|
412
442
|
// plain snippet ending in its (-led result.
|
|
@@ -434,24 +464,6 @@ const MODIFY_DID_YOU_MEAN = {
|
|
|
434
464
|
scaling: 'scale', size: 'width / height', rotate: 'rotation', angle: 'rotation', shear: 'skewX / skewY', skew: 'skewX / skewY',
|
|
435
465
|
alpha: 'opacity', text: 'content', stroke_width: 'strokeWidth', lineWidth: 'strokeWidth',
|
|
436
466
|
};
|
|
437
|
-
/**
|
|
438
|
-
* Every `properties` key create_item can act on: what the engine's create()
|
|
439
|
-
* and normalizeParams() read (generated — see shape-params.ts), plus the keys
|
|
440
|
-
* generateCreateItemCode consumes itself before the engine sees them.
|
|
441
|
-
*/
|
|
442
|
-
const CREATE_KNOWN_KEYS = new Set([
|
|
443
|
-
...ACCEPTED_CREATE_PARAMS,
|
|
444
|
-
...NORMALIZE_PARAM_READS,
|
|
445
|
-
// withRadiusAxes, gradients / shadow / blend / opacity emitted here, and
|
|
446
|
-
// setDynamicContent for text.
|
|
447
|
-
'radiusX', 'radiusY', 'contentType', 'contentFormat', 'countdownTarget', 'countdownEndText',
|
|
448
|
-
// Lifetimes, on ANY item: this generator applies them through app.setLifetime
|
|
449
|
-
// (emitLifetime), whatever create() itself reads.
|
|
450
|
-
'bornAt', 'ttl',
|
|
451
|
-
// A solid fill's own alpha (FxTool 12 STROKEFILL); confirmed per call by
|
|
452
|
-
// emitFillOpacityCheck, since an older studio ignores it.
|
|
453
|
-
'fillOpacity',
|
|
454
|
-
]);
|
|
455
467
|
/**
|
|
456
468
|
* Load a font family before text is drawn with it, and say if it still is not there.
|
|
457
469
|
*
|
|
@@ -472,11 +484,11 @@ export function emitEnsureFont(family) {
|
|
|
472
484
|
return `
|
|
473
485
|
let __font = null;
|
|
474
486
|
if (typeof app.checkFont === 'function') {
|
|
475
|
-
const __v0 = app.checkFont(${
|
|
487
|
+
const __v0 = app.checkFont(${jsLiteral(family)});
|
|
476
488
|
if (__v0 && __v0.available === false) {
|
|
477
489
|
if (typeof app.ensureFontsLoaded === 'function') { try { await app.ensureFontsLoaded('all'); } catch (_) { /* reported below */ } }
|
|
478
|
-
try { if (typeof document !== 'undefined' && document.fonts) await document.fonts.load('16px ' + ${
|
|
479
|
-
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)});
|
|
480
492
|
let __via = 'studio';
|
|
481
493
|
// Not in the studio's own sheets (Monoton, a common neon face, is not):
|
|
482
494
|
// the page's CSP allows fonts.googleapis.com stylesheets and
|
|
@@ -484,21 +496,21 @@ if (typeof app.checkFont === 'function') {
|
|
|
484
496
|
if (!(__v1 && __v1.available === true) && typeof document !== 'undefined' && document.head) {
|
|
485
497
|
__via = 'google-fonts';
|
|
486
498
|
await new Promise(function(res) {
|
|
487
|
-
const id = 'pp-gf-' + ${
|
|
499
|
+
const id = 'pp-gf-' + ${jsLiteral(primary)}.replace(/[^a-z0-9]+/gi, '-');
|
|
488
500
|
if (document.getElementById(id)) return res();
|
|
489
501
|
const l = document.createElement('link');
|
|
490
502
|
l.id = id; l.rel = 'stylesheet';
|
|
491
|
-
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';
|
|
492
504
|
l.onload = res; l.onerror = res; setTimeout(res, 5000);
|
|
493
505
|
document.head.appendChild(l);
|
|
494
506
|
});
|
|
495
|
-
try { if (document.fonts) await document.fonts.load('16px ' + ${
|
|
496
|
-
__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)});
|
|
497
509
|
}
|
|
498
510
|
__font = (__v1 && __v1.available === true)
|
|
499
|
-
? { family: ${
|
|
500
|
-
: { family: ${
|
|
501
|
-
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. '
|
|
502
514
|
+ ((__v1 && __v1.reason) || '') + ' font list_available names the families this studio can load.' };
|
|
503
515
|
}
|
|
504
516
|
}`;
|
|
@@ -569,13 +581,13 @@ function takeDirection(props) {
|
|
|
569
581
|
function emitDirectionCheck(itemExpr, req) {
|
|
570
582
|
if ('error' in req)
|
|
571
583
|
return `
|
|
572
|
-
const __direction = ${
|
|
573
|
-
const v =
|
|
584
|
+
const __direction = ${jsLiteral({ applied: false, error: req.error })};`;
|
|
585
|
+
const v = jsLiteral(req.value);
|
|
574
586
|
return `
|
|
575
587
|
const __direction = (function(it) {
|
|
576
588
|
if (!it || it.className !== 'PointText') return { applied: false, note: 'direction applies to text items only.' };
|
|
577
589
|
return it.data && it.data.direction === ${v}
|
|
578
|
-
? { applied: true, value: ${v}${req.resolved ? `, resolved: ${
|
|
590
|
+
? { applied: true, value: ${v}${req.resolved ? `, resolved: ${jsLiteral(req.resolved)}` : ''} }
|
|
579
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.' };
|
|
580
592
|
})(${itemExpr});`;
|
|
581
593
|
}
|
|
@@ -675,7 +687,7 @@ function takeStrokePosition(props, allowNull) {
|
|
|
675
687
|
}
|
|
676
688
|
function emitStrokePositionCheck(itemExpr, req) {
|
|
677
689
|
if ('error' in req)
|
|
678
|
-
return `\nconst __strokePos = ${
|
|
690
|
+
return `\nconst __strokePos = ${jsLiteral({ applied: false, error: req.error })};`;
|
|
679
691
|
if (req.value === 'center')
|
|
680
692
|
return `\nconst __strokePos = { applied: true, value: 'center' };`;
|
|
681
693
|
return `
|
|
@@ -693,7 +705,7 @@ function anchorHold(anchor) {
|
|
|
693
705
|
function emitTextFit(itemExpr, box, holdFallback = 'undefined') {
|
|
694
706
|
return `
|
|
695
707
|
if (typeof app.fitText === 'function') {
|
|
696
|
-
const __fbox = ${
|
|
708
|
+
const __fbox = ${jsLiteral(box)};
|
|
697
709
|
if (__fbox.hold === undefined) { const __h = ${holdFallback}; if (__h) __fbox.hold = __h; }
|
|
698
710
|
const __fr = app.fitText(${itemExpr}, __fbox);
|
|
699
711
|
__textFit = __fr && __fr.ok !== false
|
|
@@ -734,13 +746,26 @@ function authoringFrameJs(name) {
|
|
|
734
746
|
return (typeof app.exportFrameRect === 'function' && app.exportFrameRect()) || cs || { width: 1920, height: 1080 };
|
|
735
747
|
})();`;
|
|
736
748
|
}
|
|
737
|
-
const TEXT_STYLE_KEYS = ['fontWeight', 'fontStyle', 'leading', 'lineHeight', 'textAlign', 'align', 'text'];
|
|
749
|
+
const TEXT_STYLE_KEYS = ['fontWeight', 'fontStyle', 'leading', 'lineHeight', 'letterSpacing', 'textAlign', 'align', 'text'];
|
|
750
|
+
/**
|
|
751
|
+
* letterSpacing as the studio draws it, or undefined if it cannot be. create()
|
|
752
|
+
* and modifyItem() do not read it (B108); the studio draws the item's own
|
|
753
|
+
* letterSpacing, a CSS length, as its keyframes set it, and a number is px.
|
|
754
|
+
*/
|
|
755
|
+
function letterSpacingCss(v) {
|
|
756
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
757
|
+
return `${v}px`;
|
|
758
|
+
if (typeof v === 'string' && /^-?\d*\.?\d+(px|em)?$/.test(v.trim()))
|
|
759
|
+
return /\d$/.test(v.trim()) ? `${v.trim()}px` : v.trim();
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
738
762
|
function emitTextStyle(itemExpr, props) {
|
|
739
763
|
const weight = props.fontWeight;
|
|
740
764
|
const italic = props.fontStyle === 'italic' || props.fontStyle === 'oblique';
|
|
741
765
|
const lh = typeof props.lineHeight === 'number' ? props.lineHeight : undefined;
|
|
742
766
|
const leading = typeof props.leading === 'number' ? props.leading : undefined;
|
|
743
|
-
|
|
767
|
+
const ls = letterSpacingCss(props.letterSpacing);
|
|
768
|
+
if (weight === undefined && props.fontStyle === undefined && leading === undefined && lh === undefined && ls === undefined)
|
|
744
769
|
return '';
|
|
745
770
|
return `
|
|
746
771
|
(function(it) {
|
|
@@ -749,10 +774,11 @@ function emitTextStyle(itemExpr, props) {
|
|
|
749
774
|
const edge = function(b) { return j === 'left' ? b.left : j === 'right' ? b.right : b.center.x; };
|
|
750
775
|
const b0 = it.bounds, ax = edge(b0), ay = b0.center.y;
|
|
751
776
|
${weight !== undefined || props.fontStyle !== undefined
|
|
752
|
-
? `it.fontWeight = ${
|
|
777
|
+
? `it.fontWeight = ${jsLiteral(`${italic ? 'italic ' : ''}${weight ?? 'normal'}`)};`
|
|
753
778
|
: ''}
|
|
754
779
|
${leading !== undefined ? `it.leading = ${leading};` : ''}
|
|
755
780
|
${lh !== undefined ? `it.leading = ${lh} <= 4 ? ${lh} * it.fontSize : ${lh};` : ''}
|
|
781
|
+
${ls !== undefined ? `it.letterSpacing = ${jsLiteral(ls)};` : ''}
|
|
756
782
|
const b1 = it.bounds;
|
|
757
783
|
it.position = it.position.add([ax - edge(b1), ay - b1.center.y]);
|
|
758
784
|
})(${itemExpr});`;
|
|
@@ -901,6 +927,9 @@ function emitLifetime(itemExpr, bornAt, ttl) {
|
|
|
901
927
|
* Template for modifying items
|
|
902
928
|
*/
|
|
903
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);
|
|
904
933
|
const { contentType, contentFormat, countdownTarget, countdownEndText, ...restProperties } = properties;
|
|
905
934
|
// Aliases for text items: text -> content (B66), textAlign / align -> justification (B89).
|
|
906
935
|
// A key consumed here was applied, so the unread-key report below must not
|
|
@@ -944,23 +973,23 @@ function generateModifyItemCode(itemId, properties, dataFlags) {
|
|
|
944
973
|
// The warning the engine logs on that path is console.warn, which production
|
|
945
974
|
// strips, so the failure was invisible twice over.
|
|
946
975
|
let code = `
|
|
947
|
-
// Modify item ${
|
|
976
|
+
// Modify item ${ID}
|
|
948
977
|
let _modified = true;`;
|
|
949
978
|
if (Object.keys(restProperties).length > 0) {
|
|
950
979
|
code += `
|
|
951
|
-
_modified = app.modifyItem(
|
|
952
|
-
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' }; }`;
|
|
953
982
|
}
|
|
954
983
|
else {
|
|
955
984
|
code += `
|
|
956
|
-
if (!app.getItemById(
|
|
985
|
+
if (!app.getItemById(${ID})) { return { success: false, error: 'no item ' + ${ID} }; }`;
|
|
957
986
|
}
|
|
958
987
|
// Handle dynamic content type changes
|
|
959
988
|
if (contentType !== undefined) {
|
|
960
989
|
if (contentType === null || contentType === 'none' || contentType === '') {
|
|
961
990
|
// Remove dynamic content
|
|
962
991
|
code += `
|
|
963
|
-
const entry = app.itemRegistry.get(
|
|
992
|
+
const entry = app.itemRegistry.get(${ID});
|
|
964
993
|
if (entry && entry.item && app.removeDynamicContent) app.removeDynamicContent(entry.item);`;
|
|
965
994
|
}
|
|
966
995
|
else {
|
|
@@ -973,8 +1002,8 @@ if (entry && entry.item && app.removeDynamicContent) app.removeDynamicContent(en
|
|
|
973
1002
|
if (countdownEndText)
|
|
974
1003
|
opts.countdownEndText = countdownEndText;
|
|
975
1004
|
code += `
|
|
976
|
-
const entry = app.itemRegistry.get(
|
|
977
|
-
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)});`;
|
|
978
1007
|
}
|
|
979
1008
|
}
|
|
980
1009
|
// AN AUDIO ITEM'S LEVEL IS NOT AN ITEM PROPERTY. modifyItem applies changes
|
|
@@ -986,13 +1015,13 @@ if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.it
|
|
|
986
1015
|
if (levelKey) {
|
|
987
1016
|
const level = Math.max(0, Math.min(1, properties[levelKey]));
|
|
988
1017
|
code += `
|
|
989
|
-
const _ae = app.itemRegistry.get(
|
|
1018
|
+
const _ae = app.itemRegistry.get(${ID});
|
|
990
1019
|
if (_ae && _ae.type === 'audio') {
|
|
991
1020
|
_ae.properties = _ae.properties || {};
|
|
992
1021
|
_ae.properties.audioGain = ${level};
|
|
993
1022
|
_ae.properties.gain = ${level};
|
|
994
1023
|
const _A = (typeof window !== 'undefined') && window.PinePaperAgent;
|
|
995
|
-
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;
|
|
996
1025
|
if (_m && app.audioLayer && typeof app.audioLayer.setVolume === 'function') app.audioLayer.setVolume(${level}, _m.id);
|
|
997
1026
|
}`;
|
|
998
1027
|
}
|
|
@@ -1000,17 +1029,17 @@ if (_ae && _ae.type === 'audio') {
|
|
|
1000
1029
|
// does not pass on; applied to a raster, or to the raster an image group holds.
|
|
1001
1030
|
if (typeof properties.smoothing === 'string') {
|
|
1002
1031
|
code += `
|
|
1003
|
-
const _sm = app.itemRegistry.get(
|
|
1032
|
+
const _sm = app.itemRegistry.get(${ID});
|
|
1004
1033
|
const _smR = _sm && _sm.item && (_sm.item.className === 'Raster' ? _sm.item
|
|
1005
1034
|
: (typeof _sm.item.getItem === 'function' ? _sm.item.getItem({ className: 'Raster' }) : null));
|
|
1006
|
-
if (_smR) _smR.smoothing = ${
|
|
1035
|
+
if (_smR) _smR.smoothing = ${jsLiteral(properties.smoothing)};`;
|
|
1007
1036
|
}
|
|
1008
1037
|
// No fill on modify too: null / 'transparent' / 'none' clear it outright,
|
|
1009
1038
|
// after modifyItem, which would otherwise read 'transparent' as black.
|
|
1010
1039
|
const fillNone = (v) => v === null || (typeof v === 'string' && /^(transparent|none)$/i.test(v.trim()));
|
|
1011
1040
|
if (fillNone(properties.fillColor) || (properties.fillColor === undefined && properties.color !== undefined && fillNone(properties.color))) {
|
|
1012
1041
|
code += `
|
|
1013
|
-
const _nf = app.itemRegistry.get(
|
|
1042
|
+
const _nf = app.itemRegistry.get(${ID});
|
|
1014
1043
|
if (_nf && _nf.item) _nf.item.fillColor = null;`;
|
|
1015
1044
|
}
|
|
1016
1045
|
// SKEW AND AFFINE, FOR MOCKUPS (round 9 HH). A box face or a phone screen in
|
|
@@ -1026,7 +1055,7 @@ if (_nf && _nf.item) _nf.item.fillColor = null;`;
|
|
|
1026
1055
|
const affine = skewX !== 0 || skewY !== 0 || matrix !== null;
|
|
1027
1056
|
if (affine) {
|
|
1028
1057
|
code += `
|
|
1029
|
-
const _af = app.itemRegistry.get(
|
|
1058
|
+
const _af = app.itemRegistry.get(${ID});
|
|
1030
1059
|
const _afi = _af && _af.item;
|
|
1031
1060
|
if (_afi) {
|
|
1032
1061
|
${skewX !== 0 || skewY !== 0 ? `_afi.shear(Math.tan(${skewX} * Math.PI / 180), Math.tan(${skewY} * Math.PI / 180), _afi.bounds.center);` : ''}
|
|
@@ -1037,30 +1066,32 @@ if (_afi) {
|
|
|
1037
1066
|
if (modFit) {
|
|
1038
1067
|
code += `
|
|
1039
1068
|
let __textFit = null;
|
|
1040
|
-
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)")}`;
|
|
1041
1070
|
}
|
|
1042
1071
|
const textStyle = emitTextStyle('_ts && _ts.item', properties);
|
|
1043
1072
|
if (textStyle) {
|
|
1044
1073
|
code += `
|
|
1045
|
-
const _ts = app.itemRegistry.get(
|
|
1074
|
+
const _ts = app.itemRegistry.get(${ID});${textStyle}`;
|
|
1046
1075
|
}
|
|
1047
1076
|
const modLifetime = properties.bornAt !== undefined || properties.ttl !== undefined;
|
|
1048
1077
|
if (modLifetime) {
|
|
1049
1078
|
code += `
|
|
1050
1079
|
let __lifetime = null;
|
|
1051
|
-
const _lt = app.itemRegistry.get(
|
|
1080
|
+
const _lt = app.itemRegistry.get(${ID});${emitLifetime('_lt && _lt.item', properties.bornAt, properties.ttl)}`;
|
|
1052
1081
|
}
|
|
1053
1082
|
if (dataFlags && Object.keys(dataFlags).length > 0) {
|
|
1054
1083
|
code += `
|
|
1055
|
-
const _flagged = app.itemRegistry.get(
|
|
1056
|
-
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)});`;
|
|
1057
1086
|
}
|
|
1058
1087
|
const modFont = typeof properties.fontFamily === 'string' && properties.fontFamily.trim() !== '';
|
|
1059
1088
|
// AN UNREAD KEY HAD NO EFFECT, AND THE CALL SAID SUCCESS. {scaling: 8} on a
|
|
1060
1089
|
// raster answered success and left it 16 px; the engine reads `scale`. Same
|
|
1061
1090
|
// report create_item gives, against the engine's modify path (generated).
|
|
1091
|
+
if (textStyle && letterSpacingCss(properties.letterSpacing) !== undefined)
|
|
1092
|
+
aliased.add('letterSpacing');
|
|
1062
1093
|
const modIgnored = Object.keys(properties).filter((k) => !MODIFY_KNOWN_KEYS.has(k) && !aliased.has(k));
|
|
1063
|
-
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.`
|
|
1064
1095
|
+ modIgnored.filter((k) => MODIFY_DID_YOU_MEAN[k]).map((k) => ` Did you mean ${MODIFY_DID_YOU_MEAN[k]} (not ${k})?`).join(''))}`;
|
|
1065
1096
|
code += `
|
|
1066
1097
|
app.historyManager.saveState();
|
|
@@ -1068,11 +1099,11 @@ ${properties.content !== undefined ? `
|
|
|
1068
1099
|
// CONTENT UNDER A CONTENT TRACK (round 10, 10.13): the keyframes set the text
|
|
1069
1100
|
// on every frame, so a direct edit is overwritten at once — it answered
|
|
1070
1101
|
// success and changed nothing visible.
|
|
1071
|
-
const __ctItem = app.getItemById(
|
|
1102
|
+
const __ctItem = app.getItemById(${ID});
|
|
1072
1103
|
const __ctKeys = (__ctItem && __ctItem.data && Array.isArray(__ctItem.data.keyframes))
|
|
1073
1104
|
? __ctItem.data.keyframes.filter(function(k) { return k && k.properties && Object.prototype.hasOwnProperty.call(k.properties, 'content'); }).length : 0;` : ''}
|
|
1074
|
-
${modDirection ? emitDirectionCheck(`app.getItemById(
|
|
1075
|
-
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} };`;
|
|
1076
1107
|
// Wrapped, because the body now RETURNS early on a miss rather than falling
|
|
1077
1108
|
// through to an unconditional success. Async only when a font must load
|
|
1078
1109
|
// before modifyItem re-measures the text.
|
|
@@ -1085,8 +1116,9 @@ return { success: true, itemId: '${itemId}'${properties.content !== undefined ?
|
|
|
1085
1116
|
* Template for deleting items
|
|
1086
1117
|
*/
|
|
1087
1118
|
function generateDeleteItemCode(itemId) {
|
|
1119
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1088
1120
|
return `
|
|
1089
|
-
// Delete item ${
|
|
1121
|
+
// Delete item ${ID}
|
|
1090
1122
|
//
|
|
1091
1123
|
// Delegate to app.deleteItem rather than hand-rolling the removal. The old
|
|
1092
1124
|
// inline version called \`app.itemRegistry.remove(...)\`, which does not exist —
|
|
@@ -1103,11 +1135,11 @@ function generateDeleteItemCode(itemId) {
|
|
|
1103
1135
|
if (typeof app.deleteItem !== 'function') {
|
|
1104
1136
|
return { success: false, error: 'app.deleteItem unavailable — update FxTool' };
|
|
1105
1137
|
}
|
|
1106
|
-
const ok = app.deleteItem(
|
|
1138
|
+
const ok = app.deleteItem(${ID});
|
|
1107
1139
|
if (!ok) {
|
|
1108
|
-
return { success: false, error: 'Item not found: ${
|
|
1140
|
+
return { success: false, error: 'Item not found: ' + ${ID} };
|
|
1109
1141
|
}
|
|
1110
|
-
return { success: true, itemId:
|
|
1142
|
+
return { success: true, itemId: ${ID} };
|
|
1111
1143
|
})();
|
|
1112
1144
|
`.trim();
|
|
1113
1145
|
}
|
|
@@ -1123,6 +1155,92 @@ function generateDeleteItemCode(itemId) {
|
|
|
1123
1155
|
export function relationRefusalReasonJs(typeJs) {
|
|
1124
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; })()`;
|
|
1125
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
|
+
}
|
|
1126
1244
|
function generateAddRelationCode(sourceId, targetId, relationType, params) {
|
|
1127
1245
|
// AN UNKNOWN ENDPOINT IS REFUSED, NOT STORED (round 9 GG, 1.74). The engine
|
|
1128
1246
|
// stores a relation to an id it cannot resolve, returns true, and says "it
|
|
@@ -1143,7 +1261,7 @@ function generateAddRelationCode(sourceId, targetId, relationType, params) {
|
|
|
1143
1261
|
}
|
|
1144
1262
|
const success = app.addRelation('${sourceId}', '${targetId}', '${relationType}', ${JSON.stringify(params, null, 2)});
|
|
1145
1263
|
if (!success) {
|
|
1146
|
-
const __why = ${relationRefusalReasonJs(
|
|
1264
|
+
const __why = ${relationRefusalReasonJs(jsLiteral(relationType))};
|
|
1147
1265
|
return { success: false, relationType: '${relationType}', error: 'the studio refused the ${relationType} relation between ${sourceId} and ${targetId}' + (__why ? ': ' + __why : '.') };
|
|
1148
1266
|
}
|
|
1149
1267
|
app.historyManager.saveState();
|
|
@@ -1191,6 +1309,98 @@ export function emitterCountWarnings(spec) {
|
|
|
1191
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.`
|
|
1192
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.`);
|
|
1193
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
|
+
*/
|
|
1320
|
+
/**
|
|
1321
|
+
* An emitter spec as the studio reads it, and what the reply says about it.
|
|
1322
|
+
*
|
|
1323
|
+
* The studio reads velocities and offsets as [x, y] pairs only and drops any
|
|
1324
|
+
* other entry, so velocities: [{x, y}] made particles with no velocity and no
|
|
1325
|
+
* word (VELOC23). Point objects are turned into pairs here. And a burst draws
|
|
1326
|
+
* `count` particles, the i-th taking velocities[i % length]: with fewer
|
|
1327
|
+
* velocities than particles the extras repeat a velocity and draw on top of an
|
|
1328
|
+
* earlier particle, and with more the extras go unused, both in silence
|
|
1329
|
+
* (VELOCMISMATCH23). That is said, not changed: cycling is the studio's rule.
|
|
1330
|
+
* `assumeDefaultCount` is false for a set, whose spec is only the keys changed.
|
|
1331
|
+
*/
|
|
1332
|
+
export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
1333
|
+
const warnings = [];
|
|
1334
|
+
if (!spec || typeof spec !== 'object')
|
|
1335
|
+
return { spec, warnings };
|
|
1336
|
+
const pairs = (v) => (Array.isArray(v)
|
|
1337
|
+
? v.map((p) => (p && typeof p === 'object' && !Array.isArray(p) && ('x' in p || 'y' in p)
|
|
1338
|
+
? [Number(p.x ?? 0), Number(p.y ?? 0)]
|
|
1339
|
+
: p))
|
|
1340
|
+
: v);
|
|
1341
|
+
const fix = (o) => {
|
|
1342
|
+
const out = { ...o };
|
|
1343
|
+
for (const k of ['velocities', 'offsets'])
|
|
1344
|
+
if (out[k] !== undefined)
|
|
1345
|
+
out[k] = pairs(out[k]);
|
|
1346
|
+
return out;
|
|
1347
|
+
};
|
|
1348
|
+
const out = fix(spec);
|
|
1349
|
+
if (Array.isArray(spec.bursts))
|
|
1350
|
+
out.bursts = spec.bursts.map((b) => (b && typeof b === 'object' ? fix(b) : b));
|
|
1351
|
+
// The studio's default count per burst (ParticleEmitter DEFAULT_LOOK).
|
|
1352
|
+
const DEFAULT_COUNT = 12;
|
|
1353
|
+
const mismatch = (where, count, velocities) => {
|
|
1354
|
+
if (!Array.isArray(velocities) || !velocities.length)
|
|
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;
|
|
1359
|
+
const n = typeof count === 'number' && Number.isFinite(count) ? Math.round(count) : (assumeDefaultCount ? DEFAULT_COUNT : null);
|
|
1360
|
+
if (n === null || n === velocities.length)
|
|
1361
|
+
return;
|
|
1362
|
+
warnings.push(n > velocities.length
|
|
1363
|
+
? `${where}: count ${n} with ${velocities.length} velocities: particle i takes velocities[i % ${velocities.length}], so ${n - velocities.length} particles repeat an earlier particle's path and draw on top of it. Give one velocity per particle, or set count to ${velocities.length}.`
|
|
1364
|
+
: `${where}: count ${n} with ${velocities.length} velocities: only the first ${n} velocities are used. Set count to ${velocities.length} to draw them all.`);
|
|
1365
|
+
};
|
|
1366
|
+
// The spec's own pair once; a burst only where it sets either itself (it inherits the rest).
|
|
1367
|
+
const bursts = Array.isArray(out.bursts) ? out.bursts : [];
|
|
1368
|
+
const inherited = bursts.some((b) => !b || typeof b !== 'object' || (b.count === undefined && b.velocities === undefined));
|
|
1369
|
+
if (out.velocities !== undefined && (out.count !== undefined || inherited))
|
|
1370
|
+
mismatch('spec', out.count, out.velocities);
|
|
1371
|
+
bursts.forEach((b, i) => {
|
|
1372
|
+
const own = (b && typeof b === 'object' ? b : {});
|
|
1373
|
+
if (own.count !== undefined || own.velocities !== undefined)
|
|
1374
|
+
mismatch(`bursts[${i}]`, own.count ?? out.count, own.velocities ?? out.velocities);
|
|
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); });
|
|
1402
|
+
return { spec: out, warnings };
|
|
1403
|
+
}
|
|
1194
1404
|
/**
|
|
1195
1405
|
* moves_along_path, from the parameters a caller actually writes.
|
|
1196
1406
|
*
|
|
@@ -1341,15 +1551,16 @@ app.historyManager.saveState();
|
|
|
1341
1551
|
* Template for querying relations
|
|
1342
1552
|
*/
|
|
1343
1553
|
function generateQueryRelationsCode(itemId, relationType, direction = 'outgoing') {
|
|
1554
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1344
1555
|
if (direction === 'outgoing') {
|
|
1345
1556
|
const typeArg = relationType ? `'${relationType}'` : 'undefined';
|
|
1346
1557
|
return `
|
|
1347
|
-
// Query outgoing relations for ${
|
|
1348
|
-
const relations = app.getRelations(
|
|
1558
|
+
// Query outgoing relations for ${ID}
|
|
1559
|
+
const relations = app.getRelations(${ID}, ${typeArg});
|
|
1349
1560
|
|
|
1350
1561
|
// Format results
|
|
1351
1562
|
const formatted = relations.map(r => ({
|
|
1352
|
-
sourceId: r.sourceId ||
|
|
1563
|
+
sourceId: r.sourceId || ${ID},
|
|
1353
1564
|
// RelationRegistry.getAssociations returns { relation, target, params } —
|
|
1354
1565
|
// there is no \`targetId\` on it. Reading the wrong field made EVERY edge come
|
|
1355
1566
|
// back with an undefined destination, so the graph was unreadable through
|
|
@@ -1365,13 +1576,13 @@ const formatted = relations.map(r => ({
|
|
|
1365
1576
|
else {
|
|
1366
1577
|
const typeArg = relationType ? `'${relationType}'` : 'undefined';
|
|
1367
1578
|
return `
|
|
1368
|
-
// Query incoming relations for ${
|
|
1369
|
-
const results = app.queryByRelationTarget(
|
|
1579
|
+
// Query incoming relations for ${ID}
|
|
1580
|
+
const results = app.queryByRelationTarget(${ID}, ${typeArg});
|
|
1370
1581
|
|
|
1371
1582
|
// Format results
|
|
1372
1583
|
const relations = results.map(r => ({
|
|
1373
1584
|
sourceId: r.itemId,
|
|
1374
|
-
targetId:
|
|
1585
|
+
targetId: ${ID},
|
|
1375
1586
|
// queryByTarget returns the real relation name on each result. Hardcoding
|
|
1376
1587
|
// the filter string labelled every edge 'unknown' on an unfiltered query —
|
|
1377
1588
|
// which is exactly the query you make when exploring an unfamiliar graph.
|
|
@@ -1387,13 +1598,14 @@ const relations = results.map(r => ({
|
|
|
1387
1598
|
* Template for simple animation
|
|
1388
1599
|
*/
|
|
1389
1600
|
function generateAnimateCode(itemId, animationType, speed, intensity, delay) {
|
|
1601
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1390
1602
|
const extra = (intensity !== undefined ? `,\n animationIntensity: ${intensity}` : '') +
|
|
1391
1603
|
(delay !== undefined ? `,\n animationDelay: ${delay}` : '');
|
|
1392
1604
|
return `
|
|
1393
|
-
// Apply ${animationType} animation to ${
|
|
1394
|
-
const item = app.getItemById(
|
|
1605
|
+
// Apply ${animationType} animation to ${ID}
|
|
1606
|
+
const item = app.getItemById(${ID});
|
|
1395
1607
|
if (!item) {
|
|
1396
|
-
throw new Error('Item not found: ${
|
|
1608
|
+
throw new Error('Item not found: ' + ${ID});
|
|
1397
1609
|
}
|
|
1398
1610
|
app.animate(item, {
|
|
1399
1611
|
animationType: '${animationType}',
|
|
@@ -1409,13 +1621,13 @@ app.animate(item, {
|
|
|
1409
1621
|
if (r) {
|
|
1410
1622
|
return {
|
|
1411
1623
|
success: false,
|
|
1412
|
-
itemId:
|
|
1624
|
+
itemId: ${ID},
|
|
1413
1625
|
error: 'Unknown animationType "' + r.requested + '" — nothing will move.',
|
|
1414
1626
|
requested: r.requested,
|
|
1415
1627
|
known: r.known,
|
|
1416
1628
|
};
|
|
1417
1629
|
}
|
|
1418
|
-
return { success: true, itemId:
|
|
1630
|
+
return { success: true, itemId: ${ID}, animationType: '${animationType}' };
|
|
1419
1631
|
})();
|
|
1420
1632
|
`.trim();
|
|
1421
1633
|
}
|
|
@@ -1465,10 +1677,11 @@ function normalizeKeyframePositions(keyframes) {
|
|
|
1465
1677
|
* "item_7 is not on the canvas" is actionable, silence is not.
|
|
1466
1678
|
*/
|
|
1467
1679
|
function requireItem(itemId, what) {
|
|
1680
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1468
1681
|
const isRef = typeof itemId === 'string' && itemId.startsWith('$');
|
|
1469
1682
|
const targetExpr = isRef
|
|
1470
|
-
? `((typeof window !== 'undefined' && window.__pinepaper_batch_item_ids && window.__pinepaper_batch_item_ids[${itemId.substring(1)}]) ||
|
|
1471
|
-
:
|
|
1683
|
+
? `((typeof window !== 'undefined' && window.__pinepaper_batch_item_ids && window.__pinepaper_batch_item_ids[${itemId.substring(1)}]) || ${ID})`
|
|
1684
|
+
: `${ID}`;
|
|
1472
1685
|
return `
|
|
1473
1686
|
const _targetId = ${targetExpr};
|
|
1474
1687
|
const _target = (app.getItemById && app.getItemById(_targetId))
|
|
@@ -1506,6 +1719,7 @@ function unanimatedTextKeysJs(keys) {
|
|
|
1506
1719
|
/** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
|
|
1507
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.'`;
|
|
1508
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
|
|
1509
1723
|
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
1510
1724
|
const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
|
|
1511
1725
|
const opts = { duration: calculatedDuration, loop };
|
|
@@ -1587,9 +1801,9 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1587
1801
|
const easingAtCheck = easingAt !== undefined ? `
|
|
1588
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.');` : '';
|
|
1589
1803
|
const runtimeWarning = `${textKeysJs ? ', ...(__ignored.length ? { ignoredProperties: __ignored } : {})' : ''}, ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
|
|
1590
|
-
const addAnimTarget = itemId.startsWith('$') ? '_targetId' :
|
|
1804
|
+
const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `${ID}`;
|
|
1591
1805
|
return `
|
|
1592
|
-
// Apply keyframe animation to ${
|
|
1806
|
+
// Apply keyframe animation to ${ID}
|
|
1593
1807
|
(function() {
|
|
1594
1808
|
${requireItem(itemId, 'the animation')}${audioLevelGuard}
|
|
1595
1809
|
// The track as it was, so the result can say what happened to it: merged
|
|
@@ -1689,15 +1903,16 @@ function emitGeneratorRun(generatorName, paramsJson, region) {
|
|
|
1689
1903
|
* Template for applying effects
|
|
1690
1904
|
*/
|
|
1691
1905
|
function generateApplyEffectCode(itemId, effectType, params) {
|
|
1906
|
+
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1692
1907
|
return `
|
|
1693
|
-
// Apply ${effectType} effect to ${
|
|
1694
|
-
const item = app.getItemById(
|
|
1908
|
+
// Apply ${effectType} effect to ${ID}
|
|
1909
|
+
const item = app.getItemById(${ID});
|
|
1695
1910
|
if (!item) {
|
|
1696
|
-
throw new Error('Item not found: ${
|
|
1911
|
+
throw new Error('Item not found: ' + ${ID});
|
|
1697
1912
|
}
|
|
1698
1913
|
app.applyEffect(item, '${effectType}', ${JSON.stringify(params, null, 2)});
|
|
1699
1914
|
|
|
1700
|
-
({ success: true, itemId:
|
|
1915
|
+
({ success: true, itemId: ${ID}, effectType: '${effectType}' });
|
|
1701
1916
|
`.trim();
|
|
1702
1917
|
}
|
|
1703
1918
|
/**
|
|
@@ -2008,6 +2223,25 @@ const MEDIA_PURGE = `
|
|
|
2008
2223
|
if (__PA && typeof __PA.listMedia === 'function' && typeof __PA.removeMedia === 'function') {
|
|
2009
2224
|
__PA.listMedia().forEach(function(m) { try { if (__PA.removeMedia(m.id)) __mediaRemoved++; } catch (_) { /* keep clearing */ } });
|
|
2010
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 = [];`;
|
|
2011
2245
|
/**
|
|
2012
2246
|
* Template for clearing all items from canvas
|
|
2013
2247
|
*/
|
|
@@ -2031,6 +2265,7 @@ if (typeof app.clearCanvas === 'function') {
|
|
|
2031
2265
|
if (app.itemRegistry) app.itemRegistry.clear();
|
|
2032
2266
|
if (app.relationRegistry) app.relationRegistry.clear();
|
|
2033
2267
|
}
|
|
2268
|
+
${ID_COUNTER_RESET}
|
|
2034
2269
|
|
|
2035
2270
|
if (app.historyManager) app.historyManager.saveState();
|
|
2036
2271
|
|
|
@@ -2044,9 +2279,11 @@ const remaining = app.itemRegistry ? app.itemRegistry.getAll().length : 0;
|
|
|
2044
2279
|
*/
|
|
2045
2280
|
function generateExportSVGCode() {
|
|
2046
2281
|
return `
|
|
2047
|
-
// Export animated SVG
|
|
2282
|
+
// Export animated SVG. An export that produced nothing is a failure, named, not a success with an empty string.
|
|
2048
2283
|
const svgString = app.exportAnimatedSVG();
|
|
2049
|
-
(
|
|
2284
|
+
(typeof svgString !== 'string' || svgString.length === 0)
|
|
2285
|
+
? ({ success: false, step: 'export', error: 'export failed at the export step: exportAnimatedSVG returned ' + (svgString === '' ? 'an empty string' : (svgString === null ? 'null' : typeof svgString)) + ', so there is no SVG to return' })
|
|
2286
|
+
: ({ success: true, svgString, hasAnimations: svgString.includes('<animate') });
|
|
2050
2287
|
`.trim();
|
|
2051
2288
|
}
|
|
2052
2289
|
/**
|
|
@@ -2279,13 +2516,23 @@ export function planKeyframeMerges(operations) {
|
|
|
2279
2516
|
* through modifyItem after create, so the engine's own bookkeeping runs.
|
|
2280
2517
|
*/
|
|
2281
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.';
|
|
2282
2528
|
function withBrandDefaults(code, itemType, props) {
|
|
2283
2529
|
if (props.noBrand === true)
|
|
2284
2530
|
return code;
|
|
2285
2531
|
const isText = itemType === 'text';
|
|
2286
2532
|
if (!isText && !BRAND_FILL_TYPES.has(itemType))
|
|
2287
2533
|
return code;
|
|
2288
|
-
|
|
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);
|
|
2289
2536
|
const hasFont = props.fontFamily !== undefined;
|
|
2290
2537
|
if (hasColor && (!isText || hasFont))
|
|
2291
2538
|
return code;
|
|
@@ -2374,6 +2621,15 @@ ${itemSettersApplyJs(s, '__sid', '__res')}
|
|
|
2374
2621
|
return __res;
|
|
2375
2622
|
})();`;
|
|
2376
2623
|
}
|
|
2624
|
+
/**
|
|
2625
|
+
* A batch op's item reference as a JS expression: `$N` is the Nth item this
|
|
2626
|
+
* batch created, anything else is an id, quoted. Only digits follow the `$` —
|
|
2627
|
+
* the raw splice it replaces put `$1);…;(` straight into the snippet, and an id
|
|
2628
|
+
* with a quote in it broke the batch's syntax.
|
|
2629
|
+
*/
|
|
2630
|
+
export function batchRef(raw) {
|
|
2631
|
+
return typeof raw === 'string' && /^\$\d+$/.test(raw) ? `__ref(${raw.slice(1)})` : JSON.stringify(String(raw));
|
|
2632
|
+
}
|
|
2377
2633
|
function asReturningBody(snippet) {
|
|
2378
2634
|
const single = snippet.trim();
|
|
2379
2635
|
if (single.startsWith('(async function()'))
|
|
@@ -2878,6 +3134,11 @@ export class PinePaperCodeGenerator {
|
|
|
2878
3134
|
return { success: true, presetId: ${JSON.stringify(validated.presetId)}, relationType: r.relationType, params: r.params, values: r.values };
|
|
2879
3135
|
})();`.trim();
|
|
2880
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
|
+
}
|
|
2881
3142
|
if (validated.relationType === 'moves_along_path') {
|
|
2882
3143
|
return generateMovesAlongPathCode(validated.sourceId, validated.targetId ?? undefined, (validated.params ?? {}));
|
|
2883
3144
|
}
|
|
@@ -3554,6 +3815,15 @@ if (typeof _gen === 'function') {
|
|
|
3554
3815
|
for (const relation of relations) {
|
|
3555
3816
|
const { source, target, type, params = {} } = relation;
|
|
3556
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
|
+
}
|
|
3557
3827
|
codeParts.push(`
|
|
3558
3828
|
// Add relation: ${source} ${type} ${target}
|
|
3559
3829
|
(function() {
|
|
@@ -3565,7 +3835,7 @@ if (typeof _gen === 'function') {
|
|
|
3565
3835
|
if (app.addRelation(sourceId, targetId, '${type}', params)) {
|
|
3566
3836
|
results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
|
|
3567
3837
|
} else {
|
|
3568
|
-
const __why = ${relationRefusalReasonJs(
|
|
3838
|
+
const __why = ${relationRefusalReasonJs(jsLiteral(type))};
|
|
3569
3839
|
(results.relationsRefused = results.relationsRefused || []).push({ source: '${source}', target: '${target}', type: '${type}', reason: __why || 'the studio refused it' });
|
|
3570
3840
|
results.success = false;
|
|
3571
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.';
|
|
@@ -3983,7 +4253,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3983
4253
|
if (app.patternGroup) app.patternGroup.removeChildren();
|
|
3984
4254
|
if (app.itemRegistry) app.itemRegistry.clear();
|
|
3985
4255
|
if (app.relationRegistry) app.relationRegistry.clear();
|
|
3986
|
-
}
|
|
4256
|
+
}${ID_COUNTER_RESET}
|
|
3987
4257
|
`;
|
|
3988
4258
|
}
|
|
3989
4259
|
if (canvasPreset) {
|
|
@@ -4226,7 +4496,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4226
4496
|
if (app.patternGroup) app.patternGroup.removeChildren();
|
|
4227
4497
|
if (app.itemRegistry) app.itemRegistry.clear();
|
|
4228
4498
|
if (app.relationRegistry) app.relationRegistry.clear();
|
|
4229
|
-
}
|
|
4499
|
+
}${ID_COUNTER_RESET}
|
|
4230
4500
|
`;
|
|
4231
4501
|
}
|
|
4232
4502
|
else {
|
|
@@ -4234,7 +4504,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4234
4504
|
// Clear items but preserve background
|
|
4235
4505
|
if (app.textItemGroup) app.textItemGroup.removeChildren();
|
|
4236
4506
|
if (app.itemRegistry) app.itemRegistry.clear();
|
|
4237
|
-
if (app.relationRegistry) app.relationRegistry.clear()
|
|
4507
|
+
if (app.relationRegistry) app.relationRegistry.clear();${ID_COUNTER_RESET}
|
|
4238
4508
|
`;
|
|
4239
4509
|
}
|
|
4240
4510
|
if (canvasPreset) {
|
|
@@ -4265,11 +4535,16 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4265
4535
|
*/
|
|
4266
4536
|
generateAgentBatchExecute(input) {
|
|
4267
4537
|
const validated = AgentBatchExecuteInputSchema.parse(input);
|
|
4268
|
-
const {
|
|
4538
|
+
const { atomic } = validated;
|
|
4539
|
+
// ring / grid / orbit / stagger / patch become the batch's own ops (batch-macros.ts).
|
|
4540
|
+
const operations = expandBatchMacros(validated.operations);
|
|
4269
4541
|
const isAtomic = atomic !== false;
|
|
4270
4542
|
// Fold duplicate keyframe_animate targets so a later op can't silently
|
|
4271
4543
|
// clobber an earlier track (app.addAnimation replaces, never merges).
|
|
4272
4544
|
const mergePlan = planKeyframeMerges(operations);
|
|
4545
|
+
// Timeline ops compile here, on the server: the handler refuses a batch whose timeline does not compile before
|
|
4546
|
+
// anything runs; a direct caller gets the op's own failure instead.
|
|
4547
|
+
const timelines = compileBatchTimelines(operations);
|
|
4273
4548
|
let code = `
|
|
4274
4549
|
// Batch execute ${operations.length} operations
|
|
4275
4550
|
(async function() {
|
|
@@ -4284,7 +4559,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4284
4559
|
|| (typeof v.reason === 'string' && v.reason)
|
|
4285
4560
|
|| 'the operation reported failure without naming a reason';
|
|
4286
4561
|
};
|
|
4287
|
-
// $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
|
|
4288
4564
|
// previous batch's ids are kept only to name the right id when a $N reaches
|
|
4289
4565
|
// past what this batch has made: picking it silently shifted every $N in a
|
|
4290
4566
|
// batch that also creates (B115 follow-up).
|
|
@@ -4293,16 +4569,39 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4293
4569
|
const __ref = function (n) {
|
|
4294
4570
|
if (itemIds[n] !== undefined) return itemIds[n];
|
|
4295
4571
|
const prev = __prevBatchIds[n];
|
|
4296
|
-
throw new Error('$' + n + ' is
|
|
4297
|
-
+ (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.'));
|
|
4298
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; };
|
|
4299
4577
|
const results = [];
|
|
4300
4578
|
let success = true;
|
|
4579
|
+
// A failure's words, never "undefined" for a thrown non-Error.
|
|
4580
|
+
const _why = (e, fb) => (e && typeof e.message === 'string' && e.message.trim()) ? e.message
|
|
4581
|
+
: (typeof e === 'string' && e.trim()) ? e : fb;
|
|
4582
|
+
// THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything
|
|
4583
|
+
// failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the
|
|
4584
|
+
// result. A studio without lintScene says nothing.
|
|
4585
|
+
const __ppLintNow = function () {
|
|
4586
|
+
try {
|
|
4587
|
+
if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];
|
|
4588
|
+
const r = app.lintScene({ warningsOnly: true });
|
|
4589
|
+
return r && Array.isArray(r.warnings) ? r.warnings : [];
|
|
4590
|
+
} catch (e) {
|
|
4591
|
+
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
4592
|
+
}
|
|
4593
|
+
};
|
|
4594
|
+
// Every failed op as { step, itemId, reason }, beside the per-op results.
|
|
4595
|
+
const _failures = () => results.filter((r) => !r.success).map((r) => ({ step: r.step, itemId: r.itemId, reason: r.reason }));
|
|
4301
4596
|
|
|
4302
4597
|
try {
|
|
4303
4598
|
`;
|
|
4304
4599
|
operations.forEach((op, index) => {
|
|
4305
|
-
const opCode =
|
|
4600
|
+
const opCode = op.type === 'timeline'
|
|
4601
|
+
? (timelines.plans.has(index)
|
|
4602
|
+
? emitTimelineOpBody(timelines.plans.get(index), (t) => (t.startsWith('$') ? `__ref(${t.substring(1)})` : JSON.stringify(t)))
|
|
4603
|
+
: `return { success: false, error: ${JSON.stringify(timelines.errors.filter((e) => e.path.startsWith(`operations[${index}]`)).map((e) => `${e.path}: ${e.message}`).join('; ') || 'the timeline did not compile')} };`)
|
|
4604
|
+
: this.generateBatchOperationCode(op, index, mergePlan);
|
|
4306
4605
|
// $N variable references are documented as "items CREATED in earlier
|
|
4307
4606
|
// operations" — only `create` results may extend the itemIds array.
|
|
4308
4607
|
// animate/keyframe_animate/modify/delete also return { itemId } (the
|
|
@@ -4310,9 +4609,15 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4310
4609
|
// $N reference: a batch of [create ×5, keyframe_animate ×5, create ×5,
|
|
4311
4610
|
// keyframe_animate ×5] silently re-animated the FIRST five items and
|
|
4312
4611
|
// left the second five without keyframes (mis-oriented/mis-animated
|
|
4313
|
-
// vehicles, 2026-07-30). `group`
|
|
4314
|
-
//
|
|
4315
|
-
|
|
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';
|
|
4616
|
+
// The item this op acts on, for a failure's itemId: its own id field, else what its result names.
|
|
4617
|
+
const o = op;
|
|
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);
|
|
4620
|
+
const opStep = typeof o._step === 'string' ? `op ${index}: ${o._step}` : `op ${index} (${op.type})`;
|
|
4316
4621
|
code += `
|
|
4317
4622
|
// Operation ${index}: ${op.type}
|
|
4318
4623
|
try {
|
|
@@ -4329,7 +4634,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4329
4634
|
if (failed${index}) {
|
|
4330
4635
|
// ATOMIC: throw WITHOUT recording, and let the catch below record it
|
|
4331
4636
|
// once. Doing both put two entries in for one failed op.
|
|
4332
|
-
${isAtomic ? `
|
|
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} });
|
|
4333
4638
|
success = false;`}
|
|
4334
4639
|
} else {
|
|
4335
4640
|
results.push({ index: ${index}, success: true, result: result${index} });
|
|
@@ -4338,7 +4643,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4338
4643
|
}` : ''}
|
|
4339
4644
|
}
|
|
4340
4645
|
} catch (opError) {
|
|
4341
|
-
|
|
4646
|
+
const __r${index} = _why(opError, 'operation ${index} (${op.type}) threw with no message');
|
|
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} });
|
|
4342
4648
|
${isAtomic ? 'throw opError;' : 'success = false;'}
|
|
4343
4649
|
}
|
|
4344
4650
|
`;
|
|
@@ -4358,15 +4664,19 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4358
4664
|
}
|
|
4359
4665
|
return { success, itemIds, results, operationCount: ${operations.length},
|
|
4360
4666
|
failedCount: _failed.length,
|
|
4361
|
-
...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; ') } : {}),
|
|
4362
|
-
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
4667
|
+
...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; '), failures: _failures() } : {}),
|
|
4668
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
4363
4669
|
} catch (e) {
|
|
4364
4670
|
// A batch that created nothing keeps the previous batch's ids on record.
|
|
4365
4671
|
if (typeof window !== 'undefined' && itemIds.length) {
|
|
4366
4672
|
window.__pinepaper_batch_item_ids = itemIds;
|
|
4367
4673
|
}
|
|
4368
4674
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
4369
|
-
|
|
4675
|
+
const __fails = _failures();
|
|
4676
|
+
const __reason = _why(e, 'the batch failed with no message');
|
|
4677
|
+
return { success: false, error: __reason, itemIds, results, operationCount: ${operations.length},
|
|
4678
|
+
failures: __fails.length ? __fails : [{ step: 'batch', itemId: null, reason: __reason }],
|
|
4679
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
4370
4680
|
}
|
|
4371
4681
|
})();
|
|
4372
4682
|
`;
|
|
@@ -4403,7 +4713,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4403
4713
|
'backgroundColor', 'generatorName', 'generatorParams', 'generatorRegion',
|
|
4404
4714
|
'width', 'height', 'preset', 'keyframes', 'duration', 'loop',
|
|
4405
4715
|
'maskPreset', 'maskType', 'maskOptions', 'effectType', 'effectParams',
|
|
4406
|
-
'action', 'time', 'append'
|
|
4716
|
+
'action', 'time', 'append', '_step'
|
|
4407
4717
|
]);
|
|
4408
4718
|
for (const [k, v] of Object.entries(anyOp)) {
|
|
4409
4719
|
if (!batchKeys.has(k) && v !== undefined && opProps[k] === undefined) {
|
|
@@ -4448,9 +4758,7 @@ return __created;
|
|
|
4448
4758
|
return createCode;
|
|
4449
4759
|
}
|
|
4450
4760
|
case 'modify': {
|
|
4451
|
-
const itemRef = op.itemId
|
|
4452
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4453
|
-
: `'${op.itemId}'`;
|
|
4761
|
+
const itemRef = batchRef(op.itemId);
|
|
4454
4762
|
// modify_item's own emitter, as create is create_item's (see above):
|
|
4455
4763
|
// the batch's modifyItem call missed text styles, font loading, audio
|
|
4456
4764
|
// level, no-fill, lifetimes, smoothing and the unread-property report.
|
|
@@ -4458,36 +4766,43 @@ return __created;
|
|
|
4458
4766
|
// at run time, so it is generated with a placeholder and the runtime
|
|
4459
4767
|
// variable put in its place.
|
|
4460
4768
|
const single = generateModifyItemCode('__MID__', (op.properties || {})).trim()
|
|
4461
|
-
.replace(/
|
|
4769
|
+
.replace(/"__MID__"/g, '__mid')
|
|
4462
4770
|
.replace(/__MID__/g, "' + __mid + '")
|
|
4463
4771
|
.replace(/;\s*$/, '');
|
|
4464
4772
|
return `
|
|
4465
4773
|
const __mid = ${itemRef};
|
|
4774
|
+
if (app.itemRegistry && app.itemRegistry.getItem && !app.itemRegistry.getItem(__mid) && typeof app.getItemsByName === 'function') {
|
|
4775
|
+
// A NAME HELD BY SEVERAL ITEMS is refused, not resolved: getItemById answers
|
|
4776
|
+
// the first holder, so a patch by name would change whichever came first.
|
|
4777
|
+
const __held = app.getItemsByName(__mid) || [];
|
|
4778
|
+
if (__held.length > 1) {
|
|
4779
|
+
return { success: false, itemId: __mid, error: __held.length + ' items are named ' + JSON.stringify(__mid) + ' (' + __held.map(function (h) { return (h && h.data && h.data.id) || '?'; }).join(', ') + '); pass one of their ids.' };
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4466
4782
|
const __r = await ${single};
|
|
4467
4783
|
if (__r && __r.success !== false) __r.modified = true;
|
|
4468
4784
|
return __r;
|
|
4469
4785
|
`;
|
|
4470
4786
|
}
|
|
4471
4787
|
case 'animate':
|
|
4472
|
-
const animItemRef = op.itemId
|
|
4473
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4474
|
-
: `'${op.itemId}'`;
|
|
4788
|
+
const animItemRef = batchRef(op.itemId);
|
|
4475
4789
|
const animOpts = JSON.stringify(op.animationOptions || {});
|
|
4476
4790
|
return `
|
|
4477
4791
|
const targetId = ${animItemRef};
|
|
4478
4792
|
const item = app.getItemById(targetId);
|
|
4479
4793
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
4480
|
-
app.animate(item, { animationType:
|
|
4481
|
-
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))} };
|
|
4482
4796
|
`;
|
|
4483
4797
|
case 'relation':
|
|
4484
|
-
const srcRef = op.sourceId
|
|
4485
|
-
|
|
4486
|
-
: `'${op.sourceId}'`;
|
|
4487
|
-
const tgtRef = op.targetId?.startsWith('$')
|
|
4488
|
-
? `__ref(${op.targetId.substring(1)})`
|
|
4489
|
-
: `'${op.targetId}'`;
|
|
4798
|
+
const srcRef = batchRef(op.sourceId);
|
|
4799
|
+
const tgtRef = batchRef(op.targetId);
|
|
4490
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
|
+
`;
|
|
4491
4806
|
return `
|
|
4492
4807
|
const sourceId = ${srcRef};
|
|
4493
4808
|
const targetId = ${tgtRef};
|
|
@@ -4495,22 +4810,20 @@ const source = app.getItemById(sourceId);
|
|
|
4495
4810
|
const target = app.getItemById(targetId);
|
|
4496
4811
|
if (!source) throw new Error('Source not found: ' + sourceId);
|
|
4497
4812
|
if (!target) throw new Error('Target not found: ' + targetId);
|
|
4498
|
-
const relationId = app.addRelation(source, target,
|
|
4813
|
+
const relationId = app.addRelation(source, target, ${JSON.stringify(String(op.relationType))}, ${relOpts});
|
|
4499
4814
|
// NOT BOUND IS NOT A SUCCESS (owner rule 1, B119). addRelation answers false
|
|
4500
4815
|
// when it refuses — spring_follow "not loaded yet" — and this op returned
|
|
4501
4816
|
// { relationId: false } with nothing marking it failed, so the batch said
|
|
4502
4817
|
// success over a relation that does not exist.
|
|
4503
4818
|
if (!relationId) {
|
|
4504
|
-
const __why = ${relationRefusalReasonJs(
|
|
4819
|
+
const __why = ${relationRefusalReasonJs(jsLiteral(op.relationType))};
|
|
4505
4820
|
return { success: false, relationId: false, sourceId, targetId, relationType: ${JSON.stringify(op.relationType)},
|
|
4506
4821
|
error: 'the studio refused the ' + ${JSON.stringify(op.relationType)} + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };
|
|
4507
4822
|
}
|
|
4508
|
-
return { relationId, sourceId, targetId, relationType:
|
|
4823
|
+
return { relationId, sourceId, targetId, relationType: ${JSON.stringify(String(op.relationType))} };
|
|
4509
4824
|
`;
|
|
4510
4825
|
case 'delete':
|
|
4511
|
-
const delRef = op.itemId
|
|
4512
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4513
|
-
: `'${op.itemId}'`;
|
|
4826
|
+
const delRef = batchRef(op.itemId);
|
|
4514
4827
|
return `
|
|
4515
4828
|
const targetId = ${delRef};
|
|
4516
4829
|
const item = app.getItemById(targetId);
|
|
@@ -4521,10 +4834,10 @@ return { itemId: targetId, deleted: true };
|
|
|
4521
4834
|
`;
|
|
4522
4835
|
case 'group': {
|
|
4523
4836
|
// Resolve each ref ($N → itemIds[N]; literal → quoted id) into a JS array.
|
|
4524
|
-
const refs = (op.itemIds || []).map((ref) =>
|
|
4837
|
+
const refs = (op.itemIds || []).map((ref) => batchRef(ref));
|
|
4525
4838
|
const idsArrayExpr = `[${refs.join(', ')}]`;
|
|
4526
4839
|
const nameExpr = op.groupName !== undefined ? JSON.stringify(op.groupName) : 'undefined';
|
|
4527
|
-
// Returns { groupId,
|
|
4840
|
+
// Returns { groupId, itemId }: the group takes the next $N slot (O2), as a create does.
|
|
4528
4841
|
return `
|
|
4529
4842
|
return (function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
|
|
4530
4843
|
})();
|
|
@@ -4533,8 +4846,8 @@ return (function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
|
|
|
4533
4846
|
case 'set_background':
|
|
4534
4847
|
const bgColor = op.backgroundColor || op.color || op.properties?.backgroundColor || op.properties?.color || '#000000';
|
|
4535
4848
|
return `
|
|
4536
|
-
app.setBackgroundColor(
|
|
4537
|
-
return { success: true, backgroundColor:
|
|
4849
|
+
app.setBackgroundColor(${JSON.stringify(String(bgColor))});
|
|
4850
|
+
return { success: true, backgroundColor: ${JSON.stringify(String(bgColor))} };
|
|
4538
4851
|
`;
|
|
4539
4852
|
case 'execute_generator':
|
|
4540
4853
|
const genName = op.generatorName || 'drawSunburst';
|
|
@@ -4565,14 +4878,12 @@ return { success: true, width: (_cs && _cs.width) || ${w}, height: (_cs && _cs.h
|
|
|
4565
4878
|
return `
|
|
4566
4879
|
// Keyframes merged into operation ${foldTarget} (same target). A keyframe track
|
|
4567
4880
|
// is replaced wholesale, so emitting both here would silently drop one.
|
|
4568
|
-
return { itemId: ${
|
|
4881
|
+
return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget} };
|
|
4569
4882
|
`;
|
|
4570
4883
|
}
|
|
4571
4884
|
const mergedPayload = mergePlan?.merged.get(index);
|
|
4572
4885
|
const rawItemId = String(op.itemId ?? op.id ?? op.properties?.itemId ?? op.properties?.id);
|
|
4573
|
-
const kfItemRef = rawItemId
|
|
4574
|
-
? `__ref(${rawItemId.substring(1)})`
|
|
4575
|
-
: `'${rawItemId}'`;
|
|
4886
|
+
const kfItemRef = batchRef(rawItemId);
|
|
4576
4887
|
const kfSource = mergedPayload
|
|
4577
4888
|
? mergedPayload.keyframes
|
|
4578
4889
|
: (op.keyframes || (op.properties && op.properties.keyframes) || (op.params && op.params.keyframes) || []);
|
|
@@ -4614,9 +4925,7 @@ return { success: true, itemId: targetId, duration: ${kfDuration}, loop: ${kfLoo
|
|
|
4614
4925
|
`;
|
|
4615
4926
|
}
|
|
4616
4927
|
case 'apply_mask': {
|
|
4617
|
-
const maskItemRef = op.itemId
|
|
4618
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4619
|
-
: `'${op.itemId}'`;
|
|
4928
|
+
const maskItemRef = batchRef(op.itemId);
|
|
4620
4929
|
const maskOpts = JSON.stringify(op.maskOptions || {});
|
|
4621
4930
|
const preset = op.maskPreset || op.maskType || 'wipeLeft';
|
|
4622
4931
|
return `
|
|
@@ -4633,9 +4942,7 @@ return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
|
|
|
4633
4942
|
`;
|
|
4634
4943
|
}
|
|
4635
4944
|
case 'apply_effect': {
|
|
4636
|
-
const effItemRef = op.itemId
|
|
4637
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4638
|
-
: `'${op.itemId}'`;
|
|
4945
|
+
const effItemRef = batchRef(op.itemId);
|
|
4639
4946
|
const effParams = JSON.stringify(op.effectParams || {});
|
|
4640
4947
|
return `
|
|
4641
4948
|
const targetId = ${effItemRef};
|
|
@@ -5120,6 +5427,19 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
5120
5427
|
// cannot be kept accurate — it breaks on any edit above it — so this repo
|
|
5121
5428
|
// does not use them. Camera animation still
|
|
5122
5429
|
// drives motion during export — framing only fixes the output frame.
|
|
5430
|
+
// THE SCENE LINT, BEFORE THE EXPORT (FxTool lintScene().warnings): what the file will show wrong without
|
|
5431
|
+
// anything failing — a centre-anchored text that runs out of the frame exports as "IGNAL" for "SIGNAL". Read from the
|
|
5432
|
+
// scene as it is, never a refusal; a studio without the rule says nothing, a lint that throws says so.
|
|
5433
|
+
const __lint = (function () {
|
|
5434
|
+
if (typeof app.lintScene !== 'function') return null;
|
|
5435
|
+
try {
|
|
5436
|
+
const r = app.lintScene({ warningsOnly: true });
|
|
5437
|
+
return r && Array.isArray(r.warnings) ? r.warnings : null;
|
|
5438
|
+
} catch (e) {
|
|
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') }];
|
|
5440
|
+
}
|
|
5441
|
+
})();
|
|
5442
|
+
|
|
5123
5443
|
let cameraDims = null;
|
|
5124
5444
|
if (framing === 'camera') {
|
|
5125
5445
|
if (!['gif', 'apng', 'mp4', 'webm'].includes(format)) {
|
|
@@ -5200,14 +5520,36 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
5200
5520
|
});
|
|
5201
5521
|
|
|
5202
5522
|
let result = { success: false, platform, format, quality, framing };
|
|
5203
|
-
// 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
|
|
5204
5546
|
// MCP's frame callback are applied by the frame loop, and a seek does not run
|
|
5205
5547
|
// it — so a png at time 0.8 s drew nothing staged at 0.8 s, and an item past
|
|
5206
5548
|
// its ttl still showed (retest). The visibility pass runs here for stills.
|
|
5207
5549
|
if (['png', 'jpg', 'webp', 'svg', 'pdf'].indexOf(format) !== -1) {
|
|
5208
5550
|
const __L = globalThis.__ppMcp;
|
|
5209
5551
|
if (__L && __L.lifetimeApp === app && typeof __L.applyLifetimes === 'function') {
|
|
5210
|
-
try { __L.applyLifetimes(
|
|
5552
|
+
try { __L.applyLifetimes(__stillT !== null ? __stillT : (typeof app.playbackTime === 'number' ? app.playbackTime : 0)); } catch (_) { /* the still renders either way */ }
|
|
5211
5553
|
}
|
|
5212
5554
|
}
|
|
5213
5555
|
// The engine's report on the audio track of THIS export (FxTool b41860aa:
|
|
@@ -5216,18 +5558,19 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
5216
5558
|
const __vx = app.exportEngine && app.exportEngine.videoExporter;
|
|
5217
5559
|
if (__vx && 'lastAudioReport' in __vx) __vx.lastAudioReport = null;
|
|
5218
5560
|
if (__vx && 'lastVideoReport' in __vx) __vx.lastVideoReport = null;
|
|
5219
|
-
${stillTime !== undefined ? `
|
|
5220
5561
|
// A STILL AT A CHOSEN MOMENT (round 7 X, 1.57). Without this a png is
|
|
5221
5562
|
// whatever frame the playhead is on — two identical builds gave PNGs that
|
|
5222
5563
|
// differed in the animated streak while their MP4s were byte-identical.
|
|
5223
5564
|
// Seek, render, and put the playhead back afterwards.
|
|
5224
5565
|
const __prevT = typeof app.playbackTime === 'number' ? app.playbackTime : 0;
|
|
5225
|
-
if (
|
|
5226
|
-
|
|
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();
|
|
5227
5572
|
}
|
|
5228
|
-
|
|
5229
|
-
if (typeof paper !== 'undefined' && paper.view && typeof paper.view.update === 'function') paper.view.update();
|
|
5230
|
-
` : ''}
|
|
5573
|
+
|
|
5231
5574
|
try {
|
|
5232
5575
|
// jpg / webp render as png, then re-encode below.
|
|
5233
5576
|
switch ((format === 'jpg' || format === 'webp') ? 'png' : format) {
|
|
@@ -5262,7 +5605,7 @@ ${stillTime !== undefined ? `
|
|
|
5262
5605
|
if ('lastRegionExcluded' in app) app.lastRegionExcluded = null;
|
|
5263
5606
|
// A video in the region is waited for at this time (round 12, 12.5):
|
|
5264
5607
|
// the render drew before the clip's seek landed.
|
|
5265
|
-
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));
|
|
5266
5609
|
const regionUrl = app.renderRegionToDataURL(new paper.Rectangle(${region.x}, ${region.y}, ${region.width}, ${region.height}), rw, rh${region.excludeForeign === false ? '' : ', { excludeForeign: true }'});
|
|
5267
5610
|
const __excluded = Array.isArray(app.lastRegionExcluded) ? app.lastRegionExcluded.filter(function(id) { return id; }) : null;
|
|
5268
5611
|
// ITEMS THAT CROSS THE REGION'S EDGE (round 9 II, 1.78). In the sheet
|
|
@@ -5319,7 +5662,9 @@ ${stillTime !== undefined ? `
|
|
|
5319
5662
|
|| app.canvasSize
|
|
5320
5663
|
|| { width: dimensions.width, height: dimensions.height };
|
|
5321
5664
|
const pngScale = pngBoard.width > 0 ? dimensions.width / pngBoard.width : 1;
|
|
5322
|
-
|
|
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 } : {}) });
|
|
5323
5668
|
const dataUrl = (png && png.dataUrl) ? png.dataUrl : png;
|
|
5324
5669
|
result = {
|
|
5325
5670
|
success: true,
|
|
@@ -5668,7 +6013,7 @@ ${stillTime !== undefined ? `
|
|
|
5668
6013
|
if (typeof fr.embedded === 'number') __tl.fonts.embedded = fr.embedded; // cumulative for the document
|
|
5669
6014
|
if (typeof fr.hiddenTextItems === 'number') __tl.fonts.hiddenTextItems = (__tl.fonts.hiddenTextItems || 0) + fr.hiddenTextItems;
|
|
5670
6015
|
}
|
|
5671
|
-
} catch (e) { if (!__tl.fonts) __tl.fonts = { embedded: 0, reason: e && e.message }; }
|
|
6016
|
+
} catch (e) { if (!__tl.fonts) __tl.fonts = { embedded: 0, reason: (e && typeof e.message === 'string' && e.message) || 'embedding the text fonts failed with no reason given' }; }
|
|
5672
6017
|
}
|
|
5673
6018
|
if (jpg) doc.addImage(jpg, 'JPEG', 0, 0, wMM, hMM); else doc.addImage(png, 'PNG', 0, 0, wMM, hMM);
|
|
5674
6019
|
if (__layer) {
|
|
@@ -5676,7 +6021,7 @@ ${stillTime !== undefined ? `
|
|
|
5676
6021
|
const tr = await app.exportEngine.addTextLayer(doc, { x: 0, y: 0, width: wMM, height: hMM });
|
|
5677
6022
|
if (tr && tr.ok !== false) { __tl.written += tr.linesWritten || 0; __tl.skipped += tr.linesSkipped || 0; }
|
|
5678
6023
|
else __tl.reasons.push(id + ': ' + ((tr && tr.reason) || 'refused'));
|
|
5679
|
-
} catch (e) { __tl.reasons.push(id + ': ' + e.message); }
|
|
6024
|
+
} catch (e) { __tl.reasons.push(id + ': ' + ((e && typeof e.message === 'string' && e.message) || 'the text layer failed with no reason given')); }
|
|
5680
6025
|
}
|
|
5681
6026
|
pagesOut.push({ sceneId: id, widthMM: Math.round(wMM * 10) / 10, heightMM: Math.round(hMM * 10) / 10 });
|
|
5682
6027
|
}
|
|
@@ -5739,7 +6084,7 @@ ${stillTime !== undefined ? `
|
|
|
5739
6084
|
result = { success: false, error: 'Unknown format: ' + format };
|
|
5740
6085
|
}
|
|
5741
6086
|
} catch (e) {
|
|
5742
|
-
result = { success: false, error: e.message, platform, format };
|
|
6087
|
+
result = { success: false, error: (e && typeof e.message === 'string' && e.message) || (typeof e === 'string' && e) || ('the ' + format + ' export threw with no message'), platform, format };
|
|
5743
6088
|
}
|
|
5744
6089
|
|
|
5745
6090
|
// One site, after every branch has set its result: a successful export says
|
|
@@ -5777,11 +6122,19 @@ ${stillTime !== undefined ? `
|
|
|
5777
6122
|
result = Object.assign({}, result, { format, data: encoded, mimeType: mime, size: Math.round(String(encoded).length * 0.75), encodeQuality: settings.compression });
|
|
5778
6123
|
}
|
|
5779
6124
|
} catch (e) {
|
|
5780
|
-
result = { success: false, platform, format, error: (e && e.message) ||
|
|
6125
|
+
result = { success: false, platform, format, error: (e && typeof e.message === 'string' && e.message) || (typeof e === 'string' && e) || ('re-encoding the still as ' + format + ' failed with no message') };
|
|
5781
6126
|
}
|
|
5782
6127
|
}
|
|
5783
6128
|
|
|
5784
6129
|
if (result && result.success) Object.assign(result, fidelity(format));
|
|
6130
|
+
if (result && result.success && __lint && __lint.length && !result.lintWarnings) {
|
|
6131
|
+
result.lintWarnings = __lint;
|
|
6132
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
6133
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat(__lint.map(function (w) {
|
|
6134
|
+
return { code: String(w.code || 'lint'), message: String(w.message || w.code || 'scene lint warning'), ...(w.itemId ? { items: [w.itemId] } : {}) };
|
|
6135
|
+
}));
|
|
6136
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
6137
|
+
}
|
|
5785
6138
|
if (result && result.success && __fit) result.platformFit = __fit;
|
|
5786
6139
|
${usesCanvasSize ? '' : ` // A PRESET SIZE IS NOT THE CANVAS SIZE, AND THAT IS SAID (21 PLATFORMOVERRIDE).
|
|
5787
6140
|
// A named platform renders the FILE at the preset's size; the canvas is not
|
|
@@ -6035,8 +6388,10 @@ ${(() => {
|
|
|
6035
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).`,
|
|
6036
6389
|
})};\n`;
|
|
6037
6390
|
})()}
|
|
6038
|
-
|
|
6039
|
-
|
|
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
|
+
}
|
|
6040
6395
|
// SCREEN-SPACE ITEMS (FxTool D20): SVG and Lottie write geometry, not the
|
|
6041
6396
|
// camera's frame map, so a HUD item is placed as if the camera had not moved.
|
|
6042
6397
|
if (result && result.success && typeof app.screenSpaceItems === 'function' && ['svg', 'lottie', 'dotlottie'].indexOf(${JSON.stringify(format)}) !== -1) {
|
|
@@ -6764,7 +7119,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6764
7119
|
// unchanged. The map itself cannot be moved without breaking geoToCanvas
|
|
6765
7120
|
// (it projects against the untransformed map, so markers would land in the
|
|
6766
7121
|
// wrong place). Until the engine has a map view transform, say so.
|
|
6767
|
-
return `(function() { return { success: false, error: ${
|
|
7122
|
+
return `(function() { return { success: false, error: ${jsLiteral(MAP_VIEW_REFUSAL('pan'))} }; })();`;
|
|
6768
7123
|
}
|
|
6769
7124
|
/**
|
|
6770
7125
|
* Generate code to zoom the map
|
|
@@ -6777,7 +7132,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6777
7132
|
if (validated.duration !== undefined)
|
|
6778
7133
|
options.duration = validated.duration;
|
|
6779
7134
|
// Refused for the same reason as pan (see generatePanMap).
|
|
6780
|
-
return `(function() { return { success: false, error: ${
|
|
7135
|
+
return `(function() { return { success: false, error: ${jsLiteral(MAP_VIEW_REFUSAL('zoom'))} }; })();`;
|
|
6781
7136
|
}
|
|
6782
7137
|
/**
|
|
6783
7138
|
* Generate code to export map configuration
|
|
@@ -7713,14 +8068,41 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7713
8068
|
// An async IIFE is still a `(`-led expression statement, so the governor
|
|
7714
8069
|
// captures its value, and executeCode's eval fallback already resolves a
|
|
7715
8070
|
// returned promise. Both paths await it; neither needed changing.
|
|
8071
|
+
//
|
|
8072
|
+
// The code runs inside its own async arrow so its `return` is still its value; the value comes back under
|
|
8073
|
+
// __ppValue with the scene lint beside it, and a throw comes back as { step, itemId, reason }, never a bare
|
|
8074
|
+
// message (and never "undefined" for a thrown non-Error).
|
|
7716
8075
|
return `
|
|
7717
8076
|
// ${description}
|
|
7718
8077
|
(async function() {
|
|
8078
|
+
// THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything
|
|
8079
|
+
// failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the
|
|
8080
|
+
// result. A studio without lintScene says nothing.
|
|
8081
|
+
const __ppLintNow = function () {
|
|
8082
|
+
try {
|
|
8083
|
+
if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];
|
|
8084
|
+
const r = app.lintScene({ warningsOnly: true });
|
|
8085
|
+
return r && Array.isArray(r.warnings) ? r.warnings : [];
|
|
8086
|
+
} catch (e) {
|
|
8087
|
+
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
8088
|
+
}
|
|
8089
|
+
};
|
|
8090
|
+
let __ppValue;
|
|
7719
8091
|
try {
|
|
8092
|
+
__ppValue = await (async () => {
|
|
7720
8093
|
${validated.code}
|
|
8094
|
+
})();
|
|
7721
8095
|
} catch (error) {
|
|
7722
|
-
|
|
7723
|
-
|
|
8096
|
+
const __ppReason = (error && typeof error.message === 'string' && error.message.trim()) ? error.message
|
|
8097
|
+
: (typeof error === 'string' && error.trim()) ? error : 'the custom code threw with no message';
|
|
8098
|
+
return { success: false, step: 'execute_custom_code', itemId: (error && typeof error.itemId === 'string') ? error.itemId : null,
|
|
8099
|
+
reason: __ppReason, error: __ppReason, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
8100
|
+
}
|
|
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, {}.
|
|
8104
|
+
const __ppL = __ppLintNow();
|
|
8105
|
+
return __ppL.length ? { __ppWrapped: true, __ppValue: __ppValue, __ppLint: __ppL } : __ppValue;
|
|
7724
8106
|
})();
|
|
7725
8107
|
`.trim();
|
|
7726
8108
|
}
|
|
@@ -8051,7 +8433,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
8051
8433
|
`.trim();
|
|
8052
8434
|
}
|
|
8053
8435
|
generateImportImage(input) {
|
|
8054
|
-
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;
|
|
8055
8437
|
// IMPORTSHADOW: the shadow keys, applied through the engine's own modify
|
|
8056
8438
|
// door once the image is placed (and masked) — the route modify_item takes.
|
|
8057
8439
|
const shadow = {};
|
|
@@ -8169,6 +8551,11 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
8169
8551
|
? { applied: true, shadowColor: placed.shadowColor ? String(placed.shadowColor.toCSS ? placed.shadowColor.toCSS(true) : placed.shadowColor) : null, shadowBlur: placed.shadowBlur }
|
|
8170
8552
|
: { applied: false, warning: 'the image was imported but its shadow was not applied — set it with pinepaper_modify_item on ' + itemId + '.' };
|
|
8171
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)};
|
|
8172
8559
|
` : ''} const b = placed.bounds;
|
|
8173
8560
|
if (!b || !(b.width > 0) || !(b.height > 0)) {
|
|
8174
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.' };
|
|
@@ -8176,7 +8563,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
8176
8563
|
return {
|
|
8177
8564
|
success: true,
|
|
8178
8565
|
itemId: itemId,
|
|
8179
|
-
message: 'Image imported and placed on canvas.'
|
|
8566
|
+
message: 'Image imported and placed on canvas.',${name ? `\n name: ${JSON.stringify(name)},` : ''}
|
|
8180
8567
|
bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: '${mask}'` : ''}${hasShadow ? ',\n shadow: __shadow' : ''},
|
|
8181
8568
|
// Stored smaller than its source (the studio's cap): said, with the
|
|
8182
8569
|
// way to keep every pixel. The studio reports native size where it can.
|
|
@@ -8528,6 +8915,53 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
|
|
|
8528
8915
|
};
|
|
8529
8916
|
})();
|
|
8530
8917
|
`.trim();
|
|
8918
|
+
}
|
|
8919
|
+
/**
|
|
8920
|
+
* pinepaper_pattern (AR-2 P2): one stored declaration moves many items (FxTool app.addPattern / modifyPattern /
|
|
8921
|
+
* removePattern / listPatterns / getPattern). No keyframes are written; the engine evaluates the node per frame.
|
|
8922
|
+
*/
|
|
8923
|
+
generatePattern(input) {
|
|
8924
|
+
const v = PatternInputSchema.parse(input);
|
|
8925
|
+
const S = (x) => JSON.stringify(x);
|
|
8926
|
+
const need = (fn) => `if (typeof app.${fn} !== 'function') return { success: false, error: 'app.${fn} unavailable — this studio predates patterns (FxTool AR-2 P1); update it' };`;
|
|
8927
|
+
const idNeeded = v.action !== 'list' && !v.id ? `return { success: false, error: '${v.action} needs the pattern id' };` : '';
|
|
8928
|
+
let body;
|
|
8929
|
+
switch (v.action) {
|
|
8930
|
+
case 'create': {
|
|
8931
|
+
const spec = { id: v.id, kind: v.kind, members: v.members, params: v.params || {}, ...(v.window ? { window: v.window } : {}) };
|
|
8932
|
+
body = `${need('addPattern')}
|
|
8933
|
+
if (!${S(v.kind ?? null)}) return { success: false, error: ${S('create needs kind: ' + PATTERN_KINDS.join(', '))} };
|
|
8934
|
+
if (!${S(v.members ?? null)}) return { success: false, error: 'create needs members: { names: [...] }, { ids: [...] }, { tag } or an array' };
|
|
8935
|
+
const r = app.addPattern(${S(spec)});
|
|
8936
|
+
return Object.assign({ success: !!(r && r.ok) }, r || {}, app.lastReceipt ? { receipt: app.lastReceipt } : {});`;
|
|
8937
|
+
break;
|
|
8938
|
+
}
|
|
8939
|
+
case 'modify': {
|
|
8940
|
+
const ch = { ...(v.params ? { params: v.params } : {}), ...(v.members ? { members: v.members } : {}), ...(v.window !== undefined ? { window: v.window } : {}) };
|
|
8941
|
+
body = `${need('modifyPattern')}
|
|
8942
|
+
const r = app.modifyPattern(${S(v.id)}, ${S(ch)});
|
|
8943
|
+
return Object.assign({ success: !!(r && r.ok) }, r || {}, app.lastReceipt ? { receipt: app.lastReceipt } : {});`;
|
|
8944
|
+
break;
|
|
8945
|
+
}
|
|
8946
|
+
case 'remove':
|
|
8947
|
+
body = `${need('removePattern')}
|
|
8948
|
+
const had = app.removePattern(${S(v.id)});
|
|
8949
|
+
return had ? { success: true, removed: ${S(v.id)} } : { success: false, error: 'no pattern ' + ${S(JSON.stringify(v.id ?? ''))} };`;
|
|
8950
|
+
break;
|
|
8951
|
+
case 'get':
|
|
8952
|
+
body = `${need('getPattern')}
|
|
8953
|
+
const p = app.getPattern(${S(v.id)});
|
|
8954
|
+
return p ? Object.assign({ success: true }, p) : { success: false, error: 'no pattern ' + ${S(JSON.stringify(v.id ?? ''))} };`;
|
|
8955
|
+
break;
|
|
8956
|
+
default:
|
|
8957
|
+
body = `${need('listPatterns')}
|
|
8958
|
+
const list = app.listPatterns();
|
|
8959
|
+
return { success: true, count: list.length, patterns: list };`;
|
|
8960
|
+
}
|
|
8961
|
+
return `(function() {
|
|
8962
|
+
${idNeeded}
|
|
8963
|
+
${body}
|
|
8964
|
+
})();`;
|
|
8531
8965
|
}
|
|
8532
8966
|
generateStagger(input) {
|
|
8533
8967
|
const validated = StaggerInputSchema.parse(input);
|
|
@@ -9162,6 +9596,8 @@ ${resolveSource}
|
|
|
9162
9596
|
})();`.trim();
|
|
9163
9597
|
}
|
|
9164
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 || '');
|
|
9165
9601
|
switch (input.action) {
|
|
9166
9602
|
case 'fit': {
|
|
9167
9603
|
const mode = input.mode || 'contain';
|
|
@@ -9173,31 +9609,37 @@ ${resolveSource}
|
|
|
9173
9609
|
// id space from canvas item ids. PinePaperAgent.fitToFrame resolves either.
|
|
9174
9610
|
const A = (typeof window !== 'undefined') && window.PinePaperAgent;
|
|
9175
9611
|
if (A && typeof A.fitToFrame === 'function') {
|
|
9176
|
-
const ok = A.fitToFrame(${
|
|
9612
|
+
const ok = A.fitToFrame(${ID}, ${JSON.stringify(mode)});
|
|
9177
9613
|
return ok
|
|
9178
|
-
? { success: true, action: 'fit', itemId:
|
|
9179
|
-
: { 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} };
|
|
9180
9616
|
}
|
|
9181
9617
|
if (typeof app.fitToFrame !== 'function') {
|
|
9182
9618
|
return { success: false, error: 'fitToFrame unavailable — update FxTool' };
|
|
9183
9619
|
}
|
|
9184
|
-
const ok = app.fitToFrame(${
|
|
9620
|
+
const ok = app.fitToFrame(${ID}, ${JSON.stringify(mode)});
|
|
9185
9621
|
return ok
|
|
9186
|
-
? { success: true, action: 'fit', itemId:
|
|
9187
|
-
: { 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} };
|
|
9188
9624
|
})();`.trim();
|
|
9189
9625
|
}
|
|
9190
9626
|
case 'nudge': {
|
|
9191
|
-
const dx = input.dx ?? 0;
|
|
9192
|
-
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.
|
|
9193
9633
|
return `
|
|
9194
9634
|
// Nudge item position
|
|
9195
9635
|
(function() {
|
|
9196
|
-
const entry = app.itemRegistry.get(${
|
|
9197
|
-
if (!entry || !entry.item) return { error: 'Item not found: ${
|
|
9198
|
-
entry.item.position.x
|
|
9199
|
-
entry.item.position.y
|
|
9200
|
-
|
|
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 } };
|
|
9201
9643
|
})();`.trim();
|
|
9202
9644
|
}
|
|
9203
9645
|
case 'flip': {
|
|
@@ -9205,10 +9647,10 @@ ${resolveSource}
|
|
|
9205
9647
|
return `
|
|
9206
9648
|
// Flip item ${dir}ly
|
|
9207
9649
|
(function() {
|
|
9208
|
-
const entry = app.itemRegistry.get(${
|
|
9209
|
-
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} };
|
|
9210
9652
|
${dir === 'horizontal' ? 'entry.item.scale(-1, 1);' : 'entry.item.scale(1, -1);'}
|
|
9211
|
-
return { success: true, action: 'flip', itemId:
|
|
9653
|
+
return { success: true, action: 'flip', itemId: ${ID}, direction: ${JSON.stringify(dir)} };
|
|
9212
9654
|
})();`.trim();
|
|
9213
9655
|
}
|
|
9214
9656
|
case 'reorder': {
|
|
@@ -9226,20 +9668,20 @@ ${resolveSource}
|
|
|
9226
9668
|
return `
|
|
9227
9669
|
// Reorder item: ${order}
|
|
9228
9670
|
(function() {
|
|
9229
|
-
const entry = app.itemRegistry.get(${
|
|
9230
|
-
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} };
|
|
9231
9673
|
const sibling = ${siblingExpr};
|
|
9232
9674
|
if (sibling) entry.item.${method}(sibling);
|
|
9233
|
-
return { success: true, action: 'reorder', itemId:
|
|
9675
|
+
return { success: true, action: 'reorder', itemId: ${ID}, order: ${JSON.stringify(order)} };
|
|
9234
9676
|
})();`.trim();
|
|
9235
9677
|
}
|
|
9236
9678
|
return `
|
|
9237
9679
|
// Reorder item: ${order}
|
|
9238
9680
|
(function() {
|
|
9239
|
-
const entry = app.itemRegistry.get(${
|
|
9240
|
-
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} };
|
|
9241
9683
|
entry.item.${method}();
|
|
9242
|
-
return { success: true, action: 'reorder', itemId:
|
|
9684
|
+
return { success: true, action: 'reorder', itemId: ${ID}, order: ${JSON.stringify(order)} };
|
|
9243
9685
|
})();`.trim();
|
|
9244
9686
|
}
|
|
9245
9687
|
default:
|
|
@@ -10026,7 +10468,7 @@ ${resolve}
|
|
|
10026
10468
|
// HAND-DRAWN SKETCHY ANIMATION & JITTER
|
|
10027
10469
|
// ===========================================================================
|
|
10028
10470
|
generateHandDrawn(input) {
|
|
10029
|
-
const S =
|
|
10471
|
+
const S = jsLiteral;
|
|
10030
10472
|
// style carries no schema default: with one, `mode` (its alias) was never read.
|
|
10031
10473
|
const requested = input.style ?? input.mode ?? 'sketchy';
|
|
10032
10474
|
const style = requested === 'ink_reveal' ? 'reveal' : requested;
|
|
@@ -10368,6 +10810,9 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10368
10810
|
(async function() {
|
|
10369
10811
|
if (!app.exportEngine || !app.exportEngine.exportWidget) return { error: 'Widget export not available' };
|
|
10370
10812
|
const result = await app.exportEngine.exportWidget(${JSON.stringify(opts)});
|
|
10813
|
+
if (!result || typeof result.json !== 'string' || result.json.length === 0) {
|
|
10814
|
+
return { success: false, step: 'export', error: 'export failed at the export step: exportWidget returned no scene JSON' + (result && result.error ? ' (' + result.error + ')' : '') };
|
|
10815
|
+
}
|
|
10371
10816
|
// THE EMBED BOX WAS ALWAYS 16:9, AND IT IS NOT SELF-CONTAINED (round 9 GG,
|
|
10372
10817
|
// 8.32). The engine writes aspect-ratio: 16/9 and max-width: 800px whatever
|
|
10373
10818
|
// the scene, so a 300x250 ad sat letterboxed in a wide box. The box is sized
|
|
@@ -10403,13 +10848,16 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10403
10848
|
(async function() {
|
|
10404
10849
|
if (!app.exportEngine || !app.exportEngine.exportWidgetHTML) return { error: 'Widget HTML export not available' };
|
|
10405
10850
|
const result = await app.exportEngine.exportWidgetHTML(${JSON.stringify(opts)});
|
|
10851
|
+
if (!result || typeof result.html !== 'string' || result.html.length === 0) {
|
|
10852
|
+
return { success: false, step: 'export', error: 'export failed at the export step: exportWidgetHTML returned no page' + (result && result.error ? ' (' + result.error + ')' : '') };
|
|
10853
|
+
}
|
|
10406
10854
|
${input.lang || input.dir || input.alt ? `// lang / dir / alt on the engine's page (it writes <html lang="en"> and no
|
|
10407
10855
|
// text alternative). Applied as string edits on the one <html> tag and the
|
|
10408
10856
|
// first <canvas>; the result says which landed.
|
|
10409
10857
|
const __esc = function(v) { return String(v).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<'); };
|
|
10410
10858
|
let __html = String(result.html || '');
|
|
10411
10859
|
const __applied = [];
|
|
10412
|
-
${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}"` : '')} + '>'; });` : ''}
|
|
10413
10861
|
${input.alt ? `// The engine's widget draws its canvas from script, so there is no <canvas>
|
|
10414
10862
|
// tag to label (prod retest): its accessible name is div#w's aria-label,
|
|
10415
10863
|
// mirrored in the screen-reader text #w-desc. Both carry the alt; a literal
|
|
@@ -10438,7 +10886,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10438
10886
|
return `(function() { return { error: 'data is required for create' }; })();`;
|
|
10439
10887
|
const fields = resolveChartFields(input.chartType, input.data, input.options || {});
|
|
10440
10888
|
if ('error' in fields)
|
|
10441
|
-
return `(function() { return { success: false, error: ${
|
|
10889
|
+
return `(function() { return { success: false, error: ${jsLiteral(fields.error)} }; })();`;
|
|
10442
10890
|
const dataStr = JSON.stringify(input.data);
|
|
10443
10891
|
const optsStr = JSON.stringify(fields.options);
|
|
10444
10892
|
return `
|
|
@@ -10924,8 +11372,8 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10924
11372
|
if (app.historyManager) app.historyManager.saveState();
|
|
10925
11373
|
const groupId = (group.data && (group.data.registryId || group.data.groupId)) || null;
|
|
10926
11374
|
// itemId too (round 8 BB, 1.70): every other create answers itemId, and a
|
|
10927
|
-
// group is an item every other tool addresses by that id. In a batch
|
|
10928
|
-
//
|
|
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).
|
|
10929
11377
|
return { success: true, groupId: groupId, itemId: groupId, groupName: group.data && group.data.groupName, itemCount: items.length };`;
|
|
10930
11378
|
}
|
|
10931
11379
|
/**
|
|
@@ -11232,6 +11680,14 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11232
11680
|
canvas: input.canvas,
|
|
11233
11681
|
defaultGeometry: input.defaultGeometry,
|
|
11234
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
|
+
}
|
|
11235
11691
|
const opsJson = JSON.stringify(ops, null, 2);
|
|
11236
11692
|
const diagJson = JSON.stringify(diagnostics);
|
|
11237
11693
|
const docJson = JSON.stringify(input.doc);
|
|
@@ -11325,7 +11781,10 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11325
11781
|
return { success: false, error: 'app.lintScene unavailable — update FxTool to a build with the relational linter (S12-E2)' };
|
|
11326
11782
|
}
|
|
11327
11783
|
const result = app.lintScene(${optsJson});
|
|
11328
|
-
|
|
11784
|
+
// warnings: what the engine's own rules found (TEXT_CLIPS_FRAME: a centre-anchored text running past the
|
|
11785
|
+
// frame edge). Passed through when the studio reports them; an older studio has none to pass.
|
|
11786
|
+
return { success: true, density: result.density, suggestions: result.suggestions,
|
|
11787
|
+
...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}) };
|
|
11329
11788
|
})();`.trim();
|
|
11330
11789
|
}
|
|
11331
11790
|
/**
|
|
@@ -12251,8 +12710,10 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
|
|
|
12251
12710
|
}
|
|
12252
12711
|
}
|
|
12253
12712
|
/** pinepaper_emitter (D29): create('emitter', spec) / setEmitter(id, patch). */
|
|
12254
|
-
generateEmitter(
|
|
12255
|
-
const S =
|
|
12713
|
+
generateEmitter(raw) {
|
|
12714
|
+
const S = jsLiteral;
|
|
12715
|
+
const normalized = normalizeEmitterSpec(raw.spec, raw.action !== 'set');
|
|
12716
|
+
const input = { ...raw, spec: normalized.spec };
|
|
12256
12717
|
// A COUNT OVER THE CAP IS SAID (B144). The cap is 10000 a burst on the
|
|
12257
12718
|
// current studio and was 2000 before it, clamped in silence (2300 drew
|
|
12258
12719
|
// ~1940). Which applies is read from the spec the studio STORED — its
|
|
@@ -12274,7 +12735,10 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
|
|
|
12274
12735
|
}
|
|
12275
12736
|
return out;
|
|
12276
12737
|
}` : '';
|
|
12277
|
-
const
|
|
12738
|
+
const fixed = normalized.warnings;
|
|
12739
|
+
const capJs = (stored) => checks.length || fixed.length
|
|
12740
|
+
? `, ...((function(w) { return w.length ? { warnings: w } : {}; })(${S(fixed)}.concat(${checks.length ? `__capWarnings(${stored})` : '[]'})))`
|
|
12741
|
+
: '';
|
|
12278
12742
|
if (input.action === 'set') {
|
|
12279
12743
|
return `
|
|
12280
12744
|
// Emitter: change a spec
|
|
@@ -13562,10 +14026,15 @@ ${guard('exportPNGSequence')}
|
|
|
13562
14026
|
return Object.assign({ success: true, format: 'png-sequence', data: dataUrl, mimeType: out.type || 'application/zip', size: out.size }, __pngSeqDefaults);
|
|
13563
14027
|
}
|
|
13564
14028
|
if (Array.isArray(out)) {
|
|
13565
|
-
|
|
14029
|
+
// NOT A SAVED FILE, and said so in fields: file null, and why.
|
|
14030
|
+
return { success: true, format: 'png-sequence', frames: out.length, file: null,
|
|
14031
|
+
why: 'the frames stayed in the page: the studio returned them as a list of frames, not a file, so no file was written. Pass download: true to have the browser download them as a zip.',
|
|
13566
14032
|
note: 'the frames stayed in the page — pass download: true to write them out.' };
|
|
13567
14033
|
}
|
|
13568
|
-
|
|
14034
|
+
// download: true — the studio handed the zip to the BROWSER's download, so no bytes reached this server and no file
|
|
14035
|
+
// was written by it. Said in fields, so it cannot be read as a saved file.
|
|
14036
|
+
return { success: true, format: 'png-sequence', result: out, file: null,
|
|
14037
|
+
why: 'the studio handed the zip to the browser as a download (download: true): it went to the browser\\'s download folder, no bytes reached the MCP server, and the server wrote no file.' };
|
|
13569
14038
|
})();`.trim();
|
|
13570
14039
|
}
|
|
13571
14040
|
}
|