@pinepaper.studio/mcp-server 1.6.10 → 1.6.11

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.
@@ -181,12 +181,27 @@ app.historyManager.saveState();
181
181
  */
182
182
  function generateModifyItemCode(itemId, properties) {
183
183
  const { contentType, contentFormat, countdownTarget, countdownEndText, ...restProperties } = properties;
184
+ // modifyItem(id, changes), NOT select() + modify().
185
+ //
186
+ // modify() operates on the SELECTION, so this selected an id and then edited
187
+ // whatever was selected — on an id that does not resolve, select changes
188
+ // nothing and modify edits the PREVIOUS selection instead. modifyItem takes
189
+ // the reference and returns false when it cannot resolve it, which is the
190
+ // answer this emitter needs and never asked for.
191
+ //
192
+ // The warning the engine logs on that path is console.warn, which production
193
+ // strips, so the failure was invisible twice over.
184
194
  let code = `
185
195
  // Modify item ${itemId}
186
- app.select('${itemId}');`;
187
- // Pass non-dynamic-content properties to app.modify
196
+ let _modified = true;`;
188
197
  if (Object.keys(restProperties).length > 0) {
189
- code += `\napp.modify(${JSON.stringify(restProperties, null, 2)});`;
198
+ code += `
199
+ _modified = app.modifyItem('${itemId}', ${JSON.stringify(restProperties, null, 2)}) !== false;
200
+ if (!_modified) { return { success: false, error: 'no item ${itemId} — nothing was modified' }; }`;
201
+ }
202
+ else {
203
+ code += `
204
+ if (!app.getItemById('${itemId}')) { return { success: false, error: 'no item ${itemId}' }; }`;
190
205
  }
191
206
  // Handle dynamic content type changes
192
207
  if (contentType !== undefined) {
@@ -212,10 +227,10 @@ if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.it
212
227
  }
213
228
  code += `
214
229
  app.historyManager.saveState();
215
-
216
- // Return success
217
- ({ success: true, itemId: '${itemId}' });`;
218
- return code.trim();
230
+ return { success: true, itemId: '${itemId}' };`;
231
+ // Wrapped, because the body now RETURNS early on a miss rather than falling
232
+ // through to an unconditional success.
233
+ return `(function() {\n${code.trim()}\n})();`;
219
234
  }
