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