@pinepaper.studio/mcp-server 1.5.1 → 1.5.4

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 (96) hide show
  1. package/README.md +6 -4
  2. package/dist/agent/types.d.ts +16 -16
  3. package/dist/browser/puppeteer-controller.d.ts +5 -1
  4. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  5. package/dist/browser/puppeteer-controller.js +11 -3
  6. package/dist/browser/puppeteer-controller.js.map +1 -1
  7. package/dist/browser/url-target.d.ts +26 -0
  8. package/dist/browser/url-target.d.ts.map +1 -0
  9. package/dist/browser/url-target.js +50 -0
  10. package/dist/browser/url-target.js.map +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +128 -109
  13. package/dist/index.js.map +1 -1
  14. package/dist/ontology/design-graph.d.ts +85 -0
  15. package/dist/ontology/design-graph.d.ts.map +1 -0
  16. package/dist/ontology/design-graph.js +944 -0
  17. package/dist/ontology/design-graph.js.map +1 -0
  18. package/dist/ontology/hints.d.ts +71 -0
  19. package/dist/ontology/hints.d.ts.map +1 -0
  20. package/dist/ontology/hints.js +189 -0
  21. package/dist/ontology/hints.js.map +1 -0
  22. package/dist/ontology/index.d.ts +11 -0
  23. package/dist/ontology/index.d.ts.map +1 -0
  24. package/dist/ontology/index.js +14 -0
  25. package/dist/ontology/index.js.map +1 -0
  26. package/dist/ontology/semantic-vocabulary.d.ts +12 -0
  27. package/dist/ontology/semantic-vocabulary.d.ts.map +1 -0
  28. package/dist/ontology/semantic-vocabulary.js +26 -0
  29. package/dist/ontology/semantic-vocabulary.js.map +1 -0
  30. package/dist/ontology/types.d.ts +255 -0
  31. package/dist/ontology/types.d.ts.map +1 -0
  32. package/dist/ontology/types.js +10 -0
  33. package/dist/ontology/types.js.map +1 -0
  34. package/dist/ontology/validator.d.ts +36 -0
  35. package/dist/ontology/validator.d.ts.map +1 -0
  36. package/dist/ontology/validator.js +374 -0
  37. package/dist/ontology/validator.js.map +1 -0
  38. package/dist/ontology/vocabulary.d.ts +34 -0
  39. package/dist/ontology/vocabulary.d.ts.map +1 -0
  40. package/dist/ontology/vocabulary.js +429 -0
  41. package/dist/ontology/vocabulary.js.map +1 -0
  42. package/dist/prompts/index.d.ts.map +1 -1
  43. package/dist/prompts/index.js +146 -0
  44. package/dist/prompts/index.js.map +1 -1
  45. package/dist/tools/compact-descriptions.d.ts +5 -2
  46. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  47. package/dist/tools/compact-descriptions.js +56 -235
  48. package/dist/tools/compact-descriptions.js.map +1 -1
  49. package/dist/tools/definitions.d.ts +1 -1
  50. package/dist/tools/definitions.d.ts.map +1 -1
  51. package/dist/tools/definitions.js +1842 -1784
  52. package/dist/tools/definitions.js.map +1 -1
  53. package/dist/tools/handlers/camera.d.ts +19 -0
  54. package/dist/tools/handlers/camera.d.ts.map +1 -0
  55. package/dist/tools/handlers/camera.js +94 -0
  56. package/dist/tools/handlers/camera.js.map +1 -0
  57. package/dist/tools/handlers/export.d.ts +12 -0
  58. package/dist/tools/handlers/export.d.ts.map +1 -0
  59. package/dist/tools/handlers/export.js +30 -0
  60. package/dist/tools/handlers/export.js.map +1 -0
  61. package/dist/tools/handlers/font.d.ts +12 -0
  62. package/dist/tools/handlers/font.d.ts.map +1 -0
  63. package/dist/tools/handlers/font.js +135 -0
  64. package/dist/tools/handlers/font.js.map +1 -0
  65. package/dist/tools/handlers/maps.d.ts +19 -0
  66. package/dist/tools/handlers/maps.d.ts.map +1 -0
  67. package/dist/tools/handlers/maps.js +184 -0
  68. package/dist/tools/handlers/maps.js.map +1 -0
  69. package/dist/tools/handlers/ontology.d.ts +20 -0
  70. package/dist/tools/handlers/ontology.d.ts.map +1 -0
  71. package/dist/tools/handlers/ontology.js +185 -0
  72. package/dist/tools/handlers/ontology.js.map +1 -0
  73. package/dist/tools/handlers/tool-guide.d.ts +14 -0
  74. package/dist/tools/handlers/tool-guide.d.ts.map +1 -0
  75. package/dist/tools/handlers/tool-guide.js +52 -0
  76. package/dist/tools/handlers/tool-guide.js.map +1 -0
  77. package/dist/tools/handlers.d.ts +18 -0
  78. package/dist/tools/handlers.d.ts.map +1 -1
  79. package/dist/tools/handlers.js +321 -574
  80. package/dist/tools/handlers.js.map +1 -1
  81. package/dist/tools/minimal-descriptions.d.ts +4 -0
  82. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  83. package/dist/tools/minimal-descriptions.js +65 -54
  84. package/dist/tools/minimal-descriptions.js.map +1 -1
  85. package/dist/tools/toolkits.d.ts.map +1 -1
  86. package/dist/tools/toolkits.js +67 -46
  87. package/dist/tools/toolkits.js.map +1 -1
  88. package/dist/types/code-generator.d.ts +49 -2
  89. package/dist/types/code-generator.d.ts.map +1 -1
  90. package/dist/types/code-generator.js +1456 -39
  91. package/dist/types/code-generator.js.map +1 -1
  92. package/dist/types/schemas.d.ts +1381 -404
  93. package/dist/types/schemas.d.ts.map +1 -1
  94. package/dist/types/schemas.js +426 -20
  95. package/dist/types/schemas.js.map +1 -1
  96. package/package.json +2 -2
