@pinepaper.studio/mcp-server 1.5.4 → 1.6.0

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 (88) hide show
  1. package/README.de.md +71 -0
  2. package/README.es.md +71 -0
  3. package/README.fr.md +71 -0
  4. package/README.hi.md +71 -0
  5. package/README.ja.md +71 -0
  6. package/README.ko.md +71 -0
  7. package/README.md +513 -272
  8. package/README.pt-BR.md +71 -0
  9. package/README.zh-CN.md +71 -0
  10. package/assets/easing-splashes.svg +152 -0
  11. package/assets/equation-path.svg +77 -0
  12. package/assets/hero.svg +126 -0
  13. package/assets/live-badge.svg +62 -0
  14. package/assets/poster-sunburst.svg +106 -0
  15. package/dist/__tests__/fixtures/mock-relations.d.ts +48 -0
  16. package/dist/__tests__/fixtures/mock-relations.d.ts.map +1 -1
  17. package/dist/__tests__/fixtures/mock-relations.js +31 -0
  18. package/dist/__tests__/fixtures/mock-relations.js.map +1 -1
  19. package/dist/agent/types.d.ts +6 -6
  20. package/dist/browser/puppeteer-controller.d.ts +34 -0
  21. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  22. package/dist/browser/puppeteer-controller.js +116 -5
  23. package/dist/browser/puppeteer-controller.js.map +1 -1
  24. package/dist/cli.js +2 -1
  25. package/dist/cli.js.map +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +35 -5
  28. package/dist/index.js.map +1 -1
  29. package/dist/ontology/index.d.ts +2 -0
  30. package/dist/ontology/index.d.ts.map +1 -1
  31. package/dist/ontology/index.js +2 -0
  32. package/dist/ontology/index.js.map +1 -1
  33. package/dist/ontology/ontology-compiler.d.ts +78 -0
  34. package/dist/ontology/ontology-compiler.d.ts.map +1 -0
  35. package/dist/ontology/ontology-compiler.js +235 -0
  36. package/dist/ontology/ontology-compiler.js.map +1 -0
  37. package/dist/ontology/ontology-validator.d.ts +103 -0
  38. package/dist/ontology/ontology-validator.d.ts.map +1 -0
  39. package/dist/ontology/ontology-validator.js +573 -0
  40. package/dist/ontology/ontology-validator.js.map +1 -0
  41. package/dist/ontology/types.d.ts +1 -0
  42. package/dist/ontology/types.d.ts.map +1 -1
  43. package/dist/ontology/validator.d.ts +6 -0
  44. package/dist/ontology/validator.d.ts.map +1 -1
  45. package/dist/ontology/validator.js +34 -5
  46. package/dist/ontology/validator.js.map +1 -1
  47. package/dist/ontology/vocabulary.d.ts.map +1 -1
  48. package/dist/ontology/vocabulary.js +91 -3
  49. package/dist/ontology/vocabulary.js.map +1 -1
  50. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  51. package/dist/tools/compact-descriptions.js +0 -1
  52. package/dist/tools/compact-descriptions.js.map +1 -1
  53. package/dist/tools/definitions.d.ts +1 -1
  54. package/dist/tools/definitions.d.ts.map +1 -1
  55. package/dist/tools/definitions.js +997 -573
  56. package/dist/tools/definitions.js.map +1 -1
  57. package/dist/tools/handlers/camera.d.ts.map +1 -1
  58. package/dist/tools/handlers/camera.js +7 -1
  59. package/dist/tools/handlers/camera.js.map +1 -1
  60. package/dist/tools/handlers/maps.d.ts.map +1 -1
  61. package/dist/tools/handlers/maps.js +54 -1
  62. package/dist/tools/handlers/maps.js.map +1 -1
  63. package/dist/tools/handlers/ontology.d.ts.map +1 -1
  64. package/dist/tools/handlers/ontology.js +15 -3
  65. package/dist/tools/handlers/ontology.js.map +1 -1
  66. package/dist/tools/handlers.d.ts.map +1 -1
  67. package/dist/tools/handlers.js +160 -113
  68. package/dist/tools/handlers.js.map +1 -1
  69. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  70. package/dist/tools/minimal-descriptions.js +23 -16
  71. package/dist/tools/minimal-descriptions.js.map +1 -1
  72. package/dist/tools/toolkits.d.ts.map +1 -1
  73. package/dist/tools/toolkits.js +28 -22
  74. package/dist/tools/toolkits.js.map +1 -1
  75. package/dist/types/code-generator.d.ts +190 -36
  76. package/dist/types/code-generator.d.ts.map +1 -1
  77. package/dist/types/code-generator.js +1216 -571
  78. package/dist/types/code-generator.js.map +1 -1
  79. package/dist/types/schemas.d.ts +2826 -829
  80. package/dist/types/schemas.d.ts.map +1 -1
  81. package/dist/types/schemas.js +595 -79
  82. package/dist/types/schemas.js.map +1 -1
  83. package/dist/version.d.ts +11 -0
  84. package/dist/version.d.ts.map +1 -0
  85. package/dist/version.js +18 -0
  86. package/dist/version.js.map +1 -0
  87. package/manifest.json +299 -71
  88. package/package.json +14 -3
@@ -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, } from '../types/schemas.js';
12
+ import { EffectTypeSchema, SimpleAnimationTypeSchema, GEOMETRY_OPERATIONS, } 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
@@ -55,7 +55,7 @@ RULES:
55
55
  ─── BATCH OPERATIONS (12 types, in order) ───
56
56
 
57
57
  CANVAS: set_canvas_size (width/height or preset) → set_background (color) → execute_generator (procedural art)
58
- ITEMS: create (itemType, position, properties) → modify (itemId, properties) → delete (itemId)
58
+ ITEMS: create (itemType, position, properties) → modify (itemId, properties) → delete (itemId) → group (itemIds → one entity)
59
59
  ANIMATE: animate (loop presets) → keyframe_animate (timed reveals) → relation (behavioral links)
60
60
  EFFECTS: apply_mask (reveal animations) → apply_effect (sparkle/blast/bubbles/fireflies/trail...)
61
61
  PLAY: play_timeline (start playback)
@@ -72,19 +72,35 @@ Choose based on mood:
72
72
 
73
73
  ─── ITEMS ───
74
74
 
75
- Shapes: circle, rectangle, star, ellipse, triangle, polygon, line, arc, path
75
+ Shapes: circle, rectangle, star, ellipse, triangle, polygon, line, arc, path, pentagon, hexagon, diamond, arrow, heart
76
76
  Text: text (content, fontSize, fontFamily, color, fontWeight)
77
77
  Dynamic text: set contentType on text items → clock (live time), timer (elapsed), countdown (from N seconds), stopwatch (pauseable)
78
78
  All items: opacity, shadowColor, shadowBlur, blendMode, strokeColor, strokeWidth
79
79
 
80
80
  SVG imports for recognizable objects (planes, cars, animals, buildings):
81
81
  pinepaper_search_assets → pinepaper_import_asset (850k+ SVG icons)
82
+ An imported SVG is ONE opaque item. To move/recolor its parts (a wheel, a window), pinepaper_group action:"break_apart" splits it into individually movable, re-grouped sub-parts.
83
+
84
+ Photos / raster images (on-device, no upload):
85
+ pinepaper_detect_objects — find objects (labeled boxes), or asNodes:true to promote each to an image-anchored design NODE (pp:Detected*) you can bind a shape to with a relation (relational compositing — e.g. a hat that follows a detected person as the photo moves).
86
+ pinepaper_extract_object — crop the best-matching object (label: "cat") out of a photo into its own item.
87
+
88
+ ⚠️ COMPOSITE OBJECTS — GROUP THE PARTS:
89
+ When the user asks for ONE specific named object built from multiple primitives — a car (body + wheels + windows), a robot, a house, a tree, a snowman, a character — the pieces are INDEPENDENT until grouped. Without grouping, dragging the "car" only moves the piece grabbed; the wheels stay behind, and it can't be moved/animated as one thing.
90
+ → After creating the parts, add a "group" op listing them (reference batch-created parts as "$0","$1",…). This is the default for any nameable object. Grouping is non-destructive — the user can ungroup anytime (pinepaper_group action:"ungroup"), so grouping is always the safe choice.
91
+ → Do NOT group a whole scene/infographic whose elements move independently — only group the parts that form ONE object. A scene may contain several groups (e.g. two separate cars).
92
+
93
+ ⚠️ MOVE vs ANIMATE — pick the tool from the user's intent:
94
+ • Reposition (no time, no path): "put X at (x',y')", "move X to the corner" → modify { x, y }. A single, instant position change — NOT an animation.
95
+ • Animated A→B move ("animate X moving to (x',y')", "slide it over 2s", "have it drift to the corner") → keyframe_animate with two keyframes: [{time:0, properties:{x,y}}, {time:T, properties:{x',y'}}]. Use this when the user wants the motion itself to be visible.
96
+ • Follow a multi-point PATH (the user lists several coordinates, or "make X follow this path / trace these points (x1,y1),(x2,y2),…") → relation moves_along_path with params.path = [{x,y},…] (speed, closed, easing optional). One coordinate list = one path-follow, not N separate moves.
97
+ When the request is ambiguous between a static move and an animated one, a one-shot reposition is the literal reading; only animate if motion/timing/path is implied.
82
98
 
83
99
  ─── ANIMATION (all in batch) ───
84
100
 
85
101
  Loop presets (animate): pulse, rotate, bounce, fade, wobble, slide, typewriter
86
102
  Keyframe (keyframe_animate): [{time, properties, easing}] → opacity, scale, scaleX, scaleY, x, y, rotation, fillColor, strokeColor, fontSize
87
- Relations (relation): orbits, follows, attached_to, points_at, mirrors, parallax, wave_through, morphs_to, group_morphs_to, moves_along_path (+ 9 more in batch_execute schema)
103
+ 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)
88
104
  Masks (apply_mask): wipeLeft, wipeRight, wipeUp, wipeDown, iris, irisOut, star, heart, curtainHorizontal, curtainVertical, cinematic, diagonalWipe, revealUp, revealDown
89
105
  Effects (apply_effect): sparkle, blast
90
106
 
@@ -134,9 +150,6 @@ Sprite Sheets: pinepaper_sprite_sheet — generate from skeleton poses, play, ex
134
150
  Interaction: pinepaper_interaction — continuous physics behaviors (repel, attract, follow, orbit, slingshot)
135
151
  For interactive storytelling, quizzes, games. trigger_action for state changes (incrementScore, navigate, showFeedback).
136
152
 
137
- Storage: pinepaper_storage — save/load/list/delete projects (IndexedDB persistence)
138
- For multi-project workflows and session recovery.
139
-
140
153
  Data Viz: pinepaper_create_chart — create/update/reconfigure/remove charts (bar, line, scatter, area)
141
154
  Pass data as array of objects + options. Auto-animated with expression-driven oscillation.
142
155
 
@@ -350,6 +363,11 @@ ITEM TYPES:
350
363
  - path: Custom path with segments or SVG data (properties: segments, pathData, strokeColor, fillColor, closed, smooth)
351
364
  - line: Line between two points (properties: from, to, strokeColor, strokeWidth)
352
365
  - arc: Curved arc through three points (properties: from, through, to, strokeColor)
366
+ - pentagon: Regular 5-sided polygon (properties: radius, color)
367
+ - hexagon: Regular 6-sided polygon (properties: radius, color)
368
+ - diamond: 4-sided diamond/rhombus (properties: radius, color)
369
+ - arrow: Right-pointing block arrow (properties: width, height, color)
370
+ - heart: Heart shape (properties: color)
353
371
 
354
372
  PATH FOR CUSTOM SHAPES:
355
373
  Use itemType: "path" with either:
@@ -387,7 +405,7 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
387
405
  properties: {
388
406
  itemType: {
389
407
  type: 'string',
390
- enum: ['text', 'circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc'],
408
+ enum: ['text', 'circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc', 'pentagon', 'hexagon', 'diamond', 'arrow', 'heart'],
391
409
  description: 'Type of item to create',
392
410
  },
393
411
  position: {
@@ -431,6 +449,14 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
431
449
  type: 'number',
432
450
  description: 'Animation speed multiplier (for simple animations, default: 1.0)',
433
451
  },
452
+ animationIntensity: {
453
+ type: 'number',
454
+ description: 'Loop animation amplitude (0.1 = ±10%, default 0.15); drives pulse/wobble/bounce/breathe amplitude and is honored by SVG/SMIL/widget export.',
455
+ },
456
+ animationDelay: {
457
+ type: 'number',
458
+ description: 'Loop animation start delay in seconds.',
459
+ },
434
460
  keyframes: {
435
461
  type: 'array',
436
462
  description: "Keyframe array for inline keyframe animation (requires animationType: 'keyframe')",
@@ -489,7 +515,7 @@ GRADIENT OBJECT FORMAT:
489
515
 
490
516
  LETTER-COLLAGE PROPS (non-destructive — registry ids, animations, relations, effects, deformations all preserved):
491
517
  - palette (collage group): Swap palette name in-place. e.g. "wordle", "spring", "sunset", "magazine". Walks existing letters, rewrites tile + text fills. No geometry change.
492
- - collageStyle (collage group): Swap visual style — "tile" | "magazine" | "paperCut" | "fold" | "gradient" | "image". Geometry rebuilds; every registryId is preserved via graft.
518
+ - collageStyle (collage group): Swap visual style — "tile" | "magazine" | "paperCut" | "fold" | "gradient" | "image" | "particle". Geometry rebuilds; every registryId is preserved via graft.
493
519
  - palette + collageStyle (collage group): Combine in one call to apply both atomically.
494
520
  - bgColor (single letter group, child of a collage): Tile/background fill for that one letter.
495
521
  - textColor (single letter group): PointText fill for that one letter.
@@ -1225,6 +1251,718 @@ EXAMPLE:
1225
1251
  },
1226
1252
  },
1227
1253
  },
