mcp-3d-printer-server 0.1.0 → 0.2.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/.env.example +17 -0
- package/README.md +226 -1
- package/dist/index.js +487 -0
- package/dist/stl/stl-manipulator.d.ts +127 -0
- package/dist/stl/stl-manipulator.js +870 -0
- package/dist/test/stl-manipulator-test.d.ts +2 -0
- package/dist/test/stl-manipulator-test.js +91 -0
- package/package.json +8 -5
- package/src/index.ts +585 -0
- package/src/stl/stl-manipulator.ts +1119 -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/test/sample_cube.stl +86 -0
- package/test/sample_cube_extended.stl +172 -0
- package/test/sample_cube_modified.stl +86 -0
- package/test/sample_cube_rotated.stl +86 -0
- package/test/sample_cube_scaled.stl +86 -0
- package/test/sample_cube_translated.stl +86 -0
- package/test/sample_cube_visualization.svg +52 -0
|
@@ -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';
|