bambu-printer-mcp 1.1.33 → 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.
@@ -112,6 +112,54 @@ const PROFILE_METADATA_KEYS = new Set([
112
112
  'name', 'type', 'from', 'inherits', 'instantiation', 'setting_id', 'version', 'is_custom_defined',
113
113
  'compatible_printers', 'compatible_printers_condition', 'compatible_prints', 'compatible_prints_condition',
114
114
  ]);
115
+ function describeSlicerFailure(error, stdout, stderr, cli) {
116
+ const status = typeof error.signal === "string" && error.signal
117
+ ? `crashed with ${error.signal}`
118
+ : typeof error.code === "number" ? `exited with code ${error.code}` : "failed";
119
+ const reason = cli ? bambuCliFailureReason(error.code, cli) : undefined;
120
+ const output = stderr.trim() ? "" : stdout.trim().split("\n").slice(-20).join("\n");
121
+ return `Slicer failed: the slicer ${status}${reason ? ` (${reason})` : ""}. ${error.message.trim()}${output ? `\nSlicer output: ${output}` : ""}`;
122
+ }
123
+ // Bambu Studio and OrcaSlicer CLI errors (libslic3r/Utils.hpp). The process
124
+ // exits with the low byte of the code; on Linux result.json names the error.
125
+ const BAMBU_CLI_ERRORS = {
126
+ [-2]: "invalid parameters to the slicer",
127
+ [-3]: "input files not found",
128
+ [-5]: "preset file cannot be parsed",
129
+ [-6]: "model file cannot be parsed",
130
+ [-9]: "copying objects failed",
131
+ [-17]: "printer is not compatible with the process preset",
132
+ [-21]: "auto-arranging objects failed",
133
+ [-22]: "auto-orienting objects failed",
134
+ [-50]: "a plate is empty or has no object fully inside it; arrange the model or place it on the plate",
135
+ [-51]: "incorrect slicing parameters",
136
+ [-52]: "objects extend over the bed boundary",
137
+ [-60]: "no printable objects remain after skipping",
138
+ [-61]: "filaments are not compatible with the plate type",
139
+ [-62]: "the filaments' temperatures differ too much",
140
+ [-63]: "objects collide in print-by-object mode",
141
+ [-64]: "objects collide",
142
+ [-100]: "slicing the model failed",
143
+ [-101]: "G-code conflicts after slicing",
144
+ [-102]: "G-code in an unprintable area",
145
+ };
146
+ function bambuCliFailureReason(exitCode, cli) {
147
+ try {
148
+ if (fs.statSync(cli.resultPath).mtimeMs >= cli.startedAt) {
149
+ const result = JSON.parse(fs.readFileSync(cli.resultPath, "utf8"));
150
+ if (typeof result.error_string === "string" && result.error_string.trim()) {
151
+ return `${result.error_string.trim()} [${result.return_code}]`;
152
+ }
153
+ }
154
+ }
155
+ catch {
156
+ // result.json is written only on Linux and only for handled errors.
157
+ }
158
+ if (typeof exitCode !== "number")
159
+ return undefined;
160
+ const code = Object.keys(BAMBU_CLI_ERRORS).map(Number).find(value => (value & 0xff) === exitCode);
161
+ return code === undefined ? undefined : `${BAMBU_CLI_ERRORS[code]} [${code}]`;
162
+ }
115
163
  export class STLManipulator extends EventEmitter {
116
164
  constructor(tempDir = path.join(process.cwd(), 'temp')) {
117
165
  super();
@@ -745,6 +793,32 @@ export class STLManipulator extends EventEmitter {
745
793
  this.activeOperations.delete(operationId);
746
794
  }
747
795
  }
796
+ /**
797
+ * Apply slice_stl's scale and rotations to a copy of the mesh as OrcaSlicer's
798
+ * CLI would, about the model's centre. Orca adds each --rotate* to the
799
+ * object's Euler angles (ModelVolume::rotate), so the result rotates about X,
800
+ * then Y, then Z whatever the argument order, after scaling. The slicer then
801
+ * places the result on the plate.
802
+ */
803
+ async transformMeshForCli(stlFilePath, options) {
804
+ const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
805
+ const center = new THREE.Vector3();
806
+ boundingBox.getCenter(center);
807
+ const radians = (degrees) => degrees * Math.PI / 180;
808
+ geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
809
+ if (options.scale !== undefined)
810
+ geometry.applyMatrix4(new THREE.Matrix4().makeScale(options.scale, options.scale, options.scale));
811
+ if (options.rotateX)
812
+ geometry.applyMatrix4(new THREE.Matrix4().makeRotationX(radians(options.rotateX)));
813
+ if (options.rotateY)
814
+ geometry.applyMatrix4(new THREE.Matrix4().makeRotationY(radians(options.rotateY)));
815
+ if (options.rotate)
816
+ geometry.applyMatrix4(new THREE.Matrix4().makeRotationZ(radians(options.rotate)));
817
+ geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(center.x, center.y, center.z));
818
+ const outputFilePath = path.join(this.tempDir, `${path.basename(stlFilePath, path.extname(stlFilePath))}_transformed_${this.generateOperationId()}.stl`);
819
+ await this.saveSTL(geometry, outputFilePath);
820
+ return outputFilePath;
821
+ }
748
822
  /**
749
823
  * Rotate an STL model around specific axes
750
824
  */
