bambu-printer-mcp 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/index.js +64 -3
- package/dist/stl/stl-manipulator.d.ts +32 -1
- package/dist/stl/stl-manipulator.js +134 -8
- package/package.json +1 -1
- package/src/index.ts +51 -4
- package/src/stl/stl-manipulator.ts +176 -12
package/README.md
CHANGED
|
@@ -716,6 +716,53 @@ Slice an STL or 3MF file using an external slicer and return the path to the out
|
|
|
716
716
|
|
|
717
717
|
For printing on a Bambu printer, the recommended workflow is: slice with `bambustudio` to get a sliced 3MF, then pass that output path to `print_3mf`.
|
|
718
718
|
|
|
719
|
+
#### BambuStudio Slicer Options
|
|
720
|
+
|
|
721
|
+
When `slicer_type` is `bambustudio` (the default), these additional parameters are available on `slice_stl`:
|
|
722
|
+
|
|
723
|
+
| Parameter | Type | Description |
|
|
724
|
+
|-----------|------|-------------|
|
|
725
|
+
| `uptodate` | boolean | Update 3MF configs to latest BambuStudio presets |
|
|
726
|
+
| `repetitions` | number | Number of copies to print |
|
|
727
|
+
| `orient` | boolean | Auto-orient model for optimal printability |
|
|
728
|
+
| `arrange` | boolean | Auto-arrange objects on the build plate |
|
|
729
|
+
| `ensure_on_bed` | boolean | Lift floating models onto the bed |
|
|
730
|
+
| `clone_objects` | string | Clone counts per object, comma-separated (e.g. `"1,3,1,10"`) |
|
|
731
|
+
| `skip_objects` | string | Object indices to skip, comma-separated (e.g. `"3,5,10"`) |
|
|
732
|
+
| `load_filaments` | string | Filament profile paths, semicolon-separated |
|
|
733
|
+
| `load_filament_ids` | string | Filament-to-object mapping, comma-separated |
|
|
734
|
+
| `enable_timelapse` | boolean | Enable timelapse-aware slicing |
|
|
735
|
+
| `allow_mix_temp` | boolean | Allow mixed-temperature filaments on one plate |
|
|
736
|
+
| `scale` | number | Uniform scale factor |
|
|
737
|
+
| `rotate` | number | Z-axis rotation in degrees |
|
|
738
|
+
| `rotate_x` | number | X-axis rotation in degrees |
|
|
739
|
+
| `rotate_y` | number | Y-axis rotation in degrees |
|
|
740
|
+
| `min_save` | boolean | Produce smaller output 3MF (faster uploads) |
|
|
741
|
+
| `skip_modified_gcodes` | boolean | Ignore stale custom gcodes in the 3MF |
|
|
742
|
+
| `slice_plate` | number | Which plate to slice (0 = all plates, default: 0) |
|
|
743
|
+
|
|
744
|
+
**Example: Slice with auto-orient and 3 copies**
|
|
745
|
+
```json
|
|
746
|
+
{
|
|
747
|
+
"stl_path": "/path/to/model.stl",
|
|
748
|
+
"bambu_model": "p1s",
|
|
749
|
+
"orient": true,
|
|
750
|
+
"arrange": true,
|
|
751
|
+
"repetitions": 3
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
#### Smart Defaults (print_3mf auto-slice)
|
|
756
|
+
|
|
757
|
+
When `print_3mf` detects an unsliced 3MF and auto-slices it, these defaults are applied automatically:
|
|
758
|
+
|
|
759
|
+
- `uptodate: true` -- prevents stale config bugs from downloaded 3MFs
|
|
760
|
+
- `ensure_on_bed: true` -- safety net, lifts floating models onto the bed
|
|
761
|
+
- `min_save: true` -- smaller output for faster FTP uploads to the printer
|
|
762
|
+
- `skip_modified_gcodes: true` -- strips custom gcodes from other users' profiles
|
|
763
|
+
|
|
764
|
+
These defaults keep you safe when printing downloaded models. When calling `slice_stl` directly, you have full control over every flag.
|
|
765
|
+
|
|
719
766
|
</details>
|
|
720
767
|
|
|
721
768
|
<details>
|
package/dist/index.js
CHANGED
|
@@ -333,7 +333,25 @@ class BambuPrinterMCPServer {
|
|
|
333
333
|
},
|
|
334
334
|
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
335
335
|
slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
|
|
336
|
-
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" }
|
|
336
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
337
|
+
uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest BambuStudio version. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
|
|
338
|
+
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
339
|
+
orient: { type: "boolean", description: "Auto-orient the model for optimal printability (minimize supports, maximize bed adhesion). Recommended for raw STL imports that lack a pre-set orientation." },
|
|
340
|
+
arrange: { type: "boolean", description: "Auto-arrange all objects on the build plate with optimal spacing. Recommended when importing STLs or adding multiple objects. Set false to preserve existing plate layout." },
|
|
341
|
+
ensure_on_bed: { type: "boolean", description: "Detect models floating above the bed and lower them onto the build surface. Safety net for imported models with incorrect Z origins." },
|
|
342
|
+
clone_objects: { type: "string", description: "Duplicate specific objects on the plate. Comma-separated clone counts per object index, e.g. '1,3,1,10' clones object 0 once, object 1 three times, etc." },
|
|
343
|
+
skip_objects: { type: "string", description: "Skip specific objects during slicing by index. Comma-separated, e.g. '3,5,10'. Useful for multi-object 3MFs where you only want to print some parts." },
|
|
344
|
+
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
345
|
+
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." },
|
|
346
|
+
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." },
|
|
347
|
+
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." },
|
|
348
|
+
scale: { type: "number", description: "Uniform scale factor applied to all axes. 1.0 = original size, 2.0 = double, 0.5 = half. Applied before slicing." },
|
|
349
|
+
rotate: { type: "number", description: "Rotate the model around the Z-axis (vertical) by this many degrees before slicing. Positive = counterclockwise when viewed from above." },
|
|
350
|
+
rotate_x: { type: "number", description: "Rotate the model around the X-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
|
|
351
|
+
rotate_y: { type: "number", description: "Rotate the model around the Y-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
|
|
352
|
+
min_save: { type: "boolean", description: "Write a smaller output 3MF by omitting non-essential metadata. Reduces file size for faster FTP upload to the printer." },
|
|
353
|
+
skip_modified_gcodes: { type: "boolean", description: "Strip custom start/end gcodes embedded in the 3MF. Recommended for downloaded 3MFs since custom gcodes from other users' profiles may be unsafe for your printer." },
|
|
354
|
+
slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates (default). Use 1, 2, etc. to slice only a specific plate in multi-plate 3MF projects." }
|
|
337
355
|
},
|
|
338
356
|
required: ["stl_path", "bambu_model"]
|
|
339
357
|
}
|
|
@@ -599,8 +617,45 @@ class BambuPrinterMCPServer {
|
|
|
599
617
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
600
618
|
// Resolve printer preset for BambuStudio slicer
|
|
601
619
|
const printerPreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
|
|
620
|
+
const sliceBambuOptions = {};
|
|
621
|
+
if (args?.uptodate !== undefined)
|
|
622
|
+
sliceBambuOptions.uptodate = Boolean(args.uptodate);
|
|
623
|
+
if (args?.repetitions !== undefined)
|
|
624
|
+
sliceBambuOptions.repetitions = Number(args.repetitions);
|
|
625
|
+
if (args?.orient !== undefined)
|
|
626
|
+
sliceBambuOptions.orient = Boolean(args.orient);
|
|
627
|
+
if (args?.arrange !== undefined)
|
|
628
|
+
sliceBambuOptions.arrange = Boolean(args.arrange);
|
|
629
|
+
if (args?.ensure_on_bed !== undefined)
|
|
630
|
+
sliceBambuOptions.ensureOnBed = Boolean(args.ensure_on_bed);
|
|
631
|
+
if (args?.clone_objects !== undefined)
|
|
632
|
+
sliceBambuOptions.cloneObjects = String(args.clone_objects);
|
|
633
|
+
if (args?.skip_objects !== undefined)
|
|
634
|
+
sliceBambuOptions.skipObjects = String(args.skip_objects);
|
|
635
|
+
if (args?.load_filaments !== undefined)
|
|
636
|
+
sliceBambuOptions.loadFilaments = String(args.load_filaments);
|
|
637
|
+
if (args?.load_filament_ids !== undefined)
|
|
638
|
+
sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
639
|
+
if (args?.enable_timelapse !== undefined)
|
|
640
|
+
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
641
|
+
if (args?.allow_mix_temp !== undefined)
|
|
642
|
+
sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
643
|
+
if (args?.scale !== undefined)
|
|
644
|
+
sliceBambuOptions.scale = Number(args.scale);
|
|
645
|
+
if (args?.rotate !== undefined)
|
|
646
|
+
sliceBambuOptions.rotate = Number(args.rotate);
|
|
647
|
+
if (args?.rotate_x !== undefined)
|
|
648
|
+
sliceBambuOptions.rotateX = Number(args.rotate_x);
|
|
649
|
+
if (args?.rotate_y !== undefined)
|
|
650
|
+
sliceBambuOptions.rotateY = Number(args.rotate_y);
|
|
651
|
+
if (args?.min_save !== undefined)
|
|
652
|
+
sliceBambuOptions.minSave = Boolean(args.min_save);
|
|
653
|
+
if (args?.skip_modified_gcodes !== undefined)
|
|
654
|
+
sliceBambuOptions.skipModifiedGcodes = Boolean(args.skip_modified_gcodes);
|
|
655
|
+
if (args?.slice_plate !== undefined)
|
|
656
|
+
sliceBambuOptions.slicePlate = Number(args.slice_plate);
|
|
602
657
|
result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
|
|
603
|
-
printerPreset);
|
|
658
|
+
printerPreset, sliceBambuOptions);
|
|
604
659
|
break;
|
|
605
660
|
}
|
|
606
661
|
case "print_3mf": {
|
|
@@ -623,8 +678,14 @@ class BambuPrinterMCPServer {
|
|
|
623
678
|
const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
|
|
624
679
|
if (!hasGcode) {
|
|
625
680
|
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
681
|
+
const autoSliceOptions = {
|
|
682
|
+
uptodate: true,
|
|
683
|
+
ensureOnBed: true,
|
|
684
|
+
minSave: true,
|
|
685
|
+
skipModifiedGcodes: true,
|
|
686
|
+
};
|
|
626
687
|
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
|
|
627
|
-
printPreset);
|
|
688
|
+
printPreset, autoSliceOptions);
|
|
628
689
|
console.log("Auto-sliced to: " + threeMFPath);
|
|
629
690
|
}
|
|
630
691
|
}
|
|
@@ -22,6 +22,26 @@ export type TransformationParams = {
|
|
|
22
22
|
relative?: boolean;
|
|
23
23
|
selectionBounds?: THREE.Box3;
|
|
24
24
|
};
|
|
25
|
+
export interface BambuSliceOptions {
|
|
26
|
+
uptodate?: boolean;
|
|
27
|
+
repetitions?: number;
|
|
28
|
+
orient?: boolean;
|
|
29
|
+
arrange?: boolean;
|
|
30
|
+
ensureOnBed?: boolean;
|
|
31
|
+
cloneObjects?: string;
|
|
32
|
+
skipObjects?: string;
|
|
33
|
+
loadFilaments?: string;
|
|
34
|
+
loadFilamentIds?: string;
|
|
35
|
+
enableTimelapse?: boolean;
|
|
36
|
+
allowMixTemp?: boolean;
|
|
37
|
+
scale?: number;
|
|
38
|
+
rotate?: number;
|
|
39
|
+
rotateX?: number;
|
|
40
|
+
rotateY?: number;
|
|
41
|
+
minSave?: boolean;
|
|
42
|
+
skipModifiedGcodes?: boolean;
|
|
43
|
+
slicePlate?: number;
|
|
44
|
+
}
|
|
25
45
|
export declare class STLManipulator extends EventEmitter {
|
|
26
46
|
private tempDir;
|
|
27
47
|
private activeOperations;
|
|
@@ -48,7 +68,17 @@ export declare class STLManipulator extends EventEmitter {
|
|
|
48
68
|
boundingBox: BoundingBox;
|
|
49
69
|
vertexCount: number;
|
|
50
70
|
faceCount: number;
|
|
71
|
+
objects?: Array<{
|
|
72
|
+
name: string;
|
|
73
|
+
vertexCount: number;
|
|
74
|
+
faceCount: number;
|
|
75
|
+
boundingBox: BoundingBox;
|
|
76
|
+
}>;
|
|
51
77
|
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Extract mesh info from a 3MF file by parsing the XML object models.
|
|
80
|
+
*/
|
|
81
|
+
private get3MFInfo;
|
|
52
82
|
/**
|
|
53
83
|
* Scale an STL model uniformly or along specific axes
|
|
54
84
|
*/
|
|
@@ -97,9 +127,10 @@ export declare class STLManipulator extends EventEmitter {
|
|
|
97
127
|
* @param slicerProfile Optional path to the slicer profile/config file
|
|
98
128
|
* @param progressCallback Optional callback for progress updates
|
|
99
129
|
* @param printerPreset Optional BambuStudio printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
|
|
130
|
+
* @param bambuOptions Optional BambuStudio-specific CLI flags
|
|
100
131
|
* @returns Path to the generated G-code or sliced 3MF file
|
|
101
132
|
*/
|
|
102
|
-
sliceSTL(stlFilePath: string, slicerType: 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio', slicerPath: string, slicerProfile?: string, progressCallback?: ProgressCallback, printerPreset?: string): Promise<string>;
|
|
133
|
+
sliceSTL(stlFilePath: string, slicerType: 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio', slicerPath: string, slicerProfile?: string, progressCallback?: ProgressCallback, printerPreset?: string, bambuOptions?: BambuSliceOptions): Promise<string>;
|
|
103
134
|
/**
|
|
104
135
|
* Enhanced version of confirmTemperatures with better error handling
|
|
105
136
|
* @param gcodePath Path to the G-code file
|
|
@@ -85,8 +85,12 @@ export class STLManipulator extends EventEmitter {
|
|
|
85
85
|
*/
|
|
86
86
|
async getSTLInfo(stlFilePath) {
|
|
87
87
|
try {
|
|
88
|
-
const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
|
|
89
88
|
const fileStats = fs.statSync(stlFilePath);
|
|
89
|
+
// Handle 3MF files: extract mesh data from embedded object models
|
|
90
|
+
if (stlFilePath.toLowerCase().endsWith('.3mf')) {
|
|
91
|
+
return await this.get3MFInfo(stlFilePath, fileStats.size);
|
|
92
|
+
}
|
|
93
|
+
const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
|
|
90
94
|
// Count faces (each face is a triangle in STL)
|
|
91
95
|
const positionAttribute = geometry.getAttribute('position');
|
|
92
96
|
const vertexCount = positionAttribute.count;
|
|
@@ -115,6 +119,89 @@ export class STLManipulator extends EventEmitter {
|
|
|
115
119
|
throw new Error(`Failed to get STL info: ${error instanceof Error ? error.message : String(error)}`);
|
|
116
120
|
}
|
|
117
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Extract mesh info from a 3MF file by parsing the XML object models.
|
|
124
|
+
*/
|
|
125
|
+
async get3MFInfo(filePath, fileSize) {
|
|
126
|
+
const JSZip = (await import('jszip')).default;
|
|
127
|
+
const data = fs.readFileSync(filePath);
|
|
128
|
+
const zip = await JSZip.loadAsync(data);
|
|
129
|
+
// Find all object model files
|
|
130
|
+
const objectFiles = Object.keys(zip.files).filter((name) => /^3D\/Objects\/.*\.model$/i.test(name));
|
|
131
|
+
const overallBox = new THREE.Box3(new THREE.Vector3(Infinity, Infinity, Infinity), new THREE.Vector3(-Infinity, -Infinity, -Infinity));
|
|
132
|
+
let totalVerts = 0;
|
|
133
|
+
let totalFaces = 0;
|
|
134
|
+
const objects = [];
|
|
135
|
+
for (const objFile of objectFiles) {
|
|
136
|
+
const content = await zip.file(objFile).async('string');
|
|
137
|
+
// Parse vertices from XML: <vertex x="..." y="..." z="..."/>
|
|
138
|
+
const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
|
|
139
|
+
const triangleRegex = /<(?:\w+:)?triangle\s/g;
|
|
140
|
+
const objBox = new THREE.Box3(new THREE.Vector3(Infinity, Infinity, Infinity), new THREE.Vector3(-Infinity, -Infinity, -Infinity));
|
|
141
|
+
let vCount = 0;
|
|
142
|
+
let match;
|
|
143
|
+
while ((match = vertexRegex.exec(content)) !== null) {
|
|
144
|
+
const x = parseFloat(match[1]);
|
|
145
|
+
const y = parseFloat(match[2]);
|
|
146
|
+
const z = parseFloat(match[3]);
|
|
147
|
+
objBox.expandByPoint(new THREE.Vector3(x, y, z));
|
|
148
|
+
overallBox.expandByPoint(new THREE.Vector3(x, y, z));
|
|
149
|
+
vCount++;
|
|
150
|
+
}
|
|
151
|
+
let fCount = 0;
|
|
152
|
+
while (triangleRegex.exec(content) !== null)
|
|
153
|
+
fCount++;
|
|
154
|
+
totalVerts += vCount;
|
|
155
|
+
totalFaces += fCount;
|
|
156
|
+
if (vCount > 0) {
|
|
157
|
+
const center = new THREE.Vector3();
|
|
158
|
+
objBox.getCenter(center);
|
|
159
|
+
const dims = new THREE.Vector3();
|
|
160
|
+
objBox.getSize(dims);
|
|
161
|
+
objects.push({
|
|
162
|
+
name: path.basename(objFile),
|
|
163
|
+
vertexCount: vCount,
|
|
164
|
+
faceCount: fCount,
|
|
165
|
+
boundingBox: { min: objBox.min, max: objBox.max, center, dimensions: dims },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// If no object files found, try the main 3dmodel.model
|
|
170
|
+
if (objects.length === 0) {
|
|
171
|
+
const mainModel = zip.file('3D/3dmodel.model');
|
|
172
|
+
if (mainModel) {
|
|
173
|
+
const content = await mainModel.async('string');
|
|
174
|
+
const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
|
|
175
|
+
const triangleRegex = /<(?:\w+:)?triangle\s/g;
|
|
176
|
+
let match;
|
|
177
|
+
while ((match = vertexRegex.exec(content)) !== null) {
|
|
178
|
+
const x = parseFloat(match[1]);
|
|
179
|
+
const y = parseFloat(match[2]);
|
|
180
|
+
const z = parseFloat(match[3]);
|
|
181
|
+
overallBox.expandByPoint(new THREE.Vector3(x, y, z));
|
|
182
|
+
totalVerts++;
|
|
183
|
+
}
|
|
184
|
+
while (triangleRegex.exec(content) !== null)
|
|
185
|
+
totalFaces++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (totalVerts === 0) {
|
|
189
|
+
throw new Error('No mesh data found in 3MF file. The archive may not contain any 3D objects.');
|
|
190
|
+
}
|
|
191
|
+
const center = new THREE.Vector3();
|
|
192
|
+
overallBox.getCenter(center);
|
|
193
|
+
const dimensions = new THREE.Vector3();
|
|
194
|
+
overallBox.getSize(dimensions);
|
|
195
|
+
return {
|
|
196
|
+
filePath,
|
|
197
|
+
fileName: path.basename(filePath),
|
|
198
|
+
fileSize,
|
|
199
|
+
boundingBox: { min: overallBox.min, max: overallBox.max, center, dimensions },
|
|
200
|
+
vertexCount: totalVerts,
|
|
201
|
+
faceCount: totalFaces,
|
|
202
|
+
objects,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
118
205
|
/**
|
|
119
206
|
* Scale an STL model uniformly or along specific axes
|
|
120
207
|
*/
|
|
@@ -651,9 +738,10 @@ export class STLManipulator extends EventEmitter {
|
|
|
651
738
|
* @param slicerProfile Optional path to the slicer profile/config file
|
|
652
739
|
* @param progressCallback Optional callback for progress updates
|
|
653
740
|
* @param printerPreset Optional BambuStudio printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
|
|
741
|
+
* @param bambuOptions Optional BambuStudio-specific CLI flags
|
|
654
742
|
* @returns Path to the generated G-code or sliced 3MF file
|
|
655
743
|
*/
|
|
656
|
-
async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset) {
|
|
744
|
+
async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset, bambuOptions) {
|
|
657
745
|
const operationId = this.generateOperationId();
|
|
658
746
|
this.activeOperations.set(operationId, true);
|
|
659
747
|
if (!fs.existsSync(slicerPath)) {
|
|
@@ -722,19 +810,57 @@ export class STLManipulator extends EventEmitter {
|
|
|
722
810
|
const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
|
|
723
811
|
const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
|
|
724
812
|
args = [
|
|
725
|
-
'--slice',
|
|
813
|
+
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
726
814
|
'--export-3mf', bambuOutputPath,
|
|
727
815
|
];
|
|
728
816
|
if (slicerProfile) {
|
|
729
817
|
args.push('--load-settings', slicerProfile);
|
|
730
818
|
}
|
|
731
|
-
//
|
|
732
|
-
|
|
733
|
-
//
|
|
734
|
-
//
|
|
819
|
+
// Always allow newer-version 3MF files (the CLI rejects them by default)
|
|
820
|
+
args.push('--allow-newer-file');
|
|
821
|
+
// If a printer preset name is given AND no explicit profile was provided,
|
|
822
|
+
// write a minimal settings JSON containing the printer_settings_id so
|
|
823
|
+
// BambuStudio resolves the correct machine profile from its built-in presets.
|
|
735
824
|
if (printerPreset && !slicerProfile) {
|
|
736
|
-
|
|
825
|
+
const presetJson = path.join(this.tempDir, '_printer_preset.json');
|
|
826
|
+
fs.writeFileSync(presetJson, JSON.stringify({ printer_settings_id: printerPreset }));
|
|
827
|
+
args.push('--load-settings', presetJson);
|
|
737
828
|
}
|
|
829
|
+
// BambuSliceOptions flags
|
|
830
|
+
if (bambuOptions?.uptodate)
|
|
831
|
+
args.push('--uptodate');
|
|
832
|
+
if (bambuOptions?.ensureOnBed)
|
|
833
|
+
args.push('--ensure-on-bed');
|
|
834
|
+
if (bambuOptions?.enableTimelapse)
|
|
835
|
+
args.push('--enable-timelapse');
|
|
836
|
+
if (bambuOptions?.allowMixTemp)
|
|
837
|
+
args.push('--allow-mix-temp');
|
|
838
|
+
if (bambuOptions?.minSave)
|
|
839
|
+
args.push('--min-save');
|
|
840
|
+
if (bambuOptions?.skipModifiedGcodes)
|
|
841
|
+
args.push('--skip-modified-gcodes');
|
|
842
|
+
if (bambuOptions?.orient !== undefined)
|
|
843
|
+
args.push('--orient', bambuOptions.orient ? '1' : '0');
|
|
844
|
+
if (bambuOptions?.arrange !== undefined)
|
|
845
|
+
args.push('--arrange', bambuOptions.arrange ? '1' : '0');
|
|
846
|
+
if (bambuOptions?.repetitions !== undefined)
|
|
847
|
+
args.push('--repetitions', String(bambuOptions.repetitions));
|
|
848
|
+
if (bambuOptions?.scale !== undefined)
|
|
849
|
+
args.push('--scale', String(bambuOptions.scale));
|
|
850
|
+
if (bambuOptions?.rotate !== undefined)
|
|
851
|
+
args.push('--rotate', String(bambuOptions.rotate));
|
|
852
|
+
if (bambuOptions?.rotateX !== undefined)
|
|
853
|
+
args.push('--rotate-x', String(bambuOptions.rotateX));
|
|
854
|
+
if (bambuOptions?.rotateY !== undefined)
|
|
855
|
+
args.push('--rotate-y', String(bambuOptions.rotateY));
|
|
856
|
+
if (bambuOptions?.cloneObjects)
|
|
857
|
+
args.push('--clone-objects', bambuOptions.cloneObjects);
|
|
858
|
+
if (bambuOptions?.skipObjects)
|
|
859
|
+
args.push('--skip-objects', bambuOptions.skipObjects);
|
|
860
|
+
if (bambuOptions?.loadFilaments)
|
|
861
|
+
args.push('--load-filaments', bambuOptions.loadFilaments);
|
|
862
|
+
if (bambuOptions?.loadFilamentIds)
|
|
863
|
+
args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
|
|
738
864
|
args.push(stlFilePath);
|
|
739
865
|
// Override outputFilePath for bambustudio since it produces 3MF, not gcode
|
|
740
866
|
outputFilePath = bambuOutputPath;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bambu-printer-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "MCP server for Bambu Lab 3D printers with STL manipulation, slicing, and print management",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": "https://github.com/DMontgomery40/bambu-printer-mcp",
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import fs from "fs";
|
|
|
16
16
|
import path from "path";
|
|
17
17
|
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
|
-
import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
19
|
+
import { STLManipulator, type BambuSliceOptions } from "./stl/stl-manipulator.js";
|
|
20
20
|
import { parse3MF } from './3mf_parser.js';
|
|
21
21
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
22
22
|
|
|
@@ -405,7 +405,25 @@ class BambuPrinterMCPServer {
|
|
|
405
405
|
},
|
|
406
406
|
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
407
407
|
slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
|
|
408
|
-
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" }
|
|
408
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
409
|
+
uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest BambuStudio version. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
|
|
410
|
+
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
411
|
+
orient: { type: "boolean", description: "Auto-orient the model for optimal printability (minimize supports, maximize bed adhesion). Recommended for raw STL imports that lack a pre-set orientation." },
|
|
412
|
+
arrange: { type: "boolean", description: "Auto-arrange all objects on the build plate with optimal spacing. Recommended when importing STLs or adding multiple objects. Set false to preserve existing plate layout." },
|
|
413
|
+
ensure_on_bed: { type: "boolean", description: "Detect models floating above the bed and lower them onto the build surface. Safety net for imported models with incorrect Z origins." },
|
|
414
|
+
clone_objects: { type: "string", description: "Duplicate specific objects on the plate. Comma-separated clone counts per object index, e.g. '1,3,1,10' clones object 0 once, object 1 three times, etc." },
|
|
415
|
+
skip_objects: { type: "string", description: "Skip specific objects during slicing by index. Comma-separated, e.g. '3,5,10'. Useful for multi-object 3MFs where you only want to print some parts." },
|
|
416
|
+
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
417
|
+
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." },
|
|
418
|
+
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." },
|
|
419
|
+
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." },
|
|
420
|
+
scale: { type: "number", description: "Uniform scale factor applied to all axes. 1.0 = original size, 2.0 = double, 0.5 = half. Applied before slicing." },
|
|
421
|
+
rotate: { type: "number", description: "Rotate the model around the Z-axis (vertical) by this many degrees before slicing. Positive = counterclockwise when viewed from above." },
|
|
422
|
+
rotate_x: { type: "number", description: "Rotate the model around the X-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
|
|
423
|
+
rotate_y: { type: "number", description: "Rotate the model around the Y-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
|
|
424
|
+
min_save: { type: "boolean", description: "Write a smaller output 3MF by omitting non-essential metadata. Reduces file size for faster FTP upload to the printer." },
|
|
425
|
+
skip_modified_gcodes: { type: "boolean", description: "Strip custom start/end gcodes embedded in the 3MF. Recommended for downloaded 3MFs since custom gcodes from other users' profiles may be unsafe for your printer." },
|
|
426
|
+
slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates (default). Use 1, 2, etc. to slice only a specific plate in multi-plate 3MF projects." }
|
|
409
427
|
},
|
|
410
428
|
required: ["stl_path", "bambu_model"]
|
|
411
429
|
}
|
|
@@ -703,11 +721,33 @@ class BambuPrinterMCPServer {
|
|
|
703
721
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
704
722
|
// Resolve printer preset for BambuStudio slicer
|
|
705
723
|
const printerPreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
|
|
724
|
+
|
|
725
|
+
const sliceBambuOptions: BambuSliceOptions = {};
|
|
726
|
+
if (args?.uptodate !== undefined) sliceBambuOptions.uptodate = Boolean(args.uptodate);
|
|
727
|
+
if (args?.repetitions !== undefined) sliceBambuOptions.repetitions = Number(args.repetitions);
|
|
728
|
+
if (args?.orient !== undefined) sliceBambuOptions.orient = Boolean(args.orient);
|
|
729
|
+
if (args?.arrange !== undefined) sliceBambuOptions.arrange = Boolean(args.arrange);
|
|
730
|
+
if (args?.ensure_on_bed !== undefined) sliceBambuOptions.ensureOnBed = Boolean(args.ensure_on_bed);
|
|
731
|
+
if (args?.clone_objects !== undefined) sliceBambuOptions.cloneObjects = String(args.clone_objects);
|
|
732
|
+
if (args?.skip_objects !== undefined) sliceBambuOptions.skipObjects = String(args.skip_objects);
|
|
733
|
+
if (args?.load_filaments !== undefined) sliceBambuOptions.loadFilaments = String(args.load_filaments);
|
|
734
|
+
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
735
|
+
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
736
|
+
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
737
|
+
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
738
|
+
if (args?.rotate !== undefined) sliceBambuOptions.rotate = Number(args.rotate);
|
|
739
|
+
if (args?.rotate_x !== undefined) sliceBambuOptions.rotateX = Number(args.rotate_x);
|
|
740
|
+
if (args?.rotate_y !== undefined) sliceBambuOptions.rotateY = Number(args.rotate_y);
|
|
741
|
+
if (args?.min_save !== undefined) sliceBambuOptions.minSave = Boolean(args.min_save);
|
|
742
|
+
if (args?.skip_modified_gcodes !== undefined) sliceBambuOptions.skipModifiedGcodes = Boolean(args.skip_modified_gcodes);
|
|
743
|
+
if (args?.slice_plate !== undefined) sliceBambuOptions.slicePlate = Number(args.slice_plate);
|
|
744
|
+
|
|
706
745
|
result = await this.stlManipulator.sliceSTL(
|
|
707
746
|
String(args.stl_path), slicerType, slicerPath,
|
|
708
747
|
slicerProfile || undefined,
|
|
709
748
|
undefined, // progressCallback
|
|
710
|
-
printerPreset
|
|
749
|
+
printerPreset,
|
|
750
|
+
sliceBambuOptions
|
|
711
751
|
);
|
|
712
752
|
break;
|
|
713
753
|
}
|
|
@@ -737,10 +777,17 @@ class BambuPrinterMCPServer {
|
|
|
737
777
|
);
|
|
738
778
|
if (!hasGcode) {
|
|
739
779
|
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
780
|
+
const autoSliceOptions: BambuSliceOptions = {
|
|
781
|
+
uptodate: true,
|
|
782
|
+
ensureOnBed: true,
|
|
783
|
+
minSave: true,
|
|
784
|
+
skipModifiedGcodes: true,
|
|
785
|
+
};
|
|
740
786
|
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
741
787
|
threeMFPath, slicerType, slicerPath, slicerProfile || undefined,
|
|
742
788
|
undefined, // progressCallback
|
|
743
|
-
printPreset
|
|
789
|
+
printPreset,
|
|
790
|
+
autoSliceOptions
|
|
744
791
|
);
|
|
745
792
|
console.log("Auto-sliced to: " + threeMFPath);
|
|
746
793
|
}
|
|
@@ -40,6 +40,28 @@ export type TransformationParams = {
|
|
|
40
40
|
selectionBounds?: THREE.Box3;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
// BambuStudio CLI flags exposed via the slice_stl tool
|
|
44
|
+
export interface BambuSliceOptions {
|
|
45
|
+
uptodate?: boolean; // --uptodate: update 3MF configs to latest presets
|
|
46
|
+
repetitions?: number; // --repetitions N: print N copies
|
|
47
|
+
orient?: boolean; // --orient 1: auto-orient for printability
|
|
48
|
+
arrange?: boolean; // --arrange 1: auto-arrange objects on plate
|
|
49
|
+
ensureOnBed?: boolean; // --ensure-on-bed: lift floating models onto bed
|
|
50
|
+
cloneObjects?: string; // --clone-objects "1,3,1,10": clone counts per object
|
|
51
|
+
skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
|
|
52
|
+
loadFilaments?: string; // --load-filaments "f1.json;f2.json": filament profiles
|
|
53
|
+
loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
|
|
54
|
+
enableTimelapse?: boolean; // --enable-timelapse: timelapse-aware slicing
|
|
55
|
+
allowMixTemp?: boolean; // --allow-mix-temp: allow mixed-temp filaments
|
|
56
|
+
scale?: number; // --scale factor: uniform scale
|
|
57
|
+
rotate?: number; // --rotate degrees: Z-axis rotation
|
|
58
|
+
rotateX?: number; // --rotate-x degrees
|
|
59
|
+
rotateY?: number; // --rotate-y degrees
|
|
60
|
+
minSave?: boolean; // --min-save: smaller output 3MF
|
|
61
|
+
skipModifiedGcodes?: boolean;// --skip-modified-gcodes: ignore stale custom gcodes
|
|
62
|
+
slicePlate?: number; // --slice N: which plate (default: 0 = all)
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
export class STLManipulator extends EventEmitter {
|
|
44
66
|
private tempDir: string;
|
|
45
67
|
private activeOperations: Map<string, boolean> = new Map();
|
|
@@ -141,23 +163,30 @@ export class STLManipulator extends EventEmitter {
|
|
|
141
163
|
boundingBox: BoundingBox;
|
|
142
164
|
vertexCount: number;
|
|
143
165
|
faceCount: number;
|
|
166
|
+
objects?: Array<{ name: string; vertexCount: number; faceCount: number; boundingBox: BoundingBox }>;
|
|
144
167
|
}> {
|
|
145
168
|
try {
|
|
146
|
-
const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
|
|
147
169
|
const fileStats = fs.statSync(stlFilePath);
|
|
148
|
-
|
|
170
|
+
|
|
171
|
+
// Handle 3MF files: extract mesh data from embedded object models
|
|
172
|
+
if (stlFilePath.toLowerCase().endsWith('.3mf')) {
|
|
173
|
+
return await this.get3MFInfo(stlFilePath, fileStats.size);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
|
|
177
|
+
|
|
149
178
|
// Count faces (each face is a triangle in STL)
|
|
150
179
|
const positionAttribute = geometry.getAttribute('position');
|
|
151
180
|
const vertexCount = positionAttribute.count;
|
|
152
181
|
const faceCount = vertexCount / 3;
|
|
153
|
-
|
|
182
|
+
|
|
154
183
|
// Calculate center and dimensions
|
|
155
184
|
const center = new THREE.Vector3();
|
|
156
185
|
boundingBox.getCenter(center);
|
|
157
|
-
|
|
186
|
+
|
|
158
187
|
const dimensions = new THREE.Vector3();
|
|
159
188
|
boundingBox.getSize(dimensions);
|
|
160
|
-
|
|
189
|
+
|
|
161
190
|
return {
|
|
162
191
|
filePath: stlFilePath,
|
|
163
192
|
fileName: path.basename(stlFilePath),
|
|
@@ -177,6 +206,115 @@ export class STLManipulator extends EventEmitter {
|
|
|
177
206
|
}
|
|
178
207
|
}
|
|
179
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Extract mesh info from a 3MF file by parsing the XML object models.
|
|
211
|
+
*/
|
|
212
|
+
private async get3MFInfo(filePath: string, fileSize: number): Promise<{
|
|
213
|
+
filePath: string;
|
|
214
|
+
fileName: string;
|
|
215
|
+
fileSize: number;
|
|
216
|
+
boundingBox: BoundingBox;
|
|
217
|
+
vertexCount: number;
|
|
218
|
+
faceCount: number;
|
|
219
|
+
objects: Array<{ name: string; vertexCount: number; faceCount: number; boundingBox: BoundingBox }>;
|
|
220
|
+
}> {
|
|
221
|
+
const JSZip = (await import('jszip')).default;
|
|
222
|
+
const data = fs.readFileSync(filePath);
|
|
223
|
+
const zip = await JSZip.loadAsync(data);
|
|
224
|
+
|
|
225
|
+
// Find all object model files
|
|
226
|
+
const objectFiles = Object.keys(zip.files).filter(
|
|
227
|
+
(name) => /^3D\/Objects\/.*\.model$/i.test(name)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const overallBox = new THREE.Box3(
|
|
231
|
+
new THREE.Vector3(Infinity, Infinity, Infinity),
|
|
232
|
+
new THREE.Vector3(-Infinity, -Infinity, -Infinity)
|
|
233
|
+
);
|
|
234
|
+
let totalVerts = 0;
|
|
235
|
+
let totalFaces = 0;
|
|
236
|
+
const objects: Array<{ name: string; vertexCount: number; faceCount: number; boundingBox: BoundingBox }> = [];
|
|
237
|
+
|
|
238
|
+
for (const objFile of objectFiles) {
|
|
239
|
+
const content = await zip.file(objFile)!.async('string');
|
|
240
|
+
// Parse vertices from XML: <vertex x="..." y="..." z="..."/>
|
|
241
|
+
const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
|
|
242
|
+
const triangleRegex = /<(?:\w+:)?triangle\s/g;
|
|
243
|
+
|
|
244
|
+
const objBox = new THREE.Box3(
|
|
245
|
+
new THREE.Vector3(Infinity, Infinity, Infinity),
|
|
246
|
+
new THREE.Vector3(-Infinity, -Infinity, -Infinity)
|
|
247
|
+
);
|
|
248
|
+
let vCount = 0;
|
|
249
|
+
let match: RegExpExecArray | null;
|
|
250
|
+
while ((match = vertexRegex.exec(content)) !== null) {
|
|
251
|
+
const x = parseFloat(match[1]);
|
|
252
|
+
const y = parseFloat(match[2]);
|
|
253
|
+
const z = parseFloat(match[3]);
|
|
254
|
+
objBox.expandByPoint(new THREE.Vector3(x, y, z));
|
|
255
|
+
overallBox.expandByPoint(new THREE.Vector3(x, y, z));
|
|
256
|
+
vCount++;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let fCount = 0;
|
|
260
|
+
while (triangleRegex.exec(content) !== null) fCount++;
|
|
261
|
+
|
|
262
|
+
totalVerts += vCount;
|
|
263
|
+
totalFaces += fCount;
|
|
264
|
+
|
|
265
|
+
if (vCount > 0) {
|
|
266
|
+
const center = new THREE.Vector3();
|
|
267
|
+
objBox.getCenter(center);
|
|
268
|
+
const dims = new THREE.Vector3();
|
|
269
|
+
objBox.getSize(dims);
|
|
270
|
+
objects.push({
|
|
271
|
+
name: path.basename(objFile),
|
|
272
|
+
vertexCount: vCount,
|
|
273
|
+
faceCount: fCount,
|
|
274
|
+
boundingBox: { min: objBox.min, max: objBox.max, center, dimensions: dims },
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// If no object files found, try the main 3dmodel.model
|
|
280
|
+
if (objects.length === 0) {
|
|
281
|
+
const mainModel = zip.file('3D/3dmodel.model');
|
|
282
|
+
if (mainModel) {
|
|
283
|
+
const content = await mainModel.async('string');
|
|
284
|
+
const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
|
|
285
|
+
const triangleRegex = /<(?:\w+:)?triangle\s/g;
|
|
286
|
+
let match: RegExpExecArray | null;
|
|
287
|
+
while ((match = vertexRegex.exec(content)) !== null) {
|
|
288
|
+
const x = parseFloat(match[1]);
|
|
289
|
+
const y = parseFloat(match[2]);
|
|
290
|
+
const z = parseFloat(match[3]);
|
|
291
|
+
overallBox.expandByPoint(new THREE.Vector3(x, y, z));
|
|
292
|
+
totalVerts++;
|
|
293
|
+
}
|
|
294
|
+
while (triangleRegex.exec(content) !== null) totalFaces++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (totalVerts === 0) {
|
|
299
|
+
throw new Error('No mesh data found in 3MF file. The archive may not contain any 3D objects.');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const center = new THREE.Vector3();
|
|
303
|
+
overallBox.getCenter(center);
|
|
304
|
+
const dimensions = new THREE.Vector3();
|
|
305
|
+
overallBox.getSize(dimensions);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
filePath,
|
|
309
|
+
fileName: path.basename(filePath),
|
|
310
|
+
fileSize,
|
|
311
|
+
boundingBox: { min: overallBox.min, max: overallBox.max, center, dimensions },
|
|
312
|
+
vertexCount: totalVerts,
|
|
313
|
+
faceCount: totalFaces,
|
|
314
|
+
objects,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
180
318
|
/**
|
|
181
319
|
* Scale an STL model uniformly or along specific axes
|
|
182
320
|
*/
|
|
@@ -850,6 +988,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
850
988
|
* @param slicerProfile Optional path to the slicer profile/config file
|
|
851
989
|
* @param progressCallback Optional callback for progress updates
|
|
852
990
|
* @param printerPreset Optional BambuStudio printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
|
|
991
|
+
* @param bambuOptions Optional BambuStudio-specific CLI flags
|
|
853
992
|
* @returns Path to the generated G-code or sliced 3MF file
|
|
854
993
|
*/
|
|
855
994
|
async sliceSTL(
|
|
@@ -858,7 +997,8 @@ export class STLManipulator extends EventEmitter {
|
|
|
858
997
|
slicerPath: string,
|
|
859
998
|
slicerProfile?: string,
|
|
860
999
|
progressCallback?: ProgressCallback,
|
|
861
|
-
printerPreset?: string
|
|
1000
|
+
printerPreset?: string,
|
|
1001
|
+
bambuOptions?: BambuSliceOptions
|
|
862
1002
|
): Promise<string> {
|
|
863
1003
|
const operationId = this.generateOperationId();
|
|
864
1004
|
this.activeOperations.set(operationId, true);
|
|
@@ -935,19 +1075,43 @@ export class STLManipulator extends EventEmitter {
|
|
|
935
1075
|
const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
|
|
936
1076
|
const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
|
|
937
1077
|
args = [
|
|
938
|
-
'--slice',
|
|
1078
|
+
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
939
1079
|
'--export-3mf', bambuOutputPath,
|
|
940
1080
|
];
|
|
941
1081
|
if (slicerProfile) {
|
|
942
1082
|
args.push('--load-settings', slicerProfile);
|
|
943
1083
|
}
|
|
944
|
-
//
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
//
|
|
1084
|
+
// Always allow newer-version 3MF files (the CLI rejects them by default)
|
|
1085
|
+
args.push('--allow-newer-file');
|
|
1086
|
+
|
|
1087
|
+
// If a printer preset name is given AND no explicit profile was provided,
|
|
1088
|
+
// write a minimal settings JSON containing the printer_settings_id so
|
|
1089
|
+
// BambuStudio resolves the correct machine profile from its built-in presets.
|
|
948
1090
|
if (printerPreset && !slicerProfile) {
|
|
949
|
-
|
|
1091
|
+
const presetJson = path.join(this.tempDir, '_printer_preset.json');
|
|
1092
|
+
fs.writeFileSync(presetJson, JSON.stringify({ printer_settings_id: printerPreset }));
|
|
1093
|
+
args.push('--load-settings', presetJson);
|
|
950
1094
|
}
|
|
1095
|
+
|
|
1096
|
+
// BambuSliceOptions flags
|
|
1097
|
+
if (bambuOptions?.uptodate) args.push('--uptodate');
|
|
1098
|
+
if (bambuOptions?.ensureOnBed) args.push('--ensure-on-bed');
|
|
1099
|
+
if (bambuOptions?.enableTimelapse) args.push('--enable-timelapse');
|
|
1100
|
+
if (bambuOptions?.allowMixTemp) args.push('--allow-mix-temp');
|
|
1101
|
+
if (bambuOptions?.minSave) args.push('--min-save');
|
|
1102
|
+
if (bambuOptions?.skipModifiedGcodes) args.push('--skip-modified-gcodes');
|
|
1103
|
+
if (bambuOptions?.orient !== undefined) args.push('--orient', bambuOptions.orient ? '1' : '0');
|
|
1104
|
+
if (bambuOptions?.arrange !== undefined) args.push('--arrange', bambuOptions.arrange ? '1' : '0');
|
|
1105
|
+
if (bambuOptions?.repetitions !== undefined) args.push('--repetitions', String(bambuOptions.repetitions));
|
|
1106
|
+
if (bambuOptions?.scale !== undefined) args.push('--scale', String(bambuOptions.scale));
|
|
1107
|
+
if (bambuOptions?.rotate !== undefined) args.push('--rotate', String(bambuOptions.rotate));
|
|
1108
|
+
if (bambuOptions?.rotateX !== undefined) args.push('--rotate-x', String(bambuOptions.rotateX));
|
|
1109
|
+
if (bambuOptions?.rotateY !== undefined) args.push('--rotate-y', String(bambuOptions.rotateY));
|
|
1110
|
+
if (bambuOptions?.cloneObjects) args.push('--clone-objects', bambuOptions.cloneObjects);
|
|
1111
|
+
if (bambuOptions?.skipObjects) args.push('--skip-objects', bambuOptions.skipObjects);
|
|
1112
|
+
if (bambuOptions?.loadFilaments) args.push('--load-filaments', bambuOptions.loadFilaments);
|
|
1113
|
+
if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
|
|
1114
|
+
|
|
951
1115
|
args.push(stlFilePath);
|
|
952
1116
|
// Override outputFilePath for bambustudio since it produces 3MF, not gcode
|
|
953
1117
|
outputFilePath = bambuOutputPath;
|