@pinepaper.studio/mcp-server 1.5.5 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +71 -0
- package/README.es.md +71 -0
- package/README.fr.md +71 -0
- package/README.hi.md +71 -0
- package/README.ja.md +71 -0
- package/README.ko.md +71 -0
- package/README.md +513 -272
- package/README.pt-BR.md +71 -0
- package/README.zh-CN.md +71 -0
- package/assets/easing-splashes.svg +152 -0
- package/assets/equation-path.svg +77 -0
- package/assets/hero.svg +126 -0
- package/assets/live-badge.svg +62 -0
- package/assets/poster-sunburst.svg +106 -0
- package/dist/__tests__/fixtures/mock-relations.d.ts +48 -0
- package/dist/__tests__/fixtures/mock-relations.d.ts.map +1 -1
- package/dist/__tests__/fixtures/mock-relations.js +31 -0
- package/dist/__tests__/fixtures/mock-relations.js.map +1 -1
- package/dist/agent/types.d.ts +6 -6
- package/dist/browser/puppeteer-controller.d.ts +34 -0
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +116 -5
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/ontology/index.d.ts +2 -0
- package/dist/ontology/index.d.ts.map +1 -1
- package/dist/ontology/index.js +2 -0
- package/dist/ontology/index.js.map +1 -1
- package/dist/ontology/ontology-compiler.d.ts +78 -0
- package/dist/ontology/ontology-compiler.d.ts.map +1 -0
- package/dist/ontology/ontology-compiler.js +235 -0
- package/dist/ontology/ontology-compiler.js.map +1 -0
- package/dist/ontology/ontology-validator.d.ts +103 -0
- package/dist/ontology/ontology-validator.d.ts.map +1 -0
- package/dist/ontology/ontology-validator.js +573 -0
- package/dist/ontology/ontology-validator.js.map +1 -0
- package/dist/ontology/types.d.ts +1 -0
- package/dist/ontology/types.d.ts.map +1 -1
- package/dist/ontology/validator.d.ts +6 -0
- package/dist/ontology/validator.d.ts.map +1 -1
- package/dist/ontology/validator.js +34 -5
- package/dist/ontology/validator.js.map +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +91 -3
- package/dist/ontology/vocabulary.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 +978 -27
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/handlers/maps.d.ts.map +1 -1
- package/dist/tools/handlers/maps.js +54 -1
- package/dist/tools/handlers/maps.js.map +1 -1
- package/dist/tools/handlers/ontology.d.ts.map +1 -1
- package/dist/tools/handlers/ontology.js +15 -3
- package/dist/tools/handlers/ontology.js.map +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +137 -6
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +20 -1
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +25 -2
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +190 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1068 -30
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +2726 -116
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +594 -4
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +299 -71
- package/package.json +14 -3
|
@@ -16,10 +16,13 @@ CreateLetterCollageInputSchema, AnimateLetterCollageInputSchema,
|
|
|
16
16
|
LoadMapInputSchema, HighlightRegionsInputSchema, UnhighlightRegionsInputSchema, ApplyDataColorsInputSchema, AddMarkerInputSchema, AddMapLabelsInputSchema, PanMapInputSchema, ZoomMapInputSchema, ImportCustomMapInputSchema, GetRegionAtPointInputSchema,
|
|
17
17
|
// Map animation/CSV types
|
|
18
18
|
AnimateMapRegionsInputSchema, AnimateMapWaveInputSchema, StopMapAnimationsInputSchema, ExportMapRegionCSVInputSchema, ImportMapRegionCSVInputSchema, SelectMapRegionsInputSchema, DeselectMapRegionsInputSchema,
|
|
19
|
+
// Globe + world tour types
|
|
20
|
+
GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTourInputSchema, StopWorldTourInputSchema, PinToGlobeInputSchema, TourItemInputSchema,
|
|
19
21
|
// Custom relation/code types
|
|
20
22
|
RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
|
|
21
23
|
// Mask types
|
|
22
24
|
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, } from './schemas.js';
|
|
25
|
+
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
23
26
|
// =============================================================================
|
|
24
27
|
// HELPER FUNCTIONS
|
|
25
28
|
// =============================================================================
|
|
@@ -273,7 +276,9 @@ const relations = results.map(r => ({
|
|
|
273
276
|
/**
|
|
274
277
|
* Template for simple animation
|
|
275
278
|
*/
|
|
276
|
-
function generateAnimateCode(itemId, animationType, speed) {
|
|
279
|
+
function generateAnimateCode(itemId, animationType, speed, intensity, delay) {
|
|
280
|
+
const extra = (intensity !== undefined ? `,\n animationIntensity: ${intensity}` : '') +
|
|
281
|
+
(delay !== undefined ? `,\n animationDelay: ${delay}` : '');
|
|
277
282
|
return `
|
|
278
283
|
// Apply ${animationType} animation to ${itemId}
|
|
279
284
|
const item = app.getItemById('${itemId}');
|
|
@@ -282,7 +287,7 @@ if (!item) {
|
|
|
282
287
|
}
|
|
283
288
|
app.animate(item, {
|
|
284
289
|
animationType: '${animationType}',
|
|
285
|
-
animationSpeed: ${speed}
|
|
290
|
+
animationSpeed: ${speed}${extra}
|
|
286
291
|
});
|
|
287
292
|
|
|
288
293
|
({ success: true, itemId: '${itemId}', animationType: '${animationType}' });
|
|
@@ -307,7 +312,7 @@ function normalizeKeyframePositions(keyframes) {
|
|
|
307
312
|
/**
|
|
308
313
|
* Template for keyframe animation
|
|
309
314
|
*/
|
|
310
|
-
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint) {
|
|
315
|
+
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits) {
|
|
311
316
|
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
312
317
|
const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
|
|
313
318
|
const opts = { duration: calculatedDuration, loop };
|
|
@@ -317,6 +322,8 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
317
322
|
opts.clipInPoint = clipInPoint;
|
|
318
323
|
if (clipOutPoint !== undefined)
|
|
319
324
|
opts.clipOutPoint = clipOutPoint;
|
|
325
|
+
if (timeUnits !== undefined)
|
|
326
|
+
opts.timeUnits = timeUnits;
|
|
320
327
|
return `
|
|
321
328
|
// Apply keyframe animation to ${itemId}
|
|
322
329
|
app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
|
|
@@ -327,12 +334,15 @@ app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
|
|
|
327
334
|
/**
|
|
328
335
|
* Template for executing generators
|
|
329
336
|
*/
|
|
330
|
-
function generateExecuteGeneratorCode(generatorName, params) {
|
|
337
|
+
function generateExecuteGeneratorCode(generatorName, params, region) {
|
|
338
|
+
// FxTool signature: app.executeGenerator(name, params, options).
|
|
339
|
+
// region lives in options (added in FxTool c81781c).
|
|
340
|
+
const options = region ? { region } : {};
|
|
331
341
|
return `
|
|
332
|
-
// Execute ${generatorName} generator
|
|
342
|
+
// Execute ${generatorName} generator${region ? ` in region ${region.width}×${region.height} @ (${region.x},${region.y})` : ''}
|
|
333
343
|
(async function() {
|
|
334
|
-
await app.executeGenerator('${generatorName}', ${JSON.stringify(params, null, 2)});
|
|
335
|
-
return { success: true, generator: '${generatorName}' };
|
|
344
|
+
await app.executeGenerator('${generatorName}', ${JSON.stringify(params, null, 2)}, ${JSON.stringify(options)});
|
|
345
|
+
return { success: true, generator: '${generatorName}'${region ? ', region: ' + JSON.stringify(region) : ''} };
|
|
336
346
|
})();
|
|
337
347
|
`.trim();
|
|
338
348
|
}
|
|
@@ -401,12 +411,12 @@ const items = filtered.map(entry => ({
|
|
|
401
411
|
/**
|
|
402
412
|
* Template for timeline control
|
|
403
413
|
*/
|
|
404
|
-
function generatePlayTimelineCode(action, duration, loop, time) {
|
|
414
|
+
function generatePlayTimelineCode(action, duration, loop, time, deterministic) {
|
|
405
415
|
switch (action) {
|
|
406
416
|
case 'play':
|
|
407
417
|
return `
|
|
408
418
|
// Play keyframe timeline
|
|
409
|
-
app.playKeyframeTimeline(${duration || 5}, ${loop ?? false});
|
|
419
|
+
app.playKeyframeTimeline(${duration || 5}, ${loop ?? false}, { timeUnits: 'seconds' });
|
|
410
420
|
({ success: true, action: 'play', duration: ${duration || 5}, loop: ${loop ?? false} });
|
|
411
421
|
`.trim();
|
|
412
422
|
case 'pause':
|
|
@@ -422,6 +432,18 @@ app.stopKeyframeTimeline();
|
|
|
422
432
|
({ success: true, action: 'stop' });
|
|
423
433
|
`.trim();
|
|
424
434
|
case 'seek':
|
|
435
|
+
if (deterministic) {
|
|
436
|
+
return `
|
|
437
|
+
// Deterministic seek — evaluate the whole scene at the exact time (keyframes + relations + generators)
|
|
438
|
+
(function() {
|
|
439
|
+
const t = ${time || 0};
|
|
440
|
+
if (typeof app.sceneAt === 'function') { app.sceneAt(t); return { success: true, action: 'seek', time: t, deterministic: true }; }
|
|
441
|
+
// Fallback for older FxTool builds without sceneAt
|
|
442
|
+
app.setPlaybackTime(t);
|
|
443
|
+
return { success: true, action: 'seek', time: t, deterministic: false, note: 'app.sceneAt unavailable — used setPlaybackTime (keyframe state only); update FxTool for deterministic relations/generators' };
|
|
444
|
+
})();
|
|
445
|
+
`.trim();
|
|
446
|
+
}
|
|
425
447
|
return `
|
|
426
448
|
// Seek to time
|
|
427
449
|
app.setPlaybackTime(${time || 0});
|
|
@@ -555,6 +577,82 @@ const formatted = generators.map(g => ({
|
|
|
555
577
|
({ generators: formatted });
|
|
556
578
|
`.trim();
|
|
557
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* True when app.create() derives this item's geometry from explicit coordinates
|
|
582
|
+
* and therefore ignores params.position:
|
|
583
|
+
* - path built from `segments` or `pathData`
|
|
584
|
+
* - line built from `from`/`to`
|
|
585
|
+
* - arc built from `from`/`through`/`to`
|
|
586
|
+
* Without those coordinate props these types fall back to the position-derived
|
|
587
|
+
* point, so create() already places them correctly and we must not interfere.
|
|
588
|
+
*/
|
|
589
|
+
export function isCoordinateBuilt(itemType, props) {
|
|
590
|
+
switch (itemType) {
|
|
591
|
+
case 'path':
|
|
592
|
+
return props.segments !== undefined || props.pathData !== undefined;
|
|
593
|
+
case 'line':
|
|
594
|
+
return props.from !== undefined || props.to !== undefined;
|
|
595
|
+
case 'arc':
|
|
596
|
+
return props.from !== undefined || props.through !== undefined || props.to !== undefined;
|
|
597
|
+
default:
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* app.addAnimation() REPLACES data.keyframes wholesale, so two
|
|
603
|
+
* keyframe_animate ops targeting one item meant the first track was silently
|
|
604
|
+
* discarded — the batch still reported success while (e.g.) a rotation channel
|
|
605
|
+
* vanished because a later op set a colour channel on the same item.
|
|
606
|
+
*
|
|
607
|
+
* Plan a merge: fold every duplicate target's keyframes into its FIRST op
|
|
608
|
+
* (keeping author order), union by timestamp with later ops winning on a
|
|
609
|
+
* per-property conflict. Op slots are preserved so `results` stays 1:1 with
|
|
610
|
+
* `operations` and `$N` indices are untouched; the folded slots emit an
|
|
611
|
+
* explicit `{ merged: true }` marker so the merge is visible to the caller
|
|
612
|
+
* rather than being another silent rewrite.
|
|
613
|
+
*/
|
|
614
|
+
export function planKeyframeMerges(operations) {
|
|
615
|
+
const byTarget = new Map();
|
|
616
|
+
operations.forEach((op, i) => {
|
|
617
|
+
if (op.type !== 'keyframe_animate' || !op.itemId)
|
|
618
|
+
return;
|
|
619
|
+
const list = byTarget.get(op.itemId) || [];
|
|
620
|
+
list.push(i);
|
|
621
|
+
byTarget.set(op.itemId, list);
|
|
622
|
+
});
|
|
623
|
+
const plan = { merged: new Map(), foldedInto: new Map() };
|
|
624
|
+
for (const indices of byTarget.values()) {
|
|
625
|
+
if (indices.length < 2)
|
|
626
|
+
continue;
|
|
627
|
+
const first = indices[0];
|
|
628
|
+
// Union keyframes by time; later ops shallow-merge over earlier ones.
|
|
629
|
+
const byTime = new Map();
|
|
630
|
+
let duration = 0;
|
|
631
|
+
let loop = false;
|
|
632
|
+
for (const i of indices) {
|
|
633
|
+
const op = operations[i];
|
|
634
|
+
for (const kf of op.keyframes || []) {
|
|
635
|
+
const prev = byTime.get(kf.time);
|
|
636
|
+
byTime.set(kf.time, {
|
|
637
|
+
time: kf.time,
|
|
638
|
+
properties: { ...(prev?.properties || {}), ...(kf.properties || {}) },
|
|
639
|
+
...((kf.easing ?? prev?.easing) !== undefined ? { easing: kf.easing ?? prev?.easing } : {}),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const opMax = op.keyframes?.length ? Math.max(...op.keyframes.map(k => k.time)) : 0;
|
|
643
|
+
duration = Math.max(duration, op.duration || opMax);
|
|
644
|
+
loop = loop || op.loop === true;
|
|
645
|
+
if (i !== first)
|
|
646
|
+
plan.foldedInto.set(i, first);
|
|
647
|
+
}
|
|
648
|
+
plan.merged.set(first, {
|
|
649
|
+
keyframes: [...byTime.values()].sort((a, b) => a.time - b.time),
|
|
650
|
+
duration: duration || 5,
|
|
651
|
+
loop,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return plan;
|
|
655
|
+
}
|
|
558
656
|
/**
|
|
559
657
|
* Template for batch create - creates multiple items with single history save
|
|
560
658
|
*/
|
|
@@ -638,6 +736,10 @@ export class PinePaperCodeGenerator {
|
|
|
638
736
|
properties.animationType = validated.animationType;
|
|
639
737
|
if (validated.animationSpeed !== undefined)
|
|
640
738
|
properties.animationSpeed = validated.animationSpeed;
|
|
739
|
+
if (validated.animationIntensity !== undefined)
|
|
740
|
+
properties.animationIntensity = validated.animationIntensity;
|
|
741
|
+
if (validated.animationDelay !== undefined)
|
|
742
|
+
properties.animationDelay = validated.animationDelay;
|
|
641
743
|
if (validated.keyframes !== undefined)
|
|
642
744
|
properties.keyframes = validated.keyframes;
|
|
643
745
|
return generateCreateItemCode(validated.itemType, validated.position, properties);
|
|
@@ -679,21 +781,21 @@ export class PinePaperCodeGenerator {
|
|
|
679
781
|
*/
|
|
680
782
|
generateAnimate(input) {
|
|
681
783
|
const validated = AnimateItemInputSchema.parse(input);
|
|
682
|
-
return generateAnimateCode(validated.itemId, validated.animationType, validated.speed);
|
|
784
|
+
return generateAnimateCode(validated.itemId, validated.animationType, validated.speed, validated.intensity, validated.delay);
|
|
683
785
|
}
|
|
684
786
|
/**
|
|
685
787
|
* Generate code for keyframe animation
|
|
686
788
|
*/
|
|
687
789
|
generateKeyframeAnimate(input) {
|
|
688
790
|
const validated = KeyframeAnimateInputSchema.parse(input);
|
|
689
|
-
return generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint);
|
|
791
|
+
return generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint, validated.timeUnits);
|
|
690
792
|
}
|
|
691
793
|
/**
|
|
692
794
|
* Generate code for executing a generator
|
|
693
795
|
*/
|
|
694
796
|
generateExecuteGenerator(input) {
|
|
695
797
|
const validated = ExecuteGeneratorInputSchema.parse(input);
|
|
696
|
-
return generateExecuteGeneratorCode(validated.generatorName, validated.params);
|
|
798
|
+
return generateExecuteGeneratorCode(validated.generatorName, validated.params, validated.region);
|
|
697
799
|
}
|
|
698
800
|
/**
|
|
699
801
|
* Generate code for applying an effect
|
|
@@ -711,8 +813,8 @@ export class PinePaperCodeGenerator {
|
|
|
711
813
|
/**
|
|
712
814
|
* Generate code for timeline control
|
|
713
815
|
*/
|
|
714
|
-
generatePlayTimeline(action, duration, loop, time) {
|
|
715
|
-
return generatePlayTimelineCode(action, duration, loop, time);
|
|
816
|
+
generatePlayTimeline(action, duration, loop, time, deterministic) {
|
|
817
|
+
return generatePlayTimelineCode(action, duration, loop, time, deterministic);
|
|
716
818
|
}
|
|
717
819
|
/**
|
|
718
820
|
* Generate code for setting background color
|
|
@@ -870,6 +972,28 @@ throw new Error('Either svgString or url must be provided');
|
|
|
870
972
|
viewport: ctx.viewport,
|
|
871
973
|
};
|
|
872
974
|
})();
|
|
975
|
+
`.trim();
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Generate code for semantic validation (FxTool OntologyValidator) — runs in the
|
|
979
|
+
* browser against the live scene and returns structured diagnostics.
|
|
980
|
+
*/
|
|
981
|
+
generateValidate(input) {
|
|
982
|
+
const mode = input.mode || 'scene';
|
|
983
|
+
const opJson = JSON.stringify(input.op ?? null);
|
|
984
|
+
return `
|
|
985
|
+
// Semantic validation — structured diagnostics
|
|
986
|
+
(function() {
|
|
987
|
+
if (typeof app.validateScene !== 'function' || typeof app.validateOp !== 'function') {
|
|
988
|
+
return {
|
|
989
|
+
success: false,
|
|
990
|
+
error: 'app.validateScene/validateOp not available — requires FxTool with OntologyValidator v1 (commit 7f151f4 or later).',
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
var mode = ${JSON.stringify(mode)};
|
|
994
|
+
var result = (mode === 'op') ? app.validateOp(${opJson}) : app.validateScene();
|
|
995
|
+
return { success: true, mode: mode, ok: result.ok, diagnostics: result.diagnostics };
|
|
996
|
+
})();
|
|
873
997
|
`.trim();
|
|
874
998
|
}
|
|
875
999
|
/**
|
|
@@ -1728,6 +1852,9 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1728
1852
|
const validated = AgentBatchExecuteInputSchema.parse(input);
|
|
1729
1853
|
const { operations, atomic } = validated;
|
|
1730
1854
|
const isAtomic = atomic !== false;
|
|
1855
|
+
// Fold duplicate keyframe_animate targets so a later op can't silently
|
|
1856
|
+
// clobber an earlier track (app.addAnimation replaces, never merges).
|
|
1857
|
+
const mergePlan = planKeyframeMerges(operations);
|
|
1731
1858
|
let code = `
|
|
1732
1859
|
// Batch execute ${operations.length} operations
|
|
1733
1860
|
(async function() {
|
|
@@ -1738,7 +1865,17 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1738
1865
|
try {
|
|
1739
1866
|
`;
|
|
1740
1867
|
operations.forEach((op, index) => {
|
|
1741
|
-
const opCode = this.generateBatchOperationCode(op, index);
|
|
1868
|
+
const opCode = this.generateBatchOperationCode(op, index, mergePlan);
|
|
1869
|
+
// $N variable references are documented as "items CREATED in earlier
|
|
1870
|
+
// operations" — only `create` results may extend the itemIds array.
|
|
1871
|
+
// animate/keyframe_animate/modify/delete also return { itemId } (the
|
|
1872
|
+
// TARGET's id), and unconditionally pushing those shifted every later
|
|
1873
|
+
// $N reference: a batch of [create ×5, keyframe_animate ×5, create ×5,
|
|
1874
|
+
// keyframe_animate ×5] silently re-animated the FIRST five items and
|
|
1875
|
+
// left the second five without keyframes (mis-oriented/mis-animated
|
|
1876
|
+
// vehicles, 2026-07-30). `group` already follows this rule by
|
|
1877
|
+
// returning groupId instead of itemId.
|
|
1878
|
+
const claimsItemSlot = op.type === 'create';
|
|
1742
1879
|
code += `
|
|
1743
1880
|
// Operation ${index}: ${op.type}
|
|
1744
1881
|
try {
|
|
@@ -1746,9 +1883,9 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1746
1883
|
${opCode}
|
|
1747
1884
|
})();
|
|
1748
1885
|
results.push({ index: ${index}, success: true, result: result${index} });
|
|
1749
|
-
if (result${index} && result${index}.itemId) {
|
|
1886
|
+
${claimsItemSlot ? `if (result${index} && result${index}.itemId) {
|
|
1750
1887
|
itemIds.push(result${index}.itemId);
|
|
1751
|
-
}
|
|
1888
|
+
}` : ''}
|
|
1752
1889
|
} catch (opError) {
|
|
1753
1890
|
results.push({ index: ${index}, success: false, error: opError.message });
|
|
1754
1891
|
${isAtomic ? 'throw opError;' : 'success = false;'}
|
|
@@ -1772,7 +1909,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1772
1909
|
/**
|
|
1773
1910
|
* Generate code for a single batch operation
|
|
1774
1911
|
*/
|
|
1775
|
-
generateBatchOperationCode(op, index) {
|
|
1912
|
+
generateBatchOperationCode(op, index, mergePlan) {
|
|
1776
1913
|
switch (op.type) {
|
|
1777
1914
|
case 'create': {
|
|
1778
1915
|
const pos = op.position || { x: 400, y: 300 };
|
|
@@ -1790,6 +1927,19 @@ const itemId = item.data && item.data.id ? item.data.id : app.registerItem(item,
|
|
|
1790
1927
|
if (createProps.opacity !== undefined) {
|
|
1791
1928
|
createCode += `\nif ('opacity' in item) item.opacity = ${JSON.stringify(createProps.opacity)};`;
|
|
1792
1929
|
}
|
|
1930
|
+
// Coordinate-built items (path from segments/pathData, line/arc from
|
|
1931
|
+
// from/through/to) derive their geometry from those coordinates and
|
|
1932
|
+
// IGNORE params.position — so `create` at a point silently produced an
|
|
1933
|
+
// item at its raw coordinates instead. Re-seat it after create when the
|
|
1934
|
+
// caller explicitly asked for a position. Guarded on an explicit
|
|
1935
|
+
// op.position so absolute-coordinate geometry (the common case, which
|
|
1936
|
+
// passes no position) is never snapped to the injected default.
|
|
1937
|
+
if (op.position && isCoordinateBuilt(op.itemType, createProps)) {
|
|
1938
|
+
createCode += `
|
|
1939
|
+
// create() builds this item type from its own coordinates and ignores
|
|
1940
|
+
// params.position — apply the caller's explicit position after the fact.
|
|
1941
|
+
if (item.position) item.position = new paper.Point(${pos.x}, ${pos.y});`;
|
|
1942
|
+
}
|
|
1793
1943
|
createCode += `
|
|
1794
1944
|
// Ensure item is visible above backgrounds/generators
|
|
1795
1945
|
if (item.bringToFront) item.bringToFront();
|
|
@@ -1850,6 +2000,17 @@ item.remove();
|
|
|
1850
2000
|
if (app.itemRegistry) app.itemRegistry.unregister(targetId);
|
|
1851
2001
|
return { itemId: targetId, deleted: true };
|
|
1852
2002
|
`;
|
|
2003
|
+
case 'group': {
|
|
2004
|
+
// Resolve each ref ($N → itemIds[N]; literal → quoted id) into a JS array.
|
|
2005
|
+
const refs = (op.itemIds || []).map((ref) => ref?.startsWith('$') ? `itemIds[${ref.substring(1)}]` : `'${ref}'`);
|
|
2006
|
+
const idsArrayExpr = `[${refs.join(', ')}]`;
|
|
2007
|
+
const nameExpr = op.groupName !== undefined ? JSON.stringify(op.groupName) : 'undefined';
|
|
2008
|
+
// Returns { groupId, ... } (no itemId) so it does NOT shift $N indices — like relation.
|
|
2009
|
+
return `
|
|
2010
|
+
return (function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
|
|
2011
|
+
})();
|
|
2012
|
+
`;
|
|
2013
|
+
}
|
|
1853
2014
|
case 'set_background':
|
|
1854
2015
|
const bgColor = op.backgroundColor || op.properties?.color || '#000000';
|
|
1855
2016
|
return `
|
|
@@ -1859,9 +2020,10 @@ return { success: true, backgroundColor: '${bgColor}' };
|
|
|
1859
2020
|
case 'execute_generator':
|
|
1860
2021
|
const genName = op.generatorName || 'drawSunburst';
|
|
1861
2022
|
const genParams = JSON.stringify(op.generatorParams || {});
|
|
2023
|
+
const genOptions = JSON.stringify(op.generatorRegion ? { region: op.generatorRegion } : {});
|
|
1862
2024
|
return `
|
|
1863
|
-
await app.executeGenerator('${genName}', ${genParams});
|
|
1864
|
-
return { success: true, generator: '${genName}' };
|
|
2025
|
+
await app.executeGenerator('${genName}', ${genParams}, ${genOptions});
|
|
2026
|
+
return { success: true, generator: '${genName}'${op.generatorRegion ? ', region: ' + JSON.stringify(op.generatorRegion) : ''} };
|
|
1865
2027
|
`;
|
|
1866
2028
|
case 'set_canvas_size': {
|
|
1867
2029
|
const w = op.width || 1080;
|
|
@@ -1874,12 +2036,25 @@ return { success: true, width: ${w}, height: ${h} };
|
|
|
1874
2036
|
`;
|
|
1875
2037
|
}
|
|
1876
2038
|
case 'keyframe_animate': {
|
|
2039
|
+
// Folded into an earlier op on the same target (see planKeyframeMerges).
|
|
2040
|
+
const foldTarget = mergePlan?.foldedInto.get(index);
|
|
2041
|
+
if (foldTarget !== undefined) {
|
|
2042
|
+
return `
|
|
2043
|
+
// Keyframes merged into operation ${foldTarget} (same target). A keyframe track
|
|
2044
|
+
// is replaced wholesale, so emitting both here would silently drop one.
|
|
2045
|
+
return { itemId: ${op.itemId?.startsWith('$') ? `itemIds[${op.itemId.substring(1)}]` : `'${op.itemId}'`}, merged: true, mergedInto: ${foldTarget} };
|
|
2046
|
+
`;
|
|
2047
|
+
}
|
|
2048
|
+
const mergedPayload = mergePlan?.merged.get(index);
|
|
1877
2049
|
const kfItemRef = op.itemId?.startsWith('$')
|
|
1878
2050
|
? `itemIds[${op.itemId.substring(1)}]`
|
|
1879
2051
|
: `'${op.itemId}'`;
|
|
1880
|
-
const
|
|
1881
|
-
const
|
|
1882
|
-
const
|
|
2052
|
+
const kfSource = mergedPayload ? mergedPayload.keyframes : (op.keyframes || []);
|
|
2053
|
+
const kfJson = JSON.stringify(normalizeKeyframePositions(kfSource));
|
|
2054
|
+
const kfDuration = mergedPayload
|
|
2055
|
+
? mergedPayload.duration
|
|
2056
|
+
: (op.duration || (op.keyframes?.length ? Math.max(...op.keyframes.map(k => k.time)) : 5));
|
|
2057
|
+
const kfLoop = mergedPayload ? mergedPayload.loop : (op.loop ?? false);
|
|
1883
2058
|
return `
|
|
1884
2059
|
const targetId = ${kfItemRef};
|
|
1885
2060
|
app.addAnimation(targetId, ${kfJson}, { duration: ${kfDuration}, loop: ${kfLoop} });
|
|
@@ -1920,7 +2095,7 @@ return { itemId: targetId, effectType: '${op.effectType || 'sparkle'}' };
|
|
|
1920
2095
|
const ptLoop = op.loop ?? false;
|
|
1921
2096
|
if (ptAction === 'play') {
|
|
1922
2097
|
return `
|
|
1923
|
-
app.playKeyframeTimeline(${ptDuration}, ${ptLoop});
|
|
2098
|
+
app.playKeyframeTimeline(${ptDuration}, ${ptLoop}, { timeUnits: 'seconds' });
|
|
1924
2099
|
return { success: true, action: 'play', duration: ${ptDuration}, loop: ${ptLoop} };
|
|
1925
2100
|
`;
|
|
1926
2101
|
}
|
|
@@ -2259,7 +2434,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2259
2434
|
*/
|
|
2260
2435
|
generateCreateLetterCollage(input) {
|
|
2261
2436
|
const validated = CreateLetterCollageInputSchema.parse(input);
|
|
2262
|
-
const { text, style = 'tile', palette, position, fontSize = 48, fontFamily = 'Inter, sans-serif', spacing = 1.1, gradientPalette, gradientDirection = 'vertical', cornerRadius = 4, shadowEnabled = true, } = validated;
|
|
2437
|
+
const { text, style = 'tile', palette, position, fontSize = 48, fontFamily = 'Inter, sans-serif', spacing = 1.1, gradientPalette, gradientDirection = 'vertical', cornerRadius = 4, shadowEnabled = true, particleSpacing, particleDotSize, } = validated;
|
|
2263
2438
|
// Build options object
|
|
2264
2439
|
const options = {
|
|
2265
2440
|
style,
|
|
@@ -2277,6 +2452,10 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2277
2452
|
options.gradientPalette = gradientPalette;
|
|
2278
2453
|
if (gradientDirection)
|
|
2279
2454
|
options.gradientDirection = gradientDirection;
|
|
2455
|
+
if (particleSpacing !== undefined)
|
|
2456
|
+
options.particleSpacing = particleSpacing;
|
|
2457
|
+
if (particleDotSize !== undefined)
|
|
2458
|
+
options.particleDotSize = particleDotSize;
|
|
2280
2459
|
return `
|
|
2281
2460
|
// Create letter collage
|
|
2282
2461
|
(function() {
|
|
@@ -2858,6 +3037,198 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2858
3037
|
return { success: false, error: error.message };
|
|
2859
3038
|
}
|
|
2860
3039
|
})();
|
|
3040
|
+
`.trim();
|
|
3041
|
+
}
|
|
3042
|
+
// -------------------------------------------------------------------------
|
|
3043
|
+
// Globe + World Tour
|
|
3044
|
+
// -------------------------------------------------------------------------
|
|
3045
|
+
/**
|
|
3046
|
+
* Generate code to enable globe (orthographic) mode
|
|
3047
|
+
*/
|
|
3048
|
+
generateGlobeEnable(input) {
|
|
3049
|
+
const validated = GlobeEnableInputSchema.parse(input);
|
|
3050
|
+
const optionsStr = JSON.stringify(validated);
|
|
3051
|
+
return `
|
|
3052
|
+
// Enable globe mode
|
|
3053
|
+
(function() {
|
|
3054
|
+
if (!app.mapSystem) {
|
|
3055
|
+
return { success: false, error: 'Map system not available' };
|
|
3056
|
+
}
|
|
3057
|
+
if (typeof app.mapSystem.enableGlobeMode !== 'function') {
|
|
3058
|
+
return { success: false, error: 'enableGlobeMode unavailable on this build (update the canvas)' };
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
try {
|
|
3062
|
+
app.mapSystem.enableGlobeMode(${optionsStr});
|
|
3063
|
+
return { success: true };
|
|
3064
|
+
} catch (error) {
|
|
3065
|
+
return { success: false, error: error.message };
|
|
3066
|
+
}
|
|
3067
|
+
})();
|
|
3068
|
+
`.trim();
|
|
3069
|
+
}
|
|
3070
|
+
/**
|
|
3071
|
+
* Generate code to rotate the globe to a coordinate (versor slerp)
|
|
3072
|
+
*/
|
|
3073
|
+
generateGlobeRotateTo(input) {
|
|
3074
|
+
const validated = GlobeRotateToInputSchema.parse(input);
|
|
3075
|
+
const durationArg = validated.duration !== undefined ? `, ${validated.duration}` : '';
|
|
3076
|
+
return `
|
|
3077
|
+
// Rotate globe to coordinate
|
|
3078
|
+
(function() {
|
|
3079
|
+
if (!app.mapSystem) {
|
|
3080
|
+
return { success: false, error: 'Map system not available' };
|
|
3081
|
+
}
|
|
3082
|
+
if (typeof app.mapSystem.rotateGlobeTo !== 'function') {
|
|
3083
|
+
return { success: false, error: 'rotateGlobeTo unavailable on this build (update the canvas)' };
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
try {
|
|
3087
|
+
app.mapSystem.rotateGlobeTo(${validated.lon}, ${validated.lat}${durationArg});
|
|
3088
|
+
return { success: true, rotateTo: [${validated.lon}, ${validated.lat}] };
|
|
3089
|
+
} catch (error) {
|
|
3090
|
+
return { success: false, error: error.message };
|
|
3091
|
+
}
|
|
3092
|
+
})();
|
|
3093
|
+
`.trim();
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* Generate code to continuously spin the globe
|
|
3097
|
+
*/
|
|
3098
|
+
generateGlobeSpin(input) {
|
|
3099
|
+
const validated = GlobeSpinInputSchema.parse(input);
|
|
3100
|
+
const optionsStr = JSON.stringify(validated);
|
|
3101
|
+
return `
|
|
3102
|
+
// Spin globe continuously
|
|
3103
|
+
(function() {
|
|
3104
|
+
if (!app.mapSystem) {
|
|
3105
|
+
return { success: false, error: 'Map system not available' };
|
|
3106
|
+
}
|
|
3107
|
+
if (typeof app.mapSystem.animateGlobeRotation !== 'function') {
|
|
3108
|
+
return { success: false, error: 'animateGlobeRotation unavailable on this build (update the canvas)' };
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
try {
|
|
3112
|
+
const callbackId = app.mapSystem.animateGlobeRotation(${optionsStr});
|
|
3113
|
+
return { success: true, callbackId: callbackId || null };
|
|
3114
|
+
} catch (error) {
|
|
3115
|
+
return { success: false, error: error.message };
|
|
3116
|
+
}
|
|
3117
|
+
})();
|
|
3118
|
+
`.trim();
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Generate code to run a world tour across regions or coordinates
|
|
3122
|
+
*/
|
|
3123
|
+
generateWorldTour(input) {
|
|
3124
|
+
const validated = WorldTourInputSchema.parse(input);
|
|
3125
|
+
const optionsStr = JSON.stringify(validated);
|
|
3126
|
+
return `
|
|
3127
|
+
// World tour across regions / coordinates
|
|
3128
|
+
(function() {
|
|
3129
|
+
if (!app.mapSystem) {
|
|
3130
|
+
return { success: false, error: 'Map system not available' };
|
|
3131
|
+
}
|
|
3132
|
+
if (typeof app.mapSystem.worldTour !== 'function') {
|
|
3133
|
+
return { success: false, error: 'worldTour unavailable on this build (update the canvas)' };
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
try {
|
|
3137
|
+
const tourId = app.mapSystem.worldTour(${optionsStr});
|
|
3138
|
+
return { success: true, tourId: tourId || null };
|
|
3139
|
+
} catch (error) {
|
|
3140
|
+
return { success: false, error: error.message };
|
|
3141
|
+
}
|
|
3142
|
+
})();
|
|
3143
|
+
`.trim();
|
|
3144
|
+
}
|
|
3145
|
+
/**
|
|
3146
|
+
* Generate code to stop a running world tour
|
|
3147
|
+
*/
|
|
3148
|
+
generateStopWorldTour(input) {
|
|
3149
|
+
const validated = StopWorldTourInputSchema.parse(input);
|
|
3150
|
+
const idArg = validated.id !== undefined ? JSON.stringify(validated.id) : '';
|
|
3151
|
+
return `
|
|
3152
|
+
// Stop world tour
|
|
3153
|
+
(function() {
|
|
3154
|
+
if (!app.mapSystem) {
|
|
3155
|
+
return { success: false, error: 'Map system not available' };
|
|
3156
|
+
}
|
|
3157
|
+
if (typeof app.mapSystem.stopWorldTour !== 'function') {
|
|
3158
|
+
return { success: false, error: 'stopWorldTour unavailable on this build (update the canvas)' };
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
try {
|
|
3162
|
+
app.mapSystem.stopWorldTour(${idArg});
|
|
3163
|
+
return { success: true };
|
|
3164
|
+
} catch (error) {
|
|
3165
|
+
return { success: false, error: error.message };
|
|
3166
|
+
}
|
|
3167
|
+
})();
|
|
3168
|
+
`.trim();
|
|
3169
|
+
}
|
|
3170
|
+
/**
|
|
3171
|
+
* Generate code to pin a canvas item to a globe surface coordinate
|
|
3172
|
+
*/
|
|
3173
|
+
generatePinToGlobe(input) {
|
|
3174
|
+
const validated = PinToGlobeInputSchema.parse(input);
|
|
3175
|
+
const itemId = validated.itemId;
|
|
3176
|
+
const opts = JSON.stringify({ hideOnFarSide: validated.hideOnFarSide });
|
|
3177
|
+
return `
|
|
3178
|
+
// Pin item to globe coordinate
|
|
3179
|
+
(function() {
|
|
3180
|
+
if (!app.mapSystem) {
|
|
3181
|
+
return { success: false, error: 'Map system not available' };
|
|
3182
|
+
}
|
|
3183
|
+
if (typeof app.mapSystem.pinItemToGlobe !== 'function') {
|
|
3184
|
+
return { success: false, error: 'pinItemToGlobe unavailable on this build (update the canvas)' };
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
const item = (app.getItemById && app.getItemById(${JSON.stringify(itemId)})) || (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(${JSON.stringify(itemId)}) && (app.itemRegistry.get(${JSON.stringify(itemId)}).item || app.itemRegistry.get(${JSON.stringify(itemId)})));
|
|
3188
|
+
if (!item) {
|
|
3189
|
+
return { success: false, error: 'Item ${itemId} not found' };
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
try {
|
|
3193
|
+
const pinned = app.mapSystem.pinItemToGlobe(item, ${validated.lon}, ${validated.lat}, ${opts});
|
|
3194
|
+
return { success: !!pinned, pinned: !!pinned };
|
|
3195
|
+
} catch (error) {
|
|
3196
|
+
return { success: false, error: error.message };
|
|
3197
|
+
}
|
|
3198
|
+
})();
|
|
3199
|
+
`.trim();
|
|
3200
|
+
}
|
|
3201
|
+
/**
|
|
3202
|
+
* Generate code to tour a canvas item along regions or coordinates
|
|
3203
|
+
*/
|
|
3204
|
+
generateTourItem(input) {
|
|
3205
|
+
const validated = TourItemInputSchema.parse(input);
|
|
3206
|
+
const itemId = validated.itemId;
|
|
3207
|
+
const { itemId: _omit, regions, coords, ...rest } = validated;
|
|
3208
|
+
const optsStr = JSON.stringify(rest);
|
|
3209
|
+
return `
|
|
3210
|
+
// Tour item along regions / coordinates
|
|
3211
|
+
(function() {
|
|
3212
|
+
if (!app.mapSystem) {
|
|
3213
|
+
return { success: false, error: 'Map system not available' };
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
const item = (app.getItemById && app.getItemById(${JSON.stringify(itemId)})) || (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(${JSON.stringify(itemId)}) && (app.itemRegistry.get(${JSON.stringify(itemId)}).item || app.itemRegistry.get(${JSON.stringify(itemId)})));
|
|
3217
|
+
if (!item) {
|
|
3218
|
+
return { success: false, error: 'Item ${itemId} not found' };
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
try {
|
|
3222
|
+
${regions
|
|
3223
|
+
? `if (typeof app.mapSystem.tourRegions !== 'function') { return { success: false, error: 'tourRegions unavailable on this build (update the canvas)' }; }
|
|
3224
|
+
const tourId = app.mapSystem.tourRegions(item, ${JSON.stringify(regions)}, ${optsStr});`
|
|
3225
|
+
: `if (typeof app.mapSystem.tourItemAlongCoords !== 'function') { return { success: false, error: 'tourItemAlongCoords unavailable on this build (update the canvas)' }; }
|
|
3226
|
+
const tourId = app.mapSystem.tourItemAlongCoords(item, ${JSON.stringify(coords || [])}, ${optsStr});`}
|
|
3227
|
+
return { success: true, tourId: tourId || null };
|
|
3228
|
+
} catch (error) {
|
|
3229
|
+
return { success: false, error: error.message };
|
|
3230
|
+
}
|
|
3231
|
+
})();
|
|
2861
3232
|
`.trim();
|
|
2862
3233
|
}
|
|
2863
3234
|
/**
|
|
@@ -3715,15 +4086,23 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
3715
4086
|
// IMAGE PROCESSING
|
|
3716
4087
|
// ===========================================================================
|
|
3717
4088
|
generateImageFilter(input) {
|
|
3718
|
-
|
|
4089
|
+
// Resolve the raster from the registry id (unwrapping groups when the
|
|
4090
|
+
// build has the _resolveRaster helper), then use the real GPU-filter
|
|
4091
|
+
// facades. The previous emitter called app.imageTools.applyFilter, a
|
|
4092
|
+
// method that never existed — the tool errored on every use.
|
|
4093
|
+
const resolve = ` const item = (typeof app._resolveRaster === 'function')
|
|
4094
|
+
? app._resolveRaster(${JSON.stringify(input.itemId)})
|
|
4095
|
+
: (app.itemRegistry ? app.itemRegistry.getItem(${JSON.stringify(input.itemId)}) : null);
|
|
4096
|
+
if (!item) return { error: 'Item not found or not a raster: ' + ${JSON.stringify(input.itemId)} };
|
|
4097
|
+
if (typeof app.applyImageFilter !== 'function') return { error: 'GPU image filters unavailable — update FxTool' };`;
|
|
3719
4098
|
switch (input.action) {
|
|
3720
4099
|
case 'apply':
|
|
3721
4100
|
return `
|
|
3722
4101
|
// Apply image filter
|
|
3723
4102
|
(async function() {
|
|
3724
|
-
|
|
4103
|
+
${resolve}
|
|
3725
4104
|
try {
|
|
3726
|
-
|
|
4105
|
+
await app.applyImageFilter(item, ${JSON.stringify(input.filterName || '')}, ${JSON.stringify(input.params || {})});
|
|
3727
4106
|
return { success: true, action: 'apply', itemId: ${JSON.stringify(input.itemId)}, filter: ${JSON.stringify(input.filterName || '')} };
|
|
3728
4107
|
} catch (e) {
|
|
3729
4108
|
return { error: 'Failed to apply filter: ' + e.message };
|
|
@@ -3733,10 +4112,10 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
3733
4112
|
return `
|
|
3734
4113
|
// Apply filter chain
|
|
3735
4114
|
(async function() {
|
|
3736
|
-
|
|
4115
|
+
${resolve}
|
|
3737
4116
|
try {
|
|
3738
4117
|
const filters = ${JSON.stringify(input.filters || [])};
|
|
3739
|
-
|
|
4118
|
+
await app.applyImageFilterChain(item, filters);
|
|
3740
4119
|
return { success: true, action: 'chain', itemId: ${JSON.stringify(input.itemId)}, filterCount: filters.length };
|
|
3741
4120
|
} catch (e) {
|
|
3742
4121
|
return { error: 'Failed to apply filter chain: ' + e.message };
|
|
@@ -4489,6 +4868,665 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
4489
4868
|
return `(function() { return { error: 'Unknown measurement action: ${input.action}' }; })();`;
|
|
4490
4869
|
}
|
|
4491
4870
|
}
|
|
4871
|
+
/**
|
|
4872
|
+
* Z-order: select the item, then call the matching app stacking method
|
|
4873
|
+
* (bringToFront / sendToBack / bringForward / sendBackward).
|
|
4874
|
+
*/
|
|
4875
|
+
generateArrange(input) {
|
|
4876
|
+
const method = {
|
|
4877
|
+
front: 'bringToFront',
|
|
4878
|
+
back: 'sendToBack',
|
|
4879
|
+
forward: 'bringForward',
|
|
4880
|
+
backward: 'sendBackward',
|
|
4881
|
+
}[input.action];
|
|
4882
|
+
const id = JSON.stringify(input.itemId);
|
|
4883
|
+
return `
|
|
4884
|
+
// Arrange ${input.itemId} → ${input.action}
|
|
4885
|
+
(function() {
|
|
4886
|
+
const item = app.getItemById(${id});
|
|
4887
|
+
if (!item) return { success: false, error: 'Item not found: ' + ${id} };
|
|
4888
|
+
if (typeof app.${method} !== 'function') return { success: false, error: 'app.${method} unavailable — update FxTool' };
|
|
4889
|
+
app.select(${id});
|
|
4890
|
+
app.${method}();
|
|
4891
|
+
return { success: true, itemId: ${id}, action: '${input.action}' };
|
|
4892
|
+
})();`.trim();
|
|
4893
|
+
}
|
|
4894
|
+
/**
|
|
4895
|
+
* On-device object detection (FxTool ImageWorkflow). Finds objects in an image;
|
|
4896
|
+
* with asNodes promotes each detection to a typed, image-anchored design node
|
|
4897
|
+
* (pp:Detected*) instead of a labeled box. Async (runs an on-device ML model).
|
|
4898
|
+
*/
|
|
4899
|
+
generateDetectObjects(input) {
|
|
4900
|
+
const args = JSON.stringify({
|
|
4901
|
+
...(input.itemId !== undefined ? { itemId: input.itemId } : {}),
|
|
4902
|
+
...(input.threshold !== undefined ? { threshold: input.threshold } : {}),
|
|
4903
|
+
...(input.asNodes !== undefined ? { asNodes: input.asNodes } : {}),
|
|
4904
|
+
...(input.queries !== undefined ? { queries: input.queries } : {}),
|
|
4905
|
+
});
|
|
4906
|
+
return `
|
|
4907
|
+
// Detect objects${input.queries !== undefined ? ' (open-vocabulary / OWL-ViT)' : ''}${input.asNodes ? ' (asNodes — relational compositing)' : ''}
|
|
4908
|
+
(async function() {
|
|
4909
|
+
if (typeof app.detectObjects !== 'function') {
|
|
4910
|
+
return { success: false, error: 'app.detectObjects unavailable — update FxTool to a build that exposes on-device object detection on app' };
|
|
4911
|
+
}
|
|
4912
|
+
try {
|
|
4913
|
+
const res = await app.detectObjects(${args});
|
|
4914
|
+
return Object.assign({ success: !!(res && res.ok) }, res || {});
|
|
4915
|
+
} catch (e) { return { success: false, error: String((e && e.message) || e) }; }
|
|
4916
|
+
})();`.trim();
|
|
4917
|
+
}
|
|
4918
|
+
/**
|
|
4919
|
+
* Extract the best-matching detected region from an image as a NEW item (FxTool
|
|
4920
|
+
* ImageWorkflow.extractObject). Async (runs detection first).
|
|
4921
|
+
*/
|
|
4922
|
+
generateExtractObject(input) {
|
|
4923
|
+
const args = JSON.stringify({
|
|
4924
|
+
...(input.label !== undefined ? { label: input.label } : {}),
|
|
4925
|
+
...(input.itemId !== undefined ? { itemId: input.itemId } : {}),
|
|
4926
|
+
...(input.x !== undefined ? { x: input.x } : {}),
|
|
4927
|
+
...(input.y !== undefined ? { y: input.y } : {}),
|
|
4928
|
+
...(input.threshold !== undefined ? { threshold: input.threshold } : {}),
|
|
4929
|
+
});
|
|
4930
|
+
return `
|
|
4931
|
+
// Extract object${input.label ? ` "${input.label}"` : ''} from the image
|
|
4932
|
+
(async function() {
|
|
4933
|
+
if (typeof app.extractObject !== 'function') {
|
|
4934
|
+
return { success: false, error: 'app.extractObject unavailable — update FxTool to a build that exposes on-device object detection on app' };
|
|
4935
|
+
}
|
|
4936
|
+
try {
|
|
4937
|
+
const res = await app.extractObject(${args});
|
|
4938
|
+
return Object.assign({ success: !!(res && res.ok) }, res || {});
|
|
4939
|
+
} catch (e) { return { success: false, error: String((e && e.message) || e) }; }
|
|
4940
|
+
})();`.trim();
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Camera director (FxTool DirectorCompiler): compile a shot list into ONE
|
|
4944
|
+
* camera_animates walkthrough. 'auto' derives a shot per item; 'shots' applies
|
|
4945
|
+
* an explicit list. WYSIWYG between editor and any export size (one camera).
|
|
4946
|
+
*/
|
|
4947
|
+
generateCameraDirector(input) {
|
|
4948
|
+
if (input.action === 'shots') {
|
|
4949
|
+
const shotsJson = JSON.stringify(input.shots ?? []);
|
|
4950
|
+
const opts = JSON.stringify({ loop: !!input.loop });
|
|
4951
|
+
return `
|
|
4952
|
+
// Camera director — apply explicit shot list
|
|
4953
|
+
(function() {
|
|
4954
|
+
if (typeof app.applyDirectorShots !== 'function') {
|
|
4955
|
+
return { success: false, error: 'app.applyDirectorShots unavailable — update FxTool to a build with the camera director' };
|
|
4956
|
+
}
|
|
4957
|
+
const shots = ${shotsJson};
|
|
4958
|
+
const ok = app.applyDirectorShots(shots, ${opts});
|
|
4959
|
+
return { success: !!ok, mode: 'shots', shotCount: shots.length, loop: ${!!input.loop} };
|
|
4960
|
+
})();`.trim();
|
|
4961
|
+
}
|
|
4962
|
+
const opts = JSON.stringify({
|
|
4963
|
+
...(input.order !== undefined ? { order: input.order } : {}),
|
|
4964
|
+
...(input.hold !== undefined ? { hold: input.hold } : {}),
|
|
4965
|
+
...(input.establishing !== undefined ? { establishing: input.establishing } : {}),
|
|
4966
|
+
loop: !!input.loop,
|
|
4967
|
+
});
|
|
4968
|
+
return `
|
|
4969
|
+
// Camera director — auto-direct a walkthrough of the scene
|
|
4970
|
+
(function() {
|
|
4971
|
+
if (typeof app.autoDirectStory !== 'function') {
|
|
4972
|
+
return { success: false, error: 'app.autoDirectStory unavailable — update FxTool to a build with the camera director' };
|
|
4973
|
+
}
|
|
4974
|
+
const shots = app.autoDirectStory(${opts});
|
|
4975
|
+
if (!shots) return { success: false, mode: 'auto', error: 'No items to direct — add content first' };
|
|
4976
|
+
return { success: true, mode: 'auto', shotCount: shots.length, shots: shots };
|
|
4977
|
+
})();`.trim();
|
|
4978
|
+
}
|
|
4979
|
+
/**
|
|
4980
|
+
* Inner JS that groups resolved item ids into ONE draggable entity via the
|
|
4981
|
+
* GroupManager (createGroup → addItemsToGroup). idsArrayExpr is a JS expression
|
|
4982
|
+
* evaluating to an array of id strings; nameExpr is a JS string expression or 'undefined'.
|
|
4983
|
+
* Shared by the standalone pinepaper_group tool and the batch "group" operation.
|
|
4984
|
+
*/
|
|
4985
|
+
groupCodeBody(idsArrayExpr, nameExpr) {
|
|
4986
|
+
return `
|
|
4987
|
+
if (!app.groupManager) return { success: false, error: 'GroupManager not available — update FxTool' };
|
|
4988
|
+
const ids = (${idsArrayExpr}).filter(function(x) { return x != null; });
|
|
4989
|
+
const items = ids.map(function(id) { return app.getItemById(id); }).filter(Boolean);
|
|
4990
|
+
if (!items.length) return { success: false, error: 'No valid items to group (check the ids / $N refs)' };
|
|
4991
|
+
const group = app.groupManager.createGroup(${nameExpr});
|
|
4992
|
+
if (!group) return { success: false, error: 'Failed to create group' };
|
|
4993
|
+
app.groupManager.addItemsToGroup(items, group);
|
|
4994
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
4995
|
+
const groupId = (group.data && (group.data.registryId || group.data.groupId)) || null;
|
|
4996
|
+
return { success: true, groupId: groupId, groupName: group.data && group.data.groupName, itemCount: items.length };`;
|
|
4997
|
+
}
|
|
4998
|
+
/**
|
|
4999
|
+
* Group a set of items into one draggable entity, or ungroup one back into loose
|
|
5000
|
+
* items. Grouping is non-destructive — ungroup restores the individual items.
|
|
5001
|
+
*/
|
|
5002
|
+
generateGroup(input) {
|
|
5003
|
+
if (input.action === 'break_apart') {
|
|
5004
|
+
const idExpr = JSON.stringify(input.itemId || '');
|
|
5005
|
+
return `
|
|
5006
|
+
// Break apart ${input.itemId || ''} into movable parts
|
|
5007
|
+
(function() {
|
|
5008
|
+
if (typeof app.breakApart !== 'function') return { success: false, error: 'app.breakApart unavailable — update FxTool to a build with SVG/group segmentation' };
|
|
5009
|
+
const target = app.getItemById(${idExpr});
|
|
5010
|
+
if (!target) return { success: false, error: 'Item not found: ' + ${idExpr} };
|
|
5011
|
+
const result = app.breakApart(target);
|
|
5012
|
+
if (!result || !result.parts || !result.parts.length) return { success: false, error: 'Nothing to break apart (no sub-parts found)' };
|
|
5013
|
+
const partIds = result.parts.map(function(p) { return p && p.data && (p.data.registryId || p.data.id); }).filter(Boolean);
|
|
5014
|
+
const groupId = (result.group && result.group.data && (result.group.data.registryId || result.group.data.groupId)) || null;
|
|
5015
|
+
return { success: true, action: 'break_apart', groupId: groupId, partIds: partIds, partCount: partIds.length };
|
|
5016
|
+
})();`.trim();
|
|
5017
|
+
}
|
|
5018
|
+
if (input.action === 'ungroup') {
|
|
5019
|
+
const idExpr = JSON.stringify(input.groupId || '');
|
|
5020
|
+
return `
|
|
5021
|
+
// Ungroup ${input.groupId || ''}
|
|
5022
|
+
(function() {
|
|
5023
|
+
if (!app.groupManager) return { success: false, error: 'GroupManager not available — update FxTool' };
|
|
5024
|
+
const gid = ${idExpr};
|
|
5025
|
+
const group = app.getItemById(gid) || (app.groupManager.getGroupById ? app.groupManager.getGroupById(gid) : null);
|
|
5026
|
+
if (!group) return { success: false, error: 'Group not found: ' + gid };
|
|
5027
|
+
app.groupManager.ungroupAll(group);
|
|
5028
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5029
|
+
return { success: true, ungrouped: true, groupId: gid };
|
|
5030
|
+
})();`.trim();
|
|
5031
|
+
}
|
|
5032
|
+
const idsArrayExpr = JSON.stringify(input.itemIds ?? []);
|
|
5033
|
+
const nameExpr = input.groupName !== undefined ? JSON.stringify(input.groupName) : 'undefined';
|
|
5034
|
+
return `
|
|
5035
|
+
// Group ${(input.itemIds || []).length} items${input.groupName ? ` as "${input.groupName}"` : ''}
|
|
5036
|
+
(function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
|
|
5037
|
+
})();`.trim();
|
|
5038
|
+
}
|
|
5039
|
+
/**
|
|
5040
|
+
* Audit the live scene (or pre-validate a batch of proposed ops) with FxTool's
|
|
5041
|
+
* OntologyValidator — structured { ok, diagnostics } feedback instead of console warnings.
|
|
5042
|
+
*/
|
|
5043
|
+
generateValidateScene(input) {
|
|
5044
|
+
const guard = `const v = app.sceneValidator;
|
|
5045
|
+
if (!v) return { success: false, error: 'app.sceneValidator unavailable — update FxTool to a build with the OntologyValidator' };`;
|
|
5046
|
+
if (input.ops) {
|
|
5047
|
+
const opsJson = JSON.stringify(input.ops);
|
|
5048
|
+
return `
|
|
5049
|
+
// Pre-validate proposed ops against the live scene
|
|
5050
|
+
(function() {
|
|
5051
|
+
${guard}
|
|
5052
|
+
if (typeof v.validateOps !== 'function') return { success: false, error: 'validateOps unavailable in this FxTool build' };
|
|
5053
|
+
const r = v.validateOps(${opsJson});
|
|
5054
|
+
return { success: true, mode: 'ops', ok: r.ok, diagnosticCount: r.diagnostics.length, diagnostics: r.diagnostics };
|
|
5055
|
+
})();`.trim();
|
|
5056
|
+
}
|
|
5057
|
+
return `
|
|
5058
|
+
// Audit the current scene
|
|
5059
|
+
(function() {
|
|
5060
|
+
${guard}
|
|
5061
|
+
const r = v.validateScene();
|
|
5062
|
+
return { success: true, mode: 'scene', ok: r.ok, diagnosticCount: r.diagnostics.length, diagnostics: r.diagnostics };
|
|
5063
|
+
})();`.trim();
|
|
5064
|
+
}
|
|
5065
|
+
/**
|
|
5066
|
+
* Deterministic headless frame capture (S3): app.captureFramesAt(times, { seed })
|
|
5067
|
+
* seeds Math.random once around the sequence and evaluates the scene at each time via
|
|
5068
|
+
* sceneAt(t). Returns a cheap per-frame hash by default (token-light) so an agent can
|
|
5069
|
+
* verify determinism (re-run → identical hashes) and whether frames actually change.
|
|
5070
|
+
*/
|
|
5071
|
+
generateCaptureFrames(input) {
|
|
5072
|
+
const timesJson = JSON.stringify(input.times);
|
|
5073
|
+
const seed = input.seed !== undefined ? input.seed : 0;
|
|
5074
|
+
const includeDataUrls = !!input.includeDataUrls;
|
|
5075
|
+
return `
|
|
5076
|
+
// Deterministic frame capture
|
|
5077
|
+
(function() {
|
|
5078
|
+
if (typeof app.captureFramesAt !== 'function') {
|
|
5079
|
+
return { success: false, error: 'app.captureFramesAt unavailable — update FxTool to a build with the deterministic capture entrypoint' };
|
|
5080
|
+
}
|
|
5081
|
+
const includeDataUrls = ${includeDataUrls};
|
|
5082
|
+
const hashStr = function(s) { let h = 5381; for (let i = 0; i < s.length; i++) { h = (((h << 5) + h) ^ s.charCodeAt(i)) >>> 0; } return h.toString(16); };
|
|
5083
|
+
const frames = app.captureFramesAt(${timesJson}, {
|
|
5084
|
+
seed: ${seed},
|
|
5085
|
+
capture: function(c, t, i) {
|
|
5086
|
+
// Prefer app.captureFrameDataURL(): it renders only the used region, so an
|
|
5087
|
+
// unbounded canvas's fill-workspace margin is never baked into the frame.
|
|
5088
|
+
// Fall back to the raw canvas toDataURL on pre-capture-region FxTool builds.
|
|
5089
|
+
const url = (typeof app.captureFrameDataURL === 'function')
|
|
5090
|
+
? app.captureFrameDataURL()
|
|
5091
|
+
: ((c && c.toDataURL) ? c.toDataURL() : '');
|
|
5092
|
+
const f = { index: i, time: t, hash: hashStr(url), bytes: url.length };
|
|
5093
|
+
if (includeDataUrls) f.dataUrl = url;
|
|
5094
|
+
return f;
|
|
5095
|
+
}
|
|
5096
|
+
});
|
|
5097
|
+
const uniqueHashes = new Set(frames.map(function(f) { return f.hash; })).size;
|
|
5098
|
+
return { success: true, seed: ${seed}, frameCount: frames.length, uniqueHashes: uniqueHashes, allIdentical: uniqueHashes <= 1, frames: frames };
|
|
5099
|
+
})();`.trim();
|
|
5100
|
+
}
|
|
5101
|
+
/**
|
|
5102
|
+
* Ontology→scene compiler (S12-E3). Compiles the JSON-LD design graph SERVER-SIDE
|
|
5103
|
+
* (deterministic, unit-tested) into create/addRelation ops, then emits code that
|
|
5104
|
+
* applies them. Only root nodes carry coordinates; structural relations place the
|
|
5105
|
+
* rest at runtime. Compile diagnostics are baked into the emitted result.
|
|
5106
|
+
*/
|
|
5107
|
+
generateInstantiateOntology(input) {
|
|
5108
|
+
const { ops, diagnostics } = new OntologyCompiler({
|
|
5109
|
+
canvas: input.canvas,
|
|
5110
|
+
defaultGeometry: input.defaultGeometry,
|
|
5111
|
+
}).compile(input.doc);
|
|
5112
|
+
const opsJson = JSON.stringify(ops, null, 2);
|
|
5113
|
+
const diagJson = JSON.stringify(diagnostics);
|
|
5114
|
+
return `
|
|
5115
|
+
// Instantiate ontology → scene (${ops.filter((o) => o.op === 'create').length} items, ${ops.filter((o) => o.op === 'addRelation').length} relations)
|
|
5116
|
+
(function() {
|
|
5117
|
+
if (typeof app.create !== 'function' || typeof app.addRelation !== 'function') {
|
|
5118
|
+
return { success: false, error: 'app.create / app.addRelation unavailable' };
|
|
5119
|
+
}
|
|
5120
|
+
const ops = ${opsJson};
|
|
5121
|
+
const diagnostics = ${diagJson};
|
|
5122
|
+
const itemIds = [];
|
|
5123
|
+
const errors = [];
|
|
5124
|
+
for (const op of ops) {
|
|
5125
|
+
try {
|
|
5126
|
+
if (op.op === 'create') {
|
|
5127
|
+
app.create(op.type, { id: op.id, x: op.x, y: op.y, width: op.width, height: op.height });
|
|
5128
|
+
itemIds.push(op.id);
|
|
5129
|
+
} else if (op.op === 'addRelation') {
|
|
5130
|
+
app.addRelation(op.from, op.to, op.relation, op.params || {});
|
|
5131
|
+
}
|
|
5132
|
+
} catch (e) {
|
|
5133
|
+
errors.push({ op: op.op, id: op.id || (op.from + '->' + op.to), error: e && e.message });
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5137
|
+
return { success: errors.length === 0, itemIds: itemIds, itemCount: itemIds.length, diagnostics: diagnostics, errors: errors };
|
|
5138
|
+
})();`.trim();
|
|
5139
|
+
}
|
|
5140
|
+
/**
|
|
5141
|
+
* Relational-density lint of the live scene (S12-E2) via app.lintScene(). Reports
|
|
5142
|
+
* relationalDensity + edge/component counts and suggests structural relations for
|
|
5143
|
+
* unrelated item pairs already in a structural configuration. Read-only/advisory.
|
|
5144
|
+
*/
|
|
5145
|
+
generateLintScene(input) {
|
|
5146
|
+
const optsJson = JSON.stringify({
|
|
5147
|
+
...(input.eps !== undefined ? { eps: input.eps } : {}),
|
|
5148
|
+
...(input.cap !== undefined ? { cap: input.cap } : {}),
|
|
5149
|
+
});
|
|
5150
|
+
return `
|
|
5151
|
+
// Relational-density lint (advisory)
|
|
5152
|
+
(function() {
|
|
5153
|
+
if (typeof app.lintScene !== 'function') {
|
|
5154
|
+
return { success: false, error: 'app.lintScene unavailable — update FxTool to a build with the relational linter (S12-E2)' };
|
|
5155
|
+
}
|
|
5156
|
+
const result = app.lintScene(${optsJson});
|
|
5157
|
+
return { success: true, density: result.density, suggestions: result.suggestions };
|
|
5158
|
+
})();`.trim();
|
|
5159
|
+
}
|
|
5160
|
+
/**
|
|
5161
|
+
* Media (video/audio) via window.PinePaperAgent. Upload is URL-based (the agent
|
|
5162
|
+
* hands a URL, FxTool fetches → File). Async IIFE so uploads resolve before the
|
|
5163
|
+
* value is returned (the governor awaits it).
|
|
5164
|
+
*/
|
|
5165
|
+
generateMedia(input) {
|
|
5166
|
+
const guard = ` const A = (typeof window !== 'undefined') && window.PinePaperAgent;
|
|
5167
|
+
if (!A || typeof A.uploadVideo !== 'function') { return { success: false, error: 'window.PinePaperAgent media API unavailable — update FxTool to a media-capable build' }; }`;
|
|
5168
|
+
switch (input.action) {
|
|
5169
|
+
case 'upload_video': {
|
|
5170
|
+
const opts = JSON.stringify({
|
|
5171
|
+
...(input.position ? { position: input.position } : {}),
|
|
5172
|
+
...(input.scale !== undefined ? { scale: input.scale } : {}),
|
|
5173
|
+
...(input.timeOffset !== undefined ? { timeOffset: input.timeOffset } : {}),
|
|
5174
|
+
...(input.clipInPoint !== undefined ? { clipInPoint: input.clipInPoint } : {}),
|
|
5175
|
+
...(input.clipOutPoint !== undefined ? { clipOutPoint: input.clipOutPoint } : {}),
|
|
5176
|
+
});
|
|
5177
|
+
return `
|
|
5178
|
+
// Upload video from URL
|
|
5179
|
+
(async function() {
|
|
5180
|
+
${guard}
|
|
5181
|
+
const info = await A.uploadVideo(${JSON.stringify(input.url)}, ${opts});
|
|
5182
|
+
return { success: true, action: 'upload_video', media: info };
|
|
5183
|
+
})();`.trim();
|
|
5184
|
+
}
|
|
5185
|
+
case 'upload_audio': {
|
|
5186
|
+
const opts = JSON.stringify({
|
|
5187
|
+
...(input.volume !== undefined ? { volume: input.volume } : {}),
|
|
5188
|
+
...(input.loop !== undefined ? { loop: input.loop } : {}),
|
|
5189
|
+
...(input.muted !== undefined ? { muted: input.muted } : {}),
|
|
5190
|
+
...(input.timeOffset !== undefined ? { timeOffset: input.timeOffset } : {}),
|
|
5191
|
+
});
|
|
5192
|
+
return `
|
|
5193
|
+
// Upload audio from URL
|
|
5194
|
+
(async function() {
|
|
5195
|
+
${guard}
|
|
5196
|
+
const info = await A.uploadAudio(${JSON.stringify(input.url)}, ${opts});
|
|
5197
|
+
return { success: true, action: 'upload_audio', media: info };
|
|
5198
|
+
})();`.trim();
|
|
5199
|
+
}
|
|
5200
|
+
case 'list':
|
|
5201
|
+
return `
|
|
5202
|
+
// List media
|
|
5203
|
+
(function() {
|
|
5204
|
+
${guard}
|
|
5205
|
+
return { success: true, action: 'list', media: A.listMedia() };
|
|
5206
|
+
})();`.trim();
|
|
5207
|
+
case 'remove':
|
|
5208
|
+
return `
|
|
5209
|
+
// Remove media
|
|
5210
|
+
(function() {
|
|
5211
|
+
${guard}
|
|
5212
|
+
const removed = A.removeMedia(${JSON.stringify(input.id)});
|
|
5213
|
+
return { success: removed, action: 'remove', id: ${JSON.stringify(input.id)}, removed: removed };
|
|
5214
|
+
})();`.trim();
|
|
5215
|
+
case 'set_playback_rate':
|
|
5216
|
+
return `
|
|
5217
|
+
// Set media playback rate
|
|
5218
|
+
(function() {
|
|
5219
|
+
${guard}
|
|
5220
|
+
const ok = A.setMediaPlaybackRate(${JSON.stringify(input.id)}, ${input.rate});
|
|
5221
|
+
return { success: ok, action: 'set_playback_rate', id: ${JSON.stringify(input.id)}, rate: ${input.rate} };
|
|
5222
|
+
})();`.trim();
|
|
5223
|
+
case 'set_clip':
|
|
5224
|
+
return `
|
|
5225
|
+
// Re-trim an existing media clip (media-time seconds)
|
|
5226
|
+
(function() {
|
|
5227
|
+
${guard}
|
|
5228
|
+
if (typeof A.setMediaClip !== 'function') { return { success: false, error: 'setMediaClip unavailable — update FxTool' }; }
|
|
5229
|
+
const ok = A.setMediaClip(${JSON.stringify(input.id)}, ${input.inPoint}, ${input.outPoint});
|
|
5230
|
+
return { success: ok, action: 'set_clip', id: ${JSON.stringify(input.id)}, inPoint: ${input.inPoint}, outPoint: ${input.outPoint} };
|
|
5231
|
+
})();`.trim();
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
/**
|
|
5235
|
+
* One-shot image crop via the PinePaper.cropImage facade (Track A parity).
|
|
5236
|
+
* The replacement raster keeps the item's registry id.
|
|
5237
|
+
*/
|
|
5238
|
+
generateCropImage(input) {
|
|
5239
|
+
const opts = input.aspectRatio ? JSON.stringify({ aspectRatio: input.aspectRatio }) : '{}';
|
|
5240
|
+
return `
|
|
5241
|
+
// Crop image ${input.itemId}
|
|
5242
|
+
(async function() {
|
|
5243
|
+
if (typeof app.cropImage !== 'function') { return { success: false, error: 'app.cropImage unavailable — update FxTool to a build with one-shot image ops' }; }
|
|
5244
|
+
const result = await app.cropImage(${JSON.stringify(input.itemId)}, ${JSON.stringify(input.rect)}, ${opts});
|
|
5245
|
+
if (!result) { return { success: false, error: 'crop failed — item is not a raster or rect does not intersect it' }; }
|
|
5246
|
+
return { success: true, itemId: result.data.id, width: Math.round(result.width), height: Math.round(result.height) };
|
|
5247
|
+
})();`.trim();
|
|
5248
|
+
}
|
|
5249
|
+
/**
|
|
5250
|
+
* One-shot chroma key (background removal) via PinePaper.applyChromaKey.
|
|
5251
|
+
* Threshold/smoothing auto-estimate when omitted.
|
|
5252
|
+
*/
|
|
5253
|
+
generateChromaKey(input) {
|
|
5254
|
+
const opts = JSON.stringify({
|
|
5255
|
+
...(input.color !== undefined ? { color: input.color } : {}),
|
|
5256
|
+
...(input.threshold !== undefined ? { threshold: input.threshold } : {}),
|
|
5257
|
+
...(input.smoothing !== undefined ? { smoothing: input.smoothing } : {}),
|
|
5258
|
+
});
|
|
5259
|
+
return `
|
|
5260
|
+
// Chroma-key image ${input.itemId}
|
|
5261
|
+
(async function() {
|
|
5262
|
+
if (typeof app.applyChromaKey !== 'function') { return { success: false, error: 'app.applyChromaKey unavailable — update FxTool to a build with one-shot image ops' }; }
|
|
5263
|
+
const result = await app.applyChromaKey(${JSON.stringify(input.itemId)}, ${opts});
|
|
5264
|
+
if (!result) { return { success: false, error: 'chroma key failed — item is not a raster or the color is invalid' }; }
|
|
5265
|
+
return { success: true, itemId: result.data.id, params: result.data.chromaKeyParams };
|
|
5266
|
+
})();`.trim();
|
|
5267
|
+
}
|
|
5268
|
+
/**
|
|
5269
|
+
* Rigging (skeletons/bones/IK/breakdown-pose keyframes) via app.riggingSystem.
|
|
5270
|
+
* One consolidated emitter; each action maps 1:1 to a riggingSystem method with
|
|
5271
|
+
* FxTool's exact param contract. Guarded on app.riggingSystem.
|
|
5272
|
+
*/
|
|
5273
|
+
generateRigging(input) {
|
|
5274
|
+
const g = ` const R = app.riggingSystem;
|
|
5275
|
+
if (!R) { return { success: false, error: 'app.riggingSystem unavailable — update FxTool to a rigging-capable build' }; }`;
|
|
5276
|
+
const wrap = (comment, body) => `\n// ${comment}\n(function() {\n${g}\n${body}\n})();`.trim();
|
|
5277
|
+
const S = (v) => JSON.stringify(v);
|
|
5278
|
+
switch (input.action) {
|
|
5279
|
+
case 'create_skeleton': {
|
|
5280
|
+
const root = input.rootPosition ? S(input.rootPosition) : S({ x: 400, y: 300 });
|
|
5281
|
+
return wrap('Rigging: create skeleton', ` const skeletonId = R.createSkeleton(${S(input.name ?? null)}, ${root});
|
|
5282
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5283
|
+
return { success: !!skeletonId, action: 'create_skeleton', skeletonId: skeletonId };`);
|
|
5284
|
+
}
|
|
5285
|
+
case 'add_bone': {
|
|
5286
|
+
const config = S({
|
|
5287
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
5288
|
+
...(input.parentBoneId !== undefined ? { parentBoneId: input.parentBoneId } : {}),
|
|
5289
|
+
...(input.length !== undefined ? { length: input.length } : {}),
|
|
5290
|
+
...(input.angle !== undefined ? { angle: input.angle } : {}),
|
|
5291
|
+
...(input.flexibility !== undefined ? { flexibility: input.flexibility } : {}),
|
|
5292
|
+
...(input.segments !== undefined ? { segments: input.segments } : {}),
|
|
5293
|
+
});
|
|
5294
|
+
return wrap('Rigging: add bone', ` const boneId = R.addBone(${S(input.skeletonId)}, ${config});
|
|
5295
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5296
|
+
return { success: !!boneId, action: 'add_bone', boneId: boneId };`);
|
|
5297
|
+
}
|
|
5298
|
+
case 'attach_item': {
|
|
5299
|
+
const opts = S(input.attachPoint !== undefined ? { attachPoint: input.attachPoint } : {});
|
|
5300
|
+
return wrap('Rigging: attach item to bone', ` const ok = R.attachItem(${S(input.skeletonId)}, ${S(input.boneId)}, ${S(input.itemId)}, ${opts});
|
|
5301
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5302
|
+
return { success: !!ok, action: 'attach_item', itemId: ${S(input.itemId)}, boneId: ${S(input.boneId)} };`);
|
|
5303
|
+
}
|
|
5304
|
+
case 'create_ik_chain': {
|
|
5305
|
+
const config = S({
|
|
5306
|
+
boneIds: input.boneIds,
|
|
5307
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
5308
|
+
...(input.solverType !== undefined ? { solverType: input.solverType } : {}),
|
|
5309
|
+
...(input.iterations !== undefined ? { iterations: input.iterations } : {}),
|
|
5310
|
+
...(input.tolerance !== undefined ? { tolerance: input.tolerance } : {}),
|
|
5311
|
+
...(input.strength !== undefined ? { strength: input.strength } : {}),
|
|
5312
|
+
...(input.poleVector !== undefined ? { poleVector: input.poleVector } : {}),
|
|
5313
|
+
});
|
|
5314
|
+
return wrap('Rigging: create IK chain', ` const chainId = R.createIKChain(${S(input.skeletonId)}, ${config});
|
|
5315
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5316
|
+
return { success: !!chainId, action: 'create_ik_chain', chainId: chainId };`);
|
|
5317
|
+
}
|
|
5318
|
+
case 'add_pose_keyframe': {
|
|
5319
|
+
const opts = S({
|
|
5320
|
+
...(input.favor !== undefined ? { favor: input.favor } : {}),
|
|
5321
|
+
...(input.breakdown !== undefined ? { breakdown: input.breakdown } : {}),
|
|
5322
|
+
...(input.curve !== undefined ? { curve: input.curve } : {}),
|
|
5323
|
+
...(input.boneOffsets !== undefined ? { boneOffsets: input.boneOffsets } : {}),
|
|
5324
|
+
...(input.movingHold !== undefined ? { movingHold: input.movingHold } : {}),
|
|
5325
|
+
...(input.holdDrift !== undefined ? { holdDrift: input.holdDrift } : {}),
|
|
5326
|
+
});
|
|
5327
|
+
return wrap('Rigging: add pose keyframe', ` const ok = R.addPoseKeyframe(${S(input.skeletonId)}, ${input.time}, ${S(input.pose)}, ${S(input.easing ?? 'linear')}, ${opts});
|
|
5328
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5329
|
+
return { success: !!ok, action: 'add_pose_keyframe', time: ${input.time} };`);
|
|
5330
|
+
}
|
|
5331
|
+
case 'set_target_path': {
|
|
5332
|
+
const opts = S({
|
|
5333
|
+
...(input.duration !== undefined ? { duration: input.duration } : {}),
|
|
5334
|
+
...(input.loop !== undefined ? { loop: input.loop } : {}),
|
|
5335
|
+
});
|
|
5336
|
+
return wrap('Rigging: set IK target path', ` const ok = R.setTargetPath(${S(input.skeletonId)}, ${S(input.chainId)}, ${S(input.waypoints)}, ${opts});
|
|
5337
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5338
|
+
return { success: !!ok, action: 'set_target_path', chainId: ${S(input.chainId)} };`);
|
|
5339
|
+
}
|
|
5340
|
+
case 'save_pose':
|
|
5341
|
+
return wrap('Rigging: save pose', ` const poseId = R.savePose(${S(input.skeletonId)}, ${S(input.name ?? null)});
|
|
5342
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5343
|
+
return { success: !!poseId, action: 'save_pose', poseId: poseId };`);
|
|
5344
|
+
case 'save_shape_key':
|
|
5345
|
+
return wrap('Rigging: save shape key', ` const shapeKeyId = R.saveShapeKey(${S(input.skeletonId)}, ${S(input.name ?? null)});
|
|
5346
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5347
|
+
return { success: !!shapeKeyId, action: 'save_shape_key', shapeKeyId: shapeKeyId };`);
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
/**
|
|
5351
|
+
* Drive app.constructionSequence (Layer 3): build a step-by-step reveal of a
|
|
5352
|
+
* figure, play it on the timeline, clear it, or list sequences. Item refs are
|
|
5353
|
+
* registryId strings — build resolves them via app.getItemById.
|
|
5354
|
+
*/
|
|
5355
|
+
generateConstructionSequence(input) {
|
|
5356
|
+
const guard = `const cs = app.constructionSequence;
|
|
5357
|
+
if (!cs) return { success: false, error: 'app.constructionSequence unavailable — update FxTool to a build with the construction-sequence library' };`;
|
|
5358
|
+
switch (input.action) {
|
|
5359
|
+
case 'build': {
|
|
5360
|
+
const stepsJson = JSON.stringify(input.steps ?? []);
|
|
5361
|
+
const opts = JSON.stringify({
|
|
5362
|
+
...(input.stepDuration !== undefined ? { stepDuration: input.stepDuration } : {}),
|
|
5363
|
+
...(input.fadeIn !== undefined ? { fadeIn: input.fadeIn } : {}),
|
|
5364
|
+
});
|
|
5365
|
+
return `
|
|
5366
|
+
// Build construction sequence
|
|
5367
|
+
(function() {
|
|
5368
|
+
${guard}
|
|
5369
|
+
const rec = cs.build(${stepsJson}, ${opts});
|
|
5370
|
+
return {
|
|
5371
|
+
success: true,
|
|
5372
|
+
action: 'build',
|
|
5373
|
+
sequenceId: rec.id,
|
|
5374
|
+
stepCount: rec.steps.length,
|
|
5375
|
+
stepDuration: rec.stepDuration,
|
|
5376
|
+
fadeIn: rec.fadeIn,
|
|
5377
|
+
totalDuration: rec.totalDuration,
|
|
5378
|
+
};
|
|
5379
|
+
})();`.trim();
|
|
5380
|
+
}
|
|
5381
|
+
case 'play': {
|
|
5382
|
+
const seqArg = input.sequenceId ? JSON.stringify(input.sequenceId) : 'null';
|
|
5383
|
+
const opts = JSON.stringify({
|
|
5384
|
+
loop: !!input.loop,
|
|
5385
|
+
...(input.duration !== undefined ? { duration: input.duration } : {}),
|
|
5386
|
+
});
|
|
5387
|
+
return `
|
|
5388
|
+
// Play construction sequence
|
|
5389
|
+
(function() {
|
|
5390
|
+
${guard}
|
|
5391
|
+
let seqId = ${seqArg};
|
|
5392
|
+
if (!seqId) { const all = cs.list(); seqId = all.length ? all[all.length - 1].id : null; }
|
|
5393
|
+
if (!seqId) return { success: false, error: 'No sequence to play — build one first' };
|
|
5394
|
+
const duration = cs.play(seqId, ${opts});
|
|
5395
|
+
return { success: true, action: 'play', sequenceId: seqId, duration: duration, loop: ${!!input.loop} };
|
|
5396
|
+
})();`.trim();
|
|
5397
|
+
}
|
|
5398
|
+
case 'clear': {
|
|
5399
|
+
const seqArg = input.sequenceId ? JSON.stringify(input.sequenceId) : 'null';
|
|
5400
|
+
return `
|
|
5401
|
+
// Clear construction sequence
|
|
5402
|
+
(function() {
|
|
5403
|
+
${guard}
|
|
5404
|
+
let seqId = ${seqArg};
|
|
5405
|
+
if (!seqId) { const all = cs.list(); seqId = all.length ? all[all.length - 1].id : null; }
|
|
5406
|
+
if (!seqId) return { success: false, error: 'No sequence to clear' };
|
|
5407
|
+
const ok = cs.clear(seqId);
|
|
5408
|
+
return { success: ok, action: 'clear', sequenceId: seqId };
|
|
5409
|
+
})();`.trim();
|
|
5410
|
+
}
|
|
5411
|
+
case 'list':
|
|
5412
|
+
default:
|
|
5413
|
+
return `
|
|
5414
|
+
// List construction sequences
|
|
5415
|
+
(function() {
|
|
5416
|
+
${guard}
|
|
5417
|
+
const sequences = cs.list().map(function(r) {
|
|
5418
|
+
return { id: r.id, stepCount: r.steps.length, stepDuration: r.stepDuration, totalDuration: r.totalDuration };
|
|
5419
|
+
});
|
|
5420
|
+
return { success: true, action: 'list', sequences: sequences };
|
|
5421
|
+
})();`.trim();
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
/**
|
|
5425
|
+
* Run an app.geometry.* construction helper (Layer 1) and, when createAs is
|
|
5426
|
+
* given, build a canvas item from the result. The op name is whitelisted by the
|
|
5427
|
+
* Zod enum, so it is safe to index app.geometry[op] directly. Mirrors FxTool's
|
|
5428
|
+
* documented composition: app.create('polygon', { points: app.geometry.regularPolygon(...) }).
|
|
5429
|
+
*/
|
|
5430
|
+
generateGeometry(input) {
|
|
5431
|
+
const op = JSON.stringify(input.operation);
|
|
5432
|
+
const argsJson = JSON.stringify(input.args ?? []);
|
|
5433
|
+
const createAsJson = input.createAs ? JSON.stringify(input.createAs) : 'null';
|
|
5434
|
+
return `
|
|
5435
|
+
// Geometry construction: ${input.operation}
|
|
5436
|
+
(function() {
|
|
5437
|
+
const g = app.geometry;
|
|
5438
|
+
if (!g || typeof g[${op}] !== 'function') {
|
|
5439
|
+
return { success: false, error: 'app.geometry.' + ${op} + ' unavailable — update FxTool to a build with the geometry construction library' };
|
|
5440
|
+
}
|
|
5441
|
+
const result = g[${op}](...${argsJson});
|
|
5442
|
+
if (result == null) {
|
|
5443
|
+
return { success: false, operation: ${op}, result: null, error: 'Degenerate construction (parallel lines, collinear points, or a point inside the circle) — returned null' };
|
|
5444
|
+
}
|
|
5445
|
+
|
|
5446
|
+
let itemId = null;
|
|
5447
|
+
const createAs = ${createAsJson};
|
|
5448
|
+
if (createAs) {
|
|
5449
|
+
const { itemType, radius, ...style } = createAs;
|
|
5450
|
+
let item = null;
|
|
5451
|
+
if (Array.isArray(result)) {
|
|
5452
|
+
// Vertex list (regularPolygon / star / polygonFromVertices / tangent points)
|
|
5453
|
+
item = app.create('polygon', { points: result, ...style });
|
|
5454
|
+
} else if (result.center && typeof result.radius === 'number') {
|
|
5455
|
+
// Circle / circumcircle
|
|
5456
|
+
item = app.create('circle', { x: result.center.x, y: result.center.y, radius: result.radius, ...style });
|
|
5457
|
+
} else if (typeof result.x === 'number' && typeof result.y === 'number') {
|
|
5458
|
+
// Point → small marker
|
|
5459
|
+
item = app.create(itemType || 'circle', { x: result.x, y: result.y, radius: (radius != null ? radius : 6), ...style });
|
|
5460
|
+
} else {
|
|
5461
|
+
return { success: true, operation: ${op}, result, itemId: null, note: 'Result is not directly creatable (line/scalar) — use the returned geometry as input to other tools' };
|
|
5462
|
+
}
|
|
5463
|
+
if (item) {
|
|
5464
|
+
if (item.bringToFront) item.bringToFront();
|
|
5465
|
+
itemId = item.data && item.data.registryId;
|
|
5466
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
return { success: true, operation: ${op}, result, itemId };
|
|
5471
|
+
})();`.trim();
|
|
5472
|
+
}
|
|
5473
|
+
/**
|
|
5474
|
+
* Generate code for an equation-driven path (Expression IR — S10 B5).
|
|
5475
|
+
* Emits app.createEquationPath(opts); the whole validated input IS the opts
|
|
5476
|
+
* object. Guards on the app method so old FxTool builds degrade gracefully.
|
|
5477
|
+
*/
|
|
5478
|
+
generateEquationPath(input) {
|
|
5479
|
+
const optsJson = JSON.stringify(input);
|
|
5480
|
+
const kindJson = JSON.stringify(input.kind);
|
|
5481
|
+
return `
|
|
5482
|
+
// Equation-driven path: ${input.kind}${input.preset ? ' ' + input.preset : ''}
|
|
5483
|
+
(function() {
|
|
5484
|
+
if (typeof app.createEquationPath !== 'function') {
|
|
5485
|
+
return { success: false, error: 'app.createEquationPath unavailable — update FxTool to a build with the Expression IR equation-path API (S10 B5)' };
|
|
5486
|
+
}
|
|
5487
|
+
const path = app.createEquationPath(${optsJson});
|
|
5488
|
+
if (!path) {
|
|
5489
|
+
return { success: false, error: 'Equation path produced no points — check expr/xExpr/yExpr, the sample range, or the preset name' };
|
|
5490
|
+
}
|
|
5491
|
+
const itemId = path.data && path.data.registryId;
|
|
5492
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5493
|
+
return { success: true, kind: ${kindJson}, itemId };
|
|
5494
|
+
})();`.trim();
|
|
5495
|
+
}
|
|
5496
|
+
/**
|
|
5497
|
+
* Generate code for the pp:event channel (S11 event-driven scene chains).
|
|
5498
|
+
* create → app.createEvent(name, opts) → eventId; pulse → app.pulseEvent.
|
|
5499
|
+
* Guards on the app methods so old FxTool builds degrade gracefully.
|
|
5500
|
+
*/
|
|
5501
|
+
generateEvent(input) {
|
|
5502
|
+
if (input.action === 'create') {
|
|
5503
|
+
const nameJson = JSON.stringify(input.name);
|
|
5504
|
+
const optsJson = JSON.stringify({ payloadType: input.payloadType, x: input.x, y: input.y });
|
|
5505
|
+
return `
|
|
5506
|
+
// Create event: ${input.name}
|
|
5507
|
+
(function() {
|
|
5508
|
+
if (typeof app.createEvent !== 'function') {
|
|
5509
|
+
return { success: false, error: 'app.createEvent unavailable — update FxTool to a build with the event system (S11)' };
|
|
5510
|
+
}
|
|
5511
|
+
const eventId = app.createEvent(${nameJson}, ${optsJson});
|
|
5512
|
+
if (!eventId) { return { success: false, error: 'Event creation failed' }; }
|
|
5513
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
5514
|
+
return { success: true, action: 'create', eventId };
|
|
5515
|
+
})();`.trim();
|
|
5516
|
+
}
|
|
5517
|
+
// pulse — a runtime fire, not a scene edit (no history save)
|
|
5518
|
+
const eventIdJson = JSON.stringify(input.eventId);
|
|
5519
|
+
const payloadJson = JSON.stringify(input.payload ?? null);
|
|
5520
|
+
return `
|
|
5521
|
+
// Pulse event: ${input.eventId}
|
|
5522
|
+
(function() {
|
|
5523
|
+
if (typeof app.pulseEvent !== 'function') {
|
|
5524
|
+
return { success: false, error: 'app.pulseEvent unavailable — update FxTool to a build with the event system (S11)' };
|
|
5525
|
+
}
|
|
5526
|
+
app.pulseEvent(${eventIdJson}, ${payloadJson});
|
|
5527
|
+
return { success: true, action: 'pulse', eventId: ${eventIdJson} };
|
|
5528
|
+
})();`.trim();
|
|
5529
|
+
}
|
|
4492
5530
|
}
|
|
4493
5531
|
// Export singleton instance
|
|
4494
5532
|
export const codeGenerator = new PinePaperCodeGenerator();
|