220
235
  /**
221
236
  * Template for deleting items
@@ -675,9 +690,24 @@ function generateSetCanvasSizeCode(width, height, preset) {
675
690
  : `{ width: ${width}, height: ${height} }`;
676
691
  return `
677
692
  // Set canvas size
678
- app.setCanvasSize(${sizeArg});
679
- app.historyManager.saveState();
680
- ({ success: true, width: ${width}, height: ${height} });
693
+ (function() {
694
+ // READ THE VERDICT. This called setCanvasSize and then declared success with
695
+ // the dimensions it had ASKED for — so an unknown preset, or a width of -5,
696
+ // came back as a successful resize while the canvas was left exactly as it
697
+ // was. The engine now refuses by name; older builds return undefined, which
698
+ // is not a refusal and must not be read as one.
699
+ const r = app.setCanvasSize(${sizeArg});
700
+ if (r && r.ok === false) {
701
+ return { success: false, error: r.reason || 'the canvas size was refused',
702
+ ...(r.known ? { knownPresets: r.known } : {}) };
703
+ }
704
+ app.historyManager.saveState();
705
+ // Report what the canvas ACTUALLY took where the engine says so, rather than
706
+ // echoing the request back.
707
+ return { success: true, width: (r && r.width) || ${width}, height: (r && r.height) || ${height},
708
+ ...(r && r.preset ? { preset: r.preset } : {}),
709
+ ...(r && r.unbounded !== undefined ? { unbounded: r.unbounded } : {}) };
710
+ })();
681
711
  `.trim();
682
712
  }
683
713
  /**
@@ -1220,19 +1250,33 @@ export class PinePaperCodeGenerator {
1220
1250
  if (url) {
1221
1251
  return `
1222
1252
  // Import SVG from URL${source === 'figma' ? ' (Figma-normalised)' : ''}
1223
- const response = await fetch('${url}');
1224
- const svgText = await response.text();
1225
- const _r = ${importExpr('svgText')};
1226
- const imported = _r.item;
1227
- if (imported) {
1253
+ // WRAPPED, because these were bare top-level statements with an await. That
1254
+ // parses under the governor, which builds an AsyncFunction, and is a SYNTAX
1255
+ // ERROR on the eval fallback — so importing an SVG by URL never ran at all on
1256
+ // a studio without runGenerated. A paren-led async IIFE parses on both and
1257
+ // still gives the governor a trailing expression to capture.
1258
+ (async function() {
1259
+ let response;
1260
+ try {
1261
+ response = await fetch('${url}');
1262
+ } catch (netErr) {
1263
+ return { success: false, error: 'could not reach ${url} — ' + ((netErr && netErr.message) || 'network request failed') };
1264
+ }
1265
+ if (!response.ok) {
1266
+ return { success: false, error: 'the server refused ${url} — HTTP ' + response.status + ' ' + (response.statusText || ''), status: response.status };
1267
+ }
1268
+ const svgText = await response.text();
1269
+ const _r = ${importExpr('svgText')};
1270
+ const imported = _r.item;
1271
+ if (!imported) {
1272
+ return { success: false, error: 'the response from ${url} did not parse as SVG. Check the URL serves the file itself rather than a page around it.' };
1273
+ }
1228
1274
  imported.position = new paper.Point(${position.x}, ${position.y});
1229
1275
  imported.scale(${scale});
1230
1276
  const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
1231
1277
  app.historyManager.saveState();
1232
- ({ success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} } });
1233
- } else {
1234
- throw new Error('Failed to import SVG from URL');
1235
- }
1278
+ return { success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} } };
1279
+ })();
1236
1280
  `.trim();
1237
1281
  }
1238
1282
  if (svgString) {
@@ -2465,8 +2509,12 @@ return { itemId };
2465
2509
  const modifyProps = JSON.stringify(op.properties || {});
2466
2510
  return `
2467
2511
  const targetId = ${itemRef};
2468
- app.select(targetId);
2469
- app.modify(${modifyProps});
2512
+ // modifyItem(id, changes), not select() + modify(). modify() edits the
2513
+ // SELECTION, so an id that does not resolve left the PREVIOUS selection to be
2514
+ // edited instead — and this then reported modified:true either way.
2515
+ if (app.modifyItem(targetId, ${modifyProps}) === false) {
2516
+ return { success: false, itemId: targetId, error: 'no item ' + targetId + ' — nothing was modified' };
2517
+ }
2470
2518
  return { itemId: targetId, modified: true };
2471
2519
  `;
2472
2520
  case 'animate':
@@ -2541,9 +2589,16 @@ return { success: true, generator: '${genName}'${op.generatorRegion ? ', region:
2541
2589
  const h = op.height || 1080;
2542
2590
  const sizeArg = op.preset ? `'${op.preset}'` : `{ width: ${w}, height: ${h} }`;
2543
2591
  return `
2544
- app.setCanvasSize(${sizeArg});
2592
+ // Read the verdict, as the standalone tool now does: an unknown preset or an
2593
+ // out-of-range dimension is refused by name, and reporting the size we ASKED
2594
+ // for left the batch claiming a resize that never happened.
2595
+ const _cs = app.setCanvasSize(${sizeArg});
2596
+ if (_cs && _cs.ok === false) {
2597
+ return { success: false, error: _cs.reason || 'the canvas size was refused',
2598
+ ...(_cs.known ? { knownPresets: _cs.known } : {}) };
2599
+ }
2545
2600
  await new Promise(r => setTimeout(r, 50));
2546
- return { success: true, width: ${w}, height: ${h} };
2601
+ return { success: true, width: (_cs && _cs.width) || ${w}, height: (_cs && _cs.height) || ${h} };
2547
2602
  `;
2548
2603
  }
2549
2604
  case 'keyframe_animate': {
@@ -4071,7 +4126,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
4071
4126
  // records the baked offset at item.data._bakedRotation and sums the two itself
4072
4127
  // (PinePaper.js does exactly this); read the same sum rather than the half of
4073
4128
  // it that happens to be zero.
4074
- const _rot = (item.data?._bakedRotation || 0) + (item.rotation || 0);
4129
+ const _rot = typeof app.authoredRotation === 'function' ? app.authoredRotation(item) : ((item.data?._bakedRotation || 0) + (item.rotation || 0));
4075
4130
  if (_rot) itemData.properties.rotation = _rot;
4076
4131
  if (item.data?.content) itemData.properties.content = item.data.content;
4077
4132
  if (item.visible === false) itemData.properties.visible = false;
@@ -4433,6 +4488,33 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
4433
4488
  }
4434
4489
  })();
4435
4490
  `.trim();
4491
+ }
4492
+ /**
4493
+ * What THIS item can animate, from the engine's own table.
4494
+ *
4495
+ * The table is derived from the applier rather than written beside it, so it
4496
+ * cannot drift from what actually gets applied — and it is filtered by item
4497
+ * type, because the trim/dash four only exist on a path and a rectangle has
4498
+ * no fontSize.
4499
+ *
4500
+ * `interpolates: false` is passed through VERBATIM and never flattened away.
4501
+ * `content` and `blendMode` are DISCRETE — they hold their value until the
4502
+ * next keyframe rather than blending toward it — and treating content as
4503
+ * tweenable is what made every word in a cycler appear half a beat early.
4504
+ * A list that says only "animatable" rebuilds that bug in the next agent.
4505
+ */
4506
+ generateListItemAnimatableProperties(itemId) {
4507
+ return `
4508
+ // Animatable properties of ${JSON.stringify(itemId)}
4509
+ (function() {
4510
+ if (typeof app.listAnimatableProperties !== 'function') {
4511
+ return { success: false, error: 'app.listAnimatableProperties unavailable — update the studio. The keyframe tool lists the properties it accepts in its own description meanwhile.' };
4512
+ }
4513
+ const item = app.getItemById(${JSON.stringify(itemId)});
4514
+ if (!item) { return { success: false, error: 'no item ${itemId}' }; }
4515
+ const r = app.listAnimatableProperties(item);
4516
+ return { success: true, ...r };
4517
+ })();`.trim();
4436
4518
  }
