@pinepaper.studio/mcp-server 1.6.29 → 1.6.30

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.
Files changed (59) hide show
  1. package/README.md +52 -0
  2. package/dist/browser/puppeteer-controller.d.ts +40 -0
  3. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  4. package/dist/browser/puppeteer-controller.js +138 -1
  5. package/dist/browser/puppeteer-controller.js.map +1 -1
  6. package/dist/build-info.json +2 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +15 -9
  9. package/dist/index.js.map +1 -1
  10. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  11. package/dist/ontology/upstream-vocabulary.js +2 -2
  12. package/dist/ontology/vocabulary.d.ts.map +1 -1
  13. package/dist/ontology/vocabulary.js +2 -0
  14. package/dist/ontology/vocabulary.js.map +1 -1
  15. package/dist/tools/definitions.d.ts.map +1 -1
  16. package/dist/tools/definitions.js +25 -17
  17. package/dist/tools/definitions.js.map +1 -1
  18. package/dist/tools/effect-check.d.ts +1 -1
  19. package/dist/tools/effect-check.d.ts.map +1 -1
  20. package/dist/tools/effect-check.js.map +1 -1
  21. package/dist/tools/engine-lists.d.ts +3 -3
  22. package/dist/tools/engine-lists.js +3 -3
  23. package/dist/tools/engine-methods.d.ts.map +1 -1
  24. package/dist/tools/engine-methods.js +3 -0
  25. package/dist/tools/engine-methods.js.map +1 -1
  26. package/dist/tools/engine-surface.d.ts +1 -1
  27. package/dist/tools/engine-surface.d.ts.map +1 -1
  28. package/dist/tools/engine-surface.js +19 -4
  29. package/dist/tools/engine-surface.js.map +1 -1
  30. package/dist/tools/handlers/generate.d.ts +2 -2
  31. package/dist/tools/handlers/generate.d.ts.map +1 -1
  32. package/dist/tools/handlers/generate.js +4 -4
  33. package/dist/tools/handlers/generate.js.map +1 -1
  34. package/dist/tools/handlers.d.ts.map +1 -1
  35. package/dist/tools/handlers.js +41 -2
  36. package/dist/tools/handlers.js.map +1 -1
  37. package/dist/tools/lane.d.ts +8 -2
  38. package/dist/tools/lane.d.ts.map +1 -1
  39. package/dist/tools/lane.js +22 -3
  40. package/dist/tools/lane.js.map +1 -1
  41. package/dist/tools/p5-compat/p5-helpers.d.ts +6 -17
  42. package/dist/tools/p5-compat/p5-helpers.d.ts.map +1 -1
  43. package/dist/tools/p5-compat/p5-helpers.js +8 -1
  44. package/dist/tools/p5-compat/p5-helpers.js.map +1 -1
  45. package/dist/tools/shape-params.d.ts +2 -2
  46. package/dist/tools/shape-params.d.ts.map +1 -1
  47. package/dist/tools/shape-params.js +5 -2
  48. package/dist/tools/shape-params.js.map +1 -1
  49. package/dist/tools/stick-vocabulary.d.ts +1 -1
  50. package/dist/tools/stick-vocabulary.js +1 -1
  51. package/dist/types/code-generator.d.ts.map +1 -1
  52. package/dist/types/code-generator.js +70 -34
  53. package/dist/types/code-generator.js.map +1 -1
  54. package/dist/types/schemas.d.ts +11 -0
  55. package/dist/types/schemas.d.ts.map +1 -1
  56. package/dist/types/schemas.js +4 -0
  57. package/dist/types/schemas.js.map +1 -1
  58. package/manifest.json +1 -1
  59. package/package.json +1 -1
