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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * BambuStudio CLI profile flattener.
3
+ *
4
+ * Background: BambuStudio's bundled profile JSONs (Resources/profiles/BBL/...)
5
+ * use an `inherits` chain that the GUI resolves at runtime but the CLI does
6
+ * not. Passing a leaf profile straight to `--load-settings` / `--load-filaments`
7
+ * yields a partial config and the slicer asserts (e.g. `nozzle_volume_type
8
+ * not found` -> SIGSEGV / assertion in MutablePolygon.cpp / Geometry.hpp).
9
+ * See https://github.com/bambulab/BambuStudio/issues/9636 and #9968.
10
+ *
11
+ * This module:
12
+ * 1. Indexes every BBL profile JSON by its `name` field.
13
+ * 2. Recursively walks `inherits`, deep-merging parent into child.
14
+ * 3. Derives `nozzle_volume_type` from `default_nozzle_volume_type[0]`
15
+ * (the GUI does this implicitly; the CLI doesn't).
16
+ * 4. Merges CLI-specific machine_limits from `BBL/cli_config.json` so the
17
+ * printer doesn't run unsafe accelerations / jerks.
18
+ * 5. Writes the flattened JSON to a temp file the caller passes to
19
+ * BambuStudio CLI.
20
+ *
21
+ * BBL only. Other vendors are out of scope and rejected explicitly so we
22
+ * fail loud rather than producing dangerous gcode for hardware we don't own.
23
+ */
24
+ export type ProfileKind = "machine" | "process" | "filament";
25
+ export interface FlattenedProfiles {
26
+ machinePath: string;
27
+ processPath: string;
28
+ filamentPaths: string[];
29
+ /** Diagnostics for callers / logs. Not part of the slicer invocation. */
30
+ meta: {
31
+ profilesRoot: string;
32
+ machineLeafName: string;
33
+ processLeafName: string;
34
+ filamentLeafNames: string[];
35
+ cliOverlayApplied: boolean;
36
+ };
37
+ }
38
+ export interface FlattenOptions {
39
+ /** e.g. "Bambu Lab H2S 0.4 nozzle" */
40
+ machineLeaf: string;
41
+ /** e.g. "0.20mm Standard @BBL H2S" */
42
+ processLeaf: string;
43
+ /** e.g. ["Bambu PLA Basic @BBL H2S"] */
44
+ filamentLeaves: string[];
45
+ /** Absolute path to `.../Resources/profiles`. */
46
+ profilesRoot: string;
47
+ /** Where to write flattened temp files. */
48
+ tempDir: string;
49
+ /** Vendor subdir under profilesRoot. Currently only "BBL" supported. */
50
+ vendor?: string;
51
+ /**
52
+ * Override for installed nozzle flow type. The printer reports this on
53
+ * boot from its physical nozzle scan; both nozzles always match (you
54
+ * can't mix Standard + High Flow, just like you can't mix 0.2 + 0.4).
55
+ * If omitted we use `default_nozzle_volume_type` from the profile tree
56
+ * (= "Standard" on stock BBL machines). Pass "High Flow" when HF
57
+ * nozzles are installed.
58
+ */
59
+ nozzleVolumeType?: "Standard" | "High Flow";
60
+ /** BambuStudio display name, e.g. "Textured PEI Plate" or "Cool Plate". */
61
+ bedType?: string;
62
+ }
63
+ /**
64
+ * Flatten the leaf profiles, post-process for CLI, and write to temp files.
65
+ *
66
+ * Throws on unknown leaf names, missing profilesRoot, or cycles.
67
+ */
68
+ export declare function flattenForCli(opts: FlattenOptions): Promise<FlattenedProfiles>;
69
+ /**
70
+ * Given the SLICER_PATH (path to BambuStudio executable), walk up to the
71
+ * Resources/profiles directory. Falls back to common platform paths.
72
+ *
73
+ * Override via BAMBU_PROFILES_ROOT env.
74
+ */
75
+ export declare function detectProfilesRoot(slicerPath?: string): string;
@@ -0,0 +1,446 @@
1
+ /**
2
+ * BambuStudio CLI profile flattener.
3
+ *
4
+ * Background: BambuStudio's bundled profile JSONs (Resources/profiles/BBL/...)
5
+ * use an `inherits` chain that the GUI resolves at runtime but the CLI does
6
+ * not. Passing a leaf profile straight to `--load-settings` / `--load-filaments`
7
+ * yields a partial config and the slicer asserts (e.g. `nozzle_volume_type
8
+ * not found` -> SIGSEGV / assertion in MutablePolygon.cpp / Geometry.hpp).
9
+ * See https://github.com/bambulab/BambuStudio/issues/9636 and #9968.
10
+ *
11
+ * This module:
12
+ * 1. Indexes every BBL profile JSON by its `name` field.
13
+ * 2. Recursively walks `inherits`, deep-merging parent into child.
14
+ * 3. Derives `nozzle_volume_type` from `default_nozzle_volume_type[0]`
15
+ * (the GUI does this implicitly; the CLI doesn't).
16
+ * 4. Merges CLI-specific machine_limits from `BBL/cli_config.json` so the
17
+ * printer doesn't run unsafe accelerations / jerks.
18
+ * 5. Writes the flattened JSON to a temp file the caller passes to
19
+ * BambuStudio CLI.
20
+ *
21
+ * BBL only. Other vendors are out of scope and rejected explicitly so we
22
+ * fail loud rather than producing dangerous gcode for hardware we don't own.
23
+ */
24
+ import fs from "node:fs/promises";
25
+ import path from "node:path";
26
+ import crypto from "node:crypto";
27
+ /* -------------------------------------------------------------------------- */
28
+ /* Indexing */
29
+ /* -------------------------------------------------------------------------- */
30
+ /**
31
+ * Build a name -> {filePath, data} map for every JSON in
32
+ * <profilesRoot>/<vendor>/{machine,process,filament}/.
33
+ *
34
+ * The `name` field inside each JSON is the lookup key (this is what
35
+ * `inherits` references). We index every file, including non-instantiable
36
+ * abstract bases like `fdm_machine_common` and `fdm_bbl_3dp_001_common`,
37
+ * because those are the parents we'll walk to.
38
+ */
39
+ async function buildNameIndex(profilesRoot, vendor) {
40
+ const index = new Map();
41
+ const subdirs = ["machine", "process", "filament"];
42
+ for (const sub of subdirs) {
43
+ const dir = path.join(profilesRoot, vendor, sub);
44
+ let entries;
45
+ try {
46
+ entries = await fs.readdir(dir);
47
+ }
48
+ catch {
49
+ // Vendor or subdir missing -- skip silently; flatten will fail later
50
+ // with a precise "name not found" error.
51
+ continue;
52
+ }
53
+ for (const entry of entries) {
54
+ if (!entry.endsWith(".json"))
55
+ continue;
56
+ const filePath = path.join(dir, entry);
57
+ let raw;
58
+ try {
59
+ raw = await fs.readFile(filePath, "utf8");
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ let data;
65
+ try {
66
+ data = JSON.parse(raw);
67
+ }
68
+ catch {
69
+ // Malformed profile -- skip, don't poison the index.
70
+ continue;
71
+ }
72
+ const name = data["name"];
73
+ if (typeof name !== "string" || name.length === 0)
74
+ continue;
75
+ // First-write wins. BBL doesn't have name collisions in practice;
76
+ // log if it ever does so we notice.
77
+ if (!index.has(name)) {
78
+ index.set(name, { filePath, data });
79
+ }
80
+ }
81
+ }
82
+ return index;
83
+ }
84
+ /* -------------------------------------------------------------------------- */
85
+ /* Inheritance walk + merge */
86
+ /* -------------------------------------------------------------------------- */
87
+ /**
88
+ * Resolve the full inheritance chain for `leafName` and return a single
89
+ * deep-merged object. Child wins on key collision.
90
+ *
91
+ * Throws on:
92
+ * - Unknown name (broken `inherits` reference).
93
+ * - Cycles (A -> B -> A).
94
+ */
95
+ function flattenByName(leafName, index) {
96
+ const chain = [];
97
+ const visited = new Set();
98
+ let cursor = leafName;
99
+ while (cursor) {
100
+ if (visited.has(cursor)) {
101
+ throw new Error(`Profile inheritance cycle detected at "${cursor}" (chain: ${[...visited].join(" -> ")})`);
102
+ }
103
+ visited.add(cursor);
104
+ const entry = index.get(cursor);
105
+ if (!entry) {
106
+ throw new Error(`Profile "${cursor}" not found in index. ` +
107
+ `Inherits chain so far: ${[...visited].join(" -> ")}. ` +
108
+ `This usually means the leaf name is misspelled or the profile tree is incomplete.`);
109
+ }
110
+ chain.push(entry.data);
111
+ const parent = entry.data["inherits"];
112
+ cursor = typeof parent === "string" && parent.length > 0 ? parent : undefined;
113
+ }
114
+ // Merge root-most parent first, leaf last (so leaf wins).
115
+ const merged = {};
116
+ for (let i = chain.length - 1; i >= 0; i--) {
117
+ Object.assign(merged, chain[i]);
118
+ }
119
+ return merged;
120
+ }
121
+ /* -------------------------------------------------------------------------- */
122
+ /* CLI post-processing */
123
+ /* -------------------------------------------------------------------------- */
124
+ /**
125
+ * Bambu's CLI looks up `nozzle_volume_type`, but the profile tree only
126
+ * defines `default_nozzle_volume_type`. The GUI propagates the latter
127
+ * (or the value reported by the printer's nozzle scan on boot) into the
128
+ * former at runtime. We do the same.
129
+ *
130
+ * Shape: array of strings, one entry per physical nozzle. Hardware
131
+ * invariant from Bambu: BOTH nozzles must match (same diameter, same
132
+ * flow type) -- you cannot install 0.2 + 0.4, and you cannot install
133
+ * Standard + High Flow. So the array always contains identical entries.
134
+ *
135
+ * Verified against a GUI-sliced H2D project_settings.config:
136
+ * ["High Flow", "High Flow"] (dual-nozzle, HF installed)
137
+ * ["Standard", "Standard"] (dual-nozzle, stock)
138
+ * ["Standard"] (single-extruder default)
139
+ *
140
+ * Override priority:
141
+ * 1. Caller-supplied `nozzleVolumeType` (e.g. printer reported HF).
142
+ * 2. Existing `nozzle_volume_type` already in the flattened profile.
143
+ * 3. `default_nozzle_volume_type` from the profile tree.
144
+ * 4. ["Standard"] x extruder count.
145
+ */
146
+ function deriveNozzleVolumeType(flat, override) {
147
+ if (override) {
148
+ const count = inferExtruderCount(flat);
149
+ flat["nozzle_volume_type"] = Array(count).fill(override);
150
+ return;
151
+ }
152
+ if (Array.isArray(flat["nozzle_volume_type"])) {
153
+ enforceMatchingNozzles(flat["nozzle_volume_type"]);
154
+ return;
155
+ }
156
+ const def = flat["default_nozzle_volume_type"];
157
+ if (Array.isArray(def) && def.every((v) => typeof v === "string")) {
158
+ enforceMatchingNozzles(def);
159
+ flat["nozzle_volume_type"] = [...def];
160
+ return;
161
+ }
162
+ const extruderCount = inferExtruderCount(flat);
163
+ flat["nozzle_volume_type"] = Array(extruderCount).fill("Standard");
164
+ }
165
+ /**
166
+ * Hardware invariant: all nozzles on a Bambu printer have identical flow
167
+ * type (and diameter). If a profile somehow declares mixed types it's
168
+ * either bad input or a future mistake -- fail loud so we don't ship
169
+ * gcode that could damage the printer.
170
+ */
171
+ function enforceMatchingNozzles(arr) {
172
+ if (arr.length <= 1)
173
+ return;
174
+ const first = arr[0];
175
+ for (let i = 1; i < arr.length; i++) {
176
+ if (arr[i] !== first) {
177
+ throw new Error(`nozzle_volume_type entries must all match (Bambu hardware invariant). ` +
178
+ `Got: ${JSON.stringify(arr)}. ` +
179
+ `Both nozzles on H2-series printers always have identical flow type.`);
180
+ }
181
+ }
182
+ }
183
+ /** Best-effort extruder count for fallback nozzle_volume_type sizing. */
184
+ function inferExtruderCount(flat) {
185
+ for (const key of ["nozzle_diameter", "extruder_type", "extruder_variant_list"]) {
186
+ const v = flat[key];
187
+ if (Array.isArray(v) && v.length > 0)
188
+ return v.length;
189
+ }
190
+ return 1;
191
+ }
192
+ /**
193
+ * Bambu ships `BBL/cli_config.json` with per-printer overlays containing
194
+ * machine_limits keys (cli_safe_acceleration_*, cli_safe_jerk_*,
195
+ * cli_safe_speed_*). Without these the slicer can emit movements faster
196
+ * than the printer's safe envelope -- dangerous on real hardware.
197
+ *
198
+ * The overlay keys are scoped under printer.<printer_name>.machine_limits.
199
+ * We look up by the leaf machine's `printer_model` or `name` and merge
200
+ * those keys into the flattened machine profile.
201
+ *
202
+ * Returns true if an overlay was found and applied, false otherwise.
203
+ */
204
+ async function applyCliOverlay(flat, profilesRoot, vendor) {
205
+ const cliConfigPath = path.join(profilesRoot, vendor, "cli_config.json");
206
+ let raw;
207
+ try {
208
+ raw = await fs.readFile(cliConfigPath, "utf8");
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ let cliConfig;
214
+ try {
215
+ cliConfig = JSON.parse(raw);
216
+ }
217
+ catch {
218
+ return false;
219
+ }
220
+ const printerSection = cliConfig["printer"];
221
+ if (!printerSection || typeof printerSection !== "object")
222
+ return false;
223
+ // Match key: cli_config.json keys are bare printer names like
224
+ // "Bambu Lab H2D" / "Bambu Lab A1". Try, in order: explicit printer_model,
225
+ // printer_settings_id with the " 0.4 nozzle" suffix stripped, raw name
226
+ // with that suffix stripped, raw name as-is.
227
+ const candidates = [
228
+ flat["printer_model"],
229
+ typeof flat["printer_settings_id"] === "string"
230
+ ? stripNozzleSuffix(flat["printer_settings_id"])
231
+ : undefined,
232
+ typeof flat["name"] === "string" ? stripNozzleSuffix(flat["name"]) : undefined,
233
+ flat["name"],
234
+ ].filter((v) => typeof v === "string" && v.length > 0);
235
+ for (const key of candidates) {
236
+ const block = printerSection[key];
237
+ if (!block || typeof block !== "object")
238
+ continue;
239
+ const mlimits = block["machine_limits"];
240
+ if (!mlimits || typeof mlimits !== "object")
241
+ continue;
242
+ // Merge machine_limits into the flat profile. These are CLI-only safety
243
+ // values; they should never be overridden by the leaf.
244
+ Object.assign(flat, mlimits);
245
+ return true;
246
+ }
247
+ return false;
248
+ }
249
+ /**
250
+ * Ensure the chosen machine is in the process/filament's
251
+ * `compatible_printers` list. This mirrors what the GUI does when you
252
+ * save a project with a printer/process combo that wasn't in the
253
+ * shipped compat list.
254
+ */
255
+ function ensureMachineInCompatList(flat, machineLeaf) {
256
+ const list = flat["compatible_printers"];
257
+ if (!Array.isArray(list)) {
258
+ flat["compatible_printers"] = [machineLeaf];
259
+ return;
260
+ }
261
+ if (!list.includes(machineLeaf)) {
262
+ list.push(machineLeaf);
263
+ }
264
+ }
265
+ function stripNozzleSuffix(name) {
266
+ return name.replace(/\s+\d+\.\d+\s+nozzle.*$/, "");
267
+ }
268
+ /**
269
+ * Adjust fields for CLI consumption.
270
+ *
271
+ * - Strip `inherits` and `instantiation` (chain marker, abstract-base flag).
272
+ * The GUI consumes them; the CLI treats anything still present as part of
273
+ * the merged config.
274
+ * - Rewrite `from`. Leaf BBL profiles ship with `"from": "system"`, which
275
+ * the CLI rejects when loaded via `--load-settings` (system presets
276
+ * aren't supposed to be loaded that way). The GUI rewrites this to
277
+ * `"from": "project"` when it embeds the merged config in a 3MF. We do
278
+ * the same so the CLI accepts our flattened temp file.
279
+ * - Strip `setting_id` / `is_custom_defined` (user-registry markers that
280
+ * make no sense on a transient flattened file).
281
+ */
282
+ function normalizeForCli(flat, kind, leafName) {
283
+ // Set inherits to the leaf name BEFORE we delete it below, so the
284
+ // CLI's system_name resolution lands on the right value.
285
+ // (See note further down for why this matters.)
286
+ flat["inherits"] = leafName;
287
+ // Identity field. The CLI sets this internally, but we mirror it for
288
+ // consistency with what the GUI's project_settings.config emits.
289
+ switch (kind) {
290
+ case "machine":
291
+ flat["printer_settings_id"] = leafName;
292
+ break;
293
+ case "process":
294
+ flat["print_settings_id"] = leafName;
295
+ break;
296
+ case "filament":
297
+ flat["filament_settings_id"] = [leafName];
298
+ break;
299
+ }
300
+ // CRITICAL: when from=="User", the CLI computes `system_name = inherits`
301
+ // and the compatibility check compares the *other* configs'
302
+ // `compatible_printers` list against that system_name. The leaf
303
+ // profiles' compat lists contain the leaf machine name (e.g.
304
+ // "Bambu Lab H2S 0.4 nozzle"), so we must set `inherits` on the
305
+ // flattened machine to that same leaf name. For process/filament,
306
+ // `inherits` doesn't drive the compat check we hit, but we set it for
307
+ // symmetry so any future check succeeds the same way.
308
+ // Source: BambuStudio.cpp ~line 2222 (from!="system" -> system_name = inherits).
309
+ // NOTE: do NOT delete inherits -- we just set it above. The CLI uses
310
+ // its value (when from=="User") for the system_name compat check.
311
+ delete flat["instantiation"];
312
+ delete flat["setting_id"];
313
+ delete flat["is_custom_defined"];
314
+ // Always rewrite `from` to "User". "Project" is what the GUI uses when
315
+ // embedding in a 3MF, but the CLI accepts "User" for --load-settings
316
+ // paths and that's semantically what we are.
317
+ flat["from"] = "User";
318
+ // Mirror the GUI's behavior of clearing compatibility constraints in the
319
+ // merged project config. The leaf process/filament profiles ship with
320
+ // `compatible_printers: ["Bambu Lab H2S 0.4 nozzle"]` etc., and the CLI
321
+ // re-validates that list against our flattened machine config -- which
322
+ // has had `from` rewritten and inheritance collapsed, so the equality
323
+ // check fails. The GUI's project_settings.config has these set to null
324
+ // because at that point compatibility is already established by the
325
+ // user. We do the same.
326
+ // Leave compatible_printers / compatible_prints as the leaf declared
327
+ // them. The GUI nulls these in embedded project configs, but the CLI's
328
+ // --load-settings path requires a non-empty list and matches against
329
+ // the machine's `name` / `printer_settings_id`. The leaves ship with
330
+ // the correct list ([machine_leaf_name]) so just preserve.
331
+ // _condition fields can stay null (string-typed, tolerated as null).
332
+ }
333
+ function applyBedType(processFlat, bedType) {
334
+ if (!bedType)
335
+ return;
336
+ processFlat.curr_bed_type = bedType;
337
+ }
338
+ function applyMachineModelBedMetadata(machineFlat, index) {
339
+ const modelName = machineFlat.printer_model;
340
+ if (typeof modelName !== "string")
341
+ return;
342
+ const model = index.get(modelName)?.data;
343
+ if (!model)
344
+ return;
345
+ for (const key of ["default_bed_type", "image_bed_type", "not_support_bed_type"]) {
346
+ if (!(key in machineFlat) && key in model) {
347
+ machineFlat[key] = model[key];
348
+ }
349
+ }
350
+ }
351
+ /* -------------------------------------------------------------------------- */
352
+ /* Public entry point */
353
+ /* -------------------------------------------------------------------------- */
354
+ /**
355
+ * Flatten the leaf profiles, post-process for CLI, and write to temp files.
356
+ *
357
+ * Throws on unknown leaf names, missing profilesRoot, or cycles.
358
+ */
359
+ export async function flattenForCli(opts) {
360
+ const vendor = opts.vendor ?? "BBL";
361
+ if (vendor !== "BBL") {
362
+ throw new Error(`profile-flatten: only BBL vendor is supported (got "${vendor}"). ` +
363
+ `Other vendors are out of scope -- contributions welcome but untested.`);
364
+ }
365
+ // Confirm the profiles root looks plausible before doing real work.
366
+ const probe = path.join(opts.profilesRoot, vendor, "machine");
367
+ try {
368
+ await fs.access(probe);
369
+ }
370
+ catch {
371
+ throw new Error(`profile-flatten: profilesRoot "${opts.profilesRoot}" does not contain "${vendor}/machine". ` +
372
+ `Set BAMBU_PROFILES_ROOT or check your BambuStudio install.`);
373
+ }
374
+ const index = await buildNameIndex(opts.profilesRoot, vendor);
375
+ // Flatten each leaf.
376
+ const machineFlat = flattenByName(opts.machineLeaf, index);
377
+ const processFlat = flattenByName(opts.processLeaf, index);
378
+ const filamentFlats = opts.filamentLeaves.map((n) => flattenByName(n, index));
379
+ // CLI-specific post-processing on machine profile only.
380
+ deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
381
+ applyMachineModelBedMetadata(machineFlat, index);
382
+ const cliOverlayApplied = await applyCliOverlay(machineFlat, opts.profilesRoot, vendor);
383
+ // Normalize each flattened profile for CLI consumption.
384
+ normalizeForCli(machineFlat, "machine", opts.machineLeaf);
385
+ normalizeForCli(processFlat, "process", opts.processLeaf);
386
+ filamentFlats.forEach((f, i) => normalizeForCli(f, "filament", opts.filamentLeaves[i]));
387
+ applyBedType(processFlat, opts.bedType);
388
+ // Mirror the GUI's auto-extend behavior: when the caller explicitly
389
+ // chose a process or filament that wasn't pre-declared compatible with
390
+ // the chosen machine (e.g. "0.20mm Standard @BBL P1P" used on a P1S),
391
+ // add the machine name to the process/filament compat list. The GUI
392
+ // does this implicitly when saving a project.
393
+ ensureMachineInCompatList(processFlat, opts.machineLeaf);
394
+ filamentFlats.forEach((f) => ensureMachineInCompatList(f, opts.machineLeaf));
395
+ // Write temp files. Hash the leaf name into the filename so concurrent
396
+ // slices for different printers don't collide.
397
+ await fs.mkdir(opts.tempDir, { recursive: true });
398
+ const machinePath = await writeTemp(opts.tempDir, "machine", opts.machineLeaf, machineFlat);
399
+ const processPath = await writeTemp(opts.tempDir, "process", opts.processLeaf, processFlat);
400
+ const filamentPaths = [];
401
+ for (let i = 0; i < filamentFlats.length; i++) {
402
+ filamentPaths.push(await writeTemp(opts.tempDir, `filament-${i}`, opts.filamentLeaves[i], filamentFlats[i]));
403
+ }
404
+ return {
405
+ machinePath,
406
+ processPath,
407
+ filamentPaths,
408
+ meta: {
409
+ profilesRoot: opts.profilesRoot,
410
+ machineLeafName: opts.machineLeaf,
411
+ processLeafName: opts.processLeaf,
412
+ filamentLeafNames: opts.filamentLeaves,
413
+ cliOverlayApplied,
414
+ },
415
+ };
416
+ }
417
+ async function writeTemp(tempDir, kind, leafName, data) {
418
+ const hash = crypto.createHash("sha1").update(leafName).digest("hex").slice(0, 8);
419
+ const safe = leafName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
420
+ const filename = `flat-${kind}-${safe}-${hash}.json`;
421
+ const out = path.join(tempDir, filename);
422
+ await fs.writeFile(out, JSON.stringify(data, null, 2), "utf8");
423
+ return out;
424
+ }
425
+ /* -------------------------------------------------------------------------- */
426
+ /* Profile root detection */
427
+ /* -------------------------------------------------------------------------- */
428
+ /**
429
+ * Given the SLICER_PATH (path to BambuStudio executable), walk up to the
430
+ * Resources/profiles directory. Falls back to common platform paths.
431
+ *
432
+ * Override via BAMBU_PROFILES_ROOT env.
433
+ */
434
+ export function detectProfilesRoot(slicerPath) {
435
+ if (process.env["BAMBU_PROFILES_ROOT"]) {
436
+ return process.env["BAMBU_PROFILES_ROOT"];
437
+ }
438
+ if (slicerPath) {
439
+ // macOS: /Applications/BambuStudio.app/Contents/MacOS/BambuStudio
440
+ // -> /Applications/BambuStudio.app/Contents/Resources/profiles
441
+ const macGuess = path.resolve(path.dirname(slicerPath), "..", "Resources", "profiles");
442
+ return macGuess;
443
+ }
444
+ // Default macOS install.
445
+ return "/Applications/BambuStudio.app/Contents/Resources/profiles";
446
+ }
@@ -22,6 +22,10 @@ export type TransformationParams = {
22
22
  relative?: boolean;
23
23
  selectionBounds?: THREE.Box3;
24
24
  };
25
+ export declare const SLICER_TYPES: readonly ["bambustudio", "orcaslicer", "orcaslicer-bambulab", "prusaslicer", "cura", "slic3r"];
26
+ export type SlicerType = typeof SLICER_TYPES[number];
27
+ export declare function normalizeSlicerType(rawSlicerType: string): SlicerType;
28
+ export declare function isBambuCompatibleSlicer(slicerType: SlicerType): boolean;
25
29
  export interface BambuSliceOptions {
26
30
  uptodate?: boolean;
27
31
  repetitions?: number;
@@ -32,6 +36,7 @@ export interface BambuSliceOptions {
32
36
  skipObjects?: string;
33
37
  loadFilaments?: string;
34
38
  loadFilamentIds?: string;
39
+ bedType?: string;
35
40
  enableTimelapse?: boolean;
36
41
  allowMixTemp?: boolean;
37
42
  scale?: number;
@@ -50,6 +55,33 @@ export declare class STLManipulator extends EventEmitter {
50
55
  * Generate a unique operation ID
51
56
  */
52
57
  private generateOperationId;
58
+ private getAvailableProfileRoots;
59
+ private findProfileFile;
60
+ private buildFilamentIdIndex;
61
+ private readJsonFile;
62
+ private stripAbsoluteExtruderResets;
63
+ private sanitizeProcessForOrca;
64
+ private writeTempJson;
65
+ private resolveBambuLikeSettingsBundle;
66
+ /**
67
+ * Optionally rewrite a Bambu-like settings bundle so the paths point at
68
+ * fully-flattened temp configs instead of the BBL-shipped leaf JSONs.
69
+ *
70
+ * BambuStudio's CLI does not resolve the `inherits` chain when loading
71
+ * profiles via --load-settings / --load-filaments, which causes a
72
+ * cluster of upstream bugs (see https://github.com/bambulab/BambuStudio/issues/9636
73
+ * and #9968). Our flattener (src/slicer/profile-flatten.ts) reproduces
74
+ * what the GUI does at slice time so the CLI accepts the configs.
75
+ *
76
+ * Opt-in via `BAMBU_CLI_FLATTEN=true`. When the env var is unset or
77
+ * not "true"/"1", returns the bundle unchanged so behavior is
78
+ * backward-compatible. When enabled, only BBL-shipped leaves get
79
+ * flattened; user-provided custom configs pass through untouched.
80
+ */
81
+ private maybeFlattenBundle;
82
+ private resolveBambuStudioBedType;
83
+ /** Read a profile JSON's top-level `name` field, or null if unreadable. */
84
+ private readLeafName;
53
85
  /**
54
86
  * Load STL file and return geometry and bounding box
55
87
  */
@@ -130,7 +162,7 @@ export declare class STLManipulator extends EventEmitter {
130
162
  * @param bambuOptions Optional BambuStudio-specific CLI flags
131
163
  * @returns Path to the generated G-code or sliced 3MF file
132
164
  */
133
- sliceSTL(stlFilePath: string, slicerType: 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio', slicerPath: string, slicerProfile?: string, progressCallback?: ProgressCallback, printerPreset?: string, bambuOptions?: BambuSliceOptions): Promise<string>;
165
+ sliceSTL(stlFilePath: string, slicerType: SlicerType, slicerPath: string, slicerProfile?: string, progressCallback?: ProgressCallback, printerPreset?: string, bambuOptions?: BambuSliceOptions): Promise<string>;
134
166
  /**
135
167
  * Enhanced version of confirmTemperatures with better error handling
136
168
  * @param gcodePath Path to the G-code file