@pinepaper.studio/mcp-server 1.5.3 → 1.5.5

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.
Files changed (77) hide show
  1. package/README.md +6 -4
  2. package/dist/agent/types.d.ts +16 -16
  3. package/dist/browser/url-target.d.ts +26 -0
  4. package/dist/browser/url-target.d.ts.map +1 -0
  5. package/dist/browser/url-target.js +50 -0
  6. package/dist/browser/url-target.js.map +1 -0
  7. package/dist/cli.js +2 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +48 -4
  11. package/dist/index.js.map +1 -1
  12. package/dist/ontology/hints.d.ts +71 -0
  13. package/dist/ontology/hints.d.ts.map +1 -0
  14. package/dist/ontology/hints.js +189 -0
  15. package/dist/ontology/hints.js.map +1 -0
  16. package/dist/ontology/vocabulary.d.ts.map +1 -1
  17. package/dist/ontology/vocabulary.js +34 -0
  18. package/dist/ontology/vocabulary.js.map +1 -1
  19. package/dist/prompts/index.d.ts.map +1 -1
  20. package/dist/prompts/index.js +146 -0
  21. package/dist/prompts/index.js.map +1 -1
  22. package/dist/tools/compact-descriptions.d.ts +3 -0
  23. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  24. package/dist/tools/compact-descriptions.js +17 -9
  25. package/dist/tools/compact-descriptions.js.map +1 -1
  26. package/dist/tools/definitions.d.ts +1 -1
  27. package/dist/tools/definitions.d.ts.map +1 -1
  28. package/dist/tools/definitions.js +1251 -2327
  29. package/dist/tools/definitions.js.map +1 -1
  30. package/dist/tools/handlers/camera.d.ts +19 -0
  31. package/dist/tools/handlers/camera.d.ts.map +1 -0
  32. package/dist/tools/handlers/camera.js +100 -0
  33. package/dist/tools/handlers/camera.js.map +1 -0
  34. package/dist/tools/handlers/export.d.ts +12 -0
  35. package/dist/tools/handlers/export.d.ts.map +1 -0
  36. package/dist/tools/handlers/export.js +30 -0
  37. package/dist/tools/handlers/export.js.map +1 -0
  38. package/dist/tools/handlers/font.d.ts +12 -0
  39. package/dist/tools/handlers/font.d.ts.map +1 -0
  40. package/dist/tools/handlers/font.js +135 -0
  41. package/dist/tools/handlers/font.js.map +1 -0
  42. package/dist/tools/handlers/maps.d.ts +19 -0
  43. package/dist/tools/handlers/maps.d.ts.map +1 -0
  44. package/dist/tools/handlers/maps.js +184 -0
  45. package/dist/tools/handlers/maps.js.map +1 -0
  46. package/dist/tools/handlers/ontology.d.ts +20 -0
  47. package/dist/tools/handlers/ontology.d.ts.map +1 -0
  48. package/dist/tools/handlers/ontology.js +185 -0
  49. package/dist/tools/handlers/ontology.js.map +1 -0
  50. package/dist/tools/handlers/tool-guide.d.ts +14 -0
  51. package/dist/tools/handlers/tool-guide.d.ts.map +1 -0
  52. package/dist/tools/handlers/tool-guide.js +52 -0
  53. package/dist/tools/handlers/tool-guide.js.map +1 -0
  54. package/dist/tools/handlers.d.ts +18 -0
  55. package/dist/tools/handlers.d.ts.map +1 -1
  56. package/dist/tools/handlers.js +213 -695
  57. package/dist/tools/handlers.js.map +1 -1
  58. package/dist/tools/minimal-descriptions.d.ts +4 -0
  59. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  60. package/dist/tools/minimal-descriptions.js +45 -66
  61. package/dist/tools/minimal-descriptions.js.map +1 -1
  62. package/dist/tools/toolkits.d.ts.map +1 -1
  63. package/dist/tools/toolkits.js +36 -62
  64. package/dist/tools/toolkits.js.map +1 -1
  65. package/dist/types/code-generator.d.ts +35 -34
  66. package/dist/types/code-generator.d.ts.map +1 -1
  67. package/dist/types/code-generator.js +869 -474
  68. package/dist/types/code-generator.js.map +1 -1
  69. package/dist/types/schemas.d.ts +1047 -988
  70. package/dist/types/schemas.d.ts.map +1 -1
  71. package/dist/types/schemas.js +282 -84
  72. package/dist/types/schemas.js.map +1 -1
  73. package/dist/version.d.ts +11 -0
  74. package/dist/version.d.ts.map +1 -0
  75. package/dist/version.js +18 -0
  76. package/dist/version.js.map +1 -0
  77. package/package.json +1 -1
@@ -10,10 +10,6 @@ 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
@@ -82,7 +78,7 @@ function generateBlendModeCode(itemVarName, blendMode) {
82
78
  */
83
79
  function generateCreateItemCode(itemType, position, properties) {
84
80
  // Extract special properties that need separate handling
85
- const { color, fillColor, strokeColor, shadowColor, shadowBlur, shadowOffset, blendMode, opacity, ...baseProperties } = properties;
81
+ const { color, fillColor, strokeColor, shadowColor, shadowBlur, shadowOffset, blendMode, opacity, contentType, contentFormat, countdownTarget, countdownEndText, ...baseProperties } = properties;
86
82
  const params = {
87
83
  x: position.x,
88
84
  y: position.y,
@@ -127,6 +123,17 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
127
123
  }
128
124
  // Ensure item is visible above backgrounds/generators
129
125
  code += `\nif (item.bringToFront) item.bringToFront();`;
126
+ // Add dynamic content support for text items
127
+ if (contentType && itemType === 'text') {
128
+ const opts = {};
129
+ if (contentFormat)
130
+ opts.format = contentFormat;
131
+ if (countdownTarget !== undefined)
132
+ opts.countdownTarget = countdownTarget;
133
+ if (countdownEndText)
134
+ opts.countdownEndText = countdownEndText;
135
+ code += `\nif (app.setDynamicContent) app.setDynamicContent(item, '${contentType}', ${JSON.stringify(opts)});`;
136
+ }
130
137
  code += `
131
138
  const itemId = item.data.registryId;
132
139
  app.historyManager.saveState();
@@ -139,15 +146,42 @@ app.historyManager.saveState();
139
146
  * Template for modifying items
140
147
  */
141
148
  function generateModifyItemCode(itemId, properties) {
142
- return `
149
+ const { contentType, contentFormat, countdownTarget, countdownEndText, ...restProperties } = properties;
150
+ let code = `
143
151
  // Modify item ${itemId}
144
- app.select('${itemId}');
145
- app.modify(${JSON.stringify(properties, null, 2)});
152
+ app.select('${itemId}');`;
153
+ // Pass non-dynamic-content properties to app.modify
154
+ if (Object.keys(restProperties).length > 0) {
155
+ code += `\napp.modify(${JSON.stringify(restProperties, null, 2)});`;
156
+ }
157
+ // Handle dynamic content type changes
158
+ if (contentType !== undefined) {
159
+ if (contentType === null || contentType === 'none' || contentType === '') {
160
+ // Remove dynamic content
161
+ code += `
162
+ const entry = app.itemRegistry.get('${itemId}');
163
+ if (entry && entry.item && app.removeDynamicContent) app.removeDynamicContent(entry.item);`;
164
+ }
165
+ else {
166
+ // Set dynamic content
167
+ const opts = {};
168
+ if (contentFormat)
169
+ opts.format = contentFormat;
170
+ if (countdownTarget !== undefined)
171
+ opts.countdownTarget = countdownTarget;
172
+ if (countdownEndText)
173
+ opts.countdownEndText = countdownEndText;
174
+ code += `
175
+ const entry = app.itemRegistry.get('${itemId}');
176
+ if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.item, '${contentType}', ${JSON.stringify(opts)});`;
177
+ }
178
+ }
179
+ code += `
146
180
  app.historyManager.saveState();
147
181
 
148
182
  // Return success
149
- ({ success: true, itemId: '${itemId}' });
150
- `.trim();
183
+ ({ success: true, itemId: '${itemId}' });`;
184
+ return code.trim();
151
185
  }
152
186
  /**
153
187
  * Template for deleting items
@@ -254,17 +288,40 @@ app.animate(item, {
254
288
  ({ success: true, itemId: '${itemId}', animationType: '${animationType}' });
255
289
  `.trim();
256
290
  }
291
+ /**
292
+ * PinePaper's keyframe interpolator only tweens `position` when it is an [x, y]
293
+ * ARRAY. Callers (and other agents) routinely pass a { x, y } object, which is
294
+ * silently ignored — the item then stays frozen for the whole animation. Accept
295
+ * both shapes by normalizing { x, y } -> [x, y] before the keyframes are emitted.
296
+ */
297
+ function normalizeKeyframePositions(keyframes) {
298
+ return (keyframes || []).map((kf) => {
299
+ const props = kf?.properties;
300
+ const p = props?.position;
301
+ if (p && !Array.isArray(p) && typeof p === 'object' && 'x' in p && 'y' in p) {
302
+ return { ...kf, properties: { ...props, position: [p.x, p.y] } };
303
+ }
304
+ return kf;
305
+ });
306
+ }
257
307
  /**
258
308
  * Template for keyframe animation
259
309
  */
260
- function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false) {
261
- const keyframesJson = JSON.stringify(keyframes, null, 2);
310
+ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint) {
311
+ const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
262
312
  const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
313
+ const opts = { duration: calculatedDuration, loop };
314
+ if (timeOffset !== undefined)
315
+ opts.timeOffset = timeOffset;
316
+ if (clipInPoint !== undefined)
317
+ opts.clipInPoint = clipInPoint;
318
+ if (clipOutPoint !== undefined)
319
+ opts.clipOutPoint = clipOutPoint;
263
320
  return `
264
321
  // Apply keyframe animation to ${itemId}
265
- app.addAnimation('${itemId}', ${keyframesJson}, { duration: ${calculatedDuration}, loop: ${loop} });
322
+ app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
266
323
 
267
- ({ success: true, itemId: '${itemId}', duration: ${calculatedDuration}, loop: ${loop} });
324
+ ({ success: true, itemId: '${itemId}', duration: ${calculatedDuration}, loop: ${loop}${timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : ''}${clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : ''}${clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : ''} });
268
325
  `.trim();
269
326
  }
270
327
  /**
@@ -411,35 +468,29 @@ const size = app.getCanvasSize();
411
468
  */
