@pinepaper.studio/mcp-server 1.6.26 → 1.6.28

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 (90) hide show
  1. package/README.md +155 -2
  2. package/SECURITY.md +4 -3
  3. package/dist/agent/types.d.ts +3 -3
  4. package/dist/browser/puppeteer-controller.d.ts +11 -0
  5. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  6. package/dist/browser/puppeteer-controller.js +38 -2
  7. package/dist/browser/puppeteer-controller.js.map +1 -1
  8. package/dist/build-info.json +2 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/ontology/hints.d.ts +3 -1
  13. package/dist/ontology/hints.d.ts.map +1 -1
  14. package/dist/ontology/hints.js +4 -2
  15. package/dist/ontology/hints.js.map +1 -1
  16. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  17. package/dist/ontology/upstream-vocabulary.js +2 -2
  18. package/dist/ontology/vocabulary.d.ts.map +1 -1
  19. package/dist/ontology/vocabulary.js +2 -0
  20. package/dist/ontology/vocabulary.js.map +1 -1
  21. package/dist/tools/choreograph.d.ts.map +1 -1
  22. package/dist/tools/choreograph.js +7 -0
  23. package/dist/tools/choreograph.js.map +1 -1
  24. package/dist/tools/definitions.d.ts.map +1 -1
  25. package/dist/tools/definitions.js +58 -28
  26. package/dist/tools/definitions.js.map +1 -1
  27. package/dist/tools/engine-lists.d.ts +20 -3
  28. package/dist/tools/engine-lists.d.ts.map +1 -1
  29. package/dist/tools/engine-lists.js +156 -3
  30. package/dist/tools/engine-lists.js.map +1 -1
  31. package/dist/tools/engine-methods.d.ts.map +1 -1
  32. package/dist/tools/engine-methods.js +2 -0
  33. package/dist/tools/engine-methods.js.map +1 -1
  34. package/dist/tools/engine-surface.d.ts +1 -1
  35. package/dist/tools/engine-surface.d.ts.map +1 -1
  36. package/dist/tools/engine-surface.js +19 -4
  37. package/dist/tools/engine-surface.js.map +1 -1
  38. package/dist/tools/export-file-receipt.d.ts +64 -0
  39. package/dist/tools/export-file-receipt.d.ts.map +1 -1
  40. package/dist/tools/export-file-receipt.js +91 -13
  41. package/dist/tools/export-file-receipt.js.map +1 -1
  42. package/dist/tools/expression-grammar.d.ts +10 -0
  43. package/dist/tools/expression-grammar.d.ts.map +1 -0
  44. package/dist/tools/expression-grammar.js +33 -0
  45. package/dist/tools/expression-grammar.js.map +1 -0
  46. package/dist/tools/handlers/export.d.ts.map +1 -1
  47. package/dist/tools/handlers/export.js +58 -4
  48. package/dist/tools/handlers/export.js.map +1 -1
  49. package/dist/tools/handlers/generate.js +2 -2
  50. package/dist/tools/handlers/generate.js.map +1 -1
  51. package/dist/tools/handlers.d.ts +5 -0
  52. package/dist/tools/handlers.d.ts.map +1 -1
  53. package/dist/tools/handlers.js +74 -2
  54. package/dist/tools/handlers.js.map +1 -1
  55. package/dist/tools/lane.d.ts +10 -5
  56. package/dist/tools/lane.d.ts.map +1 -1
  57. package/dist/tools/lane.js +49 -8
  58. package/dist/tools/lane.js.map +1 -1
  59. package/dist/tools/scene-cache-export.d.ts +52 -0
  60. package/dist/tools/scene-cache-export.d.ts.map +1 -0
  61. package/dist/tools/scene-cache-export.js +231 -0
  62. package/dist/tools/scene-cache-export.js.map +1 -0
  63. package/dist/tools/shape-params.d.ts +2 -2
  64. package/dist/tools/shape-params.js +2 -2
  65. package/dist/tools/stick-vocabulary.d.ts +1 -1
  66. package/dist/tools/stick-vocabulary.js +1 -1
  67. package/dist/tools/toolkits.d.ts +1 -1
  68. package/dist/tools/toolkits.d.ts.map +1 -1
  69. package/dist/tools/toolkits.js +8 -2
  70. package/dist/tools/toolkits.js.map +1 -1
  71. package/dist/types/code-generator.d.ts +1 -0
  72. package/dist/types/code-generator.d.ts.map +1 -1
  73. package/dist/types/code-generator.js +536 -255
  74. package/dist/types/code-generator.js.map +1 -1
  75. package/dist/types/schemas.d.ts +238 -201
  76. package/dist/types/schemas.d.ts.map +1 -1
  77. package/dist/types/schemas.js +14 -24
  78. package/dist/types/schemas.js.map +1 -1
  79. package/dist/utils/asset-paths.d.ts.map +1 -1
  80. package/dist/utils/asset-paths.js +11 -0
  81. package/dist/utils/asset-paths.js.map +1 -1
  82. package/dist/utils/capabilities.d.ts +15 -0
  83. package/dist/utils/capabilities.d.ts.map +1 -0
  84. package/dist/utils/capabilities.js +63 -0
  85. package/dist/utils/capabilities.js.map +1 -0
  86. package/dist/utils/env.d.ts.map +1 -1
  87. package/dist/utils/env.js +2 -1
  88. package/dist/utils/env.js.map +1 -1
  89. package/manifest.json +18 -2
  90. package/package.json +1 -1
