bambu-printer-mcp 1.1.32 → 1.1.34

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.
@@ -11,6 +11,7 @@ import * as crypto from 'crypto';
11
11
  import { execFile } from 'child_process';
12
12
  import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
13
13
  import { profileJsonFiles } from '../slicer/profile-files.js';
14
+ import { materializeOrcaProfilesRoot, stageOrcaCliProfiles } from '../slicer/orca-profile-cache.js';
14
15
  import { sliceWithOrcaApi, validateRemoteSliceOptions } from '../slicer/remote-slicer.js';
15
16
 
16
17
  const readFileAsync = promisify(fs.readFile);
@@ -200,6 +201,69 @@ const PROFILE_METADATA_KEYS = new Set([
200
201
  'compatible_printers', 'compatible_printers_condition', 'compatible_prints', 'compatible_prints_condition',
201
202
  ]);
202
203
 
204
+ /**
205
+ * Why the slicer process failed. execFile's message already carries stderr;
206
+ * a crash often leaves stderr empty (FULU OrcaSlicer-bambulab v1.0.0
207
+ * segfaults with no output), so name the signal or exit code and add the end
208
+ * of stdout instead.
209
+ */
210
+ /** Where a Bambu-style CLI records its error on Linux, and when this run started. */
211
+ type CliResultSource = { resultPath: string; startedAt: number };
212
+
213
+ function describeSlicerFailure(
214
+ error: { message: string; code?: unknown; signal?: unknown },
215
+ stdout: string,
216
+ stderr: string,
217
+ cli?: CliResultSource
218
+ ): string {
219
+ const status = typeof error.signal === "string" && error.signal
220
+ ? `crashed with ${error.signal}`
221
+ : typeof error.code === "number" ? `exited with code ${error.code}` : "failed";
222
+ const reason = cli ? bambuCliFailureReason(error.code, cli) : undefined;
223
+ const output = stderr.trim() ? "" : stdout.trim().split("\n").slice(-20).join("\n");
224
+ return `Slicer failed: the slicer ${status}${reason ? ` (${reason})` : ""}. ${error.message.trim()}${output ? `\nSlicer output: ${output}` : ""}`;
225
+ }
226
+
227
+ // Bambu Studio and OrcaSlicer CLI errors (libslic3r/Utils.hpp). The process
228
+ // exits with the low byte of the code; on Linux result.json names the error.
229
+ const BAMBU_CLI_ERRORS: Record<number, string> = {
230
+ [-2]: "invalid parameters to the slicer",
231
+ [-3]: "input files not found",
232
+ [-5]: "preset file cannot be parsed",
233
+ [-6]: "model file cannot be parsed",
234
+ [-9]: "copying objects failed",
235
+ [-17]: "printer is not compatible with the process preset",
236
+ [-21]: "auto-arranging objects failed",
237
+ [-22]: "auto-orienting objects failed",
238
+ [-50]: "a plate is empty or has no object fully inside it; arrange the model or place it on the plate",
239
+ [-51]: "incorrect slicing parameters",
240
+ [-52]: "objects extend over the bed boundary",
241
+ [-60]: "no printable objects remain after skipping",
242
+ [-61]: "filaments are not compatible with the plate type",
243
+ [-62]: "the filaments' temperatures differ too much",
244
+ [-63]: "objects collide in print-by-object mode",
245
+ [-64]: "objects collide",
246
+ [-100]: "slicing the model failed",
247
+ [-101]: "G-code conflicts after slicing",
248
+ [-102]: "G-code in an unprintable area",
249
+ };
250
+
251
+ function bambuCliFailureReason(exitCode: unknown, cli: CliResultSource): string | undefined {
252
+ try {
253
+ if (fs.statSync(cli.resultPath).mtimeMs >= cli.startedAt) {
254
+ const result = JSON.parse(fs.readFileSync(cli.resultPath, "utf8"));
255
+ if (typeof result.error_string === "string" && result.error_string.trim()) {
256
+ return `${result.error_string.trim()} [${result.return_code}]`;
257
+ }
258
+ }
259
+ } catch {
260
+ // result.json is written only on Linux and only for handled errors.
261
+ }
262
+ if (typeof exitCode !== "number") return undefined;
263
+ const code = Object.keys(BAMBU_CLI_ERRORS).map(Number).find(value => (value & 0xff) === exitCode);
264
+ return code === undefined ? undefined : `${BAMBU_CLI_ERRORS[code]} [${code}]`;
265
+ }
266
+
203
267
  export class STLManipulator extends EventEmitter {
204
268
  private tempDir: string;
205
269
  private activeOperations: Map<string, boolean> = new Map();
@@ -291,7 +355,7 @@ export class STLManipulator extends EventEmitter {
291
355
  return value;
292
356
  }
293
357
 
294
- private sanitizeProcessForOrca(processConfig: any, printerPreset?: string): any {
358
+ private sanitizeProcessForOrca(processConfig: any, printerPreset?: string, preserveExtrusionMode = false): any {
295
359
  const sanitized = { ...processConfig };
296
360
 
297
361
  sanitized.type = 'process';
@@ -312,7 +376,7 @@ export class STLManipulator extends EventEmitter {
312
376
  sanitized.prime_tower_brim_width = String(sanitized.prime_tower_brim_width);
313
377
  }
314
378
 
315
- return this.useAbsoluteExtrusionForOrca(sanitized);
379
+ return preserveExtrusionMode ? sanitized : this.useAbsoluteExtrusionForOrca(sanitized);
316
380
  }
317
381
 
318
382
  /** Orca slices with absolute E distances; per-layer `G92 E0` resets would break them. */
@@ -344,7 +408,8 @@ export class STLManipulator extends EventEmitter {
344
408
  slicerProfile?: string,
345
409
  printerPreset?: string,
346
410
  bambuOptions?: BambuSliceOptions,
347
- activeProfilesRoot?: string
411
+ activeProfilesRoot?: string,
412
+ preserveOrcaExtrusionMode = false
348
413
  ): Promise<BambuSettingsBundle> {
349
414
  const roots = activeProfilesRoot
350
415
  ? [path.join(activeProfilesRoot, 'BBL')]
@@ -417,7 +482,7 @@ export class STLManipulator extends EventEmitter {
417
482
  outputBase,
418
483
  slicerType === 'orcaslicer' ? 'process_orca' : 'process',
419
484
  slicerType === 'orcaslicer'
420
- ? this.sanitizeProcessForOrca(mergedProcess, printerPreset)
485
+ ? this.sanitizeProcessForOrca(mergedProcess, printerPreset, preserveOrcaExtrusionMode)
421
486
  : mergedProcess
422
487
  );
423
488
 
@@ -460,7 +525,7 @@ export class STLManipulator extends EventEmitter {
460
525
  ? this.writeTempJson(
461
526
  outputBase,
462
527
  'process_default_orca',
463
- this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset)
528
+ this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset, preserveOrcaExtrusionMode)
464
529
  )
465
530
  : defaultProcessPath;
466
531
  }
@@ -976,6 +1041,29 @@ export class STLManipulator extends EventEmitter {
976
1041
  }
977
1042
  }
