@pinepaper.studio/mcp-server 1.6.6 → 1.6.8

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 (178) hide show
  1. package/README.de.md +24 -1
  2. package/README.es.md +24 -1
  3. package/README.fr.md +24 -1
  4. package/README.hi.md +21 -1
  5. package/README.ja.md +22 -1
  6. package/README.ko.md +22 -1
  7. package/README.md +329 -14
  8. package/README.pt-BR.md +24 -1
  9. package/README.zh-CN.md +20 -1
  10. package/assets/edit-whenever.svg +75 -0
  11. package/dist/agent/export-manager.d.ts.map +1 -1
  12. package/dist/agent/export-manager.js +2 -2
  13. package/dist/agent/export-manager.js.map +1 -1
  14. package/dist/agent/session-manager.d.ts.map +1 -1
  15. package/dist/agent/session-manager.js.map +1 -1
  16. package/dist/assets/repositories/fontawesome.d.ts.map +1 -1
  17. package/dist/assets/repositories/fontawesome.js.map +1 -1
  18. package/dist/assets/repositories/openclipart.d.ts.map +1 -1
  19. package/dist/character/data/concepts.json +1 -0
  20. package/dist/character/data/depictions.json +1 -0
  21. package/dist/character/data/styles.json +1 -0
  22. package/dist/character/duration-scale.d.ts +13 -0
  23. package/dist/character/duration-scale.d.ts.map +1 -0
  24. package/dist/character/duration-scale.js +13 -0
  25. package/dist/character/duration-scale.js.map +1 -0
  26. package/dist/character/path-bbox.d.ts +82 -0
  27. package/dist/character/path-bbox.d.ts.map +1 -0
  28. package/dist/character/path-bbox.js +496 -0
  29. package/dist/character/path-bbox.js.map +1 -0
  30. package/dist/character/perform.d.ts +149 -0
  31. package/dist/character/perform.d.ts.map +1 -0
  32. package/dist/character/perform.js +626 -0
  33. package/dist/character/perform.js.map +1 -0
  34. package/dist/character/resolve.d.ts +51 -0
  35. package/dist/character/resolve.d.ts.map +1 -0
  36. package/dist/character/resolve.js +239 -0
  37. package/dist/character/resolve.js.map +1 -0
  38. package/dist/design/design-systems.d.ts +126 -0
  39. package/dist/design/design-systems.d.ts.map +1 -0
  40. package/dist/design/design-systems.js +240 -0
  41. package/dist/design/design-systems.js.map +1 -0
  42. package/dist/execution/canvas-state.d.ts +7 -0
  43. package/dist/execution/canvas-state.d.ts.map +1 -1
  44. package/dist/execution/canvas-state.js +20 -8
  45. package/dist/execution/canvas-state.js.map +1 -1
  46. package/dist/index.d.ts +20 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +12 -5
  49. package/dist/index.js.map +1 -1
  50. package/dist/ontology/design-graph.d.ts +9 -0
  51. package/dist/ontology/design-graph.d.ts.map +1 -1
  52. package/dist/ontology/design-graph.js +98 -4
  53. package/dist/ontology/design-graph.js.map +1 -1
  54. package/dist/ontology/types.d.ts +42 -0
  55. package/dist/ontology/types.d.ts.map +1 -1
  56. package/dist/ontology/vocabulary.d.ts.map +1 -1
  57. package/dist/ontology/vocabulary.js +139 -0
  58. package/dist/ontology/vocabulary.js.map +1 -1
  59. package/dist/taxonomy/classifier.js +1 -1
  60. package/dist/taxonomy/classifier.js.map +1 -1
  61. package/dist/taxonomy/error-recovery.d.ts.map +1 -1
  62. package/dist/taxonomy/matcher.d.ts.map +1 -1
  63. package/dist/taxonomy/matcher.js.map +1 -1
  64. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  65. package/dist/tools/compact-descriptions.js +25 -0
  66. package/dist/tools/compact-descriptions.js.map +1 -1
  67. package/dist/tools/definitions.d.ts +1 -1
  68. package/dist/tools/definitions.d.ts.map +1 -1
  69. package/dist/tools/definitions.js +1236 -45
  70. package/dist/tools/definitions.js.map +1 -1
  71. package/dist/tools/engine-methods.d.ts +18 -0
  72. package/dist/tools/engine-methods.d.ts.map +1 -0
  73. package/dist/tools/engine-methods.js +239 -0
  74. package/dist/tools/engine-methods.js.map +1 -0
  75. package/dist/tools/handlers/character.d.ts +121 -0
  76. package/dist/tools/handlers/character.d.ts.map +1 -0
  77. package/dist/tools/handlers/character.js +315 -0
  78. package/dist/tools/handlers/character.js.map +1 -0
  79. package/dist/tools/handlers.d.ts +2 -0
  80. package/dist/tools/handlers.d.ts.map +1 -1
  81. package/dist/tools/handlers.js +386 -22
  82. package/dist/tools/handlers.js.map +1 -1
  83. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  84. package/dist/tools/minimal-descriptions.js +24 -4
  85. package/dist/tools/minimal-descriptions.js.map +1 -1
  86. package/dist/tools/toolkits.d.ts.map +1 -1
  87. package/dist/tools/toolkits.js +21 -0
  88. package/dist/tools/toolkits.js.map +1 -1
  89. package/dist/types/code-generator.d.ts +141 -2
  90. package/dist/types/code-generator.d.ts.map +1 -1
  91. package/dist/types/code-generator.js +2246 -78
  92. package/dist/types/code-generator.js.map +1 -1
  93. package/dist/types/schemas.d.ts +9295 -603
  94. package/dist/types/schemas.d.ts.map +1 -1
  95. package/dist/types/schemas.js +1134 -61
  96. package/dist/types/schemas.js.map +1 -1
  97. package/dist/vendor/design/art-deco-generator.d.ts +52 -0
  98. package/dist/vendor/design/art-deco-generator.d.ts.map +1 -0
  99. package/dist/vendor/design/art-deco-generator.js +144 -0
  100. package/dist/vendor/design/art-deco-generator.js.map +1 -0
  101. package/dist/vendor/design/art-nouveau-generator.d.ts +46 -0
  102. package/dist/vendor/design/art-nouveau-generator.d.ts.map +1 -0
  103. package/dist/vendor/design/art-nouveau-generator.js +134 -0
  104. package/dist/vendor/design/art-nouveau-generator.js.map +1 -0
  105. package/dist/vendor/design/bauhaus-generator.d.ts +53 -0
  106. package/dist/vendor/design/bauhaus-generator.d.ts.map +1 -0
  107. package/dist/vendor/design/bauhaus-generator.js +126 -0
  108. package/dist/vendor/design/bauhaus-generator.js.map +1 -0
  109. package/dist/vendor/design/brutalist-generator.d.ts +53 -0
  110. package/dist/vendor/design/brutalist-generator.d.ts.map +1 -0
  111. package/dist/vendor/design/brutalist-generator.js +148 -0
  112. package/dist/vendor/design/brutalist-generator.js.map +1 -0
  113. package/dist/vendor/design/claymorphism-generator.d.ts +46 -0
  114. package/dist/vendor/design/claymorphism-generator.d.ts.map +1 -0
  115. package/dist/vendor/design/claymorphism-generator.js +135 -0
  116. package/dist/vendor/design/claymorphism-generator.js.map +1 -0
  117. package/dist/vendor/design/corporate-memphis-generator.d.ts +47 -0
  118. package/dist/vendor/design/corporate-memphis-generator.d.ts.map +1 -0
  119. package/dist/vendor/design/corporate-memphis-generator.js +144 -0
  120. package/dist/vendor/design/corporate-memphis-generator.js.map +1 -0
  121. package/dist/vendor/design/cyberpunk-generator.d.ts +52 -0
  122. package/dist/vendor/design/cyberpunk-generator.d.ts.map +1 -0
  123. package/dist/vendor/design/cyberpunk-generator.js +162 -0
  124. package/dist/vendor/design/cyberpunk-generator.js.map +1 -0
  125. package/dist/vendor/design/de-stijl-generator.d.ts +50 -0
  126. package/dist/vendor/design/de-stijl-generator.d.ts.map +1 -0
  127. package/dist/vendor/design/de-stijl-generator.js +154 -0
  128. package/dist/vendor/design/de-stijl-generator.js.map +1 -0
  129. package/dist/vendor/design/design-style.d.ts +3 -0
  130. package/dist/vendor/design/design-style.d.ts.map +1 -0
  131. package/dist/vendor/design/design-style.js +12 -0
  132. package/dist/vendor/design/design-style.js.map +1 -0
  133. package/dist/vendor/design/design-systems-dtcg.d.ts +59 -0
  134. package/dist/vendor/design/design-systems-dtcg.d.ts.map +1 -0
  135. package/dist/vendor/design/design-systems-dtcg.js +571 -0
  136. package/dist/vendor/design/design-systems-dtcg.js.map +1 -0
  137. package/dist/vendor/design/frutiger-aero-generator.d.ts +46 -0
  138. package/dist/vendor/design/frutiger-aero-generator.d.ts.map +1 -0
  139. package/dist/vendor/design/frutiger-aero-generator.js +133 -0
  140. package/dist/vendor/design/frutiger-aero-generator.js.map +1 -0
  141. package/dist/vendor/design/memphis-generator.d.ts +56 -0
  142. package/dist/vendor/design/memphis-generator.d.ts.map +1 -0
  143. package/dist/vendor/design/memphis-generator.js +158 -0
  144. package/dist/vendor/design/memphis-generator.js.map +1 -0
  145. package/dist/vendor/design/mid-century-upa-generator.d.ts +46 -0
  146. package/dist/vendor/design/mid-century-upa-generator.d.ts.map +1 -0
  147. package/dist/vendor/design/mid-century-upa-generator.js +137 -0
  148. package/dist/vendor/design/mid-century-upa-generator.js.map +1 -0
  149. package/dist/vendor/design/neumorphic-generator.d.ts +53 -0
  150. package/dist/vendor/design/neumorphic-generator.d.ts.map +1 -0
  151. package/dist/vendor/design/neumorphic-generator.js +182 -0
  152. package/dist/vendor/design/neumorphic-generator.js.map +1 -0
  153. package/dist/vendor/design/op-art-generator.d.ts +45 -0
  154. package/dist/vendor/design/op-art-generator.d.ts.map +1 -0
  155. package/dist/vendor/design/op-art-generator.js +116 -0
  156. package/dist/vendor/design/op-art-generator.js.map +1 -0
  157. package/dist/vendor/design/pixel-art-generator.d.ts +45 -0
  158. package/dist/vendor/design/pixel-art-generator.d.ts.map +1 -0
  159. package/dist/vendor/design/pixel-art-generator.js +143 -0
  160. package/dist/vendor/design/pixel-art-generator.js.map +1 -0
  161. package/dist/vendor/design/pop-art-generator.d.ts +53 -0
  162. package/dist/vendor/design/pop-art-generator.d.ts.map +1 -0
  163. package/dist/vendor/design/pop-art-generator.js +164 -0
  164. package/dist/vendor/design/pop-art-generator.js.map +1 -0
  165. package/dist/vendor/design/risograph-generator.d.ts +53 -0
  166. package/dist/vendor/design/risograph-generator.d.ts.map +1 -0
  167. package/dist/vendor/design/risograph-generator.js +170 -0
  168. package/dist/vendor/design/risograph-generator.js.map +1 -0
  169. package/dist/vendor/design/swiss-style-generator.d.ts +55 -0
  170. package/dist/vendor/design/swiss-style-generator.d.ts.map +1 -0
  171. package/dist/vendor/design/swiss-style-generator.js +147 -0
  172. package/dist/vendor/design/swiss-style-generator.js.map +1 -0
  173. package/dist/vendor/design/vaporwave-generator.d.ts +51 -0
  174. package/dist/vendor/design/vaporwave-generator.d.ts.map +1 -0
  175. package/dist/vendor/design/vaporwave-generator.js +139 -0
  176. package/dist/vendor/design/vaporwave-generator.js.map +1 -0
  177. package/manifest.json +65 -1
  178. package/package.json +18 -9
@@ -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.
@@ -73,6 +78,10 @@ Choose based on mood:
73
78
  ─── ITEMS ───
74
79
 
75
80
  Shapes: circle, rectangle, star, ellipse, triangle, polygon, line, arc, path, pentagon, hexagon, diamond, arrow, heart
81
+ Surfaces (render-time, evaluated per pixel — not shapes): shader, field
82
+ shader: a lit surface — water, liquid_metal, heatmap, gem_smoke. Set behind:true to put it UNDER the art, which is what scenery wants.
83
+ field: one expression, many marks — xExpr/yExpr/count, re-evaluated every frame against t.
84
+ Both draw fully in a cloud render; locally they stand in as a flat plate.
76
85
  Text: text (content, fontSize, fontFamily, color, fontWeight)
77
86
  Dynamic text: set contentType on text items → clock (live time), timer (elapsed), countdown (from N seconds), stopwatch (pauseable)
78
87
  All items: opacity, shadowColor, shadowBlur, blendMode, strokeColor, strokeWidth
@@ -98,7 +107,7 @@ Photos / raster images (on-device, no upload):
98
107
 
99
108
  ─── ANIMATION (all in batch) ───
100
109
 
101
- Loop presets (animate): pulse, rotate, bounce, fade, wobble, slide, typewriter
110
+ Loop presets (animate): bounce, breathe, fade, glow, jelly, path, pulse, rotate, scrollDown, scrollLeft, scrollRight, scrollUp, shake, slideLeftRight, slideUpDown, swing, typewriter, wobble
102
111
  Keyframe (keyframe_animate): [{time, properties, easing}] → opacity, scale, scaleX, scaleY, x, y, rotation, fillColor, strokeColor, fontSize
103
112
  Relations (relation): orbits, follows, attached_to, points_at, mirrors, parallax, wave_through, morphs_to, group_morphs_to, moves_along_path, is_midpoint_of, is_circumcenter_of, construction_reveal (+ geometric constraints & more in batch_execute schema)
104
113
  Masks (apply_mask): wipeLeft, wipeRight, wipeUp, wipeDown, iris, irisOut, star, heart, curtainHorizontal, curtainVertical, cinematic, diagonalWipe, revealUp, revealDown
@@ -165,7 +174,7 @@ Measurement: pinepaper_measurement — rulers, grid, snap-to-grid, get item dime
165
174
  ─── CANVAS & EXPORT ───
166
175
 
167
176
  Presets: instagram (1080x1080), youtube (1920x1080), tiktok (1080x1920), twitter (1200x675)
