@pinepaper.studio/mcp-server 1.6.7 → 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 +9 -5
- package/README.es.md +9 -5
- package/README.fr.md +9 -5
- package/README.hi.md +8 -5
- package/README.ja.md +8 -5
- package/README.ko.md +8 -4
- package/README.md +171 -17
- package/README.pt-BR.md +9 -5
- package/README.zh-CN.md +6 -4
- 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 -4
- 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 -3
- 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 +20 -0
- package/dist/ontology/vocabulary.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 +343 -24
- 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 +316 -16
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +13 -2
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +7 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +96 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1200 -12
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +6792 -151
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +587 -17
- 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 +29 -1
- package/package.json +13 -5
package/dist/types/schemas.js
CHANGED
|
@@ -98,6 +98,32 @@ 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',
|
|
101
127
|
// RENDER-TIME SURFACES. These two are not Paper items and never enter the
|
|
102
128
|
// scene tree: they are drawn per pixel by the cloud rasterizer, straight from
|
|
103
129
|
// the scene document. They are here because a capability nobody can name is a
|
|
@@ -242,6 +268,7 @@ export const KeyframeSchema = z.object({
|
|
|
242
268
|
// deliberately absent: the agent authors those THROUGH that tool, and a second
|
|
243
269
|
// name here would be a worse way to do the same thing. The test file holds that
|
|
244
270
|
// exclusion list, with the reason.
|
|
271
|
+
/** Engine parity ENFORCED BY src/__tests__/unit/relation-parity.test.ts (fixture: engine-relations.txt). */
|
|
245
272
|
export const RelationTypeSchema = z.enum([
|
|
246
273
|
// --- Motion, spatial and transform ---
|
|
247
274
|
'orbits',
|
|
@@ -255,6 +282,7 @@ export const RelationTypeSchema = z.enum([
|
|
|
255
282
|
'anchored_in_world',
|
|
256
283
|
'spring_follow',
|
|
257
284
|
'repels',
|
|
285
|
+
'attracts',
|
|
258
286
|
'wiggle',
|
|
259
287
|
// --- Animation and sequencing ---
|
|
260
288
|
'animates',
|
|
@@ -540,6 +568,13 @@ export const MovesAlongPathParamsSchema = z.object({
|
|
|
540
568
|
// || opts) — so they must survive validation.
|
|
541
569
|
export const EquationPathInputSchema = z.object({
|
|
542
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.'),
|
|
543
578
|
x: z.number().optional().describe('Canvas origin x (default: canvas center)'),
|
|
544
579
|
y: z.number().optional().describe('Canvas origin y (default: canvas center)'),
|
|
545
580
|
style: z.record(z.unknown()).optional().describe('Path style forwarded to create (strokeColor, strokeWidth, fillColor, …)'),
|
|
@@ -563,12 +598,22 @@ export const DrivenByParamsSchema = z.object({
|
|
|
563
598
|
// time_expression: self-relation (targetId=null). The source's `property` is
|
|
564
599
|
// driven by a math expression of t (scene time) and v (baseValue). signal:true
|
|
565
600
|
// parses the expression into the Expression IR and evaluates a pure f(t)
|
|
566
|
-
// (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.
|
|
567
612
|
export const TimeExpressionParamsSchema = z.object({
|
|
568
613
|
property: z.string().optional().default('y').describe('Property to drive: x, y, rotation, opacity, scale'),
|
|
569
|
-
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.'),
|
|
570
615
|
baseValue: z.number().optional().default(0).describe('Base value accessible as v in the expression'),
|
|
571
|
-
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.'),
|
|
572
617
|
});
|
|
573
618
|
// =============================================================================
|
|
574
619
|
// EVENT-DRIVEN SCENE CHAINS (FxTool S11). Source of each is a pp:event.
|
|
@@ -701,6 +746,7 @@ export const ExecuteCustomCodeInputSchema = z.object({
|
|
|
701
746
|
// =============================================================================
|
|
702
747
|
// GENERATOR TYPES
|
|
703
748
|
// =============================================================================
|
|
749
|
+
/** Engine parity ENFORCED BY src/__tests__/unit/preflight-gate-parity.test.ts (fixture: engine-generators.txt). */
|
|
704
750
|
export const GeneratorNameSchema = z.enum([
|
|
705
751
|
// Classic generators
|
|
706
752
|
'drawSunburst',
|
|
@@ -775,6 +821,24 @@ export const GeneratorNameSchema = z.enum([
|
|
|
775
821
|
'drawScatter',
|
|
776
822
|
'drawStackedWaves',
|
|
777
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',
|
|
778
842
|
]).describe('Background generator name');
|
|
779
843
|
// ---------------------------------------------------------------------------
|
|
780
844
|
// Generator parameter schemas — Phase 1 (enhanced existing)
|
|
@@ -927,6 +991,7 @@ export const NoiseTextureParamsSchema = z.object({
|
|
|
927
991
|
// =============================================================================
|
|
928
992
|
// EFFECT TYPES
|
|
929
993
|
// =============================================================================
|
|
994
|
+
/** Engine parity ENFORCED BY src/__tests__/unit/preflight-gate-parity.test.ts (fixture: engine-effects.txt). */
|
|
930
995
|
export const EffectTypeSchema = z.enum([
|
|
931
996
|
'sparkle',
|
|
932
997
|
'blast',
|
|
@@ -952,6 +1017,13 @@ export const EffectTypeSchema = z.enum([
|
|
|
952
1017
|
'vortex',
|
|
953
1018
|
'rain_veil',
|
|
954
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',
|
|
955
1027
|
]).describe('Visual effect type (particle effects + shader auras)');
|
|
956
1028
|
export const SparkleParamsSchema = z.object({
|
|
957
1029
|
color: ColorSchema.optional().default('#fbbf24').describe('Sparkle color'),
|
|
@@ -1069,8 +1141,26 @@ export const DeleteItemInputSchema = z.object({
|
|
|
1069
1141
|
export const AddRelationInputSchema = z.object({
|
|
1070
1142
|
sourceId: z.string().describe('Registry ID of the source item'),
|
|
1071
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)'),
|
|
1072
|
-
relationType: RelationTypeSchema,
|
|
1144
|
+
relationType: RelationTypeSchema.optional().describe('The relation to create. Required unless presetId is given.'),
|
|
1073
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'],
|
|
1074
1164
|
});
|
|
1075
1165
|
// Remove Relation
|
|
1076
1166
|
export const RemoveRelationInputSchema = z.object({
|
|
@@ -1103,6 +1193,41 @@ export const KeyframeAnimateInputSchema = z.object({
|
|
|
1103
1193
|
clipInPoint: z.number().optional().describe('Skip the first N seconds of keyframe data — clip head trim.'),
|
|
1104
1194
|
clipOutPoint: z.number().optional().describe('Stop at N seconds into the keyframe data — clip tail trim. Default: lastKeyframeTime.'),
|
|
1105
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
|
+
});
|
|
1106
1231
|
// Motion Capture (BVH)
|
|
1107
1232
|
//
|
|
1108
1233
|
// FxTool exposes two distinct operations and the difference is the whole point:
|
|
@@ -1703,6 +1828,10 @@ export const AgentPlatformSchema = z.enum([
|
|
|
1703
1828
|
*/
|
|
1704
1829
|
export const AgentExportFormatSchema = z.enum([
|
|
1705
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',
|
|
1706
1835
|
]).describe('Export file format');
|
|
1707
1836
|
/**
|
|
1708
1837
|
* Screenshot policy for agent jobs
|
|
@@ -1881,6 +2010,17 @@ export const AgentBatchExecuteInputSchema = z.object({
|
|
|
1881
2010
|
* could not. This brings the two in line.
|
|
1882
2011
|
*/
|
|
1883
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;
|
|
1884
2024
|
/**
|
|
1885
2025
|
* Smart export input schema
|
|
1886
2026
|
*/
|
|
@@ -1890,10 +2030,29 @@ export const AgentExportInputSchema = z.object({
|
|
|
1890
2030
|
quality: z.enum(['draft', 'standard', 'high']).optional().default('standard').describe('Export quality level'),
|
|
1891
2031
|
includeRecommendations: z.boolean().optional().default(true).describe('Include alternative format recommendations'),
|
|
1892
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.'),
|
|
1893
|
-
duration: z.number().min(0.5).max(
|
|
1894
|
-
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.'),
|
|
1895
2037
|
}).describe('Smart export options')
|
|
1896
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
|
+
}
|
|
1897
2056
|
if (val.format === 'gif' && val.duration > GIF_MAX_DURATION_S) {
|
|
1898
2057
|
ctx.addIssue({
|
|
1899
2058
|
code: z.ZodIssueCode.custom,
|
|
@@ -2352,8 +2511,12 @@ export const QueryOntologyInputSchema = z.object({
|
|
|
2352
2511
|
});
|
|
2353
2512
|
// Capabilities query schema: discover and match capabilities across text styles, text effects, generators, deforms, relations, etc.
|
|
2354
2513
|
export const QueryCapabilitiesInputSchema = z.object({
|
|
2355
|
-
action: z.enum(['list', 'choose', 'coverage', 'find']).default('list')
|
|
2356
|
-
.describe("'list' (all capabilities or filtered by kind) · 'choose' (context/mood-weighted recommendation) · 'coverage' (breakdown of indexable capabilities) · 'find' (lookup by key)"),
|
|
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."),
|
|
2357
2520
|
kind: z.union([
|
|
2358
2521
|
z.string(),
|
|
2359
2522
|
z.array(z.string()),
|
|
@@ -2663,6 +2826,28 @@ export const ProvenanceInputSchema = z.object({
|
|
|
2663
2826
|
sourceRef: z.string().optional(),
|
|
2664
2827
|
meta: z.record(z.string(), z.unknown()).optional(),
|
|
2665
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'];
|
|
2666
2851
|
export const SceneDiffInputSchema = z.object({
|
|
2667
2852
|
/** 'history' compares two undo states; 'version' compares live vs a saved version. */
|
|
2668
2853
|
action: z.enum(['history', 'version']),
|
|
@@ -2714,7 +2899,7 @@ export const ComposeInputSchema = z.object({
|
|
|
2714
2899
|
* colours.
|
|
2715
2900
|
*/
|
|
2716
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."),
|
|
2717
|
-
stitch: z.enum(
|
|
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.'),
|
|
2718
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.'),
|
|
2719
2904
|
});
|
|
2720
2905
|
export const AudioBeatsInputSchema = z.object({
|
|
@@ -3095,8 +3280,17 @@ export const MediaInputSchema = z.object({
|
|
|
3095
3280
|
* platform impossibility, not an omission.
|
|
3096
3281
|
*/
|
|
3097
3282
|
export const TextStyleInputSchema = z.object({
|
|
3098
|
-
action: z.enum(['apply_style', 'set_font_axes', 'list_styles'])
|
|
3099
|
-
.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.'),
|
|
3100
3294
|
itemId: z.string().optional().describe('Text item id — apply_style / set_font_axes.'),
|
|
3101
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)."),
|
|
3102
3296
|
palette: z.union([z.string(), z.array(z.string())]).optional().describe('apply_style: a named colourway or an explicit color array.'),
|
|
@@ -3144,8 +3338,8 @@ export const DesignMediumInputSchema = z.object({
|
|
|
3144
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'"),
|
|
3145
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."),
|
|
3146
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.'),
|
|
3147
|
-
stitch: z.enum(
|
|
3148
|
-
.describe("
|
|
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."),
|
|
3149
3343
|
field: z.object({
|
|
3150
3344
|
kind: z.enum(['radial', 'spine', 'constant']),
|
|
3151
3345
|
cx: z.number().optional(), cy: z.number().optional(),
|
|
@@ -3161,6 +3355,17 @@ export const DesignMediumInputSchema = z.object({
|
|
|
3161
3355
|
color: z.string().optional().describe("apply_thread: thread colour, defaulting to the item's own fill."),
|
|
3162
3356
|
seed: z.number().int().optional().describe('apply_thread: PRNG seed (default 1). Same seed stitches the same way.'),
|
|
3163
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.'),
|
|
3164
3369
|
// --- Hatching (pp: the p5.brush marks, as vector) ---
|
|
3165
3370
|
//
|
|
3166
3371
|
// `field` above is the THREAD direction field (radial/spine/constant) and is
|
|
@@ -3278,8 +3483,19 @@ export const GameInputSchema = z.object({
|
|
|
3278
3483
|
* itself — the tool does not restate it, so it cannot drift.
|
|
3279
3484
|
*/
|
|
3280
3485
|
export const World3DInputSchema = z.object({
|
|
3281
|
-
action: z.enum([
|
|
3282
|
-
|
|
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'"),
|
|
3283
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."),
|
|
3284
3500
|
character: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional().describe('create: include the walkable character (true/config).'),
|
|
3285
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.'),
|
|
@@ -3291,18 +3507,372 @@ export const World3DInputSchema = z.object({
|
|
|
3291
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.'),
|
|
3292
3508
|
pose: z.record(z.string(), z.unknown()).optional().describe('set_actor_pose: { x?, z?, angle?, … } — the setter a timeline or agent drives.'),
|
|
3293
3509
|
camera: z.record(z.string(), z.unknown()).optional().describe("set_camera: { mode: 'follow'|'fixed'|'orbit', target?, radius?, speed?, eye?, lookAt? }."),
|
|
3294
|
-
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."),
|
|
3295
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.'),
|
|
3296
3580
|
})
|
|
3297
3581
|
.refine((v) => v.action !== 'configure' || !!v.patch, { message: 'configure requires patch', path: ['patch'] })
|
|
3298
3582
|
.refine((v) => !['remove_actor', 'set_actor_pose'].includes(v.action) || !!v.actorId, { message: 'this action requires actorId', path: ['actorId'] })
|
|
3299
3583
|
.refine((v) => v.action !== 'set_actor_pose' || !!v.pose, { message: 'set_actor_pose requires pose', path: ['pose'] })
|
|
3300
3584
|
.refine((v) => v.action !== 'set_camera' || !!v.camera, { message: 'set_camera requires camera', path: ['camera'] })
|
|
3301
3585
|
.refine((v) => v.action !== 'add_object' || !!v.object, { message: 'add_object requires object', path: ['object'] })
|
|
3302
|
-
.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'] });
|
|
3303
3609
|
// =============================================================================
|
|
3304
3610
|
// ONE-SHOT IMAGE OPS (crop / chroma key — Track A agent parity, 2026-07-31)
|
|
3305
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'] });
|
|
3306
3876
|
export const CropImageInputSchema = z.object({
|
|
3307
3877
|
itemId: z.string().describe('Registry id of the raster item (or a group containing one).'),
|
|
3308
3878
|
rect: z.object({
|