bambu-printer-mcp 1.1.9 → 1.1.11

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/dist/index.js CHANGED
@@ -1599,7 +1599,7 @@ class BambuPrinterMCPServer {
1599
1599
  description: "Type of slicer to use. Bambu-compatible choices (bambustudio, orcaslicer, orcaslicer-bambulab) export sliced 3MF; aliases such as fulu-orca and orca-studio are accepted."
1600
1600
  },
1601
1601
  slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
1602
- slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
1602
+ slicer_profile: { type: "string", description: "Path to an optional process profile/config file. The exact bambu_model/nozzle machine preset is still required." },
1603
1603
  template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when slicing a new STL or 3MF." },
1604
1604
  template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
1605
1605
  template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
@@ -49,6 +49,8 @@ export interface FlattenOptions {
49
49
  filamentLeaves: string[];
50
50
  /** Absolute path to `.../Resources/profiles`. */
51
51
  profilesRoot: string;
52
+ /** Configured BBL directories for custom process/filament dependencies only. */
53
+ userProfileRoots?: string[];
52
54
  /** Where to write flattened temp files. */
53
55
  tempDir: string;
54
56
  /** Vendor subdir under profilesRoot. Currently only "BBL" supported. */
@@ -82,6 +84,8 @@ export interface FlattenOptions {
82
84
  wipe_tower_y?: unknown;
83
85
  };
84
86
  }
87
+ /** Resolve bundled machine defaults before choosing process and filament leaves. */
88
+ export declare function resolveBblMachineProfile(profilesRoot: string, machineLeaf: string): Promise<Record<string, unknown>>;
85
89
  /** Best-effort extruder count for fallback nozzle_volume_type sizing. */
86
90
  /** BambuStudio's built-in filament_colour default. */
87
91
  export declare const DEFAULT_FILAMENT_COLOUR = "#00AE42";
@@ -97,4 +101,4 @@ export declare function flattenForCli(opts: FlattenOptions): Promise<FlattenedPr
97
101
  *
98
102
  * Override via BAMBU_PROFILES_ROOT env.
99
103
  */
100
- export declare function detectProfilesRoot(slicerPath?: string): string;
104
+ export declare function detectProfilesRoot(slicerPath?: string, slicerType?: string): string;
@@ -39,11 +39,14 @@ import crypto from "node:crypto";
39
39
  * abstract bases like `fdm_machine_common` and `fdm_bbl_3dp_001_common`,
40
40
  * because those are the parents we'll walk to.
41
41
  */
