mcp-3d-printer-server 0.2.3 → 1.0.1
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 +267 -29
- package/dist/3mf_parser.d.ts +3 -0
- package/dist/3mf_parser.js +216 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1018 -0
- package/dist/index.js.map +1 -0
- package/dist/printers/bambu.d.ts +42 -0
- package/dist/printers/bambu.js +278 -0
- package/dist/printers/creality.d.ts +10 -0
- package/dist/printers/creality.js +90 -0
- package/dist/printers/duet.d.ts +10 -0
- package/dist/printers/duet.js +66 -0
- package/dist/printers/klipper.d.ts +10 -0
- package/dist/printers/klipper.js +62 -0
- package/dist/printers/octoprint.d.ts +10 -0
- package/dist/printers/octoprint.js +97 -0
- package/dist/printers/printer-factory.d.ts +9 -0
- package/dist/printers/printer-factory.js +33 -0
- package/dist/printers/prusa.d.ts +10 -0
- package/dist/printers/prusa.js +93 -0
- package/dist/printers/repetier.d.ts +10 -0
- package/dist/printers/repetier.js +59 -0
- package/dist/stl/stl-manipulator.d.ts +140 -0
- package/dist/stl/stl-manipulator.js +1081 -0
- package/dist/test/stl-manipulator-test.d.ts +2 -0
- package/dist/test/stl-manipulator-test.js +91 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.js +26 -0
- package/package.json +14 -4
- package/src/3mf_parser.ts +223 -0
- package/src/index.ts +239 -10
- package/src/printers/bambu.ts +305 -100
- package/src/stl/stl-manipulator.ts +350 -117
- package/src/test/3DBenchy.3mf +0 -0
- package/src/test/stl-manipulator-test.js +114 -0
- package/src/test/stl-manipulator-test.ts +124 -0
- package/src/test/tsconfig.json +13 -0
- package/src/types.ts +65 -0
- package/.env.example +0 -17
- package/scripts/mcp-inspector.js +0 -49
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { STLManipulator } from '../stl/stl-manipulator.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
// Create a test directory
|
|
7
|
+
const TEST_DIR = path.join(process.cwd(), 'test');
|
|
8
|
+
if (!fs.existsSync(TEST_DIR)) {
|
|
9
|
+
fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
// Path for a sample STL file
|
|
12
|
+
const sampleStlPath = path.join(TEST_DIR, 'sample_cube.stl');
|
|
13
|
+
// Create a simple cube STL for testing if it doesn't exist
|
|
14
|
+
async function createSampleCube() {
|
|
15
|
+
if (fs.existsSync(sampleStlPath)) {
|
|
16
|
+
console.log('Sample STL already exists, skipping creation');
|
|
17
|
+
return sampleStlPath;
|
|
18
|
+
}
|
|
19
|
+
console.log('Creating sample cube STL...');
|
|
20
|
+
// Create a simple cube geometry
|
|
21
|
+
const geometry = new THREE.BoxGeometry(10, 10, 10);
|
|
22
|
+
const material = new THREE.MeshStandardMaterial();
|
|
23
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
24
|
+
// Import STLExporter
|
|
25
|
+
const { STLExporter } = await import('three/examples/jsm/exporters/STLExporter.js');
|
|
26
|
+
const exporter = new STLExporter();
|
|
27
|
+
// Export as STL
|
|
28
|
+
const stlString = exporter.parse(mesh);
|
|
29
|
+
// Write to file
|
|
30
|
+
fs.writeFileSync(sampleStlPath, stlString);
|
|
31
|
+
console.log(`Sample cube saved to ${sampleStlPath}`);
|
|
32
|
+
return sampleStlPath;
|
|
33
|
+
}
|
|
34
|
+
// Test the STL manipulator
|
|
35
|
+
async function testSTLManipulator() {
|
|
36
|
+
try {
|
|
37
|
+
// Create a sample STL for testing
|
|
38
|
+
const stlPath = await createSampleCube();
|
|
39
|
+
// Create an instance of the STL manipulator with the test directory
|
|
40
|
+
const manipulator = new STLManipulator(TEST_DIR);
|
|
41
|
+
// Listen for events from the manipulator
|
|
42
|
+
manipulator.on('operationComplete', (data) => {
|
|
43
|
+
console.log('Operation complete:', data);
|
|
44
|
+
});
|
|
45
|
+
manipulator.on('operationError', (data) => {
|
|
46
|
+
console.error('Operation error:', data);
|
|
47
|
+
});
|
|
48
|
+
// Test progress callback
|
|
49
|
+
const progressCallback = (progress, message) => {
|
|
50
|
+
console.log(`Progress: ${progress}% - ${message || ''}`);
|
|
51
|
+
};
|
|
52
|
+
// Test 1: Get information about the STL file
|
|
53
|
+
console.log('\n=== Test 1: Get STL Info ===');
|
|
54
|
+
const stlInfo = await manipulator.getSTLInfo(stlPath);
|
|
55
|
+
console.log('STL Info:', stlInfo);
|
|
56
|
+
// Test 2: Scale the STL file
|
|
57
|
+
console.log('\n=== Test 2: Scale STL ===');
|
|
58
|
+
const scaledStlPath = await manipulator.scaleSTL(stlPath, 2.0, progressCallback);
|
|
59
|
+
console.log(`Scaled STL saved to: ${scaledStlPath}`);
|
|
60
|
+
// Test 3: Rotate the STL file
|
|
61
|
+
console.log('\n=== Test 3: Rotate STL ===');
|
|
62
|
+
const rotatedStlPath = await manipulator.rotateSTL(stlPath, [45, 0, 0], progressCallback);
|
|
63
|
+
console.log(`Rotated STL saved to: ${rotatedStlPath}`);
|
|
64
|
+
// Test 4: Translate the STL file
|
|
65
|
+
console.log('\n=== Test 4: Translate STL ===');
|
|
66
|
+
const translatedStlPath = await manipulator.translateSTL(stlPath, [5, 10, 0], progressCallback);
|
|
67
|
+
console.log(`Translated STL saved to: ${translatedStlPath}`);
|
|
68
|
+
// Test 5: Extend the base of the STL file
|
|
69
|
+
console.log('\n=== Test 5: Extend Base ===');
|
|
70
|
+
const extendedStlPath = await manipulator.extendBase(stlPath, 0.5, progressCallback);
|
|
71
|
+
console.log(`Extended STL saved to: ${extendedStlPath}`);
|
|
72
|
+
// Test 6: Modify a section of the STL file
|
|
73
|
+
console.log('\n=== Test 6: Modify Section (Top) ===');
|
|
74
|
+
const modifiedStlPath = await manipulator.modifySection(stlPath, 'top', {
|
|
75
|
+
type: 'scale',
|
|
76
|
+
value: [1.5, 1.5, 1.5]
|
|
77
|
+
}, progressCallback);
|
|
78
|
+
console.log(`Modified STL saved to: ${modifiedStlPath}`);
|
|
79
|
+
// Test 7: Generate visualization of the STL file
|
|
80
|
+
console.log('\n=== Test 7: Generate Visualization ===');
|
|
81
|
+
const visualizationPath = await manipulator.generateVisualization(stlPath, 400, 400, progressCallback);
|
|
82
|
+
console.log(`Visualization saved to: ${visualizationPath}`);
|
|
83
|
+
console.log('\nAll tests completed successfully!');
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error('An error occurred during testing:', error);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Run the tests
|
|
91
|
+
testSTLManipulator().catch(console.error);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AxiosInstance } from "axios";
|
|
2
|
+
import { BambuPrinter } from "bambu-js";
|
|
3
|
+
export type BambuFTP = {
|
|
4
|
+
readDir: (path: string) => Promise<string[]>;
|
|
5
|
+
sendFile: (sourcePath: string, destinationPath: string, progressCallback?: (progress: number) => void) => Promise<void>;
|
|
6
|
+
removeFile: (path: string) => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export declare abstract class PrinterImplementation {
|
|
9
|
+
protected apiClient: AxiosInstance;
|
|
10
|
+
constructor(apiClient: AxiosInstance);
|
|
11
|
+
abstract getStatus(host: string, port: string, apiKey: string): Promise<any>;
|
|
12
|
+
abstract getFiles(host: string, port: string, apiKey: string): Promise<any>;
|
|
13
|
+
abstract getFile(host: string, port: string, apiKey: string, filename: string): Promise<any>;
|
|
14
|
+
abstract uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
|
|
15
|
+
abstract startJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
|
|
16
|
+
abstract cancelJob(host: string, port: string, apiKey: string): Promise<any>;
|
|
17
|
+
abstract setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
|
|
18
|
+
}
|
|
19
|
+
export declare class BambuPrinterStore {
|
|
20
|
+
private printers;
|
|
21
|
+
get(host: string, serial: string, token: string): InstanceType<typeof BambuPrinter>;
|
|
22
|
+
disconnectAll(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface SectionBounds {
|
|
25
|
+
minX: number;
|
|
26
|
+
minY: number;
|
|
27
|
+
minZ: number;
|
|
28
|
+
maxX: number;
|
|
29
|
+
maxY: number;
|
|
30
|
+
maxZ: number;
|
|
31
|
+
}
|
|
32
|
+
export interface ThreeMFMetadata {
|
|
33
|
+
[key: string]: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ThreeMFObject {
|
|
36
|
+
id: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
type?: string;
|
|
39
|
+
mesh?: any;
|
|
40
|
+
}
|
|
41
|
+
export interface ThreeMFBuildItem {
|
|
42
|
+
objectId: string;
|
|
43
|
+
transform?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface AMSFilamentMapping {
|
|
46
|
+
[filamentId: string]: number;
|
|
47
|
+
}
|
|
48
|
+
export interface BambuSlicerConfig {
|
|
49
|
+
layer_height?: number;
|
|
50
|
+
first_layer_height?: number;
|
|
51
|
+
sparse_infill_density?: number;
|
|
52
|
+
sparse_infill_pattern?: string;
|
|
53
|
+
support_enabled?: boolean;
|
|
54
|
+
support_type?: string;
|
|
55
|
+
support_threshold_angle?: number;
|
|
56
|
+
raft_layers?: number;
|
|
57
|
+
brim_width?: number;
|
|
58
|
+
wall_loops?: number;
|
|
59
|
+
top_shell_layers?: number;
|
|
60
|
+
bottom_shell_layers?: number;
|
|
61
|
+
nozzle_temperature?: number[];
|
|
62
|
+
bed_temperature?: number;
|
|
63
|
+
filament_type?: string[];
|
|
64
|
+
flow_ratio?: number[];
|
|
65
|
+
ams_mapping?: AMSFilamentMapping;
|
|
66
|
+
[key: string]: any;
|
|
67
|
+
}
|
|
68
|
+
export interface ThreeMFData {
|
|
69
|
+
metadata: ThreeMFMetadata;
|
|
70
|
+
objects: ThreeMFObject[];
|
|
71
|
+
build: {
|
|
72
|
+
items: ThreeMFBuildItem[];
|
|
73
|
+
};
|
|
74
|
+
slicerConfig?: Partial<BambuSlicerConfig>;
|
|
75
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BambuPrinter } from "bambu-js";
|
|
2
|
+
// Base class for printer implementations
|
|
3
|
+
export class PrinterImplementation {
|
|
4
|
+
constructor(apiClient) {
|
|
5
|
+
this.apiClient = apiClient;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
// Store for Bambu printers
|
|
9
|
+
export class BambuPrinterStore {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.printers = new Map();
|
|
12
|
+
}
|
|
13
|
+
get(host, serial, token) {
|
|
14
|
+
const key = `${host}-${serial}`;
|
|
15
|
+
if (!this.printers.has(key)) {
|
|
16
|
+
const printer = new BambuPrinter(host, serial, token);
|
|
17
|
+
this.printers.set(key, printer);
|
|
18
|
+
}
|
|
19
|
+
return this.printers.get(key);
|
|
20
|
+
}
|
|
21
|
+
async disconnectAll() {
|
|
22
|
+
for (const printer of this.printers.values()) {
|
|
23
|
+
await printer.disconnect();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-3d-printer-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "MCP server for connecting Claude with 3D printer management systems",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "dist/index.js",
|
|
6
6
|
"repository": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
|
@@ -12,8 +12,14 @@
|
|
|
12
12
|
"test": "npm run build && node dist/test/stl-manipulator-test.js"
|
|
13
13
|
},
|
|
14
14
|
"bin": {
|
|
15
|
-
"mcp-3d-printer-server": "./
|
|
15
|
+
"mcp-3d-printer-server": "./dist/index.js"
|
|
16
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
17
23
|
"keywords": [
|
|
18
24
|
"mcp",
|
|
19
25
|
"3d-printer",
|
|
@@ -34,11 +40,15 @@
|
|
|
34
40
|
"bambu-js": "^2.2.2",
|
|
35
41
|
"dotenv": "^16.3.1",
|
|
36
42
|
"form-data": "^4.0.0",
|
|
37
|
-
"
|
|
43
|
+
"jszip": "^3.10.1",
|
|
44
|
+
"three": "^0.173.0",
|
|
45
|
+
"xml2js": "^0.6.2"
|
|
38
46
|
},
|
|
39
47
|
"devDependencies": {
|
|
48
|
+
"@types/jszip": "^3.4.0",
|
|
40
49
|
"@types/node": "^18.0.0",
|
|
41
50
|
"@types/three": "^0.173.0",
|
|
51
|
+
"@types/xml2js": "^0.4.14",
|
|
42
52
|
"nodemon": "^2.0.22"
|
|
43
53
|
}
|
|
44
54
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import JSZip from 'jszip';
|
|
2
|
+
import { parseStringPromise } from 'xml2js';
|
|
3
|
+
import { ThreeMFData, BambuSlicerConfig, ThreeMFMetadata, ThreeMFObject, ThreeMFBuildItem, AMSFilamentMapping } from './types.js';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
// Parses Bambu Studio's JSON config data
|
|
7
|
+
function parseBambuJSONConfig(jsonData: any): Partial<BambuSlicerConfig> {
|
|
8
|
+
const config: Partial<BambuSlicerConfig> = {};
|
|
9
|
+
const amsMapping: AMSFilamentMapping = {};
|
|
10
|
+
|
|
11
|
+
// Map known keys, converting types as needed
|
|
12
|
+
if (jsonData.layer_height !== undefined) config.layer_height = parseFloat(jsonData.layer_height);
|
|
13
|
+
if (jsonData.initial_layer_print_height !== undefined) config.first_layer_height = parseFloat(jsonData.initial_layer_print_height);
|
|
14
|
+
if (jsonData.sparse_infill_density !== undefined) config.sparse_infill_density = parseFloat(jsonData.sparse_infill_density);
|
|
15
|
+
if (jsonData.sparse_infill_pattern !== undefined) config.sparse_infill_pattern = String(jsonData.sparse_infill_pattern);
|
|
16
|
+
if (jsonData.enable_support !== undefined) config.support_enabled = String(jsonData.enable_support) === '1' || String(jsonData.enable_support).toLowerCase() === 'true';
|
|
17
|
+
if (jsonData.support_type !== undefined) config.support_type = String(jsonData.support_type);
|
|
18
|
+
if (jsonData.support_angle !== undefined) config.support_threshold_angle = parseFloat(jsonData.support_angle);
|
|
19
|
+
if (jsonData.raft_layers !== undefined) config.raft_layers = parseInt(String(jsonData.raft_layers), 10);
|
|
20
|
+
if (jsonData.brim_width !== undefined) config.brim_width = parseFloat(jsonData.brim_width);
|
|
21
|
+
if (jsonData.wall_loops !== undefined) config.wall_loops = parseInt(String(jsonData.wall_loops), 10);
|
|
22
|
+
if (jsonData.top_shell_layers !== undefined) config.top_shell_layers = parseInt(String(jsonData.top_shell_layers), 10);
|
|
23
|
+
if (jsonData.bottom_shell_layers !== undefined) config.bottom_shell_layers = parseInt(String(jsonData.bottom_shell_layers), 10);
|
|
24
|
+
|
|
25
|
+
// Temperatures (handle potential arrays)
|
|
26
|
+
if (Array.isArray(jsonData.nozzle_temperature) && jsonData.nozzle_temperature.length > 0) {
|
|
27
|
+
config.nozzle_temperature = jsonData.nozzle_temperature.map((t: string | number) => parseFloat(String(t)));
|
|
28
|
+
} else if (jsonData.nozzle_temperature !== undefined) {
|
|
29
|
+
config.nozzle_temperature = [parseFloat(String(jsonData.nozzle_temperature))];
|
|
30
|
+
}
|
|
31
|
+
// Bed Temp (prefer initial layer temp)
|
|
32
|
+
let bedTempKey = jsonData.hot_plate_temp_initial_layer !== undefined ? 'hot_plate_temp_initial_layer' : 'hot_plate_temp';
|
|
33
|
+
if (jsonData[bedTempKey] !== undefined) {
|
|
34
|
+
if (Array.isArray(jsonData[bedTempKey]) && jsonData[bedTempKey].length > 0) {
|
|
35
|
+
config.bed_temperature = parseFloat(String(jsonData[bedTempKey][0]));
|
|
36
|
+
} else {
|
|
37
|
+
config.bed_temperature = parseFloat(String(jsonData[bedTempKey]));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Filaments and Flow (handle potential arrays)
|
|
42
|
+
if (Array.isArray(jsonData.filament_type) && jsonData.filament_type.length > 0) {
|
|
43
|
+
config.filament_type = jsonData.filament_type.map(String);
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(jsonData.filament_flow_ratio) && jsonData.filament_flow_ratio.length > 0) {
|
|
46
|
+
config.flow_ratio = jsonData.filament_flow_ratio.map((f: string | number) => parseFloat(String(f)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// AMS Mapping (derive from filament_settings_id order)
|
|
50
|
+
if (Array.isArray(jsonData.filament_settings_id) && jsonData.filament_settings_id.length > 0) {
|
|
51
|
+
jsonData.filament_settings_id.forEach((filamentId: string, index: number) => {
|
|
52
|
+
// Map filament ID (e.g., "Generic PLA @BBL P1P") to its index (assumed AMS slot)
|
|
53
|
+
if (filamentId) { // Ensure filamentId is not empty
|
|
54
|
+
amsMapping[filamentId] = index;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (Object.keys(amsMapping).length > 0) {
|
|
58
|
+
config.ams_mapping = amsMapping;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Store remaining keys found in the config
|
|
63
|
+
for (const key in jsonData) {
|
|
64
|
+
if (!config.hasOwnProperty(key)) {
|
|
65
|
+
// Simple assignment for remaining keys, preserving original type if possible
|
|
66
|
+
config[key] = jsonData[key];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function parseBambuConfig(zip: JSZip): Promise<Partial<BambuSlicerConfig>> {
|
|
74
|
+
// Look for common Bambu config file names
|
|
75
|
+
const potentialFiles = ['Metadata/project_settings.config', 'Metadata/model_settings.config', 'Metadata/slice_info.config'];
|
|
76
|
+
let configFile = null;
|
|
77
|
+
let configContent = '';
|
|
78
|
+
|
|
79
|
+
for (const name of potentialFiles) {
|
|
80
|
+
const file = zip.file(name);
|
|
81
|
+
if (file) {
|
|
82
|
+
configFile = file;
|
|
83
|
+
configContent = await file.async('string');
|
|
84
|
+
console.log(`Found Bambu config file: ${configFile.name}`);
|
|
85
|
+
break; // Use the first one found (project usually has the most)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (configFile && configContent) {
|
|
90
|
+
try {
|
|
91
|
+
// Attempt to parse as JSON
|
|
92
|
+
const jsonData = JSON.parse(configContent);
|
|
93
|
+
const parsedConfig = parseBambuJSONConfig(jsonData);
|
|
94
|
+
console.log('Bambu config parsed successfully as JSON.');
|
|
95
|
+
return parsedConfig;
|
|
96
|
+
} catch (jsonError: any) {
|
|
97
|
+
console.warn(`Failed to parse ${configFile.name} as JSON: ${jsonError.message}. Attempting INI parse as fallback...`);
|
|
98
|
+
// Fallback: Try parsing as INI if JSON fails (though unlikely based on sample)
|
|
99
|
+
try {
|
|
100
|
+
const parsedConfig = parseFallbackINIConfig(configContent); // Keep a simple INI parser as fallback
|
|
101
|
+
console.log('Bambu config parsed successfully as INI (fallback).');
|
|
102
|
+
return parsedConfig;
|
|
103
|
+
} catch (iniError: any) {
|
|
104
|
+
console.error(`Error parsing Bambu config ${configFile.name} as INI (fallback):`, iniError);
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
console.log('No Bambu-specific config file found in Metadata directory.');
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fallback INI parser (Simplified version of the previous one)
|
|
115
|
+
function parseFallbackINIConfig(configContent: string): Partial<BambuSlicerConfig> {
|
|
116
|
+
const config: Partial<BambuSlicerConfig> = {};
|
|
117
|
+
const lines = configContent.split(/\r?\n/);
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
const trimmedLine = line.trim();
|
|
120
|
+
if (!trimmedLine || trimmedLine.startsWith('#') || trimmedLine.startsWith(';')) continue;
|
|
121
|
+
const equalsIndex = trimmedLine.indexOf('=');
|
|
122
|
+
if (equalsIndex === -1) continue;
|
|
123
|
+
const key = trimmedLine.substring(0, equalsIndex).trim();
|
|
124
|
+
const value = trimmedLine.substring(equalsIndex + 1).trim();
|
|
125
|
+
// Simple assignment - no type conversion here for fallback
|
|
126
|
+
config[key] = value;
|
|
127
|
+
}
|
|
128
|
+
return config;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function parse3DModelConfig(zip: JSZip): Promise<{ metadata: ThreeMFMetadata, objects: ThreeMFObject[], build: { items: ThreeMFBuildItem[] } }> {
|
|
132
|
+
const modelFile = zip.file('3D/3dmodel.model');
|
|
133
|
+
if (!modelFile) {
|
|
134
|
+
throw new Error('3D/3dmodel.model not found in 3MF archive');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const modelContent = await modelFile.async('string');
|
|
138
|
+
try {
|
|
139
|
+
const parsedXml = await parseStringPromise(modelContent, {
|
|
140
|
+
explicitArray: false,
|
|
141
|
+
mergeAttrs: true,
|
|
142
|
+
charkey: 'value'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const modelData = parsedXml.model;
|
|
146
|
+
if (!modelData) {
|
|
147
|
+
throw new Error('Invalid 3dmodel.model format: <model> tag not found.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const metadata: ThreeMFMetadata = { unit: modelData.unit || 'millimeter' };
|
|
151
|
+
if (modelData.metadata) {
|
|
152
|
+
const metas = Array.isArray(modelData.metadata) ? modelData.metadata : [modelData.metadata];
|
|
153
|
+
metas.forEach((meta: any) => {
|
|
154
|
+
if (meta.name && meta.value !== undefined) {
|
|
155
|
+
metadata[meta.name] = String(meta.value);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const objects: ThreeMFObject[] = [];
|
|
161
|
+
if (modelData.resources?.object) {
|
|
162
|
+
const resources = Array.isArray(modelData.resources.object) ? modelData.resources.object : [modelData.resources.object];
|
|
163
|
+
resources.forEach((objResource: any) => {
|
|
164
|
+
objects.push({
|
|
165
|
+
id: objResource.id,
|
|
166
|
+
name: objResource.name,
|
|
167
|
+
type: objResource.type || 'model'
|
|
168
|
+
// Actual mesh data (vertices, triangles) is nested deeper if needed
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const buildItems: ThreeMFBuildItem[] = [];
|
|
174
|
+
if (modelData.build?.item) {
|
|
175
|
+
const items = Array.isArray(modelData.build.item) ? modelData.build.item : [modelData.build.item];
|
|
176
|
+
items.forEach((item: any) => {
|
|
177
|
+
buildItems.push({
|
|
178
|
+
objectId: item.objectid,
|
|
179
|
+
transform: item.transform
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log('3dmodel.model parsed successfully.');
|
|
185
|
+
return { metadata, objects, build: { items: buildItems } };
|
|
186
|
+
|
|
187
|
+
} catch (error: any) {
|
|
188
|
+
console.error('Error parsing 3dmodel.model XML:', error);
|
|
189
|
+
throw new Error(`Failed to parse 3dmodel.model: ${error.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { ThreeMFData };
|
|
194
|
+
|
|
195
|
+
export async function parse3MF(filePath: string): Promise<ThreeMFData> {
|
|
196
|
+
console.log(`Parsing 3MF file: ${filePath}`);
|
|
197
|
+
try {
|
|
198
|
+
const data = await fs.readFile(filePath);
|
|
199
|
+
const zip = await JSZip.loadAsync(data);
|
|
200
|
+
|
|
201
|
+
// List files for debugging
|
|
202
|
+
// const fileList = Object.keys(zip.files);
|
|
203
|
+
// console.log('Files in 3MF:', fileList);
|
|
204
|
+
|
|
205
|
+
const { metadata, objects, build } = await parse3DModelConfig(zip);
|
|
206
|
+
const bambuConfig = await parseBambuConfig(zip);
|
|
207
|
+
|
|
208
|
+
// Combine data into the final structure
|
|
209
|
+
const combinedData: ThreeMFData = {
|
|
210
|
+
metadata,
|
|
211
|
+
objects,
|
|
212
|
+
build,
|
|
213
|
+
slicerConfig: bambuConfig
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
console.log('3MF parsing completed.');
|
|
217
|
+
return combinedData;
|
|
218
|
+
|
|
219
|
+
} catch (error: any) {
|
|
220
|
+
console.error(`Error parsing 3MF file ${filePath}:`, error);
|
|
221
|
+
throw new Error(`Failed to parse 3MF file: ${error.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|