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.
- package/CONTRIBUTORS.md +5 -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/slicer/orca-profile-cache.d.ts +18 -0
- package/dist/slicer/orca-profile-cache.js +460 -0
- package/dist/slicer/profile-flatten.js +1 -1
- package/dist/stl/stl-manipulator.d.ts +8 -0
- package/dist/stl/stl-manipulator.js +148 -21
- 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/slicer/orca-profile-cache.ts +379 -0
- package/src/slicer/profile-flatten.ts +1 -1
- package/src/stl/stl-manipulator.ts +156 -16
|
@@ -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
|
const readFileAsync = promisify(fs.readFile);
|
|
16
17
|
const writeFileAsync = promisify(fs.writeFile);
|
|
@@ -111,6 +112,54 @@ const PROFILE_METADATA_KEYS = new Set([
|
|
|
111
112
|
'name', 'type', 'from', 'inherits', 'instantiation', 'setting_id', 'version', 'is_custom_defined',
|
|
112
113
|
'compatible_printers', 'compatible_printers_condition', 'compatible_prints', 'compatible_prints_condition',
|
|
113
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
|
+
}
|
|
114
163
|
export class STLManipulator extends EventEmitter {
|
|
115
164
|
constructor(tempDir = path.join(process.cwd(), 'temp')) {
|
|
116
165
|
super();
|
|
@@ -184,7 +233,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
184
233
|
}
|
|
185
234
|
return value;
|
|
186
235
|
}
|
|
187
|
-
sanitizeProcessForOrca(processConfig, printerPreset) {
|
|
236
|
+
sanitizeProcessForOrca(processConfig, printerPreset, preserveExtrusionMode = false) {
|
|
188
237
|
const sanitized = { ...processConfig };
|
|
189
238
|
sanitized.type = 'process';
|
|
190
239
|
if (!sanitized.from || sanitized.from === 'project') {
|
|
@@ -200,7 +249,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
200
249
|
else if (sanitized.prime_tower_brim_width !== undefined) {
|
|
201
250
|
sanitized.prime_tower_brim_width = String(sanitized.prime_tower_brim_width);
|
|
202
251
|
}
|
|
203
|
-
return this.useAbsoluteExtrusionForOrca(sanitized);
|
|
252
|
+
return preserveExtrusionMode ? sanitized : this.useAbsoluteExtrusionForOrca(sanitized);
|
|
204
253
|
}
|
|
205
254
|
/** Orca slices with absolute E distances; per-layer `G92 E0` resets would break them. */
|
|
206
255
|
useAbsoluteExtrusionForOrca(config) {
|
|
@@ -218,7 +267,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
218
267
|
fs.writeFileSync(outPath, serialized);
|
|
219
268
|
return outPath;
|
|
220
269
|
}
|
|
221
|
-
async resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, activeProfilesRoot) {
|
|
270
|
+
async resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, activeProfilesRoot, preserveOrcaExtrusionMode = false) {
|
|
222
271
|
const roots = activeProfilesRoot
|
|
223
272
|
? [path.join(activeProfilesRoot, 'BBL')]
|
|
224
273
|
: this.getAvailableProfileRoots();
|
|
@@ -279,7 +328,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
279
328
|
type: 'process',
|
|
280
329
|
};
|
|
281
330
|
processPath = this.writeTempJson(outputBase, slicerType === 'orcaslicer' ? 'process_orca' : 'process', slicerType === 'orcaslicer'
|
|
282
|
-
? this.sanitizeProcessForOrca(mergedProcess, printerPreset)
|
|
331
|
+
? this.sanitizeProcessForOrca(mergedProcess, printerPreset, preserveOrcaExtrusionMode)
|
|
283
332
|
: mergedProcess);
|
|
284
333
|
const defaultFilamentProfiles = !bambuOptions?.loadFilaments && Array.isArray(parsedProfile.default_filament_profile)
|
|
285
334
|
? parsedProfile.default_filament_profile
|
|
@@ -317,7 +366,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
317
366
|
if (defaultProcessPath) {
|
|
318
367
|
processPath =
|
|
319
368
|
slicerType === 'orcaslicer'
|
|
320
|
-
? this.writeTempJson(outputBase, 'process_default_orca', this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset))
|
|
369
|
+
? this.writeTempJson(outputBase, 'process_default_orca', this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset, preserveOrcaExtrusionMode))
|
|
321
370
|
: defaultProcessPath;
|
|
322
371
|
}
|
|
323
372
|
}
|
|
@@ -744,6 +793,32 @@ export class STLManipulator extends EventEmitter {
|
|
|
744
793
|
this.activeOperations.delete(operationId);
|
|
745
794
|
}
|
|
746
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
|
+
}
|
|
747
822
|
/**
|
|
748
823
|
* Rotate an STL model around specific axes
|
|
749
824
|
*/
|
|
@@ -1240,6 +1315,9 @@ export class STLManipulator extends EventEmitter {
|
|
|
1240
1315
|
const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
|
|
1241
1316
|
let outputFilePath = path.join(this.tempDir, outputFileName);
|
|
1242
1317
|
let args = [];
|
|
1318
|
+
let cliResult;
|
|
1319
|
+
const orcaTemporaryDirectories = [];
|
|
1320
|
+
const temporaryFiles = [];
|
|
1243
1321
|
try {
|
|
1244
1322
|
if (progressCallback)
|
|
1245
1323
|
progressCallback(0, `Starting slicing with ${slicerType}...`);
|
|
@@ -1291,8 +1369,13 @@ export class STLManipulator extends EventEmitter {
|
|
|
1291
1369
|
const profileSlicerType = remote ? 'orcaslicer' : slicerType;
|
|
1292
1370
|
const bambuOutputPath = path.join(this.tempDir, outputBase + (remote ? `_${operationId}` : '') + '_sliced.3mf');
|
|
1293
1371
|
const outputDir = path.dirname(bambuOutputPath);
|
|
1294
|
-
const
|
|
1295
|
-
const
|
|
1372
|
+
const selectedProfilesRoot = detectProfilesRoot(remote ? undefined : slicerPath, profileSlicerType);
|
|
1373
|
+
const profilesRoot = !remote && profileSlicerType === 'orcaslicer'
|
|
1374
|
+
? await materializeOrcaProfilesRoot(selectedProfilesRoot, this.tempDir)
|
|
1375
|
+
: selectedProfilesRoot;
|
|
1376
|
+
if (profilesRoot !== selectedProfilesRoot)
|
|
1377
|
+
orcaTemporaryDirectories.push(profilesRoot);
|
|
1378
|
+
const rawBundle = await this.resolveBambuLikeSettingsBundle(outputBase, profileSlicerType, slicerProfile, printerPreset, bambuOptions, profilesRoot, profileSlicerType === 'orcaslicer' && profilesRoot !== selectedProfilesRoot);
|
|
1296
1379
|
// Resolve every BBL dependency before launching the slicer.
|
|
1297
1380
|
const settingsBundle = await this.maybeFlattenBundle(await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, profilesRoot);
|
|
1298
1381
|
// The machine preset assumes the stock hotend; record the installed one so
|
|
@@ -1312,14 +1395,21 @@ export class STLManipulator extends EventEmitter {
|
|
|
1312
1395
|
// absolute-extrusion normalization after resolving those ancestors.
|
|
1313
1396
|
// The machine owns the extrusion mode and layer G-code once process
|
|
1314
1397
|
// files lose machine keys, so normalize it there as well.
|
|
1315
|
-
if (profileSlicerType === 'orcaslicer' && settingsBundle.settingsArg) {
|
|
1398
|
+
if (profileSlicerType === 'orcaslicer' && settingsBundle.settingsArg && profilesRoot === selectedProfilesRoot) {
|
|
1316
1399
|
const [machinePath, processPath] = settingsBundle.settingsArg.split(';');
|
|
1317
1400
|
settingsBundle.settingsArg = [
|
|
1318
1401
|
this.writeTempJson(outputBase, 'machine_orca_resolved', this.useAbsoluteExtrusionForOrca(this.readJsonFile(machinePath))),
|
|
1319
1402
|
this.writeTempJson(outputBase, 'process_orca_resolved', this.sanitizeProcessForOrca(this.readJsonFile(processPath), printerPreset)),
|
|
1320
1403
|
].join(';');
|
|
1321
1404
|
}
|
|
1322
|
-
|
|
1405
|
+
let cliBundle = this.isolateMachineSettings(settingsBundle);
|
|
1406
|
+
let orcaExtraArgs = [];
|
|
1407
|
+
if (!remote && profileSlicerType === 'orcaslicer' && profilesRoot !== selectedProfilesRoot) {
|
|
1408
|
+
const staged = await stageOrcaCliProfiles(profilesRoot, this.tempDir, cliBundle);
|
|
1409
|
+
cliBundle = { ...cliBundle, settingsArg: staged.settingsArg, filamentPaths: staged.filamentPaths };
|
|
1410
|
+
orcaExtraArgs = staged.extraArgs;
|
|
1411
|
+
orcaTemporaryDirectories.push(staged.dataDir);
|
|
1412
|
+
}
|
|
1323
1413
|
if (remote) {
|
|
1324
1414
|
const [machinePath, processPath] = cliBundle.settingsArg?.split(';') ?? [];
|
|
1325
1415
|
if (!machinePath || !processPath)
|
|
@@ -1331,7 +1421,29 @@ export class STLManipulator extends EventEmitter {
|
|
|
1331
1421
|
});
|
|
1332
1422
|
break;
|
|
1333
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 });
|
|
1334
1445
|
args = [
|
|
1446
|
+
...orcaExtraArgs,
|
|
1335
1447
|
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
1336
1448
|
'--outputdir', outputDir,
|
|
1337
1449
|
'--export-3mf', path.basename(bambuOutputPath),
|
|
@@ -1358,16 +1470,23 @@ export class STLManipulator extends EventEmitter {
|
|
|
1358
1470
|
args.push('--orient', bambuOptions.orient ? '1' : '0');
|
|
1359
1471
|
if (bambuOptions?.arrange !== undefined)
|
|
1360
1472
|
args.push('--arrange', bambuOptions.arrange ? '1' : '0');
|
|
1361
|
-
if (bambuOptions?.repetitions !== undefined)
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
+
}
|
|
1371
1490
|
if (bambuOptions?.cloneObjects)
|
|
1372
1491
|
args.push('--clone-objects', bambuOptions.cloneObjects);
|
|
1373
1492
|
if (bambuOptions?.skipObjects)
|
|
@@ -1378,7 +1497,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1378
1497
|
}
|
|
1379
1498
|
if (bambuOptions?.loadFilamentIds)
|
|
1380
1499
|
args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
|
|
1381
|
-
args.push(
|
|
1500
|
+
args.push(inputPath);
|
|
1382
1501
|
// Override outputFilePath for bambustudio since it produces 3MF, not gcode
|
|
1383
1502
|
outputFilePath = bambuOutputPath;
|
|
1384
1503
|
}
|
|
@@ -1396,7 +1515,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1396
1515
|
if (error) {
|
|
1397
1516
|
console.error(`Slicer Error: ${error.message}`);
|
|
1398
1517
|
console.error(`Slicer Stderr: ${stderr}`);
|
|
1399
|
-
reject(new Error(
|
|
1518
|
+
reject(new Error(describeSlicerFailure(error, stdout, stderr, cliResult)));
|
|
1400
1519
|
}
|
|
1401
1520
|
else {
|
|
1402
1521
|
console.error(`Slicer Stdout: ${stdout}`);
|
|
@@ -1455,6 +1574,14 @@ export class STLManipulator extends EventEmitter {
|
|
|
1455
1574
|
}
|
|
1456
1575
|
finally {
|
|
1457
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
|
+
}
|
|
1580
|
+
for (const directory of orcaTemporaryDirectories.reverse()) {
|
|
1581
|
+
await fs.promises.rm(directory, { recursive: true, force: true }).catch(error => {
|
|
1582
|
+
console.error('Could not remove temporary Orca profiles:', error);
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1458
1585
|
}
|
|
1459
1586
|
}
|
|
1460
1587
|
/**
|