bambu-printer-mcp 1.0.1 → 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/README.md +23 -87
- 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
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
A Bambu Lab-focused MCP server for controlling Bambu printers, manipulating STL files, and managing end-to-end 3MF print workflows from Claude Desktop, Claude Code, or any MCP-compatible client.
|
|
11
11
|
|
|
12
|
-
This is a stripped-down, Bambu-only fork of [mcp-3D-printer-server](https://github.com/DMontgomery40/mcp-3D-printer-server). All OctoPrint, Klipper, Duet, Repetier, Prusa Connect, and Creality Cloud support has been removed. What remains is a focused,
|
|
12
|
+
This is a stripped-down, Bambu-only fork of [mcp-3D-printer-server](https://github.com/DMontgomery40/mcp-3D-printer-server). All OctoPrint, Klipper, Duet, Repetier, Prusa Connect, and Creality Cloud support has been removed. What remains is a focused, lean implementation for Bambu Lab hardware.
|
|
13
13
|
|
|
14
14
|
<details>
|
|
15
15
|
<summary><strong>Click to expand Table of Contents</strong></summary>
|
|
@@ -25,8 +25,7 @@ This is a stripped-down, Bambu-only fork of [mcp-3D-printer-server](https://gith
|
|
|
25
25
|
- [Install from source](#install-from-source)
|
|
26
26
|
- [Configuration](#configuration)
|
|
27
27
|
- [Environment variables reference](#environment-variables-reference)
|
|
28
|
-
- [Usage
|
|
29
|
-
- [Usage with Claude Code](#usage-with-claude-code)
|
|
28
|
+
- [Usage](#usage)
|
|
30
29
|
- [Enabling Developer Mode (Required)](#enabling-developer-mode-required)
|
|
31
30
|
- [Finding Your Bambu Printer's Serial Number and Access Token](#finding-your-bambu-printers-serial-number-and-access-token)
|
|
32
31
|
- [AMS (Automatic Material System) Setup](#ams-automatic-material-system-setup)
|
|
@@ -56,7 +55,7 @@ This is a stripped-down, Bambu-only fork of [mcp-3D-printer-server](https://gith
|
|
|
56
55
|
|
|
57
56
|
**What this is not.** This package intentionally supports only Bambu Lab printers. It does not include adapters for OctoPrint, Klipper (Moonraker), Duet, Repetier, Prusa Connect, or Creality Cloud. If you need multi-printer support, use the parent project [mcp-3D-printer-server](https://github.com/DMontgomery40/mcp-3D-printer-server) instead.
|
|
58
57
|
|
|
59
|
-
**Why a separate package?** The parent project carries all printer adapters in a single binary. When working exclusively with Bambu hardware, that breadth adds
|
|
58
|
+
**Why a separate package?** The parent project carries all printer adapters in a single binary. When working exclusively with Bambu hardware, that breadth adds unnecessary weight. This fork strips the project to its Bambu core for a smaller, faster install. Both packages share the same protocol fixes and safety features.
|
|
60
59
|
|
|
61
60
|
**Note on resource usage.** STL manipulation loads entire mesh geometry into memory. For large or complex STL files (greater than 10 MB), these operations can be memory-intensive. See [General Limitations and Considerations](#general-limitations-and-considerations) for details.
|
|
62
61
|
|
|
@@ -182,48 +181,9 @@ BLENDER_MCP_BRIDGE_COMMAND= # Shell command to invoke your Blender MCP bri
|
|
|
182
181
|
|
|
183
182
|
---
|
|
184
183
|
|
|
185
|
-
## Usage
|
|
184
|
+
## Usage
|
|
186
185
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
**Using npx (recommended -- always runs the latest version):**
|
|
190
|
-
|
|
191
|
-
```json
|
|
192
|
-
{
|
|
193
|
-
"mcpServers": {
|
|
194
|
-
"bambu-printer": {
|
|
195
|
-
"command": "npx",
|
|
196
|
-
"args": ["-y", "bambu-printer-mcp"],
|
|
197
|
-
"env": {
|
|
198
|
-
"PRINTER_HOST": "192.168.1.100",
|
|
199
|
-
"BAMBU_SERIAL": "01P00A123456789",
|
|
200
|
-
"BAMBU_TOKEN": "your_access_token",
|
|
201
|
-
"BAMBU_MODEL": "p1s"
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**Using a globally installed binary:**
|
|
209
|
-
|
|
210
|
-
```json
|
|
211
|
-
{
|
|
212
|
-
"mcpServers": {
|
|
213
|
-
"bambu-printer": {
|
|
214
|
-
"command": "bambu-printer-mcp",
|
|
215
|
-
"env": {
|
|
216
|
-
"PRINTER_HOST": "192.168.1.100",
|
|
217
|
-
"BAMBU_SERIAL": "01P00A123456789",
|
|
218
|
-
"BAMBU_TOKEN": "your_access_token",
|
|
219
|
-
"BAMBU_MODEL": "p1s"
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
**With slicer configured (for auto-slice and print_3mf workflows):**
|
|
186
|
+
Add this server to your MCP client's config (Claude Desktop, Claude Code, Cursor, Codex CLI, or any MCP-compatible client). The config format is the same everywhere -- an `mcpServers` entry with the command and env vars:
|
|
227
187
|
|
|
228
188
|
```json
|
|
229
189
|
{
|
|
@@ -244,53 +204,29 @@ Edit your Claude Desktop configuration file. On macOS this is at `~/Library/Appl
|
|
|
244
204
|
}
|
|
245
205
|
```
|
|
246
206
|
|
|
247
|
-
|
|
207
|
+
Where this config lives depends on your client:
|
|
248
208
|
|
|
249
|
-
|
|
209
|
+
| Client | Config location |
|
|
210
|
+
|--------|----------------|
|
|
211
|
+
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
212
|
+
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
213
|
+
| Claude Code (project) | `.mcp.json` in project root |
|
|
214
|
+
| Claude Code (global) | `~/.claude/settings.json` |
|
|
215
|
+
| Cursor | MCP settings in Cursor preferences |
|
|
216
|
+
| Codex CLI | MCP config per Codex docs |
|
|
250
217
|
|
|
251
|
-
|
|
218
|
+
Restart your client after editing the config.
|
|
252
219
|
|
|
253
|
-
|
|
220
|
+
### Recommended: use with codemode-mcp
|
|
254
221
|
|
|
255
|
-
|
|
222
|
+
For any MCP server with a large tool surface, wrapping it behind [codemode-mcp](https://github.com/jx-codes/codemode-mcp) dramatically reduces token usage. Instead of exposing every tool definition to the model (which can consume tens of thousands of tokens per turn), codemode lets the agent write code against a two-tool interface (`search()` and `execute()`), loading only the tools it needs on demand.
|
|
256
223
|
|
|
257
|
-
|
|
258
|
-
{
|
|
259
|
-
"mcpServers": {
|
|
260
|
-
"bambu-printer": {
|
|
261
|
-
"command": "npx",
|
|
262
|
-
"args": ["-y", "bambu-printer-mcp"],
|
|
263
|
-
"env": {
|
|
264
|
-
"PRINTER_HOST": "192.168.1.100",
|
|
265
|
-
"BAMBU_SERIAL": "01P00A123456789",
|
|
266
|
-
"BAMBU_TOKEN": "your_access_token",
|
|
267
|
-
"BAMBU_MODEL": "p1s",
|
|
268
|
-
"SLICER_TYPE": "bambustudio",
|
|
269
|
-
"SLICER_PATH": "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio"
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
```
|
|
224
|
+
Anthropic and Cloudflare independently demonstrated this pattern reduces MCP token costs by up to 98%:
|
|
275
225
|
|
|
276
|
-
|
|
226
|
+
- [Code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp) (Anthropic)
|
|
227
|
+
- [Code Mode: give agents an entire API in 1,000 tokens](https://blog.cloudflare.com/code-mode-mcp/) (Cloudflare)
|
|
277
228
|
|
|
278
|
-
|
|
279
|
-
{
|
|
280
|
-
"mcpServers": {
|
|
281
|
-
"bambu-printer": {
|
|
282
|
-
"command": "npx",
|
|
283
|
-
"args": ["-y", "bambu-printer-mcp"],
|
|
284
|
-
"env": {
|
|
285
|
-
"PRINTER_HOST": "192.168.1.100",
|
|
286
|
-
"BAMBU_SERIAL": "01P00A123456789",
|
|
287
|
-
"BAMBU_TOKEN": "your_access_token",
|
|
288
|
-
"BAMBU_MODEL": "p1s"
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
```
|
|
229
|
+
This applies to all MCP servers, not just this one.
|
|
294
230
|
|
|
295
231
|
---
|
|
296
232
|
|
|
@@ -464,7 +400,7 @@ Bambu Lab printers do not use a conventional REST API. Instead, they expose two
|
|
|
464
400
|
|
|
465
401
|
### What this fork fixes
|
|
466
402
|
|
|
467
|
-
|
|
403
|
+
Both this package and the parent project (`mcp-3D-printer-server`) include fixes for two protocol-level issues in the underlying `bambu-js` library.
|
|
468
404
|
|
|
469
405
|
**Bug 1: FTP double-path error in bambu-js.**
|
|
470
406
|
|
|
@@ -489,7 +425,7 @@ private async ftpUpload(host, token, localPath, remotePath): Promise<void> {
|
|
|
489
425
|
|
|
490
426
|
**Bug 2: AMS mapping format in the project_file MQTT command.**
|
|
491
427
|
|
|
492
|
-
The `bambu-js` library's project file command hardcodes `use_ams: true` and does not support the `ams_mapping` field at all.
|
|
428
|
+
The `bambu-js` library's project file command hardcodes `use_ams: true` and does not support the `ams_mapping` field at all. Without the fix, the mapping is a simple array of slot indices (e.g., `[0, 2]`), which does not match the OpenBambuAPI specification.
|
|
493
429
|
|
|
494
430
|
According to the OpenBambuAPI spec, `ams_mapping` must be a 5-element array where each position corresponds to a filament color slot in the print file. Unused positions must be padded with `-1`. For example, a print using only AMS slot 0 sends `[-1, -1, -1, -1, 0]`.
|
|
495
431
|
|
|
@@ -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
|