@pinepaper.studio/mcp-server 1.6.21 → 1.6.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -2
- 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/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/tools/choreograph.d.ts +4 -0
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +10 -2
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +36 -11
- 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 +2 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +21 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/brand-intake.d.ts.map +1 -1
- package/dist/tools/handlers/brand-intake.js +42 -1
- package/dist/tools/handlers/brand-intake.js.map +1 -1
- package/dist/tools/handlers.d.ts +18 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +124 -27
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.d.ts.map +1 -1
- package/dist/tools/shape-params.js +8 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/types/code-generator.d.ts +36 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +353 -44
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +52 -1
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +15 -0
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/asset-paths.d.ts +11 -0
- package/dist/utils/asset-paths.d.ts.map +1 -0
- package/dist/utils/asset-paths.js +67 -0
- package/dist/utils/asset-paths.js.map +1 -0
- package/dist/utils/safe-fetch.d.ts +28 -0
- package/dist/utils/safe-fetch.d.ts.map +1 -0
- package/dist/utils/safe-fetch.js +221 -0
- package/dist/utils/safe-fetch.js.map +1 -0
- package/dist/vendor/design/bauhaus-generator.d.ts.map +1 -1
- package/dist/vendor/design/bauhaus-generator.js +20 -5
- package/dist/vendor/design/bauhaus-generator.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +6 -3
|
@@ -74,17 +74,43 @@ function normalizeGradientSpec(v, key) {
|
|
|
74
74
|
*/
|
|
75
75
|
function generateGradientCode(gradient, itemVarName, position, propertyName = 'fillColor') {
|
|
76
76
|
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
77
|
const stopsCode = stops.map(s => `['${s.color}', ${s.offset}]`).join(', ');
|
|
78
|
+
if (type === 'radial') {
|
|
79
|
+
// A RADIAL CORE BELONGS ON THE ITEM, NOT ON THE NUMBER ASKED FOR (17
|
|
80
|
+
// RADIALOFFSET). The origin was the REQUESTED position and the radius a
|
|
81
|
+
// fixed 50 px: whenever (x, y) placed the item as a corner (an anchor, a
|
|
82
|
+
// default position) the core landed up-left by half the item, and a 300 px
|
|
83
|
+
// circle got a 50 px core. Read from the created item's bounds instead —
|
|
84
|
+
// centre and right-centre, as the engine's own _parseColor does — with the
|
|
85
|
+
// request only as the fallback when the item has no bounds. An origin or
|
|
86
|
+
// destination the caller gave is kept as given.
|
|
87
|
+
const fx = position.x, fy = position.y;
|
|
88
|
+
const o = origin ? `[${origin[0]}, ${origin[1]}]` : `(__gb ? [__gb.x + __gb.width / 2, __gb.y + __gb.height / 2] : [${fx}, ${fy}])`;
|
|
89
|
+
const d = destination ? `[${destination[0]}, ${destination[1]}]`
|
|
90
|
+
: origin ? `(__gb ? [${origin[0]} + __gb.width / 2, ${origin[1]}] : [${origin[0]} + 50, ${origin[1]}])`
|
|
91
|
+
: `(__gb ? [__gb.x + __gb.width, __gb.y + __gb.height / 2] : [${fx + 50}, ${fy}])`;
|
|
92
|
+
return `
|
|
93
|
+
${itemVarName}.${propertyName} = (function() {
|
|
94
|
+
const __b = ${itemVarName} && ${itemVarName}.bounds;
|
|
95
|
+
const __gb = __b && isFinite(__b.x) && isFinite(__b.y) && __b.width > 0 && __b.height > 0 ? __b : null;
|
|
96
|
+
return {
|
|
97
|
+
gradient: {
|
|
98
|
+
stops: [${stopsCode}],
|
|
99
|
+
radial: true
|
|
100
|
+
},
|
|
101
|
+
origin: ${o},
|
|
102
|
+
destination: ${d}
|
|
103
|
+
};
|
|
104
|
+
})();`;
|
|
105
|
+
}
|
|
106
|
+
// Linear: a diagonal across a 100 px box at the requested position.
|
|
107
|
+
const defaultOrigin = origin || [position.x - 50, position.y - 50];
|
|
108
|
+
const defaultDestination = destination || [position.x + 50, position.y + 50];
|
|
83
109
|
return `
|
|
84
110
|
${itemVarName}.${propertyName} = {
|
|
85
111
|
gradient: {
|
|
86
112
|
stops: [${stopsCode}],
|
|
87
|
-
radial:
|
|
113
|
+
radial: false
|
|
88
114
|
},
|
|
89
115
|
origin: [${defaultOrigin[0]}, ${defaultOrigin[1]}],
|
|
90
116
|
destination: [${defaultDestination[0]}, ${defaultDestination[1]}]
|
|
@@ -172,6 +198,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
172
198
|
textDirection.resolved = baseDirection(createProps.content);
|
|
173
199
|
}
|
|
174
200
|
const textTabular = itemType === 'text' && wantsTabular(createProps) === true;
|
|
201
|
+
const createFillOpacity = createProps.fillOpacity !== undefined;
|
|
175
202
|
const textStrokePos = itemType === 'text' ? takeStrokePosition(createProps, false) : null;
|
|
176
203
|
const params = {
|
|
177
204
|
...(omitXY ? {} : { x: position.x, y: position.y }),
|
|
@@ -337,6 +364,8 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
|
|
|
337
364
|
code += emitDirectionCheck('item', textDirection);
|
|
338
365
|
if (textTabular)
|
|
339
366
|
code += emitTabularCheck('item');
|
|
367
|
+
if (createFillOpacity)
|
|
368
|
+
code += emitFillOpacityCheck('item');
|
|
340
369
|
if (textStrokePos)
|
|
341
370
|
code += emitStrokePositionCheck('item', textStrokePos);
|
|
342
371
|
if (textFit)
|
|
@@ -374,7 +403,7 @@ const itemId = item.data.registryId;
|
|
|
374
403
|
app.historyManager.saveState();
|
|
375
404
|
|
|
376
405
|
// 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
|
|
406
|
+
({ 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
407
|
? `, 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
408
|
+ 'Check the spelling against this item type\'s documented properties.')}`
|
|
380
409
|
: ''} });`;
|
|
@@ -398,7 +427,7 @@ const MODIFY_KNOWN_KEYS = new Set([
|
|
|
398
427
|
...NORMALIZE_PARAM_READS,
|
|
399
428
|
'contentType', 'contentFormat', 'countdownTarget', 'countdownEndText',
|
|
400
429
|
'audioGain', 'volume', 'gain', 'bornAt', 'ttl', 'smoothing',
|
|
401
|
-
'fontStyle', 'leading', 'lineHeight', 'skewX', 'skewY', 'matrix', 'fit', 'direction', 'textDirection', 'dir', 'tabularFigures', 'fontVariantNumeric', 'fontFeatures', 'strokePosition',
|
|
430
|
+
'fontStyle', 'leading', 'lineHeight', 'skewX', 'skewY', 'matrix', 'fit', 'direction', 'textDirection', 'dir', 'tabularFigures', 'fontVariantNumeric', 'fontFeatures', 'strokePosition', 'fillOpacity',
|
|
402
431
|
]);
|
|
403
432
|
/** Spellings callers reach for, and the key the engine actually reads. */
|
|
404
433
|
const MODIFY_DID_YOU_MEAN = {
|
|
@@ -419,6 +448,9 @@ const CREATE_KNOWN_KEYS = new Set([
|
|
|
419
448
|
// Lifetimes, on ANY item: this generator applies them through app.setLifetime
|
|
420
449
|
// (emitLifetime), whatever create() itself reads.
|
|
421
450
|
'bornAt', 'ttl',
|
|
451
|
+
// A solid fill's own alpha (FxTool 12 STROKEFILL); confirmed per call by
|
|
452
|
+
// emitFillOpacityCheck, since an older studio ignores it.
|
|
453
|
+
'fillOpacity',
|
|
422
454
|
]);
|
|
423
455
|
/**
|
|
424
456
|
* Load a font family before text is drawn with it, and say if it still is not there.
|
|
@@ -566,15 +598,61 @@ function wantsTabular(p) {
|
|
|
566
598
|
const list = Array.isArray(f) ? f.map(String) : typeof f === 'string' ? [f] : f && typeof f === 'object' ? Object.keys(f).filter((k) => f[k]) : [];
|
|
567
599
|
return list.some((x) => /\btnum\b/.test(x));
|
|
568
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Whether tabular figures CHANGED the layout is the engine's to measure: the
|
|
603
|
+
* request flag (data.tabularFigures) is set even when the font's digits are
|
|
604
|
+
* already one width, where the layout moves nothing (9 TABNUM: DejaVu Sans
|
|
605
|
+
* reported applied while every advance matched the control). A studio with
|
|
606
|
+
* app.tabularFiguresReport measures the item's own font, and only its answer
|
|
607
|
+
* is reported as applied. One without it can only say the flag was recorded:
|
|
608
|
+
* 'unverified', never true.
|
|
609
|
+
*/
|
|
569
610
|
function emitTabularCheck(itemExpr) {
|
|
570
611
|
return `
|
|
571
612
|
const __tabular = (function(it) {
|
|
572
613
|
if (!it || it.className !== 'PointText') return { applied: false, note: 'tabular figures apply to text items only.' };
|
|
614
|
+
if (typeof app.tabularFiguresReport === 'function') {
|
|
615
|
+
let r = null;
|
|
616
|
+
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) + ').' }; }
|
|
617
|
+
if (r && typeof r === 'object') {
|
|
618
|
+
const out = { applied: r.applied === true };
|
|
619
|
+
if (typeof r.steadyWidth === 'boolean') out.steadyWidth = r.steadyWidth;
|
|
620
|
+
if (r.reason) out.reason = r.reason;
|
|
621
|
+
if (r.note) out.note = r.note;
|
|
622
|
+
else if (r.reason === 'rtl') out.note = 'right-to-left text keeps proportional digits.';
|
|
623
|
+
else if (r.reason === 'not-requested') out.note = 'the studio did not record the request, so digits keep their proportional widths.';
|
|
624
|
+
if (typeof r.spread === 'number') out.digitSpread = r.spread;
|
|
625
|
+
if (r.width && typeof r.width === 'object') out.width = r.width;
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
return { applied: 'unverified', note: 'the studio returned no tabular-figures report for this text.' };
|
|
629
|
+
}
|
|
573
630
|
return it.data && it.data.tabularFigures === true
|
|
574
|
-
? { applied:
|
|
631
|
+
? { 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
632
|
: { 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
633
|
})(${itemExpr});`;
|
|
577
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* fillOpacity (FxTool 12 STROKEFILL): a solid fill's own alpha, apart from the
|
|
637
|
+
* item's opacity and its stroke, so an outline-only type is fillOpacity: 0.
|
|
638
|
+
* The engine records it on data.fillOpacity and re-applies it when a later
|
|
639
|
+
* modify changes the fill colour. A studio without it ignores the key, so the
|
|
640
|
+
* reply is read from the item: the recorded value and the fill's alpha.
|
|
641
|
+
*/
|
|
642
|
+
function emitFillOpacityCheck(itemExpr) {
|
|
643
|
+
return `
|
|
644
|
+
const __fillOpacity = (function(it) {
|
|
645
|
+
if (!it) return { applied: false, note: 'no item to check.' };
|
|
646
|
+
const v = it.data && it.data.fillOpacity;
|
|
647
|
+
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.' };
|
|
648
|
+
const c = it.fillColor;
|
|
649
|
+
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.' };
|
|
650
|
+
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.' };
|
|
651
|
+
return (typeof c.alpha === 'number' && Math.abs(c.alpha - v) > 0.01)
|
|
652
|
+
? { applied: false, value: v, note: 'the studio recorded fillOpacity but the fill is drawn at alpha ' + c.alpha + '.' }
|
|
653
|
+
: { applied: true, value: v };
|
|
654
|
+
})(${itemExpr});`;
|
|
655
|
+
}
|
|
578
656
|
function takeStrokePosition(props, allowNull) {
|
|
579
657
|
if (!('strokePosition' in props))
|
|
580
658
|
return null;
|
|
@@ -853,6 +931,7 @@ function generateModifyItemCode(itemId, properties, dataFlags) {
|
|
|
853
931
|
delete restProperties.fit;
|
|
854
932
|
const modDirection = takeDirection(restProperties);
|
|
855
933
|
const modTabular = wantsTabular(restProperties) === true;
|
|
934
|
+
const modFillOpacity = restProperties.fillOpacity !== undefined;
|
|
856
935
|
const modStrokePos = takeStrokePosition(restProperties, true);
|
|
857
936
|
// modifyItem(id, changes), NOT select() + modify().
|
|
858
937
|
//
|
|
@@ -992,8 +1071,8 @@ ${properties.content !== undefined ? `
|
|
|
992
1071
|
const __ctItem = app.getItemById('${itemId}');
|
|
993
1072
|
const __ctKeys = (__ctItem && __ctItem.data && Array.isArray(__ctItem.data.keyframes))
|
|
994
1073
|
? __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} };`;
|
|
1074
|
+
${modDirection ? emitDirectionCheck(`app.getItemById('${itemId}')`, modDirection) : ''}${modTabular ? emitTabularCheck(`app.getItemById('${itemId}')`) : ''}${modFillOpacity ? emitFillOpacityCheck(`app.getItemById('${itemId}')`) : ''}${modStrokePos ? emitStrokePositionCheck(`app.getItemById('${itemId}')`, modStrokePos) : ''}
|
|
1075
|
+
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
1076
|
// Wrapped, because the body now RETURNS early on a miss rather than falling
|
|
998
1077
|
// through to an unconditional success. Async only when a font must load
|
|
999
1078
|
// before modifyItem re-measures the text.
|
|
@@ -1035,6 +1114,15 @@ function generateDeleteItemCode(itemId) {
|
|
|
1035
1114
|
/**
|
|
1036
1115
|
* Template for adding relations
|
|
1037
1116
|
*/
|
|
1117
|
+
/**
|
|
1118
|
+
* A JS expression: the studio's own reason for refusing a relation of `typeJs`,
|
|
1119
|
+
* or null. addRelation answers a bare false; the WHY (B119: "relation
|
|
1120
|
+
* 'spring_follow' is not loaded yet…") is recorded with app.noteWarning, which
|
|
1121
|
+
* reaches the reply only as a governor warning. Read here without draining it.
|
|
1122
|
+
*/
|
|
1123
|
+
export function relationRefusalReasonJs(typeJs) {
|
|
1124
|
+
return `(function(){ var ws = (app && app._engineWarnings) || []; for (var i = ws.length - 1; i >= 0; i--) { var w = ws[i]; if (w && w.relation === ${typeJs} && typeof w.message === 'string') return w.message; } return null; })()`;
|
|
1125
|
+
}
|
|
1038
1126
|
function generateAddRelationCode(sourceId, targetId, relationType, params) {
|
|
1039
1127
|
// AN UNKNOWN ENDPOINT IS REFUSED, NOT STORED (round 9 GG, 1.74). The engine
|
|
1040
1128
|
// stores a relation to an id it cannot resolve, returns true, and says "it
|
|
@@ -1055,13 +1143,54 @@ function generateAddRelationCode(sourceId, targetId, relationType, params) {
|
|
|
1055
1143
|
}
|
|
1056
1144
|
const success = app.addRelation('${sourceId}', '${targetId}', '${relationType}', ${JSON.stringify(params, null, 2)});
|
|
1057
1145
|
if (!success) {
|
|
1058
|
-
|
|
1146
|
+
const __why = ${relationRefusalReasonJs(JSON.stringify(relationType))};
|
|
1147
|
+
return { success: false, relationType: '${relationType}', error: 'the studio refused the ${relationType} relation between ${sourceId} and ${targetId}' + (__why ? ': ' + __why : '.') };
|
|
1059
1148
|
}
|
|
1060
1149
|
app.historyManager.saveState();
|
|
1061
1150
|
return { success: true, sourceId: '${sourceId}', targetId: '${targetId}', relationType: '${relationType}' };
|
|
1062
1151
|
})();
|
|
1063
1152
|
`.trim();
|
|
1064
1153
|
}
|
|
1154
|
+
/**
|
|
1155
|
+
* The per-burst particle cap of the CURRENT studio (FxTool ParticleEmitter
|
|
1156
|
+
* MAX_BURST_COUNT, B144). Studios before that change capped at
|
|
1157
|
+
* EMITTER_LEGACY_MAX_PER_BURST, in silence. Which one a studio has is read
|
|
1158
|
+
* back from the spec it stored (generateEmitter), not assumed.
|
|
1159
|
+
*/
|
|
1160
|
+
export const EMITTER_MAX_PER_BURST = 10000;
|
|
1161
|
+
/** The cap of studios before B144 (normalizeLook clamped at 2000 without a word). */
|
|
1162
|
+
export const EMITTER_LEGACY_MAX_PER_BURST = 2000;
|
|
1163
|
+
/**
|
|
1164
|
+
* Every count in an emitter spec (the spec's own and each burst's) that some
|
|
1165
|
+
* studio may cap: above the legacy cap. `index` is null for the spec's own
|
|
1166
|
+
* count (each burst without its own inherits it).
|
|
1167
|
+
*/
|
|
1168
|
+
export function emitterCountsToCheck(spec) {
|
|
1169
|
+
const out = [];
|
|
1170
|
+
if (!spec || typeof spec !== 'object')
|
|
1171
|
+
return out;
|
|
1172
|
+
const capped = (v) => typeof v === 'number' && Number.isFinite(v) && Math.round(v) > EMITTER_LEGACY_MAX_PER_BURST;
|
|
1173
|
+
if (capped(spec.count))
|
|
1174
|
+
out.push({ where: 'count', index: null, asked: Math.round(spec.count) });
|
|
1175
|
+
if (Array.isArray(spec.bursts)) {
|
|
1176
|
+
spec.bursts.forEach((b, i) => {
|
|
1177
|
+
const c = b && typeof b === 'object' ? b.count : undefined;
|
|
1178
|
+
if (capped(c))
|
|
1179
|
+
out.push({ where: `bursts[${i}].count`, index: i, asked: Math.round(c) });
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
return out;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* What can be said about the counts WITHOUT the studio's answer: above the
|
|
1186
|
+
* current cap is capped on every studio; between the two caps depends on the
|
|
1187
|
+
* studio. Used when the stored spec cannot be read back.
|
|
1188
|
+
*/
|
|
1189
|
+
export function emitterCountWarnings(spec) {
|
|
1190
|
+
return emitterCountsToCheck(spec).map(({ where, asked }) => asked > EMITTER_MAX_PER_BURST
|
|
1191
|
+
? `${where} ${asked} is over the ${EMITTER_MAX_PER_BURST}-particle cap per burst of the current studio (${EMITTER_LEGACY_MAX_PER_BURST} on older studios), so it draws at most ${EMITTER_MAX_PER_BURST}. Split it into bursts at the same t for more.`
|
|
1192
|
+
: `${where} ${asked} is over the ${EMITTER_LEGACY_MAX_PER_BURST}-particle cap per burst of studios before the ${EMITTER_MAX_PER_BURST} cap, and this studio did not report what it drew: on an older studio it draws ${EMITTER_LEGACY_MAX_PER_BURST}. Split it into bursts at the same t to be sure.`);
|
|
1193
|
+
}
|
|
1065
1194
|
/**
|
|
1066
1195
|
* moves_along_path, from the parameters a caller actually writes.
|
|
1067
1196
|
*
|
|
@@ -1132,7 +1261,27 @@ function generateMovesAlongPathCode(sourceId, targetId, raw) {
|
|
|
1132
1261
|
if (len > 0) params.speed = len / (150 * ${duration});
|
|
1133
1262
|
if (params.closed === undefined) params.closed = false;
|
|
1134
1263
|
}` : ''}
|
|
1135
|
-
|
|
1264
|
+
// AN EQUATION ROUTE WITH A DURATION (queue item 22, ORBITDUR). There are no
|
|
1265
|
+
// points here to turn a duration into speed, so it was dropped and the curve
|
|
1266
|
+
// ran at the default speed. A studio whose rule times an equation (FxTool
|
|
1267
|
+
// claude/engine-core-queue: its equation param says duration times it) gets
|
|
1268
|
+
// duration, and delay on the same playhead clock instead of a window (a
|
|
1269
|
+
// window would start the timed ride part-way through). An older studio hands
|
|
1270
|
+
// a timed equation to the arm that cannot read one and loses the curve, so it
|
|
1271
|
+
// keeps today's untimed ride and is told.
|
|
1272
|
+
const __warnings = [];
|
|
1273
|
+
let __eqTimed = false;
|
|
1274
|
+
${hasDuration ? `if (params.equation && !hasPoints) {
|
|
1275
|
+
const eqDesc = rule && rule.params && rule.params.equation && rule.params.equation.description;
|
|
1276
|
+
if (typeof eqDesc === 'string' && /duration/.test(eqDesc)) {
|
|
1277
|
+
__eqTimed = true;
|
|
1278
|
+
params.duration = ${duration};
|
|
1279
|
+
${hasDelay ? `params.delay = ${delay};` : ''}
|
|
1280
|
+
} else {
|
|
1281
|
+
__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.');
|
|
1282
|
+
}
|
|
1283
|
+
}` : ''}
|
|
1284
|
+
${hasDelay ? `if (!__eqTimed && !params.window) params.window = { start: ${delay} };` : ''}
|
|
1136
1285
|
// Before its window opens the relation does not run, so the item stays where
|
|
1137
1286
|
// it was left — the retest saw it parked at the path END. Put it at the start.
|
|
1138
1287
|
if (Array.isArray(params.path) && params.path.length) {
|
|
@@ -1145,7 +1294,8 @@ function generateMovesAlongPathCode(sourceId, targetId, raw) {
|
|
|
1145
1294
|
app.historyManager.saveState();
|
|
1146
1295
|
return { success: true, sourceId: ${JSON.stringify(sourceId)}, relationType: 'moves_along_path', route: 'sampled-points',
|
|
1147
1296
|
points: Array.isArray(params.path) ? params.path.length : 0, speed: params.speed, closed: params.closed,
|
|
1148
|
-
${ref ? `pathFrom: ${JSON.stringify(ref)},` : ''} window: params.window
|
|
1297
|
+
${ref ? `pathFrom: ${JSON.stringify(ref)},` : ''} window: params.window,
|
|
1298
|
+
...(__eqTimed ? { duration: params.duration } : {}), ...(__warnings.length ? { warnings: __warnings } : {}) };
|
|
1149
1299
|
})();`.trim();
|
|
1150
1300
|
}
|
|
1151
1301
|
/**
|
|
@@ -1327,7 +1477,35 @@ if (!_target) {
|
|
|
1327
1477
|
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
1478
|
}`.trim();
|
|
1329
1479
|
}
|
|
1330
|
-
|
|
1480
|
+
/**
|
|
1481
|
+
* Text keys a studio may or may not animate (queue item 2, FxTool
|
|
1482
|
+
* claude/engine-core-queue): fontStyle, textAlign, lineHeight and letterSpacing
|
|
1483
|
+
* apply as keyframes on the current studio, which lists them (textAlign as
|
|
1484
|
+
* justification, lineHeight as leading) in listAnimatableProperties. An older
|
|
1485
|
+
* studio stores the keys and draws nothing, so the check runs IN THE PAGE
|
|
1486
|
+
* against that list, and only the keys it does not name are reported.
|
|
1487
|
+
*/
|
|
1488
|
+
const TEXT_KEY_ENGINE_NAMES = {
|
|
1489
|
+
fontStyle: ['fontStyle'],
|
|
1490
|
+
textAlign: ['textAlign', 'justification'],
|
|
1491
|
+
lineHeight: ['lineHeight', 'leading'],
|
|
1492
|
+
letterSpacing: ['letterSpacing'],
|
|
1493
|
+
};
|
|
1494
|
+
/** A JS expression (run in the page) → the keys of `keys` this studio does not animate. '' when none are text keys. */
|
|
1495
|
+
function unanimatedTextKeysJs(keys) {
|
|
1496
|
+
const spec = [...new Set(keys)].filter((k) => k in TEXT_KEY_ENGINE_NAMES).map((k) => ({ key: k, names: TEXT_KEY_ENGINE_NAMES[k] }));
|
|
1497
|
+
if (!spec.length)
|
|
1498
|
+
return '';
|
|
1499
|
+
return `(function() {
|
|
1500
|
+
let __l = null;
|
|
1501
|
+
try { __l = typeof app.listAnimatableProperties === 'function' ? app.listAnimatableProperties() : null; } catch (_) { __l = null; }
|
|
1502
|
+
const __names = ((__l && __l.properties) || []).map(function(p) { return p && p.name; });
|
|
1503
|
+
return ${JSON.stringify(spec)}.filter(function(s) { return !s.names.some(function(n) { return __names.indexOf(n) >= 0; }); }).map(function(s) { return s.key; });
|
|
1504
|
+
})()`;
|
|
1505
|
+
}
|
|
1506
|
+
/** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
|
|
1507
|
+
const UNANIMATED_TEXT_KEYS_SENTENCE = `' is not an animatable property in this studio (app.listAnimatableProperties does not list it), so it had no effect. The current studio animates fontStyle, textAlign, lineHeight and letterSpacing; here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX.'`;
|
|
1508
|
+
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
|
|
1331
1509
|
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
1332
1510
|
const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
|
|
1333
1511
|
const opts = { duration: calculatedDuration, loop };
|
|
@@ -1343,6 +1521,11 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1343
1521
|
// 2475333b). An engine without merge ignores the option and replaces.
|
|
1344
1522
|
if (append)
|
|
1345
1523
|
opts.mode = 'merge';
|
|
1524
|
+
// EASEPLACE (queue item 5): which side of a key its easing shapes. The
|
|
1525
|
+
// current studio reads (and removes) the option; an older one leaves it on
|
|
1526
|
+
// the options object, which is how the result can say it was not applied.
|
|
1527
|
+
if (easingAt !== undefined)
|
|
1528
|
+
opts.easingAt = easingAt;
|
|
1346
1529
|
// A FADE ON AN AUDIO ITEM WAS ACCEPTED AND NEVER HEARD. addAnimation keyframes
|
|
1347
1530
|
// the audio item's sentinel, and the mix reads one static audioGain per clip,
|
|
1348
1531
|
// so {audioGain: 0→1} answered success, duration 15, and exported at a flat
|
|
@@ -1394,12 +1577,16 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1394
1577
|
const loopNote = loop
|
|
1395
1578
|
? `, 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
1579
|
: '';
|
|
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.`)}`
|
|
1580
|
+
const easingNote = easingAt !== 'departure' && first?.easing && first.easing !== 'linear'
|
|
1581
|
+
? `, 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
1582
|
: '';
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1583
|
+
const textKeysJs = unanimatedTextKeysJs(keyframes.flatMap((k) => Object.keys(k.properties ?? {})));
|
|
1584
|
+
const textKeyCheck = textKeysJs ? `
|
|
1585
|
+
const __ignored = ${textKeysJs};
|
|
1586
|
+
if (__ignored.length) __warn.push(__ignored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE});` : '';
|
|
1587
|
+
const easingAtCheck = easingAt !== undefined ? `
|
|
1588
|
+
if (__opts && Object.prototype.hasOwnProperty.call(__opts, 'easingAt')) __warn.push('easingAt ' + ${JSON.stringify(JSON.stringify(easingAt))} + ' was not applied: this studio predates it, so every easing was read as the move ARRIVING at its key. Put each easing on the key the move goes to instead.');` : '';
|
|
1589
|
+
const runtimeWarning = `${textKeysJs ? ', ...(__ignored.length ? { ignoredProperties: __ignored } : {})' : ''}, ...(__warn.length ? { warning: __warn.join(' ') } : {})`;
|
|
1403
1590
|
const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `'${itemId}'`;
|
|
1404
1591
|
return `
|
|
1405
1592
|
// Apply keyframe animation to ${itemId}
|
|
@@ -1410,7 +1597,9 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1410
1597
|
// merge support — an older engine takes the option and replaces anyway.
|
|
1411
1598
|
const __ki = _target && (_target.item || _target);
|
|
1412
1599
|
const __prev = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];
|
|
1413
|
-
|
|
1600
|
+
const __opts = ${JSON.stringify(opts)};
|
|
1601
|
+
app.addAnimation(${addAnimTarget}, ${keyframesJson}, __opts);
|
|
1602
|
+
const __warn = [];${easingAtCheck}${textKeyCheck}
|
|
1414
1603
|
const __now = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];
|
|
1415
1604
|
const __kept = __prev.every(function(t) { return __now.indexOf(t) >= 0; });${append ? `
|
|
1416
1605
|
// Property gaps across the merged track (the same check a single call makes
|
|
@@ -1423,7 +1612,7 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1423
1612
|
? { appended: true, previousKeys: __prev.length, keys: __now.length }
|
|
1424
1613
|
: { 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
1614
|
: `{ 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}${
|
|
1615
|
+
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
1616
|
})();
|
|
1428
1617
|
`.trim();
|
|
1429
1618
|
}
|
|
@@ -2730,7 +2919,9 @@ export class PinePaperCodeGenerator {
|
|
|
2730
2919
|
// silence; refuse before anything is written. setStepTiming ships in the
|
|
2731
2920
|
// same change, so it is the probe.
|
|
2732
2921
|
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,
|
|
2922
|
+
const code = generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint, validated.timeUnits,
|
|
2923
|
+
// mode: 'merge' is the studio's name for append (B147).
|
|
2924
|
+
validated.append === true || validated.mode === 'merge', validated.easingAt);
|
|
2734
2925
|
if (!holds)
|
|
2735
2926
|
return code;
|
|
2736
2927
|
return `(async function() {
|
|
@@ -3370,8 +3561,15 @@ if (typeof _gen === 'function') {
|
|
|
3370
3561
|
const targetId = nameToId['${target}'];
|
|
3371
3562
|
if (sourceId && targetId) {
|
|
3372
3563
|
const params = ${paramsStr};
|
|
3373
|
-
|
|
3374
|
-
|
|
3564
|
+
// A refused relation is not listed as made (owner rule 1).
|
|
3565
|
+
if (app.addRelation(sourceId, targetId, '${type}', params)) {
|
|
3566
|
+
results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
|
|
3567
|
+
} else {
|
|
3568
|
+
const __why = ${relationRefusalReasonJs(JSON.stringify(type))};
|
|
3569
|
+
(results.relationsRefused = results.relationsRefused || []).push({ source: '${source}', target: '${target}', type: '${type}', reason: __why || 'the studio refused it' });
|
|
3570
|
+
results.success = false;
|
|
3571
|
+
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.';
|
|
3572
|
+
}
|
|
3375
3573
|
}
|
|
3376
3574
|
})();
|
|
3377
3575
|
`);
|
|
@@ -4298,6 +4496,15 @@ const target = app.getItemById(targetId);
|
|
|
4298
4496
|
if (!source) throw new Error('Source not found: ' + sourceId);
|
|
4299
4497
|
if (!target) throw new Error('Target not found: ' + targetId);
|
|
4300
4498
|
const relationId = app.addRelation(source, target, '${op.relationType}', ${relOpts});
|
|
4499
|
+
// NOT BOUND IS NOT A SUCCESS (owner rule 1, B119). addRelation answers false
|
|
4500
|
+
// when it refuses — spring_follow "not loaded yet" — and this op returned
|
|
4501
|
+
// { relationId: false } with nothing marking it failed, so the batch said
|
|
4502
|
+
// success over a relation that does not exist.
|
|
4503
|
+
if (!relationId) {
|
|
4504
|
+
const __why = ${relationRefusalReasonJs(JSON.stringify(op.relationType))};
|
|
4505
|
+
return { success: false, relationId: false, sourceId, targetId, relationType: ${JSON.stringify(op.relationType)},
|
|
4506
|
+
error: 'the studio refused the ' + ${JSON.stringify(op.relationType)} + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };
|
|
4507
|
+
}
|
|
4301
4508
|
return { relationId, sourceId, targetId, relationType: '${op.relationType}' };
|
|
4302
4509
|
`;
|
|
4303
4510
|
case 'delete':
|
|
@@ -4375,7 +4582,10 @@ return { itemId: ${op.itemId?.startsWith('$') ? `__ref(${op.itemId.substring(1)}
|
|
|
4375
4582
|
: (op.duration || op.properties?.duration || (kfSource.length ? Math.max(...kfSource.map((k) => Number(k.time) || 0)) : 5));
|
|
4376
4583
|
const kfLoop = mergedPayload ? mergedPayload.loop : (op.loop ?? op.properties?.loop ?? false);
|
|
4377
4584
|
// Same contract as pinepaper_keyframe_animate: replace unless append.
|
|
4378
|
-
|
|
4585
|
+
// mode: 'merge' is the studio's spelling (its keyframe_track_replaced
|
|
4586
|
+
// warning names it), so it means append here too (B147).
|
|
4587
|
+
const kfAppend = op.append === true || op.properties?.append === true
|
|
4588
|
+
|| op.mode === 'merge' || op.properties?.mode === 'merge';
|
|
4379
4589
|
// Batch ops are passthrough, so these arrive unvalidated: numbers only, and timeUnits from its enum.
|
|
4380
4590
|
const kfExtra = [];
|
|
4381
4591
|
for (const key of ['timeOffset', 'clipInPoint', 'clipOutPoint']) {
|
|
@@ -4387,9 +4597,9 @@ return { itemId: ${op.itemId?.startsWith('$') ? `__ref(${op.itemId.substring(1)}
|
|
|
4387
4597
|
if (kfUnits === 'seconds' || kfUnits === 'ms')
|
|
4388
4598
|
kfExtra.push(`timeUnits: '${kfUnits}'`);
|
|
4389
4599
|
const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
|
|
4390
|
-
const
|
|
4391
|
-
|
|
4392
|
-
const kfIgnored =
|
|
4600
|
+
const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
|
|
4601
|
+
const kfTextCheck = kfTextKeysJs ? `\nconst _kfIgnored = ${kfTextKeysJs};` : '';
|
|
4602
|
+
const kfIgnored = kfTextKeysJs ? `, ...(_kfIgnored.length ? { ignoredProperties: _kfIgnored, warning: _kfIgnored.join(', ') + ${UNANIMATED_TEXT_KEYS_SENTENCE} } : {})` : '';
|
|
4393
4603
|
return `
|
|
4394
4604
|
const targetId = ${kfItemRef};
|
|
4395
4605
|
const _target = (app.getItemById && app.getItemById(targetId))
|
|
@@ -4399,7 +4609,7 @@ if (!_target) {
|
|
|
4399
4609
|
}
|
|
4400
4610
|
const _kfItem = _target.item || _target;
|
|
4401
4611
|
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} })
|
|
4612
|
+
app.addAnimation(targetId, ${kfJson}, { duration: ${kfDuration}, loop: ${kfLoop}${kfAppend ? ", mode: 'merge'" : ''}${kfExtraStr} });${kfTextCheck}
|
|
4403
4613
|
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
4614
|
`;
|
|
4405
4615
|
}
|
|
@@ -5573,7 +5783,30 @@ ${stillTime !== undefined ? `
|
|
|
5573
5783
|
|
|
5574
5784
|
if (result && result.success) Object.assign(result, fidelity(format));
|
|
5575
5785
|
if (result && result.success && __fit) result.platformFit = __fit;
|
|
5576
|
-
// A
|
|
5786
|
+
${usesCanvasSize ? '' : ` // A PRESET SIZE IS NOT THE CANVAS SIZE, AND THAT IS SAID (21 PLATFORMOVERRIDE).
|
|
5787
|
+
// A named platform renders the FILE at the preset's size; the canvas is not
|
|
5788
|
+
// resized, so the scene keeps the canvas's layout, scaled to fill the file.
|
|
5789
|
+
// A 640x360 canvas under youtube came back 1920x1080 with nothing said. The
|
|
5790
|
+
// size the route reports is compared with the canvas; the current studio's
|
|
5791
|
+
// video report warns about an upscale too, but as plain text this reply does
|
|
5792
|
+
// not pass on, so it is said here for every studio.
|
|
5793
|
+
// Not said for camera framing (the camera chooses that frame) or an
|
|
5794
|
+
// unbounded canvas (its stills crop to the artwork, said on their own), and
|
|
5795
|
+
// a scale the caller asked for is theirs, not the preset's.
|
|
5796
|
+
if (result && result.success && framing !== 'camera' && __cv && !__cv.unbounded && __cv.width > 0 && __cv.height > 0
|
|
5797
|
+
&& result.dimensions && typeof result.dimensions === 'object' && result.dimensions.width > 0 && result.dimensions.height > 0) {
|
|
5798
|
+
const __fw = result.dimensions.width, __fh = result.dimensions.height;
|
|
5799
|
+
const __us = ${scale !== undefined ? scale : 1};
|
|
5800
|
+
if (Math.abs(__fw - __cv.width * __us) > 2 || Math.abs(__fh - __cv.height * __us) > 2) {
|
|
5801
|
+
const __k = Math.round((__fw / __cv.width) * 1000) / 1000;
|
|
5802
|
+
result.platformSize = { preset: __presetBox.width + 'x' + __presetBox.height, canvas: __cv.width + 'x' + __cv.height, file: __fw + 'x' + __fh, scale: __k, canvasResized: false };
|
|
5803
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5804
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'platform_size_differs_from_canvas',
|
|
5805
|
+
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.' }]);
|
|
5806
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
`} // A LANDSCAPE CANVAS FOR A PORTRAIT PLATFORM (or the reverse) is not an
|
|
5577
5810
|
// aspect nuance but the wrong deliverable: tiktok on a 16:9 canvas came out
|
|
5578
5811
|
// 1920x1080 with only a note (round 10, 10.7). Said where a caller looks.
|
|
5579
5812
|
if (result && result.success && __fit) {
|
|
@@ -7818,7 +8051,17 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7818
8051
|
`.trim();
|
|
7819
8052
|
}
|
|
7820
8053
|
generateImportImage(input) {
|
|
7821
|
-
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize } = input;
|
|
8054
|
+
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize, shadowColor, shadowBlur, shadowOffset } = input;
|
|
8055
|
+
// IMPORTSHADOW: the shadow keys, applied through the engine's own modify
|
|
8056
|
+
// door once the image is placed (and masked) — the route modify_item takes.
|
|
8057
|
+
const shadow = {};
|
|
8058
|
+
if (shadowColor !== undefined)
|
|
8059
|
+
shadow.shadowColor = shadowColor;
|
|
8060
|
+
if (shadowBlur !== undefined)
|
|
8061
|
+
shadow.shadowBlur = shadowBlur;
|
|
8062
|
+
if (shadowOffset !== undefined)
|
|
8063
|
+
shadow.shadowOffset = shadowOffset;
|
|
8064
|
+
const hasShadow = Object.keys(shadow).length > 0;
|
|
7822
8065
|
// Each fragment used to be written with a LEADING comma, as if it followed
|
|
7823
8066
|
// an entry that no longer exists — so the first one opened the object with
|
|
7824
8067
|
// `{, position: …}` and the emitted code was a syntax error. Passing any
|
|
@@ -7916,6 +8159,16 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7916
8159
|
}
|
|
7917
8160
|
if (app.itemRegistry && typeof app.itemRegistry.rebind === 'function') app.itemRegistry.rebind(itemId, masked);
|
|
7918
8161
|
placed = masked;
|
|
8162
|
+
` : ''}${hasShadow ? ` // The shadow goes on what is DRAWN, after a mask has replaced the raster.
|
|
8163
|
+
let __shadow = null;
|
|
8164
|
+
{
|
|
8165
|
+
const __want = ${JSON.stringify(shadow)};
|
|
8166
|
+
let __ok = false;
|
|
8167
|
+
try { __ok = app.modifyItem(itemId, __want) !== false; } catch (_) { __ok = false; }
|
|
8168
|
+
__shadow = __ok
|
|
8169
|
+
? { applied: true, shadowColor: placed.shadowColor ? String(placed.shadowColor.toCSS ? placed.shadowColor.toCSS(true) : placed.shadowColor) : null, shadowBlur: placed.shadowBlur }
|
|
8170
|
+
: { applied: false, warning: 'the image was imported but its shadow was not applied — set it with pinepaper_modify_item on ' + itemId + '.' };
|
|
8171
|
+
}
|
|
7919
8172
|
` : ''} const b = placed.bounds;
|
|
7920
8173
|
if (!b || !(b.width > 0) || !(b.height > 0)) {
|
|
7921
8174
|
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 +8177,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7924
8177
|
success: true,
|
|
7925
8178
|
itemId: itemId,
|
|
7926
8179
|
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}'` : ''},
|
|
8180
|
+
bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: '${mask}'` : ''}${hasShadow ? ',\n shadow: __shadow' : ''},
|
|
7928
8181
|
// Stored smaller than its source (the studio's cap): said, with the
|
|
7929
8182
|
// way to keep every pixel. The studio reports native size where it can.
|
|
7930
8183
|
...((entry && entry.nativeWidth && entry.width && entry.nativeWidth > entry.width)
|
|
@@ -11043,7 +11296,9 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
11043
11296
|
app.create(op.type, { id: op.id, x: op.x, y: op.y, width: op.width, height: op.height });
|
|
11044
11297
|
itemIds.push(op.id);
|
|
11045
11298
|
} else if (op.op === 'addRelation') {
|
|
11046
|
-
app.addRelation(op.from, op.to, op.relation, op.params || {})
|
|
11299
|
+
if (!app.addRelation(op.from, op.to, op.relation, op.params || {})) {
|
|
11300
|
+
errors.push({ op: op.op, id: op.from + '->' + op.to, error: 'the studio refused the ' + op.relation + ' relation' });
|
|
11301
|
+
}
|
|
11047
11302
|
}
|
|
11048
11303
|
} catch (e) {
|
|
11049
11304
|
errors.push({ op: op.op, id: op.id || (op.from + '->' + op.to), error: e && e.message });
|
|
@@ -11592,9 +11847,41 @@ ${guard}
|
|
|
11592
11847
|
const __baked = (__it.data && typeof __it.data._bakedRotation === 'number') ? __it.data._bakedRotation : 0;
|
|
11593
11848
|
${authoringFrameJs('__fr')}
|
|
11594
11849
|
const __frame = { x: __fr.x || 0, y: __fr.y || 0, width: __fr.width, height: __fr.height };
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11850
|
+
// THE ACTOR AT THE FIRST BEAT, not where the playhead left it (B154 /
|
|
11851
|
+
// HOOKCLOCK3). Live values of a keyframed item depend on the playhead, so a
|
|
11852
|
+
// second call anchored at +0 / +57 / +22 / +43 px from session to session
|
|
11853
|
+
// and multiplied scales the first track had applied. app.poseAt (FxTool,
|
|
11854
|
+
// claude/engine-core-queue) answers at a track time; older studios fall back
|
|
11855
|
+
// to the live read.
|
|
11856
|
+
const __at0 = ${S(Math.min(...input.beats.map((b) => Number(b && b.at) || 0)))};
|
|
11857
|
+
// A track that already keys the beat properties defines the pose before the
|
|
11858
|
+
// first beat; the B109 hold key at 0 would overwrite its key there. A track
|
|
11859
|
+
// of other properties (a colour) still needs the hold key.
|
|
11860
|
+
const __beatProps = ['x', 'y', 'scaleX', 'scaleY', 'scale', 'rotation', 'opacity', 'position'];
|
|
11861
|
+
const __hasTrack = !!(__it.data && Array.isArray(__it.data.keyframes) && __it.data.keyframes.some(function (k) {
|
|
11862
|
+
return Object.keys((k && k.properties) || {}).some(function (p) { return __beatProps.indexOf(p) >= 0; });
|
|
11863
|
+
}));
|
|
11864
|
+
let __pose = null;
|
|
11865
|
+
let __poseErr = null;
|
|
11866
|
+
if (typeof app.poseAt === 'function') {
|
|
11867
|
+
try { __pose = app.poseAt(__it, __at0); } catch (e) { __poseErr = (e && e.message) || String(e); }
|
|
11868
|
+
if (__pose && !(isFinite(__pose.x) && isFinite(__pose.y) && __pose.width > 0 && __pose.height > 0)) __pose = null;
|
|
11869
|
+
}
|
|
11870
|
+
const __anchorWarnings = [];
|
|
11871
|
+
if (!__pose && __hasTrack) {
|
|
11872
|
+
__anchorWarnings.push((__poseErr ? 'app.poseAt failed (' + __poseErr + ')' : 'this studio has no app.poseAt')
|
|
11873
|
+
+ ', 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.');
|
|
11874
|
+
}
|
|
11875
|
+
if (!__pose && __it.className === 'Raster' && ${S(input.beats.some((b) => b && b.verb === 'roll'))}) {
|
|
11876
|
+
__anchorWarnings.push((__poseErr ? 'app.poseAt failed' : 'this studio has no app.poseAt')
|
|
11877
|
+
+ ', so the roll turns on the picture\\'s half-box, not its drawn radius: a picture with a transparent margin slides as it rolls.');
|
|
11878
|
+
}
|
|
11879
|
+
const __actor = __pose
|
|
11880
|
+
? { 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,
|
|
11881
|
+
rollRadius: __pose.drawnHeight ? __pose.drawnHeight / 2 : undefined }
|
|
11882
|
+
: { x: __it.position.x, y: __it.position.y,
|
|
11883
|
+
w: __b.width / (Math.abs(__sc.x) || 1), h: __b.height / (Math.abs(__sc.y) || 1),
|
|
11884
|
+
sx: __sc.x || 1, sy: __sc.y || 1, rot: (__it.rotation || 0) + __baked, hasTrack: __hasTrack };
|
|
11598
11885
|
const __r = (${compileChoreography.toString()})(__actor, __frame, ${S(input.beats)});
|
|
11599
11886
|
if (__r.error) { return { success: false, itemId: ${id}, error: __r.error }; }
|
|
11600
11887
|
const __prev = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes : [];
|
|
@@ -11603,7 +11890,7 @@ ${guard}
|
|
|
11603
11890
|
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
11891
|
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
11892
|
const __now = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes.map(function (k) { return k && k.time; }) : [];
|
|
11606
|
-
const __warnings = (__r.warnings || [])
|
|
11893
|
+
const __warnings = __anchorWarnings.concat(__r.warnings || []);
|
|
11607
11894
|
if (__prevTimes.length && !__prevTimes.every(function (t) { return __now.indexOf(t) >= 0; })) {
|
|
11608
11895
|
__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
11896
|
}
|
|
@@ -11966,27 +12253,49 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
|
|
|
11966
12253
|
/** pinepaper_emitter (D29): create('emitter', spec) / setEmitter(id, patch). */
|
|
11967
12254
|
generateEmitter(input) {
|
|
11968
12255
|
const S = (v) => JSON.stringify(v);
|
|
12256
|
+
// A COUNT OVER THE CAP IS SAID (B144). The cap is 10000 a burst on the
|
|
12257
|
+
// current studio and was 2000 before it, clamped in silence (2300 drew
|
|
12258
|
+
// ~1940). Which applies is read from the spec the studio STORED — its
|
|
12259
|
+
// normalised count is what it draws — so the reply names the number
|
|
12260
|
+
// actually drawn on whichever studio ran it. Only when nothing can be read
|
|
12261
|
+
// back does it fall back to what is known without the studio.
|
|
12262
|
+
const checks = emitterCountsToCheck(input.spec);
|
|
12263
|
+
const fallback = emitterCountWarnings(input.spec);
|
|
12264
|
+
const capFn = checks.length ? `
|
|
12265
|
+
function __capWarnings(stored) {
|
|
12266
|
+
const checks = ${S(checks)};
|
|
12267
|
+
if (!stored || typeof stored !== 'object') return ${S(fallback)};
|
|
12268
|
+
const out = [];
|
|
12269
|
+
for (const c of checks) {
|
|
12270
|
+
const b = c.index === null ? null : (Array.isArray(stored.bursts) ? stored.bursts[c.index] : null);
|
|
12271
|
+
const drawn = b && typeof b.count === 'number' ? b.count : stored.count;
|
|
12272
|
+
if (typeof drawn !== 'number') return ${S(fallback)};
|
|
12273
|
+
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.');
|
|
12274
|
+
}
|
|
12275
|
+
return out;
|
|
12276
|
+
}` : '';
|
|
12277
|
+
const capJs = (stored) => checks.length ? `, ...((function(w) { return w.length ? { warnings: w } : {}; })(__capWarnings(${stored})))` : '';
|
|
11969
12278
|
if (input.action === 'set') {
|
|
11970
12279
|
return `
|
|
11971
12280
|
// Emitter: change a spec
|
|
11972
12281
|
(function() {
|
|
11973
|
-
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }
|
|
12282
|
+
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }${capFn}
|
|
11974
12283
|
const r = app.setEmitter(${S(input.itemId)}, ${S(input.spec)});
|
|
11975
12284
|
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 };
|
|
12285
|
+
return { success: true, action: 'set', itemId: ${S(input.itemId)}, spec: r.spec${capJs('r.spec')} };
|
|
11977
12286
|
})();`.trim();
|
|
11978
12287
|
}
|
|
11979
12288
|
return `
|
|
11980
12289
|
// Emitter: create
|
|
11981
12290
|
(function() {
|
|
11982
|
-
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }
|
|
12291
|
+
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }${capFn}
|
|
11983
12292
|
const it = app.create('emitter', ${S(input.spec)});
|
|
11984
12293
|
// A bad spec comes back null with an emitter-invalid warning; the reason
|
|
11985
12294
|
// reaches this reply through the governor's warnings.
|
|
11986
12295
|
if (!it) { return { success: false, error: 'the emitter spec was refused' }; }
|
|
11987
12296
|
const id = it.data && (it.data.registryId || it.data.id);
|
|
11988
12297
|
if (!id) { return { success: false, error: 'the emitter was made but not registered — no usable id' }; }
|
|
11989
|
-
return { success: true, action: 'create', itemId: id };
|
|
12298
|
+
return { success: true, action: 'create', itemId: id${capJs('it.data && it.data.emitter')} };
|
|
11990
12299
|
})();`.trim();
|
|
11991
12300
|
}
|
|
11992
12301
|
/** pinepaper_render_hook (D29): drawing saved as source, re-installed on load. */
|
|
@@ -12982,7 +13291,7 @@ ${needWorld}
|
|
|
12982
13291
|
* already decided by a pure function, so nothing is left for the page to
|
|
12983
13292
|
* work out.
|
|
12984
13293
|
*/
|
|
12985
|
-
generateDesignCompose(scene, ops, style, sizeWarning) {
|
|
13294
|
+
generateDesignCompose(scene, ops, style, sizeWarning, report = {}) {
|
|
12986
13295
|
const S = (v) => JSON.stringify(v);
|
|
12987
13296
|
return `
|
|
12988
13297
|
// Design: compose a ${style} scene — ${ops.length} items, laid out server-side
|
|
@@ -13012,7 +13321,7 @@ ${needWorld}
|
|
|
13012
13321
|
// success is worse than a poster that says which four elements are missing.
|
|
13013
13322
|
return { success: failed.length === 0, style: ${S(style)}, itemIds: ids, itemCount: ids.length, failed: failed,
|
|
13014
13323
|
...(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 ?? '')} } : {}) };
|
|
13324
|
+
...(${S(sizeWarning ?? '')} ? { warning: ${S(sizeWarning ?? '')} } : {}), ...${S(report)} };
|
|
13016
13325
|
})();`.trim();
|
|
13017
13326
|
}
|
|
13018
13327
|
/**
|