@pinepaper.studio/mcp-server 1.6.7 → 1.6.9

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 (174) hide show
  1. package/README.de.md +9 -5
  2. package/README.es.md +9 -5
  3. package/README.fr.md +9 -5
  4. package/README.hi.md +8 -5
  5. package/README.ja.md +8 -5
  6. package/README.ko.md +8 -4
  7. package/README.md +256 -17
  8. package/README.pt-BR.md +9 -5
  9. package/README.zh-CN.md +6 -4
  10. package/dist/browser/index.d.ts +1 -1
  11. package/dist/browser/index.d.ts.map +1 -1
  12. package/dist/browser/index.js +1 -1
  13. package/dist/browser/index.js.map +1 -1
  14. package/dist/browser/puppeteer-controller.d.ts +86 -2
  15. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  16. package/dist/browser/puppeteer-controller.js +143 -60
  17. package/dist/browser/puppeteer-controller.js.map +1 -1
  18. package/dist/character/data/concepts.json +1 -0
  19. package/dist/character/data/depictions.json +1 -0
  20. package/dist/character/data/styles.json +1 -0
  21. package/dist/character/duration-scale.d.ts +13 -0
  22. package/dist/character/duration-scale.d.ts.map +1 -0
  23. package/dist/character/duration-scale.js +13 -0
  24. package/dist/character/duration-scale.js.map +1 -0
  25. package/dist/character/path-bbox.d.ts +82 -0
  26. package/dist/character/path-bbox.d.ts.map +1 -0
  27. package/dist/character/path-bbox.js +496 -0
  28. package/dist/character/path-bbox.js.map +1 -0
  29. package/dist/character/perform.d.ts +149 -0
  30. package/dist/character/perform.d.ts.map +1 -0
  31. package/dist/character/perform.js +626 -0
  32. package/dist/character/perform.js.map +1 -0
  33. package/dist/character/resolve.d.ts +51 -0
  34. package/dist/character/resolve.d.ts.map +1 -0
  35. package/dist/character/resolve.js +239 -0
  36. package/dist/character/resolve.js.map +1 -0
  37. package/dist/cli.js +12 -0
  38. package/dist/cli.js.map +1 -1
  39. package/dist/design/design-systems.d.ts +224 -0
  40. package/dist/design/design-systems.d.ts.map +1 -0
  41. package/dist/design/design-systems.js +434 -0
  42. package/dist/design/design-systems.js.map +1 -0
  43. package/dist/execution/canvas-state.d.ts +7 -0
  44. package/dist/execution/canvas-state.d.ts.map +1 -1
  45. package/dist/execution/canvas-state.js +20 -8
  46. package/dist/execution/canvas-state.js.map +1 -1
  47. package/dist/index.d.ts +20 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +12 -4
  50. package/dist/index.js.map +1 -1
  51. package/dist/ontology/design-graph.d.ts +9 -0
  52. package/dist/ontology/design-graph.d.ts.map +1 -1
  53. package/dist/ontology/design-graph.js +98 -3
  54. package/dist/ontology/design-graph.js.map +1 -1
  55. package/dist/ontology/types.d.ts +42 -0
  56. package/dist/ontology/types.d.ts.map +1 -1
  57. package/dist/ontology/vocabulary.d.ts.map +1 -1
  58. package/dist/ontology/vocabulary.js +20 -0
  59. package/dist/ontology/vocabulary.js.map +1 -1
  60. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  61. package/dist/tools/compact-descriptions.js +25 -0
  62. package/dist/tools/compact-descriptions.js.map +1 -1
  63. package/dist/tools/definitions.d.ts +1 -1
  64. package/dist/tools/definitions.d.ts.map +1 -1
  65. package/dist/tools/definitions.js +404 -25
  66. package/dist/tools/definitions.js.map +1 -1
  67. package/dist/tools/engine-methods.d.ts +18 -0
  68. package/dist/tools/engine-methods.d.ts.map +1 -0
  69. package/dist/tools/engine-methods.js +241 -0
  70. package/dist/tools/engine-methods.js.map +1 -0
  71. package/dist/tools/handlers/character.d.ts +121 -0
  72. package/dist/tools/handlers/character.d.ts.map +1 -0
  73. package/dist/tools/handlers/character.js +315 -0
  74. package/dist/tools/handlers/character.js.map +1 -0
  75. package/dist/tools/handlers.d.ts +2 -0
  76. package/dist/tools/handlers.d.ts.map +1 -1
  77. package/dist/tools/handlers.js +522 -18
  78. package/dist/tools/handlers.js.map +1 -1
  79. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  80. package/dist/tools/minimal-descriptions.js +15 -3
  81. package/dist/tools/minimal-descriptions.js.map +1 -1
  82. package/dist/tools/toolkits.d.ts.map +1 -1
  83. package/dist/tools/toolkits.js +8 -0
  84. package/dist/tools/toolkits.js.map +1 -1
  85. package/dist/types/code-generator.d.ts +106 -1
  86. package/dist/types/code-generator.d.ts.map +1 -1
  87. package/dist/types/code-generator.js +1273 -18
  88. package/dist/types/code-generator.js.map +1 -1
  89. package/dist/types/schemas.d.ts +6805 -124
  90. package/dist/types/schemas.d.ts.map +1 -1
  91. package/dist/types/schemas.js +627 -17
  92. package/dist/types/schemas.js.map +1 -1
  93. package/dist/vendor/design/art-deco-generator.d.ts +52 -0
  94. package/dist/vendor/design/art-deco-generator.d.ts.map +1 -0
  95. package/dist/vendor/design/art-deco-generator.js +144 -0
  96. package/dist/vendor/design/art-deco-generator.js.map +1 -0
  97. package/dist/vendor/design/art-nouveau-generator.d.ts +46 -0
  98. package/dist/vendor/design/art-nouveau-generator.d.ts.map +1 -0
  99. package/dist/vendor/design/art-nouveau-generator.js +134 -0
  100. package/dist/vendor/design/art-nouveau-generator.js.map +1 -0
  101. package/dist/vendor/design/bauhaus-generator.d.ts +53 -0
  102. package/dist/vendor/design/bauhaus-generator.d.ts.map +1 -0
  103. package/dist/vendor/design/bauhaus-generator.js +126 -0
  104. package/dist/vendor/design/bauhaus-generator.js.map +1 -0
  105. package/dist/vendor/design/brutalist-generator.d.ts +53 -0
  106. package/dist/vendor/design/brutalist-generator.d.ts.map +1 -0
  107. package/dist/vendor/design/brutalist-generator.js +148 -0
  108. package/dist/vendor/design/brutalist-generator.js.map +1 -0
  109. package/dist/vendor/design/claymorphism-generator.d.ts +46 -0
  110. package/dist/vendor/design/claymorphism-generator.d.ts.map +1 -0
  111. package/dist/vendor/design/claymorphism-generator.js +135 -0
  112. package/dist/vendor/design/claymorphism-generator.js.map +1 -0
  113. package/dist/vendor/design/corporate-memphis-generator.d.ts +47 -0
  114. package/dist/vendor/design/corporate-memphis-generator.d.ts.map +1 -0
  115. package/dist/vendor/design/corporate-memphis-generator.js +144 -0
  116. package/dist/vendor/design/corporate-memphis-generator.js.map +1 -0
  117. package/dist/vendor/design/cyberpunk-generator.d.ts +52 -0
  118. package/dist/vendor/design/cyberpunk-generator.d.ts.map +1 -0
  119. package/dist/vendor/design/cyberpunk-generator.js +162 -0
  120. package/dist/vendor/design/cyberpunk-generator.js.map +1 -0
  121. package/dist/vendor/design/de-stijl-generator.d.ts +50 -0
  122. package/dist/vendor/design/de-stijl-generator.d.ts.map +1 -0
  123. package/dist/vendor/design/de-stijl-generator.js +154 -0
  124. package/dist/vendor/design/de-stijl-generator.js.map +1 -0
  125. package/dist/vendor/design/design-style.d.ts +3 -0
  126. package/dist/vendor/design/design-style.d.ts.map +1 -0
  127. package/dist/vendor/design/design-style.js +12 -0
  128. package/dist/vendor/design/design-style.js.map +1 -0
  129. package/dist/vendor/design/design-systems-dtcg.d.ts +59 -0
  130. package/dist/vendor/design/design-systems-dtcg.d.ts.map +1 -0
  131. package/dist/vendor/design/design-systems-dtcg.js +571 -0
  132. package/dist/vendor/design/design-systems-dtcg.js.map +1 -0
  133. package/dist/vendor/design/frutiger-aero-generator.d.ts +46 -0
  134. package/dist/vendor/design/frutiger-aero-generator.d.ts.map +1 -0
  135. package/dist/vendor/design/frutiger-aero-generator.js +133 -0
  136. package/dist/vendor/design/frutiger-aero-generator.js.map +1 -0
  137. package/dist/vendor/design/memphis-generator.d.ts +56 -0
  138. package/dist/vendor/design/memphis-generator.d.ts.map +1 -0
  139. package/dist/vendor/design/memphis-generator.js +158 -0
  140. package/dist/vendor/design/memphis-generator.js.map +1 -0
  141. package/dist/vendor/design/mid-century-upa-generator.d.ts +46 -0
  142. package/dist/vendor/design/mid-century-upa-generator.d.ts.map +1 -0
  143. package/dist/vendor/design/mid-century-upa-generator.js +137 -0
  144. package/dist/vendor/design/mid-century-upa-generator.js.map +1 -0
  145. package/dist/vendor/design/neumorphic-generator.d.ts +53 -0
  146. package/dist/vendor/design/neumorphic-generator.d.ts.map +1 -0
  147. package/dist/vendor/design/neumorphic-generator.js +182 -0
  148. package/dist/vendor/design/neumorphic-generator.js.map +1 -0
  149. package/dist/vendor/design/op-art-generator.d.ts +45 -0
  150. package/dist/vendor/design/op-art-generator.d.ts.map +1 -0
  151. package/dist/vendor/design/op-art-generator.js +116 -0
  152. package/dist/vendor/design/op-art-generator.js.map +1 -0
  153. package/dist/vendor/design/pixel-art-generator.d.ts +45 -0
  154. package/dist/vendor/design/pixel-art-generator.d.ts.map +1 -0
  155. package/dist/vendor/design/pixel-art-generator.js +143 -0
  156. package/dist/vendor/design/pixel-art-generator.js.map +1 -0
  157. package/dist/vendor/design/pop-art-generator.d.ts +53 -0
  158. package/dist/vendor/design/pop-art-generator.d.ts.map +1 -0
  159. package/dist/vendor/design/pop-art-generator.js +164 -0
  160. package/dist/vendor/design/pop-art-generator.js.map +1 -0
  161. package/dist/vendor/design/risograph-generator.d.ts +53 -0
  162. package/dist/vendor/design/risograph-generator.d.ts.map +1 -0
  163. package/dist/vendor/design/risograph-generator.js +170 -0
  164. package/dist/vendor/design/risograph-generator.js.map +1 -0
  165. package/dist/vendor/design/swiss-style-generator.d.ts +55 -0
  166. package/dist/vendor/design/swiss-style-generator.d.ts.map +1 -0
  167. package/dist/vendor/design/swiss-style-generator.js +147 -0
  168. package/dist/vendor/design/swiss-style-generator.js.map +1 -0
  169. package/dist/vendor/design/vaporwave-generator.d.ts +51 -0
  170. package/dist/vendor/design/vaporwave-generator.d.ts.map +1 -0
  171. package/dist/vendor/design/vaporwave-generator.js +139 -0
  172. package/dist/vendor/design/vaporwave-generator.js.map +1 -0
  173. package/manifest.json +33 -1
  174. package/package.json +13 -5
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { COMPACT_DESCRIPTIONS } from './compact-descriptions.js';
11
11
  import { MINIMAL_DESCRIPTIONS } from './minimal-descriptions.js';