412
469
  function generateClearCanvasCode() {
413
470
  return `
414
- // Clear all items from canvas
415
- const entries = app.itemRegistry.getAll();
416
- const itemIds = entries.map(entry => entry.id || entry.itemId);
417
- let removedCount = 0;
418
-
419
- for (const itemId of itemIds) {
420
- try {
421
- const item = app.getItemById(itemId);
422
- if (item) {
423
- item.remove();
424
- app.itemRegistry.remove(itemId);
425
- removedCount++;
426
- }
427
- } catch (e) {
428
- console.warn('Failed to remove item:', itemId, e);
429
- }
430
- }
431
-
432
- // Also clear any relations
433
- if (app.clearAllRelations) {
434
- app.clearAllRelations();
471
+ // Clear all items from canvas. Prefer app.clearCanvas() — the canonical
472
+ // reset that also tears down relations, the (virtual) camera animation,
473
+ // generators, effects and background. The old manual loop here was broken:
474
+ // it keyed item lookups on entry.id/entry.itemId (the registry getAll()
475
+ // entries don't expose those — getItemById wants the registry id), so it
476
+ // removed 0 items, and it called the non-existent app.clearAllRelations.
477
+ const before = app.itemRegistry ? app.itemRegistry.getAll().length : 0;
478
+
479
+ if (typeof app.clearCanvas === 'function') {
480
+ app.clearCanvas();
481
+ } else {
482
+ // Fallback for older builds without clearCanvas().
483
+ if (app.textItemGroup) app.textItemGroup.removeChildren();
484
+ if (app.patternGroup) app.patternGroup.removeChildren();
485
+ if (app.itemRegistry) app.itemRegistry.clear();
486
+ if (app.relationRegistry) app.relationRegistry.clear();
435
487
  }
436
488
 
437
- app.historyManager.saveState();
489
+ if (app.historyManager) app.historyManager.saveState();
438
490
 
439
- // Verify canvas is empty
440
- const remaining = app.itemRegistry.getAll().length;
491
+ const remaining = app.itemRegistry ? app.itemRegistry.getAll().length : 0;
441
492
 
442
- ({ success: true, removedCount, remainingItems: remaining });
493
+ ({ success: true, removedCount: Math.max(0, before - remaining), remainingItems: remaining });
443
494
  `.trim();
444
495
  }
445
496
  /**
@@ -582,7 +633,14 @@ export class PinePaperCodeGenerator {
582
633
  */
583
634
  generateCreateItem(input) {
584
635
  const validated = CreateItemInputSchema.parse(input);
585
- return generateCreateItemCode(validated.itemType, validated.position, validated.properties);
636
+ const properties = { ...validated.properties };
637
+ if (validated.animationType !== undefined)
638
+ properties.animationType = validated.animationType;
639
+ if (validated.animationSpeed !== undefined)
640
+ properties.animationSpeed = validated.animationSpeed;
641
+ if (validated.keyframes !== undefined)
642
+ properties.keyframes = validated.keyframes;
643
+ return generateCreateItemCode(validated.itemType, validated.position, properties);
586
644
  }
587
645
  /**
588
646
  * Generate code for modifying an item
@@ -628,7 +686,7 @@ export class PinePaperCodeGenerator {
628
686
  */
629
687
  generateKeyframeAnimate(input) {
630
688
  const validated = KeyframeAnimateInputSchema.parse(input);
631
- return generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop);
689
+ return generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint);
632
690
  }
633
691
  /**
634
692
  * Generate code for executing a generator
@@ -748,6 +806,70 @@ if (imported) {
748
806
  return `
749
807
  // Error: No SVG source provided
750
808
  throw new Error('Either svgString or url must be provided');
809
+ `.trim();
810
+ }
811
+ /**
812
+ * Generate code for importing a Mermaid diagram (flowchart, stateDiagram,
813
+ * sequenceDiagram, erDiagram, classDiagram).
814
+ */
815
+ generateImportMermaid(mermaidText, options = {}) {
816
+ const escaped = mermaidText.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
817
+ const optsJson = JSON.stringify(options);
818
+ return `
819
+ // Import Mermaid diagram
820
+ (function() {
821
+ if (!app.importMermaid) return { success: false, error: 'importMermaid not available — diagramSystem missing' };
822
+ const mermaidText = \`${escaped}\`;
823
+ const result = app.importMermaid(mermaidText, ${optsJson});
824
+ return {
825
+ success: !!result.success,
826
+ nodeCount: result.nodes ? result.nodes.length : 0,
827
+ edgeCount: result.edges ? result.edges.length : 0,
828
+ nodes: result.nodes || [],
829
+ edges: result.edges || [],
830
+ errors: result.errors || [],
831
+ };
832
+ })();
833
+ `.trim();
834
+ }
835
+ /**
836
+ * Generate code for capturing the canvas as pp: ontology. Wraps FxTool's
837
+ * app.exportCanvasOntology() — defined in PinePaper.js after the
838
+ * canvas-state-to-ontology promotion. Output mirrors the cloud-sync
839
+ * postMessage contract: { canvasWidth, canvasHeight, itemCount,
840
+ * itemTypes, hasAnimations, relationCount, items, triples, [viewport] }.
841
+ *
842
+ * Failure modes: app missing the method → return { success: false, error }
843
+ * with a clear FxTool-version hint. Internal exceptions propagate from
844
+ * the browser-side function (TripleLanguage warns to console; we don't
845
+ * fail the tool call on triple-only failures).
846
+ */
847
+ generateGetCanvasOntology(options = {}) {
848
+ const optsJson = JSON.stringify(options);
849
+ return `
850
+ // Capture canvas as pp: ontology
851
+ (function() {
852
+ if (!app.exportCanvasOntology) {
853
+ return {
854
+ success: false,
855
+ error: 'app.exportCanvasOntology not available — requires FxTool with canvas-ontology promotion. Falling back to pinepaper_get_items is acceptable but loses the triples representation.',
856
+ };
857
+ }
858
+ const ctx = app.exportCanvasOntology(${optsJson});
859
+ return {
860
+ success: true,
861
+ canvasWidth: ctx.canvasWidth,
862
+ canvasHeight: ctx.canvasHeight,
863
+ canvasPreset: ctx.canvasPreset,
864
+ itemCount: ctx.itemCount,
865
+ itemTypes: ctx.itemTypes,
866
+ hasAnimations: ctx.hasAnimations,
867
+ relationCount: ctx.relationCount,
868
+ items: ctx.items,
869
+ triples: ctx.triples,
870
+ viewport: ctx.viewport,
871
+ };
872
+ })();
751
873
  `.trim();
752
874
  }
753
875
  /**
@@ -1062,13 +1184,9 @@ if (typeof app.generators !== 'undefined' && app.generators['${backgroundGenerat
1062
1184
  const sourceId = nameToId['${source}'];
1063
1185
  const targetId = nameToId['${target}'];
1064
1186
  if (sourceId && targetId) {
1065
- const sourceItem = app.itemRegistry.get(sourceId);
1066
- const targetItem = app.itemRegistry.get(targetId);
1067
- if (sourceItem && targetItem) {
1068
- const params = ${paramsStr};
1069
- app.relationManager.addRelation(sourceItem, targetItem, '${type}', params);
1070
- results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
1071
- }
1187
+ const params = ${paramsStr};
1188
+ app.addRelation(sourceId, targetId, '${type}', params);
1189
+ results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
1072
1190
  }
1073
1191
  })();
1074
1192
  `);
@@ -1346,10 +1464,11 @@ throw new Error('Unknown diagram mode action: ${action}');
1346
1464
  */
1347
1465
  generateAgentStartJob(input) {
1348
1466
  const validated = AgentStartJobInputSchema.parse(input);
1349
- const { name, screenshotPolicy, canvasPreset, clearCanvas } = validated;
1467
+ const { name, screenshotPolicy, canvasPreset, clearCanvas, includeOntology } = validated;
1350
1468
  const nameStr = name ? `'${name.replace(/'/g, "\\'")}'` : 'null';
1351
1469
  const policyStr = screenshotPolicy || 'on_complete';
1352
1470
  const shouldClear = clearCanvas !== false;
1471
+ const wantOntology = includeOntology !== false;
1353
1472
  let code = `
1354
1473
  // Start agent job
1355
1474
  (async function() {
@@ -1378,10 +1497,31 @@ throw new Error('Unknown diagram mode action: ${action}');
1378
1497
  app.setCanvasSize('${canvasPreset}');
1379
1498
  // Allow canvas resize to take effect
1380
1499
  await new Promise(r => setTimeout(r, 50));
1500
+ `;
1501
+ }
1502
+ if (wantOntology) {
1503
+ code += `
1504
+ // Capture initial canvas ontology so the agent has baseline context
1505
+ // without a follow-up pinepaper_get_canvas_ontology call. After
1506
+ // clearCanvas this is a tiny empty-state payload; with clearCanvas:false
1507
+ // it captures whatever's already on the canvas. Older FxTool builds
1508
+ // without exportCanvasOntology produce an _ontologyUnavailable: true
1509
+ // marker so the agent knows to fall back to pinepaper_get_items.
1510
+ let _ontology = null;
1511
+ let _ontologyUnavailable = false;
1512
+ try {
1513
+ if (app.exportCanvasOntology) {
1514
+ _ontology = app.exportCanvasOntology();
1515
+ } else {
1516
+ _ontologyUnavailable = true;
1517
+ }
1518
+ } catch (e) {
1519
+ _ontologyUnavailable = true;
1520
+ }
1381
1521
  `;
1382
1522
  }
1383
1523
  code += `
1384
- // Return job context with canvas size
1524
+ // Return job context with canvas size${wantOntology ? ' + initial ontology' : ''}
1385
1525
  const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
1386
1526
  return {
1387
1527
  success: true,
@@ -1390,7 +1530,9 @@ throw new Error('Unknown diagram mode action: ${action}');
1390
1530
  screenshotPolicy: '${policyStr}',
1391
1531
  canvasPreset: ${canvasPreset ? `'${canvasPreset}'` : 'null'},
1392
1532
  canvasCleared: ${shouldClear},
1393
- canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }
1533
+ canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }${wantOntology ? `,
1534
+ ontology: _ontology,
1535
+ ontologyUnavailable: _ontologyUnavailable || undefined` : ''}
1394
1536
  };
1395
1537
  })();
1396
1538
  `;
@@ -1461,14 +1603,7 @@ throw new Error('Unknown diagram mode action: ${action}');
1461
1603
  if (item.shadowColor || item.shadowBlur) analysis.hasShadows = true;
1462
1604
  });
1463
1605
 
1464
- // Check animation manager for registered animations
1465
- if (app.animationManager) {
1466
- const animations = app.animationManager.getAll ? app.animationManager.getAll() : (app.animationManager.animations || []);
1467
- if (animations.length > 0) {
1468
- analysis.hasAnimations = true;
1469
- animationSet.add('managed');
1470
- }
1471
- }
1606
+ // FxTool has no animationManager — animations are detected via item.data.animationType walk above
1472
1607
  // Check timeline for active animations
