mcp-3d-printer-server 0.2.1 → 0.2.2

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.
Files changed (37) hide show
  1. package/README.md +22 -0
  2. package/package.json +2 -1
  3. package/dist/index.d.ts +0 -2
  4. package/dist/index.js +0 -800
  5. package/dist/index.js.map +0 -1
  6. package/dist/printers/bambu.d.ts +0 -25
  7. package/dist/printers/bambu.js +0 -111
  8. package/dist/printers/creality.d.ts +0 -10
  9. package/dist/printers/creality.js +0 -90
  10. package/dist/printers/duet.d.ts +0 -10
  11. package/dist/printers/duet.js +0 -66
  12. package/dist/printers/klipper.d.ts +0 -10
  13. package/dist/printers/klipper.js +0 -62
  14. package/dist/printers/octoprint.d.ts +0 -10
  15. package/dist/printers/octoprint.js +0 -97
  16. package/dist/printers/printer-factory.d.ts +0 -9
  17. package/dist/printers/printer-factory.js +0 -33
  18. package/dist/printers/prusa.d.ts +0 -10
  19. package/dist/printers/prusa.js +0 -93
  20. package/dist/printers/repetier.d.ts +0 -10
  21. package/dist/printers/repetier.js +0 -59
  22. package/dist/stl/stl-manipulator.d.ts +0 -127
  23. package/dist/stl/stl-manipulator.js +0 -870
  24. package/dist/test/stl-manipulator-test.d.ts +0 -2
  25. package/dist/test/stl-manipulator-test.js +0 -91
  26. package/dist/types.d.ts +0 -23
  27. package/dist/types.js +0 -26
  28. package/src/test/stl-manipulator-test.js +0 -114
  29. package/src/test/stl-manipulator-test.ts +0 -124
  30. package/src/test/tsconfig.json +0 -13
  31. package/test/sample_cube.stl +0 -86
  32. package/test/sample_cube_extended.stl +0 -172
  33. package/test/sample_cube_modified.stl +0 -86
  34. package/test/sample_cube_rotated.stl +0 -86
  35. package/test/sample_cube_scaled.stl +0 -86
  36. package/test/sample_cube_translated.stl +0 -86
  37. package/test/sample_cube_visualization.svg +0 -52