978
1043
 
1044
+ /**
1045
+ * Apply slice_stl's scale and rotations to a copy of the mesh as OrcaSlicer's
1046
+ * CLI would, about the model's centre. Orca adds each --rotate* to the
1047
+ * object's Euler angles (ModelVolume::rotate), so the result rotates about X,
1048
+ * then Y, then Z whatever the argument order, after scaling. The slicer then
1049
+ * places the result on the plate.
1050
+ */
1051
+ private async transformMeshForCli(stlFilePath: string, options: BambuSliceOptions): Promise<string> {
1052
+ const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
1053
+ const center = new THREE.Vector3();
1054
+ boundingBox.getCenter(center);
1055
+ const radians = (degrees: number) => degrees * Math.PI / 180;
1056
+ geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
1057
+ if (options.scale !== undefined) geometry.applyMatrix4(new THREE.Matrix4().makeScale(options.scale, options.scale, options.scale));
1058
+ if (options.rotateX) geometry.applyMatrix4(new THREE.Matrix4().makeRotationX(radians(options.rotateX)));
1059
+ if (options.rotateY) geometry.applyMatrix4(new THREE.Matrix4().makeRotationY(radians(options.rotateY)));
1060
+ if (options.rotate) geometry.applyMatrix4(new THREE.Matrix4().makeRotationZ(radians(options.rotate)));
1061
+ geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(center.x, center.y, center.z));
1062
+ const outputFilePath = path.join(this.tempDir, `${path.basename(stlFilePath, path.extname(stlFilePath))}_transformed_${this.generateOperationId()}.stl`);
1063
+ await this.saveSTL(geometry, outputFilePath);
1064
+ return outputFilePath;
1065
+ }
1066
+
979
1067
  /**
980
1068
  * Rotate an STL model around specific axes
981
1069
  */