1473
1608
  if (app.timeline && (app.timeline.isPlaying || app.timeline.animations?.length > 0)) {
1474
1609
  analysis.hasAnimations = true;
@@ -1639,16 +1774,29 @@ throw new Error('Unknown diagram mode action: ${action}');
1639
1774
  */
1640
1775
  generateBatchOperationCode(op, index) {
1641
1776
  switch (op.type) {
1642
- case 'create':
1777
+ case 'create': {
1643
1778
  const pos = op.position || { x: 400, y: 300 };
1644
- const props = JSON.stringify(op.properties || {});
1645
- return `
1779
+ const createProps = (op.properties || {});
1780
+ const props = JSON.stringify(createProps);
1781
+ let createCode = `
1646
1782
  const item = app.create('${op.itemType}', { position: { x: ${pos.x}, y: ${pos.y} }, ...${props} });
1647
- const itemId = item.data && item.data.id ? item.data.id : app.registerItem(item, '${op.itemType}', { source: 'mcp-batch' });
1783
+ const itemId = item.data && item.data.id ? item.data.id : app.registerItem(item, '${op.itemType}', { source: 'mcp-batch' });`;
1784
+ // app.create() does not read blendMode/opacity from its params, but the
1785
+ // underlying Paper.js item supports them directly — apply post-create so
1786
+ // callers can pass them inline in `properties` and have them take effect.
1787
+ if (createProps.blendMode !== undefined) {
1788
+ createCode += `\nif ('blendMode' in item) item.blendMode = ${JSON.stringify(createProps.blendMode)};`;
1789
+ }
1790
+ if (createProps.opacity !== undefined) {
1791
+ createCode += `\nif ('opacity' in item) item.opacity = ${JSON.stringify(createProps.opacity)};`;
1792
+ }
1793
+ createCode += `
1648
1794
  // Ensure item is visible above backgrounds/generators
1649
1795
  if (item.bringToFront) item.bringToFront();
1650
1796
  return { itemId };
1651
1797
  `;
1798
+ return createCode;
1799
+ }
1652
1800
  case 'modify':
1653
1801
  const itemRef = op.itemId?.startsWith('$')
1654
1802
  ? `itemIds[${op.itemId.substring(1)}]`
@@ -1729,7 +1877,7 @@ return { success: true, width: ${w}, height: ${h} };
1729
1877
  const kfItemRef = op.itemId?.startsWith('$')
1730
1878
  ? `itemIds[${op.itemId.substring(1)}]`
1731
1879
  : `'${op.itemId}'`;
1732
- const kfJson = JSON.stringify(op.keyframes || []);
1880
+ const kfJson = JSON.stringify(normalizeKeyframePositions(op.keyframes || []));
1733
1881
  const kfDuration = op.duration || (op.keyframes?.length ? Math.max(...op.keyframes.map(k => k.time)) : 5);
1734
1882
  const kfLoop = op.loop ?? false;
1735
1883
  return `
@@ -1743,16 +1891,14 @@ return { itemId: targetId, duration: ${kfDuration}, loop: ${kfLoop} };
1743
1891
  ? `itemIds[${op.itemId.substring(1)}]`
1744
1892
  : `'${op.itemId}'`;
1745
1893
  const maskOpts = JSON.stringify(op.maskOptions || {});
1894
+ const preset = op.maskPreset || op.maskType || 'wipeLeft';
1746
1895
  return `
1747
1896
  const targetId = ${maskItemRef};
1748
- if (!app.maskSystem) throw new Error('Mask system not available');
1749
- const result = app.maskSystem.applyAnimatedMask(targetId, {
1750
- preset: ${op.maskPreset ? `'${op.maskPreset}'` : 'null'},
1751
- maskType: ${op.maskType ? `'${op.maskType}'` : 'null'},
1752
- options: ${maskOpts},
1753
- maskOptions: ${maskOpts}
1754
- });
1755
- return result;
1897
+ const item = app.getItemById(targetId);
1898
+ if (!item) throw new Error('Item not found: ' + targetId);
1899
+ if (!app.applyAnimatedMask) throw new Error('Mask system not available');
1900
+ const maskedGroup = app.applyAnimatedMask(item, '${preset}', ${maskOpts});
1901
+ return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
1756
1902
  `;
1757
1903
  }
1758
1904
  case 'apply_effect': {
@@ -1800,8 +1946,9 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1800
1946
  */
1801
1947
  generateAgentExport(input) {
1802
1948
  const validated = AgentExportInputSchema.parse(input);
1803
- const { platform, format, quality } = validated;
1949
+ const { platform, format, quality, framing, duration } = validated;
1804
1950
  const qualityLevel = quality || 'standard';
1951
+ const videoDuration = duration ?? 5;
1805
1952
  // Quality settings (bitrate in bps for VideoEncoder)
1806
1953
  const qualitySettings = {
1807
1954
  draft: { compression: 0.6, fps: 15, dpi: 72, bitrate: 2_000_000 },
@@ -1830,10 +1977,38 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1830
1977
  const platform = '${platform}';
1831
1978
  const format = '${exportFormat}';
1832
1979
  const quality = '${qualityLevel}';
1980
+ const framing = '${framing}';
1833
1981
  const settings = ${JSON.stringify(qualitySettings)};
1834
1982
  const dimensions = ${JSON.stringify({ width: preset.width, height: preset.height })};
1835
1983
 
1836
- let result = { success: false, platform, format, quality };
1984
+ // Resolve camera-view framing dims when requested. Reads the first
1985
+ // camera_animates keyframe's zoom and divides the canvas dims by it
1986
+ // (mirrors FxTool ExportEngine.js:466-485). Camera animation still
1987
+ // drives motion during export — framing only fixes the output frame.
1988
+ let cameraDims = null;
1989
+ if (framing === 'camera') {
1990
+ if (!['gif', 'mp4', 'webm'].includes(format)) {
1991
+ return { success: false, platform, format, framing, error: 'framing: "camera" is only supported for video formats (gif, mp4, webm). Use format: "mp4" / "gif" / "webm" or omit framing.' };
1992
+ }
1993
+ const rr = app.relationRegistry;
1994
+ const hasWalkthrough = rr && typeof rr.hasCameraAnimation === 'function' && rr.hasCameraAnimation();
1995
+ if (!hasWalkthrough) {
1996
+ return { success: false, platform, format, framing, error: 'framing: "camera" requires a camera_animates walkthrough on the canvas. Add one or omit framing.' };
1997
+ }
1998
+ const params = rr.getCameraAnimationParams ? rr.getCameraAnimationParams() : null;
1999
+ if (!params || (params.mode || 'keyframes') !== 'keyframes' || !params.keyframes || !params.keyframes.length) {
2000
+ return { success: false, platform, format, framing, error: 'framing: "camera" requires a keyframe-mode camera walkthrough with at least one keyframe.' };
2001
+ }
2002
+ const firstKf = params.keyframes[0];
2003
+ const firstZoom = firstKf && firstKf.zoom > 0 ? firstKf.zoom : 1;
2004
+ const canvasEl = document.querySelector('canvas');
2005
+ cameraDims = {
2006
+ width: Math.round((canvasEl ? canvasEl.width : dimensions.width) / firstZoom),
2007
+ height: Math.round((canvasEl ? canvasEl.height : dimensions.height) / firstZoom),
2008
+ };
2009
+ }
2010
+
2011
+ let result = { success: false, platform, format, quality, framing };
1837
2012
 
1838
2013
  try {
1839
2014
  switch (format) {
@@ -1881,28 +2056,44 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1881
2056
  case 'mp4':
1882
2057
  case 'webm': {
1883
2058
  const videoMimeType = { mp4: 'video/mp4', webm: 'video/webm', gif: 'image/gif' }[format];
1884
- // Use _quickExportVideo (delegates to VideoExporter.export internally)
1885
- if (app.exportEngine && app.exportEngine._quickExportVideo) {
1886
- const videoResult = await app.exportEngine._quickExportVideo(format, { fps: settings.fps, bitrate: settings.bitrate, duration: 5 }, false);
2059
+ // FxTool VideoExporter._calculateBitrate multiplies a per-resolution
2060
+ // base by settings.quality (0.5-1.0). Our existing per-tier compression
2061
+ // values (0.6 / 0.85 / 0.95) map directly to that scale. Without this,
2062
+ // ExportEngine._quickExportVideo overrides quality with undefined for
2063
+ // mp4/webm, _calculateBitrate yields NaN, and VideoEncoder.configure
2064
+ // rejects with 'malformed bitrate value'.
2065
+ const baseVideoSettings = { format, fps: settings.fps, quality: settings.compression, duration: ${videoDuration} };
2066
+ const blobToDataUrl = (b) => new Promise(resolve => {
2067
+ const r = new FileReader();
2068
+ r.onloadend = () => resolve(r.result);
2069
+ r.readAsDataURL(b);
2070
+ });
2071
+ // Camera framing requires going direct to videoExporter so width/height
2072
+ // pass through — _quickExportVideo strips dim fields.
2073
+ if (cameraDims && app.exportEngine && app.exportEngine.videoExporter) {
2074
+ const blob = await app.exportEngine.videoExporter.export({ ...baseVideoSettings, width: cameraDims.width, height: cameraDims.height });
2075
+ const dataUrl = await blobToDataUrl(blob);
2076
+ result = { success: true, platform, format, framing, data: dataUrl, mimeType: videoMimeType, size: blob.size, dimensions: cameraDims };
2077
+ } else if ((format === 'mp4' || format === 'webm') && app.exportEngine && app.exportEngine.videoExporter) {
2078
+ // Bypass _quickExportVideo for mp4/webm — it hardcodes
2079
+ // quality:undefined for these formats and produces a NaN bitrate.
2080
+ const blob = await app.exportEngine.videoExporter.export(baseVideoSettings);
2081
+ const dataUrl = await blobToDataUrl(blob);
2082
+ result = { success: true, platform, format, framing, data: dataUrl, mimeType: videoMimeType, size: blob.size };
2083
+ } else if (app.exportEngine && app.exportEngine._quickExportVideo) {
2084
+ // GIF path (working): _quickExportVideo forwards gifQuality to
2085
+ // gif.js. Also serves as the fallback if videoExporter is absent.
2086
+ const videoResult = await app.exportEngine._quickExportVideo(format, baseVideoSettings, false);
1887
2087
  if (videoResult && videoResult.blob) {
1888
- const reader = new FileReader();
1889
- const dataUrl = await new Promise(resolve => {
1890
- reader.onloadend = () => resolve(reader.result);
1891
- reader.readAsDataURL(videoResult.blob);
1892
- });
1893
- result = { success: true, platform, format, data: dataUrl, mimeType: videoMimeType, size: videoResult.blob.size };
2088
+ const dataUrl = await blobToDataUrl(videoResult.blob);
2089
+ result = { success: true, platform, format, framing, data: dataUrl, mimeType: videoMimeType, size: videoResult.blob.size };
1894
2090
  } else {
1895
2091
  result = { success: false, error: format.toUpperCase() + ' export returned no data' };
1896
2092
  }
1897
2093
  } else if (app.exportEngine && app.exportEngine.videoExporter) {
1898
- // Fallback: call VideoExporter.export directly
1899
- const blob = await app.exportEngine.videoExporter.export({ format, fps: settings.fps, bitrate: settings.bitrate, duration: 5 });
1900
- const reader = new FileReader();
1901
- const dataUrl = await new Promise(resolve => {
1902
- reader.onloadend = () => resolve(reader.result);
1903
- reader.readAsDataURL(blob);
1904
- });
1905
- result = { success: true, platform, format, data: dataUrl, mimeType: videoMimeType, size: blob.size };
2094
+ const blob = await app.exportEngine.videoExporter.export(baseVideoSettings);
2095
+ const dataUrl = await blobToDataUrl(blob);
2096
+ result = { success: true, platform, format, framing, data: dataUrl, mimeType: videoMimeType, size: blob.size };
1906
2097
  } else {
1907
2098
  result = { success: false, error: format.toUpperCase() + ' export not available' };
1908
2099
  }
@@ -1994,14 +2185,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1994
2185
  if (item.shadowColor || item.shadowBlur) hasShadow = true;
1995
2186
  });
1996
2187
 
1997
- // Check animation manager for registered animations
1998
- if (app.animationManager) {
1999
- const animations = app.animationManager.getAll ? app.animationManager.getAll() : (app.animationManager.animations || []);
2000
- if (animations.length > 0) {
2001
- analysis.hasAnimations = true;
2002
- animationSet.add('managed');
2003
- }
2004
- }
2188
+ // FxTool has no animationManager — animations are detected via item.data.animationType walk above
2005
2189
  // Check timeline for active animations
2006
2190
  if (app.timeline && (app.timeline.isPlaying || app.timeline.animations?.length > 0)) {
2007
2191
  analysis.hasAnimations = true;
@@ -2065,257 +2249,6 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
2065
2249
  suggestedPlatforms
2066
2250
  };
2067
2251
  })();
2068
- `.trim();
2069
- }
2070
- // ===========================================================================
2071
- // INTERACTIVE TRIGGER CODE GENERATORS
2072
- // ===========================================================================
2073
- /**
2074
- * Generate code for adding a trigger
2075
- */
2076
- generateAddTrigger(input) {
2077
- const validated = AddTriggerInputSchema.parse(input);
2078
- const { itemId, event, actions, condition, timelineOffset } = validated;
2079
- const actionsStr = JSON.stringify(actions);
2080
- const conditionStr = condition ? `'${condition.replace(/'/g, "\\'")}'` : 'null';
2081
- const offsetStr = timelineOffset !== undefined ? timelineOffset : 'null';
2082
- return `
2083
- // Add trigger to ${itemId}
2084
- const item = app.getItemById('${itemId}');
2085
- if (!item) throw new Error('Item not found: ${itemId}');
2086
-
2087
- const trigger = {
2088
- event: '${event}',
2089
- actions: ${actionsStr},
2090
- condition: ${conditionStr},
2091
- timelineOffset: ${offsetStr}
2092
- };
2093
-
2094
- // Initialize triggers array if needed
2095
- if (!item.data) item.data = {};
2096
- if (!item.data.triggers) item.data.triggers = [];
2097
-
2098
- // Add trigger
2099
- item.data.triggers.push(trigger);
2100
-
2101
- // Register event handler
2102
- if (app.triggerManager) {
2103
- app.triggerManager.registerTrigger(item, trigger);
2104
- }
2105
-
2106
- app.historyManager.saveState();
2107
- ({ success: true, itemId: '${itemId}', event: '${event}', triggerId: item.data.triggers.length - 1 });
2108
- `.trim();
2109
- }
2110
- /**
2111
- * Generate code for removing a trigger
2112
- */
2113
- generateRemoveTrigger(input) {
2114
- const validated = RemoveTriggerInputSchema.parse(input);
2115
- const { itemId, event, removeAll } = validated;
2116
- if (removeAll) {
2117
- return `
2118
- // Remove all triggers from ${itemId}
2119
- const item = app.getItemById('${itemId}');
2120
- if (!item) throw new Error('Item not found: ${itemId}');
2121
-
2122
- const removedCount = item.data?.triggers?.length || 0;
2123
- if (item.data) item.data.triggers = [];
2124
-
2125
- if (app.triggerManager) {
2126
- app.triggerManager.unregisterAll(item);
2127
- }
2128
-
2129
- app.historyManager.saveState();
2130
- ({ success: true, itemId: '${itemId}', removedCount, removeAll: true });
2131
- `.trim();
2132
- }
2133
- return `
2134
- // Remove ${event} trigger from ${itemId}
2135
- const item = app.getItemById('${itemId}');
2136
- if (!item) throw new Error('Item not found: ${itemId}');
2137
-
2138
- let removedCount = 0;
2139
- if (item.data?.triggers) {
2140
- const before = item.data.triggers.length;
2141
- item.data.triggers = item.data.triggers.filter(t => t.event !== '${event}');
2142
- removedCount = before - item.data.triggers.length;
2143
- }
2144
-
2145
- if (app.triggerManager) {
2146
- app.triggerManager.unregisterEvent(item, '${event}');
2147
- }
2148
-
2149
- app.historyManager.saveState();
2150
- ({ success: true, itemId: '${itemId}', event: '${event}', removedCount });
2151
- `.trim();
2152
- }
2153
- /**
2154
- * Generate code for querying triggers
2155
- */
2156
- generateQueryTriggers(input) {
2157
- const validated = QueryTriggersInputSchema.parse(input);
2158
- const { itemId, event } = validated;
2159
- if (itemId) {
2160
- return `
2161
- // Query triggers for ${itemId}
2162
- const item = app.getItemById('${itemId}');
2163
- if (!item) throw new Error('Item not found: ${itemId}');
2164
-
2165
- let triggers = item.data?.triggers || [];
2166
- ${event ? `triggers = triggers.filter(t => t.event === '${event}');` : ''}
2167
-
2168
- ({ success: true, itemId: '${itemId}', triggers, count: triggers.length });
2169
- `.trim();
2170
- }
2171
- return `
2172
- // Query all triggers
2173
- const allTriggers = [];
2174
- const items = app.itemRegistry ? app.itemRegistry.getAll() : [];
2175
-
2176
- items.forEach(entry => {
2177
- const item = entry.item;
2178
- if (item.data?.triggers?.length > 0) {
2179
- let triggers = item.data.triggers;
2180
- ${event ? `triggers = triggers.filter(t => t.event === '${event}');` : ''}
2181
- triggers.forEach((trigger, idx) => {
2182
- allTriggers.push({
2183
- itemId: entry.id,
2184
- triggerId: idx,
2185
- ...trigger
2186
- });
2187
- });
2188
- }
2189
- });
2190
-
2191
- ({ success: true, triggers: allTriggers, count: allTriggers.length${event ? `, event: '${event}'` : ''} });
2192
- `.trim();
2193
- }
2194
- // ===========================================================================
2195
- // QUIZ CODE GENERATORS
2196
- // ===========================================================================
2197
- /**
2198
- * Generate code for creating a quiz
2199
- */
2200
- generateCreateQuiz(input) {
2201
- const validated = CreateQuizInputSchema.parse(input);
2202
- const { title, questions, passingScore, showScore, allowRetry, shuffleQuestions, shuffleOptions } = validated;
2203
- const quizConfig = JSON.stringify({
2204
- title: title || 'Untitled Quiz',
2205
- questions,
2206
- passingScore: passingScore || 70,
2207
- showScore: showScore !== false,
2208
- allowRetry: allowRetry !== false,
2209
- shuffleQuestions: shuffleQuestions || false,
2210
- shuffleOptions: shuffleOptions || false,
2211
- }, null, 2);
2212
- return `
2213
- // Create quiz
2214
- const quizConfig = ${quizConfig};
2215
-
2216
- // Initialize quiz manager if needed
2217
- if (!app.quizManager) {
2218
- app.quizManager = {
2219
- quizzes: [],
2220
- activeQuiz: null,
2221
- createQuiz: function(config) {
2222
- const quiz = {
2223
- id: 'quiz_' + Date.now(),
2224
- ...config,
2225
- state: {
2226
- currentQuestion: 0,
2227
- answers: [],
2228
- score: 0,
2229
- maxScore: config.questions.reduce((sum, q) => sum + q.points, 0),
2230
- complete: false
2231
- }
2232
- };
2233
- this.quizzes.push(quiz);
2234
- this.activeQuiz = quiz;
2235
- return quiz;
2236
- }
2237
- };
2238
- }
2239
-
2240
- const quiz = app.quizManager.createQuiz(quizConfig);
2241
- app.historyManager.saveState();
2242
-
2243
- ({
2244
- success: true,
2245
- quizId: quiz.id,
2246
- title: quiz.title,
2247
- questionCount: quiz.questions.length,
2248
- maxScore: quiz.state.maxScore,
2249
- passingScore: quiz.passingScore
2250
- });
2251
- `.trim();
2252
- }
2253
- /**
2254
- * Generate code for getting quiz state
2255
- */
2256
- generateGetQuizState(input) {
2257
- const validated = GetQuizStateInputSchema.parse(input);
2258
- const { quizId } = validated;
2259
- const quizRef = quizId
2260
- ? `app.quizManager.quizzes.find(q => q.id === '${quizId}')`
2261
- : 'app.quizManager?.activeQuiz';
2262
- return `
2263
- // Get quiz state
2264
- if (!app.quizManager) {
2265
- return { success: false, error: 'No quiz manager initialized' };
2266
- }
2267
-
2268
- const quiz = ${quizRef};
2269
- if (!quiz) {
2270
- return { success: false, error: 'Quiz not found' };
2271
- }
2272
-
2273
- ({
2274
- success: true,
2275
- quizId: quiz.id,
2276
- title: quiz.title,
2277
- currentQuestion: quiz.state.currentQuestion,
2278
- totalQuestions: quiz.questions.length,
2279
- score: quiz.state.score,
2280
- maxScore: quiz.state.maxScore,
2281
- answers: quiz.state.answers,
2282
- complete: quiz.state.complete,
2283
- passed: quiz.state.complete ? (quiz.state.score / quiz.state.maxScore * 100) >= quiz.passingScore : null
2284
- });
2285
- `.trim();
2286
- }
2287
- /**
2288
- * Generate code for resetting a quiz
2289
- */
2290
- generateResetQuiz(input) {
2291
- const validated = ResetQuizInputSchema.parse(input);
2292
- const { quizId } = validated;
2293
- const quizRef = quizId
2294
- ? `app.quizManager.quizzes.find(q => q.id === '${quizId}')`
2295
- : 'app.quizManager?.activeQuiz';
2296
- return `
2297
- // Reset quiz
2298
- if (!app.quizManager) {
2299
- return { success: false, error: 'No quiz manager initialized' };
2300
- }
2301
-
2302
- const quiz = ${quizRef};
2303
- if (!quiz) {
2304
- return { success: false, error: 'Quiz not found' };
2305
- }
2306
-
2307
- // Reset state
2308
- quiz.state = {
2309
- currentQuestion: 0,
2310
- answers: [],
2311
- score: 0,
2312
- maxScore: quiz.questions.reduce((sum, q) => sum + q.points, 0),
2313
- complete: false
2314
- };
2315
-
2316
- app.historyManager.saveState();
2317
-
2318
- ({ success: true, quizId: quiz.id, reset: true });
2319
2252
  `.trim();
2320
2253
  }
2321
2254
  // ===========================================================================
@@ -2345,7 +2278,7 @@ app.historyManager.saveState();
2345
2278
  if (gradientDirection)
2346
2279
  options.gradientDirection = gradientDirection;
2347
2280
  return `
2348
- // Create letter collage: "${text}"
2281
+ // Create letter collage
2349
2282
  (function() {
2350
2283
  const text = ${JSON.stringify(text)};
2351
2284
  const options = ${JSON.stringify(options, null, 2)};
@@ -2360,7 +2293,21 @@ app.historyManager.saveState();
2360
2293
  app.historyManager.saveState('create_letter_collage');
2361
2294
  }
2362
2295
 
2363
- return result;
2296
+ // Return a SERIALIZABLE summary — never the raw result. result.group and
2297
+ // result.letters are live Paper.js objects (circular parent/children refs,
2298
+ // methods, canvas handles); the browser round-trip (page.evaluate / CDP
2299
+ // return-by-value) can't serialize them, so returning result throws
2300
+ // "Failed to execute code in browser" even though the collage was already
2301
+ // created — a false negative. Surface registry ids instead.
2302
+ const collageId = (result && (result.collageId
2303
+ || (result.group && result.group.data && (result.group.data.id || result.group.data.registryId)))) || null;
2304
+ const letterIds = (result && Array.isArray(result.letters))
2305
+ ? result.letters.map(function(l){ return (l && l.data && (l.data.id || l.data.registryId)) || null; }).filter(Boolean)
2306
+ : [];
2307
+ if (!collageId) {
2308
+ return { success: false, error: 'createLetterCollage returned no id', letterCount: letterIds.length, letterIds: letterIds };
2309
+ }
2310
+ return { success: true, collageId: collageId, itemId: collageId, letterCount: letterIds.length, letterIds: letterIds };
2364
2311
  })();
2365
2312
  `.trim();
2366
2313
  }
@@ -2390,80 +2337,6 @@ app.historyManager.saveState();
2390
2337
 
2391
2338
  return result;
2392
2339
  })();
