@pinepaper.studio/mcp-server 1.6.2 → 1.6.4
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 +45 -8
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +561 -7
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +73 -2
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +21 -3
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +25 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +51 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +775 -37
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +2688 -119
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +357 -8
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +65 -9
- package/package.json +4 -4
|
@@ -192,16 +192,28 @@ app.historyManager.saveState();
|
|
|
192
192
|
function generateDeleteItemCode(itemId) {
|
|
193
193
|
return `
|
|
194
194
|
// Delete item ${itemId}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
|
|
195
|
+
//
|
|
196
|
+
// Delegate to app.deleteItem rather than hand-rolling the removal. The old
|
|
197
|
+
// inline version called \`app.itemRegistry.remove(...)\`, which does not exist —
|
|
198
|
+
// the method is \`unregister\` — so the Paper item left the canvas while its
|
|
199
|
+
// REGISTRY ROW SURVIVED, pointing at a detached object. The scene then reported
|
|
200
|
+
// nodes that were not on screen, agents could still address them, and relations
|
|
201
|
+
// to them looked alive. It also skipped the identity-keyed trackers
|
|
202
|
+
// (animatedItems / keyframeItems / selectedItems), so a deleted item that was
|
|
203
|
+
// animating kept being ticked every frame.
|
|
204
|
+
//
|
|
205
|
+
// app.deleteItem does all of that, plus the redraw, the history snapshot and
|
|
206
|
+
// the collaboration mutation event, and it resolves any ref form.
|
|
207
|
+
(function () {
|
|
208
|
+
if (typeof app.deleteItem !== 'function') {
|
|
209
|
+
return { success: false, error: 'app.deleteItem unavailable — update FxTool' };
|
|
210
|
+
}
|
|
211
|
+
const ok = app.deleteItem('${itemId}');
|
|
212
|
+
if (!ok) {
|
|
213
|
+
return { success: false, error: 'Item not found: ${itemId}' };
|
|
214
|
+
}
|
|
215
|
+
return { success: true, itemId: '${itemId}' };
|
|
216
|
+
})();
|
|
205
217
|
`.trim();
|
|
206
218
|
}
|
|
207
219
|
/**
|
|
@@ -247,7 +259,11 @@ const relations = app.getRelations('${itemId}', ${typeArg});
|
|
|
247
259
|
// Format results
|
|
248
260
|
const formatted = relations.map(r => ({
|
|
249
261
|
sourceId: r.sourceId || '${itemId}',
|
|
250
|
-
|
|
262
|
+
// RelationRegistry.getAssociations returns { relation, target, params } —
|
|
263
|
+
// there is no \`targetId\` on it. Reading the wrong field made EVERY edge come
|
|
264
|
+
// back with an undefined destination, so the graph was unreadable through
|
|
265
|
+
// this tool even though it was wired correctly on the canvas.
|
|
266
|
+
targetId: r.target ?? r.targetId ?? null,
|
|
251
267
|
relationType: r.relation || r.type,
|
|
252
268
|
params: r.params || {}
|
|
253
269
|
}));
|
|
@@ -265,7 +281,10 @@ const results = app.queryByRelationTarget('${itemId}', ${typeArg});
|
|
|
265
281
|
const relations = results.map(r => ({
|
|
266
282
|
sourceId: r.itemId,
|
|
267
283
|
targetId: '${itemId}',
|
|
268
|
-
|
|
284
|
+
// queryByTarget returns the real relation name on each result. Hardcoding
|
|
285
|
+
// the filter string labelled every edge 'unknown' on an unfiltered query —
|
|
286
|
+
// which is exactly the query you make when exploring an unfamiliar graph.
|
|
287
|
+
relationType: r.relation || ${relationType ? `'${relationType}'` : "'unknown'"},
|
|
269
288
|
params: r.params || {}
|
|
270
289
|
}));
|
|
271
290
|
|
|
@@ -869,19 +888,38 @@ export class PinePaperCodeGenerator {
|
|
|
869
888
|
/**
|
|
870
889
|
* Generate code for importing SVG
|
|
871
890
|
*/
|
|
872
|
-
generateImportSVG(svgString, url, position = { x: 400, y: 300 }, scale = 1.0) {
|
|
891
|
+
generateImportSVG(svgString, url, position = { x: 400, y: 300 }, scale = 1.0, source) {
|
|
892
|
+
// Figma is an OPTION on this tool rather than a tool of its own: it is the
|
|
893
|
+
// same import with a normalisation pass in front, and the tool count is
|
|
894
|
+
// already the thing the consolidation backlog objects to.
|
|
895
|
+
//
|
|
896
|
+
// The pass is not cosmetic. Figma's "Copy as SVG" carries a root
|
|
897
|
+
// fill="none" that INHERITS (everything renders invisible — the classic "I
|
|
898
|
+
// pasted my icon and got nothing"), frequently no viewBox, and global ids
|
|
899
|
+
// like `clip0` that make a second import silently adopt the first one's
|
|
900
|
+
// clipPath.
|
|
901
|
+
const importExpr = (varName) => source === 'figma'
|
|
902
|
+
? `(function () {
|
|
903
|
+
if (typeof app.importFigmaSVG !== 'function') {
|
|
904
|
+
return { item: app.importSVG(${varName}), changes: ['figma normalisation unavailable — update PinePaper Studio'] };
|
|
905
|
+
}
|
|
906
|
+
const r = app.importFigmaSVG(${varName});
|
|
907
|
+
return { item: r.item || null, changes: r.changes || [] };
|
|
908
|
+
})()`
|
|
909
|
+
: `{ item: app.importSVG(${varName}), changes: [] }`;
|
|
873
910
|
if (url) {
|
|
874
911
|
return `
|
|
875
|
-
// Import SVG from URL
|
|
912
|
+
// Import SVG from URL${source === 'figma' ? ' (Figma-normalised)' : ''}
|
|
876
913
|
const response = await fetch('${url}');
|
|
877
914
|
const svgText = await response.text();
|
|
878
|
-
const
|
|
915
|
+
const _r = ${importExpr('svgText')};
|
|
916
|
+
const imported = _r.item;
|
|
879
917
|
if (imported) {
|
|
880
918
|
imported.position = new paper.Point(${position.x}, ${position.y});
|
|
881
919
|
imported.scale(${scale});
|
|
882
920
|
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
883
921
|
app.historyManager.saveState();
|
|
884
|
-
({ success: true, itemId, position: { x: ${position.x}, y: ${position.y} } });
|
|
922
|
+
({ success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} } });
|
|
885
923
|
} else {
|
|
886
924
|
throw new Error('Failed to import SVG from URL');
|
|
887
925
|
}
|
|
@@ -891,15 +929,16 @@ if (imported) {
|
|
|
891
929
|
// Escape the SVG string for embedding in code
|
|
892
930
|
const escapedSvg = svgString.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
893
931
|
return `
|
|
894
|
-
// Import SVG string
|
|
932
|
+
// Import SVG string${source === 'figma' ? ' (Figma-normalised)' : ''}
|
|
895
933
|
const svgString = \`${escapedSvg}\`;
|
|
896
|
-
const
|
|
934
|
+
const _r = ${importExpr('svgString')};
|
|
935
|
+
const imported = _r.item;
|
|
897
936
|
if (imported) {
|
|
898
937
|
imported.position = new paper.Point(${position.x}, ${position.y});
|
|
899
938
|
imported.scale(${scale});
|
|
900
939
|
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
901
940
|
app.historyManager.saveState();
|
|
902
|
-
({ success: true, itemId, position: { x: ${position.x}, y: ${position.y} } });
|
|
941
|
+
({ success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} } });
|
|
903
942
|
} else {
|
|
904
943
|
throw new Error('Failed to import SVG');
|
|
905
944
|
}
|
|
@@ -2121,7 +2160,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2121
2160
|
*/
|
|
2122
2161
|
generateAgentExport(input) {
|
|
2123
2162
|
const validated = AgentExportInputSchema.parse(input);
|
|
2124
|
-
const { platform, format, quality, framing, duration } = validated;
|
|
2163
|
+
const { platform, format, quality, framing, duration, estimateOnly } = validated;
|
|
2125
2164
|
const qualityLevel = quality || 'standard';
|
|
2126
2165
|
const videoDuration = duration ?? 5;
|
|
2127
2166
|
// Quality settings (bitrate in bps for VideoEncoder)
|
|
@@ -2156,6 +2195,33 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2156
2195
|
const settings = ${JSON.stringify(qualitySettings)};
|
|
2157
2196
|
const dimensions = ${JSON.stringify({ width: preset.width, height: preset.height })};
|
|
2158
2197
|
|
|
2198
|
+
// Preflight: same resolved settings as the real export, but render nothing.
|
|
2199
|
+
// Branching HERE rather than in a separate tool is deliberate — the estimate
|
|
2200
|
+
// has to see the same platform dimensions, fps and quality the export would
|
|
2201
|
+
// use, and a parallel tool would re-derive them and drift.
|
|
2202
|
+
if (${estimateOnly ? 'true' : 'false'}) {
|
|
2203
|
+
if (typeof app.estimateExportSize !== 'function') {
|
|
2204
|
+
return { success: false, estimateOnly: true, error: 'This PinePaper build has no estimateExportSize() — update the editor to preflight export size.' };
|
|
2205
|
+
}
|
|
2206
|
+
const est = await app.estimateExportSize({
|
|
2207
|
+
format,
|
|
2208
|
+
duration: ${videoDuration},
|
|
2209
|
+
fps: settings.fps,
|
|
2210
|
+
quality,
|
|
2211
|
+
width: dimensions.width,
|
|
2212
|
+
height: dimensions.height,
|
|
2213
|
+
});
|
|
2214
|
+
return {
|
|
2215
|
+
success: true,
|
|
2216
|
+
estimateOnly: true,
|
|
2217
|
+
platform, format, quality,
|
|
2218
|
+
duration: ${videoDuration},
|
|
2219
|
+
fps: settings.fps,
|
|
2220
|
+
dimensions: dimensions.width + 'x' + dimensions.height,
|
|
2221
|
+
estimate: est,
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2159
2225
|
// Resolve camera-view framing dims when requested. Reads the first
|
|
2160
2226
|
// camera_animates keyframe's zoom and divides the canvas dims by it
|
|
2161
2227
|
// (mirrors FxTool ExportEngine.js:466-485). Camera animation still
|
|
@@ -3243,12 +3309,20 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3243
3309
|
const relations = [];
|
|
3244
3310
|
const decorative = [];
|
|
3245
3311
|
|
|
3246
|
-
// Collect items from registry
|
|
3247
|
-
|
|
3248
|
-
|
|
3312
|
+
// Collect items from registry.
|
|
3313
|
+
//
|
|
3314
|
+
// ItemRegistry exposes get/getAll — never \`entries()\`. Iterating a
|
|
3315
|
+
// non-existent method threw on EVERY call, so this tool has never once
|
|
3316
|
+
// returned a scene. getAll() yields registry ENTRIES
|
|
3317
|
+
// ({ itemId, item, type, properties }), not bare Paper items, so the
|
|
3318
|
+
// geometry has to be read off entry.item.
|
|
3319
|
+
if (app.itemRegistry && typeof app.itemRegistry.getAll === 'function') {
|
|
3320
|
+
for (const entry of app.itemRegistry.getAll()) {
|
|
3321
|
+
const item = entry.item;
|
|
3322
|
+
if (!item) continue;
|
|
3249
3323
|
const itemData = {
|
|
3250
|
-
id:
|
|
3251
|
-
type: item.data?.type || item.className?.toLowerCase() || 'unknown',
|
|
3324
|
+
id: entry.itemId,
|
|
3325
|
+
type: entry.type || item.data?.type || item.className?.toLowerCase() || 'unknown',
|
|
3252
3326
|
position: item.position ? { x: item.position.x, y: item.position.y } : null,
|
|
3253
3327
|
bounds: item.bounds ? {
|
|
3254
3328
|
x: item.bounds.x,
|
|
@@ -3266,6 +3340,30 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3266
3340
|
if (item.opacity !== undefined) itemData.properties.opacity = item.opacity;
|
|
3267
3341
|
if (item.rotation) itemData.properties.rotation = item.rotation;
|
|
3268
3342
|
if (item.data?.content) itemData.properties.content = item.data.content;
|
|
3343
|
+
if (item.visible === false) itemData.properties.visible = false;
|
|
3344
|
+
|
|
3345
|
+
// THE SECOND TIMELINE. A video/audio item carries its own clip window
|
|
3346
|
+
// (media-time in/out) plus where that window sits on the project
|
|
3347
|
+
// timeline. Without it a consumer sees "there is a video here" and has
|
|
3348
|
+
// no idea which 5 seconds of it play, or when.
|
|
3349
|
+
const p = entry.properties || {};
|
|
3350
|
+
if (p.inPoint !== undefined || p.outPoint !== undefined || p.clipStartTime !== undefined) {
|
|
3351
|
+
itemData.clip = {
|
|
3352
|
+
clipStartTime: p.clipStartTime ?? 0, // project-timeline start, s
|
|
3353
|
+
inPoint: p.inPoint ?? 0, // media-time in, s
|
|
3354
|
+
outPoint: p.outPoint ?? null // media-time out, s
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
// When this item is on screen at all, merged across keyframe clip
|
|
3359
|
+
// window / loop / mask / relation windows / effects / media clip.
|
|
3360
|
+
// null end = open-ended. Static items report null.
|
|
3361
|
+
if (typeof app.activeWindowOf === 'function') {
|
|
3362
|
+
try {
|
|
3363
|
+
const w = app.activeWindowOf(entry.itemId);
|
|
3364
|
+
if (w) itemData.activeWindow = w;
|
|
3365
|
+
} catch (_) { /* non-fatal — an item without a window is just static */ }
|
|
3366
|
+
}
|
|
3269
3367
|
|
|
3270
3368
|
if (item.data?.isDecorative) {
|
|
3271
3369
|
decorative.push(itemData);
|
|
@@ -3275,14 +3373,19 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3275
3373
|
}
|
|
3276
3374
|
}
|
|
3277
3375
|
|
|
3278
|
-
// Collect relations
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3376
|
+
// Collect relations — the EDGE LIST, which with the items above is the
|
|
3377
|
+
// scene DAG.
|
|
3378
|
+
//
|
|
3379
|
+
// \`app.getRelations()\` takes an itemId; called bare it returns [], so the
|
|
3380
|
+
// old code emitted an empty edge list for every scene. exportForSave() is
|
|
3381
|
+
// the canonical whole-graph dump (the same one history persistence uses)
|
|
3382
|
+
// and returns { fromId, toId, relation, params }.
|
|
3383
|
+
if (app.relationRegistry && typeof app.relationRegistry.exportForSave === 'function') {
|
|
3384
|
+
for (const rel of app.relationRegistry.exportForSave()) {
|
|
3282
3385
|
relations.push({
|
|
3283
|
-
sourceId: rel.
|
|
3284
|
-
targetId: rel.
|
|
3285
|
-
type: rel.
|
|
3386
|
+
sourceId: rel.fromId,
|
|
3387
|
+
targetId: rel.toId ?? null, // null = self-relation, not a missing edge
|
|
3388
|
+
type: rel.relation,
|
|
3286
3389
|
params: rel.params
|
|
3287
3390
|
});
|
|
3288
3391
|
}
|
|
@@ -3698,9 +3801,18 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3698
3801
|
}
|
|
3699
3802
|
generateImportImage(input) {
|
|
3700
3803
|
const { url, position, maxWidth, maxHeight, mask } = input;
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3804
|
+
// Each fragment used to be written with a LEADING comma, as if it followed
|
|
3805
|
+
// an entry that no longer exists — so the first one opened the object with
|
|
3806
|
+
// `{, position: …}` and the emitted code was a syntax error. Passing any
|
|
3807
|
+
// option at all therefore failed 100% of the time, which is why imported
|
|
3808
|
+
// images could never be positioned, sized or masked. Join instead, so the
|
|
3809
|
+
// separators come from the number of options rather than from each one.
|
|
3810
|
+
const optParts = [
|
|
3811
|
+
position ? `position: { x: ${position.x}, y: ${position.y} }` : '',
|
|
3812
|
+
maxWidth !== undefined ? `maxWidth: ${maxWidth}` : '',
|
|
3813
|
+
maxHeight !== undefined ? `maxHeight: ${maxHeight}` : '',
|
|
3814
|
+
].filter(Boolean);
|
|
3815
|
+
const optsLiteral = optParts.length ? `{ ${optParts.join(', ')} }` : '{}';
|
|
3704
3816
|
return `
|
|
3705
3817
|
// Import image from URL
|
|
3706
3818
|
(async function() {
|
|
@@ -3709,9 +3821,20 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3709
3821
|
}
|
|
3710
3822
|
try {
|
|
3711
3823
|
const entry = await app.imageTools.uploadFromURL('${url}');
|
|
3712
|
-
const opts =
|
|
3824
|
+
const opts = ${optsLiteral};
|
|
3713
3825
|
const raster = await app.imageTools.placeImage(entry.id, Object.keys(opts).length > 0 ? opts : undefined);
|
|
3714
|
-
${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''}
|
|
3826
|
+
${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The REGISTRY id is \`data.id\`. \`data.itemId\` has never existed, so this
|
|
3827
|
+
// fell through to \`raster.id\` — a Paper.js NUMBER — and the handle the tool
|
|
3828
|
+
// returned could not be used as a relation endpoint or with modify/animate.
|
|
3829
|
+
// Register the raster if placeImage somehow left it unregistered, so the
|
|
3830
|
+
// caller always gets a usable id rather than a number that looks like one.
|
|
3831
|
+
let itemId = raster.data?.id;
|
|
3832
|
+
if (!itemId && typeof app.registerItem === 'function') {
|
|
3833
|
+
itemId = app.registerItem(raster, 'image', { source: 'mcp-import' });
|
|
3834
|
+
}
|
|
3835
|
+
if (!itemId) {
|
|
3836
|
+
return { error: 'Image placed but not registered — no usable item id. This is a bug; report the scene.' };
|
|
3837
|
+
}
|
|
3715
3838
|
return {
|
|
3716
3839
|
success: true,
|
|
3717
3840
|
itemId: itemId,
|
|
@@ -3990,8 +4113,210 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
3990
4113
|
return `(function() { return { error: 'Unknown selection action: ${input.action}' }; })();`;
|
|
3991
4114
|
}
|
|
3992
4115
|
}
|
|
4116
|
+
/**
|
|
4117
|
+
* @private Guard + call one PinePaper facade, uniformly.
|
|
4118
|
+
*
|
|
4119
|
+
* Every one of the 1.6.4 tools wraps a facade that a given editor build may
|
|
4120
|
+
* not have yet — the server ships independently of the app. A missing method
|
|
4121
|
+
* has to come back as a NAMED error, not a TypeError inside runGenerated:
|
|
4122
|
+
* this repo has shipped three emitters that called methods which never
|
|
4123
|
+
* existed (`imageTools.applyFilter`, `itemRegistry.entries`,
|
|
4124
|
+
* `itemRegistry.remove`) and each reported success or an opaque throw.
|
|
4125
|
+
*/
|
|
4126
|
+
_facadeCall(method, args, label) {
|
|
4127
|
+
return `
|
|
4128
|
+
// ${label}
|
|
4129
|
+
(async function() {
|
|
4130
|
+
if (typeof app.${method} !== 'function') {
|
|
4131
|
+
return { success: false, error: 'app.${method}() unavailable — update PinePaper Studio to a build that has it.' };
|
|
4132
|
+
}
|
|
4133
|
+
const r = await app.${method}(${args});
|
|
4134
|
+
// Facades report { ok } — normalise to { success } so every tool result reads
|
|
4135
|
+
// the same way, while keeping the original payload intact.
|
|
4136
|
+
if (r && typeof r === 'object' && 'ok' in r) {
|
|
4137
|
+
return { success: r.ok !== false, ...r };
|
|
4138
|
+
}
|
|
4139
|
+
return { success: true, result: r };
|
|
4140
|
+
})();`.trim();
|
|
4141
|
+
}
|
|
4142
|
+
generateBrandKit(input) {
|
|
4143
|
+
const opts = JSON.stringify({ ...(input.selectionOnly ? { selectionOnly: true } : {}) });
|
|
4144
|
+
const method = input.action === 'plan' ? 'planBrandKit' : 'applyBrandKit';
|
|
4145
|
+
return this._facadeCall(method, `${JSON.stringify(input.kit)}, ${opts}`, `Brand kit: ${input.action}`);
|
|
4146
|
+
}
|
|
4147
|
+
generateComponent(input) {
|
|
4148
|
+
const ids = JSON.stringify(input.itemIds || []);
|
|
4149
|
+
switch (input.action) {
|
|
4150
|
+
case 'define':
|
|
4151
|
+
// defineComponent takes ITEMS, not ids — resolve through the registry
|
|
4152
|
+
// here so the tool can speak the id space every other tool speaks.
|
|
4153
|
+
return `
|
|
4154
|
+
// Component: define
|
|
4155
|
+
(function() {
|
|
4156
|
+
if (typeof app.defineComponent !== 'function') {
|
|
4157
|
+
return { success: false, error: 'app.defineComponent() unavailable — update PinePaper Studio.' };
|
|
4158
|
+
}
|
|
4159
|
+
const items = ${ids}.map((id) => app.itemRegistry.get(id)?.item).filter(Boolean);
|
|
4160
|
+
if (!items.length) return { success: false, error: 'no live items for the given ids' };
|
|
4161
|
+
const r = app.defineComponent(items, ${JSON.stringify({ name: input.name })});
|
|
4162
|
+
return { success: r.ok !== false, ...r };
|
|
4163
|
+
})();`.trim();
|
|
4164
|
+
case 'list':
|
|
4165
|
+
return this._facadeCall('listComponents', '', 'Component: list');
|
|
4166
|
+
case 'instantiate':
|
|
4167
|
+
return this._facadeCall('instantiateComponent', `${JSON.stringify(input.componentId || '')}, ${JSON.stringify({
|
|
4168
|
+
...(input.position ? { position: input.position } : {}),
|
|
4169
|
+
...(input.overrides ? { overrides: input.overrides } : {}),
|
|
4170
|
+
})}`, 'Component: instantiate');
|
|
4171
|
+
case 'set_override':
|
|
4172
|
+
return this._facadeCall('setComponentOverride', `${JSON.stringify(input.instanceId || '')}, ${JSON.stringify(input.componentKey || '')}, ${JSON.stringify(input.prop || '')}, ${JSON.stringify(input.value ?? null)}`, 'Component: set override');
|
|
4173
|
+
case 'sync':
|
|
4174
|
+
return this._facadeCall('syncComponent', JSON.stringify(input.componentId || ''), 'Component: sync');
|
|
4175
|
+
case 'update_from_instance':
|
|
4176
|
+
return this._facadeCall('updateComponentFromInstance', JSON.stringify(input.instanceId || ''), 'Component: update from instance');
|
|
4177
|
+
case 'detach':
|
|
4178
|
+
return this._facadeCall('detachComponentInstance', JSON.stringify(input.instanceId || ''), 'Component: detach');
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
generateArtboard(input) {
|
|
4182
|
+
switch (input.action) {
|
|
4183
|
+
case 'list_presets':
|
|
4184
|
+
return this._facadeCall('listArtboardPresets', '', 'Artboard: list presets');
|
|
4185
|
+
case 'set': {
|
|
4186
|
+
const target = input.preset
|
|
4187
|
+
? JSON.stringify(input.preset)
|
|
4188
|
+
: JSON.stringify({ width: input.width, height: input.height });
|
|
4189
|
+
return this._facadeCall('setArtboard', target, 'Artboard: set');
|
|
4190
|
+
}
|
|
4191
|
+
case 'set_constraints':
|
|
4192
|
+
return this._facadeCall('setItemConstraints', `${JSON.stringify(input.itemId || '')}, ${JSON.stringify({
|
|
4193
|
+
...(input.horizontal ? { horizontal: input.horizontal } : {}),
|
|
4194
|
+
...(input.vertical ? { vertical: input.vertical } : {}),
|
|
4195
|
+
})}`, 'Artboard: set constraints');
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
generateComment(input) {
|
|
4199
|
+
switch (input.action) {
|
|
4200
|
+
case 'add':
|
|
4201
|
+
return this._facadeCall('addComment', JSON.stringify({
|
|
4202
|
+
text: input.text,
|
|
4203
|
+
...(input.author ? { author: input.author } : {}),
|
|
4204
|
+
...(input.itemId ? { itemId: input.itemId } : {}),
|
|
4205
|
+
...(input.x !== undefined ? { x: input.x } : {}),
|
|
4206
|
+
...(input.y !== undefined ? { y: input.y } : {}),
|
|
4207
|
+
...(input.time !== undefined ? { time: input.time } : {}),
|
|
4208
|
+
}), 'Comment: add');
|
|
4209
|
+
case 'list':
|
|
4210
|
+
return this._facadeCall('listComments', JSON.stringify({
|
|
4211
|
+
...(input.time !== undefined ? { time: input.time } : {}),
|
|
4212
|
+
...(input.includeResolved ? { includeResolved: true } : {}),
|
|
4213
|
+
}), 'Comment: list');
|
|
4214
|
+
case 'resolve':
|
|
4215
|
+
return this._facadeCall('resolveComment', `${JSON.stringify(input.id || '')}, ${input.resolved === false ? 'false' : 'true'}`, 'Comment: resolve');
|
|
4216
|
+
case 'delete':
|
|
4217
|
+
return this._facadeCall('deleteComment', JSON.stringify(input.id || ''), 'Comment: delete');
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
generateProvenance(input) {
|
|
4221
|
+
const id = JSON.stringify(input.itemId);
|
|
4222
|
+
switch (input.action) {
|
|
4223
|
+
case 'get': return this._facadeCall('getItemProvenance', id, 'Provenance: get');
|
|
4224
|
+
case 'lineage': return this._facadeCall('getLineage', id, 'Provenance: lineage');
|
|
4225
|
+
case 'dependents': return this._facadeCall('getDependents', id, 'Provenance: dependents');
|
|
4226
|
+
case 'record':
|
|
4227
|
+
return this._facadeCall('recordLineage', `${id}, ${JSON.stringify(input.kind || 'derived')}, ${JSON.stringify(input.sourceRef || '')}, ${JSON.stringify(input.meta || {})}`, 'Provenance: record lineage');
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
generateCompose(input) {
|
|
4231
|
+
switch (input.action) {
|
|
4232
|
+
case 'list_patterns':
|
|
4233
|
+
return this._facadeCall('listCollagePatterns', '', 'Compose: list patterns');
|
|
4234
|
+
case 'list_treatments':
|
|
4235
|
+
return this._facadeCall('listCameraTreatments', '', 'Compose: list treatments');
|
|
4236
|
+
case 'list_reveals':
|
|
4237
|
+
return this._facadeCall('listReveals', '', 'Compose: list reveals');
|
|
4238
|
+
case 'list_styles':
|
|
4239
|
+
return this._facadeCall('listStyles', '', 'Compose: list styles');
|
|
4240
|
+
case 'set_treatment':
|
|
4241
|
+
return this._facadeCall('setCollageTreatment', `${JSON.stringify(input.rootId || '')}, ${JSON.stringify(input.treatment || '')}, ${JSON.stringify({ ...(input.loop ? { loop: true } : {}) })}`, 'Compose: set treatment');
|
|
4242
|
+
case 'apply':
|
|
4243
|
+
default:
|
|
4244
|
+
// Forward the WHOLE surface. The first version forwarded only
|
|
4245
|
+
// treatment/camera/loop/craft — the temporal (reveal), style, text,
|
|
4246
|
+
// vector and audio halves of composeCollage were unreachable through
|
|
4247
|
+
// MCP, so agents converged on the four static patterns the description
|
|
4248
|
+
// happened to name. A capability gap reads as model bias from outside.
|
|
4249
|
+
return this._facadeCall('composeCollage', `${JSON.stringify(input.pattern || '')}, ${JSON.stringify(input.itemIds || [])}, ${JSON.stringify({
|
|
4250
|
+
...(input.treatment ? { treatment: input.treatment } : {}),
|
|
4251
|
+
...(input.applyCamera === false ? { applyCamera: false } : {}),
|
|
4252
|
+
...(input.loop ? { loop: true } : {}),
|
|
4253
|
+
...(input.craft ? { craft: input.craft } : {}),
|
|
4254
|
+
...(input.reveal !== undefined ? { reveal: input.reveal } : {}),
|
|
4255
|
+
...(input.revealOptions ? { revealOptions: input.revealOptions } : {}),
|
|
4256
|
+
...(input.style ? { style: input.style } : {}),
|
|
4257
|
+
...(input.text ? { text: input.text } : {}),
|
|
4258
|
+
...(input.assets ? { assets: input.assets } : {}),
|
|
4259
|
+
...(input.audio ? { audio: input.audio } : {}),
|
|
4260
|
+
...(input.grid ? { grid: true } : {}),
|
|
4261
|
+
})}`, 'Compose: apply pattern');
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
generateAudioBeats(input) {
|
|
4265
|
+
if (input.action === 'analyze') {
|
|
4266
|
+
return this._facadeCall('analyzeAudio', `${JSON.stringify(input.source || '')}, ${JSON.stringify({
|
|
4267
|
+
...(input.sensitivity !== undefined ? { sensitivity: input.sensitivity } : {}),
|
|
4268
|
+
...(input.minGap !== undefined ? { minGap: input.minGap } : {}),
|
|
4269
|
+
})}`, 'Audio: analyze beats');
|
|
4270
|
+
}
|
|
4271
|
+
return this._facadeCall('animateToBeat', `${JSON.stringify(input.itemId || '')}, ${JSON.stringify({
|
|
4272
|
+
...(input.source ? { source: input.source } : {}),
|
|
4273
|
+
...(input.beats ? { beats: input.beats } : {}),
|
|
4274
|
+
...(input.grid ? { grid: true } : {}),
|
|
4275
|
+
...(input.property ? { property: input.property } : {}),
|
|
4276
|
+
...(input.base !== undefined ? { base: input.base } : {}),
|
|
4277
|
+
...(input.accent !== undefined ? { accent: input.accent } : {}),
|
|
4278
|
+
...(input.decay !== undefined ? { decay: input.decay } : {}),
|
|
4279
|
+
...(input.sensitivity !== undefined ? { sensitivity: input.sensitivity } : {}),
|
|
4280
|
+
})}`, 'Audio: animate to beat');
|
|
4281
|
+
}
|
|
4282
|
+
generateTemplateParams(input) {
|
|
4283
|
+
if (input.action === 'get') {
|
|
4284
|
+
return this._facadeCall('getTemplateParams', JSON.stringify(input.templateId), 'Template params: get');
|
|
4285
|
+
}
|
|
4286
|
+
return this._facadeCall('applyTemplateWithParams', `${JSON.stringify(input.templateId)}, ${JSON.stringify(input.params || {})}`, 'Template params: apply');
|
|
4287
|
+
}
|
|
4288
|
+
generateSceneDiff(input) {
|
|
4289
|
+
if (input.action === 'version') {
|
|
4290
|
+
return this._facadeCall('diffAgainstVersion', JSON.stringify(input.versionId || ''), 'Scene diff: vs version');
|
|
4291
|
+
}
|
|
4292
|
+
return this._facadeCall('diffHistoryStates', `${input.indexA ?? 0}, ${input.indexB ?? 0}`, 'Scene diff: history');
|
|
4293
|
+
}
|
|
3993
4294
|
generateTransform(input) {
|
|
3994
4295
|
switch (input.action) {
|
|
4296
|
+
case 'fit': {
|
|
4297
|
+
const mode = input.mode || 'contain';
|
|
4298
|
+
return `
|
|
4299
|
+
// Fit item to the export frame (${mode})
|
|
4300
|
+
(function() {
|
|
4301
|
+
// Route through the agent facade, not app.fitToFrame directly: an agent that
|
|
4302
|
+
// just called upload_video holds a MEDIA id (vraster_…), which is a different
|
|
4303
|
+
// id space from canvas item ids. PinePaperAgent.fitToFrame resolves either.
|
|
4304
|
+
const A = (typeof window !== 'undefined') && window.PinePaperAgent;
|
|
4305
|
+
if (A && typeof A.fitToFrame === 'function') {
|
|
4306
|
+
const ok = A.fitToFrame(${JSON.stringify(input.itemId || '')}, '${mode}');
|
|
4307
|
+
return ok
|
|
4308
|
+
? { success: true, action: 'fit', itemId: '${input.itemId}', mode: '${mode}' }
|
|
4309
|
+
: { success: false, error: 'Item not found or not fittable: ${input.itemId}' };
|
|
4310
|
+
}
|
|
4311
|
+
if (typeof app.fitToFrame !== 'function') {
|
|
4312
|
+
return { success: false, error: 'fitToFrame unavailable — update FxTool' };
|
|
4313
|
+
}
|
|
4314
|
+
const ok = app.fitToFrame(${JSON.stringify(input.itemId || '')}, '${mode}');
|
|
4315
|
+
return ok
|
|
4316
|
+
? { success: true, action: 'fit', itemId: '${input.itemId}', mode: '${mode}' }
|
|
4317
|
+
: { success: false, error: 'Item not found: ${input.itemId}' };
|
|
4318
|
+
})();`.trim();
|
|
4319
|
+
}
|
|
3995
4320
|
case 'nudge': {
|
|
3996
4321
|
const dx = input.dx ?? 0;
|
|
3997
4322
|
const dy = input.dy ?? 0;
|
|
@@ -4123,6 +4448,32 @@ ${resolve}
|
|
|
4123
4448
|
})();`.trim();
|
|
4124
4449
|
default:
|
|
4125
4450
|
return `(function() { return { error: 'Unknown image_filter action: ${input.action}' }; })();`;
|
|
4451
|
+
case 'analyze_palette':
|
|
4452
|
+
return `
|
|
4453
|
+
// Analyze palette: the dominant colours, largest area first (GPU path)
|
|
4454
|
+
(async function() {
|
|
4455
|
+
if (typeof app.analyzePalette !== 'function') { return { success: false, error: 'app.analyzePalette unavailable — update FxTool to a palette-capable build' }; }
|
|
4456
|
+
try {
|
|
4457
|
+
const r = await app.analyzePalette(${JSON.stringify(input.itemId)}, ${JSON.stringify({ ...(input.maxSwatches !== undefined ? { maxSwatches: input.maxSwatches } : {}) })});
|
|
4458
|
+
// swatches: [{hex, share, …}] largest-first — feed hex values straight into
|
|
4459
|
+
// recolor_palette mapping or the paletteMap filter's swatches[].
|
|
4460
|
+
return { success: true, width: r.width, height: r.height, opaquePixels: r.opaquePixels, swatches: r.swatches };
|
|
4461
|
+
} catch (e) {
|
|
4462
|
+
return { success: false, error: e && e.message ? e.message : String(e) };
|
|
4463
|
+
}
|
|
4464
|
+
})();`.trim();
|
|
4465
|
+
case 'recolor_palette':
|
|
4466
|
+
return `
|
|
4467
|
+
// Recolor palette: swap colours, keep the shading (GPU path)
|
|
4468
|
+
(async function() {
|
|
4469
|
+
if (typeof app.recolorPalette !== 'function') { return { success: false, error: 'app.recolorPalette unavailable — update FxTool to a palette-capable build' }; }
|
|
4470
|
+
try {
|
|
4471
|
+
await app.recolorPalette(${JSON.stringify(input.itemId)}, ${JSON.stringify(input.mapping)}, ${JSON.stringify({ ...(input.amount !== undefined ? { amount: input.amount } : {}), ...(input.preserveShading !== undefined ? { preserveShading: input.preserveShading } : {}) })});
|
|
4472
|
+
return { success: true, action: 'recolor_palette', itemId: ${JSON.stringify(input.itemId)} };
|
|
4473
|
+
} catch (e) {
|
|
4474
|
+
return { success: false, error: e && e.message ? e.message : String(e) };
|
|
4475
|
+
}
|
|
4476
|
+
})();`.trim();
|
|
4126
4477
|
}
|
|
4127
4478
|
}
|
|
4128
4479
|
generateLasso(input) {
|
|
@@ -5228,6 +5579,338 @@ ${guard}
|
|
|
5228
5579
|
if (typeof A.setMediaClip !== 'function') { return { success: false, error: 'setMediaClip unavailable — update FxTool' }; }
|
|
5229
5580
|
const ok = A.setMediaClip(${JSON.stringify(input.id)}, ${input.inPoint}, ${input.outPoint});
|
|
5230
5581
|
return { success: ok, action: 'set_clip', id: ${JSON.stringify(input.id)}, inPoint: ${input.inPoint}, outPoint: ${input.outPoint} };
|
|
5582
|
+
})();`.trim();
|
|
5583
|
+
// ── Video-editing actions (v1.6.4) — these live on `app` (PinePaper
|
|
5584
|
+
// facades), not the agent media API, so they carry their own guards.
|
|
5585
|
+
// The facades snapshot history themselves; no saveState here.
|
|
5586
|
+
case 'set_time_remap': {
|
|
5587
|
+
const S = (v) => JSON.stringify(v);
|
|
5588
|
+
return `
|
|
5589
|
+
// Remap a clip's time — speed ramps, freeze frames, reverse (null clears)
|
|
5590
|
+
(function() {
|
|
5591
|
+
if (typeof app.setTimeRemap !== 'function') { return { success: false, error: 'app.setTimeRemap unavailable — update FxTool to a time-remap-capable build' }; }
|
|
5592
|
+
const r = app.setTimeRemap(${S(input.id)}, ${S(input.remapTrack ?? null)});
|
|
5593
|
+
return r && r.ok ? { success: true, action: 'set_time_remap', points: r.points } : { success: false, error: (r && r.error) || 'remap failed' };
|
|
5594
|
+
})();`.trim();
|
|
5595
|
+
}
|
|
5596
|
+
case 'speed_ramp': {
|
|
5597
|
+
const S = (v) => JSON.stringify(v);
|
|
5598
|
+
return `
|
|
5599
|
+
// Speed ramp: consecutive {duration, speed} segments compiled to a remap curve
|
|
5600
|
+
(function() {
|
|
5601
|
+
if (typeof app.speedRamp !== 'function') { return { success: false, error: 'app.speedRamp unavailable — update FxTool to a time-remap-capable build' }; }
|
|
5602
|
+
const r = app.speedRamp(${S(input.id)}, ${S(input.segments)});
|
|
5603
|
+
return r && r.ok ? { success: true, action: 'speed_ramp', points: r.points } : { success: false, error: (r && r.error) || 'speed ramp failed' };
|
|
5604
|
+
})();`.trim();
|
|
5605
|
+
}
|
|
5606
|
+
case 'match_cut': {
|
|
5607
|
+
const S = (v) => JSON.stringify(v);
|
|
5608
|
+
const opts = S({
|
|
5609
|
+
...(input.subject !== undefined ? { subject: input.subject } : {}),
|
|
5610
|
+
...(input.label !== undefined ? { label: input.label } : {}),
|
|
5611
|
+
...(input.at !== undefined ? { at: input.at } : {}),
|
|
5612
|
+
...(input.settle !== undefined ? { settle: input.settle } : {}),
|
|
5613
|
+
...(input.fade !== undefined ? { fade: input.fade } : {}),
|
|
5614
|
+
...(input.consent !== undefined ? { consent: input.consent } : {}),
|
|
5615
|
+
});
|
|
5616
|
+
return `
|
|
5617
|
+
// Match cut: align the SUBJECT across the cut (on-device detection)
|
|
5618
|
+
(async function() {
|
|
5619
|
+
if (typeof app.matchCut !== 'function') { return { success: false, error: 'app.matchCut unavailable — update FxTool to a match-cut-capable build' }; }
|
|
5620
|
+
const r = await app.matchCut(${S(input.fromItemId)}, ${S(input.toItemId)}, ${opts});
|
|
5621
|
+
if (r && r.needsConsent) { return { success: false, needsConsent: true, cost: r.cost, error: 'Detection model download needs consent — re-call with consent: true' }; }
|
|
5622
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'match cut failed' }; }
|
|
5623
|
+
return { success: true, action: 'match_cut', aspectMismatch: r.aspectMismatch || false };
|
|
5624
|
+
})();`.trim();
|
|
5625
|
+
}
|
|
5626
|
+
case 'apply_track_matte': {
|
|
5627
|
+
const S = (v) => JSON.stringify(v);
|
|
5628
|
+
const opts = S({
|
|
5629
|
+
...(input.channel !== undefined ? { channel: input.channel } : {}),
|
|
5630
|
+
...(input.invert !== undefined ? { invert: input.invert } : {}),
|
|
5631
|
+
...(input.strength !== undefined ? { strength: input.strength } : {}),
|
|
5632
|
+
...(input.hideMatte !== undefined ? { hideMatte: input.hideMatte } : {}),
|
|
5633
|
+
...(input.live !== undefined ? { live: input.live } : {}),
|
|
5634
|
+
});
|
|
5635
|
+
return `
|
|
5636
|
+
// Track matte: the matte item's ${input.channel || 'luma'} drives the target's alpha${input.live ? ' (LIVE — re-cuts as the matte animates)' : ''}
|
|
5637
|
+
(async function() {
|
|
5638
|
+
if (typeof app.applyTrackMatte !== 'function') { return { success: false, error: 'app.applyTrackMatte unavailable — update FxTool to a track-matte-capable build' }; }
|
|
5639
|
+
try {
|
|
5640
|
+
await app.applyTrackMatte(${S(input.id)}, ${S(input.matteItemId)}, ${opts});
|
|
5641
|
+
return { success: true, action: 'apply_track_matte', id: ${S(input.id)}, matte: ${S(input.matteItemId)}, live: ${S(!!input.live)} };
|
|
5642
|
+
} catch (e) {
|
|
5643
|
+
// The facade throws on a missing raster/matte rather than returning {ok:false}.
|
|
5644
|
+
return { success: false, error: e && e.message ? e.message : String(e) };
|
|
5645
|
+
}
|
|
5646
|
+
})();`.trim();
|
|
5647
|
+
}
|
|
5648
|
+
case 'stop_live_matte': {
|
|
5649
|
+
const S = (v) => JSON.stringify(v);
|
|
5650
|
+
return `
|
|
5651
|
+
// Stop a live matte, leaving the last cut in place
|
|
5652
|
+
(function() {
|
|
5653
|
+
if (typeof app.stopLiveMatte !== 'function') { return { success: false, error: 'app.stopLiveMatte unavailable — update FxTool' }; }
|
|
5654
|
+
const r = app.stopLiveMatte(${S(input.id)});
|
|
5655
|
+
return r && r.ok ? { success: true, action: 'stop_live_matte' } : { success: false, error: (r && r.error) || 'no live matte on that item' };
|
|
5656
|
+
})();`.trim();
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
/**
|
|
5661
|
+
* pinepaper_text_style — display styles + variable-font axes.
|
|
5662
|
+
* All three facades return their own {ok, …} envelopes; forwarded as-is.
|
|
5663
|
+
*/
|
|
5664
|
+
generateTextStyle(input) {
|
|
5665
|
+
const S = (v) => JSON.stringify(v);
|
|
5666
|
+
switch (input.action) {
|
|
5667
|
+
case 'apply_style': {
|
|
5668
|
+
const opts = S({
|
|
5669
|
+
...(input.palette !== undefined ? { palette: input.palette } : {}),
|
|
5670
|
+
...(input.variant !== undefined ? { variant: input.variant } : {}),
|
|
5671
|
+
...(input.content !== undefined ? { content: input.content } : {}),
|
|
5672
|
+
...(input.fontFamily !== undefined ? { fontFamily: input.fontFamily } : {}),
|
|
5673
|
+
...(input.fontSize !== undefined ? { fontSize: input.fontSize } : {}),
|
|
5674
|
+
});
|
|
5675
|
+
return `
|
|
5676
|
+
// Display text style: ${input.styleKey} — stacked-layer title, id adopted from the text
|
|
5677
|
+
(function() {
|
|
5678
|
+
if (typeof app.applyTextStyle !== 'function') { return { success: false, error: 'app.applyTextStyle unavailable — update FxTool to a text-styles build' }; }
|
|
5679
|
+
const r = app.applyTextStyle(${S(input.itemId)}, ${S(input.styleKey)}, ${opts});
|
|
5680
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'style failed' }; }
|
|
5681
|
+
// The styled group ADOPTS the text item's registry id — r.id is the SAME id
|
|
5682
|
+
// the caller passed, so their handle keeps working. Say so in the result.
|
|
5683
|
+
return { success: true, action: 'apply_style', id: r.id, style: r.style, layers: r.layers, palette: r.palette, bounds: r.bounds };
|
|
5684
|
+
})();`.trim();
|
|
5685
|
+
}
|
|
5686
|
+
case 'set_font_axes': {
|
|
5687
|
+
return `
|
|
5688
|
+
// Variable-font axes (standard trio: weight/width/slant — Canvas 2D has no custom axes)
|
|
5689
|
+
(function() {
|
|
5690
|
+
if (typeof app.setFontAxes !== 'function') { return { success: false, error: 'app.setFontAxes unavailable — update FxTool to a variable-font build' }; }
|
|
5691
|
+
const r = app.setFontAxes(${S(input.itemId)}, ${S(input.axes)});
|
|
5692
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'axes failed' }; }
|
|
5693
|
+
// rejected is the interesting half: an axis silently ignored is the failure
|
|
5694
|
+
// mode this surface exists to prevent.
|
|
5695
|
+
return { success: true, action: 'set_font_axes', applied: r.applied, rejected: r.rejected || [] };
|
|
5696
|
+
})();`.trim();
|
|
5697
|
+
}
|
|
5698
|
+
case 'list_styles': {
|
|
5699
|
+
return `
|
|
5700
|
+
// Text styles + palettes + font axes — the picker surface
|
|
5701
|
+
(function() {
|
|
5702
|
+
if (typeof app.listTextStyles !== 'function') { return { success: false, error: 'app.listTextStyles unavailable — update FxTool to a text-styles build' }; }
|
|
5703
|
+
return {
|
|
5704
|
+
success: true,
|
|
5705
|
+
styles: app.listTextStyles(),
|
|
5706
|
+
palettes: typeof app.listTextPalettes === 'function' ? app.listTextPalettes() : [],
|
|
5707
|
+
fontAxes: typeof app.listFontAxes === 'function' ? app.listFontAxes() : null,
|
|
5708
|
+
};
|
|
5709
|
+
})();`.trim();
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
/**
|
|
5714
|
+
* Shatter a raster into a grid of tiles. The group INHERITS the original's
|
|
5715
|
+
* registry id (relations/handles keep pointing at the thing in its place),
|
|
5716
|
+
* and the result is deliberately inert until something animates the pieces.
|
|
5717
|
+
*/
|
|
5718
|
+
generateShatterImage(input) {
|
|
5719
|
+
const S = (v) => JSON.stringify(v);
|
|
5720
|
+
const opts = S({
|
|
5721
|
+
...(input.pieces !== undefined ? { pieces: input.pieces } : {}),
|
|
5722
|
+
...(input.rows !== undefined ? { rows: input.rows } : {}),
|
|
5723
|
+
...(input.cols !== undefined ? { cols: input.cols } : {}),
|
|
5724
|
+
...(input.keepSource !== undefined ? { keepSource: input.keepSource } : {}),
|
|
5725
|
+
});
|
|
5726
|
+
return `
|
|
5727
|
+
// Shatter ${input.itemId} into tiles (inert until animated — that is the point)
|
|
5728
|
+
(function() {
|
|
5729
|
+
if (typeof app.shatterImage !== 'function') { return { success: false, error: 'app.shatterImage unavailable — update FxTool to a shatter-capable build' }; }
|
|
5730
|
+
const r = app.shatterImage(${S(input.itemId)}, ${opts});
|
|
5731
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'shatter failed' }; }
|
|
5732
|
+
// groupId === the original item's id (adopted). tiles/rows/cols tell the
|
|
5733
|
+
// caller what grid they actually got — 100 pieces of a 3:2 photo is 12x8=96.
|
|
5734
|
+
return { success: true, groupId: r.groupId, tiles: r.tiles, rows: r.rows, cols: r.cols };
|
|
5735
|
+
})();`.trim();
|
|
5736
|
+
}
|
|
5737
|
+
/**
|
|
5738
|
+
* Import a decomposed character (layer manifest + per-layer images) as a
|
|
5739
|
+
* role-bound Group. The facade is async and does its own heavy-module
|
|
5740
|
+
* loading (the cold-boot rigging-rules trap is handled inside it).
|
|
5741
|
+
*/
|
|
5742
|
+
generateImportLayeredCharacter(input) {
|
|
5743
|
+
const S = (v) => JSON.stringify(v);
|
|
5744
|
+
const opts = S({
|
|
5745
|
+
...(input.position !== undefined ? { position: input.position } : {}),
|
|
5746
|
+
...(input.scale !== undefined ? { scale: input.scale } : {}),
|
|
5747
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
5748
|
+
});
|
|
5749
|
+
return `
|
|
5750
|
+
// Import a layered character (decomposer output → role-bound parts; blink works out of the box)
|
|
5751
|
+
(async function() {
|
|
5752
|
+
if (typeof app.importLayeredCharacter !== 'function') { return { success: false, error: 'app.importLayeredCharacter unavailable — update FxTool to a layered-character build' }; }
|
|
5753
|
+
// The importer's contract is a Map (it calls images.get) — the wire format is
|
|
5754
|
+
// a plain object, so rebuild the Map here.
|
|
5755
|
+
const bundle = { info: ${S(input.info)}, images: new Map(Object.entries(${S(input.images)})) };
|
|
5756
|
+
const r = await app.importLayeredCharacter(bundle, ${opts});
|
|
5757
|
+
if (!r || !r.groupId) { return { success: false, error: (r && r.error) || 'layered character import failed' }; }
|
|
5758
|
+
// roles maps part roles → item ids; zero roles wired means the character
|
|
5759
|
+
// renders but will NOT animate — surface the count so that cannot hide.
|
|
5760
|
+
return { success: true, groupId: r.groupId, parts: (r.parts || []).length, roles: r.roles || {}, rolesWired: Object.keys(r.roles || {}).length, warnings: r.warnings || [] };
|
|
5761
|
+
})();`.trim();
|
|
5762
|
+
}
|
|
5763
|
+
/**
|
|
5764
|
+
* pinepaper_game — pathfinding + tilemaps via the PinePaper facades.
|
|
5765
|
+
* Both are pure data ops; the interesting glue is in the doc: paths feed
|
|
5766
|
+
* moves_along_path, collisionRects feed pinepaper_physics.
|
|
5767
|
+
*/
|
|
5768
|
+
generateGame(input) {
|
|
5769
|
+
const S = (v) => JSON.stringify(v);
|
|
5770
|
+
switch (input.action) {
|
|
5771
|
+
case 'pathfind': {
|
|
5772
|
+
const opts = S({ ...(input.diagonal !== undefined ? { diagonal: input.diagonal } : {}) });
|
|
5773
|
+
return `
|
|
5774
|
+
// A* pathfind — waypoints for moves_along_path
|
|
5775
|
+
(async function() {
|
|
5776
|
+
if (typeof app.findPath !== 'function') { return { success: false, error: 'app.findPath unavailable — update FxTool to a games-capable build' }; }
|
|
5777
|
+
const r = await app.findPath(${S(input.grid)}, ${S(input.start)}, ${S(input.goal)}, ${opts});
|
|
5778
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'pathfind failed' }; }
|
|
5779
|
+
// path is world-space [{x,y}] — feed it DIRECTLY to pinepaper_add_relation
|
|
5780
|
+
// moves_along_path params.path, or to a keyframe track.
|
|
5781
|
+
return { success: true, path: r.path, waypoints: r.path.length };
|
|
5782
|
+
})();`.trim();
|
|
5783
|
+
}
|
|
5784
|
+
case 'create_tilemap': {
|
|
5785
|
+
const spec = S({
|
|
5786
|
+
cols: input.cols, rows: input.rows,
|
|
5787
|
+
...(input.tileSize !== undefined ? { tileSize: input.tileSize } : {}),
|
|
5788
|
+
...(input.origin !== undefined ? { origin: input.origin } : {}),
|
|
5789
|
+
...(input.tileset !== undefined ? { tileset: input.tileset } : {}),
|
|
5790
|
+
...(input.fills !== undefined ? { fills: input.fills } : {}),
|
|
5791
|
+
});
|
|
5792
|
+
return `
|
|
5793
|
+
// Tilemap: board data + pathfinding grid + merged collision rects
|
|
5794
|
+
(async function() {
|
|
5795
|
+
if (typeof app.createTilemap !== 'function') { return { success: false, error: 'app.createTilemap unavailable — update FxTool to a games-capable build' }; }
|
|
5796
|
+
const r = await app.createTilemap(${spec});
|
|
5797
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'tilemap failed' }; }
|
|
5798
|
+
// map: persist it (item.data / project document) — it is DATA, not items;
|
|
5799
|
+
// grid: pass straight back into pathfind; collisionRects: pinepaper_physics
|
|
5800
|
+
// bodies (greedy-merged, so a 60x1 wall is ONE rect, not 60).
|
|
5801
|
+
return { success: true, map: r.map, grid: r.grid, collisionRects: r.collisionRects, rectCount: r.collisionRects.length };
|
|
5802
|
+
})();`.trim();
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
/**
|
|
5807
|
+
* pinepaper_world3d — every action rides the PinePaper world facades. The
|
|
5808
|
+
* world is lazily created by createWorld3D; every other action guards on the
|
|
5809
|
+
* facade AND on a world existing, with the create hint in the error.
|
|
5810
|
+
*/
|
|
5811
|
+
generateWorld3D(input) {
|
|
5812
|
+
const S = (v) => JSON.stringify(v);
|
|
5813
|
+
const needWorld = ` if (!app._world3d) { return { success: false, error: "no 3D world — call pinepaper_world3d create first" }; }`;
|
|
5814
|
+
switch (input.action) {
|
|
5815
|
+
case 'create': {
|
|
5816
|
+
const opts = S({ ...(input.character !== undefined ? { character: input.character } : {}) });
|
|
5817
|
+
return `
|
|
5818
|
+
// World3D: create — terrain, sky, shadows, walkable character, under the Paper canvas
|
|
5819
|
+
(async function() {
|
|
5820
|
+
if (typeof app.createWorld3D !== 'function') { return { success: false, error: 'app.createWorld3D unavailable — update FxTool to a world3d-capable build' }; }
|
|
5821
|
+
await app.createWorld3D(${S(input.spec ?? 'forest')}, ${opts});
|
|
5822
|
+
// describe() is the world's own parameter schema — return the preset list +
|
|
5823
|
+
// top-level keys so the agent knows what configure can touch, without the
|
|
5824
|
+
// full multi-KB schema on every create.
|
|
5825
|
+
const d = typeof app.describeWorld3D === 'function' ? await app.describeWorld3D() : null;
|
|
5826
|
+
return { success: true, action: 'create', spec: ${S(input.spec ?? 'forest')}, configurableKeys: d ? Object.keys(d.params || d) : [] };
|
|
5827
|
+
})();`.trim();
|
|
5828
|
+
}
|
|
5829
|
+
case 'describe':
|
|
5830
|
+
return `
|
|
5831
|
+
// World3D: the engine's own parameter schema — types, ranges, descriptions
|
|
5832
|
+
(async function() {
|
|
5833
|
+
if (typeof app.describeWorld3D !== 'function') { return { success: false, error: 'app.describeWorld3D unavailable — update FxTool' }; }
|
|
5834
|
+
return { success: true, schema: await app.describeWorld3D() };
|
|
5835
|
+
})();`.trim();
|
|
5836
|
+
case 'configure':
|
|
5837
|
+
return `
|
|
5838
|
+
// World3D: live configure (deep-merged, schema-validated)
|
|
5839
|
+
(function() {
|
|
5840
|
+
if (typeof app.configureWorld3D !== 'function') { return { success: false, error: 'app.configureWorld3D unavailable — update FxTool' }; }
|
|
5841
|
+
${needWorld}
|
|
5842
|
+
const r = app.configureWorld3D(${S(input.patch)});
|
|
5843
|
+
// The validator names the right key on a wrong one — forward it verbatim.
|
|
5844
|
+
return r && r.ok ? { success: true, action: 'configure' } : { success: false, error: (r && r.error) || 'configure failed' };
|
|
5845
|
+
})();`.trim();
|
|
5846
|
+
case 'add_actor': {
|
|
5847
|
+
const actor = S({
|
|
5848
|
+
...(input.actorId !== undefined ? { id: input.actorId } : {}),
|
|
5849
|
+
...(input.x !== undefined ? { x: input.x } : {}),
|
|
5850
|
+
...(input.z !== undefined ? { z: input.z } : {}),
|
|
5851
|
+
...(input.height !== undefined ? { height: input.height } : {}),
|
|
5852
|
+
...(input.sprite !== undefined ? { sprite: input.sprite } : {}),
|
|
5853
|
+
...(input.live !== undefined ? { live: input.live } : {}),
|
|
5854
|
+
});
|
|
5855
|
+
return `
|
|
5856
|
+
// World3D: put an actor on the stage${input.live ? ' (live sprite — a rigged character PERFORMS, not a photograph of itself)' : ''}
|
|
5857
|
+
(function() {
|
|
5858
|
+
if (typeof app.addWorldActor !== 'function') { return { success: false, error: 'app.addWorldActor unavailable — update FxTool' }; }
|
|
5859
|
+
${needWorld}
|
|
5860
|
+
const id = app.addWorldActor(${actor});
|
|
5861
|
+
return id ? { success: true, actorId: id } : { success: false, error: 'actor not added — is the sprite id a canvas item?' };
|
|
5862
|
+
})();`.trim();
|
|
5863
|
+
}
|
|
5864
|
+
case 'remove_actor':
|
|
5865
|
+
return `
|
|
5866
|
+
// World3D: remove actor
|
|
5867
|
+
(function() {
|
|
5868
|
+
${needWorld}
|
|
5869
|
+
return { success: !!app.removeWorldActor(${S(input.actorId)}), actorId: ${S(input.actorId)} };
|
|
5870
|
+
})();`.trim();
|
|
5871
|
+
case 'list_actors':
|
|
5872
|
+
return `
|
|
5873
|
+
// World3D: the addressable stage
|
|
5874
|
+
(function() {
|
|
5875
|
+
${needWorld}
|
|
5876
|
+
return { success: true, actors: app.listWorldActors() };
|
|
5877
|
+
})();`.trim();
|
|
5878
|
+
case 'set_actor_pose':
|
|
5879
|
+
return `
|
|
5880
|
+
// World3D: pose an actor — the setter a timeline, sequencer or agent drives
|
|
5881
|
+
(function() {
|
|
5882
|
+
${needWorld}
|
|
5883
|
+
return { success: !!app.setWorldActorPose(${S(input.actorId)}, ${S(input.pose)}), actorId: ${S(input.actorId)} };
|
|
5884
|
+
})();`.trim();
|
|
5885
|
+
case 'set_camera':
|
|
5886
|
+
return `
|
|
5887
|
+
// World3D: direct the camera (follow | fixed | orbit)
|
|
5888
|
+
(function() {
|
|
5889
|
+
${needWorld}
|
|
5890
|
+
const r = app.setWorldCamera(${S(input.camera)});
|
|
5891
|
+
return r === false ? { success: false, error: 'camera config rejected' } : { success: true };
|
|
5892
|
+
})();`.trim();
|
|
5893
|
+
case 'add_object':
|
|
5894
|
+
return `
|
|
5895
|
+
// World3D: place an object (y defaults to sitting on the terrain)
|
|
5896
|
+
(function() {
|
|
5897
|
+
${needWorld}
|
|
5898
|
+
const id = app.addWorldObject(${S(input.object)});
|
|
5899
|
+
return id ? { success: true, objectId: id } : { success: false, error: 'object not added' };
|
|
5900
|
+
})();`.trim();
|
|
5901
|
+
case 'remove_object':
|
|
5902
|
+
return `
|
|
5903
|
+
// World3D: remove object
|
|
5904
|
+
(function() {
|
|
5905
|
+
${needWorld}
|
|
5906
|
+
return { success: !!app.removeWorldObject(${S(input.objectId)}), objectId: ${S(input.objectId)} };
|
|
5907
|
+
})();`.trim();
|
|
5908
|
+
case 'remove_world':
|
|
5909
|
+
return `
|
|
5910
|
+
// World3D: tear the world down — Paper's canvas is untouched (it was always its own layer)
|
|
5911
|
+
(function() {
|
|
5912
|
+
if (typeof app.removeWorld3D !== 'function') { return { success: false, error: 'app.removeWorld3D unavailable — update FxTool' }; }
|
|
5913
|
+
return { success: !!app.removeWorld3D() };
|
|
5231
5914
|
})();`.trim();
|
|
5232
5915
|
}
|
|
5233
5916
|
}
|
|
@@ -5345,6 +6028,61 @@ ${guard}
|
|
|
5345
6028
|
return wrap('Rigging: save shape key', ` const shapeKeyId = R.saveShapeKey(${S(input.skeletonId)}, ${S(input.name ?? null)});
|
|
5346
6029
|
if (app.historyManager) app.historyManager.saveState();
|
|
5347
6030
|
return { success: !!shapeKeyId, action: 'save_shape_key', shapeKeyId: shapeKeyId };`);
|
|
6031
|
+
// ── Mocap / rig import — these live on `app` (PinePaper facades), not
|
|
6032
|
+
// riggingSystem, and they are async, so they get their own async IIFE
|
|
6033
|
+
// instead of the sync `wrap` above. Each returns the facade's own result
|
|
6034
|
+
// object: it already carries {ok, error, warnings…} and inventing a
|
|
6035
|
+
// second envelope here would just hide the interesting fields.
|
|
6036
|
+
case 'import_bvh': {
|
|
6037
|
+
const opts = S({
|
|
6038
|
+
...(input.view !== undefined ? { view: input.view } : {}),
|
|
6039
|
+
...(input.fps !== undefined ? { fps: input.fps } : {}),
|
|
6040
|
+
...(input.height !== undefined ? { height: input.height } : {}),
|
|
6041
|
+
...(input.rootPosition !== undefined ? { position: input.rootPosition } : {}),
|
|
6042
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
6043
|
+
});
|
|
6044
|
+
return `
|
|
6045
|
+
// Rigging: import BVH mocap as a new rig (stick figure included)
|
|
6046
|
+
(async function() {
|
|
6047
|
+
if (typeof app.importBVH !== 'function') { return { success: false, error: 'app.importBVH unavailable — update FxTool to a BVH-capable build' }; }
|
|
6048
|
+
const r = await app.importBVH(${S(input.bvhText ?? '')}, ${opts});
|
|
6049
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'BVH import failed' }; }
|
|
6050
|
+
// (facade snapshots history itself — a second saveState here would cost an extra undo step)
|
|
6051
|
+
return { success: true, action: 'import_bvh', skeletonId: r.skeletonId, poses: r.poses, duration: r.duration, warnings: r.warnings || [] };
|
|
6052
|
+
})();`.trim();
|
|
6053
|
+
}
|
|
6054
|
+
case 'retarget_bvh': {
|
|
6055
|
+
const opts = S({
|
|
6056
|
+
...(input.fps !== undefined ? { fps: input.fps } : {}),
|
|
6057
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
6058
|
+
});
|
|
6059
|
+
return `
|
|
6060
|
+
// Rigging: retarget a BVH clip onto an EXISTING rig (matched by bone name)
|
|
6061
|
+
(async function() {
|
|
6062
|
+
if (typeof app.retargetBVH !== 'function') { return { success: false, error: 'app.retargetBVH unavailable — update FxTool to a BVH-capable build' }; }
|
|
6063
|
+
const r = await app.retargetBVH(${S(input.bvhText ?? '')}, ${S(input.skeletonId)}, ${opts});
|
|
6064
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'BVH retarget failed' }; }
|
|
6065
|
+
// (facade snapshots history itself — a second saveState here would cost an extra undo step)
|
|
6066
|
+
// matched/unmatched matter: a retarget that matched 2 of 15 bones "succeeded"
|
|
6067
|
+
// and looks broken — the caller needs the numbers to know which happened.
|
|
6068
|
+
return { success: true, action: 'retarget_bvh', skeletonId: r.skeletonId, matched: r.matched, unmatchedSource: r.unmatchedSource, unmatchedTarget: r.unmatchedTarget, poses: r.poses, duration: r.duration, warnings: r.warnings || [] };
|
|
6069
|
+
})();`.trim();
|
|
6070
|
+
}
|
|
6071
|
+
case 'import_spine': {
|
|
6072
|
+
const opts = S({
|
|
6073
|
+
...(input.rootPosition !== undefined ? { position: input.rootPosition } : {}),
|
|
6074
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
6075
|
+
});
|
|
6076
|
+
return `
|
|
6077
|
+
// Rigging: import a Spine JSON export (bones, poses, attachment placeholders)
|
|
6078
|
+
(async function() {
|
|
6079
|
+
if (typeof app.importSpine !== 'function') { return { success: false, error: 'app.importSpine unavailable — update FxTool to a Spine-capable build' }; }
|
|
6080
|
+
const r = await app.importSpine(${S(input.spineJson ?? '')}, ${opts});
|
|
6081
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'Spine import failed' }; }
|
|
6082
|
+
// (facade snapshots history itself — a second saveState here would cost an extra undo step)
|
|
6083
|
+
return { success: true, action: 'import_spine', skeletonId: r.skeletonId, bones: r.bones, placeholders: r.placeholders, animations: (r.animations || []).map(function(a) { return a.name; }), warnings: r.warnings || [] };
|
|
6084
|
+
})();`.trim();
|
|
6085
|
+
}
|
|
5348
6086
|
}
|
|
5349
6087
|
}
|
|
5350
6088
|
/**
|