@pinepaper.studio/mcp-server 1.6.6 → 1.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +24 -1
- package/README.es.md +24 -1
- package/README.fr.md +24 -1
- package/README.hi.md +21 -1
- package/README.ja.md +22 -1
- package/README.ko.md +22 -1
- package/README.md +329 -14
- package/README.pt-BR.md +24 -1
- package/README.zh-CN.md +20 -1
- package/assets/edit-whenever.svg +75 -0
- package/dist/agent/export-manager.d.ts.map +1 -1
- package/dist/agent/export-manager.js +2 -2
- package/dist/agent/export-manager.js.map +1 -1
- package/dist/agent/session-manager.d.ts.map +1 -1
- package/dist/agent/session-manager.js.map +1 -1
- package/dist/assets/repositories/fontawesome.d.ts.map +1 -1
- package/dist/assets/repositories/fontawesome.js.map +1 -1
- package/dist/assets/repositories/openclipart.d.ts.map +1 -1
- package/dist/character/data/concepts.json +1 -0
- package/dist/character/data/depictions.json +1 -0
- package/dist/character/data/styles.json +1 -0
- package/dist/character/duration-scale.d.ts +13 -0
- package/dist/character/duration-scale.d.ts.map +1 -0
- package/dist/character/duration-scale.js +13 -0
- package/dist/character/duration-scale.js.map +1 -0
- package/dist/character/path-bbox.d.ts +82 -0
- package/dist/character/path-bbox.d.ts.map +1 -0
- package/dist/character/path-bbox.js +496 -0
- package/dist/character/path-bbox.js.map +1 -0
- package/dist/character/perform.d.ts +149 -0
- package/dist/character/perform.d.ts.map +1 -0
- package/dist/character/perform.js +626 -0
- package/dist/character/perform.js.map +1 -0
- package/dist/character/resolve.d.ts +51 -0
- package/dist/character/resolve.d.ts.map +1 -0
- package/dist/character/resolve.js +239 -0
- package/dist/character/resolve.js.map +1 -0
- package/dist/design/design-systems.d.ts +126 -0
- package/dist/design/design-systems.d.ts.map +1 -0
- package/dist/design/design-systems.js +240 -0
- package/dist/design/design-systems.js.map +1 -0
- package/dist/execution/canvas-state.d.ts +7 -0
- package/dist/execution/canvas-state.d.ts.map +1 -1
- package/dist/execution/canvas-state.js +20 -8
- package/dist/execution/canvas-state.js.map +1 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/ontology/design-graph.d.ts +9 -0
- package/dist/ontology/design-graph.d.ts.map +1 -1
- package/dist/ontology/design-graph.js +98 -4
- package/dist/ontology/design-graph.js.map +1 -1
- package/dist/ontology/types.d.ts +42 -0
- package/dist/ontology/types.d.ts.map +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +139 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/taxonomy/classifier.js +1 -1
- package/dist/taxonomy/classifier.js.map +1 -1
- package/dist/taxonomy/error-recovery.d.ts.map +1 -1
- package/dist/taxonomy/matcher.d.ts.map +1 -1
- package/dist/taxonomy/matcher.js.map +1 -1
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +25 -0
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +1236 -45
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-methods.d.ts +18 -0
- package/dist/tools/engine-methods.d.ts.map +1 -0
- package/dist/tools/engine-methods.js +239 -0
- package/dist/tools/engine-methods.js.map +1 -0
- package/dist/tools/handlers/character.d.ts +121 -0
- package/dist/tools/handlers/character.d.ts.map +1 -0
- package/dist/tools/handlers/character.js +315 -0
- package/dist/tools/handlers/character.js.map +1 -0
- package/dist/tools/handlers.d.ts +2 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +386 -22
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +24 -4
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +21 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +141 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +2246 -78
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +9295 -603
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +1134 -61
- package/dist/types/schemas.js.map +1 -1
- package/dist/vendor/design/art-deco-generator.d.ts +52 -0
- package/dist/vendor/design/art-deco-generator.d.ts.map +1 -0
- package/dist/vendor/design/art-deco-generator.js +144 -0
- package/dist/vendor/design/art-deco-generator.js.map +1 -0
- package/dist/vendor/design/art-nouveau-generator.d.ts +46 -0
- package/dist/vendor/design/art-nouveau-generator.d.ts.map +1 -0
- package/dist/vendor/design/art-nouveau-generator.js +134 -0
- package/dist/vendor/design/art-nouveau-generator.js.map +1 -0
- package/dist/vendor/design/bauhaus-generator.d.ts +53 -0
- package/dist/vendor/design/bauhaus-generator.d.ts.map +1 -0
- package/dist/vendor/design/bauhaus-generator.js +126 -0
- package/dist/vendor/design/bauhaus-generator.js.map +1 -0
- package/dist/vendor/design/brutalist-generator.d.ts +53 -0
- package/dist/vendor/design/brutalist-generator.d.ts.map +1 -0
- package/dist/vendor/design/brutalist-generator.js +148 -0
- package/dist/vendor/design/brutalist-generator.js.map +1 -0
- package/dist/vendor/design/claymorphism-generator.d.ts +46 -0
- package/dist/vendor/design/claymorphism-generator.d.ts.map +1 -0
- package/dist/vendor/design/claymorphism-generator.js +135 -0
- package/dist/vendor/design/claymorphism-generator.js.map +1 -0
- package/dist/vendor/design/corporate-memphis-generator.d.ts +47 -0
- package/dist/vendor/design/corporate-memphis-generator.d.ts.map +1 -0
- package/dist/vendor/design/corporate-memphis-generator.js +144 -0
- package/dist/vendor/design/corporate-memphis-generator.js.map +1 -0
- package/dist/vendor/design/cyberpunk-generator.d.ts +52 -0
- package/dist/vendor/design/cyberpunk-generator.d.ts.map +1 -0
- package/dist/vendor/design/cyberpunk-generator.js +162 -0
- package/dist/vendor/design/cyberpunk-generator.js.map +1 -0
- package/dist/vendor/design/de-stijl-generator.d.ts +50 -0
- package/dist/vendor/design/de-stijl-generator.d.ts.map +1 -0
- package/dist/vendor/design/de-stijl-generator.js +154 -0
- package/dist/vendor/design/de-stijl-generator.js.map +1 -0
- package/dist/vendor/design/design-style.d.ts +3 -0
- package/dist/vendor/design/design-style.d.ts.map +1 -0
- package/dist/vendor/design/design-style.js +12 -0
- package/dist/vendor/design/design-style.js.map +1 -0
- package/dist/vendor/design/design-systems-dtcg.d.ts +59 -0
- package/dist/vendor/design/design-systems-dtcg.d.ts.map +1 -0
- package/dist/vendor/design/design-systems-dtcg.js +571 -0
- package/dist/vendor/design/design-systems-dtcg.js.map +1 -0
- package/dist/vendor/design/frutiger-aero-generator.d.ts +46 -0
- package/dist/vendor/design/frutiger-aero-generator.d.ts.map +1 -0
- package/dist/vendor/design/frutiger-aero-generator.js +133 -0
- package/dist/vendor/design/frutiger-aero-generator.js.map +1 -0
- package/dist/vendor/design/memphis-generator.d.ts +56 -0
- package/dist/vendor/design/memphis-generator.d.ts.map +1 -0
- package/dist/vendor/design/memphis-generator.js +158 -0
- package/dist/vendor/design/memphis-generator.js.map +1 -0
- package/dist/vendor/design/mid-century-upa-generator.d.ts +46 -0
- package/dist/vendor/design/mid-century-upa-generator.d.ts.map +1 -0
- package/dist/vendor/design/mid-century-upa-generator.js +137 -0
- package/dist/vendor/design/mid-century-upa-generator.js.map +1 -0
- package/dist/vendor/design/neumorphic-generator.d.ts +53 -0
- package/dist/vendor/design/neumorphic-generator.d.ts.map +1 -0
- package/dist/vendor/design/neumorphic-generator.js +182 -0
- package/dist/vendor/design/neumorphic-generator.js.map +1 -0
- package/dist/vendor/design/op-art-generator.d.ts +45 -0
- package/dist/vendor/design/op-art-generator.d.ts.map +1 -0
- package/dist/vendor/design/op-art-generator.js +116 -0
- package/dist/vendor/design/op-art-generator.js.map +1 -0
- package/dist/vendor/design/pixel-art-generator.d.ts +45 -0
- package/dist/vendor/design/pixel-art-generator.d.ts.map +1 -0
- package/dist/vendor/design/pixel-art-generator.js +143 -0
- package/dist/vendor/design/pixel-art-generator.js.map +1 -0
- package/dist/vendor/design/pop-art-generator.d.ts +53 -0
- package/dist/vendor/design/pop-art-generator.d.ts.map +1 -0
- package/dist/vendor/design/pop-art-generator.js +164 -0
- package/dist/vendor/design/pop-art-generator.js.map +1 -0
- package/dist/vendor/design/risograph-generator.d.ts +53 -0
- package/dist/vendor/design/risograph-generator.d.ts.map +1 -0
- package/dist/vendor/design/risograph-generator.js +170 -0
- package/dist/vendor/design/risograph-generator.js.map +1 -0
- package/dist/vendor/design/swiss-style-generator.d.ts +55 -0
- package/dist/vendor/design/swiss-style-generator.d.ts.map +1 -0
- package/dist/vendor/design/swiss-style-generator.js +147 -0
- package/dist/vendor/design/swiss-style-generator.js.map +1 -0
- package/dist/vendor/design/vaporwave-generator.d.ts +51 -0
- package/dist/vendor/design/vaporwave-generator.d.ts.map +1 -0
- package/dist/vendor/design/vaporwave-generator.js +139 -0
- package/dist/vendor/design/vaporwave-generator.js.map +1 -0
- package/manifest.json +65 -1
- package/package.json +18 -9
package/dist/types/schemas.js
CHANGED
|
@@ -98,6 +98,44 @@ export const ItemTypeSchema = z.enum([
|
|
|
98
98
|
'diamond',
|
|
99
99
|
'arrow',
|
|
100
100
|
'heart',
|
|
101
|
+
// THE REST OF THE ENGINE'S SHAPE REGISTRY — same argument as the two below,
|
|
102
|
+
// applied to shapes that were already being drawn.
|
|
103
|
+
//
|
|
104
|
+
// js/shapes/index.js registers 34 shapes across five modules. Flowchart, UML
|
|
105
|
+
// and network route through pinepaper_create_diagram_shape, so 23 were
|
|
106
|
+
// reachable. These eleven were reachable from NOTHING: three basic shapes,
|
|
107
|
+
// and the whole `comment` module — every speech bubble, thought bubble and
|
|
108
|
+
// callout the engine can draw.
|
|
109
|
+
//
|
|
110
|
+
// The engine was never the limit. PinePaper.create() delegates any registry
|
|
111
|
+
// type — "basic shapes, comment/bubble shapes, etc." in its own comment —
|
|
112
|
+
// and already forwards `tailDirection` and `tailSize`, which exist only for
|
|
113
|
+
// bubbles. It has been able to draw a speech bubble the whole time; no tool
|
|
114
|
+
// could name one, so an agent asked for a comic panel or an annotated
|
|
115
|
+
// diagram had to hand-build the outline or decline.
|
|
116
|
+
'disk',
|
|
117
|
+
'circle-outline',
|
|
118
|
+
'arrow-right',
|
|
119
|
+
'speech-bubble',
|
|
120
|
+
'speech-bubble-square',
|
|
121
|
+
'speech-bubble-pointed',
|
|
122
|
+
'thought-bubble',
|
|
123
|
+
'double-bubble',
|
|
124
|
+
'quote-bubble',
|
|
125
|
+
'comment-box',
|
|
126
|
+
'callout-box',
|
|
127
|
+
// RENDER-TIME SURFACES. These two are not Paper items and never enter the
|
|
128
|
+
// scene tree: they are drawn per pixel by the cloud rasterizer, straight from
|
|
129
|
+
// the scene document. They are here because a capability nobody can name is a
|
|
130
|
+
// capability nobody has — the cloud has drawn shaders and fields for months
|
|
131
|
+
// and no tool has ever offered them, so the only way to reach one was to
|
|
132
|
+
// hand-write a scene file.
|
|
133
|
+
//
|
|
134
|
+
// In the browser they degrade to a flat plate of their own colour (see
|
|
135
|
+
// generateCreateItemCode) so a composition built with one still reads
|
|
136
|
+
// locally; the shaded surface appears in a cloud render.
|
|
137
|
+
'shader',
|
|
138
|
+
'field',
|
|
101
139
|
]);
|
|
102
140
|
// Text item properties
|
|
103
141
|
export const TextPropertiesSchema = z.object({
|
|
@@ -182,14 +220,32 @@ export const ArcPropertiesSchema = z.object({
|
|
|
182
220
|
// =============================================================================
|
|
183
221
|
// ANIMATION TYPES
|
|
184
222
|
// =============================================================================
|
|
223
|
+
// The loop animations the engine's driver can actually run.
|
|
224
|
+
//
|
|
225
|
+
// Pinned to the engine by animation-type-parity.test.ts. It offered 'slide',
|
|
226
|
+
// which has NEVER existed — the real ones are slideLeftRight and slideUpDown.
|
|
227
|
+
// The engine stored the unknown value and the driver's switch fell through, so
|
|
228
|
+
// the item sat still while its data claimed a slide, and nothing anywhere said
|
|
229
|
+
// so. It also hid twelve types that do work.
|
|
185
230
|
export const SimpleAnimationTypeSchema = z.enum([
|
|
186
|
-
'pulse',
|
|
187
|
-
'rotate',
|
|
188
231
|
'bounce',
|
|
232
|
+
'breathe',
|
|
189
233
|
'fade',
|
|
190
|
-
'
|
|
191
|
-
'
|
|
234
|
+
'glow',
|
|
235
|
+
'jelly',
|
|
236
|
+
'path',
|
|
237
|
+
'pulse',
|
|
238
|
+
'rotate',
|
|
239
|
+
'scrollDown',
|
|
240
|
+
'scrollLeft',
|
|
241
|
+
'scrollRight',
|
|
242
|
+
'scrollUp',
|
|
243
|
+
'shake',
|
|
244
|
+
'slideLeftRight',
|
|
245
|
+
'slideUpDown',
|
|
246
|
+
'swing',
|
|
192
247
|
'typewriter',
|
|
248
|
+
'wobble',
|
|
193
249
|
]).describe('Simple loop animation type');
|
|
194
250
|
// Keyframe schema
|
|
195
251
|
export const KeyframeSchema = z.object({
|
|
@@ -200,7 +256,21 @@ export const KeyframeSchema = z.object({
|
|
|
200
256
|
// =============================================================================
|
|
201
257
|
// RELATION TYPES
|
|
202
258
|
// =============================================================================
|
|
259
|
+
// The relation vocabulary the engine can actually run.
|
|
260
|
+
//
|
|
261
|
+
// Held in step with the engine by relation-parity.test.ts, against a fixture of
|
|
262
|
+
// the registry's own map. This list is a hard gate: a name missing from it is
|
|
263
|
+
// REJECTED at validation, so for 95 live relations the engine could do it and
|
|
264
|
+
// no agent could name it — and for a model those are the same condition. The
|
|
265
|
+
// whole interactive vocabulary sat on the wrong side of that line.
|
|
266
|
+
//
|
|
267
|
+
// Relations a dedicated tool emits (deform_*, effect_*, geo_*, bone_*, …) are
|
|
268
|
+
// deliberately absent: the agent authors those THROUGH that tool, and a second
|
|
269
|
+
// name here would be a worse way to do the same thing. The test file holds that
|
|
270
|
+
// exclusion list, with the reason.
|
|
271
|
+
/** Engine parity ENFORCED BY src/__tests__/unit/relation-parity.test.ts (fixture: engine-relations.txt). */
|
|
203
272
|
export const RelationTypeSchema = z.enum([
|
|
273
|
+
// --- Motion, spatial and transform ---
|
|
204
274
|
'orbits',
|
|
205
275
|
'follows',
|
|
206
276
|
'attached_to',
|
|
@@ -209,61 +279,100 @@ export const RelationTypeSchema = z.enum([
|
|
|
209
279
|
'mirrors',
|
|
210
280
|
'parallax',
|
|
211
281
|
'bounds_to',
|
|
282
|
+
'anchored_in_world',
|
|
283
|
+
'spring_follow',
|
|
284
|
+
'repels',
|
|
285
|
+
'attracts',
|
|
286
|
+
'wiggle',
|
|
287
|
+
// --- Animation and sequencing ---
|
|
212
288
|
'animates',
|
|
213
|
-
// Manim-inspired animation relations
|
|
214
289
|
'grows_from',
|
|
215
290
|
'staggered_with',
|
|
216
291
|
'indicates',
|
|
217
292
|
'circumscribes',
|
|
218
293
|
'wave_through',
|
|
219
|
-
'camera_follows',
|
|
220
|
-
'camera_animates',
|
|
221
294
|
'morphs_to',
|
|
222
|
-
// Pair-by-index group morph; same call shape works for any two paper.Groups
|
|
223
295
|
'group_morphs_to',
|
|
224
|
-
// Self-relation; item moves along a custom-drawn path stored in params
|
|
225
296
|
'moves_along_path',
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'
|
|
230
|
-
'
|
|
231
|
-
'
|
|
232
|
-
|
|
233
|
-
'
|
|
234
|
-
'
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
'
|
|
239
|
-
'
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
'
|
|
246
|
-
'
|
|
247
|
-
'
|
|
248
|
-
'
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
'
|
|
255
|
-
'
|
|
256
|
-
'
|
|
257
|
-
'
|
|
258
|
-
'
|
|
259
|
-
'
|
|
297
|
+
'construction_reveal',
|
|
298
|
+
'triggers_animation',
|
|
299
|
+
'syncs_with',
|
|
300
|
+
'tours',
|
|
301
|
+
'synced_to_audio',
|
|
302
|
+
'expresses',
|
|
303
|
+
// --- Camera ---
|
|
304
|
+
'camera_follows',
|
|
305
|
+
'camera_animates',
|
|
306
|
+
// --- Geometry ---
|
|
307
|
+
'is_midpoint_of',
|
|
308
|
+
'lies_on_line',
|
|
309
|
+
'is_centroid_of',
|
|
310
|
+
'is_circumcenter_of',
|
|
311
|
+
'concentric_with',
|
|
312
|
+
// --- Procedural / expression ---
|
|
313
|
+
'driven_by',
|
|
314
|
+
'time_expression',
|
|
315
|
+
// --- Input triggers — a user action FIRES an event channel ---
|
|
316
|
+
'on_click_fire',
|
|
317
|
+
'on_pointer_enter_fire',
|
|
318
|
+
'on_pointer_exit_fire',
|
|
319
|
+
'on_key_fire',
|
|
320
|
+
// --- Event reactions — an event fires and something happens ---
|
|
321
|
+
'on_event_fire_after',
|
|
322
|
+
'on_event_fire_if',
|
|
323
|
+
'on_event_add_relation',
|
|
324
|
+
'on_event_remove_relation',
|
|
325
|
+
'on_event_set_color',
|
|
326
|
+
'on_event_set_property',
|
|
327
|
+
'on_event_set_property_from_template',
|
|
328
|
+
'on_event_set_visibility',
|
|
329
|
+
'on_event_set_active',
|
|
330
|
+
'on_event_set_data',
|
|
331
|
+
'on_event_increment',
|
|
332
|
+
'on_event_toggle',
|
|
333
|
+
'on_event_store_set',
|
|
334
|
+
'on_event_store_increment',
|
|
335
|
+
// --- Proximity — the pointer entering or leaving an item ---
|
|
336
|
+
'on_enter_set_color',
|
|
337
|
+
'on_enter_set_property',
|
|
338
|
+
'on_enter_set_property_from_template',
|
|
339
|
+
'on_enter_set_visibility',
|
|
340
|
+
'on_enter_set_data',
|
|
341
|
+
'on_enter_increment',
|
|
342
|
+
'on_enter_toggle',
|
|
343
|
+
'on_exit_set_color',
|
|
344
|
+
'on_exit_set_property',
|
|
345
|
+
'on_exit_set_property_from_template',
|
|
346
|
+
'on_exit_set_visibility',
|
|
347
|
+
'on_exit_set_data',
|
|
348
|
+
'on_exit_increment',
|
|
349
|
+
'on_exit_toggle',
|
|
350
|
+
// --- Exclusive state — at most one member active ---
|
|
351
|
+
'exclusive_group',
|
|
352
|
+
'menubar_group',
|
|
353
|
+
'restores_from',
|
|
354
|
+
// --- Structural and diagram ---
|
|
355
|
+
'on_top_of',
|
|
356
|
+
'below',
|
|
357
|
+
'beside',
|
|
358
|
+
'inside',
|
|
359
|
+
'centered_on',
|
|
360
|
+
'aligned_with',
|
|
361
|
+
'part_of',
|
|
362
|
+
'connects_to',
|
|
363
|
+
'attached_to_tail',
|
|
364
|
+
'head_points_to',
|
|
260
365
|
]).describe('Type of relationship between items');
|
|
261
366
|
// Relation parameter schemas
|
|
262
367
|
export const OrbitsParamsSchema = z.object({
|
|
263
368
|
radius: z.number().optional().default(100).describe('Orbit radius'),
|
|
264
369
|
speed: z.number().optional().default(1).describe('Rotation speed'),
|
|
265
370
|
direction: z.enum(['clockwise', 'counterclockwise']).optional().default('counterclockwise'),
|
|
266
|
-
|
|
371
|
+
// The one angle in the relation vocabulary that is NOT degrees. Kept because
|
|
372
|
+
// changing it outright would silently re-time every scene that sets it — a 57x
|
|
373
|
+
// error of exactly the kind the "angles are DEGREES" convention exists to stop.
|
|
374
|
+
phase: z.number().optional().default(0).describe('Starting angle in RADIANS — the documented exception to the degrees convention, kept for existing scenes. Prefer phaseDegrees.'),
|
|
375
|
+
phaseDegrees: z.number().optional().describe('Starting angle in DEGREES, matching the convention every other angle uses. Takes precedence over phase when set.'),
|
|
267
376
|
});
|
|
268
377
|
export const FollowsParamsSchema = z.object({
|
|
269
378
|
offset: PointArraySchema.optional().default([0, 0]).describe('Position offset from target'),
|
|
@@ -308,8 +417,19 @@ export const GrowsFromParamsSchema = z.object({
|
|
|
308
417
|
});
|
|
309
418
|
export const StaggeredWithParamsSchema = z.object({
|
|
310
419
|
index: z.number().describe('0-based position in the stagger sequence'),
|
|
311
|
-
stagger: z.number().optional().default(0.1).describe('
|
|
312
|
-
|
|
420
|
+
stagger: z.number().optional().default(0.1).describe('Seconds between neighbours. Ignored when amount is set.'),
|
|
421
|
+
// Shape params (pp:Stagger). The plain index x stagger path is taken first and
|
|
422
|
+
// unchanged; these only engage when a shape is actually asked for AND count is
|
|
423
|
+
// known — there is no "end" to measure from without it, so an origin like
|
|
424
|
+
// 'center' would have nothing to mean.
|
|
425
|
+
count: z.number().optional().describe('How many items are in the stagger. Required by any origin other than "start".'),
|
|
426
|
+
from: z.enum(['start', 'end', 'center', 'edges', 'random']).optional().default('start').describe('Where the stagger begins — what turns a delay into a direction. Requires count.'),
|
|
427
|
+
amount: z.number().optional().describe('Total seconds for the WHOLE sequence, split among the items. Overrides stagger.'),
|
|
428
|
+
grid: z.string().optional().describe('Rows and columns as "rows,cols" (or "auto") for a 2D stagger. Requires count.'),
|
|
429
|
+
axis: z.enum(['x', 'y']).optional().describe('Restrict a grid stagger to one axis.'),
|
|
430
|
+
distributeEase: z.enum(['linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInCubic', 'easeOutCubic']).optional().default('linear')
|
|
431
|
+
.describe("Curve that distributes the START TIMES — not the items' own animation curve."),
|
|
432
|
+
effect: z.enum(['fadeIn', 'fadeOut', 'growIn', 'slideIn', 'popIn', 'typewriter']).optional().default('fadeIn').describe('Stagger effect type'),
|
|
313
433
|
});
|
|
314
434
|
export const IndicatesParamsSchema = z.object({
|
|
315
435
|
scale: z.number().optional().default(1.3).describe('Max scale during indication'),
|
|
@@ -448,6 +568,13 @@ export const MovesAlongPathParamsSchema = z.object({
|
|
|
448
568
|
// || opts) — so they must survive validation.
|
|
449
569
|
export const EquationPathInputSchema = z.object({
|
|
450
570
|
...equationSpecShape,
|
|
571
|
+
solveOde: z.object({
|
|
572
|
+
equations: z.union([z.string(), z.array(z.string())]).describe("The derivative(s), as expressions — e.g. 'y' for dy/dt = y, or ['v', '-9.8'] for a falling body."),
|
|
573
|
+
initialState: z.array(z.number()).describe('The state at t = 0, one number per equation.'),
|
|
574
|
+
tEnd: z.number().optional().describe('How far to integrate (default 10).'),
|
|
575
|
+
dt: z.number().optional().describe('Step size (default 0.01). Smaller is more accurate and slower.'),
|
|
576
|
+
method: z.enum(['rk4', 'euler']).optional().describe("Integrator (default 'rk4' — fourth-order Runge-Kutta). 'euler' is faster and visibly wrong on anything stiff."),
|
|
577
|
+
}).optional().describe('Numerically integrate an ODE instead of plotting a closed-form expression, and return the SOLUTION rather than drawing it. The trajectory comes back as data, so a caller can inspect it, feed it to a path, or drive keyframes with it.'),
|
|
451
578
|
x: z.number().optional().describe('Canvas origin x (default: canvas center)'),
|
|
452
579
|
y: z.number().optional().describe('Canvas origin y (default: canvas center)'),
|
|
453
580
|
style: z.record(z.unknown()).optional().describe('Path style forwarded to create (strokeColor, strokeWidth, fillColor, …)'),
|
|
@@ -471,12 +598,22 @@ export const DrivenByParamsSchema = z.object({
|
|
|
471
598
|
// time_expression: self-relation (targetId=null). The source's `property` is
|
|
472
599
|
// driven by a math expression of t (scene time) and v (baseValue). signal:true
|
|
473
600
|
// parses the expression into the Expression IR and evaluates a pure f(t)
|
|
474
|
-
// (replay-stable)
|
|
601
|
+
// (replay-stable).
|
|
602
|
+
//
|
|
603
|
+
// THERE IS NO LONGER A PER-FRAME FALLBACK FOR random(). The engine used to
|
|
604
|
+
// build a `new Function` from the scene's own text on the per-frame path, which
|
|
605
|
+
// made the authoring language JavaScript rather than the grammar the other
|
|
606
|
+
// renderers speak — `^` was bitwise XOR there and exponentiation everywhere
|
|
607
|
+
// else, so one scene string drew two different pictures, and `random()` could
|
|
608
|
+
// not be refused, so a scene could never be baked or exported deterministically.
|
|
609
|
+
// Both paths now go through the same parser, `random` is rejected by name, and
|
|
610
|
+
// `^` is exponentiation. This comment said "falls back to per-frame" until
|
|
611
|
+
// 2026-09-15; it described behaviour the engine had removed.
|
|
475
612
|
export const TimeExpressionParamsSchema = z.object({
|
|
476
613
|
property: z.string().optional().default('y').describe('Property to drive: x, y, rotation, opacity, scale'),
|
|
477
|
-
expression: z.string().optional().default('sin(t * 2) * 50 + 300').describe('Math expression of t (time) and v (baseValue), e.g. "sin(t*2)*50 + v"'),
|
|
614
|
+
expression: z.string().optional().default('sin(t * 2) * 50 + 300').describe('Math expression of t (time) and v (baseValue), e.g. "sin(t*2)*50 + v". `^` is exponentiation, not XOR. Available: sin cos tan asin acos atan sinh cosh tanh abs floor ceil round trunc sign sqrt cbrt exp log log2 log10 · pow min max atan2 hypot clamp mod · fract pingpong inverseLerp remap noise sec csc cot · easeInQuad easeOutQuad easeInOutQuad easeInCubic easeOutCubic easeInOutCubic · constants PI, E, TAU. `random()` is REJECTED by name on both the signal and per-frame paths — a scene that cannot render the same way twice can never be baked or exported deterministically.'),
|
|
478
615
|
baseValue: z.number().optional().default(0).describe('Base value accessible as v in the expression'),
|
|
479
|
-
signal: z.boolean().optional().default(false).describe('Deterministic IR mode — parse expression to IR, evaluate as pure f(t)'),
|
|
616
|
+
signal: z.boolean().optional().default(false).describe('Deterministic IR mode — parse expression to IR, evaluate as pure f(t). Both modes now use the same parser and the same grammar; signal additionally makes the result bakeable as a pure function of time.'),
|
|
480
617
|
});
|
|
481
618
|
// =============================================================================
|
|
482
619
|
// EVENT-DRIVEN SCENE CHAINS (FxTool S11). Source of each is a pp:event.
|
|
@@ -609,6 +746,7 @@ export const ExecuteCustomCodeInputSchema = z.object({
|
|
|
609
746
|
// =============================================================================
|
|
610
747
|
// GENERATOR TYPES
|
|
611
748
|
// =============================================================================
|
|
749
|
+
/** Engine parity ENFORCED BY src/__tests__/unit/preflight-gate-parity.test.ts (fixture: engine-generators.txt). */
|
|
612
750
|
export const GeneratorNameSchema = z.enum([
|
|
613
751
|
// Classic generators
|
|
614
752
|
'drawSunburst',
|
|
@@ -683,6 +821,24 @@ export const GeneratorNameSchema = z.enum([
|
|
|
683
821
|
'drawScatter',
|
|
684
822
|
'drawStackedWaves',
|
|
685
823
|
'draw3DParametricCurve',
|
|
824
|
+
// THE GATE WAS NARROWER THAN THE ENGINE, and generatorName is a hard
|
|
825
|
+
// preflight gate — an unlisted name is rejected before the browser sees
|
|
826
|
+
// it. The engine registers 74 generators and this offered 64, so these
|
|
827
|
+
// ten were live and uncallable. Seven are the GPU field generators, which
|
|
828
|
+
// is the expensive half: create_item's `field` type, design_medium's flow
|
|
829
|
+
// fields and the design graph's own recipe example all name drawGPUTunnel,
|
|
830
|
+
// so a captured scene recording `generator: 'drawGPUTunnel'` could not be
|
|
831
|
+
// replayed through the tool that exists to replay it.
|
|
832
|
+
'draw3DWorld',
|
|
833
|
+
'drawCharacterEyes',
|
|
834
|
+
'drawGPUCaustics',
|
|
835
|
+
'drawGPUClouds',
|
|
836
|
+
'drawGPUOcean',
|
|
837
|
+
'drawGPUPlasma',
|
|
838
|
+
'drawGPUStarfield',
|
|
839
|
+
'drawGPUTunnel',
|
|
840
|
+
'drawGPUVoronoi',
|
|
841
|
+
'drawRiggedCharacter',
|
|
686
842
|
]).describe('Background generator name');
|
|
687
843
|
// ---------------------------------------------------------------------------
|
|
688
844
|
// Generator parameter schemas — Phase 1 (enhanced existing)
|
|
@@ -835,6 +991,7 @@ export const NoiseTextureParamsSchema = z.object({
|
|
|
835
991
|
// =============================================================================
|
|
836
992
|
// EFFECT TYPES
|
|
837
993
|
// =============================================================================
|
|
994
|
+
/** Engine parity ENFORCED BY src/__tests__/unit/preflight-gate-parity.test.ts (fixture: engine-effects.txt). */
|
|
838
995
|
export const EffectTypeSchema = z.enum([
|
|
839
996
|
'sparkle',
|
|
840
997
|
'blast',
|
|
@@ -856,6 +1013,17 @@ export const EffectTypeSchema = z.enum([
|
|
|
856
1013
|
'heatmap',
|
|
857
1014
|
'liquid_metal',
|
|
858
1015
|
'gem_smoke',
|
|
1016
|
+
'electric_arc',
|
|
1017
|
+
'vortex',
|
|
1018
|
+
'rain_veil',
|
|
1019
|
+
'caustics',
|
|
1020
|
+
// ink_bleed was the LAST aura and the gate rejected it. The engine's own
|
|
1021
|
+
// comment says it 'has shipped unroutable through applyEffect since it was
|
|
1022
|
+
// added' — past tense, describing a bug it then fixed: _publishNames()
|
|
1023
|
+
// publishes every aura definition, and PinePaper.applyEffect warms the
|
|
1024
|
+
// subsystem before routing, so all eight route now. Reading the comment
|
|
1025
|
+
// would have kept it out; reading the mechanism put it in.
|
|
1026
|
+
'ink_bleed',
|
|
859
1027
|
]).describe('Visual effect type (particle effects + shader auras)');
|
|
860
1028
|
export const SparkleParamsSchema = z.object({
|
|
861
1029
|
color: ColorSchema.optional().default('#fbbf24').describe('Sparkle color'),
|
|
@@ -923,7 +1091,7 @@ export const CreateItemInputSchema = z.object({
|
|
|
923
1091
|
itemType: ItemTypeSchema,
|
|
924
1092
|
position: PositionSchema.optional().default({ x: 400, y: 300 }),
|
|
925
1093
|
properties: z.record(z.unknown()).optional().default({}),
|
|
926
|
-
animationType: z.string().optional().describe('Inline animation to apply on creation. Loop presets (pulse, rotate,
|
|
1094
|
+
animationType: z.string().optional().describe('Inline animation to apply on creation. Loop presets (bounce, breathe, fade, glow, jelly, path, pulse, rotate, scrollDown, scrollLeft, scrollRight, scrollUp, shake, slideLeftRight, slideUpDown, swing, typewriter, wobble) or "keyframe" with keyframes array. Equivalent to a follow-up pinepaper_animate / pinepaper_keyframe_animate call.'),
|
|
927
1095
|
animationSpeed: z.number().optional().describe('Speed multiplier for loop animations (default: 1.0). Ignored when animationType is "keyframe".'),
|
|
928
1096
|
animationIntensity: z.number().optional().describe('Loop animation amplitude (0.1 = ±10%, default 0.15). Drives pulse/wobble/bounce/breathe amplitude; honored by SVG/SMIL/widget export.'),
|
|
929
1097
|
animationDelay: z.number().optional().describe('Loop animation start delay in seconds.'),
|
|
@@ -973,8 +1141,26 @@ export const DeleteItemInputSchema = z.object({
|
|
|
973
1141
|
export const AddRelationInputSchema = z.object({
|
|
974
1142
|
sourceId: z.string().describe('Registry ID of the source item'),
|
|
975
1143
|
targetId: z.string().nullish().describe('Registry ID of the target item; null/omitted for self-relations (animates, moves_along_path, construction_reveal, time_expression)'),
|
|
976
|
-
relationType: RelationTypeSchema,
|
|
1144
|
+
relationType: RelationTypeSchema.optional().describe('The relation to create. Required unless presetId is given.'),
|
|
977
1145
|
params: z.record(z.unknown()).optional().default({}),
|
|
1146
|
+
presetId: z.string().optional()
|
|
1147
|
+
.describe("Apply a community relation PRESET instead of naming a type and params yourself — a tuned motion someone already got right, with its symbols exposed as knobs. List them with pinepaper_query_capabilities { action: 'catalogue', catalogue: 'relation_presets' }. The preset decides the relationType, so relationType is not needed and is ignored if sent."),
|
|
1148
|
+
presetValues: z.record(z.number()).optional()
|
|
1149
|
+
.describe('presetId: symbol → number. Any symbol you omit takes the midpoint of its declared range, so a preset applies sensibly with no values at all.'),
|
|
1150
|
+
})
|
|
1151
|
+
.refine((v) => !!v.relationType || !!v.presetId, { message: 'relationType is required unless presetId is given', path: ['relationType'] })
|
|
1152
|
+
// A PRESET IS A SELF-RELATION and has no target slot. applyRelationPreset
|
|
1153
|
+
// takes (item, presetId, values) — pass a targetId alongside one and the
|
|
1154
|
+
// emitted call has nowhere to put it, so a caller adopting a preset for a
|
|
1155
|
+
// two-item relation silently got a self-relation with no diagnostic.
|
|
1156
|
+
// Refused here rather than dropped there.
|
|
1157
|
+
.refine((v) => !v.presetId || v.targetId == null, {
|
|
1158
|
+
message: 'presetId cannot be combined with targetId — a relation preset applies to one item. Name the relationType directly for a two-item relation.',
|
|
1159
|
+
path: ['targetId'],
|
|
1160
|
+
})
|
|
1161
|
+
.refine((v) => !v.presetId || !v.relationType, {
|
|
1162
|
+
message: 'presetId cannot be combined with relationType — the preset decides its own type. Send one or the other.',
|
|
1163
|
+
path: ['relationType'],
|
|
978
1164
|
});
|
|
979
1165
|
// Remove Relation
|
|
980
1166
|
export const RemoveRelationInputSchema = z.object({
|
|
@@ -1007,6 +1193,63 @@ export const KeyframeAnimateInputSchema = z.object({
|
|
|
1007
1193
|
clipInPoint: z.number().optional().describe('Skip the first N seconds of keyframe data — clip head trim.'),
|
|
1008
1194
|
clipOutPoint: z.number().optional().describe('Stop at N seconds into the keyframe data — clip tail trim. Default: lastKeyframeTime.'),
|
|
1009
1195
|
});
|
|
1196
|
+
// Character — a symbol and some beats, expanded against the design graph.
|
|
1197
|
+
//
|
|
1198
|
+
// Mirrors `pinepaper_character`'s inputSchema field for field. Only `concept`
|
|
1199
|
+
// is required: everything else has a defensible default, and the point of the
|
|
1200
|
+
// tool is that a caller who knows nothing but the name of a figure gets one.
|
|
1201
|
+
export const CharacterBeatSchema = z.object({
|
|
1202
|
+
at: z.number().describe('When, in seconds'),
|
|
1203
|
+
channel: z.string().describe('A channel the concept declares — blink, say, headTurn, bob, tailFlick…'),
|
|
1204
|
+
until: z.number().optional().describe('For a channel that occupies a span rather than an instant'),
|
|
1205
|
+
value: z.number().optional().describe('How far, for a channel that takes a magnitude'),
|
|
1206
|
+
});
|
|
1207
|
+
export const CharacterInputSchema = z.object({
|
|
1208
|
+
concept: z.string().describe('Concept id from the design graph, e.g. "pp:Character" or "pp:Pigeon".'),
|
|
1209
|
+
at: z.object({ x: z.number().optional(), y: z.number().optional() }).optional().describe('Where the figure stands.'),
|
|
1210
|
+
height: z.number().optional().describe('Drawn height in pixels.'),
|
|
1211
|
+
id: z.string().optional().describe('Prefix for the created items (default: the concept name).'),
|
|
1212
|
+
style: z.string().optional().describe('Style id for the depiction (default: the concept\'s own).'),
|
|
1213
|
+
variant: z.string().optional().describe('Depiction variant (default: "default").'),
|
|
1214
|
+
durationSeconds: z.number().optional().describe('Length of the piece, so every track spans it.'),
|
|
1215
|
+
beats: z.array(CharacterBeatSchema).optional().describe('What happens, in order.'),
|
|
1216
|
+
timings: z.array(z.object({ at: z.number(), until: z.number().optional() })).optional()
|
|
1217
|
+
.describe('Word or syllable timings from a voice track; each entry drives one mouth opening.'),
|
|
1218
|
+
// THESE WERE HANDLED AND UNDECLARED. resolveCharacter reads all four and its
|
|
1219
|
+
// own comment says a call written the way the rest of the vocabulary reads
|
|
1220
|
+
// must not land dead-centre at the default size — but zod strips unknown
|
|
1221
|
+
// keys, so `{concept, position, scale}` did exactly that, silently.
|
|
1222
|
+
position: z.object({ x: z.number().optional(), y: z.number().optional() }).optional()
|
|
1223
|
+
.describe('Where the figure stands. A synonym for `at`, because the rest of this surface says position; `at` wins if both are sent.'),
|
|
1224
|
+
scale: z.number().optional()
|
|
1225
|
+
.describe('Size as a multiplier of the 300px default, for callers who think in scale rather than pixels. `height` wins if both are sent.'),
|
|
1226
|
+
palette: z.record(z.string(), z.string()).optional()
|
|
1227
|
+
.describe("Override the depiction's own colours: part id → colour. Only the parts you name change."),
|
|
1228
|
+
ink: z.string().optional()
|
|
1229
|
+
.describe("Ink colour for the stroked copies that make the figure read as drawn rather than filled. The style supplies its own; this overrides it."),
|
|
1230
|
+
});
|
|
1231
|
+
// Motion Capture (BVH)
|
|
1232
|
+
//
|
|
1233
|
+
// FxTool exposes two distinct operations and the difference is the whole point:
|
|
1234
|
+
// importBVH builds a NEW skeleton shaped like the capture file
|
|
1235
|
+
// retargetBVH drives an EXISTING rig — proportions stay yours, only MOTION comes from the clip
|
|
1236
|
+
// Angles transfer as bind-pose deltas (target.rest + (source(t) - source(rest))), so a T-posed CMU
|
|
1237
|
+
// rest is not slammed onto a rig with a relaxed stance. Bone matching carries the BVH child-joint
|
|
1238
|
+
// shift ("LeftLeg" is the thigh); anything unmatched is REPORTED, never silently half-driven.
|
|
1239
|
+
export const ImportMotionCaptureInputSchema = z.object({
|
|
1240
|
+
bvh: z.string().describe('BVH clip text (CMU / Mixamo export). Required.'),
|
|
1241
|
+
mode: z.enum(['import', 'retarget']).optional().default('import')
|
|
1242
|
+
.describe("'import' builds a new skeleton from the file; 'retarget' drives an existing rig named by skeletonId — use retarget when the character already exists, so its proportions are preserved and only the motion is taken from the capture."),
|
|
1243
|
+
skeletonId: z.string().optional()
|
|
1244
|
+
.describe("Rig to drive. REQUIRED when mode='retarget'; ignored for 'import'."),
|
|
1245
|
+
fps: z.number().optional().describe('Pose sampling rate. CMU records at 120fps and nobody wants 120 poses a second; 0 keeps every frame. Default 15.'),
|
|
1246
|
+
height: z.number().optional().describe('Skeleton height on the canvas in px (import mode). Default 320.'),
|
|
1247
|
+
position: z.object({ x: z.number(), y: z.number() }).optional()
|
|
1248
|
+
.describe('Where the ROOT joint stands (import mode). Defaults to canvas centre, 70% down.'),
|
|
1249
|
+
name: z.string().optional().describe('Name for the created skeleton (import mode).'),
|
|
1250
|
+
boneMap: z.record(z.string(), z.string()).optional()
|
|
1251
|
+
.describe('BVH edge name → this rig\'s bone name. Overrides the alias table when a clip uses non-standard joint names; check unmatchedSource/unmatchedTarget in the result to see what needs mapping.'),
|
|
1252
|
+
});
|
|
1010
1253
|
// Execute Generator
|
|
1011
1254
|
//
|
|
1012
1255
|
// Region (added FxTool c81781c): when set, the generator draws into a clipped
|
|
@@ -1042,7 +1285,13 @@ export const GetItemsInputSchema = z.object({
|
|
|
1042
1285
|
});
|
|
1043
1286
|
// Play Timeline
|
|
1044
1287
|
export const PlayTimelineInputSchema = z.object({
|
|
1045
|
-
action: z.enum([
|
|
1288
|
+
action: z.enum([
|
|
1289
|
+
'play', 'pause', 'stop', 'seek',
|
|
1290
|
+
// pp:TimeScale — the rate the scene clock runs at.
|
|
1291
|
+
'set_time_scale', 'get_time_scale',
|
|
1292
|
+
// pp:InputDrivenPlayback — progress driven by an input instead of a clock.
|
|
1293
|
+
'get_progress', 'set_progress', 'bind_scroll', 'unbind_scroll', 'list_scrub_anchors',
|
|
1294
|
+
]),
|
|
1046
1295
|
duration: z.number().optional().describe('Duration for play action'),
|
|
1047
1296
|
loop: z.boolean().optional(),
|
|
1048
1297
|
time: z.number().optional().describe('Time to seek to'),
|
|
@@ -1050,6 +1299,22 @@ export const PlayTimelineInputSchema = z.object({
|
|
|
1050
1299
|
.boolean()
|
|
1051
1300
|
.optional()
|
|
1052
1301
|
.describe('For seek: evaluate the scene at the exact time via app.sceneAt(t) — ticks keyframes + relations + generators deterministically (same t → same frame), not just the keyframe state. Use before a screenshot for a reproducible frame.'),
|
|
1302
|
+
// Deliberately unclamped: 0 freezes the clock without stopping playback, and a
|
|
1303
|
+
// negative rate runs the scene backwards. Both are things the engine supports
|
|
1304
|
+
// and a min(0) would quietly forbid.
|
|
1305
|
+
rate: z.number().optional()
|
|
1306
|
+
.describe('set_time_scale: 1 is real time, 0.5 half speed, 0 freezes without stopping, negative runs backwards. Export is unaffected — a scene watched at 0.5x still exports its real duration.'),
|
|
1307
|
+
progress: z.number().min(0).max(1).optional()
|
|
1308
|
+
.describe('set_progress: how far through the timeline, 0..1 — the unit a scrubber or a progress bar actually has.'),
|
|
1309
|
+
scroll: z.object({
|
|
1310
|
+
elementId: z.string().optional().describe('DOM id of the element to track (defaults to the canvas)'),
|
|
1311
|
+
start: z.string().optional().describe("Anchor where the range begins, e.g. 'top bottom' (the moment the element appears). See list_scrub_anchors."),
|
|
1312
|
+
end: z.string().optional().describe("Anchor where the range ends, e.g. 'bottom top' (the moment it is gone)."),
|
|
1313
|
+
scrub: z.number().min(0).optional()
|
|
1314
|
+
.describe('Seconds of smoothing — the played position chases the scroll instead of snapping to it (default 0.3). 0 snaps.'),
|
|
1315
|
+
range: z.array(z.number()).length(2).optional()
|
|
1316
|
+
.describe('[from, to] in seconds — drive only a sub-range of the timeline instead of the whole thing.'),
|
|
1317
|
+
}).optional().describe('bind_scroll options'),
|
|
1053
1318
|
});
|
|
1054
1319
|
// Canvas Control
|
|
1055
1320
|
export const SetBackgroundColorInputSchema = z.object({
|
|
@@ -1187,7 +1452,7 @@ export const SceneAnimationSchema = z.object({
|
|
|
1187
1452
|
/** Reference name of the item to animate */
|
|
1188
1453
|
target: z.string().describe('Name of the item to animate'),
|
|
1189
1454
|
/** Type of animation */
|
|
1190
|
-
type: z.enum(['pulse', 'rotate', '
|
|
1455
|
+
type: z.enum(['bounce', 'breathe', 'fade', 'glow', 'jelly', 'path', 'pulse', 'rotate', 'scrollDown', 'scrollLeft', 'scrollRight', 'scrollUp', 'shake', 'slideLeftRight', 'slideUpDown', 'swing', 'typewriter', 'wobble']),
|
|
1191
1456
|
/** Animation speed (default: 1.0) */
|
|
1192
1457
|
speed: z.number().optional(),
|
|
1193
1458
|
/** Animation parameters */
|
|
@@ -1346,6 +1611,7 @@ export const CreateDiagramShapeInputSchema = z.object({
|
|
|
1346
1611
|
* Connect items input
|
|
1347
1612
|
*/
|
|
1348
1613
|
export const ConnectInputSchema = z.object({
|
|
1614
|
+
id: z.string().optional().describe('Stable connector ID. Assign your own and reuse it as `connectorId` in update/remove — the engine otherwise mints a Date.now()-based ID no caller can predict'),
|
|
1349
1615
|
sourceItemId: z.string().describe('Registry ID of source item'),
|
|
1350
1616
|
targetItemId: z.string().describe('Registry ID of target item'),
|
|
1351
1617
|
routing: ConnectorRoutingSchema.optional().default('orthogonal'),
|
|
@@ -1363,6 +1629,7 @@ export const ConnectInputSchema = z.object({
|
|
|
1363
1629
|
* Connect specific ports input
|
|
1364
1630
|
*/
|
|
1365
1631
|
export const ConnectPortsInputSchema = z.object({
|
|
1632
|
+
id: z.string().optional().describe('Stable connector ID. Assign your own and reuse it as `connectorId` in update/remove — the engine otherwise mints a Date.now()-based ID no caller can predict'),
|
|
1366
1633
|
sourceItemId: z.string().describe('Registry ID of source item'),
|
|
1367
1634
|
sourcePort: PortPositionSchema.describe('Port position on source'),
|
|
1368
1635
|
targetItemId: z.string().describe('Registry ID of target item'),
|
|
@@ -1561,6 +1828,10 @@ export const AgentPlatformSchema = z.enum([
|
|
|
1561
1828
|
*/
|
|
1562
1829
|
export const AgentExportFormatSchema = z.enum([
|
|
1563
1830
|
'svg', 'png', 'gif', 'mp4', 'webm', 'pdf',
|
|
1831
|
+
// AUDIO-ONLY. The soundtrack on its own, with no frames rendered — so
|
|
1832
|
+
// platform dimensions, framing and quality do not apply to it, and no
|
|
1833
|
+
// platform preset resolves to it. It has to be asked for by name.
|
|
1834
|
+
'wav',
|
|
1564
1835
|
]).describe('Export file format');
|
|
1565
1836
|
/**
|
|
1566
1837
|
* Screenshot policy for agent jobs
|
|
@@ -1739,6 +2010,17 @@ export const AgentBatchExecuteInputSchema = z.object({
|
|
|
1739
2010
|
* could not. This brings the two in line.
|
|
1740
2011
|
*/
|
|
1741
2012
|
export const GIF_MAX_DURATION_S = 15;
|
|
2013
|
+
/**
|
|
2014
|
+
* Longest mp4/webm export the tool will accept, in seconds.
|
|
2015
|
+
*
|
|
2016
|
+
* Was 60. A ten-minute export is only reachable because the bytes no longer
|
|
2017
|
+
* cross the browser→server bridge as one base64 string: above an inline
|
|
2018
|
+
* ceiling the studio keeps the file in its export store and the handler pages
|
|
2019
|
+
* it out in chunks. On a studio with no export store the emitted code refuses
|
|
2020
|
+
* by name with the numbers instead, so raising this cap cannot turn into an
|
|
2021
|
+
* out-of-memory — it turns into a message naming the ceiling and what to do.
|
|
2022
|
+
*/
|
|
2023
|
+
export const VIDEO_MAX_DURATION_S = 600;
|
|
1742
2024
|
/**
|
|
1743
2025
|
* Smart export input schema
|
|
1744
2026
|
*/
|
|
@@ -1748,10 +2030,29 @@ export const AgentExportInputSchema = z.object({
|
|
|
1748
2030
|
quality: z.enum(['draft', 'standard', 'high']).optional().default('standard').describe('Export quality level'),
|
|
1749
2031
|
includeRecommendations: z.boolean().optional().default(true).describe('Include alternative format recommendations'),
|
|
1750
2032
|
framing: z.enum(['canvas', 'camera']).optional().default('canvas').describe('Output framing: "canvas" (full canvas, default) or "camera" (camera_animates first-keyframe viewport — fails if no walkthrough exists). Camera animation still drives motion within the fixed output frame.'),
|
|
1751
|
-
duration: z.number().min(0.5).max(
|
|
1752
|
-
estimateOnly: z.boolean().optional().default(false).describe('Preflight only: return the estimated file size for these EXACT settings and render nothing. Use before a long or high-quality export to check the size first. Modeled for mp4/webm/gif; png/pdf/svg return confidence "none" because no dimension-based model exists for them.'),
|
|
2033
|
+
duration: z.number().min(0.5).max(VIDEO_MAX_DURATION_S).optional().default(5).describe(`Video duration in seconds for animated formats (mp4/webm/gif). Default 5. Max ${VIDEO_MAX_DURATION_S} for mp4/webm, max ${GIF_MAX_DURATION_S} for gif (GIF is not codec-bounded, so file size scales with frames × dimensions, and it cannot stream to the export store). Static formats (png/svg/pdf) ignore this. Past roughly a minute the export is held in the studio's export store and paged back to a file rather than returned inline; a studio without that store refuses by name and says so.`),
|
|
2034
|
+
estimateOnly: z.boolean().optional().default(false).describe('Preflight only: return the estimated file size for these EXACT settings and render nothing. Use before a long or high-quality export to check the size first. Modeled for mp4/webm/gif; png/pdf/svg return confidence "none" because no dimension-based model exists for them; wav is reported as "exact", because uncompressed PCM size is arithmetic rather than a guess.'),
|
|
2035
|
+
sampleRate: z.number().int().positive().optional().describe('wav only: samples per second (default 48000). Ignored by every other format.'),
|
|
2036
|
+
bitDepth: z.union([z.literal(16), z.literal(32)]).optional().describe('wav only: 16 (default) or 32-bit float. Ignored by every other format.'),
|
|
1753
2037
|
}).describe('Smart export options')
|
|
1754
2038
|
.superRefine((val, ctx) => {
|
|
2039
|
+
// wav carries no picture, so the visual knobs are not merely ignored — a
|
|
2040
|
+
// caller who set them believes something about the output that is not
|
|
2041
|
+
// true. Said once, rather than silently dropped.
|
|
2042
|
+
if (val.format === 'wav' && val.framing === 'camera') {
|
|
2043
|
+
ctx.addIssue({
|
|
2044
|
+
code: z.ZodIssueCode.custom,
|
|
2045
|
+
path: ['framing'],
|
|
2046
|
+
message: 'framing does not apply to a wav export — it renders no frames. Drop framing, or export mp4/webm if you want the picture with the sound.',
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
if (val.format !== 'wav' && (val.sampleRate !== undefined || val.bitDepth !== undefined)) {
|
|
2050
|
+
ctx.addIssue({
|
|
2051
|
+
code: z.ZodIssueCode.custom,
|
|
2052
|
+
path: ['sampleRate'],
|
|
2053
|
+
message: `sampleRate and bitDepth apply only to format "wav" (got "${val.format}").`,
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
1755
2056
|
if (val.format === 'gif' && val.duration > GIF_MAX_DURATION_S) {
|
|
1756
2057
|
ctx.addIssue({
|
|
1757
2058
|
code: z.ZodIssueCode.custom,
|
|
@@ -2208,6 +2509,33 @@ export const QueryOntologyInputSchema = z.object({
|
|
|
2208
2509
|
category: z.string().optional(),
|
|
2209
2510
|
includeAbstract: z.boolean().optional().default(false),
|
|
2210
2511
|
});
|
|
2512
|
+
// Capabilities query schema: discover and match capabilities across text styles, text effects, generators, deforms, relations, etc.
|
|
2513
|
+
export const QueryCapabilitiesInputSchema = z.object({
|
|
2514
|
+
action: z.enum(['list', 'choose', 'coverage', 'find', 'catalogue', 'studio']).default('list')
|
|
2515
|
+
.describe("'list' (all capabilities or filtered by kind) · 'choose' (context/mood-weighted recommendation) · 'coverage' (breakdown of indexable capabilities) · 'find' (lookup by key) · 'catalogue' (the registries the aggregate does NOT gather — see `catalogue`) · 'studio' (which engine METHODS this studio has, so you learn what will refuse BEFORE calling it)"),
|
|
2516
|
+
catalogue: z.enum([
|
|
2517
|
+
'rig_presets', 'shader_effects', 'stroke_decorations', 'precomps', 'images',
|
|
2518
|
+
'segment_edit_kinds', 'shatter_orders', 'world_meshes', 'relation_presets',
|
|
2519
|
+
]).optional().describe("action 'catalogue': which registry to read. These sit OUTSIDE app.getCapabilities' aggregate — that one already covers styles, effects, deforms, entrances, animations, collages, palettes, masks, maskShapes and cutouts, and 'list' reaches all of those. Use this for the ones it does not gather: rig presets, shader effects, stroke decorations, the precomp list, the image library, segment-edit kinds, shatter orders and world meshes."),
|
|
2520
|
+
kind: z.union([
|
|
2521
|
+
z.string(),
|
|
2522
|
+
z.array(z.string()),
|
|
2523
|
+
]).optional().describe("Filter by capability kind: 'style'|'effect'|'deform'|'entrance'|'animation'|'generator'|'collage'|'palette'|'mask'|'cutout'|'relation'"),
|
|
2524
|
+
mood: z.union([
|
|
2525
|
+
z.string(),
|
|
2526
|
+
z.array(z.string()),
|
|
2527
|
+
]).optional().describe("For choose: target feeling ('reveal'|'triumphant'|'failure'|'calm'|'technical'|'energetic')"),
|
|
2528
|
+
subject: z.string().optional().describe('For choose: contextual subject keywords / prompt text to score against capability definitions'),
|
|
2529
|
+
avoid: z.union([
|
|
2530
|
+
z.string(),
|
|
2531
|
+
z.array(z.string()),
|
|
2532
|
+
]).optional().describe('For choose: mood terms or feelings to penalize'),
|
|
2533
|
+
seed: z.string().optional().describe('For choose: deterministic seed string for reproducible selection'),
|
|
2534
|
+
key: z.string().optional().describe('For find: exact capability key to look up'),
|
|
2535
|
+
exclude: z.array(z.string()).optional().describe('For choose: list of capability keys to exclude from recommendation'),
|
|
2536
|
+
warm: z.boolean().optional()
|
|
2537
|
+
.describe('Load the lazy registries before answering (default true). The generator registry and the rigging/blending/deform relation rules only exist once touched, so a cold answer omits them — pass false only to re-read what is already resident, cheaply.'),
|
|
2538
|
+
});
|
|
2211
2539
|
// =============================================================================
|
|
2212
2540
|
// SCENE MANAGEMENT SCHEMAS
|
|
2213
2541
|
// =============================================================================
|
|
@@ -2232,6 +2560,175 @@ export const ScenePlaybackInputSchema = z.object({
|
|
|
2232
2560
|
index: z.number().optional(),
|
|
2233
2561
|
});
|
|
2234
2562
|
// =============================================================================
|
|
2563
|
+
// RELATIVE TIMING, STAGGERS, FLIP (GSAP's vocabulary, PinePaper's engine)
|
|
2564
|
+
// =============================================================================
|
|
2565
|
+
// pp:TimelinePosition — saying WHEN relative to something else.
|
|
2566
|
+
//
|
|
2567
|
+
// The position spec is a number or one of the string forms; it stays loose on
|
|
2568
|
+
// purpose, because the grammar is open ("intro+=0.5" names a caller's label) and
|
|
2569
|
+
// an enum here could only ever be a stale subset of it. list_forms is the
|
|
2570
|
+
// discoverable list.
|
|
2571
|
+
export const SequenceInputSchema = z.object({
|
|
2572
|
+
action: z.enum(['place', 'resolve', 'list_forms']).default('place')
|
|
2573
|
+
.describe("'place' threads a run of clips, each resolved against the ones before it · 'resolve' answers a single position · 'list_forms' returns the accepted grammar with what each form means"),
|
|
2574
|
+
clips: z.array(z.object({
|
|
2575
|
+
id: z.string().optional().describe('Your own name for the clip — echoed back on the placement'),
|
|
2576
|
+
duration: z.number().min(0).describe('How long the clip runs, in seconds'),
|
|
2577
|
+
position: z.union([z.number(), z.string()]).optional()
|
|
2578
|
+
.describe('Where it starts. Omit to append at the end of the timeline so far.'),
|
|
2579
|
+
label: z.string().optional().describe('Name this moment so later clips can position against it. A label marks where the clip STARTS.'),
|
|
2580
|
+
})).optional().describe('place: the run of clips, in order'),
|
|
2581
|
+
position: z.union([z.number(), z.string()]).optional().describe('resolve: the single position spec to resolve'),
|
|
2582
|
+
context: z.object({
|
|
2583
|
+
timelineEnd: z.number().optional().describe('Where the timeline currently ends — what a bare "+=1" counts from'),
|
|
2584
|
+
prevStart: z.number().optional().describe('Start of the most recently added clip — what "<" means'),
|
|
2585
|
+
prevEnd: z.number().optional().describe('End of the most recently added clip — what ">" means'),
|
|
2586
|
+
insertDuration: z.number().optional().describe('Duration of the clip being placed — the basis for a "+=25%" / "-=25%" percentage'),
|
|
2587
|
+
}).optional().describe('resolve: what the spec is measured against'),
|
|
2588
|
+
labels: z.record(z.string(), z.number()).optional().describe('Named moments in seconds, addressable as "name" or "name+=0.5"'),
|
|
2589
|
+
startAt: z.number().optional().describe('place: where the run begins (default 0)'),
|
|
2590
|
+
});
|
|
2591
|
+
// pp:Stagger — the shape of a delay across many items.
|
|
2592
|
+
const StaggerOptsSchema = z.object({
|
|
2593
|
+
// `each` and `amount` are ALTERNATIVES, not a pair: each fixes the gap between
|
|
2594
|
+
// neighbours so more items make a longer sequence; amount fixes the total so
|
|
2595
|
+
// more items crowd together.
|
|
2596
|
+
each: z.number().min(0).optional().describe('Seconds between neighbours (default 0.1). More items ⇒ a longer sequence.'),
|
|
2597
|
+
amount: z.number().min(0).optional().describe('Total seconds for the WHOLE run, split among the items. Overrides each — more items ⇒ they crowd together.'),
|
|
2598
|
+
from: z.union([
|
|
2599
|
+
z.enum(['start', 'end', 'center', 'edges', 'random']),
|
|
2600
|
+
z.number(),
|
|
2601
|
+
z.array(z.number()).length(2),
|
|
2602
|
+
]).optional().describe("Where the stagger begins: a named origin, an item index, or [fx, fy] fractions for a grid ([0.5, 0.5] is the centre)."),
|
|
2603
|
+
grid: z.array(z.number()).length(2).optional().describe('[rows, columns] for a 2D stagger. Items are read row-major.'),
|
|
2604
|
+
axis: z.enum(['x', 'y']).optional().describe('Restrict a grid stagger to one axis.'),
|
|
2605
|
+
ease: z.enum(['linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInCubic', 'easeOutCubic']).optional()
|
|
2606
|
+
.describe("Curve that distributes the START TIMES — not the items' own animation curve."),
|
|
2607
|
+
seed: z.number().optional().describe("from: 'random' — same seed, same shuffle (default 1)."),
|
|
2608
|
+
});
|
|
2609
|
+
export const StaggerInputSchema = z.object({
|
|
2610
|
+
action: z.enum(['apply', 'preview', 'list_origins']).default('apply')
|
|
2611
|
+
.describe("'apply' writes the delays onto real items · 'preview' returns the delays for a count without touching the canvas · 'list_origins' names the origins"),
|
|
2612
|
+
itemIds: z.array(z.string()).optional().describe('apply: registry ids IN THE ORDER the stagger should follow (row-major for a grid)'),
|
|
2613
|
+
count: z.number().int().min(0).optional().describe('preview: how many items to compute delays for'),
|
|
2614
|
+
opts: StaggerOptsSchema.optional().describe('Shape of the stagger'),
|
|
2615
|
+
});
|
|
2616
|
+
// pp:Flip — a transition DERIVED from a change rather than described.
|
|
2617
|
+
export const FlipInputSchema = z.object({
|
|
2618
|
+
action: z.enum(['record', 'apply']).default('record')
|
|
2619
|
+
.describe("'record' captures where things are NOW · 'apply' animates from that record to wherever they have since been moved"),
|
|
2620
|
+
itemIds: z.array(z.string()).optional()
|
|
2621
|
+
.describe('record: which items to capture (defaults to everything on the item layer, which is the usual case for a re-layout)'),
|
|
2622
|
+
duration: z.number().min(0).optional().describe('apply: seconds the transition runs (engine default)'),
|
|
2623
|
+
easing: z.string().optional().describe('apply: named easing for the generated keyframes'),
|
|
2624
|
+
enter: z.string().optional().describe('apply: entrance preset for items that appeared during the change, so they animate in rather than popping'),
|
|
2625
|
+
stagger: StaggerOptsSchema.optional().describe("apply: stagger the transition across the items, in the items' own order"),
|
|
2626
|
+
});
|
|
2627
|
+
// Scene graph: interactive multi-node story/quiz state graphs (cards, answers, mutex, event wiring)
|
|
2628
|
+
//
|
|
2629
|
+
// The node and opts fields below mirror what the engine's compiler actually
|
|
2630
|
+
// reads (js/core/SceneGraph.js). zod strips unknown keys, so a field missing
|
|
2631
|
+
// here is not "passed through" — it is silently dropped on the way to an engine
|
|
2632
|
+
// that would have honoured it. `next`, `duration`, `outcome` and `subject` were
|
|
2633
|
+
// each unreachable for that reason.
|
|
2634
|
+
const SceneGraphNodeSchema = z.object({
|
|
2635
|
+
id: z.string().describe('Unique node id (e.g., "q1", "win", "lose")'),
|
|
2636
|
+
prompt: z.string().optional().describe('Prompt or headline text for question cards'),
|
|
2637
|
+
text: z.string().optional().describe('Text for message/end cards'),
|
|
2638
|
+
kind: z.enum(['node', 'card', 'end']).optional()
|
|
2639
|
+
.describe("'node'/'card' (question or content) or 'end' (terminal/outcome). Anything that is not 'end' is a card."),
|
|
2640
|
+
answers: z.array(z.object({
|
|
2641
|
+
text: z.string().describe('Answer button text'),
|
|
2642
|
+
correct: z.boolean().optional().describe('Whether this is the correct answer (increments score when scoreItemId given)'),
|
|
2643
|
+
to: z.string().min(1).describe('Destination node id when clicked'),
|
|
2644
|
+
})).optional().describe('Interactive answer choices (for question cards)'),
|
|
2645
|
+
next: z.string().min(1).optional()
|
|
2646
|
+
.describe('Unconditional destination — a linear story beat that auto-advances instead of waiting for a click. Use INSTEAD of answers, not alongside a lone fake button.'),
|
|
2647
|
+
duration: z.number().positive().optional()
|
|
2648
|
+
.describe('Seconds this card holds before `next` fires (default 3). Ignored without `next`.'),
|
|
2649
|
+
outcome: z.enum(['win', 'lose']).optional()
|
|
2650
|
+
.describe('Force the outcome of a terminal node. Without it the engine infers win/lose from the wording, which only works in English.'),
|
|
2651
|
+
});
|
|
2652
|
+
export const SceneGraphInputSchema = z.object({
|
|
2653
|
+
action: z.enum(['create', 'validate']).default('create')
|
|
2654
|
+
.describe("'create' builds the graph on the canvas · 'validate' runs the same check WITHOUT drawing anything, returning errors, warnings, unreachable nodes and cycles"),
|
|
2655
|
+
graph: z.object({
|
|
2656
|
+
start: z.string().optional().describe('Node id of the initial active card (defaults to the first node)'),
|
|
2657
|
+
nodes: z.array(SceneGraphNodeSchema).min(1).describe('Array of scene graph nodes'),
|
|
2658
|
+
})
|
|
2659
|
+
// Structural integrity, checked HERE because in code mode there is no engine
|
|
2660
|
+
// to catch it: the snippet is handed to the caller and fails silently later.
|
|
2661
|
+
// Duplicate ids in particular are invisible engine-side — `indexNodes` keys a
|
|
2662
|
+
// Map by id, so the second node quietly replaces the first.
|
|
2663
|
+
.superRefine((graph, ctx) => {
|
|
2664
|
+
const seen = new Set();
|
|
2665
|
+
graph.nodes.forEach((node, i) => {
|
|
2666
|
+
if (seen.has(node.id)) {
|
|
2667
|
+
ctx.addIssue({
|
|
2668
|
+
code: z.ZodIssueCode.custom,
|
|
2669
|
+
path: ['nodes', i, 'id'],
|
|
2670
|
+
message: `duplicate node id "${node.id}" — the later node silently replaces the earlier one`,
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
seen.add(node.id);
|
|
2674
|
+
});
|
|
2675
|
+
const has = (id) => seen.has(id);
|
|
2676
|
+
if (graph.start !== undefined && !has(graph.start)) {
|
|
2677
|
+
ctx.addIssue({
|
|
2678
|
+
code: z.ZodIssueCode.custom,
|
|
2679
|
+
path: ['start'],
|
|
2680
|
+
message: `start node "${graph.start}" does not exist`,
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
graph.nodes.forEach((node, i) => {
|
|
2684
|
+
(node.answers || []).forEach((answer, a) => {
|
|
2685
|
+
if (!has(answer.to)) {
|
|
2686
|
+
ctx.addIssue({
|
|
2687
|
+
code: z.ZodIssueCode.custom,
|
|
2688
|
+
path: ['nodes', i, 'answers', a, 'to'],
|
|
2689
|
+
message: `node "${node.id}" points at "${answer.to}", which does not exist — clicking it would do nothing`,
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
if (node.next !== undefined && !has(node.next)) {
|
|
2694
|
+
ctx.addIssue({
|
|
2695
|
+
code: z.ZodIssueCode.custom,
|
|
2696
|
+
path: ['nodes', i, 'next'],
|
|
2697
|
+
message: `node "${node.id}" advances to "${node.next}", which does not exist`,
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
// Mirrors the engine's own refusal: a non-terminal card with no edge is
|
|
2701
|
+
// a dead end the player can never leave.
|
|
2702
|
+
if (node.kind !== 'end' && !node.answers?.length && node.next === undefined) {
|
|
2703
|
+
ctx.addIssue({
|
|
2704
|
+
code: z.ZodIssueCode.custom,
|
|
2705
|
+
path: ['nodes', i],
|
|
2706
|
+
message: `node "${node.id}" is not terminal but has no way out — give it answers, a next, or kind: 'end'`,
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2710
|
+
})
|
|
2711
|
+
.describe('The declarative scene graph definition'),
|
|
2712
|
+
opts: z.object({
|
|
2713
|
+
width: z.number().optional().describe('Layout width (defaults to view width or canvas width)'),
|
|
2714
|
+
height: z.number().optional().describe('Layout height (defaults to view height or canvas height)'),
|
|
2715
|
+
centerX: z.number().optional().describe('Center X position for card stack'),
|
|
2716
|
+
centerY: z.number().optional().describe('Center Y position for card stack'),
|
|
2717
|
+
groupName: z.string().optional().describe('Prefix / seed namespace for generated IDs and effect selection'),
|
|
2718
|
+
scoreItemId: z.string().optional().describe('Optional scoreboard text item id to increment on correct answers'),
|
|
2719
|
+
subject: z.string().optional()
|
|
2720
|
+
.describe("Extra context folded into each card's text-effect selection — the theme of the piece, e.g. 'space exploration'."),
|
|
2721
|
+
plain: z.boolean().optional().describe('Opt out of rich effects/entrances for a bare structural graph'),
|
|
2722
|
+
style: z.object({
|
|
2723
|
+
panelColor: z.string().optional().describe('Background panel fill color (default #1E1B4B)'),
|
|
2724
|
+
textColor: z.string().optional().describe('Headline/prompt text color (default #F8FAFC)'),
|
|
2725
|
+
answerColor: z.string().optional().describe('Answer text color (default #93C5FD)'),
|
|
2726
|
+
headlineSize: z.number().optional().describe('Font size for headline/prompt (default 34)'),
|
|
2727
|
+
bodySize: z.number().optional().describe('Font size for body/answers (default 24)'),
|
|
2728
|
+
}).optional().describe('Visual styling options for generated cards and text'),
|
|
2729
|
+
}).optional().describe('Layout, style, and scoring options'),
|
|
2730
|
+
});
|
|
2731
|
+
// =============================================================================
|
|
2235
2732
|
// SELECTION, TRANSFORM & HISTORY TOOLS
|
|
2236
2733
|
// =============================================================================
|
|
2237
2734
|
export const SelectionInputSchema = z.object({
|
|
@@ -2329,6 +2826,28 @@ export const ProvenanceInputSchema = z.object({
|
|
|
2329
2826
|
sourceRef: z.string().optional(),
|
|
2330
2827
|
meta: z.record(z.string(), z.unknown()).optional(),
|
|
2331
2828
|
});
|
|
2829
|
+
/**
|
|
2830
|
+
* The stitches the thread medium publishes — ONE list, because it had four copies.
|
|
2831
|
+
*
|
|
2832
|
+
* Mirrors FxTool's `STITCH_OPS` in js/core/ThreadPainting.js — ENFORCED BY
|
|
2833
|
+
* src/__tests__/unit/preflight-gate-parity.test.ts against the fixture
|
|
2834
|
+
* src/__tests__/fixtures/engine-stitches.txt, both directions, with a
|
|
2835
|
+
* guard-the-guard that plants a seventh stitch and asserts the comparison
|
|
2836
|
+
* fails. A parity claim that cites the test enforcing it is checkable in one
|
|
2837
|
+
* command; one that does not is a memory of someone having checked once, and
|
|
2838
|
+
* this sentence was that memory for six commits. The table is
|
|
2839
|
+
* vocabulary `applyThreadPainting` dispatches on; it refuses an unknown name
|
|
2840
|
+
* rather than falling through to a default fill.
|
|
2841
|
+
*
|
|
2842
|
+
* There were four copies of this list here — two Zod enums and their two JSON
|
|
2843
|
+
* mirrors — and fixing one left `pinepaper_compose` unable to ask for two
|
|
2844
|
+
* stitches `apply_thread` could. The engine had the identical split for the
|
|
2845
|
+
* identical reason: `MEDIA.thread.stitches` was hardcoded to the same stale
|
|
2846
|
+
* four while STITCH_OPS published six (FxTool 6aaa9069). Two repos, four
|
|
2847
|
+
* copies, one list, drifted the same direction on the same day — so it is
|
|
2848
|
+
* declared once here and spread everywhere it is needed.
|
|
2849
|
+
*/
|
|
2850
|
+
export const THREAD_STITCHES = ['longAndShort', 'satin', 'seed', 'stem', 'runningSeam', 'crossStitch'];
|
|
2332
2851
|
export const SceneDiffInputSchema = z.object({
|
|
2333
2852
|
/** 'history' compares two undo states; 'version' compares live vs a saved version. */
|
|
2334
2853
|
action: z.enum(['history', 'version']),
|
|
@@ -2364,6 +2883,24 @@ export const ComposeInputSchema = z.object({
|
|
|
2364
2883
|
audio: z.string().optional(),
|
|
2365
2884
|
/** With audio: snap to an even tempo grid instead of raw onsets. */
|
|
2366
2885
|
grid: z.boolean().optional(),
|
|
2886
|
+
/**
|
|
2887
|
+
* WHICH DESIGN LANGUAGE. Resolves the craft — gutter, margin, hue budget,
|
|
2888
|
+
* type scale, alignment discipline — so those move together instead of being
|
|
2889
|
+
* chosen one at a time. A default, not a handcuff: an explicit `craft` still
|
|
2890
|
+
* wins. naive | playful | poster | editorial | technical.
|
|
2891
|
+
*/
|
|
2892
|
+
register: z.string().optional().describe('Design register — naive, playful, poster, editorial, technical. Sets the craft; an explicit `craft` still overrides it.'),
|
|
2893
|
+
level: z.number().int().min(1).max(4).optional().describe('Craft level 1-4 WITHIN the register (sketch, competent, refined, art-directed). Climbing never widens a budget — restraint is the direction of craft.'),
|
|
2894
|
+
/**
|
|
2895
|
+
* WHAT MAKES THE MARKS. Only media a whole composition can be rendered in
|
|
2896
|
+
* are accepted: `vector` (what composing already produces) and `thread`
|
|
2897
|
+
* (needlepainting, stitched after the arrangement). The rest are refused
|
|
2898
|
+
* with the medium's own reason rather than composed as flat shapes in its
|
|
2899
|
+
* colours.
|
|
2900
|
+
*/
|
|
2901
|
+
medium: z.enum(['vector', 'thread']).optional().describe("Render the composition in a medium. 'thread' stitches every closed path; photographs and text CANNOT be stitched and come back in `medium.skipped` with the reason, so read that count — a composition can render completely and be entirely unstitched."),
|
|
2902
|
+
stitch: z.enum(THREAD_STITCHES).optional().describe('Which stitch, when medium is thread. Default longAndShort. The same six pinepaper_design_medium offers — call its list_stitches for each one.'),
|
|
2903
|
+
stitchBudget: z.number().int().min(200).max(20000).optional().describe('Total marks across the composition (default 6000). The stitch is scaled to fit rather than the fill being truncated — a half-stitched shape looks broken, coarser thread does not.'),
|
|
2367
2904
|
});
|
|
2368
2905
|
export const AudioBeatsInputSchema = z.object({
|
|
2369
2906
|
action: z.enum(['analyze', 'animate_to_beat']),
|
|
@@ -2743,8 +3280,17 @@ export const MediaInputSchema = z.object({
|
|
|
2743
3280
|
* platform impossibility, not an omission.
|
|
2744
3281
|
*/
|
|
2745
3282
|
export const TextStyleInputSchema = z.object({
|
|
2746
|
-
action: z.enum(['apply_style', 'set_font_axes', 'list_styles'])
|
|
2747
|
-
.describe("'apply_style' (stacked-layer display style) · 'set_font_axes' (variable-font weight/width/slant) · 'list_styles' (styles + palettes + axes, for pickers)"),
|
|
3283
|
+
action: z.enum(['apply_style', 'set_font_axes', 'list_styles', 'cursive', 'wrap', 'unwrap', 'to_collage'])
|
|
3284
|
+
.describe("'apply_style' (stacked-layer display style) · 'set_font_axes' (variable-font weight/width/slant) · 'list_styles' (styles + palettes + axes, for pickers) · 'cursive' (draw text as STROKED handwriting — a path, not a glyph, so it can be drawn on over time) · 'wrap' / 'unwrap' (break a text item to a width, reversibly) · 'to_collage' (turn an EXISTING text item into a letter collage, keeping its place)"),
|
|
3285
|
+
text: z.string().optional().describe('cursive: the words to write. to_collage: the text to build the collage from, if it differs from the item.'),
|
|
3286
|
+
maxWidth: z.number().optional().describe('wrap: the width to break at, in canvas units. unwrap restores the original single line, so this is not destructive.'),
|
|
3287
|
+
cursiveOptions: z.object({
|
|
3288
|
+
x: z.number().optional(), y: z.number().optional(),
|
|
3289
|
+
scale: z.number().optional().describe('Size multiplier (default 1).'),
|
|
3290
|
+
strokeColor: z.string().optional().describe("Ink colour (default '#ffffff')."),
|
|
3291
|
+
strokeWidth: z.number().optional().describe('Pen width (default 2).'),
|
|
3292
|
+
}).optional().describe('cursive: where and how it is written. The result is a STROKED PATH, so pinepaper_animate\'s draw-on and pinepaper_path\'s outline_stroke both apply to it — which is the reason to prefer it over a script font.'),
|
|
3293
|
+
collageOptions: z.record(z.string(), z.unknown()).optional().describe('to_collage: style, palette and the rest, same vocabulary as pinepaper_create_letter_collage.'),
|
|
2748
3294
|
itemId: z.string().optional().describe('Text item id — apply_style / set_font_axes.'),
|
|
2749
3295
|
styleKey: z.string().optional().describe("apply_style: style name from list_styles (e.g. 'stacked', 'arcade' — the pixel/arcade styles suggest their own face)."),
|
|
2750
3296
|
palette: z.union([z.string(), z.array(z.string())]).optional().describe('apply_style: a named colourway or an explicit color array.'),
|
|
@@ -2760,6 +3306,104 @@ export const TextStyleInputSchema = z.object({
|
|
|
2760
3306
|
})
|
|
2761
3307
|
.refine((v) => v.action !== 'apply_style' || (!!v.itemId && !!v.styleKey), { message: 'apply_style requires itemId and styleKey', path: ['styleKey'] })
|
|
2762
3308
|
.refine((v) => v.action !== 'set_font_axes' || (!!v.itemId && !!v.axes && Object.keys(v.axes).length > 0), { message: 'set_font_axes requires itemId and at least one axis', path: ['axes'] });
|
|
3309
|
+
/**
|
|
3310
|
+
* pinepaper_text_effect — the 37 character-level text effects
|
|
3311
|
+
* (terminaltexteffects' vocabulary, reimplemented in FxTool from source).
|
|
3312
|
+
*
|
|
3313
|
+
* The contract that separates this from `pinepaper_text_style`, and the one
|
|
3314
|
+
* thing a caller must know: this tool EXPLODES the text item into one item per
|
|
3315
|
+
* character and REMOVES the original. `text_style` ADOPTS the text's registry
|
|
3316
|
+
* id; this DESTROYS it. Relations, keyframes and agent handles pointing at the
|
|
3317
|
+
* source id die with it — `keepSource: true` leaves the original in place if
|
|
3318
|
+
* they matter.
|
|
3319
|
+
*
|
|
3320
|
+
* `effect` is a free string, not an enum: the engine owns the vocabulary, its
|
|
3321
|
+
* own error names every valid key, and `action: 'list'` is authoritative. An
|
|
3322
|
+
* enum here would pin the server to today's 37 and go stale on the 38th.
|
|
3323
|
+
*
|
|
3324
|
+
* The planner is pure and emits KEYFRAMES, so the result is ordinary animated
|
|
3325
|
+
* items — it scrubs, survives undo/session restore, and exports through the
|
|
3326
|
+
* existing MP4/SMIL/Lottie paths rather than needing a per-exporter animator.
|
|
3327
|
+
*/
|
|
3328
|
+
/**
|
|
3329
|
+
* The MEDIUM axis — what makes the marks, as distinct from which design
|
|
3330
|
+
* language (register) and how well executed (level).
|
|
3331
|
+
*
|
|
3332
|
+
* Every medium declares a fidelity, and the tool refuses an `absent` one with
|
|
3333
|
+
* the reason rather than producing flat shapes in its colours. Ask
|
|
3334
|
+
* `list_media` before promising a medium in prose.
|
|
3335
|
+
*/
|
|
3336
|
+
export const DesignMediumInputSchema = z.object({
|
|
3337
|
+
action: z.enum(['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options'])
|
|
3338
|
+
.describe("'list_media' (7 media with fidelity + limitation) · 'resolve' (can this medium be made here, and how honestly) · 'list_stitches' · 'apply_thread' (render an item in thread) · 'apply_hatch' (rule an item with hatching — value through line density) · 'list_flow_fields' · 'list_hatch_options'"),
|
|
3339
|
+
medium: z.string().optional().describe("resolve: medium key — vector, thread, hatch, watercolor, ink, cutPaper, charcoal, oil, encaustic. Call list_media for the live set with each one's fidelity; the catalogue grows."),
|
|
3340
|
+
itemId: z.string().optional().describe('apply_thread / apply_hatch: a closed path, compound path, or a group of them. Its own silhouette is the region and its fill is the ink colour.'),
|
|
3341
|
+
stitch: z.enum(THREAD_STITCHES).optional()
|
|
3342
|
+
.describe("Which stitch, when the medium is thread. Default longAndShort. These six are THE_STITCH_OPS the engine publishes — call 'list_stitches' for each one's description and its own parameters. An unknown name is refused by the engine rather than quietly stitched as a default fill."),
|
|
3343
|
+
field: z.object({
|
|
3344
|
+
kind: z.enum(['radial', 'spine', 'constant']),
|
|
3345
|
+
cx: z.number().optional(), cy: z.number().optional(),
|
|
3346
|
+
angle: z.number().optional(),
|
|
3347
|
+
spine: z.array(PositionSchema).optional(),
|
|
3348
|
+
across: z.boolean().optional(),
|
|
3349
|
+
}).optional().describe("apply_thread: the direction field — the thing that makes it needlepainting rather than hatching. Default radial from the shape centre. 'spine' runs stitches along a midrib or feather shaft; 'constant' is flat hatch."),
|
|
3350
|
+
stitchLen: z.number().positive().optional().describe('apply_thread: nominal stitch length px (default 18).'),
|
|
3351
|
+
rowGap: z.number().positive().optional().describe('apply_thread: row spacing px. Defaults from the thread width so the rows abut; setting it wider is what makes a fill read as hatching.'),
|
|
3352
|
+
variance: z.number().min(0).max(1).optional().describe('apply_thread: length jitter 0..1 (default 0.35) — the variance IS the long-and-short shading.'),
|
|
3353
|
+
width: z.number().positive().optional().describe('apply_thread: thread width px (default 2).'),
|
|
3354
|
+
sheen: z.boolean().optional().describe('apply_thread: draw the highlight along each stitch (default true) — thread is not matte.'),
|
|
3355
|
+
color: z.string().optional().describe("apply_thread: thread colour, defaulting to the item's own fill."),
|
|
3356
|
+
seed: z.number().int().optional().describe('apply_thread: PRNG seed (default 1). Same seed stitches the same way.'),
|
|
3357
|
+
count: z.number().int().positive().optional().describe('apply_thread: stitch count for seed/satin.'),
|
|
3358
|
+
// THE OPTIONS THE ENGINE READS AND NOTHING NAMED. ThreadPainting.paint reads
|
|
3359
|
+
// every one of these; apply_thread passed ten of its eighteen, so eight
|
|
3360
|
+
// controls existed and were unreachable — the same gap that made the stick
|
|
3361
|
+
// kit's options source-only.
|
|
3362
|
+
slant: z.number().optional().describe('apply_thread: stitch slant in DEGREES — satin (default 0) and stem (default 22). A satin fill laid at a slant is what separates embroidery from ruling.'),
|
|
3363
|
+
overlap: z.number().min(0).optional().describe('apply_thread: how far consecutive stem stitches overlap (default 0.4) — what makes a stem outline read as rope rather than a dashed line.'),
|
|
3364
|
+
stagger: z.number().min(0).max(1).optional().describe('apply_thread: offsets alternate longAndShort rows (default 0.5) — stops stitch ends lining up into seams down the fill.'),
|
|
3365
|
+
gapLen: z.number().positive().optional().describe('apply_thread: gap between runningSeam stitches, px (default 4). With stitchLen, this is the dash rhythm of a machine seam.'),
|
|
3366
|
+
gridSize: z.number().positive().optional().describe('apply_thread: crossStitch grid pitch, px (default 12) — the size of each X, and so the resolution of the whole fill.'),
|
|
3367
|
+
roughness: z.number().min(0).optional()
|
|
3368
|
+
.describe('apply_thread: hand wobble. Jitters every point of every stitch after the engine lays them, so the fill reads as sewn rather than plotted. 0 is machine-exact. Reproducible for a given seed. This one is applied by the tool, not the engine.'),
|
|
3369
|
+
// --- Hatching (pp: the p5.brush marks, as vector) ---
|
|
3370
|
+
//
|
|
3371
|
+
// `field` above is the THREAD direction field (radial/spine/constant) and is
|
|
3372
|
+
// a different thing from `flowField` here (hand/curved/waves/…). Separate
|
|
3373
|
+
// names because one steers stitches around a form and the other bends a
|
|
3374
|
+
// ruling line the way a hand would drift; sharing a key would silently apply
|
|
3375
|
+
// one where the other was meant.
|
|
3376
|
+
distance: z.number().positive().optional()
|
|
3377
|
+
.describe('apply_hatch: spacing between lines in px (default 6). THIS IS THE VALUE CONTROL — the same shape at 6px and 3px reads as light and dark with no change of colour.'),
|
|
3378
|
+
angle: z.number().optional().describe('apply_hatch: ruling angle in DEGREES (default 45).'),
|
|
3379
|
+
gradient: z.number().min(0).max(1).optional()
|
|
3380
|
+
.describe('apply_hatch: 0..1 spacing growth per line (default 0, even). Non-zero makes the density fall off across the shape — a shaded ramp rather than a flat tone.'),
|
|
3381
|
+
rand: z.number().min(0).max(1).optional()
|
|
3382
|
+
.describe('apply_hatch: jitter as a fraction of distance (default 0) — takes the mechanical evenness off.'),
|
|
3383
|
+
continuous: z.boolean().optional()
|
|
3384
|
+
.describe('apply_hatch: join the lines into ONE serpentine path (default false) — a pen that never leaves the paper, and a single item instead of many.'),
|
|
3385
|
+
flowField: z.enum(['hand', 'curved', 'zigzag', 'waves', 'seabed', 'spiral', 'columns']).optional()
|
|
3386
|
+
.describe('apply_hatch: bend each line along a flow field. The straight scanline is the ruling; the field is what makes it read as drawn rather than printed. See list_flow_fields.'),
|
|
3387
|
+
t: z.number().optional().describe('apply_hatch: flow-field time offset — advances the field without changing the seed.'),
|
|
3388
|
+
})
|
|
3389
|
+
.refine((v) => v.action !== 'apply_thread' || !!v.itemId, { message: 'apply_thread requires itemId', path: ['itemId'] })
|
|
3390
|
+
.refine((v) => v.action !== 'resolve' || !!v.medium, { message: 'resolve requires medium', path: ['medium'] })
|
|
3391
|
+
.refine((v) => v.action !== 'apply_hatch' || !!v.itemId, { message: 'apply_hatch requires itemId', path: ['itemId'] });
|
|
3392
|
+
export const TextEffectInputSchema = z.object({
|
|
3393
|
+
action: z.enum(['apply', 'list'])
|
|
3394
|
+
.describe("'apply' (explode a text item into animated characters) · 'list' (the 37 effects with key/label/definition, for a picker)"),
|
|
3395
|
+
itemId: z.string().optional().describe('apply: the text item to animate. Must be a text item with non-whitespace content.'),
|
|
3396
|
+
effect: z.string().optional().describe("apply: effect key from 'list' (e.g. scattered, decrypt, matrix, burn, fireworks, rain, blackhole)."),
|
|
3397
|
+
duration: z.number().positive().optional().describe('apply: seconds for the whole effect (default 2.5). Every effect ends AT REST, so this is the settle time.'),
|
|
3398
|
+
seed: z.number().int().optional().describe('apply: PRNG seed (default 1). Same seed + same text = identical animation, so exports are reproducible.'),
|
|
3399
|
+
gradient: z.boolean().optional().describe("apply: false keeps each character's authored fill. Default TRUE — the upstream effects all end by painting the block with a gradient, which is what their reference frames show."),
|
|
3400
|
+
gradientStops: z.array(z.string()).optional().describe('apply: hex colors overriding the effect\'s own gradient.'),
|
|
3401
|
+
gradientDirection: z.enum(['vertical', 'horizontal', 'radial', 'diagonal']).optional().describe('apply: gradient axis across the text block.'),
|
|
3402
|
+
gradientSteps: z.number().int().positive().optional().describe('apply: quantization steps in the gradient ramp.'),
|
|
3403
|
+
keepSource: z.boolean().optional().describe('apply: leave the original text item in place instead of removing it — the escape hatch when relations/keyframes reference its id.'),
|
|
3404
|
+
options: z.record(z.string(), z.unknown()).optional().describe('apply: per-effect knobs, passed through verbatim (each of the 37 has its own). Spread FIRST, so named fields above win on collision.'),
|
|
3405
|
+
})
|
|
3406
|
+
.refine((v) => v.action !== 'apply' || (!!v.itemId && !!v.effect), { message: 'apply requires itemId and effect', path: ['effect'] });
|
|
2763
3407
|
/**
|
|
2764
3408
|
* pinepaper_shatter_image — split a raster into a grid of per-tile rasters.
|
|
2765
3409
|
* Deliberately INERT on its own: the pieces sit exactly where the picture was
|
|
@@ -2839,8 +3483,19 @@ export const GameInputSchema = z.object({
|
|
|
2839
3483
|
* itself — the tool does not restate it, so it cannot drift.
|
|
2840
3484
|
*/
|
|
2841
3485
|
export const World3DInputSchema = z.object({
|
|
2842
|
-
action: z.enum([
|
|
2843
|
-
|
|
3486
|
+
action: z.enum([
|
|
3487
|
+
'create', 'describe', 'configure', 'add_actor', 'remove_actor', 'list_actors', 'set_actor_pose',
|
|
3488
|
+
'set_camera', 'add_object', 'remove_object', 'remove_world',
|
|
3489
|
+
'extrude_path', 'lathe_path', 'list_meshes', 'remove_mesh',
|
|
3490
|
+
'add_light', 'set_light', 'remove_light', 'list_lights',
|
|
3491
|
+
'add_material', 'set_material', 'remove_material', 'list_materials',
|
|
3492
|
+
'import_obj', 'import_gltf', 'list_mesh_clips', 'set_mesh_clip',
|
|
3493
|
+
'set_nav_target', 'get_nav_target', 'ground_height', 'raycast',
|
|
3494
|
+
'dolly_camera', 'pan_camera', 'world_to_canvas', 'canvas_to_ground',
|
|
3495
|
+
'add_body', 'remove_body', 'list_bodies', 'step_physics', 'impulse', 'set_velocity', 'fire_projectile',
|
|
3496
|
+
'line_of_sight', 'set_mesh_instances',
|
|
3497
|
+
])
|
|
3498
|
+
.describe("'create' (a preset world) · 'describe' (every parameter with type/range — CALL THIS before configure) · 'configure' (live deep-merge patch, schema-validated) · actor ops · 'set_camera' (follow|fixed|orbit) · object ops · 'remove_world' · MESH AUTHORING: 'extrude_path' / 'lathe_path' turn a canvas path into real geometry, 'list_meshes', 'remove_mesh' · 'add_light' / 'set_light' / 'remove_light' / 'list_lights' (at most 8 point lights) · 'add_material' / 'set_material' / 'remove_material' / 'list_materials'"),
|
|
2844
3499
|
spec: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe("create: a preset id ('forest', 'snowMountain', …) or a full world spec object."),
|
|
2845
3500
|
character: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional().describe('create: include the walkable character (true/config).'),
|
|
2846
3501
|
patch: z.record(z.string(), z.unknown()).optional().describe('configure: partial world spec, deep-merged and validated — a wrong key errors naming the right one.'),
|
|
@@ -2852,18 +3507,372 @@ export const World3DInputSchema = z.object({
|
|
|
2852
3507
|
live: z.boolean().optional().describe('add_actor: re-rasterize the item as it animates — a rigged character PERFORMS in the world instead of standing there as a photograph of itself.'),
|
|
2853
3508
|
pose: z.record(z.string(), z.unknown()).optional().describe('set_actor_pose: { x?, z?, angle?, … } — the setter a timeline or agent drives.'),
|
|
2854
3509
|
camera: z.record(z.string(), z.unknown()).optional().describe("set_camera: { mode: 'follow'|'fixed'|'orbit', target?, radius?, speed?, eye?, lookAt? }."),
|
|
2855
|
-
object: z.record(z.string(), z.unknown()).optional().describe(
|
|
3510
|
+
object: z.record(z.string(), z.unknown()).optional().describe("add_object: { x, z, height?, color?, y? } (defaults to sitting on the terrain). metalness and roughness are ACCEPTED AND IGNORED here: a plain object is drawn by the prop shader, which has no such uniform, so they are stored and listed and never reach a pixel. They render on the MESH path only — 'extrude_path' / 'lathe_path' and imported geometry. Use a material there instead of setting them here and seeing nothing."),
|
|
2856
3511
|
objectId: z.string().optional().describe('remove_object: the object id.'),
|
|
3512
|
+
// --- Mesh authoring: a canvas path becomes real geometry -----------------
|
|
3513
|
+
pathId: z.string().optional().describe("extrude_path / lathe_path: the canvas path to turn into geometry. Extrude sweeps it along depth; lathe revolves its profile around the Y axis. The path is unchanged — the mesh is new."),
|
|
3514
|
+
mesh: z.object({
|
|
3515
|
+
id: z.string().optional().describe('Mesh id (generated if omitted).'),
|
|
3516
|
+
depth: z.number().optional().describe('extrude_path: how far to sweep, in world units.'),
|
|
3517
|
+
unitsPerPixel: z.number().optional().describe('Canvas pixels → world units. The whole scale relationship between the drawing and the world.'),
|
|
3518
|
+
caps: z.boolean().optional().describe('extrude_path: close the two ends. A capped extrude is a solid; an uncapped one is a ribbon.'),
|
|
3519
|
+
segments: z.number().optional().describe('extrude_path: segments along the sweep · lathe_path: segments around the revolution.'),
|
|
3520
|
+
arc: z.number().optional().describe('lathe_path: how far round to revolve, in degrees. 360 is a full solid of revolution; less leaves it open.'),
|
|
3521
|
+
flatness: z.number().optional().describe('Curve flattening tolerance when the path is sampled into rings — smaller is finer and heavier.'),
|
|
3522
|
+
x: z.number().optional().describe('Placement in world space.'),
|
|
3523
|
+
y: z.number().optional().describe('Placement in world space (omit to let it sit where the terrain puts it).'),
|
|
3524
|
+
z: z.number().optional().describe('Placement in world space.'),
|
|
3525
|
+
rotY: z.number().optional().describe('Rotation about the Y axis, in RADIANS — the value is stored raw and fed straight to cos()/sin() in the shader, with no conversion anywhere on the path. This is an exception to the degrees convention every other angle in this surface follows; pass rotYDegrees instead if you would rather write degrees. (`arc` on lathe_path IS degrees — the engine converts that one.)'),
|
|
3526
|
+
rotYDegrees: z.number().optional().describe('Rotation about the Y axis in degrees, converted to radians for you. Convenience only — the engine and the mesh provenance record both speak rotY in radians, so a round-tripped capture uses rotY.'),
|
|
3527
|
+
scale: z.number().optional().describe('Uniform scale applied after the geometry is built.'),
|
|
3528
|
+
doubleSided: z.boolean().optional().describe('Draw back faces too. An uncapped extrude usually wants this.'),
|
|
3529
|
+
}).optional().describe("extrude_path / lathe_path options. These field names ARE the ones the engine records as the mesh's provenance, so a mesh captured in a design graph can be rebuilt by spreading its meshProvenance.opts straight back in here."),
|
|
3530
|
+
meshId: z.string().optional().describe('remove_mesh: the mesh id.'),
|
|
3531
|
+
// --- Lights ---------------------------------------------------------------
|
|
3532
|
+
light: z.object({
|
|
3533
|
+
id: z.string().optional().describe('Light id (generated if omitted).'),
|
|
3534
|
+
x: z.number().optional().describe('World-space x (default 0).'),
|
|
3535
|
+
y: z.number().optional().describe('World-space y (default 3 — above the ground, not on it).'),
|
|
3536
|
+
z: z.number().optional().describe('World-space z (default 0).'),
|
|
3537
|
+
color: z.array(z.number()).length(3).optional().describe('Linear RGB, each component ≥ 0 (default [1, 0.86, 0.62], a warm lamp).'),
|
|
3538
|
+
intensity: z.number().optional().describe('Brightness (default 6).'),
|
|
3539
|
+
range: z.number().optional().describe('How far the light reaches, in world units (default 20).'),
|
|
3540
|
+
}).optional().describe('add_light / set_light: the light spec. At most 8 point lights exist at once — the shader array is fixed-size, so a ninth is refused by name rather than ignored.'),
|
|
3541
|
+
lightId: z.string().optional().describe('set_light / remove_light: the light id.'),
|
|
3542
|
+
// --- Materials ------------------------------------------------------------
|
|
3543
|
+
material: z.object({
|
|
3544
|
+
id: z.string().optional().describe('Material id (generated if omitted).'),
|
|
3545
|
+
color: z.array(z.number()).length(3).optional().describe('Base colour, linear RGB (default [0.8, 0.8, 0.82]).'),
|
|
3546
|
+
emissive: z.array(z.number()).length(3).optional().describe('Light the surface emits regardless of the scene (default [0, 0, 0]).'),
|
|
3547
|
+
metalness: z.number().min(0).max(1).optional().describe('0 dielectric → 1 metal.'),
|
|
3548
|
+
roughness: z.number().min(0).max(1).optional().describe('0 mirror → 1 fully diffuse. Floored at 0.045: a true zero is a numerically unstable mirror.'),
|
|
3549
|
+
emissiveIntensity: z.number().min(0).optional().describe('Scales `emissive` before the frame is built, so it reaches every path that draws emissive.'),
|
|
3550
|
+
clearcoat: z.number().min(0).max(1).optional().describe('A lacquer layer over the base — car paint, varnished wood.'),
|
|
3551
|
+
clearcoatRoughness: z.number().min(0).max(1).optional().describe("The coat's own roughness. NOT floored like the base, so a mirror-smooth lacquer stays expressible."),
|
|
3552
|
+
sheenColor: z.array(z.number()).length(3).optional().describe('Retroreflective sheen colour — cloth, velvet.'),
|
|
3553
|
+
sheenRoughness: z.number().min(0).max(1).optional().describe('How broad the sheen lobe is.'),
|
|
3554
|
+
}).optional().describe("add_material / set_material: a NAMED, SHARED surface referenced by many objects, so one edit restyles all of them. metalness and roughness reach the shader on the MESH path only. There are no aoMapIntensity / normalScale / envMapIntensity knobs: each scales a map that does not exist, and a knob that scales nothing is worse than a missing one."),
|
|
3555
|
+
materialId: z.string().optional().describe('set_material / remove_material: the material id.'),
|
|
3556
|
+
// --- Import ---------------------------------------------------------------
|
|
3557
|
+
source: z.string().optional().describe('import_obj: the OBJ file TEXT. import_gltf: a URL, or a data: URI of the .glb bytes.'),
|
|
3558
|
+
importOptions: z.record(z.string(), z.unknown()).optional().describe('import_obj / import_gltf: placement and scale options, passed through to the engine.'),
|
|
3559
|
+
clip: z.string().optional().describe('set_mesh_clip: which imported animation clip a skinned mesh plays.'),
|
|
3560
|
+
crossfade: z.number().optional().describe('set_mesh_clip: seconds to blend out of the clip that was playing. The blend happens in LOCAL pose space — a palette cannot be blended after the fact.'),
|
|
3561
|
+
// --- Navigation, picking and the camera ----------------------------------
|
|
3562
|
+
navTarget: z.enum(['2d', '3d']).optional().describe("set_nav_target: which layer the pointer drives — '2d' the Paper canvas, '3d' the world."),
|
|
3563
|
+
point: z.object({ x: z.number(), y: z.number(), z: z.number().optional() }).optional()
|
|
3564
|
+
.describe('ground_height / canvas_to_ground / world_to_canvas: the point to convert. ground_height and canvas_to_ground read x and y (canvas) or x and z (world); world_to_canvas takes a world point.'),
|
|
3565
|
+
origin: z.array(z.number()).length(3).optional().describe('raycast: ray origin in world space, [x, y, z].'),
|
|
3566
|
+
direction: z.array(z.number()).length(3).optional().describe('raycast: ray direction in world space, [x, y, z]. Need not be normalised.'),
|
|
3567
|
+
multiplier: z.number().optional().describe('dolly_camera: greater than 1 moves away, less than 1 moves closer.'),
|
|
3568
|
+
dx: z.number().optional().describe('pan_camera: horizontal pan in SCREEN pixels.'),
|
|
3569
|
+
dy: z.number().optional().describe('pan_camera: vertical pan in SCREEN pixels.'),
|
|
3570
|
+
// --- Physics in the world -------------------------------------------------
|
|
3571
|
+
body: z.record(z.string(), z.unknown()).optional()
|
|
3572
|
+
.describe('add_body / fire_projectile: the body spec. Deliberately NARROW — a sphere or a capsule under gravity, colliding with the world (terrain, objects, actors, imported meshes). There is no stacking solver and no joints; for those use pinepaper_physics, which is the 2D world.'),
|
|
3573
|
+
bodyId: z.string().optional().describe('remove_body / impulse / set_velocity: the body id.'),
|
|
3574
|
+
vector: z.array(z.number()).length(3).optional().describe('impulse: the impulse to apply, [x, y, z]. set_velocity: the velocity to set, [x, y, z].'),
|
|
3575
|
+
dt: z.number().optional().describe('step_physics: seconds to advance. Returns the contacts made and the bodies removed in that step.'),
|
|
3576
|
+
from: z.array(z.number()).length(3).optional().describe('line_of_sight: the eye point, [x, y, z].'),
|
|
3577
|
+
to: z.array(z.number()).length(3).optional().describe('line_of_sight: the target point, [x, y, z].'),
|
|
3578
|
+
instances: z.array(z.array(z.number())).optional()
|
|
3579
|
+
.describe('set_mesh_instances: one entry per copy, six numbers each — x, y, z, scale, rotY (RADIANS), and a free variant a shader may read for per-instance colour or phase.'),
|
|
2857
3580
|
})
|
|
2858
3581
|
.refine((v) => v.action !== 'configure' || !!v.patch, { message: 'configure requires patch', path: ['patch'] })
|
|
2859
3582
|
.refine((v) => !['remove_actor', 'set_actor_pose'].includes(v.action) || !!v.actorId, { message: 'this action requires actorId', path: ['actorId'] })
|
|
2860
3583
|
.refine((v) => v.action !== 'set_actor_pose' || !!v.pose, { message: 'set_actor_pose requires pose', path: ['pose'] })
|
|
2861
3584
|
.refine((v) => v.action !== 'set_camera' || !!v.camera, { message: 'set_camera requires camera', path: ['camera'] })
|
|
2862
3585
|
.refine((v) => v.action !== 'add_object' || !!v.object, { message: 'add_object requires object', path: ['object'] })
|
|
2863
|
-
.refine((v) => v.action !== 'remove_object' || !!v.objectId, { message: 'remove_object requires objectId', path: ['objectId'] })
|
|
3586
|
+
.refine((v) => v.action !== 'remove_object' || !!v.objectId, { message: 'remove_object requires objectId', path: ['objectId'] })
|
|
3587
|
+
.refine((v) => !['extrude_path', 'lathe_path'].includes(v.action) || !!v.pathId, { message: 'this action requires pathId — the canvas path to turn into geometry', path: ['pathId'] })
|
|
3588
|
+
.refine((v) => v.action !== 'remove_mesh' || !!v.meshId, { message: 'remove_mesh requires meshId', path: ['meshId'] })
|
|
3589
|
+
.refine((v) => v.action !== 'add_light' || !!v.light, { message: 'add_light requires light', path: ['light'] })
|
|
3590
|
+
.refine((v) => v.action !== 'set_light' || (!!v.lightId && !!v.light), { message: 'set_light requires lightId and light', path: ['lightId'] })
|
|
3591
|
+
.refine((v) => v.action !== 'remove_light' || !!v.lightId, { message: 'remove_light requires lightId', path: ['lightId'] })
|
|
3592
|
+
.refine((v) => v.action !== 'add_material' || !!v.material, { message: 'add_material requires material', path: ['material'] })
|
|
3593
|
+
.refine((v) => v.action !== 'set_material' || (!!v.materialId && !!v.material), { message: 'set_material requires materialId and material', path: ['materialId'] })
|
|
3594
|
+
.refine((v) => v.action !== 'remove_material' || !!v.materialId, { message: 'remove_material requires materialId', path: ['materialId'] })
|
|
3595
|
+
.refine((v) => !['import_obj', 'import_gltf'].includes(v.action) || !!v.source, { message: 'this action requires source', path: ['source'] })
|
|
3596
|
+
.refine((v) => !['list_mesh_clips', 'set_mesh_clip'].includes(v.action) || !!v.meshId, { message: 'this action requires meshId', path: ['meshId'] })
|
|
3597
|
+
.refine((v) => v.action !== 'set_mesh_clip' || !!v.clip, { message: 'set_mesh_clip requires clip', path: ['clip'] })
|
|
3598
|
+
.refine((v) => v.action !== 'set_nav_target' || !!v.navTarget, { message: 'set_nav_target requires navTarget', path: ['navTarget'] })
|
|
3599
|
+
.refine((v) => !['ground_height', 'canvas_to_ground', 'world_to_canvas'].includes(v.action) || !!v.point, { message: 'this action requires point', path: ['point'] })
|
|
3600
|
+
.refine((v) => v.action !== 'raycast' || (!!v.origin && !!v.direction), { message: 'raycast requires origin and direction', path: ['origin'] })
|
|
3601
|
+
.refine((v) => v.action !== 'dolly_camera' || v.multiplier !== undefined, { message: 'dolly_camera requires multiplier', path: ['multiplier'] })
|
|
3602
|
+
.refine((v) => v.action !== 'pan_camera' || (v.dx !== undefined && v.dy !== undefined), { message: 'pan_camera requires dx and dy', path: ['dx'] })
|
|
3603
|
+
.refine((v) => !['add_body', 'fire_projectile'].includes(v.action) || !!v.body, { message: 'this action requires body', path: ['body'] })
|
|
3604
|
+
.refine((v) => !['remove_body', 'impulse', 'set_velocity'].includes(v.action) || !!v.bodyId, { message: 'this action requires bodyId', path: ['bodyId'] })
|
|
3605
|
+
.refine((v) => !['impulse', 'set_velocity'].includes(v.action) || !!v.vector, { message: 'this action requires vector', path: ['vector'] })
|
|
3606
|
+
.refine((v) => v.action !== 'step_physics' || v.dt !== undefined, { message: 'step_physics requires dt', path: ['dt'] })
|
|
3607
|
+
.refine((v) => v.action !== 'line_of_sight' || (!!v.from && !!v.to), { message: 'line_of_sight requires from and to', path: ['from'] })
|
|
3608
|
+
.refine((v) => v.action !== 'set_mesh_instances' || (!!v.meshId && !!v.instances), { message: 'set_mesh_instances requires meshId and instances', path: ['meshId'] });
|
|
2864
3609
|
// =============================================================================
|
|
2865
3610
|
// ONE-SHOT IMAGE OPS (crop / chroma key — Track A agent parity, 2026-07-31)
|
|
2866
3611
|
// =============================================================================
|
|
3612
|
+
/**
|
|
3613
|
+
* pinepaper_design_system — the design vocabulary, as data an agent can read.
|
|
3614
|
+
*
|
|
3615
|
+
* TWO DIFFERENT THINGS, deliberately not merged, because the upstream is
|
|
3616
|
+
* explicit that they are not the same:
|
|
3617
|
+
*
|
|
3618
|
+
* - A DESIGN SYSTEM is a real, licensed, versioned product — Material 3,
|
|
3619
|
+
* IBM Carbon, Polaris, Fluent 2, USWDS, GOV.UK. Its tokens are facts with a
|
|
3620
|
+
* licence attached, expressed in the W3C DTCG format ($value, $type,
|
|
3621
|
+
* $description). Where a motion token had to be filled in because the
|
|
3622
|
+
* published system does not specify one, it is marked
|
|
3623
|
+
* `pinepaper:authored` — so a caller can tell a vendor's curve from ours.
|
|
3624
|
+
* - An AESTHETIC STYLE is a look: Bauhaus, Art Deco, Swiss, vaporwave,
|
|
3625
|
+
* risograph. It carries no licence and no vendor. Eighteen of them can
|
|
3626
|
+
* COMPOSE a scene rather than only describe one.
|
|
3627
|
+
*
|
|
3628
|
+
* These run in this server, not in the browser. The generators are pure
|
|
3629
|
+
* functions over numbers, so the scene is computed here and arrives as items;
|
|
3630
|
+
* nothing about a design vocabulary needs the canvas to be open to decide it.
|
|
3631
|
+
*/
|
|
3632
|
+
export const DesignSystemInputSchema = z.object({
|
|
3633
|
+
action: z.enum(['list_systems', 'get_system', 'list_easings', 'list_styles', 'compose'])
|
|
3634
|
+
.describe("'list_systems' every licensed design system with its vendor, licence and version · 'get_system' one system's full DTCG tokens · 'list_easings' every motion curve across all of them as named easings, with provenance · 'list_styles' the aesthetic styles and which can compose · 'compose' build a scene in a style."),
|
|
3635
|
+
systemId: z.string().optional().describe("get_system: which system, e.g. 'material_3', 'ibm_carbon', 'uswds'. Call list_systems for the ids."),
|
|
3636
|
+
tokenType: z.enum(['dimension', 'duration', 'cubicBezier', 'color', 'fontFamily', 'fontWeight', 'number', 'shadow', 'grid']).optional()
|
|
3637
|
+
.describe('get_system: return only tokens of this DTCG type.'),
|
|
3638
|
+
style: z.string().optional().describe("compose: the aesthetic style, e.g. 'bauhaus_geometric', 'art_deco_geometric', 'vaporwave_retro'. Call list_styles for the ones that can compose."),
|
|
3639
|
+
title: z.string().optional().describe('compose: the headline. Most styles are poster-shaped and build around it.'),
|
|
3640
|
+
subtitle: z.string().optional().describe('compose: the supporting line.'),
|
|
3641
|
+
body: z.string().optional().describe('compose: optional body copy, where the style has somewhere to put it.'),
|
|
3642
|
+
width: z.number().optional().describe('compose: canvas width (the style picks its own default otherwise).'),
|
|
3643
|
+
height: z.number().optional().describe('compose: canvas height.'),
|
|
3644
|
+
variant: z.string().optional().describe("compose: a style-specific variant where one exists, e.g. art deco's emerald colourway."),
|
|
3645
|
+
draw: z.boolean().optional().default(true)
|
|
3646
|
+
.describe('compose: draw the scene on the canvas (default). false returns the computed scene as DATA and draws nothing — the same distinction story\'s `distill` makes, and for the same reason: a caller should be able to read a composition before committing to it.'),
|
|
3647
|
+
authoredOnly: z.boolean().optional().describe('list_easings: return only the curves PinePaper authored to fill a gap, or only the ones a vendor actually publishes, rather than both mixed together.'),
|
|
3648
|
+
})
|
|
3649
|
+
.refine((v) => v.action !== 'get_system' || !!v.systemId, { message: 'get_system requires systemId — call list_systems for the ids', path: ['systemId'] })
|
|
3650
|
+
.refine((v) => v.action !== 'compose' || !!v.style, { message: 'compose requires style — call list_styles for the ones that can compose', path: ['style'] })
|
|
3651
|
+
.refine((v) => v.action !== 'compose' || !!v.title, { message: 'compose requires title', path: ['title'] });
|
|
3652
|
+
/**
|
|
3653
|
+
* pinepaper_stick — the vendored stick-figure kit.
|
|
3654
|
+
*
|
|
3655
|
+
* A different construction from pinepaper_character, which places a figure
|
|
3656
|
+
* from the design graph by concept. This is the stick kit: a rigged figure
|
|
3657
|
+
* posed, walking, travelling, holding a prop, with a garment and hair — plus
|
|
3658
|
+
* `set`, which builds the furniture it stands on and among.
|
|
3659
|
+
*
|
|
3660
|
+
* The geometry is vendored from mcp-cloud, so this is a three-repo artifact:
|
|
3661
|
+
* a change to the kit lands there first and is re-vendored into FxTool.
|
|
3662
|
+
*/
|
|
3663
|
+
export const StickInputSchema = z.object({
|
|
3664
|
+
action: z.enum(['figure', 'set']).describe("'figure' a posed, walking or travelling stick figure · 'set' the floor, wall and objects it stands on and among."),
|
|
3665
|
+
id: z.string().optional().describe('Prefix for the created items.'),
|
|
3666
|
+
kind: z.string().optional().describe("figure: which figure. set: which piece of the scene kit ('floor', 'wall', a named object) — list-free, because the kit is vendored and grows there."),
|
|
3667
|
+
at: z.object({ x: z.number().optional(), y: z.number().optional() }).optional().describe('Where it goes.'),
|
|
3668
|
+
scale: z.number().optional().describe('Size multiplier.'),
|
|
3669
|
+
facing: z.enum(['left', 'right']).optional().describe('Which way the figure looks.'),
|
|
3670
|
+
pose: z.string().optional().describe("figure: a named pose from the kit's POSES."),
|
|
3671
|
+
poseAt: z.number().optional().describe('figure: the time, in seconds, at which that pose is struck.'),
|
|
3672
|
+
expression: z.string().optional().describe("figure: a single facial expression from the kit's EXPRESSIONS."),
|
|
3673
|
+
expressions: z.array(z.object({ at: z.number(), name: z.string() })).optional().describe('figure: expressions over time, each at a moment.'),
|
|
3674
|
+
walk: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional().describe('figure: a walk cycle — true for the default, or a config.'),
|
|
3675
|
+
travel: z.record(z.string(), z.unknown()).optional().describe('figure: move the figure across the scene while it walks, rather than walking in place.'),
|
|
3676
|
+
prop: z.string().optional().describe('figure: something held in hand.'),
|
|
3677
|
+
propSide: z.enum(['left', 'right']).optional().describe('figure: which hand holds it.'),
|
|
3678
|
+
garment: z.string().optional().describe('figure: clothing.'),
|
|
3679
|
+
trouser: z.string().optional().describe('figure: legwear.'),
|
|
3680
|
+
hair: z.string().optional().describe('figure: hair style.'),
|
|
3681
|
+
withHair: z.boolean().optional().describe('figure: draw hair at all.'),
|
|
3682
|
+
groundY: z.number().optional().describe('The y the figure stands on.'),
|
|
3683
|
+
surfaceY: z.number().optional().describe('The y of the surface an object sits on.'),
|
|
3684
|
+
durationSeconds: z.number().optional().describe('figure: length of the performance, so every track spans it.'),
|
|
3685
|
+
object: z.record(z.string(), z.unknown()).optional().describe('set: the object spec.'),
|
|
3686
|
+
items: z.array(z.record(z.string(), z.unknown())).optional().describe('set: several pieces at once.'),
|
|
3687
|
+
floor: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional().describe('set: draw a floor.'),
|
|
3688
|
+
wall: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional().describe('set: draw a wall.'),
|
|
3689
|
+
});
|
|
3690
|
+
/**
|
|
3691
|
+
* pinepaper_story — a piece of prose becomes a scene.
|
|
3692
|
+
*
|
|
3693
|
+
* `distill` reduces an article to its beats WITHOUT drawing anything, which is
|
|
3694
|
+
* the half worth having on its own: a caller can read what the distiller made
|
|
3695
|
+
* of the text, edit it, and only then assemble. `from_text` does both in one
|
|
3696
|
+
* call, `apply_spec` assembles a spec you already have, and `plan_book` lays
|
|
3697
|
+
* images out as pages.
|
|
3698
|
+
*/
|
|
3699
|
+
export const StoryInputSchema = z.object({
|
|
3700
|
+
action: z.enum(['distill', 'from_text', 'apply_spec', 'plan_book'])
|
|
3701
|
+
.describe("'distill' text → story beats, drawing NOTHING · 'from_text' distill and assemble in one call · 'apply_spec' assemble a spec you already have · 'plan_book' lay images out as pages."),
|
|
3702
|
+
text: z.string().optional().describe('distill / from_text: the prose.'),
|
|
3703
|
+
spec: z.record(z.string(), z.unknown()).optional().describe('apply_spec: the story spec — usually one from `distill`, edited.'),
|
|
3704
|
+
images: z.array(z.union([z.string(), z.record(z.string(), z.unknown())])).optional().describe('plan_book: the images to lay out, as ids or specs.'),
|
|
3705
|
+
options: z.record(z.string(), z.unknown()).optional().describe('Assembly or layout options, passed through.'),
|
|
3706
|
+
})
|
|
3707
|
+
.refine((v) => !['distill', 'from_text'].includes(v.action) || !!v.text, { message: 'this action requires text', path: ['text'] })
|
|
3708
|
+
.refine((v) => v.action !== 'apply_spec' || !!v.spec, { message: 'apply_spec requires spec', path: ['spec'] })
|
|
3709
|
+
.refine((v) => v.action !== 'plan_book' || (Array.isArray(v.images) && v.images.length > 0), { message: 'plan_book requires images', path: ['images'] });
|
|
3710
|
+
/**
|
|
3711
|
+
* pinepaper_interchange — the formats other tools read and write.
|
|
3712
|
+
*
|
|
3713
|
+
* pinepaper_agent_export covers the PLATFORM formats: png, svg, mp4, webm,
|
|
3714
|
+
* gif, pdf, sized for Instagram or YouTube. These are the interchange ones —
|
|
3715
|
+
* a Lottie an app plays, a GLB a 3D tool opens, a BVH a rig imports, a PNG
|
|
3716
|
+
* sequence an editor ingests — and they had no tool at all.
|
|
3717
|
+
*
|
|
3718
|
+
* Import is here too, because a format you can only write is half a bridge.
|
|
3719
|
+
*/
|
|
3720
|
+
export const InterchangeInputSchema = z.object({
|
|
3721
|
+
action: z.enum(['export_lottie', 'export_dotlottie', 'import_lottie', 'export_glb', 'export_bvh', 'export_png_sequence'])
|
|
3722
|
+
.describe("'export_lottie' (JSON) · 'export_dotlottie' (the zipped .lottie) · 'import_lottie' (a Lottie or .lottie back onto the canvas) · 'export_glb' (the perspective 3D objects — needs some to exist) · 'export_bvh' (a skeleton's motion — needs a rig) · 'export_png_sequence' (one file per frame)."),
|
|
3723
|
+
options: z.record(z.string(), z.unknown()).optional().describe('Format options, passed through to the exporter: frame range, dimensions, fps, naming.'),
|
|
3724
|
+
data: z.union([z.string(), z.record(z.string(), z.unknown())]).optional()
|
|
3725
|
+
.describe('import_lottie: the Lottie JSON — an object, a JSON string, or a URL.'),
|
|
3726
|
+
skeletonId: z.string().optional().describe('export_bvh: which skeleton to write. A BVH is one skeleton\'s motion, so this is not optional in practice.'),
|
|
3727
|
+
})
|
|
3728
|
+
.refine((v) => v.action !== 'import_lottie' || v.data !== undefined, { message: 'import_lottie requires data', path: ['data'] })
|
|
3729
|
+
.refine((v) => v.action !== 'export_bvh' || !!v.skeletonId, { message: 'export_bvh requires skeletonId — a BVH is one skeleton\'s motion', path: ['skeletonId'] });
|
|
3730
|
+
/**
|
|
3731
|
+
* pinepaper_sound — synthesis, and the drawing that is the same object.
|
|
3732
|
+
*
|
|
3733
|
+
* The engine carries a whole audio graph: tones, chords, named percussion and
|
|
3734
|
+
* SFX, plain-language sound descriptions, and — the part worth the tool — a
|
|
3735
|
+
* two-way bridge between a sound and a Paper path. `create` draws a sound as a
|
|
3736
|
+
* waveform path you can then edit; `timbre_from_path` reads any drawn path
|
|
3737
|
+
* back as a timbre. A curve someone drew becomes the harmonic content of a
|
|
3738
|
+
* note.
|
|
3739
|
+
*
|
|
3740
|
+
* None of it was reachable. `pinepaper_audio_beats` analyses audio that
|
|
3741
|
+
* already exists; this makes audio.
|
|
3742
|
+
*/
|
|
3743
|
+
export const SoundInputSchema = z.object({
|
|
3744
|
+
action: z.enum([
|
|
3745
|
+
'list_instruments', 'list_percussion', 'list_sfx',
|
|
3746
|
+
'play_tone', 'play_chord', 'chord_frequencies', 'play_percussion', 'play_sfx',
|
|
3747
|
+
'play_spec', 'from_text', 'play_from_text',
|
|
3748
|
+
'create', 'timbre_from_path', 'set_placement', 'remove', 'stop_all',
|
|
3749
|
+
'define_instrument', 'define_percussion', 'define_sfx', 'render_soundtrack',
|
|
3750
|
+
]).describe("Catalogues: 'list_instruments' · 'list_percussion' · 'list_sfx'. Play: 'play_tone' · 'play_chord' · 'play_percussion' · 'play_sfx' · 'play_spec' · 'play_from_text'. Read without playing: 'chord_frequencies' · 'from_text' (a plain-language description resolved to a spec) · 'timbre_from_path'. Canvas: 'create' (a sound drawn AS a waveform path) · 'set_placement' · 'remove' · 'stop_all'. Define: 'define_instrument' · 'define_percussion' · 'define_sfx' — the six built-in instruments are a starting set, not a claim that music contains six; anything missing can be defined here and is then first-class, listed by the list_* actions and playable by name. Render: 'render_soundtrack' — mix every placed sound to a WAV file offline, with no Web Audio and no playback."),
|
|
3751
|
+
note: z.string().optional().describe("play_tone: scientific pitch, e.g. 'A4' or 'C#3'."),
|
|
3752
|
+
root: z.string().optional().describe("play_chord / chord_frequencies: the root note, e.g. 'C4'."),
|
|
3753
|
+
chord: z.string().optional().default('major').describe("play_chord / chord_frequencies: the chord kind, e.g. 'major', 'minor', 'maj7', 'dim'. Call the engine rather than guessing at exotic names."),
|
|
3754
|
+
name: z.string().optional().describe("play_percussion / play_sfx: the named drum or effect — list them first (SFX include beep, pop, wind, whoosh, zap). define_*: the name to register under. It is NORMALISED to [a-z][a-z0-9_-]{0,31}, so 'Rhodes' registers as 'rhodes' — the response returns the canonical name the engine chose, and that is the name to play."),
|
|
3755
|
+
partials: z.array(z.object({
|
|
3756
|
+
h: z.number().positive().describe('Harmonic number — 1 is the fundamental, 2 the octave above.'),
|
|
3757
|
+
amp: z.number().min(0).describe('Relative amplitude of this partial.'),
|
|
3758
|
+
})).optional().describe("define_*: the harmonic table that IS the timbre. Required for define_instrument — a melodic instrument with no partials renders pure silence, and the engine refuses rather than handing back a note nobody can hear. Percussion and SFX may use noise instead."),
|
|
3759
|
+
envelope: z.object({
|
|
3760
|
+
attack: z.number().min(0).optional(),
|
|
3761
|
+
decay: z.number().min(0).optional(),
|
|
3762
|
+
sustain: z.number().min(0).max(1).optional(),
|
|
3763
|
+
release: z.number().min(0).optional(),
|
|
3764
|
+
}).optional().describe('define_*: ADSR shape, in seconds except sustain which is a level (0-1).'),
|
|
3765
|
+
gain: z.number().min(0).max(1).optional().describe('define_*: output level. Defaults differ by kind — 0.8 instrument, 0.85 percussion, 0.65 sfx.'),
|
|
3766
|
+
aliases: z.array(z.string()).optional().describe("define_*: extra names reaching the same sound, so 'rhodes' can also answer to 'electric piano'. An alias NEVER shadows a real catalogue entry — the table is consulted first — so aliasing an existing name is silently ignored rather than overriding it."),
|
|
3767
|
+
noise: z.number().min(0).max(1).optional().describe('define_percussion / define_sfx: noise content. A hat, a clap, wind and whoosh are pure noise, so these two kinds accept a partial table OR noise > 0; with neither the engine refuses, because the result is silent.'),
|
|
3768
|
+
noiseFreq: z.number().positive().optional().describe('define_percussion / define_sfx: centre frequency of the noise band, in Hz.'),
|
|
3769
|
+
hz: z.number().positive().optional().describe('define_percussion / define_sfx: base frequency in Hz.'),
|
|
3770
|
+
pitch: z.object({
|
|
3771
|
+
from: z.number().positive().describe('Starting frequency in Hz.'),
|
|
3772
|
+
tau: z.number().positive().max(5).describe('Glide time constant in seconds (max 5).'),
|
|
3773
|
+
}).optional().describe("define_percussion / define_sfx: the pitch glide, which is what makes a kick a kick — the engine's own note is that the glide IS the drum. Omit it and a defined kick comes out a beep."),
|
|
3774
|
+
sampleRate: z.number().int().positive().optional().describe('render_soundtrack: samples per second (default 48000).'),
|
|
3775
|
+
bitDepth: z.union([z.literal(16), z.literal(32)]).optional().describe('render_soundtrack: 16 (default) or 32-bit float.'),
|
|
3776
|
+
text: z.string().optional().describe("from_text / play_from_text: a plain-language description, e.g. 'a soft warm bell on A4'. 'from_text' resolves it to a spec WITHOUT playing, so a caller can inspect or edit before committing."),
|
|
3777
|
+
spec: z.record(z.string(), z.unknown()).optional().describe('play_spec / create: the sound spec — partials, envelope, duration. Usually one you got from from_text or a catalogue entry rather than wrote by hand.'),
|
|
3778
|
+
options: z.record(z.string(), z.unknown()).optional().describe('Playback options for the play_* actions: duration, gain, pan, and the rest the audio graph accepts.'),
|
|
3779
|
+
itemId: z.string().optional().describe('timbre_from_path: the path whose SHAPE becomes the harmonic content. set_placement / remove: the sound item.'),
|
|
3780
|
+
samples: z.number().int().positive().optional().describe('timbre_from_path: how many points to sample along the path (default 256). More is a finer timbre and a slower read.'),
|
|
3781
|
+
placement: z.record(z.string(), z.unknown()).optional().describe('set_placement: where and when the sound sits on the timeline.'),
|
|
3782
|
+
visual: z.object({
|
|
3783
|
+
width: z.number().optional().describe('Waveform path width in canvas units (default 220).'),
|
|
3784
|
+
height: z.number().optional().describe('Waveform path height in canvas units (default 64).'),
|
|
3785
|
+
position: z.object({ x: z.number(), y: z.number() }).optional(),
|
|
3786
|
+
color: z.string().optional(),
|
|
3787
|
+
}).optional().describe('create: how the waveform path is drawn. The sound IS this item — edit the path and the timbre changes with it.'),
|
|
3788
|
+
})
|
|
3789
|
+
.refine((v) => v.action !== 'play_tone' || !!v.note, { message: 'play_tone requires note', path: ['note'] })
|
|
3790
|
+
.refine((v) => !['play_chord', 'chord_frequencies'].includes(v.action) || !!v.root, { message: 'this action requires root', path: ['root'] })
|
|
3791
|
+
.refine((v) => !['play_percussion', 'play_sfx'].includes(v.action) || !!v.name, { message: 'this action requires name — list the catalogue first', path: ['name'] })
|
|
3792
|
+
.refine((v) => !['from_text', 'play_from_text'].includes(v.action) || !!v.text, { message: 'this action requires text', path: ['text'] })
|
|
3793
|
+
.refine((v) => v.action !== 'play_spec' || !!v.spec, { message: 'play_spec requires spec', path: ['spec'] })
|
|
3794
|
+
.refine((v) => !['timbre_from_path', 'set_placement', 'remove'].includes(v.action) || !!v.itemId, { message: 'this action requires itemId', path: ['itemId'] })
|
|
3795
|
+
.refine((v) => v.action !== 'set_placement' || !!v.placement, { message: 'set_placement requires placement', path: ['placement'] })
|
|
3796
|
+
.refine((v) => !['define_instrument', 'define_percussion', 'define_sfx'].includes(v.action) || !!v.name, { message: 'defining a sound requires name', path: ['name'] })
|
|
3797
|
+
// The engine's own rule, stated here so the refusal arrives before the round
|
|
3798
|
+
// trip: a melodic instrument needs partials, while percussion and SFX may be
|
|
3799
|
+
// pure noise. Mirrored rather than tightened — tightening it would refuse a
|
|
3800
|
+
// hat, which is exactly the sound that has no partials.
|
|
3801
|
+
.refine((v) => v.action !== 'define_instrument' || (v.partials?.length ?? 0) > 0, { message: 'define_instrument requires at least one partial — an instrument with none renders silence', path: ['partials'] })
|
|
3802
|
+
.refine((v) => !['define_percussion', 'define_sfx'].includes(v.action)
|
|
3803
|
+
|| (v.partials?.length ?? 0) > 0 || (v.noise ?? 0) > 0, { message: 'this action needs a partial table or noise > 0 — with neither the sound is silent', path: ['noise'] })
|
|
3804
|
+
// Bound by the scene, like every other timed export.
|
|
3805
|
+
.refine((v) => v.action !== 'render_soundtrack' || (v.options?.duration ?? 1) <= VIDEO_MAX_DURATION_S, { message: `render_soundtrack duration is capped at ${VIDEO_MAX_DURATION_S}s, the same ceiling as video`, path: ['options'] });
|
|
3806
|
+
/**
|
|
3807
|
+
* pinepaper_motion — the generators' Animation knob, pointed at anything.
|
|
3808
|
+
*
|
|
3809
|
+
* The same engine every generator's own animation runs on, reachable for any
|
|
3810
|
+
* group or list of items. Two kinds: a GROUP motion (drift, sway, rotate,
|
|
3811
|
+
* pulse, wave, bounce) moves the target as one; a FIELD motion (ripple,
|
|
3812
|
+
* breathe, undulate) sweeps a crest through the children from an origin, with
|
|
3813
|
+
* a chosen waveform. The field kind is the one nothing else here can do.
|
|
3814
|
+
*
|
|
3815
|
+
* `list` first. The catalogue is the engine's own and names every motion,
|
|
3816
|
+
* waveform and origin it will accept — the alternative is guessing at an enum
|
|
3817
|
+
* that moves.
|
|
3818
|
+
*/
|
|
3819
|
+
export const MotionInputSchema = z.object({
|
|
3820
|
+
action: z.enum(['list', 'apply']).describe("'list' every motion, waveform and origin the engine offers · 'apply' one to a target."),
|
|
3821
|
+
itemId: z.string().optional().describe('apply: a group or single item to move.'),
|
|
3822
|
+
itemIds: z.array(z.string()).optional().describe('apply: several items, wrapped in a new group that becomes the motion host. The members keep their own identity and ids.'),
|
|
3823
|
+
motion: z.string().optional().describe("apply: the motion name — a GROUP motion (drift, sway, rotate, pulse, wave, bounce) moves the target as one; a FIELD motion (ripple, breathe, undulate) sweeps a crest through its children. Call 'list' for the engine's own set rather than guessing."),
|
|
3824
|
+
speed: z.number().optional().describe('apply: cycles per second, roughly. Higher is faster.'),
|
|
3825
|
+
intensity: z.number().optional().describe('apply: amplitude. 0.15 is the usual default elsewhere in this surface.'),
|
|
3826
|
+
waveform: z.enum(['sine', 'triangle', 'square', 'sawtooth', 'spike']).optional()
|
|
3827
|
+
.describe("apply: the shape of one cycle. 'spike' is a narrow bump — it was called 'pulse' until the engine noticed one word was naming both a waveform and an animation; the old spelling is still accepted on the way in, but 'spike' is the name."),
|
|
3828
|
+
origin: z.enum(['center', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'left', 'right', 'top', 'bottom', 'random', 'roam']).optional()
|
|
3829
|
+
.describe("apply, FIELD motions only: where the crest starts and sweeps from. 'random' gives every element its own phase (a twinkle rather than a wave); 'roam' hops between the corners and the centre."),
|
|
3830
|
+
seed: z.number().optional().describe('apply: makes a random origin reproducible — the same seed lays out the same phases every run.'),
|
|
3831
|
+
})
|
|
3832
|
+
.refine((v) => v.action !== 'apply' || !!v.itemId || (Array.isArray(v.itemIds) && v.itemIds.length > 0), { message: 'apply requires itemId or itemIds', path: ['itemId'] })
|
|
3833
|
+
.refine((v) => v.action !== 'apply' || !!v.motion, { message: "apply requires motion — call action 'list' for the names the engine accepts", path: ['motion'] });
|
|
3834
|
+
/**
|
|
3835
|
+
* pinepaper_path — the destructive path operations Paper.js has always had and
|
|
3836
|
+
* nothing here could reach: booleans, simplify, outline-stroke, open/close,
|
|
3837
|
+
* repeat patterns, plus read-back geometry and a lock.
|
|
3838
|
+
*
|
|
3839
|
+
* Every one of these REPLACES or ADDS items rather than restyling them, which
|
|
3840
|
+
* is why they live together and away from `pinepaper_modify_item`: a boolean
|
|
3841
|
+
* consumes its operands, and an outlined stroke is a new filled shape where a
|
|
3842
|
+
* stroked line used to be. The engine refuses each by name — "a boolean needs
|
|
3843
|
+
* at least two paths", "that path has no stroke to outline" — and those
|
|
3844
|
+
* refusals are passed through rather than reworded.
|
|
3845
|
+
*/
|
|
3846
|
+
export const PathOpInputSchema = z.object({
|
|
3847
|
+
action: z.enum(['boolean', 'simplify', 'outline_stroke', 'toggle_closed', 'pattern', 'get_geometry', 'set_locked', 'unlock_all'])
|
|
3848
|
+
.describe("'boolean' (unite|subtract|intersect|exclude|divide, consumes its operands) · 'simplify' (fewer segments, same shape) · 'outline_stroke' (a stroked line becomes a filled shape) · 'toggle_closed' · 'pattern' (repeat an item: concentric|radial|grid|extrude) · 'get_geometry' (read segments back) · 'set_locked' / 'unlock_all'"),
|
|
3849
|
+
itemId: z.string().optional().describe('The item to operate on — every action except boolean and unlock_all.'),
|
|
3850
|
+
itemIds: z.array(z.string()).optional().describe('boolean: at least two paths. The FIRST is the base; the rest are applied to it in order.'),
|
|
3851
|
+
op: z.enum(['unite', 'subtract', 'intersect', 'exclude', 'divide']).optional().default('unite')
|
|
3852
|
+
.describe("boolean: 'unite' merges · 'subtract' removes the later shapes from the first · 'intersect' keeps the overlap · 'exclude' keeps everything but the overlap · 'divide' splits into every region."),
|
|
3853
|
+
tolerance: z.number().positive().optional()
|
|
3854
|
+
.describe('simplify: how far the new curve may stray, in canvas units (default 2.5 — higher is fewer points and a looser shape). outline_stroke: flattening tolerance (defaults to an eighth of the stroke radius).'),
|
|
3855
|
+
kind: z.enum(['concentric', 'radial', 'grid', 'extrude']).optional().default('radial')
|
|
3856
|
+
.describe("pattern: 'concentric' rings outward · 'radial' around a circle · 'grid' rows and columns · 'extrude' a depth-wise stack."),
|
|
3857
|
+
pattern: z.object({
|
|
3858
|
+
count: z.number().optional().describe('concentric / radial / extrude: how many copies.'),
|
|
3859
|
+
radius: z.number().optional().describe('radial / concentric: distance from the original.'),
|
|
3860
|
+
angle: z.number().optional().describe('radial: total sweep in DEGREES (360 is a full ring).'),
|
|
3861
|
+
phase: z.number().optional().describe('radial: where the first copy starts, in DEGREES.'),
|
|
3862
|
+
rows: z.number().optional().describe('grid: row count.'),
|
|
3863
|
+
cols: z.number().optional().describe('grid: column count.'),
|
|
3864
|
+
gapX: z.number().optional().describe('grid: horizontal spacing in canvas units.'),
|
|
3865
|
+
gapY: z.number().optional().describe('grid: vertical spacing in canvas units.'),
|
|
3866
|
+
step: z.number().optional().describe('extrude / concentric: spacing between successive copies.'),
|
|
3867
|
+
depth: z.number().optional().describe('extrude: total depth the stack spans.'),
|
|
3868
|
+
rotateItems: z.boolean().optional().describe('radial: turn each copy to face along the ring instead of keeping its original angle.'),
|
|
3869
|
+
shade: z.boolean().optional().describe('extrude: darken copies with depth, so the stack reads as receding.'),
|
|
3870
|
+
shadeStrength: z.number().optional().describe('extrude: how strong that darkening is.'),
|
|
3871
|
+
}).optional().describe('pattern options. Copies are capped — past a few hundred this stops being a drawing aid and becomes a generator, which is a different tool.'),
|
|
3872
|
+
locked: z.boolean().optional().default(true).describe('set_locked: true locks (also clearing selectable and draggable, so it cannot be grabbed), false unlocks.'),
|
|
3873
|
+
})
|
|
3874
|
+
.refine((v) => v.action !== 'boolean' || (Array.isArray(v.itemIds) && v.itemIds.length >= 2), { message: 'boolean requires itemIds with at least two paths', path: ['itemIds'] })
|
|
3875
|
+
.refine((v) => ['boolean', 'unlock_all'].includes(v.action) || !!v.itemId, { message: 'this action requires itemId', path: ['itemId'] });
|
|
2867
3876
|
export const CropImageInputSchema = z.object({
|
|
2868
3877
|
itemId: z.string().describe('Registry id of the raster item (or a group containing one).'),
|
|
2869
3878
|
rect: z.object({
|
|
@@ -2892,7 +3901,71 @@ export const RiggingInputSchema = z.object({
|
|
|
2892
3901
|
'create_skeleton', 'add_bone', 'attach_item', 'create_ik_chain',
|
|
2893
3902
|
'add_pose_keyframe', 'set_target_path', 'save_pose', 'save_shape_key',
|
|
2894
3903
|
'import_bvh', 'retarget_bvh', 'import_spine',
|
|
3904
|
+
// ── Pose motion. The engine has had these for releases; nothing could
|
|
3905
|
+
// reach them, which is not the same as their not existing.
|
|
3906
|
+
'list_skeletons', 'list_bones', 'list_poses', 'load_pose', 'interpolate_poses',
|
|
3907
|
+
'list_pose_libraries', 'load_pose_library',
|
|
3908
|
+
'play_pose_sequence', 'stop_pose_sequence', 'stitch_poses',
|
|
3909
|
+
'apply_pose_transition',
|
|
3910
|
+
'auto_walk', 'auto_breath', 'auto_idle', 'auto_jump',
|
|
3911
|
+
'move_root', 'stop_root_track',
|
|
3912
|
+
'add_secondary_motion', 'skin_path', 'bake_animation',
|
|
3913
|
+
'list_shape_keys', 'load_shape_key',
|
|
2895
3914
|
]).describe('Rigging operation'),
|
|
3915
|
+
// ── Pose motion ────────────────────────────────────────────────────────
|
|
3916
|
+
poseId: z.string().optional().describe('Saved pose id — load_pose.'),
|
|
3917
|
+
poseIdA: z.string().optional().describe('First pose — interpolate_poses.'),
|
|
3918
|
+
poseIdB: z.string().optional().describe('Second pose — interpolate_poses.'),
|
|
3919
|
+
t: z.number().min(0).max(1).optional().describe('Blend 0=A … 1=B — interpolate_poses.'),
|
|
3920
|
+
/**
|
|
3921
|
+
* A pose sequence: keys over normalized time, each naming a SAVED pose or an
|
|
3922
|
+
* inline { boneId: angleDeg } map. The saved-pose form is the point — an
|
|
3923
|
+
* animation that is literally a graph over the poses in the Poses panel.
|
|
3924
|
+
*/
|
|
3925
|
+
sequence: z.array(z.object({
|
|
3926
|
+
t: z.number().describe('Time (normalized within the clip unless duration is given).'),
|
|
3927
|
+
pose: z.string().optional().describe('Saved pose id.'),
|
|
3928
|
+
angles: z.record(z.string(), z.number()).optional().describe('Inline { boneId: angleDeg }.'),
|
|
3929
|
+
ease: z.string().optional(),
|
|
3930
|
+
})).optional().describe('Pose keys — play_pose_sequence.'),
|
|
3931
|
+
/**
|
|
3932
|
+
* Clips to STITCH into one continuous performance. Each clip is a sequence
|
|
3933
|
+
* plus how it joins: `repeat` plays it N times, `loop` marks it cyclic (only
|
|
3934
|
+
* a cyclic clip can be phase-matched), `blend` is the seam overlap.
|
|
3935
|
+
*/
|
|
3936
|
+
clips: z.array(z.object({
|
|
3937
|
+
keys: z.array(z.object({
|
|
3938
|
+
t: z.number().optional(),
|
|
3939
|
+
pose: z.string().optional(),
|
|
3940
|
+
angles: z.record(z.string(), z.number()).optional(),
|
|
3941
|
+
ease: z.string().optional(),
|
|
3942
|
+
})).optional(),
|
|
3943
|
+
poses: z.array(z.object({
|
|
3944
|
+
pose: z.string().optional(),
|
|
3945
|
+
angles: z.record(z.string(), z.number()).optional(),
|
|
3946
|
+
})).optional().describe('Bare list — spread evenly across the clip.'),
|
|
3947
|
+
duration: z.number().positive().optional(),
|
|
3948
|
+
repeat: z.number().int().min(1).max(200).optional(),
|
|
3949
|
+
loop: z.boolean().optional().describe('Cyclic: can be entered at any phase.'),
|
|
3950
|
+
blend: z.number().min(0).optional().describe('Override the seam overlap into this clip.'),
|
|
3951
|
+
matchPhase: z.boolean().optional(),
|
|
3952
|
+
})).optional().describe('Clips in order — stitch_poses.'),
|
|
3953
|
+
blend: z.number().min(0).optional().describe('Seam overlap in seconds (default 0.25) — stitch_poses.'),
|
|
3954
|
+
blendSteps: z.number().int().min(2).max(64).optional().describe('Keys emitted across a seam (default 6) — stitch_poses.'),
|
|
3955
|
+
matchPhase: z.boolean().optional().describe('Enter a cyclic clip at the phase closest to where the last one ended (default true) — stitch_poses.'),
|
|
3956
|
+
plan: z.boolean().optional().describe('Return the seams WITHOUT installing the result — stitch_poses. Read `residual` per seam: it is the mismatch the blend is hiding, in degrees.'),
|
|
3957
|
+
libraryName: z.string().optional().describe("Stock pose set — load_pose_library. The LIBRARIES are rig shapes: humanoid, quadruped, bird, fish. Loading one saves its poses (idle, walk_00…walk_07, walk_contact_L, run_*, …) against your bones. The walk/run/flap SEQUENCES are transitions, not libraries — play those with apply_pose_transition once the library is loaded. list_pose_libraries returns both lists."),
|
|
3958
|
+
boneMap: z.record(z.string(), z.string()).optional().describe('Bone NAME → bone id — load_pose_library. Omit and it is built from list_bones, which is what you want unless the rig uses non-standard names.'),
|
|
3959
|
+
transitionName: z.string().optional().describe('Named pose transition — apply_pose_transition.'),
|
|
3960
|
+
poseIdMap: z.record(z.string(), z.string()).optional().describe('Transition pose name → saved pose id — apply_pose_transition.'),
|
|
3961
|
+
boneNames: z.array(z.string()).optional().describe('Ordered bone names for a spring chain — add_secondary_motion.'),
|
|
3962
|
+
keyframes: z.array(z.object({
|
|
3963
|
+
t: z.number().describe('SCENE SECONDS, not a 0..1 fraction.'),
|
|
3964
|
+
x: z.number(), y: z.number(), ease: z.string().optional(),
|
|
3965
|
+
})).optional().describe('World root positions over SCENE TIME — move_root. This is what makes a figure travel instead of walking on the spot. `t` is in seconds on the same clock as the pose sequence, so match the last keyframe to the performance duration or the figure arrives early and stands there. Pass options.loop with options.period to cycle it.'),
|
|
3966
|
+
shapeKeyId: z.string().optional().describe('Saved shape key id — load_shape_key.'),
|
|
3967
|
+
weight: z.number().optional().describe('Shape key weight 0..1 (default 1) — load_shape_key.'),
|
|
3968
|
+
options: z.record(z.string(), z.unknown()).optional().describe('Pass-through options for auto_* / bake_animation / move_root / skin_path / play_pose_sequence.'),
|
|
2896
3969
|
// ── Mocap / rig import (import_bvh, retarget_bvh, import_spine) ──
|
|
2897
3970
|
// The file CONTENTS travel in the call (MCP has no filesystem): .bvh text for
|
|
2898
3971
|
// BVH, the exported .json for Spine. import_bvh builds a NEW stick-figure rig
|