168
- 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
169
178
  Widget: pinepaper_export_widget (pp:PinePaper ontology JSON), pinepaper_export_widget_html (self-contained HTML with tree-shaken runtime)`;
170
179
  /*
171
180
  * The AI_AGENT_GUIDE constant above is included as a comment in verbose/compact modes
@@ -368,6 +377,28 @@ ITEM TYPES:
368
377
  - diamond: 4-sided diamond/rhombus (properties: radius, color)
369
378
  - arrow: Right-pointing block arrow (properties: width, height, color)
370
379
  - heart: Heart shape (properties: color)
380
+ - disk: Filled disk — the region inside a circle (properties: radius, color)
381
+ - circle-outline: The circle CURVE — stroked, no interior (properties: radius,
382
+ strokeColor, strokeWidth)
383
+ - arrow-right: Alias for arrow — the same definition under the name the shape
384
+ grids use. Identical output; both are listed because both are accepted.
385
+
386
+ SPEECH AND ANNOTATION SHAPES — the engine draws these; use one instead of
387
+ hand-building an outline from a path. All take width, height and color, and the
388
+ descriptions below are the engine's own. The six with a tail also take
389
+ tailDirection (bottom-left | bottom-right | bottom | top-left | top-right | top
390
+ | left | right) and tailSize (0-1, default 0.35):
391
+ - speech-bubble: Classic round speech bubble with triangular tail.
392
+ - speech-bubble-square: Rectangular speech bubble with sharp corners and
393
+ pointed tail.
394
+ - speech-bubble-pointed: Tooltip-style bubble with arrow pointer (tail defaults
395
+ to "bottom").
396
+ - thought-bubble: Cloud-like thought bubble with trailing dots.
397
+ - comment-box: Modern chat message bubble with high corner radius.
398
+ - callout-box: Annotation callout with narrow arrow (tail defaults to "left").
399
+ - double-bubble: Two overlapping bubbles for conversation (properties:
400
+ secondaryColor).
401
+ - quote-bubble: Stylized bubble with quotation marks.
371
402
 
372
403
  PATH FOR CUSTOM SHAPES:
373
404
  Use itemType: "path" with either:
@@ -397,6 +428,24 @@ Items on the canvas can originate from multiple sources — all become first-cla
397
428
  - pinepaper_import_asset: Pre-made asset import → becomes a full item
398
429
  All imported items participate in the relation and animation systems just like natively created items.
399
430
 
431
+ RENDER-TIME SURFACES — 'shader' and 'field':
432
+ These are not shapes. A shape is built as an item and drawn from its geometry; a
433
+ surface has no geometry — it is evaluated, per pixel or per sample, as each frame
434
+ is drawn. Use them for things a vector cannot be: an open sea, a drifting field
435
+ of marks, a lit material.
436
+ - shader: pick one of water | liquid_metal | heatmap | gem_smoke, give it width
437
+ and height, and set behind:true to place it UNDER the vector art — which is
438
+ what scenery wants, a sea the boats float ON rather than a sheen over them.
439
+ - field: xExpr/yExpr/count draw many marks from one expression, re-evaluated
440
+ every frame against t. rExpr and aExpr vary size and alpha per mark.
441
+ Both draw fully in a cloud render; on a local canvas they stand in as a flat
442
+ plate of their own colour so the layout still reads.
443
+
444
+ LIFETIMES — bornAt and ttl, on any item:
445
+ The only way to build a piece that CUTS between shots. Without them every item
446
+ is on screen for the whole render, which is what makes a long piece read as one
447
+ crowded frame instead of a sequence.
448
+
400
449
  WORKFLOW TIP:
401
450
  After creating items, use pinepaper_add_relation to animate them (e.g., orbits, follows, attached_to).
402
451
  For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal stripes, use pinepaper_create_diagonal_stripes.`,
@@ -405,8 +454,8 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
405
454
  properties: {
406
455
  itemType: {
407
456
  type: 'string',
408
- enum: ['text', 'circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc', 'pentagon', 'hexagon', 'diamond', 'arrow', 'heart'],
409
- description: 'Type of item to create',
457
+ 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'],
458
+ 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.",
410
459
  },
411
460
  position: {
412
461
  oneOf: [
@@ -429,7 +478,33 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
429
478
  },
430
479
  properties: {
431
480
  type: 'object',
432
- description: 'Type-specific properties (content, radius, color, fontSize, etc.)',
481
+ description: `Type-specific properties (content, radius, color, fontSize, etc.)
482
+
483
+ LIFETIME — on ANY item, and the only way to build a piece that CUTS between shots:
484
+ - bornAt: seconds. The item does not exist before this instant.
485
+ - ttl: seconds it lives for. Omit for "until the end".
486
+ Without these every item is on screen for the whole render, which is what makes
487
+ a long piece read as one crowded frame instead of a sequence of shots.
488
+
489
+ SHADER (itemType 'shader') — a lit surface, drawn per pixel:
490
+ - 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'
491
+ - width, height: the quad it fills
492
+ - shaderIntensity: 0..2, the effect's amplitude (water: chop)
493
+ - shaderPalette: 0 day | 1 low sun | 2 night
494
+ - behind: true draws it UNDER the vector art, which is what scenery needs —
495
+ a sea the boats float ON rather than a sheen laid over them. Default false.
496
+
497
+ FIELD (itemType 'field') — one expression, many marks. The generative form:
498
+ a whole surface as a function of the sample index and the clock, rather than
499
+ thousands of authored items.
500
+ - xExpr, yExpr: expressions for each mark's position
501
+ - rExpr, aExpr: optional radius and alpha per mark. A radius <= 0 is skipped,
502
+ so subtracting a threshold draws only the crests of a wave.
503
+ - count: how many marks (up to 200000)
504
+ - Variables in scope: i (0..count-1), n (count), t (seconds), u (i/n), pi, e, tau.
505
+ Functions: sin cos tan asin acos atan sinh cosh tanh exp ln log10 log2 sqrt
506
+ cbrt abs floor ceil round sign atan2 min max pow hypot, and % for modulo.
507
+ - Example — a rolling swell: xExpr "(i % 120) * 8", yExpr "400 + sin((i % 120) * 0.4 - t * 2) * 12", count 3600`,
433
508
  additionalProperties: true,
434
509
  },
435
510
  data: {
@@ -443,7 +518,7 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
443
518
  },
444
519
  animationType: {
445
520
  type: 'string',
446
- description: "Animation type to apply on creation (e.g., 'pulse', 'bounce', 'rotate', 'fade', 'wobble', 'slide', 'typewriter', 'keyframe')",
521
+ description: "Animation type to apply on creation (e.g., 'pulse', 'bounce', 'rotate', 'fade', 'slideLeftRight', 'scrollUp', 'typewriter', 'keyframe'). NOT 'slide' — that has never existed; the driver silently does nothing with an unknown type.",
447
522
  },
448
523
  animationSpeed: {
449
524
  type: 'number',
@@ -509,6 +584,7 @@ MODIFIABLE PROPERTIES:
509
584
  - shadowBlur: Shadow blur radius
510
585
  - shadowOffset: Shadow offset [x, y]
511
586
  - blendMode: Blend mode (normal, multiply, screen, overlay, etc.)
587
+ - pathData: SVG path data — RESHAPE the geometry itself, not just its styling. This is how you straighten a hand-drawn box or correct a traced outline without deleting and recreating the item, which would lose its id and every relation and keyframe pointing at it. Requires an FxTool build from 2026-08 or later: before that the property was silently dropped while the same call applied the fill and stroke and reported success.
512
588
 
513
589
  GRADIENT OBJECT FORMAT:
514
590
  {type: "linear"|"radial", stops: [{color: "#fff", offset: 0}, {color: "#000", offset: 1}], origin: [x,y], destination: [x,y]}
@@ -734,6 +810,56 @@ EXAMPLES:
734
810
  // ---------------------------------------------------------------------------
735
811
  // LAYER 1 — ATOMS: IMPORT TOOLS
736
812
  // ---------------------------------------------------------------------------
813
+ {
814
+ name: 'pinepaper_import_motion_capture',
815
+ annotations: {
816
+ title: 'Import Motion Capture (BVH)',
817
+ readOnlyHint: false,
818
+ destructiveHint: false,
819
+ idempotentHint: false,
820
+ openWorldHint: true,
821
+ },
822
+ description: `Import a BVH motion-capture clip (CMU, Mixamo) as an animated skeleton, or retarget one onto a rig that already exists.
823
+
824
+ TWO MODES, and picking the right one is the whole decision:
825
+ - mode:'import' → builds a NEW skeleton shaped like the capture file. Use when there is no character yet.
826
+ - mode:'retarget' → drives an EXISTING rig named by skeletonId. Proportions stay the character's; only the MOTION comes from the clip. Use whenever the character already exists.
827
+
828
+ USE WHEN:
829
+ - The user supplies a .bvh clip, or asks for real captured motion rather than a simple animationType
830
+ - A rigged character should perform a walk / run / gesture
831
+ - Simple animation (pulse, bounce, rotate) is too coarse for the request
832
+
833
+ DO NOT USE FOR:
834
+ - Simple property animation → pinepaper_animate
835
+ - Timed keyframe tracks you author yourself → pinepaper_keyframe_animate
836
+
837
+ RESULT:
838
+ - import → { skeletonId, bones, poses, duration }
839
+ - retarget → { skeletonId, matched, unmatchedSource, unmatchedTarget }
840
+
841
+ BONE MATCHING: a BVH edge is named for the joint it ENDS at, so "LeftLeg" is the thigh and "LeftFoot" the shin — the alias table accounts for this. Anything it cannot place is reported in unmatchedSource/unmatchedTarget rather than silently driving half the rig; pass boneMap to resolve those explicitly.
842
+
843
+ SAMPLING: CMU records at 120fps. fps defaults to 15 because 120 poses per second is unusable on a canvas timeline; fps:0 keeps every frame.`,
844
+ inputSchema: {
845
+ type: 'object',
846
+ properties: {
847
+ bvh: { type: 'string', description: 'BVH clip text.' },
848
+ mode: { type: 'string', enum: ['import', 'retarget'], description: "'import' = new skeleton from the file; 'retarget' = drive the existing rig in skeletonId, keeping its proportions. Default 'import'." },
849
+ skeletonId: { type: 'string', description: "Rig to drive. REQUIRED when mode='retarget'." },
850
+ fps: { type: 'number', description: 'Pose sampling rate; 0 keeps every frame. Default 15.' },
851
+ height: { type: 'number', description: 'Skeleton height on canvas in px (import mode). Default 320.' },
852
+ position: {
853
+ type: 'object',
854
+ description: 'Where the ROOT joint stands (import mode).',
855
+ properties: { x: { type: 'number' }, y: { type: 'number' } },
856
+ },
857
+ name: { type: 'string', description: 'Name for the created skeleton (import mode).' },
858
+ boneMap: { type: 'object', description: "BVH edge name → this rig's bone name; overrides the alias table.", additionalProperties: { type: 'string' } },
859
+ },
860
+ required: ['bvh'],
861
+ },
862
+ },
737
863
  {
738
864
  name: 'pinepaper_import_svg',
739
865
  annotations: {
@@ -1349,7 +1475,11 @@ EXAMPLES:
1349
1475
  y: { type: 'number', description: 'Canvas origin y (default: canvas center)' },
1350
1476
  flipY: { type: 'boolean', description: 'Math y-up → screen y-down (default true)' },
1351
1477
  style: { type: 'object', description: 'Path style forwarded to create (strokeColor, strokeWidth, fillColor, …)' },
1352
- warp: { description: 'Chained parametric warp(s): { dx, dy } expressions of x, y, t — one object or an array' },
1478
+ 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." },
1479
+ warp: {
1480
+ anyOf: [{ type: 'object' }, { type: 'array', items: { type: 'object' } }],
1481
+ description: 'Chained parametric warp(s): { dx, dy } expressions of x, y, t — one object or an array',
1482
+ },
1353
1483
  },
1354
1484
  },
1355
1485
  },
@@ -1384,7 +1514,10 @@ RETURNS: create → { success, action, eventId }; pulse → { success, action, e
1384
1514
  x: { type: 'number', description: 'Canvas x for the event marker — action create' },
1385
1515
  y: { type: 'number', description: 'Canvas y for the event marker — action create' },
1386
1516
  eventId: { type: 'string', description: "Event id to fire — required for action 'pulse'" },
1387
- payload: { description: "Optional value forwarded to listeners — action 'pulse'" },
1517
+ payload: {
1518
+ anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }, { type: 'object' }, { type: 'array' }],
1519
+ description: "Optional value forwarded to listeners — action 'pulse'",
1520
+ },
1388
1521
  },
1389
1522
  required: ['action'],
1390
1523
  },