12
- import { EffectTypeSchema, SimpleAnimationTypeSchema, GEOMETRY_OPERATIONS, } from '../types/schemas.js';
12
+ import { EffectTypeSchema, SimpleAnimationTypeSchema, GEOMETRY_OPERATIONS, THREAD_STITCHES, } from '../types/schemas.js';
13
13
  /**
14
14
  * Vocabulary lists derived from Zod schemas so tool descriptions stay in
15
15
  * sync automatically. The description-schema-parity test
@@ -26,6 +26,11 @@ export const AI_AGENT_GUIDE = `⚠️ You are connected to PinePaper Studio via
26
26
  NEVER create standalone HTML pages, React components, or web apps as a substitute. Do NOT use frontend design skills or CSS-only animations instead of PinePaper.
27
27
  PinePaper tools ARE the implementation — they generate Paper.js/JavaScript code that runs on the canvas and produce Canvas 2D, SVG, MP4, GIF, PNG exports.
28
28
  If a tool fails, report the error — do NOT fall back to generating HTML pages.
29
+
30
+ THE DOORS THAT ARE OPEN, because four lines of prohibition above this one will otherwise read as a ban on the capability rather than on the substitution:
31
+ - HTML IS a supported output. \`pinepaper_export_widget_html\` exports the scene as a self-contained HTML page, and \`pinepaper_export_widget\` as an embeddable widget. Asked for a web page or an embed, build the scene with tools and export it — that is the intended path, not a workaround.
32
+ - CODE IS a supported input. \`pinepaper_execute_custom_code\` runs Paper.js against the live canvas, and \`pinepaper_execute_generator\` runs a named generator. Reach for them when no tool fits; they are part of the surface, not an escape from it.
33
+ - The rule above is about SUBSTITUTION — writing a CSS animation instead of calling a tool. It is not a rule against the formats themselves, and a model that reads it as one will refuse work this server can do.
29
34
  The generated code is shown in tool responses and can be copied into PinePaper's Code Console.
30
35
 
31
36
  ⚠️ KNOWN CONFLICT: The "frontend-design" plugin instructs agents to generate HTML/CSS pages for visual content. This conflicts with PinePaper. If you keep generating HTML instead of calling PinePaper tools, the user should disable the frontend-design plugin.
@@ -169,7 +174,7 @@ Measurement: pinepaper_measurement — rulers, grid, snap-to-grid, get item dime
169
174
  ─── CANVAS & EXPORT ───
170
175
 
171
176
  Presets: instagram (1080x1080), youtube (1920x1080), tiktok (1080x1920), twitter (1200x675)
172
- Export: pinepaper_agent_export (SVG/PNG/GIF/MP4/WebM/PDF), pinepaper_export_svg
177
+ Export: pinepaper_agent_export (SVG/PNG/GIF/MP4/WebM/PDF, plus WAV for the soundtrack alone), pinepaper_export_svg
173
178
  Widget: pinepaper_export_widget (pp:PinePaper ontology JSON), pinepaper_export_widget_html (self-contained HTML with tree-shaken runtime)`;
174
179
  /*
175
180
  * The AI_AGENT_GUIDE constant above is included as a comment in verbose/compact modes
@@ -346,7 +351,9 @@ USE WHEN:
346
351
  IMPORTANT:
347
352
  - Save the returned itemId! You need it for pinepaper_add_relation, pinepaper_modify_item, pinepaper_delete_item, etc.
348
353
  - If there's a welcome template on the canvas, use pinepaper_clear_canvas first
349
- - Position defaults to canvas center (400, 300). Use position: {x, y} or [x, y] (both forms accepted) to place elsewhere.
354
+ - Position defaults to canvas center (400, 300). Use position: {x, y} or [x, y] (both forms accepted) to place elsewhere. Position is the item's bounding-box CENTRE, not its top-left corner.
355
+ - ROTATION is a property, at creation: properties: { rotation: 45 } — degrees, clockwise, about the item's own centre. It does not need a second call, and pinepaper_update_item takes the same property afterwards; keyframe_animate and the driven_by relation both animate it. A radial figure — a sunburst of thin triangles, a clock face, a compass rose — is N creates at rotation: i * (360/N), which is what pinepaper_agent_batch_execute is for. Nothing here needs to be faked with arcs or chevrons.
356
+ ⚠️ Rotation applies at create and in keyframes; it is BAKED INTO THE GEOMETRY, so reading item.rotation back returns 0. Verify by geometry or pixels, never by re-reading the property — a 12-ray sunburst has 12 visibly different rays that every report rotation 0, and checking the property is how you conclude a working feature does nothing.
350
357
  - DO NOT take screenshots after every create operation. Trust the API response - if it returns success, the item was created. Take ONE screenshot at the end to verify the final result.
351
358
 
352
359
  ⚠️ COMPLEX CHARACTERS/ILLUSTRATIONS:
@@ -372,6 +379,28 @@ ITEM TYPES:
372
379
  - diamond: 4-sided diamond/rhombus (properties: radius, color)
373
380
  - arrow: Right-pointing block arrow (properties: width, height, color)
374
381
  - heart: Heart shape (properties: color)
382
+ - disk: Filled disk — the region inside a circle (properties: radius, color)
383
+ - circle-outline: The circle CURVE — stroked, no interior (properties: radius,
384
+ strokeColor, strokeWidth)
385
+ - arrow-right: Alias for arrow — the same definition under the name the shape
386
+ grids use. Identical output; both are listed because both are accepted.
387
+
388
+ SPEECH AND ANNOTATION SHAPES — the engine draws these; use one instead of
389
+ hand-building an outline from a path. All take width, height and color, and the
390
+ descriptions below are the engine's own. The six with a tail also take
391
+ tailDirection (bottom-left | bottom-right | bottom | top-left | top-right | top
392
+ | left | right) and tailSize (0-1, default 0.35):
393
+ - speech-bubble: Classic round speech bubble with triangular tail.
394
+ - speech-bubble-square: Rectangular speech bubble with sharp corners and
395
+ pointed tail.
396
+ - speech-bubble-pointed: Tooltip-style bubble with arrow pointer (tail defaults
397
+ to "bottom").
398
+ - thought-bubble: Cloud-like thought bubble with trailing dots.
399
+ - comment-box: Modern chat message bubble with high corner radius.
400
+ - callout-box: Annotation callout with narrow arrow (tail defaults to "left").
401
+ - double-bubble: Two overlapping bubbles for conversation (properties:
402
+ secondaryColor).
403
+ - quote-bubble: Stylized bubble with quotation marks.
375
404
 
376
405
  PATH FOR CUSTOM SHAPES:
377
406
  Use itemType: "path" with either:
@@ -427,7 +456,7 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
427
456
  properties: {
428
457
  itemType: {
429
458
  type: 'string',
430
- enum: ['text', 'circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc', 'pentagon', 'hexagon', 'diamond', 'arrow', 'heart', 'shader', 'field'],
459
+ enum: ['text', 'circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc', 'pentagon', 'hexagon', 'diamond', 'arrow', 'heart', 'disk', 'circle-outline', 'arrow-right', 'speech-bubble', 'speech-bubble-square', 'speech-bubble-pointed', 'thought-bubble', 'double-bubble', 'quote-bubble', 'comment-box', 'callout-box', 'shader', 'field'],
431
460
  description: "Type of item to create. 'shader' and 'field' are RENDER-TIME SURFACES drawn per pixel by the cloud renderer — locally they stand in as a flat plate. See the shader/field properties below.",
432
461
  },
433
462
  position: {
@@ -460,7 +489,7 @@ Without these every item is on screen for the whole render, which is what makes
460
489
  a long piece read as one crowded frame instead of a sequence of shots.
461
490
 
462
491
  SHADER (itemType 'shader') — a lit surface, drawn per pixel:
463
- - shader: 'water' (open sea, lit swell) | 'liquid_metal' (chrome) | 'heatmap' (thermal halo) | 'gem_smoke' (smoke) | 'electric_arc' (branching discharge, stepped flicker) | 'vortex' (hollow bright RING, sheared by differential rotation) | 'rain_veil' | 'caustics'
492
+ - shader: 'water' (open sea, lit swell) | 'liquid_metal' (chrome) | 'heatmap' (thermal halo) | 'gem_smoke' (smoke) | 'electric_arc' (branching discharge, stepped flicker) | 'vortex' (hollow bright RING, sheared by differential rotation) | 'rain_veil' | 'caustics' | 'ink_bleed'
464
493
  - width, height: the quad it fills
465
494
  - shaderIntensity: 0..2, the effect's amplitude (water: chop)
466
495
  - shaderPalette: 0 day | 1 low sun | 2 night
@@ -1448,6 +1477,7 @@ EXAMPLES:
1448
1477
  y: { type: 'number', description: 'Canvas origin y (default: canvas center)' },
1449
1478
  flipY: { type: 'boolean', description: 'Math y-up → screen y-down (default true)' },
1450
1479
  style: { type: 'object', description: 'Path style forwarded to create (strokeColor, strokeWidth, fillColor, …)' },
1480
+ solveOde: { type: 'object', description: "Integrate an ODE instead of plotting a closed form, and return the SOLUTION rather than drawing it: { equations, initialState, tEnd?, dt?, method? ('rk4' default, or 'euler' — faster and visibly wrong on anything stiff) }. The trajectory comes back as data, to inspect, feed to a path, or drive keyframes with." },
1451
1481
  warp: {
1452
1482
  anyOf: [{ type: 'object' }, { type: 'array', items: { type: 'object' } }],
1453
1483
  description: 'Chained parametric warp(s): { dx, dy } expressions of x, y, t — one object or an array',
@@ -1900,7 +1930,11 @@ EXAMPLE: { action: 'apply_style', itemId: 'title_1', styleKey: 'arcade', fontFam
1900
1930
  inputSchema: {
1901
1931
  type: 'object',
1902
1932
  properties: {
1903
- action: { type: 'string', enum: ['apply_style', 'set_font_axes', 'list_styles'], description: 'Text style operation' },
1933
+ action: { type: 'string', enum: ['apply_style', 'set_font_axes', 'list_styles', 'cursive', 'wrap', 'unwrap', 'to_collage'], description: "Text style operation. '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." },
1934
+ text: { type: 'string', description: 'cursive: the words to write. to_collage: the text to build from, if it differs from the item.' },
1935
+ maxWidth: { type: 'number', description: 'wrap: the width to break at, in canvas units. unwrap restores the original, so this is not destructive.' },
1936
+ cursiveOptions: { type: 'object', description: 'cursive: { x?, y?, scale?, strokeColor?, strokeWidth? }.' },
1937
+ collageOptions: { type: 'object', description: 'to_collage: style, palette and the rest — same vocabulary as pinepaper_create_letter_collage.' },
1904
1938
  itemId: { type: 'string', description: 'Text item id — apply_style / set_font_axes.' },
1905
1939
  styleKey: { type: 'string', description: 'apply_style: style name from list_styles.' },
1906
1940
  palette: {
@@ -1970,9 +2004,9 @@ EXAMPLE — a stitched leaf:
1970
2004
  type: 'object',
1971
2005
  properties: {
1972
2006
  action: { type: 'string', enum: ['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options'], description: 'Medium operation' },
1973
- medium: { type: 'string', description: 'resolve: vector | thread | ink | cutPaper | charcoal | oil | encaustic' },
2007
+ medium: { type: 'string', description: 'resolve: vector | thread | ink | watercolor | hatch | cutPaper | charcoal | oil | encaustic. Six are native (vector, thread, ink, watercolor, hatch, cutPaper), two stylised, one absent — call list_media for the live table rather than trusting this line.' },
1974
2008
  itemId: { type: 'string', description: 'apply_thread / apply_hatch: a closed path, compound path, or a group of them.' },
1975
- stitch: { type: 'string', enum: ['longAndShort', 'satin', 'seed', 'stem'], description: 'apply_thread: default longAndShort.' },
2009
+ 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." },
1976
2010
  field: { type: 'object', description: "apply_thread: { kind: 'radial'|'spine'|'constant', cx, cy, angle, spine[], across } — default radial from the shape centre." },
1977
2011
  stitchLen: { type: 'number', description: 'apply_thread: nominal stitch length px (default 18).' },
1978
2012
  rowGap: { type: 'number', description: 'apply_thread: row spacing px; defaults from thread width so rows abut.' },
@@ -1982,6 +2016,12 @@ EXAMPLE — a stitched leaf:
1982
2016
  color: { type: 'string', description: "apply_thread: thread colour (defaults to the item's fill)." },
1983
2017
  seed: { type: 'number', description: 'apply_thread: PRNG seed (default 1) — same seed stitches the same way.' },
1984
2018
  count: { type: 'number', description: 'apply_thread: stitch count for seed/satin.' },
2019
+ slant: { type: 'number', description: 'apply_thread: stitch slant in DEGREES — satin (default 0) and stem (default 22).' },
2020
+ overlap: { type: 'number', description: 'apply_thread: how far consecutive stem stitches overlap (default 0.4) — what makes a stem outline read as rope.' },
2021
+ stagger: { type: 'number', description: 'apply_thread: offsets alternate longAndShort rows (default 0.5) — stops stitch ends lining up into seams.' },
2022
+ gapLen: { type: 'number', description: 'apply_thread: gap between runningSeam stitches, px (default 4) — with stitchLen, the dash rhythm of a machine seam.' },
2023
+ gridSize: { type: 'number', description: 'apply_thread: crossStitch grid pitch, px (default 12) — the size of each X, and so the resolution of the fill.' },
2024
+ roughness: { type: 'number', description: 'apply_thread: hand wobble. Jitters every point of every stitch after the engine lays them, so the fill reads as sewn rather than plotted. 0 is machine-exact; reproducible for a given seed. Applied by the tool, not the engine.' },
1985
2025
  distance: { type: 'number', description: 'apply_hatch: line spacing px (default 6). THE VALUE CONTROL — 6 vs 3 is light vs dark.' },
1986
2026
  angle: { type: 'number', description: 'apply_hatch: ruling angle in DEGREES (default 45).' },
1987
2027
  gradient: { type: 'number', description: 'apply_hatch: 0..1 spacing growth per line (default 0, even) — a shaded ramp rather than a flat tone.' },
@@ -2214,6 +2254,233 @@ RECIPE — a playable maze: create_tilemap with wall fills → batch_create colo
2214
2254
  required: ['action'],
2215
2255
  },
2216
2256
  },
2257
+ {
2258
+ name: 'pinepaper_design_system',
2259
+ description: `The design vocabulary, as data you can read — and eighteen styles that can build a scene.
2260
+
2261
+ TWO DIFFERENT THINGS, kept apart on purpose:
2262
+
2263
+ A DESIGN SYSTEM is a real, licensed, versioned product — Material Design 3, IBM Carbon, Shopify Polaris, Fluent 2, Adobe Spectrum, GitHub Primer, USWDS, GOV.UK, Shadcn/Radix, SLDS 2, Tailwind, Chakra and more. Its tokens are facts with a licence attached, in the W3C DTCG format ($value, $type, $description). Twenty-one of them are here. Where a motion curve had to be filled in because the published system does not specify one, the token is marked "pinepaper:authored" — so you can tell a vendor's curve from ours instead of treating both as equally official.
2264
+
2265
+ An AESTHETIC STYLE is a look: Bauhaus, Art Deco, Swiss typographic, De Stijl, vaporwave, risograph, brutalist, Memphis, claymorphism, op art, pixel art. No licence, no vendor. Thirty-one are named; eighteen can COMPOSE a scene rather than only be described, and 'list_styles' says which.
2266
+
2267
+ - list_systems / get_system: the licensed systems and their tokens, filterable by token type.
2268
+ - list_easings: every motion curve across every system as a named easing, with its licence and whether it was authored here. Use these to make a motion match a system it has to sit beside.
2269
+ - list_motion: those curves AND the duration scale sitting beside them — the other half of a motion token, published nowhere until now. A curve is a shape; a motion is a shape over a length, and matching a system's easing while inventing its timing gets only the easy half right. Durations come back in ms and in seconds, because every animation tool here takes seconds. These are the licensed SYSTEMS' motion scales: an aesthetic style carries no upstream motion data, so pair one of these with a style rather than expecting the style to name its own.
2270
+ - list_styles: every style with its TOKENS — palette, background, font stacks, type sizes and any variants — derived by composing it, so they are what the generator will actually draw. Read these to hand-build in a style rather than reverse-engineering its colours.
2271
+ - compose: build a titled scene in a style. Layout is decided in this server by a pure function, so 'compose' with draw:false returns the scene and the ops as DATA and draws nothing — read it before committing, the same way pinepaper_story's distill works. Element boxes are converted to the centre anchor app.create wants, and text carries its alignment, so a left-aligned headline starts at its x instead of centring on it.
2272
+
2273
+ A partial composition reports as a failure with the elements that did not create, rather than as a success with half a poster.`,
2274
+ inputSchema: {
2275
+ type: 'object',
2276
+ properties: {
2277
+ action: { type: 'string', enum: ['list_systems', 'get_system', 'list_easings', 'list_motion', 'list_styles', 'compose'], description: 'Which design-system operation.' },
2278
+ systemId: { type: 'string', description: "get_system: e.g. 'material_3', 'ibm_carbon', 'uswds'. Call list_systems for ids." },
2279
+ tokenType: { type: 'string', enum: ['dimension', 'duration', 'cubicBezier', 'color', 'fontFamily', 'fontWeight', 'number', 'shadow', 'grid'], description: 'get_system: return only tokens of this DTCG type.' },
2280
+ style: { type: 'string', description: "compose: the aesthetic style, e.g. 'bauhaus_geometric'. Call list_styles for the composable ones." },
2281
+ title: { type: 'string', description: 'compose: the headline most styles build around.' },
2282
+ subtitle: { type: 'string', description: 'compose: the supporting line.' },
2283
+ body: { type: 'string', description: 'compose: body copy, where the style has room for it.' },
2284
+ width: { type: 'number', description: 'compose: canvas width (the style picks its own default otherwise).' },
2285
+ height: { type: 'number', description: 'compose: canvas height.' },
2286
+ variant: { type: 'string', description: "compose: a style-specific variant, e.g. art deco's 'emerald'." },
2287
+ draw: { type: 'boolean', description: 'compose: draw it (default true). false returns the scene and ops as data and draws nothing.' },
2288
+ authoredOnly: { type: 'boolean', description: 'list_easings: true for only the curves authored here, false for only what vendors publish.' },
2289
+ },
2290
+ required: ['action'],
2291
+ },
2292
+ },
2293
+ {
2294
+ name: 'pinepaper_stick',
2295
+ description: `The vendored stick-figure kit — a rigged figure posed, walking, travelling, holding a prop, with a garment and hair, plus the set it stands on and among.
2296
+
2297
+ A different construction from pinepaper_character, which places a figure from the DESIGN GRAPH by concept ("pp:Pigeon"). This is the stick kit specifically, and its options — pose, walk, travel, prop, garment, trouser, hair, expression and a timeline of expressions — were discoverable only by reading the engine's source.
2298
+
2299
+ action 'figure' builds the figure. action 'set' builds the floor, the wall and the objects around it, so a figure has somewhere to be.
2300
+
2301
+ 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.`,
2302
+ inputSchema: {
2303
+ type: 'object',
2304
+ properties: {
2305
+ action: { type: 'string', enum: ['figure', 'set'], description: "'figure' or 'set'." },
2306
+ id: { type: 'string', description: 'Prefix for the created items.' },
2307
+ kind: { type: 'string', description: 'figure: which figure. set: which piece of the kit.' },
2308
+ at: { type: 'object', description: '{ x?, y? } — where it goes.' },
2309
+ scale: { type: 'number', description: 'Size multiplier.' },
2310
+ facing: { type: 'string', enum: ['left', 'right'], description: 'Which way the figure looks.' },
2311
+ pose: { type: 'string', description: "figure: a named pose from the kit's POSES." },
2312
+ poseAt: { type: 'number', description: 'figure: seconds at which that pose is struck.' },
2313
+ expression: { type: 'string', description: 'figure: one facial expression.' },
2314
+ expressions: { type: 'array', items: { type: 'object' }, description: 'figure: [{ at, name }] — expressions over time.' },
2315
+ walk: { anyOf: [{ type: 'boolean' }, { type: 'object' }], description: 'figure: a walk cycle.' },
2316
+ travel: { type: 'object', description: 'figure: move across the scene while walking, rather than walking in place.' },
2317
+ prop: { type: 'string', description: 'figure: something held in hand.' },
2318
+ propSide: { type: 'string', enum: ['left', 'right'], description: 'figure: which hand.' },
2319
+ garment: { type: 'string', description: 'figure: clothing.' },
2320
+ trouser: { type: 'string', description: 'figure: legwear.' },
2321
+ hair: { type: 'string', description: 'figure: hair style.' },
2322
+ withHair: { type: 'boolean', description: 'figure: draw hair at all.' },
2323
+ groundY: { type: 'number', description: 'The y the figure stands on.' },
2324
+ surfaceY: { type: 'number', description: 'The y a set object sits on.' },
2325
+ durationSeconds: { type: 'number', description: 'figure: length of the performance.' },
2326
+ object: { type: 'object', description: 'set: the object spec.' },
2327
+ items: { type: 'array', items: { type: 'object' }, description: 'set: several pieces at once.' },
2328
+ floor: { anyOf: [{ type: 'boolean' }, { type: 'object' }], description: 'set: draw a floor.' },
2329
+ wall: { anyOf: [{ type: 'boolean' }, { type: 'object' }], description: 'set: draw a wall.' },
2330
+ },
2331
+ required: ['action'],
2332
+ },
2333
+ },
2334
+ {
2335
+ name: 'pinepaper_story',
2336
+ description: `A piece of prose becomes a scene.
2337
+
2338
+ - distill: reduce an article to its story beats and draw NOTHING. This is the half worth having on its own — a caller can read what the distiller made of the text, edit the beats, and only then assemble. Discovering the reading by looking at a finished scene is the expensive way round.
2339
+ - from_text: distill and assemble in one call, when the reading does not need checking.
2340
+ - apply_spec: assemble a spec you already have, usually a distilled one you edited.
2341
+ - plan_book: lay images out as pages.`,
2342
+ inputSchema: {
2343
+ type: 'object',
2344
+ properties: {
2345
+ action: { type: 'string', enum: ['distill', 'from_text', 'apply_spec', 'plan_book'], description: 'Which story operation.' },
2346
+ text: { type: 'string', description: 'distill / from_text: the prose.' },
2347
+ spec: { type: 'object', description: 'apply_spec: the story spec, usually a distilled one you edited.' },
2348
+ images: { type: 'array', items: { anyOf: [{ type: 'string' }, { type: 'object' }] }, description: 'plan_book: images as ids or specs.' },
2349
+ options: { type: 'object', description: 'Assembly or layout options.' },
2350
+ },
2351
+ required: ['action'],
2352
+ },
2353
+ },
2354
+ {
2355
+ name: 'pinepaper_interchange',
2356
+ description: `The formats other tools read and write — Lottie, dotLottie, GLB, BVH, PNG sequence.
2357
+
2358
+ 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.
2359
+
2360
+ 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.`,
2361
+ inputSchema: {
2362
+ type: 'object',
2363
+ properties: {
2364
+ action: { type: 'string', enum: ['export_lottie', 'export_dotlottie', 'import_lottie', 'export_glb', 'export_bvh', 'export_png_sequence'], description: 'Which interchange operation.' },
2365
+ options: { type: 'object', description: 'Format options passed through to the exporter: frame range, dimensions, fps, naming.' },
2366
+ data: { anyOf: [{ type: 'string' }, { type: 'object' }], description: 'import_lottie: the Lottie JSON — object, JSON string, or URL.' },
2367
+ skeletonId: { type: 'string', description: "export_bvh: which skeleton to write. A BVH is one skeleton's motion." },
2368
+ },
2369
+ required: ['action'],
2370
+ },
2371
+ },
2372
+ {
2373
+ name: 'pinepaper_sound',
2374
+ description: `Synthesis — and the drawing that is the same object.
2375
+
2376
+ The engine carries a full audio graph, and none of it was reachable. pinepaper_audio_beats ANALYSES audio that already exists; this MAKES audio.
2377
+
2378
+ - Catalogues: list_instruments, list_percussion, list_sfx. Call one before naming a drum or effect — the names are the engine's.
2379
+ - Play: play_tone ('A4'), play_chord (root + kind: major, minor, maj7, dim…), play_percussion, play_sfx (beep, pop, wind, whoosh, zap), play_spec, play_from_text.
2380
+ - Read without playing: chord_frequencies (the Hz a chord resolves to), from_text (a plain-language description — "a soft warm bell on A4" — resolved to a spec you can inspect or edit before committing), timbre_from_path.
2381
+
2382
+ THE PART WORTH KNOWING: sound and drawing are the same object here, both ways.
2383
+ - create draws a sound AS a waveform path on the canvas. Edit the path and the timbre changes with it — it is not a picture of the sound, it is the sound.
2384
+ - timbre_from_path reads ANY drawn path back as harmonic content. A curve someone drew by hand becomes the partials of a note.
2385
+
2386
+ A path with no length cannot be a timbre, and the engine answers a flat single-partial timbre for anything that is not one — a real answer and a refusal look identical — so the item is checked before the call and a non-path is refused by name.
2387
+
2388
+ THE CATALOGUE IS A STARTING SET, NOT THE LIMIT. Six instruments, five drums and six effects are what the engine ships with; that is not a claim about how many instruments exist. define_instrument, define_percussion and define_sfx register a new one at runtime — a rhodes, a koto, a taiko, a specific synth patch — and it is first-class from then on: it appears in the list_* catalogues and plays by name like any built-in.
2389
+ - A melodic instrument needs a partial table, because one with no partials renders silence. Percussion and SFX may be pure noise instead, because a hat and a whoosh are.
2390
+ - For a drum, pass pitch: { from, tau }. The glide is what makes a kick a kick; without it you get a beep.
2391
+ - Names are normalised to [a-z][a-z0-9_-]. Register 'Rhodes' and the response returns canonicalName 'rhodes' — play THAT.
2392
+
2393
+ 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.`,
2394
+ inputSchema: {
2395
+ type: 'object',
2396
+ properties: {
2397
+ action: { type: 'string', enum: ['list_instruments', 'list_percussion', 'list_sfx', 'play_tone', 'play_chord', 'chord_frequencies', 'play_percussion', 'play_sfx', 'play_spec', 'from_text', 'play_from_text', 'create', 'timbre_from_path', 'set_placement', 'remove', 'stop_all', 'define_instrument', 'define_percussion', 'define_sfx', 'render_soundtrack'], description: 'Which sound operation.' },
2398
+ partials: { type: 'array', items: { type: 'object' }, description: 'define_*: [{ h, amp }] — the harmonic table that IS the timbre. Required for define_instrument.' },
2399
+ envelope: { type: 'object', description: 'define_*: { attack, decay, sustain, release } — seconds, except sustain which is a level 0-1.' },
2400
+ gain: { type: 'number', description: 'define_*: output level 0-1 (0.8 instrument, 0.85 percussion, 0.65 sfx).' },
2401
+ aliases: { type: 'array', items: { type: 'string' }, description: "define_*: extra names for the same sound. An alias never shadows a real catalogue entry." },
2402
+ noise: { type: 'number', description: 'define_percussion / define_sfx: noise content 0-1. A hat and a clap are pure noise, so these kinds take partials OR noise.' },
2403
+ noiseFreq: { type: 'number', description: 'define_percussion / define_sfx: noise band centre in Hz.' },
2404
+ hz: { type: 'number', description: 'define_percussion / define_sfx: base frequency in Hz.' },
2405
+ pitch: { type: 'object', description: 'define_percussion / define_sfx: { from, tau } pitch glide. The glide IS the drum — omit it and a kick comes out a beep.' },
2406
+ sampleRate: { type: 'number', description: 'render_soundtrack: samples per second (default 48000).' },
2407
+ bitDepth: { type: 'number', enum: [16, 32], description: 'render_soundtrack: 16 (default) or 32-bit float.' },
2408
+ note: { type: 'string', description: "play_tone: scientific pitch, e.g. 'A4'." },
2409
+ root: { type: 'string', description: "play_chord / chord_frequencies: root note, e.g. 'C4'." },
2410
+ chord: { type: 'string', description: "play_chord / chord_frequencies: chord kind (default 'major')." },
2411
+ name: { type: 'string', description: 'play_percussion / play_sfx: the named drum or effect — list them first.' },
2412
+ text: { type: 'string', description: "from_text / play_from_text: a plain-language description. 'from_text' resolves WITHOUT playing." },
2413
+ spec: { type: 'object', description: 'play_spec / create: the sound spec, usually one from from_text or a catalogue.' },
2414
+ options: { type: 'object', description: 'Playback options for the play_* actions.' },
2415
+ itemId: { type: 'string', description: 'timbre_from_path: the path whose shape becomes the timbre. set_placement / remove: the sound item.' },
2416
+ samples: { type: 'number', description: 'timbre_from_path: points sampled along the path (default 256).' },
2417
+ placement: { type: 'object', description: 'set_placement: where and when the sound sits on the timeline.' },
2418
+ visual: { type: 'object', description: 'create: { width?, height?, position?, color? } for the waveform path.' },
2419
+ },
2420
+ required: ['action'],
2421
+ },
2422
+ },
2423
+ {
2424
+ name: 'pinepaper_motion',
2425
+ 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.
2426
+
2427
+ Two kinds, and the second is the one nothing else here can do:
2428
+ - GROUP motions (drift, sway, rotate, pulse, wave, bounce) move the target as one.
2429
+ - 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.
2430
+
2431
+ 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.
2432
+
2433
+ Origins apply to field motions: center, the four corners, the four edges, 'random' (every element on its own phase — a twinkle rather than a wave), and 'roam' (hopping between the corners and the centre). Pass a seed to make 'random' reproducible.
2434
+
2435
+ Waveform 'spike' is a narrow bump. It was called 'pulse' until the engine noticed that one word was naming both a waveform and an animation; saved scenes still carry the old spelling and it is still accepted, but 'spike' is the name.
2436
+
2437
+ This is not pinepaper_animate: that applies a loop preset to ONE item. This moves a group, or moves a wave through one.`,
2438
+ inputSchema: {
2439
+ type: 'object',
2440
+ properties: {
2441
+ action: { type: 'string', enum: ['list', 'apply'], description: "'list' the catalogue · 'apply' a motion." },
2442
+ itemId: { type: 'string', description: 'apply: a group or single item.' },
2443
+ itemIds: { type: 'array', items: { type: 'string' }, description: 'apply: several items, wrapped in a new group that hosts the motion. Members keep their own ids.' },
2444
+ motion: { type: 'string', description: "apply: the motion name. Call 'list' for the set the engine accepts." },
2445
+ speed: { type: 'number', description: 'apply: roughly cycles per second.' },
2446
+ intensity: { type: 'number', description: 'apply: amplitude (0.15 is the usual default elsewhere).' },
2447
+ waveform: { type: 'string', enum: ['sine', 'triangle', 'square', 'sawtooth', 'spike'], description: 'apply: the shape of one cycle.' },
2448
+ origin: { type: 'string', enum: ['center', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'left', 'right', 'top', 'bottom', 'random', 'roam'], description: 'apply, field motions only: where the crest sweeps from.' },
2449
+ seed: { type: 'number', description: "apply: makes a 'random' origin reproducible." },
2450
+ },
2451
+ required: ['action'],
2452
+ },
2453
+ },
2454
+ {
2455
+ name: 'pinepaper_path',
2456
+ description: `Destructive path operations — the ones that REPLACE or ADD items rather than restyling them.
2457
+
2458
+ - boolean: unite | subtract | intersect | exclude | divide. CONSUMES its operands and leaves one result, so the input ids stop existing. The first id is the base; the rest apply to it in order.
2459
+ - simplify: fewer segments, same shape. Higher tolerance is fewer points and a looser curve.
2460
+ - outline_stroke: a stroked line becomes a FILLED SHAPE of the same width — the thing you do before exporting to a format with no stroke model, or before a boolean against a line.
2461
+ - toggle_closed: open a closed path or close an open one.
2462
+ - pattern: repeat an item — concentric rings, a radial ring, a grid, or a depth-wise extrude stack.
2463
+ - get_geometry: read the segments back (anchor and handle coordinates).
2464
+ - set_locked / unlock_all: a locked item is also unselectable and undraggable, so it cannot be grabbed by accident.
2465
+
2466
+ These live apart from pinepaper_modify_item on purpose: modify restyles an item and keeps its id, every action here changes what items EXIST. A boolean's operands are gone afterwards, and their relations and keyframes go with them.
2467
+
2468
+ The engine names each refusal — "a boolean needs at least two paths", "that path has no stroke to outline", "simplify needs a path" — and those come back verbatim rather than being reworded here.`,
2469
+ inputSchema: {
2470
+ type: 'object',
2471
+ properties: {
2472
+ action: { type: 'string', enum: ['boolean', 'simplify', 'outline_stroke', 'toggle_closed', 'pattern', 'get_geometry', 'set_locked', 'unlock_all'], description: 'Which path operation.' },
2473
+ itemId: { type: 'string', description: 'The item to operate on — every action except boolean and unlock_all.' },
2474
+ itemIds: { type: 'array', items: { type: 'string' }, description: 'boolean: at least two path ids. The first is the base.' },
2475
+ op: { type: 'string', enum: ['unite', 'subtract', 'intersect', 'exclude', 'divide'], description: 'boolean: which operation (default unite).' },
2476
+ tolerance: { type: 'number', description: 'simplify: how far the new curve may stray, in canvas units (default 2.5). outline_stroke: flattening tolerance.' },
2477
+ kind: { type: 'string', enum: ['concentric', 'radial', 'grid', 'extrude'], description: 'pattern: which repeat (default radial).' },
2478
+ pattern: { type: 'object', description: 'pattern options: { count?, radius?, angle?, phase?, rows?, cols?, gapX?, gapY?, step?, depth?, rotateItems?, shade?, shadeStrength? }. angle and phase are DEGREES. Copies are capped — past a few hundred this is a generator, not a drawing aid.' },
2479
+ locked: { type: 'boolean', description: 'set_locked: true locks, false unlocks (default true).' },
2480
+ },
2481
+ required: ['action'],
2482
+ },
2483
+ },
2217
2484
  {
2218
2485
  name: 'pinepaper_world3d',
2219
2486
  annotations: {
@@ -2239,7 +2506,7 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2239
2506
  inputSchema: {
2240
2507
  type: 'object',
2241
2508
  properties: {
2242
- action: { type: 'string', enum: ['create', 'describe', 'configure', 'add_actor', 'remove_actor', 'list_actors', 'set_actor_pose', 'set_camera', 'add_object', 'remove_object', 'remove_world'], description: 'World operation' },
2509
+ action: { type: 'string', enum: ['create', 'describe', 'configure', 'add_actor', 'remove_actor', 'list_actors', 'set_actor_pose', 'set_camera', 'add_object', 'remove_object', 'remove_world', 'extrude_path', 'lathe_path', 'list_meshes', 'remove_mesh', 'add_light', 'set_light', 'remove_light', 'list_lights', 'add_material', 'set_material', 'remove_material', 'list_materials', 'import_obj', 'import_gltf', 'list_mesh_clips', 'set_mesh_clip', 'set_nav_target', 'get_nav_target', 'ground_height', 'raycast', 'dolly_camera', 'pan_camera', 'world_to_canvas', 'canvas_to_ground', 'add_body', 'remove_body', 'list_bodies', 'step_physics', 'impulse', 'set_velocity', 'fire_projectile', 'line_of_sight', 'set_mesh_instances'], description: 'World operation. extrude_path/lathe_path turn a canvas path into real geometry; lights are capped at 8; a material is shared by every object referencing it.' },
2243
2510
  spec: {
2244
2511
  anyOf: [{ type: 'string' }, { type: 'object' }],
2245
2512
  description: "create: preset id ('forest'|'snowMountain'|'field'|'jungle') or a full spec object.",
@@ -2257,7 +2524,32 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2257
2524
  live: { type: 'boolean', description: 'add_actor: re-rasterize as the item animates.' },
2258
2525
  pose: { type: 'object', description: 'set_actor_pose: { x?, z?, angle? }.' },
2259
2526
  camera: { type: 'object', description: "set_camera: { mode: follow|fixed|orbit, target?, radius?, speed?, eye?, lookAt? }." },
2260
- object: { type: 'object', description: 'add_object: { x, z, height?, color?, y? }.' },
2527
+ object: { type: 'object', description: 'add_object: { x, z, height?, color?, y? }. metalness/roughness are accepted and IGNORED here — a plain object is drawn by the prop shader, which declares no such uniform. They render on the mesh path only.' },
2528
+ pathId: { type: 'string', description: 'extrude_path / lathe_path: the canvas path to turn into geometry. The path itself is unchanged.' },
2529
+ mesh: { type: 'object', description: 'extrude_path / lathe_path options: { id?, depth?, unitsPerPixel?, caps?, segments?, arc?, flatness?, x?, y?, z?, rotY?, rotYDegrees?, scale?, doubleSided? }. arc is DEGREES; rotY is RADIANS (fed straight to cos/sin in the shader) — pass rotYDegrees to write degrees. These names are the ones the engine records as the mesh provenance, so a captured meshProvenance.opts spreads straight back in.' },
2530
+ meshId: { type: 'string', description: 'remove_mesh: the mesh id.' },
2531
+ light: { type: 'object', description: 'add_light / set_light: { id?, x?, y?, z?, color?, intensity?, range? }. At most 8 point lights; the ninth is refused by name because the shader array is fixed-size.' },
2532
+ lightId: { type: 'string', description: 'set_light / remove_light: the light id.' },
2533
+ material: { type: 'object', description: 'add_material / set_material: { 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.' },
2534
+ materialId: { type: 'string', description: 'set_material / remove_material: the material id.' },
2535
+ source: { type: 'string', description: 'import_obj: the OBJ file TEXT. import_gltf: a URL or a data: URI of the .glb bytes.' },
2536
+ importOptions: { type: 'object', description: 'import_obj / import_gltf: placement and scale options, passed through.' },
2537
+ clip: { type: 'string', description: 'set_mesh_clip: which imported animation clip a skinned mesh plays.' },
2538
+ crossfade: { type: 'number', description: 'set_mesh_clip: seconds to blend out of the previous clip (blended in LOCAL pose space).' },
2539
+ navTarget: { type: 'string', enum: ['2d', '3d'], description: "set_nav_target: which layer the pointer drives." },
2540
+ point: { type: 'object', description: 'ground_height / canvas_to_ground / world_to_canvas: { x, y, z? }.' },
2541
+ origin: { type: 'array', items: { type: 'number' }, description: 'raycast: ray origin [x, y, z].' },
2542
+ direction: { type: 'array', items: { type: 'number' }, description: 'raycast: ray direction [x, y, z], need not be normalised.' },
2543
+ multiplier: { type: 'number', description: 'dolly_camera: >1 away, <1 closer.' },
2544
+ dx: { type: 'number', description: 'pan_camera: horizontal pan in SCREEN pixels.' },
2545
+ dy: { type: 'number', description: 'pan_camera: vertical pan in SCREEN pixels.' },
2546
+ body: { type: 'object', description: 'add_body / fire_projectile: the body spec. NARROW by design — a sphere or capsule under gravity colliding with the world. No stacking solver, no joints; pinepaper_physics is the 2D world and has those.' },
2547
+ bodyId: { type: 'string', description: 'remove_body / impulse / set_velocity: the body id.' },
2548
+ vector: { type: 'array', items: { type: 'number' }, description: 'impulse / set_velocity: [x, y, z].' },
2549
+ dt: { type: 'number', description: 'step_physics: seconds to advance. Returns contacts made and bodies removed.' },
2550
+ from: { type: 'array', items: { type: 'number' }, description: 'line_of_sight: eye point [x, y, z].' },
2551
+ to: { type: 'array', items: { type: 'number' }, description: 'line_of_sight: target point [x, y, z].' },
2552
+ instances: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: 'set_mesh_instances: one entry per copy, six numbers — x, y, z, scale, rotY (RADIANS), variant.' },
2261
2553
  objectId: { type: 'string', description: 'remove_object: object id.' },
2262
2554
  },
2263
2555
  required: ['action'],
@@ -2306,6 +2598,10 @@ NOT LIP SYNC. \`say\` opens and closes a mouth for the length of a line. Matchin
2306
2598
  description: 'Where the figure stands.',
2307
2599
  },
2308
2600
  height: { type: 'number', description: 'Drawn height in pixels. Every amplitude in the graph is a fraction, so the performance scales with it.' },
2601
+ position: { type: 'object', description: 'Where the figure stands — a synonym for `at`, because the rest of this surface says position. `at` wins if both are sent.' },
2602
+ scale: { type: 'number', description: 'Size as a multiplier of the 300px default, for callers who think in scale rather than pixels. `height` wins if both are sent.' },
2603
+ palette: { type: 'object', description: "Override the depiction's own colours: part id → colour. Only the parts you name change." },
2604
+ ink: { type: 'string', description: 'Ink colour for the stroked copies that make the figure read as drawn rather than filled. Overrides the style\'s own.' },
2309
2605
  id: { type: 'string', description: 'Prefix for the created items (default: the concept name).' },
2310
2606
  style: { type: 'string', description: 'Style id for the depiction (default: the concept\'s own).' },
2311
2607
  variant: { type: 'string', description: 'Depiction variant (default: "default").' },
@@ -2608,7 +2904,7 @@ GEOMETRIC CONSTRUCTION CONSTRAINTS (Layer 2) — the source item is RE-DERIVED e
2608
2904
 
2609
2905
  PROCEDURAL / DETERMINISTIC BINDING (Expression IR — S10 G1) — bind a property to another property or to a math function of time. Pass params.signal:true to route through the pure, seed/frame-deterministic signal interpreter (replay-stable — same time → identical output); omit it for the per-frame compute fallback.
2610
2906
  - driven_by: source's sourceProperty = target's targetProperty * multiplier + offset (optionally clamped). sourceProperty ∈ x|y|rotation|opacity|scale|strokeWidth|fillColor|strokeColor; targetProperty ∈ x|y|rotation|opacity|scale. For fillColor/strokeColor the (clamped 0..1) driven value interpolates colorFrom→colorTo — so a relation drives COLOR. NEEDS a target (the property source). params: sourceProperty, targetProperty, multiplier, offset, clamp {min,max}, colorFrom, colorTo, signal. e.g. tint a dot by a planet's x: sourceProperty 'fillColor', targetProperty 'x', colorFrom '#0055ff', colorTo '#ff3300', signal true.
2611
- - time_expression: self-relation (targetId=null); the source's params.property is driven by a math expression of t (scene time) and v (params.baseValue). params: property (default 'y'), expression (e.g. 'sin(t*2)*50 + v'), baseValue, signal. With signal:true the expression is parsed into the Expression IR and evaluated as a pure f(t); it falls back to per-frame if it uses random() or an unknown symbol (non-deterministic).
2907
+ - time_expression: self-relation (targetId=null); the source's params.property is driven by a math expression of t (scene time) and v (params.baseValue). params: property (default 'y'), expression (e.g. 'sin(t*2)*50 + v'), baseValue, signal. Both modes parse the SAME grammar — the caret is exponentiation (not XOR), and random() is rejected by name rather than falling back, because a scene that renders differently twice cannot be baked or exported. signal:true additionally evaluates it as a pure f(t) via the Expression IR. Beyond the usual trig/log/pow set: clamp, mod, fract, pingpong, inverseLerp, remap, noise, sec, csc, cot, the six ease in/out quad and cubic curves, and the constants PI, E and TAU.
2612
2908
 
2613
2909
  EVENT-DRIVEN SCENE CHAINS (S11) — build a LONG scene as a graph of timed events, not a keyframe track. First make events with pinepaper_event (action:'create' → eventId). The relations below have an EVENT as their sourceId; firing the event runs the reaction. Chain beats over animation time with on_event_fire_after (timeline:'canvas'), have beats mutate the scene with on_event_add_relation, then kick it off with pinepaper_event action:'pulse'. Because canvas-timed fires are on the animation clock, the whole chain is scrub-, loop-, and replay-stable.
2614
2910
  - on_event_fire_after: sourceId=event, targetId=the NEXT event. When source fires, pulse target after params.delay ms. params.timeline: 'canvas' (animation clock — use for scene chains) | 'wall' (real setTimeout). This is the chaining primitive.
@@ -2650,7 +2946,7 @@ Relations are COMPOSITIONAL - an item can have multiple relations that work toge
2650
2946
  },
2651
2947
  relationType: {
2652
2948
  type: 'string',
2653
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', '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'],
2949
+ 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'],
2654
2950
  description: 'Type of relationship',
2655
2951
  },
2656
2952
  params: {
@@ -2658,8 +2954,17 @@ Relations are COMPOSITIONAL - an item can have multiple relations that work toge
2658
2954
  description: 'Relation-specific parameters',
2659
2955
  additionalProperties: true,
2660
2956
  },
2957
+ presetId: {
2958
+ type: 'string',
2959
+ description: "Adopt a community relation PRESET instead of naming a type and params yourself — a tuned motion with its symbols exposed as knobs. List them with pinepaper_query_capabilities { action: 'catalogue', catalogue: 'relation_presets' }. The preset decides the relationType, so relationType is not needed alongside it.",
2960
+ },
2961
+ presetValues: {
2962
+ type: 'object',
2963
+ description: 'presetId: symbol → number. An omitted symbol takes the midpoint of its declared range, so a preset applies sensibly with no values at all.',
2964
+ additionalProperties: true,
2965
+ },
2661
2966
  },
2662
- required: ['sourceId', 'relationType'],
2967
+ required: ['sourceId'],
2663
2968
  },
2664
2969
  },
2665
2970
  {
@@ -2684,7 +2989,7 @@ USE WHEN:
2684
2989
  targetId: { type: 'string', description: 'Target item ID' },
2685
2990
  relationType: {
2686
2991
  type: 'string',
2687
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', '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'],
2992
+ 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'],
2688
2993
  description: 'Specific relation type to remove (optional - removes all if not specified)',
2689
2994
  },
2690
2995
  },
@@ -2712,7 +3017,7 @@ USE WHEN:
2712
3017
  itemId: { type: 'string', description: 'Item to query relations for' },
2713
3018
  relationType: {
2714
3019
  type: 'string',
2715
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', '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'],
3020
+ 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'],
2716
3021
  description: 'Filter by relation type (optional)',
2717
3022
  },
2718
3023
  direction: {
@@ -2810,10 +3115,15 @@ USE WHEN:
2810
3115
  - Custom item registration
2811
3116
  - Accessing PinePaper APIs directly
2812
3117
  - Prototyping new features
3118
+ - A scene whose repetition is the point: dozens of items built in a loop. This is the cheapest route by an order of magnitude — a 63-item animated scene is ~4k characters here against ~147k as keyframe tool calls — because the loop collapses the repetition instead of restating it per item. Synchronous build work is bounded by the governor's 4s deadline, which such a scene fits easily.
3119
+
3120
+ WHAT THE CHEAP ROUTE COSTS, so the choice is deliberate: items built and animated in raw JS carry NO behavioral record. No graph edge, nothing SMIL or widget export can lift, nothing the ontology can read — the scene renders and cannot describe itself. Relations are the canonical behavior surface here, so for motion that is a function of time prefer pinepaper_add_relation with 'time_expression' (still ~5.7x cheaper than keyframes), and for a whole group moving together staggered_with or wave_through. Reach for a loop when the geometry is procedural, not merely to avoid the typing.
2813
3121
 
2814
3122
  AVAILABLE GLOBALS:
2815
3123
  - app: PinePaper application instance
2816
3124
  - app.create(type, params): Create items
3125
+ - app.addAnimation(itemId, keyframes, options): Attach keyframe TRACKS programmatically — the other half of a procedural scene, and what makes the loop route cheap: the keyframes are generated, not typed. Accepts an id or the item itself; options.timeUnits ('seconds'|'ms') overrides the ms auto-detect, which long-form timelines need
3126
+ - app.animate(item, params): Attach a LOOP animation (pulse, bounce, wobble, …) — the other kind of motion, and not what addAnimation does
2817
3127
  - app.addRelation(sourceId, targetId, type, params): Add relations
2818
3128
  - app.registerRelationRule(name, definition): Register custom relations
2819
3129
  - app.itemRegistry: Access all registered items
@@ -2932,6 +3242,13 @@ USE WHEN:
2932
3242
  - "first fade in, then rotate, then fade out"
2933
3243
  - Any animation with specific timing or sequential stages
2934
3244
 
3245
+ BEFORE AUTHORING DENSE TRACKS — three routes are cheaper, and two of them keep the scene describable. Measured on the same 63-item, 32-keyframes-each scene:
3246
+ - Motion that is a FUNCTION OF TIME (bob, pulse, sweep, phase-offset wave) → pinepaper_add_relation with relationType 'time_expression'. One call per property instead of one keyframe per sample: 26k characters against 147k. It also survives into the design graph, save/reload and widget export, which keyframes generated from a formula do not describe.
3247
+ - PER-ITEM VARIATION across many items → staggered_with or wave_through express the whole group in ONE call. That collapses the 63-fold repetition a per-item time_expression still pays for.
3248
+ - GENUINELY PROCEDURAL scenes → pinepaper_execute_custom_code with app.create + app.addAnimation (which attaches keyframe tracks programmatically, so the keyframes are generated rather than typed), at 4k characters for the same scene — cheapest by an order of magnitude, because a loop collapses the repetition entirely. The trade is real and worth making deliberately: raw JS leaves NO behavioral record — no graph edge, nothing SMIL or widget export can lift, nothing the ontology can read. Scene building fits the governor's 4s synchronous deadline easily (it is the injected loop guards, not the 10s async-tail budget, that bites a build loop).
3249
+
3250
+ Keyframes are for motion that is AUTHORED, not computed. A track of hundreds of entries produced from a formula is the signal that one of the three above is the right route.
3251
+
2935
3252
  ANIMATABLE PROPERTIES:
2936
3253
  - position: [x, y] array (a { x, y } object is also accepted and normalized)
2937
3254
  - x, y: Individual coordinates
@@ -3596,7 +3913,7 @@ SUPPORTED ANIMATIONS: bounce, breathe, fade, glow, jelly, path, pulse, rotate, s
3596
3913
  target: { type: 'string', description: 'Name of target item' },
3597
3914
  type: {
3598
3915
  type: 'string',
3599
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', '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'],
3916
+ 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'],
3600
3917
  },
3601
3918
  params: {
3602
3919
  type: 'object',
@@ -4242,7 +4559,7 @@ flags as a badly-composed scene, and they do not survive an artboard change.`,
4242
4559
  register: { type: 'string', enum: ['naive', 'playful', 'poster', 'editorial', 'technical'], description: 'Which design language. Resolves gutter, margin, hue budget and type scale together; an explicit craft still overrides it.' },
4243
4560
  level: { type: 'number', description: 'Craft level 1-4 within the register: sketch, competent, refined, art-directed.' },
4244
4561
  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." },
4245
- stitch: { type: 'string', enum: ['longAndShort', 'satin', 'seed', 'stem'], description: 'Which stitch when medium is thread (default longAndShort).' },
4562
+ stitch: { type: 'string', enum: [...THREAD_STITCHES], description: 'Which stitch when medium is thread (default longAndShort) — the same six pinepaper_design_medium offers.' },
4246
4563
  stitchBudget: { type: 'number', description: 'Total marks across the composition (default 6000); the stitch scales to fit rather than the fill being cut short.' },
4247
4564
  action: { type: 'string', enum: ['list_patterns', 'list_treatments', 'apply', 'set_treatment'] },
4248
4565
  pattern: { type: 'string', description: 'Pattern key from list_patterns' },
@@ -4827,8 +5144,13 @@ EXAMPLE — Choose effect for a victory card:
4827
5144
  properties: {
4828
5145
  action: {
4829
5146
  type: 'string',
4830
- enum: ['list', 'choose', 'coverage', 'find'],
4831
- description: 'Query mode: list, choose, coverage, or find (default: list)',
5147
+ enum: ['list', 'choose', 'coverage', 'find', 'catalogue', 'studio'],
5148
+ description: "Query mode: list, choose, coverage, find, catalogue, or studio (default: list). 'catalogue' reads a registry the aggregate does not gather. 'studio' reports which engine METHODS this studio has, so you learn what will refuse BEFORE calling it — long-form export needs exportEngine.exportToStore, mesh authoring needs extrudeToMesh, and this is how you find out without trying each.",
5149
+ },
5150
+ catalogue: {
5151
+ type: 'string',
5152
+ enum: ['rig_presets', 'shader_effects', 'stroke_decorations', 'precomps', 'images', 'segment_edit_kinds', 'shatter_orders', 'world_meshes', 'relation_presets'],
5153
+ description: "action 'catalogue': which registry to read. These sit OUTSIDE the aggregate, which already covers styles, effects, deforms, entrances, animations, collages, palettes, masks, maskShapes and cutouts — 'list' reaches all of those.",
4832
5154
  },
4833
5155
  kind: {
4834
5156
  anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
@@ -6447,6 +6769,7 @@ SHADER AURAS (WebGL2, silhouette-clipped — same tool, routed to the aura syste
6447
6769
  - vortex: The bright band is a RING, not a disc — condensation tracks the steepest pressure gradient, so the core is hollow, and differential rotation shears the field rather than spinning one picture round.
6448
6770
  - rain_veil: Drifting sheets of falling rain across the silhouette.
6449
6771
  - caustics: Refracted light patterns, as through moving water.
6772
+ - ink_bleed: Ink wicking outward into the surface, as on absorbent paper. Routable since the engine began publishing every aura name and warming the subsystem before routing; it was defined-but-unreachable before that, which is why it arrived last.
6450
6773
  Aura params: { intensity, radius, palette, mode ('inside'|'outside'|'overlay'), tint, tintStrength, offsetX, offsetY, channels: { a: {speed, curve}, b: {speed, curve} } } — curves: linear, sawtooth, sine, triangle, pulse, ease, bounce, noise.
6451
6774
 
6452
6775
  EXAMPLE — bubbles with magenta interior over a blue rim:
@@ -6459,7 +6782,7 @@ EXAMPLE — gold liquid-metal aura outside a logo:
6459
6782
  itemId: { type: 'string', description: 'Registry ID of the item' },
6460
6783
  effectType: {
6461
6784
  type: 'string',
6462
- 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'],
6785
+ 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', 'ink_bleed'],
6463
6786
  description: 'Type of effect',
6464
6787
  },
6465
6788
  params: {
@@ -6940,7 +7263,7 @@ PRIVACY: No screenshots, no tool call arguments, no user content — only struct
6940
7263
  idempotentHint: false,
6941
7264
  openWorldHint: false,
6942
7265
  },
6943
- description: `Start a new agent job for creating animations, videos, graphics, or any visual content. Do NOT generate HTML or React — use PinePaper tools instead.
7266
+ description: `Start a new agent job for creating animations, videos, graphics, or any visual content. Do not hand-write HTML or React INSTEAD OF calling these tools — but if the user wants an HTML page or an embeddable widget, that is pinepaper_export_widget_html and pinepaper_export_widget, and they are the right answer rather than a fallback.
6944
7267
 
6945
7268
  ⚠️ Call ONCE per pipeline. NEVER restart — creates duplicates.
6946
7269
 
@@ -7243,7 +7566,7 @@ EXAMPLE — Animated sky scene with timed reveals:
7243
7566
  targetId: { type: 'string', description: 'For relation: target item ID or $N' },
7244
7567
  relationType: {
7245
7568
  type: 'string',
7246
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', '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'],
7569
+ 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'],
7247
7570
  description: 'For relation: type',
7248
7571
  },
7249
7572
  relationOptions: { type: 'object', description: 'For relation: options' },
@@ -7286,6 +7609,50 @@ EXAMPLE — Animated sky scene with timed reveals:
7286
7609
  required: ['operations'],
7287
7610
  },
7288
7611
  },
7612
+ {
7613
+ name: 'pinepaper_export_store',
7614
+ annotations: {
7615
+ title: 'Export Store',
7616
+ readOnlyHint: false,
7617
+ destructiveHint: true,
7618
+ idempotentHint: false,
7619
+ openWorldHint: false,
7620
+ },
7621
+ description: `Recover an export the studio is still holding.
7622
+
7623
+ A long export is not returned inline — the studio keeps the encoded file and pinepaper_agent_export pages it out to a file for you. When that PAGING half fails, the render has already succeeded and the bytes are sitting in the store. This is how you go and get them, instead of re-rendering something that already exists.
7624
+
7625
+ ACTIONS:
7626
+ - list: {} — what is held, newest first: {id, format, size, createdAt}. The id IS the stored filename and the store is origin-persistent, so ids survive a page reload; an id lost with a dropped tool result is recoverable here.
7627
+ - save: {exportId} — page it out to a file and release it once the file is whole. Returns a filePath, the same shape agent_export returns. Safe to repeat: reading a held export mutates nothing, so a failed save can simply be run again.
7628
+ - release: {exportId} — drop it WITHOUT saving. The store is the only copy; this is not undoable.
7629
+
7630
+ WHEN TO REACH FOR IT:
7631
+ - An export failed with "Failed to write data to data pipe". That is the browser's transport refusing to carry one oversized value, not the encoder failing — the bytes may already be in the store. Call 'list' BEFORE re-rendering.
7632
+ - An export error named an exportId and a byte count reached. That error is a true promise: call 'save' with the id.
7633
+
7634
+ RETENTION, AND THE ONE WAY TO LOSE WORK:
7635
+ Held exports last until released, or until a LATER export needs the space — and eviction drops the OLDEST first. Rendering five chunks and paging them out at the end can evict chunk 1 to make room for chunk 5. Save or release each export before starting the next one. An id dropped this way usually says "evicted" rather than "not found", including after a page reload — but that record is best-effort browser storage, so a miss can still be genuinely ambiguous. Treat the absence of "evicted" as "unknown", never as proof the export never existed.`,
7636
+ inputSchema: {
7637
+ type: 'object',
7638
+ properties: {
7639
+ action: {
7640
+ type: 'string',
7641
+ enum: ['list', 'save', 'release'],
7642
+ description: "'list' (what is held), 'save' (page it to a file, then release), 'release' (drop it unsaved).",
7643
+ },
7644
+ exportId: {
7645
+ type: 'string',
7646
+ description: "Required for 'save' and 'release'. From a failed export's error, or from 'list'.",
7647
+ },
7648
+ platform: {
7649
+ type: 'string',
7650
+ description: "save: a label for the saved filename only. Defaults to 'recovered'.",
7651
+ },
7652
+ },
7653
+ required: ['action'],
7654
+ },
7655
+ },
7289
7656
  {
7290
7657
  name: 'pinepaper_agent_export',
7291
7658
  annotations: {
@@ -7330,8 +7697,17 @@ EXAMPLES:
7330
7697
  },
7331
7698
  format: {
7332
7699
  type: 'string',
7333
- enum: ['svg', 'png', 'gif', 'mp4', 'webm', 'pdf'],
7334
- description: 'Override format (auto-detected if not specified)',
7700
+ enum: ['svg', 'png', 'gif', 'mp4', 'webm', 'pdf', 'wav'],
7701
+ description: "Override format (auto-detected if not specified). 'wav' exports the SOUNDTRACK ON ITS OWN, with no frames rendered — no platform preset resolves to it, so it must be asked for by name, and platform dimensions, framing and quality do not apply.",
7702
+ },
7703
+ sampleRate: {
7704
+ type: 'number',
7705
+ description: 'wav only: samples per second (default 48000). Rejected for any other format.',
7706
+ },
7707
+ bitDepth: {
7708
+ type: 'number',
7709
+ enum: [16, 32],
7710
+ description: 'wav only: 16 (default) or 32-bit float. Rejected for any other format.',
7335
7711
  },
7336
7712
  quality: {
7337
7713
  type: 'string',
@@ -7345,7 +7721,7 @@ EXAMPLES:
7345
7721
  },
7346
7722
  estimateOnly: {
7347
7723
  type: 'boolean',
7348
- description: 'Preflight only — return the estimated file size for these exact settings and render nothing. Modeled for mp4/webm/gif; png/pdf/svg report confidence "none" (no dimension-based model exists for them).',
7724
+ description: 'Preflight only — return the estimated file size for these exact settings and render nothing. Modeled for mp4/webm/gif; png/pdf/svg report confidence "none" (no dimension-based model exists for them); wav reports confidence "exact", because uncompressed PCM size is arithmetic.',
7349
7725
  },
7350
7726
  duration: {
7351
7727
  type: 'number',
@@ -8095,6 +8471,9 @@ ACTIONS:
8095
8471
  required: ['action'],
8096
8472
  },
8097
8473
  },
8474
+ // ---------------------------------------------------------------------------
8475
+ // STITCHCRAFT PROCEDURAL EMBROIDERY & THREAD
8476
+ // ---------------------------------------------------------------------------
8098
8477
  ];
8099
8478
  /**
8100
8479
  * Get a tool definition by name