mcp-3d-printer-server 1.2.5 → 1.2.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/CHANGELOG.md +228 -0
- package/CONTRIBUTORS.md +49 -0
- package/README.md +529 -739
- package/dist/3mf_parser.js +8 -8
- package/dist/blender-mcp-bridge.d.ts +11 -0
- package/dist/blender-mcp-bridge.js +661 -0
- package/dist/fulu-orca.js +29 -5
- package/dist/index.js +752 -212
- package/dist/printers/bambu-node-guard.d.ts +2 -0
- package/dist/printers/bambu-node-guard.js +38 -0
- package/dist/printers/bambu.d.ts +53 -7
- package/dist/printers/bambu.js +420 -120
- package/dist/printers/creality.d.ts +15 -6
- package/dist/printers/creality.js +42 -9
- package/dist/printers/duet.d.ts +11 -6
- package/dist/printers/duet.js +57 -34
- package/dist/printers/generic-base.d.ts +69 -0
- package/dist/printers/generic-base.js +209 -0
- package/dist/printers/klipper.d.ts +17 -6
- package/dist/printers/klipper.js +63 -16
- package/dist/printers/octoprint.d.ts +18 -6
- package/dist/printers/octoprint.js +64 -9
- package/dist/printers/printer-factory.d.ts +11 -2
- package/dist/printers/printer-factory.js +9 -8
- package/dist/printers/prusa.d.ts +14 -6
- package/dist/printers/prusa.js +56 -9
- package/dist/printers/repetier.d.ts +18 -6
- package/dist/printers/repetier.js +48 -6
- package/dist/safety/archive.d.ts +11 -0
- package/dist/safety/archive.js +214 -0
- package/dist/safety/artifact.d.ts +6 -0
- package/dist/safety/artifact.js +66 -0
- package/dist/safety/confirmation.d.ts +22 -0
- package/dist/safety/confirmation.js +43 -0
- package/dist/safety/expected-peaks.d.ts +24 -0
- package/dist/safety/expected-peaks.js +24 -0
- package/dist/safety/gcode-thermal.d.ts +64 -0
- package/dist/safety/gcode-thermal.js +382 -0
- package/dist/safety/limits.d.ts +31 -0
- package/dist/safety/limits.js +105 -0
- package/dist/safety/print-file.d.ts +25 -0
- package/dist/safety/print-file.js +517 -0
- package/dist/safety/printer-state.d.ts +51 -0
- package/dist/safety/printer-state.js +365 -0
- package/dist/slicer/bambu-cli.d.ts +110 -0
- package/dist/slicer/bambu-cli.js +547 -0
- package/dist/slicer/profile-flatten.d.ts +104 -0
- package/dist/slicer/profile-flatten.js +676 -0
- package/dist/slicer/run-slicer.d.ts +18 -0
- package/dist/slicer/run-slicer.js +102 -0
- package/dist/slicer/slicer-error.d.ts +27 -0
- package/dist/slicer/slicer-error.js +53 -0
- package/dist/slicer/templates.d.ts +126 -0
- package/dist/slicer/templates.js +255 -0
- package/dist/stl/stl-manipulator.d.ts +25 -3
- package/dist/stl/stl-manipulator.js +214 -232
- package/package.json +30 -14
- package/server.json +29 -0
- package/src/3mf_parser.ts +8 -8
- package/src/blender-mcp-bridge.ts +611 -0
- package/src/fulu-orca.ts +36 -6
- package/src/index.ts +888 -255
- package/src/printers/bambu-node-guard.ts +44 -0
- package/src/printers/bambu.ts +458 -158
- package/src/printers/creality.ts +40 -9
- package/src/printers/duet.ts +64 -53
- package/src/printers/generic-base.ts +286 -0
- package/src/printers/klipper.ts +83 -28
- package/src/printers/octoprint.ts +74 -21
- package/src/printers/printer-factory.ts +21 -11
- package/src/printers/prusa.ts +53 -10
- package/src/printers/repetier.ts +55 -17
- package/src/safety/archive.ts +181 -0
- package/src/safety/artifact.ts +58 -0
- package/src/safety/confirmation.ts +56 -0
- package/src/safety/expected-peaks.ts +47 -0
- package/src/safety/gcode-thermal.ts +404 -0
- package/src/safety/limits.ts +98 -0
- package/src/safety/print-file.ts +354 -0
- package/src/safety/printer-state.ts +339 -0
- package/src/slicer/bambu-cli.ts +633 -0
- package/src/slicer/profile-flatten.ts +830 -0
- package/src/slicer/run-slicer.ts +126 -0
- package/src/slicer/slicer-error.ts +72 -0
- package/src/slicer/templates.ts +288 -0
- package/src/stl/stl-manipulator.ts +247 -275
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bambu-compatible CLI (BambuStudio, OrcaSlicer, FULU OrcaSlicer-bambulab)
|
|
3
|
+
* profile preparation. Ported from bambu-printer-mcp's STLManipulator so both
|
|
4
|
+
* servers share the same safety semantics:
|
|
5
|
+
*
|
|
6
|
+
* - The exact model/nozzle machine preset must exist in the selected
|
|
7
|
+
* installation's BBL tree; it is never borrowed from another tree.
|
|
8
|
+
* - `inherits` and `include` are resolved recursively (profile-flatten.ts);
|
|
9
|
+
* missing references, cycles, and malformed profiles stop preparation.
|
|
10
|
+
* - Custom process/filament overrides keep their own values on top of their
|
|
11
|
+
* resolved parents; filament slots keep their positional order.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import crypto from "node:crypto";
|
|
17
|
+
import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from "./profile-flatten.js";
|
|
18
|
+
export const BAMBU_CLI_BED_TYPES = {
|
|
19
|
+
textured_plate: "Textured PEI Plate",
|
|
20
|
+
cool_plate: "Cool Plate",
|
|
21
|
+
engineering_plate: "Engineering Plate",
|
|
22
|
+
hot_plate: "High Temp Plate",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* User/system profile directories searched for custom process and filament
|
|
26
|
+
* dependencies (never for machine presets). BAMBU_SLICER_PROFILE_DIRS
|
|
27
|
+
* (path-delimiter separated) replaces the defaults; an empty value disables them.
|
|
28
|
+
*/
|
|
29
|
+
export function configuredBambuProfileDirs() {
|
|
30
|
+
const override = process.env.BAMBU_SLICER_PROFILE_DIRS;
|
|
31
|
+
if (override !== undefined) {
|
|
32
|
+
return override.split(path.delimiter).filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
const home = os.homedir();
|
|
35
|
+
const roots = [
|
|
36
|
+
path.join(home, "Library", "Application Support"),
|
|
37
|
+
path.join(home, ".config"),
|
|
38
|
+
...(process.env.APPDATA ? [process.env.APPDATA] : []),
|
|
39
|
+
...(process.env.LOCALAPPDATA ? [process.env.LOCALAPPDATA] : []),
|
|
40
|
+
];
|
|
41
|
+
const appNames = ["BambuStudio", "Bambu Studio", "OrcaSlicer", "Orca Slicer", "OrcaStudio", "Orca Studio"];
|
|
42
|
+
return roots.flatMap((root) => appNames.map((appName) => path.join(root, appName, "system", "BBL")));
|
|
43
|
+
}
|
|
44
|
+
export function resolveBambuCliBedType(bedType) {
|
|
45
|
+
if (!bedType)
|
|
46
|
+
return undefined;
|
|
47
|
+
const normalized = bedType.trim().toLowerCase();
|
|
48
|
+
if (normalized === "supertack_plate") {
|
|
49
|
+
throw new Error("The Bambu CLI SuperTack bed type is not verified; use a pre-sliced 3MF or choose textured_plate, cool_plate, engineering_plate, or hot_plate.");
|
|
50
|
+
}
|
|
51
|
+
return BAMBU_CLI_BED_TYPES[normalized] || bedType;
|
|
52
|
+
}
|
|
53
|
+
export class BambuCliProfilePreparer {
|
|
54
|
+
constructor(tempDir) {
|
|
55
|
+
this.tempDir = tempDir;
|
|
56
|
+
}
|
|
57
|
+
readJsonFile(filePath) {
|
|
58
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
59
|
+
}
|
|
60
|
+
writeTempJson(outputBase, suffix, value) {
|
|
61
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
62
|
+
const hash = crypto.createHash("sha256").update(serialized).digest("hex").slice(0, 16);
|
|
63
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
64
|
+
const safeBase = outputBase.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
65
|
+
const outPath = path.join(this.tempDir, `${safeBase}_${suffix}_${hash}.json`);
|
|
66
|
+
// Concurrent slices can prepare the same content-addressed file; write a
|
|
67
|
+
// private temp file and rename it so no reader ever sees a partial file.
|
|
68
|
+
const staging = `${outPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
69
|
+
fs.writeFileSync(staging, serialized);
|
|
70
|
+
fs.renameSync(staging, outPath);
|
|
71
|
+
return outPath;
|
|
72
|
+
}
|
|
73
|
+
findProfileFile(category, profileName, roots) {
|
|
74
|
+
if (!profileName)
|
|
75
|
+
return undefined;
|
|
76
|
+
// Names are looked up as files; never let a name escape the profile tree.
|
|
77
|
+
if (profileName.includes("/") || profileName.includes("\\") || profileName.includes("\0"))
|
|
78
|
+
return undefined;
|
|
79
|
+
for (const root of roots) {
|
|
80
|
+
const candidate = path.join(root, category, `${profileName}.json`);
|
|
81
|
+
if (fs.existsSync(candidate))
|
|
82
|
+
return candidate;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
buildFilamentIdIndex(roots) {
|
|
87
|
+
const index = new Map();
|
|
88
|
+
for (const root of roots) {
|
|
89
|
+
const filamentDir = path.join(root, "filament");
|
|
90
|
+
if (!fs.existsSync(filamentDir))
|
|
91
|
+
continue;
|
|
92
|
+
for (const entry of fs.readdirSync(filamentDir)) {
|
|
93
|
+
if (!entry.endsWith(".json"))
|
|
94
|
+
continue;
|
|
95
|
+
const filePath = path.join(filamentDir, entry);
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
98
|
+
const filamentId = typeof parsed?.filament_id === "string" ? parsed.filament_id.trim() : "";
|
|
99
|
+
if (filamentId && !index.has(filamentId))
|
|
100
|
+
index.set(filamentId, filePath);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// A malformed unrelated profile cannot satisfy an ID lookup; a
|
|
104
|
+
// requested ID that stays unresolved is rejected by the caller.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return index;
|
|
109
|
+
}
|
|
110
|
+
stripAbsoluteExtruderResets(value) {
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
return value.split(/\r?\n/).filter((line) => line.trim().toUpperCase() !== "G92 E0").join("\n");
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
return value.filter((line) => String(line).trim().toUpperCase() !== "G92 E0");
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
/** Orca's existing absolute-extrusion normalization for process G-code. */
|
|
120
|
+
sanitizeProcessForOrca(processConfig, printerPreset) {
|
|
121
|
+
const sanitized = { ...processConfig };
|
|
122
|
+
sanitized.type = "process";
|
|
123
|
+
if (!sanitized.from || sanitized.from === "project")
|
|
124
|
+
sanitized.from = "User";
|
|
125
|
+
if (printerPreset)
|
|
126
|
+
sanitized.compatible_printers = [printerPreset];
|
|
127
|
+
if (sanitized.prime_tower_brim_width !== undefined && Number(sanitized.prime_tower_brim_width) < 0) {
|
|
128
|
+
sanitized.prime_tower_brim_width = "0";
|
|
129
|
+
}
|
|
130
|
+
else if (sanitized.prime_tower_brim_width !== undefined) {
|
|
131
|
+
sanitized.prime_tower_brim_width = String(sanitized.prime_tower_brim_width);
|
|
132
|
+
}
|
|
133
|
+
sanitized.use_relative_e_distances = "0";
|
|
134
|
+
sanitized.before_layer_change_gcode = this.stripAbsoluteExtruderResets(sanitized.before_layer_change_gcode);
|
|
135
|
+
sanitized.layer_gcode = this.stripAbsoluteExtruderResets(sanitized.layer_gcode);
|
|
136
|
+
sanitized.layer_change_gcode = this.stripAbsoluteExtruderResets(sanitized.layer_change_gcode);
|
|
137
|
+
return sanitized;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The same normalization for the machine file. Machine isolation drops these
|
|
141
|
+
* keys from the process when the machine defines them, so the machine copy
|
|
142
|
+
* must carry the absolute-extrusion values or Orca would pair absolute E with
|
|
143
|
+
* the machine's G92 E0 resets.
|
|
144
|
+
*/
|
|
145
|
+
sanitizeMachineForOrca(machineConfig) {
|
|
146
|
+
const sanitized = { ...machineConfig };
|
|
147
|
+
if (sanitized.use_relative_e_distances !== undefined)
|
|
148
|
+
sanitized.use_relative_e_distances = "0";
|
|
149
|
+
for (const key of ["before_layer_change_gcode", "layer_gcode", "layer_change_gcode"]) {
|
|
150
|
+
if (sanitized[key] !== undefined)
|
|
151
|
+
sanitized[key] = this.stripAbsoluteExtruderResets(sanitized[key]);
|
|
152
|
+
}
|
|
153
|
+
return sanitized;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Select the machine preset, process, and positional filament profiles
|
|
157
|
+
* from the active installation. Throws before any slicer runs when the
|
|
158
|
+
* exact machine preset or a named dependency is missing.
|
|
159
|
+
*/
|
|
160
|
+
async resolveSettingsBundle(outputBase, slicerType, activeProfilesRoot, printerPreset, slicerProfile, options = {}) {
|
|
161
|
+
const roots = [path.join(activeProfilesRoot, "BBL")];
|
|
162
|
+
const userRoots = configuredBambuProfileDirs();
|
|
163
|
+
const findProfile = (kind, name) => this.findProfileFile(kind, name, kind === "machine" ? roots : [...roots, ...userRoots]);
|
|
164
|
+
if (!printerPreset) {
|
|
165
|
+
throw new Error("Printer preset is required for Bambu-compatible CLI slicing; select the exact bambu_model and nozzle_diameter.");
|
|
166
|
+
}
|
|
167
|
+
const machinePath = findProfile("machine", printerPreset);
|
|
168
|
+
if (!machinePath) {
|
|
169
|
+
throw new Error(`Printer profile "${printerPreset}" was not found in ${path.join(roots[0], "machine")}. ` +
|
|
170
|
+
`Check bambu_model/nozzle_diameter against the installed ${slicerType} version, or set BAMBU_PROFILES_ROOT ` +
|
|
171
|
+
`to the matching profile tree; refusing to use printer defaults or another installation's preset.`);
|
|
172
|
+
}
|
|
173
|
+
let machineConfig;
|
|
174
|
+
try {
|
|
175
|
+
machineConfig = this.readJsonFile(machinePath);
|
|
176
|
+
if (!machineConfig || Array.isArray(machineConfig) || machineConfig.name !== printerPreset) {
|
|
177
|
+
throw new Error("expected a JSON machine profile with the selected preset name");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
throw new Error(`Printer profile "${printerPreset}" is invalid: ${error?.message ?? error}`);
|
|
182
|
+
}
|
|
183
|
+
// Some FULU/Orca machine leaves inherit their default process/filaments.
|
|
184
|
+
// Resolve those selections before building the positional CLI bundle.
|
|
185
|
+
machineConfig = await resolveBblMachineProfile(activeProfilesRoot, printerPreset);
|
|
186
|
+
const filamentPaths = [];
|
|
187
|
+
let processPath;
|
|
188
|
+
let processSource;
|
|
189
|
+
if (slicerProfile) {
|
|
190
|
+
if (!fs.existsSync(slicerProfile)) {
|
|
191
|
+
throw new Error(`Slicer process profile not found: ${slicerProfile} (from slicer_profile, a template, or SLICER_PROFILE). ` +
|
|
192
|
+
"Fix the path, or leave slicer_profile and SLICER_PROFILE empty to use the machine preset's default process.");
|
|
193
|
+
}
|
|
194
|
+
let parsedProfile = null;
|
|
195
|
+
try {
|
|
196
|
+
parsedProfile = this.readJsonFile(slicerProfile);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
parsedProfile = null;
|
|
200
|
+
}
|
|
201
|
+
if (parsedProfile && typeof parsedProfile === "object" && !Array.isArray(parsedProfile)) {
|
|
202
|
+
processSource = { filePath: slicerProfile, profile: parsedProfile };
|
|
203
|
+
const inheritedProcessName = (typeof parsedProfile.inherits === "string" && parsedProfile.inherits) ||
|
|
204
|
+
(typeof parsedProfile.print_settings_id === "string" && parsedProfile.print_settings_id !== parsedProfile.name
|
|
205
|
+
? parsedProfile.print_settings_id
|
|
206
|
+
: undefined) ||
|
|
207
|
+
(typeof parsedProfile.default_print_profile === "string" ? parsedProfile.default_print_profile : undefined) ||
|
|
208
|
+
(typeof machineConfig?.default_print_profile === "string" ? machineConfig.default_print_profile : undefined);
|
|
209
|
+
const inheritedProcessPath = findProfile("process", inheritedProcessName);
|
|
210
|
+
const inheritedProcess = inheritedProcessPath ? this.readJsonFile(inheritedProcessPath) : {};
|
|
211
|
+
const mergedProcess = { ...inheritedProcess, ...parsedProfile, type: "process" };
|
|
212
|
+
processPath = this.writeTempJson(outputBase, slicerType === "orcaslicer" ? "process_orca" : "process", slicerType === "orcaslicer" ? this.sanitizeProcessForOrca(mergedProcess, printerPreset) : mergedProcess);
|
|
213
|
+
const defaultFilamentProfiles = !options.loadFilaments && Array.isArray(parsedProfile.default_filament_profile)
|
|
214
|
+
? parsedProfile.default_filament_profile
|
|
215
|
+
: [];
|
|
216
|
+
for (const profileName of defaultFilamentProfiles) {
|
|
217
|
+
const filamentPath = findProfile("filament", String(profileName));
|
|
218
|
+
if (!filamentPath) {
|
|
219
|
+
throw new Error(`Filament profile "${String(profileName)}" was not found; refusing to omit its slot.`);
|
|
220
|
+
}
|
|
221
|
+
filamentPaths.push(filamentPath);
|
|
222
|
+
}
|
|
223
|
+
if (!options.loadFilaments && filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
|
|
224
|
+
const filamentIdIndex = this.buildFilamentIdIndex([...roots, ...userRoots]);
|
|
225
|
+
for (const filamentId of parsedProfile.filament_ids) {
|
|
226
|
+
const filamentPath = filamentIdIndex.get(String(filamentId));
|
|
227
|
+
if (!filamentPath) {
|
|
228
|
+
throw new Error(`Filament ID "${String(filamentId)}" could not be resolved; refusing to omit its slot.`);
|
|
229
|
+
}
|
|
230
|
+
filamentPaths.push(filamentPath);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Non-JSON configs are read again during resolution and rejected there.
|
|
236
|
+
processPath = slicerProfile;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (!processPath) {
|
|
240
|
+
const defaultProcessName = typeof machineConfig?.default_print_profile === "string" ? machineConfig.default_print_profile : undefined;
|
|
241
|
+
const defaultProcessPath = findProfile("process", defaultProcessName);
|
|
242
|
+
if (!defaultProcessPath) {
|
|
243
|
+
throw new Error(`Default process profile "${defaultProcessName ?? "(none declared)"}" for "${printerPreset}" was not found; ` +
|
|
244
|
+
"pass slicer_profile with a process profile JSON.");
|
|
245
|
+
}
|
|
246
|
+
processPath =
|
|
247
|
+
slicerType === "orcaslicer"
|
|
248
|
+
? this.writeTempJson(outputBase, "process_default_orca", this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset))
|
|
249
|
+
: defaultProcessPath;
|
|
250
|
+
}
|
|
251
|
+
if (!options.loadFilaments && filamentPaths.length === 0 && Array.isArray(machineConfig?.default_filament_profile)) {
|
|
252
|
+
for (const profileName of machineConfig.default_filament_profile) {
|
|
253
|
+
const filamentPath = findProfile("filament", String(profileName));
|
|
254
|
+
if (!filamentPath) {
|
|
255
|
+
throw new Error(`Filament profile "${String(profileName)}" was not found; refusing to omit its slot.`);
|
|
256
|
+
}
|
|
257
|
+
filamentPaths.push(filamentPath);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (options.loadFilaments) {
|
|
261
|
+
filamentPaths.length = 0;
|
|
262
|
+
for (const filamentPath of options.loadFilaments.split(";")) {
|
|
263
|
+
const trimmed = filamentPath.trim();
|
|
264
|
+
if (!trimmed)
|
|
265
|
+
continue;
|
|
266
|
+
if (!fs.existsSync(trimmed)) {
|
|
267
|
+
throw new Error(`Filament profile not found: ${trimmed}; refusing to omit its slot.`);
|
|
268
|
+
}
|
|
269
|
+
filamentPaths.push(trimmed);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
settingsArg: [machinePath, processPath].join(";"),
|
|
274
|
+
filamentPaths,
|
|
275
|
+
processSource,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** --load-filaments is positional; a single override must cover every project slot. */
|
|
279
|
+
async expandProjectFilaments(inputPath, bundle) {
|
|
280
|
+
if (!inputPath.toLowerCase().endsWith(".3mf") || bundle.filamentPaths.length === 0)
|
|
281
|
+
return bundle;
|
|
282
|
+
const JSZip = (await import("jszip")).default;
|
|
283
|
+
const zip = await JSZip.loadAsync(await fs.promises.readFile(inputPath));
|
|
284
|
+
const file = zip.file("Metadata/project_settings.config");
|
|
285
|
+
// Geometry-only 3MFs have no embedded filament slots.
|
|
286
|
+
if (!file)
|
|
287
|
+
return bundle;
|
|
288
|
+
let config;
|
|
289
|
+
try {
|
|
290
|
+
config = JSON.parse(await file.async("string"));
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
throw new Error("Cannot determine project filament slots: project_settings.config is not valid JSON.");
|
|
294
|
+
}
|
|
295
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
296
|
+
throw new Error("Cannot determine project filament slots: invalid project_settings.config.");
|
|
297
|
+
}
|
|
298
|
+
const data = config;
|
|
299
|
+
const count = Math.max(0, ...["filament_settings_id", "filament_type", "filament_colour", "filament_diameter"]
|
|
300
|
+
.map((key) => (Array.isArray(data[key]) ? data[key].length : 0)));
|
|
301
|
+
if (count === 0)
|
|
302
|
+
throw new Error("Cannot determine project filament slots from project_settings.config.");
|
|
303
|
+
// Keep the project's slot colours; loaded profiles carry none of their own.
|
|
304
|
+
const colours = Array.isArray(data.filament_colour) && data.filament_colour.length === count &&
|
|
305
|
+
data.filament_colour.every((c) => typeof c === "string" && /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(c))
|
|
306
|
+
? data.filament_colour
|
|
307
|
+
: undefined;
|
|
308
|
+
const projectTowerPosition = Object.fromEntries(["wipe_tower_x", "wipe_tower_y"]
|
|
309
|
+
.filter((key) => data[key] !== undefined).map((key) => [key, data[key]]));
|
|
310
|
+
if (bundle.filamentPaths.length === 1) {
|
|
311
|
+
return { ...bundle, filamentPaths: Array(count).fill(bundle.filamentPaths[0]), filamentColours: colours, projectTowerPosition };
|
|
312
|
+
}
|
|
313
|
+
if (bundle.filamentPaths.length !== count) {
|
|
314
|
+
throw new Error(`Project declares ${count} filament slots, but ${bundle.filamentPaths.length} profiles were supplied. ` +
|
|
315
|
+
"Provide one profile for all slots or one per slot.");
|
|
316
|
+
}
|
|
317
|
+
return { ...bundle, filamentColours: colours, projectTowerPosition };
|
|
318
|
+
}
|
|
319
|
+
/** Resolve BBL dependencies before invoking the CLI; failures stop the slice. */
|
|
320
|
+
async flattenBundle(bundle, options, activeProfilesRoot) {
|
|
321
|
+
const parts = bundle.settingsArg?.split(";").filter(Boolean) ?? [];
|
|
322
|
+
const readProfile = (filePath) => {
|
|
323
|
+
try {
|
|
324
|
+
const data = this.readJsonFile(filePath);
|
|
325
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
326
|
+
throw new Error("expected a JSON profile object");
|
|
327
|
+
}
|
|
328
|
+
return data;
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
throw new Error(`Cannot read slicer profile "${filePath}": ${err?.message ?? err}`);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const settings = parts.map(readProfile);
|
|
335
|
+
// Read every slot: filtering unreadable names would change positional mapping.
|
|
336
|
+
const filaments = bundle.filamentPaths.map(readProfile);
|
|
337
|
+
const profilesRoot = activeProfilesRoot ?? detectProfilesRoot(process.env.SLICER_PATH);
|
|
338
|
+
const profileRoots = [path.join(profilesRoot, "BBL")];
|
|
339
|
+
const hasReferences = (profile) => (typeof profile.inherits === "string" && profile.inherits.length > 0) ||
|
|
340
|
+
(profile.include !== undefined && profile.include !== null);
|
|
341
|
+
const isBundledFile = (filePath) => profileRoots.some((root) => {
|
|
342
|
+
const relative = path.relative(root, filePath);
|
|
343
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
344
|
+
});
|
|
345
|
+
const isBundled = (filePath, profile) => String(profile.from).toLowerCase() === "system" || isBundledFile(filePath);
|
|
346
|
+
const needsResolution = (filePath, profile) => hasReferences(profile) || isBundled(filePath, profile);
|
|
347
|
+
if (![...settings, ...filaments].some((profile, i) => needsResolution([...parts, ...bundle.filamentPaths][i], profile)))
|
|
348
|
+
return bundle;
|
|
349
|
+
if (parts.length !== 2) {
|
|
350
|
+
throw new Error("Resolving BBL profiles requires both a machine and process profile; refusing to slice with unresolved settings.");
|
|
351
|
+
}
|
|
352
|
+
const [machinePath, processPath] = parts;
|
|
353
|
+
const [machine, processProfile] = settings;
|
|
354
|
+
const leafName = (filePath, profile) => {
|
|
355
|
+
// A user preset inherits its system identity, while keeping its own values.
|
|
356
|
+
const name = !isBundled(filePath, profile) && typeof profile.inherits === "string" && profile.inherits
|
|
357
|
+
? profile.inherits
|
|
358
|
+
: profile.name;
|
|
359
|
+
if (typeof name !== "string" || !name) {
|
|
360
|
+
if (needsResolution(filePath, profile))
|
|
361
|
+
throw new Error(`Cannot determine the profile name for "${filePath}".`);
|
|
362
|
+
return path.basename(filePath, ".json");
|
|
363
|
+
}
|
|
364
|
+
return name;
|
|
365
|
+
};
|
|
366
|
+
const flat = await flattenForCli({
|
|
367
|
+
machineLeaf: leafName(machinePath, machine),
|
|
368
|
+
// A generated path loses bundled provenance; inherited metadata can also
|
|
369
|
+
// make a custom preset look like a system profile. Use the original source
|
|
370
|
+
// only for identity and retain the prepared values in sourceProfiles below.
|
|
371
|
+
processLeaf: bundle.processSource
|
|
372
|
+
? leafName(bundle.processSource.filePath, bundle.processSource.profile)
|
|
373
|
+
: leafName(processPath, processProfile),
|
|
374
|
+
filamentLeaves: filaments.map((profile, i) => leafName(bundle.filamentPaths[i], profile)),
|
|
375
|
+
profilesRoot,
|
|
376
|
+
userProfileRoots: configuredBambuProfileDirs(),
|
|
377
|
+
tempDir: this.tempDir,
|
|
378
|
+
bedType: resolveBambuCliBedType(options.bedType),
|
|
379
|
+
filamentColours: options.filamentColours ?? bundle.filamentColours,
|
|
380
|
+
projectTowerPosition: bundle.projectTowerPosition,
|
|
381
|
+
sourceProfiles: {
|
|
382
|
+
machine: isBundledFile(machinePath) ? undefined : machine,
|
|
383
|
+
process: isBundledFile(processPath) ? undefined : processProfile,
|
|
384
|
+
filaments: filaments.map((profile, i) => (isBundledFile(bundle.filamentPaths[i]) ? undefined : profile)),
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
// Standalone files retain their own identity and settings, but still need
|
|
388
|
+
// the CLI colour/tower fields. Do not discard those required overlays when
|
|
389
|
+
// choosing the original profile instead of the normalized BBL output.
|
|
390
|
+
const withCliFields = (filePath, profile, flatPath, keys) => {
|
|
391
|
+
const flattened = readProfile(flatPath);
|
|
392
|
+
const fields = Object.fromEntries(keys.filter((key) => flattened[key] !== undefined).map((key) => [key, flattened[key]]));
|
|
393
|
+
if (Object.entries(fields).every(([key, value]) => JSON.stringify(profile[key]) === JSON.stringify(value)))
|
|
394
|
+
return filePath;
|
|
395
|
+
return this.writeTempJson(path.basename(filePath, ".json"), "cli", { ...profile, ...fields });
|
|
396
|
+
};
|
|
397
|
+
return {
|
|
398
|
+
settingsArg: [
|
|
399
|
+
needsResolution(machinePath, machine) ? flat.machinePath : machinePath,
|
|
400
|
+
needsResolution(processPath, processProfile)
|
|
401
|
+
? flat.processPath
|
|
402
|
+
: withCliFields(processPath, processProfile, flat.processPath, ["wipe_tower_x", "wipe_tower_y"]),
|
|
403
|
+
].join(";"),
|
|
404
|
+
filamentPaths: filaments.map((profile, i) => needsResolution(bundle.filamentPaths[i], profile)
|
|
405
|
+
? flat.filamentPaths[i]
|
|
406
|
+
: withCliFields(bundle.filamentPaths[i], profile, flat.filamentPaths[i], ["filament_colour"])),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Full preparation: select profiles, expand project slots, resolve every
|
|
411
|
+
* BBL dependency, and apply Orca's post-resolution normalization.
|
|
412
|
+
*/
|
|
413
|
+
async prepare(inputPath, outputBase, slicerType, activeProfilesRoot, printerPreset, slicerProfile, options) {
|
|
414
|
+
const rawBundle = await this.resolveSettingsBundle(outputBase, slicerType, activeProfilesRoot, printerPreset, slicerProfile, options);
|
|
415
|
+
const bundle = await this.flattenBundle(await this.expandProjectFilaments(inputPath, rawBundle), options, activeProfilesRoot);
|
|
416
|
+
// The machine preset assumes the stock hotend; record the installed one so
|
|
417
|
+
// the sliced job's nozzle_type matches what the printer reports.
|
|
418
|
+
if (options.nozzleType && bundle.settingsArg) {
|
|
419
|
+
const [machinePath, ...rest] = bundle.settingsArg.split(";");
|
|
420
|
+
const machine = this.readJsonFile(machinePath);
|
|
421
|
+
const current = machine.nozzle_type;
|
|
422
|
+
machine.nozzle_type = Array.isArray(current) && current.length > 0 ? current.map(() => options.nozzleType) : [options.nozzleType];
|
|
423
|
+
bundle.settingsArg = [this.writeTempJson(outputBase, `machine_${options.nozzleType}`, machine), ...rest].join(";");
|
|
424
|
+
}
|
|
425
|
+
// Inherited process G-code is now present; apply Orca's existing
|
|
426
|
+
// absolute-extrusion normalization after resolving those ancestors.
|
|
427
|
+
if (slicerType === "orcaslicer" && bundle.settingsArg) {
|
|
428
|
+
const [machinePath, processPath] = bundle.settingsArg.split(";");
|
|
429
|
+
bundle.settingsArg = [
|
|
430
|
+
this.writeTempJson(outputBase, "machine_orca_resolved", this.sanitizeMachineForOrca(this.readJsonFile(machinePath))),
|
|
431
|
+
this.writeTempJson(outputBase, "process_orca_resolved", this.sanitizeProcessForOrca(this.readJsonFile(processPath), printerPreset)),
|
|
432
|
+
].join(";");
|
|
433
|
+
}
|
|
434
|
+
return this.isolateMachineSettings(bundle);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* The CLI applies every key in every --load-settings/--load-filaments file,
|
|
438
|
+
* so machine settings carried by a process or filament file (for example a
|
|
439
|
+
* template 3MF's project_settings from another printer) would replace the
|
|
440
|
+
* selected preset's start G-code, model, and bed geometry. The machine
|
|
441
|
+
* preset owns those keys; drop them from the other profiles.
|
|
442
|
+
*/
|
|
443
|
+
isolateMachineSettings(bundle) {
|
|
444
|
+
const [machinePath, processPath] = bundle.settingsArg?.split(";") ?? [];
|
|
445
|
+
if (!machinePath || !processPath)
|
|
446
|
+
return bundle;
|
|
447
|
+
const machineKeys = Object.keys(this.readJsonFile(machinePath)).filter((key) => !PROFILE_METADATA_KEYS.has(key));
|
|
448
|
+
const isolate = (filePath, label) => {
|
|
449
|
+
const profile = this.readJsonFile(filePath);
|
|
450
|
+
const foreign = machineKeys.filter((key) => Object.prototype.hasOwnProperty.call(profile, key));
|
|
451
|
+
if (foreign.length === 0)
|
|
452
|
+
return filePath;
|
|
453
|
+
const isolated = { ...profile };
|
|
454
|
+
for (const key of foreign)
|
|
455
|
+
delete isolated[key];
|
|
456
|
+
console.error(`Ignoring ${foreign.length} machine setting(s) in ${label} profile ${filePath} ` +
|
|
457
|
+
`(the selected machine preset owns them): ${foreign.slice(0, 8).join(", ")}${foreign.length > 8 ? ", ..." : ""}`);
|
|
458
|
+
return this.writeTempJson(path.basename(filePath, ".json"), `${label}_without_machine`, isolated);
|
|
459
|
+
};
|
|
460
|
+
return {
|
|
461
|
+
...bundle,
|
|
462
|
+
settingsArg: [machinePath, isolate(processPath, "process")].join(";"),
|
|
463
|
+
filamentPaths: bundle.filamentPaths.map((filePath) => isolate(filePath, "filament")),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/** Keys every profile kind carries; they describe the file, not machine hardware. */
|
|
468
|
+
const PROFILE_METADATA_KEYS = new Set([
|
|
469
|
+
"name", "type", "from", "inherits", "instantiation", "setting_id", "version", "is_custom_defined",
|
|
470
|
+
"compatible_printers", "compatible_printers_condition", "compatible_prints", "compatible_prints_condition",
|
|
471
|
+
]);
|
|
472
|
+
/** CLI flags for the prepared bundle and caller options (input path appended by the caller). */
|
|
473
|
+
export function buildBambuCliArgs(bundle, outputDir, exportName, options) {
|
|
474
|
+
const args = [
|
|
475
|
+
"--slice", String(options.slicePlate ?? 0),
|
|
476
|
+
"--outputdir", outputDir,
|
|
477
|
+
"--export-3mf", exportName,
|
|
478
|
+
];
|
|
479
|
+
if (bundle.settingsArg)
|
|
480
|
+
args.push("--load-settings", bundle.settingsArg);
|
|
481
|
+
// Always allow newer-version 3MF files (the CLI rejects them by default).
|
|
482
|
+
args.push("--allow-newer-file");
|
|
483
|
+
if (options.uptodate)
|
|
484
|
+
args.push("--uptodate");
|
|
485
|
+
if (options.ensureOnBed)
|
|
486
|
+
args.push("--ensure-on-bed");
|
|
487
|
+
if (options.enableTimelapse)
|
|
488
|
+
args.push("--enable-timelapse");
|
|
489
|
+
if (options.allowMixTemp)
|
|
490
|
+
args.push("--allow-mix-temp");
|
|
491
|
+
if (options.minSave)
|
|
492
|
+
args.push("--min-save");
|
|
493
|
+
if (options.skipModifiedGcodes)
|
|
494
|
+
args.push("--skip-modified-gcodes");
|
|
495
|
+
if (options.orient !== undefined)
|
|
496
|
+
args.push("--orient", options.orient ? "1" : "0");
|
|
497
|
+
if (options.arrange !== undefined)
|
|
498
|
+
args.push("--arrange", options.arrange ? "1" : "0");
|
|
499
|
+
if (options.repetitions !== undefined)
|
|
500
|
+
args.push("--repetitions", String(options.repetitions));
|
|
501
|
+
if (options.scale !== undefined)
|
|
502
|
+
args.push("--scale", String(options.scale));
|
|
503
|
+
if (options.rotate !== undefined)
|
|
504
|
+
args.push("--rotate", String(options.rotate));
|
|
505
|
+
if (options.rotateX !== undefined)
|
|
506
|
+
args.push("--rotate-x", String(options.rotateX));
|
|
507
|
+
if (options.rotateY !== undefined)
|
|
508
|
+
args.push("--rotate-y", String(options.rotateY));
|
|
509
|
+
if (options.cloneObjects)
|
|
510
|
+
args.push("--clone-objects", options.cloneObjects);
|
|
511
|
+
if (options.skipObjects)
|
|
512
|
+
args.push("--skip-objects", options.skipObjects);
|
|
513
|
+
if (bundle.filamentPaths.length > 0) {
|
|
514
|
+
args.push("--load-filaments", bundle.filamentPaths.join(";"));
|
|
515
|
+
args.push("--load-defaultfila");
|
|
516
|
+
}
|
|
517
|
+
if (options.loadFilamentIds)
|
|
518
|
+
args.push("--load-filament-ids", options.loadFilamentIds);
|
|
519
|
+
return args;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* A sliced Bambu project must contain at least one nonempty
|
|
523
|
+
* Metadata/plate_<n>.gcode entry. A .gcode.md5 checksum or the unsliced
|
|
524
|
+
* input geometry is not printable output.
|
|
525
|
+
*/
|
|
526
|
+
export async function assertSlicedProjectOutput(outputPath) {
|
|
527
|
+
const JSZip = (await import("jszip")).default;
|
|
528
|
+
let zip;
|
|
529
|
+
try {
|
|
530
|
+
zip = await JSZip.loadAsync(await fs.promises.readFile(outputPath));
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
throw new Error(`Slicer output ${outputPath} is not a readable 3MF archive: ${error?.message ?? error}`);
|
|
534
|
+
}
|
|
535
|
+
const plates = [];
|
|
536
|
+
for (const entry of Object.values(zip.files)) {
|
|
537
|
+
if (entry.dir || !/^Metadata\/plate_\d+\.gcode$/i.test(entry.name))
|
|
538
|
+
continue;
|
|
539
|
+
const content = await entry.async("uint8array");
|
|
540
|
+
if (content.length > 0)
|
|
541
|
+
plates.push(entry.name);
|
|
542
|
+
}
|
|
543
|
+
if (plates.length === 0) {
|
|
544
|
+
throw new Error(`Slicer output ${outputPath} does not contain any nonempty Metadata/plate_<n>.gcode entry; it is not a printable sliced project.`);
|
|
545
|
+
}
|
|
546
|
+
return plates.sort();
|
|
547
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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, and
|
|
14
|
+
* applies each level's `include` templates (G-code templates for
|
|
15
|
+
* machines, per-variant defaults for filaments) the way the GUI does.
|
|
16
|
+
* 3. Derives `nozzle_volume_type` from `default_nozzle_volume_type[0]`
|
|
17
|
+
* (the GUI does this implicitly; the CLI doesn't).
|
|
18
|
+
* 4. Validates the model in `BBL/cli_config.json` and merges its CLI-specific
|
|
19
|
+
* machine_limits where supplied for safe accelerations / jerks.
|
|
20
|
+
* 5. Writes the flattened JSON to a temp file the caller passes to
|
|
21
|
+
* BambuStudio CLI.
|
|
22
|
+
*
|
|
23
|
+
* BBL only. Other vendors are out of scope and rejected explicitly so we
|
|
24
|
+
* fail loud rather than producing dangerous gcode for hardware we don't own.
|
|
25
|
+
*/
|
|
26
|
+
export type ProfileKind = "machine" | "process" | "filament";
|
|
27
|
+
export interface FlattenedProfiles {
|
|
28
|
+
machinePath: string;
|
|
29
|
+
processPath: string;
|
|
30
|
+
filamentPaths: string[];
|
|
31
|
+
/** Diagnostics for callers / logs. Not part of the slicer invocation. */
|
|
32
|
+
meta: {
|
|
33
|
+
profilesRoot: string;
|
|
34
|
+
machineLeafName: string;
|
|
35
|
+
processLeafName: string;
|
|
36
|
+
filamentLeafNames: string[];
|
|
37
|
+
/** False only for an explicit standalone custom machine. */
|
|
38
|
+
cliConfigValidated: boolean;
|
|
39
|
+
/** Some validated official model configs intentionally have no limits. */
|
|
40
|
+
cliOverlayApplied: boolean;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface FlattenOptions {
|
|
44
|
+
/** e.g. "Bambu Lab H2S 0.4 nozzle" */
|
|
45
|
+
machineLeaf: string;
|
|
46
|
+
/** e.g. "0.20mm Standard @BBL H2S" */
|
|
47
|
+
processLeaf: string;
|
|
48
|
+
/** e.g. ["Bambu PLA Basic @BBL H2S"] */
|
|
49
|
+
filamentLeaves: string[];
|
|
50
|
+
/** Absolute path to `.../Resources/profiles`. */
|
|
51
|
+
profilesRoot: string;
|
|
52
|
+
/** Configured BBL directories for custom process/filament dependencies only. */
|
|
53
|
+
userProfileRoots?: string[];
|
|
54
|
+
/** Where to write flattened temp files. */
|
|
55
|
+
tempDir: string;
|
|
56
|
+
/** Vendor subdir under profilesRoot. Currently only "BBL" supported. */
|
|
57
|
+
vendor?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Override for installed nozzle flow type. The printer reports this on
|
|
60
|
+
* boot from its physical nozzle scan; both nozzles always match (you
|
|
61
|
+
* can't mix Standard + High Flow, just like you can't mix 0.2 + 0.4).
|
|
62
|
+
* If omitted we use `default_nozzle_volume_type` from the profile tree
|
|
63
|
+
* (= "Standard" on stock BBL machines). Pass "High Flow" when HF
|
|
64
|
+
* nozzles are installed.
|
|
65
|
+
*/
|
|
66
|
+
nozzleVolumeType?: "Standard" | "High Flow";
|
|
67
|
+
/** BambuStudio display name, e.g. "Textured PEI Plate" or "Cool Plate". */
|
|
68
|
+
bedType?: string;
|
|
69
|
+
/** Actual input configs, including user overrides on top of BBL parents. */
|
|
70
|
+
sourceProfiles?: {
|
|
71
|
+
machine?: Record<string, unknown>;
|
|
72
|
+
process?: Record<string, unknown>;
|
|
73
|
+
filaments?: (Record<string, unknown> | undefined)[];
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Positional `#RRGGBB` colour per filament slot (e.g. from the input 3MF
|
|
77
|
+
* project or the caller). Missing entries keep the profile's own colour or
|
|
78
|
+
* fall back to DEFAULT_FILAMENT_COLOUR.
|
|
79
|
+
*/
|
|
80
|
+
filamentColours?: (string | undefined)[];
|
|
81
|
+
/** Saved per-plate positions, used only when the process sets no position. */
|
|
82
|
+
projectTowerPosition?: {
|
|
83
|
+
wipe_tower_x?: unknown;
|
|
84
|
+
wipe_tower_y?: unknown;
|
|
85
|
+
};
|
|
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>>;
|
|
89
|
+
/** Best-effort extruder count for fallback nozzle_volume_type sizing. */
|
|
90
|
+
/** BambuStudio's built-in filament_colour default. */
|
|
91
|
+
export declare const DEFAULT_FILAMENT_COLOUR = "#00AE42";
|
|
92
|
+
/**
|
|
93
|
+
* Flatten the leaf profiles, post-process for CLI, and write to temp files.
|
|
94
|
+
*
|
|
95
|
+
* Throws on unknown leaf names, missing profilesRoot, or cycles.
|
|
96
|
+
*/
|
|
97
|
+
export declare function flattenForCli(opts: FlattenOptions): Promise<FlattenedProfiles>;
|
|
98
|
+
/**
|
|
99
|
+
* Given the SLICER_PATH (path to BambuStudio executable), walk up to the
|
|
100
|
+
* profile directory for that installation (macOS, Windows, or Linux prefix).
|
|
101
|
+
*
|
|
102
|
+
* Override via BAMBU_PROFILES_ROOT env.
|
|
103
|
+
*/
|
|
104
|
+
export declare function detectProfilesRoot(slicerPath?: string, slicerType?: string): string;
|