@@ -82,7 +82,7 @@ function generateBlendModeCode(itemVarName, blendMode) {
82
82
  */
83
83
  function generateCreateItemCode(itemType, position, properties) {
84
84
  // Extract special properties that need separate handling
85
- const { color, fillColor, strokeColor, shadowColor, shadowBlur, shadowOffset, blendMode, opacity, ...baseProperties } = properties;
85
+ const { color, fillColor, strokeColor, shadowColor, shadowBlur, shadowOffset, blendMode, opacity, contentType, contentFormat, countdownTarget, countdownEndText, ...baseProperties } = properties;
86
86
  const params = {
87
87
  x: position.x,
88
88
  y: position.y,
@@ -127,6 +127,17 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
127
127
  }
128
128
  // Ensure item is visible above backgrounds/generators
129
129
  code += `\nif (item.bringToFront) item.bringToFront();`;
130
+ // Add dynamic content support for text items
131
+ if (contentType && itemType === 'text') {
132
+ const opts = {};
133
+ if (contentFormat)
134
+ opts.format = contentFormat;
135
+ if (countdownTarget !== undefined)
136
+ opts.countdownTarget = countdownTarget;
137
+ if (countdownEndText)
138
+ opts.countdownEndText = countdownEndText;
139
+ code += `\nif (app.setDynamicContent) app.setDynamicContent(item, '${contentType}', ${JSON.stringify(opts)});`;
140
+ }
130
141
  code += `
131
142
  const itemId = item.data.registryId;
132
143
  app.historyManager.saveState();
@@ -139,15 +150,42 @@ app.historyManager.saveState();
139
150
  * Template for modifying items
140
151
  */
141
152
  function generateModifyItemCode(itemId, properties) {
142
- return `
153
+ const { contentType, contentFormat, countdownTarget, countdownEndText, ...restProperties } = properties;
154
+ let code = `
143
155
  // Modify item ${itemId}
144
- app.select('${itemId}');
145
- app.modify(${JSON.stringify(properties, null, 2)});
156
+ app.select('${itemId}');`;
157
+ // Pass non-dynamic-content properties to app.modify
158
+ if (Object.keys(restProperties).length > 0) {
159
+ code += `\napp.modify(${JSON.stringify(restProperties, null, 2)});`;
160
+ }
161
+ // Handle dynamic content type changes
162
+ if (contentType !== undefined) {
163
+ if (contentType === null || contentType === 'none' || contentType === '') {
164
+ // Remove dynamic content
165
+ code += `
166
+ const entry = app.itemRegistry.get('${itemId}');
167
+ if (entry && entry.item && app.removeDynamicContent) app.removeDynamicContent(entry.item);`;
168
+ }
169
+ else {
170
+ // Set dynamic content
171
+ const opts = {};
172
+ if (contentFormat)
173
+ opts.format = contentFormat;
174
+ if (countdownTarget !== undefined)
175
+ opts.countdownTarget = countdownTarget;
176
+ if (countdownEndText)
177
+ opts.countdownEndText = countdownEndText;
178
+ code += `
179
+ const entry = app.itemRegistry.get('${itemId}');
180
+ if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.item, '${contentType}', ${JSON.stringify(opts)});`;
181
+ }
182
+ }
183
+ code += `
146
184
  app.historyManager.saveState();
147
185
 
148
186
  // Return success
149
- ({ success: true, itemId: '${itemId}' });
150
- `.trim();
187
+ ({ success: true, itemId: '${itemId}' });`;
188
+ return code.trim();
151
189
  }
152
190
  /**
153
191
  * Template for deleting items
@@ -257,14 +295,21 @@ app.animate(item, {
257
295
  /**
258
296
  * Template for keyframe animation
259
297
  */
260
- function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false) {
298
+ function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint) {
261
299
  const keyframesJson = JSON.stringify(keyframes, null, 2);
262
300
  const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
301
+ const opts = { duration: calculatedDuration, loop };
302
+ if (timeOffset !== undefined)
303
+ opts.timeOffset = timeOffset;
304
+ if (clipInPoint !== undefined)
305
+ opts.clipInPoint = clipInPoint;
306
+ if (clipOutPoint !== undefined)
307
+ opts.clipOutPoint = clipOutPoint;
263
308
  return `
264
309
  // Apply keyframe animation to ${itemId}
265
- app.addAnimation('${itemId}', ${keyframesJson}, { duration: ${calculatedDuration}, loop: ${loop} });
310
+ app.addAnimation('${itemId}', ${keyframesJson}, ${JSON.stringify(opts)});
266
311
 
267
- ({ success: true, itemId: '${itemId}', duration: ${calculatedDuration}, loop: ${loop} });
312
+ ({ success: true, itemId: '${itemId}', duration: ${calculatedDuration}, loop: ${loop}${timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : ''}${clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : ''}${clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : ''} });
268
313
  `.trim();
269
314
  }
270
315
  /**
@@ -351,6 +396,12 @@ function generatePlayTimelineCode(action, duration, loop, time) {
351
396
  // Play keyframe timeline
352
397
  app.playKeyframeTimeline(${duration || 5}, ${loop ?? false});
353
398
  ({ success: true, action: 'play', duration: ${duration || 5}, loop: ${loop ?? false} });
399
+ `.trim();
400
+ case 'pause':
401
+ return `
402
+ // Pause keyframe timeline
403
+ app.pauseKeyframeTimeline();
404
+ ({ success: true, action: 'pause' });
354
405
  `.trim();
355
406
  case 'stop':
356
407
  return `
@@ -576,7 +627,14 @@ export class PinePaperCodeGenerator {
576
627
  */
577
628
  generateCreateItem(input) {
578
629
  const validated = CreateItemInputSchema.parse(input);
579
- return generateCreateItemCode(validated.itemType, validated.position, validated.properties);
630
+ const properties = { ...validated.properties };
631
+ if (validated.animationType !== undefined)
632
+ properties.animationType = validated.animationType;
633
+ if (validated.animationSpeed !== undefined)
634
+ properties.animationSpeed = validated.animationSpeed;
635
+ if (validated.keyframes !== undefined)
636
+ properties.keyframes = validated.keyframes;
637
+ return generateCreateItemCode(validated.itemType, validated.position, properties);
580
638
  }
581
639
  /**
582
640
  * Generate code for modifying an item
@@ -622,7 +680,7 @@ export class PinePaperCodeGenerator {
622
680
  */
623
681
  generateKeyframeAnimate(input) {
624
682
  const validated = KeyframeAnimateInputSchema.parse(input);
625
- return generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop);
683
+ return generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint);
626
684
  }
627
685
  /**
628
686
  * Generate code for executing a generator
@@ -742,6 +800,70 @@ if (imported) {
742
800
  return `
743
801
  // Error: No SVG source provided
744
802
  throw new Error('Either svgString or url must be provided');
803
+ `.trim();
804
+ }
805
+ /**
806
+ * Generate code for importing a Mermaid diagram (flowchart, stateDiagram,
807
+ * sequenceDiagram, erDiagram, classDiagram).
808
+ */
809
+ generateImportMermaid(mermaidText, options = {}) {
810
+ const escaped = mermaidText.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
811
+ const optsJson = JSON.stringify(options);
812
+ return `
813
+ // Import Mermaid diagram
814
+ (function() {
815
+ if (!app.importMermaid) return { success: false, error: 'importMermaid not available — diagramSystem missing' };
816
+ const mermaidText = \`${escaped}\`;
817
+ const result = app.importMermaid(mermaidText, ${optsJson});
818
+ return {
819
+ success: !!result.success,
820
+ nodeCount: result.nodes ? result.nodes.length : 0,
821
+ edgeCount: result.edges ? result.edges.length : 0,
822
+ nodes: result.nodes || [],
823
+ edges: result.edges || [],
824
+ errors: result.errors || [],
825
+ };
826
+ })();
827
+ `.trim();
828
+ }
829
+ /**
830
+ * Generate code for capturing the canvas as pp: ontology. Wraps FxTool's
831
+ * app.exportCanvasOntology() — defined in PinePaper.js after the
832
+ * canvas-state-to-ontology promotion. Output mirrors the cloud-sync
833
+ * postMessage contract: { canvasWidth, canvasHeight, itemCount,
834
+ * itemTypes, hasAnimations, relationCount, items, triples, [viewport] }.
835
+ *
836
+ * Failure modes: app missing the method → return { success: false, error }
837
+ * with a clear FxTool-version hint. Internal exceptions propagate from
838
+ * the browser-side function (TripleLanguage warns to console; we don't
839
+ * fail the tool call on triple-only failures).
840
+ */
841
+ generateGetCanvasOntology(options = {}) {
842
+ const optsJson = JSON.stringify(options);
843
+ return `
844
+ // Capture canvas as pp: ontology
845
+ (function() {
846
+ if (!app.exportCanvasOntology) {
847
+ return {
848
+ success: false,
849
+ error: 'app.exportCanvasOntology not available — requires FxTool with canvas-ontology promotion. Falling back to pinepaper_get_items is acceptable but loses the triples representation.',
850
+ };
851
+ }
852
+ const ctx = app.exportCanvasOntology(${optsJson});
853
+ return {
854
+ success: true,
855
+ canvasWidth: ctx.canvasWidth,
856
+ canvasHeight: ctx.canvasHeight,
857
+ canvasPreset: ctx.canvasPreset,
858
+ itemCount: ctx.itemCount,
859
+ itemTypes: ctx.itemTypes,
860
+ hasAnimations: ctx.hasAnimations,
861
+ relationCount: ctx.relationCount,
862
+ items: ctx.items,
863
+ triples: ctx.triples,
864
+ viewport: ctx.viewport,
865
+ };
866
+ })();
745
867
  `.trim();
746
868
  }
747
869
  /**
@@ -1340,10 +1462,11 @@ throw new Error('Unknown diagram mode action: ${action}');
1340
1462
  */
1341
1463
  generateAgentStartJob(input) {
1342
1464
  const validated = AgentStartJobInputSchema.parse(input);
1343
- const { name, screenshotPolicy, canvasPreset, clearCanvas } = validated;
1465
+ const { name, screenshotPolicy, canvasPreset, clearCanvas, includeOntology } = validated;
1344
1466
  const nameStr = name ? `'${name.replace(/'/g, "\\'")}'` : 'null';
1345
1467
  const policyStr = screenshotPolicy || 'on_complete';
1346
1468
  const shouldClear = clearCanvas !== false;
1469
+ const wantOntology = includeOntology !== false;
1347
1470
  let code = `
1348
1471
  // Start agent job
1349
1472
  (async function() {
@@ -1372,10 +1495,31 @@ throw new Error('Unknown diagram mode action: ${action}');
1372
1495
  app.setCanvasSize('${canvasPreset}');
1373
1496
  // Allow canvas resize to take effect
1374
1497
  await new Promise(r => setTimeout(r, 50));
1498
+ `;
1499
+ }
1500
+ if (wantOntology) {
1501
+ code += `
1502
+ // Capture initial canvas ontology so the agent has baseline context
1503
+ // without a follow-up pinepaper_get_canvas_ontology call. After
1504
+ // clearCanvas this is a tiny empty-state payload; with clearCanvas:false
1505
+ // it captures whatever's already on the canvas. Older FxTool builds
1506
+ // without exportCanvasOntology produce an _ontologyUnavailable: true
1507
+ // marker so the agent knows to fall back to pinepaper_get_items.
1508
+ let _ontology = null;
1509
+ let _ontologyUnavailable = false;
1510
+ try {
1511
+ if (app.exportCanvasOntology) {
1512
+ _ontology = app.exportCanvasOntology();
1513
+ } else {
1514
+ _ontologyUnavailable = true;
1515
+ }
1516
+ } catch (e) {
1517
+ _ontologyUnavailable = true;
1518
+ }
1375
1519
  `;
1376
1520
  }
1377
1521
  code += `
1378
- // Return job context with canvas size
1522
+ // Return job context with canvas size${wantOntology ? ' + initial ontology' : ''}
1379
1523
  const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
1380
1524
  return {
1381
1525
  success: true,
@@ -1384,7 +1528,9 @@ throw new Error('Unknown diagram mode action: ${action}');
1384
1528
  screenshotPolicy: '${policyStr}',
1385
1529
  canvasPreset: ${canvasPreset ? `'${canvasPreset}'` : 'null'},
1386
1530
  canvasCleared: ${shouldClear},
1387
- canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }
1531
+ canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }${wantOntology ? `,
1532
+ ontology: _ontology,
1533
+ ontologyUnavailable: _ontologyUnavailable || undefined` : ''}
1388
1534
  };
1389
1535
  })();
1390
1536
  `;
@@ -1794,13 +1940,13 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1794
1940
  */
1795
1941
  generateAgentExport(input) {
1796
1942
  const validated = AgentExportInputSchema.parse(input);
1797
- const { platform, format, quality } = validated;
1943
+ const { platform, format, quality, framing } = validated;
1798
1944
  const qualityLevel = quality || 'standard';
1799
- // Quality settings
1945
+ // Quality settings (bitrate in bps for VideoEncoder)
1800
1946
  const qualitySettings = {
1801
- draft: { compression: 0.6, fps: 15, dpi: 72 },
1802
- standard: { compression: 0.85, fps: 30, dpi: 150 },
1803
- high: { compression: 0.95, fps: 60, dpi: 300 },
1947
+ draft: { compression: 0.6, fps: 15, dpi: 72, bitrate: 2_000_000 },
1948
+ standard: { compression: 0.85, fps: 30, dpi: 150, bitrate: 5_000_000 },
1949
+ high: { compression: 0.95, fps: 60, dpi: 300, bitrate: 8_000_000 },
1804
1950
  }[qualityLevel];
1805
1951
  // Platform presets
1806
1952
  const platformPresets = {
@@ -1824,10 +1970,38 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1824
1970
  const platform = '${platform}';
1825
1971
  const format = '${exportFormat}';
1826
1972
  const quality = '${qualityLevel}';
1973
+ const framing = '${framing}';
1827
1974
  const settings = ${JSON.stringify(qualitySettings)};
1828
1975
  const dimensions = ${JSON.stringify({ width: preset.width, height: preset.height })};
1829
1976
 
1830
- let result = { success: false, platform, format, quality };
1977
+ // Resolve camera-view framing dims when requested. Reads the first
1978
+ // camera_animates keyframe's zoom and divides the canvas dims by it
1979
+ // (mirrors FxTool ExportEngine.js:466-485). Camera animation still
1980
+ // drives motion during export — framing only fixes the output frame.
1981
+ let cameraDims = null;
1982
+ if (framing === 'camera') {
1983
+ if (!['gif', 'mp4', 'webm'].includes(format)) {
1984
+ 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.' };
1985
+ }
1986
+ const rr = app.relationRegistry;
1987
+ const hasWalkthrough = rr && typeof rr.hasCameraAnimation === 'function' && rr.hasCameraAnimation();
1988
+ if (!hasWalkthrough) {
1989
+ return { success: false, platform, format, framing, error: 'framing: "camera" requires a camera_animates walkthrough on the canvas. Add one or omit framing.' };
1990
+ }
1991
+ const params = rr.getCameraAnimationParams ? rr.getCameraAnimationParams() : null;
1992
+ if (!params || (params.mode || 'keyframes') !== 'keyframes' || !params.keyframes || !params.keyframes.length) {
1993
+ return { success: false, platform, format, framing, error: 'framing: "camera" requires a keyframe-mode camera walkthrough with at least one keyframe.' };
1994
+ }
1995
+ const firstKf = params.keyframes[0];
1996
+ const firstZoom = firstKf && firstKf.zoom > 0 ? firstKf.zoom : 1;
1997
+ const canvasEl = document.querySelector('canvas');
1998
+ cameraDims = {
1999
+ width: Math.round((canvasEl ? canvasEl.width : dimensions.width) / firstZoom),
2000
+ height: Math.round((canvasEl ? canvasEl.height : dimensions.height) / firstZoom),
2001
+ };
2002
+ }
2003
+
2004
+ let result = { success: false, platform, format, quality, framing };
1831
2005
 
1832
2006
  try {
1833
2007
  switch (format) {
@@ -1875,28 +2049,44 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1875
2049
  case 'mp4':
1876
2050
  case 'webm': {
1877
2051
  const videoMimeType = { mp4: 'video/mp4', webm: 'video/webm', gif: 'image/gif' }[format];
1878
- // Use _quickExportVideo (delegates to VideoExporter.export internally)
1879
- if (app.exportEngine && app.exportEngine._quickExportVideo) {
1880
- const videoResult = await app.exportEngine._quickExportVideo(format, { fps: settings.fps, duration: 5 }, false);
2052
+ // FxTool VideoExporter._calculateBitrate multiplies a per-resolution
2053
+ // base by settings.quality (0.5-1.0). Our existing per-tier compression
2054
+ // values (0.6 / 0.85 / 0.95) map directly to that scale. Without this,
2055
+ // ExportEngine._quickExportVideo overrides quality with undefined for
2056
+ // mp4/webm, _calculateBitrate yields NaN, and VideoEncoder.configure
2057
+ // rejects with 'malformed bitrate value'.
2058
+ const baseVideoSettings = { format, fps: settings.fps, quality: settings.compression, duration: 5 };
2059
+ const blobToDataUrl = (b) => new Promise(resolve => {
2060
+ const r = new FileReader();
2061
+ r.onloadend = () => resolve(r.result);
2062
+ r.readAsDataURL(b);
2063
+ });
2064
+ // Camera framing requires going direct to videoExporter so width/height
2065
+ // pass through — _quickExportVideo strips dim fields.
2066
+ if (cameraDims && app.exportEngine && app.exportEngine.videoExporter) {
2067
+ const blob = await app.exportEngine.videoExporter.export({ ...baseVideoSettings, width: cameraDims.width, height: cameraDims.height });
2068
+ const dataUrl = await blobToDataUrl(blob);
2069
+ result = { success: true, platform, format, framing, data: dataUrl, mimeType: videoMimeType, size: blob.size, dimensions: cameraDims };
2070
+ } else if ((format === 'mp4' || format === 'webm') && app.exportEngine && app.exportEngine.videoExporter) {
2071
+ // Bypass _quickExportVideo for mp4/webm — it hardcodes
2072
+ // quality:undefined for these formats and produces a NaN bitrate.
2073
+ const blob = await app.exportEngine.videoExporter.export(baseVideoSettings);
2074
+ const dataUrl = await blobToDataUrl(blob);
2075
+ result = { success: true, platform, format, framing, data: dataUrl, mimeType: videoMimeType, size: blob.size };
2076
+ } else if (app.exportEngine && app.exportEngine._quickExportVideo) {
2077
+ // GIF path (working): _quickExportVideo forwards gifQuality to
2078
+ // gif.js. Also serves as the fallback if videoExporter is absent.
2079
+ const videoResult = await app.exportEngine._quickExportVideo(format, baseVideoSettings, false);
1881
2080
  if (videoResult && videoResult.blob) {
1882
- const reader = new FileReader();
1883
- const dataUrl = await new Promise(resolve => {
1884
- reader.onloadend = () => resolve(reader.result);
1885
- reader.readAsDataURL(videoResult.blob);
1886
- });
1887
- result = { success: true, platform, format, data: dataUrl, mimeType: videoMimeType, size: videoResult.blob.size };
2081
+ const dataUrl = await blobToDataUrl(videoResult.blob);
2082
+ result = { success: true, platform, format, framing, data: dataUrl, mimeType: videoMimeType, size: videoResult.blob.size };
1888
2083
  } else {
1889
2084
  result = { success: false, error: format.toUpperCase() + ' export returned no data' };
1890
2085
  }
1891
2086
  } else if (app.exportEngine && app.exportEngine.videoExporter) {
1892
- // Fallback: call VideoExporter.export directly
1893
- const blob = await app.exportEngine.videoExporter.export({ format, fps: settings.fps, duration: 5 });
1894
- const reader = new FileReader();
1895
- const dataUrl = await new Promise(resolve => {
1896
- reader.onloadend = () => resolve(reader.result);
1897
- reader.readAsDataURL(blob);
1898
- });
1899
- result = { success: true, platform, format, data: dataUrl, mimeType: videoMimeType, size: blob.size };
2087
+ const blob = await app.exportEngine.videoExporter.export(baseVideoSettings);
2088
+ const dataUrl = await blobToDataUrl(blob);
2089
+ result = { success: true, platform, format, framing, data: dataUrl, mimeType: videoMimeType, size: blob.size };
1900
2090
  } else {
1901
2091
  result = { success: false, error: format.toUpperCase() + ' export not available' };
1902
2092
  }
@@ -2339,7 +2529,7 @@ app.historyManager.saveState();
2339
2529
  if (gradientDirection)
2340
2530
  options.gradientDirection = gradientDirection;
2341
2531
  return `
2342
- // Create letter collage: "${text}"
2532
+ // Create letter collage
2343
2533
  (function() {
2344
2534
  const text = ${JSON.stringify(text)};
2345
2535
  const options = ${JSON.stringify(options, null, 2)};
@@ -2354,7 +2544,21 @@ app.historyManager.saveState();
2354
2544
  app.historyManager.saveState('create_letter_collage');
2355
2545
  }
2356
2546
 
2357
- return result;
2547
+ // Return a SERIALIZABLE summary — never the raw result. result.group and
2548
+ // result.letters are live Paper.js objects (circular parent/children refs,
2549
+ // methods, canvas handles); the browser round-trip (page.evaluate / CDP
2550
+ // return-by-value) can't serialize them, so returning result throws
2551
+ // "Failed to execute code in browser" even though the collage was already
2552
+ // created — a false negative. Surface registry ids instead.
2553
+ const collageId = (result && (result.collageId
2554
+ || (result.group && result.group.data && (result.group.data.id || result.group.data.registryId)))) || null;
2555
+ const letterIds = (result && Array.isArray(result.letters))
2556
+ ? result.letters.map(function(l){ return (l && l.data && (l.data.id || l.data.registryId)) || null; }).filter(Boolean)
2557
+ : [];
2558
+ if (!collageId) {
2559
+ return { success: false, error: 'createLetterCollage returned no id', letterCount: letterIds.length, letterIds: letterIds };
2560
+ }
2561
+ return { success: true, collageId: collageId, itemId: collageId, letterCount: letterIds.length, letterIds: letterIds };
2358
2562
  })();
2359
2563
  `.trim();
2360
2564
  }
@@ -3465,6 +3669,1219 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
3465
3669
  })();
3466
3670
  `.trim();
3467
3671
  }
3672
+ generateManageScenes(input) {
3673
+ const guard = `if (!app.sceneManager) return { error: 'SceneManager not available' };`;
3674
+ switch (input.action) {
3675
+ case 'save':
3676
+ return `
3677
+ // Save current canvas as scene
3678
+ (async function() {
3679
+ ${guard}
3680
+ try {
3681
+ const result = await app.sceneManager.saveCurrentAsScene(${JSON.stringify(input.name || 'Untitled Scene')});
3682
+ return { success: true, action: 'save', sceneId: result?.id || result, name: ${JSON.stringify(input.name || 'Untitled Scene')} };
3683
+ } catch (e) {
3684
+ return { error: 'Failed to save scene: ' + e.message };
3685
+ }
3686
+ })();
3687
+ `.trim();
3688
+ case 'load':
3689
+ return `
3690
+ // Load scene
3691
+ (async function() {
3692
+ ${guard}
3693
+ try {
3694
+ await app.sceneManager.loadScene(${JSON.stringify(input.sceneId)}${input.transition ? `, { transition: ${JSON.stringify(input.transition)} }` : ''});
3695
+ return { success: true, action: 'load', sceneId: ${JSON.stringify(input.sceneId)}${input.transition ? `, transition: ${JSON.stringify(input.transition)}` : ''} };
3696
+ } catch (e) {
3697
+ return { error: 'Failed to load scene: ' + e.message };
3698
+ }
3699
+ })();
3700
+ `.trim();
3701
+ case 'list':
3702
+ return `
3703
+ // List all scenes
3704
+ (function() {
3705
+ ${guard}
3706
+ const scenes = app.sceneManager.listScenes();
3707
+ return { success: true, action: 'list', scenes: scenes, count: scenes.length };
3708
+ })();
3709
+ `.trim();
3710
+ case 'delete':
3711
+ return `
3712
+ // Delete scene
3713
+ (function() {
3714
+ ${guard}
3715
+ app.sceneManager.deleteScene(${JSON.stringify(input.sceneId)});
3716
+ return { success: true, action: 'delete', sceneId: ${JSON.stringify(input.sceneId)} };
3717
+ })();
3718
+ `.trim();
3719
+ case 'rename':
3720
+ return `
3721
+ // Rename scene
3722
+ (function() {
3723
+ ${guard}
3724
+ app.sceneManager.renameScene(${JSON.stringify(input.sceneId)}, ${JSON.stringify(input.name)});
3725
+ return { success: true, action: 'rename', sceneId: ${JSON.stringify(input.sceneId)}, name: ${JSON.stringify(input.name)} };
3726
+ })();
3727
+ `.trim();
3728
+ case 'duplicate':
3729
+ return `
3730
+ // Duplicate scene
3731
+ (function() {
3732
+ ${guard}
3733
+ const result = app.sceneManager.duplicateScene(${JSON.stringify(input.sceneId)});
3734
+ return { success: true, action: 'duplicate', originalId: ${JSON.stringify(input.sceneId)}, newScene: result };
3735
+ })();
3736
+ `.trim();
3737
+ case 'reorder':
3738
+ return `
3739
+ // Reorder scenes
3740
+ (function() {
3741
+ ${guard}
3742
+ app.sceneManager.reorderScenes(${JSON.stringify(input.sceneIds)});
3743
+ return { success: true, action: 'reorder', order: ${JSON.stringify(input.sceneIds)} };
3744
+ })();
3745
+ `.trim();
3746
+ case 'info':
3747
+ return `
3748
+ // Get scene info
3749
+ (function() {
3750
+ ${guard}
3751
+ const scene = app.sceneManager.getScene(${JSON.stringify(input.sceneId)});
3752
+ if (!scene) return { error: 'Scene not found: ${input.sceneId}' };
3753
+ return { success: true, action: 'info', scene: scene };
3754
+ })();
3755
+ `.trim();
3756
+ case 'export':
3757
+ return `
3758
+ // Export all scenes
3759
+ (function() {
3760
+ ${guard}
3761
+ const data = app.sceneManager.exportScenes();
3762
+ return { success: true, action: 'export', data: data };
3763
+ })();
3764
+ `.trim();
3765
+ case 'import':
3766
+ return `
3767
+ // Import scenes
3768
+ (function() {
3769
+ ${guard}
3770
+ try {
3771
+ const json = ${JSON.stringify(input.scenesJson)};
3772
+ app.sceneManager.importScenes(json${input.merge !== undefined ? `, { merge: ${input.merge} }` : ''});
3773
+ return { success: true, action: 'import'${input.merge !== undefined ? `, merge: ${input.merge}` : ''} };
3774
+ } catch (e) {
3775
+ return { error: 'Failed to import scenes: ' + e.message };
3776
+ }
3777
+ })();
3778
+ `.trim();
3779
+ default:
3780
+ return `(function() { return { error: 'Unknown manage_scenes action: ${input.action}' }; })();`;
3781
+ }
3782
+ }
3783
+ generateScenePlayback(input) {
3784
+ const guard = `if (!app.sceneManager) return { error: 'SceneManager not available' };`;
3785
+ switch (input.action) {
3786
+ case 'create_chain': {
3787
+ const opts = [];
3788
+ if (input.loop !== undefined)
3789
+ opts.push(`loop: ${input.loop}`);
3790
+ if (input.autoPlay !== undefined)
3791
+ opts.push(`autoPlay: ${input.autoPlay}`);
3792
+ if (input.defaultDuration !== undefined)
3793
+ opts.push(`defaultDuration: ${input.defaultDuration * 1000}`);
3794
+ if (input.defaultTransition !== undefined)
3795
+ opts.push(`defaultTransition: ${JSON.stringify(input.defaultTransition)}`);
3796
+ if (input.transitionDuration !== undefined)
3797
+ opts.push(`transitionDuration: ${input.transitionDuration * 1000}`);
3798
+ const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
3799
+ return `
3800
+ // Create scene chain for sequential playback
3801
+ (function() {
3802
+ ${guard}
3803
+ app.sceneManager.createChain(${JSON.stringify(input.sceneIds)}${optsStr});
3804
+ return { success: true, action: 'create_chain', sceneIds: ${JSON.stringify(input.sceneIds)}${input.loop !== undefined ? `, loop: ${input.loop}` : ''}${input.defaultDuration !== undefined ? `, defaultDuration: ${input.defaultDuration}` : ''} };
3805
+ })();
3806
+ `.trim();
3807
+ }
3808
+ case 'play':
3809
+ return `
3810
+ // Play scene chain
3811
+ (async function() {
3812
+ ${guard}
3813
+ try {
3814
+ await app.sceneManager.playChain();
3815
+ return { success: true, action: 'play' };
3816
+ } catch (e) {
3817
+ return { error: 'Failed to play chain: ' + e.message };
3818
+ }
3819
+ })();
3820
+ `.trim();
3821
+ case 'pause':
3822
+ return `
3823
+ // Pause scene chain
3824
+ (function() {
3825
+ ${guard}
3826
+ app.sceneManager.pauseChain();
3827
+ return { success: true, action: 'pause' };
3828
+ })();
3829
+ `.trim();
3830
+ case 'resume':
3831
+ return `
3832
+ // Resume scene chain
3833
+ (function() {
3834
+ ${guard}
3835
+ app.sceneManager.resumeChain();
3836
+ return { success: true, action: 'resume' };
3837
+ })();
3838
+ `.trim();
3839
+ case 'stop':
3840
+ return `
3841
+ // Stop scene chain
3842
+ (function() {
3843
+ ${guard}
3844
+ app.sceneManager.stopChain();
3845
+ return { success: true, action: 'stop' };
3846
+ })();
3847
+ `.trim();
3848
+ case 'toggle_loop':
3849
+ return `
3850
+ // Toggle loop mode
3851
+ (function() {
3852
+ ${guard}
3853
+ app.sceneManager.setLoop(${input.enabled ?? true});
3854
+ return { success: true, action: 'toggle_loop', enabled: ${input.enabled ?? true} };
3855
+ })();
3856
+ `.trim();
3857
+ case 'jump':
3858
+ return `
3859
+ // Jump to scene index
3860
+ (async function() {
3861
+ ${guard}
3862
+ try {
3863
+ await app.sceneManager.jumpToChainIndex(${input.index ?? 0});
3864
+ return { success: true, action: 'jump', index: ${input.index ?? 0} };
3865
+ } catch (e) {
3866
+ return { error: 'Failed to jump to index: ' + e.message };
3867
+ }
3868
+ })();
3869
+ `.trim();
3870
+ default:
3871
+ return `(function() { return { error: 'Unknown scene_playback action: ${input.action}' }; })();`;
3872
+ }
3873
+ }
3874
+ // ===========================================================================
3875
+ // SELECTION, TRANSFORM & HISTORY
3876
+ // ===========================================================================
3877
+ generateSelection(input) {
3878
+ switch (input.action) {
3879
+ case 'select': {
3880
+ const ids = JSON.stringify(input.itemIds || []);
3881
+ const mode = input.mode || 'replace';
3882
+ return `
3883
+ // Select items
3884
+ (function() {
3885
+ const ids = ${ids};
3886
+ if (ids.length === 0) return { error: 'No itemIds provided' };
3887
+ ${mode === 'replace' ? 'app.deselectAll();' : ''}
3888
+ const selected = [];
3889
+ for (const id of ids) {
3890
+ const entry = app.itemRegistry.get(id);
3891
+ if (entry && entry.item) {
3892
+ ${mode === 'remove' ? 'entry.item.selected = false;' : 'app.select(entry.item);'}
3893
+ selected.push(id);
3894
+ }
3895
+ }
3896
+ return { success: true, action: 'select', mode: '${mode}', selected };
3897
+ })();`.trim();
3898
+ }
3899
+ case 'select_all':
3900
+ return `
3901
+ // Select all items
3902
+ (function() {
3903
+ app.selectAll();
3904
+ const items = app.getSelection().map(i => i.data?.itemId || i.name || i.id);
3905
+ return { success: true, action: 'select_all', count: items.length, items };
3906
+ })();`.trim();
3907
+ case 'deselect_all':
3908
+ return `
3909
+ // Deselect all items
3910
+ (function() {
3911
+ app.deselectAll();
3912
+ return { success: true, action: 'deselect_all' };
3913
+ })();`.trim();
3914
+ case 'get':
3915
+ return `
3916
+ // Get current selection
3917
+ (function() {
3918
+ const items = app.getSelection().map(i => ({
3919
+ itemId: i.data?.itemId || i.name || i.id,
3920
+ type: i.data?.itemType || i.className,
3921
+ bounds: i.bounds ? { x: i.bounds.x, y: i.bounds.y, width: i.bounds.width, height: i.bounds.height } : null
3922
+ }));
3923
+ return { success: true, action: 'get', count: items.length, items };
3924
+ })();`.trim();
3925
+ case 'delete_selected':
3926
+ return `
3927
+ // Delete selected items
3928
+ (function() {
3929
+ const selection = app.getSelection();
3930
+ const deleted = selection.map(i => i.data?.itemId || i.name || i.id);
3931
+ app.deleteSelected();
3932
+ return { success: true, action: 'delete_selected', deleted, count: deleted.length };
3933
+ })();`.trim();
3934
+ default:
3935
+ return `(function() { return { error: 'Unknown selection action: ${input.action}' }; })();`;
3936
+ }
3937
+ }
3938
+ generateTransform(input) {
3939
+ switch (input.action) {
3940
+ case 'nudge': {
3941
+ const dx = input.dx ?? 0;
3942
+ const dy = input.dy ?? 0;
3943
+ return `
3944
+ // Nudge item position
3945
+ (function() {
3946
+ const entry = app.itemRegistry.get(${JSON.stringify(input.itemId || '')});
3947
+ if (!entry || !entry.item) return { error: 'Item not found: ${input.itemId}' };
3948
+ entry.item.position.x += ${dx};
3949
+ entry.item.position.y += ${dy};
3950
+ return { success: true, action: 'nudge', itemId: '${input.itemId}', dx: ${dx}, dy: ${dy}, position: { x: entry.item.position.x, y: entry.item.position.y } };
3951
+ })();`.trim();
3952
+ }
3953
+ case 'flip': {
3954
+ const dir = input.direction || 'horizontal';
3955
+ return `
3956
+ // Flip item ${dir}ly
3957
+ (function() {
3958
+ const entry = app.itemRegistry.get(${JSON.stringify(input.itemId || '')});
3959
+ if (!entry || !entry.item) return { error: 'Item not found: ${input.itemId}' };
3960
+ ${dir === 'horizontal' ? 'entry.item.scale(-1, 1);' : 'entry.item.scale(1, -1);'}
3961
+ return { success: true, action: 'flip', itemId: '${input.itemId}', direction: '${dir}' };
3962
+ })();`.trim();
3963
+ }
3964
+ case 'reorder': {
3965
+ const order = input.order || 'bringToFront';
3966
+ const methodMap = {
3967
+ bringToFront: 'bringToFront',
3968
+ sendToBack: 'sendToBack',
3969
+ moveUp: 'moveAbove',
3970
+ moveDown: 'moveBelow',
3971
+ };
3972
+ const method = methodMap[order] || 'bringToFront';
3973
+ const needsSibling = order === 'moveUp' || order === 'moveDown';
3974
+ if (needsSibling) {
3975
+ const siblingExpr = order === 'moveUp' ? 'entry.item.nextSibling' : 'entry.item.previousSibling';
3976
+ return `
3977
+ // Reorder item: ${order}
3978
+ (function() {
3979
+ const entry = app.itemRegistry.get(${JSON.stringify(input.itemId || '')});
3980
+ if (!entry || !entry.item) return { error: 'Item not found: ${input.itemId}' };
3981
+ const sibling = ${siblingExpr};
3982
+ if (sibling) entry.item.${method}(sibling);
3983
+ return { success: true, action: 'reorder', itemId: '${input.itemId}', order: '${order}' };
3984
+ })();`.trim();
3985
+ }
3986
+ return `
3987
+ // Reorder item: ${order}
3988
+ (function() {
3989
+ const entry = app.itemRegistry.get(${JSON.stringify(input.itemId || '')});
3990
+ if (!entry || !entry.item) return { error: 'Item not found: ${input.itemId}' };
3991
+ entry.item.${method}();
3992
+ return { success: true, action: 'reorder', itemId: '${input.itemId}', order: '${order}' };
3993
+ })();`.trim();
3994
+ }
3995
+ default:
3996
+ return `(function() { return { error: 'Unknown transform action: ${input.action}' }; })();`;
3997
+ }
3998
+ }
3999
+ generateHistory(input) {
4000
+ const guard = `if (!app.historyManager) return { error: 'HistoryManager not available' };`;
4001
+ switch (input.action) {
4002
+ case 'undo':
4003
+ return `
4004
+ // Undo last action
4005
+ (function() {
4006
+ ${guard}
4007
+ app.historyManager.undo();
4008
+ return { success: true, action: 'undo' };
4009
+ })();`.trim();
4010
+ case 'redo':
4011
+ return `
4012
+ // Redo last undone action
4013
+ (function() {
4014
+ ${guard}
4015
+ app.historyManager.redo();
4016
+ return { success: true, action: 'redo' };
4017
+ })();`.trim();
4018
+ case 'get_state':
4019
+ return `
4020
+ // Get history state
4021
+ (function() {
4022
+ ${guard}
4023
+ const state = app.historyManager.getState();
4024
+ return { success: true, action: 'get_state', ...state };
4025
+ })();`.trim();
4026
+ default:
4027
+ return `(function() { return { error: 'Unknown history action: ${input.action}' }; })();`;
4028
+ }
4029
+ }
4030
+ // ===========================================================================
4031
+ // IMAGE PROCESSING
4032
+ // ===========================================================================
4033
+ generateImageFilter(input) {
4034
+ const guard = `if (!app.imageTools) return { error: 'ImageTools not available' };`;
4035
+ switch (input.action) {
4036
+ case 'apply':
4037
+ return `
4038
+ // Apply image filter
4039
+ (async function() {
4040
+ ${guard}
4041
+ try {
4042
+ const result = await app.imageTools.applyFilter(${JSON.stringify(input.itemId)}, ${JSON.stringify(input.filterName || '')}, ${JSON.stringify(input.params || {})});
4043
+ return { success: true, action: 'apply', itemId: ${JSON.stringify(input.itemId)}, filter: ${JSON.stringify(input.filterName || '')} };
4044
+ } catch (e) {
4045
+ return { error: 'Failed to apply filter: ' + e.message };
4046
+ }
4047
+ })();`.trim();
4048
+ case 'chain':
4049
+ return `
4050
+ // Apply filter chain
4051
+ (async function() {
4052
+ ${guard}
4053
+ try {
4054
+ const filters = ${JSON.stringify(input.filters || [])};
4055
+ const result = await app.imageTools.applyFilterChain(${JSON.stringify(input.itemId)}, filters);
4056
+ return { success: true, action: 'chain', itemId: ${JSON.stringify(input.itemId)}, filterCount: filters.length };
4057
+ } catch (e) {
4058
+ return { error: 'Failed to apply filter chain: ' + e.message };
4059
+ }
4060
+ })();`.trim();
4061
+ default:
4062
+ return `(function() { return { error: 'Unknown image_filter action: ${input.action}' }; })();`;
4063
+ }
4064
+ }
4065
+ generateLasso(input) {
4066
+ const guard = `if (!app.imageTools) return { error: 'ImageTools not available' };`;
4067
+ switch (input.action) {
4068
+ case 'activate':
4069
+ return `
4070
+ // Activate lasso selection
4071
+ (async function() {
4072
+ ${guard}
4073
+ try {
4074
+ await app.imageTools.activateLasso(${JSON.stringify(input.itemId || '')});
4075
+ return { success: true, action: 'activate', itemId: ${JSON.stringify(input.itemId || '')} };
4076
+ } catch (e) {
4077
+ return { error: 'Failed to activate lasso: ' + e.message };
4078
+ }
4079
+ })();`.trim();
4080
+ case 'apply':
4081
+ return `
4082
+ // Apply lasso selection
4083
+ (async function() {
4084
+ ${guard}
4085
+ try {
4086
+ const result = await app.imageTools.applyLasso();
4087
+ return { success: true, action: 'apply' };
4088
+ } catch (e) {
4089
+ return { error: 'Failed to apply lasso: ' + e.message };
4090
+ }
4091
+ })();`.trim();
4092
+ default:
4093
+ return `(function() { return { error: 'Unknown lasso action: ${input.action}' }; })();`;
4094
+ }
4095
+ }
4096
+ generateCutoutStyle(input) {
4097
+ const guard = `if (!app.imageTools) return { error: 'ImageTools not available' };`;
4098
+ switch (input.action) {
4099
+ case 'apply':
4100
+ return `
4101
+ // Apply cutout style
4102
+ (async function() {
4103
+ ${guard}
4104
+ try {
4105
+ const result = await app.imageTools.applyCutoutStyle(${JSON.stringify(input.itemId || '')}, ${JSON.stringify(input.preset || '')}, ${JSON.stringify(input.options || {})});
4106
+ return { success: true, action: 'apply', itemId: ${JSON.stringify(input.itemId || '')}, preset: ${JSON.stringify(input.preset || '')} };
4107
+ } catch (e) {
4108
+ return { error: 'Failed to apply cutout style: ' + e.message };
4109
+ }
4110
+ })();`.trim();
4111
+ case 'list':
4112
+ return `
4113
+ // List available cutout styles
4114
+ (function() {
4115
+ ${guard}
4116
+ const styles = app.imageTools.getCutoutStyles();
4117
+ return { success: true, action: 'list', styles };
4118
+ })();`.trim();
4119
+ default:
4120
+ return `(function() { return { error: 'Unknown cutout_style action: ${input.action}' }; })();`;
4121
+ }
4122
+ }
4123
+ // ===========================================================================
4124
+ // COMPOSITION & VIEW
4125
+ // ===========================================================================
4126
+ generatePrecomp(input) {
4127
+ switch (input.action) {
4128
+ case 'create': {
4129
+ const opts = {};
4130
+ if (input.name)
4131
+ opts.name = input.name;
4132
+ if (input.loop !== undefined)
4133
+ opts.loop = input.loop;
4134
+ if (input.duration !== undefined)
4135
+ opts.duration = input.duration;
4136
+ return `
4137
+ // Create precomp from items
4138
+ (function() {
4139
+ const itemIds = ${JSON.stringify(input.itemIds || [])};
4140
+ const opts = ${JSON.stringify(opts)};
4141
+ const precomp = app.createPrecomp(itemIds, opts);
4142
+ return { success: true, action: 'create', precompId: precomp?.id || precomp, name: ${JSON.stringify(input.name || '')} };
4143
+ })();`.trim();
4144
+ }
4145
+ case 'add':
4146
+ return `
4147
+ // Add item to precomp
4148
+ (function() {
4149
+ app.addToPrecomp(${JSON.stringify(input.precompId || '')}, ${JSON.stringify(input.itemId || '')});
4150
+ return { success: true, action: 'add', precompId: ${JSON.stringify(input.precompId || '')}, itemId: ${JSON.stringify(input.itemId || '')} };
4151
+ })();`.trim();
4152
+ case 'remove':
4153
+ return `
4154
+ // Remove item from precomp
4155
+ (function() {
4156
+ app.removeFromPrecomp(${JSON.stringify(input.precompId || '')}, ${JSON.stringify(input.itemId || '')});
4157
+ return { success: true, action: 'remove', precompId: ${JSON.stringify(input.precompId || '')}, itemId: ${JSON.stringify(input.itemId || '')} };
4158
+ })();`.trim();
4159
+ default:
4160
+ return `(function() { return { error: 'Unknown precomp action: ${input.action}' }; })();`;
4161
+ }
4162
+ }
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
+ generateBackground(input) {
4187
+ switch (input.action) {
4188
+ case 'set': {
4189
+ const mode = input.mode || 'color';
4190
+ if (mode === 'generator') {
4191
+ return `
4192
+ // Set background via generator
4193
+ (async function() {
4194
+ try {
4195
+ await app.executeGenerator(${JSON.stringify(input.generator || '')}, ${JSON.stringify(input.generatorParams || {})});
4196
+ return { success: true, action: 'set', mode: 'generator', generator: ${JSON.stringify(input.generator || '')} };
4197
+ } catch (e) {
4198
+ return { error: 'Failed to set generator background: ' + e.message };
4199
+ }
4200
+ })();`.trim();
4201
+ }
4202
+ if (mode === 'pattern') {
4203
+ return `
4204
+ // Set background pattern
4205
+ (function() {
4206
+ app.setBackgroundPattern(${JSON.stringify(input.pattern || '')});
4207
+ return { success: true, action: 'set', mode: 'pattern', pattern: ${JSON.stringify(input.pattern || '')} };
4208
+ })();`.trim();
4209
+ }
4210
+ // default: color
4211
+ return `
4212
+ // Set background color
4213
+ (function() {
4214
+ app.setBackgroundColor(${JSON.stringify(input.color || '#ffffff')});
4215
+ return { success: true, action: 'set', mode: 'color', color: ${JSON.stringify(input.color || '#ffffff')} };
4216
+ })();`.trim();
4217
+ }
4218
+ case 'clear':
4219
+ return `
4220
+ // Clear background
4221
+ (function() {
4222
+ app.clearBackground();
4223
+ return { success: true, action: 'clear' };
4224
+ })();`.trim();
4225
+ case 'get':
4226
+ return `
4227
+ // Get background info
4228
+ (function() {
4229
+ const bg = app.getBackground();
4230
+ return { success: true, action: 'get', ...bg };
4231
+ })();`.trim();
4232
+ default:
4233
+ return `(function() { return { error: 'Unknown background action: ${input.action}' }; })();`;
4234
+ }
4235
+ }
4236
+ // ===========================================================================
4237
+ // CANVAS QUERIES
4238
+ // ===========================================================================
4239
+ generateQuery(input) {
4240
+ switch (input.action) {
4241
+ case 'get_by_id':
4242
+ return `
4243
+ // Get item details by ID
4244
+ (function() {
4245
+ const entry = app.itemRegistry.get(${JSON.stringify(input.itemId || '')});
4246
+ if (!entry || !entry.item) return { error: 'Item not found: ${input.itemId}' };
4247
+ const item = entry.item;
4248
+ return {
4249
+ success: true, action: 'get_by_id',
4250
+ itemId: ${JSON.stringify(input.itemId || '')},
4251
+ type: item.data?.itemType || item.className,
4252
+ position: { x: item.position.x, y: item.position.y },
4253
+ bounds: item.bounds ? { x: item.bounds.x, y: item.bounds.y, width: item.bounds.width, height: item.bounds.height } : null,
4254
+ visible: item.visible,
4255
+ opacity: item.opacity,
4256
+ rotation: item.rotation,
4257
+ selected: item.selected,
4258
+ };
4259
+ })();`.trim();
4260
+ case 'hit_test': {
4261
+ const tolerance = input.tolerance ?? 5;
4262
+ const all = input.all ?? false;
4263
+ return `
4264
+ // Hit test at point
4265
+ (function() {
4266
+ const point = new paper.Point(${input.x ?? 0}, ${input.y ?? 0});
4267
+ ${all
4268
+ ? `const results = app.hitTestAll(point, { tolerance: ${tolerance} });
4269
+ const hits = results.map(r => ({
4270
+ itemId: r.item?.data?.itemId || r.item?.name || r.item?.id,
4271
+ type: r.item?.data?.itemType || r.item?.className,
4272
+ point: { x: r.point?.x, y: r.point?.y }
4273
+ }));
4274
+ return { success: true, action: 'hit_test', x: ${input.x ?? 0}, y: ${input.y ?? 0}, hits, count: hits.length };`
4275
+ : `const result = app.hitTest(point, { tolerance: ${tolerance} });
4276
+ if (!result) return { success: true, action: 'hit_test', x: ${input.x ?? 0}, y: ${input.y ?? 0}, hit: null };
4277
+ return {
4278
+ success: true, action: 'hit_test',
4279
+ x: ${input.x ?? 0}, y: ${input.y ?? 0},
4280
+ hit: { itemId: result.item?.data?.itemId || result.item?.name || result.item?.id, type: result.item?.data?.itemType || result.item?.className }
4281
+ };`}
4282
+ })();`.trim();
4283
+ }
4284
+ case 'is_empty':
4285
+ return `
4286
+ // Check if canvas is empty
4287
+ (function() {
4288
+ const items = app.itemRegistry.getAll();
4289
+ return { success: true, action: 'is_empty', empty: items.length === 0, count: items.length };
4290
+ })();`.trim();
4291
+ default:
4292
+ return `(function() { return { error: 'Unknown query action: ${input.action}' }; })();`;
4293
+ }
4294
+ }
4295
+ // ===========================================================================
4296
+ // DEFORMATION
4297
+ // ===========================================================================
4298
+ generateDeform(input) {
4299
+ const guard = `if (!app.deformPresets) return { error: 'DeformPresets not available' };`;
4300
+ switch (input.action) {
4301
+ case 'apply': {
4302
+ if (!input.itemId)
4303
+ return `(function() { return { error: 'itemId is required for deform apply' }; })();`;
4304
+ if (!input.preset)
4305
+ return `(function() { return { error: 'preset is required for deform apply' }; })();`;
4306
+ const params = {};
4307
+ if (input.frequency !== undefined)
4308
+ params.frequency = input.frequency;
4309
+ if (input.amplitude !== undefined)
4310
+ params.amplitude = input.amplitude;
4311
+ if (input.phase)
4312
+ params.phase = input.phase;
4313
+ if (input.loop !== undefined)
4314
+ params.loop = input.loop;
4315
+ if (input.axis)
4316
+ params.axis = input.axis;
4317
+ if (input.turns !== undefined)
4318
+ params.turns = input.turns;
4319
+ if (input.waves !== undefined)
4320
+ params.waves = input.waves;
4321
+ if (input.maxDisplacement !== undefined)
4322
+ params.maxDisplacement = input.maxDisplacement;
4323
+ if (input.speed !== undefined)
4324
+ params.speed = input.speed;
4325
+ if (input.steps !== undefined)
4326
+ params.steps = input.steps;
4327
+ const itemIdStr = JSON.stringify(input.itemId);
4328
+ return `
4329
+ // Apply deformation preset
4330
+ (function() {
4331
+ ${guard}
4332
+ const entry = app.itemRegistry.get(${itemIdStr});
4333
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4334
+ app.deformPresets.apply(entry.item, '${input.preset}', ${JSON.stringify(params)});
4335
+ return { success: true, action: 'apply', itemId: ${itemIdStr}, preset: '${input.preset}' };
4336
+ })();`.trim();
4337
+ }
4338
+ case 'trigger': {
4339
+ if (!input.itemId)
4340
+ return `(function() { return { error: 'itemId is required for deform trigger' }; })();`;
4341
+ if (!input.preset)
4342
+ return `(function() { return { error: 'preset is required for deform trigger' }; })();`;
4343
+ const params = {};
4344
+ if (input.amplitude !== undefined)
4345
+ params.amplitude = input.amplitude;
4346
+ if (input.speed !== undefined)
4347
+ params.speed = input.speed;
4348
+ const itemIdStr = JSON.stringify(input.itemId);
4349
+ return `
4350
+ // Trigger one-shot deformation
4351
+ (function() {
4352
+ ${guard}
4353
+ const entry = app.itemRegistry.get(${itemIdStr});
4354
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4355
+ app.deformPresets.trigger(entry.item, '${input.preset}', ${JSON.stringify(params)});
4356
+ return { success: true, action: 'trigger', itemId: ${itemIdStr}, preset: '${input.preset}' };
4357
+ })();`.trim();
4358
+ }
4359
+ case 'remove': {
4360
+ if (!input.itemId)
4361
+ return `(function() { return { error: 'itemId is required for deform remove' }; })();`;
4362
+ const itemIdStr = JSON.stringify(input.itemId);
4363
+ return `
4364
+ // Remove deformation and restore geometry
4365
+ (function() {
4366
+ ${guard}
4367
+ const entry = app.itemRegistry.get(${itemIdStr});
4368
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4369
+ app.deformPresets.remove(entry.item);
4370
+ return { success: true, action: 'remove', itemId: ${itemIdStr} };
4371
+ })();`.trim();
4372
+ }
4373
+ default:
4374
+ return `(function() { return { error: 'Unknown deform action: ${input.action}' }; })();`;
4375
+ }
4376
+ }
4377
+ // ===========================================================================
4378
+ // SPRITE SHEETS
4379
+ // ===========================================================================
4380
+ generateSpriteSheet(input) {
4381
+ const guard = `if (!app.spriteSheetSystem) return { error: 'SpriteSheetSystem not available' };`;
4382
+ switch (input.action) {
4383
+ case 'generate': {
4384
+ const opts = {};
4385
+ if (input.poses)
4386
+ opts.poses = input.poses;
4387
+ if (input.transition)
4388
+ opts.transition = input.transition;
4389
+ if (input.bakedAnimation)
4390
+ opts.bakedAnimation = input.bakedAnimation;
4391
+ if (input.animations)
4392
+ opts.animations = input.animations;
4393
+ if (input.padding !== undefined)
4394
+ opts.padding = input.padding;
4395
+ if (input.name)
4396
+ opts.name = input.name;
4397
+ return `
4398
+ // Generate sprite sheet from skeleton
4399
+ (async function() {
4400
+ ${guard}
4401
+ const sheet = await app.spriteSheetSystem.generateSpriteSheet(${JSON.stringify(input.skeletonId || '')}, ${JSON.stringify(opts)});
4402
+ 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 };
4403
+ })();`.trim();
4404
+ }
4405
+ case 'play': {
4406
+ const opts = {};
4407
+ if (input.x !== undefined)
4408
+ opts.x = input.x;
4409
+ if (input.y !== undefined)
4410
+ opts.y = input.y;
4411
+ if (input.animation)
4412
+ opts.animation = input.animation;
4413
+ if (input.fps !== undefined)
4414
+ opts.fps = input.fps;
4415
+ if (input.scale !== undefined)
4416
+ opts.scale = input.scale;
4417
+ return `
4418
+ // Play sprite sheet animation
4419
+ (async function() {
4420
+ ${guard}
4421
+ const player = await app.spriteSheetSystem.playSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, ${JSON.stringify(opts)});
4422
+ return { success: true, action: 'play', playerId: player.id };
4423
+ })();`.trim();
4424
+ }
4425
+ case 'export':
4426
+ return `
4427
+ // Export sprite sheet
4428
+ (async function() {
4429
+ ${guard}
4430
+ const result = await app.spriteSheetSystem.exportSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, { format: '${input.format || 'png'}', download: true, includeMetadata: true });
4431
+ return { success: true, action: 'export', format: '${input.format || 'png'}' };
4432
+ })();`.trim();
4433
+ default:
4434
+ return `(function() { return { error: 'Unknown sprite sheet action: ${input.action}' }; })();`;
4435
+ }
4436
+ }
4437
+ // ===========================================================================
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
+ // INTERACTION
4494
+ // ===========================================================================
4495
+ generateInteraction(input) {
4496
+ const guard = `if (!app.interactionSystem) return { error: 'InteractionSystem not available' };`;
4497
+ switch (input.action) {
4498
+ case 'add_behavior': {
4499
+ const itemIdStr = JSON.stringify(input.itemId || '');
4500
+ const params = JSON.stringify(input.params || {});
4501
+ return `
4502
+ // Add behavior to item
4503
+ (function() {
4504
+ ${guard}
4505
+ const entry = app.itemRegistry.get(${itemIdStr});
4506
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4507
+ const behaviorId = app.interactionSystem.addContinuousBehavior(entry.item, '${input.behaviorType}', ${params});
4508
+ return { success: true, action: 'add_behavior', itemId: ${itemIdStr}, behaviorType: '${input.behaviorType}', behaviorId };
4509
+ })();`.trim();
4510
+ }
4511
+ case 'remove_behavior': {
4512
+ const itemIdStr = JSON.stringify(input.itemId || '');
4513
+ const behaviorIdStr = JSON.stringify(input.behaviorId || '');
4514
+ return `
4515
+ // Remove behavior from item
4516
+ (function() {
4517
+ ${guard}
4518
+ const entry = app.itemRegistry.get(${itemIdStr});
4519
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4520
+ app.interactionSystem.removeContinuousBehavior(entry.item, ${behaviorIdStr});
4521
+ return { success: true, action: 'remove_behavior', itemId: ${itemIdStr}, behaviorId: ${behaviorIdStr} };
4522
+ })();`.trim();
4523
+ }
4524
+ case 'trigger_action': {
4525
+ const params = JSON.stringify(input.params || {});
4526
+ return `
4527
+ // Trigger interaction action
4528
+ (function() {
4529
+ ${guard}
4530
+ app.interactionSystem.triggerAction('${input.actionType}', ${params});
4531
+ return { success: true, action: 'trigger_action', actionType: '${input.actionType}' };
4532
+ })();`.trim();
4533
+ }
4534
+ case 'get_state':
4535
+ return `
4536
+ // Get interaction state
4537
+ (function() {
4538
+ ${guard}
4539
+ const state = app.interactionSystem.getState ? app.interactionSystem.getState() : {};
4540
+ return { success: true, action: 'get_state', ...state };
4541
+ })();`.trim();
4542
+ default:
4543
+ return `(function() { return { error: 'Unknown interaction action: ${input.action}' }; })();`;
4544
+ }
4545
+ }
4546
+ // ===========================================================================
4547
+ // WIDGET EXPORT
4548
+ // ===========================================================================
4549
+ generateExportWidget(input) {
4550
+ const opts = {};
4551
+ if (input.download !== undefined)
4552
+ opts.download = input.download;
4553
+ if (input.filename)
4554
+ opts.filename = input.filename;
4555
+ if (input.includeInteractions !== undefined)
4556
+ opts.includeInteractions = input.includeInteractions;
4557
+ if (input.minify !== undefined)
4558
+ opts.minify = input.minify;
4559
+ return `
4560
+ // Export widget (pp:PinePaper ontology JSON)
4561
+ (async function() {
4562
+ if (!app.exportWidget) return { error: 'Widget export not available' };
4563
+ const result = await app.exportWidget(${JSON.stringify(opts)});
4564
+ return { success: true, json: result.json, filename: result.filename, embedCode: result.embedCode, metadata: result.data?.metadata };
4565
+ })();`.trim();
4566
+ }
4567
+ generateExportWidgetHtml(input) {
4568
+ const opts = {};
4569
+ if (input.title)
4570
+ opts.title = input.title;
4571
+ if (input.download !== undefined)
4572
+ opts.download = input.download;
4573
+ return `
4574
+ // Export widget as self-contained HTML
4575
+ (async function() {
4576
+ if (!app.exportWidgetHTML) return { error: 'Widget HTML export not available' };
4577
+ const result = await app.exportWidgetHTML(${JSON.stringify(opts)});
4578
+ 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
+ })();`.trim();
4580
+ }
4581
+ // ===========================================================================
4582
+ // DATA VISUALIZATION (CHARTS)
4583
+ // ===========================================================================
4584
+ generateChart(input) {
4585
+ const guard = `if (!app.chartSystem) return { error: 'ChartSystem not available' };`;
4586
+ switch (input.action) {
4587
+ case 'create': {
4588
+ if (!input.chartType)
4589
+ return `(function() { return { error: 'chartType is required for create' }; })();`;
4590
+ if (!input.data)
4591
+ return `(function() { return { error: 'data is required for create' }; })();`;
4592
+ const dataStr = JSON.stringify(input.data);
4593
+ const optsStr = JSON.stringify(input.options || {});
4594
+ return `
4595
+ // Create chart
4596
+ (function() {
4597
+ ${guard}
4598
+ const group = app.createChart(${JSON.stringify(input.chartType)}, ${dataStr}, ${optsStr});
4599
+ if (!group) return { error: 'Failed to create chart' };
4600
+ const chartId = app.chartSystem.getLastChartId();
4601
+ return { success: true, action: 'create', chartType: ${JSON.stringify(input.chartType)}, chartId, dataPoints: ${input.data.length} };
4602
+ })();`.trim();
4603
+ }
4604
+ case 'update': {
4605
+ if (!input.chartId)
4606
+ return `(function() { return { error: 'chartId is required for update' }; })();`;
4607
+ const chartIdStr = JSON.stringify(input.chartId);
4608
+ const dataStr = JSON.stringify(input.data || []);
4609
+ const optsStr = JSON.stringify(input.options || {});
4610
+ return `
4611
+ // Update chart data
4612
+ (function() {
4613
+ ${guard}
4614
+ const chart = app.chartSystem.getChart(${chartIdStr});
4615
+ if (!chart) return { error: 'Chart not found: ' + ${chartIdStr} };
4616
+ app.chartSystem.update(${chartIdStr}, ${dataStr}, ${optsStr});
4617
+ return { success: true, action: 'update', chartId: ${chartIdStr} };
4618
+ })();`.trim();
4619
+ }
4620
+ case 'reconfigure': {
4621
+ if (!input.chartId)
4622
+ return `(function() { return { error: 'chartId is required for reconfigure' }; })();`;
4623
+ const chartIdStr = JSON.stringify(input.chartId);
4624
+ const optsStr = JSON.stringify(input.options || {});
4625
+ return `
4626
+ // Reconfigure chart styling
4627
+ (function() {
4628
+ ${guard}
4629
+ const chart = app.chartSystem.getChart(${chartIdStr});
4630
+ if (!chart) return { error: 'Chart not found: ' + ${chartIdStr} };
4631
+ app.chartSystem.reconfigure(${chartIdStr}, ${optsStr});
4632
+ return { success: true, action: 'reconfigure', chartId: ${chartIdStr} };
4633
+ })();`.trim();
4634
+ }
4635
+ case 'remove': {
4636
+ if (!input.chartId)
4637
+ return `(function() { return { error: 'chartId is required for remove' }; })();`;
4638
+ const chartIdStr = JSON.stringify(input.chartId);
4639
+ return `
4640
+ // Remove chart
4641
+ (function() {
4642
+ ${guard}
4643
+ app.chartSystem.remove(${chartIdStr});
4644
+ return { success: true, action: 'remove', chartId: ${chartIdStr} };
4645
+ })();`.trim();
4646
+ }
4647
+ default:
4648
+ return `(function() { return { error: 'Unknown chart action: ${input.action}' }; })();`;
4649
+ }
4650
+ }
4651
+ // ===========================================================================
4652
+ // MAGIC SYSTEM
4653
+ // ===========================================================================
4654
+ generateMagic(input) {
4655
+ const guard = `if (!app.magicSystem) return { error: 'MagicSystem not available' };`;
4656
+ switch (input.action) {
4657
+ case 'animate': {
4658
+ const opts = {};
4659
+ if (input.mood)
4660
+ opts.energy = input.mood;
4661
+ if (input.selectionOnly !== undefined)
4662
+ opts.selectionOnly = input.selectionOnly;
4663
+ return `
4664
+ // Magic auto-animate
4665
+ (async function() {
4666
+ ${guard}
4667
+ const result = await app.magicSystem.autoAnimate(${JSON.stringify(opts)});
4668
+ return { success: true, action: 'animate', itemsAnimated: result.count, mood: result.energy, type: result.type };
4669
+ })();`.trim();
4670
+ }
4671
+ case 'remix': {
4672
+ const opts = {};
4673
+ if (input.selectionOnly !== undefined)
4674
+ opts.selectionOnly = input.selectionOnly;
4675
+ return `
4676
+ // Magic style remix
4677
+ (async function() {
4678
+ ${guard}
4679
+ const result = await app.magicSystem.remixStyle(${JSON.stringify(opts)});
4680
+ return { success: true, action: 'remix', itemsRemixed: result.count, palette: result.palette };
4681
+ })();`.trim();
4682
+ }
4683
+ default:
4684
+ return `(function() { return { error: 'Unknown magic action: ${input.action}' }; })();`;
4685
+ }
4686
+ }
4687
+ // ===========================================================================
4688
+ // PHYSICS (RIGID BODY SIMULATION)
4689
+ // ===========================================================================
4690
+ generatePhysics(input) {
4691
+ const guard = `if (!app.physicsWorld) return { error: 'PhysicsWorld not available' };`;
4692
+ switch (input.action) {
4693
+ case 'init': {
4694
+ const gravity = JSON.stringify(input.gravity || { x: 0, y: 980 });
4695
+ return `
4696
+ // Initialize physics world
4697
+ (function() {
4698
+ ${guard}
4699
+ app.physicsWorld.init({ gravity: ${gravity} });
4700
+ return { success: true, action: 'init', gravity: ${gravity} };
4701
+ })();`.trim();
4702
+ }
4703
+ case 'add_body': {
4704
+ if (!input.itemId)
4705
+ return `(function() { return { error: 'itemId is required for add_body' }; })();`;
4706
+ const itemIdStr = JSON.stringify(input.itemId);
4707
+ const config = {};
4708
+ if (input.bodyType)
4709
+ config.type = input.bodyType;
4710
+ if (input.mass !== undefined)
4711
+ config.mass = input.mass;
4712
+ if (input.friction !== undefined)
4713
+ config.friction = input.friction;
4714
+ if (input.restitution !== undefined)
4715
+ config.restitution = input.restitution;
4716
+ if (input.fixedRotation !== undefined)
4717
+ config.fixedRotation = input.fixedRotation;
4718
+ if (input.shape)
4719
+ config.shape = input.shape;
4720
+ return `
4721
+ // Add physics body
4722
+ (function() {
4723
+ ${guard}
4724
+ const entry = app.itemRegistry.get(${itemIdStr});
4725
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4726
+ const ok = app.physicsWorld.addBody(${itemIdStr}, ${JSON.stringify(config)});
4727
+ return { success: ok, action: 'add_body', itemId: ${itemIdStr}, bodyType: ${JSON.stringify(input.bodyType || 'dynamic')} };
4728
+ })();`.trim();
4729
+ }
4730
+ case 'remove_body': {
4731
+ if (!input.itemId)
4732
+ return `(function() { return { error: 'itemId is required for remove_body' }; })();`;
4733
+ const itemIdStr = JSON.stringify(input.itemId);
4734
+ return `
4735
+ // Remove physics body
4736
+ (function() {
4737
+ ${guard}
4738
+ app.physicsWorld.removeBody(${itemIdStr});
4739
+ return { success: true, action: 'remove_body', itemId: ${itemIdStr} };
4740
+ })();`.trim();
4741
+ }
4742
+ case 'apply_force': {
4743
+ if (!input.itemId)
4744
+ return `(function() { return { error: 'itemId is required for apply_force' }; })();`;
4745
+ if (!input.force)
4746
+ return `(function() { return { error: 'force is required for apply_force' }; })();`;
4747
+ const itemIdStr = JSON.stringify(input.itemId);
4748
+ return `
4749
+ // Apply continuous force
4750
+ (function() {
4751
+ ${guard}
4752
+ app.physicsWorld.applyForce(${itemIdStr}, ${JSON.stringify(input.force)});
4753
+ return { success: true, action: 'apply_force', itemId: ${itemIdStr}, force: ${JSON.stringify(input.force)} };
4754
+ })();`.trim();
4755
+ }
4756
+ case 'apply_impulse': {
4757
+ if (!input.itemId)
4758
+ return `(function() { return { error: 'itemId is required for apply_impulse' }; })();`;
4759
+ if (!input.impulse)
4760
+ return `(function() { return { error: 'impulse is required for apply_impulse' }; })();`;
4761
+ const itemIdStr = JSON.stringify(input.itemId);
4762
+ return `
4763
+ // Apply instant impulse
4764
+ (function() {
4765
+ ${guard}
4766
+ app.physicsWorld.applyImpulse(${itemIdStr}, ${JSON.stringify(input.impulse)});
4767
+ return { success: true, action: 'apply_impulse', itemId: ${itemIdStr}, impulse: ${JSON.stringify(input.impulse)} };
4768
+ })();`.trim();
4769
+ }
4770
+ case 'set_velocity': {
4771
+ if (!input.itemId)
4772
+ return `(function() { return { error: 'itemId is required for set_velocity' }; })();`;
4773
+ if (!input.velocity)
4774
+ return `(function() { return { error: 'velocity is required for set_velocity' }; })();`;
4775
+ const itemIdStr = JSON.stringify(input.itemId);
4776
+ return `
4777
+ // Set body velocity
4778
+ (function() {
4779
+ ${guard}
4780
+ app.physicsWorld.setVelocity(${itemIdStr}, ${JSON.stringify(input.velocity)});
4781
+ return { success: true, action: 'set_velocity', itemId: ${itemIdStr}, velocity: ${JSON.stringify(input.velocity)} };
4782
+ })();`.trim();
4783
+ }
4784
+ case 'get_state': {
4785
+ if (!input.itemId)
4786
+ return `(function() { return { error: 'itemId is required for get_state' }; })();`;
4787
+ const itemIdStr = JSON.stringify(input.itemId);
4788
+ return `
4789
+ // Get physics body state
4790
+ (function() {
4791
+ ${guard}
4792
+ const state = app.physicsWorld.getBodyState(${itemIdStr});
4793
+ if (!state) return { error: 'No physics body for item: ' + ${itemIdStr} };
4794
+ return { success: true, action: 'get_state', itemId: ${itemIdStr}, ...state };
4795
+ })();`.trim();
4796
+ }
4797
+ case 'create_ground': {
4798
+ const y = input.y !== undefined ? input.y : 500;
4799
+ const w = input.width !== undefined ? input.width : 2000;
4800
+ return `
4801
+ // Create ground plane
4802
+ (function() {
4803
+ ${guard}
4804
+ const groundId = app.physicsWorld.createGround(${y}, ${w});
4805
+ return { success: true, action: 'create_ground', groundId, y: ${y}, width: ${w} };
4806
+ })();`.trim();
4807
+ }
4808
+ case 'create_joint': {
4809
+ if (!input.itemId)
4810
+ return `(function() { return { error: 'itemId is required for create_joint' }; })();`;
4811
+ if (!input.targetItemId)
4812
+ return `(function() { return { error: 'targetItemId is required for create_joint' }; })();`;
4813
+ if (!input.jointType)
4814
+ return `(function() { return { error: 'jointType is required for create_joint' }; })();`;
4815
+ const itemIdStr = JSON.stringify(input.itemId);
4816
+ const targetStr = JSON.stringify(input.targetItemId);
4817
+ const params = JSON.stringify({ type: input.jointType, ...(input.jointParams || {}) });
4818
+ return `
4819
+ // Create physics joint
4820
+ (function() {
4821
+ ${guard}
4822
+ const joint = app.physicsWorld.createJoint(${itemIdStr}, ${targetStr}, ${params});
4823
+ if (!joint) return { error: 'Failed to create joint between ' + ${itemIdStr} + ' and ' + ${targetStr} };
4824
+ return { success: true, action: 'create_joint', itemId: ${itemIdStr}, targetItemId: ${targetStr}, jointType: ${JSON.stringify(input.jointType)} };
4825
+ })();`.trim();
4826
+ }
4827
+ default:
4828
+ return `(function() { return { error: 'Unknown physics action: ${input.action}' }; })();`;
4829
+ }
4830
+ }
4831
+ // ===========================================================================
4832
+ // MEASUREMENT SYSTEM
4833
+ // ===========================================================================
4834
+ generateMeasurement(input) {
4835
+ const guard = `if (!app.measurementSystem) return { error: 'MeasurementSystem not available' };`;
4836
+ switch (input.action) {
4837
+ case 'set_rulers': {
4838
+ const enabled = input.enabled !== false;
4839
+ return `
4840
+ // Toggle rulers
4841
+ (function() {
4842
+ ${guard}
4843
+ app.measurementSystem.setRulersVisible(${enabled});
4844
+ return { success: true, action: 'set_rulers', enabled: ${enabled} };
4845
+ })();`.trim();
4846
+ }
4847
+ case 'set_grid': {
4848
+ const enabled = input.enabled !== false;
4849
+ return `
4850
+ // Toggle grid
4851
+ (function() {
4852
+ ${guard}
4853
+ app.measurementSystem.setGridVisible(${enabled});
4854
+ return { success: true, action: 'set_grid', enabled: ${enabled} };
4855
+ })();`.trim();
4856
+ }
4857
+ // No measurementSystem guard — reads from itemRegistry directly
4858
+ case 'get_dimensions': {
4859
+ if (!input.itemId)
4860
+ return `(function() { return { error: 'itemId is required for get_dimensions' }; })();`;
4861
+ const itemIdStr = JSON.stringify(input.itemId);
4862
+ return `
4863
+ // Get item dimensions
4864
+ (function() {
4865
+ const entry = app.itemRegistry.get(${itemIdStr});
4866
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
4867
+ const b = entry.item.bounds;
4868
+ 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 };
4869
+ })();`.trim();
4870
+ }
4871
+ case 'set_snap': {
4872
+ const enabled = input.enabled !== false;
4873
+ return `
4874
+ // Toggle snap to grid
4875
+ (function() {
4876
+ ${guard}
4877
+ app.measurementSystem.setSnapToUnitEnabled(${enabled});
4878
+ return { success: true, action: 'set_snap', enabled: ${enabled} };
4879
+ })();`.trim();
4880
+ }
4881
+ default:
4882
+ return `(function() { return { error: 'Unknown measurement action: ${input.action}' }; })();`;
4883
+ }
4884
+ }
3468
4885
  }
3469
4886
  // Export singleton instance
3470
4887
  export const codeGenerator = new PinePaperCodeGenerator();