4437
4519
  generateGetAnimatableProperties() {
4438
4520
  return `
@@ -4829,7 +4911,21 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The RE
4829
4911
  // Toggle loop mode
4830
4912
  (function() {
4831
4913
  ${guard}
4832
- app.sceneManager.setLoop(${input.enabled ?? true});
4914
+ // chainLoop is a PROPERTY, set by createChain({loop}) and read by the chain
4915
+ // advance. setLoop has never existed, so this reported a toggle it had not
4916
+ // performed — and a chain built without loop went on not looping.
4917
+ if (typeof app.sceneManager.setLoop === 'function') {
4918
+ app.sceneManager.setLoop(${input.enabled ?? true});
4919
+ } else if ('chainLoop' in app.sceneManager) {
4920
+ app.sceneManager.chainLoop = ${input.enabled ?? true};
4921
+ } else {
4922
+ return { success: false, action: 'toggle_loop',
4923
+ error: 'this build has no chain-loop control to set. Pass loop:true to create_chain instead, which is where the setting is read from.' };
4924
+ }
4925
+ if (!app.sceneManager.sceneChain || !app.sceneManager.sceneChain.length) {
4926
+ return { success: true, action: 'toggle_loop', enabled: ${input.enabled ?? true},
4927
+ note: 'no chain exists yet — this setting applies to the next one created, and create_chain takes loop directly.' };
4928
+ }
4833
4929
  return { success: true, action: 'toggle_loop', enabled: ${input.enabled ?? true} };
4834
4930
  })();
4835
4931
  `.trim();
@@ -5707,7 +5803,7 @@ ${resolve}
5707
5803
  bounds: item.bounds ? { x: item.bounds.x, y: item.bounds.y, width: item.bounds.width, height: item.bounds.height } : null,
5708
5804
  visible: item.visible,
5709
5805
  opacity: item.opacity,
5710
- rotation: (item.data?._bakedRotation || 0) + (item.rotation || 0),
5806
+ rotation: typeof app.authoredRotation === 'function' ? app.authoredRotation(item) : ((item.data?._bakedRotation || 0) + (item.rotation || 0)),
5711
5807
  selected: item.selected,
5712
5808
  };
