bambu-printer-mcp 1.1.8 → 1.1.9
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 +1 -0
- package/README.md +6 -4
- 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/CONTRIBUTORS.md
CHANGED
|
@@ -6,6 +6,7 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
|
|
|
6
6
|
|
|
7
7
|
| Contributor | Contribution |
|
|
8
8
|
| --- | --- |
|
|
9
|
+
| [Sebastian (sebas1986)](https://github.com/sebas1986) | Isolates the multi-filament CLI crash with real BambuStudio bisection, contributes per-slot colours and multi-nozzle prime-tower placement, and verifies X2D identification, status, and slicing in [#18](https://github.com/DMontgomery40/bambu-printer-mcp/pull/18). Direct X2D printing remains deferred pending the native transport. |
|
|
9
10
|
| [Stenslaen](https://github.com/Stenslaen) | Traces the delayed H2 crash to OTA model detection, documents a multi-day workaround, and reports unexpected state-transition crashes in [#7](https://github.com/DMontgomery40/bambu-printer-mcp/issues/7). |
|
|
10
11
|
| [Alejandro Oñate (alexol91)](https://github.com/alexol91) | Reports the missing machine-template G-code and multi-filament override problems in [#12](https://github.com/DMontgomery40/bambu-printer-mcp/issues/12), with measured output and a proposed fix in [#13](https://github.com/DMontgomery40/bambu-printer-mcp/pull/13). |
|
|
11
12
|
| [var-poro](https://github.com/var-poro) | Supplies P2S firmware/MQTT evidence and regression tests in [#15](https://github.com/DMontgomery40/bambu-printer-mcp/pull/15), plus profile-template resolution and inheritance tests in [#16](https://github.com/DMontgomery40/bambu-printer-mcp/pull/16). |
|
package/README.md
CHANGED
|
@@ -106,9 +106,10 @@ This fork adds a substantial set of printer control tools beyond the upstream `m
|
|
|
106
106
|
- Get detailed printer status: temperatures (nozzle, bed, chamber), print progress, current layer, time remaining, and live AMS slot data
|
|
107
107
|
- Query live AMS inventory with resolved Bambu/Orca filament profile paths via `get_printer_filaments`. Includes per-tray display names, match confidence (`high`/`medium`/`low`/`none`), resolution tier (`exact-model-nozzle`/`model`/`generic`/`unresolved`), and a summary with recommended auto-slice filament. Retries automatically when AMS data hasn't arrived yet (common on first MQTT push from idle printers).
|
|
108
108
|
- List, upload, and delete files on the printer's SD card via FTPS
|
|
109
|
-
- Capture a JPEG snapshot from the chamber camera. Supports A1, A1 mini, P1S, P1P (TCP-on-6000), and X1, X1C, X1E, P2S, H2, H2S, H2D, H2C, H2D Pro (RTSP via ffmpeg). Requires ffmpeg in PATH for the RTSP path.
|
|
109
|
+
- Capture a JPEG snapshot from the chamber camera. Supports A1, A1 mini, P1S, P1P (TCP-on-6000), and X1, X1C, X1E, P2S, H2, H2S, H2D, H2C, H2D Pro, X2D (RTSP via ffmpeg). Requires ffmpeg in PATH for the RTSP path.
|
|
110
110
|
- Upload and print pre-sliced `.gcode.3mf` files with full plate selection and calibration flag control (recommended path — see [docs/SLICING.md](./docs/SLICING.md))
|
|
111
|
-
-
|
|
111
|
+
- Slice through BambuStudio CLI with automatic BBL inheritance/include resolution, per-slot filament colours, and fallback prime-tower placement for multi-nozzle printers. Missing dependencies stop the slice; custom settings and saved project tower positions are preserved. Multi-colour slicing is verified by the contributor on BambuStudio 02.08.02.60 for Windows; older CLI versions have separate limitations. See [docs/SLICING.md](./docs/SLICING.md).
|
|
112
|
+
- Recognize X2D status and slice with its own installed BambuStudio preset (`BAMBU_MODEL=x2d`). **Direct X2D printing is not supported yet**: the internal eMMC transport is pending. These print requests stop before slicing, uploading, or issuing printer commands. Print exported projects through a supported slicer instead.
|
|
112
113
|
- Parse AMS mapping from the 3MF's embedded slicer metadata (`Metadata/plate_<n>.json` + gcode filament header) and send it correctly formatted per the OpenBambuAPI spec, with correct H2S/H2D/H2C `ams_mapping2` parallel array format
|
|
113
114
|
- **Auto-match AMS slots by RFID** (`auto_match_ams` flag on `print_3mf`). Resolves required `tray_info_idx` from the sliced 3MF against live AMS inventory. Handles same-SKU different-color filaments by matching on `(tray_info_idx, tray_color)` and tracking already-claimed slots. Dry-run with `resolve_3mf_ams_slots` before printing.
|
|
114
115
|
- Cancel, pause, and resume in-progress print jobs via MQTT
|
|
@@ -186,7 +187,7 @@ BAMBU_TOKEN=your_access_token # LAN access token from printer touchscreen
|
|
|
186
187
|
# BAMBU_PRINTER_HOST / BAMBU_PRINTER_SERIAL / BAMBU_PRINTER_ACCESS_TOKEN
|
|
187
188
|
|
|
188
189
|
# --- Printer model (CRITICAL for safe operation) ---
|
|
189
|
-
BAMBU_MODEL=p1s # Your printer model: p1s, p1p, p2s, x1c, x1e, a1, a1mini, h2d, h2s, h2c
|
|
190
|
+
BAMBU_MODEL=p1s # Your printer model: p1s, p1p, p2s, x1c, x1e, a1, a1mini, h2d, h2s, h2c, x2d
|
|
190
191
|
# Alias also accepted: BAMBU_PRINTER_MODEL
|
|
191
192
|
BED_TYPE=textured_plate # Bed plate type: textured_plate, cool_plate, engineering_plate, hot_plate, supertack_plate
|
|
192
193
|
NOZZLE_DIAMETER=0.4 # Nozzle diameter in mm (default: 0.4)
|
|
@@ -227,7 +228,7 @@ BLENDER_MCP_TIMEOUT_MS=120000
|
|
|
227
228
|
| `PRINTER_HOST` | `localhost` | Yes | IP address of the Bambu printer. Alias: `BAMBU_PRINTER_HOST` |
|
|
228
229
|
| `BAMBU_SERIAL` | | Yes | Printer serial number. Alias: `BAMBU_PRINTER_SERIAL` |
|
|
229
230
|
| `BAMBU_TOKEN` | | Yes | LAN access token. Alias: `BAMBU_PRINTER_ACCESS_TOKEN` |
|
|
230
|
-
| `BAMBU_MODEL` | | **Yes** | Printer model: `p1s`, `p1p`, `p2s`, `x1c`, `x1e`, `a1`, `a1mini`, `h2d`, `h2s`, `h2c`. **Required for safe operation** -- determines the correct G-code generation. Alias: `BAMBU_PRINTER_MODEL`. If omitted and the MCP client supports elicitation, the server will ask you interactively. Use `h2c` for H2C; do not use `h2d` as a fallback. |
|
|
231
|
+
| `BAMBU_MODEL` | | **Yes** | Printer model: `p1s`, `p1p`, `p2s`, `x1c`, `x1e`, `a1`, `a1mini`, `h2d`, `h2s`, `h2c`, `x2d`. **Required for safe operation** -- determines the correct G-code generation. Alias: `BAMBU_PRINTER_MODEL`. If omitted and the MCP client supports elicitation, the server will ask you interactively. Use `h2c` for H2C and `x2d` for X2D; do not use `h2d` as a fallback. |
|
|
231
232
|
| `BED_TYPE` | `textured_plate` | No | Bed plate type: `textured_plate`, `cool_plate`, `engineering_plate`, `hot_plate`, `supertack_plate` |
|
|
232
233
|
| `NOZZLE_DIAMETER` | `0.4` | No | Nozzle diameter in mm. Used to select the correct BambuStudio machine preset. |
|
|
233
234
|
| `SLICER_TYPE` | `bambustudio` | No | Slicer to use for slicing operations |
|
|
@@ -1246,6 +1247,7 @@ When `slicer_type` is `bambustudio` (the default), these additional parameters a
|
|
|
1246
1247
|
| `skip_objects` | string | Object indices to skip, comma-separated (e.g. `"3,5,10"`) |
|
|
1247
1248
|
| `load_filaments` | string | Filament profile paths, semicolon-separated |
|
|
1248
1249
|
| `load_filament_ids` | string | Filament-to-object mapping, comma-separated |
|
|
1250
|
+
| `filament_colours` | string | Slot colours, one `#RRGGBB` per filament slot, semicolon-separated. Explicit values take priority, followed by input 3MF colours, each custom profile's colour, then the BambuStudio default. |
|
|
1249
1251
|
| `enable_timelapse` | boolean | Enable timelapse-aware slicing |
|
|
1250
1252
|
| `allow_mix_temp` | boolean | Allow mixed-temperature filaments on one plate |
|
|
1251
1253
|
| `scale` | number | Uniform scale factor |
|
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(
|
package/src/printers/bambu.ts
CHANGED
|
@@ -47,6 +47,7 @@ const MODEL_ID_TO_NAME: Record<string, string> = {
|
|
|
47
47
|
O1D: "H2D",
|
|
48
48
|
O1E: "H2D Pro",
|
|
49
49
|
O1S: "H2S",
|
|
50
|
+
N6: "X2D",
|
|
50
51
|
N2S: "A1",
|
|
51
52
|
A1M: "A1 Mini",
|
|
52
53
|
C11: "P1P",
|
|
@@ -58,6 +59,15 @@ const MODEL_ID_TO_NAME: Record<string, string> = {
|
|
|
58
59
|
|
|
59
60
|
const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s"]);
|
|
60
61
|
|
|
62
|
+
export function assertDirectPrintSupported(model: string | undefined, serial?: string): void {
|
|
63
|
+
if (model?.trim().toLowerCase() === "x2d" || serial?.trim().toUpperCase().startsWith("20P")) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"X2D direct printing is not supported pending native eMMC transport support. " +
|
|
66
|
+
"X2D status and slicing remain available; use a supported slicer to print."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
function isH2ModelName(model: unknown): boolean {
|
|
62
72
|
return H2_MODEL_NAMES.has(String(model ?? "").trim().toLowerCase().replace(/\s+/g, " "));
|
|
63
73
|
}
|
|
@@ -151,6 +161,7 @@ class TolerantBambuClient extends BambuClient {
|
|
|
151
161
|
if (sn.startsWith("094")) return "H2D";
|
|
152
162
|
if (sn.startsWith("239")) return "H2C";
|
|
153
163
|
if (sn.startsWith("31B")) return "H2DPRO";
|
|
164
|
+
if (sn.startsWith("20P")) return "X2D";
|
|
154
165
|
if (sn.startsWith("00M")) return "X1C";
|
|
155
166
|
if (sn.startsWith("00W")) return "X1";
|
|
156
167
|
if (sn.startsWith("03W")) return "X1E";
|
|
@@ -568,6 +579,7 @@ export class BambuImplementation {
|
|
|
568
579
|
token: string,
|
|
569
580
|
options: BambuPrintOptionsInternal
|
|
570
581
|
): Promise<any> {
|
|
582
|
+
assertDirectPrintSupported(options.bambuModel, serial);
|
|
571
583
|
if (!options.filePath.toLowerCase().endsWith(".3mf")) {
|
|
572
584
|
throw new Error("print3mf requires a .3mf input file.");
|
|
573
585
|
}
|
|
@@ -1159,8 +1171,10 @@ export class BambuImplementation {
|
|
|
1159
1171
|
token: string,
|
|
1160
1172
|
filePath: string,
|
|
1161
1173
|
filename: string,
|
|
1162
|
-
print: boolean
|
|
1174
|
+
print: boolean,
|
|
1175
|
+
bambuModel?: string
|
|
1163
1176
|
) {
|
|
1177
|
+
if (print) assertDirectPrintSupported(bambuModel, serial);
|
|
1164
1178
|
await fs.access(filePath);
|
|
1165
1179
|
|
|
1166
1180
|
const normalizedFileName = filename.replace(/^\/+/, "");
|
|
@@ -1180,7 +1194,7 @@ export class BambuImplementation {
|
|
|
1180
1194
|
|
|
1181
1195
|
if (print) {
|
|
1182
1196
|
if (remotePath.toLowerCase().endsWith(".gcode")) {
|
|
1183
|
-
response.startResult = await this.startJob(host, serial, token, remotePath);
|
|
1197
|
+
response.startResult = await this.startJob(host, serial, token, remotePath, bambuModel);
|
|
1184
1198
|
} else if (remotePath.toLowerCase().endsWith(".3mf")) {
|
|
1185
1199
|
response.note =
|
|
1186
1200
|
"3MF upload complete. Use print_3mf to start a project print with plate and metadata options.";
|
|
@@ -1194,7 +1208,8 @@ export class BambuImplementation {
|
|
|
1194
1208
|
return response;
|
|
1195
1209
|
}
|
|
1196
1210
|
|
|
1197
|
-
async startJob(host: string, serial: string, token: string, filename: string) {
|
|
1211
|
+
async startJob(host: string, serial: string, token: string, filename: string, bambuModel?: string) {
|
|
1212
|
+
assertDirectPrintSupported(bambuModel, serial);
|
|
1198
1213
|
const lower = filename.toLowerCase();
|
|
1199
1214
|
if (lower.endsWith(".3mf") && !lower.endsWith(".gcode.3mf")) {
|
|
1200
1215
|
throw new Error("Use print_3mf for .3mf project files.");
|
|
@@ -1281,7 +1296,7 @@ export class BambuImplementation {
|
|
|
1281
1296
|
// P1/A1 series still use the proprietary TCP-on-6000 framed JPEG path
|
|
1282
1297
|
// (per https://github.com/Doridian/OpenBambuAPI/blob/main/video.md).
|
|
1283
1298
|
const TCP_CAMERA_MODELS = new Set(["a1", "a1mini", "p1s", "p1p"]);
|
|
1284
|
-
// X1, P2S,
|
|
1299
|
+
// X1, P2S, H2 (H2S/H2D/H2C), AND X2D all use RTSP on port 322. The
|
|
1285
1300
|
// OpenBambuAPI doc only mentions X1/P2S, but the HA bambulab
|
|
1286
1301
|
// integration's models.py shows the printer reports its own
|
|
1287
1302
|
// `ipcam.rtsp_url` for these models, and Parker (H2S) rejects the
|
|
@@ -1289,7 +1304,7 @@ export class BambuImplementation {
|
|
|
1289
1304
|
// confirmed by local H2 camera transport probes).
|
|
1290
1305
|
const RTSP_MODELS = new Set([
|
|
1291
1306
|
"x1", "x1c", "x1carbon", "x1e", "p2s",
|
|
1292
|
-
"h2", "h2s", "h2d", "h2c", "h2dpro",
|
|
1307
|
+
"h2", "h2s", "h2d", "h2c", "h2dpro", "x2d",
|
|
1293
1308
|
]);
|
|
1294
1309
|
|
|
1295
1310
|
if (!model) {
|
|
@@ -78,6 +78,14 @@ export interface FlattenOptions {
|
|
|
78
78
|
process?: Record<string, unknown>;
|
|
79
79
|
filaments?: (Record<string, unknown> | undefined)[];
|
|
80
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* Positional `#RRGGBB` colour per filament slot (e.g. from the input 3MF
|
|
83
|
+
* project or the caller). Missing entries keep the profile's own colour or
|
|
84
|
+
* fall back to DEFAULT_FILAMENT_COLOUR.
|
|
85
|
+
*/
|
|
86
|
+
filamentColours?: (string | undefined)[];
|
|
87
|
+
/** Saved per-plate positions, used only when the process sets no position. */
|
|
88
|
+
projectTowerPosition?: { wipe_tower_x?: unknown; wipe_tower_y?: unknown };
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
interface IndexedProfile {
|
|
@@ -308,6 +316,91 @@ function enforceMatchingNozzles(arr: unknown[]): void {
|
|
|
308
316
|
}
|
|
309
317
|
|
|
310
318
|
/** Best-effort extruder count for fallback nozzle_volume_type sizing. */
|
|
319
|
+
/** BambuStudio's built-in filament_colour default. */
|
|
320
|
+
export const DEFAULT_FILAMENT_COLOUR = "#00AE42";
|
|
321
|
+
const FILAMENT_COLOUR_RE = /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* No BBL filament profile defines `filament_colour`; the GUI fills it from
|
|
325
|
+
* the AMS. Without it the CLI keeps a single-entry colour vector however many
|
|
326
|
+
* filaments are loaded and crashes (access violation) as soon as a slice uses
|
|
327
|
+
* any filament after the first. Give every slot exactly one colour.
|
|
328
|
+
*/
|
|
329
|
+
function ensureFilamentColour(flat: Record<string, unknown>, colour?: string): void {
|
|
330
|
+
if (colour !== undefined) {
|
|
331
|
+
if (!FILAMENT_COLOUR_RE.test(colour)) {
|
|
332
|
+
throw new Error(`Invalid filament colour "${colour}"; expected #RRGGBB.`);
|
|
333
|
+
}
|
|
334
|
+
flat["filament_colour"] = [colour];
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const own = flat["filament_colour"];
|
|
338
|
+
if (Array.isArray(own) && typeof own[0] === "string" && FILAMENT_COLOUR_RE.test(own[0])) {
|
|
339
|
+
flat["filament_colour"] = [own[0]];
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
flat["filament_colour"] = [DEFAULT_FILAMENT_COLOUR];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// BambuStudio's defaults when a process sets no prime tower position/width.
|
|
346
|
+
const DEFAULT_WIPE_TOWER_X = 15;
|
|
347
|
+
const DEFAULT_WIPE_TOWER_Y = 220;
|
|
348
|
+
const DEFAULT_PRIME_TOWER_WIDTH = 35;
|
|
349
|
+
// Clearance inside the shared nozzle area for the auto-sized tower brim and
|
|
350
|
+
// the CLI's own safety margin (X2D fails at 5 mm from the edge, passes at 6).
|
|
351
|
+
const WIPE_TOWER_MARGIN = 15;
|
|
352
|
+
|
|
353
|
+
interface Rect { minX: number; maxX: number; minY: number; maxY: number }
|
|
354
|
+
|
|
355
|
+
function parseArea(area: string): Rect {
|
|
356
|
+
const points = area.split(",").map((p) => p.trim().split("x").map(Number));
|
|
357
|
+
if (points.length < 3 || points.some((p) => p.length !== 2 || p.some((n) => !Number.isFinite(n)))) {
|
|
358
|
+
throw new Error(`Malformed extruder_printable_area entry "${area}".`);
|
|
359
|
+
}
|
|
360
|
+
const xs = points.map((p) => p[0]);
|
|
361
|
+
const ys = points.map((p) => p[1]);
|
|
362
|
+
return { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* On multi-nozzle machines (H2D, X2D) each nozzle reaches only part of the
|
|
367
|
+
* bed (`extruder_printable_area`), and the prime tower must be reachable by
|
|
368
|
+
* every nozzle. The CLI's default tower position (x=15) lies outside the
|
|
369
|
+
* X2D/H2D right-nozzle area, so slicing fails with "G-code outside of the
|
|
370
|
+
* printable area". When the process sets no position and the default does
|
|
371
|
+
* not fit, move the tower inside the area all nozzles share.
|
|
372
|
+
*/
|
|
373
|
+
function placePrimeTowerForAllNozzles(
|
|
374
|
+
machineFlat: Record<string, unknown>,
|
|
375
|
+
processFlat: Record<string, unknown>
|
|
376
|
+
): void {
|
|
377
|
+
if (processFlat["wipe_tower_x"] !== undefined || processFlat["wipe_tower_y"] !== undefined) return;
|
|
378
|
+
if (["0", "false"].includes(String(processFlat["enable_prime_tower"]).toLowerCase())) return;
|
|
379
|
+
const areas = machineFlat["extruder_printable_area"];
|
|
380
|
+
if (!Array.isArray(areas) || areas.length < 2) return;
|
|
381
|
+
const rects = areas.map((a) => {
|
|
382
|
+
if (typeof a !== "string") throw new Error("Malformed extruder_printable_area in machine profile.");
|
|
383
|
+
return parseArea(a);
|
|
384
|
+
});
|
|
385
|
+
const shared: Rect = {
|
|
386
|
+
minX: Math.max(...rects.map((r) => r.minX)) + WIPE_TOWER_MARGIN,
|
|
387
|
+
maxX: Math.min(...rects.map((r) => r.maxX)) - WIPE_TOWER_MARGIN,
|
|
388
|
+
minY: Math.max(...rects.map((r) => r.minY)) + WIPE_TOWER_MARGIN,
|
|
389
|
+
maxY: Math.min(...rects.map((r) => r.maxY)) - WIPE_TOWER_MARGIN,
|
|
390
|
+
};
|
|
391
|
+
const width = Number(processFlat["prime_tower_width"] ?? DEFAULT_PRIME_TOWER_WIDTH);
|
|
392
|
+
const size = Number.isFinite(width) && width > 0 ? width : DEFAULT_PRIME_TOWER_WIDTH;
|
|
393
|
+
if (shared.maxX - shared.minX < size || shared.maxY - shared.minY < size) {
|
|
394
|
+
throw new Error("No bed area is reachable by every nozzle for the prime tower.");
|
|
395
|
+
}
|
|
396
|
+
const clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi);
|
|
397
|
+
const x = clamp(DEFAULT_WIPE_TOWER_X, shared.minX, shared.maxX - size);
|
|
398
|
+
const y = clamp(DEFAULT_WIPE_TOWER_Y, shared.minY, shared.maxY - size);
|
|
399
|
+
if (x === DEFAULT_WIPE_TOWER_X && y === DEFAULT_WIPE_TOWER_Y) return;
|
|
400
|
+
processFlat["wipe_tower_x"] = [String(x)];
|
|
401
|
+
processFlat["wipe_tower_y"] = [String(y)];
|
|
402
|
+
}
|
|
403
|
+
|
|
311
404
|
function inferExtruderCount(flat: Record<string, unknown>): number {
|
|
312
405
|
for (const key of ["nozzle_diameter", "extruder_type", "extruder_variant_list"]) {
|
|
313
406
|
const v = flat[key];
|
|
@@ -586,7 +679,17 @@ export async function flattenForCli(opts: FlattenOptions): Promise<FlattenedProf
|
|
|
586
679
|
normalizeForCli(machineFlat, "machine", opts.machineLeaf);
|
|
587
680
|
normalizeForCli(processFlat, "process", opts.processLeaf);
|
|
588
681
|
filamentFlats.forEach((f, i) => normalizeForCli(f, "filament", opts.filamentLeaves[i]));
|
|
682
|
+
if (opts.filamentColours && opts.filamentColours.length !== filamentFlats.length) {
|
|
683
|
+
throw new Error(
|
|
684
|
+
`${opts.filamentColours.length} filament colours were supplied for ${filamentFlats.length} filament slots.`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
filamentFlats.forEach((f, i) => ensureFilamentColour(f, opts.filamentColours?.[i]));
|
|
589
688
|
applyBedType(processFlat, opts.bedType);
|
|
689
|
+
if (processFlat["wipe_tower_x"] === undefined && processFlat["wipe_tower_y"] === undefined) {
|
|
690
|
+
Object.assign(processFlat, opts.projectTowerPosition);
|
|
691
|
+
}
|
|
692
|
+
placePrimeTowerForAllNozzles(machineFlat, processFlat);
|
|
590
693
|
|
|
591
694
|
// Mirror the GUI's auto-extend behavior: when the caller explicitly
|
|
592
695
|
// chose a process or filament that wasn't pre-declared compatible with
|
|
@@ -162,6 +162,7 @@ export interface BambuSliceOptions {
|
|
|
162
162
|
skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
|
|
163
163
|
loadFilaments?: string; // --load-filaments "f1.json;f2.json": filament profiles
|
|
164
164
|
loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
|
|
165
|
+
filamentColours?: string[]; // positional #RRGGBB per loaded filament slot
|
|
165
166
|
bedType?: string; // Bambu bed type, e.g. textured_plate or cool_plate
|
|
166
167
|
enableTimelapse?: boolean; // --enable-timelapse: timelapse-aware slicing
|
|
167
168
|
allowMixTemp?: boolean; // --allow-mix-temp: allow mixed-temp filaments
|
|
@@ -177,6 +178,10 @@ export interface BambuSliceOptions {
|
|
|
177
178
|
interface BambuSettingsBundle {
|
|
178
179
|
settingsArg?: string;
|
|
179
180
|
filamentPaths: string[];
|
|
181
|
+
/** Positional slot colours declared by the input 3MF project. */
|
|
182
|
+
filamentColours?: string[];
|
|
183
|
+
/** Saved per-plate tower coordinates must survive default process loading. */
|
|
184
|
+
projectTowerPosition?: { wipe_tower_x?: unknown; wipe_tower_y?: unknown };
|
|
180
185
|
/** Identity provenance before the process is merged into a generated file. */
|
|
181
186
|
processSource?: { filePath: string; profile: Record<string, unknown> };
|
|
182
187
|
}
|
|
@@ -534,21 +539,32 @@ export class STLManipulator extends EventEmitter {
|
|
|
534
539
|
profilesRoot,
|
|
535
540
|
tempDir: this.tempDir,
|
|
536
541
|
bedType: this.resolveBambuStudioBedType(bambuOptions?.bedType),
|
|
542
|
+
filamentColours: bambuOptions?.filamentColours ?? bundle.filamentColours,
|
|
543
|
+
projectTowerPosition: bundle.projectTowerPosition,
|
|
537
544
|
sourceProfiles: {
|
|
538
545
|
machine: isBundledFile(machinePath) ? undefined : machine,
|
|
539
546
|
process: isBundledFile(processPath) ? undefined : processProfile,
|
|
540
547
|
filaments: filaments.map((profile, i) => isBundledFile(bundle.filamentPaths[i]) ? undefined : profile),
|
|
541
548
|
},
|
|
542
549
|
});
|
|
543
|
-
// Standalone
|
|
544
|
-
//
|
|
550
|
+
// Standalone files retain their own identity and settings, but still need
|
|
551
|
+
// the CLI colour/tower fields. Do not discard those required overlays when
|
|
552
|
+
// choosing the original profile instead of the normalized BBL output.
|
|
553
|
+
const withCliFields = (filePath: string, profile: Record<string, unknown>, flatPath: string, keys: string[]): string => {
|
|
554
|
+
const flattened = readProfile(flatPath);
|
|
555
|
+
const fields = Object.fromEntries(keys.filter(key => flattened[key] !== undefined).map(key => [key, flattened[key]]));
|
|
556
|
+
if (Object.entries(fields).every(([key, value]) => JSON.stringify(profile[key]) === JSON.stringify(value))) return filePath;
|
|
557
|
+
return this.writeTempJson(path.basename(filePath, '.json'), 'cli', { ...profile, ...fields });
|
|
558
|
+
};
|
|
545
559
|
return {
|
|
546
560
|
settingsArg: [
|
|
547
561
|
needsResolution(machinePath, machine) ? flat.machinePath : machinePath,
|
|
548
|
-
needsResolution(processPath, processProfile) ? flat.processPath
|
|
562
|
+
needsResolution(processPath, processProfile) ? flat.processPath
|
|
563
|
+
: withCliFields(processPath, processProfile, flat.processPath, ['wipe_tower_x', 'wipe_tower_y']),
|
|
549
564
|
].join(';'),
|
|
550
565
|
filamentPaths: filaments.map((profile, i) =>
|
|
551
|
-
needsResolution(bundle.filamentPaths[i], profile) ? flat.filamentPaths[i]
|
|
566
|
+
needsResolution(bundle.filamentPaths[i], profile) ? flat.filamentPaths[i]
|
|
567
|
+
: withCliFields(bundle.filamentPaths[i], profile, flat.filamentPaths[i], ['filament_colour'])),
|
|
552
568
|
};
|
|
553
569
|
}
|
|
554
570
|
|
|
@@ -571,13 +587,20 @@ export class STLManipulator extends EventEmitter {
|
|
|
571
587
|
const count = Math.max(0, ...['filament_settings_id', 'filament_type', 'filament_colour', 'filament_diameter']
|
|
572
588
|
.map(key => Array.isArray(data[key]) ? data[key].length : 0));
|
|
573
589
|
if (count === 0) throw new Error('Cannot determine project filament slots from project_settings.config.');
|
|
590
|
+
// Keep the project's slot colours; loaded profiles carry none of their own.
|
|
591
|
+
const colours = Array.isArray(data.filament_colour) && data.filament_colour.length === count &&
|
|
592
|
+
data.filament_colour.every(c => typeof c === 'string' && /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(c))
|
|
593
|
+
? data.filament_colour as string[]
|
|
594
|
+
: undefined;
|
|
595
|
+
const projectTowerPosition = Object.fromEntries(['wipe_tower_x', 'wipe_tower_y']
|
|
596
|
+
.filter(key => data[key] !== undefined).map(key => [key, data[key]]));
|
|
574
597
|
if (bundle.filamentPaths.length === 1) {
|
|
575
|
-
return { ...bundle, filamentPaths: Array(count).fill(bundle.filamentPaths[0]) };
|
|
598
|
+
return { ...bundle, filamentPaths: Array(count).fill(bundle.filamentPaths[0]), filamentColours: colours, projectTowerPosition };
|
|
576
599
|
}
|
|
577
600
|
if (bundle.filamentPaths.length !== count) {
|
|
578
601
|
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.`);
|
|
579
602
|
}
|
|
580
|
-
return bundle;
|
|
603
|
+
return { ...bundle, filamentColours: colours, projectTowerPosition };
|
|
581
604
|
}
|
|
582
605
|
|
|
583
606
|
private resolveBambuStudioBedType(bedType?: string): string | undefined {
|