@@ -1795,10 +1928,17 @@ EXAMPLE: { action: 'apply_style', itemId: 'title_1', styleKey: 'arcade', fontFam
1795
1928
  inputSchema: {
1796
1929
  type: 'object',
1797
1930
  properties: {
1798
- action: { type: 'string', enum: ['apply_style', 'set_font_axes', 'list_styles'], description: 'Text style operation' },
1931
+ 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." },
1932
+ text: { type: 'string', description: 'cursive: the words to write. to_collage: the text to build from, if it differs from the item.' },
1933
+ maxWidth: { type: 'number', description: 'wrap: the width to break at, in canvas units. unwrap restores the original, so this is not destructive.' },
1934
+ cursiveOptions: { type: 'object', description: 'cursive: { x?, y?, scale?, strokeColor?, strokeWidth? }.' },
1935
+ collageOptions: { type: 'object', description: 'to_collage: style, palette and the rest — same vocabulary as pinepaper_create_letter_collage.' },
1799
1936
  itemId: { type: 'string', description: 'Text item id — apply_style / set_font_axes.' },
1800
1937
  styleKey: { type: 'string', description: 'apply_style: style name from list_styles.' },
1801
- palette: { description: 'apply_style: named colourway (string) or explicit color array.' },
1938
+ palette: {
1939
+ anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
1940
+ description: 'apply_style: named colourway (string) or explicit color array.',
1941
+ },
1802
1942
  variant: { type: 'number', description: 'apply_style: variation index within the style.' },
1803
1943
  content: { type: 'string', description: 'apply_style: replace the text while styling.' },
1804
1944
  fontFamily: { type: 'string', description: "apply_style: font override; 'suggested' = the face the style was designed around." },
@@ -1808,6 +1948,137 @@ EXAMPLE: { action: 'apply_style', itemId: 'title_1', styleKey: 'arcade', fontFam
1808
1948
  required: ['action'],
1809
1949
  },
1810
1950
  },
1951
+ {
1952
+ name: 'pinepaper_design_medium',
1953
+ annotations: {
1954
+ title: 'Design Medium (What Makes The Marks)',
1955
+ readOnlyHint: false,
1956
+ destructiveHint: false,
1957
+ idempotentHint: false,
1958
+ openWorldHint: false,
1959
+ },
1960
+ description: `The THIRD design axis: the MEDIUM — what physically makes the marks.
1961
+
1962
+ Register is which design language a piece speaks; level is how well it is executed; a medium is a set of physical constraints that produce characteristic marks. All three are independent: an embroidered schematic is a technical register at a refined level in a thread medium, and no single better/worse ranking can say that.
1963
+
1964
+ READ THIS BEFORE PROMISING A MEDIUM. Naming a medium does not create its marks. Every medium declares a FIDELITY:
1965
+ - native — the characteristic marks ARE vector geometry (vector, thread, ink, cutPaper)
1966
+ - stylised — a recognisable impression; the limitation is stated on the entry (charcoal, oil)
1967
+ - absent — no vertex-level expression; the tool REFUSES with the reason instead of producing flat shapes in its colours (encaustic)
1968
+
1969
+ 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.
1970
+
1971
+ ACTIONS:
1972
+ - 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).
1973
+ - resolve: { medium } → { ok, fidelity, reason, markMaker }. ok:false with a reason is a real answer — pass it on rather than trying anyway.
1974
+ - list_stitches: the 4 thread stitches with their params.
1975
+ - apply_thread: render an item in thread. Its own silhouette is the region and its own fill is the thread colour, so an existing drawing becomes stitched rather than needing to be redrawn. The source is HIDDEN, not destroyed.
1976
+ - apply_hatch: rule an item with hatching. Same deal — silhouette is the region, source is hidden, re-hatching replaces.
1977
+ - list_flow_fields: the 7 fields that bend a hatch line.
1978
+ - list_hatch_options: the hatch defaults.
1979
+
1980
+ 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.
1981
+
1982
+ STITCH CHOICE: longAndShort (default) is the shading fill — the length variance IS the blend. satin spans the shape edge to edge in one sheet, which is right for a petal or a letter stroke and wrong for a round shape. seed is texture, not direction. stem is an OUTLINE mark and leaves the interior bare — that is the stitch, not a bug.
1983
+
1984
+ An unknown stitch is refused rather than defaulted, and re-stitching an item REPLACES its previous stitching instead of stacking.
1985
+
1986
+ HATCHING STATES VALUE THROUGH LINE DENSITY, not colour. The same shape at distance 6 and at distance 3 reads as light and dark with nothing else changed — that is the whole medium, and it is why distance is the parameter to reach for rather than a lighter ink. gradient makes the density fall off across the shape, which is a shaded ramp rather than a flat tone.
1987
+
1988
+ The straight ruling is what a printer makes; flowField is what makes it read as DRAWN. hand is the small correlated wander of a hand-drawn line, waves for water/hair/contours, spiral for wood grain around a knot, columns for rake marks. continuous joins the whole set into one serpentine path — a pen that never leaves the paper, and one item instead of many.
1989
+
1990
+ Hatching needs a CLOSED outline. Text has none: convert it to glyph paths (pinepaper_text_style) and hatch the result. A group is hatched child by child, up to 40 paths; past that the result says so rather than quietly stopping.
1991
+
1992
+ Note that a thread field and a hatch flowField are different things and are named differently for it: 'field' steers stitches around a form, 'flowField' bends a ruling line the way a hand drifts.
1993
+
1994
+ EXAMPLE — a hatched shadow side, drawn rather than printed:
1995
+ { "action": "apply_hatch", "itemId": "item_3", "distance": 3, "angle": 30, "flowField": "hand", "rand": 0.2 }
1996
+
1997
+ EXAMPLE — a stitched leaf:
1998
+ { "action": "apply_thread", "itemId": "item_3", "stitch": "longAndShort",
1999
+ "field": { "kind": "spine", "spine": [{"x":400,"y":200},{"x":400,"y":520}] },
2000
+ "stitchLen": 26, "variance": 0.45 }`,
2001
+ inputSchema: {
2002
+ type: 'object',
2003
+ properties: {
2004
+ action: { type: 'string', enum: ['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options'], description: 'Medium operation' },
2005
+ 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.' },
2006
+ itemId: { type: 'string', description: 'apply_thread / apply_hatch: a closed path, compound path, or a group of them.' },
2007
+ 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." },
2008
+ field: { type: 'object', description: "apply_thread: { kind: 'radial'|'spine'|'constant', cx, cy, angle, spine[], across } — default radial from the shape centre." },
2009
+ stitchLen: { type: 'number', description: 'apply_thread: nominal stitch length px (default 18).' },
2010
+ rowGap: { type: 'number', description: 'apply_thread: row spacing px; defaults from thread width so rows abut.' },
2011
+ variance: { type: 'number', description: 'apply_thread: length jitter 0..1 (default 0.35) — the long-and-short shading.' },
2012
+ width: { type: 'number', description: 'apply_thread: thread width px (default 2).' },
2013
+ sheen: { type: 'boolean', description: 'apply_thread: highlight along each stitch (default true).' },
2014
+ color: { type: 'string', description: "apply_thread: thread colour (defaults to the item's fill)." },
2015
+ seed: { type: 'number', description: 'apply_thread: PRNG seed (default 1) — same seed stitches the same way.' },
2016
+ count: { type: 'number', description: 'apply_thread: stitch count for seed/satin.' },
2017
+ slant: { type: 'number', description: 'apply_thread: stitch slant in DEGREES — satin (default 0) and stem (default 22).' },
2018
+ overlap: { type: 'number', description: 'apply_thread: how far consecutive stem stitches overlap (default 0.4) — what makes a stem outline read as rope.' },
2019
+ stagger: { type: 'number', description: 'apply_thread: offsets alternate longAndShort rows (default 0.5) — stops stitch ends lining up into seams.' },
2020
+ gapLen: { type: 'number', description: 'apply_thread: gap between runningSeam stitches, px (default 4) — with stitchLen, the dash rhythm of a machine seam.' },
2021
+ gridSize: { type: 'number', description: 'apply_thread: crossStitch grid pitch, px (default 12) — the size of each X, and so the resolution of the fill.' },
2022
+ 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.' },
2023
+ distance: { type: 'number', description: 'apply_hatch: line spacing px (default 6). THE VALUE CONTROL — 6 vs 3 is light vs dark.' },
2024
+ angle: { type: 'number', description: 'apply_hatch: ruling angle in DEGREES (default 45).' },
2025
+ gradient: { type: 'number', description: 'apply_hatch: 0..1 spacing growth per line (default 0, even) — a shaded ramp rather than a flat tone.' },
2026
+ rand: { type: 'number', description: 'apply_hatch: jitter as a fraction of distance (default 0) — takes the mechanical evenness off.' },
2027
+ continuous: { type: 'boolean', description: 'apply_hatch: join the lines into ONE serpentine path (default false).' },
2028
+ flowField: {
2029
+ type: 'string',
2030
+ enum: ['hand', 'curved', 'zigzag', 'waves', 'seabed', 'spiral', 'columns'],
2031
+ description: 'apply_hatch: bend each line along a flow field — what makes it read as drawn rather than printed.',
2032
+ },
2033
+ t: { type: 'number', description: 'apply_hatch: flow-field time offset — advances the field without changing the seed.' },
2034
+ },
2035
+ required: ['action'],
2036
+ },
2037
+ },
2038
+ {
2039
+ name: 'pinepaper_text_effect',
2040
+ annotations: {
2041
+ title: 'Text Effects (37 Character-Level Animations)',
2042
+ readOnlyHint: false,
2043
+ destructiveHint: true,
2044
+ idempotentHint: false,
2045
+ openWorldHint: false,
2046
+ },
2047
+ description: `Animate text CHARACTER BY CHARACTER — 37 effects (terminaltexteffects' vocabulary, reimplemented from source).
2048
+
2049
+ ⚠️ 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.
2050
+
2051
+ ACTIONS:
2052
+ - list: {} — the 37 effects as {key, label, definition}. Call this first; effect keys come from here.
2053
+ - apply: { itemId, effect, duration?, seed?, gradient?, gradientStops?, gradientDirection?, gradientSteps?, keepSource?, options? } — explode and animate.
2054
+
2055
+ 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.
2056
+
2057
+ COLOR: by default the resting characters are painted with a GRADIENT across the text block — that is what the upstream effects do and what their reference frames show. Pass gradient: false to keep the text's authored fill, or gradientStops to choose your own.
2058
+
2059
+ DETERMINISM: seed defaults to 1, so the same text + effect + seed animates identically every run. Vary seed for a different scatter of the same effect.
2060
+
2061
+ EFFECT KEYS (37): scattered, expand, spray, slice, bouncyballs, pour, slide, print, randomsequence, wipe, sweep, highlight, middleout, fireworks, blackhole, unstable, crumble, rings, swarm, orbittingvolley, overflow, decrypt, errorcorrect, matrix, binarypath, rain, burn, smoke, thunderstorm, bubbles, waves, beams, laseretch, spotlights, synthgrid, vhstape, colorshift.
2062
+
2063
+ EXAMPLE: { action: 'apply', itemId: 'title_1', effect: 'decrypt', duration: 3, gradientStops: ['#00ff9c', '#0066ff'] }`,
2064
+ inputSchema: {
2065
+ type: 'object',
2066
+ properties: {
2067
+ action: { type: 'string', enum: ['apply', 'list'], description: 'Text effect operation' },
2068
+ itemId: { type: 'string', description: 'apply: the text item to animate (must have non-whitespace content).' },
2069
+ effect: { type: 'string', description: "apply: effect key from 'list'." },
2070
+ duration: { type: 'number', description: 'apply: seconds for the whole effect (default 2.5); ends at rest.' },
2071
+ seed: { type: 'number', description: 'apply: PRNG seed (default 1) — same seed reproduces the animation exactly.' },
2072
+ gradient: { type: 'boolean', description: "apply: false keeps the text's authored fill. Default true (gradient across the block)." },
2073
+ gradientStops: { type: 'array', items: { type: 'string' }, description: 'apply: hex colors overriding the effect gradient.' },
2074
+ gradientDirection: { type: 'string', enum: ['vertical', 'horizontal', 'radial', 'diagonal'], description: 'apply: gradient axis.' },
2075
+ gradientSteps: { type: 'number', description: 'apply: quantization steps in the gradient ramp.' },
2076
+ keepSource: { type: 'boolean', description: 'apply: keep the original text item — use when relations/keyframes reference its id.' },
2077
+ options: { type: 'object', description: 'apply: per-effect knobs, passed through verbatim.' },
2078
+ },
2079
+ required: ['action'],
2080
+ },
2081
+ },
1811
2082
  {
1812
2083
  name: 'pinepaper_crop_image',
1813
2084
  annotations: {
@@ -1981,6 +2252,231 @@ RECIPE — a playable maze: create_tilemap with wall fills → batch_create colo
1981
2252
  required: ['action'],
1982
2253
  },
1983
2254
  },
2255
+ {
2256
+ name: 'pinepaper_design_system',
2257
+ description: `The design vocabulary, as data you can read — and eighteen styles that can build a scene.
2258
+
2259
+ TWO DIFFERENT THINGS, kept apart on purpose:
2260
+
2261
+ 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.
2262
+
2263
+ 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.
2264
+
2265
+ - list_systems / get_system: the licensed systems and their tokens, filterable by token type.
2266
+ - 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.
2267
+ - list_styles / 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.
2268
+
2269
+ A partial composition reports as a failure with the elements that did not create, rather than as a success with half a poster.`,
2270
+ inputSchema: {
2271
+ type: 'object',
2272
+ properties: {
2273
+ action: { type: 'string', enum: ['list_systems', 'get_system', 'list_easings', 'list_styles', 'compose'], description: 'Which design-system operation.' },
2274
+ systemId: { type: 'string', description: "get_system: e.g. 'material_3', 'ibm_carbon', 'uswds'. Call list_systems for ids." },
2275
+ tokenType: { type: 'string', enum: ['dimension', 'duration', 'cubicBezier', 'color', 'fontFamily', 'fontWeight', 'number', 'shadow', 'grid'], description: 'get_system: return only tokens of this DTCG type.' },
2276
+ style: { type: 'string', description: "compose: the aesthetic style, e.g. 'bauhaus_geometric'. Call list_styles for the composable ones." },
2277
+ title: { type: 'string', description: 'compose: the headline most styles build around.' },
2278
+ subtitle: { type: 'string', description: 'compose: the supporting line.' },
2279
+ body: { type: 'string', description: 'compose: body copy, where the style has room for it.' },
2280
+ width: { type: 'number', description: 'compose: canvas width (the style picks its own default otherwise).' },
2281
+ height: { type: 'number', description: 'compose: canvas height.' },
2282
+ variant: { type: 'string', description: "compose: a style-specific variant, e.g. art deco's 'emerald'." },
2283
+ draw: { type: 'boolean', description: 'compose: draw it (default true). false returns the scene and ops as data and draws nothing.' },
2284
+ authoredOnly: { type: 'boolean', description: 'list_easings: true for only the curves authored here, false for only what vendors publish.' },
2285
+ },
2286
+ required: ['action'],
2287
+ },
2288
+ },
2289
+ {
2290
+ name: 'pinepaper_stick',
2291
+ 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.
2292
+
2293
+ 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.
2294
+
2295
+ action 'figure' builds the figure. action 'set' builds the floor, the wall and the objects around it, so a figure has somewhere to be.
2296
+
2297
+ 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.`,
2298
+ inputSchema: {
2299
+ type: 'object',
2300
+ properties: {
2301
+ action: { type: 'string', enum: ['figure', 'set'], description: "'figure' or 'set'." },
2302
+ id: { type: 'string', description: 'Prefix for the created items.' },
2303
+ kind: { type: 'string', description: 'figure: which figure. set: which piece of the kit.' },
2304
+ at: { type: 'object', description: '{ x?, y? } — where it goes.' },
2305
+ scale: { type: 'number', description: 'Size multiplier.' },
2306
+ facing: { type: 'string', enum: ['left', 'right'], description: 'Which way the figure looks.' },
2307
+ pose: { type: 'string', description: "figure: a named pose from the kit's POSES." },
2308
+ poseAt: { type: 'number', description: 'figure: seconds at which that pose is struck.' },
2309
+ expression: { type: 'string', description: 'figure: one facial expression.' },
2310
+ expressions: { type: 'array', items: { type: 'object' }, description: 'figure: [{ at, name }] — expressions over time.' },
2311
+ walk: { anyOf: [{ type: 'boolean' }, { type: 'object' }], description: 'figure: a walk cycle.' },
2312
+ travel: { type: 'object', description: 'figure: move across the scene while walking, rather than walking in place.' },
2313
+ prop: { type: 'string', description: 'figure: something held in hand.' },
2314
+ propSide: { type: 'string', enum: ['left', 'right'], description: 'figure: which hand.' },
2315
+ garment: { type: 'string', description: 'figure: clothing.' },
2316
+ trouser: { type: 'string', description: 'figure: legwear.' },
2317
+ hair: { type: 'string', description: 'figure: hair style.' },
2318
+ withHair: { type: 'boolean', description: 'figure: draw hair at all.' },
2319
+ groundY: { type: 'number', description: 'The y the figure stands on.' },
2320
+ surfaceY: { type: 'number', description: 'The y a set object sits on.' },
2321
+ durationSeconds: { type: 'number', description: 'figure: length of the performance.' },
2322
+ object: { type: 'object', description: 'set: the object spec.' },
2323
+ items: { type: 'array', items: { type: 'object' }, description: 'set: several pieces at once.' },
2324
+ floor: { anyOf: [{ type: 'boolean' }, { type: 'object' }], description: 'set: draw a floor.' },
2325
+ wall: { anyOf: [{ type: 'boolean' }, { type: 'object' }], description: 'set: draw a wall.' },
2326
+ },
2327
+ required: ['action'],
2328
+ },
2329
+ },
2330
+ {
2331
+ name: 'pinepaper_story',
2332
+ description: `A piece of prose becomes a scene.
2333
+
2334
+ - 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.
2335
+ - from_text: distill and assemble in one call, when the reading does not need checking.
2336
+ - apply_spec: assemble a spec you already have, usually a distilled one you edited.
2337
+ - plan_book: lay images out as pages.`,
2338
+ inputSchema: {
2339
+ type: 'object',
2340
+ properties: {
2341
+ action: { type: 'string', enum: ['distill', 'from_text', 'apply_spec', 'plan_book'], description: 'Which story operation.' },
2342
+ text: { type: 'string', description: 'distill / from_text: the prose.' },
2343
+ spec: { type: 'object', description: 'apply_spec: the story spec, usually a distilled one you edited.' },
2344
+ images: { type: 'array', items: { anyOf: [{ type: 'string' }, { type: 'object' }] }, description: 'plan_book: images as ids or specs.' },
2345
+ options: { type: 'object', description: 'Assembly or layout options.' },
2346
+ },
2347
+ required: ['action'],
2348
+ },
2349
+ },
2350
+ {
2351
+ name: 'pinepaper_interchange',
2352
+ description: `The formats other tools read and write — Lottie, dotLottie, GLB, BVH, PNG sequence.
2353
+
2354
+ 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.
2355
+
2356
+ 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.`,
2357
+ inputSchema: {
2358
+ type: 'object',
2359
+ properties: {
2360
+ action: { type: 'string', enum: ['export_lottie', 'export_dotlottie', 'import_lottie', 'export_glb', 'export_bvh', 'export_png_sequence'], description: 'Which interchange operation.' },
2361
+ options: { type: 'object', description: 'Format options passed through to the exporter: frame range, dimensions, fps, naming.' },
2362
+ data: { anyOf: [{ type: 'string' }, { type: 'object' }], description: 'import_lottie: the Lottie JSON — object, JSON string, or URL.' },
2363
+ skeletonId: { type: 'string', description: "export_bvh: which skeleton to write. A BVH is one skeleton's motion." },
2364
+ },
2365
+ required: ['action'],
2366
+ },
2367
+ },
2368
+ {
2369
+ name: 'pinepaper_sound',
2370
+ description: `Synthesis — and the drawing that is the same object.
2371
+
2372
+ The engine carries a full audio graph, and none of it was reachable. pinepaper_audio_beats ANALYSES audio that already exists; this MAKES audio.
2373
+
2374
+ - Catalogues: list_instruments, list_percussion, list_sfx. Call one before naming a drum or effect — the names are the engine's.
2375
+ - 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.
2376
+ - 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.
2377
+
2378
+ THE PART WORTH KNOWING: sound and drawing are the same object here, both ways.
2379
+ - 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.
2380
+ - timbre_from_path reads ANY drawn path back as harmonic content. A curve someone drew by hand becomes the partials of a note.
2381
+
2382
+ 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.
2383
+
2384
+ 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.
2385
+ - 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.
2386
+ - For a drum, pass pitch: { from, tau }. The glide is what makes a kick a kick; without it you get a beep.
2387
+ - Names are normalised to [a-z][a-z0-9_-]. Register 'Rhodes' and the response returns canonicalName 'rhodes' — play THAT.
2388
+
2389
+ 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.`,
2390
+ inputSchema: {
2391
+ type: 'object',
2392
+ properties: {
2393
+ 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.' },
2394
+ partials: { type: 'array', items: { type: 'object' }, description: 'define_*: [{ h, amp }] — the harmonic table that IS the timbre. Required for define_instrument.' },
2395
+ envelope: { type: 'object', description: 'define_*: { attack, decay, sustain, release } — seconds, except sustain which is a level 0-1.' },
2396
+ gain: { type: 'number', description: 'define_*: output level 0-1 (0.8 instrument, 0.85 percussion, 0.65 sfx).' },
2397
+ aliases: { type: 'array', items: { type: 'string' }, description: "define_*: extra names for the same sound. An alias never shadows a real catalogue entry." },
2398
+ 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.' },
2399
+ noiseFreq: { type: 'number', description: 'define_percussion / define_sfx: noise band centre in Hz.' },
2400
+ hz: { type: 'number', description: 'define_percussion / define_sfx: base frequency in Hz.' },
2401
+ 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.' },
2402
+ sampleRate: { type: 'number', description: 'render_soundtrack: samples per second (default 48000).' },
2403
+ bitDepth: { type: 'number', enum: [16, 32], description: 'render_soundtrack: 16 (default) or 32-bit float.' },
2404
+ note: { type: 'string', description: "play_tone: scientific pitch, e.g. 'A4'." },
2405
+ root: { type: 'string', description: "play_chord / chord_frequencies: root note, e.g. 'C4'." },
2406
+ chord: { type: 'string', description: "play_chord / chord_frequencies: chord kind (default 'major')." },
2407
+ name: { type: 'string', description: 'play_percussion / play_sfx: the named drum or effect — list them first.' },
2408
+ text: { type: 'string', description: "from_text / play_from_text: a plain-language description. 'from_text' resolves WITHOUT playing." },
2409
+ spec: { type: 'object', description: 'play_spec / create: the sound spec, usually one from from_text or a catalogue.' },
2410
+ options: { type: 'object', description: 'Playback options for the play_* actions.' },
2411
+ itemId: { type: 'string', description: 'timbre_from_path: the path whose shape becomes the timbre. set_placement / remove: the sound item.' },
2412
+ samples: { type: 'number', description: 'timbre_from_path: points sampled along the path (default 256).' },
2413
+ placement: { type: 'object', description: 'set_placement: where and when the sound sits on the timeline.' },
2414
+ visual: { type: 'object', description: 'create: { width?, height?, position?, color? } for the waveform path.' },
2415
+ },
2416
+ required: ['action'],
2417
+ },
2418
+ },
2419
+ {
2420
+ name: 'pinepaper_motion',
2421
+ 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.
2422
+
2423
+ Two kinds, and the second is the one nothing else here can do:
2424
+ - GROUP motions (drift, sway, rotate, pulse, wave, bounce) move the target as one.
2425
+ - 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.
2426
+
2427
+ 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.
2428
+
2429
+ 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.
2430
+
2431
+ 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.
2432
+
2433
+ This is not pinepaper_animate: that applies a loop preset to ONE item. This moves a group, or moves a wave through one.`,
2434
+ inputSchema: {
2435
+ type: 'object',
2436
+ properties: {
2437
+ action: { type: 'string', enum: ['list', 'apply'], description: "'list' the catalogue · 'apply' a motion." },
2438
+ itemId: { type: 'string', description: 'apply: a group or single item.' },
2439
+ itemIds: { type: 'array', items: { type: 'string' }, description: 'apply: several items, wrapped in a new group that hosts the motion. Members keep their own ids.' },
2440
+ motion: { type: 'string', description: "apply: the motion name. Call 'list' for the set the engine accepts." },
2441
+ speed: { type: 'number', description: 'apply: roughly cycles per second.' },
2442
+ intensity: { type: 'number', description: 'apply: amplitude (0.15 is the usual default elsewhere).' },
2443
+ waveform: { type: 'string', enum: ['sine', 'triangle', 'square', 'sawtooth', 'spike'], description: 'apply: the shape of one cycle.' },
2444
+ 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.' },
2445
+ seed: { type: 'number', description: "apply: makes a 'random' origin reproducible." },
2446
+ },
2447
+ required: ['action'],
2448
+ },
2449
+ },
2450
+ {
2451
+ name: 'pinepaper_path',
2452
+ description: `Destructive path operations — the ones that REPLACE or ADD items rather than restyling them.
2453
+
2454
+ - 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.
2455
+ - simplify: fewer segments, same shape. Higher tolerance is fewer points and a looser curve.
2456
+ - 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.
2457
+ - toggle_closed: open a closed path or close an open one.
2458
+ - pattern: repeat an item — concentric rings, a radial ring, a grid, or a depth-wise extrude stack.
2459
+ - get_geometry: read the segments back (anchor and handle coordinates).
2460
+ - set_locked / unlock_all: a locked item is also unselectable and undraggable, so it cannot be grabbed by accident.
2461
+
2462
+ 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.
2463
+
2464
+ 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.`,
2465
+ inputSchema: {
2466
+ type: 'object',
2467
+ properties: {
2468
+ action: { type: 'string', enum: ['boolean', 'simplify', 'outline_stroke', 'toggle_closed', 'pattern', 'get_geometry', 'set_locked', 'unlock_all'], description: 'Which path operation.' },
2469
+ itemId: { type: 'string', description: 'The item to operate on — every action except boolean and unlock_all.' },
2470
+ itemIds: { type: 'array', items: { type: 'string' }, description: 'boolean: at least two path ids. The first is the base.' },
2471
+ op: { type: 'string', enum: ['unite', 'subtract', 'intersect', 'exclude', 'divide'], description: 'boolean: which operation (default unite).' },
2472
+ tolerance: { type: 'number', description: 'simplify: how far the new curve may stray, in canvas units (default 2.5). outline_stroke: flattening tolerance.' },
2473
+ kind: { type: 'string', enum: ['concentric', 'radial', 'grid', 'extrude'], description: 'pattern: which repeat (default radial).' },
2474
+ 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.' },
2475
+ locked: { type: 'boolean', description: 'set_locked: true locks, false unlocks (default true).' },
2476
+ },
2477
+ required: ['action'],
2478
+ },
2479
+ },
1984
2480
  {
1985
2481
  name: 'pinepaper_world3d',
1986
2482
  annotations: {
@@ -1999,16 +2495,22 @@ ACTIONS:
1999
2495
  - 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.
2000
2496
  - remove_actor / list_actors / set_actor_pose: { actorId, pose: { x?, z?, angle? } } — the setter a timeline, sequencer or agent drives frame by frame.
2001
2497
  - set_camera: { camera: { mode: 'follow' | 'fixed' | 'orbit', target? (actorId), radius?, speed?, eye?, lookAt? } }
2002
- - add_object: { object: { x, z, height?, color? } } (y defaults to sitting ON the terrain) / remove_object: { objectId }
2498
+ - add_object: { object: { x, z, height?, color?, metalness?, roughness?, emissiveIntensity? } } (y defaults to sitting ON the terrain) / remove_object: { objectId }
2003
2499
  - remove_world: {} — tears the world down; the Paper canvas was always its own layer and is untouched.
2004
2500
 
2005
2501
  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).`,
2006
2502
  inputSchema: {
2007
2503
  type: 'object',
2008
2504
  properties: {
2009
- 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' },
2010
- spec: { description: "create: preset id ('forest'|'snowMountain'|'field'|'jungle') or a full spec object." },
2011
- character: { description: 'create: include the walkable character (boolean or config).' },
2505
+ 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.' },
2506
+ spec: {
2507
+ anyOf: [{ type: 'string' }, { type: 'object' }],
2508
+ description: "create: preset id ('forest'|'snowMountain'|'field'|'jungle') or a full spec object.",
2509
+ },
2510
+ character: {
2511
+ anyOf: [{ type: 'boolean' }, { type: 'object' }],
2512
+ description: 'create: include the walkable character (boolean or config).',
2513
+ },
2012
2514
  patch: { type: 'object', description: 'configure: partial world spec, validated against describe.' },
2013
2515
  actorId: { type: 'string', description: 'Actor id.' },
2014
2516
  x: { type: 'number', description: 'add_actor: world x.' },
@@ -2018,12 +2520,118 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2018
2520
  live: { type: 'boolean', description: 'add_actor: re-rasterize as the item animates.' },
2019
2521
  pose: { type: 'object', description: 'set_actor_pose: { x?, z?, angle? }.' },
2020
2522
  camera: { type: 'object', description: "set_camera: { mode: follow|fixed|orbit, target?, radius?, speed?, eye?, lookAt? }." },
2021
- object: { type: 'object', description: 'add_object: { x, z, height?, color?, y? }.' },
2523
+ 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.' },
2524
+ pathId: { type: 'string', description: 'extrude_path / lathe_path: the canvas path to turn into geometry. The path itself is unchanged.' },
2525
+ 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.' },
2526
+ meshId: { type: 'string', description: 'remove_mesh: the mesh id.' },
2527
+ 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.' },
2528
+ lightId: { type: 'string', description: 'set_light / remove_light: the light id.' },
2529
+ 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.' },
2530
+ materialId: { type: 'string', description: 'set_material / remove_material: the material id.' },
2531
+ source: { type: 'string', description: 'import_obj: the OBJ file TEXT. import_gltf: a URL or a data: URI of the .glb bytes.' },
2532
+ importOptions: { type: 'object', description: 'import_obj / import_gltf: placement and scale options, passed through.' },
2533
+ clip: { type: 'string', description: 'set_mesh_clip: which imported animation clip a skinned mesh plays.' },
2534
+ crossfade: { type: 'number', description: 'set_mesh_clip: seconds to blend out of the previous clip (blended in LOCAL pose space).' },
2535
+ navTarget: { type: 'string', enum: ['2d', '3d'], description: "set_nav_target: which layer the pointer drives." },
2536
+ point: { type: 'object', description: 'ground_height / canvas_to_ground / world_to_canvas: { x, y, z? }.' },
2537
+ origin: { type: 'array', items: { type: 'number' }, description: 'raycast: ray origin [x, y, z].' },
2538
+ direction: { type: 'array', items: { type: 'number' }, description: 'raycast: ray direction [x, y, z], need not be normalised.' },
2539
+ multiplier: { type: 'number', description: 'dolly_camera: >1 away, <1 closer.' },
2540
+ dx: { type: 'number', description: 'pan_camera: horizontal pan in SCREEN pixels.' },
2541
+ dy: { type: 'number', description: 'pan_camera: vertical pan in SCREEN pixels.' },
2542
+ 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.' },
2543
+ bodyId: { type: 'string', description: 'remove_body / impulse / set_velocity: the body id.' },
2544
+ vector: { type: 'array', items: { type: 'number' }, description: 'impulse / set_velocity: [x, y, z].' },
2545
+ dt: { type: 'number', description: 'step_physics: seconds to advance. Returns contacts made and bodies removed.' },
2546
+ from: { type: 'array', items: { type: 'number' }, description: 'line_of_sight: eye point [x, y, z].' },
2547
+ to: { type: 'array', items: { type: 'number' }, description: 'line_of_sight: target point [x, y, z].' },
2548
+ 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.' },
2022
2549
  objectId: { type: 'string', description: 'remove_object: object id.' },
2023
2550
  },
2024
2551
  required: ['action'],
2025
2552
  },
2026
2553
  },
2554
+ {
2555
+ name: 'pinepaper_character',
2556
+ annotations: {
2557
+ title: 'Character (place and direct a figure from the graph)',
2558
+ readOnlyHint: false,
2559
+ idempotentHint: false,
2560
+ },
2561
+ description: `Place a CHARACTER from the design graph and DIRECT it — no geometry, no bones, no poses.
2562
+
2563
+ The knowledge of how a figure is built and how it moves lives in the graph; this asks for it by name and states WHAT happens. The alternative is thirty-odd exactly-right calls — create the skeleton, add each bone with the right parent and angle, create each shape, attach each to the right bone, then author poses — where one mistake anywhere leaves a broken figure.
2564
+
2565
+ { concept: "pp:Character", at: { x: 470, y: 250 }, height: 300,
2566
+ beats: [ { at: 1.0, channel: "blink" },
2567
+ { at: 1.8, channel: "say", until: 4.2 },
2568
+ { at: 5.0, channel: "headTurn", value: -0.8 } ] }
2569
+
2570
+ CHANNELS ARE DECLARED BY THE CONCEPT, not by you. Ask pinepaper_ontology for a concept to see the ones it has. Four kinds exist:
2571
+ - 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.
2572
+ - 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.
2573
+ - shift: every part moved together, for a turn a flat drawing cannot rotate into.
2574
+ - alternate / speak: swaps between drawn states, for shapes that genuinely differ rather than deform.
2575
+
2576
+ BEATS ARE INSTANTS OR SPANS. \`at\` is when it happens; \`until\` is how long for a channel that occupies time. \`value\` is how far, for channels that take a magnitude.
2577
+
2578
+ WHAT YOU GET BACK: the items and keyframe tracks the beats compile to. They are ordinary scene items afterwards — modify, animate and relate them like anything else.
2579
+
2580
+ NOT LIP SYNC. \`say\` opens and closes a mouth for the length of a line. Matching phonemes needs the word timings a voice track carries; pass \`timings\` if you have them and each entry drives one mouth opening.`,
2581
+ inputSchema: {
2582
+ type: 'object',
2583
+ properties: {
2584
+ concept: {
2585
+ type: 'string',
2586
+ description: 'Concept id from the design graph, e.g. "pp:Character" or "pp:Pigeon". Its parts, rig and channels come with it.',
2587
+ },
2588
+ at: {
2589
+ type: 'object',
2590
+ properties: {
2591
+ x: { type: 'number', description: 'X of the figure\'s centre' },
2592
+ y: { type: 'number', description: 'Y of the figure\'s centre' },
2593
+ },
2594
+ description: 'Where the figure stands.',
2595
+ },
2596
+ height: { type: 'number', description: 'Drawn height in pixels. Every amplitude in the graph is a fraction, so the performance scales with it.' },
2597
+ 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.' },
2598
+ 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.' },
2599
+ palette: { type: 'object', description: "Override the depiction's own colours: part id → colour. Only the parts you name change." },
2600
+ 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.' },
2601
+ id: { type: 'string', description: 'Prefix for the created items (default: the concept name).' },
2602
+ style: { type: 'string', description: 'Style id for the depiction (default: the concept\'s own).' },
2603
+ variant: { type: 'string', description: 'Depiction variant (default: "default").' },
2604
+ durationSeconds: { type: 'number', description: 'Length of the piece, so every track spans it. A track that starts late leaves the figure invisible until it does.' },
2605
+ beats: {
2606
+ type: 'array',
2607
+ description: 'What happens, in order. Channels the concept does not declare are ignored rather than approximated.',
2608
+ items: {
2609
+ type: 'object',
2610
+ properties: {
2611
+ at: { type: 'number', description: 'When, in seconds' },
2612
+ channel: { type: 'string', description: 'A channel the concept declares — blink, say, headTurn, bob, tailFlick…' },
2613
+ until: { type: 'number', description: 'For a channel that occupies a span rather than an instant' },
2614
+ value: { type: 'number', description: 'How far, for a channel that takes a magnitude' },
2615
+ },
2616
+ required: ['at', 'channel'],
2617
+ },
2618
+ },
2619
+ timings: {
2620
+ type: 'array',
2621
+ description: 'Optional word or syllable timings from a voice track. Each entry drives one mouth opening, which is what turns the shape of speech into lip sync.',
2622
+ items: {
2623
+ type: 'object',
2624
+ properties: {
2625
+ at: { type: 'number', description: 'Start, in seconds' },
2626
+ until: { type: 'number', description: 'End, in seconds' },
2627
+ },
2628
+ required: ['at'],
2629
+ },
2630
+ },
2631
+ },
2632
+ required: ['concept'],
2633
+ },
2634
+ },
2027
2635
  {
2028
2636
  name: 'pinepaper_rigging',
2029
2637
  annotations: {
@@ -2037,6 +2645,19 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2037
2645
 
2038
2646
  WORKFLOW: create_skeleton → add_bone (×N, parent them into a hierarchy) → attach_item (bind shapes to bones) → optionally create_ik_chain → animate with add_pose_keyframe (or save_pose then reference it).
2039
2647
 
2648
+ POSE MOTION — build a performance out of saved poses:
2649
+ - START HERE on a rig you did not build yourself: list_bones gives the ids an inline pose needs ({ boneId: angleDeg }), and list_pose_libraries + load_pose_library give a fresh rig a stock walk/run/jump set to work with. A newly rigged character has ZERO saved poses, so without one of these there is nothing to sequence.
2650
+ - save_pose / list_poses / load_pose / interpolate_poses: the pose library. A pose is a snapshot of bone angles; everything below is a graph over these.
2651
+ - play_pose_sequence: one clip. Keys name a saved pose id or an inline { boneId: angleDeg } map. ONE sequence per skeleton — starting another replaces it.
2652
+ - stitch_poses: join CLIPS into one continuous performance (walk ×3 → jump → land → idle) and install it as a single sequence. This is how you get a transition rather than a cut.
2653
+ · A cyclic clip (loop: true) is entered at the phase CLOSEST to where the previous clip ended, so the legs do not teleport mid-stride. matchPhase: false to disable.
2654
+ · Every seam reports 'residual' — the angular mismatch, in degrees, that the blend could not remove. Near zero is a real match; a large number means the blend is HIDING a cut. Read it.
2655
+ · plan: true returns the seams without touching the skeleton, so you can check the joins first.
2656
+ - 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.
2657
+ - 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.
2658
+ - bake_animation: freeze the whole rig into plain item keyframes for export. Needs items ATTACHED to bones — a bare skeleton has no output.
2659
+ - add_secondary_motion: spring chains for tails, hair, cloth. skin_path: per-vertex deformation of a curved path.
2660
+
2040
2661
  ACTIONS:
2041
2662
  - create_skeleton: { name?, rootPosition? } → { skeletonId }
2042
2663
  - add_bone: { skeletonId, name?, parentBoneId? (omit for root), length? (80), angle? (deg), flexibility? (0 rigid…1), segments? } → { boneId }
@@ -2054,7 +2675,7 @@ BREAKDOWN POSES (S12): a breakdown keyframe shapes the ARC + SPACING between key
2054
2675
  inputSchema: {
2055
2676
  type: 'object',
2056
2677
  properties: {
2057
- action: { type: 'string', enum: ['create_skeleton', 'add_bone', 'attach_item', 'create_ik_chain', 'add_pose_keyframe', 'set_target_path', 'save_pose', 'save_shape_key', 'import_bvh', 'retarget_bvh', 'import_spine'], description: 'Rigging operation' },
2678
+ action: { type: 'string', enum: ['create_skeleton', 'add_bone', 'attach_item', 'create_ik_chain', 'add_pose_keyframe', 'set_target_path', 'save_pose', 'save_shape_key', 'import_bvh', 'retarget_bvh', 'import_spine', 'list_skeletons', 'list_bones', 'list_pose_libraries', 'load_pose_library', 'list_poses', 'load_pose', 'interpolate_poses', 'play_pose_sequence', 'stop_pose_sequence', 'stitch_poses', 'apply_pose_transition', 'auto_walk', 'auto_breath', 'auto_idle', 'auto_jump', 'move_root', 'stop_root_track', 'add_secondary_motion', 'skin_path', 'bake_animation', 'list_shape_keys', 'load_shape_key'], description: 'Rigging operation' },
2058
2679
  bvhText: { type: 'string', description: 'import_bvh / retarget_bvh: the .bvh file contents.' },
2059
2680
  spineJson: { type: 'string', description: 'import_spine: the Spine JSON export, as a string.' },
2060
2681
  view: { type: 'string', enum: ['side', 'front'], description: 'import_bvh: 3D→2D projection plane. CMU walks read best from the side (default).' },
@@ -2065,6 +2686,25 @@ BREAKDOWN POSES (S12): a breakdown keyframe shapes the ARC + SPACING between key
2065
2686
  itemId: { type: 'string', description: 'Canvas item id — attach_item.' },
2066
2687
  chainId: { type: 'string', description: 'IK chain id — set_target_path.' },
2067
2688
  name: { type: 'string', description: 'Name — create_skeleton / add_bone / create_ik_chain / save_pose / save_shape_key.' },
2689
+ poseId: { type: 'string', description: 'Saved pose id — load_pose.' },
2690
+ libraryName: { type: 'string', description: 'Stock pose set — load_pose_library. list_pose_libraries for the names.' },
2691
+ boneMap: { type: 'object', description: 'Bone NAME → bone id — load_pose_library. Omit to build it from list_bones.' },
2692
+ poseIdA: { type: 'string', description: 'First pose — interpolate_poses.' },
2693
+ poseIdB: { type: 'string', description: 'Second pose — interpolate_poses.' },
2694
+ t: { type: 'number', description: 'Blend 0=A … 1=B — interpolate_poses.' },
2695
+ sequence: { type: 'array', items: { type: 'object' }, description: 'Pose keys [{ t, pose | angles, ease }] — play_pose_sequence.' },
2696
+ clips: { type: 'array', items: { type: 'object' }, description: 'Clips in order for stitch_poses: [{ keys | poses, duration, repeat, loop, blend, matchPhase }]. A clip key names a SAVED pose id or an inline { boneId: angleDeg } map.' },
2697
+ blend: { type: 'number', description: 'Seam overlap in seconds (default 0.25) — stitch_poses.' },
2698
+ blendSteps: { type: 'number', description: 'Keys emitted across a seam (default 6) — stitch_poses.' },
2699
+ matchPhase: { type: 'boolean', description: 'Enter a cyclic clip at the phase closest to where the last one ended (default true) — stitch_poses.' },
2700
+ plan: { type: 'boolean', description: 'stitch_poses: return the seams WITHOUT installing the result.' },
2701
+ transitionName: { type: 'string', description: 'Named transition — apply_pose_transition.' },
2702
+ poseIdMap: { type: 'object', description: 'Transition pose name → saved pose id — apply_pose_transition.' },
2703
+ boneNames: { type: 'array', items: { type: 'string' }, description: 'Ordered bone names for a spring chain — add_secondary_motion.' },
2704
+ keyframes: { type: 'array', items: { type: 'object' }, description: 'World root positions [{ t, x, y, ease }] — move_root. `t` is SCENE SECONDS on the same clock as the pose sequence, not a 0..1 fraction: match the last keyframe to the performance duration or the figure arrives early and stands still for the rest.' },
2705
+ shapeKeyId: { type: 'string', description: 'Saved shape key id — load_shape_key.' },
2706
+ weight: { type: 'number', description: 'Shape key weight (default 1) — load_shape_key.' },
2707
+ options: { type: 'object', description: 'Pass-through options — auto_* / bake_animation / move_root / skin_path / play_pose_sequence.' },
2068
2708
  rootPosition: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, description: 'Skeleton root position — create_skeleton.' },
2069
2709
  parentBoneId: { type: 'string', description: 'Parent bone id (omit for root) — add_bone.' },
2070
2710
  length: { type: 'number', description: 'Bone length px (default 80) — add_bone.' },
@@ -2079,7 +2719,10 @@ BREAKDOWN POSES (S12): a breakdown keyframe shapes the ARC + SPACING between key
2079
2719
  strength: { type: 'number', description: 'Chain strength 0..1 (1) — create_ik_chain.' },
2080
2720
  poleVector: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, description: 'Pole vector hint — create_ik_chain.' },
2081
2721
  time: { type: 'number', description: 'Keyframe time s — add_pose_keyframe.' },
2082
- pose: { description: 'Saved pose id (string) OR { boneId: angleDeg } map — add_pose_keyframe.' },
2722
+ pose: {
2723
+ anyOf: [{ type: 'string' }, { type: 'object', additionalProperties: { type: 'number' } }],
2724
+ description: 'Saved pose id (string) OR { boneId: angleDeg } map — add_pose_keyframe.',
2725
+ },
2083
2726
  easing: { type: 'string', description: 'Named easing (linear) — add_pose_keyframe.' },
2084
2727
  favor: { type: 'number', description: 'Breakdown spacing bias −1..1 — add_pose_keyframe.' },
2085
2728
  breakdown: { type: 'boolean', description: 'Mark as breakdown pose — add_pose_keyframe.' },
@@ -2198,7 +2841,7 @@ USE WHEN:
2198
2841
  - Building live geometric constructions (GeoGebra-style — dragging an anchor updates the dependent)
2199
2842
 
2200
2843
  RELATION TYPES:
2201
- - orbits: Circular motion around target (params: radius, speed, direction, phase)
2844
+ - orbits: Circular motion around target (params: radius, speed, direction, phaseDegrees). Use phaseDegrees. 'phase' is the one parameter in the whole relation vocabulary measured in RADIANS — the documented exception to "angles are DEGREES", kept only for scenes that already set it. phaseDegrees wins when both are given.
2202
2845
  - follows: Move toward target with smoothing (params: offset, smoothing, delay)
2203
2846
  - attached_to: Fixed offset from target (params: offset, inherit_rotation)
2204
2847
  - maintains_distance: Stay fixed distance from target (params: distance, strength)
@@ -2206,6 +2849,35 @@ RELATION TYPES:
2206
2849
  - mirrors: Mirror position across axis (params: axis, center)
2207
2850
  - parallax: Move relative by depth (params: depth, origin)
2208
2851
  - bounds_to: Stay within bounds (params: padding, bounce)
2852
+ - anchored_in_world: Pin the item to a 3D world position so it tracks the world camera.
2853
+ - spring_follow: Follow with spring dynamics rather than smoothing — it overshoots and settles.
2854
+ - repels: Move away from the target inside a radius and ease back when it leaves (params: force, maxDistance, minDistance, returnSpeed). Crowd avoidance, and reacting to a cursor without being dragged by it.
2855
+ - wiggle: Continuous procedural noise on the item. Richer than a sine: it does not repeat visibly.
2856
+ - syncs_with: Run the source's animation on the TARGET's timeline (params: offset, speedRatio) — they share a clock and stay in step even when the target's timing changes, which merely starting together does not give you.
2857
+ - triggers_animation: Start the target's animation on a condition (params: trigger, delay, atTime). The declarative "and then": sequencing in the graph instead of in a callback nobody can query.
2858
+ - tours / synced_to_audio: Move through a sequence of targets; bind playback to an audio track.
2859
+ - connects_to: Diagram edge (params: via, sourcePort, targetPort, label).
2860
+ - attached_to_tail / head_points_to: Bind a connector's tail / head to a diagram port (params: port). Together they are why a connector stays connected when a flowchart is rearranged.
2861
+ - expresses: Play a facial expression over a root's part_of children (params: expression — blink|smile|surprise…, interval, duration, easing). No interval means sustained. It walks part_of from the ROOT, so parts with roles but no parent are INERT: the relation applies to nothing and reports success, which is what makes an unparented pair of eyes look finished while being unable to blink.
2862
+ - part_of: Semantic membership with a named role (params: role, offset, structural). structural: true records the part WITHOUT driving its position — the right choice when the parent is a group that already transforms its children, since driving it too would fight the group.
2863
+ - staggered_with: Delay an item's animation by its place in a sequence (params: index, stagger, effect). For a SHAPED stagger — outward from the centre, converging from the edges, across a grid — also pass count (required: without it there is no "end" to measure distance from), plus from (start|end|center|edges|random), amount (total seconds for the whole run, overrides stagger), grid ("rows,cols" or "auto"), axis (x|y) and distributeEase. Plain index x stagger still works and is taken first. To stagger items directly rather than through a relation, use pinepaper_stagger.
2864
+
2865
+ INTERACTIVE RELATIONS — the state machine lives in the graph.
2866
+ An event channel (pinepaper_event) is the hub: something FIRES it, and reactions bound to it run. Nothing here is a callback; it is all queryable graph, so it scrubs, exports and survives a reload.
2867
+
2868
+ - Input triggers (item → event): on_click_fire, on_pointer_enter_fire, on_pointer_exit_fire, on_key_fire (params: key, modifiers, global, preventDefault). { key: 'Enter' } makes Enter activate a button exactly as a click does. The chord is matched EXACTLY in both directions — { key: 'Enter' } does NOT fire on Ctrl+Enter, and { key: 's' } does not steal the browser's Ctrl+S; ask for a modifier with modifiers: { ctrl: true }. Listens only while the source item has focus unless global: true, so it will not take keys from the rest of the page.
2869
+ - Event reactions (event → item), params in common: property, value, key, by, color, template, delay, timeline.
2870
+ · on_event_set_property / _set_color / _set_visibility / _set_data — write a value.
2871
+ · on_event_set_property_from_template — write a value built from a '{token}' TEMPLATE read off item.data, so the text can quote live state ("Score: {score}") instead of a literal fixed when you authored it.
2872
+ · on_event_increment (params: property, by) / on_event_toggle — counters and flip-flops.
2873
+ · on_event_fire_after (params: delay, timeline) — chain one beat to the next.
2874
+ · on_event_fire_if — the only reaction that TESTS graph state rather than mutating it; params.predicate is an ExpressionIR program.
2875
+ · on_event_add_relation / on_event_remove_relation — the scene rewires itself.
2876
+ · on_event_store_set / on_event_store_increment / restores_from — persistent state. The target is a store node whose id is the storage SCOPE, so two stores in a scene and two widgets on a page do not collide. restores_from is the read half and fires ONCE at load.
2877
+ - Proximity (pointer over an item): on_enter_* / on_exit_* mirror the on_event_* reactions above (set_color, set_property, set_property_from_template, set_visibility, set_data, increment, toggle) — hover states without a single line of script.
2878
+ - Exclusive state: exclusive_group makes at most one member active (on_event_set_active is how you activate one — tabs, steppers, quiz cards). menubar_group is the same grouping WITHOUT the mutex.
2879
+
2880
+ There are 80 relation types and this list names the families rather than every member. For the live catalogue — what exists in THIS build, with definitions — call pinepaper_query_capabilities { kind: 'relation' }, which reads the engine's registry rather than a list written down here.
2209
2881
 
2210
2882
  RELATION COMPATIBILITY:
2211
2883
  - Universal relations (position-based) work with ALL item types including imported SVGs/images:
@@ -2228,7 +2900,7 @@ GEOMETRIC CONSTRUCTION CONSTRAINTS (Layer 2) — the source item is RE-DERIVED e
2228
2900
 
2229
2901
  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.
2230
2902
  - 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.
2231
- - 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).
2903
+ - 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.
2232
2904
 
2233
2905
  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.
2234
2906
  - 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.
@@ -2270,7 +2942,7 @@ Relations are COMPOSITIONAL - an item can have multiple relations that work toge
2270
2942
  },
2271
2943
  relationType: {
2272
2944
  type: 'string',
2273
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'camera_follows', 'camera_animates', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'construction_reveal', 'driven_by', 'time_expression', 'on_event_fire_after', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_visibility', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with'],
2945
+ 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'],
2274
2946
  description: 'Type of relationship',
2275
2947
  },
2276
2948
  params: {
@@ -2278,8 +2950,17 @@ Relations are COMPOSITIONAL - an item can have multiple relations that work toge
2278
2950
  description: 'Relation-specific parameters',
2279
2951
  additionalProperties: true,
2280
2952
  },
2953
+ presetId: {
2954
+ type: 'string',
2955
+ 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.",
2956
+ },
2957
+ presetValues: {
2958
+ type: 'object',
2959
+ description: 'presetId: symbol → number. An omitted symbol takes the midpoint of its declared range, so a preset applies sensibly with no values at all.',
2960
+ additionalProperties: true,
2961
+ },
2281
2962
  },
