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.
- package/dist/stl/stl-manipulator.d.ts +10 -0
- package/dist/stl/stl-manipulator.js +96 -6
- package/package.json +1 -1
- package/src/stl/stl-manipulator.ts +130 -10
|
@@ -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
|
-
//
|
|
732
|
-
|
|
733
|
-
//
|
|
734
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
//
|
|
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
|
-
|
|
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
|