@pinepaper.studio/mcp-server 1.5.3 → 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 (71) 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/index.d.ts.map +1 -1
  8. package/dist/index.js +47 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/ontology/hints.d.ts +71 -0
  11. package/dist/ontology/hints.d.ts.map +1 -0
  12. package/dist/ontology/hints.js +189 -0
  13. package/dist/ontology/hints.js.map +1 -0
  14. package/dist/ontology/vocabulary.d.ts.map +1 -1
  15. package/dist/ontology/vocabulary.js +34 -0
  16. package/dist/ontology/vocabulary.js.map +1 -1
  17. package/dist/prompts/index.d.ts.map +1 -1
  18. package/dist/prompts/index.js +146 -0
  19. package/dist/prompts/index.js.map +1 -1
  20. package/dist/tools/compact-descriptions.d.ts +3 -0
  21. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  22. package/dist/tools/compact-descriptions.js +17 -8
  23. package/dist/tools/compact-descriptions.js.map +1 -1
  24. package/dist/tools/definitions.d.ts +1 -1
  25. package/dist/tools/definitions.d.ts.map +1 -1
  26. package/dist/tools/definitions.js +1090 -1639
  27. package/dist/tools/definitions.js.map +1 -1
  28. package/dist/tools/handlers/camera.d.ts +19 -0
  29. package/dist/tools/handlers/camera.d.ts.map +1 -0
  30. package/dist/tools/handlers/camera.js +94 -0
  31. package/dist/tools/handlers/camera.js.map +1 -0
  32. package/dist/tools/handlers/export.d.ts +12 -0
  33. package/dist/tools/handlers/export.d.ts.map +1 -0
  34. package/dist/tools/handlers/export.js +30 -0
  35. package/dist/tools/handlers/export.js.map +1 -0
  36. package/dist/tools/handlers/font.d.ts +12 -0
  37. package/dist/tools/handlers/font.d.ts.map +1 -0
  38. package/dist/tools/handlers/font.js +135 -0
  39. package/dist/tools/handlers/font.js.map +1 -0
  40. package/dist/tools/handlers/maps.d.ts +19 -0
  41. package/dist/tools/handlers/maps.d.ts.map +1 -0
  42. package/dist/tools/handlers/maps.js +184 -0
  43. package/dist/tools/handlers/maps.js.map +1 -0
  44. package/dist/tools/handlers/ontology.d.ts +20 -0
  45. package/dist/tools/handlers/ontology.d.ts.map +1 -0
  46. package/dist/tools/handlers/ontology.js +185 -0
  47. package/dist/tools/handlers/ontology.js.map +1 -0
  48. package/dist/tools/handlers/tool-guide.d.ts +14 -0
  49. package/dist/tools/handlers/tool-guide.d.ts.map +1 -0
  50. package/dist/tools/handlers/tool-guide.js +52 -0
  51. package/dist/tools/handlers/tool-guide.js.map +1 -0
  52. package/dist/tools/handlers.d.ts +18 -0
  53. package/dist/tools/handlers.d.ts.map +1 -1
  54. package/dist/tools/handlers.js +205 -603
  55. package/dist/tools/handlers.js.map +1 -1
  56. package/dist/tools/minimal-descriptions.d.ts +4 -0
  57. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  58. package/dist/tools/minimal-descriptions.js +45 -54
  59. package/dist/tools/minimal-descriptions.js.map +1 -1
  60. package/dist/tools/toolkits.d.ts.map +1 -1
  61. package/dist/tools/toolkits.js +36 -45
  62. package/dist/tools/toolkits.js.map +1 -1
  63. package/dist/types/code-generator.d.ts +36 -1
  64. package/dist/types/code-generator.d.ts.map +1 -1
  65. package/dist/types/code-generator.js +823 -35
  66. package/dist/types/code-generator.js.map +1 -1
  67. package/dist/types/schemas.d.ts +1116 -444
  68. package/dist/types/schemas.d.ts.map +1 -1
  69. package/dist/types/schemas.js +290 -18
  70. package/dist/types/schemas.js.map +1 -1
  71. package/package.json +1 -1
@@ -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
  /**
@@ -582,7 +627,14 @@ export class PinePaperCodeGenerator {
582
627
  */
583
628
  generateCreateItem(input) {
584
629
  const validated = CreateItemInputSchema.parse(input);
585
- 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);
586
638
  }
587
639
  /**
588
640
  * Generate code for modifying an item
@@ -628,7 +680,7 @@ export class PinePaperCodeGenerator {
628
680
  */
629
681
  generateKeyframeAnimate(input) {
630
682
  const validated = KeyframeAnimateInputSchema.parse(input);
631
- 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);
632
684
  }