@@ -13,7 +13,8 @@ import { craftToolDefinitions } from './craft-tools.js';
13
13
  import { TOOL_TAGS } from './toolkits.js';
14
14
  import { STICK_GAITS, STICK_POSES, STICK_SEQUENCES, STICK_EXPRESSIONS, STICK_PROPS } from './stick-vocabulary.js';
15
15
  import { AgentBatchOperationTypeSchema, GeneratorNameSchema, ItemTypeSchema } from '../types/schemas.js';
16
- import { DESIGN_MEDIA, DESIGN_MEDIA_APPLY, FILTER_TYPES, FILTER_DOCS, LINEAGE_KINDS, STYLED_SCENE_STYLES, STYLED_SCENE_STYLE_INFO, PATTERN_KINDS } from './engine-lists.js';
16
+ import { expressionGrammarText } from './expression-grammar.js';
17
+ import { DESIGN_MEDIA, DESIGN_MEDIA_APPLY, FILTER_TYPES, FILTER_DOCS, LINEAGE_KINDS, STYLED_SCENE_STYLES, STYLED_SCENE_STYLE_INFO, PATTERN_KINDS, ANIMATION_TYPES, ANIMATION_WAVEFORMS, ANIMATION_ORIGINS } from './engine-lists.js';
17
18
  import { EffectTypeSchema, AgentExportFormatSchema, RelationTypeSchema, SimpleAnimationTypeSchema, EasingSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, KEYFRAME_EASING_JSON_SCHEMA, GEOMETRY_OPERATIONS, THREAD_STITCHES, } from '../types/schemas.js';