5713
5809
  })();`.trim();
@@ -6481,7 +6577,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
6481
6577
  const entry = app.itemRegistry.get(${itemIdStr});
6482
6578
  if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
6483
6579
  const b = entry.item.bounds;
6484
- return { success: true, action: 'get_dimensions', itemId: ${itemIdStr}, x: b.x, y: b.y, width: b.width, height: b.height, rotation: (entry.item.data?._bakedRotation || 0) + (entry.item.rotation || 0) };
6580
+ return { success: true, action: 'get_dimensions', itemId: ${itemIdStr}, x: b.x, y: b.y, width: b.width, height: b.height, rotation: typeof app.authoredRotation === 'function' ? app.authoredRotation(entry.item) : ((entry.item.data?._bakedRotation || 0) + (entry.item.rotation || 0)) };
6485
6581
  })();`.trim();
6486
6582
  }
6487
6583
  case 'set_snap': {
@@ -6990,10 +7086,25 @@ ${guard}
6990
7086
  });
6991
7087
  return `
6992
7088
  // Display text style: ${input.styleKey} — stacked-layer title, id adopted from the text
6993
- (function() {
7089
+ (async function() {
6994
7090
  if (typeof app.applyTextStyle !== 'function') { return { success: false, error: 'app.applyTextStyle unavailable — update FxTool to a text-styles build' }; }
7091
+ // LOAD THE FACES FIRST. The decorative stylesheet is fetched on pointerenter
7092
+ // over the style grid — a gesture no agent ever makes — so every decorative
7093
+ // family was missing in production and a style asking for one silently drew
7094
+ // the fallback and reported success. ensureFontsLoaded is the fix; guarded,
7095
+ // because a studio without it behaves as it always did.
7096
+ if (typeof app.ensureFontsLoaded === 'function') {
7097
+ try { await app.ensureFontsLoaded('decorative'); } catch (_) { /* the pending flag below still reports it */ }
7098
+ }
6995
7099
  const r = app.applyTextStyle(${S(input.itemId)}, ${S(input.styleKey)}, ${opts});
6996
7100
  if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'style failed' }; }
7101
+ // fontPending: the face is not there YET, so this drew in the fallback. Said
7102
+ // out loud rather than left to be discovered in the export.
7103
+ if (r.fontPending) {
7104
+ return { success: true, action: 'apply_style', id: r.id, style: r.style, layers: r.layers,
7105
+ palette: r.palette, bounds: r.bounds, fontPending: true,
7106
+ warning: 'the face for this style had not loaded, so it drew in a FALLBACK font. Re-apply the style, or call pinepaper_font check with the family and your text to see what is really being used.' };
7107
+ }
6997
7108
  // The styled group ADOPTS the text item's registry id — r.id is the SAME id
6998
7109
  // the caller passed, so their handle keeps working. Say so in the result.
6999
7110
  return { success: true, action: 'apply_style', id: r.id, style: r.style, layers: r.layers, palette: r.palette, bounds: r.bounds };