@pinepaper.studio/mcp-server 1.5.5 → 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 (76) 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/index.d.ts.map +1 -1
  25. package/dist/index.js +32 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/ontology/index.d.ts +2 -0
  28. package/dist/ontology/index.d.ts.map +1 -1
  29. package/dist/ontology/index.js +2 -0
  30. package/dist/ontology/index.js.map +1 -1
  31. package/dist/ontology/ontology-compiler.d.ts +78 -0
  32. package/dist/ontology/ontology-compiler.d.ts.map +1 -0
  33. package/dist/ontology/ontology-compiler.js +235 -0
  34. package/dist/ontology/ontology-compiler.js.map +1 -0
  35. package/dist/ontology/ontology-validator.d.ts +103 -0
  36. package/dist/ontology/ontology-validator.d.ts.map +1 -0
  37. package/dist/ontology/ontology-validator.js +573 -0
  38. package/dist/ontology/ontology-validator.js.map +1 -0
  39. package/dist/ontology/types.d.ts +1 -0
  40. package/dist/ontology/types.d.ts.map +1 -1
  41. package/dist/ontology/validator.d.ts +6 -0
  42. package/dist/ontology/validator.d.ts.map +1 -1
  43. package/dist/ontology/validator.js +34 -5
  44. package/dist/ontology/validator.js.map +1 -1
  45. package/dist/ontology/vocabulary.d.ts.map +1 -1
  46. package/dist/ontology/vocabulary.js +91 -3
  47. package/dist/ontology/vocabulary.js.map +1 -1
  48. package/dist/tools/definitions.d.ts +1 -1
  49. package/dist/tools/definitions.d.ts.map +1 -1
  50. package/dist/tools/definitions.js +978 -27
  51. package/dist/tools/definitions.js.map +1 -1
  52. package/dist/tools/handlers/maps.d.ts.map +1 -1
  53. package/dist/tools/handlers/maps.js +54 -1
  54. package/dist/tools/handlers/maps.js.map +1 -1
  55. package/dist/tools/handlers/ontology.d.ts.map +1 -1
  56. package/dist/tools/handlers/ontology.js +15 -3
  57. package/dist/tools/handlers/ontology.js.map +1 -1
  58. package/dist/tools/handlers.d.ts.map +1 -1
  59. package/dist/tools/handlers.js +137 -6
  60. package/dist/tools/handlers.js.map +1 -1
  61. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  62. package/dist/tools/minimal-descriptions.js +20 -1
  63. package/dist/tools/minimal-descriptions.js.map +1 -1
  64. package/dist/tools/toolkits.d.ts.map +1 -1
  65. package/dist/tools/toolkits.js +25 -2
  66. package/dist/tools/toolkits.js.map +1 -1
  67. package/dist/types/code-generator.d.ts +190 -2
  68. package/dist/types/code-generator.d.ts.map +1 -1
  69. package/dist/types/code-generator.js +1068 -30
  70. package/dist/types/code-generator.js.map +1 -1
  71. package/dist/types/schemas.d.ts +2726 -116
  72. package/dist/types/schemas.d.ts.map +1 -1
  73. package/dist/types/schemas.js +594 -4
  74. package/dist/types/schemas.js.map +1 -1
  75. package/manifest.json +299 -71
  76. 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
 
@@ -347,6 +363,11 @@ ITEM TYPES:
347
363
  - path: Custom path with segments or SVG data (properties: segments, pathData, strokeColor, fillColor, closed, smooth)
348
364
  - line: Line between two points (properties: from, to, strokeColor, strokeWidth)
349
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)
350
371
 
351
372
  PATH FOR CUSTOM SHAPES:
352
373
  Use itemType: "path" with either:
@@ -384,7 +405,7 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
384
405
  properties: {
385
406
  itemType: {
386
407
  type: 'string',
387
- 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'],
388
409
  description: 'Type of item to create',
389
410
  },
390
411
  position: {
@@ -428,6 +449,14 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
428
449
  type: 'number',
429
450
  description: 'Animation speed multiplier (for simple animations, default: 1.0)',
430
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
+ },
431
460
  keyframes: {
432
461
  type: 'array',
433
462
  description: "Keyframe array for inline keyframe animation (requires animationType: 'keyframe')",
@@ -486,7 +515,7 @@ GRADIENT OBJECT FORMAT:
486
515
 
487
516
  LETTER-COLLAGE PROPS (non-destructive — registry ids, animations, relations, effects, deformations all preserved):
488
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.
489
- - 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.
490
519
  - palette + collageStyle (collage group): Combine in one call to apply both atomically.
491
520
  - bgColor (single letter group, child of a collage): Tile/background fill for that one letter.
492
521
  - textColor (single letter group): PointText fill for that one letter.
@@ -1222,6 +1251,718 @@ EXAMPLE:
1222
1251
  },
1223
1252
  },
1224
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
+ },
1225
1966
  // ---------------------------------------------------------------------------
1226
1967
  // LAYER 2 — RULES: RELATION TOOLS (KEY FOR ANIMATION)
1227
1968
  // ---------------------------------------------------------------------------
@@ -1245,7 +1986,13 @@ USE WHEN:
1245
1986
  - "reflection mirrors original" → relationType: mirrors
1246
1987
  - "background moves with parallax" → relationType: parallax
1247
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
1248
1994
  - Creating physics-based or behavioral animations
1995
+ - Building live geometric constructions (GeoGebra-style — dragging an anchor updates the dependent)
1249
1996
 
1250
1997
  RELATION TYPES:
1251
1998
  - orbits: Circular motion around target (params: radius, speed, direction, phase)
@@ -1266,7 +2013,39 @@ RELATION COMPATIBILITY:
1266
2013
  Text→Text: font blend
1267
2014
  Cross-type: particle denoising transition
1268
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.
1269
- - 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.
1270
2049
 
1271
2050
  VERIFYING ANIMATION WORKS:
1272
2051
  After adding a relation, verify the animation is running:
@@ -1288,7 +2067,7 @@ Relations are COMPOSITIONAL - an item can have multiple relations that work toge
1288
2067
  },
1289
2068
  relationType: {
1290
2069
  type: 'string',
1291
- 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'],
1292
2071
  description: 'Type of relationship',
1293
2072
  },
1294
2073
  params: {
@@ -1322,7 +2101,7 @@ USE WHEN:
1322
2101
  targetId: { type: 'string', description: 'Target item ID' },
1323
2102
  relationType: {
1324
2103
  type: 'string',
1325
- 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'],
1326
2105
  description: 'Specific relation type to remove (optional - removes all if not specified)',
1327
2106
  },
1328
2107
  },
@@ -1350,7 +2129,7 @@ USE WHEN:
1350
2129
  itemId: { type: 'string', description: 'Item to query relations for' },
1351
2130
  relationType: {
1352
2131
  type: 'string',
1353
- 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'],
1354
2133
  description: 'Filter by relation type (optional)',
1355
2134
  },
1356
2135
  direction: {
@@ -1530,6 +2309,14 @@ ANIMATION TYPES:
1530
2309
  type: 'number',
1531
2310
  description: 'Animation speed multiplier (default: 1.0)',
1532
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
+ },
1533
2320
  },
1534
2321
  required: ['itemId', 'animationType'],
1535
2322
  },
@@ -1584,6 +2371,7 @@ EXAMPLE — Reveal at 2s, skip first second of keyframe data, end at 4s of keyfr
1584
2371
  type: 'object',
