bambu-printer-mcp 1.0.5 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +537 -38
- package/dist/3mf_parser.d.ts +15 -1
- package/dist/3mf_parser.js +258 -1
- package/dist/ams-mapping.d.ts +3 -0
- package/dist/ams-mapping.js +45 -0
- package/dist/bambu-network-bridge.d.ts +63 -0
- package/dist/bambu-network-bridge.js +393 -0
- package/dist/index.js +2084 -95
- package/dist/printers/bambu.d.ts +133 -0
- package/dist/printers/bambu.js +1004 -54
- package/dist/slicer/profile-flatten.d.ts +75 -0
- package/dist/slicer/profile-flatten.js +446 -0
- package/dist/stl/stl-manipulator.d.ts +33 -1
- package/dist/stl/stl-manipulator.js +370 -33
- package/dist/types.d.ts +35 -0
- package/package.json +8 -3
- package/patches/bambu-node+3.22.21.patch +16 -0
- package/src/3mf_parser.ts +301 -3
- package/src/ams-mapping.ts +47 -0
- package/src/bambu-network-bridge.ts +526 -0
- package/src/index.ts +2648 -108
- package/src/printers/bambu.ts +1215 -57
- package/src/slicer/profile-flatten.ts +562 -0
- package/src/stl/stl-manipulator.ts +475 -36
- package/src/types.ts +41 -0
|
@@ -3,14 +3,26 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
|
3
3
|
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js';
|
|
4
4
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
+
import os from 'os';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import { promisify } from 'util';
|
|
8
9
|
import { EventEmitter } from 'events';
|
|
9
10
|
import * as crypto from 'crypto';
|
|
10
11
|
import { execFile } from 'child_process';
|
|
12
|
+
import { flattenForCli, detectProfilesRoot } from '../slicer/profile-flatten.js';
|
|
11
13
|
|
|
12
14
|
const readFileAsync = promisify(fs.readFile);
|
|
13
15
|
const writeFileAsync = promisify(fs.writeFile);
|
|
16
|
+
const BAMBU_PROFILE_ROOTS = [
|
|
17
|
+
'/Applications/BambuStudio.app/Contents/Resources/profiles/BBL',
|
|
18
|
+
'/Applications/OrcaSlicer.app/Contents/Resources/profiles/BBL',
|
|
19
|
+
];
|
|
20
|
+
const BAMBU_CLI_BED_TYPES: Record<string, string> = {
|
|
21
|
+
textured_plate: 'Textured PEI Plate',
|
|
22
|
+
cool_plate: 'Cool Plate',
|
|
23
|
+
engineering_plate: 'Engineering Plate',
|
|
24
|
+
hot_plate: 'High Temp Plate',
|
|
25
|
+
};
|
|
14
26
|
|
|
15
27
|
// Define types for progress tracking
|
|
16
28
|
export type ProgressCallback = (progress: number, message?: string) => void;
|
|
@@ -40,7 +52,106 @@ export type TransformationParams = {
|
|
|
40
52
|
selectionBounds?: THREE.Box3;
|
|
41
53
|
};
|
|
42
54
|
|
|
43
|
-
|
|
55
|
+
export const SLICER_TYPES = [
|
|
56
|
+
'bambustudio',
|
|
57
|
+
'orcaslicer',
|
|
58
|
+
'orcaslicer-bambulab',
|
|
59
|
+
'prusaslicer',
|
|
60
|
+
'cura',
|
|
61
|
+
'slic3r',
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
export type SlicerType = typeof SLICER_TYPES[number];
|
|
65
|
+
|
|
66
|
+
const SLICER_TYPE_ALIASES: Record<string, SlicerType> = {
|
|
67
|
+
bambustudio: 'bambustudio',
|
|
68
|
+
'bambu-studio': 'bambustudio',
|
|
69
|
+
bblstudio: 'bambustudio',
|
|
70
|
+
orcaslicer: 'orcaslicer',
|
|
71
|
+
'orca-slicer': 'orcaslicer',
|
|
72
|
+
orca: 'orcaslicer',
|
|
73
|
+
'orcaslicer-bambulab': 'orcaslicer-bambulab',
|
|
74
|
+
'orca-slicer-bambulab': 'orcaslicer-bambulab',
|
|
75
|
+
'orca-bambulab': 'orcaslicer-bambulab',
|
|
76
|
+
'fulu-orca': 'orcaslicer-bambulab',
|
|
77
|
+
'fulu-orcaslicer': 'orcaslicer-bambulab',
|
|
78
|
+
'fulu-orca-slicer': 'orcaslicer-bambulab',
|
|
79
|
+
'orca-studio': 'orcaslicer-bambulab',
|
|
80
|
+
orcastudio: 'orcaslicer-bambulab',
|
|
81
|
+
'orcaslicer-bmcu': 'orcaslicer-bambulab',
|
|
82
|
+
'orca-bmcu': 'orcaslicer-bambulab',
|
|
83
|
+
prusaslicer: 'prusaslicer',
|
|
84
|
+
'prusa-slicer': 'prusaslicer',
|
|
85
|
+
cura: 'cura',
|
|
86
|
+
curaengine: 'cura',
|
|
87
|
+
slic3r: 'slic3r',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function normalizeSlicerType(rawSlicerType: string): SlicerType {
|
|
91
|
+
const key = rawSlicerType.trim().toLowerCase().replace(/_/g, '-');
|
|
92
|
+
const normalized = SLICER_TYPE_ALIASES[key];
|
|
93
|
+
|
|
94
|
+
if (!normalized) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Unsupported slicer_type: "${rawSlicerType}". Valid values: ${SLICER_TYPES.join(', ')}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return normalized;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isBambuCompatibleSlicer(slicerType: SlicerType): boolean {
|
|
104
|
+
return (
|
|
105
|
+
slicerType === 'bambustudio' ||
|
|
106
|
+
slicerType === 'orcaslicer' ||
|
|
107
|
+
slicerType === 'orcaslicer-bambulab'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isPathLikeExecutable(slicerPath: string): boolean {
|
|
112
|
+
return (
|
|
113
|
+
path.isAbsolute(slicerPath) ||
|
|
114
|
+
slicerPath.startsWith("./") ||
|
|
115
|
+
slicerPath.startsWith("../") ||
|
|
116
|
+
slicerPath.includes("/") ||
|
|
117
|
+
slicerPath.includes("\\") ||
|
|
118
|
+
/^[A-Za-z]:/.test(slicerPath)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function configuredBambuProfileDirs(): string[] {
|
|
123
|
+
const override = process.env.BAMBU_SLICER_PROFILE_DIRS;
|
|
124
|
+
if (override !== undefined) {
|
|
125
|
+
return override.split(path.delimiter).filter(Boolean);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const home = os.homedir();
|
|
129
|
+
const macAppSupport = path.join(home, 'Library', 'Application Support');
|
|
130
|
+
const linuxConfig = path.join(home, '.config');
|
|
131
|
+
const appData = process.env.APPDATA;
|
|
132
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
133
|
+
const appNames = [
|
|
134
|
+
'BambuStudio',
|
|
135
|
+
'Bambu Studio',
|
|
136
|
+
'OrcaSlicer',
|
|
137
|
+
'Orca Slicer',
|
|
138
|
+
'OrcaStudio',
|
|
139
|
+
'Orca Studio',
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const roots = [
|
|
143
|
+
macAppSupport,
|
|
144
|
+
linuxConfig,
|
|
145
|
+
...(appData ? [appData] : []),
|
|
146
|
+
...(localAppData ? [localAppData] : []),
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
return roots.flatMap((root) =>
|
|
150
|
+
appNames.map((appName) => path.join(root, appName, 'system', 'BBL'))
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Bambu-compatible slicer CLI flags exposed via the slice_stl tool
|
|
44
155
|
export interface BambuSliceOptions {
|
|
45
156
|
uptodate?: boolean; // --uptodate: update 3MF configs to latest presets
|
|
46
157
|
repetitions?: number; // --repetitions N: print N copies
|
|
@@ -51,6 +162,7 @@ export interface BambuSliceOptions {
|
|
|
51
162
|
skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
|
|
52
163
|
loadFilaments?: string; // --load-filaments "f1.json;f2.json": filament profiles
|
|
53
164
|
loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
|
|
165
|
+
bedType?: string; // Bambu bed type, e.g. textured_plate or cool_plate
|
|
54
166
|
enableTimelapse?: boolean; // --enable-timelapse: timelapse-aware slicing
|
|
55
167
|
allowMixTemp?: boolean; // --allow-mix-temp: allow mixed-temp filaments
|
|
56
168
|
scale?: number; // --scale factor: uniform scale
|
|
@@ -82,6 +194,340 @@ export class STLManipulator extends EventEmitter {
|
|
|
82
194
|
return crypto.randomUUID();
|
|
83
195
|
}
|
|
84
196
|
|
|
197
|
+
private getAvailableProfileRoots(): string[] {
|
|
198
|
+
return [...BAMBU_PROFILE_ROOTS, ...configuredBambuProfileDirs()].filter((root) => fs.existsSync(root));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private findProfileFile(
|
|
202
|
+
category: 'machine' | 'process' | 'filament',
|
|
203
|
+
profileName?: string
|
|
204
|
+
): string | undefined {
|
|
205
|
+
if (!profileName) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const root of this.getAvailableProfileRoots()) {
|
|
210
|
+
const candidate = path.join(root, category, `${profileName}.json`);
|
|
211
|
+
if (fs.existsSync(candidate)) {
|
|
212
|
+
return candidate;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private buildFilamentIdIndex(): Map<string, string> {
|
|
220
|
+
const index = new Map<string, string>();
|
|
221
|
+
|
|
222
|
+
for (const root of this.getAvailableProfileRoots()) {
|
|
223
|
+
const filamentDir = path.join(root, 'filament');
|
|
224
|
+
if (!fs.existsSync(filamentDir)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const entry of fs.readdirSync(filamentDir)) {
|
|
229
|
+
if (!entry.endsWith('.json')) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const filePath = path.join(filamentDir, entry);
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
236
|
+
const filamentId = typeof parsed?.filament_id === 'string' ? parsed.filament_id.trim() : '';
|
|
237
|
+
if (filamentId && !index.has(filamentId)) {
|
|
238
|
+
index.set(filamentId, filePath);
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// Ignore malformed JSON.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return index;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private readJsonFile(filePath: string): any {
|
|
250
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private stripAbsoluteExtruderResets(value: unknown): unknown {
|
|
254
|
+
if (typeof value === 'string') {
|
|
255
|
+
return value
|
|
256
|
+
.split(/\r?\n/)
|
|
257
|
+
.filter((line) => line.trim().toUpperCase() !== 'G92 E0')
|
|
258
|
+
.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
return value.filter((line) => String(line).trim().toUpperCase() !== 'G92 E0');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private sanitizeProcessForOrca(processConfig: any, printerPreset?: string): any {
|
|
269
|
+
const sanitized = { ...processConfig };
|
|
270
|
+
|
|
271
|
+
sanitized.type = 'process';
|
|
272
|
+
if (!sanitized.from || sanitized.from === 'project') {
|
|
273
|
+
sanitized.from = 'User';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (printerPreset) {
|
|
277
|
+
sanitized.compatible_printers = [printerPreset];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (
|
|
281
|
+
sanitized.prime_tower_brim_width !== undefined &&
|
|
282
|
+
Number(sanitized.prime_tower_brim_width) < 0
|
|
283
|
+
) {
|
|
284
|
+
sanitized.prime_tower_brim_width = '0';
|
|
285
|
+
} else if (sanitized.prime_tower_brim_width !== undefined) {
|
|
286
|
+
sanitized.prime_tower_brim_width = String(sanitized.prime_tower_brim_width);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
sanitized.use_relative_e_distances = '0';
|
|
290
|
+
sanitized.before_layer_change_gcode = this.stripAbsoluteExtruderResets(
|
|
291
|
+
sanitized.before_layer_change_gcode
|
|
292
|
+
);
|
|
293
|
+
sanitized.layer_gcode = this.stripAbsoluteExtruderResets(sanitized.layer_gcode);
|
|
294
|
+
sanitized.layer_change_gcode = this.stripAbsoluteExtruderResets(
|
|
295
|
+
sanitized.layer_change_gcode
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return sanitized;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private writeTempJson(outputBase: string, suffix: string, value: any): string {
|
|
302
|
+
const outPath = path.join(this.tempDir, `${outputBase}_${suffix}.json`);
|
|
303
|
+
fs.writeFileSync(outPath, JSON.stringify(value, null, 2));
|
|
304
|
+
return outPath;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private resolveBambuLikeSettingsBundle(
|
|
308
|
+
outputBase: string,
|
|
309
|
+
slicerType: 'orcaslicer' | 'orcaslicer-bambulab' | 'bambustudio',
|
|
310
|
+
slicerProfile?: string,
|
|
311
|
+
printerPreset?: string,
|
|
312
|
+
bambuOptions?: BambuSliceOptions
|
|
313
|
+
): { settingsArg?: string; filamentPaths: string[] } {
|
|
314
|
+
const machinePath = this.findProfileFile('machine', printerPreset);
|
|
315
|
+
const machineConfig = machinePath ? this.readJsonFile(machinePath) : null;
|
|
316
|
+
const hasSlicerProfile = !!slicerProfile && fs.existsSync(slicerProfile);
|
|
317
|
+
const filamentPaths: string[] = [];
|
|
318
|
+
|
|
319
|
+
let processPath: string | undefined;
|
|
320
|
+
let parsedProfile: any = null;
|
|
321
|
+
|
|
322
|
+
if (hasSlicerProfile) {
|
|
323
|
+
try {
|
|
324
|
+
parsedProfile = this.readJsonFile(slicerProfile!);
|
|
325
|
+
} catch {
|
|
326
|
+
parsedProfile = null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (parsedProfile && typeof parsedProfile === 'object') {
|
|
331
|
+
const inheritedProcessName =
|
|
332
|
+
(typeof parsedProfile.inherits === 'string' && parsedProfile.inherits) ||
|
|
333
|
+
(typeof parsedProfile.print_settings_id === 'string' &&
|
|
334
|
+
parsedProfile.print_settings_id !== parsedProfile.name
|
|
335
|
+
? parsedProfile.print_settings_id
|
|
336
|
+
: undefined) ||
|
|
337
|
+
(typeof parsedProfile.default_print_profile === 'string'
|
|
338
|
+
? parsedProfile.default_print_profile
|
|
339
|
+
: undefined) ||
|
|
340
|
+
(typeof machineConfig?.default_print_profile === 'string'
|
|
341
|
+
? machineConfig.default_print_profile
|
|
342
|
+
: undefined);
|
|
343
|
+
|
|
344
|
+
const inheritedProcessPath = this.findProfileFile('process', inheritedProcessName);
|
|
345
|
+
const inheritedProcess =
|
|
346
|
+
inheritedProcessPath && fs.existsSync(inheritedProcessPath)
|
|
347
|
+
? this.readJsonFile(inheritedProcessPath)
|
|
348
|
+
: {};
|
|
349
|
+
|
|
350
|
+
const mergedProcess = {
|
|
351
|
+
...inheritedProcess,
|
|
352
|
+
...parsedProfile,
|
|
353
|
+
type: 'process',
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
processPath = this.writeTempJson(
|
|
357
|
+
outputBase,
|
|
358
|
+
slicerType === 'orcaslicer' ? 'process_orca' : 'process',
|
|
359
|
+
slicerType === 'orcaslicer'
|
|
360
|
+
? this.sanitizeProcessForOrca(mergedProcess, printerPreset)
|
|
361
|
+
: mergedProcess
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const defaultFilamentProfiles = Array.isArray(parsedProfile.default_filament_profile)
|
|
365
|
+
? parsedProfile.default_filament_profile
|
|
366
|
+
: [];
|
|
367
|
+
for (const profileName of defaultFilamentProfiles) {
|
|
368
|
+
const filamentPath = this.findProfileFile('filament', String(profileName));
|
|
369
|
+
if (filamentPath) {
|
|
370
|
+
filamentPaths.push(filamentPath);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
|
|
375
|
+
const filamentIdIndex = this.buildFilamentIdIndex();
|
|
376
|
+
for (const filamentId of parsedProfile.filament_ids) {
|
|
377
|
+
const filamentPath = filamentIdIndex.get(String(filamentId));
|
|
378
|
+
if (filamentPath) {
|
|
379
|
+
filamentPaths.push(filamentPath);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} else if (hasSlicerProfile) {
|
|
384
|
+
processPath = slicerProfile;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!processPath) {
|
|
388
|
+
const defaultProcessName =
|
|
389
|
+
typeof machineConfig?.default_print_profile === 'string'
|
|
390
|
+
? machineConfig.default_print_profile
|
|
391
|
+
: undefined;
|
|
392
|
+
const defaultProcessPath = this.findProfileFile('process', defaultProcessName);
|
|
393
|
+
if (defaultProcessPath) {
|
|
394
|
+
processPath =
|
|
395
|
+
slicerType === 'orcaslicer'
|
|
396
|
+
? this.writeTempJson(
|
|
397
|
+
outputBase,
|
|
398
|
+
'process_default_orca',
|
|
399
|
+
this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset)
|
|
400
|
+
)
|
|
401
|
+
: defaultProcessPath;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (
|
|
406
|
+
!bambuOptions?.loadFilaments &&
|
|
407
|
+
filamentPaths.length === 0 &&
|
|
408
|
+
Array.isArray(machineConfig?.default_filament_profile)
|
|
409
|
+
) {
|
|
410
|
+
for (const profileName of machineConfig.default_filament_profile) {
|
|
411
|
+
const filamentPath = this.findProfileFile('filament', String(profileName));
|
|
412
|
+
if (filamentPath) {
|
|
413
|
+
filamentPaths.push(filamentPath);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (bambuOptions?.loadFilaments) {
|
|
419
|
+
filamentPaths.length = 0;
|
|
420
|
+
for (const filamentPath of bambuOptions.loadFilaments.split(';')) {
|
|
421
|
+
const trimmed = filamentPath.trim();
|
|
422
|
+
if (trimmed) {
|
|
423
|
+
filamentPaths.push(trimmed);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const settingsParts = [machinePath, processPath].filter(
|
|
429
|
+
(entry): entry is string => Boolean(entry)
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
settingsArg: settingsParts.length > 0 ? settingsParts.join(';') : undefined,
|
|
434
|
+
filamentPaths: Array.from(new Set(filamentPaths)),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Optionally rewrite a Bambu-like settings bundle so the paths point at
|
|
440
|
+
* fully-flattened temp configs instead of the BBL-shipped leaf JSONs.
|
|
441
|
+
*
|
|
442
|
+
* BambuStudio's CLI does not resolve the `inherits` chain when loading
|
|
443
|
+
* profiles via --load-settings / --load-filaments, which causes a
|
|
444
|
+
* cluster of upstream bugs (see https://github.com/bambulab/BambuStudio/issues/9636
|
|
445
|
+
* and #9968). Our flattener (src/slicer/profile-flatten.ts) reproduces
|
|
446
|
+
* what the GUI does at slice time so the CLI accepts the configs.
|
|
447
|
+
*
|
|
448
|
+
* Opt-in via `BAMBU_CLI_FLATTEN=true`. When the env var is unset or
|
|
449
|
+
* not "true"/"1", returns the bundle unchanged so behavior is
|
|
450
|
+
* backward-compatible. When enabled, only BBL-shipped leaves get
|
|
451
|
+
* flattened; user-provided custom configs pass through untouched.
|
|
452
|
+
*/
|
|
453
|
+
private async maybeFlattenBundle(
|
|
454
|
+
bundle: { settingsArg?: string; filamentPaths: string[] },
|
|
455
|
+
bambuOptions?: BambuSliceOptions,
|
|
456
|
+
activeSlicerPath?: string
|
|
457
|
+
): Promise<{ settingsArg?: string; filamentPaths: string[] }> {
|
|
458
|
+
const flag = process.env.BAMBU_CLI_FLATTEN;
|
|
459
|
+
if (flag !== 'true' && flag !== '1') return bundle;
|
|
460
|
+
if (!bundle.settingsArg) return bundle;
|
|
461
|
+
|
|
462
|
+
// settingsArg is "machine.json;process.json".
|
|
463
|
+
const parts = bundle.settingsArg.split(';').filter(Boolean);
|
|
464
|
+
if (parts.length < 2) return bundle;
|
|
465
|
+
const [machinePath, processPath] = parts;
|
|
466
|
+
|
|
467
|
+
// Pull leaf names from each JSON's `name` field. If any is missing or
|
|
468
|
+
// looks non-BBL (e.g. a user-saved custom config), bail out and
|
|
469
|
+
// return the original bundle untouched.
|
|
470
|
+
const machineLeaf = this.readLeafName(machinePath);
|
|
471
|
+
const processLeaf = this.readLeafName(processPath);
|
|
472
|
+
const filamentLeaves = bundle.filamentPaths
|
|
473
|
+
.map((p) => this.readLeafName(p))
|
|
474
|
+
.filter((n): n is string => Boolean(n));
|
|
475
|
+
|
|
476
|
+
if (!machineLeaf || !processLeaf || filamentLeaves.length === 0) {
|
|
477
|
+
console.log(
|
|
478
|
+
'[cli-flatten] skipping: could not derive BBL leaf names from bundle paths'
|
|
479
|
+
);
|
|
480
|
+
return bundle;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const bedType = this.resolveBambuStudioBedType(bambuOptions?.bedType);
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const profilesRoot = detectProfilesRoot(activeSlicerPath || process.env.SLICER_PATH);
|
|
487
|
+
const flat = await flattenForCli({
|
|
488
|
+
machineLeaf,
|
|
489
|
+
processLeaf,
|
|
490
|
+
filamentLeaves,
|
|
491
|
+
profilesRoot,
|
|
492
|
+
tempDir: this.tempDir,
|
|
493
|
+
bedType,
|
|
494
|
+
});
|
|
495
|
+
console.log(
|
|
496
|
+
`[cli-flatten] applied for ${machineLeaf} (cliOverlay=${flat.meta.cliOverlayApplied})`
|
|
497
|
+
);
|
|
498
|
+
return {
|
|
499
|
+
settingsArg: `${flat.machinePath};${flat.processPath}`,
|
|
500
|
+
filamentPaths: flat.filamentPaths,
|
|
501
|
+
};
|
|
502
|
+
} catch (err: any) {
|
|
503
|
+
console.error(
|
|
504
|
+
`[cli-flatten] failed, falling back to unflattened bundle: ${err?.message ?? err}`
|
|
505
|
+
);
|
|
506
|
+
return bundle;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private resolveBambuStudioBedType(bedType?: string): string | undefined {
|
|
511
|
+
if (!bedType) return undefined;
|
|
512
|
+
const normalized = bedType.trim().toLowerCase();
|
|
513
|
+
if (normalized === 'supertack_plate') {
|
|
514
|
+
throw new Error(
|
|
515
|
+
'BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF or choose textured_plate, cool_plate, engineering_plate, or hot_plate.'
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return BAMBU_CLI_BED_TYPES[normalized] || bedType;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Read a profile JSON's top-level `name` field, or null if unreadable. */
|
|
522
|
+
private readLeafName(filePath: string): string | null {
|
|
523
|
+
try {
|
|
524
|
+
const data = this.readJsonFile(filePath);
|
|
525
|
+
return typeof data?.name === 'string' && data.name.length > 0 ? data.name : null;
|
|
526
|
+
} catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
85
531
|
/**
|
|
86
532
|
* Load STL file and return geometry and bounding box
|
|
87
533
|
*/
|
|
@@ -993,7 +1439,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
993
1439
|
*/
|
|
994
1440
|
async sliceSTL(
|
|
995
1441
|
stlFilePath: string,
|
|
996
|
-
slicerType:
|
|
1442
|
+
slicerType: SlicerType,
|
|
997
1443
|
slicerPath: string,
|
|
998
1444
|
slicerProfile?: string,
|
|
999
1445
|
progressCallback?: ProgressCallback,
|
|
@@ -1003,7 +1449,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1003
1449
|
const operationId = this.generateOperationId();
|
|
1004
1450
|
this.activeOperations.set(operationId, true);
|
|
1005
1451
|
|
|
1006
|
-
if (!fs.existsSync(slicerPath)) {
|
|
1452
|
+
if (isPathLikeExecutable(slicerPath) && !fs.existsSync(slicerPath)) {
|
|
1007
1453
|
throw new Error(`Slicer executable not found at: ${slicerPath}`);
|
|
1008
1454
|
}
|
|
1009
1455
|
if (slicerProfile && !fs.existsSync(slicerProfile)) {
|
|
@@ -1032,25 +1478,6 @@ export class STLManipulator extends EventEmitter {
|
|
|
1032
1478
|
args = args.filter(arg => arg !== '');
|
|
1033
1479
|
break;
|
|
1034
1480
|
|
|
1035
|
-
case 'orcaslicer': // Add OrcaSlicer case
|
|
1036
|
-
args = [
|
|
1037
|
-
// OrcaSlicer might use --load-settings like Bambu, or --load like Prusa
|
|
1038
|
-
// Assuming --load-settings based on discussion, adjust if needed.
|
|
1039
|
-
// Also assuming profile path points to the main .ini or .json config.
|
|
1040
|
-
'--load-settings', slicerProfile || '',
|
|
1041
|
-
'--output', outputFilePath, // Assuming --output works like PrusaSlicer
|
|
1042
|
-
stlFilePath
|
|
1043
|
-
];
|
|
1044
|
-
// Alternative if --output doesn't specify filename:
|
|
1045
|
-
// args = [
|
|
1046
|
-
// '--load-settings', slicerProfile || '',
|
|
1047
|
-
// '--outputdir', this.tempDir,
|
|
1048
|
-
// stlFilePath
|
|
1049
|
-
// ];
|
|
1050
|
-
// Remove empty profile arg if not provided
|
|
1051
|
-
args = args.filter(arg => arg !== '');
|
|
1052
|
-
break;
|
|
1053
|
-
|
|
1054
1481
|
case 'cura':
|
|
1055
1482
|
// CuraEngine CLI args are different, often requiring -s for settings
|
|
1056
1483
|
// Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
|
|
@@ -1067,32 +1494,41 @@ export class STLManipulator extends EventEmitter {
|
|
|
1067
1494
|
break;
|
|
1068
1495
|
|
|
1069
1496
|
case 'bambustudio':
|
|
1070
|
-
|
|
1497
|
+
case 'orcaslicer':
|
|
1498
|
+
case 'orcaslicer-bambulab':
|
|
1499
|
+
// Bambu-compatible CLI: slice and export as 3MF with embedded gcode.
|
|
1500
|
+
// FULU's OrcaSlicer-bambulab fork keeps this Bambu-style path.
|
|
1071
1501
|
// For 3MF input: slice in place and export sliced 3MF
|
|
1072
1502
|
// For STL input: slice and export as 3MF
|
|
1073
1503
|
{
|
|
1074
1504
|
const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
|
|
1075
1505
|
const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
|
|
1076
1506
|
const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
|
|
1507
|
+
const outputDir = path.dirname(bambuOutputPath);
|
|
1508
|
+
const rawBundle = this.resolveBambuLikeSettingsBundle(
|
|
1509
|
+
outputBase,
|
|
1510
|
+
slicerType,
|
|
1511
|
+
slicerProfile,
|
|
1512
|
+
printerPreset,
|
|
1513
|
+
bambuOptions
|
|
1514
|
+
);
|
|
1515
|
+
// Opt-in flatten step: walks the BBL `inherits` chain so the
|
|
1516
|
+
// CLI accepts what we hand it. See maybeFlattenBundle docstring.
|
|
1517
|
+
const settingsBundle =
|
|
1518
|
+
slicerType === 'bambustudio'
|
|
1519
|
+
? await this.maybeFlattenBundle(rawBundle, bambuOptions, slicerPath)
|
|
1520
|
+
: rawBundle;
|
|
1077
1521
|
args = [
|
|
1078
1522
|
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
1079
|
-
'--
|
|
1523
|
+
'--outputdir', outputDir,
|
|
1524
|
+
'--export-3mf', path.basename(bambuOutputPath),
|
|
1080
1525
|
];
|
|
1081
|
-
if (
|
|
1082
|
-
args.push('--load-settings',
|
|
1526
|
+
if (settingsBundle.settingsArg) {
|
|
1527
|
+
args.push('--load-settings', settingsBundle.settingsArg);
|
|
1083
1528
|
}
|
|
1084
1529
|
// Always allow newer-version 3MF files (the CLI rejects them by default)
|
|
1085
1530
|
args.push('--allow-newer-file');
|
|
1086
1531
|
|
|
1087
|
-
// If a printer preset name is given AND no explicit profile was provided,
|
|
1088
|
-
// write a minimal settings JSON containing the printer_settings_id so
|
|
1089
|
-
// BambuStudio resolves the correct machine profile from its built-in presets.
|
|
1090
|
-
if (printerPreset && !slicerProfile) {
|
|
1091
|
-
const presetJson = path.join(this.tempDir, '_printer_preset.json');
|
|
1092
|
-
fs.writeFileSync(presetJson, JSON.stringify({ printer_settings_id: printerPreset }));
|
|
1093
|
-
args.push('--load-settings', presetJson);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
1532
|
// BambuSliceOptions flags
|
|
1097
1533
|
if (bambuOptions?.uptodate) args.push('--uptodate');
|
|
1098
1534
|
if (bambuOptions?.ensureOnBed) args.push('--ensure-on-bed');
|
|
@@ -1109,7 +1545,10 @@ export class STLManipulator extends EventEmitter {
|
|
|
1109
1545
|
if (bambuOptions?.rotateY !== undefined) args.push('--rotate-y', String(bambuOptions.rotateY));
|
|
1110
1546
|
if (bambuOptions?.cloneObjects) args.push('--clone-objects', bambuOptions.cloneObjects);
|
|
1111
1547
|
if (bambuOptions?.skipObjects) args.push('--skip-objects', bambuOptions.skipObjects);
|
|
1112
|
-
if (
|
|
1548
|
+
if (settingsBundle.filamentPaths.length > 0) {
|
|
1549
|
+
args.push('--load-filaments', settingsBundle.filamentPaths.join(';'));
|
|
1550
|
+
args.push('--load-defaultfila');
|
|
1551
|
+
}
|
|
1113
1552
|
if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
|
|
1114
1553
|
|
|
1115
1554
|
args.push(stlFilePath);
|
package/src/types.ts
CHANGED
|
@@ -56,3 +56,44 @@ export interface ThreeMFData {
|
|
|
56
56
|
build: { items: ThreeMFBuildItem[] };
|
|
57
57
|
slicerConfig?: Partial<BambuSlicerConfig>;
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
export interface CollarCharmRole {
|
|
61
|
+
role: 'inner' | 'outer';
|
|
62
|
+
objectIndex: number;
|
|
63
|
+
name: string;
|
|
64
|
+
area: number;
|
|
65
|
+
filamentPosition: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CollarCharmAnalysis {
|
|
69
|
+
plateIndex: number;
|
|
70
|
+
usedFilamentPositions: number[];
|
|
71
|
+
amsSlots: number[];
|
|
72
|
+
roles: CollarCharmRole[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ThreeMFFilamentRequirement {
|
|
76
|
+
filamentPosition: number;
|
|
77
|
+
filamentId: number;
|
|
78
|
+
tray_info_idx: string | null;
|
|
79
|
+
type: string | null;
|
|
80
|
+
color: string | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ThreeMFAmsRequirements {
|
|
84
|
+
plateIndex: number;
|
|
85
|
+
usedFilamentPositions: number[];
|
|
86
|
+
filaments: ThreeMFFilamentRequirement[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ThreeMFPlateObject {
|
|
90
|
+
id: number;
|
|
91
|
+
name: string;
|
|
92
|
+
area: number | null;
|
|
93
|
+
bbox: unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ThreeMFPlateObjects {
|
|
97
|
+
plateIndex: number;
|
|
98
|
+
objects: ThreeMFPlateObject[];
|
|
99
|
+
}
|