2393
- `.trim();
2394
- }
2395
- /**
2396
- * Generate code to get letter collage options
2397
- */
2398
- generateGetLetterCollageOptions() {
2399
- return `
2400
- // Get letter collage options
2401
- (function() {
2402
- if (!app.getLetterCollageOptions) {
2403
- // Return static options if function not available
2404
- return {
2405
- success: true,
2406
- styles: ['tile', 'magazine', 'paperCut', 'fold', 'gradient', 'image'],
2407
- tilePalettes: {
2408
- game: ['wordle', 'scrabble'],
2409
- vibrant: ['candy', 'neon', 'rainbow'],
2410
- soft: ['pastel', 'cotton'],
2411
- natural: ['earth', 'ocean', 'forest', 'sunset'],
2412
- professional: ['corporate', 'minimal', 'slate'],
2413
- seasonal: ['christmas', 'halloween', 'spring'],
2414
- magazine: ['magazine', 'newspaper', 'vintage'],
2415
- paperCraft: ['paperCraft', 'origami', 'craftPaper']
2416
- },
2417
- gradientPalettes: ['rainbow', 'sunset', 'ocean', 'fire', 'gold', 'rose', 'ice', 'cyberpunk', 'neonGlow', 'purplePink'],
2418
- gradientDirections: ['vertical', 'horizontal', 'diagonal', 'radial']
2419
- };
2420
- }
2421
-
2422
- return app.getLetterCollageOptions();
2423
- })();
2424
- `.trim();
2425
- }
2426
- // ===========================================================================
2427
- // CANVAS PRESETS CODE GENERATORS
2428
- // ===========================================================================
2429
- /**
2430
- * Generate code to get canvas presets
2431
- */
2432
- generateGetCanvasPresets() {
2433
- return `
2434
- // Get canvas presets
2435
- (function() {
2436
- if (app.getCanvasPresets) {
2437
- return app.getCanvasPresets();
2438
- }
2439
-
2440
- // Return static presets if function not available
2441
- return {
2442
- success: true,
2443
- presets: [
2444
- { key: 'youtube-thumbnail', name: 'YouTube Thumbnail', width: 1280, height: 720, aspectRatio: '16:9', category: 'video' },
2445
- { key: 'youtube-short', name: 'YouTube Shorts', width: 1080, height: 1920, aspectRatio: '9:16', category: 'video' },
2446
- { key: 'tiktok', name: 'TikTok', width: 1080, height: 1920, aspectRatio: '9:16', category: 'video' },
2447
- { key: 'instagram-story', name: 'Instagram Story', width: 1080, height: 1920, aspectRatio: '9:16', category: 'social' },
2448
- { key: 'instagram-post', name: 'Instagram Post (Square)', width: 1080, height: 1080, aspectRatio: '1:1', category: 'social' },
2449
- { key: 'instagram-landscape', name: 'Instagram Landscape', width: 1080, height: 566, aspectRatio: '1.91:1', category: 'social' },
2450
- { key: 'instagram-portrait', name: 'Instagram Portrait', width: 1080, height: 1350, aspectRatio: '4:5', category: 'social' },
2451
- { key: 'facebook-post', name: 'Facebook Post', width: 1200, height: 630, aspectRatio: '1.91:1', category: 'social' },
2452
- { key: 'facebook-cover', name: 'Facebook Cover', width: 820, height: 312, aspectRatio: '2.63:1', category: 'social' },
2453
- { key: 'facebook-story', name: 'Facebook Story', width: 1080, height: 1920, aspectRatio: '9:16', category: 'social' },
2454
- { key: 'twitter-post', name: 'Twitter/X Post', width: 1200, height: 675, aspectRatio: '16:9', category: 'social' },
2455
- { key: 'twitter-header', name: 'Twitter/X Header', width: 1500, height: 500, aspectRatio: '3:1', category: 'social' },
2456
- { key: 'linkedin-post', name: 'LinkedIn Post', width: 1200, height: 627, aspectRatio: '1.91:1', category: 'social' },
2457
- { key: 'linkedin-banner', name: 'LinkedIn Banner', width: 1584, height: 396, aspectRatio: '4:1', category: 'social' },
2458
- { key: 'pinterest-pin', name: 'Pinterest Pin', width: 1000, height: 1500, aspectRatio: '2:3', category: 'social' },
2459
- { key: 'presentation-16x9', name: 'Presentation 16:9', width: 1920, height: 1080, aspectRatio: '16:9', category: 'presentation' },
2460
- { key: 'presentation-4x3', name: 'Presentation 4:3', width: 1024, height: 768, aspectRatio: '4:3', category: 'presentation' },
2461
- { key: 'hd-720p', name: 'HD 720p', width: 1280, height: 720, aspectRatio: '16:9', category: 'video' },
2462
- { key: 'full-hd-1080p', name: 'Full HD 1080p', width: 1920, height: 1080, aspectRatio: '16:9', category: 'video' },
2463
- { key: 'default', name: 'Default', width: 800, height: 600, aspectRatio: '4:3', category: 'general' }
2464
- ]
2465
- };
2466
- })();
2467
2340
  `.trim();
2468
2341
  }
2469
2342
  // ===========================================================================
@@ -3248,22 +3121,27 @@ app.historyManager.saveState();
3248
3121
  const optionsStr = validated.options ? JSON.stringify(validated.options) : '{}';
3249
3122
  const keyframesStr = validated.keyframes ? JSON.stringify(validated.keyframes) : 'null';
3250
3123
  const maskOptionsStr = validated.maskOptions ? JSON.stringify(validated.maskOptions) : '{}';
3124
+ const preset = validated.preset || validated.maskType || 'wipeLeft';
3125
+ // Per-preset options. Custom keyframes are an animated-mask extension —
3126
+ // when supplied, fall through to maskingSystem.applyCustomMask instead.
3251
3127
  return `