1585
2372
  properties: {
1586
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." },
1587
2375
  keyframes: {
1588
2376
  type: 'array',
1589
2377
  items: {
@@ -1642,7 +2430,9 @@ EXAMPLE — Reveal at 2s, skip first second of keyframe data, end at 4s of keyfr
1642
2430
  USE WHEN:
1643
2431
  - Starting/stopping/pausing timeline playback
1644
2432
  - Seeking to specific time
1645
- - 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.`,
1646
2436
  inputSchema: {
1647
2437
  type: 'object',
1648
2438
  properties: {
@@ -1663,6 +2453,10 @@ USE WHEN:
1663
2453
  type: 'number',
1664
2454
  description: 'Time to seek to (for seek action)',
1665
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
+ },
1666
2460
  },
1667
2461
  required: ['action'],
1668
2462
  },
@@ -2133,7 +2927,7 @@ EXAMPLE - Bouncing Balls:
2133
2927
  }
2134
2928
 
2135
2929
  SUPPORTED ITEM TYPES: text, circle, star, rectangle, triangle, polygon, ellipse, path, line, arc
2136
- 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
2137
2931
  SUPPORTED ANIMATIONS: pulse, rotate, bounce, fade, wobble, slide, typewriter`,
2138
2932
  inputSchema: {
2139
2933
  type: 'object',
@@ -2179,7 +2973,7 @@ SUPPORTED ANIMATIONS: pulse, rotate, bounce, fade, wobble, slide, typewriter`,
2179
2973
  target: { type: 'string', description: 'Name of target item' },
2180
2974
  type: {
2181
2975
  type: 'string',
2182
- 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'],
2183
2977
  },
2184
2978
  params: {
2185
2979
  type: 'object',
@@ -2464,7 +3258,8 @@ ACTIONS:
2464
3258
  - apply: Apply a single filter. Params: itemId, filterName, params
2465
3259
  - chain: Apply multiple filters in sequence. Params: itemId, filters (array of {name, params})
2466
3260
 
2467
- 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.)`,
2468
3263
  inputSchema: {
2469
3264
  type: 'object',
2470
3265
  properties: {
@@ -2791,6 +3586,7 @@ ACTIONS:
2791
3586
  - get_state: Get interaction state (score, progress, step)
2792
3587
 
2793
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).
2794
3590
 
2795
3591
  ACTION TYPES: navigate, show, hide, animate, stopAnimation, setState, incrementScore, playTimeline, pauseTimeline, seekTimeline, showFeedback, complete`,
2796
3592
  inputSchema: {
@@ -3750,6 +4546,55 @@ ACTIONS:
3750
4546
  required: ['action'],
3751
4547
  },
3752
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
+ },
3753
4598
  // ---------------------------------------------------------------------------
3754
4599
  // DOMAIN: FONT TOOLS — single action-dispatched tool replaces 16 wrappers
3755
4600
  // ---------------------------------------------------------------------------
@@ -3848,6 +4693,7 @@ STYLE TYPES:
3848
4693
  - fold: Origami-style folded letters
3849
4694
  - gradient: Each letter with gradient fill (use with gradient palettes)
3850
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.
3851
4697
 
3852
4698
  TILE PALETTES:
3853
4699
  - Game: wordle, scrabble
@@ -3876,7 +4722,7 @@ EXAMPLES:
3876
4722
  },
3877
4723
  style: {
3878
4724
  type: 'string',
3879
- enum: ['tile', 'magazine', 'paperCut', 'fold', 'gradient', 'image'],
4725
+ enum: ['tile', 'magazine', 'paperCut', 'fold', 'gradient', 'image', 'particle'],
3880
4726
  description: 'Style type (default: tile)',
3881
4727
  },
3882
4728
  palette: {
@@ -3931,6 +4777,14 @@ EXAMPLES:
3931
4777
  type: 'boolean',
3932
4778
  description: 'Enable drop shadows (default: true)',
3933
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
+ },
3934
4788
  },
3935
4789
  required: ['text'],
3936
4790
  },
@@ -4048,6 +4902,25 @@ PineMath — Math/Science:
4048
4902
  - drawSpectrumAnalyzer: FFT signal visualization (waveform: sine|square|sawtooth|triangle|noise|composite, frequency, amplitude, sampleRate, window: none|hann|hamming|blackman, waveColor, spectrumColor)
4049
4903
  - draw3DSurface: 3D surfaces from equations (preset: torus|sphere|wave|klein|mobius|custom, xExpr, yExpr, zExpr, uSteps, vSteps, scale, rotateSpeed)
4050
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
+
4051
4924
  COMMON PARAMS (all generators):
4052
4925
  - bgColor: Background color ("none"/"transparent" for no background)
4053
4926
  - interactive: Make generated items selectable/draggable (default false)
@@ -4065,6 +4938,9 @@ COMMON PARAMS (all generators):
4065
4938
  'drawGlobeWireframe',
4066
4939
  'drawFunctionPlot', 'drawParametricCurve', 'drawSimulation',
4067
4940
  'drawSpectrumAnalyzer', 'draw3DSurface',
4941
+ 'drawTruchet', 'drawHalftone', 'drawRibbons',
4942
+ 'drawFormulaArt', 'drawParametricCollection', 'drawShaderArt', 'drawYeganehMountains',
4943
+ 'drawBlobs', 'drawLowPoly', 'drawPeaks', 'drawScatter', 'drawStackedWaves', 'draw3DParametricCurve',
4068
4944
  ],
4069
4945
  description: 'Generator name',
4070
4946
  },
@@ -4073,6 +4949,17 @@ COMMON PARAMS (all generators):
4073
4949
  description: 'Generator-specific parameters',
4074
4950
  additionalProperties: true,
4075
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
+ },
4076
4963
  },
4077
4964
  required: ['generatorName'],
4078
4965
  },
@@ -4118,7 +5005,7 @@ USE WHEN:
4118
5005
  - Celebration effects (confetti)
4119
5006
  - Enhancing visual impact (ripple, glow, electric)
4120
5007
 
4121
- EFFECTS (15 total):
5008
+ EFFECTS (15 particle + 3 shader auras):
4122
5009
  - sparkle: Glitter/sparkle particles (color, speed, size)
4123
5010
  - blast: Explosion burst effect (color, radius, count)
4124
5011
  - smoke: Rising smoke plumes (color, speed, size, drift, height, growthRate)
@@ -4135,15 +5022,23 @@ EFFECTS (15 total):
4135
5022
  - shockwave: Expanding concentric rings — burst type (speed, maxRadius, count, thickness)
4136
5023
  - trail: Fading afterimages following movement (speed, count, size, fadeRate)
4137
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
+
4138
5031
  EXAMPLE — bubbles with magenta interior over a blue rim:
4139
- { 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 } }`,
4140
5035
  inputSchema: {
4141
5036
  type: 'object',
4142
5037
  properties: {
4143
5038
  itemId: { type: 'string', description: 'Registry ID of the item' },
4144
5039
  effectType: {
4145
5040
  type: 'string',
4146
- 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'],
4147
5042
  description: 'Type of effect',
4148
5043
  },
4149
5044
  params: {
@@ -4795,7 +5690,7 @@ WORKFLOW:
4795
5690
  1. pinepaper_agent_start_job → 2. pinepaper_agent_batch_execute (everything) → 3. pinepaper_agent_end_job (screenshot for validation)
4796
5691
  After validation: user reviews screenshot → feedback → modify/recreate as needed.
4797
5692
 
4798
- OPERATION TYPES (12) — use in this order:
5693
+ OPERATION TYPES (13) — use in this order:
4799
5694
 
4800
5695
  CANVAS SETUP:
4801
5696
  set_canvas_size — {width, height} or {preset: "instagram"|"youtube"|"tiktok"|...}
@@ -4806,7 +5701,7 @@ CANVAS SETUP:
4806
5701
 
4807
5702
  ITEMS:
4808
5703
  create — {itemType, position: {x,y}, properties}
4809
- 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
4810
5705
  Properties per type:
4811
5706
  text: {content, fontSize, fontFamily, color, fontWeight}
4812
5707
  circle: {radius, color, strokeColor, strokeWidth}
@@ -4815,8 +5710,9 @@ ITEMS:
4815
5710
  ellipse: {radiusX, radiusY, color}
4816
5711
  triangle/polygon: {width/sides, height/radius, color}
4817
5712
  All: opacity, shadowColor, shadowBlur, blendMode, strokeColor, strokeWidth
4818
- 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).
4819
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.
4820
5716
 
4821
5717
  ANIMATION:
4822
5718
  animate — Loop animation: {itemId, animationType, animationOptions}
@@ -4827,6 +5723,8 @@ ANIMATION:
4827
5723
  relation — Behavioral link: {sourceId, targetId, relationType, relationOptions}
4828
5724
  relationType: orbits, follows, attached_to, points_at, mirrors, parallax, wave_through, morphs_to, group_morphs_to, moves_along_path (+ 9 more in schema)
4829
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
+
4830
5728
  EFFECTS:
4831
5729
  apply_mask — Reveal effect: {itemId, maskPreset, maskOptions}
4832
5730
  maskPreset: wipeLeft, wipeRight, wipeUp, wipeDown, iris, irisOut, star, heart, curtainHorizontal, curtainVertical, cinematic, diagonalWipe, revealUp, revealDown
@@ -4867,7 +5765,7 @@ EXAMPLE — Animated sky scene with timed reveals:
4867
5765
  properties: {
4868
5766
  type: {
4869
5767
  type: 'string',
4870
- 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'],
4871
5769
  description: 'Operation type',
4872
5770
  },
4873
5771
  // Canvas setup
@@ -4884,7 +5782,7 @@ EXAMPLE — Animated sky scene with timed reveals:
4884
5782
  // Items
4885
5783
  itemType: {
4886
5784
  type: 'string',
4887
- 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'],
4888
5786
  description: 'For create: item type',
4889
5787
  },
4890
5788
  position: {
@@ -4894,6 +5792,9 @@ EXAMPLE — Animated sky scene with timed reveals:
4894
5792
  },
4895
5793
  properties: { type: 'object', description: 'For create/modify: item properties' },
4896
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").' },
4897
5798
  // Loop animation
4898
5799
  animationType: {
4899
5800
  type: 'string',
@@ -4921,7 +5822,7 @@ EXAMPLE — Animated sky scene with timed reveals:
4921
5822
  targetId: { type: 'string', description: 'For relation: target item ID or $N' },
4922
5823
  relationType: {
4923
5824
  type: 'string',
4924
- 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'],
4925
5826
  description: 'For relation: type',
4926
5827
  },
4927
5828
  relationOptions: { type: 'object', description: 'For relation: options' },
@@ -4936,7 +5837,7 @@ EXAMPLE — Animated sky scene with timed reveals:
4936
5837
  // Effects
4937
5838
  effectType: {
4938
5839
  type: 'string',
4939
- 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'],
4940
5841
  description: 'For apply_effect: effect type',
4941
5842
  },
4942
5843
  effectParams: { type: 'object', description: 'For apply_effect: parameters' },
@@ -5331,16 +6232,18 @@ USE WHEN:
5331
6232
  },
5332
6233
  description: `Validate and score a template definition against the PinePaper ontology.
5333
6234
 
5334
- Two-phase analysis:
6235
+ Three-phase analysis:
5335
6236
  1. Schema validation — checks required fields, known item/relation types, keyframe structure, semantic vocabulary compliance
5336
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".
5337
6239
 
5338
6240
  Quality tiers: basic (<0.4), fair (0.4-0.6), good (0.6-0.8), excellent (>=0.8)
5339
6241
 
5340
6242
  USE WHEN:
5341
6243
  - Checking a template for structural issues before publishing
5342
6244
  - Comparing quality scores between template variants
5343
- - 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)`,
5344
6247
  inputSchema: {
5345
6248
  type: 'object',
5346
6249
  properties: {
@@ -5352,6 +6255,54 @@ USE WHEN:
5352
6255
  required: ['definition'],
5353
6256
  },
5354
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
+ },
5355
6306
  {
5356
6307
  name: 'pinepaper_query_ontology',
5357
6308
  annotations: {