@pinepaper.studio/mcp-server 1.6.12 → 1.6.13
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 +162 -0
- package/dist/agent/export-manager.d.ts.map +1 -1
- package/dist/agent/export-manager.js +9 -7
- package/dist/agent/export-manager.js.map +1 -1
- package/dist/assets/repositories/iconify.d.ts +0 -5
- package/dist/assets/repositories/iconify.d.ts.map +1 -1
- package/dist/assets/repositories/iconify.js +17 -5
- package/dist/assets/repositories/iconify.js.map +1 -1
- package/dist/browser/puppeteer-controller.d.ts +16 -0
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +32 -2
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/design/design-systems.d.ts +27 -2
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +20 -0
- package/dist/design/design-systems.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +36 -5
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +5 -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 +16 -11
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers.d.ts +13 -2
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +219 -13
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.js +2 -2
- 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 +10 -1
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +448 -74
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +94 -9
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +14 -5
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -128,6 +128,27 @@ function generateCreateItemCode(itemType, position, properties) {
|
|
|
128
128
|
const item = app.create('rectangle', ${JSON.stringify(plate, null, 2)});
|
|
129
129
|
if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.data.renderParams = ${JSON.stringify(params)}; }`;
|
|
130
130
|
}
|
|
131
|
+
// RTL IS NOT SUPPORTED, AND SILENCE WAS THE WORST WAY TO SAY SO.
|
|
132
|
+
//
|
|
133
|
+
// create('text', {direction:'rtl'}) dropped the property with no error and
|
|
134
|
+
// left the base direction LTR, so an Arabic sentence's final period landed
|
|
135
|
+
// at the reader's START — visibly wrong, and attributable to nothing. The
|
|
136
|
+
// engine has no text-direction support at all: `direction` there is an arc
|
|
137
|
+
// sweep and an animation direction, and `justification` is the only
|
|
138
|
+
// text-related alignment it reads.
|
|
139
|
+
//
|
|
140
|
+
// Refused by name rather than accepted-and-dropped, and rather than adding a
|
|
141
|
+
// prop that would be dropped one layer further on. The alignment half is
|
|
142
|
+
// real and is named, because it is what a caller can actually use today.
|
|
143
|
+
const rtlAsked = params.direction ?? params.textDirection;
|
|
144
|
+
if (rtlAsked !== undefined) {
|
|
145
|
+
return `
|
|
146
|
+
// Create ${itemType} — refused, rather than silently left ltr
|
|
147
|
+
({ success: false, error: ${JSON.stringify(`direction / textDirection is not supported: this studio has no text-direction support, so ${String(rtlAsked)} would have been accepted and the text left LTR. `
|
|
148
|
+
+ 'That is visible as a right-to-left sentence whose final punctuation lands at the start. '
|
|
149
|
+
+ "You can align with justification: 'right', which is read, but the BASE direction needs engine support — "
|
|
150
|
+
+ 'pass pre-shaped text, or split the line, until it lands.')} });`.trim();
|
|
151
|
+
}
|
|
131
152
|
// AN UNKNOWN ANCHOR IS INVISIBLE OVER MCP.
|
|
132
153
|
//
|
|
133
154
|
// The engine names a bad anchor through console.warn and leaves (x, y) as
|
|
@@ -416,6 +437,28 @@ function normalizeKeyframePositions(keyframes) {
|
|
|
416
437
|
/**
|
|
417
438
|
* Template for keyframe animation
|
|
418
439
|
*/
|
|
440
|
+
/**
|
|
441
|
+
* AN UNKNOWN itemId IS A SILENT SUCCESS, ACROSS A WHOLE FAMILY OF TOOLS.
|
|
442
|
+
*
|
|
443
|
+
* `app.addAnimation('c0', …)` on an id that does not exist calls
|
|
444
|
+
* console.warn and returns undefined — and production strips console.warn, so
|
|
445
|
+
* over MCP nothing is said at all. The emitter then returned
|
|
446
|
+
* `{success: true}` because the call did not throw. A pilot animated a
|
|
447
|
+
* connector id that was never registered and got success for every keyframe.
|
|
448
|
+
*
|
|
449
|
+
* Emitted as a shared preamble so the check reads the same everywhere and a
|
|
450
|
+
* new tool taking an itemId can adopt it in one line. It resolves through the
|
|
451
|
+
* registry the way the engine does, and names the id it could not find —
|
|
452
|
+
* "item_7 is not on the canvas" is actionable, silence is not.
|
|
453
|
+
*/
|
|
454
|
+
function requireItem(itemId, what) {
|
|
455
|
+
return `
|
|
456
|
+
const _target = (app.getItemById && app.getItemById(${JSON.stringify(itemId)}))
|
|
457
|
+
|| (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(${JSON.stringify(itemId)}));
|
|
458
|
+
if (!_target) {
|
|
459
|
+
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.' };
|
|
460
|
+
}`.trim();
|
|
461
|
+
}
|
|
419
462
|
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits) {
|
|
420
463
|
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
421
464
|
const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
|
|
@@ -430,9 +473,11 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
430
473
|
opts.timeUnits = timeUnits;
|
|
431
474
|
return `
|
|
432
475
|
// Apply keyframe animation to ${itemId}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
(
|
|
476
|
+
(function() {
|
|
477
|
+
${requireItem(itemId, 'the animation')}
|
|
478
|
+
app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
|
|
479
|
+
return { success: true, itemId: '${itemId}', duration: ${calculatedDuration}, loop: ${loop}${timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : ''}${clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : ''}${clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : ''} };
|
|
480
|
+
})();
|
|
436
481
|
`.trim();
|
|
437
482
|
}
|
|
438
483
|
/**
|
|
@@ -1199,6 +1244,49 @@ const INLINE_REMOTE_IMAGES = `
|
|
|
1199
1244
|
return { svg: svgText, notes: notes };
|
|
1200
1245
|
}
|
|
1201
1246
|
`;
|
|
1247
|
+
/**
|
|
1248
|
+
* A HEX COLOUR IN THE 3D WORLD POISONS EVERY LATER EXPORT.
|
|
1249
|
+
*
|
|
1250
|
+
* addWorldObject hands `color` to a WebGL uniform — its own example is
|
|
1251
|
+
* `color: [0.9, 0.3, 0.2]`, three floats in 0..1. Give it '#ef4444' and
|
|
1252
|
+
* uniform3fv throws "cannot be converted to a sequence"; the object is
|
|
1253
|
+
* reported as added, and then EVERY subsequent MP4 export fails, because the
|
|
1254
|
+
* broken uniform is in the scene from then on. Same shape as the tainted
|
|
1255
|
+
* canvas: one bad value, success reported, and the damage surfaces somewhere
|
|
1256
|
+
* else entirely.
|
|
1257
|
+
*
|
|
1258
|
+
* Converted here. A caller writing '#ef4444' is not wrong — it is the colour
|
|
1259
|
+
* spelling every other tool in this package takes — so the emitter absorbs the
|
|
1260
|
+
* difference rather than the caller learning a second convention.
|
|
1261
|
+
*/
|
|
1262
|
+
function world3dColor(value) {
|
|
1263
|
+
if (Array.isArray(value)) {
|
|
1264
|
+
// Already a triple. Accept 0-255 too, since that is the other common form.
|
|
1265
|
+
const nums = value.filter((v) => typeof v === 'number');
|
|
1266
|
+
if (nums.length < 3)
|
|
1267
|
+
return value;
|
|
1268
|
+
return nums.slice(0, 3).map((n) => (n > 1 ? n / 255 : n));
|
|
1269
|
+
}
|
|
1270
|
+
if (typeof value !== 'string')
|
|
1271
|
+
return value;
|
|
1272
|
+
const hex = value.trim().replace(/^#/, '');
|
|
1273
|
+
const full = hex.length === 3 ? hex.split('').map((c) => c + c).join('') : hex;
|
|
1274
|
+
if (!/^[0-9a-f]{6}$/i.test(full))
|
|
1275
|
+
return value;
|
|
1276
|
+
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
|
|
1277
|
+
}
|
|
1278
|
+
/** Recursively convert every `color` field in a world3d payload. */
|
|
1279
|
+
function world3dColors(value) {
|
|
1280
|
+
if (Array.isArray(value))
|
|
1281
|
+
return value.map((v) => world3dColors(v));
|
|
1282
|
+
if (!value || typeof value !== 'object')
|
|
1283
|
+
return value;
|
|
1284
|
+
const out = {};
|
|
1285
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1286
|
+
out[k] = k === 'color' ? world3dColor(v) : world3dColors(v);
|
|
1287
|
+
}
|
|
1288
|
+
return out;
|
|
1289
|
+
}
|
|
1202
1290
|
export class PinePaperCodeGenerator {
|
|
1203
1291
|
/**
|
|
1204
1292
|
* Generate code for creating an item
|
|
@@ -2032,7 +2120,25 @@ const connector = app.diagramSystem.connect(sourceItem, targetItem, config);
|
|
|
2032
2120
|
const connectorId = connector.data?.registryId || connector.id;
|
|
2033
2121
|
app.historyManager.saveState();
|
|
2034
2122
|
|
|
2035
|
-
|
|
2123
|
+
// A REQUESTED id IS NOT NECESSARILY THE id YOU GOT.
|
|
2124
|
+
//
|
|
2125
|
+
// Connector itself honours 'config.id || <generated>', but DiagramSystem.connect
|
|
2126
|
+
// rebuilds the config from an allowlist — routing, lineColor, lineWidth,
|
|
2127
|
+
// headStyle, headSize, boltEnabled — and 'id' is not on it. So a caller's id is
|
|
2128
|
+
// dropped before the Connector sees it and a timestamp id is minted instead,
|
|
2129
|
+
// which then makes update_connector / remove_connector say "not found" for the
|
|
2130
|
+
// id the caller chose. Engine-side, raised with fxtool; until it lands, the
|
|
2131
|
+
// mismatch is REPORTED rather than left for a later call to trip over.
|
|
2132
|
+
({
|
|
2133
|
+
connectorId,
|
|
2134
|
+
sourceItemId: '${sourceItemId}',
|
|
2135
|
+
targetItemId: '${targetItemId}',
|
|
2136
|
+
...(${JSON.stringify(id ?? null)} && connectorId !== ${JSON.stringify(id ?? null)}
|
|
2137
|
+
? { requestedId: ${JSON.stringify(id ?? null)},
|
|
2138
|
+
idHonoured: false,
|
|
2139
|
+
note: 'this studio dropped the id you asked for and minted its own — use connectorId above for update_connector and remove_connector, not the one you passed.' }
|
|
2140
|
+
: {}),
|
|
2141
|
+
});
|
|
2036
2142
|
`.trim();
|
|
2037
2143
|
}
|
|
2038
2144
|
/**
|
|
@@ -2445,12 +2551,16 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2445
2551
|
|
|
2446
2552
|
// Check relations
|
|
2447
2553
|
if (app.relationRegistry) {
|
|
2448
|
-
|
|
2449
|
-
analysis
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2554
|
+
// getAll() has never existed on the registry, so this branch always took
|
|
2555
|
+
// the empty fallback and every analysis reported NO relations. getStats()
|
|
2556
|
+
// is the real accessor and answers both questions directly:
|
|
2557
|
+
// associationsByType is keyed by relation type.
|
|
2558
|
+
const relStats = typeof app.relationRegistry.getStats === 'function'
|
|
2559
|
+
? app.relationRegistry.getStats()
|
|
2560
|
+
: null;
|
|
2561
|
+
(Object.keys((relStats && relStats.associationsByType) || {})).forEach(t => relationSet.add(t));
|
|
2453
2562
|
analysis.relationTypes = Array.from(relationSet);
|
|
2563
|
+
analysis.hasRelations = !!relStats && (relStats.activeItems > 0 || analysis.relationTypes.length > 0);
|
|
2454
2564
|
if (analysis.hasRelations) analysis.hasAnimations = true;
|
|
2455
2565
|
}
|
|
2456
2566
|
|
|
@@ -3158,13 +3268,38 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3158
3268
|
if (!params || (params.mode || 'keyframes') !== 'keyframes' || !params.keyframes || !params.keyframes.length) {
|
|
3159
3269
|
return { success: false, platform, format, framing, error: 'framing: "camera" requires a keyframe-mode camera walkthrough with at least one keyframe.' };
|
|
3160
3270
|
}
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3271
|
+
// MATCH THE ENGINE'S FRAME EXACTLY, AND DO NOT SECOND-GUESS THE ZOOM.
|
|
3272
|
+
//
|
|
3273
|
+
// This divided canvasEl.width — device pixels — so a camera export came
|
|
3274
|
+
// out 2234x1472 whatever the board was. That is fixed by reading the
|
|
3275
|
+
// artboard, and the chain below is deliberately the same one the engine's
|
|
3276
|
+
// own _exportFrameSize() uses: getCanvasSize(), then app.canvasSize, then
|
|
3277
|
+
// a known frame. Two places computing one number is how they drift.
|
|
3278
|
+
//
|
|
3279
|
+
// The zoom divide is gone too, and that is the part worth explaining.
|
|
3280
|
+
// VideoExporter._frameCropRect returns the WHOLE canvas element whenever a
|
|
3281
|
+
// camera animation exists — the camera owns the view, so it captures
|
|
3282
|
+
// everything the camera frames. Dividing by the first keyframe's zoom
|
|
3283
|
+
// therefore never reframed anything; it only shrank the output, so a
|
|
3284
|
+
// zoom-2 export produced a half-size video of the same content. Asking for
|
|
3285
|
+
// the artboard's own size is both simpler and right.
|
|
3286
|
+
//
|
|
3287
|
+
// NOTE: the engine currently scales that whole-canvas crop into the output
|
|
3288
|
+
// frame without preserving aspect, so the picture is squashed by the ratio
|
|
3289
|
+
// between the workspace canvas and the board (measured: 1.17 landscape,
|
|
3290
|
+
// 0.37 portrait). That is VideoExporter._frameCropRect, not this — raised
|
|
3291
|
+
// with fxtool-f2. Matching frames here means that when it is fixed, these
|
|
3292
|
+
// two cannot disagree about the aspect on top of it.
|
|
3293
|
+
let camBase = null;
|
|
3294
|
+
if (typeof app.getCanvasSize === 'function') {
|
|
3295
|
+
const cs = app.getCanvasSize();
|
|
3296
|
+
if (cs && cs.width > 0) camBase = { width: cs.width, height: cs.height };
|
|
3297
|
+
}
|
|
3298
|
+
if (!camBase && app.canvasSize && app.canvasSize.width > 0) {
|
|
3299
|
+
camBase = { width: app.canvasSize.width, height: app.canvasSize.height };
|
|
3300
|
+
}
|
|
3301
|
+
if (!camBase) camBase = { width: dimensions.width, height: dimensions.height };
|
|
3302
|
+
cameraDims = { width: camBase.width, height: camBase.height };
|
|
3168
3303
|
}
|
|
3169
3304
|
|
|
3170
3305
|
let result = { success: false, platform, format, quality, framing };
|
|
@@ -3186,14 +3321,35 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3186
3321
|
|
|
3187
3322
|
case 'png':
|
|
3188
3323
|
if (app.exportEngine && app.exportEngine.exportPNG) {
|
|
3189
|
-
|
|
3324
|
+
// PNG IGNORED THE PLATFORM ENTIRELY.
|
|
3325
|
+
//
|
|
3326
|
+
// exportPNG takes dpi or scale and no dimensions, so passing only
|
|
3327
|
+
// dpi scaled the CANVAS by dpi/96 and the preset was never applied:
|
|
3328
|
+
// platform 'youtube-thumbnail' (1280x720) and platform 'auto' on a
|
|
3329
|
+
// 1920x1080 board both returned 3000x1688, which is 1920 x 150/96.
|
|
3330
|
+
// Third member of the same family as the camera dims and analyze —
|
|
3331
|
+
// a pixel count that came from the rendering scale rather than from
|
|
3332
|
+
// what the caller asked for.
|
|
3333
|
+
//
|
|
3334
|
+
// dimensions is already resolved: the preset for a named platform,
|
|
3335
|
+
// the board itself for auto. Turning it into a scale is the only
|
|
3336
|
+
// lever exportPNG offers, so that is what it gets.
|
|
3337
|
+
const pngBoard = (typeof app.getCanvasSize === 'function' && app.getCanvasSize())
|
|
3338
|
+
|| app.canvasSize
|
|
3339
|
+
|| { width: dimensions.width, height: dimensions.height };
|
|
3340
|
+
const pngScale = pngBoard.width > 0 ? dimensions.width / pngBoard.width : 1;
|
|
3341
|
+
const png = await app.exportEngine.exportPNG({ scale: pngScale });
|
|
3342
|
+
const dataUrl = (png && png.dataUrl) ? png.dataUrl : png;
|
|
3190
3343
|
result = {
|
|
3191
3344
|
success: true,
|
|
3192
3345
|
platform,
|
|
3193
3346
|
format: 'png',
|
|
3194
3347
|
data: dataUrl,
|
|
3195
3348
|
mimeType: 'image/png',
|
|
3196
|
-
size: Math.round(dataUrl.length * 0.75)
|
|
3349
|
+
size: Math.round(String(dataUrl).length * 0.75),
|
|
3350
|
+
dimensions: (png && png.width)
|
|
3351
|
+
? { width: png.width, height: png.height }
|
|
3352
|
+
: { width: dimensions.width, height: dimensions.height },
|
|
3197
3353
|
};
|
|
3198
3354
|
} else {
|
|
3199
3355
|
const canvas = document.querySelector('canvas');
|
|
@@ -3460,12 +3616,16 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3460
3616
|
|
|
3461
3617
|
// Check relations
|
|
3462
3618
|
if (app.relationRegistry) {
|
|
3463
|
-
|
|
3464
|
-
analysis
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3619
|
+
// getAll() has never existed on the registry, so this branch always took
|
|
3620
|
+
// the empty fallback and every analysis reported NO relations. getStats()
|
|
3621
|
+
// is the real accessor and answers both questions directly:
|
|
3622
|
+
// associationsByType is keyed by relation type.
|
|
3623
|
+
const relStats = typeof app.relationRegistry.getStats === 'function'
|
|
3624
|
+
? app.relationRegistry.getStats()
|
|
3625
|
+
: null;
|
|
3626
|
+
(Object.keys((relStats && relStats.associationsByType) || {})).forEach(t => relationSet.add(t));
|
|
3468
3627
|
analysis.relationTypes = Array.from(relationSet);
|
|
3628
|
+
analysis.hasRelations = !!relStats && (relStats.activeItems > 0 || analysis.relationTypes.length > 0);
|
|
3469
3629
|
if (analysis.hasRelations) analysis.hasAnimations = true;
|
|
3470
3630
|
}
|
|
3471
3631
|
|
|
@@ -3631,7 +3791,7 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3631
3791
|
// interactivity flags have other names entirely. Every one was accepted,
|
|
3632
3792
|
// JSON-stringified into the call and dropped — a caller who set fillColor
|
|
3633
3793
|
// got the default grey with no indication why.
|
|
3634
|
-
const { fillColor, strokeColor, strokeWidth, hoverFill, hoverStroke, enableHover, enableClick, ...passthrough } = raw;
|
|
3794
|
+
const { fillColor, strokeColor, strokeWidth, hoverFill, hoverStroke, enableHover, enableClick, options: extraOptions, ...passthrough } = raw;
|
|
3635
3795
|
// The engine's own style vocabulary — see stylePresets in MapSystem.js.
|
|
3636
3796
|
const styles = {};
|
|
3637
3797
|
if (fillColor !== undefined)
|
|
@@ -3653,7 +3813,15 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3653
3813
|
// learns why their outline did not move.
|
|
3654
3814
|
if (hoverStroke !== undefined)
|
|
3655
3815
|
styles.hoverStroke = hoverStroke;
|
|
3656
|
-
|
|
3816
|
+
// `options` was declared by the tool's description and stripped by the
|
|
3817
|
+
// schema, so `{style: 'dark'}` never reached loadMap. It carries the
|
|
3818
|
+
// options not named individually above — style presets, showOcean,
|
|
3819
|
+
// fitBounds, preserve — and is spread FIRST so an explicitly named field
|
|
3820
|
+
// still wins over the same key inside it.
|
|
3821
|
+
const engineOptions = {
|
|
3822
|
+
...(extraOptions && typeof extraOptions === 'object' ? extraOptions : {}),
|
|
3823
|
+
...passthrough,
|
|
3824
|
+
};
|
|
3657
3825
|
if (Object.keys(styles).length > 0)
|
|
3658
3826
|
engineOptions.styles = styles;
|
|
3659
3827
|
if (enableHover !== undefined)
|
|
@@ -3693,7 +3861,28 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3693
3861
|
generateHighlightRegions(input) {
|
|
3694
3862
|
const validated = HighlightRegionsInputSchema.parse(input);
|
|
3695
3863
|
const regionIds = JSON.stringify(validated.regionIds);
|
|
3696
|
-
|
|
3864
|
+
// highlightRegions(ids, style) reads style.fill / .stroke / .strokeWidth.
|
|
3865
|
+
// This passed fillColor / strokeColor / strokeWidth, so the first two were
|
|
3866
|
+
// dropped and every highlight came out the default blue. A caller reaching
|
|
3867
|
+
// for `color` — the obvious name, and the one a pilot actually used — was
|
|
3868
|
+
// rejected by the schema before it got this far; it is accepted now and
|
|
3869
|
+
// means the fill.
|
|
3870
|
+
const ho = (validated.options ?? {});
|
|
3871
|
+
const style = {};
|
|
3872
|
+
const fill = ho.color ?? ho.fillColor ?? ho.fill;
|
|
3873
|
+
if (fill !== undefined)
|
|
3874
|
+
style.fill = fill;
|
|
3875
|
+
const stroke = ho.strokeColor ?? ho.stroke;
|
|
3876
|
+
if (stroke !== undefined)
|
|
3877
|
+
style.stroke = stroke;
|
|
3878
|
+
if (ho.strokeWidth !== undefined)
|
|
3879
|
+
style.strokeWidth = ho.strokeWidth;
|
|
3880
|
+
for (const [k, v] of Object.entries(ho)) {
|
|
3881
|
+
if (!['color', 'fillColor', 'fill', 'strokeColor', 'stroke', 'strokeWidth'].includes(k) && v !== undefined) {
|
|
3882
|
+
style[k] = v;
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
const optionsStr = JSON.stringify(style);
|
|
3697
3886
|
return `
|
|
3698
3887
|
// Highlight map regions
|
|
3699
3888
|
(function() {
|
|
@@ -3738,7 +3927,45 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3738
3927
|
generateApplyDataColors(input) {
|
|
3739
3928
|
const validated = ApplyDataColorsInputSchema.parse(input);
|
|
3740
3929
|
const dataStr = JSON.stringify(validated.data);
|
|
3741
|
-
|
|
3930
|
+
// THE ENGINE TAKES A COLOUR RAMP, NOT THE NAME OF ONE.
|
|
3931
|
+
//
|
|
3932
|
+
// applyDataColors destructures `colorScale = ['#f7fbff', '#08519c']` — two
|
|
3933
|
+
// endpoints it interpolates between — plus `domain` and `legend`. This tool
|
|
3934
|
+
// published a preset NAME ('greens'), which the engine then indexed as an
|
|
3935
|
+
// array: colorScale[0] of a string is 'g', and every region came out
|
|
3936
|
+
// near-black. It also published minValue/maxValue/showLegend, none of which
|
|
3937
|
+
// the engine reads.
|
|
3938
|
+
//
|
|
3939
|
+
// Translated here rather than in the schema: the names are the friendlier
|
|
3940
|
+
// surface and this is ordinary cross-repo vocabulary drift, which is what
|
|
3941
|
+
// the emitter exists to absorb.
|
|
3942
|
+
const RAMPS = {
|
|
3943
|
+
blues: ['#f7fbff', '#08519c'],
|
|
3944
|
+
greens: ['#f7fcf5', '#006d2c'],
|
|
3945
|
+
reds: ['#fff5f0', '#a50f15'],
|
|
3946
|
+
oranges: ['#fff5eb', '#a63603'],
|
|
3947
|
+
purples: ['#fcfbfd', '#54278f'],
|
|
3948
|
+
heat: ['#ffffb2', '#bd0026'],
|
|
3949
|
+
};
|
|
3950
|
+
const o = validated.options ?? {};
|
|
3951
|
+
const engineOptions = {};
|
|
3952
|
+
if (o.colorScale)
|
|
3953
|
+
engineOptions.colorScale = RAMPS[o.colorScale] ?? RAMPS.blues;
|
|
3954
|
+
if (o.minValue !== undefined || o.maxValue !== undefined) {
|
|
3955
|
+
const values = Object.values(validated.data);
|
|
3956
|
+
engineOptions.domain = [
|
|
3957
|
+
o.minValue ?? Math.min(...values),
|
|
3958
|
+
o.maxValue ?? Math.max(...values),
|
|
3959
|
+
];
|
|
3960
|
+
}
|
|
3961
|
+
if (o.showLegend !== undefined)
|
|
3962
|
+
engineOptions.legend = o.showLegend;
|
|
3963
|
+
for (const [k, v] of Object.entries(o)) {
|
|
3964
|
+
if (!['colorScale', 'minValue', 'maxValue', 'showLegend'].includes(k) && v !== undefined) {
|
|
3965
|
+
engineOptions[k] = v;
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
const optionsStr = JSON.stringify(engineOptions);
|
|
3742
3969
|
return `
|
|
3743
3970
|
// Apply choropleth data colors
|
|
3744
3971
|
(function() {
|
|
@@ -3805,8 +4032,18 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3805
4032
|
}
|
|
3806
4033
|
|
|
3807
4034
|
try {
|
|
3808
|
-
|
|
3809
|
-
|
|
4035
|
+
// addLabels() has never existed. addRegionLabel(regionId, options) is the
|
|
4036
|
+
// engine's own per-region call, and this tool's input is a list of regions,
|
|
4037
|
+
// so the loop IS the mapping — applyCustomLabels takes a
|
|
4038
|
+
// {regionId: text} map instead, which is a different question.
|
|
4039
|
+
const _labelled = [];
|
|
4040
|
+
const _missed = [];
|
|
4041
|
+
for (const _r of ${regionsStr}) {
|
|
4042
|
+
const _item = app.mapSystem.addRegionLabel(_r, ${optionsStr});
|
|
4043
|
+
(_item ? _labelled : _missed).push(_r);
|
|
4044
|
+
}
|
|
4045
|
+
return { success: _labelled.length > 0, labelled: _labelled, count: _labelled.length,
|
|
4046
|
+
...(_missed.length ? { notLabelled: _missed, note: 'no region on this map matched these ids — check get_highlighted_map_regions or the map you loaded.' } : {}) };
|
|
3810
4047
|
} catch (error) {
|
|
3811
4048
|
return { success: false, error: error.message };
|
|
3812
4049
|
}
|
|
@@ -3831,8 +4068,13 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3831
4068
|
}
|
|
3832
4069
|
|
|
3833
4070
|
try {
|
|
3834
|
-
|
|
3835
|
-
|
|
4071
|
+
// panTo() has never existed on any studio, and there is no lat/lon pan for
|
|
4072
|
+
// a flat map to route this to. Refused by NAME with the calls that do work,
|
|
4073
|
+
// rather than letting the caller take an undefined-is-not-a-function.
|
|
4074
|
+
return { success: false, error: 'pan_map is not supported: the engine has no lat/lon pan. '
|
|
4075
|
+
+ 'On a globe, rotateGlobeTo brings a coordinate to the front; on a flat map, zoom to a REGION '
|
|
4076
|
+
+ '(zoomToRegion) or call resetView for the whole view. This tool accepted coordinates and did '
|
|
4077
|
+
+ 'nothing for as long as it existed.' };
|
|
3836
4078
|
} catch (error) {
|
|
3837
4079
|
return { success: false, error: error.message };
|
|
3838
4080
|
}
|
|
@@ -3857,8 +4099,11 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3857
4099
|
}
|
|
3858
4100
|
|
|
3859
4101
|
try {
|
|
3860
|
-
|
|
3861
|
-
|
|
4102
|
+
// zoomTo(level) has never existed. The engine zooms to a REGION, not to a
|
|
4103
|
+
// numeric level, so there is nothing to convert a level into.
|
|
4104
|
+
return { success: false, error: 'zoom_map takes a numeric level and the engine has no such call: it zooms to a REGION. '
|
|
4105
|
+
+ 'Use zoomToRegion with a region id, or resetView for the full view. A level was accepted here and did '
|
|
4106
|
+
+ 'nothing for as long as this tool existed.' };
|
|
3862
4107
|
} catch (error) {
|
|
3863
4108
|
return { success: false, error: error.message };
|
|
3864
4109
|
}
|
|
@@ -3877,8 +4122,15 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3877
4122
|
}
|
|
3878
4123
|
|
|
3879
4124
|
try {
|
|
3880
|
-
|
|
3881
|
-
|
|
4125
|
+
// exportMap() has never existed. exportGeoJSON() is the real one, and it
|
|
4126
|
+
// returns the geometry rather than a "configuration" — named accordingly
|
|
4127
|
+
// instead of being spread into a shape the caller cannot rely on.
|
|
4128
|
+
if (typeof app.mapSystem.exportGeoJSON !== 'function') {
|
|
4129
|
+
return { success: false, error: 'this studio cannot export map data — app.mapSystem.exportGeoJSON is unavailable.' };
|
|
4130
|
+
}
|
|
4131
|
+
const geojson = app.mapSystem.exportGeoJSON();
|
|
4132
|
+
const info = typeof app.mapSystem.getMapSourceInfo === 'function' ? app.mapSystem.getMapSourceInfo() : null;
|
|
4133
|
+
return { success: true, geojson, ...(info ? { source: info } : {}) };
|
|
3882
4134
|
} catch (error) {
|
|
3883
4135
|
return { success: false, error: error.message };
|
|
3884
4136
|
}
|
|
@@ -3945,7 +4197,16 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3945
4197
|
}
|
|
3946
4198
|
|
|
3947
4199
|
try {
|
|
3948
|
-
|
|
4200
|
+
// getRegionAtPoint() has never existed; the engine exposes no hit test.
|
|
4201
|
+
// canvasToGeo turns a point into COORDINATES, which is a different answer,
|
|
4202
|
+
// so it is offered rather than quietly substituted.
|
|
4203
|
+
if (typeof app.mapSystem.canvasToGeo === 'function') {
|
|
4204
|
+
const coord = app.mapSystem.canvasToGeo(${validated.x}, ${validated.y});
|
|
4205
|
+
return { success: false, coordinate: coord,
|
|
4206
|
+
error: 'the engine has no region hit test. That point is at ' + JSON.stringify(coord)
|
|
4207
|
+
+ ' — find regions by id with get_highlighted_map_regions or getSelectedRegions.' };
|
|
4208
|
+
}
|
|
4209
|
+
const region = null;
|
|
3949
4210
|
if (region) {
|
|
3950
4211
|
return { success: true, regionId: region.id, regionName: region.name, properties: region.properties };
|
|
3951
4212
|
}
|
|
@@ -3999,7 +4260,7 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
3999
4260
|
}
|
|
4000
4261
|
|
|
4001
4262
|
try {
|
|
4002
|
-
const result = app.mapSystem.
|
|
4263
|
+
const result = app.mapSystem.animateRegionsWave({
|
|
4003
4264
|
duration: ${validated.duration || 10},
|
|
4004
4265
|
loop: ${validated.loop !== false},
|
|
4005
4266
|
colors: ${colorsStr},
|
|
@@ -4697,6 +4958,17 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4697
4958
|
const keyframesStr = validated.keyframes ? JSON.stringify(validated.keyframes) : 'null';
|
|
4698
4959
|
const maskOptionsStr = validated.maskOptions ? JSON.stringify(validated.maskOptions) : '{}';
|
|
4699
4960
|
const preset = validated.preset || validated.maskType || 'wipeLeft';
|
|
4961
|
+
// MODE 3 PASSED AN ANIMATION PRESET WHERE A MASK SHAPE GOES.
|
|
4962
|
+
//
|
|
4963
|
+
// applyCustomMask(item, maskType, keyframes, options) wants a SHAPE —
|
|
4964
|
+
// 'rectangle', 'circle', 'star'. With preset + keyframes this handed it
|
|
4965
|
+
// 'iris' or 'wipeLeft', which is an animation name, so the shape was
|
|
4966
|
+
// whatever the engine falls back to and the keyframes drove the wrong
|
|
4967
|
+
// geometry. maskType is the field that carries the shape, and it is only
|
|
4968
|
+
// used as the preset above when no preset was given at all.
|
|
4969
|
+
const customShape = validated.maskType && validated.maskType !== preset
|
|
4970
|
+
? validated.maskType
|
|
4971
|
+
: 'rectangle';
|
|
4700
4972
|
// Per-preset options. Custom keyframes are an animated-mask extension —
|
|
4701
4973
|
// when supplied, fall through to maskingSystem.applyCustomMask instead.
|
|
4702
4974
|
return `
|
|
@@ -4712,7 +4984,7 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4712
4984
|
const mergedOpts = Object.assign({}, ${optionsStr}, ${maskOptionsStr});
|
|
4713
4985
|
let maskedGroup;
|
|
4714
4986
|
if (${keyframesStr} && app.maskingSystem && app.maskingSystem.applyCustomMask) {
|
|
4715
|
-
maskedGroup = app.maskingSystem.applyCustomMask(item, '${
|
|
4987
|
+
maskedGroup = app.maskingSystem.applyCustomMask(item, '${customShape}', ${keyframesStr}, mergedOpts);
|
|
4716
4988
|
} else {
|
|
4717
4989
|
maskedGroup = app.applyAnimatedMask(item, '${preset}', mergedOpts);
|
|
4718
4990
|
}
|
|
@@ -5016,6 +5288,18 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
5016
5288
|
// the network again — so a host that allows fetch but blocks the image
|
|
5017
5289
|
// load (CORS on <img>, a tainted-canvas refusal) now works too.
|
|
5018
5290
|
let src = '${url}';
|
|
5291
|
+
// A STAGED KEY, not the bytes. The handler writes anything over ~64KB to
|
|
5292
|
+
// window.__ppStage and passes the key instead, because the governor's
|
|
5293
|
+
// loop-guard transform bails on a code string that large and takes the
|
|
5294
|
+
// run's return value with it. See ExecuteCodeOptions.stage.
|
|
5295
|
+
if (src.indexOf('__ppStage:') === 0) {
|
|
5296
|
+
const _key = src.slice('__ppStage:'.length);
|
|
5297
|
+
const _staged = window.__ppStage && window.__ppStage[_key];
|
|
5298
|
+
if (!_staged) {
|
|
5299
|
+
return { success: false, error: 'the image bytes were staged as ' + _key + ' and are not on the page — this is a bug in the MCP server, not your call.' };
|
|
5300
|
+
}
|
|
5301
|
+
src = _staged;
|
|
5302
|
+
}
|
|
5019
5303
|
if (!src.startsWith('data:')) {
|
|
5020
5304
|
let res;
|
|
5021
5305
|
try {
|
|
@@ -5872,7 +6156,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The RE
|
|
|
5872
6156
|
// Get history state
|
|
5873
6157
|
(function() {
|
|
5874
6158
|
${guard}
|
|
5875
|
-
const state = app.historyManager.
|
|
6159
|
+
const state = app.historyManager.getInfo();
|
|
5876
6160
|
return { success: true, action: 'get_state', ...state };
|
|
5877
6161
|
})();`.trim();
|
|
5878
6162
|
default:
|
|
@@ -5949,38 +6233,45 @@ ${resolve}
|
|
|
5949
6233
|
}
|
|
5950
6234
|
}
|
|
5951
6235
|
generateLasso(input) {
|
|
5952
|
-
|
|
6236
|
+
// LASSO IS A MOUSE TOOL. IT HAS NO HEADLESS FORM.
|
|
6237
|
+
//
|
|
6238
|
+
// These called app.imageTools.activateLasso and .applyLasso, neither of
|
|
6239
|
+
// which exists — imageTools is an ImageToolsManager and the lasso lives on
|
|
6240
|
+
// app.lassoTool. `activate` is real there, but the extraction is driven by
|
|
6241
|
+
// startStroke / continueStroke / endStroke: a freehand path the USER draws.
|
|
6242
|
+
// There is no applyLasso to finish it, so wiring activate alone would leave
|
|
6243
|
+
// a caller in a mode it cannot get out of or complete.
|
|
6244
|
+
//
|
|
6245
|
+
// Refused by name, with the tools that do the same job without a pointer.
|
|
6246
|
+
const why = 'the lasso is an interactive mouse tool (app.lassoTool: activate, then startStroke/continueStroke/endStroke as the user draws). '
|
|
6247
|
+
+ 'There is no headless call that completes an extraction, so this tool could never work over MCP. '
|
|
6248
|
+
+ 'To cut a region without a pointer, use pinepaper_extract_object (detects and extracts by description), '
|
|
6249
|
+
+ 'or pinepaper_cutout_style / the mask actions on pinepaper_image_tools to shape an existing raster.';
|
|
5953
6250
|
switch (input.action) {
|
|
5954
6251
|
case 'activate':
|
|
5955
|
-
return `
|
|
5956
|
-
// Activate lasso selection
|
|
5957
|
-
(async function() {
|
|
5958
|
-
${guard}
|
|
5959
|
-
try {
|
|
5960
|
-
await app.imageTools.activateLasso(${JSON.stringify(input.itemId || '')});
|
|
5961
|
-
return { success: true, action: 'activate', itemId: ${JSON.stringify(input.itemId || '')} };
|
|
5962
|
-
} catch (e) {
|
|
5963
|
-
return { error: 'Failed to activate lasso: ' + e.message };
|
|
5964
|
-
}
|
|
5965
|
-
})();`.trim();
|
|
5966
6252
|
case 'apply':
|
|
5967
6253
|
return `
|
|
5968
|
-
//
|
|
5969
|
-
(
|
|
5970
|
-
${guard}
|
|
5971
|
-
try {
|
|
5972
|
-
const result = await app.imageTools.applyLasso();
|
|
5973
|
-
return { success: true, action: 'apply' };
|
|
5974
|
-
} catch (e) {
|
|
5975
|
-
return { error: 'Failed to apply lasso: ' + e.message };
|
|
5976
|
-
}
|
|
5977
|
-
})();`.trim();
|
|
6254
|
+
// Lasso — refused, rather than calling into a method that does not exist
|
|
6255
|
+
({ success: false, action: ${JSON.stringify(input.action)}, error: ${JSON.stringify(why)} });`.trim();
|
|
5978
6256
|
default:
|
|
5979
6257
|
return `(function() { return { error: 'Unknown lasso action: ${input.action}' }; })();`;
|
|
5980
6258
|
}
|
|
5981
6259
|
}
|
|
5982
6260
|
generateCutoutStyle(input) {
|
|
5983
|
-
|
|
6261
|
+
// THE FACADE WAS THE WRONG ONE ENTIRELY.
|
|
6262
|
+
//
|
|
6263
|
+
// These called app.imageTools.applyCutoutStyle / .getCutoutStyles, and
|
|
6264
|
+
// neither exists anywhere in the engine — imageTools is an
|
|
6265
|
+
// ImageToolsManager and knows nothing about cutout presets. Reported from
|
|
6266
|
+
// production as "apply returns success:true with the error nested inside",
|
|
6267
|
+
// which is what awaiting a call on undefined inside a try looks like.
|
|
6268
|
+
//
|
|
6269
|
+
// The real surface is app.cutoutStyles (a CutoutStylePresets), with
|
|
6270
|
+
// applyPreset(item, name, options) and getPresets(). The parity guard
|
|
6271
|
+
// could not see this: readFacades resolved a class by CONVENTION —
|
|
6272
|
+
// imageTools -> ImageTools, which is not the class's name — so the facade
|
|
6273
|
+
// was never mapped, and an unmapped facade is not checked.
|
|
6274
|
+
const guard = `if (!app.cutoutStyles) return { success: false, error: 'cutout styles are unavailable on this studio (app.cutoutStyles is absent) — update PinePaper Studio.' };`;
|
|
5984
6275
|
switch (input.action) {
|
|
5985
6276
|
case 'apply':
|
|
5986
6277
|
return `
|
|
@@ -5988,7 +6279,11 @@ ${resolve}
|
|
|
5988
6279
|
(async function() {
|
|
5989
6280
|
${guard}
|
|
5990
6281
|
try {
|
|
5991
|
-
const
|
|
6282
|
+
const target = app.getItemById && app.getItemById(${JSON.stringify(input.itemId || '')});
|
|
6283
|
+
if (!target) {
|
|
6284
|
+
return { success: false, action: 'apply', error: 'no item ' + ${JSON.stringify(input.itemId || '')} + ' — applyPreset takes a live item, so the id has to resolve first.' };
|
|
6285
|
+
}
|
|
6286
|
+
const result = await app.cutoutStyles.applyPreset(target, ${JSON.stringify(input.preset || '')}, ${JSON.stringify(input.options || {})});
|
|
5992
6287
|
// An unknown preset returns the very item it was given, unchanged — so a
|
|
5993
6288
|
// truthy result proves nothing. The engine records the refusal on it.
|
|
5994
6289
|
const cutRej = result && result.data && result.data.cutoutStyleRejected;
|
|
@@ -6005,7 +6300,9 @@ ${resolve}
|
|
|
6005
6300
|
// List available cutout styles
|
|
6006
6301
|
(function() {
|
|
6007
6302
|
${guard}
|
|
6008
|
-
const styles = app.
|
|
6303
|
+
const styles = typeof app.getCutoutStylePresets === 'function'
|
|
6304
|
+
? app.getCutoutStylePresets()
|
|
6305
|
+
: app.cutoutStyles.getPresets();
|
|
6009
6306
|
return { success: true, action: 'list', styles };
|
|
6010
6307
|
})();`.trim();
|
|
6011
6308
|
default:
|
|
@@ -6046,14 +6343,25 @@ ${resolve}
|
|
|
6046
6343
|
|
|
6047
6344
|
const precomp = app.createPrecomp(items, opts);
|
|
6048
6345
|
if (!precomp) return { success: false, error: 'createPrecomp returned nothing' };
|
|
6049
|
-
|
|
6346
|
+
// STRINGIFIED, because the schema declares precompId as a string and the
|
|
6347
|
+
// engine mints a NUMBER. Returning the number meant the id could not be
|
|
6348
|
+
// round-tripped: pass it back to add/remove and zod rejects it before the
|
|
6349
|
+
// call is made.
|
|
6350
|
+
const _pid = precomp && precomp.id !== undefined ? precomp.id : precomp;
|
|
6351
|
+
return { success: true, action: 'create', precompId: String(_pid), itemCount: items.length, name: ${JSON.stringify(input.name || '')} };
|
|
6050
6352
|
})();`.trim();
|
|
6051
6353
|
}
|
|
6052
6354
|
case 'add':
|
|
6053
6355
|
return `
|
|
6054
6356
|
// Add item to precomp
|
|
6055
6357
|
(function() {
|
|
6056
|
-
|
|
6358
|
+
// addToPrecomp answers falsy when the precomp or the item is not found, and
|
|
6359
|
+
// this reported success regardless — the same silent-success shape as an
|
|
6360
|
+
// unknown itemId elsewhere.
|
|
6361
|
+
const _ok = app.addToPrecomp(${JSON.stringify(input.precompId || '')}, ${JSON.stringify(input.itemId || '')});
|
|
6362
|
+
if (_ok === false || _ok === null || _ok === undefined) {
|
|
6363
|
+
return { success: false, action: 'add', error: 'nothing was added — check the precompId (create returns it as a string) and the itemId with pinepaper_get_items.' };
|
|
6364
|
+
}
|
|
6057
6365
|
return { success: true, action: 'add', precompId: ${JSON.stringify(input.precompId || '')}, itemId: ${JSON.stringify(input.itemId || '')} };
|
|
6058
6366
|
})();`.trim();
|
|
6059
6367
|
case 'remove':
|
|
@@ -6572,7 +6880,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
6572
6880
|
// Trigger interaction action
|
|
6573
6881
|
(function() {
|
|
6574
6882
|
${guard}
|
|
6575
|
-
app.interactionSystem.
|
|
6883
|
+
app.interactionSystem.executeAction('${input.actionType}', ${params});
|
|
6576
6884
|
return { success: true, action: 'trigger_action', actionType: '${input.actionType}' };
|
|
6577
6885
|
})();`.trim();
|
|
6578
6886
|
}
|
|
@@ -7468,6 +7776,9 @@ ${guard}
|
|
|
7468
7776
|
...(r.note ? { note: r.note } : {}) };
|
|
7469
7777
|
})();`.trim();
|
|
7470
7778
|
}
|
|
7779
|
+
// 'list' is the spelling pinepaper_text_effect uses for the same action,
|
|
7780
|
+
// accepted here so moving between the two tools does not dead-end.
|
|
7781
|
+
case 'list':
|
|
7471
7782
|
case 'list_styles': {
|
|
7472
7783
|
return `
|
|
7473
7784
|
// Text styles + palettes + font axes — the picker surface
|
|
@@ -7703,9 +8014,42 @@ ${guard}
|
|
|
7703
8014
|
// r.items are live Paper objects — NOT serializable, deliberately dropped.
|
|
7704
8015
|
// The source item id is GONE unless keepSource was set, so r.ids is now the
|
|
7705
8016
|
// caller's entire handle set; returning it is what makes the tool usable.
|
|
8017
|
+
// EVERY EFFECT PLAYS FROM t=0, so in a multi-shot video they all fire on the
|
|
8018
|
+
// opening frame and only the first title reads. The engine has no start
|
|
8019
|
+
// delay, so the keyframes it just wrote are shifted here — the same thing a
|
|
8020
|
+
// caller was doing by hand through execute_custom_code.
|
|
8021
|
+
//
|
|
8022
|
+
// Shifted, then held: without an explicit invisible keyframe at 0 the
|
|
8023
|
+
// characters sit in their FIRST pose until the effect starts, which for most
|
|
8024
|
+
// of the 37 is a scatter that looks like a bug. hideAfter closes the other
|
|
8025
|
+
// end, because they otherwise survive to the end of the clip.
|
|
8026
|
+
const _startAt = ${S(input.startAt ?? 0)};
|
|
8027
|
+
const _hideAfter = ${S(input.hideAfter ?? null)};
|
|
8028
|
+
let _shifted = 0;
|
|
8029
|
+
if ((_startAt > 0 || _hideAfter) && Array.isArray(r.ids)) {
|
|
8030
|
+
for (const _id of r.ids) {
|
|
8031
|
+
const _it = app.getItemById && app.getItemById(_id);
|
|
8032
|
+
const _kfs = _it && _it.data && _it.data.keyframes;
|
|
8033
|
+
if (!Array.isArray(_kfs) || _kfs.length === 0) continue;
|
|
8034
|
+
if (_startAt > 0) {
|
|
8035
|
+
for (const _k of _kfs) _k.time += _startAt;
|
|
8036
|
+
_kfs.unshift({ time: 0, properties: { opacity: 0 }, easing: 'linear' });
|
|
8037
|
+
_kfs.splice(1, 0, { time: Math.max(0, _startAt - 0.001), properties: { opacity: 0 }, easing: 'linear' });
|
|
8038
|
+
}
|
|
8039
|
+
if (_hideAfter) {
|
|
8040
|
+
_kfs.push({ time: Math.max(0, _hideAfter - 0.001), properties: { opacity: 1 }, easing: 'linear' });
|
|
8041
|
+
_kfs.push({ time: _hideAfter, properties: { opacity: 0 }, easing: 'linear' });
|
|
8042
|
+
}
|
|
8043
|
+
_kfs.sort((a, b) => a.time - b.time);
|
|
8044
|
+
_shifted++;
|
|
8045
|
+
}
|
|
8046
|
+
}
|
|
7706
8047
|
return {
|
|
7707
8048
|
success: true, action: 'apply', effect: r.effect, ids: r.ids, count: r.count,
|
|
7708
|
-
duration: r.duration, sourceRemoved: ${S(input.keepSource !== true)},
|
|
8049
|
+
duration: (r.duration || 0) + _startAt, sourceRemoved: ${S(input.keepSource !== true)},
|
|
8050
|
+
...(_startAt > 0 ? { startAt: _startAt } : {}),
|
|
8051
|
+
...(_hideAfter ? { hideAfter: _hideAfter } : {}),
|
|
8052
|
+
...((_startAt > 0 || _hideAfter) ? { retimed: _shifted } : {}),
|
|
7709
8053
|
};
|
|
7710
8054
|
})();`.trim();
|
|
7711
8055
|
}
|
|
@@ -7828,7 +8172,7 @@ ${guard}
|
|
|
7828
8172
|
// World3D: create — terrain, sky, shadows, walkable character, under the Paper canvas
|
|
7829
8173
|
(async function() {
|
|
7830
8174
|
if (typeof app.createWorld3D !== 'function') { return { success: false, error: 'app.createWorld3D unavailable — update FxTool to a world3d-capable build' }; }
|
|
7831
|
-
await app.createWorld3D(${S(input.spec ?? 'forest')}, ${opts});
|
|
8175
|
+
await app.createWorld3D(${S(world3dColors(input.spec ?? 'forest'))}, ${opts});
|
|
7832
8176
|
// describe() is the world's own parameter schema — return the preset list +
|
|
7833
8177
|
// top-level keys so the agent knows what configure can touch, without the
|
|
7834
8178
|
// full multi-KB schema on every create.
|
|
@@ -7849,7 +8193,7 @@ ${guard}
|
|
|
7849
8193
|
(function() {
|
|
7850
8194
|
if (typeof app.configureWorld3D !== 'function') { return { success: false, error: 'app.configureWorld3D unavailable — update FxTool' }; }
|
|
7851
8195
|
${needWorld}
|
|
7852
|
-
const r = app.configureWorld3D(${S(input.patch)});
|
|
8196
|
+
const r = app.configureWorld3D(${S(world3dColors(input.patch))});
|
|
7853
8197
|
// The validator names the right key on a wrong one — forward it verbatim.
|
|
7854
8198
|
return r && r.ok ? { success: true, action: 'configure' } : { success: false, error: (r && r.error) || 'configure failed' };
|
|
7855
8199
|
})();`.trim();
|
|
@@ -7905,7 +8249,7 @@ ${needWorld}
|
|
|
7905
8249
|
// World3D: place an object (y defaults to sitting on the terrain)
|
|
7906
8250
|
(function() {
|
|
7907
8251
|
${needWorld}
|
|
7908
|
-
const id = app.addWorldObject(${S(input.object)});
|
|
8252
|
+
const id = app.addWorldObject(${S(world3dColors(input.object))});
|
|
7909
8253
|
return id ? { success: true, objectId: id } : { success: false, error: 'object not added' };
|
|
7910
8254
|
})();`.trim();
|
|
7911
8255
|
case 'remove_object':
|
|
@@ -8442,6 +8786,24 @@ ${guard('exportBVH')}
|
|
|
8442
8786
|
${guard('exportPNGSequence')}
|
|
8443
8787
|
const out = await app.exportPNGSequence(${opts});
|
|
8444
8788
|
if (!out) { return { success: false, error: 'PNG sequence produced nothing' }; }
|
|
8789
|
+
// A BLOB SERIALISES TO {}. exporter.export() answers a Blob, and returning it
|
|
8790
|
+
// raw produced a success whose result was an empty object — which reads as
|
|
8791
|
+
// "it worked and gave me nothing" rather than "the bytes could not cross
|
|
8792
|
+
// the bridge". Structured clone drops a Blob's
|
|
8793
|
+
// contents; only a string survives.
|
|
8794
|
+
if (typeof Blob !== 'undefined' && out instanceof Blob) {
|
|
8795
|
+
const dataUrl = await new Promise(function(res, rej) {
|
|
8796
|
+
const fr = new FileReader();
|
|
8797
|
+
fr.onload = function() { res(fr.result); };
|
|
8798
|
+
fr.onerror = function() { rej(new Error('could not read the PNG sequence')); };
|
|
8799
|
+
fr.readAsDataURL(out);
|
|
8800
|
+
});
|
|
8801
|
+
return { success: true, format: 'png-sequence', data: dataUrl, mimeType: out.type || 'application/zip', size: out.size };
|
|
8802
|
+
}
|
|
8803
|
+
if (Array.isArray(out)) {
|
|
8804
|
+
return { success: true, format: 'png-sequence', frames: out.length,
|
|
8805
|
+
note: 'the frames stayed in the page — pass download: true to write them out.' };
|
|
8806
|
+
}
|
|
8445
8807
|
return { success: true, format: 'png-sequence', result: out };
|
|
8446
8808
|
})();`.trim();
|
|
8447
8809
|
}
|
|
@@ -8586,15 +8948,27 @@ ${guard('renderSoundtrackWav')}
|
|
|
8586
8948
|
};
|
|
8587
8949
|
})();`.trim();
|
|
8588
8950
|
}
|
|
8589
|
-
case 'create':
|
|
8951
|
+
case 'create': {
|
|
8952
|
+
const v = (input.visual ?? {});
|
|
8953
|
+
const { startTime, duration, ...visualOnly } = v;
|
|
8954
|
+
const placement = (startTime !== undefined || duration !== undefined)
|
|
8955
|
+
? { ...(startTime !== undefined ? { startTime } : {}), ...(duration !== undefined ? { duration } : {}) }
|
|
8956
|
+
: null;
|
|
8590
8957
|
return `
|
|
8591
8958
|
// Sound: draw it AS a waveform path — the item and the sound are one thing
|
|
8592
8959
|
(function() {
|
|
8593
8960
|
${guard('createSound')}
|
|
8594
|
-
const item = app.createSound(${S(input.spec ?? {})}, ${S(
|
|
8961
|
+
const item = app.createSound(${S(input.spec ?? {})}, ${S(visualOnly)});
|
|
8595
8962
|
if (!item) { return { success: false, error: 'the sound produced no waveform path' }; }
|
|
8596
|
-
|
|
8963
|
+
const _sid = item.data && item.data.id;
|
|
8964
|
+
// ONE CALL PER CUE, NOT TWO. startTime/duration were stripped from visual,
|
|
8965
|
+
// so placing a cue meant create + set_placement every time — a pilot made
|
|
8966
|
+
// 160 calls where 80 would do. Applied here when given, using the same
|
|
8967
|
+
// setSoundPlacement the separate action calls.
|
|
8968
|
+
${placement ? `if (_sid && app.setSoundPlacement) { app.setSoundPlacement(_sid, ${S(placement)}); }` : ''}
|
|
8969
|
+
return { success: true, itemId: _sid, itemType: 'sound'${placement ? `, placement: ${S(placement)}` : ''} };
|
|
8597
8970
|
})();`.trim();
|
|
8971
|
+
}
|
|
8598
8972
|
case 'timbre_from_path':
|
|
8599
8973
|
return `
|
|
8600
8974
|
// Sound: read a drawn path's shape as harmonic content
|