@pinepaper.studio/mcp-server 1.6.22 → 1.6.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +115 -5
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SECURITY.md +46 -0
- package/dist/agent/types.d.ts +114 -106
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/browser/puppeteer-controller.js +2 -2
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/design/design-systems.d.ts +50 -0
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +220 -35
- package/dist/design/design-systems.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/ontology/hints.d.ts.map +1 -1
- package/dist/ontology/hints.js +5 -1
- package/dist/ontology/hints.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +1 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/batch-macros.d.ts +27 -0
- package/dist/tools/batch-macros.d.ts.map +1 -0
- package/dist/tools/batch-macros.js +157 -0
- package/dist/tools/batch-macros.js.map +1 -0
- package/dist/tools/choreograph.d.ts +4 -0
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +12 -2
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/compact-descriptions.js +3 -3
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +122 -15
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +2 -2
- package/dist/tools/engine-lists.js +2 -2
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +3 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +52 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/export-file-check.d.ts +31 -0
- package/dist/tools/export-file-check.d.ts.map +1 -0
- package/dist/tools/export-file-check.js +103 -0
- package/dist/tools/export-file-check.js.map +1 -0
- package/dist/tools/export-file-receipt.d.ts +56 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -0
- package/dist/tools/export-file-receipt.js +151 -0
- package/dist/tools/export-file-receipt.js.map +1 -0
- package/dist/tools/handlers/beats.d.ts +7 -44
- package/dist/tools/handlers/beats.d.ts.map +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +31 -6
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +41 -270
- package/dist/tools/handlers/generate.d.ts.map +1 -1
- package/dist/tools/handlers.d.ts +84 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +389 -82
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +34 -0
- package/dist/tools/lane.d.ts.map +1 -0
- package/dist/tools/lane.js +210 -0
- package/dist/tools/lane.js.map +1 -0
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +3 -2
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/nested-ignored.d.ts +16 -0
- package/dist/tools/nested-ignored.d.ts.map +1 -0
- package/dist/tools/nested-ignored.js +83 -0
- package/dist/tools/nested-ignored.js.map +1 -0
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.d.ts.map +1 -1
- package/dist/tools/shape-params.js +9 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +4 -4
- package/dist/tools/stick-vocabulary.d.ts.map +1 -1
- package/dist/tools/stick-vocabulary.js +8 -4
- package/dist/tools/stick-vocabulary.js.map +1 -1
- package/dist/tools/timeline.d.ts +136 -0
- package/dist/tools/timeline.d.ts.map +1 -0
- package/dist/tools/timeline.js +1036 -0
- package/dist/tools/timeline.js.map +1 -0
- package/dist/tools/toolkits.d.ts +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +12 -2
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +66 -3
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +629 -96
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +5278 -24999
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +93 -40
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +5 -1
- package/package.json +8 -7
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* This code is designed to run in the browser context where window.PinePaper is available.
|
|
6
6
|
*/
|
|
7
7
|
import { compileChoreography } from '../tools/choreograph.js';
|
|
8
|
+
import { compileBatchTimelines, emitTimelineOpBody } from '../tools/timeline.js';
|
|
8
9
|
import { normalizeSvgSize } from '../utils/svg-size.js';
|
|
9
10
|
import { WORLD3D_COLOR_PATHS } from '../tools/engine-lists.js';
|
|
10
11
|
import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
|
|
11
12
|
import { ACCEPTED_CREATE_PARAMS, NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
|
|
12
13
|
import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
|
|
14
|
+
import { expandBatchMacros } from '../tools/batch-macros.js';
|
|
13
15
|
import { CreateItemInputSchema, ScreenSpaceSchema, StepTimingSchema, CounterSchema, ImportMotionCaptureInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
|
|
14
16
|
// Diagram types
|
|
15
17
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
@@ -26,7 +28,7 @@ GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTou
|
|
|
26
28
|
// Custom relation/code types
|
|
27
29
|
RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
|
|
28
30
|
// Mask types
|
|
29
|
-
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, MaskTypeSchema, MaskPresetSchema, } from './schemas.js';
|
|
31
|
+
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, PatternInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, MaskTypeSchema, MaskPresetSchema, } from './schemas.js';
|
|
30
32
|
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
31
33
|
// =============================================================================
|
|
32
34
|
// HELPER FUNCTIONS
|
|
@@ -74,17 +76,43 @@ function normalizeGradientSpec(v, key) {
|
|
|
74
76
|
*/
|
|
75
77
|
function generateGradientCode(gradient, itemVarName, position, propertyName = 'fillColor') {
|
|
76
78
|
const { type, stops, origin, destination } = gradient;
|
|
77
|
-
// Calculate default origin/destination based on item position.
|
|
78
|
-
// For radial gradients, origin defaults to the item center (position.x, position.y),
|
|
79
|
-
// not an offset (-50, -50), and destination to the radius edge (B88).
|
|
80
|
-
const defaultOrigin = origin || (type === 'radial' ? [position.x, position.y] : [position.x - 50, position.y - 50]);
|
|
81
|
-
const defaultDestination = destination || (type === 'radial' ? [position.x + 50, position.y] : [position.x + 50, position.y + 50]);
|
|
82
79
|
const stopsCode = stops.map(s => `['${s.color}', ${s.offset}]`).join(', ');
|
|
80
|
+
if (type === 'radial') {
|
|
81
|
+
// A RADIAL CORE BELONGS ON THE ITEM, NOT ON THE NUMBER ASKED FOR (17
|
|
82
|
+
// RADIALOFFSET). The origin was the REQUESTED position and the radius a
|
|
83
|
+
// fixed 50 px: whenever (x, y) placed the item as a corner (an anchor, a
|
|
84
|
+
// default position) the core landed up-left by half the item, and a 300 px
|
|
85
|
+
// circle got a 50 px core. Read from the created item's bounds instead —
|
|
86
|
+
// centre and right-centre, as the engine's own _parseColor does — with the
|
|
87
|
+
// request only as the fallback when the item has no bounds. An origin or
|
|
88
|
+
// destination the caller gave is kept as given.
|
|
89
|
+
const fx = position.x, fy = position.y;
|
|
90
|
+
const o = origin ? `[${origin[0]}, ${origin[1]}]` : `(__gb ? [__gb.x + __gb.width / 2, __gb.y + __gb.height / 2] : [${fx}, ${fy}])`;
|
|
91
|
+
const d = destination ? `[${destination[0]}, ${destination[1]}]`
|
|
92
|
+
: origin ? `(__gb ? [${origin[0]} + __gb.width / 2, ${origin[1]}] : [${origin[0]} + 50, ${origin[1]}])`
|
|
93
|
+
: `(__gb ? [__gb.x + __gb.width, __gb.y + __gb.height / 2] : [${fx + 50}, ${fy}])`;
|
|
94
|
+
return `
|
|
95
|
+
${itemVarName}.${propertyName} = (function() {
|
|
96
|
+
const __b = ${itemVarName} && ${itemVarName}.bounds;
|
|
97
|
+
const __gb = __b && isFinite(__b.x) && isFinite(__b.y) && __b.width > 0 && __b.height > 0 ? __b : null;
|
|
98
|
+
return {
|
|
99
|
+
gradient: {
|
|
100
|
+
stops: [${stopsCode}],
|
|
101
|
+
radial: true
|
|
102
|
+
},
|
|
103
|
+
origin: ${o},
|
|
104
|
+
destination: ${d}
|
|
105
|
+
};
|
|
106
|
+
})();`;
|
|
107
|
+
}
|
|
108
|
+
// Linear: a diagonal across a 100 px box at the requested position.
|
|
109
|
+
const defaultOrigin = origin || [position.x - 50, position.y - 50];
|
|
110
|
+
const defaultDestination = destination || [position.x + 50, position.y + 50];
|
|
83
111
|
return `
|
|
84
112
|
${itemVarName}.${propertyName} = {
|
|
85
113
|
gradient: {
|
|
86
114
|
stops: [${stopsCode}],
|
|
87
|
-
radial:
|
|
115
|
+
radial: false
|
|
88
116
|
},
|
|
89
117
|
origin: [${defaultOrigin[0]}, ${defaultOrigin[1]}],
|
|
90
118
|
destination: [${defaultDestination[0]}, ${defaultDestination[1]}]
|
|
@@ -172,6 +200,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
172
200
|
textDirection.resolved = baseDirection(createProps.content);
|
|
173
201
|
}
|
|
174
202
|
const textTabular = itemType === 'text' && wantsTabular(createProps) === true;
|
|
203
|
+
const createFillOpacity = createProps.fillOpacity !== undefined;
|
|
175
204
|
const textStrokePos = itemType === 'text' ? takeStrokePosition(createProps, false) : null;
|
|
176
205
|
const params = {
|
|
177
206
|
...(omitXY ? {} : { x: position.x, y: position.y }),
|
|
@@ -282,7 +311,8 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
282
311
|
const textOnly = (k) => TEXT_STYLE_KEYS.includes(k) || k === 'fit' || k === 'textDirection' || k === 'dir'
|
|
283
312
|
|| TABULAR_KEYS.includes(k) || k === 'strokePosition';
|
|
284
313
|
const ignored = Object.keys(properties).filter((k) => itemType === 'text'
|
|
285
|
-
? !CREATE_KNOWN_KEYS.has(k) && !textOnly(k) && !DIRECTION_KEYS.includes(k)
|
|
314
|
+
? (!CREATE_KNOWN_KEYS.has(k) && !textOnly(k) && !DIRECTION_KEYS.includes(k))
|
|
315
|
+
|| (k === 'letterSpacing' && letterSpacingCss(properties[k]) === undefined)
|
|
286
316
|
: !CREATE_KNOWN_KEYS.has(k) || textOnly(k));
|
|
287
317
|
// Build the code
|
|
288
318
|
let code = `
|
|
@@ -337,6 +367,8 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
|
|
|
337
367
|
code += emitDirectionCheck('item', textDirection);
|
|
338
368
|
if (textTabular)
|
|
339
369
|
code += emitTabularCheck('item');
|
|
370
|
+
if (createFillOpacity)
|
|
371
|
+
code += emitFillOpacityCheck('item');
|
|
340
372
|
if (textStrokePos)
|
|
341
373
|
code += emitStrokePositionCheck('item', textStrokePos);
|
|
342
374
|
if (textFit)
|
|
@@ -374,7 +406,7 @@ const itemId = item.data.registryId;
|
|
|
374
406
|
app.historyManager.saveState();
|
|
375
407
|
|
|
376
408
|
// Return item info
|
|
377
|
-
({ itemId, type: '${itemType}'${textFit ? ', textFit: __textFit' : ''}${textDirection ? ', direction: __direction' : ''}${textTabular ? ', tabularFigures: __tabular' : ''}${textStrokePos ? ', strokePosition: __strokePos' : ''}, position: ${omitXY ? `(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
|
|
409
|
+
({ itemId, type: '${itemType}'${textFit ? ', textFit: __textFit' : ''}${textDirection ? ', direction: __direction' : ''}${textTabular ? ', tabularFigures: __tabular' : ''}${createFillOpacity ? ', fillOpacity: __fillOpacity' : ''}${textStrokePos ? ', strokePosition: __strokePos' : ''}, position: ${omitXY ? `(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
|
|
378
410
|
? `, ignoredProperties: ${JSON.stringify(ignored)}, warning: ${JSON.stringify(`${ignored.join(', ')} ${ignored.length > 1 ? 'are' : 'is'} not read when creating an item, so ${ignored.length > 1 ? 'they had' : 'it had'} no effect. `
|
|
379
411
|
+ 'Check the spelling against this item type\'s documented properties.')}`
|
|
380
412
|
: ''} });`;
|
|
@@ -398,7 +430,7 @@ const MODIFY_KNOWN_KEYS = new Set([
|
|
|
398
430
|
...NORMALIZE_PARAM_READS,
|
|
399
431
|
'contentType', 'contentFormat', 'countdownTarget', 'countdownEndText',
|
|
400
432
|
'audioGain', 'volume', 'gain', 'bornAt', 'ttl', 'smoothing',
|
|
401
|
-
'fontStyle', 'leading', 'lineHeight', 'skewX', 'skewY', 'matrix', 'fit', 'direction', 'textDirection', 'dir', 'tabularFigures', 'fontVariantNumeric', 'fontFeatures', 'strokePosition',
|
|
433
|
+
'fontStyle', 'leading', 'lineHeight', 'skewX', 'skewY', 'matrix', 'fit', 'direction', 'textDirection', 'dir', 'tabularFigures', 'fontVariantNumeric', 'fontFeatures', 'strokePosition', 'fillOpacity',
|
|
402
434
|
]);
|
|
403
435
|
/** Spellings callers reach for, and the key the engine actually reads. */
|
|
404
436
|
const MODIFY_DID_YOU_MEAN = {
|
|
@@ -419,6 +451,9 @@ const CREATE_KNOWN_KEYS = new Set([
|
|
|
419
451
|
// Lifetimes, on ANY item: this generator applies them through app.setLifetime
|
|
420
452
|
// (emitLifetime), whatever create() itself reads.
|
|
421
453
|
'bornAt', 'ttl',
|
|
454
|
+
// A solid fill's own alpha (FxTool 12 STROKEFILL); confirmed per call by
|
|
455
|
+
// emitFillOpacityCheck, since an older studio ignores it.
|
|
456
|
+
'fillOpacity',
|
|
422
457
|
]);
|
|
423
458
|
/**
|
|
424
459
|
* Load a font family before text is drawn with it, and say if it still is not there.
|
|
@@ -566,15 +601,61 @@ function wantsTabular(p) {
|
|
|
566
601
|
const list = Array.isArray(f) ? f.map(String) : typeof f === 'string' ? [f] : f && typeof f === 'object' ? Object.keys(f).filter((k) => f[k]) : [];
|
|
567
602
|
return list.some((x) => /\btnum\b/.test(x));
|
|
568
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Whether tabular figures CHANGED the layout is the engine's to measure: the
|
|
606
|
+
* request flag (data.tabularFigures) is set even when the font's digits are
|
|
607
|
+
* already one width, where the layout moves nothing (9 TABNUM: DejaVu Sans
|
|
608
|
+
* reported applied while every advance matched the control). A studio with
|
|
609
|
+
* app.tabularFiguresReport measures the item's own font, and only its answer
|
|
610
|
+
* is reported as applied. One without it can only say the flag was recorded:
|
|
611
|
+
* 'unverified', never true.
|
|
612
|
+
*/
|
|
569
613
|
function emitTabularCheck(itemExpr) {
|
|
570
614
|
return `
|
|
571
615
|
const __tabular = (function(it) {
|
|
572
616
|
if (!it || it.className !== 'PointText') return { applied: false, note: 'tabular figures apply to text items only.' };
|
|
617
|
+
if (typeof app.tabularFiguresReport === 'function') {
|
|
618
|
+
let r = null;
|
|
619
|
+
try { r = app.tabularFiguresReport(it); } catch (e) { return { applied: 'unverified', note: 'the studio could not measure tabular figures on this text (' + ((e && e.message) || e) + ').' }; }
|
|
620
|
+
if (r && typeof r === 'object') {
|
|
621
|
+
const out = { applied: r.applied === true };
|
|
622
|
+
if (typeof r.steadyWidth === 'boolean') out.steadyWidth = r.steadyWidth;
|
|
623
|
+
if (r.reason) out.reason = r.reason;
|
|
624
|
+
if (r.note) out.note = r.note;
|
|
625
|
+
else if (r.reason === 'rtl') out.note = 'right-to-left text keeps proportional digits.';
|
|
626
|
+
else if (r.reason === 'not-requested') out.note = 'the studio did not record the request, so digits keep their proportional widths.';
|
|
627
|
+
if (typeof r.spread === 'number') out.digitSpread = r.spread;
|
|
628
|
+
if (r.width && typeof r.width === 'object') out.width = r.width;
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
return { applied: 'unverified', note: 'the studio returned no tabular-figures report for this text.' };
|
|
632
|
+
}
|
|
573
633
|
return it.data && it.data.tabularFigures === true
|
|
574
|
-
? { applied:
|
|
634
|
+
? { applied: 'unverified', note: 'this studio recorded tabular figures but cannot measure whether they change the layout (it has no tabularFiguresReport): with a font whose digits are already one width they change nothing.' }
|
|
575
635
|
: { applied: false, note: 'this studio does not lay out tabular figures (it needs an engine with them): digits keep their proportional widths. A monospaced font is the fallback.' };
|
|
576
636
|
})(${itemExpr});`;
|
|
577
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* fillOpacity (FxTool 12 STROKEFILL): a solid fill's own alpha, apart from the
|
|
640
|
+
* item's opacity and its stroke, so an outline-only type is fillOpacity: 0.
|
|
641
|
+
* The engine records it on data.fillOpacity and re-applies it when a later
|
|
642
|
+
* modify changes the fill colour. A studio without it ignores the key, so the
|
|
643
|
+
* reply is read from the item: the recorded value and the fill's alpha.
|
|
644
|
+
*/
|
|
645
|
+
function emitFillOpacityCheck(itemExpr) {
|
|
646
|
+
return `
|
|
647
|
+
const __fillOpacity = (function(it) {
|
|
648
|
+
if (!it) return { applied: false, note: 'no item to check.' };
|
|
649
|
+
const v = it.data && it.data.fillOpacity;
|
|
650
|
+
if (typeof v !== 'number') return { applied: false, note: 'this studio does not read fillOpacity (it needs an engine with it): the fill keeps its own alpha. opacity fades the whole item, stroke included; an rgba() fill sets the fill alone.' };
|
|
651
|
+
const c = it.fillColor;
|
|
652
|
+
if (!c) return { applied: true, value: v, note: 'the item has no fill now, so there is nothing to see; a fill colour set later with modify_item takes this alpha.' };
|
|
653
|
+
if (c.gradient) return { applied: false, value: v, note: 'a gradient fill keeps its own stop colours: put the alpha in the stops (rgba) instead.' };
|
|
654
|
+
return (typeof c.alpha === 'number' && Math.abs(c.alpha - v) > 0.01)
|
|
655
|
+
? { applied: false, value: v, note: 'the studio recorded fillOpacity but the fill is drawn at alpha ' + c.alpha + '.' }
|
|
656
|
+
: { applied: true, value: v };
|
|
657
|
+
})(${itemExpr});`;
|
|
658
|
+
}
|
|
578
659
|
function takeStrokePosition(props, allowNull) {
|
|
579
660
|
if (!('strokePosition' in props))
|
|
580
661
|
return null;
|
|
@@ -656,13 +737,26 @@ function authoringFrameJs(name) {
|
|
|
656
737
|
return (typeof app.exportFrameRect === 'function' && app.exportFrameRect()) || cs || { width: 1920, height: 1080 };
|
|
657
738
|
})();`;
|
|
658
739
|
}
|
|
659
|
-
const TEXT_STYLE_KEYS = ['fontWeight', 'fontStyle', 'leading', 'lineHeight', 'textAlign', 'align', 'text'];
|
|
740
|
+
const TEXT_STYLE_KEYS = ['fontWeight', 'fontStyle', 'leading', 'lineHeight', 'letterSpacing', 'textAlign', 'align', 'text'];
|
|
741
|
+
/**
|
|
742
|
+
* letterSpacing as the studio draws it, or undefined if it cannot be. create()
|
|
743
|
+
* and modifyItem() do not read it (B108); the studio draws the item's own
|
|
744
|
+
* letterSpacing, a CSS length, as its keyframes set it, and a number is px.
|
|
745
|
+
*/
|
|
746
|
+
function letterSpacingCss(v) {
|
|
747
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
748
|
+
return `${v}px`;
|
|
749
|
+
if (typeof v === 'string' && /^-?\d*\.?\d+(px|em)?$/.test(v.trim()))
|
|
750
|
+
return /\d$/.test(v.trim()) ? `${v.trim()}px` : v.trim();
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
660
753
|
function emitTextStyle(itemExpr, props) {
|
|
661
754
|
const weight = props.fontWeight;
|
|
662
755
|
const italic = props.fontStyle === 'italic' || props.fontStyle === 'oblique';
|
|
663
756
|
const lh = typeof props.lineHeight === 'number' ? props.lineHeight : undefined;
|
|
664
757
|
const leading = typeof props.leading === 'number' ? props.leading : undefined;
|
|
665
|
-
|
|
758
|
+
const ls = letterSpacingCss(props.letterSpacing);
|
|
759
|
+
if (weight === undefined && props.fontStyle === undefined && leading === undefined && lh === undefined && ls === undefined)
|
|
666
760
|
return '';
|
|
667
761
|
return `
|
|
668
762
|
(function(it) {
|
|
@@ -675,6 +769,7 @@ function emitTextStyle(itemExpr, props) {
|
|
|
675
769
|
: ''}
|
|
676
770
|
${leading !== undefined ? `it.leading = ${leading};` : ''}
|
|
677
771
|
${lh !== undefined ? `it.leading = ${lh} <= 4 ? ${lh} * it.fontSize : ${lh};` : ''}
|
|
772
|
+
${ls !== undefined ? `it.letterSpacing = ${JSON.stringify(ls)};` : ''}
|
|
678
773
|
const b1 = it.bounds;
|
|
679
774
|
it.position = it.position.add([ax - edge(b1), ay - b1.center.y]);
|
|
680
775
|
})(${itemExpr});`;
|
|
@@ -853,6 +948,7 @@ function generateModifyItemCode(itemId, properties, dataFlags) {
|
|
|
853
948
|
delete restProperties.fit;
|
|
854
949
|
const modDirection = takeDirection(restProperties);
|
|
855
950
|
const modTabular = wantsTabular(restProperties) === true;
|
|
951
|
+
const modFillOpacity = restProperties.fillOpacity !== undefined;
|
|
856
952
|
const modStrokePos = takeStrokePosition(restProperties, true);
|
|
857
953
|
// modifyItem(id, changes), NOT select() + modify().
|
|
858
954
|
//
|
|
@@ -980,6 +1076,8 @@ if (_flagged && _flagged.item && _flagged.item.data) Object.assign(_flagged.item
|
|
|
980
1076
|
// AN UNREAD KEY HAD NO EFFECT, AND THE CALL SAID SUCCESS. {scaling: 8} on a
|
|
981
1077
|
// raster answered success and left it 16 px; the engine reads `scale`. Same
|
|
982
1078
|
// report create_item gives, against the engine's modify path (generated).
|
|
1079
|
+
if (textStyle && letterSpacingCss(properties.letterSpacing) !== undefined)
|
|
1080
|
+
aliased.add('letterSpacing');
|
|
983
1081
|
const modIgnored = Object.keys(properties).filter((k) => !MODIFY_KNOWN_KEYS.has(k) && !aliased.has(k));
|
|
984
1082
|
const modWarning = modIgnored.length === 0 ? '' : `, ignoredProperties: ${JSON.stringify(modIgnored)}, warning: ${JSON.stringify(`${modIgnored.join(', ')} ${modIgnored.length > 1 ? 'are' : 'is'} not read when modifying an item, so ${modIgnored.length > 1 ? 'they had' : 'it had'} no effect.`
|
|
985
1083
|
+ modIgnored.filter((k) => MODIFY_DID_YOU_MEAN[k]).map((k) => ` Did you mean ${MODIFY_DID_YOU_MEAN[k]} (not ${k})?`).join(''))}`;
|
|
@@ -992,8 +1090,8 @@ ${properties.content !== undefined ? `
|
|
|
992
1090
|
const __ctItem = app.getItemById('${itemId}');
|
|
993
1091
|
const __ctKeys = (__ctItem && __ctItem.data && Array.isArray(__ctItem.data.keyframes))
|
|
994
1092
|
? __ctItem.data.keyframes.filter(function(k) { return k && k.properties && Object.prototype.hasOwnProperty.call(k.properties, 'content'); }).length : 0;` : ''}
|
|
995
|
-
${modDirection ? emitDirectionCheck(`app.getItemById('${itemId}')`, modDirection) : ''}${modTabular ? emitTabularCheck(`app.getItemById('${itemId}')`) : ''}${modStrokePos ? emitStrokePositionCheck(`app.getItemById('${itemId}')`, modStrokePos) : ''}
|
|
996
|
-
return { success: true, itemId: '${itemId}'${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' : ''}${modStrokePos ? ', strokePosition: __strokePos' : ''}${affine ? `, affine: ${JSON.stringify({ 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} };`;
|
|
1093
|
+
${modDirection ? emitDirectionCheck(`app.getItemById('${itemId}')`, modDirection) : ''}${modTabular ? emitTabularCheck(`app.getItemById('${itemId}')`) : ''}${modFillOpacity ? emitFillOpacityCheck(`app.getItemById('${itemId}')`) : ''}${modStrokePos ? emitStrokePositionCheck(`app.getItemById('${itemId}')`, modStrokePos) : ''}
|
|
1094
|
+
return { success: true, itemId: '${itemId}'${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: ${JSON.stringify({ 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} };`;
|
|
997
1095
|
// Wrapped, because the body now RETURNS early on a miss rather than falling
|
|
998
1096
|
// through to an unconditional success. Async only when a font must load
|
|
999
1097
|
// before modifyItem re-measures the text.
|
|
@@ -1035,6 +1133,15 @@ function generateDeleteItemCode(itemId) {
|
|
|
1035
1133
|
/**
|
|
1036
1134
|
* Template for adding relations
|
|
1037
1135
|
*/
|
|
1136
|
+
/**
|
|
1137
|
+
* A JS expression: the studio's own reason for refusing a relation of `typeJs`,
|
|
1138
|
+
* or null. addRelation answers a bare false; the WHY (B119: "relation
|
|
1139
|
+
* 'spring_follow' is not loaded yet…") is recorded with app.noteWarning, which
|
|
1140
|
+
* reaches the reply only as a governor warning. Read here without draining it.
|
|
1141
|
+
*/
|
|
1142
|
+
export function relationRefusalReasonJs(typeJs) {
|
|
1143
|
+
return `(function(){ var ws = (app && app._engineWarnings) || []; for (var i = ws.length - 1; i >= 0; i--) { var w = ws[i]; if (w && w.relation === ${typeJs} && typeof w.message === 'string') return w.message; } return null; })()`;
|
|
1144
|
+
}
|
|
1038
1145
|
function generateAddRelationCode(sourceId, targetId, relationType, params) {
|
|
1039
1146
|
// AN UNKNOWN ENDPOINT IS REFUSED, NOT STORED (round 9 GG, 1.74). The engine
|
|
1040
1147
|
// stores a relation to an id it cannot resolve, returns true, and says "it
|
|
@@ -1055,13 +1162,109 @@ function generateAddRelationCode(sourceId, targetId, relationType, params) {
|
|
|
1055
1162
|
}
|
|
1056
1163
|
const success = app.addRelation('${sourceId}', '${targetId}', '${relationType}', ${JSON.stringify(params, null, 2)});
|
|
1057
1164
|
if (!success) {
|
|
1058
|
-
|
|
1165
|
+
const __why = ${relationRefusalReasonJs(JSON.stringify(relationType))};
|
|
1166
|
+
return { success: false, relationType: '${relationType}', error: 'the studio refused the ${relationType} relation between ${sourceId} and ${targetId}' + (__why ? ': ' + __why : '.') };
|
|
1059
1167
|
}
|
|
1060
1168
|
app.historyManager.saveState();
|
|
1061
1169
|
return { success: true, sourceId: '${sourceId}', targetId: '${targetId}', relationType: '${relationType}' };
|
|
1062
1170
|
})();
|
|
1063
1171
|
`.trim();
|
|
1064
1172
|
}
|
|
1173
|
+
/**
|
|
1174
|
+
* The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
|
|
1175
|
+
* MAX_BURST_COUNT, B144). Studios before that change capped at
|
|
1176
|
+
* EMITTER_LEGACY_MAX_PER_BURST, in silence. Which one a studio has is read
|
|
1177
|
+
* back from the spec it stored (generateEmitter), not assumed.
|
|
1178
|
+
*/
|
|
1179
|
+
export const EMITTER_MAX_PER_BURST = 10000;
|
|
1180
|
+
/** The cap of studios before B144 (normalizeLook clamped at 2000 without a word). */
|
|
1181
|
+
export const EMITTER_LEGACY_MAX_PER_BURST = 2000;
|
|
1182
|
+
/**
|
|
1183
|
+
* Every count in an emitter spec (the spec's own and each burst's) that some
|
|
1184
|
+
* studio may cap: above the legacy cap. `index` is null for the spec's own
|
|
1185
|
+
* count (each burst without its own inherits it).
|
|
1186
|
+
*/
|
|
1187
|
+
export function emitterCountsToCheck(spec) {
|
|
1188
|
+
const out = [];
|
|
1189
|
+
if (!spec || typeof spec !== 'object')
|
|
1190
|
+
return out;
|
|
1191
|
+
const capped = (v) => typeof v === 'number' && Number.isFinite(v) && Math.round(v) > EMITTER_LEGACY_MAX_PER_BURST;
|
|
1192
|
+
if (capped(spec.count))
|
|
1193
|
+
out.push({ where: 'count', index: null, asked: Math.round(spec.count) });
|
|
1194
|
+
if (Array.isArray(spec.bursts)) {
|
|
1195
|
+
spec.bursts.forEach((b, i) => {
|
|
1196
|
+
const c = b && typeof b === 'object' ? b.count : undefined;
|
|
1197
|
+
if (capped(c))
|
|
1198
|
+
out.push({ where: `bursts[${i}].count`, index: i, asked: Math.round(c) });
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
return out;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* What can be said about the counts WITHOUT the studio's answer: above the
|
|
1205
|
+
* current cap is capped on every studio; between the two caps depends on the
|
|
1206
|
+
* studio. Used when the stored spec cannot be read back.
|
|
1207
|
+
*/
|
|
1208
|
+
export function emitterCountWarnings(spec) {
|
|
1209
|
+
return emitterCountsToCheck(spec).map(({ where, asked }) => asked > EMITTER_MAX_PER_BURST
|
|
1210
|
+
? `${where} ${asked} is over the ${EMITTER_MAX_PER_BURST}-particle cap per burst of the current studio (${EMITTER_LEGACY_MAX_PER_BURST} on older studios), so it draws at most ${EMITTER_MAX_PER_BURST}. Split it into bursts at the same t for more.`
|
|
1211
|
+
: `${where} ${asked} is over the ${EMITTER_LEGACY_MAX_PER_BURST}-particle cap per burst of studios before the ${EMITTER_MAX_PER_BURST} cap, and this studio did not report what it drew: on an older studio it draws ${EMITTER_LEGACY_MAX_PER_BURST}. Split it into bursts at the same t to be sure.`);
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* An emitter spec as the studio reads it, and what the reply says about it.
|
|
1215
|
+
*
|
|
1216
|
+
* The studio reads velocities and offsets as [x, y] pairs only and drops any
|
|
1217
|
+
* other entry, so velocities: [{x, y}] made particles with no velocity and no
|
|
1218
|
+
* word (VELOC23). Point objects are turned into pairs here. And a burst draws
|
|
1219
|
+
* `count` particles, the i-th taking velocities[i % length]: with fewer
|
|
1220
|
+
* velocities than particles the extras repeat a velocity and draw on top of an
|
|
1221
|
+
* earlier particle, and with more the extras go unused, both in silence
|
|
1222
|
+
* (VELOCMISMATCH23). That is said, not changed: cycling is the studio's rule.
|
|
1223
|
+
* `assumeDefaultCount` is false for a set, whose spec is only the keys changed.
|
|
1224
|
+
*/
|
|
1225
|
+
export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
1226
|
+
const warnings = [];
|
|
1227
|
+
if (!spec || typeof spec !== 'object')
|
|
1228
|
+
return { spec, warnings };
|
|
1229
|
+
const pairs = (v) => (Array.isArray(v)
|
|
1230
|
+
? v.map((p) => (p && typeof p === 'object' && !Array.isArray(p) && ('x' in p || 'y' in p)
|
|
1231
|
+
? [Number(p.x ?? 0), Number(p.y ?? 0)]
|
|
1232
|
+
: p))
|
|
1233
|
+
: v);
|
|
1234
|
+
const fix = (o) => {
|
|
1235
|
+
const out = { ...o };
|
|
1236
|
+
for (const k of ['velocities', 'offsets'])
|
|
1237
|
+
if (out[k] !== undefined)
|
|
1238
|
+
out[k] = pairs(out[k]);
|
|
1239
|
+
return out;
|
|
1240
|
+
};
|
|
1241
|
+
const out = fix(spec);
|
|
1242
|
+
if (Array.isArray(spec.bursts))
|
|
1243
|
+
out.bursts = spec.bursts.map((b) => (b && typeof b === 'object' ? fix(b) : b));
|
|
1244
|
+
// The studio's default count per burst (ParticleEmitter DEFAULT_LOOK).
|
|
1245
|
+
const DEFAULT_COUNT = 12;
|
|
1246
|
+
const mismatch = (where, count, velocities) => {
|
|
1247
|
+
if (!Array.isArray(velocities) || !velocities.length)
|
|
1248
|
+
return;
|
|
1249
|
+
const n = typeof count === 'number' && Number.isFinite(count) ? Math.round(count) : (assumeDefaultCount ? DEFAULT_COUNT : null);
|
|
1250
|
+
if (n === null || n === velocities.length)
|
|
1251
|
+
return;
|
|
1252
|
+
warnings.push(n > velocities.length
|
|
1253
|
+
? `${where}: count ${n} with ${velocities.length} velocities: particle i takes velocities[i % ${velocities.length}], so ${n - velocities.length} particles repeat an earlier particle's path and draw on top of it. Give one velocity per particle, or set count to ${velocities.length}.`
|
|
1254
|
+
: `${where}: count ${n} with ${velocities.length} velocities: only the first ${n} velocities are used. Set count to ${velocities.length} to draw them all.`);
|
|
1255
|
+
};
|
|
1256
|
+
// The spec's own pair once; a burst only where it sets either itself (it inherits the rest).
|
|
1257
|
+
const bursts = Array.isArray(out.bursts) ? out.bursts : [];
|
|
1258
|
+
const inherited = bursts.some((b) => !b || typeof b !== 'object' || (b.count === undefined && b.velocities === undefined));
|
|
1259
|
+
if (out.velocities !== undefined && (out.count !== undefined || inherited))
|
|
1260
|
+
mismatch('spec', out.count, out.velocities);
|
|
1261
|
+
bursts.forEach((b, i) => {
|
|
1262
|
+
const own = (b && typeof b === 'object' ? b : {});
|
|
1263
|
+
if (own.count !== undefined || own.velocities !== undefined)
|
|
1264
|
+
mismatch(`bursts[${i}]`, own.count ?? out.count, own.velocities ?? out.velocities);
|
|
1265
|
+
});
|
|
1266
|
+
return { spec: out, warnings };
|
|
1267
|
+
}
|
|
1065
1268
|
/**
|
|
1066
1269
|
* moves_along_path, from the parameters a caller actually writes.
|
|
1067
1270
|
*
|
|
@@ -1132,7 +1335,27 @@ function generateMovesAlongPathCode(sourceId, targetId, raw) {
|
|
|
1132
1335
|
if (len > 0) params.speed = len / (150 * ${duration});
|
|
1133
1336
|
if (params.closed === undefined) params.closed = false;
|
|
1134
1337
|
}` : ''}
|
|
1135
|
-
|
|
1338
|
+
// AN EQUATION ROUTE WITH A DURATION (queue item 22, ORBITDUR). There are no
|
|
1339
|
+
// points here to turn a duration into speed, so it was dropped and the curve
|
|
1340
|
+
// ran at the default speed. A studio whose rule times an equation (FxTool
|
|
1341
|
+
// claude/engine-core-queue: its equation param says duration times it) gets
|
|
1342
|
+
// duration, and delay on the same playhead clock instead of a window (a
|
|
1343
|
+
// window would start the timed ride part-way through). An older studio hands
|
|
1344
|
+
// a timed equation to the arm that cannot read one and loses the curve, so it
|
|
1345
|
+
// keeps today's untimed ride and is told.
|
|
1346
|
+
const __warnings = [];
|
|
1347
|
+
let __eqTimed = false;
|
|
1348
|
+
${hasDuration ? `if (params.equation && !hasPoints) {
|
|
1349
|
+
const eqDesc = rule && rule.params && rule.params.equation && rule.params.equation.description;
|
|
1350
|
+
if (typeof eqDesc === 'string' && /duration/.test(eqDesc)) {
|
|
1351
|
+
__eqTimed = true;
|
|
1352
|
+
params.duration = ${duration};
|
|
1353
|
+
${hasDelay ? `params.delay = ${delay};` : ''}
|
|
1354
|
+
} else {
|
|
1355
|
+
__warnings.push('duration ${duration} was not applied: this studio cannot time an equation route (a timed equation loses its curve there), so the route stays untimed at speed ' + (params.speed !== undefined ? params.speed : 1) + ' (1 = 150 px/s). Update the studio, or pass params.path as points.');
|
|
1356
|
+
}
|
|
1357
|
+
}` : ''}
|
|
1358
|
+
${hasDelay ? `if (!__eqTimed && !params.window) params.window = { start: ${delay} };` : ''}
|
|
1136
1359
|
// Before its window opens the relation does not run, so the item stays where
|
|
1137
1360
|
// it was left — the retest saw it parked at the path END. Put it at the start.
|
|
1138
1361
|
if (Array.isArray(params.path) && params.path.length) {
|
|
@@ -1145,7 +1368,8 @@ function generateMovesAlongPathCode(sourceId, targetId, raw) {
|
|
|
1145
1368
|
app.historyManager.saveState();
|
|
1146
1369
|
return { success: true, sourceId: ${JSON.stringify(sourceId)}, relationType: 'moves_along_path', route: 'sampled-points',
|
|
1147
1370
|
points: Array.isArray(params.path) ? params.path.length : 0, speed: params.speed, closed: params.closed,
|
|
1148
|
-
${ref ? `pathFrom: ${JSON.stringify(ref)},` : ''} window: params.window
|
|
1371
|
+
${ref ? `pathFrom: ${JSON.stringify(ref)},` : ''} window: params.window,
|
|
1372
|
+
...(__eqTimed ? { duration: params.duration } : {}), ...(__warnings.length ? { warnings: __warnings } : {}) };
|
|
1149
1373
|
})();`.trim();
|
|
1150
1374
|
}
|
|
1151
1375
|
/**
|
|
@@ -1327,7 +1551,35 @@ if (!_target) {
|
|
|
1327
1551
|
return { success: false, error: ${JSON.stringify(itemId)} + ' is not on the canvas, so ' + ${JSON.stringify(what)} + ' would have done nothing. Check the id with pinepaper_get_items — create returns the registry id, not the Paper id.' };
|
|
1328
1552
|
}`.trim();
|
|
1329
1553
|
}
|
|
1330
|
-
|
|
1554
|
+
/**
|
|
1555
|
+
* Text keys a studio may or may not animate (queue item 2, FxTool
|
|
1556
|
+
* claude/engine-core-queue): fontStyle, textAlign, lineHeight and letterSpacing
|
|
1557
|
+
* apply as keyframes on the current studio, which lists them (textAlign as
|
|
1558
|
+
* justification, lineHeight as leading) in listAnimatableProperties. An older
|
|
1559
|
+
* studio stores the keys and draws nothing, so the check runs IN THE PAGE
|
|
1560
|
+
* against that list, and only the keys it does not name are reported.
|
|
1561
|
+
*/
|
|
1562
|
+
const TEXT_KEY_ENGINE_NAMES = {
|
|
1563
|
+
fontStyle: ['fontStyle'],
|
|
1564
|
+
textAlign: ['textAlign', 'justification'],
|
|
1565
|
+
lineHeight: ['lineHeight', 'leading'],
|
|
1566
|
+
letterSpacing: ['letterSpacing'],
|
|
1567
|
+
};
|
|
1568
|
+
/** A JS expression (run in the page) → the keys of `keys` this studio does not animate. '' when none are text keys. */
|
|
1569
|
+
function unanimatedTextKeysJs(keys) {
|
|
1570
|
+
const spec = [...new Set(keys)].filter((k) => k in TEXT_KEY_ENGINE_NAMES).map((k) => ({ key: k, names: TEXT_KEY_ENGINE_NAMES[k] }));
|
|
1571
|
+
if (!spec.length)
|
|
1572
|
+
return '';
|
|
1573
|
+
return `(function() {
|
|
1574
|
+
let __l = null;
|
|
1575
|
+
try { __l = typeof app.listAnimatableProperties === 'function' ? app.listAnimatableProperties() : null; } catch (_) { __l = null; }
|
|
1576
|
+
const __names = ((__l && __l.properties) || []).map(function(p) { return p && p.name; });
|
|
1577
|
+
return ${JSON.stringify(spec)}.filter(function(s) { return !s.names.some(function(n) { return __names.indexOf(n) >= 0; }); }).map(function(s) { return s.key; });
|
|
1578
|
+
})()`;
|
|
1579
|
+
}
|
|
1580
|
+
/** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
|
|
1581
|
+
const UNANIMATED_TEXT_KEYS_SENTENCE = `' is not an animatable property in this studio (app.listAnimatableProperties does not list it), so it had no effect. The current studio animates fontStyle, textAlign, lineHeight and letterSpacing; here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX.'`;
|
|
1582
|
+
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
|
|
1331
1583
|
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
1332
1584
|
const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
|
|
1333
1585
|
const opts = { duration: calculatedDuration, loop };
|
|
@@ -1343,6 +1595,11 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1343
1595
|
// 2475333b). An engine without merge ignores the option and replaces.
|
|
1344
1596
|
if (append)
|
|
1345
1597
|
opts.mode = 'merge';
|
|
1598
|
+
// EASEPLACE (queue item 5): which side of a key its easing shapes. The
|
|
1599
|
+
// current studio reads (and removes) the option; an older one leaves it on
|
|
1600
|
+
// the options object, which is how the result can say it was not applied.
|
|
1601
|
+
if (easingAt !== undefined)
|
|
1602
|
+
opts.easingAt = easingAt;
|
|
1346
1603
|
// A FADE ON AN AUDIO ITEM WAS ACCEPTED AND NEVER HEARD. addAnimation keyframes
|
|
1347
1604
|
// the audio item's sentinel, and the mix reads one static audioGain per clip,
|
|
1348
1605
|
// so {audioGain: 0→1} answered success, duration 15, and exported at a flat
|
|
@@ -1394,12 +1651,16 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1394
1651
|
const loopNote = loop
|
|
1395
1652
|
? `, loopNote: ${JSON.stringify('loop:true repeats in the editor, but has been measured playing ONCE in an exported video and then holding the last keyframe. For an export longer than one cycle, check a frame in the second cycle — if it holds, write the keyframes out per cycle, or use a loop preset (pinepaper_animate), which does cycle in export.')}`
|
|
1396
1653
|
: '';
|
|
1397
|
-
const easingNote = first?.easing && first.easing !== 'linear'
|
|
1398
|
-
? `, note: ${JSON.stringify(`easing '${first.easing}' is on the first keyframe, where it has no effect: easing shapes the segment ARRIVING at a keyframe. Put it on the keyframe you are moving to.`)}`
|
|
1654
|
+
const easingNote = easingAt !== 'departure' && first?.easing && first.easing !== 'linear'
|
|
1655
|
+
? `, note: ${JSON.stringify(`easing '${first.easing}' is on the first keyframe, where it has no effect by default: easing shapes the segment ARRIVING at a keyframe. Put it on the keyframe you are moving to, or pass easingAt: 'departure' to read every easing as the move LEAVING its key (current studio).`)}`
|
|
1399
1656
|
: '';
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1657
|
+
const textKeysJs = unanimatedTextKeysJs(keyframes.flatMap((k) => Object.keys(k.properties ?? {})));
|
|
1658
|
+
const textKeyCheck = textKeysJs ? `
|
|
1659
|
+
const __ignored = ${textKeysJs};
|
|
1660
|
+
if (__ignored.length) __warn.push(__ignored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE});` : '';
|
|
1661
|
+
const easingAtCheck = easingAt !== undefined ? `
|
|
1662
|
+
if (__opts && Object.prototype.hasOwnProperty.call(__opts, 'easingAt')) __warn.push('easingAt ' + ${JSON.stringify(JSON.stringify(easingAt))} + ' was not applied: this studio predates it, so every easing was read as the move ARRIVING at its key. Put each easing on the key the move goes to instead.');` : '';
|
|
1663
|
+
const runtimeWarning = `${textKeysJs ? ', ...(__ignored.length ? { ignoredProperties: __ignored } : {})' : ''}, ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
|
|
1403
1664
|
const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `'${itemId}'`;
|
|
1404
1665
|
return `
|
|
1405
1666
|
// Apply keyframe animation to ${itemId}
|
|
@@ -1410,7 +1671,9 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1410
1671
|
// merge support — an older engine takes the option and replaces anyway.
|
|
1411
1672
|
const __ki = _target && (_target.item || _target);
|
|
1412
1673
|
const __prev = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];
|
|
1413
|
-
|
|
1674
|
+
const __opts = ${JSON.stringify(opts)};
|
|
1675
|
+
app.addAnimation(${addAnimTarget}, ${keyframesJson}, __opts);
|
|
1676
|
+
const __warn = [];${easingAtCheck}${textKeyCheck}
|
|
1414
1677
|
const __now = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];
|
|
1415
1678
|
const __kept = __prev.every(function(t) { return __now.indexOf(t) >= 0; });${append ? `
|
|
1416
1679
|
// Property gaps across the merged track (the same check a single call makes
|
|
@@ -1423,7 +1686,7 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1423
1686
|
? { appended: true, previousKeys: __prev.length, keys: __now.length }
|
|
1424
1687
|
: { appended: false, replaced: true, previousKeys: __prev.length, warning: 'append was asked for, but this studio replaced the existing track (' + __prev.length + ' key(s)) instead of adding to it: it has no merge mode. Put every key in one keyframe_animate call.' })`
|
|
1425
1688
|
: `{ replaced: true, previousKeys: __prev.length, note: 'the item already had a keyframe track (' + __prev.length + ' key(s)); this call replaced it. To add keys to it instead, pass append: true.' }`};
|
|
1426
|
-
return { success: true, itemId: ${addAnimTarget}, duration: ${calculatedDuration}, loop: ${loop}${append ? ', append: true' : ''}, ...(__track ? { track: __track } : {})${append ? `, ...(__gaps.length ? { propertyGaps: __gaps.slice(0, 10), gapWarning: __gaps.length + ' key(s) of the merged track leave out a property other keys animate (' + __gaps.reduce(function(a, g) { g.missing.forEach(function(p) { if (a.indexOf(p) < 0) a.push(p); }); return a; }, []).join(', ') + '). The engine can break that property at such a key; give each key the full set — hold a value by repeating it.' } : {})` : ''}${timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : ''}${clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : ''}${clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : ''}${easingNote}${loopNote}${gapNote}${
|
|
1689
|
+
return { success: true, itemId: ${addAnimTarget}, duration: ${calculatedDuration}, loop: ${loop}${append ? ', append: true' : ''}, ...(__track ? { track: __track } : {})${append ? `, ...(__gaps.length ? { propertyGaps: __gaps.slice(0, 10), gapWarning: __gaps.length + ' key(s) of the merged track leave out a property other keys animate (' + __gaps.reduce(function(a, g) { g.missing.forEach(function(p) { if (a.indexOf(p) < 0) a.push(p); }); return a; }, []).join(', ') + '). The engine can break that property at such a key; give each key the full set — hold a value by repeating it.' } : {})` : ''}${timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : ''}${clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : ''}${clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : ''}${easingNote}${loopNote}${gapNote}${runtimeWarning} };
|
|
1427
1690
|
})();
|
|
1428
1691
|
`.trim();
|
|
1429
1692
|
}
|
|
@@ -1855,9 +2118,11 @@ const remaining = app.itemRegistry ? app.itemRegistry.getAll().length : 0;
|
|
|
1855
2118
|
*/
|
|
1856
2119
|
function generateExportSVGCode() {
|
|
1857
2120
|
return `
|
|
1858
|
-
// Export animated SVG
|
|
2121
|
+
// Export animated SVG. An export that produced nothing is a failure, named, not a success with an empty string.
|
|
1859
2122
|
const svgString = app.exportAnimatedSVG();
|
|
1860
|
-
(
|
|
2123
|
+
(typeof svgString !== 'string' || svgString.length === 0)
|
|
2124
|
+
? ({ success: false, step: 'export', error: 'export failed at the export step: exportAnimatedSVG returned ' + (svgString === '' ? 'an empty string' : (svgString === null ? 'null' : typeof svgString)) + ', so there is no SVG to return' })
|
|
2125
|
+
: ({ success: true, svgString, hasAnimations: svgString.includes('<animate') });
|
|
1861
2126
|
`.trim();
|
|
1862
2127
|
}
|
|
1863
2128
|
/**
|
|
@@ -2185,6 +2450,15 @@ ${itemSettersApplyJs(s, '__sid', '__res')}
|
|
|
2185
2450
|
return __res;
|
|
2186
2451
|
})();`;
|
|
2187
2452
|
}
|
|
2453
|
+
/**
|
|
2454
|
+
* A batch op's item reference as a JS expression: `$N` is the Nth item this
|
|
2455
|
+
* batch created, anything else is an id, quoted. Only digits follow the `$` —
|
|
2456
|
+
* the raw splice it replaces put `$1);…;(` straight into the snippet, and an id
|
|
2457
|
+
* with a quote in it broke the batch's syntax.
|
|
2458
|
+
*/
|
|
2459
|
+
export function batchRef(raw) {
|
|
2460
|
+
return typeof raw === 'string' && /^\$\d+$/.test(raw) ? `__ref(${raw.slice(1)})` : JSON.stringify(String(raw));
|
|
2461
|
+
}
|
|
2188
2462
|
function asReturningBody(snippet) {
|
|
2189
2463
|
const single = snippet.trim();
|
|
2190
2464
|
if (single.startsWith('(async function()'))
|
|
@@ -2730,7 +3004,9 @@ export class PinePaperCodeGenerator {
|
|
|
2730
3004
|
// silence; refuse before anything is written. setStepTiming ships in the
|
|
2731
3005
|
// same change, so it is the probe.
|
|
2732
3006
|
const holds = validated.keyframes.some((k) => k.interpolation === 'hold' || k.hold === true);
|
|
2733
|
-
const code = generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint, validated.timeUnits,
|
|
3007
|
+
const code = generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint, validated.timeUnits,
|
|
3008
|
+
// mode: 'merge' is the studio's name for append (B147).
|
|
3009
|
+
validated.append === true || validated.mode === 'merge', validated.easingAt);
|
|
2734
3010
|
if (!holds)
|
|
2735
3011
|
return code;
|
|
2736
3012
|
return `(async function() {
|
|
@@ -3370,8 +3646,15 @@ if (typeof _gen === 'function') {
|
|
|
3370
3646
|
const targetId = nameToId['${target}'];
|
|
3371
3647
|
if (sourceId && targetId) {
|
|
3372
3648
|
const params = ${paramsStr};
|
|
3373
|
-
|
|
3374
|
-
|
|
3649
|
+
// A refused relation is not listed as made (owner rule 1).
|
|
3650
|
+
if (app.addRelation(sourceId, targetId, '${type}', params)) {
|
|
3651
|
+
results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
|
|
3652
|
+
} else {
|
|
3653
|
+
const __why = ${relationRefusalReasonJs(JSON.stringify(type))};
|
|
3654
|
+
(results.relationsRefused = results.relationsRefused || []).push({ source: '${source}', target: '${target}', type: '${type}', reason: __why || 'the studio refused it' });
|
|
3655
|
+
results.success = false;
|
|
3656
|
+
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.';
|
|
3657
|
+
}
|
|
3375
3658
|
}
|
|
3376
3659
|
})();
|
|
3377
3660
|
`);
|
|
@@ -4067,11 +4350,16 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4067
4350
|
*/
|
|
4068
4351
|
generateAgentBatchExecute(input) {
|
|
4069
4352
|
const validated = AgentBatchExecuteInputSchema.parse(input);
|
|
4070
|
-
const {
|
|
4353
|
+
const { atomic } = validated;
|
|
4354
|
+
// ring / grid / orbit / stagger / patch become the batch's own ops (batch-macros.ts).
|
|
4355
|
+
const operations = expandBatchMacros(validated.operations);
|
|
4071
4356
|
const isAtomic = atomic !== false;
|
|
4072
4357
|
// Fold duplicate keyframe_animate targets so a later op can't silently
|
|
4073
4358
|
// clobber an earlier track (app.addAnimation replaces, never merges).
|
|
4074
4359
|
const mergePlan = planKeyframeMerges(operations);
|
|
4360
|
+
// Timeline ops compile here, on the server: the handler refuses a batch whose timeline does not compile before
|
|
4361
|
+
// anything runs; a direct caller gets the op's own failure instead.
|
|
4362
|
+
const timelines = compileBatchTimelines(operations);
|
|
4075
4363
|
let code = `
|
|
4076
4364
|
// Batch execute ${operations.length} operations
|
|
4077
4365
|
(async function() {
|
|
@@ -4100,11 +4388,32 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4100
4388
|
};
|
|
4101
4389
|
const results = [];
|
|
4102
4390
|
let success = true;
|
|
4391
|
+
// A failure's words, never "undefined" for a thrown non-Error.
|
|
4392
|
+
const _why = (e, fb) => (e && typeof e.message === 'string' && e.message.trim()) ? e.message
|
|
4393
|
+
: (typeof e === 'string' && e.trim()) ? e : fb;
|
|
4394
|
+
// THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything
|
|
4395
|
+
// failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the
|
|
4396
|
+
// result. A studio without lintScene says nothing.
|
|
4397
|
+
const __ppLintNow = function () {
|
|
4398
|
+
try {
|
|
4399
|
+
if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];
|
|
4400
|
+
const r = app.lintScene();
|
|
4401
|
+
return r && Array.isArray(r.warnings) ? r.warnings : [];
|
|
4402
|
+
} catch (e) {
|
|
4403
|
+
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
4404
|
+
}
|
|
4405
|
+
};
|
|
4406
|
+
// Every failed op as { step, itemId, reason }, beside the per-op results.
|
|
4407
|
+
const _failures = () => results.filter((r) => !r.success).map((r) => ({ step: r.step, itemId: r.itemId, reason: r.reason }));
|
|
4103
4408
|
|
|
4104
4409
|
try {
|
|
4105
4410
|
`;
|
|
4106
4411
|
operations.forEach((op, index) => {
|
|
4107
|
-
const opCode =
|
|
4412
|
+
const opCode = op.type === 'timeline'
|
|
4413
|
+
? (timelines.plans.has(index)
|
|
4414
|
+
? emitTimelineOpBody(timelines.plans.get(index), (t) => (t.startsWith('$') ? `__ref(${t.substring(1)})` : JSON.stringify(t)))
|
|
4415
|
+
: `return { success: false, error: ${JSON.stringify(timelines.errors.filter((e) => e.path.startsWith(`operations[${index}]`)).map((e) => `${e.path}: ${e.message}`).join('; ') || 'the timeline did not compile')} };`)
|
|
4416
|
+
: this.generateBatchOperationCode(op, index, mergePlan);
|
|
4108
4417
|
// $N variable references are documented as "items CREATED in earlier
|
|
4109
4418
|
// operations" — only `create` results may extend the itemIds array.
|
|
4110
4419
|
// animate/keyframe_animate/modify/delete also return { itemId } (the
|
|
@@ -4115,6 +4424,10 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4115
4424
|
// vehicles, 2026-07-30). `group` already follows this rule by
|
|
4116
4425
|
// returning groupId instead of itemId.
|
|
4117
4426
|
const claimsItemSlot = op.type === 'create';
|
|
4427
|
+
// The item this op acts on, for a failure's itemId: its own id field, else what its result names.
|
|
4428
|
+
const o = op;
|
|
4429
|
+
const opItem = [o.itemId, o.targetId, o.sourceId, o.id].find((v) => typeof v === 'string') ?? null;
|
|
4430
|
+
const opStep = typeof o._step === 'string' ? `op ${index}: ${o._step}` : `op ${index} (${op.type})`;
|
|
4118
4431
|
code += `
|
|
4119
4432
|
// Operation ${index}: ${op.type}
|
|
4120
4433
|
try {
|
|
@@ -4131,7 +4444,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4131
4444
|
if (failed${index}) {
|
|
4132
4445
|
// ATOMIC: throw WITHOUT recording, and let the catch below record it
|
|
4133
4446
|
// once. Doing both put two entries in for one failed op.
|
|
4134
|
-
${isAtomic ? `
|
|
4447
|
+
${isAtomic ? `const __e${index} = new Error(failed${index}); __e${index}.itemId = (result${index} && typeof result${index}.itemId === 'string') ? result${index}.itemId : ${JSON.stringify(opItem)}; throw __e${index};` : `results.push({ index: ${index}, success: false, step: ${JSON.stringify(opStep)}, itemId: (result${index} && typeof result${index}.itemId === 'string') ? result${index}.itemId : ${JSON.stringify(opItem)}, reason: failed${index}, error: failed${index}, result: result${index} });
|
|
4135
4448
|
success = false;`}
|
|
4136
4449
|
} else {
|
|
4137
4450
|
results.push({ index: ${index}, success: true, result: result${index} });
|
|
@@ -4140,7 +4453,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4140
4453
|
}` : ''}
|
|
4141
4454
|
}
|
|
4142
4455
|
} catch (opError) {
|
|
4143
|
-
|
|
4456
|
+
const __r${index} = _why(opError, 'operation ${index} (${op.type}) threw with no message');
|
|
4457
|
+
results.push({ index: ${index}, success: false, step: ${JSON.stringify(opStep)}, itemId: (opError && typeof opError.itemId === 'string') ? opError.itemId : ${JSON.stringify(opItem)}, reason: __r${index}, error: __r${index} });
|
|
4144
4458
|
${isAtomic ? 'throw opError;' : 'success = false;'}
|
|
4145
4459
|
}
|
|
4146
4460
|
`;
|
|
@@ -4160,15 +4474,19 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4160
4474
|
}
|
|
4161
4475
|
return { success, itemIds, results, operationCount: ${operations.length},
|
|
4162
4476
|
failedCount: _failed.length,
|
|
4163
|
-
...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; ') } : {}),
|
|
4164
|
-
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
4477
|
+
...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; '), failures: _failures() } : {}),
|
|
4478
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
4165
4479
|
} catch (e) {
|
|
4166
4480
|
// A batch that created nothing keeps the previous batch's ids on record.
|
|
4167
4481
|
if (typeof window !== 'undefined' && itemIds.length) {
|
|
4168
4482
|
window.__pinepaper_batch_item_ids = itemIds;
|
|
4169
4483
|
}
|
|
4170
4484
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
4171
|
-
|
|
4485
|
+
const __fails = _failures();
|
|
4486
|
+
const __reason = _why(e, 'the batch failed with no message');
|
|
4487
|
+
return { success: false, error: __reason, itemIds, results, operationCount: ${operations.length},
|
|
4488
|
+
failures: __fails.length ? __fails : [{ step: 'batch', itemId: null, reason: __reason }],
|
|
4489
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
4172
4490
|
}
|
|
4173
4491
|
})();
|
|
4174
4492
|
`;
|
|
@@ -4205,7 +4523,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4205
4523
|
'backgroundColor', 'generatorName', 'generatorParams', 'generatorRegion',
|
|
4206
4524
|
'width', 'height', 'preset', 'keyframes', 'duration', 'loop',
|
|
4207
4525
|
'maskPreset', 'maskType', 'maskOptions', 'effectType', 'effectParams',
|
|
4208
|
-
'action', 'time', 'append'
|
|
4526
|
+
'action', 'time', 'append', '_step'
|
|
4209
4527
|
]);
|
|
4210
4528
|
for (const [k, v] of Object.entries(anyOp)) {
|
|
4211
4529
|
if (!batchKeys.has(k) && v !== undefined && opProps[k] === undefined) {
|
|
@@ -4250,9 +4568,7 @@ return __created;
|
|
|
4250
4568
|
return createCode;
|
|
4251
4569
|
}
|
|
4252
4570
|
case 'modify': {
|
|
4253
|
-
const itemRef = op.itemId
|
|
4254
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4255
|
-
: `'${op.itemId}'`;
|
|
4571
|
+
const itemRef = batchRef(op.itemId);
|
|
4256
4572
|
// modify_item's own emitter, as create is create_item's (see above):
|
|
4257
4573
|
// the batch's modifyItem call missed text styles, font loading, audio
|
|
4258
4574
|
// level, no-fill, lifetimes, smoothing and the unread-property report.
|
|
@@ -4265,15 +4581,21 @@ return __created;
|
|
|
4265
4581
|
.replace(/;\s*$/, '');
|
|
4266
4582
|
return `
|
|
4267
4583
|
const __mid = ${itemRef};
|
|
4584
|
+
if (app.itemRegistry && app.itemRegistry.getItem && !app.itemRegistry.getItem(__mid) && typeof app.getItemsByName === 'function') {
|
|
4585
|
+
// A NAME HELD BY SEVERAL ITEMS is refused, not resolved: getItemById answers
|
|
4586
|
+
// the first holder, so a patch by name would change whichever came first.
|
|
4587
|
+
const __held = app.getItemsByName(__mid) || [];
|
|
4588
|
+
if (__held.length > 1) {
|
|
4589
|
+
return { success: false, itemId: __mid, error: __held.length + ' items are named ' + JSON.stringify(__mid) + ' (' + __held.map(function (h) { return (h && h.data && h.data.id) || '?'; }).join(', ') + '); pass one of their ids.' };
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4268
4592
|
const __r = await ${single};
|
|
4269
4593
|
if (__r && __r.success !== false) __r.modified = true;
|
|
4270
4594
|
return __r;
|
|
4271
4595
|
`;
|
|
4272
4596
|
}
|
|
4273
4597
|
case 'animate':
|
|
4274
|
-
const animItemRef = op.itemId
|
|
4275
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4276
|
-
: `'${op.itemId}'`;
|
|
4598
|
+
const animItemRef = batchRef(op.itemId);
|
|
4277
4599
|
const animOpts = JSON.stringify(op.animationOptions || {});
|
|
4278
4600
|
return `
|
|
4279
4601
|
const targetId = ${animItemRef};
|
|
@@ -4283,12 +4605,8 @@ app.animate(item, { animationType: '${op.animationType}', ...${animOpts} });
|
|
|
4283
4605
|
return { itemId: targetId, animationType: '${op.animationType}' };
|
|
4284
4606
|
`;
|
|
4285
4607
|
case 'relation':
|
|
4286
|
-
const srcRef = op.sourceId
|
|
4287
|
-
|
|
4288
|
-
: `'${op.sourceId}'`;
|
|
4289
|
-
const tgtRef = op.targetId?.startsWith('$')
|
|
4290
|
-
? `__ref(${op.targetId.substring(1)})`
|
|
4291
|
-
: `'${op.targetId}'`;
|
|
4608
|
+
const srcRef = batchRef(op.sourceId);
|
|
4609
|
+
const tgtRef = batchRef(op.targetId);
|
|
4292
4610
|
const relOpts = JSON.stringify(op.relationOptions || {});
|
|
4293
4611
|
return `
|
|
4294
4612
|
const sourceId = ${srcRef};
|
|
@@ -4298,12 +4616,19 @@ const target = app.getItemById(targetId);
|
|
|
4298
4616
|
if (!source) throw new Error('Source not found: ' + sourceId);
|
|
4299
4617
|
if (!target) throw new Error('Target not found: ' + targetId);
|
|
4300
4618
|
const relationId = app.addRelation(source, target, '${op.relationType}', ${relOpts});
|
|
4619
|
+
// NOT BOUND IS NOT A SUCCESS (owner rule 1, B119). addRelation answers false
|
|
4620
|
+
// when it refuses — spring_follow "not loaded yet" — and this op returned
|
|
4621
|
+
// { relationId: false } with nothing marking it failed, so the batch said
|
|
4622
|
+
// success over a relation that does not exist.
|
|
4623
|
+
if (!relationId) {
|
|
4624
|
+
const __why = ${relationRefusalReasonJs(JSON.stringify(op.relationType))};
|
|
4625
|
+
return { success: false, relationId: false, sourceId, targetId, relationType: ${JSON.stringify(op.relationType)},
|
|
4626
|
+
error: 'the studio refused the ' + ${JSON.stringify(op.relationType)} + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };
|
|
4627
|
+
}
|
|
4301
4628
|
return { relationId, sourceId, targetId, relationType: '${op.relationType}' };
|
|
4302
4629
|
`;
|
|
4303
4630
|
case 'delete':
|
|
4304
|
-
const delRef = op.itemId
|
|
4305
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4306
|
-
: `'${op.itemId}'`;
|
|
4631
|
+
const delRef = batchRef(op.itemId);
|
|
4307
4632
|
return `
|
|
4308
4633
|
const targetId = ${delRef};
|
|
4309
4634
|
const item = app.getItemById(targetId);
|
|
@@ -4314,7 +4639,7 @@ return { itemId: targetId, deleted: true };
|
|
|
4314
4639
|
`;
|
|
4315
4640
|
case 'group': {
|
|
4316
4641
|
// Resolve each ref ($N → itemIds[N]; literal → quoted id) into a JS array.
|
|
4317
|
-
const refs = (op.itemIds || []).map((ref) =>
|
|
4642
|
+
const refs = (op.itemIds || []).map((ref) => batchRef(ref));
|
|
4318
4643
|
const idsArrayExpr = `[${refs.join(', ')}]`;
|
|
4319
4644
|
const nameExpr = op.groupName !== undefined ? JSON.stringify(op.groupName) : 'undefined';
|
|
4320
4645
|
// Returns { groupId, ... } (no itemId) so it does NOT shift $N indices — like relation.
|
|
@@ -4326,8 +4651,8 @@ return (function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
|
|
|
4326
4651
|
case 'set_background':
|
|
4327
4652
|
const bgColor = op.backgroundColor || op.color || op.properties?.backgroundColor || op.properties?.color || '#000000';
|
|
4328
4653
|
return `
|
|
4329
|
-
app.setBackgroundColor(
|
|
4330
|
-
return { success: true, backgroundColor:
|
|
4654
|
+
app.setBackgroundColor(${JSON.stringify(String(bgColor))});
|
|
4655
|
+
return { success: true, backgroundColor: ${JSON.stringify(String(bgColor))} };
|
|
4331
4656
|
`;
|
|
4332
4657
|
case 'execute_generator':
|
|
4333
4658
|
const genName = op.generatorName || 'drawSunburst';
|
|
@@ -4358,14 +4683,12 @@ return { success: true, width: (_cs && _cs.width) || ${w}, height: (_cs && _cs.h
|
|
|
4358
4683
|
return `
|
|
4359
4684
|
// Keyframes merged into operation ${foldTarget} (same target). A keyframe track
|
|
4360
4685
|
// is replaced wholesale, so emitting both here would silently drop one.
|
|
4361
|
-
return { itemId: ${
|
|
4686
|
+
return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget} };
|
|
4362
4687
|
`;
|
|
4363
4688
|
}
|
|
4364
4689
|
const mergedPayload = mergePlan?.merged.get(index);
|
|
4365
4690
|
const rawItemId = String(op.itemId ?? op.id ?? op.properties?.itemId ?? op.properties?.id);
|
|
4366
|
-
const kfItemRef = rawItemId
|
|
4367
|
-
? `__ref(${rawItemId.substring(1)})`
|
|
4368
|
-
: `'${rawItemId}'`;
|
|
4691
|
+
const kfItemRef = batchRef(rawItemId);
|
|
4369
4692
|
const kfSource = mergedPayload
|
|
4370
4693
|
? mergedPayload.keyframes
|
|
4371
4694
|
: (op.keyframes || (op.properties && op.properties.keyframes) || (op.params && op.params.keyframes) || []);
|
|
@@ -4375,7 +4698,10 @@ return { itemId: ${op.itemId?.startsWith('$') ? `__ref(${op.itemId.substring(1)}
|
|
|
4375
4698
|
: (op.duration || op.properties?.duration || (kfSource.length ? Math.max(...kfSource.map((k) => Number(k.time) || 0)) : 5));
|
|
4376
4699
|
const kfLoop = mergedPayload ? mergedPayload.loop : (op.loop ?? op.properties?.loop ?? false);
|
|
4377
4700
|
// Same contract as pinepaper_keyframe_animate: replace unless append.
|
|
4378
|
-
|
|
4701
|
+
// mode: 'merge' is the studio's spelling (its keyframe_track_replaced
|
|
4702
|
+
// warning names it), so it means append here too (B147).
|
|
4703
|
+
const kfAppend = op.append === true || op.properties?.append === true
|
|
4704
|
+
|| op.mode === 'merge' || op.properties?.mode === 'merge';
|
|
4379
4705
|
// Batch ops are passthrough, so these arrive unvalidated: numbers only, and timeUnits from its enum.
|
|
4380
4706
|
const kfExtra = [];
|
|
4381
4707
|
for (const key of ['timeOffset', 'clipInPoint', 'clipOutPoint']) {
|
|
@@ -4387,9 +4713,9 @@ return { itemId: ${op.itemId?.startsWith('$') ? `__ref(${op.itemId.substring(1)}
|
|
|
4387
4713
|
if (kfUnits === 'seconds' || kfUnits === 'ms')
|
|
4388
4714
|
kfExtra.push(`timeUnits: '${kfUnits}'`);
|
|
4389
4715
|
const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
|
|
4390
|
-
const
|
|
4391
|
-
|
|
4392
|
-
const kfIgnored =
|
|
4716
|
+
const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
|
|
4717
|
+
const kfTextCheck = kfTextKeysJs ? `\nconst _kfIgnored = ${kfTextKeysJs};` : '';
|
|
4718
|
+
const kfIgnored = kfTextKeysJs ? `, ...(_kfIgnored.length ? { ignoredProperties: _kfIgnored, warning: _kfIgnored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE} } : {})` : '';
|
|
4393
4719
|
return `
|
|
4394
4720
|
const targetId = ${kfItemRef};
|
|
4395
4721
|
const _target = (app.getItemById && app.getItemById(targetId))
|
|
@@ -4399,14 +4725,12 @@ if (!_target) {
|
|
|
4399
4725
|
}
|
|
4400
4726
|
const _kfItem = _target.item || _target;
|
|
4401
4727
|
const _prevKeys = (_kfItem.data && Array.isArray(_kfItem.data.keyframes)) ? _kfItem.data.keyframes.length : 0;
|
|
4402
|
-
app.addAnimation(targetId, ${kfJson}, { duration: ${kfDuration}, loop: ${kfLoop}${kfAppend ? ", mode: 'merge'" : ''}${kfExtraStr} })
|
|
4728
|
+
app.addAnimation(targetId, ${kfJson}, { duration: ${kfDuration}, loop: ${kfLoop}${kfAppend ? ", mode: 'merge'" : ''}${kfExtraStr} });${kfTextCheck}
|
|
4403
4729
|
return { success: true, itemId: targetId, duration: ${kfDuration}, loop: ${kfLoop}${kfAppend ? ', append: true' : ''}${kfAppend ? '' : `, ...(_prevKeys ? { track: { replaced: true, previousKeys: _prevKeys, note: 'the item already had a keyframe track; this op replaced it. To add keys to it instead, pass append: true.' } } : {})`}${kfIgnored} };
|
|
4404
4730
|
`;
|
|
4405
4731
|
}
|
|
4406
4732
|
case 'apply_mask': {
|
|
4407
|
-
const maskItemRef = op.itemId
|
|
4408
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4409
|
-
: `'${op.itemId}'`;
|
|
4733
|
+
const maskItemRef = batchRef(op.itemId);
|
|
4410
4734
|
const maskOpts = JSON.stringify(op.maskOptions || {});
|
|
4411
4735
|
const preset = op.maskPreset || op.maskType || 'wipeLeft';
|
|
4412
4736
|
return `
|
|
@@ -4423,9 +4747,7 @@ return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
|
|
|
4423
4747
|
`;
|
|
4424
4748
|
}
|
|
4425
4749
|
case 'apply_effect': {
|
|
4426
|
-
const effItemRef = op.itemId
|
|
4427
|
-
? `__ref(${op.itemId.substring(1)})`
|
|
4428
|
-
: `'${op.itemId}'`;
|
|
4750
|
+
const effItemRef = batchRef(op.itemId);
|
|
4429
4751
|
const effParams = JSON.stringify(op.effectParams || {});
|
|
4430
4752
|
return `
|
|
4431
4753
|
const targetId = ${effItemRef};
|
|
@@ -4910,6 +5232,19 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4910
5232
|
// cannot be kept accurate — it breaks on any edit above it — so this repo
|
|
4911
5233
|
// does not use them. Camera animation still
|
|
4912
5234
|
// drives motion during export — framing only fixes the output frame.
|
|
5235
|
+
// THE SCENE LINT, BEFORE THE EXPORT (FxTool lintScene().warnings): what the file will show wrong without
|
|
5236
|
+
// anything failing — a centre-anchored text that runs out of the frame exports as "IGNAL" for "SIGNAL". Read from the
|
|
5237
|
+
// scene as it is, never a refusal; a studio without the rule says nothing, a lint that throws says so.
|
|
5238
|
+
const __lint = (function () {
|
|
5239
|
+
if (typeof app.lintScene !== 'function') return null;
|
|
5240
|
+
try {
|
|
5241
|
+
const r = app.lintScene();
|
|
5242
|
+
return r && Array.isArray(r.warnings) ? r.warnings : null;
|
|
5243
|
+
} catch (e) {
|
|
5244
|
+
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run before the export: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
5245
|
+
}
|
|
5246
|
+
})();
|
|
5247
|
+
|
|
4913
5248
|
let cameraDims = null;
|
|
4914
5249
|
if (framing === 'camera') {
|
|
4915
5250
|
if (!['gif', 'apng', 'mp4', 'webm'].includes(format)) {
|
|
@@ -5458,7 +5793,7 @@ ${stillTime !== undefined ? `
|
|
|
5458
5793
|
if (typeof fr.embedded === 'number') __tl.fonts.embedded = fr.embedded; // cumulative for the document
|
|
5459
5794
|
if (typeof fr.hiddenTextItems === 'number') __tl.fonts.hiddenTextItems = (__tl.fonts.hiddenTextItems || 0) + fr.hiddenTextItems;
|
|
5460
5795
|
}
|
|
5461
|
-
} catch (e) { if (!__tl.fonts) __tl.fonts = { embedded: 0, reason: e && e.message }; }
|
|
5796
|
+
} catch (e) { if (!__tl.fonts) __tl.fonts = { embedded: 0, reason: (e && typeof e.message === 'string' && e.message) || 'embedding the text fonts failed with no reason given' }; }
|
|
5462
5797
|
}
|
|
5463
5798
|
if (jpg) doc.addImage(jpg, 'JPEG', 0, 0, wMM, hMM); else doc.addImage(png, 'PNG', 0, 0, wMM, hMM);
|
|
5464
5799
|
if (__layer) {
|
|
@@ -5466,7 +5801,7 @@ ${stillTime !== undefined ? `
|
|
|
5466
5801
|
const tr = await app.exportEngine.addTextLayer(doc, { x: 0, y: 0, width: wMM, height: hMM });
|
|
5467
5802
|
if (tr && tr.ok !== false) { __tl.written += tr.linesWritten || 0; __tl.skipped += tr.linesSkipped || 0; }
|
|
5468
5803
|
else __tl.reasons.push(id + ': ' + ((tr && tr.reason) || 'refused'));
|
|
5469
|
-
} catch (e) { __tl.reasons.push(id + ': ' + e.message); }
|
|
5804
|
+
} catch (e) { __tl.reasons.push(id + ': ' + ((e && typeof e.message === 'string' && e.message) || 'the text layer failed with no reason given')); }
|
|
5470
5805
|
}
|
|
5471
5806
|
pagesOut.push({ sceneId: id, widthMM: Math.round(wMM * 10) / 10, heightMM: Math.round(hMM * 10) / 10 });
|
|
5472
5807
|
}
|
|
@@ -5529,7 +5864,7 @@ ${stillTime !== undefined ? `
|
|
|
5529
5864
|
result = { success: false, error: 'Unknown format: ' + format };
|
|
5530
5865
|
}
|
|
5531
5866
|
} catch (e) {
|
|
5532
|
-
result = { success: false, error: e.message, platform, format };
|
|
5867
|
+
result = { success: false, error: (e && typeof e.message === 'string' && e.message) || (typeof e === 'string' && e) || ('the ' + format + ' export threw with no message'), platform, format };
|
|
5533
5868
|
}
|
|
5534
5869
|
|
|
5535
5870
|
// One site, after every branch has set its result: a successful export says
|
|
@@ -5567,13 +5902,44 @@ ${stillTime !== undefined ? `
|
|
|
5567
5902
|
result = Object.assign({}, result, { format, data: encoded, mimeType: mime, size: Math.round(String(encoded).length * 0.75), encodeQuality: settings.compression });
|
|
5568
5903
|
}
|
|
5569
5904
|
} catch (e) {
|
|
5570
|
-
result = { success: false, platform, format, error: (e && e.message) ||
|
|
5905
|
+
result = { success: false, platform, format, error: (e && typeof e.message === 'string' && e.message) || (typeof e === 'string' && e) || ('re-encoding the still as ' + format + ' failed with no message') };
|
|
5571
5906
|
}
|
|
5572
5907
|
}
|
|
5573
5908
|
|
|
5574
5909
|
if (result && result.success) Object.assign(result, fidelity(format));
|
|
5910
|
+
if (result && result.success && __lint && __lint.length && !result.lintWarnings) {
|
|
5911
|
+
result.lintWarnings = __lint;
|
|
5912
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5913
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat(__lint.map(function (w) {
|
|
5914
|
+
return { code: String(w.code || 'lint'), message: String(w.message || w.code || 'scene lint warning'), ...(w.itemId ? { items: [w.itemId] } : {}) };
|
|
5915
|
+
}));
|
|
5916
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5917
|
+
}
|
|
5575
5918
|
if (result && result.success && __fit) result.platformFit = __fit;
|
|
5576
|
-
// A
|
|
5919
|
+
${usesCanvasSize ? '' : ` // A PRESET SIZE IS NOT THE CANVAS SIZE, AND THAT IS SAID (21 PLATFORMOVERRIDE).
|
|
5920
|
+
// A named platform renders the FILE at the preset's size; the canvas is not
|
|
5921
|
+
// resized, so the scene keeps the canvas's layout, scaled to fill the file.
|
|
5922
|
+
// A 640x360 canvas under youtube came back 1920x1080 with nothing said. The
|
|
5923
|
+
// size the route reports is compared with the canvas; the current studio's
|
|
5924
|
+
// video report warns about an upscale too, but as plain text this reply does
|
|
5925
|
+
// not pass on, so it is said here for every studio.
|
|
5926
|
+
// Not said for camera framing (the camera chooses that frame) or an
|
|
5927
|
+
// unbounded canvas (its stills crop to the artwork, said on their own), and
|
|
5928
|
+
// a scale the caller asked for is theirs, not the preset's.
|
|
5929
|
+
if (result && result.success && framing !== 'camera' && __cv && !__cv.unbounded && __cv.width > 0 && __cv.height > 0
|
|
5930
|
+
&& result.dimensions && typeof result.dimensions === 'object' && result.dimensions.width > 0 && result.dimensions.height > 0) {
|
|
5931
|
+
const __fw = result.dimensions.width, __fh = result.dimensions.height;
|
|
5932
|
+
const __us = ${scale !== undefined ? scale : 1};
|
|
5933
|
+
if (Math.abs(__fw - __cv.width * __us) > 2 || Math.abs(__fh - __cv.height * __us) > 2) {
|
|
5934
|
+
const __k = Math.round((__fw / __cv.width) * 1000) / 1000;
|
|
5935
|
+
result.platformSize = { preset: __presetBox.width + 'x' + __presetBox.height, canvas: __cv.width + 'x' + __cv.height, file: __fw + 'x' + __fh, scale: __k, canvasResized: false };
|
|
5936
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5937
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'platform_size_differs_from_canvas',
|
|
5938
|
+
message: 'the ' + platform + ' preset made the file ' + __fw + 'x' + __fh + ' while the canvas is ' + __cv.width + 'x' + __cv.height + ': the scene is drawn ' + (__k > 1 ? 'larger' : 'smaller') + ' (x' + __k + ') with the canvas\\'s layout, and the canvas itself is unchanged. For a file at the canvas size, export with platform: \\'auto\\'; for a scene laid out at ' + __fw + 'x' + __fh + ', resize the canvas (pinepaper_set_canvas_size) and re-lay it.' }]);
|
|
5939
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
`} // A LANDSCAPE CANVAS FOR A PORTRAIT PLATFORM (or the reverse) is not an
|
|
5577
5943
|
// aspect nuance but the wrong deliverable: tiktok on a 16:9 canvas came out
|
|
5578
5944
|
// 1920x1080 with only a note (round 10, 10.7). Said where a caller looks.
|
|
5579
5945
|
if (result && result.success && __fit) {
|
|
@@ -7480,14 +7846,39 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7480
7846
|
// An async IIFE is still a `(`-led expression statement, so the governor
|
|
7481
7847
|
// captures its value, and executeCode's eval fallback already resolves a
|
|
7482
7848
|
// returned promise. Both paths await it; neither needed changing.
|
|
7849
|
+
//
|
|
7850
|
+
// The code runs inside its own async arrow so its `return` is still its value; the value comes back under
|
|
7851
|
+
// __ppValue with the scene lint beside it, and a throw comes back as { step, itemId, reason }, never a bare
|
|
7852
|
+
// message (and never "undefined" for a thrown non-Error).
|
|
7483
7853
|
return `
|
|
7484
7854
|
// ${description}
|
|
7485
7855
|
(async function() {
|
|
7856
|
+
// THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything
|
|
7857
|
+
// failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the
|
|
7858
|
+
// result. A studio without lintScene says nothing.
|
|
7859
|
+
const __ppLintNow = function () {
|
|
7860
|
+
try {
|
|
7861
|
+
if (typeof app === 'undefined' || !app || typeof app.lintScene !== 'function') return [];
|
|
7862
|
+
const r = app.lintScene();
|
|
7863
|
+
return r && Array.isArray(r.warnings) ? r.warnings : [];
|
|
7864
|
+
} catch (e) {
|
|
7865
|
+
return [{ code: 'LINT_FAILED', severity: 'info', message: 'the scene lint could not run: ' + ((e && typeof e.message === 'string' && e.message) || 'no reason given') }];
|
|
7866
|
+
}
|
|
7867
|
+
};
|
|
7868
|
+
let __ppValue;
|
|
7486
7869
|
try {
|
|
7870
|
+
__ppValue = await (async () => {
|
|
7487
7871
|
${validated.code}
|
|
7872
|
+
})();
|
|
7488
7873
|
} catch (error) {
|
|
7489
|
-
|
|
7490
|
-
|
|
7874
|
+
const __ppReason = (error && typeof error.message === 'string' && error.message.trim()) ? error.message
|
|
7875
|
+
: (typeof error === 'string' && error.trim()) ? error : 'the custom code threw with no message';
|
|
7876
|
+
return { success: false, step: 'execute_custom_code', itemId: (error && typeof error.itemId === 'string') ? error.itemId : null,
|
|
7877
|
+
reason: __ppReason, error: __ppReason, ...((function () { const l = __ppLintNow(); return l.length ? { __ppLint: l } : {}; })()) };
|
|
7878
|
+
}
|
|
7879
|
+
// Only wrapped when the lint has something to say: otherwise the value is returned exactly as before.
|
|
7880
|
+
const __ppL = __ppLintNow();
|
|
7881
|
+
return __ppL.length ? { __ppValue: __ppValue, __ppLint: __ppL } : __ppValue;
|
|
7491
7882
|
})();
|
|
7492
7883
|
`.trim();
|
|
7493
7884
|
}
|
|
@@ -7818,7 +8209,17 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7818
8209
|
`.trim();
|
|
7819
8210
|
}
|
|
7820
8211
|
generateImportImage(input) {
|
|
7821
|
-
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize } = input;
|
|
8212
|
+
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset } = input;
|
|
8213
|
+
// IMPORTSHADOW: the shadow keys, applied through the engine's own modify
|
|
8214
|
+
// door once the image is placed (and masked) — the route modify_item takes.
|
|
8215
|
+
const shadow = {};
|
|
8216
|
+
if (shadowColor !== undefined)
|
|
8217
|
+
shadow.shadowColor = shadowColor;
|
|
8218
|
+
if (shadowBlur !== undefined)
|
|
8219
|
+
shadow.shadowBlur = shadowBlur;
|
|
8220
|
+
if (shadowOffset !== undefined)
|
|
8221
|
+
shadow.shadowOffset = shadowOffset;
|
|
8222
|
+
const hasShadow = Object.keys(shadow).length > 0;
|
|
7822
8223
|
// Each fragment used to be written with a LEADING comma, as if it followed
|
|
7823
8224
|
// an entry that no longer exists — so the first one opened the object with
|
|
7824
8225
|
// `{, position: …}` and the emitted code was a syntax error. Passing any
|
|
@@ -7916,6 +8317,16 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7916
8317
|
}
|
|
7917
8318
|
if (app.itemRegistry && typeof app.itemRegistry.rebind === 'function') app.itemRegistry.rebind(itemId, masked);
|
|
7918
8319
|
placed = masked;
|
|
8320
|
+
` : ''}${hasShadow ? ` // The shadow goes on what is DRAWN, after a mask has replaced the raster.
|
|
8321
|
+
let __shadow = null;
|
|
8322
|
+
{
|
|
8323
|
+
const __want = ${JSON.stringify(shadow)};
|
|
8324
|
+
let __ok = false;
|
|
8325
|
+
try { __ok = app.modifyItem(itemId, __want) !== false; } catch (_) { __ok = false; }
|
|
8326
|
+
__shadow = __ok
|
|
8327
|
+
? { applied: true, shadowColor: placed.shadowColor ? String(placed.shadowColor.toCSS ? placed.shadowColor.toCSS(true) : placed.shadowColor) : null, shadowBlur: placed.shadowBlur }
|
|
8328
|
+
: { applied: false, warning: 'the image was imported but its shadow was not applied — set it with pinepaper_modify_item on ' + itemId + '.' };
|
|
8329
|
+
}
|
|
7919
8330
|
` : ''} const b = placed.bounds;
|
|
7920
8331
|
if (!b || !(b.width > 0) || !(b.height > 0)) {
|
|
7921
8332
|
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.' };
|
|
@@ -7924,7 +8335,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7924
8335
|
success: true,
|
|
7925
8336
|
itemId: itemId,
|
|
7926
8337
|
message: 'Image imported and placed on canvas.',
|
|
7927
|
-
bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: '${mask}'` : ''},
|
|
8338
|
+
bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: '${mask}'` : ''}${hasShadow ? ',\n shadow: __shadow' : ''},
|
|
7928
8339
|
// Stored smaller than its source (the studio's cap): said, with the
|
|
7929
8340
|
// way to keep every pixel. The studio reports native size where it can.
|
|
7930
8341
|
...((entry && entry.nativeWidth && entry.width && entry.nativeWidth > entry.width)
|
|
@@ -8275,6 +8686,53 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
|
|
|
8275
8686
|
};
|
|
8276
8687
|
})();
|
|
8277
8688
|
`.trim();
|
|
8689
|
+
}
|
|
8690
|
+
/**
|
|
8691
|
+
* pinepaper_pattern (AR-2 P2): one stored declaration moves many items (FxTool app.addPattern / modifyPattern /
|
|
8692
|
+
* removePattern / listPatterns / getPattern). No keyframes are written; the engine evaluates the node per frame.
|
|
8693
|
+
*/
|
|
8694
|
+
generatePattern(input) {
|
|
8695
|
+
const v = PatternInputSchema.parse(input);
|
|
8696
|
+
const S = (x) => JSON.stringify(x);
|
|
8697
|
+
const need = (fn) => `if (typeof app.${fn} !== 'function') return { success: false, error: 'app.${fn} unavailable — this studio predates patterns (FxTool AR-2 P1); update it' };`;
|
|
8698
|
+
const idNeeded = v.action !== 'list' && !v.id ? `return { success: false, error: '${v.action} needs the pattern id' };` : '';
|
|
8699
|
+
let body;
|
|
8700
|
+
switch (v.action) {
|
|
8701
|
+
case 'create': {
|
|
8702
|
+
const spec = { id: v.id, kind: v.kind, members: v.members, params: v.params || {}, ...(v.window ? { window: v.window } : {}) };
|
|
8703
|
+
body = `${need('addPattern')}
|
|
8704
|
+
if (!${S(v.kind ?? null)}) return { success: false, error: 'create needs kind: phase_wave or stagger' };
|
|
8705
|
+
if (!${S(v.members ?? null)}) return { success: false, error: 'create needs members: { names: [...] }, { ids: [...] }, { tag } or an array' };
|
|
8706
|
+
const r = app.addPattern(${S(spec)});
|
|
8707
|
+
return Object.assign({ success: !!(r && r.ok) }, r || {}, app.lastReceipt ? { receipt: app.lastReceipt } : {});`;
|
|
8708
|
+
break;
|
|
8709
|
+
}
|
|
8710
|
+
case 'modify': {
|
|
8711
|
+
const ch = { ...(v.params ? { params: v.params } : {}), ...(v.members ? { members: v.members } : {}), ...(v.window !== undefined ? { window: v.window } : {}) };
|
|
8712
|
+
body = `${need('modifyPattern')}
|
|
8713
|
+
const r = app.modifyPattern(${S(v.id)}, ${S(ch)});
|
|
8714
|
+
return Object.assign({ success: !!(r && r.ok) }, r || {}, app.lastReceipt ? { receipt: app.lastReceipt } : {});`;
|
|
8715
|
+
break;
|
|
8716
|
+
}
|
|
8717
|
+
case 'remove':
|
|
8718
|
+
body = `${need('removePattern')}
|
|
8719
|
+
const had = app.removePattern(${S(v.id)});
|
|
8720
|
+
return had ? { success: true, removed: ${S(v.id)} } : { success: false, error: 'no pattern ' + ${S(JSON.stringify(v.id ?? ''))} };`;
|
|
8721
|
+
break;
|
|
8722
|
+
case 'get':
|
|
8723
|
+
body = `${need('getPattern')}
|
|
8724
|
+
const p = app.getPattern(${S(v.id)});
|
|
8725
|
+
return p ? Object.assign({ success: true }, p) : { success: false, error: 'no pattern ' + ${S(JSON.stringify(v.id ?? ''))} };`;
|
|
8726
|
+
break;
|
|
8727
|
+
default:
|
|
8728
|
+
body = `${need('listPatterns')}
|
|
8729
|
+
const list = app.listPatterns();
|
|
8730
|
+
return { success: true, count: list.length, patterns: list };`;
|
|
8731
|
+
}
|
|
8732
|
+
return `(function() {
|
|
8733
|
+
${idNeeded}
|
|
8734
|
+
${body}
|
|
8735
|
+
})();`;
|
|
8278
8736
|
}
|
|
8279
8737
|
generateStagger(input) {
|
|
8280
8738
|
const validated = StaggerInputSchema.parse(input);
|
|
@@ -10115,6 +10573,9 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10115
10573
|
(async function() {
|
|
10116
10574
|
if (!app.exportEngine || !app.exportEngine.exportWidget) return { error: 'Widget export not available' };
|
|
10117
10575
|
const result = await app.exportEngine.exportWidget(${JSON.stringify(opts)});
|
|
10576
|
+
if (!result || typeof result.json !== 'string' || result.json.length === 0) {
|
|
10577
|
+
return { success: false, step: 'export', error: 'export failed at the export step: exportWidget returned no scene JSON' + (result && result.error ? ' (' + result.error + ')' : '') };
|
|
10578
|
+
}
|
|
10118
10579
|
// THE EMBED BOX WAS ALWAYS 16:9, AND IT IS NOT SELF-CONTAINED (round 9 GG,
|
|
10119
10580
|
// 8.32). The engine writes aspect-ratio: 16/9 and max-width: 800px whatever
|
|
10120
10581
|
// the scene, so a 300x250 ad sat letterboxed in a wide box. The box is sized
|
|
@@ -10150,6 +10611,9 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
10150
10611
|
(async function() {
|
|
10151
10612
|
if (!app.exportEngine || !app.exportEngine.exportWidgetHTML) return { error: 'Widget HTML export not available' };
|
|
10152
10613
|
const result = await app.exportEngine.exportWidgetHTML(${JSON.stringify(opts)});
|
|
10614
|
+
if (!result || typeof result.html !== 'string' || result.html.length === 0) {
|
|
10615
|
+
return { success: false, step: 'export', error: 'export failed at the export step: exportWidgetHTML returned no page' + (result && result.error ? ' (' + result.error + ')' : '') };
|
|
10616
|
+
}
|
|
10153
10617
|
${input.lang || input.dir || input.alt ? `// lang / dir / alt on the engine's page (it writes <html lang="en"> and no
|
|
10154
10618
|
// text alternative). Applied as string edits on the one <html> tag and the
|
|
10155
10619
|
// first <canvas>; the result says which landed.
|
|
@@ -11043,7 +11507,9 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11043
11507
|
app.create(op.type, { id: op.id, x: op.x, y: op.y, width: op.width, height: op.height });
|
|
11044
11508
|
itemIds.push(op.id);
|
|
11045
11509
|
} else if (op.op === 'addRelation') {
|
|
11046
|
-
app.addRelation(op.from, op.to, op.relation, op.params || {})
|
|
11510
|
+
if (!app.addRelation(op.from, op.to, op.relation, op.params || {})) {
|
|
11511
|
+
errors.push({ op: op.op, id: op.from + '->' + op.to, error: 'the studio refused the ' + op.relation + ' relation' });
|
|
11512
|
+
}
|
|
11047
11513
|
}
|
|
11048
11514
|
} catch (e) {
|
|
11049
11515
|
errors.push({ op: op.op, id: op.id || (op.from + '->' + op.to), error: e && e.message });
|
|
@@ -11070,7 +11536,10 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11070
11536
|
return { success: false, error: 'app.lintScene unavailable — update FxTool to a build with the relational linter (S12-E2)' };
|
|
11071
11537
|
}
|
|
11072
11538
|
const result = app.lintScene(${optsJson});
|
|
11073
|
-
|
|
11539
|
+
// warnings: what the engine's own rules found (TEXT_CLIPS_FRAME: a centre-anchored text running past the
|
|
11540
|
+
// frame edge). Passed through when the studio reports them; an older studio has none to pass.
|
|
11541
|
+
return { success: true, density: result.density, suggestions: result.suggestions,
|
|
11542
|
+
...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}) };
|
|
11074
11543
|
})();`.trim();
|
|
11075
11544
|
}
|
|
11076
11545
|
/**
|
|
@@ -11592,9 +12061,41 @@ ${guard}
|
|
|
11592
12061
|
const __baked = (__it.data && typeof __it.data._bakedRotation === 'number') ? __it.data._bakedRotation : 0;
|
|
11593
12062
|
${authoringFrameJs('__fr')}
|
|
11594
12063
|
const __frame = { x: __fr.x || 0, y: __fr.y || 0, width: __fr.width, height: __fr.height };
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
12064
|
+
// THE ACTOR AT THE FIRST BEAT, not where the playhead left it (B154 /
|
|
12065
|
+
// HOOKCLOCK3). Live values of a keyframed item depend on the playhead, so a
|
|
12066
|
+
// second call anchored at +0 / +57 / +22 / +43 px from session to session
|
|
12067
|
+
// and multiplied scales the first track had applied. app.poseAt (FxTool,
|
|
12068
|
+
// claude/engine-core-queue) answers at a track time; older studios fall back
|
|
12069
|
+
// to the live read.
|
|
12070
|
+
const __at0 = ${S(Math.min(...input.beats.map((b) => Number(b && b.at) || 0)))};
|
|
12071
|
+
// A track that already keys the beat properties defines the pose before the
|
|
12072
|
+
// first beat; the B109 hold key at 0 would overwrite its key there. A track
|
|
12073
|
+
// of other properties (a colour) still needs the hold key.
|
|
12074
|
+
const __beatProps = ['x', 'y', 'scaleX', 'scaleY', 'scale', 'rotation', 'opacity', 'position'];
|
|
12075
|
+
const __hasTrack = !!(__it.data && Array.isArray(__it.data.keyframes) && __it.data.keyframes.some(function (k) {
|
|
12076
|
+
return Object.keys((k && k.properties) || {}).some(function (p) { return __beatProps.indexOf(p) >= 0; });
|
|
12077
|
+
}));
|
|
12078
|
+
let __pose = null;
|
|
12079
|
+
let __poseErr = null;
|
|
12080
|
+
if (typeof app.poseAt === 'function') {
|
|
12081
|
+
try { __pose = app.poseAt(__it, __at0); } catch (e) { __poseErr = (e && e.message) || String(e); }
|
|
12082
|
+
if (__pose && !(isFinite(__pose.x) && isFinite(__pose.y) && __pose.width > 0 && __pose.height > 0)) __pose = null;
|
|
12083
|
+
}
|
|
12084
|
+
const __anchorWarnings = [];
|
|
12085
|
+
if (!__pose && __hasTrack) {
|
|
12086
|
+
__anchorWarnings.push((__poseErr ? 'app.poseAt failed (' + __poseErr + ')' : 'this studio has no app.poseAt')
|
|
12087
|
+
+ ', so the beats start from the actor\\'s LIVE transform, which on a keyframed actor is wherever the playhead left it: the first move can jump. Update the studio, or seek to ' + __at0 + ' s before choreographing.');
|
|
12088
|
+
}
|
|
12089
|
+
if (!__pose && __it.className === 'Raster' && ${S(input.beats.some((b) => b && b.verb === 'roll'))}) {
|
|
12090
|
+
__anchorWarnings.push((__poseErr ? 'app.poseAt failed' : 'this studio has no app.poseAt')
|
|
12091
|
+
+ ', so the roll turns on the picture\\'s half-box, not its drawn radius: a picture with a transparent margin slides as it rolls.');
|
|
12092
|
+
}
|
|
12093
|
+
const __actor = __pose
|
|
12094
|
+
? { x: __pose.x, y: __pose.y, w: __pose.width, h: __pose.height, sx: __pose.scaleX || 1, sy: __pose.scaleY || 1, rot: isFinite(__pose.rotation) ? __pose.rotation : (__it.rotation || 0) + __baked, hasTrack: __hasTrack,
|
|
12095
|
+
rollRadius: __pose.drawnHeight ? __pose.drawnHeight / 2 : undefined }
|
|
12096
|
+
: { x: __it.position.x, y: __it.position.y,
|
|
12097
|
+
w: __b.width / (Math.abs(__sc.x) || 1), h: __b.height / (Math.abs(__sc.y) || 1),
|
|
12098
|
+
sx: __sc.x || 1, sy: __sc.y || 1, rot: (__it.rotation || 0) + __baked, hasTrack: __hasTrack };
|
|
11598
12099
|
const __r = (${compileChoreography.toString()})(__actor, __frame, ${S(input.beats)});
|
|
11599
12100
|
if (__r.error) { return { success: false, itemId: ${id}, error: __r.error }; }
|
|
11600
12101
|
const __prev = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes : [];
|
|
@@ -11603,7 +12104,7 @@ ${guard}
|
|
|
11603
12104
|
const __other = __prev.reduce(function (acc, k) { Object.keys((k && k.properties) || {}).forEach(function (p) { if (__ours.indexOf(p) < 0 && acc.indexOf(p) < 0) acc.push(p); }); return acc; }, []);
|
|
11604
12105
|
app.addAnimation(${id}, __r.keyframes, { duration: Math.max(__r.end, __prev.reduce(function (m, k) { return Math.max(m, (k && k.time) || 0); }, 0)), loop: false, mode: 'merge' });
|
|
11605
12106
|
const __now = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes.map(function (k) { return k && k.time; }) : [];
|
|
11606
|
-
const __warnings = (__r.warnings || [])
|
|
12107
|
+
const __warnings = __anchorWarnings.concat(__r.warnings || []);
|
|
11607
12108
|
if (__prevTimes.length && !__prevTimes.every(function (t) { return __now.indexOf(t) >= 0; })) {
|
|
11608
12109
|
__warnings.push('this studio replaced the existing track of the actor (' + __prevTimes.length + ' key(s)) instead of merging: put the other keys in after choreographing.');
|
|
11609
12110
|
}
|
|
@@ -11964,29 +12465,56 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
|
|
|
11964
12465
|
}
|
|
11965
12466
|
}
|
|
11966
12467
|
/** pinepaper_emitter (D29): create('emitter', spec) / setEmitter(id, patch). */
|
|
11967
|
-
generateEmitter(
|
|
12468
|
+
generateEmitter(raw) {
|
|
11968
12469
|
const S = (v) => JSON.stringify(v);
|
|
12470
|
+
const normalized = normalizeEmitterSpec(raw.spec, raw.action !== 'set');
|
|
12471
|
+
const input = { ...raw, spec: normalized.spec };
|
|
12472
|
+
// A COUNT OVER THE CAP IS SAID (B144). The cap is 10000 a burst on the
|
|
12473
|
+
// current studio and was 2000 before it, clamped in silence (2300 drew
|
|
12474
|
+
// ~1940). Which applies is read from the spec the studio STORED — its
|
|
12475
|
+
// normalised count is what it draws — so the reply names the number
|
|
12476
|
+
// actually drawn on whichever studio ran it. Only when nothing can be read
|
|
12477
|
+
// back does it fall back to what is known without the studio.
|
|
12478
|
+
const checks = emitterCountsToCheck(input.spec);
|
|
12479
|
+
const fallback = emitterCountWarnings(input.spec);
|
|
12480
|
+
const capFn = checks.length ? `
|
|
12481
|
+
function __capWarnings(stored) {
|
|
12482
|
+
const checks = ${S(checks)};
|
|
12483
|
+
if (!stored || typeof stored !== 'object') return ${S(fallback)};
|
|
12484
|
+
const out = [];
|
|
12485
|
+
for (const c of checks) {
|
|
12486
|
+
const b = c.index === null ? null : (Array.isArray(stored.bursts) ? stored.bursts[c.index] : null);
|
|
12487
|
+
const drawn = b && typeof b.count === 'number' ? b.count : stored.count;
|
|
12488
|
+
if (typeof drawn !== 'number') return ${S(fallback)};
|
|
12489
|
+
if (drawn < c.asked) out.push(c.where + ' ' + c.asked + ' is over this studio\\'s per-burst cap, so it draws ' + drawn + '. Split it into bursts at the same t for more.');
|
|
12490
|
+
}
|
|
12491
|
+
return out;
|
|
12492
|
+
}` : '';
|
|
12493
|
+
const fixed = normalized.warnings;
|
|
12494
|
+
const capJs = (stored) => checks.length || fixed.length
|
|
12495
|
+
? `, ...((function(w) { return w.length ? { warnings: w } : {}; })(${S(fixed)}.concat(${checks.length ? `__capWarnings(${stored})` : '[]'})))`
|
|
12496
|
+
: '';
|
|
11969
12497
|
if (input.action === 'set') {
|
|
11970
12498
|
return `
|
|
11971
12499
|
// Emitter: change a spec
|
|
11972
12500
|
(function() {
|
|
11973
|
-
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }
|
|
12501
|
+
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }${capFn}
|
|
11974
12502
|
const r = app.setEmitter(${S(input.itemId)}, ${S(input.spec)});
|
|
11975
12503
|
if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the emitter change was refused' }; }
|
|
11976
|
-
return { success: true, action: 'set', itemId: ${S(input.itemId)}, spec: r.spec };
|
|
12504
|
+
return { success: true, action: 'set', itemId: ${S(input.itemId)}, spec: r.spec${capJs('r.spec')} };
|
|
11977
12505
|
})();`.trim();
|
|
11978
12506
|
}
|
|
11979
12507
|
return `
|
|
11980
12508
|
// Emitter: create
|
|
11981
12509
|
(function() {
|
|
11982
|
-
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }
|
|
12510
|
+
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }${capFn}
|
|
11983
12511
|
const it = app.create('emitter', ${S(input.spec)});
|
|
11984
12512
|
// A bad spec comes back null with an emitter-invalid warning; the reason
|
|
11985
12513
|
// reaches this reply through the governor's warnings.
|
|
11986
12514
|
if (!it) { return { success: false, error: 'the emitter spec was refused' }; }
|
|
11987
12515
|
const id = it.data && (it.data.registryId || it.data.id);
|
|
11988
12516
|
if (!id) { return { success: false, error: 'the emitter was made but not registered — no usable id' }; }
|
|
11989
|
-
return { success: true, action: 'create', itemId: id };
|
|
12517
|
+
return { success: true, action: 'create', itemId: id${capJs('it.data && it.data.emitter')} };
|
|
11990
12518
|
})();`.trim();
|
|
11991
12519
|
}
|
|
11992
12520
|
/** pinepaper_render_hook (D29): drawing saved as source, re-installed on load. */
|
|
@@ -12982,7 +13510,7 @@ ${needWorld}
|
|
|
12982
13510
|
* already decided by a pure function, so nothing is left for the page to
|
|
12983
13511
|
* work out.
|
|
12984
13512
|
*/
|
|
12985
|
-
generateDesignCompose(scene, ops, style, sizeWarning) {
|
|
13513
|
+
generateDesignCompose(scene, ops, style, sizeWarning, report = {}) {
|
|
12986
13514
|
const S = (v) => JSON.stringify(v);
|
|
12987
13515
|
return `
|
|
12988
13516
|
// Design: compose a ${style} scene — ${ops.length} items, laid out server-side
|
|
@@ -13012,7 +13540,7 @@ ${needWorld}
|
|
|
13012
13540
|
// success is worse than a poster that says which four elements are missing.
|
|
13013
13541
|
return { success: failed.length === 0, style: ${S(style)}, itemIds: ids, itemCount: ids.length, failed: failed,
|
|
13014
13542
|
...(canvasResized ? { canvasResized: canvasResized, note: 'the canvas was resized to the composition' + (${S(sizeWarning ?? '')} ? '' : '; pass width and height to compose at your canvas size') } : {}),
|
|
13015
|
-
...(${S(sizeWarning ?? '')} ? { warning: ${S(sizeWarning ?? '')} } : {}) };
|
|
13543
|
+
...(${S(sizeWarning ?? '')} ? { warning: ${S(sizeWarning ?? '')} } : {}), ...${S(report)} };
|
|
13016
13544
|
})();`.trim();
|
|
13017
13545
|
}
|
|
13018
13546
|
/**
|
|
@@ -13253,10 +13781,15 @@ ${guard('exportPNGSequence')}
|
|
|
13253
13781
|
return Object.assign({ success: true, format: 'png-sequence', data: dataUrl, mimeType: out.type || 'application/zip', size: out.size }, __pngSeqDefaults);
|
|
13254
13782
|
}
|
|
13255
13783
|
if (Array.isArray(out)) {
|
|
13256
|
-
|
|
13784
|
+
// NOT A SAVED FILE, and said so in fields: file null, and why.
|
|
13785
|
+
return { success: true, format: 'png-sequence', frames: out.length, file: null,
|
|
13786
|
+
why: 'the frames stayed in the page: the studio returned them as a list of frames, not a file, so no file was written. Pass download: true to have the browser download them as a zip.',
|
|
13257
13787
|
note: 'the frames stayed in the page — pass download: true to write them out.' };
|
|
13258
13788
|
}
|
|
13259
|
-
|
|
13789
|
+
// download: true — the studio handed the zip to the BROWSER's download, so no bytes reached this server and no file
|
|
13790
|
+
// was written by it. Said in fields, so it cannot be read as a saved file.
|
|
13791
|
+
return { success: true, format: 'png-sequence', result: out, file: null,
|
|
13792
|
+
why: 'the studio handed the zip to the browser as a download (download: true): it went to the browser\\'s download folder, no bytes reached the MCP server, and the server wrote no file.' };
|
|
13260
13793
|
})();`.trim();
|
|
13261
13794
|
}
|
|
13262
13795
|
}
|