@pinepaper.studio/mcp-server 1.6.5 → 1.6.7
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 +20 -1
- package/README.es.md +20 -1
- package/README.fr.md +20 -1
- package/README.hi.md +18 -1
- package/README.ja.md +19 -1
- package/README.ko.md +18 -1
- package/README.md +177 -5
- package/README.pt-BR.md +20 -1
- package/README.zh-CN.md +18 -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/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/ontology/design-graph.d.ts.map +1 -1
- package/dist/ontology/design-graph.js +0 -1
- package/dist/ontology/design-graph.js.map +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +119 -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/definitions.d.ts +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +906 -34
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +71 -7
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +11 -2
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +14 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +46 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1004 -24
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +2266 -215
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +553 -50
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +37 -1
- package/package.json +17 -7
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This code is designed to run in the browser context where window.PinePaper is available.
|
|
6
6
|
*/
|
|
7
7
|
import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
|
|
8
|
-
import { CreateItemInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
|
|
8
|
+
import { CreateItemInputSchema, ImportMotionCaptureInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
|
|
9
9
|
// Diagram types
|
|
10
10
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
11
11
|
// Agent flow mode types
|
|
@@ -21,7 +21,7 @@ GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTou
|
|
|
21
21
|
// Custom relation/code types
|
|
22
22
|
RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
|
|
23
23
|
// Mask types
|
|
24
|
-
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, } from './schemas.js';
|
|
24
|
+
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, } from './schemas.js';
|
|
25
25
|
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
26
26
|
// =============================================================================
|
|
27
27
|
// HELPER FUNCTIONS
|
|
@@ -97,6 +97,36 @@ function generateCreateItemCode(itemType, position, properties) {
|
|
|
97
97
|
if (strokeColor && !isGradient(strokeColor)) {
|
|
98
98
|
params.strokeColor = strokeColor;
|
|
99
99
|
}
|
|
100
|
+
// RENDER-TIME SURFACES DEGRADE, THEY DO NOT FAIL.
|
|
101
|
+
//
|
|
102
|
+
// `shader` and `field` are drawn per pixel by the cloud rasterizer from the
|
|
103
|
+
// scene document; the browser engine has no such item type and answers
|
|
104
|
+
// `create('shader')` with "unknown type — nothing was created". Emitting that
|
|
105
|
+
// call would hand every local user a silent hole where their backdrop was.
|
|
106
|
+
//
|
|
107
|
+
// So locally they become a plate: a rectangle carrying the same id, position,
|
|
108
|
+
// size and colour, which keeps the composition readable and the item
|
|
109
|
+
// addressable by every later call. The scene document still says `shader`, so
|
|
110
|
+
// a cloud render draws the real surface. The divergence is declared here
|
|
111
|
+
// rather than discovered in a frame.
|
|
112
|
+
if (itemType === 'shader' || itemType === 'field') {
|
|
113
|
+
const plate = {
|
|
114
|
+
x: position.x,
|
|
115
|
+
y: position.y,
|
|
116
|
+
width: params.width ?? 200,
|
|
117
|
+
height: params.height ?? 200,
|
|
118
|
+
fillColor: params.fillColor ?? params.fill ?? '#334155',
|
|
119
|
+
};
|
|
120
|
+
if (params.id)
|
|
121
|
+
plate.id = params.id;
|
|
122
|
+
if (params.opacity != null)
|
|
123
|
+
plate.opacity = params.opacity;
|
|
124
|
+
return `
|
|
125
|
+
// ${itemType} — a render-time surface. Drawn per pixel by the cloud renderer;
|
|
126
|
+
// stood in for locally by a flat plate so the layout still reads.
|
|
127
|
+
const item = app.create('rectangle', ${JSON.stringify(plate, null, 2)});
|
|
128
|
+
if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.data.renderParams = ${JSON.stringify(params)}; }`;
|
|
129
|
+
}
|
|
100
130
|
// Build the code
|
|
101
131
|
let code = `
|
|
102
132
|
// Create ${itemType} item
|
|
@@ -309,7 +339,23 @@ app.animate(item, {
|
|
|
309
339
|
animationSpeed: ${speed}${extra}
|
|
310
340
|
});
|
|
311
341
|
|
|
312
|
-
|
|
342
|
+
// The engine records a refused type on the item rather than throwing, because
|
|
343
|
+
// animate() is on too many paths for a throw to be safe. It used to warn only
|
|
344
|
+
// to a console the production build strips, so an unknown type left the item
|
|
345
|
+
// still and every caller told it had worked. Read the record back.
|
|
346
|
+
(() => {
|
|
347
|
+
const r = item.data && item.data.animationRejected;
|
|
348
|
+
if (r) {
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
itemId: '${itemId}',
|
|
352
|
+
error: 'Unknown animationType "' + r.requested + '" — nothing will move.',
|
|
353
|
+
requested: r.requested,
|
|
354
|
+
known: r.known,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return { success: true, itemId: '${itemId}', animationType: '${animationType}' };
|
|
358
|
+
})();
|
|
313
359
|
`.trim();
|
|
314
360
|
}
|
|
315
361
|
/**
|
|
@@ -353,6 +399,36 @@ app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
|
|
|
353
399
|
/**
|
|
354
400
|
* Template for executing generators
|
|
355
401
|
*/
|
|
402
|
+
function generateImportMotionCaptureCode(bvh, mode, skeletonId, opts = {}) {
|
|
403
|
+
// Both FxTool entry points are async and return {ok, ...} rather than throwing, so the failure
|
|
404
|
+
// path is a value the model can read (unmatchedSource/unmatchedTarget tell it exactly which bone
|
|
405
|
+
// names to put in boneMap) instead of an opaque exception.
|
|
406
|
+
const bvhLiteral = JSON.stringify(bvh);
|
|
407
|
+
const optsLiteral = JSON.stringify(opts);
|
|
408
|
+
if (mode === 'retarget') {
|
|
409
|
+
return `
|
|
410
|
+
// Retarget a BVH clip onto an existing rig — proportions from the rig, motion from the capture
|
|
411
|
+
(async function() {
|
|
412
|
+
const res = await app.retargetBVH(${bvhLiteral}, ${JSON.stringify(skeletonId ?? '')}, ${optsLiteral});
|
|
413
|
+
if (!res || res.ok === false) {
|
|
414
|
+
return { success: false, error: (res && res.error) || 'retarget failed',
|
|
415
|
+
unmatchedSource: res && res.unmatchedSource, unmatchedTarget: res && res.unmatchedTarget };
|
|
416
|
+
}
|
|
417
|
+
return { success: true, skeletonId: res.skeletonId, matched: res.matched,
|
|
418
|
+
unmatchedSource: res.unmatchedSource, unmatchedTarget: res.unmatchedTarget };
|
|
419
|
+
})()`;
|
|
420
|
+
}
|
|
421
|
+
return `
|
|
422
|
+
// Import a BVH clip as a NEW skeleton
|
|
423
|
+
(async function() {
|
|
424
|
+
const res = await app.importBVH(${bvhLiteral}, ${optsLiteral});
|
|
425
|
+
if (!res || res.ok === false) {
|
|
426
|
+
return { success: false, error: (res && res.error) || 'import failed' };
|
|
427
|
+
}
|
|
428
|
+
return { success: true, skeletonId: res.skeletonId, bones: res.bones, poses: res.poses,
|
|
429
|
+
duration: res.duration };
|
|
430
|
+
})()`;
|
|
431
|
+
}
|
|
356
432
|
function generateExecuteGeneratorCode(generatorName, params, region) {
|
|
357
433
|
// FxTool signature: app.executeGenerator(name, params, options).
|
|
358
434
|
// region lives in options (added in FxTool c81781c).
|
|
@@ -427,11 +503,120 @@ const items = filtered.map(entry => ({
|
|
|
427
503
|
({ items, count: items.length });
|
|
428
504
|
`.trim();
|
|
429
505
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
506
|
+
// Where a scroll binding parks its unbind function. `bindTimelineToScroll`
|
|
507
|
+
// returns a FUNCTION, which cannot cross the tool boundary — so it lives on the
|
|
508
|
+
// page, where a later unbind_scroll call can still reach it.
|
|
509
|
+
const SCRUB_UNBIND_KEY = '__ppScrubUnbind';
|
|
510
|
+
function generatePlayTimelineCode(action, duration, loop, time, deterministic, extra) {
|
|
511
|
+
const S = (v) => JSON.stringify(v);
|
|
434
512
|
switch (action) {
|
|
513
|
+
case 'set_time_scale': {
|
|
514
|
+
// Deliberately not clamped: 0 freezes the clock without stopping, and a
|
|
515
|
+
// negative rate runs the scene backwards.
|
|
516
|
+
const rate = extra?.rate ?? 1;
|
|
517
|
+
return `
|
|
518
|
+
// pp:TimeScale — the rate the scene clock runs at.
|
|
519
|
+
(function() {
|
|
520
|
+
if (typeof app.setTimeScale !== 'function') {
|
|
521
|
+
return { success: false, error: 'app.setTimeScale unavailable — update FxTool' };
|
|
522
|
+
}
|
|
523
|
+
// Changing the rate rebases the clock so the CURRENT position is preserved;
|
|
524
|
+
// without that, doubling the rate mid-play jumps the playhead to twice its
|
|
525
|
+
// elapsed time, which reads as a glitch rather than a speed change.
|
|
526
|
+
const rate = app.setTimeScale(${S(rate)});
|
|
527
|
+
// Export drives time itself, so a scene watched at 0.5x still exports its
|
|
528
|
+
// real duration rather than a file twice as long.
|
|
529
|
+
return { success: true, action: 'set_time_scale', rate, affectsExport: false };
|
|
530
|
+
})();
|
|
531
|
+
`.trim();
|
|
532
|
+
}
|
|
533
|
+
case 'get_time_scale':
|
|
534
|
+
return `
|
|
535
|
+
(function() {
|
|
536
|
+
if (typeof app.getTimeScale !== 'function') {
|
|
537
|
+
return { success: false, error: 'app.getTimeScale unavailable — update FxTool' };
|
|
538
|
+
}
|
|
539
|
+
return { success: true, action: 'get_time_scale', rate: app.getTimeScale() };
|
|
540
|
+
})();
|
|
541
|
+
`.trim();
|
|
542
|
+
case 'get_progress':
|
|
543
|
+
return `
|
|
544
|
+
(function() {
|
|
545
|
+
if (typeof app.getProgress !== 'function') {
|
|
546
|
+
return { success: false, error: 'app.getProgress unavailable — update FxTool' };
|
|
547
|
+
}
|
|
548
|
+
return { success: true, action: 'get_progress', progress: app.getProgress() };
|
|
549
|
+
})();
|
|
550
|
+
`.trim();
|
|
551
|
+
case 'set_progress':
|
|
552
|
+
return `
|
|
553
|
+
// Seek by proportion rather than by seconds — the unit a scrubber has.
|
|
554
|
+
(function() {
|
|
555
|
+
if (typeof app.setProgress !== 'function') {
|
|
556
|
+
return { success: false, error: 'app.setProgress unavailable — update FxTool' };
|
|
557
|
+
}
|
|
558
|
+
const time = app.setProgress(${S(extra?.progress ?? 0)});
|
|
559
|
+
return { success: true, action: 'set_progress', progress: ${S(extra?.progress ?? 0)}, time };
|
|
560
|
+
})();
|
|
561
|
+
`.trim();
|
|
562
|
+
case 'list_scrub_anchors':
|
|
563
|
+
return `
|
|
564
|
+
(function() {
|
|
565
|
+
if (typeof app.listScrubAnchors !== 'function') {
|
|
566
|
+
return { success: false, error: 'app.listScrubAnchors unavailable — update FxTool' };
|
|
567
|
+
}
|
|
568
|
+
const anchors = app.listScrubAnchors();
|
|
569
|
+
return { success: true, action: 'list_scrub_anchors', count: anchors.length, anchors };
|
|
570
|
+
})();
|
|
571
|
+
`.trim();
|
|
572
|
+
case 'bind_scroll': {
|
|
573
|
+
const scroll = extra?.scroll || {};
|
|
574
|
+
return `
|
|
575
|
+
// pp:InputDrivenPlayback — drive the timeline from scroll instead of a clock.
|
|
576
|
+
(function() {
|
|
577
|
+
if (typeof app.bindTimelineToScroll !== 'function') {
|
|
578
|
+
return { success: false, error: 'app.bindTimelineToScroll unavailable — update FxTool' };
|
|
579
|
+
}
|
|
580
|
+
const cfg = ${S(scroll)};
|
|
581
|
+
// The listener holds the app instance, so rebinding without unbinding leaks
|
|
582
|
+
// the old binding AND leaves two of them scrubbing the same timeline. Always
|
|
583
|
+
// release the previous one first.
|
|
584
|
+
let replaced = false;
|
|
585
|
+
if (typeof window !== 'undefined' && typeof window.${SCRUB_UNBIND_KEY} === 'function') {
|
|
586
|
+
try { window.${SCRUB_UNBIND_KEY}(); replaced = true; } catch (e) { /* already gone */ }
|
|
587
|
+
window.${SCRUB_UNBIND_KEY} = null;
|
|
588
|
+
}
|
|
589
|
+
const el = cfg.elementId && typeof document !== 'undefined'
|
|
590
|
+
? document.getElementById(cfg.elementId)
|
|
591
|
+
: undefined;
|
|
592
|
+
if (cfg.elementId && !el) {
|
|
593
|
+
return { success: false, error: 'No element with id "' + cfg.elementId + '" — bind_scroll tracks a real DOM element' };
|
|
594
|
+
}
|
|
595
|
+
const unbind = app.bindTimelineToScroll({
|
|
596
|
+
...(el ? { element: el } : {}),
|
|
597
|
+
...(cfg.start !== undefined ? { start: cfg.start } : {}),
|
|
598
|
+
...(cfg.end !== undefined ? { end: cfg.end } : {}),
|
|
599
|
+
...(cfg.scrub !== undefined ? { scrub: cfg.scrub } : {}),
|
|
600
|
+
...(cfg.range !== undefined ? { range: cfg.range } : {}),
|
|
601
|
+
});
|
|
602
|
+
if (typeof window !== 'undefined') window.${SCRUB_UNBIND_KEY} = unbind;
|
|
603
|
+
return { success: true, action: 'bind_scroll', bound: true, replaced, config: cfg };
|
|
604
|
+
})();
|
|
605
|
+
`.trim();
|
|
606
|
+
}
|
|
607
|
+
case 'unbind_scroll':
|
|
608
|
+
return `
|
|
609
|
+
(function() {
|
|
610
|
+
// Nothing bound is an answer, not an error — an agent tidying up should not
|
|
611
|
+
// have to know whether it bound anything.
|
|
612
|
+
if (typeof window === 'undefined' || typeof window.${SCRUB_UNBIND_KEY} !== 'function') {
|
|
613
|
+
return { success: true, action: 'unbind_scroll', bound: false };
|
|
614
|
+
}
|
|
615
|
+
window.${SCRUB_UNBIND_KEY}();
|
|
616
|
+
window.${SCRUB_UNBIND_KEY} = null;
|
|
617
|
+
return { success: true, action: 'unbind_scroll', bound: true };
|
|
618
|
+
})();
|
|
619
|
+
`.trim();
|
|
435
620
|
case 'play':
|
|
436
621
|
return `
|
|
437
622
|
// Play keyframe timeline
|
|
@@ -744,6 +929,37 @@ const grid = app.createGrid({
|
|
|
744
929
|
// =============================================================================
|
|
745
930
|
// CODE GENERATOR CLASS
|
|
746
931
|
// =============================================================================
|
|
932
|
+
/**
|
|
933
|
+
* Agent PLATFORM name → the engine's canvas-preset key.
|
|
934
|
+
*
|
|
935
|
+
* AgentPlatformSchema is an export-target vocabulary ('instagram', 'youtube')
|
|
936
|
+
* that was also being handed straight to app.setCanvasSize(), which keys on
|
|
937
|
+
* 'instagram-post' and 'full-hd-1080p'. Seven of the ten names matched nothing:
|
|
938
|
+
* the engine fell through to its default and resized the artboard to 800x600
|
|
939
|
+
* while recording the preset as applied, so an agent asking for an Instagram
|
|
940
|
+
* canvas silently got neither the size nor an error. (FxTool 496d3910 now
|
|
941
|
+
* refuses an unknown key instead — better, but it warns to a console the
|
|
942
|
+
* production build strips, so the agent still learns nothing.)
|
|
943
|
+
*
|
|
944
|
+
* Mapping rather than renaming: 'instagram' is the right word for an export
|
|
945
|
+
* TARGET and a poor one for a canvas SIZE, and only the second use was wrong.
|
|
946
|
+
*/
|
|
947
|
+
const PLATFORM_TO_CANVAS_PRESET = {
|
|
948
|
+
instagram: 'instagram-post', // 1080x1080
|
|
949
|
+
'instagram-story': 'instagram-story',
|
|
950
|
+
tiktok: 'tiktok',
|
|
951
|
+
youtube: 'full-hd-1080p', // the video frame, 1920x1080
|
|
952
|
+
'youtube-thumbnail': 'youtube-thumbnail',
|
|
953
|
+
twitter: 'twitter-post',
|
|
954
|
+
linkedin: 'linkedin-post',
|
|
955
|
+
web: 'hd-720p', // 1280x720
|
|
956
|
+
'print-a4': 'a4-portrait',
|
|
957
|
+
'print-letter': 'letter-portrait',
|
|
958
|
+
};
|
|
959
|
+
/** The engine key for a platform, falling back to the value itself. */
|
|
960
|
+
export function canvasPresetFor(platform) {
|
|
961
|
+
return PLATFORM_TO_CANVAS_PRESET[platform] || platform;
|
|
962
|
+
}
|
|
747
963
|
export class PinePaperCodeGenerator {
|
|
748
964
|
/**
|
|
749
965
|
* Generate code for creating an item
|
|
@@ -809,6 +1025,27 @@ export class PinePaperCodeGenerator {
|
|
|
809
1025
|
const validated = KeyframeAnimateInputSchema.parse(input);
|
|
810
1026
|
return generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint, validated.timeUnits);
|
|
811
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Generate code for importing / retargeting a motion-capture clip
|
|
1030
|
+
*/
|
|
1031
|
+
generateImportMotionCapture(input) {
|
|
1032
|
+
const v = ImportMotionCaptureInputSchema.parse(input);
|
|
1033
|
+
if (v.mode === 'retarget' && !v.skeletonId) {
|
|
1034
|
+
throw new Error("pinepaper_import_motion_capture: mode='retarget' requires skeletonId (the rig to drive)");
|
|
1035
|
+
}
|
|
1036
|
+
const opts = {};
|
|
1037
|
+
if (v.fps !== undefined)
|
|
1038
|
+
opts.fps = v.fps;
|
|
1039
|
+
if (v.height !== undefined)
|
|
1040
|
+
opts.height = v.height;
|
|
1041
|
+
if (v.position !== undefined)
|
|
1042
|
+
opts.position = v.position;
|
|
1043
|
+
if (v.name !== undefined)
|
|
1044
|
+
opts.name = v.name;
|
|
1045
|
+
if (v.boneMap !== undefined)
|
|
1046
|
+
opts.boneMap = v.boneMap;
|
|
1047
|
+
return generateImportMotionCaptureCode(v.bvh, v.mode, v.skeletonId, opts);
|
|
1048
|
+
}
|
|
812
1049
|
/**
|
|
813
1050
|
* Generate code for executing a generator
|
|
814
1051
|
*/
|
|
@@ -832,8 +1069,8 @@ export class PinePaperCodeGenerator {
|
|
|
832
1069
|
/**
|
|
833
1070
|
* Generate code for timeline control
|
|
834
1071
|
*/
|
|
835
|
-
generatePlayTimeline(action, duration, loop, time, deterministic) {
|
|
836
|
-
return generatePlayTimelineCode(action, duration, loop, time, deterministic);
|
|
1072
|
+
generatePlayTimeline(action, duration, loop, time, deterministic, extra) {
|
|
1073
|
+
return generatePlayTimelineCode(action, duration, loop, time, deterministic, extra);
|
|
837
1074
|
}
|
|
838
1075
|
/**
|
|
839
1076
|
* Generate code for setting background color
|
|
@@ -1420,7 +1657,7 @@ app.historyManager.saveState();
|
|
|
1420
1657
|
*/
|
|
1421
1658
|
generateConnect(input) {
|
|
1422
1659
|
const validated = ConnectInputSchema.parse(input);
|
|
1423
|
-
const { sourceItemId, targetItemId, routing, lineColor, lineWidth, lineStyle, headStyle, tailStyle, label, curvature, boltEnabled, boltColor, } = validated;
|
|
1660
|
+
const { id, sourceItemId, targetItemId, routing, lineColor, lineWidth, lineStyle, headStyle, tailStyle, label, curvature, boltEnabled, boltColor, } = validated;
|
|
1424
1661
|
const config = {
|
|
1425
1662
|
routing,
|
|
1426
1663
|
lineStyle,
|
|
@@ -1430,6 +1667,13 @@ app.historyManager.saveState();
|
|
|
1430
1667
|
boltEnabled,
|
|
1431
1668
|
boltColor,
|
|
1432
1669
|
};
|
|
1670
|
+
// A caller-supplied id is honoured by Connector (`config.id || <generated>`),
|
|
1671
|
+
// and it is the ONLY way a caller can address this connector afterwards:
|
|
1672
|
+
// update/remove take a connectorId, and the fallback the engine mints is
|
|
1673
|
+
// `connector_${Date.now()}_${random}` — unpredictable, and different on
|
|
1674
|
+
// every run of the same scene.
|
|
1675
|
+
if (id)
|
|
1676
|
+
config.id = id;
|
|
1433
1677
|
if (lineColor)
|
|
1434
1678
|
config.lineColor = lineColor;
|
|
1435
1679
|
if (lineWidth)
|
|
@@ -1457,8 +1701,13 @@ app.historyManager.saveState();
|
|
|
1457
1701
|
*/
|
|
1458
1702
|
generateConnectPorts(input) {
|
|
1459
1703
|
const validated = ConnectPortsInputSchema.parse(input);
|
|
1460
|
-
const { sourceItemId, sourcePort, targetItemId, targetPort, config } = validated;
|
|
1461
|
-
|
|
1704
|
+
const { id, sourceItemId, sourcePort, targetItemId, targetPort, config } = validated;
|
|
1705
|
+
// `id` sits at the TOP level of the tool's input but belongs inside the
|
|
1706
|
+
// engine's config object, which is where Connector reads it from. Merged
|
|
1707
|
+
// here rather than asking callers to nest it, since every other style
|
|
1708
|
+
// option they pass is nested and the id is not a style.
|
|
1709
|
+
const merged = id ? { ...(config ?? {}), id } : config;
|
|
1710
|
+
const configStr = merged ? JSON.stringify(merged, null, 2) : '{}';
|
|
1462
1711
|
return `
|
|
1463
1712
|
// Connect ports: ${sourceItemId}:${sourcePort} -> ${targetItemId}:${targetPort}
|
|
1464
1713
|
const sourceItem = app.getItemById('${sourceItemId}');
|
|
@@ -1659,8 +1908,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1659
1908
|
}
|
|
1660
1909
|
if (canvasPreset) {
|
|
1661
1910
|
code += `
|
|
1662
|
-
// Set canvas size to preset
|
|
1663
|
-
app.setCanvasSize('${canvasPreset}');
|
|
1911
|
+
// Set canvas size to preset (platform name → engine preset key)
|
|
1912
|
+
app.setCanvasSize('${canvasPresetFor(canvasPreset)}');
|
|
1664
1913
|
// Allow canvas resize to take effect
|
|
1665
1914
|
await new Promise(r => setTimeout(r, 50));
|
|
1666
1915
|
`;
|
|
@@ -1866,8 +2115,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1866
2115
|
}
|
|
1867
2116
|
if (canvasPreset) {
|
|
1868
2117
|
code += `
|
|
1869
|
-
// Set canvas to preset
|
|
1870
|
-
app.setCanvasSize('${canvasPreset}');
|
|
2118
|
+
// Set canvas to preset (platform name → engine preset key)
|
|
2119
|
+
app.setCanvasSize('${canvasPresetFor(canvasPreset)}');
|
|
1871
2120
|
// Allow canvas resize to take effect
|
|
1872
2121
|
await new Promise(r => setTimeout(r, 50));
|
|
1873
2122
|
`;
|
|
@@ -2115,6 +2364,10 @@ const item = app.getItemById(targetId);
|
|
|
2115
2364
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
2116
2365
|
if (!app.applyAnimatedMask) throw new Error('Mask system not available');
|
|
2117
2366
|
const maskedGroup = app.applyAnimatedMask(item, '${preset}', ${maskOpts});
|
|
2367
|
+
const maskRej = maskedGroup && maskedGroup.data && maskedGroup.data.maskAnimationRejected;
|
|
2368
|
+
if (maskRej) {
|
|
2369
|
+
return { success: false, itemId: targetId, error: 'Masked, but animation preset "' + maskRej.requested + '" is unknown — it will NOT animate.', requested: maskRej.requested, known: maskRej.known };
|
|
2370
|
+
}
|
|
2118
2371
|
return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
|
|
2119
2372
|
`;
|
|
2120
2373
|
}
|
|
@@ -3618,6 +3871,18 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3618
3871
|
} else {
|
|
3619
3872
|
maskedGroup = app.applyAnimatedMask(item, '${preset}', mergedOpts);
|
|
3620
3873
|
}
|
|
3874
|
+
// A mask with an unknown ANIMATION preset still masks — the item just never
|
|
3875
|
+
// animates — so !!maskedGroup is true either way. The engine records which.
|
|
3876
|
+
const rej = maskedGroup && maskedGroup.data && maskedGroup.data.maskAnimationRejected;
|
|
3877
|
+
if (rej) {
|
|
3878
|
+
return {
|
|
3879
|
+
success: false,
|
|
3880
|
+
itemId: '${validated.itemId}',
|
|
3881
|
+
error: 'Masked, but the animation preset "' + rej.requested + '" is unknown — it will NOT animate.',
|
|
3882
|
+
requested: rej.requested,
|
|
3883
|
+
known: rej.known,
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3621
3886
|
return { success: !!maskedGroup, itemId: '${validated.itemId}', preset: '${preset}' };
|
|
3622
3887
|
} catch (error) {
|
|
3623
3888
|
return { success: false, error: error.message };
|
|
@@ -4052,6 +4317,238 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The RE
|
|
|
4052
4317
|
return `(function() { return { error: 'Unknown scene_playback action: ${input.action}' }; })();`;
|
|
4053
4318
|
}
|
|
4054
4319
|
}
|
|
4320
|
+
generateSceneGraph(input) {
|
|
4321
|
+
const validated = SceneGraphInputSchema.parse(input);
|
|
4322
|
+
const S = (v) => JSON.stringify(v);
|
|
4323
|
+
if (validated.action === 'validate') {
|
|
4324
|
+
return `
|
|
4325
|
+
// Check a scene graph WITHOUT drawing it — the same check createSceneGraph runs,
|
|
4326
|
+
// so an author can fix the graph before committing to a canvas full of cards.
|
|
4327
|
+
(function() {
|
|
4328
|
+
if (typeof app.validateSceneGraph !== 'function') {
|
|
4329
|
+
return { success: false, error: 'app.validateSceneGraph unavailable — update FxTool' };
|
|
4330
|
+
}
|
|
4331
|
+
const r = app.validateSceneGraph(${S(validated.graph)});
|
|
4332
|
+
return {
|
|
4333
|
+
success: true,
|
|
4334
|
+
action: 'validate',
|
|
4335
|
+
ok: r ? r.ok : false,
|
|
4336
|
+
errors: (r && r.errors) || [],
|
|
4337
|
+
// Cycles are legitimate (a retry loop) but reported: an accidental one
|
|
4338
|
+
// looks exactly like a game that never ends.
|
|
4339
|
+
warnings: (r && r.warnings) || [],
|
|
4340
|
+
reachable: (r && r.reachable) || [],
|
|
4341
|
+
cycles: (r && r.cycles) || [],
|
|
4342
|
+
};
|
|
4343
|
+
})();
|
|
4344
|
+
`.trim();
|
|
4345
|
+
}
|
|
4346
|
+
return `
|
|
4347
|
+
// Create interactive scene graph (story / quiz cards)
|
|
4348
|
+
(async function() {
|
|
4349
|
+
if (typeof app.createSceneGraph !== 'function') {
|
|
4350
|
+
return { success: false, error: 'app.createSceneGraph unavailable — update FxTool' };
|
|
4351
|
+
}
|
|
4352
|
+
const result = await app.createSceneGraph(${S(validated.graph)}, ${S(validated.opts || {})});
|
|
4353
|
+
if (!result) return { success: false, error: 'No result returned' };
|
|
4354
|
+
return {
|
|
4355
|
+
success: result.ok !== false,
|
|
4356
|
+
ok: result.ok,
|
|
4357
|
+
errors: result.errors || [],
|
|
4358
|
+
warnings: result.warnings || [],
|
|
4359
|
+
// The engine reports these and this tool used to drop them. \`failed\` is the
|
|
4360
|
+
// one that matters: a graph can compile, render, and still leave N click
|
|
4361
|
+
// relations unwired — it looks built and is inert, and nothing else in the
|
|
4362
|
+
// result says so. A refused plan returns early without them, hence the
|
|
4363
|
+
// defaults.
|
|
4364
|
+
wired: result.wired,
|
|
4365
|
+
failed: result.failed,
|
|
4366
|
+
cycles: result.cycles || [],
|
|
4367
|
+
entranced: result.entranced,
|
|
4368
|
+
start: result.start,
|
|
4369
|
+
ids: result.ids || {},
|
|
4370
|
+
};
|
|
4371
|
+
})();
|
|
4372
|
+
`.trim();
|
|
4373
|
+
}
|
|
4374
|
+
// ===========================================================================
|
|
4375
|
+
// RELATIVE TIMING, STAGGERS, FLIP
|
|
4376
|
+
// ===========================================================================
|
|
4377
|
+
generateSequence(input) {
|
|
4378
|
+
const validated = SequenceInputSchema.parse(input);
|
|
4379
|
+
const S = (v) => JSON.stringify(v);
|
|
4380
|
+
if (validated.action === 'list_forms') {
|
|
4381
|
+
return `
|
|
4382
|
+
(function() {
|
|
4383
|
+
if (typeof app.listPositionForms !== 'function') {
|
|
4384
|
+
return { success: false, error: 'app.listPositionForms unavailable — update FxTool' };
|
|
4385
|
+
}
|
|
4386
|
+
const forms = app.listPositionForms();
|
|
4387
|
+
return { success: true, action: 'list_forms', count: forms.length, forms };
|
|
4388
|
+
})();
|
|
4389
|
+
`.trim();
|
|
4390
|
+
}
|
|
4391
|
+
if (validated.action === 'resolve') {
|
|
4392
|
+
return `
|
|
4393
|
+
// Resolve ONE relative position to absolute seconds.
|
|
4394
|
+
(function() {
|
|
4395
|
+
if (typeof app.resolveTimelinePosition !== 'function') {
|
|
4396
|
+
return { success: false, error: 'app.resolveTimelinePosition unavailable — update FxTool' };
|
|
4397
|
+
}
|
|
4398
|
+
const ctx = { ...${S(validated.context || {})}, labels: ${S(validated.labels || {})} };
|
|
4399
|
+
const seconds = app.resolveTimelinePosition(${S(validated.position ?? null)}, ctx);
|
|
4400
|
+
return { success: true, action: 'resolve', position: ${S(validated.position ?? null)}, seconds, context: ctx };
|
|
4401
|
+
})();
|
|
4402
|
+
`.trim();
|
|
4403
|
+
}
|
|
4404
|
+
return `
|
|
4405
|
+
// Place a run of clips in order, each resolved against the ones before it.
|
|
4406
|
+
(function() {
|
|
4407
|
+
if (typeof app.placeSequence !== 'function') {
|
|
4408
|
+
return { success: false, error: 'app.placeSequence unavailable — update FxTool' };
|
|
4409
|
+
}
|
|
4410
|
+
const clips = ${S(validated.clips || [])};
|
|
4411
|
+
const result = app.placeSequence(clips, {
|
|
4412
|
+
startAt: ${S(validated.startAt ?? 0)},
|
|
4413
|
+
labels: ${S(validated.labels || {})},
|
|
4414
|
+
});
|
|
4415
|
+
// Echo the caller's own clip ids back onto the placements — placeSequence
|
|
4416
|
+
// answers positionally, and an agent that named its clips should get the
|
|
4417
|
+
// names back rather than having to re-zip two arrays by index.
|
|
4418
|
+
const placed = (result.placed || []).map((p, i) => (
|
|
4419
|
+
clips[i] && clips[i].id ? { id: clips[i].id, ...p } : p
|
|
4420
|
+
));
|
|
4421
|
+
return {
|
|
4422
|
+
success: true,
|
|
4423
|
+
action: 'place',
|
|
4424
|
+
placed,
|
|
4425
|
+
labels: result.labels,
|
|
4426
|
+
// The timeline only ever grows: a clip placed early with "-=" must not pull
|
|
4427
|
+
// the end back and silently truncate everything after it.
|
|
4428
|
+
duration: result.duration,
|
|
4429
|
+
};
|
|
4430
|
+
})();
|
|
4431
|
+
`.trim();
|
|
4432
|
+
}
|
|
4433
|
+
generateStagger(input) {
|
|
4434
|
+
const validated = StaggerInputSchema.parse(input);
|
|
4435
|
+
const S = (v) => JSON.stringify(v);
|
|
4436
|
+
if (validated.action === 'list_origins') {
|
|
4437
|
+
return `
|
|
4438
|
+
(function() {
|
|
4439
|
+
if (typeof app.listStaggerOrigins !== 'function') {
|
|
4440
|
+
return { success: false, error: 'app.listStaggerOrigins unavailable — update FxTool' };
|
|
4441
|
+
}
|
|
4442
|
+
const origins = app.listStaggerOrigins();
|
|
4443
|
+
return { success: true, action: 'list_origins', count: origins.length, origins };
|
|
4444
|
+
})();
|
|
4445
|
+
`.trim();
|
|
4446
|
+
}
|
|
4447
|
+
if (validated.action === 'preview') {
|
|
4448
|
+
return `
|
|
4449
|
+
// Delays for a count, without touching the canvas — plan before committing.
|
|
4450
|
+
(function() {
|
|
4451
|
+
if (typeof app.staggerDelays !== 'function') {
|
|
4452
|
+
return { success: false, error: 'app.staggerDelays unavailable — update FxTool' };
|
|
4453
|
+
}
|
|
4454
|
+
const delays = app.staggerDelays(${S(validated.count ?? 0)}, ${S(validated.opts || {})});
|
|
4455
|
+
return {
|
|
4456
|
+
success: true,
|
|
4457
|
+
action: 'preview',
|
|
4458
|
+
count: ${S(validated.count ?? 0)},
|
|
4459
|
+
delays,
|
|
4460
|
+
span: delays.length ? Math.max(...delays) : 0,
|
|
4461
|
+
};
|
|
4462
|
+
})();
|
|
4463
|
+
`.trim();
|
|
4464
|
+
}
|
|
4465
|
+
return `
|
|
4466
|
+
// Apply a stagger to real items, in the order given.
|
|
4467
|
+
(function() {
|
|
4468
|
+
if (typeof app.staggerItems !== 'function') {
|
|
4469
|
+
return { success: false, error: 'app.staggerItems unavailable — update FxTool' };
|
|
4470
|
+
}
|
|
4471
|
+
const ids = ${S(validated.itemIds || [])};
|
|
4472
|
+
if (!ids.length) {
|
|
4473
|
+
return { success: false, error: 'apply needs itemIds, in the order the stagger should follow (row-major for a grid)' };
|
|
4474
|
+
}
|
|
4475
|
+
const r = app.staggerItems(ids, ${S(validated.opts || {})});
|
|
4476
|
+
return {
|
|
4477
|
+
success: r && r.ok === true,
|
|
4478
|
+
action: 'apply',
|
|
4479
|
+
// 'applied' can be lower than the ids given — an id that resolves to
|
|
4480
|
+
// nothing is skipped, and a stagger that silently covered fewer items than
|
|
4481
|
+
// asked for is the failure worth seeing.
|
|
4482
|
+
requested: ids.length,
|
|
4483
|
+
applied: r ? r.applied : 0,
|
|
4484
|
+
delays: r ? r.delays : [],
|
|
4485
|
+
span: r ? r.span : 0,
|
|
4486
|
+
};
|
|
4487
|
+
})();
|
|
4488
|
+
`.trim();
|
|
4489
|
+
}
|
|
4490
|
+
generateFlip(input) {
|
|
4491
|
+
const validated = FlipInputSchema.parse(input);
|
|
4492
|
+
const S = (v) => JSON.stringify(v);
|
|
4493
|
+
if (validated.action === 'record') {
|
|
4494
|
+
return `
|
|
4495
|
+
// pp:Flip step one — record where things are NOW, before the change.
|
|
4496
|
+
//
|
|
4497
|
+
// The record stays ON THE PAGE rather than travelling back through the tool
|
|
4498
|
+
// result: it is one snapshot per item of raw geometry, useless to read and
|
|
4499
|
+
// expensive to carry, and \`apply\` is the only thing that ever wants it.
|
|
4500
|
+
(function() {
|
|
4501
|
+
if (typeof app.recordFlipState !== 'function') {
|
|
4502
|
+
return { success: false, error: 'app.recordFlipState unavailable — update FxTool' };
|
|
4503
|
+
}
|
|
4504
|
+
const ids = ${S(validated.itemIds || null)};
|
|
4505
|
+
const record = ids && ids.length ? app.recordFlipState(ids) : app.recordFlipState();
|
|
4506
|
+
if (typeof window !== 'undefined') window.__ppFlipRecord = record;
|
|
4507
|
+
return {
|
|
4508
|
+
success: true,
|
|
4509
|
+
action: 'record',
|
|
4510
|
+
recorded: (record.snapshots || []).length,
|
|
4511
|
+
takenAt: record.takenAt,
|
|
4512
|
+
ids: (record.snapshots || []).map(s => s.id),
|
|
4513
|
+
};
|
|
4514
|
+
})();
|
|
4515
|
+
`.trim();
|
|
4516
|
+
}
|
|
4517
|
+
const opts = {};
|
|
4518
|
+
if (validated.duration !== undefined)
|
|
4519
|
+
opts.duration = validated.duration;
|
|
4520
|
+
if (validated.easing !== undefined)
|
|
4521
|
+
opts.easing = validated.easing;
|
|
4522
|
+
if (validated.enter !== undefined)
|
|
4523
|
+
opts.enter = validated.enter;
|
|
4524
|
+
if (validated.stagger !== undefined)
|
|
4525
|
+
opts.stagger = validated.stagger;
|
|
4526
|
+
return `
|
|
4527
|
+
// pp:Flip step two — animate from the recorded state to wherever things are now.
|
|
4528
|
+
// Writes ordinary keyframes, so the transition scrubs, exports to SMIL and
|
|
4529
|
+
// Lottie, and survives a reload.
|
|
4530
|
+
(function() {
|
|
4531
|
+
if (typeof app.flipItems !== 'function') {
|
|
4532
|
+
return { success: false, error: 'app.flipItems unavailable — update FxTool' };
|
|
4533
|
+
}
|
|
4534
|
+
const record = typeof window !== 'undefined' ? window.__ppFlipRecord : null;
|
|
4535
|
+
if (!record || !(record.snapshots || []).length) {
|
|
4536
|
+
return { success: false, error: 'Nothing was recorded — run pinepaper_flip { action: "record" } BEFORE the change, then rearrange, then apply' };
|
|
4537
|
+
}
|
|
4538
|
+
const r = app.flipItems(record, ${S(opts)});
|
|
4539
|
+
return {
|
|
4540
|
+
success: r && r.ok === true,
|
|
4541
|
+
action: 'apply',
|
|
4542
|
+
moved: r ? r.moved : 0,
|
|
4543
|
+
entered: r ? r.entered : [],
|
|
4544
|
+
// Items that LEFT during the change cannot be animated out — they are gone
|
|
4545
|
+
// already. Remove them after the flip, not before.
|
|
4546
|
+
left: r ? r.left : [],
|
|
4547
|
+
duration: r ? r.duration : 0,
|
|
4548
|
+
};
|
|
4549
|
+
})();
|
|
4550
|
+
`.trim();
|
|
4551
|
+
}
|
|
4055
4552
|
// ===========================================================================
|
|
4056
4553
|
// SELECTION, TRANSFORM & HISTORY
|
|
4057
4554
|
// ===========================================================================
|
|
@@ -4261,6 +4758,14 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The RE
|
|
|
4261
4758
|
...(input.assets ? { assets: input.assets } : {}),
|
|
4262
4759
|
...(input.audio ? { audio: input.audio } : {}),
|
|
4263
4760
|
...(input.grid ? { grid: true } : {}),
|
|
4761
|
+
// The design axes. A register resolves the craft (gutter, margin,
|
|
4762
|
+
// hue budget, type scale); a medium says what makes the marks and
|
|
4763
|
+
// is REFUSED when a composition cannot be rendered in it.
|
|
4764
|
+
...(input.register ? { register: input.register } : {}),
|
|
4765
|
+
...(input.level !== undefined ? { level: input.level } : {}),
|
|
4766
|
+
...(input.medium ? { medium: input.medium } : {}),
|
|
4767
|
+
...(input.stitch ? { stitch: input.stitch } : {}),
|
|
4768
|
+
...(input.stitchBudget !== undefined ? { stitchBudget: input.stitchBudget } : {}),
|
|
4264
4769
|
})}`, 'Compose: apply pattern');
|
|
4265
4770
|
}
|
|
4266
4771
|
}
|
|
@@ -4520,6 +5025,12 @@ ${resolve}
|
|
|
4520
5025
|
${guard}
|
|
4521
5026
|
try {
|
|
4522
5027
|
const result = await app.imageTools.applyCutoutStyle(${JSON.stringify(input.itemId || '')}, ${JSON.stringify(input.preset || '')}, ${JSON.stringify(input.options || {})});
|
|
5028
|
+
// An unknown preset returns the very item it was given, unchanged — so a
|
|
5029
|
+
// truthy result proves nothing. The engine records the refusal on it.
|
|
5030
|
+
const cutRej = result && result.data && result.data.cutoutStyleRejected;
|
|
5031
|
+
if (cutRej) {
|
|
5032
|
+
return { success: false, action: 'apply', itemId: ${JSON.stringify(input.itemId || '')}, error: 'Unknown cutout preset "' + cutRej.requested + '" — the item is UNCHANGED.', requested: cutRej.requested, known: cutRej.known };
|
|
5033
|
+
}
|
|
4523
5034
|
return { success: true, action: 'apply', itemId: ${JSON.stringify(input.itemId || '')}, preset: ${JSON.stringify(input.preset || '')} };
|
|
4524
5035
|
} catch (e) {
|
|
4525
5036
|
return { error: 'Failed to apply cutout style: ' + e.message };
|
|
@@ -4687,6 +5198,144 @@ ${resolve}
|
|
|
4687
5198
|
return `(function() { return { error: 'Unknown query action: ${input.action}' }; })();`;
|
|
4688
5199
|
}
|
|
4689
5200
|
}
|
|
5201
|
+
generateQueryCapabilities(input) {
|
|
5202
|
+
const validated = QueryCapabilitiesInputSchema.parse(input);
|
|
5203
|
+
const S = (v) => JSON.stringify(v);
|
|
5204
|
+
// Warming is the engine's default and the honest answer, so the argument is
|
|
5205
|
+
// omitted unless the caller explicitly opted out — no reason to spend bytes
|
|
5206
|
+
// restating a default.
|
|
5207
|
+
const warmArg = validated.warm === undefined ? '' : S({ warm: validated.warm });
|
|
5208
|
+
return `
|
|
5209
|
+
// Query capabilities / recommend treatments from the engine's LIVE registries.
|
|
5210
|
+
//
|
|
5211
|
+
// Reads app.getCapabilities(), which warms the lazily-constructed registries
|
|
5212
|
+
// before answering and returns a coverage report saying what it read from.
|
|
5213
|
+
// \`warm: false\` skips that and answers from whatever is already resident —
|
|
5214
|
+
// cheaper, and the coverage report labels itself \`warmed: false\` when it does.
|
|
5215
|
+
// This tool previously rebuilt that index itself, and because generatorRegistry
|
|
5216
|
+
// is null until ensureHeavyModules() lands (~1.2s after boot) and the rigging /
|
|
5217
|
+
// blending / deform relation rules only register when their subsystem is first
|
|
5218
|
+
// touched, an early call reported ZERO generators and ~77 of ~100 relations as
|
|
5219
|
+
// if that were the whole engine.
|
|
5220
|
+
(async function() {
|
|
5221
|
+
const action = ${S(validated.action || 'list')};
|
|
5222
|
+
const kind = ${S(validated.kind)};
|
|
5223
|
+
const mood = ${S(validated.mood)};
|
|
5224
|
+
const subject = ${S(validated.subject)};
|
|
5225
|
+
const avoid = ${S(validated.avoid)};
|
|
5226
|
+
const seed = ${S(validated.seed)};
|
|
5227
|
+
const key = ${S(validated.key)};
|
|
5228
|
+
const exclude = ${S(validated.exclude)};
|
|
5229
|
+
|
|
5230
|
+
if (typeof app.getCapabilities !== 'function') {
|
|
5231
|
+
return { success: false, error: 'app.getCapabilities unavailable — update FxTool' };
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
let caps = [], report = null;
|
|
5235
|
+
try {
|
|
5236
|
+
const res = await app.getCapabilities(${warmArg});
|
|
5237
|
+
caps = (res && res.capabilities) || [];
|
|
5238
|
+
report = (res && res.coverage) || null;
|
|
5239
|
+
} catch (e) {
|
|
5240
|
+
return { success: false, error: 'Failed to inspect capabilities: ' + e.message };
|
|
5241
|
+
}
|
|
5242
|
+
|
|
5243
|
+
if (action === 'coverage') {
|
|
5244
|
+
// The engine's own report: totals per kind, the entries that can be applied
|
|
5245
|
+
// but not RANKED (no label, no definition), the kinds with no source wired,
|
|
5246
|
+
// and which lazy registries were live when it answered.
|
|
5247
|
+
return { success: true, action: 'coverage', ...(report || {}), total: caps.length };
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
if (action === 'find') {
|
|
5251
|
+
if (!key) return { success: false, error: 'find action requires key' };
|
|
5252
|
+
const found = caps.find(c => c.key === String(key));
|
|
5253
|
+
return found ? { success: true, action: 'find', capability: found } : { success: false, error: 'Capability not found: ' + key };
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
let filtered = caps;
|
|
5257
|
+
if (kind) {
|
|
5258
|
+
const kinds = new Set(Array.isArray(kind) ? kind : [kind]);
|
|
5259
|
+
filtered = caps.filter(c => kinds.has(c.kind));
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5262
|
+
if (action === 'list') {
|
|
5263
|
+
return {
|
|
5264
|
+
success: true,
|
|
5265
|
+
action: 'list',
|
|
5266
|
+
count: filtered.length,
|
|
5267
|
+
total: caps.length,
|
|
5268
|
+
unwiredKinds: report ? report.unwiredKinds : undefined,
|
|
5269
|
+
capabilities: filtered.map(c => ({
|
|
5270
|
+
key: c.key, kind: c.kind, label: c.label,
|
|
5271
|
+
definition: c.definition, applyWith: c.applyWith, describable: c.describable
|
|
5272
|
+
}))
|
|
5273
|
+
};
|
|
5274
|
+
}
|
|
5275
|
+
|
|
5276
|
+
if (action === 'choose') {
|
|
5277
|
+
const STOP = new Set(['the', 'a', 'an', 'and', 'or', 'of', 'to', 'in', 'into', 'at', 'on', 'as', 'is', 'are', 'then', 'their', 'they', 'it', 'its', 'each', 'from', 'with', 'that', 'characters', 'character', 'letters', 'letter', 'text', 'frame', 'real']);
|
|
5278
|
+
const tokenize = (t) => String(t || '').toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length > 2 && !STOP.has(w));
|
|
5279
|
+
const MOOD_TERMS = {
|
|
5280
|
+
reveal: ['resolve', 'resolves', 'reveal', 'appear', 'emerge', 'decode', 'decrypt', 'unscramble', 'form', 'assemble', 'converge'],
|
|
5281
|
+
triumphant: ['burst', 'launch', 'firework', 'bloom', 'spark', 'explode', 'shine', 'glow', 'celebrate', 'rise'],
|
|
5282
|
+
failure: ['crumble', 'fall', 'collapse', 'burn', 'decay', 'dissolve', 'break', 'shatter', 'sink', 'error', 'corrupt'],
|
|
5283
|
+
calm: ['drift', 'float', 'gentle', 'slow', 'settle', 'fade', 'wave', 'ripple', 'pour'],
|
|
5284
|
+
technical: ['binary', 'matrix', 'grid', 'scan', 'code', 'digital', 'circuit', 'data', 'print', 'terminal'],
|
|
5285
|
+
energetic: ['bounce', 'swarm', 'spray', 'shoot', 'fast', 'rapid', 'scatter', 'slam', 'volley']
|
|
5286
|
+
};
|
|
5287
|
+
|
|
5288
|
+
// Deterministic tiebreak. Equal-scoring candidates used to fall out in
|
|
5289
|
+
// array order, which made \`seed\` — documented as giving reproducible
|
|
5290
|
+
// selection — do nothing at all. FNV-1a over seed+key gives a stable
|
|
5291
|
+
// shuffle per seed; with no seed the order is stable by key, so a repeated
|
|
5292
|
+
// call still answers the same way.
|
|
5293
|
+
const tiebreak = (k) => {
|
|
5294
|
+
if (!seed) return null;
|
|
5295
|
+
let h = 2166136261;
|
|
5296
|
+
const s = String(seed) + '|' + k;
|
|
5297
|
+
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
5298
|
+
return h >>> 0;
|
|
5299
|
+
};
|
|
5300
|
+
|
|
5301
|
+
const exSet = new Set(Array.isArray(exclude) ? exclude : []);
|
|
5302
|
+
const scored = filtered.filter(c => !exSet.has(c.key)).map(c => {
|
|
5303
|
+
const words = new Set(tokenize(c.text || (c.label + ' ' + c.definition)));
|
|
5304
|
+
if (!words.size) return { item: c, score: 0 };
|
|
5305
|
+
let score = 0;
|
|
5306
|
+
const moods = Array.isArray(mood) ? mood : (mood ? [mood] : []);
|
|
5307
|
+
for (const m of moods) {
|
|
5308
|
+
for (const term of (MOOD_TERMS[m] || [])) { if (words.has(term)) score += 12; }
|
|
5309
|
+
}
|
|
5310
|
+
for (const w of tokenize(subject)) { if (words.has(w)) score += 7; }
|
|
5311
|
+
for (const av of (Array.isArray(avoid) ? avoid : (avoid ? [avoid] : []))) {
|
|
5312
|
+
for (const term of (MOOD_TERMS[av] || [])) { if (words.has(term)) score -= 10; }
|
|
5313
|
+
}
|
|
5314
|
+
return { item: c, score };
|
|
5315
|
+
});
|
|
5316
|
+
|
|
5317
|
+
scored.sort((a, b) => {
|
|
5318
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
5319
|
+
const ha = tiebreak(a.item.key), hb = tiebreak(b.item.key);
|
|
5320
|
+
if (ha !== null && ha !== hb) return ha - hb;
|
|
5321
|
+
return a.item.key.localeCompare(b.item.key);
|
|
5322
|
+
});
|
|
5323
|
+
const top = scored.slice(0, 5).map(s => ({ ...s.item, score: s.score }));
|
|
5324
|
+
return {
|
|
5325
|
+
success: true,
|
|
5326
|
+
action: 'choose',
|
|
5327
|
+
recommendation: top[0] || null,
|
|
5328
|
+
topCandidates: top,
|
|
5329
|
+
// A chooser scores on description, so entries without one can never be
|
|
5330
|
+
// recommended. Saying so beats silently never offering them.
|
|
5331
|
+
unrankable: report ? report.unrankable : undefined
|
|
5332
|
+
};
|
|
5333
|
+
}
|
|
5334
|
+
|
|
5335
|
+
return { success: false, error: 'Unknown capabilities action: ' + action };
|
|
5336
|
+
})();
|
|
5337
|
+
`.trim();
|
|
5338
|
+
}
|
|
4690
5339
|
// ===========================================================================
|
|
4691
5340
|
// DEFORMATION
|
|
4692
5341
|
// ===========================================================================
|
|
@@ -5709,6 +6358,178 @@ ${guard}
|
|
|
5709
6358
|
palettes: typeof app.listTextPalettes === 'function' ? app.listTextPalettes() : [],
|
|
5710
6359
|
fontAxes: typeof app.listFontAxes === 'function' ? app.listFontAxes() : null,
|
|
5711
6360
|
};
|
|
6361
|
+
})();`.trim();
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
/**
|
|
6366
|
+
* pinepaper_text_effect — the 37 character-level text effects.
|
|
6367
|
+
*
|
|
6368
|
+
* `applyTextEffect` is ASYNC (it lazy-`import()`s the effects chunk), so this
|
|
6369
|
+
* emits an async IIFE — a sync wrapper would hand the bridge a pending
|
|
6370
|
+
* Promise instead of the result.
|
|
6371
|
+
*
|
|
6372
|
+
* The returned `items` are live Paper objects and cannot cross the bridge;
|
|
6373
|
+
* only ids/count/duration are forwarded. `ids` is the whole handle set the
|
|
6374
|
+
* caller now has, since the source item is gone.
|
|
6375
|
+
*/
|
|
6376
|
+
/**
|
|
6377
|
+
* The medium axis. `list_media` and `resolve` are READS — they exist so an
|
|
6378
|
+
* agent can find out what this build can honestly make before it promises a
|
|
6379
|
+
* medium in prose, which is the failure the axis was built to prevent.
|
|
6380
|
+
*/
|
|
6381
|
+
generateDesignMedium(input) {
|
|
6382
|
+
const S = (v) => JSON.stringify(v);
|
|
6383
|
+
const wrap = (comment, body) => `\n// ${comment}\n(function() {\n${body}\n})();`.trim();
|
|
6384
|
+
switch (input.action) {
|
|
6385
|
+
case 'list_media':
|
|
6386
|
+
return wrap('Medium: list', ` if (typeof app.listDesignMedia !== 'function') { return { success: false, error: 'medium axis unavailable — update FxTool' }; }
|
|
6387
|
+
const media = app.listDesignMedia();
|
|
6388
|
+
return { success: true, action: 'list_media', media: media, native: media.filter(function(m) { return m.fidelity === 'native'; }).map(function(m) { return m.key; }) };`);
|
|
6389
|
+
case 'resolve':
|
|
6390
|
+
return wrap('Medium: resolve', ` if (typeof app.resolveDesignMedium !== 'function') { return { success: false, error: 'medium axis unavailable — update FxTool' }; }
|
|
6391
|
+
const v = app.resolveDesignMedium(${S(input.medium)});
|
|
6392
|
+
return Object.assign({ success: true, action: 'resolve' }, v);`);
|
|
6393
|
+
case 'list_stitches':
|
|
6394
|
+
return wrap('Medium: list stitches', ` if (typeof app.listStitches !== 'function') { return { success: false, error: 'thread medium unavailable — update FxTool' }; }
|
|
6395
|
+
const stitches = app.listStitches();
|
|
6396
|
+
return { success: true, action: 'list_stitches', stitches: stitches, count: stitches.length };`);
|
|
6397
|
+
case 'apply_thread': {
|
|
6398
|
+
const opts = S({
|
|
6399
|
+
...(input.stitch !== undefined ? { stitch: input.stitch } : {}),
|
|
6400
|
+
...(input.field !== undefined ? { field: input.field } : {}),
|
|
6401
|
+
...(input.stitchLen !== undefined ? { stitchLen: input.stitchLen } : {}),
|
|
6402
|
+
...(input.rowGap !== undefined ? { rowGap: input.rowGap } : {}),
|
|
6403
|
+
...(input.variance !== undefined ? { variance: input.variance } : {}),
|
|
6404
|
+
...(input.width !== undefined ? { width: input.width } : {}),
|
|
6405
|
+
...(input.sheen !== undefined ? { sheen: input.sheen } : {}),
|
|
6406
|
+
...(input.color !== undefined ? { color: input.color } : {}),
|
|
6407
|
+
...(input.seed !== undefined ? { seed: input.seed } : {}),
|
|
6408
|
+
...(input.count !== undefined ? { count: input.count } : {}),
|
|
6409
|
+
});
|
|
6410
|
+
return wrap('Medium: render in thread', ` if (typeof app.applyThreadPainting !== 'function') { return { success: false, error: 'thread medium unavailable — update FxTool' }; }
|
|
6411
|
+
const entry = app.itemRegistry && app.itemRegistry.get(${S(input.itemId)});
|
|
6412
|
+
const src = entry && entry.item;
|
|
6413
|
+
if (!src) { return { success: false, action: 'apply_thread', error: 'no item ' + ${S(input.itemId)} }; }
|
|
6414
|
+
// Check the shape BEFORE calling, so the refusal can name the fix. The
|
|
6415
|
+
// engine warns to a console production strips, which an agent never sees.
|
|
6416
|
+
if (src.content !== undefined && typeof src.getPointAt !== 'function') {
|
|
6417
|
+
return { success: false, action: 'apply_thread', error: 'a text item has no outline to stitch. Convert it to glyph paths first (pinepaper_text_style), then stitch the result — the collage it produces is a group of closed paths, which this accepts.' };
|
|
6418
|
+
}
|
|
6419
|
+
const g = app.applyThreadPainting(${S(input.itemId)}, ${opts});
|
|
6420
|
+
if (!g) { return { success: false, action: 'apply_thread', error: 'nothing stitchable here — thread needs a CLOSED path, a compound path, or a group containing them. An open stroke, a raster and an empty group all land here. (If you passed a stitch name, check it against list_stitches.)' }; }
|
|
6421
|
+
return { success: true, action: 'apply_thread', groupId: (g.data && g.data.id) || null, stitches: g.children.length };`);
|
|
6422
|
+
}
|
|
6423
|
+
case 'list_flow_fields':
|
|
6424
|
+
return wrap('Medium: list flow fields', ` if (typeof app.listFlowFields !== 'function') { return { success: false, error: 'hatch medium unavailable — update FxTool' }; }
|
|
6425
|
+
const fields = app.listFlowFields();
|
|
6426
|
+
return { success: true, action: 'list_flow_fields', fields: fields, count: fields.length };`);
|
|
6427
|
+
case 'list_hatch_options':
|
|
6428
|
+
return wrap('Medium: hatch defaults', ` if (typeof app.listHatchOptions !== 'function') { return { success: false, error: 'hatch medium unavailable — update FxTool' }; }
|
|
6429
|
+
return { success: true, action: 'list_hatch_options', defaults: app.listHatchOptions() };`);
|
|
6430
|
+
case 'apply_hatch': {
|
|
6431
|
+
const opts = S({
|
|
6432
|
+
...(input.distance !== undefined ? { distance: input.distance } : {}),
|
|
6433
|
+
...(input.angle !== undefined ? { angle: input.angle } : {}),
|
|
6434
|
+
...(input.gradient !== undefined ? { gradient: input.gradient } : {}),
|
|
6435
|
+
...(input.rand !== undefined ? { rand: input.rand } : {}),
|
|
6436
|
+
...(input.continuous !== undefined ? { continuous: input.continuous } : {}),
|
|
6437
|
+
...(input.flowField !== undefined ? { field: input.flowField } : {}),
|
|
6438
|
+
...(input.t !== undefined ? { t: input.t } : {}),
|
|
6439
|
+
...(input.width !== undefined ? { width: input.width } : {}),
|
|
6440
|
+
...(input.color !== undefined ? { color: input.color } : {}),
|
|
6441
|
+
...(input.seed !== undefined ? { seed: input.seed } : {}),
|
|
6442
|
+
});
|
|
6443
|
+
return wrap('Medium: rule an item with hatching', ` if (typeof app.applyHatching !== 'function') { return { success: false, error: 'hatch medium unavailable — update FxTool' }; }
|
|
6444
|
+
const entry = app.itemRegistry && app.itemRegistry.get(${S(input.itemId)});
|
|
6445
|
+
const src = entry && entry.item;
|
|
6446
|
+
if (!src) { return { success: false, action: 'apply_hatch', error: 'no item ' + ${S(input.itemId)} }; }
|
|
6447
|
+
// Every refusal below is one the engine only console.warns, and production
|
|
6448
|
+
// strips the console — so an agent would otherwise get a bare null and no
|
|
6449
|
+
// idea which of five different things went wrong.
|
|
6450
|
+
if (src.content !== undefined && typeof src.getPointAt !== 'function') {
|
|
6451
|
+
return { success: false, action: 'apply_hatch', error: 'a text item has no outline to hatch. Convert it to glyph paths first (pinepaper_text_style), then hatch the result — the collage it produces is a group of closed paths, which this accepts.' };
|
|
6452
|
+
}
|
|
6453
|
+
// Count the closed paths BEFORE the call, so a truncated group can be
|
|
6454
|
+
// reported as truncated rather than looking complete.
|
|
6455
|
+
let closed = 0;
|
|
6456
|
+
const isGroup = src.children && !(typeof src.getPointAt === 'function' && src.closed);
|
|
6457
|
+
if (isGroup) {
|
|
6458
|
+
const walk = function(n) {
|
|
6459
|
+
for (const c of (n.children || [])) {
|
|
6460
|
+
if (c.children && typeof c.getPointAt !== 'function') walk(c);
|
|
6461
|
+
else if (typeof c.getPointAt === 'function' && c.closed && c.length > 0) closed++;
|
|
6462
|
+
}
|
|
6463
|
+
};
|
|
6464
|
+
walk(src);
|
|
6465
|
+
if (!closed) { return { success: false, action: 'apply_hatch', error: 'that group contains no closed paths to hatch — hatching needs a closed outline, and open strokes, rasters and empty groups all land here.' }; }
|
|
6466
|
+
}
|
|
6467
|
+
const b = src.bounds;
|
|
6468
|
+
const g = app.applyHatching(${S(input.itemId)}, ${opts});
|
|
6469
|
+
if (!g) {
|
|
6470
|
+
// Spacing wider than the shape is the usual cause and is otherwise a
|
|
6471
|
+
// silent no-op: the item just vanishes behind an empty group.
|
|
6472
|
+
return { success: false, action: 'apply_hatch', error: 'nothing was ruled. Most often the spacing exceeds the shape — distance is ' + ${S(input.distance ?? 6)} + 'px against bounds ' + Math.round(b.width) + 'x' + Math.round(b.height) + '. Otherwise the item has no closed outline: hatching needs a closed path, a compound path, or a group of them.' };
|
|
6473
|
+
}
|
|
6474
|
+
const parts = (g.data && g.data.parts) || null;
|
|
6475
|
+
return {
|
|
6476
|
+
success: true,
|
|
6477
|
+
action: 'apply_hatch',
|
|
6478
|
+
groupId: (g.data && g.data.id) || null,
|
|
6479
|
+
lines: g.children ? g.children.length : 0,
|
|
6480
|
+
angle: (g.data && g.data.angle) !== undefined ? g.data.angle : null,
|
|
6481
|
+
spacing: (g.data && g.data.spacing) !== undefined ? g.data.spacing : null,
|
|
6482
|
+
// The engine hatches at most 40 paths from a group and warns about the rest.
|
|
6483
|
+
...(parts !== null ? { parts: parts } : {}),
|
|
6484
|
+
...(closed > 40 ? { truncated: 'hatched the first 40 of ' + closed + ' closed paths' } : {}),
|
|
6485
|
+
// Re-hatching REPLACES: a second set of lines over the first is
|
|
6486
|
+
// indistinguishable from nothing having happened, so trying spacings in
|
|
6487
|
+
// turn is safe and is the flow this exists for.
|
|
6488
|
+
note: 'the source item is now hidden behind the hatching; re-hatching replaces rather than stacks',
|
|
6489
|
+
};`);
|
|
6490
|
+
}
|
|
6491
|
+
default:
|
|
6492
|
+
return wrap('Medium: unknown action', ` return { success: false, error: 'unknown action' };`);
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
generateTextEffect(input) {
|
|
6496
|
+
const S = (v) => JSON.stringify(v);
|
|
6497
|
+
switch (input.action) {
|
|
6498
|
+
case 'apply': {
|
|
6499
|
+
// options spreads FIRST so the named fields win on collision — the
|
|
6500
|
+
// schema advertises that precedence, so it has to be real here.
|
|
6501
|
+
const opts = S({
|
|
6502
|
+
...(input.options ?? {}),
|
|
6503
|
+
...(input.duration !== undefined ? { duration: input.duration } : {}),
|
|
6504
|
+
...(input.seed !== undefined ? { seed: input.seed } : {}),
|
|
6505
|
+
...(input.gradient !== undefined ? { gradient: input.gradient } : {}),
|
|
6506
|
+
...(input.gradientStops !== undefined ? { gradientStops: input.gradientStops } : {}),
|
|
6507
|
+
...(input.gradientDirection !== undefined ? { gradientDirection: input.gradientDirection } : {}),
|
|
6508
|
+
...(input.gradientSteps !== undefined ? { gradientSteps: input.gradientSteps } : {}),
|
|
6509
|
+
...(input.keepSource !== undefined ? { keepSource: input.keepSource } : {}),
|
|
6510
|
+
});
|
|
6511
|
+
return `
|
|
6512
|
+
// Text effect: ${input.effect} — the text becomes one animated item PER CHARACTER
|
|
6513
|
+
(async function() {
|
|
6514
|
+
if (typeof app.applyTextEffect !== 'function') { return { success: false, error: 'app.applyTextEffect unavailable — update FxTool to a text-effects build' }; }
|
|
6515
|
+
const r = await app.applyTextEffect(${S(input.itemId)}, ${S(input.effect)}, ${opts});
|
|
6516
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'text effect failed' }; }
|
|
6517
|
+
// r.items are live Paper objects — NOT serializable, deliberately dropped.
|
|
6518
|
+
// The source item id is GONE unless keepSource was set, so r.ids is now the
|
|
6519
|
+
// caller's entire handle set; returning it is what makes the tool usable.
|
|
6520
|
+
return {
|
|
6521
|
+
success: true, action: 'apply', effect: r.effect, ids: r.ids, count: r.count,
|
|
6522
|
+
duration: r.duration, sourceRemoved: ${S(input.keepSource !== true)},
|
|
6523
|
+
};
|
|
6524
|
+
})();`.trim();
|
|
6525
|
+
}
|
|
6526
|
+
case 'list': {
|
|
6527
|
+
return `
|
|
6528
|
+
// The 37 character-level text effects — the picker surface
|
|
6529
|
+
(async function() {
|
|
6530
|
+
if (typeof app.listTextEffects !== 'function') { return { success: false, error: 'app.listTextEffects unavailable — update FxTool to a text-effects build' }; }
|
|
6531
|
+
const effects = await app.listTextEffects();
|
|
6532
|
+
return { success: true, effects, count: effects.length };
|
|
5712
6533
|
})();`.trim();
|
|
5713
6534
|
}
|
|
5714
6535
|
}
|
|
@@ -5966,7 +6787,8 @@ ${needWorld}
|
|
|
5966
6787
|
const root = input.rootPosition ? S(input.rootPosition) : S({ x: 400, y: 300 });
|
|
5967
6788
|
return wrap('Rigging: create skeleton', ` const skeletonId = R.createSkeleton(${S(input.name ?? null)}, ${root});
|
|
5968
6789
|
if (app.historyManager) app.historyManager.saveState();
|
|
5969
|
-
return { success:
|
|
6790
|
+
if (!skeletonId) { return { success: false, action: 'create_skeleton', error: "the engine returned no skeleton id — createSkeleton does not normally fail, so treat this as a broken rigging subsystem rather than bad input" }; }
|
|
6791
|
+
return { success: true, action: 'create_skeleton', skeletonId: skeletonId };`);
|
|
5970
6792
|
}
|
|
5971
6793
|
case 'add_bone': {
|
|
5972
6794
|
const config = S({
|
|
@@ -5979,13 +6801,15 @@ ${needWorld}
|
|
|
5979
6801
|
});
|
|
5980
6802
|
return wrap('Rigging: add bone', ` const boneId = R.addBone(${S(input.skeletonId)}, ${config});
|
|
5981
6803
|
if (app.historyManager) app.historyManager.saveState();
|
|
5982
|
-
return { success:
|
|
6804
|
+
if (!boneId) { return { success: false, action: 'add_bone', error: "no such skeleton. Call list_skeletons for the ids that exist" }; }
|
|
6805
|
+
return { success: true, action: 'add_bone', boneId: boneId };`);
|
|
5983
6806
|
}
|
|
5984
6807
|
case 'attach_item': {
|
|
5985
6808
|
const opts = S(input.attachPoint !== undefined ? { attachPoint: input.attachPoint } : {});
|
|
5986
6809
|
return wrap('Rigging: attach item to bone', ` const ok = R.attachItem(${S(input.skeletonId)}, ${S(input.boneId)}, ${S(input.itemId)}, ${opts});
|
|
5987
6810
|
if (app.historyManager) app.historyManager.saveState();
|
|
5988
|
-
return { success:
|
|
6811
|
+
if (!ok) { return { success: false, action: 'attach_item', error: "no such skeleton, or no such bone on it. list_bones gives the bone ids for a skeleton" }; }
|
|
6812
|
+
return { success: true, action: 'attach_item', itemId: ${S(input.itemId)}, boneId: ${S(input.boneId)} };`);
|
|
5989
6813
|
}
|
|
5990
6814
|
case 'create_ik_chain': {
|
|
5991
6815
|
const config = S({
|
|
@@ -5999,7 +6823,8 @@ ${needWorld}
|
|
|
5999
6823
|
});
|
|
6000
6824
|
return wrap('Rigging: create IK chain', ` const chainId = R.createIKChain(${S(input.skeletonId)}, ${config});
|
|
6001
6825
|
if (app.historyManager) app.historyManager.saveState();
|
|
6002
|
-
return { success:
|
|
6826
|
+
if (!chainId) { return { success: false, action: 'create_ik_chain', error: "no such skeleton, or fewer than 2 boneIds — a chain needs at least two bones to solve. Call list_bones for the ids on this rig" }; }
|
|
6827
|
+
return { success: true, action: 'create_ik_chain', chainId: chainId };`);
|
|
6003
6828
|
}
|
|
6004
6829
|
case 'add_pose_keyframe': {
|
|
6005
6830
|
const opts = S({
|
|
@@ -6012,7 +6837,8 @@ ${needWorld}
|
|
|
6012
6837
|
});
|
|
6013
6838
|
return wrap('Rigging: add pose keyframe', ` const ok = R.addPoseKeyframe(${S(input.skeletonId)}, ${input.time}, ${S(input.pose)}, ${S(input.easing ?? 'linear')}, ${opts});
|
|
6014
6839
|
if (app.historyManager) app.historyManager.saveState();
|
|
6015
|
-
return { success:
|
|
6840
|
+
if (!ok) { return { success: false, action: 'add_pose_keyframe', error: "no such skeleton, or 'pose' named a saved pose that does not exist. Pass an inline { boneId: angleDeg } map instead, or list_poses for the saved ones" }; }
|
|
6841
|
+
return { success: true, action: 'add_pose_keyframe', time: ${input.time} };`);
|
|
6016
6842
|
}
|
|
6017
6843
|
case 'set_target_path': {
|
|
6018
6844
|
const opts = S({
|
|
@@ -6021,16 +6847,170 @@ ${needWorld}
|
|
|
6021
6847
|
});
|
|
6022
6848
|
return wrap('Rigging: set IK target path', ` const ok = R.setTargetPath(${S(input.skeletonId)}, ${S(input.chainId)}, ${S(input.waypoints)}, ${opts});
|
|
6023
6849
|
if (app.historyManager) app.historyManager.saveState();
|
|
6024
|
-
return { success:
|
|
6850
|
+
if (!ok) { return { success: false, action: 'set_target_path', error: "no such skeleton, no such IK chain on it, or waypoints was not an array. create_ik_chain returns the chainId to use here" }; }
|
|
6851
|
+
return { success: true, action: 'set_target_path', chainId: ${S(input.chainId)} };`);
|
|
6025
6852
|
}
|
|
6026
6853
|
case 'save_pose':
|
|
6027
6854
|
return wrap('Rigging: save pose', ` const poseId = R.savePose(${S(input.skeletonId)}, ${S(input.name ?? null)});
|
|
6028
6855
|
if (app.historyManager) app.historyManager.saveState();
|
|
6029
|
-
return { success:
|
|
6856
|
+
if (!poseId) { return { success: false, action: 'save_pose', error: "no such skeleton. A pose is a snapshot of ITS bone angles, so the skeleton has to exist first — call list_skeletons" }; }
|
|
6857
|
+
return { success: true, action: 'save_pose', poseId: poseId };`);
|
|
6030
6858
|
case 'save_shape_key':
|
|
6031
6859
|
return wrap('Rigging: save shape key', ` const shapeKeyId = R.saveShapeKey(${S(input.skeletonId)}, ${S(input.name ?? null)});
|
|
6032
6860
|
if (app.historyManager) app.historyManager.saveState();
|
|
6033
|
-
return { success:
|
|
6861
|
+
if (!shapeKeyId) { return { success: false, action: 'save_shape_key', error: "no such skeleton, or nothing is attached to its bones — a shape key snapshots the attached items, so a bare skeleton has nothing to capture" }; }
|
|
6862
|
+
return { success: true, action: 'save_shape_key', shapeKeyId: shapeKeyId };`);
|
|
6863
|
+
// ── Pose motion. Every case below maps 1:1 to a method that exists on
|
|
6864
|
+
// riggingSystem (or, for stitch_poses, a PinePaper facade) — checked
|
|
6865
|
+
// against the engine source, not against the docs, because the docs have
|
|
6866
|
+
// named tools that were never registered before now.
|
|
6867
|
+
case 'list_skeletons':
|
|
6868
|
+
return wrap('Rigging: list skeletons', ` const skeletons = R.listSkeletons();
|
|
6869
|
+
return { success: true, action: 'list_skeletons', skeletons: skeletons, count: skeletons.length };`);
|
|
6870
|
+
case 'list_bones':
|
|
6871
|
+
// Without this an inline pose is unwritable: a pose is
|
|
6872
|
+
// { boneId: angleDeg } and nothing else returns a bone id.
|
|
6873
|
+
return wrap('Rigging: list bones', ` if (typeof R.listBones !== 'function') { return { success: false, error: 'listBones unavailable — update FxTool' }; }
|
|
6874
|
+
const bones = R.listBones(${S(input.skeletonId)});
|
|
6875
|
+
return { success: true, action: 'list_bones', bones: bones, count: bones.length };`);
|
|
6876
|
+
case 'list_pose_libraries':
|
|
6877
|
+
return wrap('Rigging: list pose libraries', ` return (async function() {
|
|
6878
|
+
if (typeof R.ensurePoseLibraries !== 'function') { return { success: false, error: 'pose libraries unavailable — update FxTool' }; }
|
|
6879
|
+
const names = await R.ensurePoseLibraries();
|
|
6880
|
+
return { success: true, action: 'list_pose_libraries', libraries: names.libraries, transitions: names.transitions };
|
|
6881
|
+
})();`);
|
|
6882
|
+
case 'load_pose_library': {
|
|
6883
|
+
// The libraries used to load only from an editor button, so every
|
|
6884
|
+
// agent ran with none and got null from every preset name. Awaited
|
|
6885
|
+
// here so the guarantee is real rather than dependent on whether a
|
|
6886
|
+
// human happened to click something earlier in the session.
|
|
6887
|
+
const map = input.boneMap ? S(input.boneMap) : 'null';
|
|
6888
|
+
return wrap('Rigging: load a stock pose library', ` return (async function() {
|
|
6889
|
+
if (typeof R.ensurePoseLibraries !== 'function') { return { success: false, error: 'pose libraries unavailable — update FxTool' }; }
|
|
6890
|
+
const avail = await R.ensurePoseLibraries();
|
|
6891
|
+
let boneMap = ${map};
|
|
6892
|
+
if (!boneMap) {
|
|
6893
|
+
boneMap = {};
|
|
6894
|
+
for (const b of (R.listBones ? R.listBones(${S(input.skeletonId)}) : [])) { if (b.name) boneMap[b.name] = b.id; }
|
|
6895
|
+
}
|
|
6896
|
+
const res = R.loadPoseLibrary(${S(input.skeletonId)}, ${S(input.libraryName)}, boneMap);
|
|
6897
|
+
if (!res) { return { success: false, action: 'load_pose_library', error: 'no such library, or none of its bone names matched this rig. Available: ' + avail.libraries.join(', '), available: avail.libraries }; }
|
|
6898
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
6899
|
+
return { success: true, action: 'load_pose_library', library: ${S(input.libraryName)}, poses: res };
|
|
6900
|
+
})();`);
|
|
6901
|
+
}
|
|
6902
|
+
case 'list_poses':
|
|
6903
|
+
return wrap('Rigging: list poses', ` const poses = R.listPoses(${S(input.skeletonId ?? null)});
|
|
6904
|
+
return { success: true, action: 'list_poses', poses: poses, count: poses.length };`);
|
|
6905
|
+
case 'load_pose':
|
|
6906
|
+
return wrap('Rigging: load pose', ` const ok = R.loadPose(${S(input.poseId)});
|
|
6907
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
6908
|
+
if (!ok) { return { success: false, action: 'load_pose', error: "no such pose, or the skeleton it was saved against is gone. list_poses for what is still loadable" }; }
|
|
6909
|
+
return { success: true, action: 'load_pose', poseId: ${S(input.poseId)} };`);
|
|
6910
|
+
case 'interpolate_poses':
|
|
6911
|
+
return wrap('Rigging: interpolate poses', ` const ok = R.interpolatePoses(${S(input.poseIdA)}, ${S(input.poseIdB)}, ${input.t ?? 0.5});
|
|
6912
|
+
if (!ok) { return { success: false, action: 'interpolate_poses', error: "one of the two poses does not exist, or they belong to DIFFERENT skeletons — blending across rigs is not defined. Call list_poses to see which skeleton each belongs to" }; }
|
|
6913
|
+
return { success: true, action: 'interpolate_poses', t: ${input.t ?? 0.5} };`);
|
|
6914
|
+
case 'play_pose_sequence': {
|
|
6915
|
+
const opts = S({
|
|
6916
|
+
...(input.loop !== undefined ? { loop: input.loop } : {}),
|
|
6917
|
+
...(input.options ?? {}),
|
|
6918
|
+
});
|
|
6919
|
+
return wrap('Rigging: play pose sequence', ` const ok = R.playPoseSequence(${S(input.skeletonId)}, ${S(input.sequence ?? [])}, ${opts});
|
|
6920
|
+
if (!ok) { return { success: false, action: 'play_pose_sequence', error: "no such skeleton, or the sequence was empty. Each key needs a 'pose' id or an inline 'angles' map — call list_poses for the saved ids" }; }
|
|
6921
|
+
return { success: true, action: 'play_pose_sequence', keys: ${(input.sequence ?? []).length} };`);
|
|
6922
|
+
}
|
|
6923
|
+
case 'stop_pose_sequence':
|
|
6924
|
+
return wrap('Rigging: stop pose sequence', ` const ok = R.stopPoseSequence(${S(input.skeletonId)});
|
|
6925
|
+
if (!ok) { return { success: false, action: 'stop_pose_sequence', error: "nothing was playing on that skeleton — this is a no-op rather than a failure" }; }
|
|
6926
|
+
return { success: true, action: 'stop_pose_sequence' };`);
|
|
6927
|
+
case 'stitch_poses': {
|
|
6928
|
+
// The one action on `app` rather than riggingSystem: stitching resolves
|
|
6929
|
+
// saved pose ids and then installs the merged sequence through the
|
|
6930
|
+
// ordinary player, so there is one code path and one thing to scrub.
|
|
6931
|
+
const opts = S({
|
|
6932
|
+
...(input.blend !== undefined ? { blend: input.blend } : {}),
|
|
6933
|
+
...(input.blendSteps !== undefined ? { blendSteps: input.blendSteps } : {}),
|
|
6934
|
+
...(input.matchPhase !== undefined ? { matchPhase: input.matchPhase } : {}),
|
|
6935
|
+
...(input.loop !== undefined ? { loop: input.loop } : {}),
|
|
6936
|
+
...(input.plan !== undefined ? { plan: input.plan } : {}),
|
|
6937
|
+
...(input.options ?? {}),
|
|
6938
|
+
});
|
|
6939
|
+
return wrap('Rigging: stitch pose clips', ` if (typeof app.stitchPoses !== 'function') { return { success: false, error: 'app.stitchPoses unavailable — update FxTool' }; }
|
|
6940
|
+
const res = app.stitchPoses(${S(input.skeletonId)}, ${S(input.clips ?? [])}, ${opts});
|
|
6941
|
+
return Object.assign({ success: !!res.ok, action: 'stitch_poses' }, res);`);
|
|
6942
|
+
}
|
|
6943
|
+
case 'apply_pose_transition':
|
|
6944
|
+
return wrap('Rigging: apply pose transition', ` return (async function() {
|
|
6945
|
+
if (typeof R.ensurePoseLibraries === 'function') { await R.ensurePoseLibraries(); }
|
|
6946
|
+
// A transition names POSES BY NAME, so it needs name → id. Requiring the
|
|
6947
|
+
// caller to build that by hand from list_poses is busywork with one
|
|
6948
|
+
// correct answer, and getting it wrong returns a bare null. Build it here
|
|
6949
|
+
// when it is omitted, exactly as load_pose_library builds its bone map.
|
|
6950
|
+
let poseIdMap = ${input.poseIdMap ? S(input.poseIdMap) : 'null'};
|
|
6951
|
+
if (!poseIdMap || !Object.keys(poseIdMap).length) {
|
|
6952
|
+
poseIdMap = {};
|
|
6953
|
+
for (const pose of (R.listPoses ? R.listPoses(${S(input.skeletonId)}) : [])) { if (pose.name) poseIdMap[pose.name] = pose.id; }
|
|
6954
|
+
}
|
|
6955
|
+
const res = R.applyPoseTransition(${S(input.skeletonId)}, ${S(input.transitionName)}, poseIdMap);
|
|
6956
|
+
if (!res) { return { success: false, action: 'apply_pose_transition', error: "no such transition, or none of its pose names are saved on this skeleton. Load the matching library first (load_pose_library), then retry — the transition plays poses BY NAME." }; }
|
|
6957
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
6958
|
+
return { success: true, action: 'apply_pose_transition', transition: ${S(input.transitionName)} };
|
|
6959
|
+
})();`);
|
|
6960
|
+
case 'auto_walk':
|
|
6961
|
+
case 'auto_breath':
|
|
6962
|
+
case 'auto_idle':
|
|
6963
|
+
case 'auto_jump': {
|
|
6964
|
+
const method = { auto_walk: 'autoWalk', auto_breath: 'autoBreath', auto_idle: 'autoIdle', auto_jump: 'autoJump' }[input.action];
|
|
6965
|
+
// These return null when the rig has no bone the preset recognises BY
|
|
6966
|
+
// NAME, which is a real refusal and not an error — but `result: null`
|
|
6967
|
+
// tells an agent nothing, and a silent nothing is what sends it into a
|
|
6968
|
+
// retry loop. Say which names each one looks for.
|
|
6969
|
+
const needs = {
|
|
6970
|
+
// autoWalk cycles through SAVED poses named walk_00…walk_07 or
|
|
6971
|
+
// walk_contact_L/walk_passing_L/… — the ones the `humanoid` library
|
|
6972
|
+
// saves. It is not a procedural generator, it is a pose player, and
|
|
6973
|
+
// saying "needs a skeleton" when the skeleton is right there sends an
|
|
6974
|
+
// agent looking in the wrong place.
|
|
6975
|
+
auto_walk: "at least 2 saved walk poses (walk_00…, or walk_contact_L / walk_passing_L / walk_contact_R / walk_passing_R). Run load_pose_library with libraryName 'humanoid' (or 'quadruped') first — it saves exactly those",
|
|
6976
|
+
auto_breath: 'a bone named spine, chest, upper_spine or body',
|
|
6977
|
+
auto_idle: 'a bone named head, hip, upper_hub or spine',
|
|
6978
|
+
auto_jump: 'a skeleton, and saved jump poses if you want a posed jump rather than a root arc',
|
|
6979
|
+
};
|
|
6980
|
+
return wrap(`Rigging: ${input.action}`, ` const res = R.${method}(${S(input.skeletonId)}, ${S(input.options ?? {})});
|
|
6981
|
+
if (res && app.historyManager) app.historyManager.saveState();
|
|
6982
|
+
if (!res) { return { success: false, action: ${S(input.action)}, error: ${S(`refused — needs ${needs[input.action]}.`)} }; }
|
|
6983
|
+
return { success: true, action: ${S(input.action)}, result: res };`);
|
|
6984
|
+
}
|
|
6985
|
+
case 'move_root':
|
|
6986
|
+
return wrap('Rigging: root locomotion track', ` const ok = R.moveRoot(${S(input.skeletonId)}, ${S(input.keyframes ?? [])}, ${S(input.options ?? {})});
|
|
6987
|
+
if (!ok) { return { success: false, action: 'move_root', error: "no such skeleton, or keyframes was empty. Each key is { t, x, y } with t in SCENE SECONDS" }; }
|
|
6988
|
+
return { success: true, action: 'move_root', keys: ${(input.keyframes ?? []).length} };`);
|
|
6989
|
+
case 'stop_root_track':
|
|
6990
|
+
return wrap('Rigging: stop root track', ` const ok = R.stopRootTrack(${S(input.skeletonId)});
|
|
6991
|
+
if (!ok) { return { success: false, action: 'stop_root_track', error: "no root track was running on that skeleton — this is a no-op rather than a failure" }; }
|
|
6992
|
+
return { success: true, action: 'stop_root_track' };`);
|
|
6993
|
+
case 'add_secondary_motion':
|
|
6994
|
+
return wrap('Rigging: secondary motion', ` const res = R.addSecondaryMotion(${S(input.skeletonId)}, ${S(input.boneNames ?? [])}, ${S(input.options ?? {})});
|
|
6995
|
+
if (!res) { return { success: false, action: 'add_secondary_motion', error: "no such skeleton, or fewer than 2 of the given bone NAMES resolve on it. A spring chain needs at least two; list_bones for the names this rig uses" }; }
|
|
6996
|
+
return { success: true, action: 'add_secondary_motion', bones: ${(input.boneNames ?? []).length} };`);
|
|
6997
|
+
case 'skin_path':
|
|
6998
|
+
return wrap('Rigging: skin path to bones', ` const res = R.skinPath(${S(input.skeletonId)}, ${S(input.itemId)}, ${S(input.options ?? {})});
|
|
6999
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
7000
|
+
if (!res) { return { success: false, action: 'skin_path', error: "no such skeleton or item, the item is not a path, or no bone is close enough to influence it. Skinning binds vertices to nearby bones, so the path has to overlap the rig — call list_bones to see where they are" }; }
|
|
7001
|
+
return { success: true, action: 'skin_path', itemId: ${S(input.itemId)} };`);
|
|
7002
|
+
case 'bake_animation':
|
|
7003
|
+
return wrap('Rigging: bake animation to keyframes', ` const res = R.bakeAnimation(${S(input.skeletonId)}, ${S(input.options ?? {})});
|
|
7004
|
+
if (res && app.historyManager) app.historyManager.saveState();
|
|
7005
|
+
if (!res) { return { success: false, action: 'bake_animation', error: "nothing to bake — no such skeleton, or no items are ATTACHED to its bones. Baking writes keyframes onto the attached items, so a bare skeleton has no output." }; }
|
|
7006
|
+
return { success: true, action: 'bake_animation', result: res };`);
|
|
7007
|
+
case 'list_shape_keys':
|
|
7008
|
+
return wrap('Rigging: list shape keys', ` const keys = R.listShapeKeys(${S(input.skeletonId)});
|
|
7009
|
+
return { success: true, action: 'list_shape_keys', shapeKeys: keys, count: keys.length };`);
|
|
7010
|
+
case 'load_shape_key':
|
|
7011
|
+
return wrap('Rigging: load shape key', ` const ok = R.loadShapeKey(${S(input.skeletonId)}, ${S(input.shapeKeyId)}, ${input.weight ?? 1});
|
|
7012
|
+
if (!ok) { return { success: false, action: 'load_shape_key', error: "no such skeleton, or no such shape key on it. list_shape_keys for what is saved" }; }
|
|
7013
|
+
return { success: true, action: 'load_shape_key', weight: ${input.weight ?? 1} };`);
|
|
6034
7014
|
// ── Mocap / rig import — these live on `app` (PinePaper facades), not
|
|
6035
7015
|
// riggingSystem, and they are async, so they get their own async IIFE
|
|
6036
7016
|
// instead of the sync `wrap` above. Each returns the facade's own result
|