1254
+ {
1255
+ name: 'pinepaper_geometry',
1256
+ annotations: {
1257
+ title: 'Geometry Construction',
1258
+ readOnlyHint: false,
1259
+ destructiveHint: false,
1260
+ idempotentHint: false,
1261
+ openWorldHint: false,
1262
+ },
1263
+ description: `Run an exact geometric construction (FxTool's app.geometry library) and optionally create a canvas item from the result. Use this instead of hand-computing vertices, intersections, or centers — it returns precise coordinates and never NaN (degenerate inputs return null with an explanatory error).
1264
+
1265
+ Compose it with the create tools, e.g. compute a pentagon's vertices then drop them straight into a polygon. With createAs it does both in one call.
1266
+
1267
+ OPERATIONS (args are positional; points are {x,y}, lines are {p1,p2}, circles are {center,radius}, angles in radians):
1268
+ - Points: point(x,y) · distance(a,b) · midpoint(a,b) · lerpPoint(a,b,t) · centroid([pts]) · translatePoint(p,dx,dy) · rotatePoint(p,center,angleRad) · scalePoint(p,center,factor)
1269
+ - Polygons (→ vertex list): regularPolygon(cx,cy,radius,sides,rotation?) · star(cx,cy,outerR,innerR,points,rotation?) · polygonFromVertices([pts])
1270
+ - Lines (→ {p1,p2}): lineThrough(a,b) · lineDirection(line) · lineIntersection(l1,l2) · projectPointToLine(p,line) · perpendicular(line,through) · parallel(line,through) · perpendicularBisector(a,b) · reflectPoint(p,line)
1271
+ - Angles: angle(a,b,c) → radians at vertex b · angleBisector(a,vertex,c) → line
1272
+ - Circles: circle(cx,cy,r) · pointOnCircle(circ,angleRad) · circumcenter(a,b,c) · circleThrough(a,b,c) → circumcircle · incenter(a,b,c) · tangentPointsFromExternal(circ,p) → up to 2 touch points
1273
+
1274
+ createAs (optional): also create an item from the result — a vertex-list result → polygon, a circle result → circle, a point result → a small marker. Style keys (color/fillColor/strokeColor/strokeWidth) pass through to app.create. Omit for compute-only.
1275
+
1276
+ RETURNS: { success, operation, result, itemId } — result is the raw geometry; itemId is set only when createAs created something. null/degenerate constructions return { success:false, ..., error }.
1277
+
1278
+ EXAMPLES:
1279
+ - Pentagon: { operation: "regularPolygon", args: [400, 300, 120, 5], createAs: { fillColor: "#FF6B6B" } }
1280
+ - Circumcircle of a triangle: { operation: "circleThrough", args: [{x:100,y:100},{x:300,y:120},{x:200,y:300}], createAs: { strokeColor: "#333", fillColor: "transparent" } }
1281
+ - Intersection of two lines (compute only): { operation: "lineIntersection", args: [{p1:{x:0,y:0},p2:{x:100,y:100}}, {p1:{x:0,y:100},p2:{x:100,y:0}}] }
1282
+
1283
+ For figures that must stay correct as anchors move (GeoGebra-style), use the geometric constraint relations on pinepaper_add_relation (is_midpoint_of, is_circumcenter_of, …) instead.`,
1284
+ inputSchema: {
1285
+ type: 'object',
1286
+ properties: {
1287
+ operation: {
1288
+ type: 'string',
1289
+ enum: [...GEOMETRY_OPERATIONS],
1290
+ description: 'Which geometry construction helper to run.',
1291
+ },
1292
+ args: {
1293
+ type: 'array',
1294
+ description: 'Positional arguments in order. Points are {x,y} objects; lines {p1,p2}; circles {center,radius}; angles in radians.',
1295
+ },
1296
+ createAs: {
1297
+ type: 'object',
1298
+ description: 'When present, also create an item from the result (vertex list → polygon, circle → circle, point → marker). Style keys pass through to app.create. Optional itemType / radius for point markers.',
1299
+ },
1300
+ },
1301
+ required: ['operation'],
1302
+ },
1303
+ },
1304
+ {
1305
+ name: 'pinepaper_equation_path',
1306
+ annotations: {
1307
+ title: 'Equation-Driven Path',
1308
+ readOnlyHint: false,
1309
+ destructiveHint: false,
1310
+ idempotentHint: false,
1311
+ openWorldHint: false,
1312
+ },
1313
+ description: `Create a path ITEM from a math equation (FxTool's app.createEquationPath, Expression IR — S10 B5). Deterministic: the math strings are PARSED (expr-to-ir, no eval) and sampled on the Expression IR, so the same input always yields the same curve. Use this instead of hand-computing points for a curve.
1314
+
1315
+ KINDS:
1316
+ - function (default): y = f(variable). expr, e.g. "sin(x) * 80". variable defaults to 'x'.
1317
+ - parametric: x = fx(param), y = fy(param). xExpr + yExpr, e.g. xExpr "cos(3*t)*120", yExpr "sin(2*t)*80". param defaults to 't'.
1318
+ - fourier: sum of harmonics. harmonics: [{ freq, amp, phase? }, …].
1319
+ - preset: a curated curve. preset ∈ spiral | rose | lissajous | astroid | heart | spirograph (preset-specific tuning passed as top-level fields, e.g. { kind:"preset", preset:"rose", k:5, r:140 }).
1320
+
1321
+ SAMPLING & PLACEMENT: min/max (sample range) · samples (default 200) · scale (multiply coords) · x/y (canvas origin, default canvas center) · flipY (math y-up → screen y-down, default true) · style (strokeColor/strokeWidth/fillColor forwarded to create).
1322
+
1323
+ warp (optional): chained parametric warp(s) — displace each sampled point by { dx, dy }, expressions of x, y (point coords) and t (0..1 along the path). Pass one object or an array to chain.
1324
+
1325
+ RETURNS: { success, kind, itemId } — itemId is the created path. Bad expressions / empty ranges return { success:false, error }.
1326
+
1327
+ To make an item TRAVEL along an equation curve (instead of drawing it), use pinepaper_add_relation relationType moves_along_path with params.equation instead.
1328
+
1329
+ EXAMPLES:
1330
+ - Sine wave: { kind: "function", expr: "sin(x/40) * 60", min: -300, max: 300, style: { strokeColor: "#4f46e5" } }
1331
+ - Lissajous: { kind: "parametric", xExpr: "cos(3*t)*120", yExpr: "sin(2*t)*80", min: 0, max: 6.2832 }
1332
+ - Rose preset: { kind: "preset", preset: "rose", k: 5, r: 140 }`,
1333
+ inputSchema: {
1334
+ type: 'object',
1335
+ properties: {
1336
+ kind: { type: 'string', enum: ['function', 'parametric', 'fourier', 'preset'], description: "Equation kind (default 'function')" },
1337
+ expr: { type: 'string', description: "y = f(variable) — kind 'function'" },
1338
+ xExpr: { type: 'string', description: "x = fx(param) — kind 'parametric'" },
1339
+ yExpr: { type: 'string', description: "y = fy(param) — kind 'parametric'" },
1340
+ harmonics: { type: 'array', description: "kind 'fourier': [{ freq, amp, phase? }, …]" },
1341
+ preset: { type: 'string', enum: ['spiral', 'rose', 'lissajous', 'astroid', 'heart', 'spirograph'], description: "kind 'preset': curated curve" },
1342
+ variable: { type: 'string', description: "Sample variable for 'function' (default 'x')" },
1343
+ param: { type: 'string', description: "Sample variable for 'parametric'/'preset' (default 't')" },
1344
+ min: { type: 'number', description: 'Sample range start' },
1345
+ max: { type: 'number', description: 'Sample range end' },
1346
+ samples: { type: 'number', description: 'Sample count (default 200)' },
1347
+ scale: { type: 'number', description: 'Multiply equation coords (default 1)' },
1348
+ x: { type: 'number', description: 'Canvas origin x (default: canvas center)' },
1349
+ y: { type: 'number', description: 'Canvas origin y (default: canvas center)' },
1350
+ flipY: { type: 'boolean', description: 'Math y-up → screen y-down (default true)' },
1351
+ 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' },
1353
+ },
1354
+ },
1355
+ },
1356
+ {
1357
+ name: 'pinepaper_event',
1358
+ annotations: {
1359
+ title: 'Event Channel',
1360
+ readOnlyHint: false,
1361
+ destructiveHint: false,
1362
+ idempotentHint: false,
1363
+ openWorldHint: false,
1364
+ },
1365
+ description: `Manage the pp:event channel — the backbone of EVENT-DRIVEN SCENE CHAINS (FxTool S11). An event is a fireable channel; relations with an event as their source run when it fires. This is how you author a LONG scene as a graph of timed beats instead of a keyframe track.
1366
+
1367
+ ACTIONS:
1368
+ - create: make an event. { name (required), payloadType?, x?, y? } → returns { eventId }. Use the eventId as the sourceId of on_event_* relations (pinepaper_add_relation).
1369
+ - pulse: fire an event now. { eventId (required), payload? }. Kicks off a chain.
1370
+
1371
+ TYPICAL SCENE CHAIN:
1372
+ 1. pinepaper_event create → e0, e1, e2 (one per beat).
1373
+ 2. Chain them: add_relation on_event_fire_after from e0→e1 (params.delay, timeline:'canvas'), e1→e2, …
1374
+ 3. Give each beat reactions: on_event_add_relation (spawn an orbit/path), on_event_set_color, on_event_set_property, on_event_set_visibility (all via pinepaper_add_relation, sourceId=the event).
1375
+ 4. pinepaper_event pulse e0 to start. Canvas-timed fires are scrub/loop/replay-stable.
1376
+
1377
+ RETURNS: create → { success, action, eventId }; pulse → { success, action, eventId }.`,
1378
+ inputSchema: {
1379
+ type: 'object',
1380
+ properties: {
1381
+ action: { type: 'string', enum: ['create', 'pulse'], description: "'create' a pp:event · 'pulse' it to fire" },
1382
+ name: { type: 'string', description: "Event name — required for action 'create'" },
1383
+ payloadType: { type: 'string', description: "Payload type label (default 'Pulse') — action 'create'" },
1384
+ x: { type: 'number', description: 'Canvas x for the event marker — action create' },
1385
+ y: { type: 'number', description: 'Canvas y for the event marker — action create' },
1386
+ eventId: { type: 'string', description: "Event id to fire — required for action 'pulse'" },
1387
+ payload: { description: "Optional value forwarded to listeners — action 'pulse'" },
1388
+ },
1389
+ required: ['action'],
1390
+ },
1391
+ },
1392
+ {
1393
+ name: 'pinepaper_construction_sequence',
1394
+ annotations: {
1395
+ title: 'Construction Sequence',
1396
+ readOnlyHint: false,
1397
+ destructiveHint: false,
1398
+ idempotentHint: false,
1399
+ openWorldHint: false,
1400
+ },
1401
+ description: `Reveal a geometric figure step by step (FxTool's app.constructionSequence, Layer 3). Each step hides its items and attaches a timeline-driven construction_reveal relation, so playing the timeline draws the construction one step at a time — replayable, scrubbable, and editable as relation graph data (not a one-shot script). Builds on the construction_reveal relation; pair it with pinepaper_geometry / the geometric constraint relations.
1402
+
1403
+ ACTIONS:
1404
+ - build: { steps, stepDuration?, fadeIn? } — steps is an ordered array; each step is an array of item ids (or { items: [ids], label }) revealed together. Step i reveals at i * stepDuration seconds. Items are hidden until their step opens. Extends the keyframe timeline to fit. Returns { sequenceId, stepCount, totalDuration }.
1405
+ - play: { sequenceId?, loop?, duration? } — play the sequence on the timeline (defaults to the most recently built one).
1406
+ - clear: { sequenceId? } — remove the reveal relations and restore items to full opacity.
1407
+ - list: enumerate built sequences.
1408
+
1409
+ EXAMPLE (reveal two points, then their midpoint, then a circle):
1410
+ { action: "build", steps: [["pt_a","pt_b"], ["mid_ab"], ["circ"]], stepDuration: 1 }
1411
+ then { action: "play", loop: true }`,
1412
+ inputSchema: {
1413
+ type: 'object',
1414
+ properties: {
1415
+ action: {
1416
+ type: 'string',
1417
+ enum: ['build', 'play', 'clear', 'list'],
1418
+ description: 'build a sequence, play it, clear it, or list sequences.',
1419
+ },
1420
+ steps: {
1421
+ type: 'array',
1422
+ description: 'For build: ordered steps; each is an array of item ids or { items: [ids], label }. Step i reveals at i * stepDuration seconds.',
1423
+ },
1424
+ stepDuration: { type: 'number', description: 'For build: seconds between steps (default 1).' },
1425
+ fadeIn: { type: 'number', description: 'For build: per-step fade-in seconds (default 0.3).' },
1426
+ sequenceId: { type: 'string', description: 'For play/clear: the cseq_ id from build (default = most recent).' },
1427
+ loop: { type: 'boolean', description: 'For play: loop the timeline (default false).' },
1428
+ duration: { type: 'number', description: 'For play: override total duration in seconds.' },
1429
+ },
1430
+ required: ['action'],
1431
+ },
1432
+ },
1433
+ {
1434
+ name: 'pinepaper_group',
1435
+ annotations: {
1436
+ title: 'Group / Ungroup',
1437
+ readOnlyHint: false,
1438
+ destructiveHint: false,
1439
+ idempotentHint: false,
1440
+ openWorldHint: false,
1441
+ },
1442
+ description: `Combine several items into ONE entity that selects, drags, and animates as a single unit — or ungroup one back into loose items.
1443
+
1444
+ ⚠️ GROUP THE PARTS OF A COMPOSITE OBJECT. When you build a specific named object out of multiple primitives — a car (body + wheels + windows), a robot, a house, a tree, a character — the pieces are independent until you group them. WITHOUT grouping, dragging the "car" only moves the one shape you grabbed; the wheels stay behind. After creating all the parts, group them so the object behaves as one thing. Grouping is non-destructive and always safe: the user (or you, via action "ungroup") can break it apart later. In a single pipeline, prefer the "group" operation inside pinepaper_agent_batch_execute (reference the parts as "$0","$1",… ) so the object is grouped the moment it's built.
1445
+
1446
+ Do NOT group a whole scene/infographic whose elements are meant to move independently — only group the parts that form ONE object.
1447
+
1448
+ ACTIONS:
1449
+ - group: combine itemIds (registry IDs) into a new group. Optional groupName. Returns { groupId, itemCount }.
1450
+ - ungroup: dissolve the group with groupId back into its individual items (which keep their own ids). Returns { ungrouped: true }.
1451
+ - break_apart: split an IMPORTED SVG / group / compound-path (itemId) into its individually movable sub-parts, re-wrapped as one group. Use this after pinepaper_import_asset / import_svg when the user wants to move or recolor the PARTS of an imported object (a wheel, a window) — an imported icon is a single opaque item until broken apart. Each part gets its own registry id. Returns { groupId, partIds, partCount }.
1452
+
1453
+ EXAMPLES:
1454
+ - Group a car you built: { action: "group", itemIds: ["body_1","wheel_2","wheel_3","window_4"], groupName: "car" }
1455
+ - Ungroup it: { action: "ungroup", groupId: "group_17" }
1456
+ - Make an imported SVG's parts movable: { action: "break_apart", itemId: "svg_5" }`,
1457
+ inputSchema: {
1458
+ type: 'object',
1459
+ properties: {
1460
+ action: {
1461
+ type: 'string',
1462
+ enum: ['group', 'ungroup', 'break_apart'],
1463
+ description: 'group: combine itemIds into one entity. ungroup: dissolve a group. break_apart: split an imported SVG/group/compound-path into movable parts.',
1464
+ },
1465
+ itemIds: {
1466
+ type: 'array',
1467
+ items: { type: 'string' },
1468
+ description: 'For group: registry IDs of the items to combine.',
1469
+ },
1470
+ itemId: { type: 'string', description: 'For break_apart: registry ID of the imported SVG / group / compound-path to decompose.' },
1471
+ groupName: { type: 'string', description: 'For group: optional name (e.g. "car", "robot").' },
1472
+ groupId: { type: 'string', description: 'For ungroup: registry ID of the group to dissolve.' },
1473
+ },
1474
+ required: ['action'],
1475
+ },
1476
+ },
1477
+ {
1478
+ name: 'pinepaper_camera_director',
1479
+ annotations: {
1480
+ title: 'Camera Director',
1481
+ readOnlyHint: false,
1482
+ destructiveHint: false,
1483
+ idempotentHint: false,
1484
+ openWorldHint: false,
1485
+ },
1486
+ description: `Direct a cinematic camera walkthrough of the scene from a high-level SHOT LIST. Compiles to a single camera_animates keyframe track (one camera — WYSIWYG between the editor view and any export size). Great for "give me a camera tour", "pan across the scene", "zoom into each item in turn".
1487
+
1488
+ ACTIONS:
1489
+ - auto: auto-generate a walkthrough — one medium push-in shot per top-level item, smooth eased moves. Options: order ("reading" default | "reverse" | "creation"), hold (dwell seconds), establishing (open on a wide shot of everything), loop. Returns the generated shots.
1490
+ - shots: apply an explicit shot list. Each shot: { subjects: "everything" | [itemIds], framing: "tight"|"medium"|"wide" (or a zoom number), hold, moveIn ("push-in"|"pan"), ease ("smooth"|"linear"|"settle"|"snap"), transition (seconds to travel in) }.
1491
+
1492
+ Pair with pinepaper_agent_export (framing: "camera") to render the walkthrough as video.
1493
+
1494
+ EXAMPLES:
1495
+ - Auto tour: { action: "auto", establishing: true, hold: 1.5, loop: true }
1496
+ - Explicit: { action: "shots", shots: [ { subjects: "everything", framing: "wide", hold: 1 }, { subjects: ["hero_1"], framing: "tight", moveIn: "push-in", hold: 2 } ] }`,
1497
+ inputSchema: {
1498
+ type: 'object',
1499
+ properties: {
1500
+ action: { type: 'string', enum: ['auto', 'shots'], description: 'auto: generate a walkthrough. shots: apply an explicit shot list.' },
1501
+ shots: {
1502
+ type: 'array',
1503
+ description: 'For "shots": ordered shot list. Each: { subjects, framing, hold, moveIn, ease, transition }.',
1504
+ items: {
1505
+ type: 'object',
1506
+ properties: {
1507
+ subjects: { description: '"everything" or an array of item registry IDs' },
1508
+ framing: { description: '"tight" | "medium" | "wide", or a zoom number' },
1509
+ hold: { type: 'number' },
1510
+ moveIn: { type: 'string' },
1511
+ ease: { type: 'string', enum: ['smooth', 'linear', 'settle', 'snap'] },
1512
+ transition: { type: 'number' },
1513
+ },
1514
+ },
1515
+ },
1516
+ order: { type: 'string', enum: ['reading', 'reverse', 'creation'], description: 'For "auto": item visiting order.' },
1517
+ hold: { type: 'number', description: 'For "auto": per-shot dwell seconds.' },
1518
+ establishing: { type: 'boolean', description: 'For "auto": open with a wide establishing shot.' },
1519
+ loop: { type: 'boolean', description: 'Loop the walkthrough.' },
1520
+ },
1521
+ required: ['action'],
1522
+ },
1523
+ },
1524
+ {
1525
+ name: 'pinepaper_detect_objects',
1526
+ annotations: {
1527
+ title: 'Detect Objects',
1528
+ readOnlyHint: false,
1529
+ destructiveHint: false,
1530
+ idempotentHint: false,
1531
+ openWorldHint: false,
1532
+ },
1533
+ description: `Run on-device object detection (fully local — no upload) over an imported image and either draw labeled boxes or promote each detection to a design node. bbox detection, not pixel-mask segmentation.
1534
+
1535
+ First import an image (pinepaper_import_image / pinepaper_import_asset), then call this on it.
1536
+
1537
+ MODELS:
1538
+ - Default (no queries): DETR — fast, fixed 80 COCO classes (person, car, dog, chair, …).
1539
+ - queries given: OWL-ViT open-vocabulary (zero-shot) — finds WHATEVER you describe ("red umbrella", "traffic light", "logo"), not limited to 80 classes. Larger one-time on-device model download. Pass queries as a list or comma-separated string.
1540
+
1541
+ MODES:
1542
+ - Default (asNodes omitted/false): draws a labeled rectangle over each detection. Returns { ok, drawn, detections: [{label, confidence}] }.
1543
+ - asNodes: true — RELATIONAL COMPOSITING. Instead of boxes, promotes each detection to a typed, image-anchored DESIGN NODE: a registered relation target carrying a pp:Detected* type aliased (rdfs:subClassOf) to a Wikidata entity, plus its confidence. Returns { ok, promoted, count, nodes: [{id, label, ppType, wikidata, confidence}] }. You can then bind a shape on/around/at a detected object with a relation (e.g. follows / attached_to) and it tracks the image as it moves.
1544
+
1545
+ PARAMS: itemId (image; default = most recent image), threshold (0-1, default ~0.35), asNodes.
1546
+
1547
+ EXAMPLE (relational compositing — put a hat on a detected person):
1548
+ 1) { } → detect, or { asNodes: true } → get nodes [{ id: "det_3", label: "person", ... }]
1549
+ 2) create the hat → pinepaper_add_relation { sourceId: hatId, targetId: "det_3", relationType: "follows" }
1550
+
1551
+ NOTE: requires an FxTool build that exposes on-device detection (app.detectObjects). Returns { success: false, error } with a hint otherwise.`,
1552
+ inputSchema: {
1553
+ type: 'object',
1554
+ properties: {
1555
+ itemId: { type: 'string', description: 'Registry ID of the image to scan (default: most recent image).' },
1556
+ threshold: { type: 'number', description: 'Confidence threshold 0-1 (default ~0.35 DETR, ~0.1 open-vocab).' },
1557
+ asNodes: { type: 'boolean', description: 'Promote each detection to a typed, image-anchored design node (relation target) instead of drawing a box.' },
1558
+ queries: {
1559
+ description: 'Open-vocabulary search (OWL-ViT): a list of strings (or comma-separated string) describing what to find — e.g. ["red umbrella","logo"]. Omit for the default 80-class DETR detector.',
1560
+ oneOf: [
1561
+ { type: 'array', items: { type: 'string' } },
1562
+ { type: 'string' },
1563
+ ],
1564
+ },
1565
+ },
1566
+ },
1567
+ },
1568
+ {
1569
+ name: 'pinepaper_extract_object',
1570
+ annotations: {
1571
+ title: 'Extract Object',
1572
+ readOnlyHint: false,
1573
+ destructiveHint: false,
1574
+ idempotentHint: false,
1575
+ openWorldHint: false,
1576
+ },
1577
+ description: `Detect objects in an imported image and crop the best match into a NEW item — e.g. pull the cat out of a photo. On-device, no upload.
1578
+
1579
+ PARAMS: label (what to extract, e.g. "cat", "person"; default = highest-confidence detection), itemId (source image; default = most recent), x/y (optional hint to disambiguate which instance), threshold (0-1).
1580
+
1581
+ RETURNS: { ok, object (matched label), extracted, itemId (the new item) }.
1582
+
1583
+ EXAMPLE: { label: "cat" } — find the cat and crop it into its own movable item.
1584
+
1585
+ NOTE: requires an FxTool build that exposes on-device detection (app.extractObject).`,
1586
+ inputSchema: {
1587
+ type: 'object',
1588
+ properties: {
1589
+ label: { type: 'string', description: 'What to extract (e.g. "cat", "person"). Default = highest-confidence detection.' },
1590
+ itemId: { type: 'string', description: 'Registry ID of the source image (default: most recent image).' },
1591
+ x: { type: 'number', description: 'Optional hint X (canvas coords) to disambiguate which detection to extract.' },
1592
+ y: { type: 'number', description: 'Optional hint Y (canvas coords).' },
1593
+ threshold: { type: 'number', description: 'Confidence threshold 0-1.' },
1594
+ },
1595
+ },
1596
+ },
1597
+ {
1598
+ name: 'pinepaper_arrange',
1599
+ annotations: {
1600
+ title: 'Arrange (Z-Order)',
1601
+ readOnlyHint: false,
1602
+ destructiveHint: false,
1603
+ idempotentHint: false,
1604
+ openWorldHint: false,
1605
+ },
1606
+ description: `Change an item's stacking order (z-order) — which items draw on top of which.
1607
+
1608
+ ACTIONS:
1609
+ - front: bring the item to the TOP of the stack (drawn over everything).
1610
+ - back: send it to the BOTTOM (behind everything; drops to the background layer if needed).
1611
+ - forward: move it up ONE step.
1612
+ - backward: move it down ONE step.
1613
+
1614
+ USE WHEN:
1615
+ - A shape is hidden behind another and should be on top ("bring the logo to the front").
1616
+ - Layering a composition (move a background panel behind the text).
1617
+
1618
+ EXAMPLE: { itemId: "circle_3", action: "front" }`,
1619
+ inputSchema: {
1620
+ type: 'object',
1621
+ properties: {
1622
+ itemId: { type: 'string', description: 'Registry ID of the item to reorder.' },
1623
+ action: {
1624
+ type: 'string',
1625
+ enum: ['front', 'back', 'forward', 'backward'],
1626
+ description: 'front: to top. back: to bottom. forward: up one. backward: down one.',
1627
+ },
1628
+ },
1629
+ required: ['itemId', 'action'],
1630
+ },
1631
+ },
1632
+ {
1633
+ name: 'pinepaper_validate_scene',
1634
+ annotations: {
1635
+ title: 'Validate Scene',
1636
+ readOnlyHint: true,
1637
+ destructiveHint: false,
1638
+ idempotentHint: true,
1639
+ openWorldHint: false,
1640
+ },
1641
+ description: `Audit the LIVE canvas with FxTool's OntologyValidator and get structured, actionable diagnostics instead of console warnings. Each diagnostic is { code, severity, message, target, context, fix } — so you can re-plan from the feedback.
1642
+
1643
+ CHECKS: unknown item types, dangling relation targets / missing anchors, unknown properties, malformed keyframes, and relation cycles.
1644
+
1645
+ USE WHEN:
1646
+ - Confirming a scene is well-formed after a batch of edits
1647
+ - Diagnosing why a relation/animation isn't behaving
1648
+ - Pre-flighting proposed mutations before applying them
1649
+
1650
+ MODES:
1651
+ - No args → audit the current scene. Returns { ok, diagnosticCount, diagnostics }.
1652
+ - ops → validate a batch of PROPOSED ops against the live scene WITHOUT applying them. Each op is { kind: "addRelation"|"create"|"modify"|"delete", ... }. Ids created earlier in the batch count as existing for later ops, so forward references don't false-error.
1653
+
1654
+ This complements pinepaper_validate_design (which validates a definition object, not the live scene).
1655
+
1656
+ EXAMPLE (pre-flight): { ops: [ { kind: "create", id: "c1", itemType: "circle" }, { kind: "addRelation", source: "c1", target: "missing", relationType: "orbits" } ] }`,
1657
+ inputSchema: {
1658
+ type: 'object',
1659
+ properties: {
1660
+ ops: {
1661
+ type: 'array',
1662
+ description: 'Optional: proposed ops to validate against the live scene instead of auditing the current scene. Each is { kind, ... }.',
1663
+ },
1664
+ },
1665
+ },
1666
+ },
1667
+ {
1668
+ name: 'pinepaper_capture_frames',
1669
+ annotations: {
1670
+ title: 'Capture Frames (Deterministic)',
1671
+ readOnlyHint: false,
1672
+ destructiveHint: false,
1673
+ idempotentHint: true,
1674
+ openWorldHint: false,
1675
+ },
1676
+ description: `Deterministically render the scene at a list of times and snapshot each frame (FxTool's app.captureFramesAt, S3). Math.random is seeded ONCE around the whole sequence and the scene is evaluated at each time via sceneAt(t), so the same scene + same { times, seed } produces identical frames every run — even with random generators/particles.
1677
+
1678
+ By default returns a cheap per-frame hash + byte size (token-light), plus uniqueHashes / allIdentical, so you can:
1679
+ - Verify determinism: run twice with the same seed → identical hashes.
1680
+ - Detect motion: uniqueHashes > 1 means frames actually change over time (allIdentical: true on a multi-time capture usually means nothing is animating).
1681
+
1682
+ Set includeDataUrls: true to also get each frame as a PNG data URL (large — only for a few frames).
1683
+
1684
+ USE WHEN:
1685
+ - Verifying an animation is deterministic / reproducible
1686
+ - Checking whether a scene actually animates across a time range
1687
+ - Grabbing reproducible frames for a regression baseline
1688
+
1689
+ EXAMPLE: { times: [0, 0.5, 1, 1.5, 2], seed: 42 }`,
1690
+ inputSchema: {
1691
+ type: 'object',
1692
+ properties: {
1693
+ times: {
1694
+ type: 'array',
1695
+ items: { type: 'number' },
1696
+ description: 'Capture times in seconds, rendered in order.',
1697
+ },
1698
+ seed: {
1699
+ type: 'number',
1700
+ description: 'Seed for reproducible random generators/particles (default 0).',
1701
+ },
1702
+ includeDataUrls: {
1703
+ type: 'boolean',
1704
+ description: 'Include each frame as a PNG data URL (large). Default false → hashes only.',
1705
+ },
1706
+ },
1707
+ required: ['times'],
1708
+ },
1709
+ },
1710
+ {
1711
+ name: 'pinepaper_media',
1712
+ annotations: {
1713
+ title: 'Media (Video / Audio)',
1714
+ readOnlyHint: false,
1715
+ destructiveHint: false,
1716
+ idempotentHint: false,
1717
+ openWorldHint: true,
1718
+ },
1719
+ description: `Bring VIDEO and AUDIO onto the canvas from a URL, and control playback. Uploaded video is a first-class canvas item (drag, scale, animate, export like any shape); audio rides the timeline.
1720
+
1721
+ ACTIONS:
1722
+ - upload_video: { url (required), position?, scale?, timeOffset? (timeline start, s), clipInPoint?, clipOutPoint? (trim, s) } → { id, duration, width, height, name }
1723
+ - upload_audio: { url (required), volume? (0–1, default 1), loop? (default true), muted?, timeOffset? } → { id, duration, name }
1724
+ - list: → all media [{ id, kind, duration, … }]
1725
+ - remove: { id } → removed boolean
1726
+ - set_playback_rate: { id, rate (0.25–4) } → applies to video or audio
1727
+ - set_clip: { id, inPoint, outPoint (media-time s, outPoint > inPoint) } → re-trim an ALREADY-uploaded clip (video or audio)
1728
+
1729
+ NOTES:
1730
+ - URL-based: the URL is fetched in the browser, so it must be reachable + CORS-permitted.
1731
+ - Use the returned id with set_playback_rate / set_clip / remove, and (for video) modify/animate it like any item.
1732
+
1733
+ EXAMPLE: { action: 'upload_video', url: 'https://…/clip.mp4', scale: 0.5, timeOffset: 1 }`,
1734
+ inputSchema: {
1735
+ type: 'object',
1736
+ properties: {
1737
+ action: { type: 'string', enum: ['upload_video', 'upload_audio', 'list', 'remove', 'set_playback_rate', 'set_clip'], description: "Media action" },
1738
+ url: { type: 'string', description: 'Media URL — required for upload_video / upload_audio.' },
1739
+ id: { type: 'string', description: 'Media id — required for remove / set_playback_rate / set_clip.' },
1740
+ rate: { type: 'number', description: 'Playback rate 0.25–4 — set_playback_rate.' },
1741
+ inPoint: { type: 'number', description: 'Clip in-point (media-time s) — set_clip.' },
1742
+ outPoint: { type: 'number', description: 'Clip out-point (media-time s, > inPoint) — set_clip.' },
1743
+ position: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, description: 'Canvas position — upload_video.' },
1744
+ scale: { type: 'number', description: 'Scale factor — upload_video.' },
1745
+ timeOffset: { type: 'number', description: 'Timeline start offset (s) — upload_video / upload_audio.' },
1746
+ clipInPoint: { type: 'number', description: 'Trim in-point (s) — upload_video.' },
1747
+ clipOutPoint: { type: 'number', description: 'Trim out-point (s) — upload_video.' },
1748
+ volume: { type: 'number', description: 'Volume 0–1 (default 1) — upload_audio.' },
1749
+ loop: { type: 'boolean', description: 'Loop (default true) — upload_audio.' },
1750
+ muted: { type: 'boolean', description: 'Start muted — upload_audio.' },
1751
+ },
1752
+ required: ['action'],
1753
+ },
1754
+ },
1755
+ {
1756
+ name: 'pinepaper_crop_image',
1757
+ annotations: {
1758
+ title: 'Crop Image',
1759
+ readOnlyHint: false,
1760
+ destructiveHint: false,
1761
+ idempotentHint: false,
1762
+ openWorldHint: false,
1763
+ },
1764
+ description: `Crop an imported image to a rectangle — one shot, no interactive session. The replacement raster KEEPS the item's registry id (relations, animations, and connectors stay attached).
1765
+
1766
+ USE WHEN:
1767
+ - Trimming an imported photo/screenshot to the region that matters
1768
+ - Preparing an image before filters, detection, or chroma key (operations compose in any order)
1769
+
1770
+ NOTES:
1771
+ - rect is in canvas coordinates (same space as item positions) and is clamped into the image bounds.
1772
+ - For non-rectangular crops use pinepaper_apply_custom_mask (shape masks) or pinepaper_lasso (freehand).
1773
+
1774
+ EXAMPLE: { itemId: 'item_12', rect: { x: 340, y: 220, width: 320, height: 180 }, aspectRatio: '16:9' }`,
1775
+ inputSchema: {
1776
+ type: 'object',
1777
+ properties: {
1778
+ itemId: { type: 'string', description: 'Registry id of the raster item (or a group containing one)' },
1779
+ rect: {
1780
+ type: 'object',
1781
+ properties: {
1782
+ x: { type: 'number', description: 'Left edge (canvas coords)' },
1783
+ y: { type: 'number', description: 'Top edge (canvas coords)' },
1784
+ width: { type: 'number', description: 'Crop width' },
1785
+ height: { type: 'number', description: 'Crop height' },
1786
+ },
1787
+ required: ['x', 'y', 'width', 'height'],
1788
+ description: 'Crop rectangle in canvas coordinates',
1789
+ },
1790
+ aspectRatio: {
1791
+ type: 'string',
1792
+ enum: ['free', '1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3'],
1793
+ description: 'Optional aspect-ratio constraint',
1794
+ },
1795
+ },
1796
+ required: ['itemId', 'rect'],
1797
+ },
1798
+ },
1799
+ {
1800
+ name: 'pinepaper_chroma_key',
1801
+ annotations: {
1802
+ title: 'Chroma Key (Remove Background Color)',
1803
+ readOnlyHint: false,
1804
+ destructiveHint: false,
1805
+ idempotentHint: false,
1806
+ openWorldHint: false,
1807
+ },
1808
+ description: `Remove a background color from an imported image (green-screen style) — one shot, no interactive session. Pixels near the key color become transparent, with smoothed edges. The replacement raster KEEPS the item's registry id.
1809
+
1810
+ USE WHEN:
1811
+ - Removing a green/blue screen from a photo
1812
+ - Knocking out a flat background color before compositing
1813
+
1814
+ NOTES:
1815
+ - Omit threshold/smoothing to auto-estimate from the image's color histogram.
1816
+ - color accepts '#rrggbb' or a preset: green, chromaGreen, blue, chromaBlue, magenta (default: green).
1817
+ - For irregular subjects on busy backgrounds use pinepaper_lasso or pinepaper_extract_object instead.
1818
+
1819
+ EXAMPLE: { itemId: 'item_7', color: 'green' }
1820
+ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
1821
+ inputSchema: {
1822
+ type: 'object',
1823
+ properties: {
1824
+ itemId: { type: 'string', description: 'Registry id of the raster item (or a group containing one)' },
1825
+ color: { type: 'string', description: "Key color: '#rrggbb' or preset (green, chromaGreen, blue, chromaBlue, magenta). Default green." },
1826
+ threshold: { type: 'number', description: 'Color-distance threshold 0–255 for full transparency. Omit to auto-estimate.' },
1827
+ smoothing: { type: 'number', description: 'Edge-smoothing range past the threshold. Omit to auto-estimate.' },
1828
+ },
1829
+ required: ['itemId'],
1830
+ },
1831
+ },
1832
+ {
1833
+ name: 'pinepaper_rigging',
1834
+ annotations: {
1835
+ title: 'Rigging (Skeleton / Bones / IK / Poses)',
1836
+ readOnlyHint: false,
1837
+ destructiveHint: false,
1838
+ idempotentHint: false,
1839
+ openWorldHint: false,
1840
+ },
1841
+ description: `Rig and animate with SKELETONS — bones, IK chains, and pose keyframes (FxTool RiggingSystem). Build a skeleton, attach canvas items to bones, then drive it with poses. Includes the S12 "breakdown-pose" controls that turn robotic pose-to-pose into believable motion.
1842
+
1843
+ 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).
1844
+
1845
+ ACTIONS:
1846
+ - create_skeleton: { name?, rootPosition? } → { skeletonId }
1847
+ - add_bone: { skeletonId, name?, parentBoneId? (omit for root), length? (80), angle? (deg), flexibility? (0 rigid…1), segments? } → { boneId }
1848
+ - attach_item: { skeletonId, boneId, itemId, attachPoint? (0 joint…1 tip, 0.5) }
1849
+ - create_ik_chain: { skeletonId, boneIds[] (≥2, ordered), solverType? (fabrik|two_bone|ccd), iterations?, tolerance?, strength?, poleVector? } → { chainId }
1850
+ - add_pose_keyframe: { skeletonId, time (s), pose (a saved pose id OR a { boneId: angleDeg } map), easing?, and BREAKDOWN controls: favor? (−1..1 spacing bias), breakdown? (flag), curve? ([x1,y1,x2,y2] cubic-bezier ease, overrides easing), boneOffsets? ({ boneId: 0..0.95 lag } for overlap/follow-through), movingHold? (drift a held pose toward the next key), holdDrift? }
1851
+ - set_target_path: { skeletonId, chainId, waypoints[] ({x,y,out?,in?} bezier tangents), duration?, loop? } — the IK effector travels an ARC, not a straight line
1852
+ - save_pose: { skeletonId, name? } → { poseId } (snapshot current bone angles)
1853
+ - save_shape_key: { skeletonId, name? } → { shapeKeyId } (per-item visual delta for facial-style blends)
1854
+
1855
+ BREAKDOWN POSES (S12): a breakdown keyframe shapes the ARC + SPACING between key poses (favor biases spacing; boneOffsets lag bones so a tip drags its root — follow-through; movingHold keeps a hold alive). This is the difference between mechanical and lifelike motion.`,
1856
+ inputSchema: {
1857
+ type: 'object',
1858
+ properties: {
1859
+ action: { type: 'string', enum: ['create_skeleton', 'add_bone', 'attach_item', 'create_ik_chain', 'add_pose_keyframe', 'set_target_path', 'save_pose', 'save_shape_key'], description: 'Rigging operation' },
1860
+ skeletonId: { type: 'string', description: 'Skeleton id — every action except create_skeleton.' },
1861
+ boneId: { type: 'string', description: 'Bone id — attach_item.' },
1862
+ itemId: { type: 'string', description: 'Canvas item id — attach_item.' },
1863
+ chainId: { type: 'string', description: 'IK chain id — set_target_path.' },
1864
+ name: { type: 'string', description: 'Name — create_skeleton / add_bone / create_ik_chain / save_pose / save_shape_key.' },
1865
+ rootPosition: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, description: 'Skeleton root position — create_skeleton.' },
1866
+ parentBoneId: { type: 'string', description: 'Parent bone id (omit for root) — add_bone.' },
1867
+ length: { type: 'number', description: 'Bone length px (default 80) — add_bone.' },
1868
+ angle: { type: 'number', description: 'Bone rest angle deg — add_bone.' },
1869
+ flexibility: { type: 'number', description: '0 rigid…1 flexible — add_bone.' },
1870
+ segments: { type: 'number', description: 'Curve segments (0 auto) — add_bone.' },
1871
+ attachPoint: { type: 'number', description: 'Along-bone 0 joint…1 tip (0.5) — attach_item.' },
1872
+ boneIds: { type: 'array', items: { type: 'string' }, description: 'Ordered bone ids (≥2) — create_ik_chain.' },
1873
+ solverType: { type: 'string', enum: ['fabrik', 'two_bone', 'ccd'], description: 'IK solver (fabrik) — create_ik_chain.' },
1874
+ iterations: { type: 'number', description: 'Solver iterations (10) — create_ik_chain.' },
1875
+ tolerance: { type: 'number', description: 'Solver tolerance px (0.5) — create_ik_chain.' },
1876
+ strength: { type: 'number', description: 'Chain strength 0..1 (1) — create_ik_chain.' },
1877
+ poleVector: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, description: 'Pole vector hint — create_ik_chain.' },
1878
+ time: { type: 'number', description: 'Keyframe time s — add_pose_keyframe.' },
1879
+ pose: { description: 'Saved pose id (string) OR { boneId: angleDeg } map — add_pose_keyframe.' },
1880
+ easing: { type: 'string', description: 'Named easing (linear) — add_pose_keyframe.' },
1881
+ favor: { type: 'number', description: 'Breakdown spacing bias −1..1 — add_pose_keyframe.' },
1882
+ breakdown: { type: 'boolean', description: 'Mark as breakdown pose — add_pose_keyframe.' },
1883
+ curve: { type: 'array', items: { type: 'number' }, description: 'Cubic-bezier ease [x1,y1,x2,y2] — add_pose_keyframe.' },
1884
+ boneOffsets: { type: 'object', description: 'Per-bone lag { boneId: 0..0.95 } — add_pose_keyframe.' },
1885
+ movingHold: { type: 'boolean', description: 'Drift a held pose toward the next key — add_pose_keyframe.' },
1886
+ holdDrift: { type: 'number', description: 'Moving-hold drift fraction (0.06) — add_pose_keyframe.' },
1887
+ waypoints: { type: 'array', items: { type: 'object' }, description: 'IK path [{x,y,out?,in?}] — set_target_path.' },
1888
+ duration: { type: 'number', description: 'Path duration s (1) — set_target_path.' },
1889
+ loop: { type: 'boolean', description: 'Cycle the path — set_target_path.' },
1890
+ },
1891
+ required: ['action'],
1892
+ },
1893
+ },
1894
+ {
1895
+ name: 'pinepaper_instantiate_ontology',
1896
+ annotations: {
1897
+ title: 'Instantiate Ontology → Scene',
1898
+ readOnlyHint: false,
1899
+ destructiveHint: false,
1900
+ idempotentHint: false,
1901
+ openWorldHint: false,
1902
+ },
1903
+ description: `Compile a design ONTOLOGY (a pp:-namespaced JSON-LD graph of typed nodes + structural edges) into a real scene — the reverse of scene→graph extraction (FxTool S12-E3). "Author the graph, get the design."
1904
+
1905
+ Give it typed nodes (pp:Circle, pp:Text, …) and STRUCTURAL edges (pp:onTopOf, pp:below, pp:beside, pp:inside, pp:centeredOn, pp:alignedWith, or their runtime names on_top_of/…). The compiler:
1906
+ - Places only ROOT nodes (never the source of a structural relation) at absolute coordinates, tiling connected components across the canvas. Every other node is positioned AT RUNTIME by its relations — move a root and its dependents follow.
1907
+ - Emits app.create + app.addRelation for the whole graph in one call, returns { itemIds, diagnostics, errors }.
1908
+
1909
+ Accepts the DesignGraph.toJsonLd shape (pp:nodes / pp:edges), a plain { nodes, edges }, or nodes carrying inline pp: relation properties. Unknown @types fall back to a rectangle with a diagnostic (compilation never hard-fails).
1910
+
1911
+ USE WHEN: you have a structured description of a composition (a graph) and want it built as a live, relation-driven scene rather than placing each item by hand.
1912
+
1913
+ EXAMPLE doc: { "nodes": [ {"id":"bar","type":"pp:Rectangle","width":300,"height":40}, {"id":"glass","type":"pp:Circle","width":60,"height":60} ], "edges": [ {"type":"on_top_of","source":"glass","target":"bar","params":{"gap":2}} ] } → bar is placed; glass rides on top of it.`,
1914
+ inputSchema: {
1915
+ type: 'object',
1916
+ properties: {
1917
+ doc: {
1918
+ type: 'object',
1919
+ description: 'The design graph: pp:nodes / pp:edges, or { nodes, edges }, or nodes with inline pp: relation properties.',
1920
+ },
1921
+ canvas: {
1922
+ type: 'object',
1923
+ properties: {
1924
+ width: { type: 'number' },
1925
+ height: { type: 'number' },
1926
+ },
1927
+ description: 'Canvas size used to tile component roots (default 1080×1080).',
1928
+ },
1929
+ defaultGeometry: {
1930
+ type: 'object',
1931
+ description: 'Per-type default { width, height } overrides, e.g. { "text": { "width": 300, "height": 60 } }.',
1932
+ },
1933
+ },
1934
+ required: ['doc'],
1935
+ },
1936
+ },
1937
+ {
1938
+ name: 'pinepaper_lint_scene',
1939
+ annotations: {
1940
+ title: 'Lint Scene (Relational Density)',
1941
+ readOnlyHint: true,
1942
+ destructiveHint: false,
1943
+ idempotentHint: true,
1944
+ openWorldHint: false,
1945
+ },
1946
+ 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.
1947
+
1948
+ RETURNS:
1949
+ - 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.
1950
+ - 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.
1951
+
1952
+ USE WHEN:
1953
+ - After building a scene, to check it's relation-driven rather than coordinate-driven
1954
+ - To discover which items SHOULD be related (labels on shapes, a badge on a card, stacked elements) but aren't
1955
+ - Before export, as a composition-quality gate
1956
+
1957
+ PARAMS: { eps? (bounds-match tolerance px, default 4), cap? (max suggestions, default 20) }`,
1958
+ inputSchema: {
1959
+ type: 'object',
1960
+ properties: {
1961
+ eps: { type: 'number', description: 'Bounds-match tolerance in px for structural-configuration detection (default 4).' },
1962
+ cap: { type: 'number', description: 'Max suggestions to return, ranked by confidence (default 20).' },
1963
+ },
1964
+ },
1965
+ },
1228
1966
  // ---------------------------------------------------------------------------
