mcp-3d-printer-server 1.2.9 → 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 +207 -0
- package/CONTRIBUTORS.md +49 -0
- package/README.md +528 -811
- package/dist/3mf_parser.js +8 -8
- package/dist/blender-mcp-bridge.d.ts +1 -0
- package/dist/blender-mcp-bridge.js +209 -21
- package/dist/fulu-orca.js +29 -5
- package/dist/index.js +614 -110
- 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 -126
- 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 +16 -9
- package/server.json +29 -0
- package/src/3mf_parser.ts +8 -8
- package/src/blender-mcp-bridge.ts +200 -19
- package/src/fulu-orca.ts +36 -6
- package/src/index.ts +686 -137
- package/src/printers/bambu-node-guard.ts +44 -0
- package/src/printers/bambu.ts +458 -164
- 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
|
@@ -0,0 +1,676 @@
|
|
|
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
|
+
import fs from "node:fs/promises";
|
|
27
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import crypto from "node:crypto";
|
|
30
|
+
/* -------------------------------------------------------------------------- */
|
|
31
|
+
/* Indexing */
|
|
32
|
+
/* -------------------------------------------------------------------------- */
|
|
33
|
+
/**
|
|
34
|
+
* Build a name -> {filePath, data} map for every JSON in
|
|
35
|
+
* <profilesRoot>/<vendor>/{machine,process,filament}/.
|
|
36
|
+
*
|
|
37
|
+
* The `name` field inside each JSON is the lookup key (this is what
|
|
38
|
+
* `inherits` references). We index every file, including non-instantiable
|
|
39
|
+
* abstract bases like `fdm_machine_common` and `fdm_bbl_3dp_001_common`,
|
|
40
|
+
* because those are the parents we'll walk to.
|
|
41
|
+
*/
|
|
42
|
+
async function buildNameIndex(profilesRoot, vendor, userProfileRoots = []) {
|
|
43
|
+
const index = new Map();
|
|
44
|
+
index.malformed = [];
|
|
45
|
+
const subdirs = ["machine", "process", "filament"];
|
|
46
|
+
const directories = [
|
|
47
|
+
...subdirs.map(sub => path.join(profilesRoot, vendor, sub)),
|
|
48
|
+
...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
|
|
49
|
+
];
|
|
50
|
+
for (const dir of directories) {
|
|
51
|
+
let entries;
|
|
52
|
+
try {
|
|
53
|
+
entries = await fs.readdir(dir);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Vendor or subdir missing -- skip silently; flatten will fail later
|
|
57
|
+
// with a precise "name not found" error.
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry.endsWith(".json"))
|
|
62
|
+
continue;
|
|
63
|
+
const filePath = path.join(dir, entry);
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
let data;
|
|
72
|
+
try {
|
|
73
|
+
data = JSON.parse(raw);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Malformed profile -- skip, don't poison the index. A reference to
|
|
77
|
+
// it still fails, and the error names the skipped file.
|
|
78
|
+
index.malformed.push(filePath);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
82
|
+
index.malformed.push(filePath);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const name = data["name"];
|
|
86
|
+
if (typeof name !== "string" || name.length === 0)
|
|
87
|
+
continue;
|
|
88
|
+
// First-write wins. BBL doesn't have name collisions in practice;
|
|
89
|
+
// log if it ever does so we notice.
|
|
90
|
+
if (!index.has(name)) {
|
|
91
|
+
index.set(name, { filePath, data });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return index;
|
|
96
|
+
}
|
|
97
|
+
/* -------------------------------------------------------------------------- */
|
|
98
|
+
/* Inheritance walk + merge */
|
|
99
|
+
/* -------------------------------------------------------------------------- */
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the full inheritance chain for `leafName` and return a single
|
|
102
|
+
* deep-merged object. Child wins on key collision.
|
|
103
|
+
*
|
|
104
|
+
* Throws on:
|
|
105
|
+
* - Unknown name (broken `inherits` reference).
|
|
106
|
+
* - Cycles (A -> B -> A).
|
|
107
|
+
*/
|
|
108
|
+
function flattenByName(leafName, index, visiting = new Set()) {
|
|
109
|
+
if (visiting.has(leafName)) {
|
|
110
|
+
throw new Error(`Profile inheritance cycle detected (inherits/include): ${[...visiting, leafName].join(" -> ")}`);
|
|
111
|
+
}
|
|
112
|
+
const entry = index.get(leafName);
|
|
113
|
+
if (!entry) {
|
|
114
|
+
throw new Error(`Profile "${leafName}" not found in index. The profile tree is incomplete or the name is misspelled.${describeMalformed(index)}`);
|
|
115
|
+
}
|
|
116
|
+
visiting.add(leafName);
|
|
117
|
+
try {
|
|
118
|
+
return flattenData(entry.data, index, visiting);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
visiting.delete(leafName);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function describeMalformed(index) {
|
|
125
|
+
const malformed = index.malformed ?? [];
|
|
126
|
+
if (malformed.length === 0)
|
|
127
|
+
return "";
|
|
128
|
+
const shown = malformed.slice(0, 5).map((file) => path.basename(file)).join(", ");
|
|
129
|
+
return ` Skipped ${malformed.length} unreadable or malformed profile file(s): ${shown}${malformed.length > 5 ? ", ..." : ""}.`;
|
|
130
|
+
}
|
|
131
|
+
function flattenData(data, index, visiting = new Set()) {
|
|
132
|
+
const parent = data["inherits"];
|
|
133
|
+
const merged = typeof parent === "string" && parent.length > 0
|
|
134
|
+
? flattenByName(parent, index, visiting)
|
|
135
|
+
: {};
|
|
136
|
+
// Includes may themselves inherit or include templates. Each level wins
|
|
137
|
+
// over its parent, then the including profile's own settings win last.
|
|
138
|
+
applyIncludes(merged, data, index, visiting);
|
|
139
|
+
Object.assign(merged, data);
|
|
140
|
+
delete merged["include"];
|
|
141
|
+
return merged;
|
|
142
|
+
}
|
|
143
|
+
/** Resolve bundled machine defaults before choosing process and filament leaves. */
|
|
144
|
+
export async function resolveBblMachineProfile(profilesRoot, machineLeaf) {
|
|
145
|
+
return flattenByName(machineLeaf, await buildNameIndex(profilesRoot, 'BBL'));
|
|
146
|
+
}
|
|
147
|
+
/** Keys of an include template that describe the template, not the config. */
|
|
148
|
+
const INCLUDE_METADATA_KEYS = new Set(["name", "type", "from", "instantiation", "inherits", "include", "setting_id"]);
|
|
149
|
+
/**
|
|
150
|
+
* Recent BBL profiles (e.g. "Bambu Lab P2S 0.4 nozzle") no longer carry
|
|
151
|
+
* their G-code inline: `machine_start_gcode`, `machine_end_gcode`,
|
|
152
|
+
* `change_filament_gcode`, ... live in separate "... template <key>"
|
|
153
|
+
* profiles listed under `include`. Skipping them silently falls back to
|
|
154
|
+
* the generic G-code inherited from `fdm_machine_common` & co, which is
|
|
155
|
+
* wrong for the printer (no AMS filament load, other printer's moves).
|
|
156
|
+
*/
|
|
157
|
+
function applyIncludes(target, profile, index, visiting) {
|
|
158
|
+
const raw = profile["include"];
|
|
159
|
+
if (raw === undefined || raw === null)
|
|
160
|
+
return;
|
|
161
|
+
if (typeof raw !== "string" && !Array.isArray(raw)) {
|
|
162
|
+
throw new Error(`Profile "${String(profile["name"])}" has an invalid include; expected a name or array of names.`);
|
|
163
|
+
}
|
|
164
|
+
const names = typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : [];
|
|
165
|
+
for (const name of names) {
|
|
166
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
167
|
+
throw new Error(`Profile "${String(profile["name"])}" has an invalid include reference; every entry must be a nonempty name.`);
|
|
168
|
+
}
|
|
169
|
+
const entry = index.get(name);
|
|
170
|
+
if (!entry) {
|
|
171
|
+
throw new Error(`Profile "${String(profile["name"])}" includes "${name}", which is not in the index. ` +
|
|
172
|
+
`Refusing to fall back to inherited defaults (wrong G-code for this printer).${describeMalformed(index)}`);
|
|
173
|
+
}
|
|
174
|
+
for (const [key, value] of Object.entries(flattenByName(name, index, visiting))) {
|
|
175
|
+
if (!INCLUDE_METADATA_KEYS.has(key))
|
|
176
|
+
target[key] = value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/* -------------------------------------------------------------------------- */
|
|
181
|
+
/* CLI post-processing */
|
|
182
|
+
/* -------------------------------------------------------------------------- */
|
|
183
|
+
/**
|
|
184
|
+
* Bambu's CLI looks up `nozzle_volume_type`, but the profile tree only
|
|
185
|
+
* defines `default_nozzle_volume_type`. The GUI propagates the latter
|
|
186
|
+
* (or the value reported by the printer's nozzle scan on boot) into the
|
|
187
|
+
* former at runtime. We do the same.
|
|
188
|
+
*
|
|
189
|
+
* Shape: array of strings, one entry per physical nozzle. Hardware
|
|
190
|
+
* invariant from Bambu: BOTH nozzles must match (same diameter, same
|
|
191
|
+
* flow type) -- you cannot install 0.2 + 0.4, and you cannot install
|
|
192
|
+
* Standard + High Flow. So the array always contains identical entries.
|
|
193
|
+
*
|
|
194
|
+
* Verified against a GUI-sliced H2D project_settings.config:
|
|
195
|
+
* ["High Flow", "High Flow"] (dual-nozzle, HF installed)
|
|
196
|
+
* ["Standard", "Standard"] (dual-nozzle, stock)
|
|
197
|
+
* ["Standard"] (single-extruder default)
|
|
198
|
+
*
|
|
199
|
+
* Override priority:
|
|
200
|
+
* 1. Caller-supplied `nozzleVolumeType` (e.g. printer reported HF).
|
|
201
|
+
* 2. Existing `nozzle_volume_type` already in the flattened profile.
|
|
202
|
+
* 3. `default_nozzle_volume_type` from the profile tree.
|
|
203
|
+
* 4. ["Standard"] x extruder count.
|
|
204
|
+
*/
|
|
205
|
+
function deriveNozzleVolumeType(flat, override) {
|
|
206
|
+
if (override) {
|
|
207
|
+
const count = inferExtruderCount(flat);
|
|
208
|
+
flat["nozzle_volume_type"] = Array(count).fill(override);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(flat["nozzle_volume_type"])) {
|
|
212
|
+
enforceMatchingNozzles(flat["nozzle_volume_type"]);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const def = flat["default_nozzle_volume_type"];
|
|
216
|
+
if (Array.isArray(def) && def.every((v) => typeof v === "string")) {
|
|
217
|
+
enforceMatchingNozzles(def);
|
|
218
|
+
flat["nozzle_volume_type"] = [...def];
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const extruderCount = inferExtruderCount(flat);
|
|
222
|
+
flat["nozzle_volume_type"] = Array(extruderCount).fill("Standard");
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Hardware invariant: all nozzles on a Bambu printer have identical flow
|
|
226
|
+
* type (and diameter). If a profile somehow declares mixed types it's
|
|
227
|
+
* either bad input or a future mistake -- fail loud so we don't ship
|
|
228
|
+
* gcode that could damage the printer.
|
|
229
|
+
*/
|
|
230
|
+
function enforceMatchingNozzles(arr) {
|
|
231
|
+
if (arr.length <= 1)
|
|
232
|
+
return;
|
|
233
|
+
const first = arr[0];
|
|
234
|
+
for (let i = 1; i < arr.length; i++) {
|
|
235
|
+
if (arr[i] !== first) {
|
|
236
|
+
throw new Error(`nozzle_volume_type entries must all match (Bambu hardware invariant). ` +
|
|
237
|
+
`Got: ${JSON.stringify(arr)}. ` +
|
|
238
|
+
`Both nozzles on H2-series printers always have identical flow type.`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Best-effort extruder count for fallback nozzle_volume_type sizing. */
|
|
243
|
+
/** BambuStudio's built-in filament_colour default. */
|
|
244
|
+
export const DEFAULT_FILAMENT_COLOUR = "#00AE42";
|
|
245
|
+
const FILAMENT_COLOUR_RE = /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
|
|
246
|
+
/**
|
|
247
|
+
* No BBL filament profile defines `filament_colour`; the GUI fills it from
|
|
248
|
+
* the AMS. Without it the CLI keeps a single-entry colour vector however many
|
|
249
|
+
* filaments are loaded and crashes (access violation) as soon as a slice uses
|
|
250
|
+
* any filament after the first. Give every slot exactly one colour.
|
|
251
|
+
*/
|
|
252
|
+
function ensureFilamentColour(flat, colour) {
|
|
253
|
+
if (colour !== undefined) {
|
|
254
|
+
if (!FILAMENT_COLOUR_RE.test(colour)) {
|
|
255
|
+
throw new Error(`Invalid filament colour "${colour}"; expected #RRGGBB.`);
|
|
256
|
+
}
|
|
257
|
+
flat["filament_colour"] = [colour];
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const own = flat["filament_colour"];
|
|
261
|
+
if (Array.isArray(own) && typeof own[0] === "string" && FILAMENT_COLOUR_RE.test(own[0])) {
|
|
262
|
+
flat["filament_colour"] = [own[0]];
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
flat["filament_colour"] = [DEFAULT_FILAMENT_COLOUR];
|
|
266
|
+
}
|
|
267
|
+
// BambuStudio's defaults when a process sets no prime tower position/width.
|
|
268
|
+
const DEFAULT_WIPE_TOWER_X = 15;
|
|
269
|
+
const DEFAULT_WIPE_TOWER_Y = 220;
|
|
270
|
+
const DEFAULT_PRIME_TOWER_WIDTH = 35;
|
|
271
|
+
// Clearance inside the shared nozzle area for the auto-sized tower brim and
|
|
272
|
+
// the CLI's own safety margin (X2D fails at 5 mm from the edge, passes at 6).
|
|
273
|
+
const WIPE_TOWER_MARGIN = 15;
|
|
274
|
+
function parseArea(area) {
|
|
275
|
+
const points = area.split(",").map((p) => p.trim().split("x").map(Number));
|
|
276
|
+
if (points.length < 3 || points.some((p) => p.length !== 2 || p.some((n) => !Number.isFinite(n)))) {
|
|
277
|
+
throw new Error(`Malformed extruder_printable_area entry "${area}".`);
|
|
278
|
+
}
|
|
279
|
+
const xs = points.map((p) => p[0]);
|
|
280
|
+
const ys = points.map((p) => p[1]);
|
|
281
|
+
return { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* On multi-nozzle machines (H2D, X2D) each nozzle reaches only part of the
|
|
285
|
+
* bed (`extruder_printable_area`), and the prime tower must be reachable by
|
|
286
|
+
* every nozzle. The CLI's default tower position (x=15) lies outside the
|
|
287
|
+
* X2D/H2D right-nozzle area, so slicing fails with "G-code outside of the
|
|
288
|
+
* printable area". When the process sets no position and the default does
|
|
289
|
+
* not fit, move the tower inside the area all nozzles share.
|
|
290
|
+
*/
|
|
291
|
+
function placePrimeTowerForAllNozzles(machineFlat, processFlat) {
|
|
292
|
+
if (processFlat["wipe_tower_x"] !== undefined || processFlat["wipe_tower_y"] !== undefined)
|
|
293
|
+
return;
|
|
294
|
+
if (["0", "false"].includes(String(processFlat["enable_prime_tower"]).toLowerCase()))
|
|
295
|
+
return;
|
|
296
|
+
const areas = machineFlat["extruder_printable_area"];
|
|
297
|
+
if (!Array.isArray(areas) || areas.length < 2)
|
|
298
|
+
return;
|
|
299
|
+
const rects = areas.map((a) => {
|
|
300
|
+
if (typeof a !== "string")
|
|
301
|
+
throw new Error("Malformed extruder_printable_area in machine profile.");
|
|
302
|
+
return parseArea(a);
|
|
303
|
+
});
|
|
304
|
+
const shared = {
|
|
305
|
+
minX: Math.max(...rects.map((r) => r.minX)) + WIPE_TOWER_MARGIN,
|
|
306
|
+
maxX: Math.min(...rects.map((r) => r.maxX)) - WIPE_TOWER_MARGIN,
|
|
307
|
+
minY: Math.max(...rects.map((r) => r.minY)) + WIPE_TOWER_MARGIN,
|
|
308
|
+
maxY: Math.min(...rects.map((r) => r.maxY)) - WIPE_TOWER_MARGIN,
|
|
309
|
+
};
|
|
310
|
+
const width = Number(processFlat["prime_tower_width"] ?? DEFAULT_PRIME_TOWER_WIDTH);
|
|
311
|
+
const size = Number.isFinite(width) && width > 0 ? width : DEFAULT_PRIME_TOWER_WIDTH;
|
|
312
|
+
if (shared.maxX - shared.minX < size || shared.maxY - shared.minY < size) {
|
|
313
|
+
throw new Error("No bed area is reachable by every nozzle for the prime tower.");
|
|
314
|
+
}
|
|
315
|
+
const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
316
|
+
const x = clamp(DEFAULT_WIPE_TOWER_X, shared.minX, shared.maxX - size);
|
|
317
|
+
const y = clamp(DEFAULT_WIPE_TOWER_Y, shared.minY, shared.maxY - size);
|
|
318
|
+
if (x === DEFAULT_WIPE_TOWER_X && y === DEFAULT_WIPE_TOWER_Y)
|
|
319
|
+
return;
|
|
320
|
+
processFlat["wipe_tower_x"] = [String(x)];
|
|
321
|
+
processFlat["wipe_tower_y"] = [String(y)];
|
|
322
|
+
}
|
|
323
|
+
function inferExtruderCount(flat) {
|
|
324
|
+
for (const key of ["nozzle_diameter", "extruder_type", "extruder_variant_list"]) {
|
|
325
|
+
const v = flat[key];
|
|
326
|
+
if (Array.isArray(v) && v.length > 0)
|
|
327
|
+
return v.length;
|
|
328
|
+
}
|
|
329
|
+
return 1;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Validate the selected model's CLI config before making a bundled profile
|
|
333
|
+
* into a User profile. A1-family models require machine_limits overrides;
|
|
334
|
+
* other official models (including P1S and H2D) intentionally declare only
|
|
335
|
+
* downward_check. Missing config cannot tell us whether limits are required.
|
|
336
|
+
* Returns whether limits were applied, not whether validation succeeded.
|
|
337
|
+
*/
|
|
338
|
+
async function applyCliOverlay(flat, profilesRoot, vendor) {
|
|
339
|
+
const cliConfigPath = path.join(profilesRoot, vendor, "cli_config.json");
|
|
340
|
+
const fail = (reason) => {
|
|
341
|
+
throw new Error(`Cannot prepare machine profile "${String(flat["name"])}" ` +
|
|
342
|
+
`(model "${String(flat["printer_model"] ?? stripNozzleSuffix(String(flat["name"])))}"): ` +
|
|
343
|
+
`${cliConfigPath}: ${reason}. Check your BambuStudio installation or BAMBU_PROFILES_ROOT.`);
|
|
344
|
+
};
|
|
345
|
+
let raw;
|
|
346
|
+
try {
|
|
347
|
+
raw = await fs.readFile(cliConfigPath, "utf8");
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return fail("required CLI configuration is missing or unreadable");
|
|
351
|
+
}
|
|
352
|
+
let cliConfig;
|
|
353
|
+
try {
|
|
354
|
+
cliConfig = JSON.parse(raw);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return fail("invalid JSON in required CLI configuration");
|
|
358
|
+
}
|
|
359
|
+
if (!isRecord(cliConfig))
|
|
360
|
+
return fail("expected a CLI configuration object");
|
|
361
|
+
const printerSection = cliConfig["printer"];
|
|
362
|
+
if (!isRecord(printerSection))
|
|
363
|
+
return fail("expected a printer configuration object");
|
|
364
|
+
// Match key: cli_config.json keys are bare printer names like
|
|
365
|
+
// "Bambu Lab H2D" / "Bambu Lab A1". If no model is provided, try preset
|
|
366
|
+
// identity with the nozzle suffix stripped, then the name as-is.
|
|
367
|
+
// An explicit model is authoritative; do not silently use another model's
|
|
368
|
+
// limits because a stale preset name happens to match.
|
|
369
|
+
const candidates = typeof flat["printer_model"] === "string" && flat["printer_model"].trim()
|
|
370
|
+
? [flat["printer_model"]]
|
|
371
|
+
: [
|
|
372
|
+
typeof flat["printer_settings_id"] === "string"
|
|
373
|
+
? stripNozzleSuffix(flat["printer_settings_id"])
|
|
374
|
+
: undefined,
|
|
375
|
+
typeof flat["name"] === "string" ? stripNozzleSuffix(flat["name"]) : undefined,
|
|
376
|
+
flat["name"],
|
|
377
|
+
].filter((v) => typeof v === "string" && v.length > 0);
|
|
378
|
+
const model = candidates.find(key => Object.prototype.hasOwnProperty.call(printerSection, key));
|
|
379
|
+
if (!model)
|
|
380
|
+
return fail(`selected model is absent (looked for ${candidates.join(", ")})`);
|
|
381
|
+
const block = printerSection[model];
|
|
382
|
+
if (!isRecord(block))
|
|
383
|
+
return fail(`printer.${model} must be an object`);
|
|
384
|
+
const downward = block["downward_check"];
|
|
385
|
+
if (Object.prototype.hasOwnProperty.call(block, "downward_check") && (!isRecord(downward) || Object.keys(downward).length === 0 ||
|
|
386
|
+
Object.entries(downward).some(([name, values]) => !name.trim() || !Array.isArray(values) ||
|
|
387
|
+
values.some(value => typeof value !== "string" || !value.trim())))) {
|
|
388
|
+
return fail(`printer.${model}.downward_check must map profile names to arrays of names`);
|
|
389
|
+
}
|
|
390
|
+
if (!Object.prototype.hasOwnProperty.call(block, "machine_limits")) {
|
|
391
|
+
if (!isRecord(downward))
|
|
392
|
+
return fail(`printer.${model} has neither machine_limits nor downward_check`);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
const limits = block["machine_limits"];
|
|
396
|
+
if (!isRecord(limits) || Object.keys(limits).length === 0) {
|
|
397
|
+
return fail(`printer.${model}.machine_limits must be a nonempty object`);
|
|
398
|
+
}
|
|
399
|
+
const limitKey = /^cli_safe_(?:acceleration_(?:e|extruding|retracting|travel|x|y|z)|(?:jerk|speed)_(?:e|x|y|z))$/;
|
|
400
|
+
for (const [key, value] of Object.entries(limits)) {
|
|
401
|
+
if (!limitKey.test(key) || typeof value !== "string" ||
|
|
402
|
+
value.split(",").some(part => !part.trim() || !Number.isFinite(Number(part)) || Number(part) < 0)) {
|
|
403
|
+
return fail(`printer.${model}.machine_limits contains an invalid limit "${key}"`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// CLI safety values override even explicit values in the selected leaf.
|
|
407
|
+
Object.assign(flat, limits);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
function isRecord(value) {
|
|
411
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Ensure the chosen machine is in the process/filament's
|
|
415
|
+
* `compatible_printers` list. This mirrors what the GUI does when you
|
|
416
|
+
* save a project with a printer/process combo that wasn't in the
|
|
417
|
+
* shipped compat list.
|
|
418
|
+
*/
|
|
419
|
+
function ensureMachineInCompatList(flat, machineLeaf) {
|
|
420
|
+
const list = flat["compatible_printers"];
|
|
421
|
+
if (!Array.isArray(list)) {
|
|
422
|
+
flat["compatible_printers"] = [machineLeaf];
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (!list.includes(machineLeaf)) {
|
|
426
|
+
list.push(machineLeaf);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function stripNozzleSuffix(name) {
|
|
430
|
+
return name.replace(/\s+\d+\.\d+\s+nozzle.*$/, "");
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Adjust fields for CLI consumption.
|
|
434
|
+
*
|
|
435
|
+
* - Strip `inherits` and `instantiation` (chain marker, abstract-base flag).
|
|
436
|
+
* The GUI consumes them; the CLI treats anything still present as part of
|
|
437
|
+
* the merged config.
|
|
438
|
+
* - Rewrite `from`. Leaf BBL profiles ship with `"from": "system"`, which
|
|
439
|
+
* the CLI rejects when loaded via `--load-settings` (system presets
|
|
440
|
+
* aren't supposed to be loaded that way). The GUI rewrites this to
|
|
441
|
+
* `"from": "project"` when it embeds the merged config in a 3MF. We do
|
|
442
|
+
* the same so the CLI accepts our flattened temp file.
|
|
443
|
+
* - Strip `setting_id` / `is_custom_defined` (user-registry markers that
|
|
444
|
+
* make no sense on a transient flattened file).
|
|
445
|
+
*/
|
|
446
|
+
function normalizeForCli(flat, kind, leafName) {
|
|
447
|
+
// Set inherits to the leaf name BEFORE we delete it below, so the
|
|
448
|
+
// CLI's system_name resolution lands on the right value.
|
|
449
|
+
// (See note further down for why this matters.)
|
|
450
|
+
flat["inherits"] = leafName;
|
|
451
|
+
// Identity field. The CLI sets this internally, but we mirror it for
|
|
452
|
+
// consistency with what the GUI's project_settings.config emits.
|
|
453
|
+
switch (kind) {
|
|
454
|
+
case "machine":
|
|
455
|
+
flat["printer_settings_id"] = leafName;
|
|
456
|
+
break;
|
|
457
|
+
case "process":
|
|
458
|
+
flat["print_settings_id"] = leafName;
|
|
459
|
+
break;
|
|
460
|
+
case "filament":
|
|
461
|
+
flat["filament_settings_id"] = [leafName];
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
// CRITICAL: when from=="User", the CLI computes `system_name = inherits`
|
|
465
|
+
// and the compatibility check compares the *other* configs'
|
|
466
|
+
// `compatible_printers` list against that system_name. The leaf
|
|
467
|
+
// profiles' compat lists contain the leaf machine name (e.g.
|
|
468
|
+
// "Bambu Lab H2S 0.4 nozzle"), so we must set `inherits` on the
|
|
469
|
+
// flattened machine to that same leaf name. For process/filament,
|
|
470
|
+
// `inherits` doesn't drive the compat check we hit, but we set it for
|
|
471
|
+
// symmetry so any future check succeeds the same way.
|
|
472
|
+
// Source: BambuStudio.cpp ~line 2222 (from!="system" -> system_name = inherits).
|
|
473
|
+
// NOTE: do NOT delete inherits -- we just set it above. The CLI uses
|
|
474
|
+
// its value (when from=="User") for the system_name compat check.
|
|
475
|
+
delete flat["instantiation"];
|
|
476
|
+
delete flat["setting_id"];
|
|
477
|
+
delete flat["is_custom_defined"];
|
|
478
|
+
// Always rewrite `from` to "User". "Project" is what the GUI uses when
|
|
479
|
+
// embedding in a 3MF, but the CLI accepts "User" for --load-settings
|
|
480
|
+
// paths and that's semantically what we are.
|
|
481
|
+
flat["from"] = "User";
|
|
482
|
+
// Mirror the GUI's behavior of clearing compatibility constraints in the
|
|
483
|
+
// merged project config. The leaf process/filament profiles ship with
|
|
484
|
+
// `compatible_printers: ["Bambu Lab H2S 0.4 nozzle"]` etc., and the CLI
|
|
485
|
+
// re-validates that list against our flattened machine config -- which
|
|
486
|
+
// has had `from` rewritten and inheritance collapsed, so the equality
|
|
487
|
+
// check fails. The GUI's project_settings.config has these set to null
|
|
488
|
+
// because at that point compatibility is already established by the
|
|
489
|
+
// user. We do the same.
|
|
490
|
+
// Leave compatible_printers / compatible_prints as the leaf declared
|
|
491
|
+
// them. The GUI nulls these in embedded project configs, but the CLI's
|
|
492
|
+
// --load-settings path requires a non-empty list and matches against
|
|
493
|
+
// the machine's `name` / `printer_settings_id`. The leaves ship with
|
|
494
|
+
// the correct list ([machine_leaf_name]) so just preserve.
|
|
495
|
+
// _condition fields can stay null (string-typed, tolerated as null).
|
|
496
|
+
}
|
|
497
|
+
function applyBedType(processFlat, bedType) {
|
|
498
|
+
if (!bedType)
|
|
499
|
+
return;
|
|
500
|
+
processFlat.curr_bed_type = bedType;
|
|
501
|
+
}
|
|
502
|
+
function applyMachineModelBedMetadata(machineFlat, index) {
|
|
503
|
+
const modelName = machineFlat.printer_model;
|
|
504
|
+
if (typeof modelName !== "string")
|
|
505
|
+
return;
|
|
506
|
+
const model = index.get(modelName)?.data;
|
|
507
|
+
if (!model)
|
|
508
|
+
return;
|
|
509
|
+
for (const key of ["default_bed_type", "image_bed_type", "not_support_bed_type"]) {
|
|
510
|
+
if (!(key in machineFlat) && key in model) {
|
|
511
|
+
machineFlat[key] = model[key];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/* -------------------------------------------------------------------------- */
|
|
516
|
+
/* Public entry point */
|
|
517
|
+
/* -------------------------------------------------------------------------- */
|
|
518
|
+
/**
|
|
519
|
+
* Flatten the leaf profiles, post-process for CLI, and write to temp files.
|
|
520
|
+
*
|
|
521
|
+
* Throws on unknown leaf names, missing profilesRoot, or cycles.
|
|
522
|
+
*/
|
|
523
|
+
export async function flattenForCli(opts) {
|
|
524
|
+
const vendor = opts.vendor ?? "BBL";
|
|
525
|
+
if (vendor !== "BBL") {
|
|
526
|
+
throw new Error(`profile-flatten: only BBL vendor is supported (got "${vendor}"). ` +
|
|
527
|
+
`Other vendors are out of scope -- contributions welcome but untested.`);
|
|
528
|
+
}
|
|
529
|
+
// Confirm the profiles root looks plausible before doing real work.
|
|
530
|
+
const probe = path.join(opts.profilesRoot, vendor, "machine");
|
|
531
|
+
try {
|
|
532
|
+
await fs.access(probe);
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
throw new Error(`profile-flatten: profilesRoot "${opts.profilesRoot}" does not contain "${vendor}/machine". ` +
|
|
536
|
+
`Set BAMBU_PROFILES_ROOT or check your BambuStudio install.`);
|
|
537
|
+
}
|
|
538
|
+
// Machine inheritance must never resolve through user process/filament files.
|
|
539
|
+
const machineIndex = await buildNameIndex(opts.profilesRoot, vendor);
|
|
540
|
+
const index = opts.userProfileRoots?.length
|
|
541
|
+
? await buildNameIndex(opts.profilesRoot, vendor, opts.userProfileRoots)
|
|
542
|
+
: machineIndex;
|
|
543
|
+
// Flatten each leaf.
|
|
544
|
+
if (opts.sourceProfiles?.filaments && opts.sourceProfiles.filaments.length !== opts.filamentLeaves.length) {
|
|
545
|
+
throw new Error("Every filament slot must have a source profile.");
|
|
546
|
+
}
|
|
547
|
+
const machineFlat = opts.sourceProfiles?.machine
|
|
548
|
+
? flattenData(opts.sourceProfiles.machine, machineIndex)
|
|
549
|
+
: flattenByName(opts.machineLeaf, machineIndex);
|
|
550
|
+
const processFlat = opts.sourceProfiles?.process
|
|
551
|
+
? flattenData(opts.sourceProfiles.process, index)
|
|
552
|
+
: flattenByName(opts.processLeaf, index);
|
|
553
|
+
const filamentFlats = opts.filamentLeaves.map((n, i) => {
|
|
554
|
+
const source = opts.sourceProfiles?.filaments?.[i];
|
|
555
|
+
return source ? flattenData(source, index) : flattenByName(n, index);
|
|
556
|
+
});
|
|
557
|
+
// CLI-specific post-processing on machine profile only.
|
|
558
|
+
deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
|
|
559
|
+
applyMachineModelBedMetadata(machineFlat, machineIndex);
|
|
560
|
+
// A standalone custom machine can accompany process/filament profiles that
|
|
561
|
+
// need BBL resolution; do not impose bundled model config on that machine.
|
|
562
|
+
const sourceMachine = opts.sourceProfiles?.machine;
|
|
563
|
+
const standaloneMachine = sourceMachine && String(sourceMachine.from).toLowerCase() !== "system" &&
|
|
564
|
+
!(typeof sourceMachine.inherits === "string" && sourceMachine.inherits.length > 0) &&
|
|
565
|
+
(sourceMachine.include === undefined || sourceMachine.include === null);
|
|
566
|
+
const cliConfigValidated = !standaloneMachine;
|
|
567
|
+
const cliOverlayApplied = cliConfigValidated
|
|
568
|
+
? await applyCliOverlay(machineFlat, opts.profilesRoot, vendor)
|
|
569
|
+
: false;
|
|
570
|
+
// Normalize each flattened profile for CLI consumption.
|
|
571
|
+
normalizeForCli(machineFlat, "machine", opts.machineLeaf);
|
|
572
|
+
normalizeForCli(processFlat, "process", opts.processLeaf);
|
|
573
|
+
filamentFlats.forEach((f, i) => normalizeForCli(f, "filament", opts.filamentLeaves[i]));
|
|
574
|
+
if (opts.filamentColours && opts.filamentColours.length !== filamentFlats.length) {
|
|
575
|
+
throw new Error(`${opts.filamentColours.length} filament colours were supplied for ${filamentFlats.length} filament slots.`);
|
|
576
|
+
}
|
|
577
|
+
filamentFlats.forEach((f, i) => ensureFilamentColour(f, opts.filamentColours?.[i]));
|
|
578
|
+
applyBedType(processFlat, opts.bedType);
|
|
579
|
+
if (processFlat["wipe_tower_x"] === undefined && processFlat["wipe_tower_y"] === undefined) {
|
|
580
|
+
Object.assign(processFlat, opts.projectTowerPosition);
|
|
581
|
+
}
|
|
582
|
+
placePrimeTowerForAllNozzles(machineFlat, processFlat);
|
|
583
|
+
// Mirror the GUI's auto-extend behavior: when the caller explicitly
|
|
584
|
+
// chose a process or filament that wasn't pre-declared compatible with
|
|
585
|
+
// the chosen machine (e.g. "0.20mm Standard @BBL P1P" used on a P1S),
|
|
586
|
+
// add the machine name to the process/filament compat list. The GUI
|
|
587
|
+
// does this implicitly when saving a project.
|
|
588
|
+
ensureMachineInCompatList(processFlat, opts.machineLeaf);
|
|
589
|
+
filamentFlats.forEach((f) => ensureMachineInCompatList(f, opts.machineLeaf));
|
|
590
|
+
// Hash the resolved content so concurrent jobs using the same preset with
|
|
591
|
+
// different overrides never overwrite each other's input files.
|
|
592
|
+
await fs.mkdir(opts.tempDir, { recursive: true });
|
|
593
|
+
const machinePath = await writeTemp(opts.tempDir, "machine", opts.machineLeaf, machineFlat);
|
|
594
|
+
const processPath = await writeTemp(opts.tempDir, "process", opts.processLeaf, processFlat);
|
|
595
|
+
const filamentPaths = [];
|
|
596
|
+
for (let i = 0; i < filamentFlats.length; i++) {
|
|
597
|
+
filamentPaths.push(await writeTemp(opts.tempDir, `filament-${i}`, opts.filamentLeaves[i], filamentFlats[i]));
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
machinePath,
|
|
601
|
+
processPath,
|
|
602
|
+
filamentPaths,
|
|
603
|
+
meta: {
|
|
604
|
+
profilesRoot: opts.profilesRoot,
|
|
605
|
+
machineLeafName: opts.machineLeaf,
|
|
606
|
+
processLeafName: opts.processLeaf,
|
|
607
|
+
filamentLeafNames: opts.filamentLeaves,
|
|
608
|
+
cliConfigValidated,
|
|
609
|
+
cliOverlayApplied,
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
async function writeTemp(tempDir, kind, leafName, data) {
|
|
614
|
+
const serialized = JSON.stringify(data, null, 2);
|
|
615
|
+
const hash = crypto.createHash("sha256").update(leafName).update("\0").update(serialized).digest("hex").slice(0, 16);
|
|
616
|
+
const safe = leafName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
617
|
+
const filename = `flat-${kind}-${safe}-${hash}.json`;
|
|
618
|
+
const out = path.join(tempDir, filename);
|
|
619
|
+
// Concurrent slices flatten identical content to the same name; stage a
|
|
620
|
+
// private file and rename so no reader ever sees a partial profile.
|
|
621
|
+
const staging = `${out}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
622
|
+
await fs.writeFile(staging, serialized, "utf8");
|
|
623
|
+
await fs.rename(staging, out);
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
/* -------------------------------------------------------------------------- */
|
|
627
|
+
/* Profile root detection */
|
|
628
|
+
/* -------------------------------------------------------------------------- */
|
|
629
|
+
/**
|
|
630
|
+
* Given the SLICER_PATH (path to BambuStudio executable), walk up to the
|
|
631
|
+
* profile directory for that installation (macOS, Windows, or Linux prefix).
|
|
632
|
+
*
|
|
633
|
+
* Override via BAMBU_PROFILES_ROOT env.
|
|
634
|
+
*/
|
|
635
|
+
export function detectProfilesRoot(slicerPath, slicerType = 'bambustudio') {
|
|
636
|
+
if (process.env["BAMBU_PROFILES_ROOT"]) {
|
|
637
|
+
return process.env["BAMBU_PROFILES_ROOT"];
|
|
638
|
+
}
|
|
639
|
+
if (slicerPath) {
|
|
640
|
+
let executable = slicerPath;
|
|
641
|
+
if (!slicerPath.includes('/') && !slicerPath.includes('\\')) {
|
|
642
|
+
const located = (process.env.PATH ?? '').split(path.delimiter)
|
|
643
|
+
.map(dir => path.join(dir, slicerPath))
|
|
644
|
+
.find(candidate => existsSync(candidate));
|
|
645
|
+
if (located)
|
|
646
|
+
executable = located;
|
|
647
|
+
}
|
|
648
|
+
// Package-manager launchers may be symlinks into the installation prefix.
|
|
649
|
+
try {
|
|
650
|
+
executable = realpathSync(executable);
|
|
651
|
+
}
|
|
652
|
+
catch { /* Probe the supplied path below. */ }
|
|
653
|
+
const bin = path.dirname(executable);
|
|
654
|
+
const installNames = slicerType === 'bambustudio'
|
|
655
|
+
? ['BambuStudio', 'bambu-studio']
|
|
656
|
+
: ['OrcaSlicer', 'orca-slicer', 'OrcaStudio', 'orca-studio'];
|
|
657
|
+
const candidates = path.basename(bin) === 'MacOS'
|
|
658
|
+
? [path.resolve(bin, '..', 'Resources', 'profiles')]
|
|
659
|
+
: [
|
|
660
|
+
path.join(bin, 'resources', 'profiles'),
|
|
661
|
+
path.join(bin, 'Resources', 'profiles'),
|
|
662
|
+
...installNames.flatMap(name => [
|
|
663
|
+
path.resolve(bin, '..', 'share', name, 'profiles'),
|
|
664
|
+
path.resolve(bin, '..', 'share', name, 'resources', 'profiles'),
|
|
665
|
+
]),
|
|
666
|
+
];
|
|
667
|
+
const found = candidates.find(root => existsSync(path.join(root, 'BBL', 'machine')));
|
|
668
|
+
// Do not select another installation if this executable has no profile tree.
|
|
669
|
+
// flattenForCli reports the missing tree and asks for BAMBU_PROFILES_ROOT.
|
|
670
|
+
return found ?? candidates[0];
|
|
671
|
+
}
|
|
672
|
+
// Default macOS install.
|
|
673
|
+
return slicerType === 'bambustudio'
|
|
674
|
+
? '/Applications/BambuStudio.app/Contents/Resources/profiles'
|
|
675
|
+
: '/Applications/OrcaSlicer.app/Contents/Resources/profiles';
|
|
676
|
+
}
|