bambu-printer-mcp 1.1.8 → 1.1.10
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 -3
- package/README.md +188 -370
- package/dist/index.js +39 -11
- package/dist/printers/bambu.d.ts +3 -2
- package/dist/printers/bambu.js +18 -5
- package/dist/slicer/profile-flatten.d.ts +14 -0
- package/dist/slicer/profile-flatten.js +88 -0
- package/dist/stl/stl-manipulator.d.ts +1 -0
- package/dist/stl/stl-manipulator.js +25 -6
- package/package.json +1 -1
- package/src/index.ts +36 -11
- package/src/printers/bambu.ts +20 -5
- package/src/slicer/profile-flatten.ts +103 -0
- package/src/stl/stl-manipulator.ts +29 -6
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { BambuNetworkBridge } from "./bambu-network-bridge.js";
|
|
|
14
14
|
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
15
15
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
16
16
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
17
|
-
import { BambuImplementation } from "./printers/bambu.js";
|
|
17
|
+
import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
|
|
18
18
|
dotenv.config();
|
|
19
19
|
const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST || "localhost";
|
|
20
20
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
|
|
@@ -44,7 +44,7 @@ const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase(
|
|
|
44
44
|
"";
|
|
45
45
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
46
46
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
47
|
-
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c"];
|
|
47
|
+
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"];
|
|
48
48
|
const H2_BAMBU_MODELS = new Set(["h2d", "h2s", "h2c"]);
|
|
49
49
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"];
|
|
50
50
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
@@ -60,6 +60,7 @@ const BAMBU_MODEL_PRESETS = {
|
|
|
60
60
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
61
61
|
h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
|
|
62
62
|
h2c: (n) => `Bambu Lab H2C ${n} nozzle`,
|
|
63
|
+
x2d: (n) => `Bambu Lab X2D ${n} nozzle`,
|
|
63
64
|
};
|
|
64
65
|
const FILAMENT_PROFILE_DIR = "/Applications/BambuStudio.app/Contents/Resources/profiles/BBL/filament";
|
|
65
66
|
const FILAMENT_MODEL_CODES = {
|
|
@@ -73,6 +74,7 @@ const FILAMENT_MODEL_CODES = {
|
|
|
73
74
|
h2d: "H2D",
|
|
74
75
|
h2s: "H2S",
|
|
75
76
|
h2c: "H2C",
|
|
77
|
+
x2d: "X2D",
|
|
76
78
|
};
|
|
77
79
|
const COLLAR_CHARM_POLICY = getCollarCharmRolePolicy();
|
|
78
80
|
let filamentProfileIndexCache = null;
|
|
@@ -371,6 +373,19 @@ function extractPrinterDiagnostics(status) {
|
|
|
371
373
|
diagnostic_fields: diagnosticFields,
|
|
372
374
|
};
|
|
373
375
|
}
|
|
376
|
+
/** Parse "#RRGGBB;#RRGGBB" (leading '#' optional) into positional slot colours. */
|
|
377
|
+
function parseFilamentColours(value) {
|
|
378
|
+
const colours = value.split(";").map((c) => c.trim()).filter(Boolean)
|
|
379
|
+
.map((c) => (c.startsWith("#") ? c : `#${c}`).toUpperCase());
|
|
380
|
+
for (const c of colours) {
|
|
381
|
+
if (!/^#[0-9A-F]{6}([0-9A-F]{2})?$/.test(c)) {
|
|
382
|
+
throw new Error(`Invalid filament colour "${c}"; expected #RRGGBB.`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (colours.length === 0)
|
|
386
|
+
throw new Error("filament_colours must list at least one colour.");
|
|
387
|
+
return colours;
|
|
388
|
+
}
|
|
374
389
|
function validateBambuModel(model) {
|
|
375
390
|
const normalized = model.trim().toLowerCase();
|
|
376
391
|
if (!VALID_BAMBU_MODELS.includes(normalized)) {
|
|
@@ -822,6 +837,7 @@ class BambuPrinterMCPServer {
|
|
|
822
837
|
{ const: "h2d", title: "H2D" },
|
|
823
838
|
{ const: "h2s", title: "H2S" },
|
|
824
839
|
{ const: "h2c", title: "H2C" },
|
|
840
|
+
{ const: "x2d", title: "X2D" },
|
|
825
841
|
],
|
|
826
842
|
},
|
|
827
843
|
},
|
|
@@ -1549,6 +1565,7 @@ class BambuPrinterMCPServer {
|
|
|
1549
1565
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
1550
1566
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs." },
|
|
1551
1567
|
load_filament_ids: { type: "string", description: "Optional filament-to-object mapping string." },
|
|
1568
|
+
filament_colours: { type: "string", description: "Optional slot colours, one #RRGGBB per filament slot in order, separated by ';' (e.g. '#161616;#FFFFFF'). Defaults to the input 3MF's project colours, else the BambuStudio default." },
|
|
1552
1569
|
ensure_on_bed: { type: "boolean", description: "Lift floating models onto the bed." },
|
|
1553
1570
|
arrange: { type: "boolean", description: "Auto-arrange objects on the build plate." },
|
|
1554
1571
|
orient: { type: "boolean", description: "Auto-orient model for optimal printability." },
|
|
@@ -1603,6 +1620,7 @@ class BambuPrinterMCPServer {
|
|
|
1603
1620
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
1604
1621
|
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths." },
|
|
1605
1622
|
load_filament_ids: { type: "string", description: "Map filaments to objects/parts. Comma-separated IDs matching load_filaments order, e.g. '1,2,3,1' assigns filament 1 to objects 0 and 3." },
|
|
1623
|
+
filament_colours: { type: "string", description: "Optional slot colours, one #RRGGBB per filament slot in order, separated by ';' (e.g. '#161616;#FFFFFF'). Defaults to the input 3MF's project colours, else the BambuStudio default." },
|
|
1606
1624
|
enable_timelapse: { type: "boolean", description: "Insert timelapse parking moves into gcode. The toolhead parks at a fixed position each layer for camera capture. Adds ~10% print time." },
|
|
1607
1625
|
allow_mix_temp: { type: "boolean", description: "Allow filaments with different temperature requirements on the same plate. Required for multi-material prints mixing e.g. PLA and PETG." },
|
|
1608
1626
|
scale: { type: "number", description: "Uniform scale factor applied to all axes. 1.0 = original size, 2.0 = double, 0.5 = half. Applied before slicing." },
|
|
@@ -2107,7 +2125,7 @@ class BambuPrinterMCPServer {
|
|
|
2107
2125
|
bambu_model: {
|
|
2108
2126
|
type: "string",
|
|
2109
2127
|
enum: [...VALID_BAMBU_MODELS],
|
|
2110
|
-
description: "REQUIRED: Bambu Lab printer model. H2D, H2S, and H2C are the primary intended paths."
|
|
2128
|
+
description: "REQUIRED: Bambu Lab printer model. H2D, H2S, and H2C are the primary intended paths. X2D direct printing is not supported."
|
|
2111
2129
|
},
|
|
2112
2130
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2113
2131
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2343,23 +2361,27 @@ class BambuPrinterMCPServer {
|
|
|
2343
2361
|
}
|
|
2344
2362
|
break;
|
|
2345
2363
|
}
|
|
2346
|
-
case "upload_file":
|
|
2364
|
+
case "upload_file": {
|
|
2347
2365
|
if (!args?.file_path || !args?.filename) {
|
|
2348
2366
|
throw new Error("Missing required parameters: file_path and filename");
|
|
2349
2367
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2368
|
+
const print = Boolean(args.print ?? false);
|
|
2369
|
+
const uploadModel = print ? await this.resolveBambuModel(args?.bambu_model) : undefined;
|
|
2370
|
+
if (print)
|
|
2371
|
+
assertDirectPrintSupported(uploadModel, bambuSerial);
|
|
2372
|
+
result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), print, uploadModel);
|
|
2354
2373
|
break;
|
|
2374
|
+
}
|
|
2355
2375
|
case "start_print":
|
|
2356
|
-
case "start_print_job":
|
|
2376
|
+
case "start_print_job": {
|
|
2357
2377
|
if (!args?.filename) {
|
|
2358
2378
|
throw new Error("Missing required parameter: filename");
|
|
2359
2379
|
}
|
|
2360
|
-
await this.resolveBambuModel(args?.bambu_model);
|
|
2361
|
-
|
|
2380
|
+
const startModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2381
|
+
assertDirectPrintSupported(startModel, bambuSerial);
|
|
2382
|
+
result = await this.bambu.startJob(host, bambuSerial, bambuToken, String(args.filename), startModel);
|
|
2362
2383
|
break;
|
|
2384
|
+
}
|
|
2363
2385
|
case "cancel_print":
|
|
2364
2386
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken);
|
|
2365
2387
|
break;
|
|
@@ -2499,6 +2521,8 @@ class BambuPrinterMCPServer {
|
|
|
2499
2521
|
}
|
|
2500
2522
|
if (args?.load_filament_ids !== undefined)
|
|
2501
2523
|
sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
2524
|
+
if (args?.filament_colours !== undefined)
|
|
2525
|
+
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
2502
2526
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
2503
2527
|
if (args?.enable_timelapse !== undefined)
|
|
2504
2528
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
@@ -2576,6 +2600,8 @@ class BambuPrinterMCPServer {
|
|
|
2576
2600
|
}
|
|
2577
2601
|
if (args?.load_filament_ids !== undefined)
|
|
2578
2602
|
sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
2603
|
+
if (args?.filament_colours !== undefined)
|
|
2604
|
+
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
2579
2605
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
2580
2606
|
if (args?.enable_timelapse !== undefined)
|
|
2581
2607
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
@@ -2628,6 +2654,7 @@ class BambuPrinterMCPServer {
|
|
|
2628
2654
|
}
|
|
2629
2655
|
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "print_3mf");
|
|
2630
2656
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2657
|
+
assertDirectPrintSupported(printModel, bambuSerial);
|
|
2631
2658
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2632
2659
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2633
2660
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
|
|
@@ -2768,6 +2795,7 @@ class BambuPrinterMCPServer {
|
|
|
2768
2795
|
}
|
|
2769
2796
|
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "print_collar_charm");
|
|
2770
2797
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2798
|
+
assertDirectPrintSupported(printModel, bambuSerial);
|
|
2771
2799
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2772
2800
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2773
2801
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolvedTemplateSourcePath || explicitTemplatePath || undefined, TEMP_DIR);
|
package/dist/printers/bambu.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function assertDirectPrintSupported(model: string | undefined, serial?: string): void;
|
|
1
2
|
interface BambuPrintOptionsInternal {
|
|
2
3
|
projectName: string;
|
|
3
4
|
filePath: string;
|
|
@@ -67,8 +68,8 @@ export declare class BambuImplementation {
|
|
|
67
68
|
name: string;
|
|
68
69
|
exists: boolean;
|
|
69
70
|
}>;
|
|
70
|
-
uploadFile(host: string, serial: string, token: string, filePath: string, filename: string, print: boolean): Promise<Record<string, unknown>>;
|
|
71
|
-
startJob(host: string, serial: string, token: string, filename: string): Promise<{
|
|
71
|
+
uploadFile(host: string, serial: string, token: string, filePath: string, filename: string, print: boolean, bambuModel?: string): Promise<Record<string, unknown>>;
|
|
72
|
+
startJob(host: string, serial: string, token: string, filename: string, bambuModel?: string): Promise<{
|
|
72
73
|
status: string;
|
|
73
74
|
message: string;
|
|
74
75
|
file: string;
|
package/dist/printers/bambu.js
CHANGED
|
@@ -37,6 +37,7 @@ const MODEL_ID_TO_NAME = {
|
|
|
37
37
|
O1D: "H2D",
|
|
38
38
|
O1E: "H2D Pro",
|
|
39
39
|
O1S: "H2S",
|
|
40
|
+
N6: "X2D",
|
|
40
41
|
N2S: "A1",
|
|
41
42
|
A1M: "A1 Mini",
|
|
42
43
|
C11: "P1P",
|
|
@@ -46,6 +47,12 @@ const MODEL_ID_TO_NAME = {
|
|
|
46
47
|
C13: "X1E",
|
|
47
48
|
};
|
|
48
49
|
const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s"]);
|
|
50
|
+
export function assertDirectPrintSupported(model, serial) {
|
|
51
|
+
if (model?.trim().toLowerCase() === "x2d" || serial?.trim().toUpperCase().startsWith("20P")) {
|
|
52
|
+
throw new Error("X2D direct printing is not supported pending native eMMC transport support. " +
|
|
53
|
+
"X2D status and slicing remain available; use a supported slicer to print.");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
49
56
|
function isH2ModelName(model) {
|
|
50
57
|
return H2_MODEL_NAMES.has(String(model ?? "").trim().toLowerCase().replace(/\s+/g, " "));
|
|
51
58
|
}
|
|
@@ -95,6 +102,8 @@ class TolerantBambuClient extends BambuClient {
|
|
|
95
102
|
return "H2C";
|
|
96
103
|
if (sn.startsWith("31B"))
|
|
97
104
|
return "H2DPRO";
|
|
105
|
+
if (sn.startsWith("20P"))
|
|
106
|
+
return "X2D";
|
|
98
107
|
if (sn.startsWith("00M"))
|
|
99
108
|
return "X1C";
|
|
100
109
|
if (sn.startsWith("00W"))
|
|
@@ -457,6 +466,7 @@ export class BambuImplementation {
|
|
|
457
466
|
}
|
|
458
467
|
}
|
|
459
468
|
async print3mf(host, serial, token, options) {
|
|
469
|
+
assertDirectPrintSupported(options.bambuModel, serial);
|
|
460
470
|
if (!options.filePath.toLowerCase().endsWith(".3mf")) {
|
|
461
471
|
throw new Error("print3mf requires a .3mf input file.");
|
|
462
472
|
}
|
|
@@ -929,7 +939,9 @@ export class BambuImplementation {
|
|
|
929
939
|
exists,
|
|
930
940
|
};
|
|
931
941
|
}
|
|
932
|
-
async uploadFile(host, serial, token, filePath, filename, print) {
|
|
942
|
+
async uploadFile(host, serial, token, filePath, filename, print, bambuModel) {
|
|
943
|
+
if (print)
|
|
944
|
+
assertDirectPrintSupported(bambuModel, serial);
|
|
933
945
|
await fs.access(filePath);
|
|
934
946
|
const normalizedFileName = filename.replace(/^\/+/, "");
|
|
935
947
|
const remotePath = normalizedFileName.includes("/")
|
|
@@ -945,7 +957,7 @@ export class BambuImplementation {
|
|
|
945
957
|
};
|
|
946
958
|
if (print) {
|
|
947
959
|
if (remotePath.toLowerCase().endsWith(".gcode")) {
|
|
948
|
-
response.startResult = await this.startJob(host, serial, token, remotePath);
|
|
960
|
+
response.startResult = await this.startJob(host, serial, token, remotePath, bambuModel);
|
|
949
961
|
}
|
|
950
962
|
else if (remotePath.toLowerCase().endsWith(".3mf")) {
|
|
951
963
|
response.note =
|
|
@@ -957,7 +969,8 @@ export class BambuImplementation {
|
|
|
957
969
|
}
|
|
958
970
|
return response;
|
|
959
971
|
}
|
|
960
|
-
async startJob(host, serial, token, filename) {
|
|
972
|
+
async startJob(host, serial, token, filename, bambuModel) {
|
|
973
|
+
assertDirectPrintSupported(bambuModel, serial);
|
|
961
974
|
const lower = filename.toLowerCase();
|
|
962
975
|
if (lower.endsWith(".3mf") && !lower.endsWith(".gcode.3mf")) {
|
|
963
976
|
throw new Error("Use print_3mf for .3mf project files.");
|
|
@@ -1007,7 +1020,7 @@ export class BambuImplementation {
|
|
|
1007
1020
|
// P1/A1 series still use the proprietary TCP-on-6000 framed JPEG path
|
|
1008
1021
|
// (per https://github.com/Doridian/OpenBambuAPI/blob/main/video.md).
|
|
1009
1022
|
const TCP_CAMERA_MODELS = new Set(["a1", "a1mini", "p1s", "p1p"]);
|
|
1010
|
-
// X1, P2S,
|
|
1023
|
+
// X1, P2S, H2 (H2S/H2D/H2C), AND X2D all use RTSP on port 322. The
|
|
1011
1024
|
// OpenBambuAPI doc only mentions X1/P2S, but the HA bambulab
|
|
1012
1025
|
// integration's models.py shows the printer reports its own
|
|
1013
1026
|
// `ipcam.rtsp_url` for these models, and Parker (H2S) rejects the
|
|
@@ -1015,7 +1028,7 @@ export class BambuImplementation {
|
|
|
1015
1028
|
// confirmed by local H2 camera transport probes).
|
|
1016
1029
|
const RTSP_MODELS = new Set([
|
|
1017
1030
|
"x1", "x1c", "x1carbon", "x1e", "p2s",
|
|
1018
|
-
"h2", "h2s", "h2d", "h2c", "h2dpro",
|
|
1031
|
+
"h2", "h2s", "h2d", "h2c", "h2dpro", "x2d",
|
|
1019
1032
|
]);
|
|
1020
1033
|
if (!model) {
|
|
1021
1034
|
throw new Error("camera_snapshot requires bambu_model or BAMBU_MODEL so it can choose the correct Bambu camera protocol.");
|
|
@@ -70,7 +70,21 @@ export interface FlattenOptions {
|
|
|
70
70
|
process?: Record<string, unknown>;
|
|
71
71
|
filaments?: (Record<string, unknown> | undefined)[];
|
|
72
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Positional `#RRGGBB` colour per filament slot (e.g. from the input 3MF
|
|
75
|
+
* project or the caller). Missing entries keep the profile's own colour or
|
|
76
|
+
* fall back to DEFAULT_FILAMENT_COLOUR.
|
|
77
|
+
*/
|
|
78
|
+
filamentColours?: (string | undefined)[];
|
|
79
|
+
/** Saved per-plate positions, used only when the process sets no position. */
|
|
80
|
+
projectTowerPosition?: {
|
|
81
|
+
wipe_tower_x?: unknown;
|
|
82
|
+
wipe_tower_y?: unknown;
|
|
83
|
+
};
|
|
73
84
|
}
|
|
85
|
+
/** Best-effort extruder count for fallback nozzle_volume_type sizing. */
|
|
86
|
+
/** BambuStudio's built-in filament_colour default. */
|
|
87
|
+
export declare const DEFAULT_FILAMENT_COLOUR = "#00AE42";
|
|
74
88
|
/**
|
|
75
89
|
* Flatten the leaf profiles, post-process for CLI, and write to temp files.
|
|
76
90
|
*
|
|
@@ -219,6 +219,86 @@ function enforceMatchingNozzles(arr) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
/** Best-effort extruder count for fallback nozzle_volume_type sizing. */
|
|
222
|
+
/** BambuStudio's built-in filament_colour default. */
|
|
223
|
+
export const DEFAULT_FILAMENT_COLOUR = "#00AE42";
|
|
224
|
+
const FILAMENT_COLOUR_RE = /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
|
|
225
|
+
/**
|
|
226
|
+
* No BBL filament profile defines `filament_colour`; the GUI fills it from
|
|
227
|
+
* the AMS. Without it the CLI keeps a single-entry colour vector however many
|
|
228
|
+
* filaments are loaded and crashes (access violation) as soon as a slice uses
|
|
229
|
+
* any filament after the first. Give every slot exactly one colour.
|
|
230
|
+
*/
|
|
231
|
+
function ensureFilamentColour(flat, colour) {
|
|
232
|
+
if (colour !== undefined) {
|
|
233
|
+
if (!FILAMENT_COLOUR_RE.test(colour)) {
|
|
234
|
+
throw new Error(`Invalid filament colour "${colour}"; expected #RRGGBB.`);
|
|
235
|
+
}
|
|
236
|
+
flat["filament_colour"] = [colour];
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const own = flat["filament_colour"];
|
|
240
|
+
if (Array.isArray(own) && typeof own[0] === "string" && FILAMENT_COLOUR_RE.test(own[0])) {
|
|
241
|
+
flat["filament_colour"] = [own[0]];
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
flat["filament_colour"] = [DEFAULT_FILAMENT_COLOUR];
|
|
245
|
+
}
|
|
246
|
+
// BambuStudio's defaults when a process sets no prime tower position/width.
|
|
247
|
+
const DEFAULT_WIPE_TOWER_X = 15;
|
|
248
|
+
const DEFAULT_WIPE_TOWER_Y = 220;
|
|
249
|
+
const DEFAULT_PRIME_TOWER_WIDTH = 35;
|
|
250
|
+
// Clearance inside the shared nozzle area for the auto-sized tower brim and
|
|
251
|
+
// the CLI's own safety margin (X2D fails at 5 mm from the edge, passes at 6).
|
|
252
|
+
const WIPE_TOWER_MARGIN = 15;
|
|
253
|
+
function parseArea(area) {
|
|
254
|
+
const points = area.split(",").map((p) => p.trim().split("x").map(Number));
|
|
255
|
+
if (points.length < 3 || points.some((p) => p.length !== 2 || p.some((n) => !Number.isFinite(n)))) {
|
|
256
|
+
throw new Error(`Malformed extruder_printable_area entry "${area}".`);
|
|
257
|
+
}
|
|
258
|
+
const xs = points.map((p) => p[0]);
|
|
259
|
+
const ys = points.map((p) => p[1]);
|
|
260
|
+
return { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* On multi-nozzle machines (H2D, X2D) each nozzle reaches only part of the
|
|
264
|
+
* bed (`extruder_printable_area`), and the prime tower must be reachable by
|
|
265
|
+
* every nozzle. The CLI's default tower position (x=15) lies outside the
|
|
266
|
+
* X2D/H2D right-nozzle area, so slicing fails with "G-code outside of the
|
|
267
|
+
* printable area". When the process sets no position and the default does
|
|
268
|
+
* not fit, move the tower inside the area all nozzles share.
|
|
269
|
+
*/
|
|
270
|
+
function placePrimeTowerForAllNozzles(machineFlat, processFlat) {
|
|
271
|
+
if (processFlat["wipe_tower_x"] !== undefined || processFlat["wipe_tower_y"] !== undefined)
|
|
272
|
+
return;
|
|
273
|
+
if (["0", "false"].includes(String(processFlat["enable_prime_tower"]).toLowerCase()))
|
|
274
|
+
return;
|
|
275
|
+
const areas = machineFlat["extruder_printable_area"];
|
|
276
|
+
if (!Array.isArray(areas) || areas.length < 2)
|
|
277
|
+
return;
|
|
278
|
+
const rects = areas.map((a) => {
|
|
279
|
+
if (typeof a !== "string")
|
|
280
|
+
throw new Error("Malformed extruder_printable_area in machine profile.");
|
|
281
|
+
return parseArea(a);
|
|
282
|
+
});
|
|
283
|
+
const shared = {
|
|
284
|
+
minX: Math.max(...rects.map((r) => r.minX)) + WIPE_TOWER_MARGIN,
|
|
285
|
+
maxX: Math.min(...rects.map((r) => r.maxX)) - WIPE_TOWER_MARGIN,
|
|
286
|
+
minY: Math.max(...rects.map((r) => r.minY)) + WIPE_TOWER_MARGIN,
|
|
287
|
+
maxY: Math.min(...rects.map((r) => r.maxY)) - WIPE_TOWER_MARGIN,
|
|
288
|
+
};
|
|
289
|
+
const width = Number(processFlat["prime_tower_width"] ?? DEFAULT_PRIME_TOWER_WIDTH);
|
|
290
|
+
const size = Number.isFinite(width) && width > 0 ? width : DEFAULT_PRIME_TOWER_WIDTH;
|
|
291
|
+
if (shared.maxX - shared.minX < size || shared.maxY - shared.minY < size) {
|
|
292
|
+
throw new Error("No bed area is reachable by every nozzle for the prime tower.");
|
|
293
|
+
}
|
|
294
|
+
const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
295
|
+
const x = clamp(DEFAULT_WIPE_TOWER_X, shared.minX, shared.maxX - size);
|
|
296
|
+
const y = clamp(DEFAULT_WIPE_TOWER_Y, shared.minY, shared.maxY - size);
|
|
297
|
+
if (x === DEFAULT_WIPE_TOWER_X && y === DEFAULT_WIPE_TOWER_Y)
|
|
298
|
+
return;
|
|
299
|
+
processFlat["wipe_tower_x"] = [String(x)];
|
|
300
|
+
processFlat["wipe_tower_y"] = [String(y)];
|
|
301
|
+
}
|
|
222
302
|
function inferExtruderCount(flat) {
|
|
223
303
|
for (const key of ["nozzle_diameter", "extruder_type", "extruder_variant_list"]) {
|
|
224
304
|
const v = flat[key];
|
|
@@ -466,7 +546,15 @@ export async function flattenForCli(opts) {
|
|
|
466
546
|
normalizeForCli(machineFlat, "machine", opts.machineLeaf);
|
|
467
547
|
normalizeForCli(processFlat, "process", opts.processLeaf);
|
|
468
548
|
filamentFlats.forEach((f, i) => normalizeForCli(f, "filament", opts.filamentLeaves[i]));
|
|
549
|
+
if (opts.filamentColours && opts.filamentColours.length !== filamentFlats.length) {
|
|
550
|
+
throw new Error(`${opts.filamentColours.length} filament colours were supplied for ${filamentFlats.length} filament slots.`);
|
|
551
|
+
}
|
|
552
|
+
filamentFlats.forEach((f, i) => ensureFilamentColour(f, opts.filamentColours?.[i]));
|
|
469
553
|
applyBedType(processFlat, opts.bedType);
|
|
554
|
+
if (processFlat["wipe_tower_x"] === undefined && processFlat["wipe_tower_y"] === undefined) {
|
|
555
|
+
Object.assign(processFlat, opts.projectTowerPosition);
|
|
556
|
+
}
|
|
557
|
+
placePrimeTowerForAllNozzles(machineFlat, processFlat);
|
|
470
558
|
// Mirror the GUI's auto-extend behavior: when the caller explicitly
|
|
471
559
|
// chose a process or filament that wasn't pre-declared compatible with
|
|
472
560
|
// the chosen machine (e.g. "0.20mm Standard @BBL P1P" used on a P1S),
|
|
@@ -382,20 +382,32 @@ export class STLManipulator extends EventEmitter {
|
|
|
382
382
|
profilesRoot,
|
|
383
383
|
tempDir: this.tempDir,
|
|
384
384
|
bedType: this.resolveBambuStudioBedType(bambuOptions?.bedType),
|
|
385
|
+
filamentColours: bambuOptions?.filamentColours ?? bundle.filamentColours,
|
|
386
|
+
projectTowerPosition: bundle.projectTowerPosition,
|
|
385
387
|
sourceProfiles: {
|
|
386
388
|
machine: isBundledFile(machinePath) ? undefined : machine,
|
|
387
389
|
process: isBundledFile(processPath) ? undefined : processProfile,
|
|
388
390
|
filaments: filaments.map((profile, i) => isBundledFile(bundle.filamentPaths[i]) ? undefined : profile),
|
|
389
391
|
},
|
|
390
392
|
});
|
|
391
|
-
// Standalone
|
|
392
|
-
//
|
|
393
|
+
// Standalone files retain their own identity and settings, but still need
|
|
394
|
+
// the CLI colour/tower fields. Do not discard those required overlays when
|
|
395
|
+
// choosing the original profile instead of the normalized BBL output.
|
|
396
|
+
const withCliFields = (filePath, profile, flatPath, keys) => {
|
|
397
|
+
const flattened = readProfile(flatPath);
|
|
398
|
+
const fields = Object.fromEntries(keys.filter(key => flattened[key] !== undefined).map(key => [key, flattened[key]]));
|
|
399
|
+
if (Object.entries(fields).every(([key, value]) => JSON.stringify(profile[key]) === JSON.stringify(value)))
|
|
400
|
+
return filePath;
|
|
401
|
+
return this.writeTempJson(path.basename(filePath, '.json'), 'cli', { ...profile, ...fields });
|
|
402
|
+
};
|
|
393
403
|
return {
|
|
394
404
|
settingsArg: [
|
|
395
405
|
needsResolution(machinePath, machine) ? flat.machinePath : machinePath,
|
|
396
|
-
needsResolution(processPath, processProfile) ? flat.processPath
|
|
406
|
+
needsResolution(processPath, processProfile) ? flat.processPath
|
|
407
|
+
: withCliFields(processPath, processProfile, flat.processPath, ['wipe_tower_x', 'wipe_tower_y']),
|
|
397
408
|
].join(';'),
|
|
398
|
-
filamentPaths: filaments.map((profile, i) => needsResolution(bundle.filamentPaths[i], profile) ? flat.filamentPaths[i]
|
|
409
|
+
filamentPaths: filaments.map((profile, i) => needsResolution(bundle.filamentPaths[i], profile) ? flat.filamentPaths[i]
|
|
410
|
+
: withCliFields(bundle.filamentPaths[i], profile, flat.filamentPaths[i], ['filament_colour'])),
|
|
399
411
|
};
|
|
400
412
|
}
|
|
401
413
|
/** --load-filaments is positional; a single override must cover every project slot. */
|
|
@@ -417,13 +429,20 @@ export class STLManipulator extends EventEmitter {
|
|
|
417
429
|
.map(key => Array.isArray(data[key]) ? data[key].length : 0));
|
|
418
430
|
if (count === 0)
|
|
419
431
|
throw new Error('Cannot determine project filament slots from project_settings.config.');
|
|
432
|
+
// Keep the project's slot colours; loaded profiles carry none of their own.
|
|
433
|
+
const colours = Array.isArray(data.filament_colour) && data.filament_colour.length === count &&
|
|
434
|
+
data.filament_colour.every(c => typeof c === 'string' && /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(c))
|
|
435
|
+
? data.filament_colour
|
|
436
|
+
: undefined;
|
|
437
|
+
const projectTowerPosition = Object.fromEntries(['wipe_tower_x', 'wipe_tower_y']
|
|
438
|
+
.filter(key => data[key] !== undefined).map(key => [key, data[key]]));
|
|
420
439
|
if (bundle.filamentPaths.length === 1) {
|
|
421
|
-
return { ...bundle, filamentPaths: Array(count).fill(bundle.filamentPaths[0]) };
|
|
440
|
+
return { ...bundle, filamentPaths: Array(count).fill(bundle.filamentPaths[0]), filamentColours: colours, projectTowerPosition };
|
|
422
441
|
}
|
|
423
442
|
if (bundle.filamentPaths.length !== count) {
|
|
424
443
|
throw new Error(`Project declares ${count} filament slots, but ${bundle.filamentPaths.length} profiles were supplied. Provide one profile for all slots or one per slot.`);
|
|
425
444
|
}
|
|
426
|
-
return bundle;
|
|
445
|
+
return { ...bundle, filamentColours: colours, projectTowerPosition };
|
|
427
446
|
}
|
|
428
447
|
resolveBambuStudioBedType(bedType) {
|
|
429
448
|
if (!bedType)
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
|
29
29
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
30
30
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
31
31
|
import type { ThreeMFAmsRequirements } from "./types.js";
|
|
32
|
-
import { BambuImplementation } from "./printers/bambu.js";
|
|
32
|
+
import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
|
|
33
33
|
|
|
34
34
|
dotenv.config();
|
|
35
35
|
|
|
@@ -64,7 +64,7 @@ const DEFAULT_BAMBU_MODEL =
|
|
|
64
64
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
65
65
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
66
66
|
|
|
67
|
-
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c"] as const;
|
|
67
|
+
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"] as const;
|
|
68
68
|
type BambuModel = typeof VALID_BAMBU_MODELS[number];
|
|
69
69
|
const H2_BAMBU_MODELS = new Set<string>(["h2d", "h2s", "h2c"]);
|
|
70
70
|
|
|
@@ -83,6 +83,7 @@ const BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
|
|
|
83
83
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
84
84
|
h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
|
|
85
85
|
h2c: (n) => `Bambu Lab H2C ${n} nozzle`,
|
|
86
|
+
x2d: (n) => `Bambu Lab X2D ${n} nozzle`,
|
|
86
87
|
};
|
|
87
88
|
|
|
88
89
|
const FILAMENT_PROFILE_DIR =
|
|
@@ -98,6 +99,7 @@ const FILAMENT_MODEL_CODES: Record<string, string> = {
|
|
|
98
99
|
h2d: "H2D",
|
|
99
100
|
h2s: "H2S",
|
|
100
101
|
h2c: "H2C",
|
|
102
|
+
x2d: "X2D",
|
|
101
103
|
};
|
|
102
104
|
|
|
103
105
|
type FilamentProfileIndex = {
|
|
@@ -516,6 +518,19 @@ function extractPrinterDiagnostics(status: any): Record<string, unknown> {
|
|
|
516
518
|
};
|
|
517
519
|
}
|
|
518
520
|
|
|
521
|
+
/** Parse "#RRGGBB;#RRGGBB" (leading '#' optional) into positional slot colours. */
|
|
522
|
+
function parseFilamentColours(value: string): string[] {
|
|
523
|
+
const colours = value.split(";").map((c) => c.trim()).filter(Boolean)
|
|
524
|
+
.map((c) => (c.startsWith("#") ? c : `#${c}`).toUpperCase());
|
|
525
|
+
for (const c of colours) {
|
|
526
|
+
if (!/^#[0-9A-F]{6}([0-9A-F]{2})?$/.test(c)) {
|
|
527
|
+
throw new Error(`Invalid filament colour "${c}"; expected #RRGGBB.`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (colours.length === 0) throw new Error("filament_colours must list at least one colour.");
|
|
531
|
+
return colours;
|
|
532
|
+
}
|
|
533
|
+
|
|
519
534
|
function validateBambuModel(model: string): string {
|
|
520
535
|
const normalized = model.trim().toLowerCase();
|
|
521
536
|
if (!VALID_BAMBU_MODELS.includes(normalized as BambuModel)) {
|
|
@@ -1108,6 +1123,7 @@ class BambuPrinterMCPServer {
|
|
|
1108
1123
|
{ const: "h2d", title: "H2D" },
|
|
1109
1124
|
{ const: "h2s", title: "H2S" },
|
|
1110
1125
|
{ const: "h2c", title: "H2C" },
|
|
1126
|
+
{ const: "x2d", title: "X2D" },
|
|
1111
1127
|
],
|
|
1112
1128
|
},
|
|
1113
1129
|
},
|
|
@@ -2045,6 +2061,7 @@ class BambuPrinterMCPServer {
|
|
|
2045
2061
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
2046
2062
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs." },
|
|
2047
2063
|
load_filament_ids: { type: "string", description: "Optional filament-to-object mapping string." },
|
|
2064
|
+
filament_colours: { type: "string", description: "Optional slot colours, one #RRGGBB per filament slot in order, separated by ';' (e.g. '#161616;#FFFFFF'). Defaults to the input 3MF's project colours, else the BambuStudio default." },
|
|
2048
2065
|
ensure_on_bed: { type: "boolean", description: "Lift floating models onto the bed." },
|
|
2049
2066
|
arrange: { type: "boolean", description: "Auto-arrange objects on the build plate." },
|
|
2050
2067
|
orient: { type: "boolean", description: "Auto-orient model for optimal printability." },
|
|
@@ -2099,6 +2116,7 @@ class BambuPrinterMCPServer {
|
|
|
2099
2116
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
2100
2117
|
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths." },
|
|
2101
2118
|
load_filament_ids: { type: "string", description: "Map filaments to objects/parts. Comma-separated IDs matching load_filaments order, e.g. '1,2,3,1' assigns filament 1 to objects 0 and 3." },
|
|
2119
|
+
filament_colours: { type: "string", description: "Optional slot colours, one #RRGGBB per filament slot in order, separated by ';' (e.g. '#161616;#FFFFFF'). Defaults to the input 3MF's project colours, else the BambuStudio default." },
|
|
2102
2120
|
enable_timelapse: { type: "boolean", description: "Insert timelapse parking moves into gcode. The toolhead parks at a fixed position each layer for camera capture. Adds ~10% print time." },
|
|
2103
2121
|
allow_mix_temp: { type: "boolean", description: "Allow filaments with different temperature requirements on the same plate. Required for multi-material prints mixing e.g. PLA and PETG." },
|
|
2104
2122
|
scale: { type: "number", description: "Uniform scale factor applied to all axes. 1.0 = original size, 2.0 = double, 0.5 = half. Applied before slicing." },
|
|
@@ -2603,7 +2621,7 @@ class BambuPrinterMCPServer {
|
|
|
2603
2621
|
bambu_model: {
|
|
2604
2622
|
type: "string",
|
|
2605
2623
|
enum: [...VALID_BAMBU_MODELS],
|
|
2606
|
-
description: "REQUIRED: Bambu Lab printer model. H2D, H2S, and H2C are the primary intended paths."
|
|
2624
|
+
description: "REQUIRED: Bambu Lab printer model. H2D, H2S, and H2C are the primary intended paths. X2D direct printing is not supported."
|
|
2607
2625
|
},
|
|
2608
2626
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2609
2627
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2895,28 +2913,31 @@ class BambuPrinterMCPServer {
|
|
|
2895
2913
|
break;
|
|
2896
2914
|
}
|
|
2897
2915
|
|
|
2898
|
-
case "upload_file":
|
|
2916
|
+
case "upload_file": {
|
|
2899
2917
|
if (!args?.file_path || !args?.filename) {
|
|
2900
2918
|
throw new Error("Missing required parameters: file_path and filename");
|
|
2901
2919
|
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2920
|
+
const print = Boolean(args.print ?? false);
|
|
2921
|
+
const uploadModel = print ? await this.resolveBambuModel(args?.bambu_model as string | undefined) : undefined;
|
|
2922
|
+
if (print) assertDirectPrintSupported(uploadModel, bambuSerial);
|
|
2905
2923
|
result = await this.bambu.uploadFile(
|
|
2906
2924
|
host, bambuSerial, bambuToken,
|
|
2907
2925
|
String(args.file_path), String(args.filename),
|
|
2908
|
-
|
|
2926
|
+
print, uploadModel
|
|
2909
2927
|
);
|
|
2910
2928
|
break;
|
|
2929
|
+
}
|
|
2911
2930
|
|
|
2912
2931
|
case "start_print":
|
|
2913
|
-
case "start_print_job":
|
|
2932
|
+
case "start_print_job": {
|
|
2914
2933
|
if (!args?.filename) {
|
|
2915
2934
|
throw new Error("Missing required parameter: filename");
|
|
2916
2935
|
}
|
|
2917
|
-
await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2918
|
-
|
|
2936
|
+
const startModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2937
|
+
assertDirectPrintSupported(startModel, bambuSerial);
|
|
2938
|
+
result = await this.bambu.startJob(host, bambuSerial, bambuToken, String(args.filename), startModel);
|
|
2919
2939
|
break;
|
|
2940
|
+
}
|
|
2920
2941
|
|
|
2921
2942
|
case "cancel_print":
|
|
2922
2943
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken);
|
|
@@ -3118,6 +3139,7 @@ class BambuPrinterMCPServer {
|
|
|
3118
3139
|
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
3119
3140
|
}
|
|
3120
3141
|
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
3142
|
+
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3121
3143
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3122
3144
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3123
3145
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
@@ -3203,6 +3225,7 @@ class BambuPrinterMCPServer {
|
|
|
3203
3225
|
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
3204
3226
|
}
|
|
3205
3227
|
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
3228
|
+
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3206
3229
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3207
3230
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3208
3231
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
@@ -3265,6 +3288,7 @@ class BambuPrinterMCPServer {
|
|
|
3265
3288
|
"print_3mf"
|
|
3266
3289
|
);
|
|
3267
3290
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3291
|
+
assertDirectPrintSupported(printModel, bambuSerial);
|
|
3268
3292
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
3269
3293
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
3270
3294
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
|
|
@@ -3450,6 +3474,7 @@ class BambuPrinterMCPServer {
|
|
|
3450
3474
|
"print_collar_charm"
|
|
3451
3475
|
);
|
|
3452
3476
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3477
|
+
assertDirectPrintSupported(printModel, bambuSerial);
|
|
3453
3478
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
3454
3479
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
3455
3480
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
|