bambu-printer-mcp 1.0.2 → 1.0.3

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.
@@ -48,7 +48,17 @@ export declare class STLManipulator extends EventEmitter {
48
48
  boundingBox: BoundingBox;
49
49
  vertexCount: number;
50
50
  faceCount: number;
51
+ objects?: Array<{
52
+ name: string;
53
+ vertexCount: number;
54
+ faceCount: number;
55
+ boundingBox: BoundingBox;
56
+ }>;
51
57
  }>;
58
+ /**
59
+ * Extract mesh info from a 3MF file by parsing the XML object models.
60
+ */
61
+ private get3MFInfo;
52
62
  /**
53
63
  * Scale an STL model uniformly or along specific axes
54
64
  */
@@ -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
  */
@@ -728,12 +815,15 @@ export class STLManipulator extends EventEmitter {
728
815
  if (slicerProfile) {
729
816
  args.push('--load-settings', slicerProfile);
730
817
  }
731
- // CRITICAL: Pass printer preset so slicer generates correct G-code
732
- // for the target printer model. Without this, the slicer uses its
733
- // default which may be a completely different printer, causing
734
- // dangerous bed crashes or axis limit violations.
818
+ // Always allow newer-version 3MF files (the CLI rejects them by default)
819
+ args.push('--allow-newer-file');
820
+ // If a printer preset name is given AND no explicit profile was provided,
821
+ // write a minimal settings JSON containing the printer_settings_id so
822
+ // BambuStudio resolves the correct machine profile from its built-in presets.
735
823
  if (printerPreset && !slicerProfile) {
736
- args.push('--load-machine', printerPreset);
824
+ const presetJson = path.join(this.tempDir, '_printer_preset.json');
825
+ fs.writeFileSync(presetJson, JSON.stringify({ printer_settings_id: printerPreset }));
826
+ args.push('--load-settings', presetJson);
737
827
  }
738
828
  args.push(stlFilePath);
739
829
  // Override outputFilePath for bambustudio since it produces 3MF, not gcode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bambu-printer-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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",
@@ -141,23 +141,30 @@ export class STLManipulator extends EventEmitter {
141
141
  boundingBox: BoundingBox;
142
142
  vertexCount: number;
143
143
  faceCount: number;
144
+ objects?: Array<{ name: string; vertexCount: number; faceCount: number; boundingBox: BoundingBox }>;
144
145
  }> {
145
146
  try {
146
- const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
147
147
  const fileStats = fs.statSync(stlFilePath);
148
-
148
+
149
+ // Handle 3MF files: extract mesh data from embedded object models
150
+ if (stlFilePath.toLowerCase().endsWith('.3mf')) {
151
+ return await this.get3MFInfo(stlFilePath, fileStats.size);
152
+ }
153
+
154
+ const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
155
+
149
156
  // Count faces (each face is a triangle in STL)
150
157
  const positionAttribute = geometry.getAttribute('position');
151
158
  const vertexCount = positionAttribute.count;
152
159
  const faceCount = vertexCount / 3;
153
-
160
+
154
161
  // Calculate center and dimensions
155
162
  const center = new THREE.Vector3();
156
163
  boundingBox.getCenter(center);
157
-
164
+
158
165
  const dimensions = new THREE.Vector3();
159
166
  boundingBox.getSize(dimensions);
160
-
167
+
161
168
  return {
162
169
  filePath: stlFilePath,
163
170
  fileName: path.basename(stlFilePath),
@@ -177,6 +184,115 @@ export class STLManipulator extends EventEmitter {
177
184
  }
178
185
  }
179
186
 
187
+ /**
188
+ * Extract mesh info from a 3MF file by parsing the XML object models.
189
+ */
190
+ private async get3MFInfo(filePath: string, fileSize: number): Promise<{
191
+ filePath: string;
192
+ fileName: string;
193
+ fileSize: number;
194
+ boundingBox: BoundingBox;
195
+ vertexCount: number;
196
+ faceCount: number;
197
+ objects: Array<{ name: string; vertexCount: number; faceCount: number; boundingBox: BoundingBox }>;
198
+ }> {
199
+ const JSZip = (await import('jszip')).default;
200
+ const data = fs.readFileSync(filePath);
201
+ const zip = await JSZip.loadAsync(data);
202
+
203
+ // Find all object model files
204
+ const objectFiles = Object.keys(zip.files).filter(
205
+ (name) => /^3D\/Objects\/.*\.model$/i.test(name)
206
+ );
207
+
208
+ const overallBox = new THREE.Box3(
209
+ new THREE.Vector3(Infinity, Infinity, Infinity),
210
+ new THREE.Vector3(-Infinity, -Infinity, -Infinity)
211
+ );
212
+ let totalVerts = 0;
213
+ let totalFaces = 0;
214
+ const objects: Array<{ name: string; vertexCount: number; faceCount: number; boundingBox: BoundingBox }> = [];
215
+
216
+ for (const objFile of objectFiles) {
217
+ const content = await zip.file(objFile)!.async('string');
218
+ // Parse vertices from XML: <vertex x="..." y="..." z="..."/>
219
+ const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
220
+ const triangleRegex = /<(?:\w+:)?triangle\s/g;
221
+
222
+ const objBox = new THREE.Box3(
223
+ new THREE.Vector3(Infinity, Infinity, Infinity),
224
+ new THREE.Vector3(-Infinity, -Infinity, -Infinity)
225
+ );
226
+ let vCount = 0;
227
+ let match: RegExpExecArray | null;
228
+ while ((match = vertexRegex.exec(content)) !== null) {
229
+ const x = parseFloat(match[1]);
230
+ const y = parseFloat(match[2]);
231
+ const z = parseFloat(match[3]);
232
+ objBox.expandByPoint(new THREE.Vector3(x, y, z));
233
+ overallBox.expandByPoint(new THREE.Vector3(x, y, z));
234
+ vCount++;
235
+ }
236
+
237
+ let fCount = 0;
238
+ while (triangleRegex.exec(content) !== null) fCount++;
239
+
240
+ totalVerts += vCount;
241
+ totalFaces += fCount;
242
+
243
+ if (vCount > 0) {
244
+ const center = new THREE.Vector3();
245
+ objBox.getCenter(center);
246
+ const dims = new THREE.Vector3();
247
+ objBox.getSize(dims);
248
+ objects.push({
249
+ name: path.basename(objFile),
250
+ vertexCount: vCount,
251
+ faceCount: fCount,
252
+ boundingBox: { min: objBox.min, max: objBox.max, center, dimensions: dims },
253
+ });
254
+ }
255
+ }
256
+
257
+ // If no object files found, try the main 3dmodel.model
258
+ if (objects.length === 0) {
259
+ const mainModel = zip.file('3D/3dmodel.model');
260
+ if (mainModel) {
261
+ const content = await mainModel.async('string');
262
+ const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
263
+ const triangleRegex = /<(?:\w+:)?triangle\s/g;
264
+ let match: RegExpExecArray | null;
265
+ while ((match = vertexRegex.exec(content)) !== null) {
266
+ const x = parseFloat(match[1]);
267
+ const y = parseFloat(match[2]);
268
+ const z = parseFloat(match[3]);
269
+ overallBox.expandByPoint(new THREE.Vector3(x, y, z));
270
+ totalVerts++;
271
+ }
272
+ while (triangleRegex.exec(content) !== null) totalFaces++;
273
+ }
274
+ }
275
+
276
+ if (totalVerts === 0) {
277
+ throw new Error('No mesh data found in 3MF file. The archive may not contain any 3D objects.');
278
+ }
279
+
280
+ const center = new THREE.Vector3();
281
+ overallBox.getCenter(center);
282
+ const dimensions = new THREE.Vector3();
283
+ overallBox.getSize(dimensions);
284
+
285
+ return {
286
+ filePath,
287
+ fileName: path.basename(filePath),
288
+ fileSize,
289
+ boundingBox: { min: overallBox.min, max: overallBox.max, center, dimensions },
290
+ vertexCount: totalVerts,
291
+ faceCount: totalFaces,
292
+ objects,
293
+ };
294
+ }
295
+
180
296
  /**
181
297
  * Scale an STL model uniformly or along specific axes
182
298
  */
@@ -941,12 +1057,16 @@ export class STLManipulator extends EventEmitter {
941
1057
  if (slicerProfile) {
942
1058
  args.push('--load-settings', slicerProfile);
943
1059
  }
944
- // CRITICAL: Pass printer preset so slicer generates correct G-code
945
- // for the target printer model. Without this, the slicer uses its
946
- // default which may be a completely different printer, causing
947
- // dangerous bed crashes or axis limit violations.
1060
+ // Always allow newer-version 3MF files (the CLI rejects them by default)
1061
+ args.push('--allow-newer-file');
1062
+
1063
+ // If a printer preset name is given AND no explicit profile was provided,
1064
+ // write a minimal settings JSON containing the printer_settings_id so
1065
+ // BambuStudio resolves the correct machine profile from its built-in presets.
948
1066
  if (printerPreset && !slicerProfile) {
949
- args.push('--load-machine', printerPreset);
1067
+ const presetJson = path.join(this.tempDir, '_printer_preset.json');
1068
+ fs.writeFileSync(presetJson, JSON.stringify({ printer_settings_id: printerPreset }));
1069
+ args.push('--load-settings', presetJson);
950
1070
  }
951
1071
  args.push(stlFilePath);
952
1072
  // Override outputFilePath for bambustudio since it produces 3MF, not gcode