1229
1967
  // LAYER 2 — RULES: RELATION TOOLS (KEY FOR ANIMATION)
1230
1968
  // ---------------------------------------------------------------------------
@@ -1248,7 +1986,13 @@ USE WHEN:
1248
1986
  - "reflection mirrors original" → relationType: mirrors
1249
1987
  - "background moves with parallax" → relationType: parallax
1250
1988
  - "player stays in arena" → relationType: bounds_to
1989
+ - "M is the midpoint of A and B" → relationType: is_midpoint_of (params.other: B)
1990
+ - "O is the circumcenter of the triangle" → relationType: is_circumcenter_of
1991
+ - "reveal each step in turn" → relationType: construction_reveal (params.revealAt, fadeIn)
1992
+ - "bar height driven by the slider's x" / "tint the dot by the planet's position" → relationType: driven_by
1993
+ - "make it bob with sin(t)" / "property follows a math expression of time" → relationType: time_expression
1251
1994
  - Creating physics-based or behavioral animations
1995
+ - Building live geometric constructions (GeoGebra-style — dragging an anchor updates the dependent)
1252
1996
 
1253
1997
  RELATION TYPES:
1254
1998
  - orbits: Circular motion around target (params: radius, speed, direction, phase)
@@ -1269,7 +2013,39 @@ RELATION COMPATIBILITY:
1269
2013
  Text→Text: font blend