@@ -1,870 +0,0 @@
1
- import * as THREE from 'three';
2
- import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
3
- import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js';
4
- import fs from 'fs';
5
- import path from 'path';
6
- import { promisify } from 'util';
7
- import { EventEmitter } from 'events';
8
- import * as crypto from 'crypto';
9
- const readFileAsync = promisify(fs.readFile);
10
- const writeFileAsync = promisify(fs.writeFile);
11
- export class STLManipulator extends EventEmitter {
12
- constructor(tempDir = path.join(process.cwd(), 'temp')) {
13
- super();
14
- this.activeOperations = new Map();
15
- this.tempDir = tempDir;
16
- // Ensure temp directory exists
17
- if (!fs.existsSync(this.tempDir)) {
18
- fs.mkdirSync(this.tempDir, { recursive: true });
19
- }
20
- }
21
- /**
22
- * Generate a unique operation ID
23
- */
24
- generateOperationId() {
25
- return crypto.randomUUID();
26
- }
27
- /**
28
- * Load STL file and return geometry and bounding box
29
- */
30
- async loadSTL(stlFilePath, progressCallback) {
31
- try {
32
- if (progressCallback)
33
- progressCallback(10, "Loading STL file...");
34
- // Read the STL file
35
- const stlData = await readFileAsync(stlFilePath);
36
- if (progressCallback)
37
- progressCallback(30, "Parsing STL data...");
38
- // Load the STL data into a Three.js geometry
39
- const loader = new STLLoader();
40
- const geometry = loader.parse(stlData.buffer);
41
- // Create a mesh from the geometry
42
- const material = new THREE.MeshStandardMaterial();
43
- const mesh = new THREE.Mesh(geometry, material);
44
- // Compute the bounding box
45
- geometry.computeBoundingBox();
46
- const boundingBox = geometry.boundingBox;
47
- if (progressCallback)
48
- progressCallback(50, "STL loaded successfully");
49
- return { geometry, boundingBox, mesh };
50
- }
51
- catch (error) {
52
- console.error("Error loading STL file:", error);
53
- throw new Error(`Failed to load STL file: ${error instanceof Error ? error.message : String(error)}`);
54
- }
55
- }
56
- /**
57
- * Save a geometry to STL file
58
- */
59
- async saveSTL(geometry, outputFilePath, progressCallback) {
60
- try {
61
- if (progressCallback)
62
- progressCallback(80, "Exporting to STL...");
63
- // Create mesh for export
64
- const material = new THREE.MeshStandardMaterial();
65
- const mesh = new THREE.Mesh(geometry, material);
66
- // Export the mesh as STL
67
- const exporter = new STLExporter();
68
- const stlString = exporter.parse(mesh);
69
- // Write the STL to file
70
- await writeFileAsync(outputFilePath, stlString);
71
- if (progressCallback)
72
- progressCallback(100, "STL saved successfully");
73
- return outputFilePath;
74
- }
75
- catch (error) {
76
- console.error("Error saving STL file:", error);
77
- throw new Error(`Failed to save STL file: ${error instanceof Error ? error.message : String(error)}`);
78
- }
79
- }
80
- /**
81
- * Get comprehensive information about an STL file
82
- */
83
- async getSTLInfo(stlFilePath) {
84
- try {
85
- const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
86
- const fileStats = fs.statSync(stlFilePath);
87
- // Count faces (each face is a triangle in STL)
88
- const positionAttribute = geometry.getAttribute('position');
89
- const vertexCount = positionAttribute.count;
90
- const faceCount = vertexCount / 3;
91
- // Calculate center and dimensions
92
- const center = new THREE.Vector3();
93
- boundingBox.getCenter(center);
94
- const dimensions = new THREE.Vector3();
95
- boundingBox.getSize(dimensions);
96
- return {
97
- filePath: stlFilePath,
98
- fileName: path.basename(stlFilePath),
99
- fileSize: fileStats.size,
100
- boundingBox: {
101
- min: boundingBox.min,
102
- max: boundingBox.max,
103
- center,
104
- dimensions
105
- },
106
- vertexCount,
107
- faceCount
108
- };
109
- }
110
- catch (error) {
111
- console.error("Error getting STL info:", error);
112
- throw new Error(`Failed to get STL info: ${error instanceof Error ? error.message : String(error)}`);
113
- }
114
- }
115
- /**
116
- * Scale an STL model uniformly or along specific axes
117
- */
118
- async scaleSTL(stlFilePath, scaleFactors, progressCallback) {
119
- const operationId = this.generateOperationId();
120
- this.activeOperations.set(operationId, true);
121
- try {
122
- if (progressCallback)
123
- progressCallback(0, "Starting scaling operation...");
124
- // Load the STL file
125
- const { geometry, mesh } = await this.loadSTL(stlFilePath, progressCallback);
126
- if (!this.activeOperations.get(operationId)) {
127
- throw new Error("Operation cancelled");
128
- }
129
- if (progressCallback)
130
- progressCallback(60, "Applying scaling transformation...");
131
- // Apply scaling
132
- let scaleX, scaleY, scaleZ;
133
- if (typeof scaleFactors === 'number') {
134
- // Uniform scaling
135
- scaleX = scaleY = scaleZ = scaleFactors;
136
- }
137
- else {
138
- // Non-uniform scaling
139
- [scaleX, scaleY, scaleZ] = scaleFactors;
140
- }
141
- const scaleMatrix = new THREE.Matrix4().makeScale(scaleX, scaleY, scaleZ);
142
- geometry.applyMatrix4(scaleMatrix);
143
- if (!this.activeOperations.get(operationId)) {
144
- throw new Error("Operation cancelled");
145
- }
146
- // Generate output file path
147
- const outputFileName = path.basename(stlFilePath, '.stl') + '_scaled.stl';
148
- const outputFilePath = path.join(this.tempDir, outputFileName);
149
- // Save the modified STL
150
- await this.saveSTL(geometry, outputFilePath, progressCallback);
151
- this.emit('operationComplete', {
152
- operationId,
153
- type: 'scale',
154
- success: true,
155
- output: outputFilePath
156
- });
157
- return outputFilePath;
158
- }
159
- catch (error) {
160
- this.emit('operationError', {
161
- operationId,
162
- type: 'scale',
163
- error: error instanceof Error ? error.message : String(error)
164
- });
165
- throw error;
166
- }
167
- finally {
168
- this.activeOperations.delete(operationId);
169
- }
170
- }
171
- /**
172
- * Rotate an STL model around specific axes
173
- */
174
- async rotateSTL(stlFilePath, rotationAngles, // [x, y, z] in degrees
175
- progressCallback) {
176
- const operationId = this.generateOperationId();
177
- this.activeOperations.set(operationId, true);
178
- try {
179
- if (progressCallback)
180
- progressCallback(0, "Starting rotation operation...");
181
- // Load the STL file
182
- const { geometry, boundingBox } = await this.loadSTL(stlFilePath, progressCallback);
183
- if (!this.activeOperations.get(operationId)) {
184
- throw new Error("Operation cancelled");
185
- }
186
- if (progressCallback)
187
- progressCallback(60, "Applying rotation transformation...");
188
- // Convert degrees to radians
189
- const [rotX, rotY, rotZ] = rotationAngles.map(angle => angle * Math.PI / 180);
190
- // Get the center of the model
191
- const center = new THREE.Vector3();
192
- boundingBox.getCenter(center);
193
- // Create translation matrices to rotate around the center
194
- const toOriginMatrix = new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z);
195
- const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(rotX, rotY, rotZ, 'XYZ'));
196
- const fromOriginMatrix = new THREE.Matrix4().makeTranslation(center.x, center.y, center.z);
197
- // Apply the transformations
198
- geometry.applyMatrix4(toOriginMatrix);
199
- geometry.applyMatrix4(rotationMatrix);
200
- geometry.applyMatrix4(fromOriginMatrix);
201
- if (!this.activeOperations.get(operationId)) {
202
- throw new Error("Operation cancelled");
203
- }
204
- // Generate output file path
205
- const outputFileName = path.basename(stlFilePath, '.stl') + '_rotated.stl';
206
- const outputFilePath = path.join(this.tempDir, outputFileName);
207
- // Save the modified STL
208
- await this.saveSTL(geometry, outputFilePath, progressCallback);
209
- this.emit('operationComplete', {
210
- operationId,
211
- type: 'rotate',
212
- success: true,
213
- output: outputFilePath
214
- });
215
- return outputFilePath;
216
- }
217
- catch (error) {
218
- this.emit('operationError', {
219
- operationId,
220
- type: 'rotate',
221
- error: error instanceof Error ? error.message : String(error)
222
- });
223
- throw error;
224
- }
225
- finally {
226
- this.activeOperations.delete(operationId);
227
- }
228
- }
229
- /**
230
- * Translate (move) an STL model along specific axes
231
- */
232
- async translateSTL(stlFilePath, translationValues, // [x, y, z] in mm
233
- progressCallback) {
234
- const operationId = this.generateOperationId();
235
- this.activeOperations.set(operationId, true);
236
- try {
237
- if (progressCallback)
238
- progressCallback(0, "Starting translation operation...");
239
- // Load the STL file
240
- const { geometry } = await this.loadSTL(stlFilePath, progressCallback);
241
- if (!this.activeOperations.get(operationId)) {
242
- throw new Error("Operation cancelled");
243
- }
244
- if (progressCallback)
245
- progressCallback(60, "Applying translation transformation...");
246
- // Apply translation
247
- const [translateX, translateY, translateZ] = translationValues;
248
- const translationMatrix = new THREE.Matrix4().makeTranslation(translateX, translateY, translateZ);
249
- geometry.applyMatrix4(translationMatrix);
250
- if (!this.activeOperations.get(operationId)) {
251
- throw new Error("Operation cancelled");
252
- }
253
- // Generate output file path
254
- const outputFileName = path.basename(stlFilePath, '.stl') + '_translated.stl';
255
- const outputFilePath = path.join(this.tempDir, outputFileName);
256
- // Save the modified STL
257
- await this.saveSTL(geometry, outputFilePath, progressCallback);
258
- this.emit('operationComplete', {
259
- operationId,
260
- type: 'translate',
261
- success: true,
262
- output: outputFilePath
263
- });
264
- return outputFilePath;
265
- }
266
- catch (error) {
267
- this.emit('operationError', {
268
- operationId,
269
- type: 'translate',
270
- error: error instanceof Error ? error.message : String(error)
271
- });
272
- throw error;
273
- }
274
- finally {
275
- this.activeOperations.delete(operationId);
276
- }
277
- }
278
- /**
279
- * Cancel an ongoing operation
280
- */
281
- cancelOperation(operationId) {
282
- if (this.activeOperations.has(operationId)) {
283
- this.activeOperations.set(operationId, false);
284
- this.emit('operationCancelled', { operationId });
285
- return true;
286
- }
287
- return false;
288
- }
289
- /**
290
- * Generate an SVG visualization of an STL file from multiple angles
291
- * @param stlFilePath Path to the STL file
292
- * @param width Width of each view in pixels
293
- * @param height Height of each view in pixels
294
- * @param progressCallback Optional callback for progress updates
295
- * @returns Path to the generated SVG file
296
- */
297
- async generateVisualization(stlFilePath, width = 300, height = 300, progressCallback) {
298
- const operationId = this.generateOperationId();
299
- this.activeOperations.set(operationId, true);
300
- try {
301
- if (progressCallback)
302
- progressCallback(0, "Starting visualization generation...");
303
- // Load the STL file
304
- const { geometry, boundingBox, mesh } = await this.loadSTL(stlFilePath, progressCallback);
305
- if (!this.activeOperations.get(operationId)) {
306
- throw new Error("Operation cancelled");
307
- }
308
- if (progressCallback)
309
- progressCallback(50, "Setting up 3D scene...");
310
- // Create a scene
311
- const scene = new THREE.Scene();
312
- scene.add(mesh);
313
- // Create a camera
314
- const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
315
- // Calculate the ideal camera position based on bounding box
316
- const size = new THREE.Vector3();
317
- boundingBox.getSize(size);
318
- const maxDim = Math.max(size.x, size.y, size.z);
319
- const fov = camera.fov * (Math.PI / 180);
320
- const cameraDistance = (maxDim / 2) / Math.tan(fov / 2) * 1.5; // 1.5 is a factor for some padding
321
- // Add lighting to the scene
322
- const ambientLight = new THREE.AmbientLight(0x404040); // soft white light
323
- scene.add(ambientLight);
324
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
325
- directionalLight.position.set(1, 1, 1).normalize();
326
- scene.add(directionalLight);
327
- if (!this.activeOperations.get(operationId)) {
328
- throw new Error("Operation cancelled");
329
- }
330
- if (progressCallback)
331
- progressCallback(60, "Generating SVG representation...");
332
- // Since we can't use the DOM-dependent SVGRenderer in a Node.js environment,
333
- // let's create a simple representation of the model using its bounding box
334
- // This is a simplified visual representation
335
- // Create SVG content with a simple representation of the STL model
336
- const viewBox = `0 0 ${width * 2} ${height * 2}`;
337
- let svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" width="${width * 2}" height="${height * 2}">`;
338
- // Add a title and model info
339
- svgContent += `
340
- <text x="10" y="20" font-family="Arial" font-size="16" fill="black">
341
- STL Visualization: ${path.basename(stlFilePath)}
342
- </text>
343
- <text x="10" y="40" font-family="Arial" font-size="12" fill="black">
344
- Dimensions: ${size.x.toFixed(2)} x ${size.y.toFixed(2)} x ${size.z.toFixed(2)} mm
345
- </text>
346
- `;
347
- // Define views
348
- const views = [
349
- { name: "front", transform: "rotateY(0deg)" },
350
- { name: "side", transform: "rotateY(90deg)" },
351
- { name: "top", transform: "rotateX(90deg)" },
352
- { name: "isometric", transform: "rotateX(30deg) rotateY(45deg)" }
353
- ];
354
- // Draw each view
355
- for (let i = 0; i < views.length; i++) {
356
- const view = views[i];
357
- const x = (i % 2) * width + width / 2;
358
- const y = Math.floor(i / 2) * height + height / 2 + 50; // Add 50px for the header text
359
- // Calculate a representative size for the simple cube visualization
360
- const cubeSize = Math.min(width, height) * 0.4;
361
- // Draw a simple cube representation
362
- svgContent += `
363
- <g transform="translate(${x}, ${y})">
364
- <rect x="${-cubeSize / 2}" y="${-cubeSize / 2}" width="${cubeSize}" height="${cubeSize}"
365
- style="fill:#e0e0e0;stroke:#000;stroke-width:1;opacity:0.8;${view.transform}" />
366
- <text x="0" y="${cubeSize / 2 + 30}" text-anchor="middle" font-family="Arial" font-size="12" fill="black">
367
- ${view.name}
368
- </text>
369
- </g>
370
- `;
371
- if (progressCallback)
372
- progressCallback(60 + (i + 1) * 10, `Generated ${view.name} view`);
373
- }
374
- // Add STL information
375
- svgContent += `
376
- <g transform="translate(20, ${height * 2 - 60})">
377
- <text font-family="Arial" font-size="14" fill="black">
378
- File: ${path.basename(stlFilePath)}
379
- </text>
380
- <text y="20" font-family="Arial" font-size="12" fill="black">
381
- Vertices: ${mesh.geometry.attributes.position.count / 3}
382
- </text>
383
- <text y="40" font-family="Arial" font-size="12" fill="black">
384
- Dimensions: W:${size.x.toFixed(2)}mm × H:${size.y.toFixed(2)}mm × D:${size.z.toFixed(2)}mm
385
- </text>
386
- </g>
387
- `;
388
- // Close the SVG
389
- svgContent += '</svg>';
390
- if (progressCallback)
391
- progressCallback(90, "Visualization generated");
392
- if (progressCallback)
393
- progressCallback(90, "Saving visualization...");
394
- // Write the SVG to file
395
- const outputFileName = path.basename(stlFilePath, '.stl') + '_visualization.svg';
396
- const outputFilePath = path.join(this.tempDir, outputFileName);
397
- await writeFileAsync(outputFilePath, svgContent);
398
- if (progressCallback)
399
- progressCallback(100, "Visualization saved successfully");
400
- this.emit('operationComplete', {
401
- operationId,
402
- type: 'visualization',
403
- success: true,
404
- output: outputFilePath
405
- });
406
- return outputFilePath;
407
- }
408
- catch (error) {
409
- console.error("Error generating visualization:", error);
410
- this.emit('operationError', {
411
- operationId,
412
- type: 'visualization',
413
- error: error instanceof Error ? error.message : String(error)
414
- });
415
- throw new Error(`Failed to generate visualization: ${error instanceof Error ? error.message : String(error)}`);
416
- }
417
- finally {
418
- this.activeOperations.delete(operationId);
419
- }
420
- }
421
- /**
422
- * Apply a specific transformation to a selected section of an STL file
423
- * This allows for targeted modifications of specific parts of a model
424
- */
425
- async modifySection(stlFilePath, selection, transformation, progressCallback) {
426
- const operationId = this.generateOperationId();
427
- this.activeOperations.set(operationId, true);
428
- try {
429
- if (progressCallback)
430
- progressCallback(0, "Starting section modification...");
431
- // Load the STL file
432
- const { geometry, boundingBox, mesh } = await this.loadSTL(stlFilePath, progressCallback);
433
- if (!this.activeOperations.get(operationId)) {
434
- throw new Error("Operation cancelled");
435
- }
436
- if (progressCallback)
437
- progressCallback(40, "Identifying section to modify...");
438
- // Convert named sections to actual bounding boxes
439
- let selectionBox;
440
- if (selection === 'top') {
441
- // Select top third of the model
442
- const height = boundingBox.max.y - boundingBox.min.y;
443
- const topThreshold = boundingBox.max.y - (height / 3);
444
- selectionBox = new THREE.Box3(new THREE.Vector3(boundingBox.min.x, topThreshold, boundingBox.min.z), new THREE.Vector3(boundingBox.max.x, boundingBox.max.y, boundingBox.max.z));
445
- }
446
- else if (selection === 'bottom') {
447
- // Select bottom third of the model
448
- const height = boundingBox.max.y - boundingBox.min.y;
449
- const bottomThreshold = boundingBox.min.y + (height / 3);
450
- selectionBox = new THREE.Box3(new THREE.Vector3(boundingBox.min.x, boundingBox.min.y, boundingBox.min.z), new THREE.Vector3(boundingBox.max.x, bottomThreshold, boundingBox.max.z));
451
- }
452
- else if (selection === 'center') {
453
- // Select middle third of the model
454
- const height = boundingBox.max.y - boundingBox.min.y;
455
- const bottomThreshold = boundingBox.min.y + (height / 3);
456
- const topThreshold = boundingBox.max.y - (height / 3);
457
- selectionBox = new THREE.Box3(new THREE.Vector3(boundingBox.min.x, bottomThreshold, boundingBox.min.z), new THREE.Vector3(boundingBox.max.x, topThreshold, boundingBox.max.z));
458
- }
459
- else {
460
- // Use the provided bounding box
461
- selectionBox = selection;
462
- }
463
- if (progressCallback)
464
- progressCallback(50, "Applying transformation to selected section...");
465
- // Get position attribute for direct manipulation
466
- const positionAttribute = geometry.getAttribute('position');
467
- const positions = positionAttribute.array;
468
- // Create transformation matrix based on the requested operation
469
- let transformMatrix = new THREE.Matrix4();
470
- const center = new THREE.Vector3();
471
- selectionBox.getCenter(center);
472
- // Matrices for transforming around the selection center
473
- const toOriginMatrix = new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z);
474
- const fromOriginMatrix = new THREE.Matrix4().makeTranslation(center.x, center.y, center.z);
475
- // Build the appropriate transformation matrix
476
- switch (transformation.type) {
477
- case 'scale':
478
- if (typeof transformation.value === 'number') {
479
- transformMatrix = new THREE.Matrix4().makeScale(transformation.value, transformation.value, transformation.value);
480
- }
481
- else {
482
- const [scaleX, scaleY, scaleZ] = transformation.value;
483
- transformMatrix = new THREE.Matrix4().makeScale(scaleX, scaleY, scaleZ);
484
- }
485
- break;
486
- case 'rotate':
487
- const rotValues = (typeof transformation.value === 'number')
488
- ? [0, 0, transformation.value * Math.PI / 180]
489
- : transformation.value.map(v => v * Math.PI / 180);
490
- transformMatrix = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(rotValues[0], rotValues[1], rotValues[2], 'XYZ'));
491
- break;
492
- case 'translate':
493
- if (typeof transformation.value === 'number') {
494
- const translateValue = transformation.value;
495
- transformMatrix = new THREE.Matrix4().makeTranslation(translateValue, translateValue, translateValue);
496
- }
497
- else {
498
- const [transX, transY, transZ] = transformation.value;
499
- transformMatrix = new THREE.Matrix4().makeTranslation(transX, transY, transZ);
500
- }
501
- break;
502
- default:
503
- throw new Error(`Unsupported transformation type: ${transformation.type}`);
504
- }
505
- // Build complete transformation (to origin, transform, back from origin)
506
- const finalTransform = new THREE.Matrix4()
507
- .multiply(fromOriginMatrix)
508
- .multiply(transformMatrix)
509
- .multiply(toOriginMatrix);
510
- // Create temporary vector for calculations
511
- const tempVector = new THREE.Vector3();
512
- try {
513
- // Apply transformation only to vertices within the selection box
514
- for (let i = 0; i < positionAttribute.count; i++) {
515
- tempVector.fromBufferAttribute(positionAttribute, i);
516
- // Check if this vertex is within our selection box
517
- if (selectionBox.containsPoint(tempVector)) {
518
- // Apply the transformation to this vertex
519
- tempVector.applyMatrix4(finalTransform);
520
- // Update the position in the buffer
521
- positionAttribute.setXYZ(i, tempVector.x, tempVector.y, tempVector.z);
522
- }
523
- }
524
- // Mark the attribute as needing an update
525
- positionAttribute.needsUpdate = true;
526
- // Update the geometry's bounding box
527
- geometry.computeBoundingBox();
528
- }
529
- catch (error) {
530
- console.error("Error modifying vertices:", error);
531
- throw new Error(`Failed to modify section: ${error instanceof Error ? error.message : String(error)}`);
532
- }
533
- if (!this.activeOperations.get(operationId)) {
534
- throw new Error("Operation cancelled");
535
- }
536
- // Generate output file path
537
- const outputFileName = path.basename(stlFilePath, '.stl') + '_modified.stl';
538
- const outputFilePath = path.join(this.tempDir, outputFileName);
539
- // Save the modified STL
540
- await this.saveSTL(geometry, outputFilePath, progressCallback);
541
- this.emit('operationComplete', {
542
- operationId,
543
- type: 'modifySection',
544
- success: true,
545
- output: outputFilePath
546
- });
547
- return outputFilePath;
548
- }
549
- catch (error) {
550
- this.emit('operationError', {
551
- operationId,
552
- type: 'modifySection',
553
- error: error instanceof Error ? error.message : String(error)
554
- });
555
- throw error;
556
- }
557
- finally {
558
- this.activeOperations.delete(operationId);
559
- }
560
- }
561
- /**
562
- * Enhanced version of extendBase with progress reporting
563
- * @param stlFilePath Path to the input STL file
564
- * @param extensionInches Amount to extend base in inches
565
- * @param progressCallback Optional callback for progress updates
566
- * @returns Path to the modified STL file
567
- */
568
- async extendBase(stlFilePath, extensionInches, progressCallback) {
569
- const operationId = this.generateOperationId();
570
- this.activeOperations.set(operationId, true);
571
- try {
572
- if (progressCallback)
573
- progressCallback(0, "Starting base extension operation...");
574
- console.log(`Extending base of ${stlFilePath} by ${extensionInches} inches`);
575
- // Load the STL file
576
- const { geometry, boundingBox } = await this.loadSTL(stlFilePath, progressCallback);
577
- if (!this.activeOperations.get(operationId)) {
578
- throw new Error("Operation cancelled");
579
- }
580
- if (progressCallback)
581
- progressCallback(60, "Creating extended base geometry...");
582
- // Find the minimum Y value (assuming Y is up, which is common in 3D printing)
583
- const minY = boundingBox.min.y;
584
- // Convert inches to millimeters (STL files typically use mm)
585
- const extensionMm = extensionInches * 25.4;
586
- // Create a transformation matrix to move the mesh up by the extension amount
587
- const matrix = new THREE.Matrix4().makeTranslation(0, extensionMm, 0);
588
- geometry.applyMatrix4(matrix);
589
- // Create a box geometry for the base extension
590
- const baseWidth = boundingBox.max.x - boundingBox.min.x;
591
- const baseDepth = boundingBox.max.z - boundingBox.min.z;
592
- const baseGeometry = new THREE.BoxGeometry(baseWidth, extensionMm, baseDepth);
593
- // Position the base geometry
594
- const baseMatrix = new THREE.Matrix4().makeTranslation((boundingBox.min.x + boundingBox.max.x) / 2, minY + extensionMm / 2, (boundingBox.min.z + boundingBox.max.z) / 2);
595
- baseGeometry.applyMatrix4(baseMatrix);
596
- if (!this.activeOperations.get(operationId)) {
597
- throw new Error("Operation cancelled");
598
- }
599
- if (progressCallback)
600
- progressCallback(70, "Merging geometries...");
601
- if (progressCallback)
602
- progressCallback(75, "Creating merged geometry...");
603
- // Create material for both meshes
604
- const material = new THREE.MeshStandardMaterial();
605
- // Create individual meshes
606
- const originalMesh = new THREE.Mesh(geometry, material);
607
- const baseMesh = new THREE.Mesh(baseGeometry, material);
608
- // Export each mesh separately and merge the STL strings
609
- const exporter = new STLExporter();
610
- const originalStl = exporter.parse(originalMesh);
611
- const baseStl = exporter.parse(baseMesh);
612
- // Generate output file path
613
- const outputFileName = path.basename(stlFilePath, '.stl') + '_extended.stl';
614
- const outputFilePath = path.join(this.tempDir, outputFileName);
615
- if (progressCallback)
616
- progressCallback(90, "Saving extended STL...");
617
- // Write the combined STL data to file
618
- await writeFileAsync(outputFilePath, originalStl + baseStl);
619
- if (progressCallback)
620
- progressCallback(100, "STL saved successfully");
621
- this.emit('operationComplete', {
622
- operationId,
623
- type: 'extendBase',
624
- success: true,
625
- output: outputFilePath
626
- });
627
- console.log(`Modified STL saved to ${outputFilePath}`);
628
- return outputFilePath;
629
- }
630
- catch (error) {
631
- console.error("Error extending STL base:", error);
632
- this.emit('operationError', {
633
- operationId,
634
- type: 'extendBase',
635
- error: error instanceof Error ? error.message : String(error)
636
- });
637
- throw new Error(`Failed to extend STL base: ${error instanceof Error ? error.message : String(error)}`);
638
- }
639
- finally {
640
- this.activeOperations.delete(operationId);
641
- }
642
- }
643
- /**
644
- * Enhanced version of sliceSTL with progress reporting and error handling
645
- * @param stlFilePath Path to the STL file
646
- * @param slicerType Type of slicer to use ('prusaslicer', 'cura', 'slic3r')
647
- * @param slicerPath Path to the slicer executable
648
- * @param slicerProfile Profile to use for slicing
649
- * @param progressCallback Optional callback for progress updates
650
- * @returns Path to the generated G-code file
651
- */
652
- async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback) {
653
- const operationId = this.generateOperationId();
654
- this.activeOperations.set(operationId, true);
655
- try {
656
- if (progressCallback)
657
- progressCallback(0, "Starting slicing operation...");
658
- const { exec } = require('child_process');
659
- const execAsync = promisify(exec);
660
- // Verify the STL file exists
661
- if (!fs.existsSync(stlFilePath)) {
662
- throw new Error(`STL file not found: ${stlFilePath}`);
663
- }
664
- // Verify the slicer executable exists if provided
665
- if (slicerPath && !fs.existsSync(slicerPath)) {
666
- throw new Error(`Slicer executable not found: ${slicerPath}`);
667
- }
668
- if (progressCallback)
669
- progressCallback(10, "Preparing slicing command...");
670
- const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
671
- const outputFilePath = path.join(this.tempDir, outputFileName);
672
- let command = '';
673
- switch (slicerType) {
674
- case 'prusaslicer':
675
- command = `"${slicerPath}" --export-gcode --output "${outputFilePath}"`;
676
- if (slicerProfile) {
677
- if (!fs.existsSync(slicerProfile)) {
678
- console.warn(`Warning: Slicer profile not found: ${slicerProfile}`);
679
- }
680
- command += ` --load "${slicerProfile}"`;
681
- }
682
- command += ` "${stlFilePath}"`;
683
- break;
684
- case 'cura':
685
- command = `"${slicerPath}" -o "${outputFilePath}"`;
686
- if (slicerProfile) {
687
- if (!fs.existsSync(slicerProfile)) {
688
- console.warn(`Warning: Slicer profile not found: ${slicerProfile}`);
689
- }
690
- command += ` -l "${slicerProfile}"`;
691
- }
692
- command += ` "${stlFilePath}"`;
693
- break;
694
- case 'slic3r':
695
- command = `"${slicerPath}" --output "${outputFilePath}"`;
696
- if (slicerProfile) {
697
- if (!fs.existsSync(slicerProfile)) {
698
- console.warn(`Warning: Slicer profile not found: ${slicerProfile}`);
699
- }
700
- command += ` --load "${slicerProfile}"`;
701
- }
702
- command += ` "${stlFilePath}"`;
703
- break;
704
- default:
705
- throw new Error(`Unsupported slicer type: ${slicerType}`);
706
- }
707
- if (!this.activeOperations.get(operationId)) {
708
- throw new Error("Operation cancelled");
709
- }
710
- if (progressCallback)
711
- progressCallback(20, "Running slicer...");
712
- console.log(`Slicing STL with command: ${command}`);
713
- // Execute the slicing command
714
- await new Promise((resolve, reject) => {
715
- const slicerProcess = exec(command);
716
- // Set up a timeout for the slicing operation (15 minutes)
717
- const timeout = setTimeout(() => {
718
- if (slicerProcess.pid) {
719
- process.kill(slicerProcess.pid);
720
- }
721
- reject(new Error("Slicing operation timed out after 15 minutes"));
722
- }, 15 * 60 * 1000);
723
- // Handle stdout data
724
- slicerProcess.stdout?.on('data', (data) => {
725
- console.log(`Slicer stdout: ${data}`);
726
- // Attempt to extract progress information (varies by slicer)
727
- const progressMatch = data.match(/(\d+)%/);
728
- if (progressMatch && progressCallback) {
729
- const slicerProgress = parseInt(progressMatch[1], 10);
730
- // Map slicer's 0-100% to our 20-90% range
731
- const mappedProgress = 20 + (slicerProgress * 0.7);
732
- progressCallback(mappedProgress, `Slicing: ${slicerProgress}%`);
733
- }
734
- });
735
- // Handle stderr data
736
- slicerProcess.stderr?.on('data', (data) => {
737
- console.error(`Slicer stderr: ${data}`);
738
- });
739
- // Handle completion
740
- slicerProcess.on('close', (code) => {
741
- clearTimeout(timeout);
742
- if (code === 0) {
743
- resolve();
744
- }
745
- else {
746
- reject(new Error(`Slicer process exited with code ${code}`));
747
- }
748
- });
749
- // Handle errors
750
- slicerProcess.on('error', (err) => {
751
- clearTimeout(timeout);
752
- reject(err);
753
- });
754
- });
755
- if (!this.activeOperations.get(operationId)) {
756
- throw new Error("Operation cancelled");
757
- }
758
- // Verify the G-code file was created
759
- if (!fs.existsSync(outputFilePath)) {
760
- throw new Error("Slicing failed: G-code file was not created");
761
- }
762
- if (progressCallback)
763
- progressCallback(100, "Slicing completed successfully");
764
- this.emit('operationComplete', {
765
- operationId,
766
- type: 'slice',
767
- success: true,
768
- output: outputFilePath
769
- });
770
- return outputFilePath;
771
- }
772
- catch (error) {
773
- console.error("Error slicing STL:", error);
774
- this.emit('operationError', {
775
- operationId,
776
- type: 'slice',
777
- error: error instanceof Error ? error.message : String(error)
778
- });
779
- throw new Error(`Failed to slice STL: ${error instanceof Error ? error.message : String(error)}`);
780
- }
781
- finally {
782
- this.activeOperations.delete(operationId);
783
- }
784
- }
785
- /**
786
- * Enhanced version of confirmTemperatures with better error handling
787
- * @param gcodePath Path to the G-code file
788
- * @param expected Expected temperature settings
789
- * @param progressCallback Optional callback for progress updates
790
- * @returns Object with comparison results
791
- */
792
- async confirmTemperatures(gcodePath, expected, progressCallback) {
793
- const operationId = this.generateOperationId();
794
- this.activeOperations.set(operationId, true);
795
- try {
796
- if (progressCallback)
797
- progressCallback(0, "Starting temperature verification...");
798
- // Verify the G-code file exists
799
- if (!fs.existsSync(gcodePath)) {
800
- throw new Error(`G-code file not found: ${gcodePath}`);
801
- }
802
- if (progressCallback)
803
- progressCallback(20, "Reading G-code file...");
804
- // Read the G-code file
805
- const gcode = await readFileAsync(gcodePath, 'utf8');
806
- const lines = gcode.split('\n');
807
- if (!this.activeOperations.get(operationId)) {
808
- throw new Error("Operation cancelled");
809
- }
810
- if (progressCallback)
811
- progressCallback(50, "Analyzing temperature commands...");
812
- // Extract temperature settings from G-code
813
- const actual = {};
814
- const allTemperatures = { extruder: [], bed: [] };
815
- for (const line of lines) {
816
- // Look for extruder temperature (M104 or M109)
817
- const extruderMatch = line.match(/M10[49] S(\d+)/);
818
- if (extruderMatch) {
819
- const temp = parseInt(extruderMatch[1], 10);
820
- allTemperatures.extruder.push(temp);
821
- // Keep the first temperature for compatibility with original function
822
- if (!actual.extruder) {
823
- actual.extruder = temp;
824
- }
825
- }
826
- // Look for bed temperature (M140 or M190)
827
- const bedMatch = line.match(/M1[49]0 S(\d+)/);
828
- if (bedMatch) {
829
- const temp = parseInt(bedMatch[1], 10);
830
- allTemperatures.bed.push(temp);
831
- // Keep the first temperature for compatibility with original function
832
- if (!actual.bed) {
833
- actual.bed = temp;
834
- }
835
- }
836
- }
837
- if (progressCallback)
838
- progressCallback(80, "Comparing temperatures...");
839
- // Compare actual with expected
840
- let match = true;
841
- if (expected.extruder !== undefined && actual.extruder !== expected.extruder) {
842
- match = false;
843
- }
844
- if (expected.bed !== undefined && actual.bed !== expected.bed) {
845
- match = false;
846
- }
847
- if (progressCallback)
848
- progressCallback(100, "Temperature verification complete");
849
- this.emit('operationComplete', {
850
- operationId,
851
- type: 'confirmTemperatures',
852
- success: true,
853
- result: { match, actual, expected, allTemperatures }
854
- });
855
- return { match, actual, expected, allTemperatures };
856
- }
857
- catch (error) {
858
- console.error("Error confirming temperatures:", error);
859
- this.emit('operationError', {
860
- operationId,
861
- type: 'confirmTemperatures',
862
- error: error instanceof Error ? error.message : String(error)
863
- });
864
- throw new Error(`Failed to confirm temperatures: ${error instanceof Error ? error.message : String(error)}`);
865
- }
866
- finally {
867
- this.activeOperations.delete(operationId);
868
- }
869
- }
870
- }