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.
@@ -3,14 +3,26 @@ 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
 
12
14
  const readFileAsync = promisify(fs.readFile);
13
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
+ };
14
26
 
15
27
  // Define types for progress tracking
16
28
  export type ProgressCallback = (progress: number, message?: string) => void;
@@ -40,7 +52,106 @@ export type TransformationParams = {
40
52
  selectionBounds?: THREE.Box3;
41
53
  };
42
54
 
43
- // BambuStudio CLI flags exposed via the slice_stl tool
55
+ export const SLICER_TYPES = [
56
+ 'bambustudio',
57
+ 'orcaslicer',
58
+ 'orcaslicer-bambulab',
59
+ 'prusaslicer',
60
+ 'cura',
61
+ 'slic3r',
62
+ ] as const;
63
+
64
+ export type SlicerType = typeof SLICER_TYPES[number];
65
+
66
+ const SLICER_TYPE_ALIASES: Record<string, SlicerType> = {
67
+ bambustudio: 'bambustudio',
68
+ 'bambu-studio': 'bambustudio',
69
+ bblstudio: 'bambustudio',
70
+ orcaslicer: 'orcaslicer',
71
+ 'orca-slicer': 'orcaslicer',
72
+ orca: 'orcaslicer',
73
+ 'orcaslicer-bambulab': 'orcaslicer-bambulab',
74
+ 'orca-slicer-bambulab': 'orcaslicer-bambulab',
75
+ 'orca-bambulab': 'orcaslicer-bambulab',
76
+ 'fulu-orca': 'orcaslicer-bambulab',
77
+ 'fulu-orcaslicer': 'orcaslicer-bambulab',
78
+ 'fulu-orca-slicer': 'orcaslicer-bambulab',
79
+ 'orca-studio': 'orcaslicer-bambulab',
80
+ orcastudio: 'orcaslicer-bambulab',
81
+ 'orcaslicer-bmcu': 'orcaslicer-bambulab',
82
+ 'orca-bmcu': 'orcaslicer-bambulab',
83
+ prusaslicer: 'prusaslicer',
84
+ 'prusa-slicer': 'prusaslicer',
85
+ cura: 'cura',
86
+ curaengine: 'cura',
87
+ slic3r: 'slic3r',
88
+ };
89
+
90
+ export function normalizeSlicerType(rawSlicerType: string): SlicerType {
91
+ const key = rawSlicerType.trim().toLowerCase().replace(/_/g, '-');
92
+ const normalized = SLICER_TYPE_ALIASES[key];
93
+
94
+ if (!normalized) {
95
+ throw new Error(
96
+ `Unsupported slicer_type: "${rawSlicerType}". Valid values: ${SLICER_TYPES.join(', ')}`
97
+ );
98
+ }
99
+
100
+ return normalized;
101
+ }
102
+
103
+ export function isBambuCompatibleSlicer(slicerType: SlicerType): boolean {
104
+ return (
105
+ slicerType === 'bambustudio' ||
106
+ slicerType === 'orcaslicer' ||
107
+ slicerType === 'orcaslicer-bambulab'
108
+ );
109
+ }
110
+
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
+ );
120
+ }
121
+
122
+ function configuredBambuProfileDirs(): string[] {
123
+ const override = process.env.BAMBU_SLICER_PROFILE_DIRS;
124
+ if (override !== undefined) {
125
+ return override.split(path.delimiter).filter(Boolean);
126
+ }
127
+
128
+ const home = os.homedir();
129
+ const macAppSupport = path.join(home, 'Library', 'Application Support');
130
+ const linuxConfig = path.join(home, '.config');
131
+ const appData = process.env.APPDATA;
132
+ const localAppData = process.env.LOCALAPPDATA;
133
+ const appNames = [
134
+ 'BambuStudio',
135
+ 'Bambu Studio',
136
+ 'OrcaSlicer',
137
+ 'Orca Slicer',
138
+ 'OrcaStudio',
139
+ 'Orca Studio',
140
+ ];
141
+
142
+ const roots = [
143
+ macAppSupport,
144
+ linuxConfig,
145
+ ...(appData ? [appData] : []),
146
+ ...(localAppData ? [localAppData] : []),
147
+ ];
148
+
149
+ return roots.flatMap((root) =>
150
+ appNames.map((appName) => path.join(root, appName, 'system', 'BBL'))
151
+ );
152
+ }
153
+
154
+ // Bambu-compatible slicer CLI flags exposed via the slice_stl tool
44
155
  export interface BambuSliceOptions {
45
156
  uptodate?: boolean; // --uptodate: update 3MF configs to latest presets
46
157
  repetitions?: number; // --repetitions N: print N copies
@@ -51,6 +162,7 @@ export interface BambuSliceOptions {
51
162
  skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
52
163
  loadFilaments?: string; // --load-filaments "f1.json;f2.json": filament profiles
53
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
54
166
  enableTimelapse?: boolean; // --enable-timelapse: timelapse-aware slicing
55
167
  allowMixTemp?: boolean; // --allow-mix-temp: allow mixed-temp filaments
56
168
  scale?: number; // --scale factor: uniform scale
@@ -82,6 +194,340 @@ export class STLManipulator extends EventEmitter {
82
194
  return crypto.randomUUID();
83
195
  }
84
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
+
85
531
  /**
86
532
  * Load STL file and return geometry and bounding box
87
533
  */
@@ -993,7 +1439,7 @@ export class STLManipulator extends EventEmitter {
993
1439
  */
994
1440
  async sliceSTL(
995
1441
  stlFilePath: string,
996
- slicerType: 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio',
1442
+ slicerType: SlicerType,
997
1443
  slicerPath: string,
998
1444
  slicerProfile?: string,
999
1445
  progressCallback?: ProgressCallback,
@@ -1003,7 +1449,7 @@ export class STLManipulator extends EventEmitter {
1003
1449
  const operationId = this.generateOperationId();
1004
1450
  this.activeOperations.set(operationId, true);
1005
1451
 
1006
- if (!fs.existsSync(slicerPath)) {
1452
+ if (isPathLikeExecutable(slicerPath) && !fs.existsSync(slicerPath)) {
1007
1453
  throw new Error(`Slicer executable not found at: ${slicerPath}`);
1008
1454
  }
1009
1455
  if (slicerProfile && !fs.existsSync(slicerProfile)) {
@@ -1032,25 +1478,6 @@ export class STLManipulator extends EventEmitter {
1032
1478
  args = args.filter(arg => arg !== '');
1033
1479
  break;
1034
1480
 
1035
- case 'orcaslicer': // Add OrcaSlicer case
1036
- args = [
1037
- // OrcaSlicer might use --load-settings like Bambu, or --load like Prusa
1038
- // Assuming --load-settings based on discussion, adjust if needed.
1039
- // Also assuming profile path points to the main .ini or .json config.
1040
- '--load-settings', slicerProfile || '',
1041
- '--output', outputFilePath, // Assuming --output works like PrusaSlicer
1042
- stlFilePath
1043
- ];
1044
- // Alternative if --output doesn't specify filename:
1045
- // args = [
1046
- // '--load-settings', slicerProfile || '',
1047
- // '--outputdir', this.tempDir,
1048
- // stlFilePath
1049
- // ];
1050
- // Remove empty profile arg if not provided
1051
- args = args.filter(arg => arg !== '');
1052
- break;
1053
-
1054
1481
  case 'cura':
1055
1482
  // CuraEngine CLI args are different, often requiring -s for settings
1056
1483
  // Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
@@ -1067,32 +1494,41 @@ export class STLManipulator extends EventEmitter {
1067
1494
  break;
1068
1495
 
1069
1496
  case 'bambustudio':
1070
- // Bambu Studio CLI: slice and export as 3MF with embedded gcode
1497
+ case 'orcaslicer':
1498
+ case 'orcaslicer-bambulab':
1499
+ // Bambu-compatible CLI: slice and export as 3MF with embedded gcode.
1500
+ // FULU's OrcaSlicer-bambulab fork keeps this Bambu-style path.
1071
1501
  // For 3MF input: slice in place and export sliced 3MF
1072
1502
  // For STL input: slice and export as 3MF
1073
1503
  {
1074
1504
  const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
1075
1505
  const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
1076
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;
1077
1521
  args = [
1078
1522
  '--slice', String(bambuOptions?.slicePlate ?? 0),
1079
- '--export-3mf', bambuOutputPath,
1523
+ '--outputdir', outputDir,
1524
+ '--export-3mf', path.basename(bambuOutputPath),
1080
1525
  ];
1081
- if (slicerProfile) {
1082
- args.push('--load-settings', slicerProfile);
1526
+ if (settingsBundle.settingsArg) {
1527
+ args.push('--load-settings', settingsBundle.settingsArg);
1083
1528
  }
1084
1529
  // Always allow newer-version 3MF files (the CLI rejects them by default)
1085
1530
  args.push('--allow-newer-file');
1086
1531
 
1087
- // If a printer preset name is given AND no explicit profile was provided,
1088
- // write a minimal settings JSON containing the printer_settings_id so
1089
- // BambuStudio resolves the correct machine profile from its built-in presets.
1090
- if (printerPreset && !slicerProfile) {
1091
- const presetJson = path.join(this.tempDir, '_printer_preset.json');
1092
- fs.writeFileSync(presetJson, JSON.stringify({ printer_settings_id: printerPreset }));
1093
- args.push('--load-settings', presetJson);
1094
- }
1095
-
1096
1532
  // BambuSliceOptions flags
1097
1533
  if (bambuOptions?.uptodate) args.push('--uptodate');
1098
1534
  if (bambuOptions?.ensureOnBed) args.push('--ensure-on-bed');
@@ -1109,7 +1545,10 @@ export class STLManipulator extends EventEmitter {
1109
1545
  if (bambuOptions?.rotateY !== undefined) args.push('--rotate-y', String(bambuOptions.rotateY));
1110
1546
  if (bambuOptions?.cloneObjects) args.push('--clone-objects', bambuOptions.cloneObjects);
1111
1547
  if (bambuOptions?.skipObjects) args.push('--skip-objects', bambuOptions.skipObjects);
1112
- if (bambuOptions?.loadFilaments) args.push('--load-filaments', bambuOptions.loadFilaments);
1548
+ if (settingsBundle.filamentPaths.length > 0) {
1549
+ args.push('--load-filaments', settingsBundle.filamentPaths.join(';'));
1550
+ args.push('--load-defaultfila');
1551
+ }
1113
1552
  if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
1114
1553
 
1115
1554
  args.push(stlFilePath);
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
+ }