1270
2014
  Cross-type: particle denoising transition
1271
2015
  - 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.
1272
- - moves_along_path: self-relation (targetId=null); item is driven along a custom-drawn path stored in params.path (array of {x,y} or [x,y]). Params: path, speed, closed, phase, easing (linear|easeIn|easeOut|easeInOut|sine|bounce|pingpong).
2016
+ - 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). Params: path OR equation, speed, closed, phase, easing (linear|easeIn|easeOut|easeInOut|sine|bounce|pingpong).
2017
+
2018
+ 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:
2019
+ - is_midpoint_of: source = midpoint(target, params.other). params.other = id of the second endpoint (B).
2020
+ - lies_on_line: source held on the line target→params.other at fraction params.t (0=A, 1=B; default 0.5). params.other (B), params.t.
2021
+ - is_centroid_of: source = centroid(target, ...params.others). params.others = array of the remaining point ids.
2022
+ - is_circumcenter_of: source = circumcenter(target, params.other1, params.other2); holds position when the three anchors are collinear. params.other1 (B), params.other2 (C).
2023
+ - concentric_with: source stays centered on the target (shared center).
2024
+ - construction_reveal: self-relation (targetId=null); opacity 0→1 starting at params.revealAt (timeline seconds) over params.fadeIn seconds — loop- and scrub-correct. Pair with pinepaper_construction_sequence to reveal a figure one step at a time.
2025
+
2026
+ 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.
2027
+ - 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.
2028
+ - 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).
2029
+
2030
+ 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.
2031
+ - 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.
2032
+ - on_event_add_relation: sourceId=event, targetId=the item to affect. When the event fires, ADD a relation FROM that item: params { type (relation type, e.g. orbits/moves_along_path/driven_by), target (id or null for self), params (that relation's params) }. The scene evolves itself — new orbits/paths appear on a beat.
2033
+ - on_event_remove_relation: sourceId=event, targetId=item. Tears down a previously-added relation: params { type, target }.
2034
+ - on_event_set_color: sourceId=event, targetId=item. params { color, which: 'fill'|'stroke' }.
2035
+ - on_event_set_property: sourceId=event, targetId=item. params { property, value } — set any item property (opacity, rotation, strokeWidth, …).
2036
+ - on_event_set_visibility: sourceId=event, targetId=item. params { visible } (default true).
2037
+
2038
+ STRUCTURAL LAYOUT (S12) — express STATIC COMPOSITION as relations instead of hardcoded x/y. Placement is computed from the TARGET'S BOUNDS and re-derived every frame, so moving or resizing the target moves the dependent with it, and the layout survives as editable graph data. Reach for these whenever the intent is "on", "under", "next to", "inside", or "lined up with" — a create_item with literal coordinates cannot express that intent and silently breaks when the target moves. All six need a target; all accept params.signal (deterministic IR port) and params.window.
2039
+ - on_top_of: source's BOTTOM edge rests on the target's TOP edge — stacking (a cocktail on a bar). params { gap (px between edges, default 0), align: 'left'|'center'(default)|'right', overhang (extra signed x-offset) }.
2040
+ - below: mirror of on_top_of — source's TOP edge rests on the target's BOTTOM edge. params { gap, align, overhang }.
2041
+ - beside: source flanks the target horizontally. params { side: 'left'|'right' (default 'right'), gap, align: 'top'|'center'(default)|'bottom' }.
2042
+ - inside: source is placed within the target's bounds at a 9-way anchor. params { anchor: 'center'(default)|'top-left'|'top'|'top-right'|'left'|'right'|'bottom-left'|'bottom'|'bottom-right', padding (inset from the target edge) }. Contrast bounds_to, which CLAMPS a moving item; inside PLACES it.
2043
+ - centered_on: source center = target center + offset. params { offsetX, offsetY } — concentric rings at zero offset.
2044
+ - aligned_with: matches the target's center on ONE axis, leaving the other free (a partial write — use it to column-align a label while it still moves vertically). params { axis: 'x'|'y' — REQUIRED, the relation is inert without it, offset }.
2045
+
2046
+ CURSOR / MOUSE AS TARGET — a relation can target the live pointer instead of an item by passing the reserved targetId 'cursor' (aliases 'mouse' / 'pointer'); collaborator cursors are 'cursor:<peerId>'. e.g. make an item flee the mouse: relationType 'repels', targetId 'cursor', params { returnSpeed: 0.08 }. The cursor is a virtual target (no canvas item) and its interactions are inspectable in FxTool's "Mouse / Cursor" registry section.
2047
+
2048
+ TEMPORAL VALIDITY WINDOWS (temporal-model v1) — ANY relation may carry params.window = { start, end?, repeat? } (scene seconds) to gate WHEN it is active. The engine only applies the relation within [start, end) and feeds it window-relative local time (so an orbit/wave starts its phase at the window open, not at scene 0). repeat ∈ once (default) | loop | pingpong. Omit end for open-ended. Windowless relations are always active. e.g. params: { ...relationParams, window: { start: 2, end: 5 } } makes the relation live only from 2s to 5s.
1273
2049
 
1274
2050
  VERIFYING ANIMATION WORKS:
1275
2051
  After adding a relation, verify the animation is running:
@@ -1291,7 +2067,7 @@ Relations are COMPOSITIONAL - an item can have multiple relations that work toge
1291
2067
  },
1292
2068
  relationType: {
1293
2069
  type: 'string',
1294
- 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'],
2070
+ 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'],
1295
2071
  description: 'Type of relationship',
1296
2072
  },
1297
2073
  params: {
@@ -1325,7 +2101,7 @@ USE WHEN:
1325
2101
  targetId: { type: 'string', description: 'Target item ID' },
1326
2102
  relationType: {
1327
2103
  type: 'string',
1328
- 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'],
2104
+ 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'],
1329
2105
  description: 'Specific relation type to remove (optional - removes all if not specified)',
1330
2106
  },
1331
2107
  },
@@ -1353,7 +2129,7 @@ USE WHEN:
1353
2129
  itemId: { type: 'string', description: 'Item to query relations for' },
1354
2130
  relationType: {
1355
2131
  type: 'string',
1356
- 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'],
2132
+ 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'],
1357
2133
  description: 'Filter by relation type (optional)',
1358
2134
  },
1359
2135
  direction: {
@@ -1533,6 +2309,14 @@ ANIMATION TYPES:
1533
2309
  type: 'number',
1534
2310
  description: 'Animation speed multiplier (default: 1.0)',
1535
2311
  },
2312
+ intensity: {
2313
+ type: 'number',
2314
+ description: 'Animation amplitude (0.1 = ±10%, default 0.15): drives pulse/wobble/bounce/breathe amplitude and is honored by SVG/SMIL/widget export.',
2315
+ },
2316
+ delay: {
2317
+ type: 'number',
2318
+ description: 'Animation start delay in seconds.',
2319
+ },
1536
2320
  },
