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