633
685
  /**
634
686
  * Generate code for executing a generator
@@ -748,6 +800,70 @@ if (imported) {
748
800
  return `
749
801
  // Error: No SVG source provided
750
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
+ })();
751
867
  `.trim();
752
868
  }
753
869
  /**
@@ -1346,10 +1462,11 @@ throw new Error('Unknown diagram mode action: ${action}');
1346
1462
  */
1347
1463
  generateAgentStartJob(input) {
1348
1464
  const validated = AgentStartJobInputSchema.parse(input);
1349
- const { name, screenshotPolicy, canvasPreset, clearCanvas } = validated;
1465
+ const { name, screenshotPolicy, canvasPreset, clearCanvas, includeOntology } = validated;
1350
1466
  const nameStr = name ? `'${name.replace(/'/g, "\\'")}'` : 'null';
1351
1467
  const policyStr = screenshotPolicy || 'on_complete';
1352
1468
  const shouldClear = clearCanvas !== false;
1469
+ const wantOntology = includeOntology !== false;
1353
1470
  let code = `
1354
1471
  // Start agent job
1355
1472
  (async function() {
@@ -1378,10 +1495,31 @@ throw new Error('Unknown diagram mode action: ${action}');
1378
1495
  app.setCanvasSize('${canvasPreset}');
1379
1496
  // Allow canvas resize to take effect
1380
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
+ }
1381
1519
  `;
1382
1520
  }
1383
1521
  code += `
1384
- // Return job context with canvas size
1522
+ // Return job context with canvas size${wantOntology ? ' + initial ontology' : ''}
1385
1523
  const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
1386
1524
  return {
1387
1525
  success: true,
@@ -1390,7 +1528,9 @@ throw new Error('Unknown diagram mode action: ${action}');
1390
1528
  screenshotPolicy: '${policyStr}',
1391
1529
  canvasPreset: ${canvasPreset ? `'${canvasPreset}'` : 'null'},
1392
1530
  canvasCleared: ${shouldClear},
1393
- 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` : ''}
1394
1534
  };
1395
1535
  })();
1396
1536
  `;
@@ -1800,7 +1940,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1800
1940
  */
1801
1941
  generateAgentExport(input) {
1802
1942
  const validated = AgentExportInputSchema.parse(input);
1803
- const { platform, format, quality } = validated;
1943
+ const { platform, format, quality, framing } = validated;
1804
1944
  const qualityLevel = quality || 'standard';
1805
1945
  // Quality settings (bitrate in bps for VideoEncoder)
1806
1946
  const qualitySettings = {
@@ -1830,10 +1970,38 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1830
1970
  const platform = '${platform}';
1831
1971
  const format = '${exportFormat}';
1832
1972
  const quality = '${qualityLevel}';
1973
+ const framing = '${framing}';
1833
1974
  const settings = ${JSON.stringify(qualitySettings)};
1834
1975
  const dimensions = ${JSON.stringify({ width: preset.width, height: preset.height })};
1835
1976
 
1836
- 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 };
1837
2005
 
1838
2006
  try {
1839
2007
  switch (format) {
@@ -1881,28 +2049,44 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1881
2049
  case 'mp4':
1882
2050
  case 'webm': {
1883
2051
  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);
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);
1887
2080
  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 };
2081
+ const dataUrl = await blobToDataUrl(videoResult.blob);
2082
+ result = { success: true, platform, format, framing, data: dataUrl, mimeType: videoMimeType, size: videoResult.blob.size };
1894
2083
  } else {
1895
2084
  result = { success: false, error: format.toUpperCase() + ' export returned no data' };
1896
2085
  }
1897
2086
  } 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 };
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 };
1906
2090
  } else {
1907
2091
  result = { success: false, error: format.toUpperCase() + ' export not available' };
1908
2092
  }
@@ -2345,7 +2529,7 @@ app.historyManager.saveState();
2345
2529
  if (gradientDirection)
2346
2530
  options.gradientDirection = gradientDirection;
2347
2531
  return `
2348
- // Create letter collage: "${text}"
2532
+ // Create letter collage
2349
2533
  (function() {
2350
2534
  const text = ${JSON.stringify(text)};
2351
2535
  const options = ${JSON.stringify(options, null, 2)};
@@ -2360,7 +2544,21 @@ app.historyManager.saveState();
2360
2544
  app.historyManager.saveState('create_letter_collage');
2361
2545
  }
2362
2546
 
2363
- 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 };
2364
2562
  })();
2365
2563
  `.trim();
2366
2564
  }
@@ -4094,6 +4292,596 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
4094
4292
  return `(function() { return { error: 'Unknown query action: ${input.action}' }; })();`;
4095
4293
  }
4096
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
+ }
4097
4885
  }
4098
4886
  // Export singleton instance
4099
4887
  export const codeGenerator = new PinePaperCodeGenerator();