1537
2321
  required: ['itemId', 'animationType'],
1538
2322
  },
@@ -1556,7 +2340,7 @@ USE WHEN:
1556
2340
  - Any animation with specific timing or sequential stages
1557
2341
 
1558
2342
  ANIMATABLE PROPERTIES:
1559
- - position: [x, y] array
2343
+ - position: [x, y] array (a { x, y } object is also accepted and normalized)
1560
2344
  - x, y: Individual coordinates
1561
2345
  - scale: Uniform scale
1562
2346
  - scaleX, scaleY: Axis scaling
@@ -1587,6 +2371,7 @@ EXAMPLE — Reveal at 2s, skip first second of keyframe data, end at 4s of keyfr
1587
2371
  type: 'object',
1588
2372
  properties: {
1589
2373
  itemId: { type: 'string', description: 'Registry ID of the item' },
2374
+ timeUnits: { type: 'string', enum: ['seconds', 'ms'], description: "Explicit units for keyframe times/duration. Pass 'seconds' for long-form timelines — values over 100 are otherwise auto-read as milliseconds." },
1590
2375
  keyframes: {
1591
2376
  type: 'array',
1592
2377
  items: {
@@ -1645,7 +2430,9 @@ EXAMPLE — Reveal at 2s, skip first second of keyframe data, end at 4s of keyfr
1645
2430
  USE WHEN:
1646
2431
  - Starting/stopping/pausing timeline playback
1647
2432
  - Seeking to specific time
1648
- - Controlling animation state`,
2433
+ - Controlling animation state
2434
+
2435
+ 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.`,
1649
2436
  inputSchema: {
1650
2437
  type: 'object',
1651
2438
  properties: {
@@ -1666,6 +2453,10 @@ USE WHEN:
1666
2453
  type: 'number',
1667
2454
  description: 'Time to seek to (for seek action)',
1668
2455
  },
2456
+ deterministic: {
2457
+ type: 'boolean',
2458
+ description: 'For seek: evaluate the full scene at the exact time via app.sceneAt(t) — deterministic (same t → same frame). Default false.',
2459
+ },
1669
2460
  },
1670
2461
  required: ['action'],
1671
2462
  },
@@ -2040,6 +2831,8 @@ ACTIONS:
2040
2831
  Cancel any active animation.
2041
2832
  - state — {}
2042
2833
  Returns { zoom, center, isAnimating }.
2834
+ - fit_view — {}
2835
+ Fit canvas content to the viewport (calls app.fitView()).
2043
2836
 
2044
2837
  EXAMPLES:
2045
2838
  { action: "zoom", direction: "in", level: 2.5 }
@@ -2052,7 +2845,7 @@ EXAMPLES:
2052
2845
  properties: {
2053
2846
  action: {
2054
2847
  type: 'string',
2055
- enum: ['zoom', 'pan', 'move_to', 'reset', 'stop', 'state'],
2848
+ enum: ['zoom', 'pan', 'move_to', 'reset', 'stop', 'state', 'fit_view'],
2056
2849
  description: 'Camera action to perform',
2057
2850
  },
2058
2851
  direction: {
@@ -2134,7 +2927,7 @@ EXAMPLE - Bouncing Balls:
2134
2927
  }
2135
2928
 
2136
2929
  SUPPORTED ITEM TYPES: text, circle, star, rectangle, triangle, polygon, ellipse, path, line, arc
2137
- 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
2930
+ 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
2138
2931
  SUPPORTED ANIMATIONS: pulse, rotate, bounce, fade, wobble, slide, typewriter`,
2139
2932
  inputSchema: {
2140
2933
  type: 'object',
@@ -2180,7 +2973,7 @@ SUPPORTED ANIMATIONS: pulse, rotate, bounce, fade, wobble, slide, typewriter`,
2180
2973
  target: { type: 'string', description: 'Name of target item' },
2181
2974
  type: {
2182
2975
  type: 'string',
2183
- 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'],
2976
+ 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'],
2184
2977
  },
2185
2978
  params: {
2186
2979
  type: 'object',
@@ -2465,7 +3258,8 @@ ACTIONS:
2465
3258
  - apply: Apply a single filter. Params: itemId, filterName, params
2466
3259
  - chain: Apply multiple filters in sequence. Params: itemId, filters (array of {name, params})
2467
3260
 
2468
- Available filters: blur, brightness, contrast, saturate, grayscale, sepia, invert, hue-rotate, sharpen, emboss, edge-detect.`,
3261
+ Available filters (GPU raster set): blur, grayscale, sepia, brightness, contrast, saturation, invert, posterize, hsl (hue/saturation/lightness), colorTint (color, intensity, blendMode), vignette (intensity, radius), edgeDetect, halftoneDots (size, angle), halftoneCMYK (size), dither (levels).
3262
+ (Scene-wide filters — sharpen/emboss/noise/vintage etc. — are a different surface: use pinepaper_add_filter.)`,
2469
3263
  inputSchema: {
2470
3264
  type: 'object',
2471
3265
  properties: {
@@ -2592,38 +3386,6 @@ ACTIONS:
2592
3386
  required: ['action'],
2593
3387
  },
2594
3388
  },
2595
- {
2596
- name: 'pinepaper_view',
2597
- annotations: {
2598
- title: 'View',
2599
- readOnlyHint: false,
2600
- destructiveHint: false,
2601
- idempotentHint: false,
2602
- openWorldHint: false,
2603
- },
2604
- description: `View control — fit view to content or get current view state.
2605
-
2606
- ACTIONS:
2607
- - fit: Fit view to content or canvas bounds. Params: mode (content|canvas), padding (number)
2608
- - get_state: Get current view state (zoom, center, bounds)`,
2609
- inputSchema: {
2610
- type: 'object',
2611
- properties: {
2612
- action: {
2613
- type: 'string',
2614
- enum: ['fit', 'get_state'],
2615
- description: 'View action',
2616
- },
2617
- mode: {
2618
- type: 'string',
2619
- enum: ['content', 'canvas'],
2620
- description: 'Fit mode (for fit action)',
2621
- },
2622
- padding: { type: 'number', description: 'Padding around content in pixels (default: 20)' },
2623
- },
2624
- required: ['action'],
2625
- },
2626
- },
2627
3389
  {
2628
3390
  name: 'pinepaper_background',
2629
3391
  annotations: {
@@ -2696,7 +3458,7 @@ ACTIONS:
2696
3458
  },
2697
3459
  },
2698
3460
  // ---------------------------------------------------------------------------
2699
- // DEFORMATION, SPRITE SHEETS, STORAGE, INTERACTION
3461
+ // DEFORMATION, SPRITE SHEETS, INTERACTION
2700
3462
  // ---------------------------------------------------------------------------
2701
3463
  {
2702
3464
  name: 'pinepaper_deform',
@@ -2806,37 +3568,6 @@ ACTIONS:
2806
3568
  required: ['action'],
2807
3569
  },
2808
3570
  },
2809
- {
2810
- name: 'pinepaper_storage',
2811
- annotations: {
2812
- title: 'Storage',
2813
- readOnlyHint: false,
2814
- destructiveHint: true,
2815
- idempotentHint: false,
2816
- openWorldHint: false,
2817
- },
2818
- description: `Persistent project storage via IndexedDB.
2819
-
2820
- ACTIONS:
2821
- - save: Save current project. Params: name, thumbnail (bool)
2822
- - load: Load project onto canvas. Params: projectId
2823
- - list: List all saved projects
2824
- - delete: Delete a saved project. Params: projectId`,
2825
- inputSchema: {
2826
- type: 'object',
2827
- properties: {
2828
- action: {
2829
- type: 'string',
2830
- enum: ['save', 'load', 'list', 'delete'],
2831
- description: 'Storage action',
2832
- },
2833
- projectId: { type: 'string', description: 'Project ID (for load/delete)' },
2834
- name: { type: 'string', description: 'Project name (for save)' },
2835
- thumbnail: { type: 'boolean', description: 'Auto-capture thumbnail on save' },
2836
- },
2837
- required: ['action'],
2838
- },
2839
- },
2840
3571
  {
2841
3572
  name: 'pinepaper_interaction',
2842
3573
  annotations: {
@@ -2855,6 +3586,7 @@ ACTIONS:
2855
3586
  - get_state: Get interaction state (score, progress, step)
2856
3587
 
2857
3588
  BEHAVIOR TYPES: repel, attract, follow, orbit, slingshot, physics_body, draggable_constrained
3589
+ - repel params: strength, maxDistance, minDistance, returnSpeed (0–1, default 0.08 — how fast the item eases back to its home position once the target/cursor leaves; returnSpeed: 0 freezes it scattered instead of reforming).
2858
3590
 
2859
3591
  ACTION TYPES: navigate, show, hide, animate, stopAnimation, setState, incrementScore, playTimeline, pauseTimeline, seekTimeline, showFeedback, complete`,
2860
3592
  inputSchema: {
@@ -2935,7 +3667,7 @@ Params: title (page title), download (bool)`,
2935
3667
  },
2936
3668
  },
2937
3669
  // ---------------------------------------------------------------------------
2938
- // LAYER 5 — BLUEPRINTS: TEMPLATE TOOLS
3670
+ // DOMAIN: TEMPLATE TOOL
2939
3671
  // ---------------------------------------------------------------------------
2940
3672
  {
2941
3673
  name: 'pinepaper_apply_template',
@@ -2962,19 +3694,8 @@ MODES:
2962
3694
  2. LOAD MODE: Provide a templateId to apply that template
2963
3695
 
2964
3696
  TEMPLATE CATEGORIES (13):
2965
- - social-media: Social media post/story layouts
2966
- - meme: Meme templates and formats
2967
- - business: Business cards, letterheads, presentations
2968
- - education: Educational diagrams, worksheets
2969
- - creative: Artistic and creative compositions
2970
- - tech: Technology-themed designs
2971
- - global: International and multicultural designs
2972
- - indigenous: Indigenous art-inspired patterns
2973
- - seasonal: Holiday and seasonal themes
2974
- - masking: Mask reveal effect demonstrations
2975
- - scenes: Pre-built animated scenes
2976
- - diagrams: Flowchart and diagram starters
2977
- - maps: Geographic visualization templates
3697
+ - social-media, meme, business, education, creative, tech, global,
3698
+ indigenous, seasonal, masking, scenes, diagrams, maps
2978
3699
 
2979
3700
  EXAMPLES:
2980
3701
  - List all templates: { "listOnly": true }
@@ -3825,6 +4546,55 @@ ACTIONS:
3825
4546
  required: ['action'],
3826
4547
  },
3827
4548
  },
4549
+ {
4550
+ name: 'pinepaper_globe',
4551
+ annotations: {
4552
+ title: 'Map: Globe + World Tour',
4553
+ readOnlyHint: false,
4554
+ destructiveHint: false,
4555
+ idempotentHint: false,
4556
+ openWorldHint: false,
4557
+ },
4558
+ description: `Turn a loaded map into a spinning 3D globe and swing it on a cinematic world tour. Requires a map loaded via pinepaper_map { action: "load" } first; call action: "enable" to switch to globe (orthographic) mode.
4559
+
4560
+ ACTIONS:
4561
+ - enable — { rotation?: [lambda,phi,gamma], momentum?, showOcean? } switch to globe mode
4562
+ - rotate_to — { lon, lat, duration? } versor-slerp the globe to face a coordinate
4563
+ - spin — { speed?, axis?: "longitude"|"latitude", duration? } continuous auto-rotation
4564
+ - world_tour — { regions?: string[] | coords?: [lon,lat][], dwell?, travel?, tilt?, loop?, easing?, highlightColors?: string[], showLabels?, labelColor? } the headline: the globe swings to each stop, highlighting it (requires regions OR coords)
4565
+ - stop_tour — { id? } omit id to stop all tours
4566
+ - pin_item — { itemId, lon, lat, hideOnFarSide? } pin a canvas item to the globe surface
4567
+ - tour_item — { itemId, regions?: string[] | coords?: [lon,lat][], dwell?, travel?, loop?, orient? } move a canvas item along the globe (requires regions OR coords)`,
4568
+ inputSchema: {
4569
+ type: 'object',
4570
+ properties: {
4571
+ action: { type: 'string', enum: ['enable', 'rotate_to', 'spin', 'world_tour', 'stop_tour', 'pin_item', 'tour_item'], description: 'Globe action' },
4572
+ rotation: { type: 'array', items: { type: 'number' }, description: 'Initial rotation [lambda, phi, gamma] (enable)' },
4573
+ momentum: { type: 'boolean', description: 'Drag-spin inertia (enable)' },
4574
+ showOcean: { type: 'boolean', description: 'Render ocean sphere (enable)' },
4575
+ lon: { type: 'number', description: 'Longitude (rotate_to, pin_item)' },
4576
+ lat: { type: 'number', description: 'Latitude (rotate_to, pin_item)' },
4577
+ duration: { type: 'number', description: 'Animation duration in seconds (rotate_to, spin)' },
4578
+ speed: { type: 'number', description: 'Spin speed deg/sec (spin)' },
4579
+ axis: { type: 'string', enum: ['longitude', 'latitude'], description: 'Spin axis (spin)' },
4580
+ regions: { type: 'array', items: { type: 'string' }, description: 'Region IDs to visit (world_tour, tour_item)' },
4581
+ coords: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: 'Coordinate stops [lon,lat][] (world_tour, tour_item)' },
4582
+ dwell: { type: 'number', description: 'Seconds dwelling at each stop' },
4583
+ travel: { type: 'number', description: 'Seconds travelling between stops' },
4584
+ tilt: { type: 'number', description: 'Camera tilt per stop (world_tour)' },
4585
+ loop: { type: 'boolean', description: 'Loop the tour' },
4586
+ easing: { type: 'string', description: 'Swing easing curve (world_tour)' },
4587
+ highlightColors: { type: 'array', items: { type: 'string' }, description: 'Highlight colors cycled per stop (world_tour)' },
4588
+ showLabels: { type: 'boolean', description: 'Label each stop (world_tour)' },
4589
+ labelColor: { type: 'string', description: 'Label text color (world_tour)' },
4590
+ id: { type: 'string', description: 'Tour id to stop (stop_tour)' },
4591
+ itemId: { type: 'string', description: 'Canvas item id (pin_item, tour_item)' },
4592
+ hideOnFarSide: { type: 'boolean', description: 'Hide item on far side of globe (pin_item)' },
4593
+ orient: { type: 'boolean', description: 'Orient item along travel direction (tour_item)' },
4594
+ },
4595
+ required: ['action'],
4596
+ },
4597
+ },
3828
4598
  // ---------------------------------------------------------------------------
3829
4599
  // DOMAIN: FONT TOOLS — single action-dispatched tool replaces 16 wrappers
3830
4600
  // ---------------------------------------------------------------------------
@@ -3923,6 +4693,7 @@ STYLE TYPES:
3923
4693
  - fold: Origami-style folded letters
3924
4694
  - gradient: Each letter with gradient fill (use with gradient palettes)
3925
4695
  - image: Letters filled with image
4696
+ - particle: Each glyph sampled into a cloud of colored dots (re-typable "particle font"); reuses tile palettes. Optional particleSpacing / particleDotSize tune density.
3926
4697
 
3927
4698
  TILE PALETTES:
3928
4699
  - Game: wordle, scrabble
@@ -3951,7 +4722,7 @@ EXAMPLES:
3951
4722
  },
