@pinepaper.studio/mcp-server 1.5.4 → 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/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -5
- 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/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +0 -1
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +997 -573
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/handlers/camera.d.ts.map +1 -1
- package/dist/tools/handlers/camera.js +7 -1
- package/dist/tools/handlers/camera.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 +160 -113
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +23 -16
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +28 -22
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +190 -36
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1216 -571
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +2826 -829
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +595 -79
- package/dist/types/schemas.js.map +1 -1
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/manifest.json +299 -71
- package/package.json +14 -3
|
@@ -10,20 +10,19 @@ import { CreateItemInputSchema, ModifyItemInputSchema, AddRelationInputSchema, A
|
|
|
10
10
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
11
11
|
// Agent flow mode types
|
|
12
12
|
AgentStartJobInputSchema, AgentEndJobInputSchema, AgentResetInputSchema, AgentBatchExecuteInputSchema, AgentExportInputSchema, AgentAnalyzeInputSchema,
|
|
13
|
-
// Interactive/Trigger types
|
|
14
|
-
AddTriggerInputSchema, RemoveTriggerInputSchema, QueryTriggersInputSchema,
|
|
15
|
-
// Quiz types
|
|
16
|
-
CreateQuizInputSchema, GetQuizStateInputSchema, ResetQuizInputSchema,
|
|
17
13
|
// Letter collage types
|
|
18
14
|
CreateLetterCollageInputSchema, AnimateLetterCollageInputSchema,
|
|
19
15
|
// Map types
|
|
20
16
|
LoadMapInputSchema, HighlightRegionsInputSchema, UnhighlightRegionsInputSchema, ApplyDataColorsInputSchema, AddMarkerInputSchema, AddMapLabelsInputSchema, PanMapInputSchema, ZoomMapInputSchema, ImportCustomMapInputSchema, GetRegionAtPointInputSchema,
|
|
21
17
|
// Map animation/CSV types
|
|
22
18
|
AnimateMapRegionsInputSchema, AnimateMapWaveInputSchema, StopMapAnimationsInputSchema, ExportMapRegionCSVInputSchema, ImportMapRegionCSVInputSchema, SelectMapRegionsInputSchema, DeselectMapRegionsInputSchema,
|
|
19
|
+
// Globe + world tour types
|
|
20
|
+
GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTourInputSchema, StopWorldTourInputSchema, PinToGlobeInputSchema, TourItemInputSchema,
|
|
23
21
|
// Custom relation/code types
|
|
24
22
|
RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
|
|
25
23
|
// Mask types
|
|
26
24
|
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, } from './schemas.js';
|
|
25
|
+
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
27
26
|
// =============================================================================
|
|
28
27
|
// HELPER FUNCTIONS
|
|
29
28
|
// =============================================================================
|
|
@@ -277,7 +276,9 @@ const relations = results.map(r => ({
|
|
|
277
276
|
/**
|
|
278
277
|
* Template for simple animation
|
|
279
278
|
*/
|
|
280
|
-
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}` : '');
|
|
281
282
|
return `
|
|
282
283
|
// Apply ${animationType} animation to ${itemId}
|
|
283
284
|
const item = app.getItemById('${itemId}');
|
|
@@ -286,17 +287,33 @@ if (!item) {
|
|
|
286
287
|
}
|
|
287
288
|
app.animate(item, {
|
|
288
289
|
animationType: '${animationType}',
|
|
289
|
-
animationSpeed: ${speed}
|
|
290
|
+
animationSpeed: ${speed}${extra}
|
|
290
291
|
});
|
|
291
292
|
|
|
292
293
|
({ success: true, itemId: '${itemId}', animationType: '${animationType}' });
|
|
293
294
|
`.trim();
|
|
294
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* PinePaper's keyframe interpolator only tweens `position` when it is an [x, y]
|
|
298
|
+
* ARRAY. Callers (and other agents) routinely pass a { x, y } object, which is
|
|
299
|
+
* silently ignored — the item then stays frozen for the whole animation. Accept
|
|
300
|
+
* both shapes by normalizing { x, y } -> [x, y] before the keyframes are emitted.
|
|
301
|
+
*/
|
|
302
|
+
function normalizeKeyframePositions(keyframes) {
|
|
303
|
+
return (keyframes || []).map((kf) => {
|
|
304
|
+
const props = kf?.properties;
|
|
305
|
+
const p = props?.position;
|
|
306
|
+
if (p && !Array.isArray(p) && typeof p === 'object' && 'x' in p && 'y' in p) {
|
|
307
|
+
return { ...kf, properties: { ...props, position: [p.x, p.y] } };
|
|
308
|
+
}
|
|
309
|
+
return kf;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
295
312
|
/**
|
|
296
313
|
* Template for keyframe animation
|
|
297
314
|
*/
|
|
298
|
-
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint) {
|
|
299
|
-
const keyframesJson = JSON.stringify(keyframes, null, 2);
|
|
315
|
+
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits) {
|
|
316
|
+
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
300
317
|
const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
|
|
301
318
|
const opts = { duration: calculatedDuration, loop };
|
|
302
319
|
if (timeOffset !== undefined)
|
|
@@ -305,6 +322,8 @@ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false,
|
|
|
305
322
|
opts.clipInPoint = clipInPoint;
|
|
306
323
|
if (clipOutPoint !== undefined)
|
|
307
324
|
opts.clipOutPoint = clipOutPoint;
|
|
325
|
+
if (timeUnits !== undefined)
|
|
326
|
+
opts.timeUnits = timeUnits;
|
|
308
327
|
return `
|
|
309
328
|
// Apply keyframe animation to ${itemId}
|
|
310
329
|
app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
|
|
@@ -315,12 +334,15 @@ app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
|
|
|
315
334
|
/**
|
|
316
335
|
* Template for executing generators
|
|
317
336
|
*/
|
|
318
|
-
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 } : {};
|
|
319
341
|
return `
|
|
320
|
-
// Execute ${generatorName} generator
|
|
342
|
+
// Execute ${generatorName} generator${region ? ` in region ${region.width}×${region.height} @ (${region.x},${region.y})` : ''}
|
|
321
343
|
(async function() {
|
|
322
|
-
await app.executeGenerator('${generatorName}', ${JSON.stringify(params, null, 2)});
|
|
323
|
-
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) : ''} };
|
|
324
346
|
})();
|
|
325
347
|
`.trim();
|
|
326
348
|
}
|
|
@@ -389,12 +411,12 @@ const items = filtered.map(entry => ({
|
|
|
389
411
|
/**
|
|
390
412
|
* Template for timeline control
|
|
391
413
|
*/
|
|
392
|
-
function generatePlayTimelineCode(action, duration, loop, time) {
|
|
414
|
+
function generatePlayTimelineCode(action, duration, loop, time, deterministic) {
|
|
393
415
|
switch (action) {
|
|
394
416
|
case 'play':
|
|
395
417
|
return `
|
|
396
418
|
// Play keyframe timeline
|
|
397
|
-
app.playKeyframeTimeline(${duration || 5}, ${loop ?? false});
|
|
419
|
+
app.playKeyframeTimeline(${duration || 5}, ${loop ?? false}, { timeUnits: 'seconds' });
|
|
398
420
|
({ success: true, action: 'play', duration: ${duration || 5}, loop: ${loop ?? false} });
|
|
399
421
|
`.trim();
|
|
400
422
|
case 'pause':
|
|
@@ -410,6 +432,18 @@ app.stopKeyframeTimeline();
|
|
|
410
432
|
({ success: true, action: 'stop' });
|
|
411
433
|
`.trim();
|
|
412
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
|
+
}
|
|
413
447
|
return `
|
|
414
448
|
// Seek to time
|
|
415
449
|
app.setPlaybackTime(${time || 0});
|
|
@@ -456,35 +490,29 @@ const size = app.getCanvasSize();
|
|
|
456
490
|
*/
|
|
457
491
|
function generateClearCanvasCode() {
|
|
458
492
|
return `
|
|
459
|
-
// Clear all items from canvas
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Also clear any relations
|
|
478
|
-
if (app.clearAllRelations) {
|
|
479
|
-
app.clearAllRelations();
|
|
493
|
+
// Clear all items from canvas. Prefer app.clearCanvas() — the canonical
|
|
494
|
+
// reset that also tears down relations, the (virtual) camera animation,
|
|
495
|
+
// generators, effects and background. The old manual loop here was broken:
|
|
496
|
+
// it keyed item lookups on entry.id/entry.itemId (the registry getAll()
|
|
497
|
+
// entries don't expose those — getItemById wants the registry id), so it
|
|
498
|
+
// removed 0 items, and it called the non-existent app.clearAllRelations.
|
|
499
|
+
const before = app.itemRegistry ? app.itemRegistry.getAll().length : 0;
|
|
500
|
+
|
|
501
|
+
if (typeof app.clearCanvas === 'function') {
|
|
502
|
+
app.clearCanvas();
|
|
503
|
+
} else {
|
|
504
|
+
// Fallback for older builds without clearCanvas().
|
|
505
|
+
if (app.textItemGroup) app.textItemGroup.removeChildren();
|
|
506
|
+
if (app.patternGroup) app.patternGroup.removeChildren();
|
|
507
|
+
if (app.itemRegistry) app.itemRegistry.clear();
|
|
508
|
+
if (app.relationRegistry) app.relationRegistry.clear();
|
|
480
509
|
}
|
|
481
510
|
|
|
482
|
-
app.historyManager.saveState();
|
|
511
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
483
512
|
|
|
484
|
-
|
|
485
|
-
const remaining = app.itemRegistry.getAll().length;
|
|
513
|
+
const remaining = app.itemRegistry ? app.itemRegistry.getAll().length : 0;
|
|
486
514
|
|
|
487
|
-
({ success: true, removedCount, remainingItems: remaining });
|
|
515
|
+
({ success: true, removedCount: Math.max(0, before - remaining), remainingItems: remaining });
|
|
488
516
|
`.trim();
|
|
489
517
|
}
|
|
490
518
|
/**
|
|
@@ -549,6 +577,82 @@ const formatted = generators.map(g => ({
|
|
|
549
577
|
({ generators: formatted });
|
|
550
578
|
`.trim();
|
|
551
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
|
+
}
|
|
552
656
|
/**
|
|
553
657
|
* Template for batch create - creates multiple items with single history save
|
|
554
658
|
*/
|
|
@@ -632,6 +736,10 @@ export class PinePaperCodeGenerator {
|
|
|
632
736
|
properties.animationType = validated.animationType;
|
|
633
737
|
if (validated.animationSpeed !== undefined)
|
|
634
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;
|
|
635
743
|
if (validated.keyframes !== undefined)
|
|
636
744
|
properties.keyframes = validated.keyframes;
|
|
637
745
|
return generateCreateItemCode(validated.itemType, validated.position, properties);
|
|
@@ -673,21 +781,21 @@ export class PinePaperCodeGenerator {
|
|
|
673
781
|
*/
|
|
674
782
|
generateAnimate(input) {
|
|
675
783
|
const validated = AnimateItemInputSchema.parse(input);
|
|
676
|
-
return generateAnimateCode(validated.itemId, validated.animationType, validated.speed);
|
|
784
|
+
return generateAnimateCode(validated.itemId, validated.animationType, validated.speed, validated.intensity, validated.delay);
|
|
677
785
|
}
|
|
678
786
|
/**
|
|
679
787
|
* Generate code for keyframe animation
|
|
680
788
|
*/
|
|
681
789
|
generateKeyframeAnimate(input) {
|
|
682
790
|
const validated = KeyframeAnimateInputSchema.parse(input);
|
|
683
|
-
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);
|
|
684
792
|
}
|
|
685
793
|
/**
|
|
686
794
|
* Generate code for executing a generator
|
|
687
795
|
*/
|
|
688
796
|
generateExecuteGenerator(input) {
|
|
689
797
|
const validated = ExecuteGeneratorInputSchema.parse(input);
|
|
690
|
-
return generateExecuteGeneratorCode(validated.generatorName, validated.params);
|
|
798
|
+
return generateExecuteGeneratorCode(validated.generatorName, validated.params, validated.region);
|
|
691
799
|
}
|
|
692
800
|
/**
|
|
693
801
|
* Generate code for applying an effect
|
|
@@ -705,8 +813,8 @@ export class PinePaperCodeGenerator {
|
|
|
705
813
|
/**
|
|
706
814
|
* Generate code for timeline control
|
|
707
815
|
*/
|
|
708
|
-
generatePlayTimeline(action, duration, loop, time) {
|
|
709
|
-
return generatePlayTimelineCode(action, duration, loop, time);
|
|
816
|
+
generatePlayTimeline(action, duration, loop, time, deterministic) {
|
|
817
|
+
return generatePlayTimelineCode(action, duration, loop, time, deterministic);
|
|
710
818
|
}
|
|
711
819
|
/**
|
|
712
820
|
* Generate code for setting background color
|
|
@@ -864,6 +972,28 @@ throw new Error('Either svgString or url must be provided');
|
|
|
864
972
|
viewport: ctx.viewport,
|
|
865
973
|
};
|
|
866
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
|
+
})();
|
|
867
997
|
`.trim();
|
|
868
998
|
}
|
|
869
999
|
/**
|
|
@@ -1178,13 +1308,9 @@ if (typeof app.generators !== 'undefined' && app.generators['${backgroundGenerat
|
|
|
1178
1308
|
const sourceId = nameToId['${source}'];
|
|
1179
1309
|
const targetId = nameToId['${target}'];
|
|
1180
1310
|
if (sourceId && targetId) {
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
const params = ${paramsStr};
|
|
1185
|
-
app.relationManager.addRelation(sourceItem, targetItem, '${type}', params);
|
|
1186
|
-
results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
|
|
1187
|
-
}
|
|
1311
|
+
const params = ${paramsStr};
|
|
1312
|
+
app.addRelation(sourceId, targetId, '${type}', params);
|
|
1313
|
+
results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
|
|
1188
1314
|
}
|
|
1189
1315
|
})();
|
|
1190
1316
|
`);
|
|
@@ -1601,14 +1727,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1601
1727
|
if (item.shadowColor || item.shadowBlur) analysis.hasShadows = true;
|
|
1602
1728
|
});
|
|
1603
1729
|
|
|
1604
|
-
//
|
|
1605
|
-
if (app.animationManager) {
|
|
1606
|
-
const animations = app.animationManager.getAll ? app.animationManager.getAll() : (app.animationManager.animations || []);
|
|
1607
|
-
if (animations.length > 0) {
|
|
1608
|
-
analysis.hasAnimations = true;
|
|
1609
|
-
animationSet.add('managed');
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1730
|
+
// FxTool has no animationManager — animations are detected via item.data.animationType walk above
|
|
1612
1731
|
// Check timeline for active animations
|
|
1613
1732
|
if (app.timeline && (app.timeline.isPlaying || app.timeline.animations?.length > 0)) {
|
|
1614
1733
|
analysis.hasAnimations = true;
|
|
@@ -1733,6 +1852,9 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1733
1852
|
const validated = AgentBatchExecuteInputSchema.parse(input);
|
|
1734
1853
|
const { operations, atomic } = validated;
|
|
1735
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);
|
|
1736
1858
|
let code = `
|
|
1737
1859
|
// Batch execute ${operations.length} operations
|
|
1738
1860
|
(async function() {
|
|
@@ -1743,7 +1865,17 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1743
1865
|
try {
|
|
1744
1866
|
`;
|
|
1745
1867
|
operations.forEach((op, index) => {
|
|
1746
|
-
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';
|
|
1747
1879
|
code += `
|
|
1748
1880
|
// Operation ${index}: ${op.type}
|
|
1749
1881
|
try {
|
|
@@ -1751,9 +1883,9 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1751
1883
|
${opCode}
|
|
1752
1884
|
})();
|
|
1753
1885
|
results.push({ index: ${index}, success: true, result: result${index} });
|
|
1754
|
-
if (result${index} && result${index}.itemId) {
|
|
1886
|
+
${claimsItemSlot ? `if (result${index} && result${index}.itemId) {
|
|
1755
1887
|
itemIds.push(result${index}.itemId);
|
|
1756
|
-
}
|
|
1888
|
+
}` : ''}
|
|
1757
1889
|
} catch (opError) {
|
|
1758
1890
|
results.push({ index: ${index}, success: false, error: opError.message });
|
|
1759
1891
|
${isAtomic ? 'throw opError;' : 'success = false;'}
|
|
@@ -1777,18 +1909,44 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
1777
1909
|
/**
|
|
1778
1910
|
* Generate code for a single batch operation
|
|
1779
1911
|
*/
|
|
1780
|
-
generateBatchOperationCode(op, index) {
|
|
1912
|
+
generateBatchOperationCode(op, index, mergePlan) {
|
|
1781
1913
|
switch (op.type) {
|
|
1782
|
-
case 'create':
|
|
1914
|
+
case 'create': {
|
|
1783
1915
|
const pos = op.position || { x: 400, y: 300 };
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1916
|
+
const createProps = (op.properties || {});
|
|
1917
|
+
const props = JSON.stringify(createProps);
|
|
1918
|
+
let createCode = `
|
|
1786
1919
|
const item = app.create('${op.itemType}', { position: { x: ${pos.x}, y: ${pos.y} }, ...${props} });
|
|
1787
|
-
const itemId = item.data && item.data.id ? item.data.id : app.registerItem(item, '${op.itemType}', { source: 'mcp-batch' })
|
|
1920
|
+
const itemId = item.data && item.data.id ? item.data.id : app.registerItem(item, '${op.itemType}', { source: 'mcp-batch' });`;
|
|
1921
|
+
// app.create() does not read blendMode/opacity from its params, but the
|
|
1922
|
+
// underlying Paper.js item supports them directly — apply post-create so
|
|
1923
|
+
// callers can pass them inline in `properties` and have them take effect.
|
|
1924
|
+
if (createProps.blendMode !== undefined) {
|
|
1925
|
+
createCode += `\nif ('blendMode' in item) item.blendMode = ${JSON.stringify(createProps.blendMode)};`;
|
|
1926
|
+
}
|
|
1927
|
+
if (createProps.opacity !== undefined) {
|
|
1928
|
+
createCode += `\nif ('opacity' in item) item.opacity = ${JSON.stringify(createProps.opacity)};`;
|
|
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
|
+
}
|
|
1943
|
+
createCode += `
|
|
1788
1944
|
// Ensure item is visible above backgrounds/generators
|
|
1789
1945
|
if (item.bringToFront) item.bringToFront();
|
|
1790
1946
|
return { itemId };
|
|
1791
1947
|
`;
|
|
1948
|
+
return createCode;
|
|
1949
|
+
}
|
|
1792
1950
|
case 'modify':
|
|
1793
1951
|
const itemRef = op.itemId?.startsWith('$')
|
|
1794
1952
|
? `itemIds[${op.itemId.substring(1)}]`
|
|
@@ -1842,6 +2000,17 @@ item.remove();
|
|
|
1842
2000
|
if (app.itemRegistry) app.itemRegistry.unregister(targetId);
|
|
1843
2001
|
return { itemId: targetId, deleted: true };
|
|
1844
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
|
+
}
|
|
1845
2014
|
case 'set_background':
|
|
1846
2015
|
const bgColor = op.backgroundColor || op.properties?.color || '#000000';
|
|
1847
2016
|
return `
|
|
@@ -1851,9 +2020,10 @@ return { success: true, backgroundColor: '${bgColor}' };
|
|
|
1851
2020
|
case 'execute_generator':
|
|
1852
2021
|
const genName = op.generatorName || 'drawSunburst';
|
|
1853
2022
|
const genParams = JSON.stringify(op.generatorParams || {});
|
|
2023
|
+
const genOptions = JSON.stringify(op.generatorRegion ? { region: op.generatorRegion } : {});
|
|
1854
2024
|
return `
|
|
1855
|
-
await app.executeGenerator('${genName}', ${genParams});
|
|
1856
|
-
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) : ''} };
|
|
1857
2027
|
`;
|
|
1858
2028
|
case 'set_canvas_size': {
|
|
1859
2029
|
const w = op.width || 1080;
|
|
@@ -1866,12 +2036,25 @@ return { success: true, width: ${w}, height: ${h} };
|
|
|
1866
2036
|
`;
|
|
1867
2037
|
}
|
|
1868
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);
|
|
1869
2049
|
const kfItemRef = op.itemId?.startsWith('$')
|
|
1870
2050
|
? `itemIds[${op.itemId.substring(1)}]`
|
|
1871
2051
|
: `'${op.itemId}'`;
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
1874
|
-
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);
|
|
1875
2058
|
return `
|
|
1876
2059
|
const targetId = ${kfItemRef};
|
|
1877
2060
|
app.addAnimation(targetId, ${kfJson}, { duration: ${kfDuration}, loop: ${kfLoop} });
|
|
@@ -1883,16 +2066,14 @@ return { itemId: targetId, duration: ${kfDuration}, loop: ${kfLoop} };
|
|
|
1883
2066
|
? `itemIds[${op.itemId.substring(1)}]`
|
|
1884
2067
|
: `'${op.itemId}'`;
|
|
1885
2068
|
const maskOpts = JSON.stringify(op.maskOptions || {});
|
|
2069
|
+
const preset = op.maskPreset || op.maskType || 'wipeLeft';
|
|
1886
2070
|
return `
|
|
1887
2071
|
const targetId = ${maskItemRef};
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
maskOptions: ${maskOpts}
|
|
1894
|
-
});
|
|
1895
|
-
return result;
|
|
2072
|
+
const item = app.getItemById(targetId);
|
|
2073
|
+
if (!item) throw new Error('Item not found: ' + targetId);
|
|
2074
|
+
if (!app.applyAnimatedMask) throw new Error('Mask system not available');
|
|
2075
|
+
const maskedGroup = app.applyAnimatedMask(item, '${preset}', ${maskOpts});
|
|
2076
|
+
return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
|
|
1896
2077
|
`;
|
|
1897
2078
|
}
|
|
1898
2079
|
case 'apply_effect': {
|
|
@@ -1914,7 +2095,7 @@ return { itemId: targetId, effectType: '${op.effectType || 'sparkle'}' };
|
|
|
1914
2095
|
const ptLoop = op.loop ?? false;
|
|
1915
2096
|
if (ptAction === 'play') {
|
|
1916
2097
|
return `
|
|
1917
|
-
app.playKeyframeTimeline(${ptDuration}, ${ptLoop});
|
|
2098
|
+
app.playKeyframeTimeline(${ptDuration}, ${ptLoop}, { timeUnits: 'seconds' });
|
|
1918
2099
|
return { success: true, action: 'play', duration: ${ptDuration}, loop: ${ptLoop} };
|
|
1919
2100
|
`;
|
|
1920
2101
|
}
|
|
@@ -1940,8 +2121,9 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
1940
2121
|
*/
|
|
1941
2122
|
generateAgentExport(input) {
|
|
1942
2123
|
const validated = AgentExportInputSchema.parse(input);
|
|
1943
|
-
const { platform, format, quality, framing } = validated;
|
|
2124
|
+
const { platform, format, quality, framing, duration } = validated;
|
|
1944
2125
|
const qualityLevel = quality || 'standard';
|
|
2126
|
+
const videoDuration = duration ?? 5;
|
|
1945
2127
|
// Quality settings (bitrate in bps for VideoEncoder)
|
|
1946
2128
|
const qualitySettings = {
|
|
1947
2129
|
draft: { compression: 0.6, fps: 15, dpi: 72, bitrate: 2_000_000 },
|
|
@@ -2055,7 +2237,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2055
2237
|
// ExportEngine._quickExportVideo overrides quality with undefined for
|
|
2056
2238
|
// mp4/webm, _calculateBitrate yields NaN, and VideoEncoder.configure
|
|
2057
2239
|
// rejects with 'malformed bitrate value'.
|
|
2058
|
-
const baseVideoSettings = { format, fps: settings.fps, quality: settings.compression, duration:
|
|
2240
|
+
const baseVideoSettings = { format, fps: settings.fps, quality: settings.compression, duration: ${videoDuration} };
|
|
2059
2241
|
const blobToDataUrl = (b) => new Promise(resolve => {
|
|
2060
2242
|
const r = new FileReader();
|
|
2061
2243
|
r.onloadend = () => resolve(r.result);
|
|
@@ -2178,14 +2360,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2178
2360
|
if (item.shadowColor || item.shadowBlur) hasShadow = true;
|
|
2179
2361
|
});
|
|
2180
2362
|
|
|
2181
|
-
//
|
|
2182
|
-
if (app.animationManager) {
|
|
2183
|
-
const animations = app.animationManager.getAll ? app.animationManager.getAll() : (app.animationManager.animations || []);
|
|
2184
|
-
if (animations.length > 0) {
|
|
2185
|
-
analysis.hasAnimations = true;
|
|
2186
|
-
animationSet.add('managed');
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2363
|
+
// FxTool has no animationManager — animations are detected via item.data.animationType walk above
|
|
2189
2364
|
// Check timeline for active animations
|
|
2190
2365
|
if (app.timeline && (app.timeline.isPlaying || app.timeline.animations?.length > 0)) {
|
|
2191
2366
|
analysis.hasAnimations = true;
|
|
@@ -2249,257 +2424,6 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2249
2424
|
suggestedPlatforms
|
|
2250
2425
|
};
|
|
2251
2426
|
})();
|
|
2252
|
-
`.trim();
|
|
2253
|
-
}
|
|
2254
|
-
// ===========================================================================
|
|
2255
|
-
// INTERACTIVE TRIGGER CODE GENERATORS
|
|
2256
|
-
// ===========================================================================
|
|
2257
|
-
/**
|
|
2258
|
-
* Generate code for adding a trigger
|
|
2259
|
-
*/
|
|
2260
|
-
generateAddTrigger(input) {
|
|
2261
|
-
const validated = AddTriggerInputSchema.parse(input);
|
|
2262
|
-
const { itemId, event, actions, condition, timelineOffset } = validated;
|
|
2263
|
-
const actionsStr = JSON.stringify(actions);
|
|
2264
|
-
const conditionStr = condition ? `'${condition.replace(/'/g, "\\'")}'` : 'null';
|
|
2265
|
-
const offsetStr = timelineOffset !== undefined ? timelineOffset : 'null';
|
|
2266
|
-
return `
|
|
2267
|
-
// Add trigger to ${itemId}
|
|
2268
|
-
const item = app.getItemById('${itemId}');
|
|
2269
|
-
if (!item) throw new Error('Item not found: ${itemId}');
|
|
2270
|
-
|
|
2271
|
-
const trigger = {
|
|
2272
|
-
event: '${event}',
|
|
2273
|
-
actions: ${actionsStr},
|
|
2274
|
-
condition: ${conditionStr},
|
|
2275
|
-
timelineOffset: ${offsetStr}
|
|
2276
|
-
};
|
|
2277
|
-
|
|
2278
|
-
// Initialize triggers array if needed
|
|
2279
|
-
if (!item.data) item.data = {};
|
|
2280
|
-
if (!item.data.triggers) item.data.triggers = [];
|
|
2281
|
-
|
|
2282
|
-
// Add trigger
|
|
2283
|
-
item.data.triggers.push(trigger);
|
|
2284
|
-
|
|
2285
|
-
// Register event handler
|
|
2286
|
-
if (app.triggerManager) {
|
|
2287
|
-
app.triggerManager.registerTrigger(item, trigger);
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
app.historyManager.saveState();
|
|
2291
|
-
({ success: true, itemId: '${itemId}', event: '${event}', triggerId: item.data.triggers.length - 1 });
|
|
2292
|
-
`.trim();
|
|
2293
|
-
}
|
|
2294
|
-
/**
|
|
2295
|
-
* Generate code for removing a trigger
|
|
2296
|
-
*/
|
|
2297
|
-
generateRemoveTrigger(input) {
|
|
2298
|
-
const validated = RemoveTriggerInputSchema.parse(input);
|
|
2299
|
-
const { itemId, event, removeAll } = validated;
|
|
2300
|
-
if (removeAll) {
|
|
2301
|
-
return `
|
|
2302
|
-
// Remove all triggers from ${itemId}
|
|
2303
|
-
const item = app.getItemById('${itemId}');
|
|
2304
|
-
if (!item) throw new Error('Item not found: ${itemId}');
|
|
2305
|
-
|
|
2306
|
-
const removedCount = item.data?.triggers?.length || 0;
|
|
2307
|
-
if (item.data) item.data.triggers = [];
|
|
2308
|
-
|
|
2309
|
-
if (app.triggerManager) {
|
|
2310
|
-
app.triggerManager.unregisterAll(item);
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
app.historyManager.saveState();
|
|
2314
|
-
({ success: true, itemId: '${itemId}', removedCount, removeAll: true });
|
|
2315
|
-
`.trim();
|
|
2316
|
-
}
|
|
2317
|
-
return `
|
|
2318
|
-
// Remove ${event} trigger from ${itemId}
|
|
2319
|
-
const item = app.getItemById('${itemId}');
|
|
2320
|
-
if (!item) throw new Error('Item not found: ${itemId}');
|
|
2321
|
-
|
|
2322
|
-
let removedCount = 0;
|
|
2323
|
-
if (item.data?.triggers) {
|
|
2324
|
-
const before = item.data.triggers.length;
|
|
2325
|
-
item.data.triggers = item.data.triggers.filter(t => t.event !== '${event}');
|
|
2326
|
-
removedCount = before - item.data.triggers.length;
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
if (app.triggerManager) {
|
|
2330
|
-
app.triggerManager.unregisterEvent(item, '${event}');
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
app.historyManager.saveState();
|
|
2334
|
-
({ success: true, itemId: '${itemId}', event: '${event}', removedCount });
|
|
2335
|
-
`.trim();
|
|
2336
|
-
}
|
|
2337
|
-
/**
|
|
2338
|
-
* Generate code for querying triggers
|
|
2339
|
-
*/
|
|
2340
|
-
generateQueryTriggers(input) {
|
|
2341
|
-
const validated = QueryTriggersInputSchema.parse(input);
|
|
2342
|
-
const { itemId, event } = validated;
|
|
2343
|
-
if (itemId) {
|
|
2344
|
-
return `
|
|
2345
|
-
// Query triggers for ${itemId}
|
|
2346
|
-
const item = app.getItemById('${itemId}');
|
|
2347
|
-
if (!item) throw new Error('Item not found: ${itemId}');
|
|
2348
|
-
|
|
2349
|
-
let triggers = item.data?.triggers || [];
|
|
2350
|
-
${event ? `triggers = triggers.filter(t => t.event === '${event}');` : ''}
|
|
2351
|
-
|
|
2352
|
-
({ success: true, itemId: '${itemId}', triggers, count: triggers.length });
|
|
2353
|
-
`.trim();
|
|
2354
|
-
}
|
|
2355
|
-
return `
|
|
2356
|
-
// Query all triggers
|
|
2357
|
-
const allTriggers = [];
|
|
2358
|
-
const items = app.itemRegistry ? app.itemRegistry.getAll() : [];
|
|
2359
|
-
|
|
2360
|
-
items.forEach(entry => {
|
|
2361
|
-
const item = entry.item;
|
|
2362
|
-
if (item.data?.triggers?.length > 0) {
|
|
2363
|
-
let triggers = item.data.triggers;
|
|
2364
|
-
${event ? `triggers = triggers.filter(t => t.event === '${event}');` : ''}
|
|
2365
|
-
triggers.forEach((trigger, idx) => {
|
|
2366
|
-
allTriggers.push({
|
|
2367
|
-
itemId: entry.id,
|
|
2368
|
-
triggerId: idx,
|
|
2369
|
-
...trigger
|
|
2370
|
-
});
|
|
2371
|
-
});
|
|
2372
|
-
}
|
|
2373
|
-
});
|
|
2374
|
-
|
|
2375
|
-
({ success: true, triggers: allTriggers, count: allTriggers.length${event ? `, event: '${event}'` : ''} });
|
|
2376
|
-
`.trim();
|
|
2377
|
-
}
|
|
2378
|
-
// ===========================================================================
|
|
2379
|
-
// QUIZ CODE GENERATORS
|
|
2380
|
-
// ===========================================================================
|
|
2381
|
-
/**
|
|
2382
|
-
* Generate code for creating a quiz
|
|
2383
|
-
*/
|
|
2384
|
-
generateCreateQuiz(input) {
|
|
2385
|
-
const validated = CreateQuizInputSchema.parse(input);
|
|
2386
|
-
const { title, questions, passingScore, showScore, allowRetry, shuffleQuestions, shuffleOptions } = validated;
|
|
2387
|
-
const quizConfig = JSON.stringify({
|
|
2388
|
-
title: title || 'Untitled Quiz',
|
|
2389
|
-
questions,
|
|
2390
|
-
passingScore: passingScore || 70,
|
|
2391
|
-
showScore: showScore !== false,
|
|
2392
|
-
allowRetry: allowRetry !== false,
|
|
2393
|
-
shuffleQuestions: shuffleQuestions || false,
|
|
2394
|
-
shuffleOptions: shuffleOptions || false,
|
|
2395
|
-
}, null, 2);
|
|
2396
|
-
return `
|
|
2397
|
-
// Create quiz
|
|
2398
|
-
const quizConfig = ${quizConfig};
|
|
2399
|
-
|
|
2400
|
-
// Initialize quiz manager if needed
|
|
2401
|
-
if (!app.quizManager) {
|
|
2402
|
-
app.quizManager = {
|
|
2403
|
-
quizzes: [],
|
|
2404
|
-
activeQuiz: null,
|
|
2405
|
-
createQuiz: function(config) {
|
|
2406
|
-
const quiz = {
|
|
2407
|
-
id: 'quiz_' + Date.now(),
|
|
2408
|
-
...config,
|
|
2409
|
-
state: {
|
|
2410
|
-
currentQuestion: 0,
|
|
2411
|
-
answers: [],
|
|
2412
|
-
score: 0,
|
|
2413
|
-
maxScore: config.questions.reduce((sum, q) => sum + q.points, 0),
|
|
2414
|
-
complete: false
|
|
2415
|
-
}
|
|
2416
|
-
};
|
|
2417
|
-
this.quizzes.push(quiz);
|
|
2418
|
-
this.activeQuiz = quiz;
|
|
2419
|
-
return quiz;
|
|
2420
|
-
}
|
|
2421
|
-
};
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
const quiz = app.quizManager.createQuiz(quizConfig);
|
|
2425
|
-
app.historyManager.saveState();
|
|
2426
|
-
|
|
2427
|
-
({
|
|
2428
|
-
success: true,
|
|
2429
|
-
quizId: quiz.id,
|
|
2430
|
-
title: quiz.title,
|
|
2431
|
-
questionCount: quiz.questions.length,
|
|
2432
|
-
maxScore: quiz.state.maxScore,
|
|
2433
|
-
passingScore: quiz.passingScore
|
|
2434
|
-
});
|
|
2435
|
-
`.trim();
|
|
2436
|
-
}
|
|
2437
|
-
/**
|
|
2438
|
-
* Generate code for getting quiz state
|
|
2439
|
-
*/
|
|
2440
|
-
generateGetQuizState(input) {
|
|
2441
|
-
const validated = GetQuizStateInputSchema.parse(input);
|
|
2442
|
-
const { quizId } = validated;
|
|
2443
|
-
const quizRef = quizId
|
|
2444
|
-
? `app.quizManager.quizzes.find(q => q.id === '${quizId}')`
|
|
2445
|
-
: 'app.quizManager?.activeQuiz';
|
|
2446
|
-
return `
|
|
2447
|
-
// Get quiz state
|
|
2448
|
-
if (!app.quizManager) {
|
|
2449
|
-
return { success: false, error: 'No quiz manager initialized' };
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
const quiz = ${quizRef};
|
|
2453
|
-
if (!quiz) {
|
|
2454
|
-
return { success: false, error: 'Quiz not found' };
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
({
|
|
2458
|
-
success: true,
|
|
2459
|
-
quizId: quiz.id,
|
|
2460
|
-
title: quiz.title,
|
|
2461
|
-
currentQuestion: quiz.state.currentQuestion,
|
|
2462
|
-
totalQuestions: quiz.questions.length,
|
|
2463
|
-
score: quiz.state.score,
|
|
2464
|
-
maxScore: quiz.state.maxScore,
|
|
2465
|
-
answers: quiz.state.answers,
|
|
2466
|
-
complete: quiz.state.complete,
|
|
2467
|
-
passed: quiz.state.complete ? (quiz.state.score / quiz.state.maxScore * 100) >= quiz.passingScore : null
|
|
2468
|
-
});
|
|
2469
|
-
`.trim();
|
|
2470
|
-
}
|
|
2471
|
-
/**
|
|
2472
|
-
* Generate code for resetting a quiz
|
|
2473
|
-
*/
|
|
2474
|
-
generateResetQuiz(input) {
|
|
2475
|
-
const validated = ResetQuizInputSchema.parse(input);
|
|
2476
|
-
const { quizId } = validated;
|
|
2477
|
-
const quizRef = quizId
|
|
2478
|
-
? `app.quizManager.quizzes.find(q => q.id === '${quizId}')`
|
|
2479
|
-
: 'app.quizManager?.activeQuiz';
|
|
2480
|
-
return `
|
|
2481
|
-
// Reset quiz
|
|
2482
|
-
if (!app.quizManager) {
|
|
2483
|
-
return { success: false, error: 'No quiz manager initialized' };
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
const quiz = ${quizRef};
|
|
2487
|
-
if (!quiz) {
|
|
2488
|
-
return { success: false, error: 'Quiz not found' };
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
// Reset state
|
|
2492
|
-
quiz.state = {
|
|
2493
|
-
currentQuestion: 0,
|
|
2494
|
-
answers: [],
|
|
2495
|
-
score: 0,
|
|
2496
|
-
maxScore: quiz.questions.reduce((sum, q) => sum + q.points, 0),
|
|
2497
|
-
complete: false
|
|
2498
|
-
};
|
|
2499
|
-
|
|
2500
|
-
app.historyManager.saveState();
|
|
2501
|
-
|
|
2502
|
-
({ success: true, quizId: quiz.id, reset: true });
|
|
2503
2427
|
`.trim();
|
|
2504
2428
|
}
|
|
2505
2429
|
// ===========================================================================
|
|
@@ -2510,7 +2434,7 @@ app.historyManager.saveState();
|
|
|
2510
2434
|
*/
|
|
2511
2435
|
generateCreateLetterCollage(input) {
|
|
2512
2436
|
const validated = CreateLetterCollageInputSchema.parse(input);
|
|
2513
|
-
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;
|
|
2514
2438
|
// Build options object
|
|
2515
2439
|
const options = {
|
|
2516
2440
|
style,
|
|
@@ -2528,6 +2452,10 @@ app.historyManager.saveState();
|
|
|
2528
2452
|
options.gradientPalette = gradientPalette;
|
|
2529
2453
|
if (gradientDirection)
|
|
2530
2454
|
options.gradientDirection = gradientDirection;
|
|
2455
|
+
if (particleSpacing !== undefined)
|
|
2456
|
+
options.particleSpacing = particleSpacing;
|
|
2457
|
+
if (particleDotSize !== undefined)
|
|
2458
|
+
options.particleDotSize = particleDotSize;
|
|
2531
2459
|
return `
|
|
2532
2460
|
// Create letter collage
|
|
2533
2461
|
(function() {
|
|
@@ -2588,80 +2516,6 @@ app.historyManager.saveState();
|
|
|
2588
2516
|
|
|
2589
2517
|
return result;
|
|
2590
2518
|
})();
|
|
2591
|
-
`.trim();
|
|
2592
|
-
}
|
|
2593
|
-
/**
|
|
2594
|
-
* Generate code to get letter collage options
|
|
2595
|
-
*/
|
|
2596
|
-
generateGetLetterCollageOptions() {
|
|
2597
|
-
return `
|
|
2598
|
-
// Get letter collage options
|
|
2599
|
-
(function() {
|
|
2600
|
-
if (!app.getLetterCollageOptions) {
|
|
2601
|
-
// Return static options if function not available
|
|
2602
|
-
return {
|
|
2603
|
-
success: true,
|
|
2604
|
-
styles: ['tile', 'magazine', 'paperCut', 'fold', 'gradient', 'image'],
|
|
2605
|
-
tilePalettes: {
|
|
2606
|
-
game: ['wordle', 'scrabble'],
|
|
2607
|
-
vibrant: ['candy', 'neon', 'rainbow'],
|
|
2608
|
-
soft: ['pastel', 'cotton'],
|
|
2609
|
-
natural: ['earth', 'ocean', 'forest', 'sunset'],
|
|
2610
|
-
professional: ['corporate', 'minimal', 'slate'],
|
|
2611
|
-
seasonal: ['christmas', 'halloween', 'spring'],
|
|
2612
|
-
magazine: ['magazine', 'newspaper', 'vintage'],
|
|
2613
|
-
paperCraft: ['paperCraft', 'origami', 'craftPaper']
|
|
2614
|
-
},
|
|
2615
|
-
gradientPalettes: ['rainbow', 'sunset', 'ocean', 'fire', 'gold', 'rose', 'ice', 'cyberpunk', 'neonGlow', 'purplePink'],
|
|
2616
|
-
gradientDirections: ['vertical', 'horizontal', 'diagonal', 'radial']
|
|
2617
|
-
};
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
return app.getLetterCollageOptions();
|
|
2621
|
-
})();
|
|
2622
|
-
`.trim();
|
|
2623
|
-
}
|
|
2624
|
-
// ===========================================================================
|
|
2625
|
-
// CANVAS PRESETS CODE GENERATORS
|
|
2626
|
-
// ===========================================================================
|
|
2627
|
-
/**
|
|
2628
|
-
* Generate code to get canvas presets
|
|
2629
|
-
*/
|
|
2630
|
-
generateGetCanvasPresets() {
|
|
2631
|
-
return `
|
|
2632
|
-
// Get canvas presets
|
|
2633
|
-
(function() {
|
|
2634
|
-
if (app.getCanvasPresets) {
|
|
2635
|
-
return app.getCanvasPresets();
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
// Return static presets if function not available
|
|
2639
|
-
return {
|
|
2640
|
-
success: true,
|
|
2641
|
-
presets: [
|
|
2642
|
-
{ key: 'youtube-thumbnail', name: 'YouTube Thumbnail', width: 1280, height: 720, aspectRatio: '16:9', category: 'video' },
|
|
2643
|
-
{ key: 'youtube-short', name: 'YouTube Shorts', width: 1080, height: 1920, aspectRatio: '9:16', category: 'video' },
|
|
2644
|
-
{ key: 'tiktok', name: 'TikTok', width: 1080, height: 1920, aspectRatio: '9:16', category: 'video' },
|
|
2645
|
-
{ key: 'instagram-story', name: 'Instagram Story', width: 1080, height: 1920, aspectRatio: '9:16', category: 'social' },
|
|
2646
|
-
{ key: 'instagram-post', name: 'Instagram Post (Square)', width: 1080, height: 1080, aspectRatio: '1:1', category: 'social' },
|
|
2647
|
-
{ key: 'instagram-landscape', name: 'Instagram Landscape', width: 1080, height: 566, aspectRatio: '1.91:1', category: 'social' },
|
|
2648
|
-
{ key: 'instagram-portrait', name: 'Instagram Portrait', width: 1080, height: 1350, aspectRatio: '4:5', category: 'social' },
|
|
2649
|
-
{ key: 'facebook-post', name: 'Facebook Post', width: 1200, height: 630, aspectRatio: '1.91:1', category: 'social' },
|
|
2650
|
-
{ key: 'facebook-cover', name: 'Facebook Cover', width: 820, height: 312, aspectRatio: '2.63:1', category: 'social' },
|
|
2651
|
-
{ key: 'facebook-story', name: 'Facebook Story', width: 1080, height: 1920, aspectRatio: '9:16', category: 'social' },
|
|
2652
|
-
{ key: 'twitter-post', name: 'Twitter/X Post', width: 1200, height: 675, aspectRatio: '16:9', category: 'social' },
|
|
2653
|
-
{ key: 'twitter-header', name: 'Twitter/X Header', width: 1500, height: 500, aspectRatio: '3:1', category: 'social' },
|
|
2654
|
-
{ key: 'linkedin-post', name: 'LinkedIn Post', width: 1200, height: 627, aspectRatio: '1.91:1', category: 'social' },
|
|
2655
|
-
{ key: 'linkedin-banner', name: 'LinkedIn Banner', width: 1584, height: 396, aspectRatio: '4:1', category: 'social' },
|
|
2656
|
-
{ key: 'pinterest-pin', name: 'Pinterest Pin', width: 1000, height: 1500, aspectRatio: '2:3', category: 'social' },
|
|
2657
|
-
{ key: 'presentation-16x9', name: 'Presentation 16:9', width: 1920, height: 1080, aspectRatio: '16:9', category: 'presentation' },
|
|
2658
|
-
{ key: 'presentation-4x3', name: 'Presentation 4:3', width: 1024, height: 768, aspectRatio: '4:3', category: 'presentation' },
|
|
2659
|
-
{ key: 'hd-720p', name: 'HD 720p', width: 1280, height: 720, aspectRatio: '16:9', category: 'video' },
|
|
2660
|
-
{ key: 'full-hd-1080p', name: 'Full HD 1080p', width: 1920, height: 1080, aspectRatio: '16:9', category: 'video' },
|
|
2661
|
-
{ key: 'default', name: 'Default', width: 800, height: 600, aspectRatio: '4:3', category: 'general' }
|
|
2662
|
-
]
|
|
2663
|
-
};
|
|
2664
|
-
})();
|
|
2665
2519
|
`.trim();
|
|
2666
2520
|
}
|
|
2667
2521
|
// ===========================================================================
|
|
@@ -3185,54 +3039,246 @@ app.historyManager.saveState();
|
|
|
3185
3039
|
})();
|
|
3186
3040
|
`.trim();
|
|
3187
3041
|
}
|
|
3042
|
+
// -------------------------------------------------------------------------
|
|
3043
|
+
// Globe + World Tour
|
|
3044
|
+
// -------------------------------------------------------------------------
|
|
3188
3045
|
/**
|
|
3189
|
-
* Generate code to
|
|
3046
|
+
* Generate code to enable globe (orthographic) mode
|
|
3190
3047
|
*/
|
|
3191
|
-
|
|
3048
|
+
generateGlobeEnable(input) {
|
|
3049
|
+
const validated = GlobeEnableInputSchema.parse(input);
|
|
3050
|
+
const optionsStr = JSON.stringify(validated);
|
|
3192
3051
|
return `
|
|
3193
|
-
//
|
|
3052
|
+
// Enable globe mode
|
|
3194
3053
|
(function() {
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
if (app.itemRegistry) {
|
|
3202
|
-
for (const [id, item] of app.itemRegistry.entries()) {
|
|
3203
|
-
const itemData = {
|
|
3204
|
-
id: id,
|
|
3205
|
-
type: item.data?.type || item.className?.toLowerCase() || 'unknown',
|
|
3206
|
-
position: item.position ? { x: item.position.x, y: item.position.y } : null,
|
|
3207
|
-
bounds: item.bounds ? {
|
|
3208
|
-
x: item.bounds.x,
|
|
3209
|
-
y: item.bounds.y,
|
|
3210
|
-
width: item.bounds.width,
|
|
3211
|
-
height: item.bounds.height
|
|
3212
|
-
} : null,
|
|
3213
|
-
properties: {}
|
|
3214
|
-
};
|
|
3215
|
-
|
|
3216
|
-
// Collect common properties
|
|
3217
|
-
if (item.fillColor) itemData.properties.fillColor = item.fillColor.toCSS ? item.fillColor.toCSS() : item.fillColor;
|
|
3218
|
-
if (item.strokeColor) itemData.properties.strokeColor = item.strokeColor.toCSS ? item.strokeColor.toCSS() : item.strokeColor;
|
|
3219
|
-
if (item.strokeWidth) itemData.properties.strokeWidth = item.strokeWidth;
|
|
3220
|
-
if (item.opacity !== undefined) itemData.properties.opacity = item.opacity;
|
|
3221
|
-
if (item.rotation) itemData.properties.rotation = item.rotation;
|
|
3222
|
-
if (item.data?.content) itemData.properties.content = item.data.content;
|
|
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
|
+
}
|
|
3223
3060
|
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
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();
|
|
3230
3069
|
}
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
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
|
+
})();
|
|
3232
|
+
`.trim();
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* Generate code to export the complete scene state
|
|
3236
|
+
*/
|
|
3237
|
+
generateExportScene() {
|
|
3238
|
+
return `
|
|
3239
|
+
// Export complete scene state
|
|
3240
|
+
(function() {
|
|
3241
|
+
try {
|
|
3242
|
+
const items = [];
|
|
3243
|
+
const relations = [];
|
|
3244
|
+
const decorative = [];
|
|
3245
|
+
|
|
3246
|
+
// Collect items from registry
|
|
3247
|
+
if (app.itemRegistry) {
|
|
3248
|
+
for (const [id, item] of app.itemRegistry.entries()) {
|
|
3249
|
+
const itemData = {
|
|
3250
|
+
id: id,
|
|
3251
|
+
type: item.data?.type || item.className?.toLowerCase() || 'unknown',
|
|
3252
|
+
position: item.position ? { x: item.position.x, y: item.position.y } : null,
|
|
3253
|
+
bounds: item.bounds ? {
|
|
3254
|
+
x: item.bounds.x,
|
|
3255
|
+
y: item.bounds.y,
|
|
3256
|
+
width: item.bounds.width,
|
|
3257
|
+
height: item.bounds.height
|
|
3258
|
+
} : null,
|
|
3259
|
+
properties: {}
|
|
3260
|
+
};
|
|
3261
|
+
|
|
3262
|
+
// Collect common properties
|
|
3263
|
+
if (item.fillColor) itemData.properties.fillColor = item.fillColor.toCSS ? item.fillColor.toCSS() : item.fillColor;
|
|
3264
|
+
if (item.strokeColor) itemData.properties.strokeColor = item.strokeColor.toCSS ? item.strokeColor.toCSS() : item.strokeColor;
|
|
3265
|
+
if (item.strokeWidth) itemData.properties.strokeWidth = item.strokeWidth;
|
|
3266
|
+
if (item.opacity !== undefined) itemData.properties.opacity = item.opacity;
|
|
3267
|
+
if (item.rotation) itemData.properties.rotation = item.rotation;
|
|
3268
|
+
if (item.data?.content) itemData.properties.content = item.data.content;
|
|
3269
|
+
|
|
3270
|
+
if (item.data?.isDecorative) {
|
|
3271
|
+
decorative.push(itemData);
|
|
3272
|
+
} else {
|
|
3273
|
+
items.push(itemData);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// Collect relations
|
|
3279
|
+
if (app.getRelations) {
|
|
3280
|
+
const activeRelations = app.getRelations();
|
|
3281
|
+
for (const rel of activeRelations) {
|
|
3236
3282
|
relations.push({
|
|
3237
3283
|
sourceId: rel.sourceId,
|
|
3238
3284
|
targetId: rel.targetId,
|
|
@@ -3446,22 +3492,27 @@ app.historyManager.saveState();
|
|
|
3446
3492
|
const optionsStr = validated.options ? JSON.stringify(validated.options) : '{}';
|
|
3447
3493
|
const keyframesStr = validated.keyframes ? JSON.stringify(validated.keyframes) : 'null';
|
|
3448
3494
|
const maskOptionsStr = validated.maskOptions ? JSON.stringify(validated.maskOptions) : '{}';
|
|
3495
|
+
const preset = validated.preset || validated.maskType || 'wipeLeft';
|
|
3496
|
+
// Per-preset options. Custom keyframes are an animated-mask extension —
|
|
3497
|
+
// when supplied, fall through to maskingSystem.applyCustomMask instead.
|
|
3449
3498
|
return `
|
|
3450
3499
|
// Apply animated mask to ${validated.itemId}
|
|
3451
3500
|
(function() {
|
|
3452
|
-
if (!app.
|
|
3501
|
+
if (!app.applyAnimatedMask) {
|
|
3453
3502
|
return { success: false, error: 'Mask system not available' };
|
|
3454
3503
|
}
|
|
3504
|
+
const item = app.getItemById('${validated.itemId}');
|
|
3505
|
+
if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
|
|
3455
3506
|
|
|
3456
3507
|
try {
|
|
3457
|
-
const
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
}
|
|
3464
|
-
return
|
|
3508
|
+
const mergedOpts = Object.assign({}, ${optionsStr}, ${maskOptionsStr});
|
|
3509
|
+
let maskedGroup;
|
|
3510
|
+
if (${keyframesStr} && app.maskingSystem && app.maskingSystem.applyCustomMask) {
|
|
3511
|
+
maskedGroup = app.maskingSystem.applyCustomMask(item, '${preset}', ${keyframesStr}, mergedOpts);
|
|
3512
|
+
} else {
|
|
3513
|
+
maskedGroup = app.applyAnimatedMask(item, '${preset}', mergedOpts);
|
|
3514
|
+
}
|
|
3515
|
+
return { success: !!maskedGroup, itemId: '${validated.itemId}', preset: '${preset}' };
|
|
3465
3516
|
} catch (error) {
|
|
3466
3517
|
return { success: false, error: error.message };
|
|
3467
3518
|
}
|
|
@@ -3475,17 +3526,15 @@ app.historyManager.saveState();
|
|
|
3475
3526
|
return `
|
|
3476
3527
|
// Apply custom mask to ${validated.itemId}
|
|
3477
3528
|
(function() {
|
|
3478
|
-
if (!app.
|
|
3529
|
+
if (!app.maskingSystem || !app.maskingSystem.applyCustomMask) {
|
|
3479
3530
|
return { success: false, error: 'Mask system not available' };
|
|
3480
3531
|
}
|
|
3532
|
+
const item = app.getItemById('${validated.itemId}');
|
|
3533
|
+
if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
|
|
3481
3534
|
|
|
3482
3535
|
try {
|
|
3483
|
-
const
|
|
3484
|
-
|
|
3485
|
-
keyframes: ${keyframesStr},
|
|
3486
|
-
maskOptions: ${maskOptionsStr}
|
|
3487
|
-
});
|
|
3488
|
-
return result;
|
|
3536
|
+
const maskedGroup = app.maskingSystem.applyCustomMask(item, '${validated.maskType}', ${keyframesStr}, ${maskOptionsStr});
|
|
3537
|
+
return { success: !!maskedGroup, itemId: '${validated.itemId}', maskType: '${validated.maskType}' };
|
|
3489
3538
|
} catch (error) {
|
|
3490
3539
|
return { success: false, error: error.message };
|
|
3491
3540
|
}
|
|
@@ -3497,12 +3546,17 @@ app.historyManager.saveState();
|
|
|
3497
3546
|
return `
|
|
3498
3547
|
// Remove mask from ${validated.itemId}
|
|
3499
3548
|
(function() {
|
|
3500
|
-
if (!app.
|
|
3549
|
+
if (!app.removeMask) {
|
|
3501
3550
|
return { success: false, error: 'Mask system not available' };
|
|
3502
3551
|
}
|
|
3552
|
+
// The item id may point at either the masked group OR the original item;
|
|
3553
|
+
// app.removeMask expects the masked group, so resolve and walk up if needed.
|
|
3554
|
+
const item = app.getItemById('${validated.itemId}');
|
|
3555
|
+
if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
|
|
3556
|
+
const maskedGroup = (item.data && item.data.isMaskedGroup) ? item : (item.parent && item.parent.data && item.parent.data.isMaskedGroup ? item.parent : item);
|
|
3503
3557
|
|
|
3504
3558
|
try {
|
|
3505
|
-
|
|
3559
|
+
app.removeMask(maskedGroup);
|
|
3506
3560
|
return { success: true, itemId: '${validated.itemId}', message: 'Mask removed' };
|
|
3507
3561
|
} catch (error) {
|
|
3508
3562
|
return { success: false, error: error.message };
|
|
@@ -3596,10 +3650,12 @@ app.historyManager.saveState();
|
|
|
3596
3650
|
})();
|
|
3597
3651
|
`.trim();
|
|
3598
3652
|
}
|
|
3653
|
+
// =============================================================================
|
|
3654
|
+
// TEMPLATE
|
|
3655
|
+
// =============================================================================
|
|
3599
3656
|
generateApplyTemplate(input) {
|
|
3600
3657
|
const { templateId, category, listOnly } = input;
|
|
3601
3658
|
if (listOnly || !templateId) {
|
|
3602
|
-
// List mode: return available templates, optionally filtered by category
|
|
3603
3659
|
const categoryFilter = category ? `'${category}'` : 'null';
|
|
3604
3660
|
return `
|
|
3605
3661
|
// List available templates
|
|
@@ -3625,7 +3681,6 @@ app.historyManager.saveState();
|
|
|
3625
3681
|
})();
|
|
3626
3682
|
`.trim();
|
|
3627
3683
|
}
|
|
3628
|
-
// Load mode: apply a specific template
|
|
3629
3684
|
return `
|
|
3630
3685
|
// Apply template: ${templateId}
|
|
3631
3686
|
(async function() {
|
|
@@ -3901,7 +3956,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
3901
3956
|
// Select all items
|
|
3902
3957
|
(function() {
|
|
3903
3958
|
app.selectAll();
|
|
3904
|
-
const items = app.
|
|
3959
|
+
const items = app.getSelectedItems().map(i => i.data?.itemId || i.name || i.id);
|
|
3905
3960
|
return { success: true, action: 'select_all', count: items.length, items };
|
|
3906
3961
|
})();`.trim();
|
|
3907
3962
|
case 'deselect_all':
|
|
@@ -3915,7 +3970,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
3915
3970
|
return `
|
|
3916
3971
|
// Get current selection
|
|
3917
3972
|
(function() {
|
|
3918
|
-
const items = app.
|
|
3973
|
+
const items = app.getSelectedItems().map(i => ({
|
|
3919
3974
|
itemId: i.data?.itemId || i.name || i.id,
|
|
3920
3975
|
type: i.data?.itemType || i.className,
|
|
3921
3976
|
bounds: i.bounds ? { x: i.bounds.x, y: i.bounds.y, width: i.bounds.width, height: i.bounds.height } : null
|
|
@@ -3926,7 +3981,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
3926
3981
|
return `
|
|
3927
3982
|
// Delete selected items
|
|
3928
3983
|
(function() {
|
|
3929
|
-
const selection = app.
|
|
3984
|
+
const selection = app.getSelectedItems();
|
|
3930
3985
|
const deleted = selection.map(i => i.data?.itemId || i.name || i.id);
|
|
3931
3986
|
app.deleteSelected();
|
|
3932
3987
|
return { success: true, action: 'delete_selected', deleted, count: deleted.length };
|
|
@@ -4031,15 +4086,23 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
4031
4086
|
// IMAGE PROCESSING
|
|
4032
4087
|
// ===========================================================================
|
|
4033
4088
|
generateImageFilter(input) {
|
|
4034
|
-
|
|
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' };`;
|
|
4035
4098
|
switch (input.action) {
|
|
4036
4099
|
case 'apply':
|
|
4037
4100
|
return `
|
|
4038
4101
|
// Apply image filter
|
|
4039
4102
|
(async function() {
|
|
4040
|
-
|
|
4103
|
+
${resolve}
|
|
4041
4104
|
try {
|
|
4042
|
-
|
|
4105
|
+
await app.applyImageFilter(item, ${JSON.stringify(input.filterName || '')}, ${JSON.stringify(input.params || {})});
|
|
4043
4106
|
return { success: true, action: 'apply', itemId: ${JSON.stringify(input.itemId)}, filter: ${JSON.stringify(input.filterName || '')} };
|
|
4044
4107
|
} catch (e) {
|
|
4045
4108
|
return { error: 'Failed to apply filter: ' + e.message };
|
|
@@ -4049,10 +4112,10 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
4049
4112
|
return `
|
|
4050
4113
|
// Apply filter chain
|
|
4051
4114
|
(async function() {
|
|
4052
|
-
|
|
4115
|
+
${resolve}
|
|
4053
4116
|
try {
|
|
4054
4117
|
const filters = ${JSON.stringify(input.filters || [])};
|
|
4055
|
-
|
|
4118
|
+
await app.applyImageFilterChain(item, filters);
|
|
4056
4119
|
return { success: true, action: 'chain', itemId: ${JSON.stringify(input.itemId)}, filterCount: filters.length };
|
|
4057
4120
|
} catch (e) {
|
|
4058
4121
|
return { error: 'Failed to apply filter chain: ' + e.message };
|
|
@@ -4160,29 +4223,6 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
4160
4223
|
return `(function() { return { error: 'Unknown precomp action: ${input.action}' }; })();`;
|
|
4161
4224
|
}
|
|
4162
4225
|
}
|
|
4163
|
-
generateView(input) {
|
|
4164
|
-
switch (input.action) {
|
|
4165
|
-
case 'fit': {
|
|
4166
|
-
const mode = input.mode || 'content';
|
|
4167
|
-
const padding = input.padding ?? 20;
|
|
4168
|
-
return `
|
|
4169
|
-
// Fit view to ${mode}
|
|
4170
|
-
(function() {
|
|
4171
|
-
app.fitView('${mode}', ${padding});
|
|
4172
|
-
return { success: true, action: 'fit', mode: '${mode}', padding: ${padding} };
|
|
4173
|
-
})();`.trim();
|
|
4174
|
-
}
|
|
4175
|
-
case 'get_state':
|
|
4176
|
-
return `
|
|
4177
|
-
// Get current view state
|
|
4178
|
-
(function() {
|
|
4179
|
-
const state = app.getViewState();
|
|
4180
|
-
return { success: true, action: 'get_state', ...state };
|
|
4181
|
-
})();`.trim();
|
|
4182
|
-
default:
|
|
4183
|
-
return `(function() { return { error: 'Unknown view action: ${input.action}' }; })();`;
|
|
4184
|
-
}
|
|
4185
|
-
}
|
|
4186
4226
|
generateBackground(input) {
|
|
4187
4227
|
switch (input.action) {
|
|
4188
4228
|
case 'set': {
|
|
@@ -4226,8 +4266,9 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
4226
4266
|
return `
|
|
4227
4267
|
// Get background info
|
|
4228
4268
|
(function() {
|
|
4229
|
-
|
|
4230
|
-
|
|
4269
|
+
// FxTool exposes getBackgroundMode() returning the mode string only.
|
|
4270
|
+
const mode = app.getBackgroundMode ? app.getBackgroundMode() : null;
|
|
4271
|
+
return { success: true, action: 'get', mode };
|
|
4231
4272
|
})();`.trim();
|
|
4232
4273
|
default:
|
|
4233
4274
|
return `(function() { return { error: 'Unknown background action: ${input.action}' }; })();`;
|
|
@@ -4435,61 +4476,6 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
4435
4476
|
}
|
|
4436
4477
|
}
|
|
4437
4478
|
// ===========================================================================
|
|
4438
|
-
// STORAGE
|
|
4439
|
-
// ===========================================================================
|
|
4440
|
-
generateStorage(input) {
|
|
4441
|
-
const guard = `if (!app.storageManager) return { error: 'StorageManager not available' };`;
|
|
4442
|
-
switch (input.action) {
|
|
4443
|
-
case 'save': {
|
|
4444
|
-
const name = input.name ? JSON.stringify(input.name) : "'Untitled Project'";
|
|
4445
|
-
const thumbnailCode = input.thumbnail
|
|
4446
|
-
? `const thumbnail = app.exportThumbnail ? app.exportThumbnail() : null;\n if (thumbnail) project.thumbnail = thumbnail;`
|
|
4447
|
-
: '';
|
|
4448
|
-
return `
|
|
4449
|
-
// Save project
|
|
4450
|
-
(async function() {
|
|
4451
|
-
${guard}
|
|
4452
|
-
const project = { name: ${name}, data: app.serialize() };
|
|
4453
|
-
${thumbnailCode}
|
|
4454
|
-
const projectId = await app.storageManager.saveProject(project);
|
|
4455
|
-
return { success: true, action: 'save', projectId, name: ${name} };
|
|
4456
|
-
})();`.trim();
|
|
4457
|
-
}
|
|
4458
|
-
case 'load': {
|
|
4459
|
-
const projId = JSON.stringify(input.projectId || '');
|
|
4460
|
-
return `
|
|
4461
|
-
// Load project
|
|
4462
|
-
(async function() {
|
|
4463
|
-
${guard}
|
|
4464
|
-
const project = await app.storageManager.loadProject(${projId});
|
|
4465
|
-
if (!project) return { error: 'Project not found: ' + ${projId} };
|
|
4466
|
-
app.deserialize(project.data);
|
|
4467
|
-
return { success: true, action: 'load', projectId: ${projId}, name: project.name };
|
|
4468
|
-
})();`.trim();
|
|
4469
|
-
}
|
|
4470
|
-
case 'list':
|
|
4471
|
-
return `
|
|
4472
|
-
// List saved projects
|
|
4473
|
-
(async function() {
|
|
4474
|
-
${guard}
|
|
4475
|
-
const projects = await app.storageManager.listProjects();
|
|
4476
|
-
return { success: true, action: 'list', projects: projects.map(p => ({ id: p.id, name: p.name, updatedAt: p.updatedAt })), count: projects.length };
|
|
4477
|
-
})();`.trim();
|
|
4478
|
-
case 'delete': {
|
|
4479
|
-
const projId = JSON.stringify(input.projectId || '');
|
|
4480
|
-
return `
|
|
4481
|
-
// Delete project
|
|
4482
|
-
(async function() {
|
|
4483
|
-
${guard}
|
|
4484
|
-
await app.storageManager.deleteProject(${projId});
|
|
4485
|
-
return { success: true, action: 'delete', projectId: ${projId} };
|
|
4486
|
-
})();`.trim();
|
|
4487
|
-
}
|
|
4488
|
-
default:
|
|
4489
|
-
return `(function() { return { error: 'Unknown storage action: ${input.action}' }; })();`;
|
|
4490
|
-
}
|
|
4491
|
-
}
|
|
4492
|
-
// ===========================================================================
|
|
4493
4479
|
// INTERACTION
|
|
4494
4480
|
// ===========================================================================
|
|
4495
4481
|
generateInteraction(input) {
|
|
@@ -4559,8 +4545,8 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
4559
4545
|
return `
|
|
4560
4546
|
// Export widget (pp:PinePaper ontology JSON)
|
|
4561
4547
|
(async function() {
|
|
4562
|
-
if (!app.exportWidget) return { error: 'Widget export not available' };
|
|
4563
|
-
const result = await app.exportWidget(${JSON.stringify(opts)});
|
|
4548
|
+
if (!app.exportEngine || !app.exportEngine.exportWidget) return { error: 'Widget export not available' };
|
|
4549
|
+
const result = await app.exportEngine.exportWidget(${JSON.stringify(opts)});
|
|
4564
4550
|
return { success: true, json: result.json, filename: result.filename, embedCode: result.embedCode, metadata: result.data?.metadata };
|
|
4565
4551
|
})();`.trim();
|
|
4566
4552
|
}
|
|
@@ -4573,8 +4559,8 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
4573
4559
|
return `
|
|
4574
4560
|
// Export widget as self-contained HTML
|
|
4575
4561
|
(async function() {
|
|
4576
|
-
if (!app.exportWidgetHTML) return { error: 'Widget HTML export not available' };
|
|
4577
|
-
const result = await app.exportWidgetHTML(${JSON.stringify(opts)});
|
|
4562
|
+
if (!app.exportEngine || !app.exportEngine.exportWidgetHTML) return { error: 'Widget HTML export not available' };
|
|
4563
|
+
const result = await app.exportEngine.exportWidgetHTML(${JSON.stringify(opts)});
|
|
4578
4564
|
return { success: true, html: result.html, estimatedSize: result.estimatedSize, analysis: { itemTypes: [...result.analysis.itemTypes], relationTypes: [...result.analysis.relationTypes], hasSimpleAnimations: result.analysis.hasSimpleAnimations, hasKeyframeAnimations: result.analysis.hasKeyframeAnimations, hasMasks: result.analysis.hasMasks } };
|
|
4579
4565
|
})();`.trim();
|
|
4580
4566
|
}
|
|
@@ -4882,6 +4868,665 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
|
|
|
4882
4868
|
return `(function() { return { error: 'Unknown measurement action: ${input.action}' }; })();`;
|
|
4883
4869
|
}
|
|
4884
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
|
+
}
|
|
4885
5530
|
}
|
|
4886
5531
|
// Export singleton instance
|
|
4887
5532
|
export const codeGenerator = new PinePaperCodeGenerator();
|