@@ -402,8 +402,13 @@ const item = app.create(${jsLiteral(itemType)}, ${JSON.stringify(params, null, 2
402
402
  if (hasCreateScale) {
403
403
  code += `
404
404
  const __scale = (function(it) {
405
+ // A studio whose create() applies scale (FxTool #262) already has it: nothing to redo.
406
+ const __want = ${JSON.stringify(createScale)};
407
+ const __s0 = (it && it.scaling) || { x: 1, y: 1 };
408
+ const __near = function(a, b) { return b === undefined || Math.abs(a - b) < 1e-6; };
409
+ if (__near(__s0.x, __want.scaleX) && __near(__s0.y, __want.scaleY)) return { applied: true, x: __s0.x, y: __s0.y };
405
410
  if (!it || !it.data || typeof app.modifyItem !== 'function') return { applied: false, note: 'this studio has no app.modifyItem, so the scale was not applied at create; set it with pinepaper_modify_item.' };
406
- app.modifyItem(it.data.registryId, ${JSON.stringify(createScale)});
411
+ app.modifyItem(it.data.registryId, __want);
407
412
  const s = it.scaling || { x: 1, y: 1 };
408
413
  return { applied: true, x: s.x, y: s.y };
409
414
  })(item);`;
@@ -1916,12 +1921,14 @@ const TEXT_KEY_ENGINE_NAMES = {
1916
1921
  wordSpacing: ['wordSpacing'],
1917
1922
  textDecoration: ['textDecoration'],
1918
1923
  textTransform: ['textTransform'],
1919
- // Not text, same failure (SHEARKEY, mcp-cloud #29): no studio has a skew
1920
- // channel yet, so skew keys were stored and drew nothing. A sheared motion
1921
- // today is a `transform` matrix track.
1924
+ // Not text, same failure (SHEARKEY, mcp-cloud #29): a studio before FxTool
1925
+ // #263 has no skew channel, so skew keys were stored and drew nothing. The
1926
+ // current one animates skewX / skewY. `shear` is no channel on any studio:
1927
+ // keyed, it drew nothing with no word (measured on 56c79202), so it is named
1928
+ // unless a studio ever lists `shear` itself.
1922
1929
  skewX: ['skewX'],
1923
1930
  skewY: ['skewY'],
1924
- shear: ['shear', 'skewX'],
1931
+ shear: ['shear'],
1925
1932
  };
1926
1933
  /** A JS expression (run in the page) → the keys of `keys` this studio does not animate. '' when none are text keys. */
1927
1934
  function unanimatedTextKeysJs(keys) {
@@ -1936,7 +1943,7 @@ function unanimatedTextKeysJs(keys) {
1936
1943
  })()`;
1937
1944
  }
1938
1945
  /** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
1939
- const UNANIMATED_TEXT_KEYS_SENTENCE = `' is not an animatable property in this studio (app.listAnimatableProperties does not list it), so it had no effect: a key animates only where that list names it. Here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX (a skew: a transform matrix track).'`;
1946
+ const UNANIMATED_TEXT_KEYS_SENTENCE = `' is not an animatable property in this studio (app.listAnimatableProperties does not list it), so it had no effect: a key animates only where that list names it. Here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX. For a skew, key skewX / skewY (degrees) where the studio lists them, else a transform matrix track.'`;
1940
1947
  function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
1941
1948
  const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
1942
1949
  const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
@@ -2010,12 +2017,13 @@ String(JSON.stringify(levelKeys)) +
2010
2017
  const loopNote = loop
2011
2018
  ? `, loopNote: ${JSON.stringify('loop:true repeats in the editor, but has been measured playing ONCE in an exported video and then holding the last keyframe. For an export longer than one cycle, check a frame in the second cycle — if it holds, write the keyframes out per cycle, or use a loop preset (pinepaper_animate), which does cycle in export.')}`
2012
2019
  : '';
2013
- // SIZEKEY-PAIR (mcp-cloud #28): x (or position) keyed beside fontSize on one
2014
- // text item has been measured drifting in an export: the size is applied
2015
- // after the position, and a text resizes around its anchor, so the box leaves
2016
- // the keyed x. Said, with the two ways that hold.
2020
+ // SIZEKEY-PAIR (mcp-cloud #28): on a studio before FxTool #262, x (or
2021
+ // position) keyed beside fontSize on one text item drifted in an export: the
2022
+ // size was applied after the position, and a text resizes around its anchor.
2023
+ // Said there, with the two ways that hold; a current studio is quiet.
2017
2024
  const sizekeyPairNote = (allProps.includes('x') || allProps.includes('position')) && allProps.includes('fontSize')
2018
- ? `, sizekeyPairWarning: ${JSON.stringify('x (or position) and fontSize keyed on the same text item have been measured drifting off the keyed position in an export (SIZEKEY-PAIR). Animate scaleX / scaleY instead of fontSize, or move a group that holds the text.')}`
2025
+ // A studio that writes text size before position (FxTool #262, which also brought _applyScaleParams) holds x.
2026
+ ? `, ...(typeof app._applyScaleParams !== 'function' ? { sizekeyPairWarning: ${JSON.stringify('x (or position) and fontSize keyed on the same text item drift off the keyed position in an export on this studio (SIZEKEY-PAIR). Animate scaleX / scaleY instead of fontSize, or move a group that holds the text; or update the studio.')} } : {})`
2019
2027
  : '';
2020
2028
  const easingNote = easingAt !== 'departure' && first?.easing && first.easing !== 'linear'
2021
2029
  ? `, note: ${JSON.stringify(`easing '${first.easing}' is on the first keyframe, where it has no effect by default: easing shapes the segment ARRIVING at a keyframe. Put it on the keyframe you are moving to, or pass easingAt: 'departure' to read every easing as the move LEAVING its key (current studio).`)}`
@@ -5643,11 +5651,12 @@ String(JSON.stringify(scaled)) +
5643
5651
  String(format === 'webm' && transparent === true ? '' : ", 'webm'") +
5644
5652
  String(format === 'apng' && transparent === false ? ", 'apng'" : '') +
5645
5653
  "].indexOf(fmt) === -1) return [];\n const bg = app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor;\n const hasColor = !!bg && !/^(transparent|rgba\\([^)]*,\\s*0\\))$/i.test(bg);\n const hasBgItems = !!(app.patternGroup && app.patternGroup.children && app.patternGroup.children.length);\n // A 3D world under the canvas IS the background: since FxTool #50\n // (747315e3) jpg/webp stills composite it, so nothing turns white/black.\n const hasWorld = !!app._world3d;\n // So does a styled scene: one raster sized to COVER the frame (gate run 3:\n // every export after styled_scene warned of black areas it does not have).\n const h" +
5646
- "asStyledScene = !!(app.textItemGroup && app.textItemGroup.children\n && app.textItemGroup.children.some(function (it) { return it && it.data && it.data.styledScene && it.visible !== false; }));\n if (hasColor || hasBgItems || hasWorld || hasStyledScene) return [];\n return [{ code: 'alpha_dropped', message: fmt === 'apng'\n ? 'apng with transparent: false and no background colour: the frames are filled BLACK. Set a background colour, or drop transparent: false to keep the alpha.'\n : fmt === 'jpg'\n ? 'jpg has no transparency and this scene has no background, so transparent areas are filled white. Use png or webp to keep them.'\n : fmt + ' is ex" +
5647
- "ported without transparency and this scene has no background, so transparent areas come out BLACK. Set a background colour, or export apng (animated, full alpha) or png to keep it.' }];\n } catch (e) { return []; }\n }\n // TEXT THAT DOES NOT FIT (FxTool 661224ef: textOverflowReport) — text past\n // its fit box at the minimum size, or off the frame. The pre-batch check for\n // certificates, speaker cards and localised variants, on every export.\n function textOverflow() {\n try {\n const rep = (typeof app.textOverflowReport === 'function' && app.textOverflowReport()) || [];\n return rep.slice(0, 10).map(function(o) {\n return { code: 'text_overflow', message: " +
5648
- "(o.id || 'a text item') + ' (' + JSON.stringify(String(o.content || '').slice(0, 40)) + '): ' + (o.reason || 'overflows'), items: [o.id] };\n });\n } catch (e) { return []; }\n }\n function fidelity(fmt) {\n const own = alphaLoss(fmt).concat(textOverflow());\n try {\n if (!app.exportEngine || typeof app.exportEngine.exportFidelity !== 'function') {\n return { fidelity: { available: false, reason: 'this studio cannot check export fidelity — update PinePaper Studio.', ...(own.length ? { warnings: own } : {}) } };\n }\n const r = app.exportEngine.exportFidelity(fmt);\n if (!r) { return { fidelity: { available: false, reason: 'the fidelity check retur" +
5649
- "ned nothing.', ...(own.length ? { warnings: own } : {}) } }; }\n const warnings = (r.warnings || []).concat(own);\n return {\n fidelity: {\n available: true,\n warnings: warnings,\n checked: r.checked,\n ...(warnings.length === 0\n ? { note: 'this scene loses nothing to ' + fmt + '. That is a statement about THIS SCENE, not a claim that the format is lossless.' }\n : {}),\n },\n };\n } catch (e) {\n return { fidelity: { available: false, reason: (e && e.message) || 'the fidelity check threw.', ...(own.length ? { warnings: own } : {}) } };\n }\n }\n\n // Preflight: same resolved settings as t" +
5650
- "he real export, but render nothing.\n // Branching HERE rather than in a separate tool is deliberate — the estimate\n // has to see the same platform dimensions, fps and quality the export would\n // use, and a parallel tool would re-derive them and drift.\n if (" +
5654
+ "asStyledScene = !!(app.textItemGroup && app.textItemGroup.children\n && app.textItemGroup.children.some(function (it) { return it && it.data && it.data.styledScene && it.visible !== false; }));\n if (hasColor || hasBgItems || hasWorld || hasStyledScene) return [];\n // The fill is the studio's page colour, not black: every opaque export\n // path asks the engine's _pageColor, which is white when no page colour\n // is set (an older engine fell back to black for video). An apng was\n // measured white (bitrate sweep, 11.02), so name the page colour, not a hue.\n return [{ code: 'alpha_dropped', message: fmt === 'apng'\n ? 'apng with transparent: f" +
5655
+ "alse and no background colour: the frames are filled with the studio page colour (white in current studios), not a colour this scene chose. Set a background colour, or drop transparent: false to keep the alpha.'\n : fmt === 'jpg'\n ? 'jpg has no transparency and this scene has no background, so transparent areas are filled white. Use png or webp to keep them.'\n : fmt + ' is exported without transparency and this scene has no background, so transparent areas are filled with the studio page colour (white in current studios), not a colour this scene chose. Set a background colour, or export apng (animated, full alpha) or png to keep it.' }];\n } catch (e) { return [];" +
5656
+ " }\n }\n // TEXT THAT DOES NOT FIT (FxTool 661224ef: textOverflowReport) — text past\n // its fit box at the minimum size, or off the frame. The pre-batch check for\n // certificates, speaker cards and localised variants, on every export.\n function textOverflow() {\n try {\n const rep = (typeof app.textOverflowReport === 'function' && app.textOverflowReport()) || [];\n return rep.slice(0, 10).map(function(o) {\n return { code: 'text_overflow', message: (o.id || 'a text item') + ' (' + JSON.stringify(String(o.content || '').slice(0, 40)) + '): ' + (o.reason || 'overflows'), items: [o.id] };\n });\n } catch (e) { return []; }\n }\n function fidelity(fmt) {" +
5657
+ "\n const own = alphaLoss(fmt).concat(textOverflow());\n try {\n if (!app.exportEngine || typeof app.exportEngine.exportFidelity !== 'function') {\n return { fidelity: { available: false, reason: 'this studio cannot check export fidelity — update PinePaper Studio.', ...(own.length ? { warnings: own } : {}) } };\n }\n const r = app.exportEngine.exportFidelity(fmt);\n if (!r) { return { fidelity: { available: false, reason: 'the fidelity check returned nothing.', ...(own.length ? { warnings: own } : {}) } }; }\n const warnings = (r.warnings || []).concat(own);\n return {\n fidelity: {\n available: true,\n warnings: warnings," +
5658
+ "\n checked: r.checked,\n ...(warnings.length === 0\n ? { note: 'this scene loses nothing to ' + fmt + '. That is a statement about THIS SCENE, not a claim that the format is lossless.' }\n : {}),\n },\n };\n } catch (e) {\n return { fidelity: { available: false, reason: (e && e.message) || 'the fidelity check threw.', ...(own.length ? { warnings: own } : {}) } };\n }\n }\n\n // Preflight: same resolved settings as the real export, but render nothing.\n // Branching HERE rather than in a separate tool is deliberate — the estimate\n // has to see the same platform dimensions, fps and quality the export would\n // use, and a " +
5659
+ "parallel tool would re-derive them and drift.\n if (" +
5651
5660
  String(estimateOnly ? 'true' : 'false') +
5652
5661
  ") {\n if (typeof app.estimateExportSize !== 'function') {\n return { success: false, estimateOnly: true, error: 'This PinePaper build has no estimateExportSize() — update the editor to preflight export size.' };\n }\n const est = await app.estimateExportSize({\n format,\n duration: " +
5653
5662
  String(videoDuration) +
@@ -7160,6 +7169,9 @@ String(jsLiteral(format)) +
7160
7169
 
7161
7170
 
7162
7171
 
7172
+
7173
+
7174
+
7163
7175
 
7164
7176
 
7165
7177
 
@@ -10569,7 +10581,7 @@ String(jsLiteral(input.filterName || '')) +
10569
10581
  String(JSON.stringify(input.itemId)) +
10570
10582
  ", filter: " +
10571
10583
  String(JSON.stringify(input.filterName || '')) +
10572
- String(input.filterName === 'bloom' ? `, warning: 'bloom is drawn inside the image\\'s own pixels, so its halo stops at the image edge and does not spill onto the canvas (BLOOMCLIP). For a halo past the edge, use pinepaper_apply_effect glow or a shadow.'` : '') +
10584
+ String(input.filterName === 'bloom' ? `, ...(typeof app._writeRasterPixels !== 'function' ? { warning: 'on this studio bloom is drawn inside the image\\'s own pixels, so its halo stops at the image edge (BLOOMCLIP). For a halo past the edge, use pinepaper_apply_effect glow or a shadow, or update the studio.' } : {})` : '') +
10573
10585
  " };\n } catch (e) {\n return { success: false, action: 'apply', error: 'Failed to apply filter: ' + e.message };\n }\n})();"
10574
10586
  ).trim();
10575
10587
  case 'chain':
@@ -12688,12 +12700,32 @@ String(diagJson) +
12688
12700
  String(optsJson) +
12689
12701
  ");\n // THE RELATIONS AS BOUND, so a later agent can find the one to edit without exporting the scene to read it:\n // from, to, type and params, capped. And the custom relation definitions registered on this page, as given.\n const __rels = [];\n let __relCount = 0;\n try {\n const __rr = app.relationRegistry;\n const __active = __rr && typeof __rr.queryActive === 'function' ? __rr.queryActive() : [];\n __active.forEach((e) => (e.associations || []).forEach((a) => {\n __relCount++;\n if (__rels.length >= " +
12690
12702
  String(RELATIONS_READBACK_CAP) +
12691
- ") return;\n const __name = e.item && e.item.data && e.item.data.name;\n __rels.push({ from: e.itemId, ...(__name ? { fromName: __name } : {}), to: a.target === undefined ? null : a.target, type: a.relation, params: a.params || {} });\n }));\n } catch (e) { /* the relations are an addition; the lint stands without them */ }\n const __custom = (typeof window !== 'undefined' && window.__ppCustomRelations) ? Object.values(window.__ppCustomRelations) : [];\n // HOW TO CHANGE IT (#43): an editing agent needs the op, not the schemas. Each relation comes with the batch op that\n // changes it; a group bound by a name pattern (\"dot-*\", recorded in each member's params) comes once" +
12692
- ", with the ops\n // that add and remove a member; a custom motion with the call that redefines it.\n const __edit = { hint: 'To change this scene, send the ops below (your values in relationOptions) in ONE pinepaper_agent_batch_execute call; a custom motion changes by sending its pinepaper_register_custom_relation call again.', groups: [], relations: [], customRelations: [] };\n try {\n const __byPat = new Map();\n __rels.forEach((r) => {\n const pat = r.params && r.params.pattern;\n if (typeof pat === 'string') {\n const k = pat + '|' + r.type + '|' + r.to;\n if (!__byPat.has(k)) __byPat.set(k, { pattern: pat, type: r.type, to: r.to, members: [] });\n " +
12693
- " __byPat.get(k).members.push(r);\n return;\n }\n __edit.relations.push({ from: r.from, ...(r.fromName ? { fromName: r.fromName } : {}), to: r.to, relationType: r.type,\n change: { type: 'relation', sourceId: r.fromName || r.from, ...(r.to != null ? { targetId: r.to } : {}), relationType: r.type, update: true, relationOptions: r.params } });\n });\n __byPat.forEach((g) => {\n const base = Object.assign({}, g.members[0].params); delete base.index; delete base.count; delete base.pattern;\n const names = g.members.map((m) => m.fromName).filter(Boolean).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));\n const prefix = g.pattern.spl" +
12694
- "it('*')[0];\n const nums = names.map((n) => Number(n.slice(prefix.length))).filter((n) => Number.isFinite(n));\n const next = prefix + (nums.length ? Math.max.apply(null, nums) + 1 : g.members.length + 1);\n const firstEntry = app.itemRegistry.get(g.members[0].from);\n const bind = { type: 'relation', sourceId: g.pattern, ...(g.to != null ? { targetId: g.to } : {}), relationType: g.type, relationOptions: base };\n __edit.groups.push({ pattern: g.pattern, relationType: g.type, members: g.members.length, ...(names.length ? { first: names[0], last: names[names.length - 1] } : {}), relationOptions: base,\n change: Object.assign({}, bind, { update: true }),\n " +
12695
- " addMember: [{ type: 'create', itemType: (firstEntry && firstEntry.type) || 'circle', position: [0, 0], properties: { name: next } }, bind],\n ...(names.length ? { removeMember: [{ type: 'delete', itemId: names[names.length - 1] }, bind] } : {}) });\n });\n __custom.forEach((d) => __edit.customRelations.push({ name: d.name, change: { tool: 'pinepaper_register_custom_relation', args: d } }));\n } catch (e) { /* the read-back stands without the edit ops */ }\n // warnings: what the engine's own rules found (TEXT_CLIPS_FRAME: a centre-anchored text running past the\n // frame edge). Passed through when the studio reports them; an older studio has none to pass.\n return { s" +
12696
- "uccess: true, density: result.density, suggestions: result.suggestions,\n ...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}),\n relations: __rels, ...(__relCount > __rels.length ? { relationsShown: __rels.length, relationCount: __relCount } : {}),\n ...(__custom.length ? { customRelations: __custom } : {}),\n ...(__edit.groups.length || __edit.relations.length || __edit.customRelations.length ? { edit: __edit } : {}) };\n})();"
12703
+ ") return;\n const __name = e.item && e.item.data && e.item.data.name;\n __rels.push({ from: e.itemId, ...(__name ? { fromName: __name } : {}), to: a.target === undefined ? null : a.target, type: a.relation, params: a.params || {} });\n }));\n } catch (e) { /* the relations are an addition; the lint stands without them */ }\n // CUSTOM RELATIONS THIS SCENE USES (#43): one registered before a clear_canvas stayed in the read-back of the scene\n // after it. Those in use are listed; the rest are named in registeredUnused. Their source (computeFunction /\n // applyFunction) only with withSource: true, since an edit of params never needs it.\n const __usedTypes = new Set();\n " +
12704
+ "try { const __rr2 = app.relationRegistry; (__rr2 && typeof __rr2.queryActive === 'function' ? __rr2.queryActive() : []).forEach((e) => (e.associations || []).forEach((a) => __usedTypes.add(a.relation))); } catch (e) { /* none known */ }\n const __allCustom = (typeof window !== 'undefined' && window.__ppCustomRelations) ? Object.values(window.__ppCustomRelations) : [];\n const __strip = (d) => { if (" +
12705
+ String(input.withSource === true ? 'true' : 'false') +
12706
+ ") return d; const o = Object.assign({}, d); delete o.computeFunction; delete o.applyFunction; return o; };\n const __custom = __allCustom.filter((d) => __usedTypes.has(d.name)).map(__strip);\n const __unusedCustom = __allCustom.filter((d) => !__usedTypes.has(d.name)).map((d) => d.name);\n // HOW TO CHANGE IT (#43): an editing agent needs the op, not the schemas. Each relation comes with the batch op that\n // changes it; a group bound by a name pattern (\"dot-*\", recorded in each member's params) comes once, with the ops\n // that add and remove a member; a custom motion with the call that redefines it.\n const __edit = { hint: 'To change this scene, send the ops below (your values " +
12707
+ "in relationOptions) in ONE pinepaper_agent_batch_execute call; a custom motion changes by sending its pinepaper_register_custom_relation call again.', groups: [], relations: [], customRelations: [] };\n try {\n const __byPat = new Map();\n __rels.forEach((r) => {\n const pat = r.params && r.params.pattern;\n if (typeof pat === 'string') {\n const k = pat + '|' + r.type + '|' + r.to;\n if (!__byPat.has(k)) __byPat.set(k, { pattern: pat, type: r.type, to: r.to, members: [] });\n __byPat.get(k).members.push(r);\n return;\n }\n __edit.relations.push({ from: r.from, ...(r.fromName ? { fromName: r.fromName } : {}), to: r.to, relationType: r." +
12708
+ "type,\n change: { type: 'relation', sourceId: r.fromName || r.from, ...(r.to != null ? { targetId: r.to } : {}), relationType: r.type, update: true, relationOptions: r.params } });\n });\n __byPat.forEach((g) => {\n const base = Object.assign({}, g.members[0].params); delete base.index; delete base.count; delete base.pattern;\n const names = g.members.map((m) => m.fromName).filter(Boolean).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));\n const prefix = g.pattern.split('*')[0];\n const nums = names.map((n) => Number(n.slice(prefix.length))).filter((n) => Number.isFinite(n));\n const next = prefix + (nums.length ? Math.max.apply(null, " +
12709
+ "nums) + 1 : g.members.length + 1);\n const firstEntry = app.itemRegistry.get(g.members[0].from);\n const bind = { type: 'relation', sourceId: g.pattern, ...(g.to != null ? { targetId: g.to } : {}), relationType: g.type, relationOptions: base };\n __edit.groups.push({ pattern: g.pattern, relationType: g.type, members: g.members.length, ...(names.length ? { first: names[0], last: names[names.length - 1] } : {}), relationOptions: base,\n change: Object.assign({}, bind, { update: true }),\n addMember: [{ type: 'create', itemType: (firstEntry && firstEntry.type) || 'circle', position: [0, 0], properties: { name: next } }, bind],\n ...(names.length ? { removeM" +
12710
+ "ember: [{ type: 'delete', itemId: names[names.length - 1] }, bind] } : {}) });\n });\n __custom.forEach((d) => __edit.customRelations.push({ name: d.name, change: { tool: 'pinepaper_register_custom_relation', args: d } }));\n } catch (e) { /* the read-back stands without the edit ops */ }\n if (!" +
12711
+ String(input.withSource === true ? 'true' : 'false') +
12712
+ " && __custom.length) __edit.customHint = 'custom relation source is left out; pass withSource: true to read the current computeFunction / applyFunction before rewriting one.';\n // A GROUP IS LISTED ONCE (#43): members bound by a name pattern repeated the same relation once per item in\n // `relations`; they are in edit.groups, and counted here.\n const __loose = __rels.filter((r) => !(r.params && typeof r.params.pattern === 'string'));\n const __grouped = __rels.length - __loose.length;\n const __editOut = __edit.groups.length || __edit.relations.length || __edit.customRelations.length ? { edit: __edit } : {};\n if (" +
12713
+ String(input.only === 'edit' ? 'true' : 'false') +
12714
+ ") {\n return Object.assign({ success: true }, __editOut.edit ? __editOut : { edit: { hint: 'nothing to change: this scene binds no relations.' } },\n __unusedCustom.length ? { registeredUnused: __unusedCustom } : {});\n }\n // warnings: what the engine's own rules found (TEXT_CLIPS_FRAME: a centre-anchored text running past the\n // frame edge). Passed through when the studio reports them; an older studio has none to pass.\n return { success: true, density: result.density, suggestions: result.suggestions,\n ...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}),\n relations: __loose, ...(__grouped ? { groupedRelations: __grouped" +
12715
+ ", groupedNote: 'relations bound by a name pattern are listed once per group in edit.groups' } : {}),\n ...(__relCount > __rels.length ? { relationsShown: __rels.length, relationCount: __relCount } : {}),\n ...(__custom.length ? { customRelations: __custom } : {}),\n ...(__unusedCustom.length ? { registeredUnused: __unusedCustom } : {}),\n ...__editOut };\n})();"
12716
+
12717
+
12718
+
12719
+
12720
+
12721
+
12722
+
12723
+
12724
+
12725
+
12726
+
12727
+
12728
+
12697
12729
 
12698
12730
 
12699
12731
 
@@ -13699,7 +13731,7 @@ String(interval === null ? 'null' : interval) +
13699
13731
  ${guard}
13700
13732
  const r = await app.removeLook(${target});
13701
13733
  if (!r || r.ok === false) return { success: false, action: 'remove', error: (r && r.error) || 'nothing to remove' };
13702
- return { success: true, action: 'remove', target: ${target}, removed: r.removed, scene: r.scene, note: 'removing a look restores colours, strokes, shadows, fonts and pixels, not geometry; it has been measured leaving a multi-part character rig about 1 px off its pose (LOOKRESTORE). Check the pose if pixel-exact alignment matters.' };
13734
+ return { success: true, action: 'remove', target: ${target}, removed: r.removed, scene: r.scene, ...(typeof app._applySkewKeyframePose !== 'function' ? { note: 'this studio restores colours, strokes, shadows, fonts and pixels when a look is removed, not geometry; it has been measured leaving a multi-part character rig about 1 px off its pose (LOOKRESTORE). Check the pose if pixel-exact alignment matters, or update the studio.' } : {}) };
13703
13735
  })();`.trim();
13704
13736
  case 'grid':
13705
13737
  return `
@@ -13933,16 +13965,18 @@ ${body}
13933
13965
  return ("\n// Medium: apply " +
13934
13966
  String(oneLine(input.medium)) +
13935
13967
  "\n(async function() {\n if (typeof app.applyMedium !== 'function') { return { success: false, error: 'app.applyMedium unavailable — update FxTool' }; }\n" +
13936
- String(input.medium === 'ink' && input.options && input.options.keepColor && !input.options.color ? ` // INKCOLOR (mcp-cloud #30): keepColor keeps the item's colour, but with no
13937
- // color the ink MARKS take the brush default (#16141c). They take the item's
13938
- // own colour instead.
13939
- const __mopts = ${S(input.options)};
13940
- const __e0 = app.itemRegistry && app.itemRegistry.get(${S(input.itemId)});
13941
- const __it0 = (__e0 && __e0.item) || (app.getItemById ? app.getItemById(${S(input.itemId)}) : null);
13942
- const __css = function(c) { try { return c && typeof c.toCSS === 'function' ? c.toCSS(true) : null; } catch (_) { return null; } };
13943
- const __c0 = __it0 && (__css(__it0.strokeColor) || __css(__it0.fillColor));
13944
- if (__c0) __mopts.color = __c0;
13945
- const r = await app.applyMedium(${S(input.itemId)}, ${S(input.medium)}, __mopts);` : ` const r = await app.applyMedium(${S(input.itemId)}, ${S(input.medium)}, ${S(input.options ?? {})});`) +
13968
+ String(input.medium === 'ink' && input.options && input.options.keepColor && !input.options.color ? (" // INKCOLOR (mcp-cloud #30): keepColor keeps the item's colour, but with no\n // color the ink MARKS take the brush default (#16141c). They take the item's\n // own colour instead.\n const __mopts = " +
13969
+ String(S(input.options)) +
13970
+ ";\n // A studio from FxTool #261 on (which also brought _writeRasterPixels) picks the item's colour itself, gradients\n // and text included, and says so in colorNote: leave it to the studio there.\n const __e0 = typeof app._writeRasterPixels === 'function' ? null : app.itemRegistry && app.itemRegistry.get(" +
13971
+ String(S(input.itemId)) +
13972
+ ");\n const __it0 = typeof app._writeRasterPixels === 'function' ? null : ((__e0 && __e0.item) || (app.getItemById ? app.getItemById(" +
13973
+ String(S(input.itemId)) +
13974
+ ") : null));\n const __css = function(c) { try { return c && typeof c.toCSS === 'function' ? c.toCSS(true) : null; } catch (_) { return null; } };\n const __c0 = __it0 && (__css(__it0.strokeColor) || __css(__it0.fillColor));\n if (__c0) __mopts.color = __c0;\n const r = await app.applyMedium(" +
13975
+ String(S(input.itemId)) +
13976
+ ", " +
13977
+ String(S(input.medium)) +
13978
+ ", __mopts);"
13979
+ ) : ` const r = await app.applyMedium(${S(input.itemId)}, ${S(input.medium)}, ${S(input.options ?? {})});`) +
13946
13980
  "\n if (!r || r.ok === false) {\n return { success: false, error: (r && (r.error || r.reason)) || 'the medium could not be applied', medium: " +
13947
13981
  String(S(input.medium)) +
13948
13982
  ", ...(r && r.fidelity ? { fidelity: r.fidelity } : {}) };\n }\n // ink / charcoal restyle IN PLACE (FxTool D32): the same item, marked with\n // data.brushStyle on its paths and texts. Read the mark back, so an item\n // the engine passed over is a failure rather than a success (gate D32).\n if (" +
@@ -13975,6 +14009,8 @@ String(S(input.medium)) +
13975
14009
 
13976
14010
 
13977
14011
 
14012
+
14013
+
13978
14014
  ).trim();
13979
14015
  case 'sdf_stroke': {
13980
14016
  // FxTool D5: an SDF capsule stroke rendered to a bitmap and created as a