3952
4723
  style: {
3953
4724
  type: 'string',
3954
- enum: ['tile', 'magazine', 'paperCut', 'fold', 'gradient', 'image'],
4725
+ enum: ['tile', 'magazine', 'paperCut', 'fold', 'gradient', 'image', 'particle'],
3955
4726
  description: 'Style type (default: tile)',
3956
4727
  },
3957
4728
  palette: {
@@ -4006,6 +4777,14 @@ EXAMPLES:
4006
4777
  type: 'boolean',
4007
4778
  description: 'Enable drop shadows (default: true)',
4008
4779
  },
4780
+ particleSpacing: {
4781
+ type: 'number',
4782
+ description: 'Dot spacing for style="particle" (smaller = denser cloud; default ~fontSize*0.11)',
4783
+ },
4784
+ particleDotSize: {
4785
+ type: 'number',
4786
+ description: 'Base dot radius for style="particle" (default ~particleSpacing*0.44)',
4787
+ },
4009
4788
  },
4010
4789
  required: ['text'],
4011
4790
  },
@@ -4065,33 +4844,6 @@ EXAMPLES:
4065
4844
  required: ['collageId', 'animationType'],
4066
4845
  },
4067
4846
  },
4068
- {
4069
- name: 'pinepaper_get_letter_collage_options',
4070
- annotations: {
4071
- title: 'Get Letter Collage Options',
4072
- readOnlyHint: true,
4073
- destructiveHint: false,
4074
- idempotentHint: true,
4075
- openWorldHint: false,
4076
- },
4077
- description: `[Utility] Get available styles, palettes, and gradient options for letter collages.
4078
-
4079
- USE WHEN:
4080
- - Learning available letter collage styles
4081
- - Discovering palette options
4082
- - Exploring gradient possibilities
4083
- - Building UI for collage creation
4084
-
4085
- RETURNS:
4086
- - styles: All available style types
4087
- - tilePalettes: Grouped by category (game, vibrant, soft, natural, etc.)
4088
- - gradientPalettes: Available gradient color schemes
4089
- - gradientDirections: Direction options for gradients`,
4090
- inputSchema: {
4091
- type: 'object',
4092
- properties: {},
4093
- },
4094
- },
4095
4847
  // ---------------------------------------------------------------------------
4096
4848
  // DOMAIN: GENERATOR TOOLS
4097
4849
  // ---------------------------------------------------------------------------
@@ -4150,6 +4902,25 @@ PineMath — Math/Science:
4150
4902
  - drawSpectrumAnalyzer: FFT signal visualization (waveform: sine|square|sawtooth|triangle|noise|composite, frequency, amplitude, sampleRate, window: none|hann|hamming|blackman, waveColor, spectrumColor)
4151
4903
  - draw3DSurface: 3D surfaces from equations (preset: torus|sphere|wave|klein|mobius|custom, xExpr, yExpr, zExpr, uSteps, vSteps, scale, rotateSpeed)
4152
4904
 
4905
+ S6 Procedural (seeded, OKLCH palettes, curated presets, optional Motion). All take: palette (ocean|neon|ember|slate|dusk|pastel|mint|lagoon…), colorMode (gradient|random|mono), bgColor, seed (same seed → identical output), animation (none|driftX|driftY|rotate|pulse), animationSpeed:
4906
+ - drawTruchet: Truchet tiles — interlocking arcs/diagonals (tileSize, lineWidth, variant: arcs|diagonals, lineColor). Presets: Ocean Maze, Neon Circuit, Ember Weave, Slate Diagonals.
4907
+ - drawHalftone: halftone dot field (toneMode: radial|noise|linear, dotSpacing, maxDotRadius, contrast, dotColor, opacity). Presets: Sunburst Dots, Noise Field, Pop Ramp, Soft Mono.
4908
+ - drawRibbons: flowing Bézier ribbons (ribbonCount, ribbonWidth, amplitude, waviness, opacity). Presets: Deep Current, Aurora, Silk, Mint Flow.
4909
+
4910
+ GPU / GLSL math-art (transpiled to a fragment shader, 60fps; all take renderScale 0.25–1, transparentBg, speed):
4911
+ - drawFormulaArt: type a math expression in x,y,t → scalar mapped through a 3-stop palette (formula, domain, color1/color2/color3). Try sin(x*5+t)*cos(y*3).
4912
+ - drawParametricCollection: N circles or line segments indexed by k, expressions in k,N,t — Yeganeh-style (mode: circles|lines, count, xExpression/yExpression/radiusExpression or x1..y2Expression, scale, palette, bgColor).
4913
+ - drawShaderArt: paste your own GLSL fragment shader (fragmentSource; uniforms u_time, u_resolution, u_color1/2/3) or pick a preset.
4914
+ - drawYeganehMountains: GLSL port of Hamid Naderi Yeganeh's "Mountains" (mountainCount, timeOffset, skyTop, skyBottom, ridgeColor, valleyColor).
4915
+
4916
+ OKLCH seeded backgrounds (palette + seed → reproducible):
4917
+ - drawBlobs: soft organic blobs (smoothed jittered circles). Dreamy/soft backgrounds.
4918
+ - drawLowPoly: triangulated low-poly facets (gradient-filled). Modern/geometric.
4919
+ - drawPeaks: stacked zig-zag mountain ridgeline bands.
4920
+ - drawStackedWaves: layered cubic-bézier wave bands — hero/section-divider background.
4921
+ - drawScatter: scattered field of small shapes (circles/triangles/squares), palette-colored.
4922
+ - draw3DParametricCurve: plot a 3D curve (x(t),y(t),z(t)) projected to canvas. Try Helix: sin(t),cos(t),t/4.
4923
+
4153
4924
  COMMON PARAMS (all generators):
4154
4925
  - bgColor: Background color ("none"/"transparent" for no background)
4155
4926
  - interactive: Make generated items selectable/draggable (default false)
@@ -4167,6 +4938,9 @@ COMMON PARAMS (all generators):
4167
4938
  'drawGlobeWireframe',
4168
4939
  'drawFunctionPlot', 'drawParametricCurve', 'drawSimulation',
4169
4940
  'drawSpectrumAnalyzer', 'draw3DSurface',
4941
+ 'drawTruchet', 'drawHalftone', 'drawRibbons',
4942
+ 'drawFormulaArt', 'drawParametricCollection', 'drawShaderArt', 'drawYeganehMountains',
4943
+ 'drawBlobs', 'drawLowPoly', 'drawPeaks', 'drawScatter', 'drawStackedWaves', 'draw3DParametricCurve',
4170
4944
  ],
4171
4945
  description: 'Generator name',
4172
4946
  },
@@ -4175,6 +4949,17 @@ COMMON PARAMS (all generators):
4175
4949
  description: 'Generator-specific parameters',
4176
4950
  additionalProperties: true,
4177
4951
  },
4952
+ region: {
4953
+ type: 'object',
4954
+ description: 'Optional sub-region {x, y, width, height} (canvas coords) — generator draws into a clipped, translated sub-group instead of full-canvas. Region runs imply preserve (won\'t wipe the canvas) and re-running the same generator replaces its prior region; different generators\' regions coexist. Invalid/zero-size regions fall back to full-canvas.',
4955
+ properties: {
4956
+ x: { type: 'number', description: 'Region top-left X in canvas coords' },
4957
+ y: { type: 'number', description: 'Region top-left Y in canvas coords' },
4958
+ width: { type: 'number', minimum: 1, description: 'Region width in pixels' },
4959
+ height: { type: 'number', minimum: 1, description: 'Region height in pixels' },
4960
+ },
4961
+ required: ['x', 'y', 'width', 'height'],
4962
+ },
4178
4963
  },
4179
4964
  required: ['generatorName'],
4180
4965
  },
@@ -4220,7 +5005,7 @@ USE WHEN:
4220
5005
  - Celebration effects (confetti)
4221
5006
  - Enhancing visual impact (ripple, glow, electric)
4222
5007
 
4223
- EFFECTS (15 total):
5008
+ EFFECTS (15 particle + 3 shader auras):
4224
5009
  - sparkle: Glitter/sparkle particles (color, speed, size)
4225
5010
  - blast: Explosion burst effect (color, radius, count)
4226
5011
  - smoke: Rising smoke plumes (color, speed, size, drift, height, growthRate)
@@ -4237,15 +5022,23 @@ EFFECTS (15 total):
4237
5022
  - shockwave: Expanding concentric rings — burst type (speed, maxRadius, count, thickness)
4238
5023
  - trail: Fading afterimages following movement (speed, count, size, fadeRate)
4239
5024
 