3252
3128
  // Apply animated mask to ${validated.itemId}
3253
3129
  (function() {
3254
- if (!app.maskSystem) {
3130
+ if (!app.applyAnimatedMask) {
3255
3131
  return { success: false, error: 'Mask system not available' };
3256
3132
  }
3133
+ const item = app.getItemById('${validated.itemId}');
3134
+ if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
3257
3135
 
3258
3136
  try {
3259
- const result = app.maskSystem.applyAnimatedMask('${validated.itemId}', {
3260
- preset: ${validated.preset ? `'${validated.preset}'` : 'null'},
3261
- maskType: ${validated.maskType ? `'${validated.maskType}'` : 'null'},
3262
- keyframes: ${keyframesStr},
3263
- options: ${optionsStr},
3264
- maskOptions: ${maskOptionsStr}
3265
- });
3266
- return result;
3137
+ const mergedOpts = Object.assign({}, ${optionsStr}, ${maskOptionsStr});
3138
+ let maskedGroup;
3139
+ if (${keyframesStr} && app.maskingSystem && app.maskingSystem.applyCustomMask) {
3140
+ maskedGroup = app.maskingSystem.applyCustomMask(item, '${preset}', ${keyframesStr}, mergedOpts);
3141
+ } else {
3142
+ maskedGroup = app.applyAnimatedMask(item, '${preset}', mergedOpts);
3143
+ }
3144
+ return { success: !!maskedGroup, itemId: '${validated.itemId}', preset: '${preset}' };
3267
3145
  } catch (error) {
3268
3146
  return { success: false, error: error.message };
3269
3147
  }
@@ -3277,17 +3155,15 @@ app.historyManager.saveState();
3277
3155
  return `
3278
3156
  // Apply custom mask to ${validated.itemId}
3279
3157
  (function() {
3280
- if (!app.maskSystem) {
3158
+ if (!app.maskingSystem || !app.maskingSystem.applyCustomMask) {
3281
3159
  return { success: false, error: 'Mask system not available' };
3282
3160
  }
3161
+ const item = app.getItemById('${validated.itemId}');
3162
+ if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
3283
3163
 
3284
3164
  try {
3285
- const result = app.maskSystem.applyCustomMask('${validated.itemId}', {
3286
- maskType: '${validated.maskType}',
3287
- keyframes: ${keyframesStr},
3288
- maskOptions: ${maskOptionsStr}
3289
- });
3290
- return result;
3165
+ const maskedGroup = app.maskingSystem.applyCustomMask(item, '${validated.maskType}', ${keyframesStr}, ${maskOptionsStr});
3166
+ return { success: !!maskedGroup, itemId: '${validated.itemId}', maskType: '${validated.maskType}' };
3291
3167
  } catch (error) {
3292
3168
  return { success: false, error: error.message };
3293
3169
  }
@@ -3299,12 +3175,17 @@ app.historyManager.saveState();
3299
3175
  return `
3300
3176
  // Remove mask from ${validated.itemId}
3301
3177
  (function() {
3302
- if (!app.maskSystem) {
3178
+ if (!app.removeMask) {
3303
3179
  return { success: false, error: 'Mask system not available' };
3304
3180
  }
3181
+ // The item id may point at either the masked group OR the original item;
3182
+ // app.removeMask expects the masked group, so resolve and walk up if needed.
3183
+ const item = app.getItemById('${validated.itemId}');
3184
+ if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
3185
+ const maskedGroup = (item.data && item.data.isMaskedGroup) ? item : (item.parent && item.parent.data && item.parent.data.isMaskedGroup ? item.parent : item);
3305
3186
 
3306
3187
  try {
3307
- const result = app.maskSystem.removeMask('${validated.itemId}');
3188
+ app.removeMask(maskedGroup);
3308
3189
  return { success: true, itemId: '${validated.itemId}', message: 'Mask removed' };
3309
3190
  } catch (error) {
3310
3191
  return { success: false, error: error.message };
@@ -3398,10 +3279,12 @@ app.historyManager.saveState();
3398
3279
  })();
3399
3280
  `.trim();
3400
3281
  }
3282
+ // =============================================================================
3283
+ // TEMPLATE
3284
+ // =============================================================================
3401
3285
  generateApplyTemplate(input) {
3402
3286
  const { templateId, category, listOnly } = input;
3403
3287
  if (listOnly || !templateId) {
3404
- // List mode: return available templates, optionally filtered by category
3405
3288
  const categoryFilter = category ? `'${category}'` : 'null';
3406
3289
  return `
3407
3290
  // List available templates
@@ -3427,7 +3310,6 @@ app.historyManager.saveState();
3427
3310
  })();
3428
3311
  `.trim();
3429
3312
  }
3430
- // Load mode: apply a specific template
3431
3313
  return `
3432
3314
  // Apply template: ${templateId}
3433
3315
  (async function() {
@@ -3703,7 +3585,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
3703
3585
  // Select all items
3704
3586
  (function() {
3705
3587
  app.selectAll();
3706
- const items = app.getSelection().map(i => i.data?.itemId || i.name || i.id);
3588
+ const items = app.getSelectedItems().map(i => i.data?.itemId || i.name || i.id);
3707
3589
  return { success: true, action: 'select_all', count: items.length, items };
3708
3590
  })();`.trim();
3709
3591
  case 'deselect_all':
@@ -3717,7 +3599,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
3717
3599
  return `
3718
3600
  // Get current selection
3719
3601
  (function() {
3720
- const items = app.getSelection().map(i => ({
3602
+ const items = app.getSelectedItems().map(i => ({
3721
3603
  itemId: i.data?.itemId || i.name || i.id,
3722
3604
  type: i.data?.itemType || i.className,
3723
3605
  bounds: i.bounds ? { x: i.bounds.x, y: i.bounds.y, width: i.bounds.width, height: i.bounds.height } : null
@@ -3728,7 +3610,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
3728
3610
  return `
3729
3611
  // Delete selected items
3730
3612
  (function() {
3731
- const selection = app.getSelection();
3613
+ const selection = app.getSelectedItems();
3732
3614
  const deleted = selection.map(i => i.data?.itemId || i.name || i.id);
3733
3615
  app.deleteSelected();
3734
3616
  return { success: true, action: 'delete_selected', deleted, count: deleted.length };
@@ -3962,29 +3844,6 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
3962
3844
  return `(function() { return { error: 'Unknown precomp action: ${input.action}' }; })();`;
3963
3845
  }
3964
3846
  }
3965
- generateView(input) {
3966
- switch (input.action) {
3967
- case 'fit': {
3968
- const mode = input.mode || 'content';
3969
- const padding = input.padding ?? 20;
3970
- return `
3971
- // Fit view to ${mode}
3972
- (function() {
3973
- app.fitView('${mode}', ${padding});
3974
- return { success: true, action: 'fit', mode: '${mode}', padding: ${padding} };
3975
- })();`.trim();
3976
- }
3977
- case 'get_state':
3978
- return `
3979
- // Get current view state
3980
- (function() {
3981
- const state = app.getViewState();
3982
- return { success: true, action: 'get_state', ...state };
3983
- })();`.trim();
3984
- default:
3985
- return `(function() { return { error: 'Unknown view action: ${input.action}' }; })();`;
3986
- }
3987
- }
3988
3847
  generateBackground(input) {
3989
3848
  switch (input.action) {
3990
3849
  case 'set': {
@@ -4028,8 +3887,9 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
4028
3887
  return `
4029
3888
  // Get background info
4030
3889
  (function() {
4031
- const bg = app.getBackground();
4032
- return { success: true, action: 'get', ...bg };
3890
+ // FxTool exposes getBackgroundMode() returning the mode string only.
3891
+ const mode = app.getBackgroundMode ? app.getBackgroundMode() : null;
3892
+ return { success: true, action: 'get', mode };
4033
3893
  })();`.trim();
4034
3894
  default:
4035
3895
  return `(function() { return { error: 'Unknown background action: ${input.action}' }; })();`;
@@ -4094,6 +3954,541 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
4094
3954
  return `(function() { return { error: 'Unknown query action: ${input.action}' }; })();`;
4095
3955
  }
4096
3956
  }
3957
+ // ===========================================================================
3958
+ // DEFORMATION
3959
+ // ===========================================================================
3960
+ generateDeform(input) {
3961
+ const guard = `if (!app.deformPresets) return { error: 'DeformPresets not available' };`;
3962
+ switch (input.action) {
3963
+ case 'apply': {
3964
+ if (!input.itemId)
3965
+ return `(function() { return { error: 'itemId is required for deform apply' }; })();`;
3966
+ if (!input.preset)
3967
+ return `(function() { return { error: 'preset is required for deform apply' }; })();`;
3968
+ const params = {};
3969
+ if (input.frequency !== undefined)
3970
+ params.frequency = input.frequency;
3971
+ if (input.amplitude !== undefined)
3972
+ params.amplitude = input.amplitude;
3973
+ if (input.phase)
3974
+ params.phase = input.phase;
3975
+ if (input.loop !== undefined)
3976
+ params.loop = input.loop;
3977
+ if (input.axis)
3978
+ params.axis = input.axis;
3979
+ if (input.turns !== undefined)
3980
+ params.turns = input.turns;
3981
+ if (input.waves !== undefined)
3982
+ params.waves = input.waves;
3983
+ if (input.maxDisplacement !== undefined)
3984
+ params.maxDisplacement = input.maxDisplacement;
3985
+ if (input.speed !== undefined)
3986
+ params.speed = input.speed;
3987
+ if (input.steps !== undefined)
3988
+ params.steps = input.steps;
3989
+ const itemIdStr = JSON.stringify(input.itemId);
3990
+ return `
3991
+ // Apply deformation preset
3992
+ (function() {
3993
+ ${guard}
3994
+ const entry = app.itemRegistry.get(${itemIdStr});
3995
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
3996
+ app.deformPresets.apply(entry.item, '${input.preset}', ${JSON.stringify(params)});
3997
+ return { success: true, action: 'apply', itemId: ${itemIdStr}, preset: '${input.preset}' };
3998
+ })();`.trim();
3999
+ }
4000
+ case 'trigger': {
4001
+ if (!input.itemId)
4002
+ return `(function() { return { error: 'itemId is required for deform trigger' }; })();`;
4003
+ if (!input.preset)
4004
+ return `(function() { return { error: 'preset is required for deform trigger' }; })();`;
4005
+ const params = {};
4006
+ if (input.amplitude !== undefined)
4007
+ params.amplitude = input.amplitude;
4008
+ if (input.speed !== undefined)
4009
+ params.speed = input.speed;
4010
+ const itemIdStr = JSON.stringify(input.itemId);
4011
+ return `
4012
+ // Trigger one-shot deformation
4013
+ (function() {
4014
+ ${guard}
4015
+ const entry = app.itemRegistry.get(${itemIdStr});
4016
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4017
+ app.deformPresets.trigger(entry.item, '${input.preset}', ${JSON.stringify(params)});
4018
+ return { success: true, action: 'trigger', itemId: ${itemIdStr}, preset: '${input.preset}' };
4019
+ })();`.trim();
4020
+ }
4021
+ case 'remove': {
4022
+ if (!input.itemId)
4023
+ return `(function() { return { error: 'itemId is required for deform remove' }; })();`;
4024
+ const itemIdStr = JSON.stringify(input.itemId);
4025
+ return `
4026
+ // Remove deformation and restore geometry
4027
+ (function() {
4028
+ ${guard}
4029
+ const entry = app.itemRegistry.get(${itemIdStr});
4030
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4031
+ app.deformPresets.remove(entry.item);
4032
+ return { success: true, action: 'remove', itemId: ${itemIdStr} };
4033
+ })();`.trim();
4034
+ }
4035
+ default:
4036
+ return `(function() { return { error: 'Unknown deform action: ${input.action}' }; })();`;
4037
+ }
4038
+ }
4039
+ // ===========================================================================
4040
+ // SPRITE SHEETS
4041
+ // ===========================================================================
4042
+ generateSpriteSheet(input) {
4043
+ const guard = `if (!app.spriteSheetSystem) return { error: 'SpriteSheetSystem not available' };`;
4044
+ switch (input.action) {
4045
+ case 'generate': {
4046
+ const opts = {};
4047
+ if (input.poses)
4048
+ opts.poses = input.poses;
4049
+ if (input.transition)
4050
+ opts.transition = input.transition;
4051
+ if (input.bakedAnimation)
4052
+ opts.bakedAnimation = input.bakedAnimation;
4053
+ if (input.animations)
4054
+ opts.animations = input.animations;
4055
+ if (input.padding !== undefined)
4056
+ opts.padding = input.padding;
4057
+ if (input.name)
4058
+ opts.name = input.name;
4059
+ return `
4060
+ // Generate sprite sheet from skeleton
4061
+ (async function() {
4062
+ ${guard}
4063
+ const sheet = await app.spriteSheetSystem.generateSpriteSheet(${JSON.stringify(input.skeletonId || '')}, ${JSON.stringify(opts)});
4064
+ return { success: true, action: 'generate', spriteSheetId: sheet.id, name: sheet.name, width: sheet.atlasWidth, height: sheet.atlasHeight, frameCount: sheet.frames?.size ?? sheet.frames?.length ?? 0 };
4065
+ })();`.trim();
4066
+ }
4067
+ case 'play': {
4068
+ const opts = {};
4069
+ if (input.x !== undefined)
4070
+ opts.x = input.x;
4071
+ if (input.y !== undefined)
4072
+ opts.y = input.y;
4073
+ if (input.animation)
4074
+ opts.animation = input.animation;
4075
+ if (input.fps !== undefined)
4076
+ opts.fps = input.fps;
4077
+ if (input.scale !== undefined)
4078
+ opts.scale = input.scale;
4079
+ return `
4080
+ // Play sprite sheet animation
4081
+ (async function() {
4082
+ ${guard}
4083
+ const player = await app.spriteSheetSystem.playSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, ${JSON.stringify(opts)});
4084
+ return { success: true, action: 'play', playerId: player.id };
4085
+ })();`.trim();
4086
+ }
4087
+ case 'export':
4088
+ return `
4089
+ // Export sprite sheet
4090
+ (async function() {
4091
+ ${guard}
4092
+ const result = await app.spriteSheetSystem.exportSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, { format: '${input.format || 'png'}', download: true, includeMetadata: true });
4093
+ return { success: true, action: 'export', format: '${input.format || 'png'}' };
4094
+ })();`.trim();
4095
+ default:
4096
+ return `(function() { return { error: 'Unknown sprite sheet action: ${input.action}' }; })();`;
4097
+ }
4098
+ }
4099
+ // ===========================================================================
4100
+ // INTERACTION
4101
+ // ===========================================================================
4102
+ generateInteraction(input) {
4103
+ const guard = `if (!app.interactionSystem) return { error: 'InteractionSystem not available' };`;
4104
+ switch (input.action) {
4105
+ case 'add_behavior': {
4106
+ const itemIdStr = JSON.stringify(input.itemId || '');
4107
+ const params = JSON.stringify(input.params || {});
4108
+ return `
4109
+ // Add behavior to item
4110
+ (function() {
4111
+ ${guard}
4112
+ const entry = app.itemRegistry.get(${itemIdStr});
4113
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4114
+ const behaviorId = app.interactionSystem.addContinuousBehavior(entry.item, '${input.behaviorType}', ${params});
4115
+ return { success: true, action: 'add_behavior', itemId: ${itemIdStr}, behaviorType: '${input.behaviorType}', behaviorId };
4116
+ })();`.trim();
4117
+ }
4118
+ case 'remove_behavior': {
4119
+ const itemIdStr = JSON.stringify(input.itemId || '');
4120
+ const behaviorIdStr = JSON.stringify(input.behaviorId || '');
4121
+ return `
4122
+ // Remove behavior from item
4123
+ (function() {
4124
+ ${guard}
4125
+ const entry = app.itemRegistry.get(${itemIdStr});
4126
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4127
+ app.interactionSystem.removeContinuousBehavior(entry.item, ${behaviorIdStr});
4128
+ return { success: true, action: 'remove_behavior', itemId: ${itemIdStr}, behaviorId: ${behaviorIdStr} };
4129
+ })();`.trim();
4130
+ }
4131
+ case 'trigger_action': {
4132
+ const params = JSON.stringify(input.params || {});
4133
+ return `
4134
+ // Trigger interaction action
4135
+ (function() {
4136
+ ${guard}
4137
+ app.interactionSystem.triggerAction('${input.actionType}', ${params});
4138
+ return { success: true, action: 'trigger_action', actionType: '${input.actionType}' };
4139
+ })();`.trim();
4140
+ }
4141
+ case 'get_state':
4142
+ return `
4143
+ // Get interaction state
4144
+ (function() {
4145
+ ${guard}
4146
+ const state = app.interactionSystem.getState ? app.interactionSystem.getState() : {};
4147
+ return { success: true, action: 'get_state', ...state };
4148
+ })();`.trim();
4149
+ default:
4150
+ return `(function() { return { error: 'Unknown interaction action: ${input.action}' }; })();`;
4151
+ }
4152
+ }
4153
+ // ===========================================================================
4154
+ // WIDGET EXPORT
4155
+ // ===========================================================================
4156
+ generateExportWidget(input) {
4157
+ const opts = {};
4158
+ if (input.download !== undefined)
4159
+ opts.download = input.download;
4160
+ if (input.filename)
4161
+ opts.filename = input.filename;
4162
+ if (input.includeInteractions !== undefined)
4163
+ opts.includeInteractions = input.includeInteractions;
4164
+ if (input.minify !== undefined)
4165
+ opts.minify = input.minify;
4166
+ return `
4167
+ // Export widget (pp:PinePaper ontology JSON)
4168
+ (async function() {
4169
+ if (!app.exportEngine || !app.exportEngine.exportWidget) return { error: 'Widget export not available' };
4170
+ const result = await app.exportEngine.exportWidget(${JSON.stringify(opts)});
4171
+ return { success: true, json: result.json, filename: result.filename, embedCode: result.embedCode, metadata: result.data?.metadata };
4172
+ })();`.trim();
4173
+ }
4174
+ generateExportWidgetHtml(input) {
4175
+ const opts = {};
4176
+ if (input.title)
4177
+ opts.title = input.title;
4178
+ if (input.download !== undefined)
4179
+ opts.download = input.download;
4180
+ return `
4181
+ // Export widget as self-contained HTML
4182
+ (async function() {
4183
+ if (!app.exportEngine || !app.exportEngine.exportWidgetHTML) return { error: 'Widget HTML export not available' };
4184
+ const result = await app.exportEngine.exportWidgetHTML(${JSON.stringify(opts)});
4185
+ 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 } };
4186
+ })();`.trim();
4187
+ }
4188
+ // ===========================================================================
4189
+ // DATA VISUALIZATION (CHARTS)
4190
+ // ===========================================================================
4191
+ generateChart(input) {
4192
+ const guard = `if (!app.chartSystem) return { error: 'ChartSystem not available' };`;
4193
+ switch (input.action) {
4194
+ case 'create': {
4195
+ if (!input.chartType)
4196
+ return `(function() { return { error: 'chartType is required for create' }; })();`;
4197
+ if (!input.data)
4198
+ return `(function() { return { error: 'data is required for create' }; })();`;
4199
+ const dataStr = JSON.stringify(input.data);
4200
+ const optsStr = JSON.stringify(input.options || {});
4201
+ return `
4202
+ // Create chart
4203
+ (function() {
4204
+ ${guard}
4205
+ const group = app.createChart(${JSON.stringify(input.chartType)}, ${dataStr}, ${optsStr});
4206
+ if (!group) return { error: 'Failed to create chart' };
4207
+ const chartId = app.chartSystem.getLastChartId();
4208
+ return { success: true, action: 'create', chartType: ${JSON.stringify(input.chartType)}, chartId, dataPoints: ${input.data.length} };
4209
+ })();`.trim();
4210
+ }
4211
+ case 'update': {
4212
+ if (!input.chartId)
4213
+ return `(function() { return { error: 'chartId is required for update' }; })();`;
4214
+ const chartIdStr = JSON.stringify(input.chartId);
4215
+ const dataStr = JSON.stringify(input.data || []);
4216
+ const optsStr = JSON.stringify(input.options || {});
4217
+ return `
4218
+ // Update chart data
4219
+ (function() {
4220
+ ${guard}
4221
+ const chart = app.chartSystem.getChart(${chartIdStr});
4222
+ if (!chart) return { error: 'Chart not found: ' + ${chartIdStr} };
4223
+ app.chartSystem.update(${chartIdStr}, ${dataStr}, ${optsStr});
4224
+ return { success: true, action: 'update', chartId: ${chartIdStr} };
4225
+ })();`.trim();
4226
+ }
4227
+ case 'reconfigure': {
4228
+ if (!input.chartId)
4229
+ return `(function() { return { error: 'chartId is required for reconfigure' }; })();`;
4230
+ const chartIdStr = JSON.stringify(input.chartId);
4231
+ const optsStr = JSON.stringify(input.options || {});
4232
+ return `
4233
+ // Reconfigure chart styling
4234
+ (function() {
4235
+ ${guard}
4236
+ const chart = app.chartSystem.getChart(${chartIdStr});
4237
+ if (!chart) return { error: 'Chart not found: ' + ${chartIdStr} };
4238
+ app.chartSystem.reconfigure(${chartIdStr}, ${optsStr});
4239
+ return { success: true, action: 'reconfigure', chartId: ${chartIdStr} };
4240
+ })();`.trim();
4241
+ }
4242
+ case 'remove': {
4243
+ if (!input.chartId)
4244
+ return `(function() { return { error: 'chartId is required for remove' }; })();`;
4245
+ const chartIdStr = JSON.stringify(input.chartId);
4246
+ return `
4247
+ // Remove chart
4248
+ (function() {
4249
+ ${guard}
4250
+ app.chartSystem.remove(${chartIdStr});
4251
+ return { success: true, action: 'remove', chartId: ${chartIdStr} };
4252
+ })();`.trim();
4253
+ }
4254
+ default:
4255
+ return `(function() { return { error: 'Unknown chart action: ${input.action}' }; })();`;
4256
+ }
4257
+ }
4258
+ // ===========================================================================
4259
+ // MAGIC SYSTEM
4260
+ // ===========================================================================
4261
+ generateMagic(input) {
4262
+ const guard = `if (!app.magicSystem) return { error: 'MagicSystem not available' };`;
4263
+ switch (input.action) {
4264
+ case 'animate': {
4265
+ const opts = {};
4266
+ if (input.mood)
4267
+ opts.energy = input.mood;
4268
+ if (input.selectionOnly !== undefined)
4269
+ opts.selectionOnly = input.selectionOnly;
4270
+ return `
4271
+ // Magic auto-animate
4272
+ (async function() {
4273
+ ${guard}
4274
+ const result = await app.magicSystem.autoAnimate(${JSON.stringify(opts)});
4275
+ return { success: true, action: 'animate', itemsAnimated: result.count, mood: result.energy, type: result.type };
4276
+ })();`.trim();
4277
+ }
4278
+ case 'remix': {
4279
+ const opts = {};
4280
+ if (input.selectionOnly !== undefined)
4281
+ opts.selectionOnly = input.selectionOnly;
4282
+ return `
4283
+ // Magic style remix
4284
+ (async function() {
4285
+ ${guard}
4286
+ const result = await app.magicSystem.remixStyle(${JSON.stringify(opts)});
4287
+ return { success: true, action: 'remix', itemsRemixed: result.count, palette: result.palette };
4288
+ })();`.trim();
4289
+ }
4290
+ default:
4291
+ return `(function() { return { error: 'Unknown magic action: ${input.action}' }; })();`;
4292
+ }
4293
+ }
4294
+ // ===========================================================================
4295
+ // PHYSICS (RIGID BODY SIMULATION)
4296
+ // ===========================================================================
4297
+ generatePhysics(input) {
4298
+ const guard = `if (!app.physicsWorld) return { error: 'PhysicsWorld not available' };`;
4299
+ switch (input.action) {
4300
+ case 'init': {
4301
+ const gravity = JSON.stringify(input.gravity || { x: 0, y: 980 });
4302
+ return `
4303
+ // Initialize physics world
4304
+ (function() {
4305
+ ${guard}
4306
+ app.physicsWorld.init({ gravity: ${gravity} });
4307
+ return { success: true, action: 'init', gravity: ${gravity} };
4308
+ })();`.trim();
4309
+ }
4310
+ case 'add_body': {
4311
+ if (!input.itemId)
4312
+ return `(function() { return { error: 'itemId is required for add_body' }; })();`;
4313
+ const itemIdStr = JSON.stringify(input.itemId);
4314
+ const config = {};
4315
+ if (input.bodyType)
4316
+ config.type = input.bodyType;
4317
+ if (input.mass !== undefined)
4318
+ config.mass = input.mass;
4319
+ if (input.friction !== undefined)
4320
+ config.friction = input.friction;
4321
+ if (input.restitution !== undefined)
4322
+ config.restitution = input.restitution;
4323
+ if (input.fixedRotation !== undefined)
4324
+ config.fixedRotation = input.fixedRotation;
4325
+ if (input.shape)
4326
+ config.shape = input.shape;
4327
+ return `
4328
+ // Add physics body
4329
+ (function() {
4330
+ ${guard}
4331
+ const entry = app.itemRegistry.get(${itemIdStr});
4332
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4333
+ const ok = app.physicsWorld.addBody(${itemIdStr}, ${JSON.stringify(config)});
4334
+ return { success: ok, action: 'add_body', itemId: ${itemIdStr}, bodyType: ${JSON.stringify(input.bodyType || 'dynamic')} };
4335
+ })();`.trim();
4336
+ }
4337
+ case 'remove_body': {
4338
+ if (!input.itemId)
4339
+ return `(function() { return { error: 'itemId is required for remove_body' }; })();`;
4340
+ const itemIdStr = JSON.stringify(input.itemId);
4341
+ return `
4342
+ // Remove physics body
4343
+ (function() {
4344
+ ${guard}
4345
+ app.physicsWorld.removeBody(${itemIdStr});
4346
+ return { success: true, action: 'remove_body', itemId: ${itemIdStr} };
4347
+ })();`.trim();
4348
+ }
4349
+ case 'apply_force': {
4350
+ if (!input.itemId)
4351
+ return `(function() { return { error: 'itemId is required for apply_force' }; })();`;
4352
+ if (!input.force)
4353
+ return `(function() { return { error: 'force is required for apply_force' }; })();`;
4354
+ const itemIdStr = JSON.stringify(input.itemId);
4355
+ return `
4356
+ // Apply continuous force
4357
+ (function() {
4358
+ ${guard}
4359
+ app.physicsWorld.applyForce(${itemIdStr}, ${JSON.stringify(input.force)});
4360
+ return { success: true, action: 'apply_force', itemId: ${itemIdStr}, force: ${JSON.stringify(input.force)} };
4361
+ })();`.trim();
4362
+ }
4363
+ case 'apply_impulse': {
4364
+ if (!input.itemId)
4365
+ return `(function() { return { error: 'itemId is required for apply_impulse' }; })();`;
4366
+ if (!input.impulse)
4367
+ return `(function() { return { error: 'impulse is required for apply_impulse' }; })();`;
4368
+ const itemIdStr = JSON.stringify(input.itemId);
4369
+ return `
4370
+ // Apply instant impulse
4371
+ (function() {
4372
+ ${guard}
4373
+ app.physicsWorld.applyImpulse(${itemIdStr}, ${JSON.stringify(input.impulse)});
4374
+ return { success: true, action: 'apply_impulse', itemId: ${itemIdStr}, impulse: ${JSON.stringify(input.impulse)} };
4375
+ })();`.trim();
4376
+ }
4377
+ case 'set_velocity': {
4378
+ if (!input.itemId)
4379
+ return `(function() { return { error: 'itemId is required for set_velocity' }; })();`;
4380
+ if (!input.velocity)
4381
+ return `(function() { return { error: 'velocity is required for set_velocity' }; })();`;
4382
+ const itemIdStr = JSON.stringify(input.itemId);
4383
+ return `
4384
+ // Set body velocity
4385
+ (function() {
4386
+ ${guard}
4387
+ app.physicsWorld.setVelocity(${itemIdStr}, ${JSON.stringify(input.velocity)});
4388
+ return { success: true, action: 'set_velocity', itemId: ${itemIdStr}, velocity: ${JSON.stringify(input.velocity)} };
4389
+ })();`.trim();
4390
+ }
4391
+ case 'get_state': {
4392
+ if (!input.itemId)
4393
+ return `(function() { return { error: 'itemId is required for get_state' }; })();`;
4394
+ const itemIdStr = JSON.stringify(input.itemId);
4395
+ return `
4396
+ // Get physics body state
4397
+ (function() {
4398
+ ${guard}
4399
+ const state = app.physicsWorld.getBodyState(${itemIdStr});
4400
+ if (!state) return { error: 'No physics body for item: ' + ${itemIdStr} };
4401
+ return { success: true, action: 'get_state', itemId: ${itemIdStr}, ...state };
4402
+ })();`.trim();
4403
+ }
4404
+ case 'create_ground': {
4405
+ const y = input.y !== undefined ? input.y : 500;
4406
+ const w = input.width !== undefined ? input.width : 2000;
4407
+ return `
4408
+ // Create ground plane
4409
+ (function() {
4410
+ ${guard}
4411
+ const groundId = app.physicsWorld.createGround(${y}, ${w});
4412
+ return { success: true, action: 'create_ground', groundId, y: ${y}, width: ${w} };
4413
+ })();`.trim();
4414
+ }
4415
+ case 'create_joint': {
4416
+ if (!input.itemId)
4417
+ return `(function() { return { error: 'itemId is required for create_joint' }; })();`;
4418
+ if (!input.targetItemId)
4419
+ return `(function() { return { error: 'targetItemId is required for create_joint' }; })();`;
4420
+ if (!input.jointType)
4421
+ return `(function() { return { error: 'jointType is required for create_joint' }; })();`;
4422
+ const itemIdStr = JSON.stringify(input.itemId);
4423
+ const targetStr = JSON.stringify(input.targetItemId);
4424
+ const params = JSON.stringify({ type: input.jointType, ...(input.jointParams || {}) });
4425
+ return `
4426
+ // Create physics joint
4427
+ (function() {
4428
+ ${guard}
4429
+ const joint = app.physicsWorld.createJoint(${itemIdStr}, ${targetStr}, ${params});
4430
+ if (!joint) return { error: 'Failed to create joint between ' + ${itemIdStr} + ' and ' + ${targetStr} };
4431
+ return { success: true, action: 'create_joint', itemId: ${itemIdStr}, targetItemId: ${targetStr}, jointType: ${JSON.stringify(input.jointType)} };
4432
+ })();`.trim();
4433
+ }
4434
+ default:
4435
+ return `(function() { return { error: 'Unknown physics action: ${input.action}' }; })();`;
4436
+ }
4437
+ }
4438
+ // ===========================================================================
4439
+ // MEASUREMENT SYSTEM
4440
+ // ===========================================================================
4441
+ generateMeasurement(input) {
4442
+ const guard = `if (!app.measurementSystem) return { error: 'MeasurementSystem not available' };`;
4443
+ switch (input.action) {
4444
+ case 'set_rulers': {
4445
+ const enabled = input.enabled !== false;
4446
+ return `
4447
+ // Toggle rulers
4448
+ (function() {
4449
+ ${guard}
4450
+ app.measurementSystem.setRulersVisible(${enabled});
4451
+ return { success: true, action: 'set_rulers', enabled: ${enabled} };
4452
+ })();`.trim();
4453
+ }
4454
+ case 'set_grid': {
4455
+ const enabled = input.enabled !== false;
4456
+ return `
4457
+ // Toggle grid
4458
+ (function() {
4459
+ ${guard}
4460
+ app.measurementSystem.setGridVisible(${enabled});
4461
+ return { success: true, action: 'set_grid', enabled: ${enabled} };
4462
+ })();`.trim();
4463
+ }
4464
+ // No measurementSystem guard — reads from itemRegistry directly
4465
+ case 'get_dimensions': {
4466
+ if (!input.itemId)
4467
+ return `(function() { return { error: 'itemId is required for get_dimensions' }; })();`;
4468
+ const itemIdStr = JSON.stringify(input.itemId);
4469
+ return `
4470
+ // Get item dimensions
4471
+ (function() {
4472
+ const entry = app.itemRegistry.get(${itemIdStr});
4473
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4474
+ const b = entry.item.bounds;
4475
+ return { success: true, action: 'get_dimensions', itemId: ${itemIdStr}, x: b.x, y: b.y, width: b.width, height: b.height, rotation: entry.item.rotation || 0 };
4476
+ })();`.trim();
4477
+ }
4478
+ case 'set_snap': {
4479
+ const enabled = input.enabled !== false;
4480
+ return `
4481
+ // Toggle snap to grid
4482
+ (function() {
4483
+ ${guard}
4484
+ app.measurementSystem.setSnapToUnitEnabled(${enabled});
4485
+ return { success: true, action: 'set_snap', enabled: ${enabled} };
4486
+ })();`.trim();
4487
+ }
4488
+ default:
4489
+ return `(function() { return { error: 'Unknown measurement action: ${input.action}' }; })();`;
4490
+ }
4491
+ }
4097
4492
  }
4098
4493
  // Export singleton instance
4099
4494
  export const codeGenerator = new PinePaperCodeGenerator();