18
19
  /**
19
20
  * Vocabulary lists derived from Zod schemas so tool descriptions stay in
@@ -1639,7 +1640,7 @@ warp (optional): chained parametric warp(s) — displace each sampled point by {
1639
1640
 
1640
1641
  RETURNS: { success, kind, itemId } — itemId is the created path. Bad expressions / empty ranges return { success:false, error }.
1641
1642
 
1642
- To make an item TRAVEL along an equation curve (instead of drawing it), use pinepaper_add_relation relationType moves_along_path with params.equation — but note that equation is read ONLY on the relation's signal path. An option that leaves that path takes the equation with it and the item simply does not move: easing 'bounce' is piecewise and falls back, so bounce + equation is a static item reporting success. Use a signal-friendly easing (linear, easeIn, easeOut, easeInOut, sine, pingpong) with an equation, or supply params.path instead.
1643
+ To make an item TRAVEL along an equation curve (instead of drawing it), use pinepaper_add_relation relationType moves_along_path with params.equation. The same flipY applies there: by default y is math-up, so the item rides the curve mirrored about cy compared with screen coordinates (y down). Pass flipY: false to author the curve in screen coordinates.
1643
1644
 
1644
1645
  EXAMPLES:
1645
1646
  - Sine wave: { kind: "function", expr: "sin(x/40) * 60", min: -300, max: 300, style: { strokeColor: "#4f46e5" } }
@@ -3069,7 +3070,7 @@ The engine names each refusal — "a boolean needs at least two paths", "that pa
3069
3070
  description: `A real 3D WORLD under the canvas — depth-buffered terrain, sky, sun shadows, an addressable actor stage and a directed camera. ADDITIVE: Paper.js keeps every vector item, hit-test and export path; the world composites underneath the (transparent) Paper canvas.
3070
3071
 
3071
3072
  ACTIONS:
3072
- - create: { spec? ('forest' | 'snowMountain' | 'field' | 'jungle' | a full spec object), character? (true/config → a walkable character) } → { configurableKeys }
3073
+ - create: { spec? ('forest' | 'snowMountain' | 'field' | 'jungle' | a full spec object), character? (true/config → a walkable character), control? (true: a viewer walks the character with the keyboard — arrows/WASD, Space jumps, Shift runs — when the scene plays in the studio; the editor's own shortcuts pause while it is on) } → { configurableKeys, control }
3073
3074
  - describe: {} → the ENGINE'S OWN parameter schema — every world parameter with type, range and description. Call this before configure; the tool does not restate the schema, so it cannot drift from the engine.
3074
3075
  - configure: { patch } — live deep-merged change to anything describe lists (colours, fog, sun, terrain amplitude/ridge, seed…). Schema-validated: a wrong key errors NAMING the right one, never a silent no-op.
3075
3076
  - 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.
@@ -3131,6 +3132,10 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
3131
3132
  anyOf: [{ type: 'boolean' }, { type: 'object' }],
3132
3133
  description: 'create: include the walkable character (boolean or config).',
3133
3134
  },
3135
+ control: {
3136
+ type: 'boolean',
3137
+ description: 'create: true lets a viewer walk the character with the keyboard (arrows/WASD, Space jumps, Shift runs) when the scene plays in the studio. Needs the character (not character: false).',
3138
+ },
3134
3139
  patch: { type: 'object', description: 'configure: partial world spec, validated against describe. Colours (env.zenith, env.horizon, env.fogColor, env.sunColor, env.ambient, env.lowColor … env.propColorB) take hex ("#03050c") or [r, g, b] in 0..1.' },
3135
3140
  actorId: { type: 'string', description: 'Actor id.' },
3136
3141
  x: { type: 'number', description: 'add_actor: world x.' },
@@ -3420,6 +3425,8 @@ EXAMPLE doc: { "nodes": [ {"id":"bar","type":"pp:Rectangle","width":300,"height"
3420
3425
  description: `Audit the LIVE scene's relational structure (FxTool S12-E2) — read-only, nothing is changed. Answers "is this scene composed with relations, or just a pile of absolutely-positioned items?" and proposes fixes.
3421
3426
 
3422
3427
  RETURNS:
3428
+ - relations: every relation bound — [{ from, fromName?, to, type, params }], the first 200 (relationCount says how many when there are more). Use it to find the relation to change, then a batch relation op with update: true.
3429
+ - customRelations: each pinepaper_register_custom_relation definition as it was given (name, params, computeFunction, applyFunction).
3423
3430
  - density: { itemCount, relationalItemCount, relationalDensity (0..1 — fraction of items touching the relation graph), edgeCount, componentCount }. A low relationalDensity means items are placed by hardcoded coordinates and won't hold together if something moves.
3424
3431
  - suggestions: structural relations for unrelated item pairs that ALREADY sit in a structural configuration — [{ from, to, type (on_top_of/below/beside/inside/centered_on/aligned_with), params, confidence }], ranked by confidence. Apply the good ones with pinepaper_add_relation to make the layout robust.
3425
3432
 
@@ -3454,6 +3461,8 @@ PARAMS: { eps? (bounds-match tolerance px, default 4), cap? (max suggestions, de
3454
3461
  MOVES_ALONG_PATH — ride a path (a hand tracing a stroke, a car on a road):
3455
3462
  - sourceId: the item that moves. The path comes from ONE of: targetId (or params.pathId) naming a PATH ITEM, sampled into points for you; params.path as points [[x, y], …]; or params.equation.
3456
3463
  - params.duration: seconds for one traversal (or pass speed directly, 1 ≈ 150 px/s). params.delay: seconds on the timeline before it starts; until then the item waits at the path start. params.offset: [dx, dy] added to the point on the path — set it to (centre - pen tip) so a hand image's TIP, not its centre, rides the stroke. closed (loop back to the start; default from the path item), phase (0–1 start point), easing (linear | easeIn | easeOut | easeInOut | sine | bounce | pingpong), signal (deterministic, frame-rate-invariant — prefer it for export).
3464
+ - The item travels at CONSTANT SPEED along the route: the fraction of the duration elapsed is the fraction of the route's LENGTH covered, not of the equation's parameter or of the point count. Where the curve is long it moves the same speed as where it is short; easing reshapes that progress. For a route timed by the parameter instead, key the position with pinepaper_keyframe_animate.
3465
+ - params.equation is in math coordinates with y UP (flipY: true by default), placed at cx, cy: on screen the curve is mirrored about cy. Write flipY: false to author it in screen coordinates (y down).
3457
3466
  - With no path from any of those it is refused, not accepted as a still item.
3458
3467
 
3459
3468
  USE WHEN:
@@ -3489,8 +3498,9 @@ RELATION TYPES:
3489
3498
  - 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.
3490
3499
  - 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.
3491
3500
  - tours / synced_to_audio: Move through a sequence of targets; bind playback to an audio track.
3492
- - connects_to: Diagram edge (params: via, sourcePort, targetPort, label).
3493
- - 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.
3501
+ - localizes_sound: a sound pans with an item. sourceId is the sound item (pinepaper_sound create's itemId), targetId the item it follows; the sound's stereo position follows the item across the frame, in the preview and in exported audio. params: axis (x | y, default x), spread (0..1: pan at the frame edge, default 0.85), reference (camera | canvas, default camera), distanceGain (true also quietens it away from the centre), smoothing (ms, default 80).
3502
+ - connects_to: Diagram edge from source to target (params: via = the connector's item id, sourcePort / targetPort = auto | top | bottom | left | right, label). A RECORD of the connection: it draws nothing itself.
3503
+ - attached_to_tail / head_points_to: source = the connector, target = the item; params: port (auto | top | bottom | left | right). They record which port each end belongs to; the diagram system is what moves a connector's ends when an item moves, and it does so only for connectors made with pinepaper_connect. On a plain line (create itemType 'line') they record the structure and move nothing: for a connector that follows the items, make it with pinepaper_connect.
3494
3504
  - 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.
3495
3505
  - 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.
3496
3506
  - 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.
@@ -3523,7 +3533,7 @@ RELATION COMPATIBILITY:
3523
3533
  Text→Text: font blend
3524
3534
  Cross-type: particle denoising transition
3525
3535
  - group_morphs_to: pair-by-index morph between two paper.Groups (any two — graph vertices+edges, letter collages, dashboard clusters). Path.Line children deform via endpoints; other children translate; excess children fade. Params: duration, hold, loop, easing (linear|easeIn|easeOut|easeInOut), deformLines.
3526
- - moves_along_path: self-relation (targetId=null); item is driven along a path. Source the path EITHER from params.path (array of {x,y} or [x,y]) OR from params.equation (S10 B5) — a math curve the item TRAVERSES, sampled deterministically on the Expression IR: params.equation = { kind: function|parametric|fourier|preset, expr | xExpr/yExpr | harmonics | preset, min, max, samples, scale, cx, cy, flipY, warp } (same equation contract as pinepaper_equation_path, but origin is cx/cy, default 0). READ ONLY ON THE SIGNAL PATH: easing 'bounce' is piecewise and leaves it, which silently discards the equation and leaves the item where it started. Params: path OR equation, speed, closed, phase, easing (linear|easeIn|easeOut|easeInOut|sine|bounce|pingpong).
3536
+ - moves_along_path: self-relation (targetId=null); item is driven along a path. Source the path EITHER from params.path (array of {x,y} or [x,y]) OR from params.equation (S10 B5) — a math curve the item TRAVERSES, sampled deterministically on the Expression IR: params.equation = { kind: function|parametric|fourier|preset, expr | xExpr/yExpr | harmonics | preset, min, max, samples, scale, cx, cy, flipY, warp } (same equation contract as pinepaper_equation_path, but origin is cx/cy, default 0). y is MATH-UP by default (flipY: true): on screen the curve is mirrored about cy, so a curve written in screen coordinates (y down) needs flipY: false. Params: path OR equation, speed, closed, phase, easing (linear|easeIn|easeOut|easeInOut|sine|bounce|pingpong).
3527
3537
 
3528
3538
  GEOMETRIC CONSTRUCTION CONSTRAINTS (Layer 2) — the source item is RE-DERIVED every frame from its anchor item(s), so dragging an anchor updates the dependent live (GeoGebra-style), and the construction persists as relation graph data (edit it by editing relations, not by re-running code). Anchor A is the relation target; extra anchors ride in params:
3529
3539
  - is_midpoint_of: source = midpoint(target, params.other). params.other = id of the second endpoint (B).
@@ -3535,7 +3545,7 @@ GEOMETRIC CONSTRUCTION CONSTRAINTS (Layer 2) — the source item is RE-DERIVED e
3535
3545
 
3536
3546
  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.
3537
3547
  - 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.
3538
- - 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.
3548
+ - time_expression: self-relation (targetId=null); the source's params.property (x, y, rotation, opacity or scale) is driven by a formula of t (scene time) and v (params.baseValue). params: property (default 'y'), expression (e.g. 'sin(t*2)*50 + v'), baseValue, signal (true also evaluates it as a pure f(t) via the Expression IR). The formula grammar: ${expressionGrammarText()}. A name outside it, or a wrong argument count, is refused by name: it would parse to nothing and the item would not move.
3539
3549
 
3540
3550
  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.
3541
3551
  - 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.
@@ -3578,7 +3588,7 @@ Relations are COMPOSITIONAL - an item can have multiple relations that work toge
3578
3588
  },
3579
3589
  relationType: {
3580
3590
  type: 'string',
3581
- 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_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', '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'],
3591
+ 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', 'localizes_sound', '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_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', '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'],
3582
3592
  description: 'Type of relationship. A name registered with pinepaper_register_custom_relation in this session is accepted too.',
3583
3593
  },
3584
3594
  params: {
@@ -3621,7 +3631,7 @@ USE WHEN:
3621
3631
  targetId: { type: 'string', description: 'Target item ID' },
3622
3632
  relationType: {
3623
3633
  type: 'string',
3624
- 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_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', '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'],
3634
+ 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', 'localizes_sound', '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_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', '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'],
3625
3635
  description: 'Specific relation type to remove (optional - removes all if not specified)',
3626
3636
  },
3627
3637
  },
@@ -3649,7 +3659,7 @@ USE WHEN:
3649
3659
  itemId: { type: 'string', description: 'Item to query relations for' },
3650
3660
  relationType: {
3651
3661
  type: 'string',
3652
- 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_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', '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'],
3662
+ 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', 'localizes_sound', '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_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', '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'],
3653
3663
  description: 'Filter by relation type (optional)',
3654
3664
  },
3655
3665
  direction: {
@@ -3664,15 +3674,17 @@ USE WHEN:
3664
3674
  {
3665
3675
  name: 'pinepaper_register_custom_relation',
3666
3676
  annotations: {
3667
- title: 'Register Custom Relation',
3677
+ title: 'Register Custom Relation (needs PINEPAPER_ALLOW_CODE)',
3668
3678
  readOnlyHint: false,
3669
3679
  destructiveHint: false,
3670
3680
  idempotentHint: false,
3671
3681
  openWorldHint: false,
3672
3682
  },
3673
- description: `PARAMS ARE DEFINITIONS, not values: { gap: { type: 'number', default: 60, min: 0 } } (type is number | string | boolean | array | object). Once registered, the name is a valid relationType for pinepaper_add_relation in this session.
3683
+ description: `SECURITY SWITCH: needs a server started with PINEPAPER_ALLOW_CODE=1 (off by default; only the person running the server can turn it on). Otherwise it is refused and nothing runs.
3684
+
3685
+ PARAMS ARE DEFINITIONS, not values: { gap: { type: 'number', default: 60, min: 0 } } (type is number | string | boolean | array | object). Once registered, the name is a valid relationType for pinepaper_add_relation in this session.
3674
3686
 
3675
- Register a custom relation type with compute and apply functions.
3687
+ Register a custom relation type with compute and apply functions. For motion without code, use time_expression.
3676
3688
 
3677
3689
  USE WHEN:
3678
3690
  - Creating physics-based behaviors (attraction, repulsion, collision)
@@ -3735,13 +3747,15 @@ applyFunction: \`
3735
3747
  {
3736
3748
  name: 'pinepaper_execute_custom_code',
3737
3749
  annotations: {
3738
- title: 'Execute Custom Code',
3750
+ title: 'Execute Custom Code (needs PINEPAPER_ALLOW_CODE)',
3739
3751
  readOnlyHint: false,
3740
3752
  destructiveHint: true,
3741
3753
  idempotentHint: false,
3742
3754
  openWorldHint: true,
3743
3755
  },
3744
- description: `Execute arbitrary JavaScript code in the PinePaper context.
3756
+ description: `SECURITY SWITCH: needs a server started with PINEPAPER_ALLOW_CODE=1 (off by default; only the person running the server can turn it on). Otherwise it is refused and nothing runs.
3757
+
3758
+ Execute arbitrary JavaScript code in the PinePaper context.
3745
3759
 
3746
3760
  USE WHEN:
3747
3761
  - Advanced Paper.js operations not covered by other tools
@@ -3841,6 +3855,9 @@ ANIMATION TYPES (18 — the list the driver actually switches on):
3841
3855
  - scrollLeft / scrollRight / scrollUp / scrollDown: Continuous scroll, wrapping
3842
3856
  - path: Travel along an assigned path
3843
3857
  - typewriter: Character-by-character reveal
3858
+ - ripple: An opacity wave. On a GROUP it sweeps through the children outward from origin, each a little later; on a single item it pulses the item.
3859
+ - undulate: A vertical lift wave, swept through a group's children the same way (intensity 0.4 ≈ 14 px).
3860
+ ripple and undulate take waveform (the shape of one cycle) and origin (where the wave starts on a group). SVG and Lottie exports draw them static; export MP4, WebM or GIF to keep the wave.
3844
3861
 
3845
3862
  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.`,
3846
3863
  inputSchema: {
@@ -3849,7 +3866,7 @@ There is NO 'slide'. An unknown type is stored and then ignored — the item sit
3849
3866
  itemId: { type: 'string', description: 'Registry ID of the item' },
3850
3867
  animationType: {
3851
3868
  type: 'string',
3852
- enum: ['bounce', 'breathe', 'fade', 'glow', 'jelly', 'path', 'pulse', 'rotate', 'scrollDown', 'scrollLeft', 'scrollRight', 'scrollUp', 'shake', 'slideLeftRight', 'slideUpDown', 'swing', 'typewriter', 'wobble'],
3869
+ enum: [...ANIMATION_TYPES],
3853
3870
  description: 'Type of animation',
3854
3871
  },
3855
3872
  speed: {
@@ -3864,6 +3881,8 @@ There is NO 'slide'. An unknown type is stored and then ignored — the item sit
3864
3881
  type: 'number',
3865
3882
  description: 'Animation start delay in seconds.',
3866
3883
  },
3884
+ waveform: { type: 'string', enum: [...ANIMATION_WAVEFORMS], description: 'ripple / undulate: the shape of one cycle (default sine).' },
3885
+ origin: { type: 'string', enum: [...ANIMATION_ORIGINS], description: 'ripple / undulate on a group: where the wave starts (default center).' },
3867
3886
  },
3868
3887
  required: ['itemId', 'animationType'],
3869
3888
  },
@@ -3906,7 +3925,7 @@ ANIMATABLE PROPERTIES:
3906
3925
  - strokeColor: Color string
3907
3926
  - strokeWidth, width, height, radius, shadowBlur, shadowColor: Numbers / colours
3908
3927
  - fontSize: Number
3909
- - TEXT (current studio): fontStyle ('normal' / 'italic' / 'oblique', discrete — switches at the next key), textAlign ('left' / 'center' / 'right', discrete; the anchor stays put), lineHeight (px, or a multiple of fontSize up to 5), letterSpacing (px, or a CSS length such as '0.1em'). An older studio stores these keys without drawing them; the result names any it did not animate (ignoredProperties).
3928
+ - TEXT (current studio): fontStyle ('normal' / 'italic' / 'oblique', discrete — switches at the next key), textAlign ('left' / 'center' / 'right', discrete; the anchor stays put), lineHeight (px, or a multiple of fontSize up to 5), letterSpacing (px, or a CSS length such as '0.1em'). An older studio stores these keys without drawing them; the result names any it did not animate (ignoredProperties). fontWeight keys STEP: a font with fixed weights switches from one weight to the next once, partway through the move, and does not ramp. Key fontSize or scale for a smooth change.
3910
3929
  - STROKE DRAW-ON (paths): trimStart, trimEnd (0-1 of the path's length), trimOffset (shifts the visible window along it), dashArray. A draw-on is trimEnd 0 -> 1.
3911
3930
  The authoritative list for a given item is pinepaper_get_animatable_properties — ask it rather than guessing.
3912
3931
 
@@ -3916,7 +3935,7 @@ ${EASING_LINES}
3916
3935
  - or a custom cubic-bezier [x1, y1, x2, y2] — x in 0..1, y may exceed 0..1 for overshoot. keyframe_animate and camera_animate only.
3917
3936
  COUNTERS: a text item with a counter (create_item / modify_item counter) also animates the numeric property counterValue, drawn through the counter's format — keyframe it to shape a count by hand (hold on a number, then race). Its keys override the counter's own from / to.
3918
3937
 
3919
- HOLD KEYS (where the studio has them): interpolation: 'hold' on a key holds ITS value until the next key, then cuts — no in-between. Easing 'hold' / 'step' on the ARRIVING key does the same. A studio without hold keys refuses rather than interpolate; there, two keys a frame apart make the cut. For a whole layer 'on twos', set stepTiming on the item (create_item / modify_item).
3938
+ HOLD KEYS (where the studio has them): interpolation: 'hold' on a key holds ITS value until the next key, then cuts — no in-between. Easing 'hold' / 'step' on the ARRIVING key does the same. A studio without hold keys refuses rather than interpolate; there, two keys a frame apart make the cut. For a whole layer 'on twos', set stepTiming on the item (create_item / modify_item). A DISCRETE change (a bar turning red at 2 s, a label switching colour) needs this too: without hold / step every key ramps from the one before it, so the colour fades across the whole gap instead of switching. This applies to batch keyframe ops the same way.
3920
3939
 
3921
3940
  Every easing renders exactly in video, GIF and APNG. Lottie and CSS get the closest single Bézier: springPlayful loses its wobble, and the in-out quart/quint/expo/circ curves are within 2–4 %. SMIL is exact for springs and those four.
3922
3941
 
@@ -4588,7 +4607,7 @@ SUPPORTED ANIMATIONS: bounce, breathe, fade, glow, jelly, path, pulse, rotate, s
4588
4607
  target: { type: 'string', description: 'Name of target item' },
4589
4608
  type: {
4590
4609
  type: 'string',
4591
- 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_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', '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'],
4610
+ 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', 'localizes_sound', '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_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', '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'],
4592
4611
  },
4593
4612
  params: {
4594
4613
  type: 'object',
@@ -4852,6 +4871,7 @@ KINDS:
4852
4871
  - phase_wave — members move on a sine, each a phase later than the one before. params: property ('y' default, 'x', 'rotation', 'opacity'), amplitude (px, degrees or 0..1), period (seconds per cycle), wavelength (members per full cycle), phase (radians). window { from, to } limits when it plays; time counts from \`from\`.
4853
4872
  - stagger — each member's animationDelay, derived from its place. params: step (s between neighbours), start (s), order: index | reverse | center | edges.
4854
4873
  - ring / orbit (the same motion) — members spaced evenly round a circle that turns. params: center ([x, y] or {x, y}; default 960, 540), target (an item id or name: the centre follows it), radius (px, default 200), period (seconds per turn, default 6), phase (RADIANS, the first member's angle), direction (1 clockwise, -1 or 'ccw'). Each member's own position is overwritten while it plays and comes back when the pattern is removed. window as for phase_wave.
4874
+ - STACKING. phase_wave patterns on the same member and property add their offsets to one shared base on the current studio, so sequenced windows combine without drift (an older studio re-based them on each other's writes and could leave the member displaced: there, give a member one pattern per property and sequence by modifying its params and window). ring and orbit write the member's absolute position, so two of them on one member conflict: give each member one ring or orbit.
4855
4875
 
4856
4876
  MEMBERS — by names (a '*' matches any run: "letter-*"), ids, or a tag, in the order they are given; an array is taken as ids or names. A name that matches nothing is reported in the result, not ignored.
4857
4877
 
@@ -8061,6 +8081,7 @@ SPEC:
8061
8081
  UNITS (a number, or a [min, max] range picked per particle where it says range):
8062
8082
  - count: particles per burst, at most 10000 on the current studio (2000 on older studios) — a larger count is drawn at the cap and the reply warns with the number drawn. For more, add a second burst at the same t.
8063
8083
  - speed: px/s (range). angle: degrees, 0 = right, 90 = down (range). gravity: px/s². lifetime: seconds (range, > 0). size: px (range).
8084
+ - gravity, drag, friction, opacity, sizeEnd and jitter also take a range on the current studio, sampled per particle; an older studio draws the midpoint and warns emitter-range-not-read.
8064
8085
  - fadeIn / fadeOut: SECONDS at the start / end of each particle's life. fadeOut is capped at that particle's lifetime (a 0.5 s life cannot fade for 2 s; it fades over its whole life instead), and the current studio warns when it caps one. Omitted (or fade: 'linear'), the particle fades linearly over its WHOLE life; fade: 'none' turns both off.
8065
8086
  - spin: DEGREES PER SECOND, positive = clockwise on screen (range; spinAlternate flips every other particle). It turns rect, confetti, star and dash particles; a circle has no orientation, so spin on circles shows nothing (the current studio warns when every particle is a circle).
8066
8087
  - jitter: PIXELS — on the current studio each particle is displaced up to ±jitter px on each axis from its path, at full size from birth; jitterGrow: true keeps the older ramp (zero at birth, the full ±jitter at the end of life). Older studios (before jitterGrow) displace up to ±jitter/2, growing from 0 at birth. jitterHz: how many times a second that offset is re-rolled (0, the default, keeps one offset per particle).
@@ -8080,8 +8101,10 @@ EXAMPLE — confetti at 2 s:
8080
8101
  },
8081
8102
  {
8082
8103
  name: 'pinepaper_render_hook',
8083
- annotations: { title: 'Render Hook', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
8084
- description: `Custom per-frame drawing that a saved scene CARRIES: SOURCE TEXT, compiled and run under the governor's loop guards with seeded Math, saved in the project document, the template and undo, and re-installed on load. (A raw function added with code is not saved — pinepaper_export_scene { full: true } names any such as unserialized.)
8104
+ annotations: { title: 'Render Hook (register needs PINEPAPER_ALLOW_CODE)', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
8105
+ description: `SECURITY SWITCH: register needs a server started with PINEPAPER_ALLOW_CODE=1 (off by default; only the person running the server can turn it on). list and unregister always work.
8106
+
8107
+ Custom per-frame drawing that a saved scene CARRIES: SOURCE TEXT, compiled and run under the governor's loop guards with seeded Math, saved in the project document, the template and undo, and re-installed on load. (A raw function added with code is not saved — pinepaper_export_scene { full: true } names any such as unserialized.)
8085
8108
 
8086
8109
  ACTIONS:
8087
8110
  - register: { id, source, layer? ('above' | 'below'), deterministic? (true), seed? } — source evaluates to (ctx, t, info) => {}: ctx in canvas coordinates, t the scene time in seconds, info { width, height, frame, seed }. 'above' draws over every item, 'below' under them. deterministic reseeds Math.random from (seed, frame) each call, so a frame always draws the same.
@@ -8141,7 +8164,7 @@ EXAMPLE — a ball comes to life and crosses the frame:
8141
8164
  sound: { description: "true, or {pop?, whoosh?, contact?, shake? (catalogue name or false), gain? 0..1}: a sound on every pop, launch, landing and shake.", oneOf: [{ type: 'boolean' }, { type: 'object', additionalProperties: true }] },
8142
8165
  beats: {
8143
8166
  type: 'array',
8144
- description: 'Beats in time order: [{at, verb, to?, duration?, height?, times?, decay?, arc?, edge?, from?, amount?, hold?, intensity?}].',
8167
+ description: 'Beats in time order: [{at, verb, to?, duration?, height?, times?, decay?, arc?, edge?, from?, amount?, hold?, intensity?}]. intensity is the amplitude of shake in px; no other verb reads it.',
8145
8168
  items: {
8146
8169
  type: 'object',
8147
8170
  properties: {
@@ -8292,12 +8315,14 @@ EXAMPLE: { action: 'apply', target: 'item_4', graph: { nodes: [{ id: 'in', type:
8292
8315
  The engine validates the document before applying it: a truncated file, a foreign JSON blob or a document from a newer major version is refused, not half-applied. Returns counts (items, animated items, keyframes, relations): compare them with the export's to confirm nothing was lost.
8293
8316
 
8294
8317
  - path: the file the export wrote (absolute, or relative to the server's working directory).
8295
- - strict: false loads a document with validation problems anyway and returns them as warnings (default true: refuse).`,
8318
+ - strict: false loads a document with validation problems anyway and returns them as warnings (default true: refuse).
8319
+ - registerCustomRelations: true registers the custom relation definitions the file carries (a full export saves them with the scene) before loading, so its relations of those types run. Their compute and apply code runs in the studio, so pass it only for a file you trust; without it the result names them and they stay unregistered.`,
8296
8320
  inputSchema: {
8297
8321
  type: 'object',
8298
8322
  properties: {
8299
8323
  path: { type: 'string', description: 'The project document file.' },
8300
8324
  strict: { type: 'boolean', description: 'Refuse a document with validation problems (default true).' },
8325
+ registerCustomRelations: { type: 'boolean', description: 'Register the custom relation definitions saved in the file first (their code runs in the studio: only for a file you trust).' },
8301
8326
  },
8302
8327
  required: ['path'],
8303
8328
  },
@@ -8857,7 +8882,8 @@ EXAMPLE — Animated sky scene with timed reveals:
8857
8882
  append: { type: 'boolean', description: "For keyframe_animate: add these keys to the item's existing track instead of replacing it (a key at the same time merges, the new values win). Without it the op replaces the track; the op's result says which happened." },
8858
8883
  mode: { type: 'string', description: "For keyframe_animate: the studio's spelling of append — 'merge' is append: true; 'replace' (the default) replaces the track." },
8859
8884
  // Relations
8860
- sourceId: { type: 'string', description: 'For relation: source item ID or $N' },
8885
+ sourceId: { type: 'string', description: 'For relation: source item ID or $N, or a name pattern ("dot-*"): every item named like it, in name order, each with index and count added to relationOptions; re-sending it after adding members binds the new ones and updates index and count' },
8886
+ update: { type: 'boolean', description: 'relation: true changes the relationOptions of the relation already bound (same sourceId, targetId, relationType) instead of binding another' },
8861
8887
  targetId: { type: 'string', description: 'For relation: target item ID or $N' },
8862
8888
  relationType: {
8863
8889
  type: 'string',
@@ -9058,7 +9084,7 @@ VERIFY MOTION BEFORE YOU RENDER. An export takes seconds to minutes and shows yo
9058
9084
  loop: { anyOf: [{ type: 'boolean' }, { type: 'integer', minimum: 0, maximum: 1000 }], description: 'gif / apng: true = loop forever, false / 0 / 1 = play once, n = play n times (email clients often want a few plays, not forever). Refused on video formats — see SEAMLESS LOOPS.' },
9059
9085
  broadcast: { type: 'boolean', description: 'mp4 only: broadcast-safe — BT.709, limited range (16-235), tagged bt709, constant bitrate with an 8 Mbps floor at 720p+ (4 below). result.video reports what the encoder did.' },
9060
9086
  frames: { type: 'integer', minimum: 1, maximum: 36000, description: 'Animated formats: exactly this many frames instead of duration. A duration makes ceil(duration x fps) frames (7.91667 s at 24 fps is 191, not 190), so use frames for frame-exact segments.' },
9061
- deterministic: { type: 'boolean', description: 'mp4 / webm: byte-identical files for the same scene (pins the container timestamps), so exports can be checksummed or cached by hash.' },
9087
+ deterministic: { type: 'boolean', description: 'mp4 / webm: pins the container timestamps. WebM then comes out byte-identical; an MP4\'s encoded bytes can still differ run to run (the H.264 encoder is not bit-exact), so compare MP4s by the receipt\'s frameDigest, which is the same for the same frames, not by sha256.' },
9062
9088
  motionBlur: {
9063
9089
  type: 'object',
9064
9090
  description: 'mp4 / webm: real motion blur. Each frame is rendered `subframes` times across the shutter and averaged, so fast movement smears instead of strobing. Costs subframes× the render time: run estimateOnly first (it reports renders and renderCost). The result carries motionBlur (what the exporter applied).',
@@ -9117,6 +9143,8 @@ VERIFY MOTION BEFORE YOU RENDER. An export takes seconds to minutes and shows yo
9117
9143
  enum: ['canvas', 'camera'],
9118
9144
  description: 'Output framing. "canvas" (default) renders the full canvas. "camera" renders only the first-keyframe viewport of a camera_animates walkthrough — video formats only.',
9119
9145
  },
9146
+ startTime: { type: 'number', description: 'mp4 / webm: start at this time (seconds) on the timeline, the chain\'s when there is one; duration is the length of the range. Frames match a full export exactly; the frames before startTime are still rendered (not encoded). result.video.range says where it snapped.' },
9147
+ perScene: { type: 'boolean', description: 'mp4 / webm of a scene chain: one cached segment per scene, joined (video copied, audio re-encoded), so after an edit only the scenes that changed render again. Needs ffmpeg on PATH; a chain with crossfades or cut sounds is exported whole and the result says why.' },
9120
9148
  estimateOnly: {
9121
9149
  type: 'boolean',
9122
9150
  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. Also answers what THIS scene would lose to this format — a png of an animated scene is one frame, a gif drops the soundtrack — under "fidelity", which is present only when there is something to lose.',
@@ -9195,13 +9223,15 @@ EXAMPLE RESPONSE:
9195
9223
  {
9196
9224
  name: 'pinepaper_p5_draw',
9197
9225
  annotations: {
9198
- title: 'p5.js Draw',
9226
+ title: 'p5.js Draw (needs PINEPAPER_ALLOW_CODE)',
9199
9227
  readOnlyHint: false,
9200
9228
  destructiveHint: false,
9201
9229
  idempotentHint: false,
9202
9230
  openWorldHint: false,
9203
9231
  },
9204
- description: `Execute p5.js-style drawing code on the PinePaper canvas.
9232
+ description: `SECURITY SWITCH: needs a server started with PINEPAPER_ALLOW_CODE=1 (off by default; only the person running the server can turn it on). Otherwise it is refused and nothing runs.
9233
+
9234
+ Execute p5.js-style drawing code on the PinePaper canvas.
9205
9235
 
9206
9236
  AI assistants can use familiar p5.js syntax - the code is translated to Paper.js automatically.
9207
9237
  This is a SUBSET of p5.js functions optimized for static drawings.
@@ -9662,7 +9692,7 @@ VERBOSITY:
9662
9692
  toolkit: {
9663
9693
  type: 'string',
9664
9694
  description: 'Toolkit profile: core (default), full, agent, diagram, map, font, minimal, lane',
9665
- enum: ['core', 'full', 'agent', 'diagram', 'map', 'font', 'minimal', 'lane'],
9695
+ enum: ['core', 'full', 'agent', 'diagram', 'map', 'font', 'minimal', 'lane', 'edit'],
9666
9696
  },
9667
9697
  load: {
9668
9698
  type: 'array',