5025
+ SHADER AURAS (WebGL2, silhouette-clipped — same tool, routed to the aura system):
5026
+ - heatmap: Thermal shimmer aura (palette 0=Hot 1=Cool 2=Violet)
5027
+ - liquid_metal: Flowing metallic sheen (palette 0=Chrome 1=Gold 2=Copper)
5028
+ - gem_smoke: Crystalline smoke wisps
5029
+ 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.
5030
+
4240
5031
  EXAMPLE — bubbles with magenta interior over a blue rim:
4241
- { effectType: "bubbles", params: { color: "#93c5fd", fill: "#ff00aa", fillOpacity: 0.4 } }`,
5032
+ { effectType: "bubbles", params: { color: "#93c5fd", fill: "#ff00aa", fillOpacity: 0.4 } }
5033
+ EXAMPLE — gold liquid-metal aura outside a logo:
5034
+ { effectType: "liquid_metal", params: { palette: 1, mode: "outside", intensity: 0.8 } }`,
4242
5035
  inputSchema: {
4243
5036
  type: 'object',
4244
5037
  properties: {
4245
5038
  itemId: { type: 'string', description: 'Registry ID of the item' },
4246
5039
  effectType: {
4247
5040
  type: 'string',
4248
- enum: ['sparkle', 'blast', 'smoke', 'fire', 'rain', 'snow', 'confetti', 'ripple', 'glow', 'electric', 'bubbles', 'dust', 'fireflies', 'shockwave', 'trail'],
5041
+ enum: ['sparkle', 'blast', 'smoke', 'fire', 'rain', 'snow', 'confetti', 'ripple', 'glow', 'electric', 'bubbles', 'dust', 'fireflies', 'shockwave', 'trail', 'heatmap', 'liquid_metal', 'gem_smoke'],
4249
5042
  description: 'Type of effect',
4250
5043
  },
4251
5044
  params: {
@@ -4317,409 +5110,6 @@ Filters can be stacked - call multiple times to combine effects.`,
4317
5110
  },
4318
5111
  },
4319
5112
  // ---------------------------------------------------------------------------
4320
- // DOMAIN: INTERACTIVE TRIGGER TOOLS
4321
- // ---------------------------------------------------------------------------
4322
- {
4323
- name: 'pinepaper_add_trigger',
4324
- annotations: {
4325
- title: 'Add Trigger',
4326
- readOnlyHint: false,
4327
- destructiveHint: false,
4328
- idempotentHint: false,
4329
- openWorldHint: false,
4330
- },
4331
- description: `Add an interactive trigger to an item. Triggers execute actions when events occur.
4332
-
4333
- USE WHEN:
4334
- - Making items clickable
4335
- - Adding hover effects
4336
- - Creating drag interactions
4337
- - Building interactive tutorials/quizzes
4338
- - Timeline-based triggers
4339
-
4340
- TRIGGER EVENTS:
4341
- - click: When item is clicked
4342
- - hover_enter: When mouse enters item
4343
- - hover_exit: When mouse leaves item
4344
- - drag_start: When item drag begins
4345
- - drag_move: While item is being dragged
4346
- - drag_end: When item drag ends
4347
- - timeline: At specific time in animation
4348
- - scene_enter: When scene becomes active
4349
- - scene_exit: When scene deactivates
4350
- - animation_end: When item's animation completes
4351
- - quiz_answer: When quiz answer is submitted
4352
-
4353
- ACTION TYPES:
4354
- - show: Make target item visible
4355
- - hide: Hide target item
4356
- - toggle_visibility: Toggle target visibility
4357
- - play_animation: Start animation on target
4358
- - stop_animation: Stop animation on target
4359
- - navigate: Navigate to scene or URL
4360
- - update_property: Change item property
4361
- - set_variable: Set a scene variable
4362
- - submit_answer: Submit quiz answer
4363
- - increment_score: Add to quiz score
4364
- - reset_quiz: Reset quiz state
4365
-
4366
- EXAMPLES:
4367
- - Click to show: {itemId: "button_1", event: "click", actions: [{type: "show", targetItemId: "panel_1"}]}
4368
- - Hover to animate: {itemId: "star_1", event: "hover_enter", actions: [{type: "play_animation", targetItemId: "star_1", animationType: "pulse"}]}
4369
- - Click to navigate: {itemId: "next_btn", event: "click", actions: [{type: "navigate", url: "#scene2"}]}`,
4370
- inputSchema: {
4371
- type: 'object',
4372
- properties: {
4373
- itemId: {
4374
- type: 'string',
4375
- description: 'Item to attach trigger to',
4376
- },
4377
- event: {
4378
- type: 'string',
4379
- enum: ['click', 'hover_enter', 'hover_exit', 'drag_start', 'drag_move', 'drag_end', 'timeline', 'scene_enter', 'scene_exit', 'animation_end', 'quiz_answer'],
4380
- description: 'Event that fires the trigger',
4381
- },
4382
- actions: {
4383
- type: 'array',
4384
- items: {
4385
- type: 'object',
4386
- properties: {
4387
- type: {
4388
- type: 'string',
4389
- enum: ['show', 'hide', 'toggle_visibility', 'play_animation', 'stop_animation', 'navigate', 'update_property', 'set_variable', 'submit_answer', 'increment_score', 'reset_quiz'],
4390
- description: 'Action type',
4391
- },
4392
- targetItemId: {
4393
- type: 'string',
4394
- description: 'Target item for the action',
4395
- },
4396
- animationType: {
4397
- type: 'string',
4398
- description: 'Animation type for play_animation',
4399
- },
4400
- property: {
4401
- type: 'string',
4402
- description: 'Property name for update_property',
4403
- },
4404
- value: {
4405
- description: 'Value for update_property or set_variable',
4406
- },
4407
- variableName: {
4408
- type: 'string',
4409
- description: 'Variable name for set_variable',
4410
- },
4411
- url: {
4412
- type: 'string',
4413
- description: 'URL or scene ID for navigate',
4414
- },
4415
- points: {
4416
- type: 'number',
4417
- description: 'Points for increment_score',
4418
- },
4419
- },
4420
- required: ['type'],
4421
- },
4422
- description: 'Actions to execute when triggered',
4423
- },
4424
- condition: {
4425
- type: 'string',
4426
- description: 'Optional condition expression (e.g., "$score > 10")',
4427
- },
4428
- timelineOffset: {
4429
- type: 'number',
4430
- description: 'Time offset in ms for timeline triggers',
4431
- },
4432
- },
4433
- required: ['itemId', 'event', 'actions'],
4434
- },
4435
- },
4436
- {
4437
- name: 'pinepaper_remove_trigger',
4438
- annotations: {
4439
- title: 'Remove Trigger',
4440
- readOnlyHint: false,
4441
- destructiveHint: true,
4442
- idempotentHint: true,
4443
- openWorldHint: false,
4444
- },
4445
- description: `Remove triggers from an item.
4446
-
4447
- USE WHEN:
4448
- - Disabling interactivity on an item
4449
- - Cleaning up triggers before adding new ones
4450
- - Removing specific event handlers
4451
-
4452
- OPTIONS:
4453
- - Remove specific event: {itemId: "button_1", event: "click"}
4454
- - Remove all triggers: {itemId: "button_1", removeAll: true}`,
4455
- inputSchema: {
4456
- type: 'object',
4457
- properties: {
4458
- itemId: {
4459
- type: 'string',
4460
- description: 'Item to remove triggers from',
4461
- },
4462
- event: {
4463
- type: 'string',
4464
- enum: ['click', 'hover_enter', 'hover_exit', 'drag_start', 'drag_move', 'drag_end', 'timeline', 'scene_enter', 'scene_exit', 'animation_end', 'quiz_answer'],
4465
- description: 'Specific event to remove (optional)',
4466
- },
4467
- removeAll: {
4468
- type: 'boolean',
4469
- description: 'Remove all triggers from item',
4470
- },
4471
- },
4472
- required: ['itemId'],
4473
- },
4474
- },
4475
- {
4476
- name: 'pinepaper_query_triggers',
4477
- annotations: {
4478
- title: 'Query Triggers',
4479
- readOnlyHint: true,
4480
- destructiveHint: false,
4481
- idempotentHint: true,
4482
- openWorldHint: false,
4483
- },
4484
- description: `[Utility] List all triggers on the canvas or for a specific item.
4485
-
4486
- USE WHEN:
4487
- - Inspecting existing interactivity
4488
- - Debugging trigger behavior
4489
- - Understanding scene's interactive elements
4490
-
4491
- EXAMPLES:
4492
- - All triggers: {}
4493
- - Triggers on specific item: {itemId: "button_1"}
4494
- - Filter by event type: {event: "click"}`,
4495
- inputSchema: {
4496
- type: 'object',
4497
- properties: {
4498
- itemId: {
4499
- type: 'string',
4500
- description: 'Filter triggers by item ID',
4501
- },
4502
- event: {
4503
- type: 'string',
4504
- enum: ['click', 'hover_enter', 'hover_exit', 'drag_start', 'drag_move', 'drag_end', 'timeline', 'scene_enter', 'scene_exit', 'animation_end', 'quiz_answer'],
4505
- description: 'Filter by event type',
4506
- },
4507
- },
4508
- },
4509
- },
4510
- // ---------------------------------------------------------------------------
4511
- // DOMAIN: QUIZ/LMS TOOLS
4512
- // ---------------------------------------------------------------------------
4513
- {
4514
- name: 'pinepaper_create_quiz',
4515
- annotations: {
4516
- title: 'Create Quiz',
4517
- readOnlyHint: false,
4518
- destructiveHint: false,
4519
- idempotentHint: false,
4520
- openWorldHint: false,
4521
- },
4522
- description: `Create an interactive quiz with questions, answers, and scoring.
4523
-
4524
- USE WHEN:
4525
- - Building educational content
4526
- - Creating assessments
4527
- - Adding gamification elements
4528
- - Making interactive learning modules
4529
-
4530
- QUESTION TYPES:
4531
- - multiple-choice: Single correct answer from options
4532
- - multiple-select: Multiple correct answers
4533
- - drag-drop: Drag items to correct zones
4534
- - matching: Match pairs of items
4535
- - sequencing: Put items in correct order
4536
- - hotspot: Click correct area on image
4537
- - true-false: True or false question
4538
-
4539
- FEEDBACK:
4540
- Each question can have:
4541
- - correctFeedback: Message shown on correct answer
4542
- - incorrectFeedback: Message shown on wrong answer
4543
- - partialFeedback: Message for partially correct (multi-select)
4544
-
4545
- SCORING:
4546
- - Each question has a points value
4547
- - passingScore: Minimum score to pass
4548
- - showScore: Display score during quiz
4549
- - allowRetry: Allow retrying incorrect answers
4550
-
4551
- EXAMPLE:
4552
- {
4553
- "title": "Geography Quiz",
4554
- "questions": [
4555
- {
4556
- "type": "multiple-choice",
4557
- "prompt": "What is the capital of France?",
4558
- "options": [
4559
- {"id": "a", "label": "London"},
4560
- {"id": "b", "label": "Paris", "isCorrect": true},
4561
- {"id": "c", "label": "Berlin"}
4562
- ],
4563
- "points": 10,
4564
- "correctFeedback": "Correct! Paris is the capital.",
4565
- "incorrectFeedback": "Sorry, the correct answer is Paris."
4566
- }
4567
- ],
4568
- "passingScore": 70,
4569
- "showScore": true
4570
- }`,
4571
- inputSchema: {
4572
- type: 'object',
4573
- properties: {
4574
- title: {
4575
- type: 'string',
4576
- description: 'Quiz title',
4577
- },
4578
- questions: {
4579
- type: 'array',
4580
- items: {
4581
- type: 'object',
4582
- properties: {
4583
- type: {
4584
- type: 'string',
4585
- enum: ['multiple-choice', 'multiple-select', 'drag-drop', 'matching', 'sequencing', 'hotspot', 'true-false'],
4586
- description: 'Question type',
4587
- },
4588
- prompt: {
4589
- type: 'string',
4590
- description: 'Question text',
4591
- },
4592
- options: {
4593
- type: 'array',
4594
- items: {
4595
- type: 'object',
4596
- properties: {
4597
- id: { type: 'string' },
4598
- label: { type: 'string' },
4599
- isCorrect: { type: 'boolean' },
4600
- },
4601
- },
4602
- description: 'Answer options',
4603
- },
4604
- points: {
4605
- type: 'number',
4606
- description: 'Points for correct answer',
4607
- },
4608
- correctFeedback: {
4609
- type: 'string',
4610
- description: 'Feedback for correct answer',
4611
- },
4612
- incorrectFeedback: {
4613
- type: 'string',
4614
- description: 'Feedback for incorrect answer',
4615
- },
4616
- draggableItems: {
4617
- type: 'array',
4618
- items: { type: 'string' },
4619
- description: 'Item IDs that can be dragged (for drag-drop)',
4620
- },
4621
- dropZones: {
4622
- type: 'array',
4623
- items: {
4624
- type: 'object',
4625
- properties: {
4626
- id: { type: 'string' },
4627
- correctItems: { type: 'array', items: { type: 'string' } },
4628
- },
4629
- },
4630
- description: 'Drop zones with correct items (for drag-drop)',
4631
- },
4632
- },
4633
- required: ['type', 'prompt', 'points'],
4634
- },
4635
- description: 'Quiz questions',
4636
- },
4637
- passingScore: {
4638
- type: 'number',
4639
- description: 'Minimum percentage to pass (0-100)',
4640
- },
4641
- showScore: {
4642
- type: 'boolean',
4643
- description: 'Display score during quiz',
4644
- },
4645
- allowRetry: {
4646
- type: 'boolean',
4647
- description: 'Allow retrying incorrect answers',
4648
- },
4649
- shuffleQuestions: {
4650
- type: 'boolean',
4651
- description: 'Randomize question order',
4652
- },
4653
- shuffleOptions: {
4654
- type: 'boolean',
4655
- description: 'Randomize answer options',
4656
- },
4657
- },
4658
- required: ['questions'],
4659
- },
4660
- },
4661
- {
4662
- name: 'pinepaper_get_quiz_state',
4663
- annotations: {
4664
- title: 'Get Quiz State',
4665
- readOnlyHint: true,
4666
- destructiveHint: false,
4667
- idempotentHint: true,
4668
- openWorldHint: false,
4669
- },
4670
- description: `[Utility] Get the current state of an active quiz.
4671
-
4672
- USE WHEN:
4673
- - Checking quiz progress
4674
- - Getting current score
4675
- - Reviewing answered questions
4676
- - Debugging quiz behavior
4677
-
4678
- RETURNS:
4679
- - quizId: Quiz identifier
4680
- - currentQuestion: Current question index
4681
- - totalQuestions: Total number of questions
4682
- - score: Current score
4683
- - maxScore: Maximum possible score
4684
- - answers: Array of submitted answers
4685
- - passed: Whether passing score achieved (if complete)
4686
- - complete: Whether quiz is finished`,
4687
- inputSchema: {
4688
- type: 'object',
4689
- properties: {
4690
- quizId: {
4691
- type: 'string',
4692
- description: 'Quiz ID (optional, uses active quiz if not specified)',
4693
- },
4694
- },
4695
- },
4696
- },
4697
- {
4698
- name: 'pinepaper_reset_quiz',
4699
- annotations: {
4700
- title: 'Reset Quiz',
4701
- readOnlyHint: false,
4702
- destructiveHint: true,
4703
- idempotentHint: true,
4704
- openWorldHint: false,
4705
- },
4706
- description: `Reset a quiz to its initial state.
4707
-
4708
- USE WHEN:
4709
- - Restarting a quiz
4710
- - Clearing quiz progress
4711
- - Allowing user to retake quiz`,
4712
- inputSchema: {
4713
- type: 'object',
4714
- properties: {
4715
- quizId: {
4716
- type: 'string',
4717
- description: 'Quiz ID (optional, uses active quiz if not specified)',
4718
- },
4719
- },
4720
- },
4721
- },
4722
- // ---------------------------------------------------------------------------
4723
5113
  // INFRASTRUCTURE: QUERY TOOLS
4724
5114
  // ---------------------------------------------------------------------------
4725
5115
  {
@@ -5300,7 +5690,7 @@ WORKFLOW:
5300
5690
  1. pinepaper_agent_start_job → 2. pinepaper_agent_batch_execute (everything) → 3. pinepaper_agent_end_job (screenshot for validation)
5301
5691
  After validation: user reviews screenshot → feedback → modify/recreate as needed.
5302
5692
 
5303
- OPERATION TYPES (12) — use in this order:
5693
+ OPERATION TYPES (13) — use in this order:
5304
5694
 
5305
5695
  CANVAS SETUP:
5306
5696
  set_canvas_size — {width, height} or {preset: "instagram"|"youtube"|"tiktok"|...}
@@ -5311,7 +5701,7 @@ CANVAS SETUP:
5311
5701
 
5312
5702
  ITEMS:
5313
5703
  create — {itemType, position: {x,y}, properties}
5314
- itemType: text, circle, rectangle, star, triangle, polygon, ellipse, path, line, arc
5704
+ itemType: text, circle, rectangle, star, triangle, polygon, ellipse, path, line, arc, pentagon, hexagon, diamond, arrow, heart
5315
5705
  Properties per type:
5316
5706
  text: {content, fontSize, fontFamily, color, fontWeight}
5317
5707
  circle: {radius, color, strokeColor, strokeWidth}
@@ -5320,8 +5710,9 @@ ITEMS:
5320
5710
  ellipse: {radiusX, radiusY, color}
5321
5711
  triangle/polygon: {width/sides, height/radius, color}
5322
5712
  All: opacity, shadowColor, shadowBlur, blendMode, strokeColor, strokeWidth
5323
- modify — {itemId, properties}
5713
+ modify — {itemId, properties} — instant reposition/restyle. To MOVE an item, set {x,y} here (a one-shot position change, not an animation).
5324
5714
  delete — {itemId}
5715
+ group — {itemIds: ["$0","$1",...], groupName} — combine the parts of a composite object into ONE draggable/animatable entity. ALWAYS group a specific named object (car, robot, house, character) after creating its parts. Returns {groupId}; safe — user can ungroup. Do NOT group a whole scene whose pieces move independently.
5325
5716
 
5326
5717
  ANIMATION:
5327
5718
  animate — Loop animation: {itemId, animationType, animationOptions}
@@ -5332,6 +5723,8 @@ ANIMATION:
5332
5723
  relation — Behavioral link: {sourceId, targetId, relationType, relationOptions}
5333
5724
  relationType: orbits, follows, attached_to, points_at, mirrors, parallax, wave_through, morphs_to, group_morphs_to, moves_along_path (+ 9 more in schema)
5334
5725
 
5726
+ MOVE vs ANIMATE vs PATH (pick by intent): instant reposition → modify {x,y}. Animated A→B move → keyframe_animate two keyframes [{time:0,{x,y}},{time:T,{x',y'}}]. Follow a multi-point path the user lists → relation moves_along_path {params:{path:[{x,y},…]}}.
5727
+
5335
5728
  EFFECTS:
5336
5729
  apply_mask — Reveal effect: {itemId, maskPreset, maskOptions}
5337
5730
  maskPreset: wipeLeft, wipeRight, wipeUp, wipeDown, iris, irisOut, star, heart, curtainHorizontal, curtainVertical, cinematic, diagonalWipe, revealUp, revealDown
@@ -5341,7 +5734,11 @@ EFFECTS:
5341
5734
  PLAYBACK:
5342
5735
  play_timeline — {action: "play"|"stop"|"seek", duration, loop}
5343
5736
 
5344
- VARIABLE REFERENCES: "$0", "$1" etc. reference items by creation order within the batch.
5737
+ VARIABLE REFERENCES: "$0", "$1" etc. reference items by creation order within the batch. Place ALL create ops before any ops that reference them so the indices stay stable.
5738
+
5739
+ ANIMATION NOTES:
5740
+ - keyframe_animate position accepts [x, y] or { x, y } (normalized). fillColor/strokeColor accept rgba() for alpha. The create "opacity" and "blendMode" props are applied to the item.
5741
+ - For MP4/GIF export the keyframe timeline is rendered once across the duration; author motion to fill the clip and use loop:false. Looping item-animations (loop:true) are not advanced by the exporter and render frozen.
5345
5742
 
5346
5743
  EXAMPLE — Animated sky scene with timed reveals:
5347
5744
  {operations: [
@@ -5368,7 +5765,7 @@ EXAMPLE — Animated sky scene with timed reveals:
5368
5765
  properties: {
5369
5766
  type: {
5370
5767
  type: 'string',
5371
- enum: ['set_canvas_size', 'set_background', 'execute_generator', 'create', 'modify', 'delete', 'animate', 'keyframe_animate', 'relation', 'apply_mask', 'apply_effect', 'play_timeline'],
5768
+ enum: ['set_canvas_size', 'set_background', 'execute_generator', 'create', 'modify', 'delete', 'group', 'animate', 'keyframe_animate', 'relation', 'apply_mask', 'apply_effect', 'play_timeline'],
5372
5769
  description: 'Operation type',
5373
5770
  },
5374
5771
  // Canvas setup
@@ -5385,7 +5782,7 @@ EXAMPLE — Animated sky scene with timed reveals:
5385
5782
  // Items
5386
5783
  itemType: {
5387
5784
  type: 'string',
5388
- enum: ['text', 'circle', 'rectangle', 'star', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc'],
5785
+ enum: ['text', 'circle', 'rectangle', 'star', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc', 'pentagon', 'hexagon', 'diamond', 'arrow', 'heart'],
5389
5786
  description: 'For create: item type',
5390
5787
  },
5391
5788
  position: {
@@ -5395,6 +5792,9 @@ EXAMPLE — Animated sky scene with timed reveals:
5395
5792
  },
5396
5793
  properties: { type: 'object', description: 'For create/modify: item properties' },
5397
5794
  itemId: { type: 'string', description: 'Target item ID or $N reference' },
5795
+ // Group (combine composite-object parts into one entity)
5796
+ itemIds: { type: 'array', items: { type: 'string' }, description: 'For group: item IDs or $N refs to combine into one entity (the parts of a composite object).' },
5797
+ groupName: { type: 'string', description: 'For group: optional group name (e.g. "car").' },
5398
5798
  // Loop animation
5399
5799
  animationType: {
5400
5800
  type: 'string',
@@ -5422,7 +5822,7 @@ EXAMPLE — Animated sky scene with timed reveals:
5422
5822
  targetId: { type: 'string', description: 'For relation: target item ID or $N' },
5423
5823
  relationType: {
5424
5824
  type: 'string',
5425
- 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'],
5825
+ 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'],
5426
5826
  description: 'For relation: type',
5427
5827
  },
5428
5828
  relationOptions: { type: 'object', description: 'For relation: options' },
@@ -5437,7 +5837,7 @@ EXAMPLE — Animated sky scene with timed reveals:
5437
5837
  // Effects
5438
5838
  effectType: {
5439
5839
  type: 'string',
5440
- enum: ['sparkle', 'blast', 'smoke', 'fire', 'rain', 'snow', 'confetti', 'ripple', 'glow', 'electric', 'bubbles', 'dust', 'fireflies', 'shockwave', 'trail'],
5840
+ enum: ['sparkle', 'blast', 'smoke', 'fire', 'rain', 'snow', 'confetti', 'ripple', 'glow', 'electric', 'bubbles', 'dust', 'fireflies', 'shockwave', 'trail', 'heatmap', 'liquid_metal', 'gem_smoke'],
5441
5841
  description: 'For apply_effect: effect type',
5442
5842
  },
5443
5843
  effectParams: { type: 'object', description: 'For apply_effect: parameters' },
@@ -5521,6 +5921,12 @@ EXAMPLES:
5521
5921
  enum: ['canvas', 'camera'],
5522
5922
  description: 'Output framing. "canvas" (default) renders the full canvas. "camera" renders only the first-keyframe viewport of a camera_animates walkthrough — video formats only.',
5523
5923
  },
5924
+ duration: {
5925
+ type: 'number',
5926
+ minimum: 0.5,
5927
+ maximum: 60,
5928
+ description: 'Video duration in seconds for mp4/webm/gif (default 5, max 60). Static formats ignore this. If you set play_timeline to N seconds, pass the same N here.',
5929
+ },
5524
5930
  },
5525
5931
  required: ['platform'],
5526
5932
  },
@@ -5581,38 +5987,6 @@ EXAMPLE RESPONSE:
5581
5987
  },
5582
5988
  },
5583
5989
  // ---------------------------------------------------------------------------
5584
- // LAYER 1 — ATOMS: CANVAS PRESETS TOOL
5585
- // ---------------------------------------------------------------------------
5586
- {
5587
- name: 'pinepaper_get_canvas_presets',
5588
- annotations: {
5589
- title: 'Get Canvas Presets',
5590
- readOnlyHint: true,
5591
- destructiveHint: false,
5592
- idempotentHint: true,
5593
- openWorldHint: false,
5594
- },
5595
- description: `[Utility] List all available canvas presets with their dimensions.
5596
-
5597
- USE WHEN:
5598
- - Setting up canvas for specific platforms (YouTube, Instagram, TikTok, etc.)
5599
- - Discovering available size presets
5600
- - Planning content dimensions
5601
- - Building UI for size selection
5602
-
5603
- PRESETS BY CATEGORY:
5604
- - Video: youtube-thumbnail, youtube-short, tiktok, hd-720p, full-hd-1080p
5605
- - Social: instagram-story, instagram-post, facebook-post, twitter-post, linkedin-post, pinterest-pin
5606
- - Presentation: presentation-16x9, presentation-4x3
5607
-
5608
- RETURNS:
5609
- Array of presets with: key, name, width, height, aspectRatio, category`,
5610
- inputSchema: {
5611
- type: 'object',
5612
- properties: {},
5613
- },
5614
- },
5615
- // ---------------------------------------------------------------------------
5616
5990
  // COMPATIBILITY: P5.JS TOOLS
5617
5991
  // ---------------------------------------------------------------------------
5618
5992
  {
@@ -5858,16 +6232,18 @@ USE WHEN:
5858
6232
  },
5859
6233
  description: `Validate and score a template definition against the PinePaper ontology.
5860
6234
 
5861
- Two-phase analysis:
6235
+ Three-phase analysis:
5862
6236
  1. Schema validation — checks required fields, known item/relation types, keyframe structure, semantic vocabulary compliance
5863
6237
  2. Quality scoring — rates across 5 dimensions: completeness (0.20), animationDesign (0.25), semanticRichness (0.20), compositionUse (0.15), structuralDepth (0.20)
6238
+ 3. Semantic diagnostics — FxTool's OntologyValidator (the same "one validator, two surfaces" library the Cloud pre-render gate runs), executed Node-side over the definition: dangling relation targets and unknown item/relation types (with nearest-id suggestions), duplicate edges, relation cycles, and keyframe order/easing issues. Returns { ok, diagnostics:[{code, severity, message, target, fix}], validatorVersion } under "diagnostics".
5864
6239
 
5865
6240
  Quality tiers: basic (<0.4), fair (0.4-0.6), good (0.6-0.8), excellent (>=0.8)
5866
6241
 
5867
6242
  USE WHEN:
5868
6243
  - Checking a template for structural issues before publishing
5869
6244
  - Comparing quality scores between template variants
5870
- - Validating that all item/relation types are in the vocabulary`,
6245
+ - Validating that all item/relation types are in the vocabulary
6246
+ - Catching dangling references / cycles in a definition before it runs (use pinepaper_validate_scene for the LIVE canvas equivalent)`,
5871
6247
  inputSchema: {
5872
6248
  type: 'object',
5873
6249
  properties: {
@@ -5879,6 +6255,54 @@ USE WHEN:
5879
6255
  required: ['definition'],
5880
6256
  },
5881
6257
  },
6258
+ {
6259
+ name: 'pinepaper_validate',
6260
+ annotations: {
6261
+ title: 'Validate Scene/Op',
6262
+ readOnlyHint: true,
6263
+ destructiveHint: false,
6264
+ idempotentHint: true,
6265
+ openWorldHint: false,
6266
+ },
6267
+ description: `Semantically validate the LIVE scene (or a proposed mutation before you apply it) and get back STRUCTURED diagnostics — not just pass/fail.
6268
+
6269
+ Distinct from pinepaper_validate_design (which scores a template *definition* object). This runs against the live canvas and checks references, relation types, params, and reference cycles, returning:
6270
+ { ok, diagnostics: [{ code, severity, message, target?, context?, fix? }] }
6271
+
6272
+ Codes: TARGET_NOT_FOUND / SOURCE_NOT_FOUND (with a nearest-id "did you mean?" fix), UNKNOWN_RELATION, UNKNOWN_ITEM_TYPE, PARAM_TYPE, PARAM_ENUM / PARAM_RANGE (with a replace/clamp fix), RELATION_CYCLE (with the cycle path), DUPLICATE_RELATION. severity is error|warning|hint; ok is false only when an error is present. Advisory — it never mutates; fix.apply is a proposed op you may run.
6273
+
6274
+ USE WHEN:
6275
+ - Before adding a relation, to confirm the target exists and the edge won't create a cycle (mode:'op')
6276
+ - After building a scene, to lint references/types/params (mode:'scene')
6277
+ - To recover from a failed op with an actionable suggestion instead of guessing
6278
+
6279
+ mode:'scene' (default) audits the whole canvas. mode:'op' validates a single proposed mutation passed in 'op'.`,
6280
+ inputSchema: {
6281
+ type: 'object',
6282
+ properties: {
6283
+ mode: {
6284
+ type: 'string',
6285
+ enum: ['scene', 'op'],
6286
+ description: "'scene' audits the whole live canvas (default); 'op' validates a proposed mutation before applying it",
6287
+ },
6288
+ op: {
6289
+ type: 'object',
6290
+ description: "Required when mode='op'. The proposed mutation: { kind:'addRelation'|'create'|'modify', from?, to?, relation?, params?, type?, id?, changes? }",
6291
+ properties: {
6292
+ kind: { type: 'string', enum: ['addRelation', 'create', 'modify'] },
6293
+ from: { type: 'string', description: 'Source item id (addRelation)' },
6294
+ to: { type: 'string', description: 'Target item id (addRelation)' },
6295
+ relation: { type: 'string', description: 'Relation type (addRelation)' },
6296
+ params: { type: 'object', description: 'Relation params (addRelation)' },
6297
+ type: { type: 'string', description: 'Item type (create)' },
6298
+ id: { type: 'string', description: 'Item id (modify)' },
6299
+ changes: { type: 'object', description: 'Property changes (modify)' },
6300
+ },
6301
+ required: ['kind'],
6302
+ },
6303
+ },
6304
+ },
6305
+ },
5882
6306
  {
5883
6307
  name: 'pinepaper_query_ontology',
5884
6308
  annotations: {