bambu-printer-mcp 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTORS.md +26 -0
- package/README.md +98 -25
- package/dist/blender-mcp-bridge.d.ts +10 -0
- package/dist/blender-mcp-bridge.js +389 -0
- package/dist/index.js +116 -80
- package/dist/printers/bambu.js +31 -13
- package/dist/slicer/profile-flatten.d.ts +15 -4
- package/dist/slicer/profile-flatten.js +181 -74
- package/dist/stl/stl-manipulator.d.ts +3 -17
- package/dist/stl/stl-manipulator.js +142 -85
- package/package.json +8 -4
- package/patches/bambu-node+3.22.21.patch +18 -2
- package/scripts/install-patches.mjs +64 -0
- package/src/blender-mcp-bridge.ts +344 -0
- package/src/index.ts +133 -103
- package/src/printers/bambu.ts +29 -13
- package/src/slicer/profile-flatten.ts +202 -80
- package/src/stl/stl-manipulator.ts +161 -98
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
*
|
|
11
11
|
* This module:
|
|
12
12
|
* 1. Indexes every BBL profile JSON by its `name` field.
|
|
13
|
-
* 2. Recursively walks `inherits`, deep-merging parent into child
|
|
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.
|
|
14
16
|
* 3. Derives `nozzle_volume_type` from `default_nozzle_volume_type[0]`
|
|
15
17
|
* (the GUI does this implicitly; the CLI doesn't).
|
|
16
|
-
* 4.
|
|
17
|
-
*
|
|
18
|
+
* 4. Validates the model in `BBL/cli_config.json` and merges its CLI-specific
|
|
19
|
+
* machine_limits where supplied for safe accelerations / jerks.
|
|
18
20
|
* 5. Writes the flattened JSON to a temp file the caller passes to
|
|
19
21
|
* BambuStudio CLI.
|
|
20
22
|
*
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
* fail loud rather than producing dangerous gcode for hardware we don't own.
|
|
23
25
|
*/
|
|
24
26
|
import fs from "node:fs/promises";
|
|
27
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
25
28
|
import path from "node:path";
|
|
26
29
|
import crypto from "node:crypto";
|
|
27
30
|
/* -------------------------------------------------------------------------- */
|
|
@@ -92,31 +95,66 @@ async function buildNameIndex(profilesRoot, vendor) {
|
|
|
92
95
|
* - Unknown name (broken `inherits` reference).
|
|
93
96
|
* - Cycles (A -> B -> A).
|
|
94
97
|
*/
|
|
95
|
-
function flattenByName(leafName, index) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
function flattenByName(leafName, index, visiting = new Set()) {
|
|
99
|
+
if (visiting.has(leafName)) {
|
|
100
|
+
throw new Error(`Profile inheritance cycle detected (inherits/include): ${[...visiting, leafName].join(" -> ")}`);
|
|
101
|
+
}
|
|
102
|
+
const entry = index.get(leafName);
|
|
103
|
+
if (!entry) {
|
|
104
|
+
throw new Error(`Profile "${leafName}" not found in index. The profile tree is incomplete or the name is misspelled.`);
|
|
105
|
+
}
|
|
106
|
+
visiting.add(leafName);
|
|
107
|
+
try {
|
|
108
|
+
return flattenData(entry.data, index, visiting);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
visiting.delete(leafName);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function flattenData(data, index, visiting = new Set()) {
|
|
115
|
+
const parent = data["inherits"];
|
|
116
|
+
const merged = typeof parent === "string" && parent.length > 0
|
|
117
|
+
? flattenByName(parent, index, visiting)
|
|
118
|
+
: {};
|
|
119
|
+
// Includes may themselves inherit or include templates. Each level wins
|
|
120
|
+
// over its parent, then the including profile's own settings win last.
|
|
121
|
+
applyIncludes(merged, data, index, visiting);
|
|
122
|
+
Object.assign(merged, data);
|
|
123
|
+
delete merged["include"];
|
|
124
|
+
return merged;
|
|
125
|
+
}
|
|
126
|
+
/** Keys of an include template that describe the template, not the config. */
|
|
127
|
+
const INCLUDE_METADATA_KEYS = new Set(["name", "type", "from", "instantiation", "inherits", "include", "setting_id"]);
|
|
128
|
+
/**
|
|
129
|
+
* Recent BBL profiles (e.g. "Bambu Lab P2S 0.4 nozzle") no longer carry
|
|
130
|
+
* their G-code inline: `machine_start_gcode`, `machine_end_gcode`,
|
|
131
|
+
* `change_filament_gcode`, ... live in separate "... template <key>"
|
|
132
|
+
* profiles listed under `include`. Skipping them silently falls back to
|
|
133
|
+
* the generic G-code inherited from `fdm_machine_common` & co, which is
|
|
134
|
+
* wrong for the printer (no AMS filament load, other printer's moves).
|
|
135
|
+
*/
|
|
136
|
+
function applyIncludes(target, profile, index, visiting) {
|
|
137
|
+
const raw = profile["include"];
|
|
138
|
+
if (raw === undefined || raw === null)
|
|
139
|
+
return;
|
|
140
|
+
if (typeof raw !== "string" && !Array.isArray(raw)) {
|
|
141
|
+
throw new Error(`Profile "${String(profile["name"])}" has an invalid include; expected a name or array of names.`);
|
|
142
|
+
}
|
|
143
|
+
const names = typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : [];
|
|
144
|
+
for (const name of names) {
|
|
145
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
146
|
+
throw new Error(`Profile "${String(profile["name"])}" has an invalid include reference; every entry must be a nonempty name.`);
|
|
102
147
|
}
|
|
103
|
-
|
|
104
|
-
const entry = index.get(cursor);
|
|
148
|
+
const entry = index.get(name);
|
|
105
149
|
if (!entry) {
|
|
106
|
-
throw new Error(`Profile "${
|
|
107
|
-
`
|
|
108
|
-
|
|
150
|
+
throw new Error(`Profile "${String(profile["name"])}" includes "${name}", which is not in the index. ` +
|
|
151
|
+
`Refusing to fall back to inherited defaults (wrong G-code for this printer).`);
|
|
152
|
+
}
|
|
153
|
+
for (const [key, value] of Object.entries(flattenByName(name, index, visiting))) {
|
|
154
|
+
if (!INCLUDE_METADATA_KEYS.has(key))
|
|
155
|
+
target[key] = value;
|
|
109
156
|
}
|
|
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
157
|
}
|
|
119
|
-
return merged;
|
|
120
158
|
}
|
|
121
159
|
/* -------------------------------------------------------------------------- */
|
|
122
160
|
/* CLI post-processing */
|
|
@@ -190,61 +228,86 @@ function inferExtruderCount(flat) {
|
|
|
190
228
|
return 1;
|
|
191
229
|
}
|
|
192
230
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
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.
|
|
231
|
+
* Validate the selected model's CLI config before making a bundled profile
|
|
232
|
+
* into a User profile. A1-family models require machine_limits overrides;
|
|
233
|
+
* other official models (including P1S and H2D) intentionally declare only
|
|
234
|
+
* downward_check. Missing config cannot tell us whether limits are required.
|
|
235
|
+
* Returns whether limits were applied, not whether validation succeeded.
|
|
203
236
|
*/
|
|
204
237
|
async function applyCliOverlay(flat, profilesRoot, vendor) {
|
|
205
238
|
const cliConfigPath = path.join(profilesRoot, vendor, "cli_config.json");
|
|
239
|
+
const fail = (reason) => {
|
|
240
|
+
throw new Error(`Cannot prepare machine profile "${String(flat["name"])}" ` +
|
|
241
|
+
`(model "${String(flat["printer_model"] ?? stripNozzleSuffix(String(flat["name"])))}"): ` +
|
|
242
|
+
`${cliConfigPath}: ${reason}. Check your BambuStudio installation or BAMBU_PROFILES_ROOT.`);
|
|
243
|
+
};
|
|
206
244
|
let raw;
|
|
207
245
|
try {
|
|
208
246
|
raw = await fs.readFile(cliConfigPath, "utf8");
|
|
209
247
|
}
|
|
210
248
|
catch {
|
|
211
|
-
return
|
|
249
|
+
return fail("required CLI configuration is missing or unreadable");
|
|
212
250
|
}
|
|
213
251
|
let cliConfig;
|
|
214
252
|
try {
|
|
215
253
|
cliConfig = JSON.parse(raw);
|
|
216
254
|
}
|
|
217
255
|
catch {
|
|
218
|
-
return
|
|
256
|
+
return fail("invalid JSON in required CLI configuration");
|
|
219
257
|
}
|
|
258
|
+
if (!isRecord(cliConfig))
|
|
259
|
+
return fail("expected a CLI configuration object");
|
|
220
260
|
const printerSection = cliConfig["printer"];
|
|
221
|
-
if (!printerSection
|
|
222
|
-
return
|
|
261
|
+
if (!isRecord(printerSection))
|
|
262
|
+
return fail("expected a printer configuration object");
|
|
223
263
|
// Match key: cli_config.json keys are bare printer names like
|
|
224
|
-
// "Bambu Lab H2D" / "Bambu Lab A1".
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
264
|
+
// "Bambu Lab H2D" / "Bambu Lab A1". If no model is provided, try preset
|
|
265
|
+
// identity with the nozzle suffix stripped, then the name as-is.
|
|
266
|
+
// An explicit model is authoritative; do not silently use another model's
|
|
267
|
+
// limits because a stale preset name happens to match.
|
|
268
|
+
const candidates = typeof flat["printer_model"] === "string" && flat["printer_model"].trim()
|
|
269
|
+
? [flat["printer_model"]]
|
|
270
|
+
: [
|
|
271
|
+
typeof flat["printer_settings_id"] === "string"
|
|
272
|
+
? stripNozzleSuffix(flat["printer_settings_id"])
|
|
273
|
+
: undefined,
|
|
274
|
+
typeof flat["name"] === "string" ? stripNozzleSuffix(flat["name"]) : undefined,
|
|
275
|
+
flat["name"],
|
|
276
|
+
].filter((v) => typeof v === "string" && v.length > 0);
|
|
277
|
+
const model = candidates.find(key => Object.prototype.hasOwnProperty.call(printerSection, key));
|
|
278
|
+
if (!model)
|
|
279
|
+
return fail(`selected model is absent (looked for ${candidates.join(", ")})`);
|
|
280
|
+
const block = printerSection[model];
|
|
281
|
+
if (!isRecord(block))
|
|
282
|
+
return fail(`printer.${model} must be an object`);
|
|
283
|
+
const downward = block["downward_check"];
|
|
284
|
+
if (Object.prototype.hasOwnProperty.call(block, "downward_check") && (!isRecord(downward) || Object.keys(downward).length === 0 ||
|
|
285
|
+
Object.entries(downward).some(([name, values]) => !name.trim() || !Array.isArray(values) ||
|
|
286
|
+
values.some(value => typeof value !== "string" || !value.trim())))) {
|
|
287
|
+
return fail(`printer.${model}.downward_check must map profile names to arrays of names`);
|
|
288
|
+
}
|
|
289
|
+
if (!Object.prototype.hasOwnProperty.call(block, "machine_limits")) {
|
|
290
|
+
if (!isRecord(downward))
|
|
291
|
+
return fail(`printer.${model} has neither machine_limits nor downward_check`);
|
|
292
|
+
return false;
|
|
246
293
|
}
|
|
247
|
-
|
|
294
|
+
const limits = block["machine_limits"];
|
|
295
|
+
if (!isRecord(limits) || Object.keys(limits).length === 0) {
|
|
296
|
+
return fail(`printer.${model}.machine_limits must be a nonempty object`);
|
|
297
|
+
}
|
|
298
|
+
const limitKey = /^cli_safe_(?:acceleration_(?:e|extruding|retracting|travel|x|y|z)|(?:jerk|speed)_(?:e|x|y|z))$/;
|
|
299
|
+
for (const [key, value] of Object.entries(limits)) {
|
|
300
|
+
if (!limitKey.test(key) || typeof value !== "string" ||
|
|
301
|
+
value.split(",").some(part => !part.trim() || !Number.isFinite(Number(part)) || Number(part) < 0)) {
|
|
302
|
+
return fail(`printer.${model}.machine_limits contains an invalid limit "${key}"`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// CLI safety values override even explicit values in the selected leaf.
|
|
306
|
+
Object.assign(flat, limits);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
function isRecord(value) {
|
|
310
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
248
311
|
}
|
|
249
312
|
/**
|
|
250
313
|
* Ensure the chosen machine is in the process/filament's
|
|
@@ -373,13 +436,32 @@ export async function flattenForCli(opts) {
|
|
|
373
436
|
}
|
|
374
437
|
const index = await buildNameIndex(opts.profilesRoot, vendor);
|
|
375
438
|
// Flatten each leaf.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
439
|
+
if (opts.sourceProfiles?.filaments && opts.sourceProfiles.filaments.length !== opts.filamentLeaves.length) {
|
|
440
|
+
throw new Error("Every filament slot must have a source profile.");
|
|
441
|
+
}
|
|
442
|
+
const machineFlat = opts.sourceProfiles?.machine
|
|
443
|
+
? flattenData(opts.sourceProfiles.machine, index)
|
|
444
|
+
: flattenByName(opts.machineLeaf, index);
|
|
445
|
+
const processFlat = opts.sourceProfiles?.process
|
|
446
|
+
? flattenData(opts.sourceProfiles.process, index)
|
|
447
|
+
: flattenByName(opts.processLeaf, index);
|
|
448
|
+
const filamentFlats = opts.filamentLeaves.map((n, i) => {
|
|
449
|
+
const source = opts.sourceProfiles?.filaments?.[i];
|
|
450
|
+
return source ? flattenData(source, index) : flattenByName(n, index);
|
|
451
|
+
});
|
|
379
452
|
// CLI-specific post-processing on machine profile only.
|
|
380
453
|
deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
|
|
381
454
|
applyMachineModelBedMetadata(machineFlat, index);
|
|
382
|
-
|
|
455
|
+
// A standalone custom machine can accompany process/filament profiles that
|
|
456
|
+
// need BBL resolution; do not impose bundled model config on that machine.
|
|
457
|
+
const sourceMachine = opts.sourceProfiles?.machine;
|
|
458
|
+
const standaloneMachine = sourceMachine && String(sourceMachine.from).toLowerCase() !== "system" &&
|
|
459
|
+
!(typeof sourceMachine.inherits === "string" && sourceMachine.inherits.length > 0) &&
|
|
460
|
+
(sourceMachine.include === undefined || sourceMachine.include === null);
|
|
461
|
+
const cliConfigValidated = !standaloneMachine;
|
|
462
|
+
const cliOverlayApplied = cliConfigValidated
|
|
463
|
+
? await applyCliOverlay(machineFlat, opts.profilesRoot, vendor)
|
|
464
|
+
: false;
|
|
383
465
|
// Normalize each flattened profile for CLI consumption.
|
|
384
466
|
normalizeForCli(machineFlat, "machine", opts.machineLeaf);
|
|
385
467
|
normalizeForCli(processFlat, "process", opts.processLeaf);
|
|
@@ -392,8 +474,8 @@ export async function flattenForCli(opts) {
|
|
|
392
474
|
// does this implicitly when saving a project.
|
|
393
475
|
ensureMachineInCompatList(processFlat, opts.machineLeaf);
|
|
394
476
|
filamentFlats.forEach((f) => ensureMachineInCompatList(f, opts.machineLeaf));
|
|
395
|
-
//
|
|
396
|
-
//
|
|
477
|
+
// Hash the resolved content so concurrent jobs using the same preset with
|
|
478
|
+
// different overrides never overwrite each other's input files.
|
|
397
479
|
await fs.mkdir(opts.tempDir, { recursive: true });
|
|
398
480
|
const machinePath = await writeTemp(opts.tempDir, "machine", opts.machineLeaf, machineFlat);
|
|
399
481
|
const processPath = await writeTemp(opts.tempDir, "process", opts.processLeaf, processFlat);
|
|
@@ -410,16 +492,18 @@ export async function flattenForCli(opts) {
|
|
|
410
492
|
machineLeafName: opts.machineLeaf,
|
|
411
493
|
processLeafName: opts.processLeaf,
|
|
412
494
|
filamentLeafNames: opts.filamentLeaves,
|
|
495
|
+
cliConfigValidated,
|
|
413
496
|
cliOverlayApplied,
|
|
414
497
|
},
|
|
415
498
|
};
|
|
416
499
|
}
|
|
417
500
|
async function writeTemp(tempDir, kind, leafName, data) {
|
|
418
|
-
const
|
|
501
|
+
const serialized = JSON.stringify(data, null, 2);
|
|
502
|
+
const hash = crypto.createHash("sha256").update(leafName).update("\0").update(serialized).digest("hex").slice(0, 16);
|
|
419
503
|
const safe = leafName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
420
504
|
const filename = `flat-${kind}-${safe}-${hash}.json`;
|
|
421
505
|
const out = path.join(tempDir, filename);
|
|
422
|
-
await fs.writeFile(out,
|
|
506
|
+
await fs.writeFile(out, serialized, "utf8");
|
|
423
507
|
return out;
|
|
424
508
|
}
|
|
425
509
|
/* -------------------------------------------------------------------------- */
|
|
@@ -427,7 +511,7 @@ async function writeTemp(tempDir, kind, leafName, data) {
|
|
|
427
511
|
/* -------------------------------------------------------------------------- */
|
|
428
512
|
/**
|
|
429
513
|
* Given the SLICER_PATH (path to BambuStudio executable), walk up to the
|
|
430
|
-
*
|
|
514
|
+
* profile directory for that installation (macOS, Windows, or Linux prefix).
|
|
431
515
|
*
|
|
432
516
|
* Override via BAMBU_PROFILES_ROOT env.
|
|
433
517
|
*/
|
|
@@ -436,10 +520,33 @@ export function detectProfilesRoot(slicerPath) {
|
|
|
436
520
|
return process.env["BAMBU_PROFILES_ROOT"];
|
|
437
521
|
}
|
|
438
522
|
if (slicerPath) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
523
|
+
let executable = slicerPath;
|
|
524
|
+
if (!slicerPath.includes('/') && !slicerPath.includes('\\')) {
|
|
525
|
+
const located = (process.env.PATH ?? '').split(path.delimiter)
|
|
526
|
+
.map(dir => path.join(dir, slicerPath))
|
|
527
|
+
.find(candidate => existsSync(candidate));
|
|
528
|
+
if (located)
|
|
529
|
+
executable = located;
|
|
530
|
+
}
|
|
531
|
+
// Package-manager launchers may be symlinks into the installation prefix.
|
|
532
|
+
try {
|
|
533
|
+
executable = realpathSync(executable);
|
|
534
|
+
}
|
|
535
|
+
catch { /* Probe the supplied path below. */ }
|
|
536
|
+
const bin = path.dirname(executable);
|
|
537
|
+
const candidates = path.basename(bin) === 'MacOS'
|
|
538
|
+
? [path.resolve(bin, '..', 'Resources', 'profiles')]
|
|
539
|
+
: [
|
|
540
|
+
path.join(bin, 'resources', 'profiles'),
|
|
541
|
+
path.join(bin, 'Resources', 'profiles'),
|
|
542
|
+
path.resolve(bin, '..', 'share', 'BambuStudio', 'profiles'),
|
|
543
|
+
path.resolve(bin, '..', 'share', 'bambu-studio', 'profiles'),
|
|
544
|
+
path.resolve(bin, '..', 'share', 'BambuStudio', 'resources', 'profiles'),
|
|
545
|
+
];
|
|
546
|
+
const found = candidates.find(root => existsSync(path.join(root, 'BBL', 'machine')));
|
|
547
|
+
// Do not select another installation if this executable has no profile tree.
|
|
548
|
+
// flattenForCli reports the missing tree and asks for BAMBU_PROFILES_ROOT.
|
|
549
|
+
return found ?? candidates[0];
|
|
443
550
|
}
|
|
444
551
|
// Default macOS install.
|
|
445
552
|
return "/Applications/BambuStudio.app/Contents/Resources/profiles";
|
|
@@ -63,25 +63,11 @@ export declare class STLManipulator extends EventEmitter {
|
|
|
63
63
|
private sanitizeProcessForOrca;
|
|
64
64
|
private writeTempJson;
|
|
65
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
|
-
*/
|
|
66
|
+
/** Resolve BBL dependencies before invoking the CLI; failures stop the slice. */
|
|
81
67
|
private maybeFlattenBundle;
|
|
68
|
+
/** --load-filaments is positional; a single override must cover every project slot. */
|
|
69
|
+
private expandProjectFilaments;
|
|
82
70
|
private resolveBambuStudioBedType;
|
|
83
|
-
/** Read a profile JSON's top-level `name` field, or null if unreadable. */
|
|
84
|
-
private readLeafName;
|
|
85
71
|
/**
|
|
86
72
|
* Load STL file and return geometry and bounding box
|
|
87
73
|
*/
|