42
- async function buildNameIndex(profilesRoot, vendor) {
42
+ async function buildNameIndex(profilesRoot, vendor, userProfileRoots = []) {
43
43
  const index = new Map();
44
44
  const subdirs = ["machine", "process", "filament"];
45
- for (const sub of subdirs) {
46
- const dir = path.join(profilesRoot, vendor, sub);
45
+ const directories = [
46
+ ...subdirs.map(sub => path.join(profilesRoot, vendor, sub)),
47
+ ...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
48
+ ];
49
+ for (const dir of directories) {
47
50
  let entries;
48
51
  try {
49
52
  entries = await fs.readdir(dir);
@@ -72,6 +75,8 @@ async function buildNameIndex(profilesRoot, vendor) {
72
75
  // Malformed profile -- skip, don't poison the index.
73
76
  continue;
74
77
  }
78
+ if (!data || typeof data !== 'object' || Array.isArray(data))
79
+ continue;
75
80
  const name = data["name"];
76
81
  if (typeof name !== "string" || name.length === 0)
77
82
  continue;
@@ -123,6 +128,10 @@ function flattenData(data, index, visiting = new Set()) {
123
128
  delete merged["include"];
124
129
  return merged;
125
130
  }
131
+ /** Resolve bundled machine defaults before choosing process and filament leaves. */
132
+ export async function resolveBblMachineProfile(profilesRoot, machineLeaf) {
133
+ return flattenByName(machineLeaf, await buildNameIndex(profilesRoot, 'BBL'));
134
+ }
126
135
  /** Keys of an include template that describe the template, not the config. */
127
136
  const INCLUDE_METADATA_KEYS = new Set(["name", "type", "from", "instantiation", "inherits", "include", "setting_id"]);
128
137
  /**
@@ -514,14 +523,18 @@ export async function flattenForCli(opts) {
514
523
  throw new Error(`profile-flatten: profilesRoot "${opts.profilesRoot}" does not contain "${vendor}/machine". ` +
515
524
  `Set BAMBU_PROFILES_ROOT or check your BambuStudio install.`);
516
525
  }
517
- const index = await buildNameIndex(opts.profilesRoot, vendor);
526
+ // Machine inheritance must never resolve through user process/filament files.
527
+ const machineIndex = await buildNameIndex(opts.profilesRoot, vendor);
528
+ const index = opts.userProfileRoots?.length
529
+ ? await buildNameIndex(opts.profilesRoot, vendor, opts.userProfileRoots)
530
+ : machineIndex;
518
531
  // Flatten each leaf.
519
532
  if (opts.sourceProfiles?.filaments && opts.sourceProfiles.filaments.length !== opts.filamentLeaves.length) {
520
533
  throw new Error("Every filament slot must have a source profile.");
521
534
  }
522
535
  const machineFlat = opts.sourceProfiles?.machine
523
- ? flattenData(opts.sourceProfiles.machine, index)
524
- : flattenByName(opts.machineLeaf, index);
536
+ ? flattenData(opts.sourceProfiles.machine, machineIndex)
537
+ : flattenByName(opts.machineLeaf, machineIndex);
525
538
  const processFlat = opts.sourceProfiles?.process
526
539
  ? flattenData(opts.sourceProfiles.process, index)
527
540
  : flattenByName(opts.processLeaf, index);
@@ -531,7 +544,7 @@ export async function flattenForCli(opts) {
531
544
  });
532
545
  // CLI-specific post-processing on machine profile only.
533
546
  deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
534
- applyMachineModelBedMetadata(machineFlat, index);
547
+ applyMachineModelBedMetadata(machineFlat, machineIndex);
535
548
  // A standalone custom machine can accompany process/filament profiles that
536
549
  // need BBL resolution; do not impose bundled model config on that machine.
537
550
  const sourceMachine = opts.sourceProfiles?.machine;
@@ -603,7 +616,7 @@ async function writeTemp(tempDir, kind, leafName, data) {
603
616
  *
604
617
  * Override via BAMBU_PROFILES_ROOT env.
605
618
  */
606
- export function detectProfilesRoot(slicerPath) {
619
+ export function detectProfilesRoot(slicerPath, slicerType = 'bambustudio') {
607
620
  if (process.env["BAMBU_PROFILES_ROOT"]) {
608
621
  return process.env["BAMBU_PROFILES_ROOT"];
609
622
  }
@@ -622,14 +635,18 @@ export function detectProfilesRoot(slicerPath) {
622
635
  }
623
636
  catch { /* Probe the supplied path below. */ }
624
637
  const bin = path.dirname(executable);
638
+ const installNames = slicerType === 'bambustudio'
639
+ ? ['BambuStudio', 'bambu-studio']
640
+ : ['OrcaSlicer', 'orca-slicer', 'OrcaStudio', 'orca-studio'];
625
641
  const candidates = path.basename(bin) === 'MacOS'
626
642
  ? [path.resolve(bin, '..', 'Resources', 'profiles')]
627
643
  : [
628
644
  path.join(bin, 'resources', 'profiles'),
629
645
  path.join(bin, 'Resources', 'profiles'),
630
- path.resolve(bin, '..', 'share', 'BambuStudio', 'profiles'),
631
- path.resolve(bin, '..', 'share', 'bambu-studio', 'profiles'),
632
- path.resolve(bin, '..', 'share', 'BambuStudio', 'resources', 'profiles'),
646
+ ...installNames.flatMap(name => [
647
+ path.resolve(bin, '..', 'share', name, 'profiles'),
648
+ path.resolve(bin, '..', 'share', name, 'resources', 'profiles'),
649
+ ]),
633
650
  ];
634
651
  const found = candidates.find(root => existsSync(path.join(root, 'BBL', 'machine')));
635
652
  // Do not select another installation if this executable has no profile tree.
@@ -637,5 +654,7 @@ export function detectProfilesRoot(slicerPath) {
637
654
  return found ?? candidates[0];
638
655
  }
639
656
  // Default macOS install.
640
- return "/Applications/BambuStudio.app/Contents/Resources/profiles";
657
+ return slicerType === 'bambustudio'
658
+ ? '/Applications/BambuStudio.app/Contents/Resources/profiles'
659
+ : '/Applications/OrcaSlicer.app/Contents/Resources/profiles';
641
660
  }
@@ -9,7 +9,7 @@ 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
+ import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
13
13
  const readFileAsync = promisify(fs.readFile);
14
14
  const writeFileAsync = promisify(fs.writeFile);
15
15
  const BAMBU_PROFILE_ROOTS = [
@@ -206,16 +206,31 @@ export class STLManipulator extends EventEmitter {
206
206
  fs.writeFileSync(outPath, serialized);
207
207
  return outPath;
208
208
  }
209
- resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, activeProfilesRoot) {
209
+ async resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, activeProfilesRoot) {
210
210
  const roots = activeProfilesRoot
211
- ? [path.join(activeProfilesRoot, 'BBL'), ...this.getAvailableProfileRoots()]
211
+ ? [path.join(activeProfilesRoot, 'BBL')]
212
212
  : this.getAvailableProfileRoots();
213
- const findProfile = (kind, name) => this.findProfileFile(kind, name, roots);
213
+ const findProfile = (kind, name) => this.findProfileFile(kind, name, kind === 'machine' ? roots : [...roots, ...configuredBambuProfileDirs()]);
214
+ if (!printerPreset) {
215
+ throw new Error('Printer preset is required for Bambu-compatible CLI slicing; select the exact bambu_model and nozzle_diameter.');
216
+ }
214
217
  const machinePath = findProfile('machine', printerPreset);
215
- if (slicerType === 'bambustudio' && printerPreset && !machinePath) {
216
- throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching BambuStudio profile tree; refusing to use printer defaults.`);
218
+ if (!machinePath) {
219
+ throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching ${slicerType} profile tree; refusing to use printer defaults.`);
220
+ }
221
+ let machineConfig;
222
+ try {
223
+ machineConfig = this.readJsonFile(machinePath);
224
+ if (!machineConfig || Array.isArray(machineConfig) || machineConfig.name !== printerPreset) {
225
+ throw new Error('expected a JSON machine profile with the selected preset name');
226
+ }
217
227
  }
218
- const machineConfig = machinePath ? this.readJsonFile(machinePath) : null;
228
+ catch (error) {
229
+ throw new Error(`Printer profile "${printerPreset}" is invalid: ${error?.message ?? error}`);
230
+ }
231
+ // Some FULU/Orca machine leaves inherit their default process/filaments.
232
+ // Resolve those selections before building the positional CLI bundle.
233
+ machineConfig = await resolveBblMachineProfile(activeProfilesRoot ?? path.dirname(path.dirname(path.dirname(machinePath))), printerPreset);
219
234
  const hasSlicerProfile = !!slicerProfile && fs.existsSync(slicerProfile);
220
235
  const filamentPaths = [];
221
236
  let processPath;
@@ -267,7 +282,7 @@ export class STLManipulator extends EventEmitter {
267
282
  }
268
283
  }
269
284
  if (!bambuOptions?.loadFilaments && filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
270
- const filamentIdIndex = this.buildFilamentIdIndex(roots);
285
+ const filamentIdIndex = this.buildFilamentIdIndex([...roots, ...configuredBambuProfileDirs()]);
271
286
  for (const filamentId of parsedProfile.filament_ids) {
272
287
  const filamentPath = filamentIdIndex.get(String(filamentId));
273
288
  if (filamentPath) {
@@ -324,7 +339,7 @@ export class STLManipulator extends EventEmitter {
324
339
  };
325
340
  }
326
341
  /** Resolve BBL dependencies before invoking the CLI; failures stop the slice. */
327
- async maybeFlattenBundle(bundle, bambuOptions, activeSlicerPath) {
342
+ async maybeFlattenBundle(bundle, bambuOptions, activeProfilesRoot) {
328
343
  const parts = bundle.settingsArg?.split(';').filter(Boolean) ?? [];
329
344
  const readProfile = (filePath) => {
330
345
  try {
@@ -341,8 +356,8 @@ export class STLManipulator extends EventEmitter {
341
356
  const settings = parts.map(readProfile);
342
357
  // Read every slot: filtering unreadable names would change positional mapping.
343
358
  const filaments = bundle.filamentPaths.map(readProfile);
344
- const profilesRoot = detectProfilesRoot(activeSlicerPath || process.env.SLICER_PATH);
345
- const profileRoots = [path.join(profilesRoot, 'BBL'), ...this.getAvailableProfileRoots()];
359
+ const profilesRoot = activeProfilesRoot ?? detectProfilesRoot(process.env.SLICER_PATH);
360
+ const profileRoots = [path.join(profilesRoot, 'BBL')];
346
361
  const hasReferences = (profile) => (typeof profile.inherits === 'string' && profile.inherits.length > 0) ||
347
362
  (profile.include !== undefined && profile.include !== null);
348
363
  const isBundledFile = (filePath) => profileRoots.some(root => {
@@ -380,6 +395,7 @@ export class STLManipulator extends EventEmitter {
380
395
  : leafName(processPath, processProfile),
381
396
  filamentLeaves: filaments.map((profile, i) => leafName(bundle.filamentPaths[i], profile)),
382
397
  profilesRoot,
398
+ userProfileRoots: configuredBambuProfileDirs(),
383
399
  tempDir: this.tempDir,
384
400
  bedType: this.resolveBambuStudioBedType(bambuOptions?.bedType),
385
401
  filamentColours: bambuOptions?.filamentColours ?? bundle.filamentColours,
@@ -1222,11 +1238,16 @@ export class STLManipulator extends EventEmitter {
1222
1238
  const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
1223
1239
  const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
1224
1240
  const outputDir = path.dirname(bambuOutputPath);
1225
- const rawBundle = this.resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, slicerType === 'bambustudio' ? detectProfilesRoot(slicerPath) : undefined);
1241
+ const profilesRoot = detectProfilesRoot(slicerPath, slicerType);
1242
+ const rawBundle = await this.resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, profilesRoot);
1226
1243
  // Resolve every BBL dependency before launching the slicer.
1227
- const settingsBundle = slicerType === 'bambustudio'
1228
- ? await this.maybeFlattenBundle(await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, slicerPath)
1229
- : rawBundle;
1244
+ const settingsBundle = await this.maybeFlattenBundle(await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, profilesRoot);
1245
+ // Inherited process G-code is now present; apply Orca's existing
1246
+ // absolute-extrusion normalization after resolving those ancestors.
1247
+ if (slicerType === 'orcaslicer' && settingsBundle.settingsArg) {
1248
+ const [machinePath, processPath] = settingsBundle.settingsArg.split(';');
1249
+ settingsBundle.settingsArg = [machinePath, this.writeTempJson(outputBase, 'process_orca_resolved', this.sanitizeProcessForOrca(this.readJsonFile(processPath), printerPreset))].join(';');
1250
+ }
1230
1251
  args = [
1231
1252
  '--slice', String(bambuOptions?.slicePlate ?? 0),
1232
1253
  '--outputdir', outputDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bambu-printer-mcp",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
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": {
package/src/index.ts CHANGED
@@ -2095,7 +2095,7 @@ class BambuPrinterMCPServer {
2095
2095
  description: "Type of slicer to use. Bambu-compatible choices (bambustudio, orcaslicer, orcaslicer-bambulab) export sliced 3MF; aliases such as fulu-orca and orca-studio are accepted."
2096
2096
  },
2097
2097
  slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2098
- slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
2098
+ slicer_profile: { type: "string", description: "Path to an optional process profile/config file. The exact bambu_model/nozzle machine preset is still required." },
2099
2099
  template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when slicing a new STL or 3MF." },
2100
2100
  template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
2101
2101
  template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
@@ -57,6 +57,8 @@ export interface FlattenOptions {
57
57
  filamentLeaves: string[];
58
58
  /** Absolute path to `.../Resources/profiles`. */
59
59
  profilesRoot: string;
60
+ /** Configured BBL directories for custom process/filament dependencies only. */
61
+ userProfileRoots?: string[];
60
62
  /** Where to write flattened temp files. */
61
63
  tempDir: string;
62
64
  /** Vendor subdir under profilesRoot. Currently only "BBL" supported. */
@@ -110,13 +112,17 @@ type NameIndex = Map<string, IndexedProfile>;
110
112
  */
111
113
  async function buildNameIndex(
112
114
  profilesRoot: string,
113
- vendor: string
115
+ vendor: string,
116
+ userProfileRoots: string[] = []
114
117
  ): Promise<NameIndex> {
115
118
  const index: NameIndex = new Map();
116
119
  const subdirs: ProfileKind[] = ["machine", "process", "filament"];
117
120
 
118
- for (const sub of subdirs) {
119
- const dir = path.join(profilesRoot, vendor, sub);
121
+ const directories = [
122
+ ...subdirs.map(sub => path.join(profilesRoot, vendor, sub)),
123
+ ...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
124
+ ];
125
+ for (const dir of directories) {
120
126
  let entries: string[];
121
127
  try {
122
128
  entries = await fs.readdir(dir);
@@ -141,6 +147,7 @@ async function buildNameIndex(
141
147
  // Malformed profile -- skip, don't poison the index.
142
148
  continue;
143
149
  }
150
+ if (!data || typeof data !== 'object' || Array.isArray(data)) continue;
144
151
  const name = data["name"];
145
152
  if (typeof name !== "string" || name.length === 0) continue;
146
153
  // First-write wins. BBL doesn't have name collisions in practice;
@@ -203,6 +210,14 @@ function flattenData(
203
210
  return merged;
204
211
  }
205
212
 
213
+ /** Resolve bundled machine defaults before choosing process and filament leaves. */
214
+ export async function resolveBblMachineProfile(
215
+ profilesRoot: string,
216
+ machineLeaf: string
217
+ ): Promise<Record<string, unknown>> {
218
+ return flattenByName(machineLeaf, await buildNameIndex(profilesRoot, 'BBL'));
219
+ }
220
+
206
221
  /** Keys of an include template that describe the template, not the config. */
207
222
  const INCLUDE_METADATA_KEYS = new Set(["name", "type", "from", "instantiation", "inherits", "include", "setting_id"]);
208
223
 
@@ -644,15 +659,19 @@ export async function flattenForCli(opts: FlattenOptions): Promise<FlattenedProf
644
659
  );
645
660
  }
646
661
 
647
- const index = await buildNameIndex(opts.profilesRoot, vendor);
662
+ // Machine inheritance must never resolve through user process/filament files.
663
+ const machineIndex = await buildNameIndex(opts.profilesRoot, vendor);
664
+ const index = opts.userProfileRoots?.length
665
+ ? await buildNameIndex(opts.profilesRoot, vendor, opts.userProfileRoots)
666
+ : machineIndex;
648
667
 
649
668
  // Flatten each leaf.
650
669
  if (opts.sourceProfiles?.filaments && opts.sourceProfiles.filaments.length !== opts.filamentLeaves.length) {
651
670
  throw new Error("Every filament slot must have a source profile.");
652
671
  }
653
672
  const machineFlat = opts.sourceProfiles?.machine
654
- ? flattenData(opts.sourceProfiles.machine, index)
655
- : flattenByName(opts.machineLeaf, index);
673
+ ? flattenData(opts.sourceProfiles.machine, machineIndex)
674
+ : flattenByName(opts.machineLeaf, machineIndex);
656
675
  const processFlat = opts.sourceProfiles?.process
657
676
  ? flattenData(opts.sourceProfiles.process, index)
658
677
  : flattenByName(opts.processLeaf, index);
@@ -663,7 +682,7 @@ export async function flattenForCli(opts: FlattenOptions): Promise<FlattenedProf
663
682
 
664
683
  // CLI-specific post-processing on machine profile only.
665
684
  deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
666
- applyMachineModelBedMetadata(machineFlat, index);
685
+ applyMachineModelBedMetadata(machineFlat, machineIndex);
667
686
  // A standalone custom machine can accompany process/filament profiles that
668
687
  // need BBL resolution; do not impose bundled model config on that machine.
669
688
  const sourceMachine = opts.sourceProfiles?.machine;
@@ -751,7 +770,7 @@ async function writeTemp(
751
770
  *
752
771
  * Override via BAMBU_PROFILES_ROOT env.
753
772
  */
754
- export function detectProfilesRoot(slicerPath?: string): string {
773
+ export function detectProfilesRoot(slicerPath?: string, slicerType = 'bambustudio'): string {
755
774
  if (process.env["BAMBU_PROFILES_ROOT"]) {
756
775
  return process.env["BAMBU_PROFILES_ROOT"];
757
776
  }
@@ -767,14 +786,18 @@ export function detectProfilesRoot(slicerPath?: string): string {
767
786
  // Package-manager launchers may be symlinks into the installation prefix.
768
787
  try { executable = realpathSync(executable); } catch { /* Probe the supplied path below. */ }
769
788
  const bin = path.dirname(executable);
789
+ const installNames = slicerType === 'bambustudio'
790
+ ? ['BambuStudio', 'bambu-studio']
791
+ : ['OrcaSlicer', 'orca-slicer', 'OrcaStudio', 'orca-studio'];
770
792
  const candidates = path.basename(bin) === 'MacOS'
771
793
  ? [path.resolve(bin, '..', 'Resources', 'profiles')]
772
794
  : [
773
795
  path.join(bin, 'resources', 'profiles'),
774
796
  path.join(bin, 'Resources', 'profiles'),
775
- path.resolve(bin, '..', 'share', 'BambuStudio', 'profiles'),
776
- path.resolve(bin, '..', 'share', 'bambu-studio', 'profiles'),
777
- path.resolve(bin, '..', 'share', 'BambuStudio', 'resources', 'profiles'),
797
+ ...installNames.flatMap(name => [
798
+ path.resolve(bin, '..', 'share', name, 'profiles'),
799
+ path.resolve(bin, '..', 'share', name, 'resources', 'profiles'),
800
+ ]),
778
801
  ];
779
802
  const found = candidates.find(root => existsSync(path.join(root, 'BBL', 'machine')));
780
803
  // Do not select another installation if this executable has no profile tree.
@@ -783,5 +806,7 @@ export function detectProfilesRoot(slicerPath?: string): string {
783
806
  }
784
807
 
785
808
  // Default macOS install.
786
- return "/Applications/BambuStudio.app/Contents/Resources/profiles";
809
+ return slicerType === 'bambustudio'
810
+ ? '/Applications/BambuStudio.app/Contents/Resources/profiles'
811
+ : '/Applications/OrcaSlicer.app/Contents/Resources/profiles';
787
812
  }
@@ -9,7 +9,7 @@ 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
+ import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
13
13
 
14
14
  const readFileAsync = promisify(fs.readFile);
15
15
  const writeFileAsync = promisify(fs.writeFile);
@@ -324,24 +324,39 @@ export class STLManipulator extends EventEmitter {
324
324
  return outPath;
325
325
  }
326
326
 
327
- private resolveBambuLikeSettingsBundle(
327
+ private async resolveBambuLikeSettingsBundle(
328
328
  outputBase: string,
329
329
  slicerType: 'orcaslicer' | 'orcaslicer-bambulab' | 'bambustudio',
330
330
  slicerProfile?: string,
331
331
  printerPreset?: string,
332
332
  bambuOptions?: BambuSliceOptions,
333
333
  activeProfilesRoot?: string
334
- ): BambuSettingsBundle {
334
+ ): Promise<BambuSettingsBundle> {
335
335
  const roots = activeProfilesRoot
336
- ? [path.join(activeProfilesRoot, 'BBL'), ...this.getAvailableProfileRoots()]
336
+ ? [path.join(activeProfilesRoot, 'BBL')]
337
337
  : this.getAvailableProfileRoots();
338
338
  const findProfile = (kind: 'machine' | 'process' | 'filament', name?: string) =>
339
- this.findProfileFile(kind, name, roots);
339
+ this.findProfileFile(kind, name, kind === 'machine' ? roots : [...roots, ...configuredBambuProfileDirs()]);
340
+ if (!printerPreset) {
341
+ throw new Error('Printer preset is required for Bambu-compatible CLI slicing; select the exact bambu_model and nozzle_diameter.');
342
+ }
340
343
  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.`);
344
+ if (!machinePath) {
345
+ throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching ${slicerType} profile tree; refusing to use printer defaults.`);
343
346
  }
344
- const machineConfig = machinePath ? this.readJsonFile(machinePath) : null;
347
+ let machineConfig: any;
348
+ try {
349
+ machineConfig = this.readJsonFile(machinePath);
350
+ if (!machineConfig || Array.isArray(machineConfig) || machineConfig.name !== printerPreset) {
351
+ throw new Error('expected a JSON machine profile with the selected preset name');
352
+ }
353
+ } catch (error: any) {
354
+ throw new Error(`Printer profile "${printerPreset}" is invalid: ${error?.message ?? error}`);
355
+ }
356
+ // Some FULU/Orca machine leaves inherit their default process/filaments.
357
+ // Resolve those selections before building the positional CLI bundle.
358
+ machineConfig = await resolveBblMachineProfile(
359
+ activeProfilesRoot ?? path.dirname(path.dirname(path.dirname(machinePath))), printerPreset);
345
360
  const hasSlicerProfile = !!slicerProfile && fs.existsSync(slicerProfile);
346
361
  const filamentPaths: string[] = [];
347
362
 
@@ -405,7 +420,7 @@ export class STLManipulator extends EventEmitter {
405
420
  }
406
421
 
407
422
  if (!bambuOptions?.loadFilaments && filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
408
- const filamentIdIndex = this.buildFilamentIdIndex(roots);
423
+ const filamentIdIndex = this.buildFilamentIdIndex([...roots, ...configuredBambuProfileDirs()]);
409
424
  for (const filamentId of parsedProfile.filament_ids) {
410
425
  const filamentPath = filamentIdIndex.get(String(filamentId));
411
426
  if (filamentPath) {
@@ -477,7 +492,7 @@ export class STLManipulator extends EventEmitter {
477
492
  private async maybeFlattenBundle(
478
493
  bundle: BambuSettingsBundle,
479
494
  bambuOptions?: BambuSliceOptions,
480
- activeSlicerPath?: string
495
+ activeProfilesRoot?: string
481
496
  ): Promise<BambuSettingsBundle> {
482
497
  const parts = bundle.settingsArg?.split(';').filter(Boolean) ?? [];
483
498
  const readProfile = (filePath: string): Record<string, unknown> => {
@@ -494,8 +509,8 @@ export class STLManipulator extends EventEmitter {
494
509
  const settings = parts.map(readProfile);
495
510
  // Read every slot: filtering unreadable names would change positional mapping.
496
511
  const filaments = bundle.filamentPaths.map(readProfile);
497
- const profilesRoot = detectProfilesRoot(activeSlicerPath || process.env.SLICER_PATH);
498
- const profileRoots = [path.join(profilesRoot, 'BBL'), ...this.getAvailableProfileRoots()];
512
+ const profilesRoot = activeProfilesRoot ?? detectProfilesRoot(process.env.SLICER_PATH);
513
+ const profileRoots = [path.join(profilesRoot, 'BBL')];
499
514
  const hasReferences = (profile: Record<string, unknown>) =>
500
515
  (typeof profile.inherits === 'string' && profile.inherits.length > 0) ||
501
516
  (profile.include !== undefined && profile.include !== null);
@@ -537,6 +552,7 @@ export class STLManipulator extends EventEmitter {
537
552
  : leafName(processPath, processProfile),
538
553
  filamentLeaves: filaments.map((profile, i) => leafName(bundle.filamentPaths[i], profile)),
539
554
  profilesRoot,
555
+ userProfileRoots: configuredBambuProfileDirs(),
540
556
  tempDir: this.tempDir,
541
557
  bedType: this.resolveBambuStudioBedType(bambuOptions?.bedType),
542
558
  filamentColours: bambuOptions?.filamentColours ?? bundle.filamentColours,
@@ -1591,19 +1607,27 @@ export class STLManipulator extends EventEmitter {
1591
1607
  const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
1592
1608
  const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
1593
1609
  const outputDir = path.dirname(bambuOutputPath);
1594
- const rawBundle = this.resolveBambuLikeSettingsBundle(
1610
+ const profilesRoot = detectProfilesRoot(slicerPath, slicerType);
1611
+ const rawBundle = await this.resolveBambuLikeSettingsBundle(
1595
1612
  outputBase,
1596
1613
  slicerType,
1597
1614
  slicerProfile,
1598
1615
  printerPreset,
1599
1616
  bambuOptions,
1600
- slicerType === 'bambustudio' ? detectProfilesRoot(slicerPath) : undefined
1617
+ profilesRoot
1601
1618
  );
1602
1619
  // 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;
1620
+ const settingsBundle = await this.maybeFlattenBundle(
1621
+ await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, profilesRoot);
1622
+ // Inherited process G-code is now present; apply Orca's existing
1623
+ // absolute-extrusion normalization after resolving those ancestors.
1624
+ if (slicerType === 'orcaslicer' && settingsBundle.settingsArg) {
1625
+ const [machinePath, processPath] = settingsBundle.settingsArg.split(';');
1626
+ settingsBundle.settingsArg = [machinePath, this.writeTempJson(
1627
+ outputBase, 'process_orca_resolved',
1628
+ this.sanitizeProcessForOrca(this.readJsonFile(processPath), printerPreset)
1629
+ )].join(';');
1630
+ }
1607
1631
  args = [
1608
1632
  '--slice', String(bambuOptions?.slicePlate ?? 0),
1609
1633
  '--outputdir', outputDir,