@pinepaper.studio/mcp-server 1.6.20 → 1.6.21
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 +79 -5
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/build-info.json +3 -3
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +3 -3
- package/dist/ontology/upstream-vocabulary.js.map +1 -1
- package/dist/ontology/vocabulary.js +1 -1
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +10 -0
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/craft-tools.d.ts +55 -0
- package/dist/tools/craft-tools.d.ts.map +1 -0
- package/dist/tools/craft-tools.js +139 -0
- package/dist/tools/craft-tools.js.map +1 -0
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +117 -7
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/effect-class.d.ts.map +1 -1
- package/dist/tools/effect-class.js +6 -0
- package/dist/tools/effect-class.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 +4 -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 +38 -5
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +28 -28
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +125 -1
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +7 -0
- package/dist/tools/minimal-descriptions.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 +3 -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/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +9 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +10 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +605 -107
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +1745 -582
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +144 -4
- package/dist/types/schemas.js.map +1 -1
- package/dist/vendor/design/design-style.d.ts +2 -2
- package/dist/vendor/design/design-style.d.ts.map +1 -1
- package/dist/vendor/design/design-style.js +7 -2
- package/dist/vendor/design/design-style.js.map +1 -1
- package/dist/vendor/design/subdivision-tool-schema.d.ts +16 -0
- package/dist/vendor/design/subdivision-tool-schema.d.ts.map +1 -0
- package/dist/vendor/design/subdivision-tool-schema.js +104 -0
- package/dist/vendor/design/subdivision-tool-schema.js.map +1 -0
- package/dist/vendor/design/textile-tool-schema.d.ts +14 -0
- package/dist/vendor/design/textile-tool-schema.d.ts.map +1 -0
- package/dist/vendor/design/textile-tool-schema.js +195 -0
- package/dist/vendor/design/textile-tool-schema.js.map +1 -0
- package/dist/vendor/design/ui-kit-runtime.d.ts +49 -0
- package/dist/vendor/design/ui-kit-runtime.d.ts.map +1 -0
- package/dist/vendor/design/ui-kit-runtime.js +761 -0
- package/dist/vendor/design/ui-kit-runtime.js.map +1 -0
- package/dist/vendor/design/ui-kit-tool-schema.d.ts +280 -0
- package/dist/vendor/design/ui-kit-tool-schema.d.ts.map +1 -0
- package/dist/vendor/design/ui-kit-tool-schema.js +209 -0
- package/dist/vendor/design/ui-kit-tool-schema.js.map +1 -0
- package/manifest.json +29 -1
- package/package.json +2 -2
- package/dist/assets/repositories/openclipart.d.ts +0 -41
- package/dist/assets/repositories/openclipart.d.ts.map +0 -1
- package/dist/assets/repositories/openclipart.js +0 -139
- package/dist/assets/repositories/openclipart.js.map +0 -1
- package/dist/assets/repositories/svgrepo.d.ts +0 -61
- package/dist/assets/repositories/svgrepo.d.ts.map +0 -1
- package/dist/assets/repositories/svgrepo.js +0 -276
- package/dist/assets/repositories/svgrepo.js.map +0 -1
|
@@ -74,9 +74,11 @@ 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
|
-
|
|
79
|
-
|
|
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]);
|
|
80
82
|
const stopsCode = stops.map(s => `['${s.color}', ${s.offset}]`).join(', ');
|
|
81
83
|
return `
|
|
82
84
|
${itemVarName}.${propertyName} = {
|
|
@@ -130,6 +132,18 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
130
132
|
properties = { ...properties, [key]: g.gradient };
|
|
131
133
|
}
|
|
132
134
|
}
|
|
135
|
+
// Aliases for text items: text -> content (B66), textAlign / align -> justification (B89)
|
|
136
|
+
if (properties.content === undefined && properties.text !== undefined) {
|
|
137
|
+
properties = { ...properties, content: properties.text };
|
|
138
|
+
}
|
|
139
|
+
if (properties.justification === undefined) {
|
|
140
|
+
if (properties.textAlign !== undefined) {
|
|
141
|
+
properties = { ...properties, justification: properties.textAlign };
|
|
142
|
+
}
|
|
143
|
+
else if (properties.align !== undefined) {
|
|
144
|
+
properties = { ...properties, justification: properties.align };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
133
147
|
// Extract special properties that need separate handling
|
|
134
148
|
const { color, fillColor, strokeColor, shadowColor, shadowBlur, shadowOffset, blendMode, opacity, contentType, contentFormat, countdownTarget, countdownEndText, ...baseProperties } = properties;
|
|
135
149
|
// A PATH KEEPS ITS OWN COORDINATES WHEN NO POSITION WAS ASKED FOR (round 9
|
|
@@ -194,18 +208,15 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
194
208
|
if (strokeColor && !isGradient(strokeColor)) {
|
|
195
209
|
params.strokeColor = strokeColor;
|
|
196
210
|
}
|
|
197
|
-
// RENDER-TIME SURFACES
|
|
198
|
-
//
|
|
199
|
-
// `shader` and `field` are drawn per pixel by the cloud rasterizer from the
|
|
200
|
-
// scene document; the browser engine has no such item type and answers
|
|
201
|
-
// `create('shader')` with "unknown type — nothing was created". Emitting that
|
|
202
|
-
// call would hand every local user a silent hole where their backdrop was.
|
|
211
|
+
// RENDER-TIME SURFACES ARE A TAGGED PLATE.
|
|
203
212
|
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
213
|
+
// `shader` and `field` are drawn per pixel from the scene document. What is
|
|
214
|
+
// created is a rectangle carrying the id, position, size and colour, tagged
|
|
215
|
+
// data.renderAs + data.renderParams. The studio adopts that plate and draws
|
|
216
|
+
// the surface over it in the live view, stills and every export (FxTool D19);
|
|
217
|
+
// the cloud rasterizer draws it from the same tags. A studio too old to adopt
|
|
218
|
+
// it still shows a readable, addressable plate rather than a hole, which is
|
|
219
|
+
// why this does not call create('shader').
|
|
209
220
|
if (itemType === 'shader' || itemType === 'field') {
|
|
210
221
|
const plate = {
|
|
211
222
|
x: position.x,
|
|
@@ -219,8 +230,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
|
|
|
219
230
|
if (params.opacity != null)
|
|
220
231
|
plate.opacity = params.opacity;
|
|
221
232
|
return `
|
|
222
|
-
// ${itemType} — a render-time surface
|
|
223
|
-
// stood in for locally by a flat plate so the layout still reads.
|
|
233
|
+
// ${itemType} — a render-time surface: a plate tagged with what to draw over it.
|
|
224
234
|
const item = app.create('rectangle', ${JSON.stringify(plate, null, 2)});
|
|
225
235
|
if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.data.renderParams = ${JSON.stringify(params)}; }
|
|
226
236
|
// The snippet's value is its last statement's, which was the assignment above:
|
|
@@ -229,10 +239,8 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
|
|
|
229
239
|
// \`}\` as a statement end, so a bare ternary of object literals is not wrapped
|
|
230
240
|
// and its value is lost.
|
|
231
241
|
((item && item.data)
|
|
232
|
-
? { itemId: item.data.registryId, type: ${JSON.stringify(itemType)}, position: { x: ${position.x}, y: ${position.y} }
|
|
233
|
-
|
|
234
|
-
note: 'drawn here as a flat plate; the ${itemType} itself appears only in a cloud render. A local export shows the plate.' }
|
|
235
|
-
: { success: false, error: 'the stand-in plate for this ${itemType} was not created.' });`;
|
|
242
|
+
? { itemId: item.data.registryId, type: ${JSON.stringify(itemType)}, position: { x: ${position.x}, y: ${position.y} } }
|
|
243
|
+
: { success: false, error: 'the plate for this ${itemType} was not created.' });`;
|
|
236
244
|
}
|
|
237
245
|
// AN UNKNOWN ANCHOR IS INVISIBLE OVER MCP.
|
|
238
246
|
//
|
|
@@ -334,9 +342,14 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
|
|
|
334
342
|
if (textFit)
|
|
335
343
|
code += `\nlet __textFit = null;${emitTextFit('item', textFit, JSON.stringify(anchorHold(properties.anchor ?? properties.origin) ?? null))}`;
|
|
336
344
|
// After opacity, so the "on" level of the lifetime is the item's own.
|
|
337
|
-
|
|
345
|
+
// A numeric `lifetime` is the studio's alias of ttl on create (FxTool B125;
|
|
346
|
+
// emitters, where it means each particle's life, are not made here).
|
|
347
|
+
// Resolved here too: with bornAt beside it, passing ttl undefined would reset
|
|
348
|
+
// the item to "until the end" after create() had set the alias.
|
|
349
|
+
const createTtl = properties.ttl ?? (typeof properties.lifetime === 'number' ? properties.lifetime : undefined);
|
|
350
|
+
const hasLifetime = properties.bornAt !== undefined || createTtl !== undefined;
|
|
338
351
|
if (hasLifetime) {
|
|
339
|
-
code += `\nlet __lifetime = null;${emitLifetime('item', properties.bornAt,
|
|
352
|
+
code += `\nlet __lifetime = null;${emitLifetime('item', properties.bornAt, createTtl)}`;
|
|
340
353
|
}
|
|
341
354
|
// Ensure item is visible above backgrounds/generators
|
|
342
355
|
code += `\nif (item.bringToFront) item.bringToFront();`;
|
|
@@ -403,9 +416,8 @@ const CREATE_KNOWN_KEYS = new Set([
|
|
|
403
416
|
// withRadiusAxes, gradients / shadow / blend / opacity emitted here, and
|
|
404
417
|
// setDynamicContent for text.
|
|
405
418
|
'radiusX', 'radiusY', 'contentType', 'contentFormat', 'countdownTarget', 'countdownEndText',
|
|
406
|
-
// Lifetimes
|
|
407
|
-
//
|
|
408
|
-
// them "no effect" would steer a caller off the only way to cut between shots.
|
|
419
|
+
// Lifetimes, on ANY item: this generator applies them through app.setLifetime
|
|
420
|
+
// (emitLifetime), whatever create() itself reads.
|
|
409
421
|
'bornAt', 'ttl',
|
|
410
422
|
]);
|
|
411
423
|
/**
|
|
@@ -626,7 +638,25 @@ if (typeof app.fitText === 'function') {
|
|
|
626
638
|
*
|
|
627
639
|
* lineHeight is the CSS habit: <= 4 is a multiple of fontSize, above that px.
|
|
628
640
|
*/
|
|
629
|
-
|
|
641
|
+
/**
|
|
642
|
+
* THE FRAME A MOTION IS AUTHORED AGAINST.
|
|
643
|
+
*
|
|
644
|
+
* app.exportFrameRect() is the export frame. On a bounded canvas that is the
|
|
645
|
+
* artboard. On an UNBOUNDED canvas (prod's default) it is the region the items
|
|
646
|
+
* currently occupy, padded: with one actor on the canvas it is the actor
|
|
647
|
+
* itself, so choreograph called every move "off screen" and the morph camera
|
|
648
|
+
* framed against the hero alone. An unbounded canvas has no fixed frame, so
|
|
649
|
+
* motion is checked against its nominal artboard instead, flagged unbounded so
|
|
650
|
+
* the caller can say so.
|
|
651
|
+
*/
|
|
652
|
+
function authoringFrameJs(name) {
|
|
653
|
+
return `const ${name} = (function() {
|
|
654
|
+
const cs = app.getCanvasSize && app.getCanvasSize();
|
|
655
|
+
if (cs && cs.unbounded && cs.width > 0 && cs.height > 0) return { x: 0, y: 0, width: cs.width, height: cs.height, unbounded: true };
|
|
656
|
+
return (typeof app.exportFrameRect === 'function' && app.exportFrameRect()) || cs || { width: 1920, height: 1080 };
|
|
657
|
+
})();`;
|
|
658
|
+
}
|
|
659
|
+
const TEXT_STYLE_KEYS = ['fontWeight', 'fontStyle', 'leading', 'lineHeight', 'textAlign', 'align', 'text'];
|
|
630
660
|
function emitTextStyle(itemExpr, props) {
|
|
631
661
|
const weight = props.fontWeight;
|
|
632
662
|
const italic = props.fontStyle === 'italic' || props.fontStyle === 'oblique';
|
|
@@ -711,10 +741,11 @@ function resolveChartFields(chartType, data, options) {
|
|
|
711
741
|
* same way for the same reason. A 1-microsecond tolerance makes a hand-off at
|
|
712
742
|
* an exact frame time show exactly one item.
|
|
713
743
|
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
744
|
+
* The callback is the FALLBACK, for a studio without app.setLifetime: it is
|
|
745
|
+
* not saved, so a reopened scene keeps data.bornAt / ttl (the cloud renderer
|
|
746
|
+
* reads those) but not the hiding. app.setLifetime persists; the result's
|
|
747
|
+
* lifetime field says which ran. Lifetimes applied the old way are migrated:
|
|
748
|
+
* their tagged opacity keys are removed.
|
|
718
749
|
*/
|
|
719
750
|
function emitLifetime(itemExpr, bornAt, ttl) {
|
|
720
751
|
const b = Number(bornAt);
|
|
@@ -793,6 +824,27 @@ function emitLifetime(itemExpr, bornAt, ttl) {
|
|
|
793
824
|
*/
|
|
794
825
|
function generateModifyItemCode(itemId, properties, dataFlags) {
|
|
795
826
|
const { contentType, contentFormat, countdownTarget, countdownEndText, ...restProperties } = properties;
|
|
827
|
+
// Aliases for text items: text -> content (B66), textAlign / align -> justification (B89).
|
|
828
|
+
// A key consumed here was applied, so the unread-key report below must not
|
|
829
|
+
// call it "no effect".
|
|
830
|
+
const aliased = new Set();
|
|
831
|
+
if (restProperties.justification === undefined) {
|
|
832
|
+
if (restProperties.textAlign !== undefined) {
|
|
833
|
+
restProperties.justification = restProperties.textAlign;
|
|
834
|
+
delete restProperties.textAlign;
|
|
835
|
+
aliased.add('textAlign');
|
|
836
|
+
}
|
|
837
|
+
else if (restProperties.align !== undefined) {
|
|
838
|
+
restProperties.justification = restProperties.align;
|
|
839
|
+
delete restProperties.align;
|
|
840
|
+
aliased.add('align');
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (restProperties.content === undefined && restProperties.text !== undefined) {
|
|
844
|
+
restProperties.content = restProperties.text;
|
|
845
|
+
delete restProperties.text;
|
|
846
|
+
aliased.add('text');
|
|
847
|
+
}
|
|
796
848
|
// A new fit box is applied here (emitTextFit, with the hold carried over),
|
|
797
849
|
// not by modifyItem: the engine's modify fits it with no hold, overwriting
|
|
798
850
|
// the one this reads back. fit: null still goes through — that is the
|
|
@@ -928,7 +980,7 @@ if (_flagged && _flagged.item && _flagged.item.data) Object.assign(_flagged.item
|
|
|
928
980
|
// AN UNREAD KEY HAD NO EFFECT, AND THE CALL SAID SUCCESS. {scaling: 8} on a
|
|
929
981
|
// raster answered success and left it 16 px; the engine reads `scale`. Same
|
|
930
982
|
// report create_item gives, against the engine's modify path (generated).
|
|
931
|
-
const modIgnored = Object.keys(properties).filter((k) => !MODIFY_KNOWN_KEYS.has(k));
|
|
983
|
+
const modIgnored = Object.keys(properties).filter((k) => !MODIFY_KNOWN_KEYS.has(k) && !aliased.has(k));
|
|
932
984
|
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.`
|
|
933
985
|
+ modIgnored.filter((k) => MODIFY_DID_YOU_MEAN[k]).map((k) => ` Did you mean ${MODIFY_DID_YOU_MEAN[k]} (not ${k})?`).join(''))}`;
|
|
934
986
|
code += `
|
|
@@ -1225,10 +1277,22 @@ app.animate(item, {
|
|
|
1225
1277
|
*/
|
|
1226
1278
|
function normalizeKeyframePositions(keyframes) {
|
|
1227
1279
|
return (keyframes || []).map((kf) => {
|
|
1228
|
-
const
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1280
|
+
const rawProps = kf?.properties;
|
|
1281
|
+
const props = rawProps && typeof rawProps === 'object' ? rawProps : {};
|
|
1282
|
+
const p = props.position ?? kf?.position;
|
|
1283
|
+
if (p) {
|
|
1284
|
+
if (Array.isArray(p) && p.length >= 2) {
|
|
1285
|
+
return { ...kf, properties: { ...props, position: [p[0], p[1]] } };
|
|
1286
|
+
}
|
|
1287
|
+
if (typeof p === 'object' && 'x' in p && 'y' in p) {
|
|
1288
|
+
return { ...kf, properties: { ...props, position: [p.x, p.y] } };
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (kf?.x !== undefined && kf?.y !== undefined && props.position === undefined && props.x === undefined && props.y === undefined) {
|
|
1292
|
+
return { ...kf, properties: { ...props, position: [kf.x, kf.y] } };
|
|
1293
|
+
}
|
|
1294
|
+
if (rawProps === undefined && Object.keys(props).length > 0) {
|
|
1295
|
+
return { ...kf, properties: props };
|
|
1232
1296
|
}
|
|
1233
1297
|
return kf;
|
|
1234
1298
|
});
|
|
@@ -1251,9 +1315,14 @@ function normalizeKeyframePositions(keyframes) {
|
|
|
1251
1315
|
* "item_7 is not on the canvas" is actionable, silence is not.
|
|
1252
1316
|
*/
|
|
1253
1317
|
function requireItem(itemId, what) {
|
|
1318
|
+
const isRef = typeof itemId === 'string' && itemId.startsWith('$');
|
|
1319
|
+
const targetExpr = isRef
|
|
1320
|
+
? `((typeof window !== 'undefined' && window.__pinepaper_batch_item_ids && window.__pinepaper_batch_item_ids[${itemId.substring(1)}]) || '${itemId}')`
|
|
1321
|
+
: `'${itemId}'`;
|
|
1254
1322
|
return `
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1323
|
+
const _targetId = ${targetExpr};
|
|
1324
|
+
const _target = (app.getItemById && app.getItemById(_targetId))
|
|
1325
|
+
|| (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(_targetId));
|
|
1257
1326
|
if (!_target) {
|
|
1258
1327
|
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.' };
|
|
1259
1328
|
}`.trim();
|
|
@@ -1287,7 +1356,7 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1287
1356
|
// is the feature check: a key it names for audio passes through, anything
|
|
1288
1357
|
// else on an audio item is still refused (prod, until that build ships).
|
|
1289
1358
|
const audioLevelGuard = levelKeys.length === 0 ? '' : `
|
|
1290
|
-
const __ae = app.itemRegistry && app.itemRegistry.get(
|
|
1359
|
+
const __ae = app.itemRegistry && app.itemRegistry.get(_targetId);
|
|
1291
1360
|
let __audioKeyed = [];
|
|
1292
1361
|
try {
|
|
1293
1362
|
const __l = typeof app.listAnimatableProperties === 'function' ? app.listAnimatableProperties() : null;
|
|
@@ -1295,7 +1364,7 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1295
1364
|
} catch (_) { /* treat as unsupported */ }
|
|
1296
1365
|
const __unsupported = ${JSON.stringify(levelKeys)}.filter(function(k) { return __audioKeyed.indexOf(k) === -1; });
|
|
1297
1366
|
if (__ae && __ae.type === 'audio' && __unsupported.length) {
|
|
1298
|
-
return { success: false, itemId:
|
|
1367
|
+
return { success: false, itemId: _targetId, error: __unsupported.join(', ') + ' cannot be keyframed on an audio item in this studio: it mixes each audio clip at one fixed level, so a fade would be accepted and not heard. Nothing was added. '
|
|
1299
1368
|
+ (__audioKeyed.length ? 'Keyable on audio here: ' + __audioKeyed.join(', ') + '. ' : '')
|
|
1300
1369
|
+ 'Set a static level with pinepaper_modify_item {audioGain: 0..1}.' };
|
|
1301
1370
|
}`;
|
|
@@ -1328,6 +1397,10 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1328
1397
|
const easingNote = first?.easing && first.easing !== 'linear'
|
|
1329
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.`)}`
|
|
1330
1399
|
: '';
|
|
1400
|
+
const unsupportedKeys = [...new Set(keyframes.flatMap((k) => Object.keys(k.properties ?? {})))]
|
|
1401
|
+
.filter((k) => k === 'letterSpacing');
|
|
1402
|
+
const letterSpacingWarning = unsupportedKeys.length === 0 ? '' : `, ignoredProperties: ${JSON.stringify(unsupportedKeys)}, warning: ${JSON.stringify(`${unsupportedKeys.join(', ')} is not an animatable property in this engine (app.listAnimatableProperties does not list it), so it had no effect. Set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX.`)}`;
|
|
1403
|
+
const addAnimTarget = itemId.startsWith('$') ? '_targetId' : `'${itemId}'`;
|
|
1331
1404
|
return `
|
|
1332
1405
|
// Apply keyframe animation to ${itemId}
|
|
1333
1406
|
(function() {
|
|
@@ -1337,7 +1410,7 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1337
1410
|
// merge support — an older engine takes the option and replaces anyway.
|
|
1338
1411
|
const __ki = _target && (_target.item || _target);
|
|
1339
1412
|
const __prev = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];
|
|
1340
|
-
app.addAnimation(
|
|
1413
|
+
app.addAnimation(${addAnimTarget}, ${keyframesJson}, ${JSON.stringify(opts)});
|
|
1341
1414
|
const __now = (__ki && __ki.data && Array.isArray(__ki.data.keyframes)) ? __ki.data.keyframes.map(function(k) { return k && k.time; }) : [];
|
|
1342
1415
|
const __kept = __prev.every(function(t) { return __now.indexOf(t) >= 0; });${append ? `
|
|
1343
1416
|
// Property gaps across the merged track (the same check a single call makes
|
|
@@ -1350,7 +1423,7 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
1350
1423
|
? { appended: true, previousKeys: __prev.length, keys: __now.length }
|
|
1351
1424
|
: { 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.' })`
|
|
1352
1425
|
: `{ 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.' }`};
|
|
1353
|
-
return { success: true, itemId:
|
|
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}${letterSpacingWarning} };
|
|
1354
1427
|
})();
|
|
1355
1428
|
`.trim();
|
|
1356
1429
|
}
|
|
@@ -1896,6 +1969,54 @@ export function isCoordinateBuilt(itemType, props) {
|
|
|
1896
1969
|
return false;
|
|
1897
1970
|
}
|
|
1898
1971
|
}
|
|
1972
|
+
/** A keyframe's properties, with the flat spellings (x, opacity, ...) lifted in beside `properties`. */
|
|
1973
|
+
function extractKfPropsMap(kf) {
|
|
1974
|
+
const props = (kf && kf.properties && typeof kf.properties === 'object') ? { ...kf.properties } : {};
|
|
1975
|
+
if (kf) {
|
|
1976
|
+
const flat = kf;
|
|
1977
|
+
for (const key of ['x', 'y', 'opacity', 'scale', 'scaleX', 'scaleY', 'rotation', 'position', 'fill', 'fillColor', 'stroke', 'strokeColor', 'width', 'height', 'radius', 'lineHeight']) {
|
|
1978
|
+
if (flat[key] !== undefined && props[key] === undefined)
|
|
1979
|
+
props[key] = flat[key];
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return props;
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* One op's track sampled at time t: held before its first key and after its last, linear in between
|
|
1986
|
+
* (numbers and equal-length number arrays; anything else holds until the next key is reached).
|
|
1987
|
+
*/
|
|
1988
|
+
function sampleOpTrack(kfs, t) {
|
|
1989
|
+
if (!kfs.length)
|
|
1990
|
+
return {};
|
|
1991
|
+
if (kfs.length === 1 || t <= kfs[0].time)
|
|
1992
|
+
return extractKfPropsMap(kfs[0]);
|
|
1993
|
+
if (t >= kfs[kfs.length - 1].time)
|
|
1994
|
+
return extractKfPropsMap(kfs[kfs.length - 1]);
|
|
1995
|
+
let i = 0;
|
|
1996
|
+
for (let j = 0; j < kfs.length - 1; j++) {
|
|
1997
|
+
if (t >= kfs[j].time && t <= kfs[j + 1].time) {
|
|
1998
|
+
i = j;
|
|
1999
|
+
break;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const k0 = kfs[i], k1 = kfs[i + 1];
|
|
2003
|
+
const p0 = extractKfPropsMap(k0), p1 = extractKfPropsMap(k1);
|
|
2004
|
+
const f = Math.max(0, Math.min(1, (t - k0.time) / (k1.time - k0.time || 1)));
|
|
2005
|
+
const out = {};
|
|
2006
|
+
for (const k of new Set([...Object.keys(p0), ...Object.keys(p1)])) {
|
|
2007
|
+
const v0 = p0[k], v1 = p1[k];
|
|
2008
|
+
if (typeof v0 === 'number' && typeof v1 === 'number') {
|
|
2009
|
+
out[k] = v0 + (v1 - v0) * f;
|
|
2010
|
+
}
|
|
2011
|
+
else if (Array.isArray(v0) && Array.isArray(v1) && v0.length === v1.length && typeof v0[0] === 'number') {
|
|
2012
|
+
out[k] = v0.map((n, idx) => Number(n) + (Number(v1[idx]) - Number(n)) * f);
|
|
2013
|
+
}
|
|
2014
|
+
else {
|
|
2015
|
+
out[k] = (v1 !== undefined && f >= 1) ? v1 : (v0 !== undefined ? v0 : v1);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return out;
|
|
2019
|
+
}
|
|
1899
2020
|
/**
|
|
1900
2021
|
* app.addAnimation() REPLACES data.keyframes wholesale, so two
|
|
1901
2022
|
* keyframe_animate ops targeting one item meant the first track was silently
|
|
@@ -1923,28 +2044,34 @@ export function planKeyframeMerges(operations) {
|
|
|
1923
2044
|
if (indices.length < 2)
|
|
1924
2045
|
continue;
|
|
1925
2046
|
const first = indices[0];
|
|
1926
|
-
// Union keyframes by time; later ops shallow-merge over earlier ones.
|
|
1927
|
-
const byTime = new Map();
|
|
1928
2047
|
let duration = 0;
|
|
1929
2048
|
let loop = false;
|
|
1930
2049
|
for (const i of indices) {
|
|
1931
2050
|
const op = operations[i];
|
|
1932
|
-
for (const kf of op.keyframes || []) {
|
|
1933
|
-
const prev = byTime.get(kf.time);
|
|
1934
|
-
byTime.set(kf.time, {
|
|
1935
|
-
time: kf.time,
|
|
1936
|
-
properties: { ...(prev?.properties || {}), ...(kf.properties || {}) },
|
|
1937
|
-
...((kf.easing ?? prev?.easing) !== undefined ? { easing: kf.easing ?? prev?.easing } : {}),
|
|
1938
|
-
});
|
|
1939
|
-
}
|
|
1940
2051
|
const opMax = op.keyframes?.length ? Math.max(...op.keyframes.map(k => k.time)) : 0;
|
|
1941
2052
|
duration = Math.max(duration, op.duration || opMax);
|
|
1942
2053
|
loop = loop || op.loop === true;
|
|
1943
2054
|
if (i !== first)
|
|
1944
2055
|
plan.foldedInto.set(i, first);
|
|
1945
2056
|
}
|
|
2057
|
+
// B120: at every key time of ANY op, each op contributes its own track SAMPLED there (held outside
|
|
2058
|
+
// its keys, interpolated between them), later ops winning. A plain union by timestamp left a key
|
|
2059
|
+
// holding only one op's properties, so the engine broke the other op's channel at that key.
|
|
2060
|
+
const allTimes = [...new Set(indices.flatMap((i) => (operations[i].keyframes || []).map((k) => k.time)))].sort((a, b) => a - b);
|
|
2061
|
+
const mergedKeyframes = allTimes.map((t) => {
|
|
2062
|
+
let properties = {};
|
|
2063
|
+
let easing;
|
|
2064
|
+
for (const i of indices) {
|
|
2065
|
+
const kfs = operations[i].keyframes || [];
|
|
2066
|
+
properties = { ...properties, ...sampleOpTrack(kfs, t) };
|
|
2067
|
+
const exact = kfs.find((k) => k.time === t);
|
|
2068
|
+
if (exact && exact.easing !== undefined)
|
|
2069
|
+
easing = exact.easing;
|
|
2070
|
+
}
|
|
2071
|
+
return { time: t, properties, ...(easing !== undefined ? { easing } : {}) };
|
|
2072
|
+
});
|
|
1946
2073
|
plan.merged.set(first, {
|
|
1947
|
-
keyframes:
|
|
2074
|
+
keyframes: mergedKeyframes,
|
|
1948
2075
|
duration: duration || 5,
|
|
1949
2076
|
loop,
|
|
1950
2077
|
});
|
|
@@ -3959,8 +4086,19 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3959
4086
|
|| (typeof v.reason === 'string' && v.reason)
|
|
3960
4087
|
|| 'the operation reported failure without naming a reason';
|
|
3961
4088
|
};
|
|
3962
|
-
|
|
4089
|
+
// $N is the Nth item THIS batch created (the documented contract). The
|
|
4090
|
+
// previous batch's ids are kept only to name the right id when a $N reaches
|
|
4091
|
+
// past what this batch has made: picking it silently shifted every $N in a
|
|
4092
|
+
// batch that also creates (B115 follow-up).
|
|
3963
4093
|
const itemIds = [];
|
|
4094
|
+
const __prevBatchIds = ((typeof window !== 'undefined' && window.__pinepaper_batch_item_ids) || []).slice();
|
|
4095
|
+
const __ref = function (n) {
|
|
4096
|
+
if (itemIds[n] !== undefined) return itemIds[n];
|
|
4097
|
+
const prev = __prevBatchIds[n];
|
|
4098
|
+
throw new Error('$' + n + ' is the item number ' + n + ' THIS batch created, and it has created ' + itemIds.length + ' so far.'
|
|
4099
|
+
+ (prev ? ' In the previous batch $' + n + ' was ' + prev + ' — pass that id instead.' : ' Put the create ops first, or pass the item id.'));
|
|
4100
|
+
};
|
|
4101
|
+
const results = [];
|
|
3964
4102
|
let success = true;
|
|
3965
4103
|
|
|
3966
4104
|
try {
|
|
@@ -4016,11 +4154,19 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4016
4154
|
// 60-op batch should not have to scan sixty entries to learn that op 41
|
|
4017
4155
|
// could not find a preset.
|
|
4018
4156
|
const _failed = results.filter((r) => !r.success);
|
|
4157
|
+
// A batch that created nothing keeps the previous batch's ids on record.
|
|
4158
|
+
if (typeof window !== 'undefined' && itemIds.length) {
|
|
4159
|
+
window.__pinepaper_batch_item_ids = itemIds;
|
|
4160
|
+
}
|
|
4019
4161
|
return { success, itemIds, results, operationCount: ${operations.length},
|
|
4020
4162
|
failedCount: _failed.length,
|
|
4021
4163
|
...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; ') } : {}),
|
|
4022
4164
|
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
4023
4165
|
} catch (e) {
|
|
4166
|
+
// A batch that created nothing keeps the previous batch's ids on record.
|
|
4167
|
+
if (typeof window !== 'undefined' && itemIds.length) {
|
|
4168
|
+
window.__pinepaper_batch_item_ids = itemIds;
|
|
4169
|
+
}
|
|
4024
4170
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
4025
4171
|
return { success: false, error: e.message, itemIds, results, operationCount: ${operations.length}, canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
4026
4172
|
}
|
|
@@ -4043,13 +4189,40 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
4043
4189
|
// for a 12 s MP4 (round 8 DD, 1.67). It now runs generateCreateItemCode,
|
|
4044
4190
|
// the exact emitter create_item uses, adapted to return its result
|
|
4045
4191
|
// inside this op's function.
|
|
4046
|
-
const
|
|
4192
|
+
const anyOp = op;
|
|
4193
|
+
let pos = op.position;
|
|
4194
|
+
if (!pos && (typeof anyOp.x === 'number' || typeof anyOp.y === 'number')) {
|
|
4195
|
+
pos = { x: anyOp.x ?? 400, y: anyOp.y ?? 300 };
|
|
4196
|
+
}
|
|
4197
|
+
if (!pos) {
|
|
4198
|
+
pos = { x: 400, y: 300 };
|
|
4199
|
+
}
|
|
4047
4200
|
const opProps = { ...(op.properties || {}) };
|
|
4201
|
+
const batchKeys = new Set([
|
|
4202
|
+
'type', 'itemType', 'position', 'properties', 'screenSpace', 'stepTiming', 'counter',
|
|
4203
|
+
'itemId', 'itemIds', 'groupName', 'animationType', 'animationOptions',
|
|
4204
|
+
'sourceId', 'targetId', 'relationType', 'relationOptions',
|
|
4205
|
+
'backgroundColor', 'generatorName', 'generatorParams', 'generatorRegion',
|
|
4206
|
+
'width', 'height', 'preset', 'keyframes', 'duration', 'loop',
|
|
4207
|
+
'maskPreset', 'maskType', 'maskOptions', 'effectType', 'effectParams',
|
|
4208
|
+
'action', 'time', 'append'
|
|
4209
|
+
]);
|
|
4210
|
+
for (const [k, v] of Object.entries(anyOp)) {
|
|
4211
|
+
if (!batchKeys.has(k) && v !== undefined && opProps[k] === undefined) {
|
|
4212
|
+
opProps[k] = v;
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
if (opProps.content === undefined && opProps.text !== undefined) {
|
|
4216
|
+
opProps.content = opProps.text;
|
|
4217
|
+
}
|
|
4218
|
+
if (opProps.fillColor === undefined && opProps.color !== undefined) {
|
|
4219
|
+
opProps.fillColor = opProps.color;
|
|
4220
|
+
}
|
|
4048
4221
|
const opSetters = takeItemSetters(opProps, { screenSpace: op.screenSpace, stepTiming: op.stepTiming, counter: op.counter });
|
|
4049
4222
|
const createProps = withRadiusAxes(opProps);
|
|
4050
4223
|
const opBrand = { ...opProps };
|
|
4051
4224
|
delete opProps.noBrand;
|
|
4052
|
-
const returning = asReturningBody(withBrandDefaults(generateCreateItemCode(op.itemType, pos, { ...opProps }, undefined, op.position !== undefined), op.itemType, opBrand));
|
|
4225
|
+
const returning = asReturningBody(withBrandDefaults(generateCreateItemCode(op.itemType, pos, { ...opProps }, undefined, op.position !== undefined || typeof anyOp.x === 'number' || typeof anyOp.y === 'number'), op.itemType, opBrand));
|
|
4053
4226
|
let createCode = `${hasItemSetters(opSetters) ? `\n${itemSettersPreflightJs(opSetters)}` : ''}
|
|
4054
4227
|
const __created = await (async function() {
|
|
4055
4228
|
${returning}
|
|
@@ -4064,7 +4237,7 @@ ${itemSettersApplyJs(opSetters, '__created.itemId', '__created')}
|
|
|
4064
4237
|
// caller explicitly asked for a position. Guarded on an explicit
|
|
4065
4238
|
// op.position so absolute-coordinate geometry (the common case, which
|
|
4066
4239
|
// passes no position) is never snapped to the injected default.
|
|
4067
|
-
if (op.position && isCoordinateBuilt(op.itemType, createProps)) {
|
|
4240
|
+
if ((op.position || typeof anyOp.x === 'number' || typeof anyOp.y === 'number') && isCoordinateBuilt(op.itemType, createProps)) {
|
|
4068
4241
|
createCode += `
|
|
4069
4242
|
// create() builds this item type from its own coordinates and ignores
|
|
4070
4243
|
// params.position — apply the caller's explicit position after the fact.
|
|
@@ -4078,7 +4251,7 @@ return __created;
|
|
|
4078
4251
|
}
|
|
4079
4252
|
case 'modify': {
|
|
4080
4253
|
const itemRef = op.itemId?.startsWith('$')
|
|
4081
|
-
? `
|
|
4254
|
+
? `__ref(${op.itemId.substring(1)})`
|
|
4082
4255
|
: `'${op.itemId}'`;
|
|
4083
4256
|
// modify_item's own emitter, as create is create_item's (see above):
|
|
4084
4257
|
// the batch's modifyItem call missed text styles, font loading, audio
|
|
@@ -4099,7 +4272,7 @@ return __r;
|
|
|
4099
4272
|
}
|
|
4100
4273
|
case 'animate':
|
|
4101
4274
|
const animItemRef = op.itemId?.startsWith('$')
|
|
4102
|
-
? `
|
|
4275
|
+
? `__ref(${op.itemId.substring(1)})`
|
|
4103
4276
|
: `'${op.itemId}'`;
|
|
4104
4277
|
const animOpts = JSON.stringify(op.animationOptions || {});
|
|
4105
4278
|
return `
|
|
@@ -4111,10 +4284,10 @@ return { itemId: targetId, animationType: '${op.animationType}' };
|
|
|
4111
4284
|
`;
|
|
4112
4285
|
case 'relation':
|
|
4113
4286
|
const srcRef = op.sourceId?.startsWith('$')
|
|
4114
|
-
? `
|
|
4287
|
+
? `__ref(${op.sourceId.substring(1)})`
|
|
4115
4288
|
: `'${op.sourceId}'`;
|
|
4116
4289
|
const tgtRef = op.targetId?.startsWith('$')
|
|
4117
|
-
? `
|
|
4290
|
+
? `__ref(${op.targetId.substring(1)})`
|
|
4118
4291
|
: `'${op.targetId}'`;
|
|
4119
4292
|
const relOpts = JSON.stringify(op.relationOptions || {});
|
|
4120
4293
|
return `
|
|
@@ -4129,7 +4302,7 @@ return { relationId, sourceId, targetId, relationType: '${op.relationType}' };
|
|
|
4129
4302
|
`;
|
|
4130
4303
|
case 'delete':
|
|
4131
4304
|
const delRef = op.itemId?.startsWith('$')
|
|
4132
|
-
? `
|
|
4305
|
+
? `__ref(${op.itemId.substring(1)})`
|
|
4133
4306
|
: `'${op.itemId}'`;
|
|
4134
4307
|
return `
|
|
4135
4308
|
const targetId = ${delRef};
|
|
@@ -4141,7 +4314,7 @@ return { itemId: targetId, deleted: true };
|
|
|
4141
4314
|
`;
|
|
4142
4315
|
case 'group': {
|
|
4143
4316
|
// Resolve each ref ($N → itemIds[N]; literal → quoted id) into a JS array.
|
|
4144
|
-
const refs = (op.itemIds || []).map((ref) => ref?.startsWith('$') ? `
|
|
4317
|
+
const refs = (op.itemIds || []).map((ref) => ref?.startsWith('$') ? `__ref(${ref.substring(1)})` : `'${ref}'`);
|
|
4145
4318
|
const idsArrayExpr = `[${refs.join(', ')}]`;
|
|
4146
4319
|
const nameExpr = op.groupName !== undefined ? JSON.stringify(op.groupName) : 'undefined';
|
|
4147
4320
|
// Returns { groupId, ... } (no itemId) so it does NOT shift $N indices — like relation.
|
|
@@ -4151,7 +4324,7 @@ return (function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
|
|
|
4151
4324
|
`;
|
|
4152
4325
|
}
|
|
4153
4326
|
case 'set_background':
|
|
4154
|
-
const bgColor = op.backgroundColor || op.properties?.color || '#000000';
|
|
4327
|
+
const bgColor = op.backgroundColor || op.color || op.properties?.backgroundColor || op.properties?.color || '#000000';
|
|
4155
4328
|
return `
|
|
4156
4329
|
app.setBackgroundColor('${bgColor}');
|
|
4157
4330
|
return { success: true, backgroundColor: '${bgColor}' };
|
|
@@ -4185,28 +4358,54 @@ return { success: true, width: (_cs && _cs.width) || ${w}, height: (_cs && _cs.h
|
|
|
4185
4358
|
return `
|
|
4186
4359
|
// Keyframes merged into operation ${foldTarget} (same target). A keyframe track
|
|
4187
4360
|
// is replaced wholesale, so emitting both here would silently drop one.
|
|
4188
|
-
return { itemId: ${op.itemId?.startsWith('$') ? `
|
|
4361
|
+
return { itemId: ${op.itemId?.startsWith('$') ? `__ref(${op.itemId.substring(1)})` : `'${op.itemId}'`}, merged: true, mergedInto: ${foldTarget} };
|
|
4189
4362
|
`;
|
|
4190
4363
|
}
|
|
4191
4364
|
const mergedPayload = mergePlan?.merged.get(index);
|
|
4192
|
-
const
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4365
|
+
const rawItemId = String(op.itemId ?? op.id ?? op.properties?.itemId ?? op.properties?.id);
|
|
4366
|
+
const kfItemRef = rawItemId?.startsWith('$')
|
|
4367
|
+
? `__ref(${rawItemId.substring(1)})`
|
|
4368
|
+
: `'${rawItemId}'`;
|
|
4369
|
+
const kfSource = mergedPayload
|
|
4370
|
+
? mergedPayload.keyframes
|
|
4371
|
+
: (op.keyframes || (op.properties && op.properties.keyframes) || (op.params && op.params.keyframes) || []);
|
|
4196
4372
|
const kfJson = JSON.stringify(normalizeKeyframePositions(kfSource));
|
|
4197
4373
|
const kfDuration = mergedPayload
|
|
4198
4374
|
? mergedPayload.duration
|
|
4199
|
-
: (op.duration ||
|
|
4200
|
-
const kfLoop = mergedPayload ? mergedPayload.loop : (op.loop ?? false);
|
|
4375
|
+
: (op.duration || op.properties?.duration || (kfSource.length ? Math.max(...kfSource.map((k) => Number(k.time) || 0)) : 5));
|
|
4376
|
+
const kfLoop = mergedPayload ? mergedPayload.loop : (op.loop ?? op.properties?.loop ?? false);
|
|
4377
|
+
// Same contract as pinepaper_keyframe_animate: replace unless append.
|
|
4378
|
+
const kfAppend = op.append === true || op.properties?.append === true;
|
|
4379
|
+
// Batch ops are passthrough, so these arrive unvalidated: numbers only, and timeUnits from its enum.
|
|
4380
|
+
const kfExtra = [];
|
|
4381
|
+
for (const key of ['timeOffset', 'clipInPoint', 'clipOutPoint']) {
|
|
4382
|
+
const v = Number(op[key]);
|
|
4383
|
+
if (op[key] !== undefined && Number.isFinite(v))
|
|
4384
|
+
kfExtra.push(`${key}: ${v}`);
|
|
4385
|
+
}
|
|
4386
|
+
const kfUnits = op.timeUnits;
|
|
4387
|
+
if (kfUnits === 'seconds' || kfUnits === 'ms')
|
|
4388
|
+
kfExtra.push(`timeUnits: '${kfUnits}'`);
|
|
4389
|
+
const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
|
|
4390
|
+
const kfUnsupported = [...new Set((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})))]
|
|
4391
|
+
.filter((k) => k === 'letterSpacing');
|
|
4392
|
+
const kfIgnored = kfUnsupported.length === 0 ? '' : `, ignoredProperties: ${JSON.stringify(kfUnsupported)}, warning: ${JSON.stringify(`${kfUnsupported.join(', ')} is not an animatable property in this engine (app.listAnimatableProperties does not list it), so it had no effect. Set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX.`)}`;
|
|
4201
4393
|
return `
|
|
4202
4394
|
const targetId = ${kfItemRef};
|
|
4203
|
-
|
|
4204
|
-
|
|
4395
|
+
const _target = (app.getItemById && app.getItemById(targetId))
|
|
4396
|
+
|| (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(targetId));
|
|
4397
|
+
if (!_target) {
|
|
4398
|
+
return { success: false, itemId: targetId, error: 'Item not found: ' + targetId };
|
|
4399
|
+
}
|
|
4400
|
+
const _kfItem = _target.item || _target;
|
|
4401
|
+
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} });
|
|
4403
|
+
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} };
|
|
4205
4404
|
`;
|
|
4206
4405
|
}
|
|
4207
4406
|
case 'apply_mask': {
|
|
4208
4407
|
const maskItemRef = op.itemId?.startsWith('$')
|
|
4209
|
-
? `
|
|
4408
|
+
? `__ref(${op.itemId.substring(1)})`
|
|
4210
4409
|
: `'${op.itemId}'`;
|
|
4211
4410
|
const maskOpts = JSON.stringify(op.maskOptions || {});
|
|
4212
4411
|
const preset = op.maskPreset || op.maskType || 'wipeLeft';
|
|
@@ -4225,7 +4424,7 @@ return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
|
|
|
4225
4424
|
}
|
|
4226
4425
|
case 'apply_effect': {
|
|
4227
4426
|
const effItemRef = op.itemId?.startsWith('$')
|
|
4228
|
-
? `
|
|
4427
|
+
? `__ref(${op.itemId.substring(1)})`
|
|
4229
4428
|
: `'${op.itemId}'`;
|
|
4230
4429
|
const effParams = JSON.stringify(op.effectParams || {});
|
|
4231
4430
|
return `
|
|
@@ -4610,34 +4809,7 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4610
4809
|
// warnings:[] plus a note when it checked and found nothing, and the
|
|
4611
4810
|
// warnings themselves otherwise. The note carries the caveat that absence
|
|
4612
4811
|
// was standing in for.
|
|
4613
|
-
|
|
4614
|
-
// STAND-INS ARE THIS TOOL'S OWN DOING, SO THIS TOOL REPORTS THEM. create_item
|
|
4615
|
-
// draws 'shader' and 'field' locally as a flat plate tagged data.renderAs; the
|
|
4616
|
-
// engine's check sees an ordinary rectangle and cannot know a surface was
|
|
4617
|
-
// meant. So a local export of a sea came out a flat blue slab with nothing
|
|
4618
|
-
// said. Reported whatever the engine check can or cannot do.
|
|
4619
|
-
function standIns() {
|
|
4620
|
-
try {
|
|
4621
|
-
// Probed with === : a studio without it degrades to "no stand-ins".
|
|
4622
|
-
const canList = app.itemRegistry && typeof app.itemRegistry.getAll === 'function';
|
|
4623
|
-
if (!canList) return [];
|
|
4624
|
-
const found = {};
|
|
4625
|
-
for (const entry of app.itemRegistry.getAll()) {
|
|
4626
|
-
const kind = entry && entry.item && entry.item.data && entry.item.data.renderAs;
|
|
4627
|
-
if (kind === 'shader' || kind === 'field') (found[kind] = found[kind] || []).push(entry.itemId);
|
|
4628
|
-
}
|
|
4629
|
-
return Object.keys(found).map(function(kind) {
|
|
4630
|
-
const ids = found[kind];
|
|
4631
|
-
return {
|
|
4632
|
-
code: 'render_time_surface_stand_in',
|
|
4633
|
-
message: ids.length + ' ' + kind + ' item' + (ids.length > 1 ? 's are' : ' is')
|
|
4634
|
-
+ ' exported as the flat plate that stands in for ' + (ids.length > 1 ? 'them' : 'it')
|
|
4635
|
-
+ ' on a local canvas. The surface itself is drawn only by a cloud render.',
|
|
4636
|
-
items: ids.slice(0, 6),
|
|
4637
|
-
};
|
|
4638
|
-
});
|
|
4639
|
-
} catch (e) { return []; }
|
|
4640
|
-
}
|
|
4812
|
+
|
|
4641
4813
|
// TRANSPARENCY THE FORMAT CANNOT CARRY. A scene with no background exported
|
|
4642
4814
|
// to webm came out on BLACK while the engine's check said it lost nothing
|
|
4643
4815
|
// (round 7, 8.12). Only said when the scene is evidently transparent — no
|
|
@@ -4676,7 +4848,7 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4676
4848
|
} catch (e) { return []; }
|
|
4677
4849
|
}
|
|
4678
4850
|
function fidelity(fmt) {
|
|
4679
|
-
const own =
|
|
4851
|
+
const own = alphaLoss(fmt).concat(textOverflow());
|
|
4680
4852
|
try {
|
|
4681
4853
|
if (!app.exportEngine || typeof app.exportEngine.exportFidelity !== 'function') {
|
|
4682
4854
|
return { fidelity: { available: false, reason: 'this studio cannot check export fidelity — update PinePaper Studio.', ...(own.length ? { warnings: own } : {}) } };
|
|
@@ -9598,6 +9770,181 @@ ${resolve}
|
|
|
9598
9770
|
}
|
|
9599
9771
|
}
|
|
9600
9772
|
// ===========================================================================
|
|
9773
|
+
// HAND-DRAWN SKETCHY ANIMATION & JITTER
|
|
9774
|
+
// ===========================================================================
|
|
9775
|
+
generateHandDrawn(input) {
|
|
9776
|
+
const S = (v) => JSON.stringify(v);
|
|
9777
|
+
// style carries no schema default: with one, `mode` (its alias) was never read.
|
|
9778
|
+
const requested = input.style ?? input.mode ?? 'sketchy';
|
|
9779
|
+
const style = requested === 'ink_reveal' ? 'reveal' : requested;
|
|
9780
|
+
const roughness = input.roughness ?? 2;
|
|
9781
|
+
const bowing = input.bowing ?? 1;
|
|
9782
|
+
const fps = input.fps ?? input.posterizeFps ?? 12;
|
|
9783
|
+
const seed = input.seed ?? 1;
|
|
9784
|
+
const duration = input.duration ?? 2;
|
|
9785
|
+
// WOBBLE AND STOP_MOTION ANSWERED SUCCESS AND EXPORTED NOTHING. The engine's
|
|
9786
|
+
// wobble draws one frame per call (updateHandDrawnWobble(item, frameIndex)),
|
|
9787
|
+
// so a single call is a static jitter; stop_motion is a per-frame timing
|
|
9788
|
+
// helper with no item to apply to, and applyHandDrawnStyle throws for it by
|
|
9789
|
+
// name. The fallback hung an onFrame on the path, which is neither saved
|
|
9790
|
+
// nor exported. The boiling line the studio does save and export is the ink
|
|
9791
|
+
// medium, so the refusal hands over that call with these arguments.
|
|
9792
|
+
if (style === 'wobble' || style === 'stop_motion') {
|
|
9793
|
+
const use = {
|
|
9794
|
+
tool: 'pinepaper_design_medium',
|
|
9795
|
+
args: { action: 'apply', itemId: input.itemId ?? '<itemId>', medium: 'ink', options: { keepColor: true, fps, seed, amplitude: roughness } },
|
|
9796
|
+
};
|
|
9797
|
+
const error = `'${style}' has no animated form this studio saves or exports, so it would have shown in the editor and not in a video. Nothing was changed. For a hand-drawn line that boils frame to frame in exports, call pinepaper_design_medium with the arguments in \`use\` (fps is the boil rate). For a one-off rough line, use style 'sketchy'.`;
|
|
9798
|
+
return `(function() { return { success: false, style: ${S(style)}, error: ${S(error)}, use: ${S(use)} }; })();`;
|
|
9799
|
+
}
|
|
9800
|
+
// REVEAL WAS AN onFrame DASH ANIMATION, which no export plays. A draw-on the
|
|
9801
|
+
// studio saves is a trimEnd 0 -> 1 keyframe track, merged so the item's
|
|
9802
|
+
// other keyframes survive.
|
|
9803
|
+
if (style === 'reveal') {
|
|
9804
|
+
if (!input.itemId) {
|
|
9805
|
+
return `(function() { return { success: false, style: 'reveal', error: 'reveal needs an itemId: it keyframes that item\\'s stroke. Nothing was changed.' }; })();`;
|
|
9806
|
+
}
|
|
9807
|
+
// A trimEnd track on text or an image is kept and draws nothing (measured
|
|
9808
|
+
// on prod: a PointText answered success and never changed), so the item
|
|
9809
|
+
// is checked for a stroke first.
|
|
9810
|
+
const track = generateKeyframeAnimateCode(input.itemId, [
|
|
9811
|
+
{ time: 0, properties: { trimEnd: 0 }, easing: 'linear' },
|
|
9812
|
+
{ time: duration, properties: { trimEnd: 1 }, easing: 'linear' },
|
|
9813
|
+
], duration, false, undefined, undefined, undefined, undefined, true).replace(/;\s*$/, '');
|
|
9814
|
+
return `
|
|
9815
|
+
// Hand-drawn draw-on: a trimEnd track on a stroked path
|
|
9816
|
+
(function() {
|
|
9817
|
+
const found = app.getItemById(${S(input.itemId)}) || (app.itemRegistry && app.itemRegistry.get(${S(input.itemId)}));
|
|
9818
|
+
const it = found && found.item ? found.item : found;
|
|
9819
|
+
const isPath = function(p) { return !!(p && p.segments && p.segments.length); };
|
|
9820
|
+
if (it && !isPath(it) && !(it.children && it.children.some(isPath))) {
|
|
9821
|
+
return { success: false, itemId: ${S(input.itemId)}, error: 'reveal draws a stroke on, and this item has no path to draw (' + (it.className || 'not a path') + '). Nothing was changed. For words that write themselves on, use pinepaper_text_style cursive with animate: true.' };
|
|
9822
|
+
}
|
|
9823
|
+
return (
|
|
9824
|
+
${track}
|
|
9825
|
+
);
|
|
9826
|
+
})();`.trim();
|
|
9827
|
+
}
|
|
9828
|
+
const targetId = input.itemId ? S(input.itemId) : 'null';
|
|
9829
|
+
return `
|
|
9830
|
+
// Hand-drawn sketchy stroke jitter
|
|
9831
|
+
(function() {
|
|
9832
|
+
const targetId = ${targetId};
|
|
9833
|
+
const found = targetId ? (app.getItemById(targetId) || (app.itemRegistry && app.itemRegistry.get(targetId))) : (app.selectedItems && app.selectedItems[0]);
|
|
9834
|
+
const item = found && found.item ? found.item : found;
|
|
9835
|
+
if (!item) {
|
|
9836
|
+
return { success: false, error: 'Target item not found for hand-drawn styling' };
|
|
9837
|
+
}
|
|
9838
|
+
// The registry id, which every other tool takes — not Paper's numeric item.id.
|
|
9839
|
+
const itemId = targetId || (item.data && (item.data.registryId || item.data.id)) || null;
|
|
9840
|
+
|
|
9841
|
+
// 1. The engine's own sketchy jitter.
|
|
9842
|
+
if (typeof HandDrawn !== 'undefined' && HandDrawn && typeof HandDrawn.applyHandDrawnStyle === 'function') {
|
|
9843
|
+
try {
|
|
9844
|
+
HandDrawn.applyHandDrawnStyle(item, 'sketchy', { roughness: ${roughness}, bowing: ${bowing}, seed: ${seed} });
|
|
9845
|
+
if (app._scheduleRepaint) app._scheduleRepaint();
|
|
9846
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
9847
|
+
return { success: true, itemId: itemId, style: 'sketchy', engine: 'HandDrawn' };
|
|
9848
|
+
} catch (e) {
|
|
9849
|
+
console.warn('[HandDrawn] Engine error, falling back to procedural jitter:', e);
|
|
9850
|
+
}
|
|
9851
|
+
}
|
|
9852
|
+
|
|
9853
|
+
if (typeof app.applyHandDrawnJitter === 'function') {
|
|
9854
|
+
app.applyHandDrawnJitter(item, { roughness: ${roughness}, bowing: ${bowing}, seed: ${seed} });
|
|
9855
|
+
if (app._scheduleRepaint) app._scheduleRepaint();
|
|
9856
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
9857
|
+
return { success: true, itemId: itemId, style: 'sketchy', engine: 'app.applyHandDrawnJitter' };
|
|
9858
|
+
}
|
|
9859
|
+
|
|
9860
|
+
// 2. Procedural mulberry32 PRNG vertex jitter fallback
|
|
9861
|
+
function mulberry32(a) {
|
|
9862
|
+
return function() {
|
|
9863
|
+
var t = a += 0x6D2B79F5;
|
|
9864
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
9865
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
9866
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
9867
|
+
};
|
|
9868
|
+
}
|
|
9869
|
+
|
|
9870
|
+
const targets = (item.children && item.children.length) ? item.children : [item];
|
|
9871
|
+
let moved = 0;
|
|
9872
|
+
targets.forEach((t, itemIdx) => {
|
|
9873
|
+
if (!t.segments || !t.segments.length) return;
|
|
9874
|
+
const rand = mulberry32(${seed} + itemIdx * 777);
|
|
9875
|
+
for (let i = 0; i < t.segments.length; i++) {
|
|
9876
|
+
const seg = t.segments[i];
|
|
9877
|
+
seg.point.x += (rand() - 0.5) * ${roughness} * 2;
|
|
9878
|
+
seg.point.y += (rand() - 0.5) * ${roughness} * 2;
|
|
9879
|
+
if (seg.handleIn && (seg.handleIn.x !== 0 || seg.handleIn.y !== 0)) {
|
|
9880
|
+
seg.handleIn.x += (rand() - 0.5) * ${bowing} * 1.5;
|
|
9881
|
+
seg.handleIn.y += (rand() - 0.5) * ${bowing} * 1.5;
|
|
9882
|
+
}
|
|
9883
|
+
if (seg.handleOut && (seg.handleOut.x !== 0 || seg.handleOut.y !== 0)) {
|
|
9884
|
+
seg.handleOut.x += (rand() - 0.5) * ${bowing} * 1.5;
|
|
9885
|
+
seg.handleOut.y += (rand() - 0.5) * ${bowing} * 1.5;
|
|
9886
|
+
}
|
|
9887
|
+
moved++;
|
|
9888
|
+
}
|
|
9889
|
+
});
|
|
9890
|
+
if (!moved) {
|
|
9891
|
+
return { success: false, itemId: itemId, error: 'sketchy moves path points, and this item has none (text or an image?). Nothing was changed.' };
|
|
9892
|
+
}
|
|
9893
|
+
|
|
9894
|
+
if (app._scheduleRepaint) app._scheduleRepaint();
|
|
9895
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
9896
|
+
return { success: true, itemId: itemId, style: 'sketchy', roughness: ${roughness}, points: moved };
|
|
9897
|
+
})();`.trim();
|
|
9898
|
+
}
|
|
9899
|
+
// ===========================================================================
|
|
9900
|
+
// PEN FIGURES
|
|
9901
|
+
// ===========================================================================
|
|
9902
|
+
generatePenFigure(input) {
|
|
9903
|
+
const S = (v) => JSON.stringify(v);
|
|
9904
|
+
const missing = `if (typeof app.listFigures !== 'function' || typeof app.createFigure !== 'function') {
|
|
9905
|
+
return { success: false, error: 'This studio has no pen figures (app.createFigure). Nothing was drawn.' };
|
|
9906
|
+
}`;
|
|
9907
|
+
if (input.action === 'list') {
|
|
9908
|
+
return `
|
|
9909
|
+
// The pen figure catalogue
|
|
9910
|
+
(function() {
|
|
9911
|
+
${missing}
|
|
9912
|
+
const figures = app.listFigures();
|
|
9913
|
+
return { success: true, action: 'list', count: figures.length, figures: figures };
|
|
9914
|
+
})();`.trim();
|
|
9915
|
+
}
|
|
9916
|
+
if (input.figure === undefined) {
|
|
9917
|
+
return `(function() { return { success: false, error: "figure is required: a catalogue id (action 'list' names them) or an inline figure spec. Nothing was drawn." }; })();`;
|
|
9918
|
+
}
|
|
9919
|
+
const opts = {};
|
|
9920
|
+
for (const k of ['box', 'margin', 'duration', 'delay', 'loop', 'pen', 'strokeColor', 'penColor', 'strokeWidth']) {
|
|
9921
|
+
if (input[k] !== undefined)
|
|
9922
|
+
opts[k] = input[k];
|
|
9923
|
+
}
|
|
9924
|
+
// createFigure answers null for an unknown id or a bad spec, and says why
|
|
9925
|
+
// only in a console.warn, which production strips. The refusal is built
|
|
9926
|
+
// here instead, naming the catalogue.
|
|
9927
|
+
return `
|
|
9928
|
+
// Draw a figure with a pen (draws_figure)
|
|
9929
|
+
(function() {
|
|
9930
|
+
${missing}
|
|
9931
|
+
const figure = ${S(input.figure)};
|
|
9932
|
+
const out = app.createFigure(figure, ${S(opts)});
|
|
9933
|
+
if (!out || !out.ink) {
|
|
9934
|
+
const known = app.listFigures().map(function(f) { return f.id; });
|
|
9935
|
+
return { success: false, error: (typeof figure === 'string' && known.indexOf(figure) < 0
|
|
9936
|
+
? 'No figure "' + figure + '". Known: ' + known.join(', ') + '.'
|
|
9937
|
+
: 'The figure spec could not be drawn (it produced no strokes or did not parse).') + ' Nothing was drawn.' };
|
|
9938
|
+
}
|
|
9939
|
+
if (app._scheduleRepaint) app._scheduleRepaint();
|
|
9940
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
9941
|
+
return out.pen
|
|
9942
|
+
? { success: true, inkId: out.ink, penId: out.pen, title: out.title, strokes: out.strokes, duration: ${S(input.duration ?? 6)},
|
|
9943
|
+
note: 'The pen draws the ink over duration seconds (relation draws_figure, pen → ink).' }
|
|
9944
|
+
: { success: true, inkId: out.ink, penId: null, title: out.title, strokes: out.strokes, note: 'pen: false — the ink is fully drawn, with no pen and no animation.' };
|
|
9945
|
+
})();`.trim();
|
|
9946
|
+
}
|
|
9947
|
+
// ===========================================================================
|
|
9601
9948
|
// SPRITE SHEETS
|
|
9602
9949
|
// ===========================================================================
|
|
9603
9950
|
generateSpriteSheet(input) {
|
|
@@ -9971,8 +10318,9 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9971
10318
|
// Add physics body
|
|
9972
10319
|
(function() {
|
|
9973
10320
|
${guard}
|
|
9974
|
-
const entry = app.itemRegistry.get(${itemIdStr});
|
|
9975
|
-
|
|
10321
|
+
const entry = (app.getItemById && app.getItemById(${itemIdStr})) || (app.itemRegistry && (app.itemRegistry.getItem ? app.itemRegistry.getItem(${itemIdStr}) : app.itemRegistry.get(${itemIdStr})));
|
|
10322
|
+
const it = entry && (entry.item || entry);
|
|
10323
|
+
if (!it) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
9976
10324
|
const ok = app.physicsWorld.addBody(${itemIdStr}, ${JSON.stringify(config)});
|
|
9977
10325
|
return { success: ok, action: 'add_body', itemId: ${itemIdStr}, bodyType: ${JSON.stringify(input.bodyType || 'dynamic')} };
|
|
9978
10326
|
})();`.trim();
|
|
@@ -11242,7 +11590,7 @@ ${guard}
|
|
|
11242
11590
|
const __sc = __it.scaling || { x: 1, y: 1 };
|
|
11243
11591
|
const __b = __it.bounds;
|
|
11244
11592
|
const __baked = (__it.data && typeof __it.data._bakedRotation === 'number') ? __it.data._bakedRotation : 0;
|
|
11245
|
-
|
|
11593
|
+
${authoringFrameJs('__fr')}
|
|
11246
11594
|
const __frame = { x: __fr.x || 0, y: __fr.y || 0, width: __fr.width, height: __fr.height };
|
|
11247
11595
|
const __actor = { x: __it.position.x, y: __it.position.y,
|
|
11248
11596
|
w: __b.width / (Math.abs(__sc.x) || 1), h: __b.height / (Math.abs(__sc.y) || 1),
|
|
@@ -11263,6 +11611,7 @@ ${guard}
|
|
|
11263
11611
|
__warnings.push('the actor already animates ' + __other.join(', ') + '; the beat keys leave those out, which the engine can break at the beat keys. Re-key ' + __other.join(', ') + ' with every key carrying them, after choreographing.');
|
|
11264
11612
|
}
|
|
11265
11613
|
if (__r.final && !__r.final.onScreen) __warnings.push('after the last beat the actor is off screen at [' + __r.final.x + ', ' + __r.final.y + '].');
|
|
11614
|
+
if (__fr.unbounded && __warnings.some(function (w) { return /off screen/.test(w); })) __warnings.push('the canvas is unbounded, so it has no fixed frame: on-screen was checked against its nominal ' + __fr.width + 'x' + __fr.height + ' artboard. Set a canvas size or preset to fix the frame.');
|
|
11266
11615
|
const __res = { success: true, itemId: ${id}, beats: ${input.beats.length}, keyframes: __r.keyframes.length, duration: __r.end, final: __r.final };
|
|
11267
11616
|
${input.sound ? ` // SOUND ON THE ACTION (D68): one placed sound per moment the motion makes —
|
|
11268
11617
|
// pop, launch, contact, shake — loud as the moment is strong. A sound that
|
|
@@ -11322,6 +11671,7 @@ ${input.sound ? ` // SOUND ON THE ACTION (D68): one placed sound per moment the
|
|
|
11322
11671
|
if (!tid) {
|
|
11323
11672
|
const p = Object.assign({}, st.shape.properties || {}, { x: st.shape.position.x, y: st.shape.position.y });
|
|
11324
11673
|
const made = app.create(st.shape.itemType, p);
|
|
11674
|
+
if (made) { made.visible = false; }
|
|
11325
11675
|
tid = made && made.data && made.data.registryId;
|
|
11326
11676
|
if (!tid) { return { success: false, error: 'state ' + i + ': could not make the ' + st.shape.itemType + ' reference shape', states: done }; }
|
|
11327
11677
|
} else if (!(app.getItemById && app.getItemById(tid))) {
|
|
@@ -11336,7 +11686,7 @@ ${input.sound ? ` // SOUND ON THE ACTION (D68): one placed sound per moment the
|
|
|
11336
11686
|
if (n !== null && n < done.length) res.warning = 'only ' + n + ' of ' + done.length + ' morphs are on the element.';
|
|
11337
11687
|
${cam ? ` // The camera rides the SAME timing: framing each state as its morph lands.
|
|
11338
11688
|
{
|
|
11339
|
-
|
|
11689
|
+
${authoringFrameJs('fr')}
|
|
11340
11690
|
const fill = ${S(cam.fill ?? 0.45)};
|
|
11341
11691
|
const frameOf = function (it) { const b = it.bounds; return { focus: [b.center.x, b.center.y], zoom: Math.max(0.2, Math.min(8, fill * Math.min(fr.width / Math.max(1, b.width), fr.height / Math.max(1, b.height)))) }; };
|
|
11342
11692
|
const f0 = frameOf(src);
|
|
@@ -11401,6 +11751,154 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
|
|
|
11401
11751
|
}
|
|
11402
11752
|
}
|
|
11403
11753
|
}
|
|
11754
|
+
/**
|
|
11755
|
+
* Character tool aliases (pinepaper_create_character, pinepaper_character_action, pinepaper_character_pose)
|
|
11756
|
+
* Routing on-device / local model calls cleanly to the engine.
|
|
11757
|
+
*/
|
|
11758
|
+
generateCreateCharacter(input) {
|
|
11759
|
+
const S = (v) => JSON.stringify(v);
|
|
11760
|
+
const charId = S(input.characterId || input.name || `char_${Date.now()}`);
|
|
11761
|
+
const posX = typeof input.x === 'number' ? input.x : 400;
|
|
11762
|
+
const posY = typeof input.y === 'number' ? input.y : 300;
|
|
11763
|
+
const scale = typeof input.scale === 'number' ? input.scale : 1;
|
|
11764
|
+
const name = S(input.name || input.characterId || 'Character');
|
|
11765
|
+
const base = input.base ? S(input.base) : 'null';
|
|
11766
|
+
const preset = input.preset || input.concept ? S(input.preset || input.concept) : 'null';
|
|
11767
|
+
return `
|
|
11768
|
+
// Create Character
|
|
11769
|
+
(async function() {
|
|
11770
|
+
const charId = ${charId};
|
|
11771
|
+
const posX = ${posX};
|
|
11772
|
+
const posY = ${posY};
|
|
11773
|
+
const scale = ${scale};
|
|
11774
|
+
const name = ${name};
|
|
11775
|
+
const base = ${base};
|
|
11776
|
+
const preset = ${preset};
|
|
11777
|
+
|
|
11778
|
+
// 1. If original character base is specified and app.createCharacter exists
|
|
11779
|
+
if (base && typeof app.createCharacter === 'function') {
|
|
11780
|
+
const r = await app.createCharacter({ base: base, at: { x: posX, y: posY }, scale: scale });
|
|
11781
|
+
if (r && r.ok !== false) {
|
|
11782
|
+
return { success: true, characterId: charId, itemId: r.id, parts: r.parts };
|
|
11783
|
+
}
|
|
11784
|
+
}
|
|
11785
|
+
|
|
11786
|
+
// 2. Try app.create
|
|
11787
|
+
let groupId = null;
|
|
11788
|
+
if (typeof app.create === 'function') {
|
|
11789
|
+
const group = app.create('circle', { x: posX, y: posY, radius: 35 * scale, color: '#7C3AED' });
|
|
11790
|
+
if (group) {
|
|
11791
|
+
group.data = group.data || {};
|
|
11792
|
+
group.data.ppType = 'pp:Character';
|
|
11793
|
+
group.data.characterId = charId;
|
|
11794
|
+
group.name = name;
|
|
11795
|
+
groupId = group.id || (group.data && group.data.id) || charId;
|
|
11796
|
+
}
|
|
11797
|
+
} else {
|
|
11798
|
+
const p = typeof paper !== 'undefined' ? paper : ((typeof window !== 'undefined' ? window : globalThis).paper);
|
|
11799
|
+
if (p && p.Path) {
|
|
11800
|
+
const body = new p.Path.Circle({ center: [posX, posY], radius: 35 * scale, fillColor: '#7C3AED' });
|
|
11801
|
+
const head = new p.Path.Circle({ center: [posX, posY - 45 * scale], radius: 20 * scale, fillColor: '#EC4899' });
|
|
11802
|
+
const group = new p.Group([body, head]);
|
|
11803
|
+
group.name = name;
|
|
11804
|
+
if (app.textItemGroup) group.parent = app.textItemGroup;
|
|
11805
|
+
group.data = { isUserGroup: true, isLayeredCharacter: true, ppType: 'pp:Character', characterId: charId };
|
|
11806
|
+
const reg = app.itemRegistry;
|
|
11807
|
+
groupId = reg ? reg.register(group, 'group', { source: 'mcp_create_character' }, 'mcp') : charId;
|
|
11808
|
+
group.data.id = groupId;
|
|
11809
|
+
}
|
|
11810
|
+
}
|
|
11811
|
+
|
|
11812
|
+
if (app._scheduleRepaint) app._scheduleRepaint();
|
|
11813
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
11814
|
+
return { success: true, characterId: charId, itemId: groupId };
|
|
11815
|
+
})();`.trim();
|
|
11816
|
+
}
|
|
11817
|
+
generateCharacterAction(input) {
|
|
11818
|
+
const S = (v) => JSON.stringify(v);
|
|
11819
|
+
const targetId = input.characterId || input.itemId;
|
|
11820
|
+
const act = String(input.action || 'walk').toLowerCase();
|
|
11821
|
+
const speed = input.speed || 100;
|
|
11822
|
+
return `
|
|
11823
|
+
// Character Action
|
|
11824
|
+
(function() {
|
|
11825
|
+
const targetId = ${S(targetId)};
|
|
11826
|
+
const act = ${S(act)};
|
|
11827
|
+
const speed = ${speed};
|
|
11828
|
+
|
|
11829
|
+
const charItem = targetId
|
|
11830
|
+
? (app.getItemById(targetId) || (app.getAllItems && app.getAllItems().find(i => (i.data && i.data.characterId === targetId) || i.name === targetId)))
|
|
11831
|
+
: (app.selectedItems && app.selectedItems[0]);
|
|
11832
|
+
|
|
11833
|
+
if (!charItem) {
|
|
11834
|
+
return { success: false, error: 'Character not found: ' + targetId };
|
|
11835
|
+
}
|
|
11836
|
+
|
|
11837
|
+
const itemId = charItem.id || (charItem.data && charItem.data.id) || targetId;
|
|
11838
|
+
const skelId = (charItem.data && charItem.data.characterId) ? 'character_' + charItem.data.characterId : null;
|
|
11839
|
+
|
|
11840
|
+
if (act === 'walk' || act === 'run' || act === 'locomotion') {
|
|
11841
|
+
if (typeof app.addRelation === 'function') {
|
|
11842
|
+
app.addRelation(itemId, null, 'locomotion', {
|
|
11843
|
+
skeletonId: skelId,
|
|
11844
|
+
speed: speed,
|
|
11845
|
+
cycleDistance: 150,
|
|
11846
|
+
});
|
|
11847
|
+
}
|
|
11848
|
+
} else if (app.riggingSystem) {
|
|
11849
|
+
const rs = app.riggingSystem;
|
|
11850
|
+
if (skelId && rs.skeletons && rs.skeletons.has(skelId)) {
|
|
11851
|
+
if (typeof rs.applyBoneAngles === 'function') {
|
|
11852
|
+
rs.applyBoneAngles(skelId, { spine: 0.1, left_arm: -0.5, right_arm: 0.5 });
|
|
11853
|
+
if (typeof rs.solveFK === 'function') rs.solveFK(skelId);
|
|
11854
|
+
}
|
|
11855
|
+
}
|
|
11856
|
+
}
|
|
11857
|
+
|
|
11858
|
+
if (typeof app.animate === 'function' && (act === 'wave' || act === 'dance' || act === 'jump' || act === 'idle')) {
|
|
11859
|
+
app.animate(charItem, { animationType: act === 'jump' ? 'bounce' : (act === 'idle' ? 'breathe' : 'wobble') });
|
|
11860
|
+
}
|
|
11861
|
+
|
|
11862
|
+
if (app._scheduleRepaint) app._scheduleRepaint();
|
|
11863
|
+
return { success: true, characterId: targetId || itemId, action: act };
|
|
11864
|
+
})();`.trim();
|
|
11865
|
+
}
|
|
11866
|
+
generateCharacterPose(input) {
|
|
11867
|
+
const S = (v) => JSON.stringify(v);
|
|
11868
|
+
const targetId = input.characterId || input.itemId;
|
|
11869
|
+
const expr = String(input.expression || 'smile').toLowerCase();
|
|
11870
|
+
const duration = input.duration !== undefined ? input.duration : null;
|
|
11871
|
+
const interval = input.interval !== undefined ? input.interval : null;
|
|
11872
|
+
return `
|
|
11873
|
+
// Character Pose / Expression
|
|
11874
|
+
(function() {
|
|
11875
|
+
const targetId = ${S(targetId)};
|
|
11876
|
+
const expr = ${S(expr)};
|
|
11877
|
+
|
|
11878
|
+
const charItem = targetId
|
|
11879
|
+
? (app.getItemById(targetId) || (app.getAllItems && app.getAllItems().find(i => (i.data && i.data.characterId === targetId) || i.name === targetId)))
|
|
11880
|
+
: (app.selectedItems && app.selectedItems[0]);
|
|
11881
|
+
|
|
11882
|
+
if (!charItem) {
|
|
11883
|
+
return { success: false, error: 'Character not found: ' + targetId };
|
|
11884
|
+
}
|
|
11885
|
+
|
|
11886
|
+
const itemId = charItem.id || (charItem.data && charItem.data.id) || targetId;
|
|
11887
|
+
|
|
11888
|
+
// Touch riggingSystem first to ensure expression rules are initialized
|
|
11889
|
+
const rs = app.riggingSystem;
|
|
11890
|
+
if (typeof app.addRelation === 'function') {
|
|
11891
|
+
app.addRelation(itemId, null, 'expresses', {
|
|
11892
|
+
expression: expr,
|
|
11893
|
+
duration: ${duration === null ? 'null' : duration},
|
|
11894
|
+
interval: ${interval === null ? 'null' : interval},
|
|
11895
|
+
});
|
|
11896
|
+
}
|
|
11897
|
+
|
|
11898
|
+
if (app._scheduleRepaint) app._scheduleRepaint();
|
|
11899
|
+
return { success: true, characterId: targetId || itemId, expression: expr };
|
|
11900
|
+
})();`.trim();
|
|
11901
|
+
}
|
|
11404
11902
|
/**
|
|
11405
11903
|
* pinepaper_look (D64): one look over an item, a subtree or the whole scene
|
|
11406
11904
|
* — ids, keys and relations kept, removable pixel for pixel. Every apply
|