@pinepaper.studio/mcp-server 1.6.18 → 1.6.19

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 (108) hide show
  1. package/README.de.md +1 -1
  2. package/README.es.md +1 -1
  3. package/README.fr.md +1 -1
  4. package/README.hi.md +1 -1
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +97 -10
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/agent/export-manager.js +2 -2
  11. package/dist/agent/session-manager.d.ts +8 -0
  12. package/dist/agent/session-manager.d.ts.map +1 -1
  13. package/dist/agent/session-manager.js +17 -2
  14. package/dist/agent/session-manager.js.map +1 -1
  15. package/dist/assets/asset-manager.d.ts +28 -1
  16. package/dist/assets/asset-manager.d.ts.map +1 -1
  17. package/dist/assets/asset-manager.js +65 -25
  18. package/dist/assets/asset-manager.js.map +1 -1
  19. package/dist/assets/index.d.ts +1 -3
  20. package/dist/assets/index.d.ts.map +1 -1
  21. package/dist/assets/index.js +1 -3
  22. package/dist/assets/index.js.map +1 -1
  23. package/dist/assets/repositories/iconify.js +1 -1
  24. package/dist/assets/repositories/iconify.js.map +1 -1
  25. package/dist/assets/repositories/openclipart.js +1 -1
  26. package/dist/assets/repositories/openclipart.js.map +1 -1
  27. package/dist/assets/repositories/svgrepo.js +2 -2
  28. package/dist/assets/repositories/svgrepo.js.map +1 -1
  29. package/dist/assets/types.d.ts +2 -2
  30. package/dist/assets/types.d.ts.map +1 -1
  31. package/dist/browser/puppeteer-controller.d.ts +25 -0
  32. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  33. package/dist/browser/puppeteer-controller.js +107 -1
  34. package/dist/browser/puppeteer-controller.js.map +1 -1
  35. package/dist/build-info.json +2 -2
  36. package/dist/cli.js +4 -2
  37. package/dist/cli.js.map +1 -1
  38. package/dist/design/design-systems.d.ts +2 -0
  39. package/dist/design/design-systems.d.ts.map +1 -1
  40. package/dist/design/design-systems.js +2 -0
  41. package/dist/design/design-systems.js.map +1 -1
  42. package/dist/execution/canvas-state.d.ts.map +1 -1
  43. package/dist/execution/canvas-state.js +19 -43
  44. package/dist/execution/canvas-state.js.map +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +20 -7
  47. package/dist/index.js.map +1 -1
  48. package/dist/ontology/design-graph.js +2 -2
  49. package/dist/ontology/design-graph.js.map +1 -1
  50. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  51. package/dist/ontology/upstream-vocabulary.js +2 -2
  52. package/dist/ontology/validator.js +1 -1
  53. package/dist/ontology/validator.js.map +1 -1
  54. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  55. package/dist/tools/compact-descriptions.js +15 -14
  56. package/dist/tools/compact-descriptions.js.map +1 -1
  57. package/dist/tools/definitions.d.ts +1 -1
  58. package/dist/tools/definitions.d.ts.map +1 -1
  59. package/dist/tools/definitions.js +422 -126
  60. package/dist/tools/definitions.js.map +1 -1
  61. package/dist/tools/engine-lists.d.ts +17 -2
  62. package/dist/tools/engine-lists.d.ts.map +1 -1
  63. package/dist/tools/engine-lists.js +102 -2
  64. package/dist/tools/engine-lists.js.map +1 -1
  65. package/dist/tools/engine-methods.d.ts.map +1 -1
  66. package/dist/tools/engine-methods.js +14 -1
  67. package/dist/tools/engine-methods.js.map +1 -1
  68. package/dist/tools/engine-surface.d.ts +1 -1
  69. package/dist/tools/engine-surface.d.ts.map +1 -1
  70. package/dist/tools/engine-surface.js +49 -8
  71. package/dist/tools/engine-surface.js.map +1 -1
  72. package/dist/tools/handlers/camera.js +8 -8
  73. package/dist/tools/handlers/camera.js.map +1 -1
  74. package/dist/tools/handlers/export.d.ts.map +1 -1
  75. package/dist/tools/handlers/export.js +77 -5
  76. package/dist/tools/handlers/export.js.map +1 -1
  77. package/dist/tools/handlers/font.js +10 -10
  78. package/dist/tools/handlers/font.js.map +1 -1
  79. package/dist/tools/handlers/generate.d.ts +12 -12
  80. package/dist/tools/handlers/maps.d.ts.map +1 -1
  81. package/dist/tools/handlers/maps.js +23 -3
  82. package/dist/tools/handlers/maps.js.map +1 -1
  83. package/dist/tools/handlers.d.ts +21 -0
  84. package/dist/tools/handlers.d.ts.map +1 -1
  85. package/dist/tools/handlers.js +240 -37
  86. package/dist/tools/handlers.js.map +1 -1
  87. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  88. package/dist/tools/minimal-descriptions.js +32 -25
  89. package/dist/tools/minimal-descriptions.js.map +1 -1
  90. package/dist/tools/shape-params.d.ts +2 -2
  91. package/dist/tools/shape-params.d.ts.map +1 -1
  92. package/dist/tools/shape-params.js +5 -2
  93. package/dist/tools/shape-params.js.map +1 -1
  94. package/dist/tools/stick-vocabulary.d.ts +1 -1
  95. package/dist/tools/stick-vocabulary.js +1 -1
  96. package/dist/tools/toolkits.d.ts.map +1 -1
  97. package/dist/tools/toolkits.js +5 -0
  98. package/dist/tools/toolkits.js.map +1 -1
  99. package/dist/types/code-generator.d.ts +57 -3
  100. package/dist/types/code-generator.d.ts.map +1 -1
  101. package/dist/types/code-generator.js +1007 -253
  102. package/dist/types/code-generator.js.map +1 -1
  103. package/dist/types/schemas.d.ts +1842 -890
  104. package/dist/types/schemas.d.ts.map +1 -1
  105. package/dist/types/schemas.js +229 -51
  106. package/dist/types/schemas.js.map +1 -1
  107. package/manifest.json +22 -2
  108. package/package.json +1 -1
@@ -10,9 +10,9 @@
10
10
  import { COMPACT_DESCRIPTIONS } from './compact-descriptions.js';
11
11
  import { MINIMAL_DESCRIPTIONS } from './minimal-descriptions.js';
12
12
  import { STICK_GAITS, STICK_POSES, STICK_SEQUENCES, STICK_EXPRESSIONS, STICK_PROPS } from './stick-vocabulary.js';
