bambu-printer-mcp 1.0.5 → 1.1.2
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/README.md +537 -38
- package/dist/3mf_parser.d.ts +15 -1
- package/dist/3mf_parser.js +258 -1
- package/dist/ams-mapping.d.ts +3 -0
- package/dist/ams-mapping.js +45 -0
- package/dist/bambu-network-bridge.d.ts +63 -0
- package/dist/bambu-network-bridge.js +393 -0
- package/dist/index.js +2084 -95
- package/dist/printers/bambu.d.ts +133 -0
- package/dist/printers/bambu.js +1004 -54
- package/dist/slicer/profile-flatten.d.ts +75 -0
- package/dist/slicer/profile-flatten.js +446 -0
- package/dist/stl/stl-manipulator.d.ts +33 -1
- package/dist/stl/stl-manipulator.js +370 -33
- package/dist/types.d.ts +35 -0
- package/package.json +8 -3
- package/patches/bambu-node+3.22.21.patch +16 -0
- package/src/3mf_parser.ts +301 -3
- package/src/ams-mapping.ts +47 -0
- package/src/bambu-network-bridge.ts +526 -0
- package/src/index.ts +2648 -108
- package/src/printers/bambu.ts +1215 -57
- package/src/slicer/profile-flatten.ts +562 -0
- package/src/stl/stl-manipulator.ts +475 -36
- package/src/types.ts +41 -0
|
@@ -3,13 +3,103 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
|
3
3
|
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js';
|
|
4
4
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
+
import os from 'os';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import { promisify } from 'util';
|
|
8
9
|
import { EventEmitter } from 'events';
|
|
9
10
|
import * as crypto from 'crypto';
|
|
10
11
|
import { execFile } from 'child_process';
|
|
12
|
+
import { flattenForCli, detectProfilesRoot } from '../slicer/profile-flatten.js';
|
|
11
13
|
const readFileAsync = promisify(fs.readFile);
|
|
12
14
|
const writeFileAsync = promisify(fs.writeFile);
|
|
15
|
+
const BAMBU_PROFILE_ROOTS = [
|
|
16
|
+
'/Applications/BambuStudio.app/Contents/Resources/profiles/BBL',
|
|
17
|
+
'/Applications/OrcaSlicer.app/Contents/Resources/profiles/BBL',
|
|
18
|
+
];
|
|
19
|
+
const BAMBU_CLI_BED_TYPES = {
|
|
20
|
+
textured_plate: 'Textured PEI Plate',
|
|
21
|
+
cool_plate: 'Cool Plate',
|
|
22
|
+
engineering_plate: 'Engineering Plate',
|
|
23
|
+
hot_plate: 'High Temp Plate',
|
|
24
|
+
};
|
|
25
|
+
export const SLICER_TYPES = [
|
|
26
|
+
'bambustudio',
|
|
27
|
+
'orcaslicer',
|
|
28
|
+
'orcaslicer-bambulab',
|
|
29
|
+
'prusaslicer',
|
|
30
|
+
'cura',
|
|
31
|
+
'slic3r',
|
|
32
|
+
];
|
|
33
|
+
const SLICER_TYPE_ALIASES = {
|
|
34
|
+
bambustudio: 'bambustudio',
|
|
35
|
+
'bambu-studio': 'bambustudio',
|
|
36
|
+
bblstudio: 'bambustudio',
|
|
37
|
+
orcaslicer: 'orcaslicer',
|
|
38
|
+
'orca-slicer': 'orcaslicer',
|
|
39
|
+
orca: 'orcaslicer',
|
|
40
|
+
'orcaslicer-bambulab': 'orcaslicer-bambulab',
|
|
41
|
+
'orca-slicer-bambulab': 'orcaslicer-bambulab',
|
|
42
|
+
'orca-bambulab': 'orcaslicer-bambulab',
|
|
43
|
+
'fulu-orca': 'orcaslicer-bambulab',
|
|
44
|
+
'fulu-orcaslicer': 'orcaslicer-bambulab',
|
|
45
|
+
'fulu-orca-slicer': 'orcaslicer-bambulab',
|
|
46
|
+
'orca-studio': 'orcaslicer-bambulab',
|
|
47
|
+
orcastudio: 'orcaslicer-bambulab',
|
|
48
|
+
'orcaslicer-bmcu': 'orcaslicer-bambulab',
|
|
49
|
+
'orca-bmcu': 'orcaslicer-bambulab',
|
|
50
|
+
prusaslicer: 'prusaslicer',
|
|
51
|
+
'prusa-slicer': 'prusaslicer',
|
|
52
|
+
cura: 'cura',
|
|
53
|
+
curaengine: 'cura',
|
|
54
|
+
slic3r: 'slic3r',
|
|
55
|
+
};
|
|
56
|
+
export function normalizeSlicerType(rawSlicerType) {
|
|
57
|
+
const key = rawSlicerType.trim().toLowerCase().replace(/_/g, '-');
|
|
58
|
+
const normalized = SLICER_TYPE_ALIASES[key];
|
|
59
|
+
if (!normalized) {
|
|
60
|
+
throw new Error(`Unsupported slicer_type: "${rawSlicerType}". Valid values: ${SLICER_TYPES.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
return normalized;
|
|
63
|
+
}
|
|
64
|
+
export function isBambuCompatibleSlicer(slicerType) {
|
|
65
|
+
return (slicerType === 'bambustudio' ||
|
|
66
|
+
slicerType === 'orcaslicer' ||
|
|
67
|
+
slicerType === 'orcaslicer-bambulab');
|
|
68
|
+
}
|
|
69
|
+
function isPathLikeExecutable(slicerPath) {
|
|
70
|
+
return (path.isAbsolute(slicerPath) ||
|
|
71
|
+
slicerPath.startsWith("./") ||
|
|
72
|
+
slicerPath.startsWith("../") ||
|
|
73
|
+
slicerPath.includes("/") ||
|
|
74
|
+
slicerPath.includes("\\") ||
|
|
75
|
+
/^[A-Za-z]:/.test(slicerPath));
|
|
76
|
+
}
|
|
77
|
+
function configuredBambuProfileDirs() {
|
|
78
|
+
const override = process.env.BAMBU_SLICER_PROFILE_DIRS;
|
|
79
|
+
if (override !== undefined) {
|
|
80
|
+
return override.split(path.delimiter).filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
const home = os.homedir();
|
|
83
|
+
const macAppSupport = path.join(home, 'Library', 'Application Support');
|
|
84
|
+
const linuxConfig = path.join(home, '.config');
|
|
85
|
+
const appData = process.env.APPDATA;
|
|
86
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
87
|
+
const appNames = [
|
|
88
|
+
'BambuStudio',
|
|
89
|
+
'Bambu Studio',
|
|
90
|
+
'OrcaSlicer',
|
|
91
|
+
'Orca Slicer',
|
|
92
|
+
'OrcaStudio',
|
|
93
|
+
'Orca Studio',
|
|
94
|
+
];
|
|
95
|
+
const roots = [
|
|
96
|
+
macAppSupport,
|
|
97
|
+
linuxConfig,
|
|
98
|
+
...(appData ? [appData] : []),
|
|
99
|
+
...(localAppData ? [localAppData] : []),
|
|
100
|
+
];
|
|
101
|
+
return roots.flatMap((root) => appNames.map((appName) => path.join(root, appName, 'system', 'BBL')));
|
|
102
|
+
}
|
|
13
103
|
export class STLManipulator extends EventEmitter {
|
|
14
104
|
constructor(tempDir = path.join(process.cwd(), 'temp')) {
|
|
15
105
|
super();
|
|
@@ -26,6 +116,266 @@ export class STLManipulator extends EventEmitter {
|
|
|
26
116
|
generateOperationId() {
|
|
27
117
|
return crypto.randomUUID();
|
|
28
118
|
}
|
|
119
|
+
getAvailableProfileRoots() {
|
|
120
|
+
return [...BAMBU_PROFILE_ROOTS, ...configuredBambuProfileDirs()].filter((root) => fs.existsSync(root));
|
|
121
|
+
}
|
|
122
|
+
findProfileFile(category, profileName) {
|
|
123
|
+
if (!profileName) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
for (const root of this.getAvailableProfileRoots()) {
|
|
127
|
+
const candidate = path.join(root, category, `${profileName}.json`);
|
|
128
|
+
if (fs.existsSync(candidate)) {
|
|
129
|
+
return candidate;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
buildFilamentIdIndex() {
|
|
135
|
+
const index = new Map();
|
|
136
|
+
for (const root of this.getAvailableProfileRoots()) {
|
|
137
|
+
const filamentDir = path.join(root, 'filament');
|
|
138
|
+
if (!fs.existsSync(filamentDir)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
for (const entry of fs.readdirSync(filamentDir)) {
|
|
142
|
+
if (!entry.endsWith('.json')) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const filePath = path.join(filamentDir, entry);
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
148
|
+
const filamentId = typeof parsed?.filament_id === 'string' ? parsed.filament_id.trim() : '';
|
|
149
|
+
if (filamentId && !index.has(filamentId)) {
|
|
150
|
+
index.set(filamentId, filePath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Ignore malformed JSON.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return index;
|
|
159
|
+
}
|
|
160
|
+
readJsonFile(filePath) {
|
|
161
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
162
|
+
}
|
|
163
|
+
stripAbsoluteExtruderResets(value) {
|
|
164
|
+
if (typeof value === 'string') {
|
|
165
|
+
return value
|
|
166
|
+
.split(/\r?\n/)
|
|
167
|
+
.filter((line) => line.trim().toUpperCase() !== 'G92 E0')
|
|
168
|
+
.join('\n');
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(value)) {
|
|
171
|
+
return value.filter((line) => String(line).trim().toUpperCase() !== 'G92 E0');
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
sanitizeProcessForOrca(processConfig, printerPreset) {
|
|
176
|
+
const sanitized = { ...processConfig };
|
|
177
|
+
sanitized.type = 'process';
|
|
178
|
+
if (!sanitized.from || sanitized.from === 'project') {
|
|
179
|
+
sanitized.from = 'User';
|
|
180
|
+
}
|
|
181
|
+
if (printerPreset) {
|
|
182
|
+
sanitized.compatible_printers = [printerPreset];
|
|
183
|
+
}
|
|
184
|
+
if (sanitized.prime_tower_brim_width !== undefined &&
|
|
185
|
+
Number(sanitized.prime_tower_brim_width) < 0) {
|
|
186
|
+
sanitized.prime_tower_brim_width = '0';
|
|
187
|
+
}
|
|
188
|
+
else if (sanitized.prime_tower_brim_width !== undefined) {
|
|
189
|
+
sanitized.prime_tower_brim_width = String(sanitized.prime_tower_brim_width);
|
|
190
|
+
}
|
|
191
|
+
sanitized.use_relative_e_distances = '0';
|
|
192
|
+
sanitized.before_layer_change_gcode = this.stripAbsoluteExtruderResets(sanitized.before_layer_change_gcode);
|
|
193
|
+
sanitized.layer_gcode = this.stripAbsoluteExtruderResets(sanitized.layer_gcode);
|
|
194
|
+
sanitized.layer_change_gcode = this.stripAbsoluteExtruderResets(sanitized.layer_change_gcode);
|
|
195
|
+
return sanitized;
|
|
196
|
+
}
|
|
197
|
+
writeTempJson(outputBase, suffix, value) {
|
|
198
|
+
const outPath = path.join(this.tempDir, `${outputBase}_${suffix}.json`);
|
|
199
|
+
fs.writeFileSync(outPath, JSON.stringify(value, null, 2));
|
|
200
|
+
return outPath;
|
|
201
|
+
}
|
|
202
|
+
resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions) {
|
|
203
|
+
const machinePath = this.findProfileFile('machine', printerPreset);
|
|
204
|
+
const machineConfig = machinePath ? this.readJsonFile(machinePath) : null;
|
|
205
|
+
const hasSlicerProfile = !!slicerProfile && fs.existsSync(slicerProfile);
|
|
206
|
+
const filamentPaths = [];
|
|
207
|
+
let processPath;
|
|
208
|
+
let parsedProfile = null;
|
|
209
|
+
if (hasSlicerProfile) {
|
|
210
|
+
try {
|
|
211
|
+
parsedProfile = this.readJsonFile(slicerProfile);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
parsedProfile = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (parsedProfile && typeof parsedProfile === 'object') {
|
|
218
|
+
const inheritedProcessName = (typeof parsedProfile.inherits === 'string' && parsedProfile.inherits) ||
|
|
219
|
+
(typeof parsedProfile.print_settings_id === 'string' &&
|
|
220
|
+
parsedProfile.print_settings_id !== parsedProfile.name
|
|
221
|
+
? parsedProfile.print_settings_id
|
|
222
|
+
: undefined) ||
|
|
223
|
+
(typeof parsedProfile.default_print_profile === 'string'
|
|
224
|
+
? parsedProfile.default_print_profile
|
|
225
|
+
: undefined) ||
|
|
226
|
+
(typeof machineConfig?.default_print_profile === 'string'
|
|
227
|
+
? machineConfig.default_print_profile
|
|
228
|
+
: undefined);
|
|
229
|
+
const inheritedProcessPath = this.findProfileFile('process', inheritedProcessName);
|
|
230
|
+
const inheritedProcess = inheritedProcessPath && fs.existsSync(inheritedProcessPath)
|
|
231
|
+
? this.readJsonFile(inheritedProcessPath)
|
|
232
|
+
: {};
|
|
233
|
+
const mergedProcess = {
|
|
234
|
+
...inheritedProcess,
|
|
235
|
+
...parsedProfile,
|
|
236
|
+
type: 'process',
|
|
237
|
+
};
|
|
238
|
+
processPath = this.writeTempJson(outputBase, slicerType === 'orcaslicer' ? 'process_orca' : 'process', slicerType === 'orcaslicer'
|
|
239
|
+
? this.sanitizeProcessForOrca(mergedProcess, printerPreset)
|
|
240
|
+
: mergedProcess);
|
|
241
|
+
const defaultFilamentProfiles = Array.isArray(parsedProfile.default_filament_profile)
|
|
242
|
+
? parsedProfile.default_filament_profile
|
|
243
|
+
: [];
|
|
244
|
+
for (const profileName of defaultFilamentProfiles) {
|
|
245
|
+
const filamentPath = this.findProfileFile('filament', String(profileName));
|
|
246
|
+
if (filamentPath) {
|
|
247
|
+
filamentPaths.push(filamentPath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
|
|
251
|
+
const filamentIdIndex = this.buildFilamentIdIndex();
|
|
252
|
+
for (const filamentId of parsedProfile.filament_ids) {
|
|
253
|
+
const filamentPath = filamentIdIndex.get(String(filamentId));
|
|
254
|
+
if (filamentPath) {
|
|
255
|
+
filamentPaths.push(filamentPath);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (hasSlicerProfile) {
|
|
261
|
+
processPath = slicerProfile;
|
|
262
|
+
}
|
|
263
|
+
if (!processPath) {
|
|
264
|
+
const defaultProcessName = typeof machineConfig?.default_print_profile === 'string'
|
|
265
|
+
? machineConfig.default_print_profile
|
|
266
|
+
: undefined;
|
|
267
|
+
const defaultProcessPath = this.findProfileFile('process', defaultProcessName);
|
|
268
|
+
if (defaultProcessPath) {
|
|
269
|
+
processPath =
|
|
270
|
+
slicerType === 'orcaslicer'
|
|
271
|
+
? this.writeTempJson(outputBase, 'process_default_orca', this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset))
|
|
272
|
+
: defaultProcessPath;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!bambuOptions?.loadFilaments &&
|
|
276
|
+
filamentPaths.length === 0 &&
|
|
277
|
+
Array.isArray(machineConfig?.default_filament_profile)) {
|
|
278
|
+
for (const profileName of machineConfig.default_filament_profile) {
|
|
279
|
+
const filamentPath = this.findProfileFile('filament', String(profileName));
|
|
280
|
+
if (filamentPath) {
|
|
281
|
+
filamentPaths.push(filamentPath);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (bambuOptions?.loadFilaments) {
|
|
286
|
+
filamentPaths.length = 0;
|
|
287
|
+
for (const filamentPath of bambuOptions.loadFilaments.split(';')) {
|
|
288
|
+
const trimmed = filamentPath.trim();
|
|
289
|
+
if (trimmed) {
|
|
290
|
+
filamentPaths.push(trimmed);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const settingsParts = [machinePath, processPath].filter((entry) => Boolean(entry));
|
|
295
|
+
return {
|
|
296
|
+
settingsArg: settingsParts.length > 0 ? settingsParts.join(';') : undefined,
|
|
297
|
+
filamentPaths: Array.from(new Set(filamentPaths)),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Optionally rewrite a Bambu-like settings bundle so the paths point at
|
|
302
|
+
* fully-flattened temp configs instead of the BBL-shipped leaf JSONs.
|
|
303
|
+
*
|
|
304
|
+
* BambuStudio's CLI does not resolve the `inherits` chain when loading
|
|
305
|
+
* profiles via --load-settings / --load-filaments, which causes a
|
|
306
|
+
* cluster of upstream bugs (see https://github.com/bambulab/BambuStudio/issues/9636
|
|
307
|
+
* and #9968). Our flattener (src/slicer/profile-flatten.ts) reproduces
|
|
308
|
+
* what the GUI does at slice time so the CLI accepts the configs.
|
|
309
|
+
*
|
|
310
|
+
* Opt-in via `BAMBU_CLI_FLATTEN=true`. When the env var is unset or
|
|
311
|
+
* not "true"/"1", returns the bundle unchanged so behavior is
|
|
312
|
+
* backward-compatible. When enabled, only BBL-shipped leaves get
|
|
313
|
+
* flattened; user-provided custom configs pass through untouched.
|
|
314
|
+
*/
|
|
315
|
+
async maybeFlattenBundle(bundle, bambuOptions, activeSlicerPath) {
|
|
316
|
+
const flag = process.env.BAMBU_CLI_FLATTEN;
|
|
317
|
+
if (flag !== 'true' && flag !== '1')
|
|
318
|
+
return bundle;
|
|
319
|
+
if (!bundle.settingsArg)
|
|
320
|
+
return bundle;
|
|
321
|
+
// settingsArg is "machine.json;process.json".
|
|
322
|
+
const parts = bundle.settingsArg.split(';').filter(Boolean);
|
|
323
|
+
if (parts.length < 2)
|
|
324
|
+
return bundle;
|
|
325
|
+
const [machinePath, processPath] = parts;
|
|
326
|
+
// Pull leaf names from each JSON's `name` field. If any is missing or
|
|
327
|
+
// looks non-BBL (e.g. a user-saved custom config), bail out and
|
|
328
|
+
// return the original bundle untouched.
|
|
329
|
+
const machineLeaf = this.readLeafName(machinePath);
|
|
330
|
+
const processLeaf = this.readLeafName(processPath);
|
|
331
|
+
const filamentLeaves = bundle.filamentPaths
|
|
332
|
+
.map((p) => this.readLeafName(p))
|
|
333
|
+
.filter((n) => Boolean(n));
|
|
334
|
+
if (!machineLeaf || !processLeaf || filamentLeaves.length === 0) {
|
|
335
|
+
console.log('[cli-flatten] skipping: could not derive BBL leaf names from bundle paths');
|
|
336
|
+
return bundle;
|
|
337
|
+
}
|
|
338
|
+
const bedType = this.resolveBambuStudioBedType(bambuOptions?.bedType);
|
|
339
|
+
try {
|
|
340
|
+
const profilesRoot = detectProfilesRoot(activeSlicerPath || process.env.SLICER_PATH);
|
|
341
|
+
const flat = await flattenForCli({
|
|
342
|
+
machineLeaf,
|
|
343
|
+
processLeaf,
|
|
344
|
+
filamentLeaves,
|
|
345
|
+
profilesRoot,
|
|
346
|
+
tempDir: this.tempDir,
|
|
347
|
+
bedType,
|
|
348
|
+
});
|
|
349
|
+
console.log(`[cli-flatten] applied for ${machineLeaf} (cliOverlay=${flat.meta.cliOverlayApplied})`);
|
|
350
|
+
return {
|
|
351
|
+
settingsArg: `${flat.machinePath};${flat.processPath}`,
|
|
352
|
+
filamentPaths: flat.filamentPaths,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
console.error(`[cli-flatten] failed, falling back to unflattened bundle: ${err?.message ?? err}`);
|
|
357
|
+
return bundle;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
resolveBambuStudioBedType(bedType) {
|
|
361
|
+
if (!bedType)
|
|
362
|
+
return undefined;
|
|
363
|
+
const normalized = bedType.trim().toLowerCase();
|
|
364
|
+
if (normalized === 'supertack_plate') {
|
|
365
|
+
throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
|
|
366
|
+
}
|
|
367
|
+
return BAMBU_CLI_BED_TYPES[normalized] || bedType;
|
|
368
|
+
}
|
|
369
|
+
/** Read a profile JSON's top-level `name` field, or null if unreadable. */
|
|
370
|
+
readLeafName(filePath) {
|
|
371
|
+
try {
|
|
372
|
+
const data = this.readJsonFile(filePath);
|
|
373
|
+
return typeof data?.name === 'string' && data.name.length > 0 ? data.name : null;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
29
379
|
/**
|
|
30
380
|
* Load STL file and return geometry and bounding box
|
|
31
381
|
*/
|
|
@@ -744,7 +1094,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
744
1094
|
async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset, bambuOptions) {
|
|
745
1095
|
const operationId = this.generateOperationId();
|
|
746
1096
|
this.activeOperations.set(operationId, true);
|
|
747
|
-
if (!fs.existsSync(slicerPath)) {
|
|
1097
|
+
if (isPathLikeExecutable(slicerPath) && !fs.existsSync(slicerPath)) {
|
|
748
1098
|
throw new Error(`Slicer executable not found at: ${slicerPath}`);
|
|
749
1099
|
}
|
|
750
1100
|
if (slicerProfile && !fs.existsSync(slicerProfile)) {
|
|
@@ -769,24 +1119,6 @@ export class STLManipulator extends EventEmitter {
|
|
|
769
1119
|
// Remove empty profile arg if not provided
|
|
770
1120
|
args = args.filter(arg => arg !== '');
|
|
771
1121
|
break;
|
|
772
|
-
case 'orcaslicer': // Add OrcaSlicer case
|
|
773
|
-
args = [
|
|
774
|
-
// OrcaSlicer might use --load-settings like Bambu, or --load like Prusa
|
|
775
|
-
// Assuming --load-settings based on discussion, adjust if needed.
|
|
776
|
-
// Also assuming profile path points to the main .ini or .json config.
|
|
777
|
-
'--load-settings', slicerProfile || '',
|
|
778
|
-
'--output', outputFilePath, // Assuming --output works like PrusaSlicer
|
|
779
|
-
stlFilePath
|
|
780
|
-
];
|
|
781
|
-
// Alternative if --output doesn't specify filename:
|
|
782
|
-
// args = [
|
|
783
|
-
// '--load-settings', slicerProfile || '',
|
|
784
|
-
// '--outputdir', this.tempDir,
|
|
785
|
-
// stlFilePath
|
|
786
|
-
// ];
|
|
787
|
-
// Remove empty profile arg if not provided
|
|
788
|
-
args = args.filter(arg => arg !== '');
|
|
789
|
-
break;
|
|
790
1122
|
case 'cura':
|
|
791
1123
|
// CuraEngine CLI args are different, often requiring -s for settings
|
|
792
1124
|
// Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
|
|
@@ -802,30 +1134,33 @@ export class STLManipulator extends EventEmitter {
|
|
|
802
1134
|
}
|
|
803
1135
|
break;
|
|
804
1136
|
case 'bambustudio':
|
|
805
|
-
|
|
1137
|
+
case 'orcaslicer':
|
|
1138
|
+
case 'orcaslicer-bambulab':
|
|
1139
|
+
// Bambu-compatible CLI: slice and export as 3MF with embedded gcode.
|
|
1140
|
+
// FULU's OrcaSlicer-bambulab fork keeps this Bambu-style path.
|
|
806
1141
|
// For 3MF input: slice in place and export sliced 3MF
|
|
807
1142
|
// For STL input: slice and export as 3MF
|
|
808
1143
|
{
|
|
809
1144
|
const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
|
|
810
1145
|
const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
|
|
811
1146
|
const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
|
|
1147
|
+
const outputDir = path.dirname(bambuOutputPath);
|
|
1148
|
+
const rawBundle = this.resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions);
|
|
1149
|
+
// Opt-in flatten step: walks the BBL `inherits` chain so the
|
|
1150
|
+
// CLI accepts what we hand it. See maybeFlattenBundle docstring.
|
|
1151
|
+
const settingsBundle = slicerType === 'bambustudio'
|
|
1152
|
+
? await this.maybeFlattenBundle(rawBundle, bambuOptions, slicerPath)
|
|
1153
|
+
: rawBundle;
|
|
812
1154
|
args = [
|
|
813
1155
|
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
814
|
-
'--
|
|
1156
|
+
'--outputdir', outputDir,
|
|
1157
|
+
'--export-3mf', path.basename(bambuOutputPath),
|
|
815
1158
|
];
|
|
816
|
-
if (
|
|
817
|
-
args.push('--load-settings',
|
|
1159
|
+
if (settingsBundle.settingsArg) {
|
|
1160
|
+
args.push('--load-settings', settingsBundle.settingsArg);
|
|
818
1161
|
}
|
|
819
1162
|
// Always allow newer-version 3MF files (the CLI rejects them by default)
|
|
820
1163
|
args.push('--allow-newer-file');
|
|
821
|
-
// If a printer preset name is given AND no explicit profile was provided,
|
|
822
|
-
// write a minimal settings JSON containing the printer_settings_id so
|
|
823
|
-
// BambuStudio resolves the correct machine profile from its built-in presets.
|
|
824
|
-
if (printerPreset && !slicerProfile) {
|
|
825
|
-
const presetJson = path.join(this.tempDir, '_printer_preset.json');
|
|
826
|
-
fs.writeFileSync(presetJson, JSON.stringify({ printer_settings_id: printerPreset }));
|
|
827
|
-
args.push('--load-settings', presetJson);
|
|
828
|
-
}
|
|
829
1164
|
// BambuSliceOptions flags
|
|
830
1165
|
if (bambuOptions?.uptodate)
|
|
831
1166
|
args.push('--uptodate');
|
|
@@ -857,8 +1192,10 @@ export class STLManipulator extends EventEmitter {
|
|
|
857
1192
|
args.push('--clone-objects', bambuOptions.cloneObjects);
|
|
858
1193
|
if (bambuOptions?.skipObjects)
|
|
859
1194
|
args.push('--skip-objects', bambuOptions.skipObjects);
|
|
860
|
-
if (
|
|
861
|
-
args.push('--load-filaments',
|
|
1195
|
+
if (settingsBundle.filamentPaths.length > 0) {
|
|
1196
|
+
args.push('--load-filaments', settingsBundle.filamentPaths.join(';'));
|
|
1197
|
+
args.push('--load-defaultfila');
|
|
1198
|
+
}
|
|
862
1199
|
if (bambuOptions?.loadFilamentIds)
|
|
863
1200
|
args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
|
|
864
1201
|
args.push(stlFilePath);
|
package/dist/types.d.ts
CHANGED
|
@@ -55,3 +55,38 @@ export interface ThreeMFData {
|
|
|
55
55
|
};
|
|
56
56
|
slicerConfig?: Partial<BambuSlicerConfig>;
|
|
57
57
|
}
|
|
58
|
+
export interface CollarCharmRole {
|
|
59
|
+
role: 'inner' | 'outer';
|
|
60
|
+
objectIndex: number;
|
|
61
|
+
name: string;
|
|
62
|
+
area: number;
|
|
63
|
+
filamentPosition: number;
|
|
64
|
+
}
|
|
65
|
+
export interface CollarCharmAnalysis {
|
|
66
|
+
plateIndex: number;
|
|
67
|
+
usedFilamentPositions: number[];
|
|
68
|
+
amsSlots: number[];
|
|
69
|
+
roles: CollarCharmRole[];
|
|
70
|
+
}
|
|
71
|
+
export interface ThreeMFFilamentRequirement {
|
|
72
|
+
filamentPosition: number;
|
|
73
|
+
filamentId: number;
|
|
74
|
+
tray_info_idx: string | null;
|
|
75
|
+
type: string | null;
|
|
76
|
+
color: string | null;
|
|
77
|
+
}
|
|
78
|
+
export interface ThreeMFAmsRequirements {
|
|
79
|
+
plateIndex: number;
|
|
80
|
+
usedFilamentPositions: number[];
|
|
81
|
+
filaments: ThreeMFFilamentRequirement[];
|
|
82
|
+
}
|
|
83
|
+
export interface ThreeMFPlateObject {
|
|
84
|
+
id: number;
|
|
85
|
+
name: string;
|
|
86
|
+
area: number | null;
|
|
87
|
+
bbox: unknown;
|
|
88
|
+
}
|
|
89
|
+
export interface ThreeMFPlateObjects {
|
|
90
|
+
plateIndex: number;
|
|
91
|
+
objects: ThreeMFPlateObject[];
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bambu-printer-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
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": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"start:stdio": "MCP_TRANSPORT=stdio node dist/index.js",
|
|
14
14
|
"start:http": "MCP_TRANSPORT=streamable-http node dist/index.js",
|
|
15
15
|
"build": "tsc",
|
|
16
|
-
"test": "npm run build && node --test tests/**/*.test.mjs"
|
|
16
|
+
"test": "npm run build && node --test tests/*.test.mjs tests/**/*.test.mjs",
|
|
17
|
+
"postinstall": "patch-package"
|
|
17
18
|
},
|
|
18
19
|
"bin": {
|
|
19
20
|
"bambu-printer-mcp": "dist/index.js"
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
"files": [
|
|
22
23
|
"dist",
|
|
23
24
|
"src",
|
|
25
|
+
"patches",
|
|
24
26
|
"README.md",
|
|
25
27
|
"LICENSE"
|
|
26
28
|
],
|
|
@@ -42,12 +44,15 @@
|
|
|
42
44
|
"basic-ftp": "^5.0.5",
|
|
43
45
|
"dotenv": "^16.3.1",
|
|
44
46
|
"jszip": "^3.10.1",
|
|
47
|
+
"mqtt": "^5.15.1",
|
|
48
|
+
"patch-package": "^8.0.1",
|
|
45
49
|
"three": "^0.173.0",
|
|
46
50
|
"xml2js": "^0.6.2"
|
|
47
51
|
},
|
|
48
52
|
"devDependencies": {
|
|
49
53
|
"@types/node": "^18.0.0",
|
|
50
54
|
"@types/three": "^0.173.0",
|
|
51
|
-
"@types/xml2js": "^0.4.14"
|
|
55
|
+
"@types/xml2js": "^0.4.14",
|
|
56
|
+
"typescript": "^6.0.3"
|
|
52
57
|
}
|
|
53
58
|
}
|