bambu-printer-mcp 1.0.8 → 1.1.4
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 +517 -477
- 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/index.js +1536 -80
- package/dist/printers/bambu.d.ts +134 -0
- package/dist/printers/bambu.js +1013 -53
- package/dist/slicer/profile-flatten.d.ts +75 -0
- package/dist/slicer/profile-flatten.js +446 -0
- package/dist/stl/stl-manipulator.d.ts +31 -3
- package/dist/stl/stl-manipulator.js +300 -52
- 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/index.ts +1982 -81
- package/src/printers/bambu.ts +1228 -56
- package/src/slicer/profile-flatten.ts +562 -0
- package/src/stl/stl-manipulator.ts +384 -55
- package/src/types.ts +41 -0
|
@@ -9,9 +9,20 @@ import { promisify } from 'util';
|
|
|
9
9
|
import { EventEmitter } from 'events';
|
|
10
10
|
import * as crypto from 'crypto';
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
|
+
import { flattenForCli, detectProfilesRoot } from '../slicer/profile-flatten.js';
|
|
12
13
|
|
|
13
14
|
const readFileAsync = promisify(fs.readFile);
|
|
14
15
|
const writeFileAsync = promisify(fs.writeFile);
|
|
16
|
+
const BAMBU_PROFILE_ROOTS = [
|
|
17
|
+
'/Applications/BambuStudio.app/Contents/Resources/profiles/BBL',
|
|
18
|
+
'/Applications/OrcaSlicer.app/Contents/Resources/profiles/BBL',
|
|
19
|
+
];
|
|
20
|
+
const BAMBU_CLI_BED_TYPES: Record<string, string> = {
|
|
21
|
+
textured_plate: 'Textured PEI Plate',
|
|
22
|
+
cool_plate: 'Cool Plate',
|
|
23
|
+
engineering_plate: 'Engineering Plate',
|
|
24
|
+
hot_plate: 'High Temp Plate',
|
|
25
|
+
};
|
|
15
26
|
|
|
16
27
|
// Define types for progress tracking
|
|
17
28
|
export type ProgressCallback = (progress: number, message?: string) => void;
|
|
@@ -97,8 +108,15 @@ export function isBambuCompatibleSlicer(slicerType: SlicerType): boolean {
|
|
|
97
108
|
);
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
function isPathLikeExecutable(
|
|
101
|
-
return
|
|
111
|
+
function isPathLikeExecutable(slicerPath: string): boolean {
|
|
112
|
+
return (
|
|
113
|
+
path.isAbsolute(slicerPath) ||
|
|
114
|
+
slicerPath.startsWith("./") ||
|
|
115
|
+
slicerPath.startsWith("../") ||
|
|
116
|
+
slicerPath.includes("/") ||
|
|
117
|
+
slicerPath.includes("\\") ||
|
|
118
|
+
/^[A-Za-z]:/.test(slicerPath)
|
|
119
|
+
);
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
function configuredBambuProfileDirs(): string[] {
|
|
@@ -129,35 +147,10 @@ function configuredBambuProfileDirs(): string[] {
|
|
|
129
147
|
];
|
|
130
148
|
|
|
131
149
|
return roots.flatMap((root) =>
|
|
132
|
-
appNames.map((appName) => path.join(root, appName, 'system', 'BBL'
|
|
150
|
+
appNames.map((appName) => path.join(root, appName, 'system', 'BBL'))
|
|
133
151
|
);
|
|
134
152
|
}
|
|
135
153
|
|
|
136
|
-
function resolveBambuPrinterProfile(printerPreset: string): string | undefined {
|
|
137
|
-
if (printerPreset.includes('/') || printerPreset.includes('\\')) {
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
for (const profileDir of configuredBambuProfileDirs()) {
|
|
142
|
-
const profilePath = path.join(profileDir, `${printerPreset}.json`);
|
|
143
|
-
if (fs.existsSync(profilePath)) {
|
|
144
|
-
return profilePath;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function generatedBambuPrinterSettings(printerPreset: string): Record<string, string> {
|
|
152
|
-
return {
|
|
153
|
-
type: 'machine',
|
|
154
|
-
from: 'system',
|
|
155
|
-
name: printerPreset,
|
|
156
|
-
inherits: printerPreset,
|
|
157
|
-
printer_settings_id: printerPreset,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
154
|
// Bambu-compatible slicer CLI flags exposed via the slice_stl tool
|
|
162
155
|
export interface BambuSliceOptions {
|
|
163
156
|
uptodate?: boolean; // --uptodate: update 3MF configs to latest presets
|
|
@@ -169,6 +162,7 @@ export interface BambuSliceOptions {
|
|
|
169
162
|
skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
|
|
170
163
|
loadFilaments?: string; // --load-filaments "f1.json;f2.json": filament profiles
|
|
171
164
|
loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
|
|
165
|
+
bedType?: string; // Bambu bed type, e.g. textured_plate or cool_plate
|
|
172
166
|
enableTimelapse?: boolean; // --enable-timelapse: timelapse-aware slicing
|
|
173
167
|
allowMixTemp?: boolean; // --allow-mix-temp: allow mixed-temp filaments
|
|
174
168
|
scale?: number; // --scale factor: uniform scale
|
|
@@ -200,6 +194,340 @@ export class STLManipulator extends EventEmitter {
|
|
|
200
194
|
return crypto.randomUUID();
|
|
201
195
|
}
|
|
202
196
|
|
|
197
|
+
private getAvailableProfileRoots(): string[] {
|
|
198
|
+
return [...BAMBU_PROFILE_ROOTS, ...configuredBambuProfileDirs()].filter((root) => fs.existsSync(root));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private findProfileFile(
|
|
202
|
+
category: 'machine' | 'process' | 'filament',
|
|
203
|
+
profileName?: string
|
|
204
|
+
): string | undefined {
|
|
205
|
+
if (!profileName) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const root of this.getAvailableProfileRoots()) {
|
|
210
|
+
const candidate = path.join(root, category, `${profileName}.json`);
|
|
211
|
+
if (fs.existsSync(candidate)) {
|
|
212
|
+
return candidate;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private buildFilamentIdIndex(): Map<string, string> {
|
|
220
|
+
const index = new Map<string, string>();
|
|
221
|
+
|
|
222
|
+
for (const root of this.getAvailableProfileRoots()) {
|
|
223
|
+
const filamentDir = path.join(root, 'filament');
|
|
224
|
+
if (!fs.existsSync(filamentDir)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const entry of fs.readdirSync(filamentDir)) {
|
|
229
|
+
if (!entry.endsWith('.json')) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const filePath = path.join(filamentDir, entry);
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
236
|
+
const filamentId = typeof parsed?.filament_id === 'string' ? parsed.filament_id.trim() : '';
|
|
237
|
+
if (filamentId && !index.has(filamentId)) {
|
|
238
|
+
index.set(filamentId, filePath);
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// Ignore malformed JSON.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return index;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private readJsonFile(filePath: string): any {
|
|
250
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private stripAbsoluteExtruderResets(value: unknown): unknown {
|
|
254
|
+
if (typeof value === 'string') {
|
|
255
|
+
return value
|
|
256
|
+
.split(/\r?\n/)
|
|
257
|
+
.filter((line) => line.trim().toUpperCase() !== 'G92 E0')
|
|
258
|
+
.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
return value.filter((line) => String(line).trim().toUpperCase() !== 'G92 E0');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private sanitizeProcessForOrca(processConfig: any, printerPreset?: string): any {
|
|
269
|
+
const sanitized = { ...processConfig };
|
|
270
|
+
|
|
271
|
+
sanitized.type = 'process';
|
|
272
|
+
if (!sanitized.from || sanitized.from === 'project') {
|
|
273
|
+
sanitized.from = 'User';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (printerPreset) {
|
|
277
|
+
sanitized.compatible_printers = [printerPreset];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (
|
|
281
|
+
sanitized.prime_tower_brim_width !== undefined &&
|
|
282
|
+
Number(sanitized.prime_tower_brim_width) < 0
|
|
283
|
+
) {
|
|
284
|
+
sanitized.prime_tower_brim_width = '0';
|
|
285
|
+
} else if (sanitized.prime_tower_brim_width !== undefined) {
|
|
286
|
+
sanitized.prime_tower_brim_width = String(sanitized.prime_tower_brim_width);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
sanitized.use_relative_e_distances = '0';
|
|
290
|
+
sanitized.before_layer_change_gcode = this.stripAbsoluteExtruderResets(
|
|
291
|
+
sanitized.before_layer_change_gcode
|
|
292
|
+
);
|
|
293
|
+
sanitized.layer_gcode = this.stripAbsoluteExtruderResets(sanitized.layer_gcode);
|
|
294
|
+
sanitized.layer_change_gcode = this.stripAbsoluteExtruderResets(
|
|
295
|
+
sanitized.layer_change_gcode
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return sanitized;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private writeTempJson(outputBase: string, suffix: string, value: any): string {
|
|
302
|
+
const outPath = path.join(this.tempDir, `${outputBase}_${suffix}.json`);
|
|
303
|
+
fs.writeFileSync(outPath, JSON.stringify(value, null, 2));
|
|
304
|
+
return outPath;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private resolveBambuLikeSettingsBundle(
|
|
308
|
+
outputBase: string,
|
|
309
|
+
slicerType: 'orcaslicer' | 'orcaslicer-bambulab' | 'bambustudio',
|
|
310
|
+
slicerProfile?: string,
|
|
311
|
+
printerPreset?: string,
|
|
312
|
+
bambuOptions?: BambuSliceOptions
|
|
313
|
+
): { settingsArg?: string; filamentPaths: string[] } {
|
|
314
|
+
const machinePath = this.findProfileFile('machine', printerPreset);
|
|
315
|
+
const machineConfig = machinePath ? this.readJsonFile(machinePath) : null;
|
|
316
|
+
const hasSlicerProfile = !!slicerProfile && fs.existsSync(slicerProfile);
|
|
317
|
+
const filamentPaths: string[] = [];
|
|
318
|
+
|
|
319
|
+
let processPath: string | undefined;
|
|
320
|
+
let parsedProfile: any = null;
|
|
321
|
+
|
|
322
|
+
if (hasSlicerProfile) {
|
|
323
|
+
try {
|
|
324
|
+
parsedProfile = this.readJsonFile(slicerProfile!);
|
|
325
|
+
} catch {
|
|
326
|
+
parsedProfile = null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (parsedProfile && typeof parsedProfile === 'object') {
|
|
331
|
+
const inheritedProcessName =
|
|
332
|
+
(typeof parsedProfile.inherits === 'string' && parsedProfile.inherits) ||
|
|
333
|
+
(typeof parsedProfile.print_settings_id === 'string' &&
|
|
334
|
+
parsedProfile.print_settings_id !== parsedProfile.name
|
|
335
|
+
? parsedProfile.print_settings_id
|
|
336
|
+
: undefined) ||
|
|
337
|
+
(typeof parsedProfile.default_print_profile === 'string'
|
|
338
|
+
? parsedProfile.default_print_profile
|
|
339
|
+
: undefined) ||
|
|
340
|
+
(typeof machineConfig?.default_print_profile === 'string'
|
|
341
|
+
? machineConfig.default_print_profile
|
|
342
|
+
: undefined);
|
|
343
|
+
|
|
344
|
+
const inheritedProcessPath = this.findProfileFile('process', inheritedProcessName);
|
|
345
|
+
const inheritedProcess =
|
|
346
|
+
inheritedProcessPath && fs.existsSync(inheritedProcessPath)
|
|
347
|
+
? this.readJsonFile(inheritedProcessPath)
|
|
348
|
+
: {};
|
|
349
|
+
|
|
350
|
+
const mergedProcess = {
|
|
351
|
+
...inheritedProcess,
|
|
352
|
+
...parsedProfile,
|
|
353
|
+
type: 'process',
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
processPath = this.writeTempJson(
|
|
357
|
+
outputBase,
|
|
358
|
+
slicerType === 'orcaslicer' ? 'process_orca' : 'process',
|
|
359
|
+
slicerType === 'orcaslicer'
|
|
360
|
+
? this.sanitizeProcessForOrca(mergedProcess, printerPreset)
|
|
361
|
+
: mergedProcess
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const defaultFilamentProfiles = Array.isArray(parsedProfile.default_filament_profile)
|
|
365
|
+
? parsedProfile.default_filament_profile
|
|
366
|
+
: [];
|
|
367
|
+
for (const profileName of defaultFilamentProfiles) {
|
|
368
|
+
const filamentPath = this.findProfileFile('filament', String(profileName));
|
|
369
|
+
if (filamentPath) {
|
|
370
|
+
filamentPaths.push(filamentPath);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
|
|
375
|
+
const filamentIdIndex = this.buildFilamentIdIndex();
|
|
376
|
+
for (const filamentId of parsedProfile.filament_ids) {
|
|
377
|
+
const filamentPath = filamentIdIndex.get(String(filamentId));
|
|
378
|
+
if (filamentPath) {
|
|
379
|
+
filamentPaths.push(filamentPath);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} else if (hasSlicerProfile) {
|
|
384
|
+
processPath = slicerProfile;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!processPath) {
|
|
388
|
+
const defaultProcessName =
|
|
389
|
+
typeof machineConfig?.default_print_profile === 'string'
|
|
390
|
+
? machineConfig.default_print_profile
|
|
391
|
+
: undefined;
|
|
392
|
+
const defaultProcessPath = this.findProfileFile('process', defaultProcessName);
|
|
393
|
+
if (defaultProcessPath) {
|
|
394
|
+
processPath =
|
|
395
|
+
slicerType === 'orcaslicer'
|
|
396
|
+
? this.writeTempJson(
|
|
397
|
+
outputBase,
|
|
398
|
+
'process_default_orca',
|
|
399
|
+
this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset)
|
|
400
|
+
)
|
|
401
|
+
: defaultProcessPath;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (
|
|
406
|
+
!bambuOptions?.loadFilaments &&
|
|
407
|
+
filamentPaths.length === 0 &&
|
|
408
|
+
Array.isArray(machineConfig?.default_filament_profile)
|
|
409
|
+
) {
|
|
410
|
+
for (const profileName of machineConfig.default_filament_profile) {
|
|
411
|
+
const filamentPath = this.findProfileFile('filament', String(profileName));
|
|
412
|
+
if (filamentPath) {
|
|
413
|
+
filamentPaths.push(filamentPath);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (bambuOptions?.loadFilaments) {
|
|
419
|
+
filamentPaths.length = 0;
|
|
420
|
+
for (const filamentPath of bambuOptions.loadFilaments.split(';')) {
|
|
421
|
+
const trimmed = filamentPath.trim();
|
|
422
|
+
if (trimmed) {
|
|
423
|
+
filamentPaths.push(trimmed);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const settingsParts = [machinePath, processPath].filter(
|
|
429
|
+
(entry): entry is string => Boolean(entry)
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
settingsArg: settingsParts.length > 0 ? settingsParts.join(';') : undefined,
|
|
434
|
+
filamentPaths: Array.from(new Set(filamentPaths)),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Optionally rewrite a Bambu-like settings bundle so the paths point at
|
|
440
|
+
* fully-flattened temp configs instead of the BBL-shipped leaf JSONs.
|
|
441
|
+
*
|
|
442
|
+
* BambuStudio's CLI does not resolve the `inherits` chain when loading
|
|
443
|
+
* profiles via --load-settings / --load-filaments, which causes a
|
|
444
|
+
* cluster of upstream bugs (see https://github.com/bambulab/BambuStudio/issues/9636
|
|
445
|
+
* and #9968). Our flattener (src/slicer/profile-flatten.ts) reproduces
|
|
446
|
+
* what the GUI does at slice time so the CLI accepts the configs.
|
|
447
|
+
*
|
|
448
|
+
* Opt-in via `BAMBU_CLI_FLATTEN=true`. When the env var is unset or
|
|
449
|
+
* not "true"/"1", returns the bundle unchanged so behavior is
|
|
450
|
+
* backward-compatible. When enabled, only BBL-shipped leaves get
|
|
451
|
+
* flattened; user-provided custom configs pass through untouched.
|
|
452
|
+
*/
|
|
453
|
+
private async maybeFlattenBundle(
|
|
454
|
+
bundle: { settingsArg?: string; filamentPaths: string[] },
|
|
455
|
+
bambuOptions?: BambuSliceOptions,
|
|
456
|
+
activeSlicerPath?: string
|
|
457
|
+
): Promise<{ settingsArg?: string; filamentPaths: string[] }> {
|
|
458
|
+
const flag = process.env.BAMBU_CLI_FLATTEN;
|
|
459
|
+
if (flag !== 'true' && flag !== '1') return bundle;
|
|
460
|
+
if (!bundle.settingsArg) return bundle;
|
|
461
|
+
|
|
462
|
+
// settingsArg is "machine.json;process.json".
|
|
463
|
+
const parts = bundle.settingsArg.split(';').filter(Boolean);
|
|
464
|
+
if (parts.length < 2) return bundle;
|
|
465
|
+
const [machinePath, processPath] = parts;
|
|
466
|
+
|
|
467
|
+
// Pull leaf names from each JSON's `name` field. If any is missing or
|
|
468
|
+
// looks non-BBL (e.g. a user-saved custom config), bail out and
|
|
469
|
+
// return the original bundle untouched.
|
|
470
|
+
const machineLeaf = this.readLeafName(machinePath);
|
|
471
|
+
const processLeaf = this.readLeafName(processPath);
|
|
472
|
+
const filamentLeaves = bundle.filamentPaths
|
|
473
|
+
.map((p) => this.readLeafName(p))
|
|
474
|
+
.filter((n): n is string => Boolean(n));
|
|
475
|
+
|
|
476
|
+
if (!machineLeaf || !processLeaf || filamentLeaves.length === 0) {
|
|
477
|
+
console.log(
|
|
478
|
+
'[cli-flatten] skipping: could not derive BBL leaf names from bundle paths'
|
|
479
|
+
);
|
|
480
|
+
return bundle;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const bedType = this.resolveBambuStudioBedType(bambuOptions?.bedType);
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const profilesRoot = detectProfilesRoot(activeSlicerPath || process.env.SLICER_PATH);
|
|
487
|
+
const flat = await flattenForCli({
|
|
488
|
+
machineLeaf,
|
|
489
|
+
processLeaf,
|
|
490
|
+
filamentLeaves,
|
|
491
|
+
profilesRoot,
|
|
492
|
+
tempDir: this.tempDir,
|
|
493
|
+
bedType,
|
|
494
|
+
});
|
|
495
|
+
console.log(
|
|
496
|
+
`[cli-flatten] applied for ${machineLeaf} (cliOverlay=${flat.meta.cliOverlayApplied})`
|
|
497
|
+
);
|
|
498
|
+
return {
|
|
499
|
+
settingsArg: `${flat.machinePath};${flat.processPath}`,
|
|
500
|
+
filamentPaths: flat.filamentPaths,
|
|
501
|
+
};
|
|
502
|
+
} catch (err: any) {
|
|
503
|
+
console.error(
|
|
504
|
+
`[cli-flatten] failed, falling back to unflattened bundle: ${err?.message ?? err}`
|
|
505
|
+
);
|
|
506
|
+
return bundle;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private resolveBambuStudioBedType(bedType?: string): string | undefined {
|
|
511
|
+
if (!bedType) return undefined;
|
|
512
|
+
const normalized = bedType.trim().toLowerCase();
|
|
513
|
+
if (normalized === 'supertack_plate') {
|
|
514
|
+
throw new Error(
|
|
515
|
+
'BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF or choose textured_plate, cool_plate, engineering_plate, or hot_plate.'
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return BAMBU_CLI_BED_TYPES[normalized] || bedType;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Read a profile JSON's top-level `name` field, or null if unreadable. */
|
|
522
|
+
private readLeafName(filePath: string): string | null {
|
|
523
|
+
try {
|
|
524
|
+
const data = this.readJsonFile(filePath);
|
|
525
|
+
return typeof data?.name === 'string' && data.name.length > 0 ? data.name : null;
|
|
526
|
+
} catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
203
531
|
/**
|
|
204
532
|
* Load STL file and return geometry and bounding box
|
|
205
533
|
*/
|
|
@@ -1101,12 +1429,12 @@ export class STLManipulator extends EventEmitter {
|
|
|
1101
1429
|
/**
|
|
1102
1430
|
* Slice an STL or 3MF file using the specified slicer
|
|
1103
1431
|
* @param stlFilePath Path to the input STL or 3MF file
|
|
1104
|
-
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer,
|
|
1432
|
+
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, bambustudio)
|
|
1105
1433
|
* @param slicerPath Path to the slicer executable
|
|
1106
1434
|
* @param slicerProfile Optional path to the slicer profile/config file
|
|
1107
1435
|
* @param progressCallback Optional callback for progress updates
|
|
1108
|
-
* @param printerPreset Optional
|
|
1109
|
-
* @param bambuOptions Optional
|
|
1436
|
+
* @param printerPreset Optional BambuStudio printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
|
|
1437
|
+
* @param bambuOptions Optional BambuStudio-specific CLI flags
|
|
1110
1438
|
* @returns Path to the generated G-code or sliced 3MF file
|
|
1111
1439
|
*/
|
|
1112
1440
|
async sliceSTL(
|
|
@@ -1169,41 +1497,39 @@ export class STLManipulator extends EventEmitter {
|
|
|
1169
1497
|
case 'orcaslicer':
|
|
1170
1498
|
case 'orcaslicer-bambulab':
|
|
1171
1499
|
// Bambu-compatible CLI: slice and export as 3MF with embedded gcode.
|
|
1172
|
-
// FULU's OrcaSlicer-bambulab fork keeps
|
|
1173
|
-
// --slice/--export-3mf path, so it must not use the generic Prusa
|
|
1174
|
-
// --output G-code flow.
|
|
1500
|
+
// FULU's OrcaSlicer-bambulab fork keeps this Bambu-style path.
|
|
1175
1501
|
// For 3MF input: slice in place and export sliced 3MF
|
|
1176
1502
|
// For STL input: slice and export as 3MF
|
|
1177
1503
|
{
|
|
1178
1504
|
const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
|
|
1179
1505
|
const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
|
|
1180
1506
|
const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
|
|
1507
|
+
const outputDir = path.dirname(bambuOutputPath);
|
|
1508
|
+
const rawBundle = this.resolveBambuLikeSettingsBundle(
|
|
1509
|
+
outputBase,
|
|
1510
|
+
slicerType,
|
|
1511
|
+
slicerProfile,
|
|
1512
|
+
printerPreset,
|
|
1513
|
+
bambuOptions
|
|
1514
|
+
);
|
|
1515
|
+
// Opt-in flatten step: walks the BBL `inherits` chain so the
|
|
1516
|
+
// CLI accepts what we hand it. See maybeFlattenBundle docstring.
|
|
1517
|
+
const settingsBundle =
|
|
1518
|
+
slicerType === 'bambustudio'
|
|
1519
|
+
? await this.maybeFlattenBundle(rawBundle, bambuOptions, slicerPath)
|
|
1520
|
+
: rawBundle;
|
|
1181
1521
|
args = [
|
|
1182
1522
|
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
1183
|
-
'--
|
|
1523
|
+
'--outputdir', outputDir,
|
|
1524
|
+
'--export-3mf', path.basename(bambuOutputPath),
|
|
1184
1525
|
];
|
|
1185
|
-
if (
|
|
1186
|
-
args.push('--load-settings',
|
|
1526
|
+
if (settingsBundle.settingsArg) {
|
|
1527
|
+
args.push('--load-settings', settingsBundle.settingsArg);
|
|
1187
1528
|
}
|
|
1188
1529
|
// Always allow newer-version 3MF files (the CLI rejects them by default)
|
|
1189
1530
|
args.push('--allow-newer-file');
|
|
1190
1531
|
|
|
1191
|
-
//
|
|
1192
|
-
// prefer the slicer's installed machine profile. Real BambuStudio rejects
|
|
1193
|
-
// skeletal JSON without profile metadata, so only fall back to generated
|
|
1194
|
-
// settings when no installed profile can be found.
|
|
1195
|
-
if (printerPreset && !slicerProfile) {
|
|
1196
|
-
const installedProfile = resolveBambuPrinterProfile(printerPreset);
|
|
1197
|
-
if (installedProfile) {
|
|
1198
|
-
args.push('--load-settings', installedProfile);
|
|
1199
|
-
} else {
|
|
1200
|
-
const presetJson = path.join(this.tempDir, '_printer_preset.json');
|
|
1201
|
-
fs.writeFileSync(presetJson, JSON.stringify(generatedBambuPrinterSettings(printerPreset), null, 2));
|
|
1202
|
-
args.push('--load-settings', presetJson);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
// Bambu-compatible slicer flags
|
|
1532
|
+
// BambuSliceOptions flags
|
|
1207
1533
|
if (bambuOptions?.uptodate) args.push('--uptodate');
|
|
1208
1534
|
if (bambuOptions?.ensureOnBed) args.push('--ensure-on-bed');
|
|
1209
1535
|
if (bambuOptions?.enableTimelapse) args.push('--enable-timelapse');
|
|
@@ -1219,11 +1545,14 @@ export class STLManipulator extends EventEmitter {
|
|
|
1219
1545
|
if (bambuOptions?.rotateY !== undefined) args.push('--rotate-y', String(bambuOptions.rotateY));
|
|
1220
1546
|
if (bambuOptions?.cloneObjects) args.push('--clone-objects', bambuOptions.cloneObjects);
|
|
1221
1547
|
if (bambuOptions?.skipObjects) args.push('--skip-objects', bambuOptions.skipObjects);
|
|
1222
|
-
if (
|
|
1548
|
+
if (settingsBundle.filamentPaths.length > 0) {
|
|
1549
|
+
args.push('--load-filaments', settingsBundle.filamentPaths.join(';'));
|
|
1550
|
+
args.push('--load-defaultfila');
|
|
1551
|
+
}
|
|
1223
1552
|
if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
|
|
1224
1553
|
|
|
1225
1554
|
args.push(stlFilePath);
|
|
1226
|
-
// Override outputFilePath
|
|
1555
|
+
// Override outputFilePath for bambustudio since it produces 3MF, not gcode
|
|
1227
1556
|
outputFilePath = bambuOutputPath;
|
|
1228
1557
|
}
|
|
1229
1558
|
break;
|
package/src/types.ts
CHANGED
|
@@ -56,3 +56,44 @@ export interface ThreeMFData {
|
|
|
56
56
|
build: { items: ThreeMFBuildItem[] };
|
|
57
57
|
slicerConfig?: Partial<BambuSlicerConfig>;
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
export interface CollarCharmRole {
|
|
61
|
+
role: 'inner' | 'outer';
|
|
62
|
+
objectIndex: number;
|
|
63
|
+
name: string;
|
|
64
|
+
area: number;
|
|
65
|
+
filamentPosition: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CollarCharmAnalysis {
|
|
69
|
+
plateIndex: number;
|
|
70
|
+
usedFilamentPositions: number[];
|
|
71
|
+
amsSlots: number[];
|
|
72
|
+
roles: CollarCharmRole[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ThreeMFFilamentRequirement {
|
|
76
|
+
filamentPosition: number;
|
|
77
|
+
filamentId: number;
|
|
78
|
+
tray_info_idx: string | null;
|
|
79
|
+
type: string | null;
|
|
80
|
+
color: string | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ThreeMFAmsRequirements {
|
|
84
|
+
plateIndex: number;
|
|
85
|
+
usedFilamentPositions: number[];
|
|
86
|
+
filaments: ThreeMFFilamentRequirement[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ThreeMFPlateObject {
|
|
90
|
+
id: number;
|
|
91
|
+
name: string;
|
|
92
|
+
area: number | null;
|
|
93
|
+
bbox: unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ThreeMFPlateObjects {
|
|
97
|
+
plateIndex: number;
|
|
98
|
+
objects: ThreeMFPlateObject[];
|
|
99
|
+
}
|