2282
- required: ['sourceId', 'relationType'],
2963
+ required: ['sourceId'],
2283
2964
  },
2284
2965
  },
2285
2966
  {
@@ -2304,7 +2985,7 @@ USE WHEN:
2304
2985
  targetId: { type: 'string', description: 'Target item ID' },
2305
2986
  relationType: {
2306
2987
  type: 'string',
2307
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'camera_follows', 'camera_animates', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'construction_reveal', 'driven_by', 'time_expression', 'on_event_fire_after', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_visibility', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with'],
2988
+ 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'],
2308
2989
  description: 'Specific relation type to remove (optional - removes all if not specified)',
2309
2990
  },
2310
2991
  },
@@ -2332,7 +3013,7 @@ USE WHEN:
2332
3013
  itemId: { type: 'string', description: 'Item to query relations for' },
2333
3014
  relationType: {
2334
3015
  type: 'string',
2335
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'camera_follows', 'camera_animates', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'construction_reveal', 'driven_by', 'time_expression', 'on_event_fire_after', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_visibility', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with'],
3016
+ 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'],
2336
3017
  description: 'Filter by relation type (optional)',
2337
3018
  },
2338
3019
  direction: {
@@ -2483,7 +3164,8 @@ USE WHEN:
2483
3164
  - "bouncing text" → animationType: bounce
2484
3165
  - "fading effect" → animationType: fade
2485
3166
  - "wobbling button" → animationType: wobble
2486
- - "sliding header" → animationType: slide
3167
+ - "sliding header" → animationType: slideLeftRight (or slideUpDown)
3168
+ - "scrolling banner" → animationType: scrollLeft (also scrollRight/Up/Down)
2487
3169
  - "typewriter effect" → animationType: typewriter (text only)
2488
3170
 
2489
3171
  DO NOT USE WHEN:
@@ -2491,21 +3173,30 @@ DO NOT USE WHEN:
2491
3173
  - User wants sequential animations ("first fade, then rotate") → Use keyframe animation
2492
3174
  - User describes relationships ("orbit around") → Use relations
2493
3175
 
2494
- ANIMATION TYPES:
3176
+ ANIMATION TYPES (18 — the list the driver actually switches on):
2495
3177
  - pulse: Scale up/down rhythmically
3178
+ - breathe: A slower, softer pulse
2496
3179
  - rotate: Continuous rotation
3180
+ - swing: Pendulum rotation about the top
2497
3181
  - bounce: Vertical bouncing motion
2498
- - fade: Opacity cycling
3182
+ - jelly: Squash-and-stretch wobble
3183
+ - shake: Rapid small displacement
2499
3184
  - wobble: Side-to-side wobbling
2500
- - slide: Horizontal sliding
2501
- - typewriter: Character-by-character reveal`,
3185
+ - fade: Opacity cycling
3186
+ - glow: Cycling luminance
3187
+ - slideLeftRight / slideUpDown: Sliding on one axis
3188
+ - scrollLeft / scrollRight / scrollUp / scrollDown: Continuous scroll, wrapping
3189
+ - path: Travel along an assigned path
3190
+ - typewriter: Character-by-character reveal
3191
+
3192
+ There is NO 'slide'. An unknown type is stored and then ignored — the item sits still while its data claims otherwise, and the call still reports success — so use the exact names above.`,
2502
3193
  inputSchema: {
2503
3194
  type: 'object',
2504
3195
  properties: {
2505
3196
  itemId: { type: 'string', description: 'Registry ID of the item' },
2506
3197
  animationType: {
2507
3198
  type: 'string',
2508
- enum: ['pulse', 'rotate', 'bounce', 'fade', 'wobble', 'slide', 'typewriter'],
3199
+ enum: ['bounce', 'breathe', 'fade', 'glow', 'jelly', 'path', 'pulse', 'rotate', 'scrollDown', 'scrollLeft', 'scrollRight', 'scrollUp', 'shake', 'slideLeftRight', 'slideUpDown', 'swing', 'typewriter', 'wobble'],
2509
3200
  description: 'Type of animation',
2510
3201
  },
2511
3202
  speed: {
@@ -2635,15 +3326,45 @@ USE WHEN:
2635
3326
  - Seeking to specific time
2636
3327
  - Controlling animation state
2637
3328
 
2638
- 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.`,
3329
+ 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.
3330
+
3331
+ 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.
3332
+
3333
+ PROGRESS (get_progress / set_progress): the timeline as 0..1, which is the unit a scrubber or a progress bar actually has. set_progress returns the seconds it landed on.
3334
+
3335
+ SCROLL (bind_scroll / unbind_scroll / list_scrub_anchors): drive the timeline from how far the reader has scrolled instead of from a clock — what makes a scroll-told story a story rather than a video that happens to be on the page. Anchors ('top bottom' = the moment the element appears, 'bottom top' = the moment it is gone) say where the range starts and ends; scrub is a smoothing time constant so the playhead chases the scroll instead of snapping to it. Chiefly for embeds and generated code — nothing scrolls a headless page. ALWAYS unbind before rebinding or navigating away: the listener holds the scene alive. bind_scroll releases a previous binding for you, and unbind_scroll on nothing bound answers { bound: false } rather than failing.`,
2639
3336
  inputSchema: {
2640
3337
  type: 'object',
2641
3338
  properties: {
2642
3339
  action: {
2643
3340
  type: 'string',
2644
- enum: ['play', 'pause', 'stop', 'seek'],
3341
+ enum: [
3342
+ 'play', 'pause', 'stop', 'seek',
3343
+ 'set_time_scale', 'get_time_scale',
3344
+ 'get_progress', 'set_progress',
3345
+ 'bind_scroll', 'unbind_scroll', 'list_scrub_anchors',
3346
+ ],
2645
3347
  description: 'Playback action',
2646
3348
  },
3349
+ rate: {
3350
+ type: 'number',
3351
+ description: 'set_time_scale: 1 real time, 0.5 half speed, 0 freezes without stopping, negative runs backwards. Export is unaffected.',
3352
+ },
3353
+ progress: {
3354
+ type: 'number',
3355
+ description: 'set_progress: how far through the timeline, 0..1',
3356
+ },
3357
+ scroll: {
3358
+ type: 'object',
3359
+ properties: {
3360
+ elementId: { type: 'string', description: 'DOM id of the element to track (defaults to the canvas)' },
3361
+ start: { type: 'string', description: "Anchor where the range begins, e.g. 'top bottom' — see list_scrub_anchors" },
3362
+ end: { type: 'string', description: "Anchor where the range ends, e.g. 'bottom top'" },
3363
+ scrub: { type: 'number', description: 'Seconds of smoothing so the playhead chases the scroll (default 0.3; 0 snaps)' },
3364
+ range: { type: 'array', items: { type: 'number' }, description: '[from, to] seconds — drive only a sub-range of the timeline' },
3365
+ },
3366
+ description: 'bind_scroll options',
3367
+ },
2647
3368
  duration: {
2648
3369
  type: 'number',
2649
3370
  description: 'Duration for play action',
@@ -3131,7 +3852,7 @@ EXAMPLE - Bouncing Balls:
3131
3852
 
3132
3853
  SUPPORTED ITEM TYPES: text, circle, star, rectangle, triangle, polygon, ellipse, path, line, arc
3133
3854
  SUPPORTED RELATIONS: orbits, follows, attached_to, maintains_distance, points_at, mirrors, parallax, bounds_to, animates, grows_from, staggered_with, indicates, circumscribes, wave_through, camera_follows, camera_animates, morphs_to, group_morphs_to, moves_along_path, is_midpoint_of, lies_on_line, is_centroid_of, is_circumcenter_of, concentric_with, construction_reveal
3134
- SUPPORTED ANIMATIONS: pulse, rotate, bounce, fade, wobble, slide, typewriter`,
3855
+ SUPPORTED ANIMATIONS: bounce, breathe, fade, glow, jelly, path, pulse, rotate, scrollDown, scrollLeft, scrollRight, scrollUp, shake, slideLeftRight, slideUpDown, swing, typewriter, wobble`,
3135
3856
  inputSchema: {
3136
3857
  type: 'object',
3137
3858
  properties: {
@@ -3176,7 +3897,7 @@ SUPPORTED ANIMATIONS: pulse, rotate, bounce, fade, wobble, slide, typewriter`,
3176
3897
  target: { type: 'string', description: 'Name of target item' },
3177
3898
  type: {
3178
3899
  type: 'string',
3179
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'camera_follows', 'camera_animates', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'construction_reveal', 'driven_by', 'time_expression', 'on_event_fire_after', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_visibility', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with'],
3900
+ 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'],
3180
3901
  },
3181
3902
  params: {
3182
3903
  type: 'object',
@@ -3196,7 +3917,7 @@ SUPPORTED ANIMATIONS: pulse, rotate, bounce, fade, wobble, slide, typewriter`,
3196
3917
  target: { type: 'string', description: 'Name of item to animate' },
3197
3918
  type: {
3198
3919
  type: 'string',
3199
- enum: ['pulse', 'rotate', 'bounce', 'fade', 'wobble', 'slide', 'typewriter'],
3920
+ enum: ['bounce', 'breathe', 'fade', 'glow', 'jelly', 'path', 'pulse', 'rotate', 'scrollDown', 'scrollLeft', 'scrollRight', 'scrollUp', 'shake', 'slideLeftRight', 'slideUpDown', 'swing', 'typewriter', 'wobble'],
3200
3921
  },
3201
3922
  speed: { type: 'number', description: 'Animation speed (default: 1.0)' },
3202
3923
  params: {
@@ -3334,6 +4055,361 @@ EXAMPLE — Slideshow:
3334
4055
  required: ['action'],
3335
4056
  },
3336
4057
  },
4058
+ {
4059
+ name: 'pinepaper_sequence',
4060
+ annotations: {
4061
+ title: 'Timeline Sequencing (Relative Positions)',
4062
+ readOnlyHint: true,
4063
+ destructiveHint: false,
4064
+ idempotentHint: true,
4065
+ openWorldHint: false,
4066
+ },
4067
+ description: `Work out WHEN clips start, said relative to each other instead of in absolute seconds. A pure planner — it computes times and touches nothing on the canvas. (For revealing a geometric construction step by step, that is pinepaper_construction_sequence, a different tool.)
4068
+
4069
+ Nobody thinks "the subtitle starts at 1.85s"; they think "a quarter of a second after the title finishes". This resolves the second into the first, and re-resolves it when an earlier clip changes length.
4070
+
4071
+ ACTIONS:
4072
+ - place: thread a whole run of clips, each resolved against the ones before it. Returns { placed: [{start, end, duration}], labels, duration }.
4073
+ - resolve: answer a single position spec against a context you supply.
4074
+ - list_forms: the accepted grammar, each form with what it means.
4075
+
4076
+ POSITION FORMS:
4077
+ - 3 — exactly 3s from the start of the timeline
4078
+ - "<" / ">" — when the previous clip STARTED / ENDED
4079
+ - "<2" / ">2" — 2s after the previous clip started / ended
4080
+ - "+=1" / "-=1" — 1s after the timeline ends (a gap) / 1s before it (an overlap)
4081
+ - "intro" / "intro+=0.5" — at a label, or half a second after it
4082
+ - omitted — append at the end, which is what appending has always meant
4083
+
4084
+ THE ONE THING TO GET RIGHT: a percentage means different things in different forms.
4085
+ - "-=25%" and "+=25%" scale against the clip BEING INSERTED — "overlap the previous clip by a quarter of MY length".
4086
+ - "<25%" and ">25%" scale against the PREVIOUS clip — "start a quarter of the way THROUGH it".
4087
+ They agree only when the two clips are the same length, which is rarely, and getting it backwards yields timings that look almost right.
4088
+
4089
+ A label names the moment a clip STARTS, so "label+=0" lands on the clip rather than skipping it. The timeline only ever grows — placing something early with "-=" never pulls the end back.
4090
+
4091
+ EXAMPLE:
4092
+ {
4093
+ "action": "place",
4094
+ "clips": [
4095
+ { "id": "title", "duration": 2, "label": "intro" },
4096
+ { "id": "subtitle", "duration": 4, "position": "-=25%" },
4097
+ { "id": "cta", "duration": 1, "position": "intro+=0.5" }
4098
+ ]
4099
+ }`,
4100
+ inputSchema: {
4101
+ type: 'object',
4102
+ properties: {
4103
+ action: {
4104
+ type: 'string',
4105
+ enum: ['place', 'resolve', 'list_forms'],
4106
+ description: 'place (a run of clips), resolve (one position), or list_forms (default: place)',
4107
+ },
4108
+ clips: {
4109
+ type: 'array',
4110
+ items: {
4111
+ type: 'object',
4112
+ properties: {
4113
+ id: { type: 'string', description: 'Your own name for the clip — echoed back on the placement' },
4114
+ duration: { type: 'number', description: 'How long the clip runs, in seconds' },
4115
+ position: {
4116
+ anyOf: [{ type: 'number' }, { type: 'string' }],
4117
+ description: 'Where it starts. Omit to append at the end of the run so far.',
4118
+ },
4119
+ label: { type: 'string', description: 'Name this moment for later clips to position against. Marks where the clip STARTS.' },
4120
+ },
4121
+ required: ['duration'],
4122
+ },
4123
+ description: 'place: the run of clips, in order',
4124
+ },
4125
+ position: {
4126
+ anyOf: [{ type: 'number' }, { type: 'string' }],
4127
+ description: 'resolve: the single position spec to resolve',
4128
+ },
4129
+ context: {
4130
+ type: 'object',
4131
+ properties: {
4132
+ timelineEnd: { type: 'number', description: 'Where the timeline currently ends — what "+=1" counts from' },
4133
+ prevStart: { type: 'number', description: 'Start of the most recent clip — what "<" means' },
4134
+ prevEnd: { type: 'number', description: 'End of the most recent clip — what ">" means' },
4135
+ insertDuration: { type: 'number', description: 'Duration of the clip being placed — the basis for a "+=25%" percentage' },
4136
+ },
4137
+ description: 'resolve: what the spec is measured against',
4138
+ },
4139
+ labels: {
4140
+ type: 'object',
4141
+ additionalProperties: { type: 'number' },
4142
+ description: 'Named moments in seconds, addressable as "name" or "name+=0.5"',
4143
+ },
4144
+ startAt: { type: 'number', description: 'place: where the run begins (default 0)' },
4145
+ },
4146
+ },
4147
+ },
4148
+ {
4149
+ name: 'pinepaper_stagger',
4150
+ annotations: {
4151
+ title: 'Stagger',
4152
+ readOnlyHint: false,
4153
+ destructiveHint: false,
4154
+ idempotentHint: true,
4155
+ openWorldHint: false,
4156
+ },
4157
+ description: `Shape a delay across many items. What makes a stagger read as designed rather than as a queue is where it STARTS and how it SPREADS — a grid lighting up outward from the centre, a row converging from both edges.
4158
+
4159
+ Delays are written to the channel the engine and the SMIL exporter already read, so a staggered scene scrubs, exports and restores. Nothing here is playback-only.
4160
+
4161
+ ACTIONS:
4162
+ - apply: write the delays onto real items, in the order you list them (row-major for a grid).
4163
+ - preview: the delays for a count, without touching the canvas.
4164
+ - list_origins: the origins, each with what it means.
4165
+
4166
+ each VS amount — alternatives, not a pair:
4167
+ - each fixes the gap between NEIGHBOURS, so more items make a longer sequence.
4168
+ - amount fixes the TOTAL, so more items crowd together. It overrides each.
4169
+
4170
+ ORIGINS (from):
4171
+ - start — the first item goes first, in order. The plain queue.
4172
+ - end — the last goes first; the sequence runs backwards.
4173
+ - center — spreads outward from the middle of the run or grid.
4174
+ - edges — starts at both ends at once and converges on the middle.
4175
+ - random — a shuffled ORDER, not random delays: every item still lands on one of the same slots, so the rhythm survives. Seeded.
4176
+ from also takes an item index, or [fx, fy] fractions for a grid ([0.5, 0.5] is the centre).
4177
+
4178
+ ease distributes the START TIMES. It is not the items' own animation curve.
4179
+
4180
+ EXAMPLE — a 3x3 grid lighting up from the middle:
4181
+ { "action": "apply", "itemIds": ["a","b","c","d","e","f","g","h","i"], "opts": { "each": 0.1, "from": "center", "grid": [3, 3] } }`,
4182
+ inputSchema: {
4183
+ type: 'object',
4184
+ properties: {
4185
+ action: {
4186
+ type: 'string',
4187
+ enum: ['apply', 'preview', 'list_origins'],
4188
+ description: 'apply (to items), preview (delays only), or list_origins (default: apply)',
4189
+ },
4190
+ itemIds: {
4191
+ type: 'array',
4192
+ items: { type: 'string' },
4193
+ description: 'apply: registry IDs in the order the stagger should follow (row-major for a grid)',
4194
+ },
4195
+ count: { type: 'number', description: 'preview: how many items to compute delays for' },
4196
+ opts: {
4197
+ type: 'object',
4198
+ properties: {
4199
+ each: { type: 'number', description: 'Seconds between neighbours (default 0.1)' },
4200
+ amount: { type: 'number', description: 'Total seconds for the whole run — overrides each' },
4201
+ from: {
4202
+ anyOf: [
4203
+ { type: 'string', enum: ['start', 'end', 'center', 'edges', 'random'] },
4204
+ { type: 'number' },
4205
+ { type: 'array', items: { type: 'number' } },
4206
+ ],
4207
+ description: 'Named origin, an item index, or [fx, fy] fractions for a grid',
4208
+ },
4209
+ grid: { type: 'array', items: { type: 'number' }, description: '[rows, columns] for a 2D stagger' },
4210
+ axis: { type: 'string', enum: ['x', 'y'], description: 'Restrict a grid stagger to one axis' },
4211
+ ease: {
4212
+ type: 'string',
4213
+ enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInCubic', 'easeOutCubic'],
4214
+ description: "Curve that distributes the START TIMES — not the items' own animation",
4215
+ },
4216
+ seed: { type: 'number', description: "from: 'random' — same seed, same shuffle (default 1)" },
4217
+ },
4218
+ description: 'Shape of the stagger',
4219
+ },
4220
+ },
4221
+ },
4222
+ },
4223
+ {
4224
+ name: 'pinepaper_flip',
4225
+ annotations: {
4226
+ title: 'Flip (Derived Transition)',
4227
+ readOnlyHint: false,
4228
+ destructiveHint: false,
4229
+ idempotentHint: false,
4230
+ openWorldHint: false,
4231
+ },
4232
+ description: `Animate a layout change WITHOUT describing the motion. Every other animation tool asks you to say what the movement is; this asks the opposite — record where things are, rearrange them however you like, and the transition is derived from the difference.
4233
+
4234
+ That is what makes it usable for changes nobody could enumerate in advance: a re-sort, an auto-layout pass, a filter, a drag.
4235
+
4236
+ THE ORDER MATTERS:
4237
+ 1. action: 'record' — capture the current positions.
4238
+ 2. Rearrange, with any tools you like (move, align, sort, re-layout).
4239
+ 3. action: 'apply' — the motion between the two states is worked out and written.
4240
+
4241
+ Recording after the change, or skipping step 2, produces nothing to animate.
4242
+
4243
+ It writes ordinary keyframes, so the transition scrubs on the timeline, exports through the MP4 / SMIL / Lottie paths, and survives a reload. Only properties that ACTUALLY changed are written — a track that also pinned the unchanged ones would fight anything else animating them, and a flip of position would quietly freeze an item's own pulse.
4244
+
4245
+ Rotation is compared on the shortest arc: 359 to 1 degrees is a two-degree move, not a near-full spin the wrong way.
4246
+
4247
+ READ THE RESULT: 'entered' are items that appeared during the change and were given an entrance rather than popping in. 'left' are items that went away — they cannot be animated out, because they are already gone. Remove things AFTER the flip, not before.
4248
+
4249
+ EXAMPLE:
4250
+ { "action": "apply", "duration": 0.6, "easing": "easeInOut", "stagger": { "each": 0.05, "from": "center" } }`,
4251
+ inputSchema: {
4252
+ type: 'object',
4253
+ properties: {
4254
+ action: {
4255
+ type: 'string',
4256
+ enum: ['record', 'apply'],
4257
+ description: "'record' before the change, 'apply' after it (default: record)",
4258
+ },
4259
+ itemIds: {
4260
+ type: 'array',
4261
+ items: { type: 'string' },
4262
+ description: 'record: which items to capture (defaults to everything on the item layer)',
4263
+ },
4264
+ duration: { type: 'number', description: 'apply: seconds the transition runs' },
4265
+ easing: { type: 'string', description: 'apply: named easing for the generated keyframes' },
4266
+ enter: { type: 'string', description: 'apply: entrance preset for items that appeared during the change' },
4267
+ stagger: {
4268
+ type: 'object',
4269
+ properties: {
4270
+ each: { type: 'number', description: 'Seconds between neighbours' },
4271
+ amount: { type: 'number', description: 'Total seconds for the whole run — overrides each' },
4272
+ from: {
4273
+ anyOf: [
4274
+ { type: 'string', enum: ['start', 'end', 'center', 'edges', 'random'] },
4275
+ { type: 'number' },
4276
+ { type: 'array', items: { type: 'number' } },
4277
+ ],
4278
+ description: 'Named origin, an item index, or [fx, fy] fractions for a grid',
4279
+ },
4280
+ grid: { type: 'array', items: { type: 'number' }, description: '[rows, columns]' },
4281
+ axis: { type: 'string', enum: ['x', 'y'], description: 'Restrict a grid stagger to one axis' },
4282
+ ease: {
4283
+ type: 'string',
4284
+ enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInCubic', 'easeOutCubic'],
4285
+ description: 'Curve that distributes the START TIMES',
4286
+ },
4287
+ seed: { type: 'number', description: 'Seed for the random origin' },
4288
+ },
4289
+ description: "apply: stagger the transition across the items, in the items' own order",
4290
+ },
4291
+ },
4292
+ },
4293
+ },
4294
+ {
4295
+ name: 'pinepaper_scene_graph',
4296
+ annotations: {
4297
+ title: 'Scene Graph (Interactive Stories / Quizzes)',
4298
+ readOnlyHint: false,
4299
+ destructiveHint: false,
4300
+ idempotentHint: false,
4301
+ openWorldHint: false,
4302
+ },
4303
+ description: `Create an interactive multi-node story or quiz card graph. Compiles cards, answer buttons, click-to-event routing, exclusive-group mutex visibility, and scoring into native items and relations.
4304
+
4305
+ ACTIONS:
4306
+ - create (default): build the graph on the canvas.
4307
+ - validate: run the same structural check WITHOUT drawing anything. Returns errors, warnings, the nodes reachable from start, and any cycles. Cheap — use it on a graph you generated before committing a canvas full of cards to it.
4308
+
4309
+ HOW IT WORKS:
4310
+ 1. Provide a graph with nodes and choices (start node, question cards with answers pointing to other node IDs, end cards with outcome text).
4311
+ 2. The engine compiles the card stack, creates panel rectangles and styled text items, wires on_click_fire -> event -> on_event_set_active relations, and binds active card mutex state.
4312
+ 3. If scoreItemId is provided, correct answers automatically wire on_event_increment to track score.
4313
+
4314
+ TWO WAYS OUT OF A CARD: 'answers' waits for a click (a quiz). 'next' advances on its own after 'duration' seconds (a linear story beat). Every non-terminal node needs one or the other, or the player is stuck; mark outcome cards kind: 'end'.
4315
+
4316
+ READ THE RESULT: 'failed' counts relations that did not wire. A graph can render completely and still be inert, and that count is the only thing that says so. 'cycles' reports loops — legitimate for a retry, an accidental one is a game that never ends.
4317
+
4318
+ EXAMPLE — Interactive Quiz:
4319
+ {
4320
+ "graph": {
4321
+ "start": "q1",
4322
+ "nodes": [
4323
+ {
4324
+ "id": "q1",
4325
+ "prompt": "Which planet is known as the Red Planet?",
4326
+ "answers": [
4327
+ { "text": "Mars", "correct": true, "to": "win" },
4328
+ { "text": "Venus", "to": "lose" }
4329
+ ]
4330
+ },
4331
+ { "id": "win", "kind": "end", "text": "Correct! Mars is rich in iron oxide." },
4332
+ { "id": "lose", "kind": "end", "text": "Not quite! Mars is the Red Planet." }
4333
+ ]
4334
+ },
4335
+ "opts": {
4336
+ "style": { "panelColor": "#1E1B4B", "textColor": "#F8FAFC", "answerColor": "#93C5FD" }
4337
+ }
4338
+ }`,
4339
+ inputSchema: {
4340
+ type: 'object',
4341
+ properties: {
4342
+ action: {
4343
+ type: 'string',
4344
+ enum: ['create', 'validate'],
4345
+ description: "'create' builds it (default) · 'validate' checks it without drawing",
4346
+ },
4347
+ graph: {
4348
+ type: 'object',
4349
+ properties: {
4350
+ start: { type: 'string', description: 'Initial active node id' },
4351
+ nodes: {
4352
+ type: 'array',
4353
+ items: {
4354
+ type: 'object',
4355
+ properties: {
4356
+ id: { type: 'string', description: 'Unique node id' },
4357
+ prompt: { type: 'string', description: 'Prompt text for question cards' },
4358
+ text: { type: 'string', description: 'Text for message/end cards' },
4359
+ kind: { type: 'string', enum: ['node', 'card', 'end'], description: "Card kind: 'node'/'card' (content) or 'end' (terminal)" },
4360
+ answers: {
4361
+ type: 'array',
4362
+ items: {
4363
+ type: 'object',
4364
+ properties: {
4365
+ text: { type: 'string', description: 'Answer button text' },
4366
+ correct: { type: 'boolean', description: 'Is correct answer' },
4367
+ to: { type: 'string', description: 'Destination node id' },
4368
+ },
4369
+ required: ['text', 'to'],
4370
+ },
4371
+ description: 'Interactive answer buttons — the card waits for a click',
4372
+ },
4373
+ next: { type: 'string', description: 'Unconditional destination — the card auto-advances instead of waiting for a click. Use INSTEAD of answers.' },
4374
+ duration: { type: 'number', description: 'Seconds the card holds before `next` fires (default 3). Ignored without `next`.' },
4375
+ outcome: { type: 'string', enum: ['win', 'lose'], description: 'Force a terminal node\u2019s outcome. Without it the engine infers it from the wording, which only works in English.' },
4376
+ },
4377
+ required: ['id'],
4378
+ },
4379
+ description: 'List of graph nodes',
4380
+ },
4381
+ },
4382
+ required: ['nodes'],
4383
+ description: 'Scene graph definition',
4384
+ },
4385
+ opts: {
4386
+ type: 'object',
4387
+ properties: {
4388
+ width: { type: 'number', description: 'Layout width' },
4389
+ height: { type: 'number', description: 'Layout height' },
4390
+ centerX: { type: 'number', description: 'Center X coordinate' },
4391
+ centerY: { type: 'number', description: 'Center Y coordinate' },
4392
+ groupName: { type: 'string', description: 'Seed / prefix for IDs' },
4393
+ scoreItemId: { type: 'string', description: 'Scoreboard item ID to increment on correct answers' },
4394
+ subject: { type: 'string', description: "Theme of the piece, folded into each card's text-effect selection (e.g. 'space exploration')" },
4395
+ plain: { type: 'boolean', description: 'Plain mode without entrance/text effects' },
4396
+ style: {
4397
+ type: 'object',
4398
+ properties: {
4399
+ panelColor: { type: 'string', description: 'Card panel background color' },
4400
+ textColor: { type: 'string', description: 'Card text color' },
4401
+ answerColor: { type: 'string', description: 'Answer button text color' },
4402
+ headlineSize: { type: 'number', description: 'Prompt font size' },
4403
+ bodySize: { type: 'number', description: 'Answer/body font size' },
4404
+ },
4405
+ },
4406
+ },
4407
+ description: 'Layout and styling options',
4408
+ },
4409
+ },
4410
+ required: ['graph'],
4411
+ },
4412
+ },
3337
4413
  // ---------------------------------------------------------------------------
3338
4414
  // LAYER 4 — COMPOSITIONS: SELECTION, TRANSFORM & HISTORY TOOLS
3339
4415
  // ---------------------------------------------------------------------------
@@ -3464,6 +4540,11 @@ flags as a badly-composed scene, and they do not survive an artboard change.`,
3464
4540
  inputSchema: {
3465
4541
  type: 'object',
3466
4542
  properties: {
4543
+ 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.' },
4544
+ level: { type: 'number', description: 'Craft level 1-4 within the register: sketch, competent, refined, art-directed.' },
4545
+ 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." },
4546
+ stitch: { type: 'string', enum: [...THREAD_STITCHES], description: 'Which stitch when medium is thread (default longAndShort) — the same six pinepaper_design_medium offers.' },
4547
+ stitchBudget: { type: 'number', description: 'Total marks across the composition (default 6000); the stitch scales to fit rather than the fill being cut short.' },
3467
4548
  action: { type: 'string', enum: ['list_patterns', 'list_treatments', 'apply', 'set_treatment'] },
3468
4549
  pattern: { type: 'string', description: 'Pattern key from list_patterns' },
3469
4550
  itemIds: { type: 'array', items: { type: 'string' }, description: 'Items in slot order' },
@@ -3566,7 +4647,10 @@ Overrides are the point: an instance can differ from its master and still receiv
3566
4647
  },
3567
4648
  componentKey: { type: 'string', description: 'Which part of the component to override' },
3568
4649
  prop: { type: 'string', description: 'Property to override, e.g. fillColor or content' },
3569
- value: { description: 'Override value' },
4650
+ value: {
4651
+ anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }, { type: 'object' }, { type: 'array' }],
4652
+ description: 'Override value',
4653
+ },
3570
4654
  overrides: { type: 'object', description: 'Overrides applied at instantiate time' },
3571
4655
  },
3572
4656
  required: ['action'],
@@ -4006,6 +5090,88 @@ ACTIONS:
4006
5090
  required: ['action'],
4007
5091
  },
4008
5092
  },
5093
+ {
5094
+ name: 'pinepaper_query_capabilities',
5095
+ annotations: {
5096
+ title: 'Query Capabilities',
5097
+ readOnlyHint: true,
5098
+ destructiveHint: false,
5099
+ idempotentHint: true,
5100
+ openWorldHint: false,
5101
+ },
5102
+ description: `Discover and match capabilities across the engine — text styles, character-level text effects, generators, vertex deforms, and behavioral relations.
5103
+
5104
+ ACTIONS:
5105
+ - list: List all available capabilities or filter by kind (style, effect, generator, deform, relation, etc.).
5106
+ - choose: Mood- and context-weighted recommendation based on subject/prompt words and target feeling.
5107
+ - coverage: Report total indexable and describable capabilities across categories.
5108
+ - find: Direct lookup of a capability by key to inspect its description and how to apply it (applyWith).
5109
+
5110
+ MOOD VOCABULARY FOR CHOOSE:
5111
+ - reveal: resolve, reveal, appear, emerge, decode, decrypt, unscramble, form, assemble
5112
+ - triumphant: burst, launch, firework, bloom, spark, explode, shine, glow, celebrate, rise
5113
+ - failure: crumble, fall, collapse, burn, decay, dissolve, break, shatter, sink, error
5114
+ - calm: drift, float, gentle, slow, settle, fade, wave, ripple, pour
5115
+ - technical: binary, matrix, grid, scan, code, digital, circuit, data, print, terminal
5116
+ - energetic: bounce, swarm, spray, shoot, fast, rapid, scatter, slam, volley
5117
+
5118
+ EXAMPLE — Choose effect for a victory card:
5119
+ {
5120
+ "action": "choose",
5121
+ "kind": "effect",
5122
+ "mood": "triumphant",
5123
+ "subject": "celebrate correct answer",
5124
+ "seed": "quiz_q1"
5125
+ }`,
5126
+ inputSchema: {
5127
+ type: 'object',
5128
+ properties: {
5129
+ action: {
5130
+ type: 'string',
5131
+ enum: ['list', 'choose', 'coverage', 'find', 'catalogue', 'studio'],
5132
+ 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.",
5133
+ },
5134
+ catalogue: {
5135
+ type: 'string',
5136
+ enum: ['rig_presets', 'shader_effects', 'stroke_decorations', 'precomps', 'images', 'segment_edit_kinds', 'shatter_orders', 'world_meshes', 'relation_presets'],
5137
+ 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.",
5138
+ },
5139
+ kind: {
5140
+ anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
5141
+ description: 'Filter by capability kind (style, effect, deform, entrance, animation, generator, collage, palette, mask, cutout, relation)',
5142
+ },
5143
+ mood: {
5144
+ anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
5145
+ description: "Target feeling for choose: reveal, triumphant, failure, calm, technical, energetic",
5146
+ },
5147
+ subject: {
5148
+ type: 'string',
5149
+ description: 'Contextual prompt/subject text to score against capability definitions',
5150
+ },
5151
+ avoid: {
5152
+ anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
5153
+ description: 'Mood keys to penalize — only the six vocabulary keys above are matched, arbitrary words are ignored',
5154
+ },
5155
+ seed: {
5156
+ type: 'string',
5157
+ description: 'Deterministic seed string for reproducible selection',
5158
+ },
5159
+ key: {
5160
+ type: 'string',
5161
+ description: 'Capability key to look up (for find action)',
5162
+ },
5163
+ exclude: {
5164
+ type: 'array',
5165
+ items: { type: 'string' },
5166
+ description: 'Keys to exclude from choose recommendation',
5167
+ },
5168
+ warm: {
5169
+ type: 'boolean',
5170
+ description: 'Load the lazy registries before answering (default true). Generators and the rigging/blending/deform relation rules only exist once touched, so a cold answer omits them — pass false only to cheaply re-read what is already resident.',
5171
+ },
5172
+ },
5173
+ },
5174
+ },
4009
5175
  // ---------------------------------------------------------------------------
4010
5176
  // DEFORMATION, SPRITE SHEETS, INTERACTION
4011
5177
  // ---------------------------------------------------------------------------
@@ -4428,6 +5594,10 @@ RETURNS:
4428
5594
  inputSchema: {
4429
5595
  type: 'object',
4430
5596
  properties: {
5597
+ id: {
5598
+ type: 'string',
5599
+ 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.',
5600
+ },
4431
5601
  sourceItemId: {
4432
5602
  type: 'string',
4433
5603
  description: 'Registry ID of the source item (where arrow starts)',
@@ -4521,6 +5691,10 @@ Same as pinepaper_connect: routing, lineColor, lineWidth, lineStyle, headStyle,
4521
5691
  inputSchema: {
4522
5692
  type: 'object',
4523
5693
  properties: {
5694
+ id: {
5695
+ type: 'string',
5696
+ 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.',
5697
+ },
4524
5698
  sourceItemId: {
4525
5699
  type: 'string',
4526
5700
  description: 'Registry ID of the source item',
@@ -5554,7 +6728,7 @@ USE WHEN:
5554
6728
  - Celebration effects (confetti)
5555
6729
  - Enhancing visual impact (ripple, glow, electric)
5556
6730
 
5557
- EFFECTS (15 particle + 3 shader auras):
6731
+ EFFECTS (15 particle + 7 shader auras):
5558
6732
  - sparkle: Glitter/sparkle particles (color, speed, size)
5559
6733
  - blast: Explosion burst effect (color, radius, count)
5560
6734
  - smoke: Rising smoke plumes (color, speed, size, drift, height, growthRate)
@@ -5575,6 +6749,11 @@ SHADER AURAS (WebGL2, silhouette-clipped — same tool, routed to the aura syste
5575
6749
  - heatmap: Thermal shimmer aura (palette 0=Hot 1=Cool 2=Violet)
5576
6750
  - liquid_metal: Flowing metallic sheen (palette 0=Chrome 1=Gold 2=Copper)
5577
6751
  - gem_smoke: Crystalline smoke wisps
6752
+ - electric_arc: Branching discharge filaments with a hot core and inverse-square bloom. Stepped flicker, not a sine — a stroke is a train of return strokes, which reads far more electric than a smooth pulse.
6753
+ - 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.
6754
+ - rain_veil: Drifting sheets of falling rain across the silhouette.
6755
+ - caustics: Refracted light patterns, as through moving water.
6756
+ - 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.
5578
6757
  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.
5579
6758
 
5580
6759
  EXAMPLE — bubbles with magenta interior over a blue rim:
@@ -5587,7 +6766,7 @@ EXAMPLE — gold liquid-metal aura outside a logo:
5587
6766
  itemId: { type: 'string', description: 'Registry ID of the item' },
5588
6767
  effectType: {
5589
6768
  type: 'string',
5590
- enum: ['sparkle', 'blast', 'smoke', 'fire', 'rain', 'snow', 'confetti', 'ripple', 'glow', 'electric', 'bubbles', 'dust', 'fireflies', 'shockwave', 'trail', 'heatmap', 'liquid_metal', 'gem_smoke'],
6769
+ 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'],
5591
6770
  description: 'Type of effect',
5592
6771
  },
5593
6772
  params: {
@@ -6068,7 +7247,7 @@ PRIVACY: No screenshots, no tool call arguments, no user content — only struct
6068
7247
  idempotentHint: false,
6069
7248
  openWorldHint: false,
6070
7249
  },
6071
- 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.
7250
+ 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.
6072
7251
 
6073
7252
  ⚠️ Call ONCE per pipeline. NEVER restart — creates duplicates.
6074
7253
 
@@ -6347,7 +7526,7 @@ EXAMPLE — Animated sky scene with timed reveals:
6347
7526
  // Loop animation
6348
7527
  animationType: {
6349
7528
  type: 'string',
6350
- enum: ['pulse', 'rotate', 'bounce', 'fade', 'wobble', 'slide', 'typewriter'],
7529
+ enum: ['bounce', 'breathe', 'fade', 'glow', 'jelly', 'path', 'pulse', 'rotate', 'scrollDown', 'scrollLeft', 'scrollRight', 'scrollUp', 'shake', 'slideLeftRight', 'slideUpDown', 'swing', 'typewriter', 'wobble'],
6351
7530
  description: 'For animate: loop animation type',
6352
7531
  },
6353
7532
  animationOptions: { type: 'object', description: 'For animate: {speed, amplitude, direction}' },
@@ -6371,7 +7550,7 @@ EXAMPLE — Animated sky scene with timed reveals:
6371
7550
  targetId: { type: 'string', description: 'For relation: target item ID or $N' },
6372
7551
  relationType: {
6373
7552
  type: 'string',
6374
- enum: ['orbits', 'follows', 'attached_to', 'points_at', 'mirrors', 'parallax', 'animates', 'grows_from', 'staggered_with', 'wave_through', 'circumscribes', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'maintains_distance', 'bounds_to', 'indicates', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'construction_reveal', 'driven_by', 'time_expression', 'on_event_fire_after', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_visibility', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with'],
7553
+ 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'],
6375
7554
  description: 'For relation: type',
6376
7555
  },
6377
7556
  relationOptions: { type: 'object', description: 'For relation: options' },
@@ -6386,7 +7565,7 @@ EXAMPLE — Animated sky scene with timed reveals:
6386
7565
  // Effects
6387
7566
  effectType: {
6388
7567
  type: 'string',
6389
- enum: ['sparkle', 'blast', 'smoke', 'fire', 'rain', 'snow', 'confetti', 'ripple', 'glow', 'electric', 'bubbles', 'dust', 'fireflies', 'shockwave', 'trail', 'heatmap', 'liquid_metal', 'gem_smoke'],
7568
+ 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'],
6390
7569
  description: 'For apply_effect: effect type',
6391
7570
  },
6392
7571
  effectParams: { type: 'object', description: 'For apply_effect: parameters' },
@@ -6458,8 +7637,17 @@ EXAMPLES:
6458
7637
  },
6459
7638
  format: {
6460
7639
  type: 'string',
6461
- enum: ['svg', 'png', 'gif', 'mp4', 'webm', 'pdf'],
6462
- description: 'Override format (auto-detected if not specified)',
7640
+ enum: ['svg', 'png', 'gif', 'mp4', 'webm', 'pdf', 'wav'],
7641
+ 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.",
7642
+ },
7643
+ sampleRate: {
7644
+ type: 'number',
7645
+ description: 'wav only: samples per second (default 48000). Rejected for any other format.',
7646
+ },
7647
+ bitDepth: {
7648
+ type: 'number',
7649
+ enum: [16, 32],
7650
+ description: 'wav only: 16 (default) or 32-bit float. Rejected for any other format.',
6463
7651
  },
6464
7652
  quality: {
6465
7653
  type: 'string',
@@ -6473,7 +7661,7 @@ EXAMPLES:
6473
7661
  },
6474
7662
  estimateOnly: {
6475
7663
  type: 'boolean',
6476
- 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).',
7664
+ 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.',
6477
7665
  },
6478
7666
  duration: {
6479
7667
  type: 'number',
@@ -7223,6 +8411,9 @@ ACTIONS:
7223
8411
  required: ['action'],
7224
8412
  },
7225
8413
  },
8414
+ // ---------------------------------------------------------------------------
8415
+ // STITCHCRAFT PROCEDURAL EMBROIDERY & THREAD
8416
+ // ---------------------------------------------------------------------------
7226
8417
  ];
7227
8418
  /**
7228
8419
  * Get a tool definition by name