bambu-printer-mcp 1.1.6 → 1.1.9
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 +27 -0
- package/README.md +103 -28
- package/dist/blender-mcp-bridge.d.ts +10 -0
- package/dist/blender-mcp-bridge.js +389 -0
- package/dist/index.js +155 -91
- package/dist/printers/bambu.d.ts +3 -2
- package/dist/printers/bambu.js +49 -18
- package/dist/slicer/profile-flatten.d.ts +29 -4
- package/dist/slicer/profile-flatten.js +269 -74
- package/dist/stl/stl-manipulator.d.ts +4 -17
- package/dist/stl/stl-manipulator.js +161 -85
- package/package.json +8 -4
- package/patches/bambu-node+3.22.21.patch +18 -2
- package/scripts/install-patches.mjs +64 -0
- package/src/blender-mcp-bridge.ts +344 -0
- package/src/index.ts +169 -114
- package/src/printers/bambu.ts +49 -18
- package/src/slicer/profile-flatten.ts +307 -82
- package/src/stl/stl-manipulator.ts +184 -98
|
@@ -162,6 +162,7 @@ export interface BambuSliceOptions {
|
|
|
162
162
|
skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
|
|
163
163
|
loadFilaments?: string; // --load-filaments "f1.json;f2.json": filament profiles
|
|
164
164
|
loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
|
|
165
|
+
filamentColours?: string[]; // positional #RRGGBB per loaded filament slot
|
|
165
166
|
bedType?: string; // Bambu bed type, e.g. textured_plate or cool_plate
|
|
166
167
|
enableTimelapse?: boolean; // --enable-timelapse: timelapse-aware slicing
|
|
167
168
|
allowMixTemp?: boolean; // --allow-mix-temp: allow mixed-temp filaments
|
|
@@ -174,6 +175,17 @@ export interface BambuSliceOptions {
|
|
|
174
175
|
slicePlate?: number; // --slice N: which plate (default: 0 = all)
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
interface BambuSettingsBundle {
|
|
179
|
+
settingsArg?: string;
|
|
180
|
+
filamentPaths: string[];
|
|
181
|
+
/** Positional slot colours declared by the input 3MF project. */
|
|
182
|
+
filamentColours?: string[];
|
|
183
|
+
/** Saved per-plate tower coordinates must survive default process loading. */
|
|
184
|
+
projectTowerPosition?: { wipe_tower_x?: unknown; wipe_tower_y?: unknown };
|
|
185
|
+
/** Identity provenance before the process is merged into a generated file. */
|
|
186
|
+
processSource?: { filePath: string; profile: Record<string, unknown> };
|
|
187
|
+
}
|
|
188
|
+
|
|
177
189
|
export class STLManipulator extends EventEmitter {
|
|
178
190
|
private tempDir: string;
|
|
179
191
|
private activeOperations: Map<string, boolean> = new Map();
|
|
@@ -195,18 +207,24 @@ export class STLManipulator extends EventEmitter {
|
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
private getAvailableProfileRoots(): string[] {
|
|
198
|
-
|
|
210
|
+
const selectedRoot = process.env.BAMBU_PROFILES_ROOT;
|
|
211
|
+
return [
|
|
212
|
+
...(selectedRoot ? [path.join(selectedRoot, 'BBL')] : []),
|
|
213
|
+
...configuredBambuProfileDirs(),
|
|
214
|
+
...BAMBU_PROFILE_ROOTS,
|
|
215
|
+
].filter((root) => fs.existsSync(root));
|
|
199
216
|
}
|
|
200
217
|
|
|
201
218
|
private findProfileFile(
|
|
202
219
|
category: 'machine' | 'process' | 'filament',
|
|
203
|
-
profileName?: string
|
|
220
|
+
profileName?: string,
|
|
221
|
+
roots = this.getAvailableProfileRoots()
|
|
204
222
|
): string | undefined {
|
|
205
223
|
if (!profileName) {
|
|
206
224
|
return undefined;
|
|
207
225
|
}
|
|
208
226
|
|
|
209
|
-
for (const root of
|
|
227
|
+
for (const root of roots) {
|
|
210
228
|
const candidate = path.join(root, category, `${profileName}.json`);
|
|
211
229
|
if (fs.existsSync(candidate)) {
|
|
212
230
|
return candidate;
|
|
@@ -216,10 +234,10 @@ export class STLManipulator extends EventEmitter {
|
|
|
216
234
|
return undefined;
|
|
217
235
|
}
|
|
218
236
|
|
|
219
|
-
private buildFilamentIdIndex(): Map<string, string> {
|
|
237
|
+
private buildFilamentIdIndex(roots = this.getAvailableProfileRoots()): Map<string, string> {
|
|
220
238
|
const index = new Map<string, string>();
|
|
221
239
|
|
|
222
|
-
for (const root of
|
|
240
|
+
for (const root of roots) {
|
|
223
241
|
const filamentDir = path.join(root, 'filament');
|
|
224
242
|
if (!fs.existsSync(filamentDir)) {
|
|
225
243
|
continue;
|
|
@@ -299,8 +317,10 @@ export class STLManipulator extends EventEmitter {
|
|
|
299
317
|
}
|
|
300
318
|
|
|
301
319
|
private writeTempJson(outputBase: string, suffix: string, value: any): string {
|
|
302
|
-
const
|
|
303
|
-
|
|
320
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
321
|
+
const hash = crypto.createHash('sha256').update(serialized).digest('hex').slice(0, 16);
|
|
322
|
+
const outPath = path.join(this.tempDir, `${outputBase}_${suffix}_${hash}.json`);
|
|
323
|
+
fs.writeFileSync(outPath, serialized);
|
|
304
324
|
return outPath;
|
|
305
325
|
}
|
|
306
326
|
|
|
@@ -309,15 +329,25 @@ export class STLManipulator extends EventEmitter {
|
|
|
309
329
|
slicerType: 'orcaslicer' | 'orcaslicer-bambulab' | 'bambustudio',
|
|
310
330
|
slicerProfile?: string,
|
|
311
331
|
printerPreset?: string,
|
|
312
|
-
bambuOptions?: BambuSliceOptions
|
|
313
|
-
|
|
314
|
-
|
|
332
|
+
bambuOptions?: BambuSliceOptions,
|
|
333
|
+
activeProfilesRoot?: string
|
|
334
|
+
): BambuSettingsBundle {
|
|
335
|
+
const roots = activeProfilesRoot
|
|
336
|
+
? [path.join(activeProfilesRoot, 'BBL'), ...this.getAvailableProfileRoots()]
|
|
337
|
+
: this.getAvailableProfileRoots();
|
|
338
|
+
const findProfile = (kind: 'machine' | 'process' | 'filament', name?: string) =>
|
|
339
|
+
this.findProfileFile(kind, name, roots);
|
|
340
|
+
const machinePath = findProfile('machine', printerPreset);
|
|
341
|
+
if (slicerType === 'bambustudio' && printerPreset && !machinePath) {
|
|
342
|
+
throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching BambuStudio profile tree; refusing to use printer defaults.`);
|
|
343
|
+
}
|
|
315
344
|
const machineConfig = machinePath ? this.readJsonFile(machinePath) : null;
|
|
316
345
|
const hasSlicerProfile = !!slicerProfile && fs.existsSync(slicerProfile);
|
|
317
346
|
const filamentPaths: string[] = [];
|
|
318
347
|
|
|
319
348
|
let processPath: string | undefined;
|
|
320
349
|
let parsedProfile: any = null;
|
|
350
|
+
let processSource: BambuSettingsBundle['processSource'];
|
|
321
351
|
|
|
322
352
|
if (hasSlicerProfile) {
|
|
323
353
|
try {
|
|
@@ -328,6 +358,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
328
358
|
}
|
|
329
359
|
|
|
330
360
|
if (parsedProfile && typeof parsedProfile === 'object') {
|
|
361
|
+
processSource = { filePath: slicerProfile!, profile: parsedProfile };
|
|
331
362
|
const inheritedProcessName =
|
|
332
363
|
(typeof parsedProfile.inherits === 'string' && parsedProfile.inherits) ||
|
|
333
364
|
(typeof parsedProfile.print_settings_id === 'string' &&
|
|
@@ -341,7 +372,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
341
372
|
? machineConfig.default_print_profile
|
|
342
373
|
: undefined);
|
|
343
374
|
|
|
344
|
-
const inheritedProcessPath =
|
|
375
|
+
const inheritedProcessPath = findProfile('process', inheritedProcessName);
|
|
345
376
|
const inheritedProcess =
|
|
346
377
|
inheritedProcessPath && fs.existsSync(inheritedProcessPath)
|
|
347
378
|
? this.readJsonFile(inheritedProcessPath)
|
|
@@ -361,22 +392,26 @@ export class STLManipulator extends EventEmitter {
|
|
|
361
392
|
: mergedProcess
|
|
362
393
|
);
|
|
363
394
|
|
|
364
|
-
const defaultFilamentProfiles = Array.isArray(parsedProfile.default_filament_profile)
|
|
395
|
+
const defaultFilamentProfiles = !bambuOptions?.loadFilaments && Array.isArray(parsedProfile.default_filament_profile)
|
|
365
396
|
? parsedProfile.default_filament_profile
|
|
366
397
|
: [];
|
|
367
398
|
for (const profileName of defaultFilamentProfiles) {
|
|
368
|
-
const filamentPath =
|
|
399
|
+
const filamentPath = findProfile('filament', String(profileName));
|
|
369
400
|
if (filamentPath) {
|
|
370
401
|
filamentPaths.push(filamentPath);
|
|
402
|
+
} else {
|
|
403
|
+
throw new Error(`Filament profile "${String(profileName)}" was not found; refusing to omit its slot.`);
|
|
371
404
|
}
|
|
372
405
|
}
|
|
373
406
|
|
|
374
|
-
if (filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
|
|
375
|
-
const filamentIdIndex = this.buildFilamentIdIndex();
|
|
407
|
+
if (!bambuOptions?.loadFilaments && filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
|
|
408
|
+
const filamentIdIndex = this.buildFilamentIdIndex(roots);
|
|
376
409
|
for (const filamentId of parsedProfile.filament_ids) {
|
|
377
410
|
const filamentPath = filamentIdIndex.get(String(filamentId));
|
|
378
411
|
if (filamentPath) {
|
|
379
412
|
filamentPaths.push(filamentPath);
|
|
413
|
+
} else {
|
|
414
|
+
throw new Error(`Filament ID "${String(filamentId)}" could not be resolved; refusing to omit its slot.`);
|
|
380
415
|
}
|
|
381
416
|
}
|
|
382
417
|
}
|
|
@@ -389,7 +424,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
389
424
|
typeof machineConfig?.default_print_profile === 'string'
|
|
390
425
|
? machineConfig.default_print_profile
|
|
391
426
|
: undefined;
|
|
392
|
-
const defaultProcessPath =
|
|
427
|
+
const defaultProcessPath = findProfile('process', defaultProcessName);
|
|
393
428
|
if (defaultProcessPath) {
|
|
394
429
|
processPath =
|
|
395
430
|
slicerType === 'orcaslicer'
|
|
@@ -408,9 +443,11 @@ export class STLManipulator extends EventEmitter {
|
|
|
408
443
|
Array.isArray(machineConfig?.default_filament_profile)
|
|
409
444
|
) {
|
|
410
445
|
for (const profileName of machineConfig.default_filament_profile) {
|
|
411
|
-
const filamentPath =
|
|
446
|
+
const filamentPath = findProfile('filament', String(profileName));
|
|
412
447
|
if (filamentPath) {
|
|
413
448
|
filamentPaths.push(filamentPath);
|
|
449
|
+
} else {
|
|
450
|
+
throw new Error(`Filament profile "${String(profileName)}" was not found; refusing to omit its slot.`);
|
|
414
451
|
}
|
|
415
452
|
}
|
|
416
453
|
}
|
|
@@ -431,80 +468,139 @@ export class STLManipulator extends EventEmitter {
|
|
|
431
468
|
|
|
432
469
|
return {
|
|
433
470
|
settingsArg: settingsParts.length > 0 ? settingsParts.join(';') : undefined,
|
|
434
|
-
filamentPaths
|
|
471
|
+
filamentPaths,
|
|
472
|
+
processSource,
|
|
435
473
|
};
|
|
436
474
|
}
|
|
437
475
|
|
|
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
|
-
*/
|
|
476
|
+
/** Resolve BBL dependencies before invoking the CLI; failures stop the slice. */
|
|
453
477
|
private async maybeFlattenBundle(
|
|
454
|
-
bundle:
|
|
478
|
+
bundle: BambuSettingsBundle,
|
|
455
479
|
bambuOptions?: BambuSliceOptions,
|
|
456
480
|
activeSlicerPath?: string
|
|
457
|
-
): Promise<
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
+
): Promise<BambuSettingsBundle> {
|
|
482
|
+
const parts = bundle.settingsArg?.split(';').filter(Boolean) ?? [];
|
|
483
|
+
const readProfile = (filePath: string): Record<string, unknown> => {
|
|
484
|
+
try {
|
|
485
|
+
const data: unknown = this.readJsonFile(filePath);
|
|
486
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
487
|
+
throw new Error('expected a JSON profile object');
|
|
488
|
+
}
|
|
489
|
+
return data as Record<string, unknown>;
|
|
490
|
+
} catch (err: any) {
|
|
491
|
+
throw new Error(`Cannot read slicer profile "${filePath}": ${err?.message ?? err}`);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
const settings = parts.map(readProfile);
|
|
495
|
+
// Read every slot: filtering unreadable names would change positional mapping.
|
|
496
|
+
const filaments = bundle.filamentPaths.map(readProfile);
|
|
497
|
+
const profilesRoot = detectProfilesRoot(activeSlicerPath || process.env.SLICER_PATH);
|
|
498
|
+
const profileRoots = [path.join(profilesRoot, 'BBL'), ...this.getAvailableProfileRoots()];
|
|
499
|
+
const hasReferences = (profile: Record<string, unknown>) =>
|
|
500
|
+
(typeof profile.inherits === 'string' && profile.inherits.length > 0) ||
|
|
501
|
+
(profile.include !== undefined && profile.include !== null);
|
|
502
|
+
const isBundledFile = (filePath: string) =>
|
|
503
|
+
profileRoots.some(root => {
|
|
504
|
+
const relative = path.relative(root, filePath);
|
|
505
|
+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
506
|
+
});
|
|
507
|
+
const isBundled = (filePath: string, profile: Record<string, unknown>) =>
|
|
508
|
+
String(profile.from).toLowerCase() === 'system' || isBundledFile(filePath);
|
|
509
|
+
const needsResolution = (filePath: string, profile: Record<string, unknown>) =>
|
|
510
|
+
hasReferences(profile) || isBundled(filePath, profile);
|
|
511
|
+
if (![...settings, ...filaments].some((profile, i) =>
|
|
512
|
+
needsResolution([...parts, ...bundle.filamentPaths][i], profile))) return bundle;
|
|
513
|
+
if (parts.length !== 2) {
|
|
514
|
+
throw new Error('Resolving BBL profiles requires both a machine and process profile; refusing to slice with unresolved settings.');
|
|
481
515
|
}
|
|
482
516
|
|
|
483
|
-
const
|
|
517
|
+
const [machinePath, processPath] = parts;
|
|
518
|
+
const [machine, processProfile] = settings;
|
|
519
|
+
const leafName = (filePath: string, profile: Record<string, unknown>): string => {
|
|
520
|
+
// A user preset inherits its system identity, while keeping its own values.
|
|
521
|
+
const name = !isBundled(filePath, profile) && typeof profile.inherits === 'string' && profile.inherits
|
|
522
|
+
? profile.inherits
|
|
523
|
+
: profile.name;
|
|
524
|
+
if (typeof name !== 'string' || !name) {
|
|
525
|
+
if (needsResolution(filePath, profile)) throw new Error(`Cannot determine the profile name for "${filePath}".`);
|
|
526
|
+
return path.basename(filePath, '.json');
|
|
527
|
+
}
|
|
528
|
+
return name;
|
|
529
|
+
};
|
|
530
|
+
const flat = await flattenForCli({
|
|
531
|
+
machineLeaf: leafName(machinePath, machine),
|
|
532
|
+
// A generated path loses bundled provenance; inherited metadata can also
|
|
533
|
+
// make a custom preset look like a system profile. Use the original source
|
|
534
|
+
// only for identity and retain the prepared values in sourceProfiles below.
|
|
535
|
+
processLeaf: bundle.processSource
|
|
536
|
+
? leafName(bundle.processSource.filePath, bundle.processSource.profile)
|
|
537
|
+
: leafName(processPath, processProfile),
|
|
538
|
+
filamentLeaves: filaments.map((profile, i) => leafName(bundle.filamentPaths[i], profile)),
|
|
539
|
+
profilesRoot,
|
|
540
|
+
tempDir: this.tempDir,
|
|
541
|
+
bedType: this.resolveBambuStudioBedType(bambuOptions?.bedType),
|
|
542
|
+
filamentColours: bambuOptions?.filamentColours ?? bundle.filamentColours,
|
|
543
|
+
projectTowerPosition: bundle.projectTowerPosition,
|
|
544
|
+
sourceProfiles: {
|
|
545
|
+
machine: isBundledFile(machinePath) ? undefined : machine,
|
|
546
|
+
process: isBundledFile(processPath) ? undefined : processProfile,
|
|
547
|
+
filaments: filaments.map((profile, i) => isBundledFile(bundle.filamentPaths[i]) ? undefined : profile),
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
// Standalone files retain their own identity and settings, but still need
|
|
551
|
+
// the CLI colour/tower fields. Do not discard those required overlays when
|
|
552
|
+
// choosing the original profile instead of the normalized BBL output.
|
|
553
|
+
const withCliFields = (filePath: string, profile: Record<string, unknown>, flatPath: string, keys: string[]): string => {
|
|
554
|
+
const flattened = readProfile(flatPath);
|
|
555
|
+
const fields = Object.fromEntries(keys.filter(key => flattened[key] !== undefined).map(key => [key, flattened[key]]));
|
|
556
|
+
if (Object.entries(fields).every(([key, value]) => JSON.stringify(profile[key]) === JSON.stringify(value))) return filePath;
|
|
557
|
+
return this.writeTempJson(path.basename(filePath, '.json'), 'cli', { ...profile, ...fields });
|
|
558
|
+
};
|
|
559
|
+
return {
|
|
560
|
+
settingsArg: [
|
|
561
|
+
needsResolution(machinePath, machine) ? flat.machinePath : machinePath,
|
|
562
|
+
needsResolution(processPath, processProfile) ? flat.processPath
|
|
563
|
+
: withCliFields(processPath, processProfile, flat.processPath, ['wipe_tower_x', 'wipe_tower_y']),
|
|
564
|
+
].join(';'),
|
|
565
|
+
filamentPaths: filaments.map((profile, i) =>
|
|
566
|
+
needsResolution(bundle.filamentPaths[i], profile) ? flat.filamentPaths[i]
|
|
567
|
+
: withCliFields(bundle.filamentPaths[i], profile, flat.filamentPaths[i], ['filament_colour'])),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
484
570
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
571
|
+
/** --load-filaments is positional; a single override must cover every project slot. */
|
|
572
|
+
private async expandProjectFilaments(
|
|
573
|
+
inputPath: string,
|
|
574
|
+
bundle: BambuSettingsBundle
|
|
575
|
+
): Promise<BambuSettingsBundle> {
|
|
576
|
+
if (!inputPath.toLowerCase().endsWith('.3mf') || bundle.filamentPaths.length === 0) return bundle;
|
|
577
|
+
const JSZip = (await import('jszip')).default;
|
|
578
|
+
const zip = await JSZip.loadAsync(await fs.promises.readFile(inputPath));
|
|
579
|
+
const file = zip.file('Metadata/project_settings.config');
|
|
580
|
+
// Geometry-only 3MFs have no embedded filament slots.
|
|
581
|
+
if (!file) return bundle;
|
|
582
|
+
const config: unknown = JSON.parse(await file.async('string'));
|
|
583
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
584
|
+
throw new Error('Cannot determine project filament slots: invalid project_settings.config.');
|
|
585
|
+
}
|
|
586
|
+
const data = config as Record<string, unknown>;
|
|
587
|
+
const count = Math.max(0, ...['filament_settings_id', 'filament_type', 'filament_colour', 'filament_diameter']
|
|
588
|
+
.map(key => Array.isArray(data[key]) ? data[key].length : 0));
|
|
589
|
+
if (count === 0) throw new Error('Cannot determine project filament slots from project_settings.config.');
|
|
590
|
+
// Keep the project's slot colours; loaded profiles carry none of their own.
|
|
591
|
+
const colours = Array.isArray(data.filament_colour) && data.filament_colour.length === count &&
|
|
592
|
+
data.filament_colour.every(c => typeof c === 'string' && /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(c))
|
|
593
|
+
? data.filament_colour as string[]
|
|
594
|
+
: undefined;
|
|
595
|
+
const projectTowerPosition = Object.fromEntries(['wipe_tower_x', 'wipe_tower_y']
|
|
596
|
+
.filter(key => data[key] !== undefined).map(key => [key, data[key]]));
|
|
597
|
+
if (bundle.filamentPaths.length === 1) {
|
|
598
|
+
return { ...bundle, filamentPaths: Array(count).fill(bundle.filamentPaths[0]), filamentColours: colours, projectTowerPosition };
|
|
599
|
+
}
|
|
600
|
+
if (bundle.filamentPaths.length !== count) {
|
|
601
|
+
throw new Error(`Project declares ${count} filament slots, but ${bundle.filamentPaths.length} profiles were supplied. Provide one profile for all slots or one per slot.`);
|
|
507
602
|
}
|
|
603
|
+
return { ...bundle, filamentColours: colours, projectTowerPosition };
|
|
508
604
|
}
|
|
509
605
|
|
|
510
606
|
private resolveBambuStudioBedType(bedType?: string): string | undefined {
|
|
@@ -518,16 +614,6 @@ export class STLManipulator extends EventEmitter {
|
|
|
518
614
|
return BAMBU_CLI_BED_TYPES[normalized] || bedType;
|
|
519
615
|
}
|
|
520
616
|
|
|
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
|
-
|
|
531
617
|
/**
|
|
532
618
|
* Load STL file and return geometry and bounding box
|
|
533
619
|
*/
|
|
@@ -1510,14 +1596,14 @@ export class STLManipulator extends EventEmitter {
|
|
|
1510
1596
|
slicerType,
|
|
1511
1597
|
slicerProfile,
|
|
1512
1598
|
printerPreset,
|
|
1513
|
-
bambuOptions
|
|
1599
|
+
bambuOptions,
|
|
1600
|
+
slicerType === 'bambustudio' ? detectProfilesRoot(slicerPath) : undefined
|
|
1514
1601
|
);
|
|
1515
|
-
//
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
: rawBundle;
|
|
1602
|
+
// Resolve every BBL dependency before launching the slicer.
|
|
1603
|
+
const settingsBundle = slicerType === 'bambustudio'
|
|
1604
|
+
? await this.maybeFlattenBundle(
|
|
1605
|
+
await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, slicerPath)
|
|
1606
|
+
: rawBundle;
|
|
1521
1607
|
args = [
|
|
1522
1608
|
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
1523
1609
|
'--outputdir', outputDir,
|