slicer.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. /**
  2. * Utility for opening files in slicer applications
  3. *
  4. * Protocol handler URL formats (from BambuStudio/OrcaSlicer source code):
  5. *
  6. * Bambu Studio has TWO separate URL handlers:
  7. * 1. post_init() [Windows/Linux CLI args]: bambustudio://open?file=<URL>
  8. * - Checks: starts_with("bambustudio://open")
  9. * - Calls url_decode(), then split_str(url, "file=")
  10. * 2. MacOpenURL() [macOS Apple Events]: bambustudioopen://<encoded-URL>
  11. * - Checks: starts_with("bambustudioopen://")
  12. * - Strips prefix, then url_decode()
  13. *
  14. * OrcaSlicer Downloader accepts both formats via regex:
  15. * - (orcaslicer|bambustudio|...)://open?file=<URL>
  16. * - bambustudioopen://<URL>
  17. *
  18. * Key insight: every form needs encodeURIComponent on the file URL, because
  19. * the slicer calls url_decode() on the received query (post_init calls
  20. * url_decode then split_str; MacOpenURL strips the prefix then url_decode;
  21. * OrcaSlicer's Downloader regex-extracts then url_decode). Without encoding,
  22. * any already-percent-encoded character in the download URL (most commonly
  23. * %20 in filenames with spaces) decodes to a literal space and the slicer's
  24. * subsequent HTTP fetch fails with a 0-byte body or 404. See issue #1059.
  25. */
  26. export type SlicerType = 'bambu_studio' | 'orcaslicer';
  27. type Platform = 'windows' | 'macos' | 'linux' | 'unknown';
  28. /**
  29. * Detect the user's operating system
  30. */
  31. export function detectPlatform(): Platform {
  32. const userAgent = navigator.userAgent.toLowerCase();
  33. const platform = navigator.platform?.toLowerCase() || '';
  34. if (userAgent.includes('win') || platform.includes('win')) {
  35. return 'windows';
  36. }
  37. if (userAgent.includes('mac') || platform.includes('mac')) {
  38. return 'macos';
  39. }
  40. if (userAgent.includes('linux') || platform.includes('linux')) {
  41. return 'linux';
  42. }
  43. return 'unknown';
  44. }
  45. /**
  46. * Open a URL in the specified slicer application.
  47. * @param downloadUrl - The URL to the file to open
  48. * @param slicer - Which slicer to use (defaults to bambu_studio)
  49. */
  50. export function openInSlicer(downloadUrl: string, slicer: SlicerType = 'bambu_studio'): void {
  51. let url: string;
  52. const encoded = encodeURIComponent(downloadUrl);
  53. if (slicer === 'orcaslicer') {
  54. url = `orcaslicer://open?file=${encoded}`;
  55. } else {
  56. const platform = detectPlatform();
  57. if (platform === 'macos') {
  58. // macOS only: bambustudioopen scheme via MacOpenURL() callback.
  59. url = `bambustudioopen://${encoded}`;
  60. } else {
  61. // Windows/Linux: bambustudio://open?file= via post_init() CLI args.
  62. // IMPORTANT: On Linux, BS only handles "bambustudio://open" prefix —
  63. // it does NOT process "bambustudioopen://" (that's macOS-only).
  64. url = `bambustudio://open?file=${encoded}`;
  65. }
  66. }
  67. // Use a temporary <a> element to trigger the protocol handler.
  68. // This avoids navigating away from the page (unlike window.location.href).
  69. const link = document.createElement('a');
  70. link.href = url;
  71. link.style.display = 'none';
  72. document.body.appendChild(link);
  73. link.click();
  74. document.body.removeChild(link);
  75. }
  76. /**
  77. * Build a full download URL for a file
  78. * @param path - The API path (e.g., from api.getArchiveForSlicer())
  79. */
  80. export function buildDownloadUrl(path: string): string {
  81. return `${window.location.origin}${path}`;
  82. }
  83. /**
  84. * Convenience function to open an archive in the slicer
  85. * @param path - The API path to the archive
  86. * @param slicer - Which slicer to use (defaults to bambu_studio)
  87. */
  88. export function openArchiveInSlicer(path: string, slicer: SlicerType = 'bambu_studio'): void {
  89. const downloadUrl = buildDownloadUrl(path);
  90. openInSlicer(downloadUrl, slicer);
  91. }