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.
- package/CONTRIBUTORS.md +3 -1
- package/README.md +4 -2
- package/dist/bambu-network-bridge.d.ts +78 -4
- package/dist/bambu-network-bridge.js +292 -47
- package/dist/bambu-network-certs.d.ts +13 -0
- package/dist/bambu-network-certs.js +152 -0
- package/dist/index.js +28 -11
- package/dist/printers/bambu.d.ts +12 -2
- package/dist/printers/bambu.js +74 -41
- package/dist/safety/printer-state.d.ts +2 -0
- package/dist/safety/printer-state.js +42 -1
- package/dist/stl/stl-manipulator.d.ts +8 -0
- package/dist/stl/stl-manipulator.js +119 -12
- package/package.json +1 -1
- package/src/bambu-network-bridge.ts +324 -55
- package/src/bambu-network-certs.ts +154 -0
- package/src/index.ts +43 -25
- package/src/printers/bambu.ts +74 -58
- package/src/safety/printer-state.ts +42 -1
- package/src/stl/stl-manipulator.ts +126 -7
|
@@ -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
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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(
|
|
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(
|
|
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);
|