@@ -1608,6 +1696,9 @@ export class STLManipulator extends EventEmitter {
1608
1696
  let outputFilePath = path.join(this.tempDir, outputFileName);
1609
1697
 
1610
1698
  let args: string[] = [];
1699
+ let cliResult: CliResultSource | undefined;
1700
+ const orcaTemporaryDirectories: string[] = [];
1701
+ const temporaryFiles: string[] = [];
1611
1702
 
1612
1703
  try {
1613
1704
  if (progressCallback) progressCallback(0, `Starting slicing with ${slicerType}...`);
@@ -1660,14 +1751,19 @@ export class STLManipulator extends EventEmitter {
1660
1751
  const profileSlicerType = remote ? 'orcaslicer' : slicerType;
1661
1752
  const bambuOutputPath = path.join(this.tempDir, outputBase + (remote ? `_${operationId}` : '') + '_sliced.3mf');
1662
1753
  const outputDir = path.dirname(bambuOutputPath);
1663
- const profilesRoot = detectProfilesRoot(remote ? undefined : slicerPath, profileSlicerType);
1754
+ const selectedProfilesRoot = detectProfilesRoot(remote ? undefined : slicerPath, profileSlicerType);
1755
+ const profilesRoot = !remote && profileSlicerType === 'orcaslicer'
1756
+ ? await materializeOrcaProfilesRoot(selectedProfilesRoot, this.tempDir)
1757
+ : selectedProfilesRoot;
1758
+ if (profilesRoot !== selectedProfilesRoot) orcaTemporaryDirectories.push(profilesRoot);
1664
1759
  const rawBundle = await this.resolveBambuLikeSettingsBundle(
1665
1760
  outputBase,
1666
1761
  profileSlicerType,
1667
1762
  slicerProfile,
1668
1763
  printerPreset,
1669
1764
  bambuOptions,
1670
- profilesRoot
1765
+ profilesRoot,
1766
+ profileSlicerType === 'orcaslicer' && profilesRoot !== selectedProfilesRoot
1671
1767
  );
1672
1768
  // Resolve every BBL dependency before launching the slicer.
1673
1769
  const settingsBundle = await this.maybeFlattenBundle(
@@ -1689,7 +1785,7 @@ export class STLManipulator extends EventEmitter {
1689
1785
  // absolute-extrusion normalization after resolving those ancestors.
1690
1786
  // The machine owns the extrusion mode and layer G-code once process
1691
1787
  // files lose machine keys, so normalize it there as well.
1692
- if (profileSlicerType === 'orcaslicer' && settingsBundle.settingsArg) {
1788
+ if (profileSlicerType === 'orcaslicer' && settingsBundle.settingsArg && profilesRoot === selectedProfilesRoot) {
1693
1789
  const [machinePath, processPath] = settingsBundle.settingsArg.split(';');
1694
1790
  settingsBundle.settingsArg = [
1695
1791
  this.writeTempJson(outputBase, 'machine_orca_resolved',
@@ -1698,7 +1794,14 @@ export class STLManipulator extends EventEmitter {
1698
1794
  this.sanitizeProcessForOrca(this.readJsonFile(processPath), printerPreset)),
1699
1795
  ].join(';');
1700
1796
  }
1701
- const cliBundle = this.isolateMachineSettings(settingsBundle);
1797
+ let cliBundle = this.isolateMachineSettings(settingsBundle);
1798
+ let orcaExtraArgs: string[] = [];
1799
+ if (!remote && profileSlicerType === 'orcaslicer' && profilesRoot !== selectedProfilesRoot) {
1800
+ const staged = await stageOrcaCliProfiles(profilesRoot, this.tempDir, cliBundle);
1801
+ cliBundle = { ...cliBundle, settingsArg: staged.settingsArg, filamentPaths: staged.filamentPaths };
1802
+ orcaExtraArgs = staged.extraArgs;
1803
+ orcaTemporaryDirectories.push(staged.dataDir);
1804
+ }
1702
1805
  if (remote) {
1703
1806
  const [machinePath, processPath] = cliBundle.settingsArg?.split(';') ?? [];
1704
1807
  if (!machinePath || !processPath) throw new Error('Remote slicing requires complete prepared machine and process profiles.');
@@ -1709,7 +1812,30 @@ export class STLManipulator extends EventEmitter {
1709
1812
  });
1710
1813
  break;
1711
1814
  }
1815
+ // OrcaSlicer's CLI crashes (SIGSEGV in PartPlate::check_outside,
1816
+ // which needs the GUI's Plater) when --scale or --rotate-x/-y move an
1817
+ // object below the bed and it arranges, the default for STL; it also
1818
+ // rejects --repetitions for one STL. Apply STL transforms to the mesh
1819
+ // in the CLI's order instead, and refuse the 3MF case it cannot fix.
1820
+ const orcaCli = slicerType === 'orcaslicer' || slicerType === 'orcaslicer-bambulab';
1821
+ const tilts = !!bambuOptions?.rotateX || !!bambuOptions?.rotateY;
1822
+ const meshTransform = orcaCli && !is3mf && (bambuOptions?.scale !== undefined || tilts);
1823
+ if (orcaCli && is3mf && bambuOptions?.arrange === true && ((bambuOptions.scale ?? 1) > 1 || tilts)) {
1824
+ throw new Error(
1825
+ "OrcaSlicer's CLI crashes when scale above 1, rotate_x or rotate_y moves a 3MF object below the bed while arranging. " +
1826
+ "Set arrange to false, or apply the transform in the slicer and export the project."
1827
+ );
1828
+ }
1829
+ if (orcaCli && !is3mf && bambuOptions?.repetitions !== undefined && bambuOptions.cloneObjects) {
1830
+ throw new Error("Use either repetitions or clone_objects for an STL with OrcaSlicer, not both.");
1831
+ }
1832
+ const inputPath = meshTransform ? await this.transformMeshForCli(stlFilePath, bambuOptions!) : stlFilePath;
1833
+ if (inputPath !== stlFilePath) temporaryFiles.push(inputPath);
1834
+ cliResult = { resultPath: path.join(outputDir, 'result.json'), startedAt: Date.now() };
1835
+ // A result.json left by an earlier run must not explain this one.
1836
+ await fs.promises.rm(cliResult.resultPath, { force: true });
1712
1837
  args = [
1838
+ ...orcaExtraArgs,
1713
1839
  '--slice', String(bambuOptions?.slicePlate ?? 0),
1714
1840
  '--outputdir', outputDir,
1715
1841
  '--export-3mf', path.basename(bambuOutputPath),
@@ -1729,11 +1855,17 @@ export class STLManipulator extends EventEmitter {
1729
1855
  if (bambuOptions?.skipModifiedGcodes) args.push('--skip-modified-gcodes');
1730
1856
  if (bambuOptions?.orient !== undefined) args.push('--orient', bambuOptions.orient ? '1' : '0');
1731
1857
  if (bambuOptions?.arrange !== undefined) args.push('--arrange', bambuOptions.arrange ? '1' : '0');
1732
- if (bambuOptions?.repetitions !== undefined) args.push('--repetitions', String(bambuOptions.repetitions));
1733
- if (bambuOptions?.scale !== undefined) args.push('--scale', String(bambuOptions.scale));
1734
- if (bambuOptions?.rotate !== undefined) args.push('--rotate', String(bambuOptions.rotate));
1735
- if (bambuOptions?.rotateX !== undefined) args.push('--rotate-x', String(bambuOptions.rotateX));
1736
- if (bambuOptions?.rotateY !== undefined) args.push('--rotate-y', String(bambuOptions.rotateY));
1858
+ if (bambuOptions?.repetitions !== undefined) {
1859
+ // OrcaSlicer counts copies of a single loaded STL with --clone-objects.
1860
+ if (orcaCli && !is3mf) args.push('--clone-objects', String(bambuOptions.repetitions));
1861
+ else args.push('--repetitions', String(bambuOptions.repetitions));
1862
+ }
1863
+ if (!meshTransform) {
1864
+ if (bambuOptions?.scale !== undefined) args.push('--scale', String(bambuOptions.scale));
1865
+ if (bambuOptions?.rotate !== undefined) args.push('--rotate', String(bambuOptions.rotate));
1866
+ if (bambuOptions?.rotateX !== undefined) args.push('--rotate-x', String(bambuOptions.rotateX));
1867
+ if (bambuOptions?.rotateY !== undefined) args.push('--rotate-y', String(bambuOptions.rotateY));
1868
+ }
1737
1869
  if (bambuOptions?.cloneObjects) args.push('--clone-objects', bambuOptions.cloneObjects);
1738
1870
  if (bambuOptions?.skipObjects) args.push('--skip-objects', bambuOptions.skipObjects);
1739
1871
  if (cliBundle.filamentPaths.length > 0) {
@@ -1742,7 +1874,7 @@ export class STLManipulator extends EventEmitter {
1742
1874
  }
1743
1875
  if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
1744
1876
 
1745
- args.push(stlFilePath);
1877
+ args.push(inputPath);
1746
1878
  // Override outputFilePath for bambustudio since it produces 3MF, not gcode
1747
1879
  outputFilePath = bambuOutputPath;
1748
1880
  }
@@ -1762,7 +1894,7 @@ export class STLManipulator extends EventEmitter {
1762
1894
  if (error) {
1763
1895
  console.error(`Slicer Error: ${error.message}`);
1764
1896
  console.error(`Slicer Stderr: ${stderr}`);
1765
- reject(new Error(`Slicer failed: ${error.message}. Stderr: ${stderr}`));
1897
+ reject(new Error(describeSlicerFailure(error, stdout, stderr, cliResult)));
1766
1898
  } else {
1767
1899
  console.error(`Slicer Stdout: ${stdout}`);
1768
1900
  if (stderr) {
@@ -1825,6 +1957,14 @@ export class STLManipulator extends EventEmitter {
1825
1957
  throw error;
1826
1958
  } finally {
1827
1959
  this.activeOperations.delete(operationId);
1960
+ for (const file of temporaryFiles) {
1961
+ await fs.promises.rm(file, { force: true }).catch(error => console.error('Could not remove a temporary mesh:', error));
1962
+ }
1963
+ for (const directory of orcaTemporaryDirectories.reverse()) {
1964
+ await fs.promises.rm(directory, { recursive: true, force: true }).catch(error => {
1965
+ console.error('Could not remove temporary Orca profiles:', error);
1966
+ });
1967
+ }
1828
1968
  }
1829
1969
  }
1830
1970