@@ -1241,7 +1315,9 @@ export class STLManipulator extends EventEmitter {
1241
1315
  const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
1242
1316
  let outputFilePath = path.join(this.tempDir, outputFileName);
1243
1317
  let args = [];
1318
+ let cliResult;
1244
1319
  const orcaTemporaryDirectories = [];
1320
+ const temporaryFiles = [];
1245
1321
  try {
1246
1322
  if (progressCallback)
1247
1323
  progressCallback(0, `Starting slicing with ${slicerType}...`);
@@ -1345,6 +1421,27 @@ export class STLManipulator extends EventEmitter {
1345
1421
  });
1346
1422
  break;
1347
1423
  }
1424
+ // OrcaSlicer's CLI crashes (SIGSEGV in PartPlate::check_outside,
1425
+ // which needs the GUI's Plater) when --scale or --rotate-x/-y move an
1426
+ // object below the bed and it arranges, the default for STL; it also
1427
+ // rejects --repetitions for one STL. Apply STL transforms to the mesh
1428
+ // in the CLI's order instead, and refuse the 3MF case it cannot fix.
1429
+ const orcaCli = slicerType === 'orcaslicer' || slicerType === 'orcaslicer-bambulab';
1430
+ const tilts = !!bambuOptions?.rotateX || !!bambuOptions?.rotateY;
1431
+ const meshTransform = orcaCli && !is3mf && (bambuOptions?.scale !== undefined || tilts);
1432
+ if (orcaCli && is3mf && bambuOptions?.arrange === true && ((bambuOptions.scale ?? 1) > 1 || tilts)) {
1433
+ throw new Error("OrcaSlicer's CLI crashes when scale above 1, rotate_x or rotate_y moves a 3MF object below the bed while arranging. " +
1434
+ "Set arrange to false, or apply the transform in the slicer and export the project.");
1435
+ }
1436
+ if (orcaCli && !is3mf && bambuOptions?.repetitions !== undefined && bambuOptions.cloneObjects) {
1437
+ throw new Error("Use either repetitions or clone_objects for an STL with OrcaSlicer, not both.");
1438
+ }
1439
+ const inputPath = meshTransform ? await this.transformMeshForCli(stlFilePath, bambuOptions) : stlFilePath;
1440
+ if (inputPath !== stlFilePath)
1441
+ temporaryFiles.push(inputPath);
1442
+ cliResult = { resultPath: path.join(outputDir, 'result.json'), startedAt: Date.now() };
1443
+ // A result.json left by an earlier run must not explain this one.
1444
+ await fs.promises.rm(cliResult.resultPath, { force: true });
1348
1445
  args = [
1349
1446
  ...orcaExtraArgs,
1350
1447
  '--slice', String(bambuOptions?.slicePlate ?? 0),
@@ -1373,16 +1470,23 @@ export class STLManipulator extends EventEmitter {
1373
1470
  args.push('--orient', bambuOptions.orient ? '1' : '0');
1374
1471
  if (bambuOptions?.arrange !== undefined)
1375
1472
  args.push('--arrange', bambuOptions.arrange ? '1' : '0');
1376
- if (bambuOptions?.repetitions !== undefined)
1377
- args.push('--repetitions', String(bambuOptions.repetitions));
1378
- if (bambuOptions?.scale !== undefined)
1379
- args.push('--scale', String(bambuOptions.scale));
1380
- if (bambuOptions?.rotate !== undefined)
1381
- args.push('--rotate', String(bambuOptions.rotate));
1382
- if (bambuOptions?.rotateX !== undefined)
1383
- args.push('--rotate-x', String(bambuOptions.rotateX));
1384
- if (bambuOptions?.rotateY !== undefined)
1385
- args.push('--rotate-y', String(bambuOptions.rotateY));
1473
+ if (bambuOptions?.repetitions !== undefined) {
1474
+ // OrcaSlicer counts copies of a single loaded STL with --clone-objects.
1475
+ if (orcaCli && !is3mf)
1476
+ args.push('--clone-objects', String(bambuOptions.repetitions));
1477
+ else
1478
+ args.push('--repetitions', String(bambuOptions.repetitions));
1479
+ }
1480
+ if (!meshTransform) {
1481
+ if (bambuOptions?.scale !== undefined)
1482
+ args.push('--scale', String(bambuOptions.scale));
1483
+ if (bambuOptions?.rotate !== undefined)
1484
+ args.push('--rotate', String(bambuOptions.rotate));
1485
+ if (bambuOptions?.rotateX !== undefined)
1486
+ args.push('--rotate-x', String(bambuOptions.rotateX));
1487
+ if (bambuOptions?.rotateY !== undefined)
1488
+ args.push('--rotate-y', String(bambuOptions.rotateY));
1489
+ }
1386
1490
  if (bambuOptions?.cloneObjects)
1387
1491
  args.push('--clone-objects', bambuOptions.cloneObjects);
1388
1492
  if (bambuOptions?.skipObjects)
@@ -1393,7 +1497,7 @@ export class STLManipulator extends EventEmitter {
1393
1497
  }
1394
1498
  if (bambuOptions?.loadFilamentIds)
1395
1499
  args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
1396
- args.push(stlFilePath);
1500
+ args.push(inputPath);
1397
1501
  // Override outputFilePath for bambustudio since it produces 3MF, not gcode
1398
1502
  outputFilePath = bambuOutputPath;
1399
1503
  }
@@ -1411,7 +1515,7 @@ export class STLManipulator extends EventEmitter {
1411
1515
  if (error) {
1412
1516
  console.error(`Slicer Error: ${error.message}`);
1413
1517
  console.error(`Slicer Stderr: ${stderr}`);
1414
- reject(new Error(`Slicer failed: ${error.message}. Stderr: ${stderr}`));
1518
+ reject(new Error(describeSlicerFailure(error, stdout, stderr, cliResult)));
1415
1519
  }
1416
1520
  else {
1417
1521
  console.error(`Slicer Stdout: ${stdout}`);
@@ -1470,6 +1574,9 @@ export class STLManipulator extends EventEmitter {
1470
1574
  }
1471
1575
  finally {
1472
1576
  this.activeOperations.delete(operationId);
1577
+ for (const file of temporaryFiles) {
1578
+ await fs.promises.rm(file, { force: true }).catch(error => console.error('Could not remove a temporary mesh:', error));
1579
+ }
1473
1580
  for (const directory of orcaTemporaryDirectories.reverse()) {
1474
1581
  await fs.promises.rm(directory, { recursive: true, force: true }).catch(error => {
1475
1582
  console.error('Could not remove temporary Orca profiles:', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bambu-printer-mcp",
3
- "version": "1.1.33",
3
+ "version": "1.1.34",
4
4
  "description": "MCP server for Bambu Lab 3D printers with STL manipulation, slicing, and print management",
5
5
  "main": "dist/index.js",
6
6
  "repository": {