13
- import { GeneratorNameSchema, ItemTypeSchema } from '../types/schemas.js';
14
- import { DESIGN_MEDIA, DESIGN_MEDIA_APPLY } from './engine-lists.js';
15
- import { EffectTypeSchema, SimpleAnimationTypeSchema, EasingSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, KEYFRAME_EASING_JSON_SCHEMA, GEOMETRY_OPERATIONS, THREAD_STITCHES, } from '../types/schemas.js';
13
+ import { AgentBatchOperationTypeSchema, GeneratorNameSchema, ItemTypeSchema } from '../types/schemas.js';
14
+ import { DESIGN_MEDIA, DESIGN_MEDIA_APPLY, FILTER_TYPES, FILTER_DOCS, LINEAGE_KINDS, STYLED_SCENE_STYLES, STYLED_SCENE_STYLE_INFO } from './engine-lists.js';
15
+ import { EffectTypeSchema, AgentExportFormatSchema, RelationTypeSchema, SimpleAnimationTypeSchema, EasingSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, KEYFRAME_EASING_JSON_SCHEMA, GEOMETRY_OPERATIONS, THREAD_STITCHES, } from '../types/schemas.js';
16
16
  /**
17
17
  * Vocabulary lists derived from Zod schemas so tool descriptions stay in
18
18
  * sync automatically. The description-schema-parity test
@@ -111,7 +111,7 @@ Text: text (content, fontSize, fontFamily, color, fontWeight)
111
111
  All items: opacity, shadowColor, shadowBlur, blendMode, strokeColor, strokeWidth
112
112
 
113
113
  SVG imports for recognizable objects (planes, cars, animals, buildings):
114
- pinepaper_search_assets → pinepaper_import_asset (850k+ SVG icons)
114
+ pinepaper_search_assets → pinepaper_import_asset (open SVG icon repositories)
115
115
  An imported SVG is ONE opaque item. To move/recolor its parts (a wheel, a window), pinepaper_group action:"break_apart" splits it into individually movable, re-grouped sub-parts.
116
116
 
117
117
  Photos / raster images (on-device, no upload):
@@ -197,7 +197,7 @@ Measurement: pinepaper_measurement — rulers, grid, snap-to-grid, get item dime
197
197
 
198
198
  ─── CANVAS & EXPORT ───
199
199
 
200
- Presets: instagram (1080x1080), youtube (1920x1080), tiktok (1080x1920), twitter (1200x675)
200
+ Canvas presets (pinepaper_set_canvas_size preset): instagram-post (1080x1080), instagram-story (1080x1920), tiktok (1080x1920), youtube-thumbnail (1280x720), full-hd-1080p (1920x1080), twitter-post (1200x675). Platform names (instagram, youtube) are agent_export's, not canvas presets.
201
201
  Export: pinepaper_agent_export (SVG/PNG/GIF/MP4/WebM/PDF, plus WAV for the soundtrack alone), pinepaper_export_svg
202
202
  Widget: pinepaper_export_widget (pp:PinePaper ontology JSON), pinepaper_export_widget_html (self-contained HTML with tree-shaken runtime)`;
203
203
  /*
@@ -255,13 +255,11 @@ USE WHEN:
255
255
  - Custom canvas size requirements
256
256
  - Design elements extend beyond current canvas bounds
257
257
 
258
- COMMON PRESETS:
259
- - instagram-square: 1080x1080
260
- - instagram-story: 1080x1920
261
- - youtube-thumbnail: 1280x720
262
- - twitter-post: 1200x675
263
- - hd-landscape: 1920x1080
264
- - hd-portrait: 1080x1920
258
+ COMMON PRESETS (studio keys; an unknown key is refused with the full list):
259
+ - instagram-post: 1080x1080 · instagram-portrait · instagram-story: 1080x1920
260
+ - youtube-thumbnail: 1280x720 · youtube-short · tiktok: 1080x1920
261
+ - twitter-post: 1200x675 · facebook-post · linkedin-post · pinterest-pin
262
+ - full-hd-1080p: 1920x1080 · hd-720p: 1280x720 · a4-portrait / a4-landscape and other print sizes
265
263
 
266
264
  Max canvas size: 16384 per side (a 12000 px DOOH or projection canvas fits); above 8192 Safari / iOS may not render it, Chrome and Firefox do. For wedding invitations, event cards, or detailed designs, use at least 1080x1080 or larger.`,
267
265
  inputSchema: {
@@ -271,10 +269,10 @@ Max canvas size: 16384 per side (a 12000 px DOOH or projection canvas fits); abo
271
269
  height: { type: 'number', description: 'Canvas height in pixels (100-16384)' },
272
270
  preset: {
273
271
  type: 'string',
274
- description: 'Optional preset name',
272
+ description: "A studio preset key instead of width/height, e.g. 'instagram-post', 'instagram-story', 'youtube-thumbnail', 'tiktok', 'full-hd-1080p'. An unknown key is refused with the known ones.",
275
273
  },
276
274
  },
277
- required: ['width', 'height'],
275
+ // width + height, or preset alone (enforced by the Zod schema).
278
276
  },
279
277
  },
280
278
  {
@@ -1106,7 +1104,7 @@ NOTE: Import-only — there is no MCP equivalent of FxTool's exportMermaid. Merm
1106
1104
  idempotentHint: false,
1107
1105
  openWorldHint: true,
1108
1106
  },
1109
- description: `Import a raster image from a URL onto the canvas. Returns an itemId for the imported image.
1107
+ description: `Import a raster image onto the canvas from an http(s) URL, a data: URL, or a LOCAL FILE PATH (the server reads the file). Returns an itemId for the imported image.
1110
1108
 
1111
1109
  USE WHEN:
1112
1110
  - User wants to add a photo, PNG, JPG, or other raster image
@@ -1128,6 +1126,9 @@ Apply a mask shape during import to clip the image:
1128
1126
 
1129
1127
  SIZE CONSTRAINTS:
1130
1128
  Use maxWidth/maxHeight to constrain the image dimensions while preserving aspect ratio.
1129
+ - maxEdge: the longest edge the studio STORES the image at (px), or 'native' to keep every pixel. The result says when it was stored smaller.
1130
+ - nativeSize: true places the image at its source footprint even if it was stored smaller.
1131
+ - smoothing: 'off' (nearest-neighbour, for pixel art) | 'low' (default) | 'medium' | 'high'.
1131
1132
 
1132
1133
  EXAMPLES:
1133
1134
  - Import photo: url: "https://example.com/photo.jpg"
@@ -1184,7 +1185,7 @@ EXAMPLES:
1184
1185
  idempotentHint: true,
1185
1186
  openWorldHint: true,
1186
1187
  },
1187
- description: `Search for free SVG assets from open repositories (SVGRepo, OpenClipart, Iconify, FontAwesome).
1188
+ description: `Search for free SVG assets from open repositories (Iconify, Font Awesome). The result's sources says how each repository answered (ok | timeout | error); one that is down does not hold up the others, and a search that names one repository fails if it is down.
1188
1189
 
1189
1190
  USE WHEN:
1190
1191
  - User wants icons, illustrations, or clipart
@@ -1194,15 +1195,13 @@ USE WHEN:
1194
1195
  - Need graphics that are too complex to draw with paths
1195
1196
 
1196
1197
  REPOSITORIES:
1197
- - SVGRepo: 500,000+ icons, various categories and licenses
1198
- - OpenClipart: 150,000+ public domain clipart
1199
1198
  - Iconify: 200,000+ icon sets (various licenses)
1200
1199
  - Font Awesome: 2,000+ free icons (CC BY 4.0)
1201
1200
 
1202
1201
  WORKFLOW:
1203
1202
  1. Search for assets: pinepaper_search_assets(query: "rocket")
1204
1203
  2. Review results (you'll see assetIds, previews, licenses)
1205
- 3. Import chosen asset: pinepaper_import_asset(assetId: "svgrepo_rocket_12345")
1204
+ 3. Import chosen asset: pinepaper_import_asset(assetId: "iconify_mdi_rocket")
1206
1205
 
1207
1206
  IMPORTANT NOTES:
1208
1207
  - Always respect license terms (check license field in results)
@@ -1239,7 +1238,7 @@ SVG CONTENT RETRIEVAL:
1239
1238
  },
1240
1239
  repository: {
1241
1240
  type: 'string',
1242
- enum: ['all', 'svgrepo', 'openclipart', 'iconify', 'fontawesome'],
1241
+ enum: ['all', 'iconify', 'fontawesome'],
1243
1242
  description: 'Which repository to search (default: all)',
1244
1243
  },
1245
1244
  limit: {
@@ -1273,12 +1272,12 @@ USE WHEN:
1273
1272
  WORKFLOW:
1274
1273
  1. Search: pinepaper_search_assets(query: "rocket")
1275
1274
  2. Choose: Review results, pick one with desired license
1276
- 3. Import: pinepaper_import_asset(assetId: "svgrepo_rocket_12345", position: {x: 400, y: 300})
1275
+ 3. Import: pinepaper_import_asset(assetId: "iconify_mdi_rocket", position: {x: 400, y: 300})
1277
1276
  4. Modify: Use pinepaper_modify_item to adjust size, color, etc.
1278
1277
  5. Animate: Use pinepaper_animate or pinepaper_add_relation for motion
1279
1278
 
1280
1279
  PARAMETERS:
1281
- - assetId: From search results (e.g., "svgrepo_12345")
1280
+ - assetId: From search results (e.g., "iconify_mdi_rocket")
1282
1281
  - url: Alternative to assetId - direct URL to SVG
1283
1282
  - position: Where to place on canvas (default: center)
1284
1283
  - scale: Size multiplier (default: 1.0)
@@ -1297,9 +1296,9 @@ CACHING:
1297
1296
  - No disk storage (memory only)
1298
1297
 
1299
1298
  EXAMPLES:
1300
- - Import from search: {assetId: "svgrepo_rocket_001", position: {x: 400, y: 300}, scale: 2.0}
1299
+ - Import from search: {assetId: "iconify_mdi_rocket", position: {x: 400, y: 300}, scale: 2.0}
1301
1300
  - Import from URL: {url: "https://example.com/icon.svg", position: {x: 500, y: 400}}
1302
- - Import with color override: {assetId: "svgrepo_heart_123", position: {x: 400, y: 300}, color: "#ef4444"}
1301
+ - Import with color override: {assetId: "iconify_mdi_heart", position: {x: 400, y: 300}, color: "#ef4444"}
1303
1302
 
1304
1303
  RETURNS:
1305
1304
  - itemId: ID of the imported SVG group (use for further modifications)
@@ -1309,7 +1308,7 @@ RETURNS:
1309
1308
  properties: {
1310
1309
  assetId: {
1311
1310
  type: 'string',
1312
- description: 'Asset ID from search results (e.g., "svgrepo_12345")',
1311
+ description: 'Asset ID from search results (e.g., "iconify_mdi_rocket")',
1313
1312
  },
1314
1313
  url: {
1315
1314
  type: 'string',
@@ -2117,7 +2116,7 @@ MODES:
2117
2116
 
2118
2117
  This complements pinepaper_validate_design (which validates a definition object, not the live scene).
2119
2118
 
2120
- EXAMPLE (pre-flight): { ops: [ { kind: "create", id: "c1", itemType: "circle" }, { kind: "addRelation", source: "c1", target: "missing", relationType: "orbits" } ] }`,
2119
+ EXAMPLE (pre-flight): { ops: [ { kind: "create", id: "c1", itemType: "circle" }, { kind: "addRelation", from: "c1", to: "missing", relation: "orbits" } ] } (source / target / relationType are accepted as the same fields)`,
2121
2120
  inputSchema: {
2122
2121
  type: 'object',
2123
2122
  properties: {
@@ -2216,7 +2215,7 @@ ACTIONS:
2216
2215
  - set_clip: { id, inPoint, outPoint (media-time s, outPoint > inPoint), timeOffset? (canvas s: also MOVE the clip to start there) } → re-trim an ALREADY-uploaded clip (video or audio). A studio that cannot move clips refuses timeOffset rather than ignoring it.
2217
2216
  - split: { id, at (canvas s) } → cut a clip in two: { leftId, rightId, leftMediaId, rightMediaId, at }. The first half keeps its id. Refused (not claimed) where the studio cannot split or the time is outside the clip.
2218
2217
  - add_transition: { fromItemId (ends at the cut), toItemId (starts at it), transition?: { type: 'crossfade'|'dip', seconds, color } } → the same in preview and export. Split a clip, then transition between its halves, or join two clips that meet.
2219
- - For an IMAGE on the timeline, use pinepaper_import_image with bornAt / ttl (seconds on screen).
2218
+ - For an IMAGE on the timeline, import it with pinepaper_import_image, then give it bornAt / ttl (seconds on screen) with pinepaper_modify_item.
2220
2219
 
2221
2220
  NOTES:
2222
2221
  - The server fetches the URL (or reads a local path / data: URL) and hands the page the bytes, so it does not need to be CORS-permitted.
@@ -2293,6 +2292,10 @@ ACTIONS:
2293
2292
  - apply_style: { itemId, styleKey, palette?, variant?, content?, fontFamily?, fontSize? } — render a text item as a layered display title (offset copies, outlines, fills). The result GROUP ADOPTS the text's registry id, so existing relations/keyframes/handles keep pointing at it. fontFamily: 'suggested' opts into the face the style was designed around (arcade wants a pixel face — a layer stack can outline any typeface but cannot pixellate one); never automatic.
2294
2293
  - set_font_axes: { itemId, axes: { weight?, width?, slant? } } — STANDARD variable-font axes only (Canvas 2D has no font-variation-settings, so custom foundry axes are unreachable — platform limit, stated, not silent). Returns {applied, rejected}: CHECK rejected — an axis silently ignored is the failure this surface exists to prevent. All three are ordinary animatable properties: keyframe fontWeight and type breathes between weights.
2295
2294
  - list_styles: {} — styles + palettes + axes, for pickers. Call this first; styleKey strings come from here.
2295
+ - list: {} — the same as list_styles.
2296
+ - cursive: { text, cursiveOptions?: { x?, y?, scale? (1), strokeColor? ('#ffffff'), strokeWidth? (2), nib? ('monoline' | 'broad' | {angle?, width?, contrast?, taper?}), animate? (false), duration? (2), easing? ('easeOut'), timeOffset? (0), singleStroke?, showGhost?, tension? } } → { itemId, strokes, width } — writes the words as STROKED handwriting, a path rather than a glyph. animate: true writes it on by itself over duration seconds (from timeOffset), no keyframes needed; otherwise pinepaper_animate's draw-on applies to it. Unknown cursiveOptions keys are refused.
2297
+ - wrap: { itemId, maxWidth } — breaks a text item into lines no wider than maxWidth (canvas units). unwrap: { itemId } — restores the single line; wrap is not destructive.
2298
+ - to_collage: { itemId, text?, collageOptions? } — rebuilds a text item as a letter collage (same vocabulary as pinepaper_create_letter_collage); text defaults to the item's content.
2296
2299
 
2297
2300
  OVER PHOTOGRAPHS, PASS A PALETTE — NEVER THE DEFAULT. Measured on a real
2298
2301
  render of captions over photographs: highlighter, sticker, caption and badge
@@ -2314,7 +2317,7 @@ EXAMPLE: { action: 'apply_style', itemId: 'title_1', styleKey: 'arcade', fontFam
2314
2317
  action: { type: 'string', enum: ['apply_style', 'set_font_axes', 'list_styles', 'list', 'cursive', 'wrap', 'unwrap', 'to_collage'], description: "Text style operation. 'list' is accepted as an alias for 'list_styles', because pinepaper_text_effect spells the same action 'list' and an agent moving between them guesses wrong. 'cursive' draws text as STROKED handwriting — a path, not a glyph, so draw-on animation and outline_stroke both apply. 'wrap'/'unwrap' break a text item to a width, reversibly. 'to_collage' converts an EXISTING text item into a letter collage in place." },
2315
2318
  text: { type: 'string', description: 'cursive: the words to write. to_collage: the text to build from, if it differs from the item.' },
2316
2319
  maxWidth: { type: 'number', description: 'wrap: the width to break at, in canvas units. unwrap restores the original, so this is not destructive.' },
2317
- cursiveOptions: { type: 'object', description: 'cursive: { x?, y?, scale?, strokeColor?, strokeWidth? }.' },
2320
+ cursiveOptions: { type: 'object', description: "cursive: { x?, y?, scale?, strokeColor?, strokeWidth?, nib?, animate?, duration?, easing?, timeOffset?, singleStroke?, showGhost?, tension? }. animate: true writes the line on over duration seconds. nib: 'monoline' for one even line, 'broad' (or {angle, width, contrast, taper}) for thick/thin; omitted, the studio's default pen." },
2318
2321
  collageOptions: { type: 'object', description: 'to_collage: style, palette and the rest — same vocabulary as pinepaper_create_letter_collage.' },
2319
2322
  itemId: { type: 'string', description: 'Text item id — apply_style / set_font_axes.' },
2320
2323
  styleKey: { type: 'string', description: 'apply_style: style name from list_styles.' },
@@ -2352,7 +2355,7 @@ READ THIS BEFORE PROMISING A MEDIUM. Naming a medium does not create its marks.
2352
2355
  Call resolve before telling a user you will render in a medium. "yes, but as an impression" is the true answer for half of them, and a flat approximation misrepresents a medium rather than approximating it.
2353
2356
 
2354
2357
  ACTIONS:
2355
- - apply: { itemId, medium, options? } — render an item in ANY medium the engine can make: ${Object.keys(DESIGN_MEDIA_APPLY).join(', ')}. One engine entry point; a medium it cannot make (encaustic, absent here) or that is a brush rather than a fill (charcoal, ink) is refused with the reason: pass it on. watercolor options: color, brush, water 0..1, opacity, edge 0..1 (the dark rim), angle, bleed, seed. oil (stylised): color, brush, length, density, toneRange, relief, angle, seed. cutPaper (vector: survives SVG/PDF/video): color, facet, jitter, shadow, shadowColor, shadowOpacity, shadowOffset [dx, dy], seed. The result replaces the source in the stacking order and hides it; applying again replaces; one undo step.
2358
+ - apply: { itemId, medium, options? } — render an item in ANY medium the engine can make: ${Object.keys(DESIGN_MEDIA_APPLY).join(', ')}. One engine entry point; a medium this studio cannot apply is refused with the reason: pass it on. ink / charcoal (where the studio applies them) restyle a path, text, or group of them IN PLACE — same itemId, keyframes and relations kept — with a boiling hand-drawn line; raster and video exports boil, SVG/Lottie get the colour and width only. Their options: color, amplitude (px), wavelength (px), fps (boil rate, 12), seed, boil (default true), width (px, paths), keepColor. watercolor options: color, brush, water 0..1, opacity, edge 0..1 (the dark rim), angle, bleed, seed. oil (stylised): color, brush, length, density, toneRange, relief, angle, seed. cutPaper (vector: survives SVG/PDF/video): color, facet, jitter, shadow, shadowColor, shadowOpacity, shadowOffset [dx, dy], seed. The result replaces the source in the stacking order and hides it; applying again replaces; one undo step.
2356
2359
  - list_media: every medium with fidelity, definition and limitation. Read it rather than assuming a fixed set — the catalogue grows (hatch and watercolor are recent additions).
2357
2360
  - resolve: { medium } → { ok, fidelity, reason, markMaker }. ok:false with a reason is a real answer — pass it on rather than trying anyway.
2358
2361
  - list_stitches: the thread stitches with their params.
@@ -2360,6 +2363,7 @@ ACTIONS:
2360
2363
  - apply_hatch: rule an item with hatching. Same deal — silhouette is the region, source is hidden, re-hatching replaces.
2361
2364
  - list_flow_fields: the 7 fields that bend a hatch line.
2362
2365
  - list_hatch_options: the hatch defaults.
2366
+ - sdf_stroke: { points, width? (8), softness? (0.2), color?, opacity?, resolution? (2), name? } → itemId — a smooth, anti-aliased brush stroke along points ({x, y, pressure?} or [x, y, pressure?], canvas px; pressure 0..1 thins the line there), made as an image item (registered, one undo step). Runs on the CPU; a stroke too large for its bitmap budget is refused with the fix. A studio without the SDF brush refuses with the reason.
2363
2367
 
2364
2368
  THE DIRECTION FIELD IS THE WHOLE THING. Stitches that all lie the same way are hatching; stitches that follow the form are needlepainting. Use { kind: 'radial', cx, cy } for anything that radiates (a flower, an eye), { kind: 'spine', spine: [...] } to run them along a midrib or feather shaft, { kind: 'constant', angle } when you actually want flat hatch.
2365
2369
 
@@ -2385,9 +2389,14 @@ EXAMPLE — a stitched leaf:
2385
2389
  inputSchema: {
2386
2390
  type: 'object',
2387
2391
  properties: {
2388
- action: { type: 'string', enum: ['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options', 'apply'], description: 'Medium operation' },
2392
+ action: { type: 'string', enum: ['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options', 'apply', 'sdf_stroke'], description: 'Medium operation' },
2389
2393
  medium: { type: 'string', enum: [...DESIGN_MEDIA], description: `resolve / apply: the medium. apply works for ${Object.keys(DESIGN_MEDIA_APPLY).join(', ')}; the rest are refused by the engine with the reason. Call list_media for each one's fidelity and limitation.` },
2390
2394
  options: { type: 'object', description: 'apply: the medium\'s own options (see the description; color takes hex).' },
2395
+ points: { type: 'array', items: {}, description: 'sdf_stroke: [{x, y, pressure?}] or [[x, y, pressure?]] in canvas px' },
2396
+ softness: { type: 'number', description: 'sdf_stroke: edge softness 0..1 (default 0.2)' },
2397
+ opacity: { type: 'number', description: 'sdf_stroke: 0..1 (default 1)' },
2398
+ resolution: { type: 'number', description: 'sdf_stroke: bitmap px per canvas px (default 2)' },
2399
+ name: { type: 'string', description: 'sdf_stroke: item name' },
2391
2400
  itemId: { type: 'string', description: 'apply / apply_thread / apply_hatch: a closed path, compound path (holes kept), or a group of them (applied per path).' },
2392
2401
  stitch: { type: 'string', enum: [...THREAD_STITCHES], description: "apply_thread: default longAndShort. These six are the stitches the engine publishes; call 'list_stitches' for each one's description and its own parameters. An unknown name is refused rather than quietly stitched as a default fill." },
2393
2402
  field: { type: 'object', description: "apply_thread: { kind: 'radial'|'spine'|'constant', cx, cy, angle, spine[], across } — default radial from the shape centre. spine is a list of points, each {x, y} or [x, y], e.g. [{x:400,y:200},{x:400,y:520}]; this tool normalises [x, y] to {x, y} before the engine sees it." },
@@ -2429,12 +2438,12 @@ EXAMPLE — a stitched leaf:
2429
2438
  idempotentHint: false,
2430
2439
  openWorldHint: false,
2431
2440
  },
2432
- description: `Animate text CHARACTER BY CHARACTER — 37 effects (terminaltexteffects' vocabulary, reimplemented from source).
2441
+ description: `Animate text CHARACTER BY CHARACTER — the terminaltexteffects vocabulary, reimplemented from source).
2433
2442
 
2434
2443
  ⚠️ THIS REPLACES THE TEXT ITEM. Unlike pinepaper_text_style (which ADOPTS the text's registry id), this EXPLODES the text into one item per character and REMOVES the original. Any relation, keyframe, or handle pointing at the source id DIES with it. Pass keepSource: true to leave the original in place. The returned \`ids\` array is your entire new handle set.
2435
2444
 
2436
2445
  ACTIONS:
2437
- - list: {} — the 37 effects as {key, label, definition}. Call this first; effect keys come from here.
2446
+ - list: {} — every effect as {key, label, definition}. Call this first; effect keys come from here.
2438
2447
  - apply: { itemId, effect, duration?, seed?, gradient?, gradientStops?, gradientDirection?, gradientSteps?, keepSource?, options? } — explode and animate.
2439
2448
 
2440
2449
  WHAT YOU GET: ordinary keyframe-animated items. It scrubs on the timeline, survives undo and session restore, and exports through the existing MP4 / SMIL / Lottie paths — this is not a frame-callback effect that renders on screen and vanishes from your export. Every effect ends AT REST, so \`duration\` is the settle time and the text is readable afterwards.
@@ -2583,9 +2592,10 @@ RECIPE — assemble-from-pieces: shatter with keepSource, keyframe each tile fro
2583
2592
 
2584
2593
  Input is the output of a single-image layer decomposer (See-through — one flat anime drawing in, ~20 occlusion-inpainted layers out — or any PSD-style layer dump with the same manifest shape). The decomposition model does NOT run here: it needs a GPU and minutes per image; run it where it fits and import the folder.
2585
2594
 
2586
- Layer tags map onto the exact role tokens the expresses presets read (eye_left, pupil_right, mouth…), so blink / smile / talk work on the imported character immediately — through an 'expresses' RELATION (pinepaper_add_relation relationType expresses), which is the only thing that drives them. pinepaper_animate applies ambient motion (bounce, pulse, wobble) and cannot blink: asking it to is a silent no-op.
2595
+ Layer tags resolve to the role tokens the expresses presets read: decomposer tags (eyel / eyer, iridesl / iridesr, eyebrowl / eyebrowr, hairf, mouth, face…) and the role tokens themselves (eye_left, pupil_right…) both work, in any case and with _ or - separators, so blink / smile / talk work on the imported character immediately — through an 'expresses' RELATION (pinepaper_add_relation relationType expresses), which is the only thing that drives them. pinepaper_animate applies ambient motion (bounce, pulse, wobble) and cannot blink: asking it to is a silent no-op.
2587
2596
 
2588
- Params: { info — the decomposer manifest, whose parts live under info.parts (or info.tag2pinfo) as an OBJECT KEYED BY TAG, not a layers[] array: { eye_left: { xyxy: [x0,y0,x1,y1], depth_median: <draw order> }, … } plus frame_size [h,w]. An array here imports nothing and reports no parts. images ({tag: PNG data URL or CORS-reachable https URL} — data URLs are the reliable path; ~20 layers of base64 is a big call, use URLs when you can), position?, scale?, name? }
2597
+ Params: { info — the decomposer manifest, whose parts live under info.parts (or info.tag2pinfo) as an OBJECT KEYED BY TAG, not a layers[] array: { eyel: { xyxy: [x0,y0,x1,y1], depth_median: <draw order> }, … } plus frame_size [h,w]. An array here imports nothing and reports no parts. images ({tag: PNG data URL or CORS-reachable https URL} — data URLs are the reliable path; ~20 layers of base64 is a big call, use URLs when you can), position?, scale?, name? }
2598
+ A supplied image that does not land (no manifest entry, no usable xyxy) FAILS the call by name, with skipped: [...] and the groupId of what did land; layers that imported without a role are listed in unroled.
2589
2599
  Returns: { groupId, parts, roles, rolesWired, warnings }
2590
2600
 
2591
2601
  CHECK rolesWired: a character importing with 0 roles renders perfectly and silently refuses to animate — that number is the difference between a picture and a puppet.`,
@@ -2671,7 +2681,7 @@ A partial composition reports as a failure with the elements that did not create
2671
2681
  title: { type: 'string', description: 'compose: the headline most styles build around.' },
2672
2682
  subtitle: { type: 'string', description: 'compose: the supporting line.' },
2673
2683
  body: { type: 'string', description: 'compose: body copy, where the style has room for it.' },
2674
- width: { type: 'number', description: 'compose: canvas width (the style picks its own default otherwise).' },
2684
+ width: { type: 'number', description: 'compose: canvas width. compose SETS the canvas to the size it laid out for. Most styles honour width/height; a few lay out at their own fixed size, and the result warns when the requested size was not used. canvasResized says when the canvas changed.' },
2675
2685
  height: { type: 'number', description: 'compose: canvas height.' },
2676
2686
  variant: { type: 'string', description: "compose: a style-specific variant, e.g. art deco's 'emerald'." },
2677
2687
  draw: { type: 'boolean', description: 'compose: draw it (default true). false returns the scene and ops as data and draws nothing.' },
@@ -2695,7 +2705,7 @@ A different construction from pinepaper_character, which places a figure from th
2695
2705
 
2696
2706
  TO MAKE IT ACT, not just stand: gait is how it walks and is never just walking; poses is the body over time, [{at, pose}]; sequence is a named track of those. Pass gait and travel in the SAME call to cross the frame — they share one track and two calls fight over it. A mistyped gait, pose or sequence comes back NAMED in warnings with the valid list, and the figure is still built.
2697
2707
 
2698
- action 'figure' builds the figure. action 'set' builds the floor, the wall and the objects around it, so a figure has somewhere to be.
2708
+ action 'figure' builds the figure. action 'set' builds the floor, the wall and the objects around it, so a figure has somewhere to be. set takes kind: 'room' (floor and wall: groundY?, wall?, floor?), 'tabletop' (object, surfaceY), 'chair' (facing), 'table', 'counter', 'door', 'shelf' (items), 'window'; all take at and scale.
2699
2709
 
2700
2710
  The geometry is vendored from mcp-cloud, which makes this a three-repo artifact: a change to the kit lands there first and is re-vendored into the engine.`,
2701
2711
  inputSchema: {
@@ -2703,7 +2713,7 @@ The geometry is vendored from mcp-cloud, which makes this a three-repo artifact:
2703
2713
  properties: {
2704
2714
  action: { type: 'string', enum: ['figure', 'set'], description: "'figure' or 'set'." },
2705
2715
  id: { type: 'string', description: 'Prefix for the created items.' },
2706
- kind: { type: 'string', description: 'figure: which figure. set: which piece of the kit.' },
2716
+ kind: { type: 'string', description: 'figure: which figure. set: room | tabletop | chair | table | counter | door | shelf | window (an unknown one is refused with the current list).' },
2707
2717
  at: { type: 'object', description: '{ x?, y? } — where it goes.' },
2708
2718
  scale: { type: 'number', description: 'Size multiplier.' },
2709
2719
  facing: { type: 'string', enum: ['left', 'right'], description: "set: which way a CHAIR faces. A figure has none and passing it here is refused — a figure faces the way it travels." },
@@ -2792,13 +2802,25 @@ The geometry is vendored from mcp-cloud, which makes this a three-repo artifact:
2792
2802
 
2793
2803
  pinepaper_agent_export covers the PLATFORM formats: png, svg, mp4, webm, gif, pdf, sized for Instagram or YouTube. These are the interchange ones — a Lottie an app plays, a GLB a 3D tool opens, a BVH a rig imports, a PNG sequence an editor ingests. Import is here too, because a format you can only write is half a bridge.
2794
2804
 
2795
- Three of these refuse quietly in the engine: exportGLB with no perspective objects and exportBVH with no rigging system both warn to the console and return nothing, and the production build strips the console. Over MCP there is no console to read, so the precondition is checked BEFORE the call and named — "no perspective objects to export — create one with createObject3D first" rather than an empty result and a guess.`,
2805
+ Three of these refuse quietly in the engine: exportGLB with no perspective objects and exportBVH with no rigging system both warn to the console and return nothing, and the production build strips the console. Over MCP there is no console to read, so the precondition is checked BEFORE the call and named — "no perspective objects to export — create one with createObject3D first" rather than an empty result and a guess.
2806
+
2807
+ EXPORTS ARE FILES. Each export is written to a file in the export directory and the result returns { format, filePath, bytes } instead of the data — the bytes are not inlined. An export that produced nothing is refused by name.
2808
+
2809
+ EXPORT:
2810
+ - export_lottie: { options?: { fps?, duration? (s), includeRelations? } } → a .json Lottie file
2811
+ - export_dotlottie: { options?: { fps?, duration? (s), id?, loop?, autoplay? } } → a .lottie file (the zipped Lottie plus manifest)
2812
+ - export_glb: { } → a .glb file of the PERSPECTIVE 3D objects (createObject3D). Refused when there are none. It does not export world3d meshes, which are a separate renderer.
2813
+ - export_bvh: { skeletonId (required), options?: { fps? } } → a .bvh file of that skeleton's motion. Refused when nothing is rigged.
2814
+ - export_png_sequence: { options?: { duration (s), fps, width?, height?, transparent? } } → a .zip of one PNG per frame. PASS duration and fps: the studio does not size the sequence to the scene, and the result's defaultsUsed says when its own default length was used.
2815
+
2816
+ IMPORT:
2817
+ - import_lottie: { data (the Lottie JSON — an object or a JSON string), options?: { fitToCanvas?, autoPlay?, centerOnCanvas?, scale? } } → { itemId, itemIds, items, duration, bounds }; a failed import fails with the importer's errors. data is parsed as JSON, so a URL or a file path does not work; read the file and pass its contents. There is no .lottie, GLB or BVH import here: a glTF goes into a 3D world with the world3d tool's import_gltf, a BVH onto a rig with the rigging tool's import_bvh / retarget_bvh.`,
2796
2818
  inputSchema: {
2797
2819
  type: 'object',
2798
2820
  properties: {
2799
2821
  action: { type: 'string', enum: ['export_lottie', 'export_dotlottie', 'import_lottie', 'export_glb', 'export_bvh', 'export_png_sequence'], description: 'Which interchange operation.' },
2800
2822
  options: { type: 'object', description: "Format options passed through to the exporter. export_png_sequence: { duration (seconds), fps, width, height, transparent (keep alpha; default false fills the background) } — PASS duration and fps: the studio does not size the sequence to the scene, and without them it writes its own default length, which does not follow the scene (runs have measured 45 and 90 frames). The result's defaultsUsed says when that happened." },
2801
- data: { anyOf: [{ type: 'string' }, { type: 'object' }], description: 'import_lottie: the Lottie JSON — object, JSON string, or URL.' },
2823
+ data: { anyOf: [{ type: 'string' }, { type: 'object' }], description: 'import_lottie: the Lottie JSON — object or JSON string (a URL or path is not fetched).' },
2802
2824
  skeletonId: { type: 'string', description: "export_bvh: which skeleton to write. A BVH is one skeleton's motion." },
2803
2825
  },
2804
2826
  required: ['action'],
@@ -2832,6 +2854,36 @@ THE CATALOGUE IS A STARTING SET, NOT THE LIMIT. Six instruments, five drums and
2832
2854
  - For a drum, pass pitch: { from, tau }. The glide is what makes a kick a kick; without it you get a beep.
2833
2855
  - Names are normalised to [a-z][a-z0-9_-]. Register 'Rhodes' and the response returns canonicalName 'rhodes' — play THAT.
2834
2856
 
2857
+ ARGUMENTS BY ACTION. A sound spec is { note? ('A4') | hz? | chord? + root?, instrument? (a catalogue name), waveform? ('sine' | 'square' | 'sawtooth' | 'triangle'), partials? ([{ h, amp }]), envelope? ({ attack, decay, sustain, release }), gain? (0..1), pan? (-1..1), duration? (seconds) }.
2858
+
2859
+ CATALOGUES:
2860
+ - list_instruments / list_percussion / list_sfx: {} → the engine's names, built-in and defined.
2861
+
2862
+ PLAY — each returns { played }. When the studio returns no voice (it may have no audio output) the call fails and says so; placed sounds and render_soundtrack need no audio output:
2863
+ - play_tone: { note ('A4', required), options? } — options is merged into the spec, so { duration, gain, pan, instrument, envelope, waveform } all work, and options.hz overrides the note. A top-level hz is NOT play_tone's: it belongs to define_percussion / define_sfx, and play_tone without note is refused.
2864
+ - play_chord: { root ('C4'), chord? ('major' default; 'minor', 'maj7', 'dim' …), options? }
2865
+ - play_percussion / play_sfx: { name (from the catalogue), options? }
2866
+ - play_spec: { spec, options? } — options is merged into the spec.
2867
+ - play_from_text: { text, options? } — options is merged into the resolved spec.
2868
+
2869
+ READ WITHOUT PLAYING:
2870
+ - chord_frequencies: { root, chord?, options? } → { frequencies } (Hz)
2871
+ - from_text: { text ('a soft warm bell on A4') } → { spec } to inspect or edit, then play_spec or create
2872
+ - timbre_from_path: { itemId (a path with length), samples? (256) } → { timbre: { partials } }
2873
+
2874
+ ON THE CANVAS — a placed sound is a timeline item:
2875
+ - create: { spec?, visual?: { width? (220), height? (64), position? ({ x, y }), color?, startTime? (s), duration? (s) } } → { itemId, placement? } — startTime / duration place it in the same call.
2876
+ - sequence: { cues: [{ t (s, required), spec? | preset? (an instrument name) + note?, duration?, visual? }] } → { placed, itemIds, cues, failed? } — a whole bed in ONE call; failures come back by index. preset is shorthand for spec: { instrument: preset, note }.
2877
+ - set_placement: { itemId, placement: { startTime?, duration? } } (seconds; no other keys are read) → { placed }
2878
+ - remove: { itemId } → { removed } · stop_all: {} → { stopped } — stops everything sounding now; placed sounds stay placed.
2879
+
2880
+ DEFINE (name is normalised to [a-z][a-z0-9_-]; play the returned canonicalName):
2881
+ - define_instrument: { name, partials (required, ≥1), envelope?, gain? (0.8), aliases? } → { canonicalName, requestedName, normalised, listed }
2882
+ - define_percussion / define_sfx: { name, partials? or noise? (0..1; one of the two is required), noiseFreq? (Hz), hz? (base Hz), pitch? ({ from (Hz), tau (s, ≤5) }), envelope?, gain?, aliases? }
2883
+
2884
+ RENDER:
2885
+ - render_soundtrack: { duration? (s, ≤600), sampleRate? (48000), bitDepth? (16 | 32) } → a WAV FILE; the result names filePath, fileSize, placed and any dropped. Refused by name when no sound is placed.
2886
+
2835
2887
  render_soundtrack mixes every placed sound to a WAV and writes it to a file. It runs offline, with no Web Audio and no playback, so it works in a headless studio. An empty scene is refused by name rather than producing a silent file, and any sound that could not be mixed is reported as dropped — a soundtrack missing a track otherwise looks exactly like a complete one.`,
2836
2888
  inputSchema: {
2837
2889
  type: 'object',
@@ -2876,7 +2928,7 @@ render_soundtrack mixes every placed sound to a WAV and writes it to a file. It
2876
2928
  description: `The generators' Animation knob, pointed at anything — the same motion engine every generator's own animation runs on, reachable for any group or list of items.
2877
2929
 
2878
2930
  Two kinds, and the second is the one nothing else here can do:
2879
- - GROUP motions (drift, sway, rotate, pulse, wave, bounce) move the target as one.
2931
+ - GROUP motions (driftX, driftY, swayX, swayY, rotate, pulse, wave, bounce) move the target as one.
2880
2932
  - FIELD motions (ripple, breathe, undulate) sweep a crest THROUGH the children from an origin, with a chosen waveform — each element moves at its own moment in the cycle. That is a wave passing through a crowd, not a crowd moving together.
2881
2933
 
2882
2934
  Call action 'list' first. It returns the engine's own catalogue — every motion, waveform and origin it will accept — so the names come from the engine rather than from an enum here that can drift away from it.
@@ -2957,9 +3009,49 @@ ACTIONS:
2957
3009
  - add_actor: { actorId?, x, z, height?, sprite? (a canvas item id), live? } → { actorId }. live: true re-rasterizes the item as it animates — a RIGGED character (walk cycle, expressions) PERFORMS in the world instead of standing there as a photograph of itself.
2958
3010
  - remove_actor / list_actors / set_actor_pose: { actorId, pose: { x?, z?, angle? } } — the setter a timeline, sequencer or agent drives frame by frame.
2959
3011
  - set_camera: { camera: { mode: 'follow' | 'fixed' | 'orbit', target? (actorId), radius?, speed?, eye?, lookAt? } }
2960
- - add_object: { object: { x, z, height?, color?, metalness?, roughness?, emissiveIntensity? } } (y defaults to sitting ON the terrain) / remove_object: { objectId }
3012
+ - add_object: { object: { x, z, height?, color? } } (y defaults to sitting ON the terrain) / remove_object: { objectId }
2961
3013
  - remove_world: {} — tears the world down; the Paper canvas was always its own layer and is untouched.
2962
3014
 
3015
+ UNITS: positions, sizes, ranges and radii are WORLD units (metres). rotY is RADIANS everywhere on the mesh path (pass mesh.rotYDegrees to write degrees); lathe arc is DEGREES. Colours take hex or [r, g, b] in 0..1. These act on an existing world: call create first.
3016
+
3017
+ MESHES — a canvas path becomes real geometry:
3018
+ - extrude_path: { pathId, mesh?: { id?, depth? (1), unitsPerPixel? (0.01: canvas px → world units), upright? (stand the drawing up; default flat, a floor plan), placement? ('centered' default: centred on x/z, base at y | 'canvas'), caps?, segments?, flatness?, x?, y?, z?, rotY?, rotYDegrees?, scale?, doubleSided?, color?, metalness?, roughness?, uniforms? } } → { meshId }. The path is unchanged.
3019
+ - lathe_path: { pathId, mesh?: same fields, plus arc? (DEGREES, 360 = full solid); segments? = steps round the revolution } → { meshId }
3020
+ - list_meshes: {} → { count, meshes } (long vertex arrays summarised to their length)
3021
+ - remove_mesh: { meshId }
3022
+ - set_mesh_instances: { meshId, instances: [[x, y, z, scale, rotY (RADIANS), variant], …] } — replaces the copies a mesh draws, six numbers each.
3023
+
3024
+ LIGHTS — point lights, at most 8 (the ninth is refused by name):
3025
+ - add_light: { light: { id?, x? (0), y? (3), z? (0), color? (hex or linear [r, g, b], may exceed 1), intensity? (6), range? (20) } } → { lightId, light }
3026
+ - set_light: { lightId, light: any of the add_light fields } — moves or retunes in place.
3027
+ - remove_light: { lightId } · list_lights: {} → { lights }
3028
+
3029
+ MATERIALS — a NAMED, SHARED surface: one edit restyles every object using it.
3030
+ - add_material: { material: { id?, color?, emissive?, emissiveIntensity?, metalness? (0..1), roughness? (0..1), clearcoat?, clearcoatRoughness?, sheenColor?, sheenRoughness? } } → { materialId, material }. metalness / roughness reach pixels on the MESH path only.
3031
+ - set_material: { materialId, material: any of the add_material fields } · remove_material: { materialId } · list_materials: {} → { materials }
3032
+
3033
+ IMPORT:
3034
+ - import_obj: { source (the .obj file TEXT), importOptions?: { id?, x?, y?, z?, rotY? (RADIANS), scale?, doubleSided?, uniforms?, color? ([r, g, b] 0..1; .mtl is never read) } } → { meshIds, warnings }
3035
+ - import_gltf: { source (the .gltf JSON TEXT, buffers embedded), importOptions?: { id?, x?, y?, z?, rotY?, scale?, doubleSided?, uniforms?, merge? (default true) } } → { meshIds, warnings }. A URL, a data: URI or a file path is NOT fetched — the string is parsed as glTF JSON. Unsupported features come back named in warnings.
3036
+ - list_mesh_clips: { meshId } → { clips } (the animation clips a skinned import carries)
3037
+ - set_mesh_clip: { meshId, clip, crossfade? (seconds) }
3038
+
3039
+ NAVIGATION, PICKING, CAMERA:
3040
+ - set_nav_target: { navTarget: '2d' | '3d' } — which layer the pointer drives · get_nav_target: {} → { navTarget }
3041
+ - ground_height: { point: { x, z } (WORLD) } → { height } of the terrain there
3042
+ - raycast: { origin: [x, y, z], direction: [x, y, z] (need not be normalised) } → { hit (registry id, point …) | null, missed }
3043
+ - line_of_sight: { from: [x, y, z], to: [x, y, z] } → { clear }
3044
+ - world_to_canvas: { point: { x, y, z? } (WORLD) } → { point: { x, y } } in canvas px; refused when the point is behind the camera
3045
+ - canvas_to_ground: { point: { x, y } (CANVAS px) } → { ground } where that pixel lands on the terrain
3046
+ - dolly_camera: { multiplier } (>1 away, <1 closer) → { camera }
3047
+ - pan_camera: { dx, dy } (SCREEN px) → { camera }
3048
+
3049
+ PHYSICS — narrow by design: spheres and capsules under gravity colliding with terrain, objects, actors and meshes. No stacking, no joints (pinepaper_physics is the 2D world and has those).
3050
+ - add_body: { body: { id?, kind? ('sphere' default | 'capsule'), position? ([x, y, z]), velocity? ([x, y, z]), radius? (0.5), height? (1.8, capsule), mass? (1), restitution? (0.3), friction? (0.6), gravityScale? (1), kinematic?, ignoreIds?, ttl? (seconds), data? } } → { bodyId, body }
3051
+ - fire_projectile: { body: { id?, position?, direction? ([0, 0, -1]), speed? (40), radius? (0.15), restitution? (0), gravityScale? (0.35), ttl? (6 s), ignoreIds? (e.g. the shooter), data? } } → { id, body } — a SWEPT sphere, so it cannot tunnel through a thin wall.
3052
+ - impulse: { bodyId, vector: [x, y, z] } · set_velocity: { bodyId, vector: [x, y, z] }
3053
+ - step_physics: { dt (seconds) } → { contacts, removed } · remove_body: { bodyId } · list_bodies: {} → { bodies }
3054
+
2963
3055
  RECIPE — a character walking through a forest: create {spec:'forest'} → import/rig a character on the canvas → add_actor {sprite: itemId, live: true} → set_camera {mode:'follow', target} → drive set_actor_pose over time (or let the built-in character control walk).`,
2964
3056
  inputSchema: {
2965
3057
  type: 'object',
@@ -2990,7 +3082,7 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2990
3082
  lightId: { type: 'string', description: 'set_light / remove_light: the light id.' },
2991
3083
  material: { type: 'object', description: 'add_material / set_material: colours (color, emissive, sheenColor) take hex or [r, g, b] in 0..1. { id?, color?, emissive?, metalness?, roughness?, emissiveIntensity?, clearcoat?, clearcoatRoughness?, sheenColor?, sheenRoughness? }. A shared surface — one edit restyles every object using it. metalness/roughness render on the mesh path only.' },
2992
3084
  materialId: { type: 'string', description: 'set_material / remove_material: the material id.' },
2993
- source: { type: 'string', description: 'import_obj: the OBJ file TEXT. import_gltf: a URL or a data: URI of the .glb bytes.' },
3085
+ source: { type: 'string', description: 'import_obj: the OBJ file TEXT. import_gltf: the .gltf JSON TEXT, buffers embedded (a URL, data: URI or path is not fetched; .glb is not supported here).' },
2994
3086
  importOptions: { type: 'object', description: 'import_obj / import_gltf: placement and scale options, passed through.' },
2995
3087
  clip: { type: 'string', description: 'set_mesh_clip: which imported animation clip a skinned mesh plays.' },
2996
3088
  crossfade: { type: 'number', description: 'set_mesh_clip: seconds to blend out of the previous clip (blended in LOCAL pose space).' },
@@ -3031,7 +3123,7 @@ The knowledge of how a figure is built and how it moves lives in the graph; this
3031
3123
  { at: 1.8, channel: "say", until: 4.2 },
3032
3124
  { at: 5.0, channel: "headTurn", value: -0.8 } ] }
3033
3125
 
3034
- CHANNELS ARE DECLARED BY THE CONCEPT, not by you. Ask pinepaper_ontology for a concept to see the ones it has. Four kinds exist:
3126
+ CHANNELS ARE DECLARED BY THE CONCEPT, not by you. Ask pinepaper_query_ontology for a concept to see the ones it has. Four kinds exist:
3035
3127
  - transform: one part deformed with an attack, a hold and a release — a blink is the eyes flattening in scaleY and easing back. This is what gives a gesture a MIDDLE; a swap between two drawings has only two ends and reads as flashing.
3036
3128
  - cycle: a repeating throw that runs for as long as you ask — a pigeon's head bob, a tail flick. Amplitude is a fraction of the part, so it means the same at any size.
3037
3129
  - shift: every part moved together, for a turn a flat drawing cannot rotate into.
@@ -3119,7 +3211,7 @@ POSE MOTION — build a performance out of saved poses:
3119
3211
  · plan: true returns the seams without touching the skeleton, so you can check the joins first.
3120
3212
  - move_root: a deterministic locomotion track, keyed in SCENE SECONDS on the same clock as the pose sequence. Without it a walk cycle is a march on the spot; with a last keyframe shorter than the performance, the figure arrives early and stands there.
3121
3213
  - auto_walk / auto_breath / auto_idle / auto_jump: one-call procedural layers. breath and idle find their bones BY NAME (spine/chest/body, head/hip/spine) and refuse with that list if the rig uses other names.
3122
- - bake_animation: freeze the whole rig into plain item keyframes for export. Needs items ATTACHED to bones — a bare skeleton has no output.
3214
+ - bake_animation: { skeletonId, options?: { duration? (2), fps? (30), detach? (true), write? (true) } } → { written, keyframes, detached, wrote } — freeze the whole rig into plain item keyframes for export. Needs items ATTACHED to bones. It DETACHES the items (the bones stop driving them, so the keyframes are what plays); pass options.detach:false to keep the rig driving.
3123
3215
  - add_secondary_motion: spring chains for tails, hair, cloth. skin_path: per-vertex deformation of a curved path.
3124
3216
 
3125
3217
  ACTIONS:
@@ -3134,6 +3226,12 @@ ACTIONS:
3134
3226
  - import_bvh: { bvhText, view? (side|front), fps? (15; 0 = every frame), height? (320px), rootPosition?, name? } → { skeletonId, poses, duration, warnings } — build a NEW rig from a BVH mocap clip (CMU / Mixamo), stick figure included. Root translation returns as a separate track rather than baked into poses.
3135
3227
  - retarget_bvh: { bvhText, skeletonId, fps?, name? } → { skeletonId, matched, unmatchedSource, unmatchedTarget, poses, duration } — drive an EXISTING rig with a BVH clip, bones matched by name. CHECK matched/unmatched in the result: a retarget that matched 2 of 15 bones "succeeds" and looks broken.
3136
3228
  - import_spine: { spineJson, rootPosition?, name? } → { skeletonId, bones, placeholders, animations, warnings } — import a Spine editor JSON export (bones, poses, attachment placeholders).
3229
+ - list_skeletons: {} → { skeletons: [{ id, name, boneCount, rootBoneId, hasIK, hasPhysics }], count } — needs no skeletonId; the place to find one on a rig you did not build.
3230
+ - stop_pose_sequence: { skeletonId } — stops the playing pose sequence; joints go back to their poses / rest. Refused (a no-op, not a fault) when nothing was playing.
3231
+ - apply_pose_transition: { skeletonId, transitionName, poseIdMap? ({ transitionPoseName: savedPoseId }) } — plays a named stock transition (walk, run, flap …; list_pose_libraries returns the names). It plays poses BY NAME, so load_pose_library first; poseIdMap is built from list_poses by name when omitted.
3232
+ - stop_root_track: { skeletonId } — stops a move_root locomotion track; the figure stays where it is. Refused (a no-op) when no track was running.
3233
+ - list_shape_keys: { skeletonId } → { shapeKeys: [{ id, name }], count }
3234
+ - load_shape_key: { skeletonId, shapeKeyId, weight? (0..1, default 1) } — blends the attached items toward a shape key saved with save_shape_key.
3137
3235
 
3138
3236
  BREAKDOWN POSES (S12): a breakdown keyframe shapes the ARC + SPACING between key poses (favor biases spacing; boneOffsets lag bones so a tip drags its root — follow-through; movingHold keeps a hold alive). This is the difference between mechanical and lifelike motion.`,
3139
3237
  inputSchema: {
@@ -3234,7 +3332,7 @@ EXAMPLE doc: { "nodes": [ {"id":"bar","type":"pp:Rectangle","width":300,"height"
3234
3332
  width: { type: 'number' },
3235
3333
  height: { type: 'number' },
3236
3334
  },
3237
- description: 'Canvas size used to tile component roots (default 1080×1080).',
3335
+ description: 'Canvas size to lay out for (default: the live canvas). The result returns registry itemIds, the document ids as docIds, and idMap pairing them.',
3238
3336
  },
3239
3337
  defaultGeometry: {
3240
3338
  type: 'object',
@@ -3412,7 +3510,7 @@ Relations are COMPOSITIONAL - an item can have multiple relations that work toge
3412
3510
  relationType: {
3413
3511
  type: 'string',
3414
3512
  enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
3415
- description: 'Type of relationship',
3513
+ description: 'Type of relationship. A name registered with pinepaper_register_custom_relation in this session is accepted too.',
3416
3514
  },
3417
3515
  params: {
3418
3516
  type: 'object',
@@ -3503,7 +3601,9 @@ USE WHEN:
3503
3601
  idempotentHint: false,
3504
3602
  openWorldHint: false,
3505
3603
  },
3506
- description: `Register a custom relation type with compute and apply functions.
3604
+ description: `PARAMS ARE DEFINITIONS, not values: { gap: { type: 'number', default: 60, min: 0 } } (type is number | string | boolean | array | object). Once registered, the name is a valid relationType for pinepaper_add_relation in this session.
3605
+
3606
+ Register a custom relation type with compute and apply functions.
3507
3607
 
3508
3608
  USE WHEN:
3509
3609
  - Creating physics-based behaviors (attraction, repulsion, collision)
@@ -3824,6 +3924,8 @@ USE WHEN:
3824
3924
  - Seeking to specific time
3825
3925
  - Controlling animation state
3826
3926
 
3927
+ PLAYBACK: play { duration?, loop? } · pause {} (holds the playhead) · stop {} (stops and rewinds) · seek { time (s), deterministic? }
3928
+
3827
3929
  DETERMINISTIC SEEK: pass deterministic: true with action 'seek' to evaluate the WHOLE scene at the exact time via app.sceneAt(t) — keyframes + relations + generators all tick to t, so the same time always yields the same frame (paired with a seed in pinepaper_capture_frames for reproducible output). Plain seek only sets the keyframe playback state. Use deterministic seek before pinepaper_browser_screenshot to capture a reproducible frame.
3828
3930
 
3829
3931
  RATE (set_time_scale / get_time_scale): 1 is real time, 0.5 half speed, 0 freezes the clock WITHOUT stopping playback, and a negative rate runs the scene backwards. Changing it preserves the current position — the clock is rebased, so the playhead does not jump. Export is deliberately unaffected: a scene watched at 0.5x still exports its real duration rather than a file twice as long.
@@ -5050,7 +5152,7 @@ flags as a badly-composed scene, and they do not survive an artboard change.`,
5050
5152
  medium: { type: 'string', enum: ['vector', 'thread'], description: "What makes the marks. 'thread' stitches every closed path AFTER arranging; photographs and text cannot be stitched and return in medium.skipped with the reason. Other media are refused with their own reason rather than faked." },
5051
5153
  stitch: { type: 'string', enum: [...THREAD_STITCHES], description: 'Which stitch when medium is thread (default longAndShort) — the same six pinepaper_design_medium offers.' },
5052
5154
  stitchBudget: { type: 'number', description: 'Total marks across the composition (default 6000); the stitch scales to fit rather than the fill being cut short.' },
5053
- action: { type: 'string', enum: ['list_patterns', 'list_treatments', 'apply', 'set_treatment'] },
5155
+ action: { type: 'string', enum: ['list_patterns', 'list_treatments', 'list_reveals', 'list_styles', 'apply', 'set_treatment'] },
5054
5156
  pattern: { type: 'string', description: 'Pattern key from list_patterns' },
5055
5157
  itemIds: { type: 'array', items: { type: 'string' }, description: 'Items in slot order' },
5056
5158
  treatment: { type: 'string', description: 'Camera treatment key' },
@@ -5058,6 +5160,15 @@ flags as a badly-composed scene, and they do not survive an artboard change.`,
5058
5160
  applyCamera: { type: 'boolean', description: 'false to arrange without compiling a camera track' },
5059
5161
  loop: { type: 'boolean' },
5060
5162
  craft: { type: 'object', description: 'Override craft ratios (gutter, margin…), as fractions of the short canvas edge' },
5163
+ // These were in the Zod schema and the description but not here, so a
5164
+ // client never saw them (1.6.19 gate C3).
5165
+ reveal: { type: ['string', 'null'], description: 'apply: temporal reveal key from list_reveals — items arriving over time' },
5166
+ revealOptions: { type: 'object', description: 'apply: reveal tuning { duration, beat, fade, beats: number[] }; explicit beats sync cuts to audio' },
5167
+ style: { type: 'string', description: 'apply: visual style bundle from list_styles (treatments + spacing + motion defaults)' },
5168
+ text: { type: 'array', items: { type: 'string' }, description: "apply: content for the pattern's TEXT slots (headline, caption…), in slot order" },
5169
+ assets: { type: 'array', items: { type: 'string' }, description: "apply: SVG markup for the pattern's VECTOR slots (badge, logo mark), in slot order" },
5170
+ audio: { type: 'string', description: 'apply: audio source to beat-sync the reveal to' },
5171
+ grid: { type: 'boolean', description: 'apply with audio: snap to an even tempo grid instead of raw onsets' },
5061
5172
  },
5062
5173
  required: ['action'],
5063
5174
  },
@@ -5244,13 +5355,13 @@ ACTIONS:
5244
5355
  - lineage: Walk back to the roots — the chain of imports/derivations that produced it. Params: itemId
5245
5356
  - dependents: What breaks if you delete it. Params: itemId — check this BEFORE removing something
5246
5357
  other items were built from.
5247
- - record: Note that this item derives from a source. Params: itemId, kind, sourceRef, meta`,
5358
+ - record: Note where this item came from. Params: itemId, kind (${LINEAGE_KINDS.join(' | ')}; default derivedFrom), sourceRef (an item id, or asset:… / component:…), meta`,
5248
5359
  inputSchema: {
5249
5360
  type: 'object',
5250
5361
  properties: {
5251
5362
  action: { type: 'string', enum: ['get', 'lineage', 'dependents', 'record'] },
5252
5363
  itemId: { type: 'string' },
5253
- kind: { type: 'string', description: 'Lineage kind, e.g. traced | imported | derived' },
5364
+ kind: { type: 'string', enum: [...LINEAGE_KINDS], description: 'record: lineage kind (default derivedFrom)' },
5254
5365
  sourceRef: { type: 'string', description: 'What it came from (item id, asset id, URL)' },
5255
5366
  meta: { type: 'object' },
5256
5367
  },
@@ -5636,6 +5747,8 @@ ACTIONS:
5636
5747
  - choose: Mood- and context-weighted recommendation based on subject/prompt words and target feeling.
5637
5748
  - coverage: Report total indexable and describable capabilities across categories.
5638
5749
  - find: Direct lookup of a capability by key to inspect its description and how to apply it (applyWith).
5750
+ - catalogue: { catalogue: 'rig_presets' | 'shader_effects' | 'stroke_decorations' | 'precomps' | 'images' | 'segment_edit_kinds' | 'shatter_orders' | 'world_meshes' | 'relation_presets' } — the registries 'list' does not gather.
5751
+ - studio: {} — which engine methods THIS studio has, so you learn what will refuse before calling it.
5639
5752
 
5640
5753
  MOOD VOCABULARY FOR CHOOSE:
5641
5754
  - reveal: resolve, reveal, appear, emerge, decode, decrypt, unscramble, form, assemble
@@ -5833,7 +5946,8 @@ ACTIONS:
5833
5946
  BEHAVIOR TYPES: repel, attract, follow, orbit, slingshot, physics_body, draggable_constrained
5834
5947
  - repel params: strength, maxDistance, minDistance, returnSpeed (0–1, default 0.08 — how fast the item eases back to its home position once the target/cursor leaves; returnSpeed: 0 freezes it scattered instead of reforming).
5835
5948
 
5836
- ACTION TYPES: navigate, show, hide, animate, stopAnimation, setState, incrementScore, playTimeline, pauseTimeline, seekTimeline, showFeedback, complete`,
5949
+ ACTION TYPES: navigate, show, hide, animate, stopAnimation, setState, incrementScore, playTimeline, pauseTimeline, seekTimeline, showFeedback, complete
5950
+ ACTION PARAMS: show / hide / stopAnimation: { items: [ids] } (itemId is accepted); animate: { items, animation?, speed? }; incrementScore: { points } (default 1; amount is accepted); setState: { key, value }.`,
5837
5951
  inputSchema: {
5838
5952
  type: 'object',
5839
5953
  properties: {
@@ -5948,7 +6062,7 @@ TEMPLATE CATEGORIES (13):
5948
6062
  EXAMPLES:
5949
6063
  - List all templates: { "listOnly": true }
5950
6064
  - List by category: { "category": "social-media", "listOnly": true }
5951
- - Load template: { "templateId": "solar-system" }`,
6065
+ - Load template: { "templateId": "solar-system-education" }`,
5952
6066
  inputSchema: {
5953
6067
  type: 'object',
5954
6068
  properties: {
@@ -6129,7 +6243,7 @@ RETURNS:
6129
6243
  properties: {
6130
6244
  id: {
6131
6245
  type: 'string',
6132
- description: 'Stable ID for the connector. Assign your own and reuse it as `connectorId` in pinepaper_update_connector / pinepaper_remove_connector — creation does not return an ID you can rely on. Without one the engine mints a timestamp-based ID that no caller can predict.',
6246
+ description: 'Requested connector id. The studio may not honour it (the result says idHonoured): always use the connectorId the result returns in pinepaper_update_connector / pinepaper_remove_connector.',
6133
6247
  },
6134
6248
  sourceItemId: {
6135
6249
  type: 'string',
@@ -6226,7 +6340,7 @@ Same as pinepaper_connect: routing, lineColor, lineWidth, lineStyle, headStyle,
6226
6340
  properties: {
6227
6341
  id: {
6228
6342
  type: 'string',
6229
- description: 'Stable ID for the connector. Assign your own and reuse it as `connectorId` in pinepaper_update_connector / pinepaper_remove_connector — creation does not return an ID you can rely on. Without one the engine mints a timestamp-based ID that no caller can predict.',
6343
+ description: 'Requested connector id. The studio may not honour it (the result says idHonoured): always use the connectorId the result returns in pinepaper_update_connector / pinepaper_remove_connector.',
6230
6344
  },
6231
6345
  sourceItemId: {
6232
6346
  type: 'string',
@@ -6667,14 +6781,13 @@ EXAMPLES:
6667
6781
  idempotentHint: false,
6668
6782
  openWorldHint: false,
6669
6783
  },
6670
- description: `Load a map and control its viewport. Call action: "load" first to set up; subsequent calls control pan / zoom / config import-export.
6784
+ description: `Load a map and import/export its config. Call action: "load" first.
6671
6785
 
6672
6786
  ACTIONS:
6673
- - load — { mapId: "usa"|"world"|"worldHighRes"|<custom>, projection?, options? }
6674
- - pan — { lat, lon, duration? }
6675
- - zoom — { level, duration? }
6787
+ - load — { mapId: "usa"|"world"|"worldHighRes"|<custom>, projection?, center?: [lon, lat], options? } — center is the projection's centre; options.scale is the d3 projection scale, UNIT-LESS (hundreds: about 600 fills India on a 1080-tall canvas). center alone does not zoom
6788
+ - pan, zoom — REFUSED: the map has no view transform, and a camera move would change only the studio view, which a canvas export ignores. For a moving frame, keyframe a camera animation and export with framing:"camera"
6676
6789
  - export_config — {}
6677
- - import_custom — { url? | geojson?, projection? }
6790
+ - import_custom — { url? | geoJson? (geojson accepted), options?: { projection?, fillColor? } }
6678
6791
 
6679
6792
  For region styling/selection use pinepaper_map_regions; for animations use pinepaper_map_animation; for CSV/GeoJSON/source-info use pinepaper_map_data.`,
6680
6793
  inputSchema: {
@@ -6683,6 +6796,7 @@ For region styling/selection use pinepaper_map_regions; for animations use pinep
6683
6796
  action: { type: 'string', enum: ['load', 'pan', 'zoom', 'export_config', 'import_custom'], description: 'Map action' },
6684
6797
  mapId: { type: 'string', description: 'Map identifier (load)' },
6685
6798
  projection: { type: 'string', description: 'Projection (load / import_custom)' },
6799
+ center: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2, description: 'load: [lon, lat] the projection centres on' },
6686
6800
  options: { type: 'object', description: 'Map options (load)', additionalProperties: true },
6687
6801
  lat: { type: 'number', description: 'Latitude (pan)' },
6688
6802
  lon: { type: 'number', description: 'Longitude (pan)' },
@@ -6706,15 +6820,17 @@ For region styling/selection use pinepaper_map_regions; for animations use pinep
6706
6820
  description: `Highlight, color, label, and select map regions. Requires a map loaded via pinepaper_map { action: "load" } first.
6707
6821
 
6708
6822
  ACTIONS:
6709
- - highlight — { regionIds: string[], color?, opacity? }
6823
+ - highlight — { regionIds: string[], color?, opacity? } opacity 0..1 tints over the land: pre-blended over the region's land fill, drawn opaque
6710
6824
  - unhighlight — { regionIds?: string[] } omit to clear all
6711
6825
  - apply_colors — { data: Record<regionId, value>, options? } choropleth coloring
6712
6826
  - add_marker — { lat, lon, label?, color?, size? }
6713
6827
  - add_labels — { regions?: string[], style?, formatter? }
6714
- - get_at_point — { x, y } region at canvas coords
6828
+ - get_at_point — { x, y } region id + name at canvas coords, and its (lon, lat)
6715
6829
  - select — { regionIds: string[] }
6716
6830
  - deselect — { regionIds?: string[] } omit to deselect all
6717
- - get_highlighted — {}`,
6831
+ - get_highlighted — {} every region highlight OR apply_colors coloured (one colour channel)
6832
+
6833
+ highlight and apply_colors return the regions they coloured, by id, and name any id not on the map (notFound / unmatched).`,
6718
6834
  inputSchema: {
6719
6835
  type: 'object',
6720
6836
  properties: {
@@ -6749,19 +6865,19 @@ ACTIONS:
6749
6865
  description: `Animate map regions with keyframes or wave effects.
6750
6866
 
6751
6867
  ACTIONS:
6752
- - animate_regions — { regions: Record<regionId, keyframes[]>, duration?, loop? }
6753
- - animate_wave — { regions?: string[], waveDirection?, speed?, color?, duration?, loop? }
6868
+ - animate_regions — { regions: { [regionId]: [{ time, fillColor (or color), opacity?, easing? }, …at least 2] }, duration?, loop? }
6869
+ - animate_wave — { colors: [at least 2], waveDirection? (horizontal | vertical | radial), waveSpeed?, duration?, loop? } — the wave runs across ALL regions
6754
6870
  - stop — { regionIds?: string[] }
6755
6871
  - get_animated — {}`,
6756
6872
  inputSchema: {
6757
6873
  type: 'object',
6758
6874
  properties: {
6759
6875
  action: { type: 'string', enum: ['animate_regions', 'animate_wave', 'stop', 'get_animated'], description: 'Animation action' },
6760
- regions: { type: 'object', description: 'Keyframes per region (animate_regions) or list (animate_wave)', additionalProperties: true },
6761
- regionIds: { type: 'array', items: { type: 'string' } },
6876
+ regions: { description: 'animate_regions: an object of keyframes per region id. stop: an array of region ids to stop (omit for all).' },
6877
+ resetColors: { type: 'boolean', description: 'stop: reset regions to their default colours (default true).' },
6878
+ colors: { type: 'array', items: { type: 'string' }, description: 'animate_wave: colours the wave cycles through (at least 2).' },
6762
6879
  waveDirection: { type: 'string', enum: ['horizontal', 'vertical', 'radial'] },
6763
- speed: { type: 'number' },
6764
- color: { type: 'string' },
6880
+ waveSpeed: { type: 'number' },
6765
6881
  duration: { type: 'number' },
6766
6882
  loop: { type: 'boolean' },
6767
6883
  },
@@ -6780,8 +6896,8 @@ ACTIONS:
6780
6896
  description: `CSV / GeoJSON import-export and source-info introspection.
6781
6897
 
6782
6898
  ACTIONS:
6783
- - export_csv — { regionIds?, columns?, download?, filename? }
6784
- - import_csv — { csv: string, mapping?: { region?, value?, color? } }
6899
+ - export_csv — { includeColors?, includeHighlighted?, includeSelected?, download?, filename? } — one row per region of the loaded map
6900
+ - import_csv — { csv (or csvText): string, applyColors?, applyHighlight?, applySelection? } — the CSV export_csv writes (one row per region id)
6785
6901
  - export_geojson — { includeStyles?, includeMetadata?, selectedOnly?, download?, filename? }
6786
6902
  - export_original_geojson — { download?, filename? } source GeoJSON before styling
6787
6903
  - source_info — {} { source, projection, quality, regionCount, hasOriginalGeoJSON, isCustomImport }`,
@@ -6789,10 +6905,14 @@ ACTIONS:
6789
6905
  type: 'object',
6790
6906
  properties: {
6791
6907
  action: { type: 'string', enum: ['export_csv', 'import_csv', 'export_geojson', 'export_original_geojson', 'source_info'], description: 'Data action' },
6792
- regionIds: { type: 'array', items: { type: 'string' } },
6793
- columns: { type: 'array', items: { type: 'string' } },
6794
- csv: { type: 'string' },
6795
- mapping: { type: 'object', additionalProperties: true },
6908
+ csv: { type: 'string', description: 'import_csv: the CSV text (csvText is accepted too).' },
6909
+ csvText: { type: 'string' },
6910
+ applyColors: { type: 'boolean' },
6911
+ applyHighlight: { type: 'boolean' },
6912
+ applySelection: { type: 'boolean' },
6913
+ includeColors: { type: 'boolean' },
6914
+ includeHighlighted: { type: 'boolean' },
6915
+ includeSelected: { type: 'boolean' },
6796
6916
  includeStyles: { type: 'boolean' },
6797
6917
  includeMetadata: { type: 'boolean' },
6798
6918
  selectedOnly: { type: 'boolean' },
@@ -7148,7 +7268,7 @@ USE WHEN:
7148
7268
  - "grid background", "geometric abstract", "noise texture"
7149
7269
  - Creating dynamic procedural backgrounds
7150
7270
 
7151
- GENERATORS (15 total):
7271
+ GENERATORS (highlights; all ${GeneratorNameSchema.options.length} are in the generatorName enum):
7152
7272
 
7153
7273
  Classic:
7154
7274
  - drawSunburst: Radial rays from center (rayCount, colors, bgColor, opacity, rayGap, gradientRays)
@@ -7292,7 +7412,7 @@ glow's own default is 1.5. Pass a pixel count here and you will be wrong by
7292
7412
  about two orders of magnitude. A recent studio warns about it
7293
7413
  (kind: 'glow-size-unit'); an older one does not.
7294
7414
 
7295
- EFFECTS (15 particle + 7 shader auras):
7415
+ EFFECTS (${EffectTypeSchema.options.length}: particles, then the shader auras from heatmap on):
7296
7416
  - sparkle: Glitter/sparkle particles (color, speed, size)
7297
7417
  - blast: Explosion burst effect (color, radius, count)
7298
7418
  - smoke: Rising smoke plumes (color, speed, size, drift, height, growthRate)
@@ -7362,27 +7482,17 @@ USE WHEN:
7362
7482
  - Post-processing the entire scene
7363
7483
  - User says "make it look vintage", "add blur", "black and white"
7364
7484
 
7365
- AVAILABLE FILTERS:
7366
- - grayscale: Convert to black and white (params: intensity 0-1)
7367
- - sepia: Warm vintage brownish tone (params: intensity 0-1)
7368
- - blur: Gaussian blur effect (params: radius 0-20)
7369
- - brightness: Adjust brightness (params: value -100 to 100)
7370
- - contrast: Adjust contrast (params: value -100 to 100)
7371
- - saturation: Adjust color saturation (params: value -100 to 100). 'saturate' is accepted as the same filter.
7372
- - invert: Invert colors (params: intensity 0-1)
7373
- - noise: Add film grain (params: intensity 0-100, monochrome: true/false)
7374
- - vignette: Darken edges (params: intensity 0-1, radius 0-1)
7375
- - vintage: Preset combining sepia, vignette, noise
7376
- - colorOverlay: Add color tint (params: color, intensity, blendMode)
7377
- - sharpen: Increase sharpness (params: intensity 0-100)
7378
- - emboss: 3D emboss (params: intensity 0-1)
7379
- - posterize: Reduce color levels (params: levels 2-32)
7485
+ AVAILABLE FILTERS (the engine's own list, with each one's parameters):
7486
+ ${FILTER_TYPES.map((f) => `- ${f}: ${FILTER_DOCS[f]}`).join('\n')}
7487
+ 'saturate' is accepted as saturation.
7380
7488
 
7381
7489
  EXAMPLES:
7382
7490
  - "Make it black and white" → filterType: "grayscale"
7383
7491
  - "Add vintage look" → filterType: "vintage", params: {intensity: 0.8}
7384
7492
  - "Blur the background" → filterType: "blur", params: {radius: 5}
7385
7493
  - "Increase contrast" → filterType: "contrast", params: {value: 30}
7494
+ - "Print it like a comic" → filterType: "halftoneCMYK", params: {size: 6}
7495
+ - "8-bit / retro screen" → filterType: "dither", params: {levels: 4}
7386
7496
 
7387
7497
  Filters can be stacked - call multiple times to combine effects.`,
7388
7498
  inputSchema: {
@@ -7390,7 +7500,7 @@ Filters can be stacked - call multiple times to combine effects.`,
7390
7500
  properties: {
7391
7501
  filterType: {
7392
7502
  type: 'string',
7393
- enum: ['grayscale', 'sepia', 'blur', 'brightness', 'contrast', 'saturation', 'invert', 'noise', 'vignette', 'vintage', 'colorOverlay', 'sharpen', 'emboss', 'posterize', 'saturate'],
7503
+ enum: [...FILTER_TYPES],
7394
7504
  description: 'Type of filter to apply',
7395
7505
  },
7396
7506
  params: {
@@ -7543,28 +7653,204 @@ OUTPUT FORMAT:
7543
7653
  idempotentHint: true,
7544
7654
  openWorldHint: false,
7545
7655
  },
7546
- description: `Export the complete scene state including all items, relations, and settings.
7656
+ description: `Export the scene: a SUMMARY for inspection by default, or with full: true a RESTORABLE project document saved to a file.
7657
+
7658
+ SUMMARY (default) — for reading the scene, not for saving it:
7659
+ - items (id, type, position, bounds, active window), relations, decorative items, backgroundColor, canvasSize
7660
+ - It does NOT carry keyframe tracks, rigging or scene chains, so a scene rebuilt from it has no motion. The result says so (note).
7661
+
7662
+ FULL (full: true) — to save work and restore it later:
7663
+ - The engine's own project document (relations, keyframes, rigging, connectors, scene chains, animation config), written to a file because it can be megabytes.
7664
+ - Returns filePath, bytes and counts (items, animated items, keyframes, relations). Restore with pinepaper_import_scene { path } and compare the counts.
7547
7665
 
7548
7666
  USE WHEN:
7549
- - Saving a complete scene for later restoration
7550
- - Creating scene backups
7551
- - Debugging scene composition
7552
- - Generating scene snapshots for version control
7667
+ - full: true — saving a scene to come back to, a backup, a hand-off to another session
7668
+ - summary — debugging composition, checking what is on the canvas`,
7669
+ inputSchema: {
7670
+ type: 'object',
7671
+ properties: {
7672
+ full: { type: 'boolean', description: 'Save a restorable project document to a file instead of returning the summary.' },
7673
+ name: { type: 'string', description: 'full: a name recorded in the document.' },
7674
+ },
7675
+ },
7676
+ },
7677
+ {
7678
+ name: 'pinepaper_styled_scene',
7679
+ annotations: {
7680
+ title: 'Styled Scene',
7681
+ readOnlyHint: false,
7682
+ destructiveHint: false,
7683
+ idempotentHint: true,
7684
+ openWorldHint: false,
7685
+ },
7686
+ description: `One composition, drawn in a built-in STYLE, as one canvas item that redraws itself every exported frame. No drawing code: pick a style, optionally shape the composition, then export.
7553
7687
 
7554
- RETURNS:
7555
- - items: Array of all canvas items with properties
7556
- - relations: Array of active relations
7557
- - decorative: Array of decorative (non-interactive) items
7558
- - backgroundColor: Current background color
7559
- - canvasSize: Canvas dimensions`,
7688
+ STYLES: ${STYLED_SCENE_STYLES.map((k) => `${k} (${STYLED_SCENE_STYLE_INFO[k]})`).join('; ')}. Default watercolor; the studio also takes its aliases (watercolour, papercut, 1bit…), and an unknown style is refused with the list.
7689
+
7690
+ THE COMPOSITION is data (spec). Every key is optional; omitted keys take the 'valley' preset, a valley going from dream to cost over 8 s: size [W, H], duration, transition {name, start, length}, camera {zoom: [from, to], pan}, palette, sun, clouds, ridges, foreground, river, settlements, structure, pylons, vegetation, effects {steam, ripples, scars}, hud {label}.
7691
+
7692
+ THEN EXPORT FOR ITS DURATION: the result carries duration, and the scene runs that long. Pass it as agent_export's duration; a different length cuts the scene short or holds its last frame.
7693
+
7694
+ KNOW:
7695
+ - Calling again with the same id REPLACES the scene, which is how to change style.
7696
+ - dither and ascii need a Chrome renderer (WebKit ignores the canvas filter they use).
7697
+ - The scene redraws every frame through a render hook. After a page reload it is a still image until this is called again.
7698
+ - It sits below every other item, sized to cover the canvas, so titles and overlays go on top as usual.
7699
+
7700
+ EXAMPLE: { style: 'ink', duration: 6 }`,
7560
7701
  inputSchema: {
7561
7702
  type: 'object',
7562
- properties: {},
7703
+ properties: {
7704
+ style: { type: 'string', description: `${STYLED_SCENE_STYLES.join(' | ')} (default watercolor)` },
7705
+ duration: { type: 'number', description: 'Seconds the scene runs (default 8). Export for this long.' },
7706
+ id: { type: 'string', description: 'Scene id (default "styled-scene"); the same id replaces the scene.' },
7707
+ spec: { type: 'object', description: "The composition; omitted keys take the 'valley' preset.", additionalProperties: true },
7708
+ },
7709
+ },
7710
+ },
7711
+ {
7712
+ name: 'pinepaper_relight',
7713
+ annotations: {
7714
+ title: 'Relight the 3D World',
7715
+ readOnlyHint: false,
7716
+ destructiveHint: false,
7717
+ idempotentHint: true,
7718
+ openWorldHint: false,
7719
+ },
7720
+ description: `Light the 3D world layer (pinepaper_world3d) with point and directional lights — a post pass, so the lit side of every surface reads brighter in the preview and the export.
7721
+
7722
+ ACTIONS:
7723
+ - set: { lights, ambient?, strength?, enabled?, preview? } — install or replace the rig. Up to 8 lights:
7724
+ point: { type: 'point', x, y, z? (120), radius? (400), color?, intensity? (1), specular? (32) }
7725
+ directional: { type: 'directional', dir: [dx, dy, dz], color?, intensity? } [0, 0, -1] faces the viewer
7726
+ Coordinates are export-frame px; colours are '#rrggbb' or [r, g, b]. ambient default 0.2, strength (normal-map) default 2.
7727
+ The result reports cost: the pass runs on the CPU, about 0.42 s per 1080p frame, so a 10 s MP4 at 30 fps takes about 2 minutes longer.
7728
+ - get: the current rig.
7729
+ - clear: remove it.
7730
+ - list_lights: the light types.
7731
+
7732
+ LIMITS — say them before you promise a look:
7733
+ - Applied in the preview and in PNG, PDF, MP4 and WebM exports. GIF and APNG are NOT relit (agent_export says so when it happens).
7734
+ - Normals come from the world's brightness, not real depth, so it reads as raking light over the picture rather than true 3D shading.
7735
+ - It lights the 3D world layer only, not 2D items drawn over it.
7736
+ - The rig is NOT saved with the project: set it again after reloading.
7737
+ - A studio without the relight pass refuses with the reason.
7738
+
7739
+ EXAMPLE: { action: 'set', lights: [{ type: 'point', x: 1500, y: 200, z: 300, radius: 1200, color: '#ffd9a0', intensity: 1.4 }], ambient: 0.15 }`,
7740
+ inputSchema: {
7741
+ type: 'object',
7742
+ properties: {
7743
+ action: { type: 'string', enum: ['set', 'get', 'clear', 'list_lights'], description: 'Relight operation' },
7744
+ lights: { type: 'array', items: { type: 'object', additionalProperties: true }, description: "set: up to 8 lights, {type:'point', x, y, z?, radius?, color?, intensity?, specular?} or {type:'directional', dir:[dx,dy,dz], color?, intensity?}" },
7745
+ ambient: { type: 'number', description: 'set: ambient light (default 0.2)' },
7746
+ strength: { type: 'number', description: 'set: normal-map strength (default 2)' },
7747
+ enabled: { type: 'boolean', description: 'set: false keeps the rig but turns it off' },
7748
+ preview: { description: 'set: true / false, or {maxSide} for the live preview overlay', oneOf: [{ type: 'boolean' }, { type: 'object', properties: { maxSide: { type: 'number' } } }] },
7749
+ },
7750
+ required: ['action'],
7751
+ },
7752
+ },
7753
+ {
7754
+ name: 'pinepaper_shader_graph',
7755
+ annotations: {
7756
+ title: 'Shader Graph',
7757
+ readOnlyHint: false,
7758
+ destructiveHint: false,
7759
+ idempotentHint: false,
7760
+ openWorldHint: false,
7761
+ },
7762
+ description: `Build a pixel-processing graph from nodes (normal map, relighting, depth of field, bloom) and run it over an item or the whole scene.
7763
+
7764
+ ACTIONS:
7765
+ - node_types: the node vocabulary with each node's params. Call this first.
7766
+ - validate: { graph } — check it, store nothing. Errors are named (unknown type, duplicate id, dangling edge, cycle, branching).
7767
+ - create: { graph } → graphId — validate and store.
7768
+ - get / remove: { graphId }. list: the stored graphs.
7769
+ - apply: { target, graph | graphId, keepSource?, asItem? }
7770
+ target = an itemId → the item's pixels become a NEW image item in its place (returned itemId); the source is hidden unless keepSource:true. One undo step.
7771
+ target = 'scene' → the rendered frame is processed and returned as a PNG (data); the scene does not change. asItem:true adds it as an image item instead.
7772
+
7773
+ GRAPH: { nodes: [{ id, type, params? }], edges: [{ from, to }] } — a LINEAR chain from SourceInput to OutputCompositor. A NormalGen feeds a Relighting node; a DepthMap feeds DepthOfField.
7774
+
7775
+ PARAMS come from node_types, which is the studio's own list. SelectiveBloom: threshold 0..1, intensity 0..10, and radius 0..64 px (the glow's reach past the bright pixels; 0 keeps the glow inside them) where the studio has it. A glow can make the new item LARGER than its source, centred on it: read its bounds from the reply rather than reusing the source's.
7776
+
7777
+ The graph runs on the CPU. Stored graphs live for the session, not in the project. A studio without the shader graph refuses with the reason.
7778
+
7779
+ EXAMPLE: { action: 'apply', target: 'item_4', graph: { nodes: [{ id: 'in', type: 'SourceInput' }, { id: 'bloom', type: 'SelectiveBloom', params: { threshold: 0.6, intensity: 2 } }, { id: 'out', type: 'OutputCompositor' }], edges: [{ from: 'in', to: 'bloom' }, { from: 'bloom', to: 'out' }] } }`,
7780
+ inputSchema: {
7781
+ type: 'object',
7782
+ properties: {
7783
+ action: { type: 'string', enum: ['node_types', 'validate', 'create', 'get', 'list', 'remove', 'apply'], description: 'Shader graph operation' },
7784
+ graph: { type: 'object', description: 'validate / create / apply: {id?, nodes: [{id, type, params?}], edges: [{from, to}]}', additionalProperties: true },
7785
+ graphId: { type: 'string', description: 'get / remove / apply: a stored graph id' },
7786
+ target: { type: 'string', description: "apply: an itemId, or 'scene'" },
7787
+ keepSource: { type: 'boolean', description: 'apply to an item: keep the source visible' },
7788
+ asItem: { type: 'boolean', description: "apply to 'scene': add the result as an image item" },
7789
+ },
7790
+ required: ['action'],
7791
+ },
7792
+ },
7793
+ {
7794
+ name: 'pinepaper_place_on_surface',
7795
+ annotations: {
7796
+ title: 'Place on Surface',
7797
+ readOnlyHint: false,
7798
+ destructiveHint: false,
7799
+ idempotentHint: true,
7800
+ openWorldHint: false,
7801
+ },
7802
+ description: `Put a UI ONTO A SCREEN inside a photo: a phone, a monitor, a billboard. It is corner-pinned into the screen's perspective, matted by the photo's own brightness so a thumb or hand stays IN FRONT, softened to the photo, and keeps a little glass glare and light spill. A card laid over a phone photo reads as a sticker; this reads as the screen.
7803
+
7804
+ - photoId: the photo (an image item). sourceId: the UI, any canvas item or group (rendered at about twice the screen's size), or an image.
7805
+ - quad: the screen's corners TL, TR, BR, BL in the photo's pixels (quadSpace 'canvas' for canvas coordinates). Omit it to DETECT the screen (the largest bright, neutral region); the result's quad and detected say what was found. If nothing is found it refuses: pass quad.
7806
+ - The result is BAKED INTO THE PHOTO'S PIXELS, so a zoom or camera move on the photo carries it. Placing again in the same slot replaces it; use different slots for several screens. Undo restores the photo.
7807
+ - Tuning: occlusion ('luma' | 'none'), key, erode, matteBlur, soften, glare, spill, spillRadius, tint [r, g, b]. hideSource (default true) hides the UI item once placed.`,
7808
+ inputSchema: {
7809
+ type: 'object',
7810
+ properties: {
7811
+ photoId: { type: 'string', description: 'The photo (an image item).' },
7812
+ sourceId: { type: 'string', description: 'The UI to place (any item or group, or an image).' },
7813
+ quad: { type: 'array', items: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 }, minItems: 4, maxItems: 4, description: 'Corners TL, TR, BR, BL as [x, y]; omit to detect.' },
7814
+ quadSpace: { type: 'string', enum: ['image', 'canvas'], description: "quad's coordinates (default 'image', the photo's pixels)." },
7815
+ occlusion: { type: 'string', enum: ['luma', 'none'], description: "'luma' (default) keeps a hand in front of the screen." },
7816
+ key: { type: 'number', description: 'Brightness that counts as screen (default: found from the photo).' },
7817
+ erode: { type: 'number', description: 'Pull the matte in from the bezel, px (default 2).' },
7818
+ matteBlur: { type: 'number', description: 'Matte edge softness (default 0.8).' },
7819
+ soften: { type: 'number', description: 'Soften the UI to the photo (default 0.5).' },
7820
+ glare: { type: 'number', description: 'Glass glare 0..1 (default 0.10).' },
7821
+ spill: { type: 'number', description: 'Light spill 0..1 (default 0.35).' },
7822
+ spillRadius: { type: 'number', description: 'Spill reach, px (default 40).' },
7823
+ tint: { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3, description: 'Multiply the UI by [r, g, b] 0..1 to match white balance.' },
7824
+ slot: { type: 'string', description: "Screen name (default 'screen'); the same slot replaces." },
7825
+ hideSource: { type: 'boolean', description: 'Hide the UI item after placing (default true).' },
7826
+ },
7827
+ required: ['photoId', 'sourceId'],
7828
+ },
7829
+ },
7830
+ {
7831
+ name: 'pinepaper_import_scene',
7832
+ annotations: {
7833
+ title: 'Import Scene',
7834
+ readOnlyHint: false,
7835
+ destructiveHint: true,
7836
+ idempotentHint: true,
7837
+ openWorldHint: false,
7838
+ },
7839
+ description: `Restore a scene from a project document saved by pinepaper_export_scene { full: true }. REPLACES the current scene.
7840
+
7841
+ The engine validates the document before applying it: a truncated file, a foreign JSON blob or a document from a newer major version is refused, not half-applied. Returns counts (items, animated items, keyframes, relations): compare them with the export's to confirm nothing was lost.
7842
+
7843
+ - path: the file the export wrote (absolute, or relative to the server's working directory).
7844
+ - strict: false loads a document with validation problems anyway and returns them as warnings (default true: refuse).`,
7845
+ inputSchema: {
7846
+ type: 'object',
7847
+ properties: {
7848
+ path: { type: 'string', description: 'The project document file.' },
7849
+ strict: { type: 'boolean', description: 'Refuse a document with validation problems (default true).' },
7850
+ },
7851
+ required: ['path'],
7563
7852
  },
7564
7853
  },
7565
- // ---------------------------------------------------------------------------
7566
- // INFRASTRUCTURE: BROWSER CONTROL TOOLS
7567
- // ---------------------------------------------------------------------------
7568
7854
  {
7569
7855
  name: 'pinepaper_browser_connect',
7570
7856
  annotations: {
@@ -7820,7 +8106,7 @@ PRIVACY: No screenshots, no tool call arguments, no user content — only struct
7820
8106
 
7821
8107
  WORKFLOW: start_job → batch_execute (everything in ONE call) → end_job (screenshot) → show user → iterate if needed.
7822
8108
 
7823
- RETURNS: canvasSize {width, height} — use this to position items within bounds. Also returns ontology: a pp: triples + item summary snapshot of the (typically empty) canvas. Reuse this baseline across the same agent turn instead of calling pinepaper_get_canvas_ontology unless you've mutated the canvas. When clearCanvas is false, the ontology captures whatever's already on the canvas.
8109
+ RETURNS: canvasSize {width, height} — use this to position items within bounds. Without a canvasPreset, an unbounded canvas is fixed at that size, so stills and video both export it (the result says so in note). Also returns ontology: a pp: triples + item summary snapshot of the (typically empty) canvas. Reuse this baseline across the same agent turn instead of calling pinepaper_get_canvas_ontology unless you've mutated the canvas. When clearCanvas is false, the ontology captures whatever's already on the canvas.
7824
8110
 
7825
8111
  CANVAS PRESETS: instagram (1080x1080), instagram-story (1080x1920), tiktok (1080x1920), youtube (1920x1080), youtube-thumbnail (1280x720), twitter (1200x675), linkedin (1200x627), web, print-a4, print-letter, print-a4-landscape, print-letter-landscape (print presets carry 300 dpi, so a PDF comes out at paper size)`,
7826
8112
  inputSchema: {
@@ -7985,17 +8271,17 @@ WORKFLOW:
7985
8271
  1. pinepaper_agent_start_job → 2. pinepaper_agent_batch_execute (everything) → 3. pinepaper_agent_end_job (screenshot for validation)
7986
8272
  After validation: user reviews screenshot → feedback → modify/recreate as needed.
7987
8273
 
7988
- OPERATION TYPES (13) — use in this order:
8274
+ OPERATION TYPES (${AgentBatchOperationTypeSchema.options.length}) — use in this order:
7989
8275
 
7990
8276
  CANVAS SETUP:
7991
- set_canvas_size — {width, height} or {preset: "instagram"|"youtube"|"tiktok"|...}
8277
+ set_canvas_size — {width, height} or {preset: "instagram-post"|"instagram-story"|"youtube-thumbnail"|"tiktok"|"full-hd-1080p"|...} (studio preset keys, not platform names; an unknown one is refused with the list)
7992
8278
  set_background — {backgroundColor: "#hex"}
7993
8279
  execute_generator — {generatorName, generatorParams} → fills canvas with procedural art (paints WITHOUT fully registering items — see get_items; custom generator names need skipValidation:true)
7994
8280
  Generators: drawBokeh, drawGradientMesh, drawWaves, drawSunburst, drawSunsetScene, drawGrid, drawCircuit, drawPattern, drawStackedCircles, drawGeometricAbstract, drawWindField, drawFluidFlow, drawOrganicFlow, drawNoiseTexture, drawGlobeWireframe
7995
8281
  PineMath: drawFunctionPlot (y=f(x) plots), drawParametricCurve (x(t),y(t)), drawSimulation (pendulum, Lorenz, spring-mass), drawSpectrumAnalyzer (FFT), draw3DSurface (torus, sphere, Klein bottle)
7996
8282
 
7997
8283
  ITEMS:
7998
- create — {itemType, position: {x,y}, properties}
8284
+ create — {itemType, position: {x,y} or [x,y], properties}
7999
8285
  itemType: text, circle, rectangle, star, triangle, polygon, ellipse, path, line, arc, pentagon, hexagon, diamond, arrow, heart
8000
8286
  Properties per type:
8001
8287
  text: {content, fontSize, fontFamily, color, fontWeight}
@@ -8037,14 +8323,14 @@ ANIMATION NOTES:
8037
8323
 
8038
8324
  EXAMPLE — Animated sky scene with timed reveals:
8039
8325
  {operations: [
8040
- {type: "set_canvas_size", preset: "youtube"},
8326
+ {type: "set_canvas_size", preset: "full-hd-1080p"},
8041
8327
  {type: "execute_generator", generatorName: "drawGradientMesh", generatorParams: {colors: ["#1e3a5f", "#87CEEB", "#fbbf24"], bgColor: "#0c1445", blobCount: 4}},
8042
8328
  {type: "create", itemType: "circle", position: {x: 1600, y: 150}, properties: {radius: 80, color: "#fbbf24", shadowColor: "#fbbf24", shadowBlur: 40}},
8043
8329
  {type: "create", itemType: "ellipse", position: {x: 400, y: 180}, properties: {radiusX: 100, radiusY: 35, color: "#ffffff", opacity: 0.8}},
8044
8330
  {type: "create", itemType: "ellipse", position: {x: 900, y: 120}, properties: {radiusX: 120, radiusY: 40, color: "#ffffff", opacity: 0.7}},
8045
8331
  {type: "create", itemType: "text", position: {x: 960, y: 500}, properties: {content: "Dream Big", fontSize: 96, color: "#ffffff", fontWeight: "bold"}},
8046
8332
  {type: "animate", itemId: "$0", animationType: "pulse", animationOptions: {speed: 0.3}},
8047
- {type: "animate", itemId: "$1", animationType: "slide", animationOptions: {speed: 0.2, direction: "left"}},
8333
+ {type: "animate", itemId: "$1", animationType: "slideLeftRight", animationOptions: {speed: 0.2}},
8048
8334
  {type: "animate", itemId: "$2", animationType: "slide", animationOptions: {speed: 0.15, direction: "left"}},
8049
8335
  {type: "keyframe_animate", itemId: "$3", keyframes: [{time: 0, properties: {opacity: 0, scale: 0.8}}, {time: 2, properties: {opacity: 1, scale: 1}, easing: "easeOutBack"}], duration: 3},
8050
8336
  {type: "apply_mask", itemId: "$3", maskPreset: "wipeLeft"},
@@ -8081,9 +8367,11 @@ EXAMPLE — Animated sky scene with timed reveals:
8081
8367
  description: 'For create: item type',
8082
8368
  },
8083
8369
  position: {
8084
- type: 'object',
8085
- properties: { x: { type: 'number' }, y: { type: 'number' } },
8086
- description: 'For create: position {x, y}',
8370
+ oneOf: [
8371
+ { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } } },
8372
+ { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 },
8373
+ ],
8374
+ description: 'For create: position {x, y} or [x, y]',
8087
8375
  },
8088
8376
  properties: { type: 'object', description: 'For create/modify: item properties' },
8089
8377
  itemId: { type: 'string', description: 'Target item ID or $N reference' },
@@ -8093,7 +8381,7 @@ EXAMPLE — Animated sky scene with timed reveals:
8093
8381
  // Loop animation
8094
8382
  animationType: {
8095
8383
  type: 'string',
8096
- enum: ['bounce', 'breathe', 'fade', 'glow', 'jelly', 'path', 'pulse', 'rotate', 'scrollDown', 'scrollLeft', 'scrollRight', 'scrollUp', 'shake', 'slideLeftRight', 'slideUpDown', 'swing', 'typewriter', 'wobble'],
8384
+ enum: [...SimpleAnimationTypeSchema.options],
8097
8385
  description: 'For animate: loop animation type',
8098
8386
  },
8099
8387
  animationOptions: { type: 'object', description: 'For animate: {speed, amplitude, direction}' },
@@ -8117,7 +8405,7 @@ EXAMPLE — Animated sky scene with timed reveals:
8117
8405
  targetId: { type: 'string', description: 'For relation: target item ID or $N' },
8118
8406
  relationType: {
8119
8407
  type: 'string',
8120
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
8408
+ enum: [...RelationTypeSchema.options],
8121
8409
  description: 'For relation: type',
8122
8410
  },
8123
8411
  relationOptions: { type: 'object', description: 'For relation: options' },
@@ -8132,7 +8420,7 @@ EXAMPLE — Animated sky scene with timed reveals:
8132
8420
  // Effects
8133
8421
  effectType: {
8134
8422
  type: 'string',
8135
- enum: ['sparkle', 'blast', 'smoke', 'fire', 'rain', 'snow', 'confetti', 'ripple', 'glow', 'electric', 'bubbles', 'dust', 'fireflies', 'shockwave', 'trail', 'heatmap', 'liquid_metal', 'gem_smoke', 'electric_arc', 'vortex', 'rain_veil', 'caustics'],
8423
+ enum: [...EffectTypeSchema.options],
8136
8424
  description: 'For apply_effect: effect type',
8137
8425
  },
8138
8426
  effectParams: { type: 'object', description: 'For apply_effect: parameters' },
@@ -8207,7 +8495,7 @@ Held exports last until released, or until a LATER export needs the space — an
8207
8495
  {
8208
8496
  name: 'pinepaper_agent_export',
8209
8497
  annotations: {
8210
- title: 'Export as MP4 Video, GIF, PNG, SVG, or PDF',
8498
+ title: 'Export Video, Stills, Animation, Ads, Captions or Audio',
8211
8499
  readOnlyHint: true,
8212
8500
  destructiveHint: false,
8213
8501
  idempotentHint: true,
@@ -8236,7 +8524,7 @@ PLATFORMS & OPTIMAL FORMATS:
8236
8524
 
8237
8525
  A preset's dimensions assume the canvas has the same aspect. When it does not, the canvas aspect is KEPT (never stretched) and its short edge takes the preset's short edge — a 4:5 canvas on instagram exports 1080x1350 — and the result carries platformFit saying so.
8238
8526
 
8239
- FORMATS: svg, png, gif, mp4, webm, pdf.
8527
+ FORMATS: ${AgentExportFormatSchema.options.join(', ')}.
8240
8528
  QUALITY: draft, standard, high.
8241
8529
  FRAMING: canvas (default, full canvas) | camera (camera_animates first-keyframe viewport — video formats only, requires a keyframe-mode walkthrough on the canvas; camera animation still drives motion within the fixed output frame).
8242
8530
 
@@ -8259,7 +8547,7 @@ VERIFY MOTION BEFORE YOU RENDER. An export takes seconds to minutes and shows yo
8259
8547
  platform: {
8260
8548
  type: 'string',
8261
8549
  enum: ['auto', 'instagram', 'instagram-story', 'tiktok', 'youtube', 'youtube-thumbnail', 'twitter', 'linkedin', 'web', 'print-a4', 'print-letter', 'print-a4-landscape', 'print-letter-landscape'],
8262
- description: "Target platform, which sets the output DIMENSIONS. 'auto' (the default) renders the canvas at its own size — use it when the canvas is already the size you want, otherwise a preset REPLACES your dimensions. Every other value forces that platform's frame regardless of the canvas.",
8550
+ description: "Target platform, which sets the output DIMENSIONS. 'auto' (the default) renders the canvas at its own size — use it when the canvas is already the size you want, otherwise a preset sets the output SIZE from the platform's short edge. The canvas aspect is kept, never stretched: on a canvas of a different aspect the output is not the platform's frame (an 800x600 canvas on instagram exports 1440x1080), and the result's platformFit says so. To get the exact frame, set the canvas to the platform's aspect first.",
8263
8551
  },
8264
8552
  format: {
8265
8553
  type: 'string',
@@ -8675,6 +8963,14 @@ Three-phase analysis:
8675
8963
 
8676
8964
  Quality tiers: basic (<0.4), fair (0.4-0.6), good (0.6-0.8), excellent (>=0.8)
8677
8965
 
8966
+ DEFINITION SHAPE (a template document; id, name, category and data are REQUIRED, the rest optional; items and relations sit under data):
8967
+ { id, name, category, dimensions?: { width, height }, duration? (s), description?, tags?: [..], semantics?: {..},
8968
+ data: {
8969
+ items: [{ id, type ('circle', 'text' …), x?, y? | position?: [x, y], content?, animationType?, keyframes?: [{ time, properties: {..}, easing? }], mask?: { type } }],
8970
+ relations: [{ type ('orbits' …), source | from, target | to, params? }],
8971
+ backgroundGenerator?, backgroundGeneratorParams?
8972
+ } }
8973
+
8678
8974
  USE WHEN:
8679
8975
  - Checking a template for structural issues before publishing
8680
8976
  - Comparing quality scores between template variants
@@ -8860,12 +9156,12 @@ USE WHEN:
8860
9156
  - You want more/less detail in tool descriptions → switch verbosity
8861
9157
 
8862
9158
  PROFILES:
8863
- - full: All 120+ tools
8864
- - agent: Core creative tools (~50 tools) — default
9159
+ - full: Every tool, no filtering
9160
+ - agent: Every tool except the diagnostics (get_performance_metrics, diagnostic_report) and register_item — default
8865
9161
  - diagram: Diagram/flowchart tools
8866
9162
  - map: Map/choropleth tools
8867
9163
  - font: Font creation tools
8868
- - minimal: Bare essentials (~15 tools)
9164
+ - minimal: Bare essentials
8869
9165
 
8870
9166
  VERBOSITY:
8871
9167
  - verbose: Full descriptions
@@ -9091,7 +9387,7 @@ ACTIONS:
9091
9387
  - set_rulers: Show/hide rulers. Params: enabled (bool)
9092
9388
  - set_grid: Show/hide unit grid. Params: enabled (bool)
9093
9389
  - get_dimensions: Get item bounds. Params: itemId → returns {x, y, width, height, rotation}
9094
- - set_snap: Enable/disable snap-to-grid. Params: enabled (bool)`,
9390
+ - set_snap: Enable/disable snapping a DRAG or RESIZE in the studio to the unit grid (only while the grid is on). Params: enabled (bool). Items placed by tools are not snapped.`,
9095
9391
  inputSchema: {
9096
9392
  type: 'object',
9097
9393
  properties: {