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
|
|
|
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
|
|
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
|
-
|
|
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)
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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(
|
|
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(
|
|
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
|
|