mcp-3d-printer-server 1.2.9 → 1.2.11
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/CHANGELOG.md +207 -0
- package/CONTRIBUTORS.md +49 -0
- package/README.md +528 -811
- package/dist/3mf_parser.js +8 -8
- package/dist/blender-mcp-bridge.d.ts +1 -0
- package/dist/blender-mcp-bridge.js +209 -21
- package/dist/fulu-orca.js +29 -5
- package/dist/index.js +614 -110
- package/dist/printers/bambu-node-guard.d.ts +2 -0
- package/dist/printers/bambu-node-guard.js +38 -0
- package/dist/printers/bambu.d.ts +53 -7
- package/dist/printers/bambu.js +420 -126
- package/dist/printers/creality.d.ts +15 -6
- package/dist/printers/creality.js +42 -9
- package/dist/printers/duet.d.ts +11 -6
- package/dist/printers/duet.js +57 -34
- package/dist/printers/generic-base.d.ts +69 -0
- package/dist/printers/generic-base.js +209 -0
- package/dist/printers/klipper.d.ts +17 -6
- package/dist/printers/klipper.js +63 -16
- package/dist/printers/octoprint.d.ts +18 -6
- package/dist/printers/octoprint.js +64 -9
- package/dist/printers/printer-factory.d.ts +11 -2
- package/dist/printers/printer-factory.js +9 -8
- package/dist/printers/prusa.d.ts +14 -6
- package/dist/printers/prusa.js +56 -9
- package/dist/printers/repetier.d.ts +18 -6
- package/dist/printers/repetier.js +48 -6
- package/dist/safety/archive.d.ts +11 -0
- package/dist/safety/archive.js +214 -0
- package/dist/safety/artifact.d.ts +6 -0
- package/dist/safety/artifact.js +66 -0
- package/dist/safety/confirmation.d.ts +22 -0
- package/dist/safety/confirmation.js +43 -0
- package/dist/safety/expected-peaks.d.ts +24 -0
- package/dist/safety/expected-peaks.js +24 -0
- package/dist/safety/gcode-thermal.d.ts +64 -0
- package/dist/safety/gcode-thermal.js +382 -0
- package/dist/safety/limits.d.ts +31 -0
- package/dist/safety/limits.js +105 -0
- package/dist/safety/print-file.d.ts +25 -0
- package/dist/safety/print-file.js +517 -0
- package/dist/safety/printer-state.d.ts +51 -0
- package/dist/safety/printer-state.js +365 -0
- package/dist/slicer/bambu-cli.d.ts +110 -0
- package/dist/slicer/bambu-cli.js +547 -0
- package/dist/slicer/profile-flatten.d.ts +104 -0
- package/dist/slicer/profile-flatten.js +676 -0
- package/dist/slicer/run-slicer.d.ts +18 -0
- package/dist/slicer/run-slicer.js +102 -0
- package/dist/slicer/slicer-error.d.ts +27 -0
- package/dist/slicer/slicer-error.js +53 -0
- package/dist/slicer/templates.d.ts +126 -0
- package/dist/slicer/templates.js +255 -0
- package/dist/stl/stl-manipulator.d.ts +25 -3
- package/dist/stl/stl-manipulator.js +214 -232
- package/package.json +16 -9
- package/server.json +29 -0
- package/src/3mf_parser.ts +8 -8
- package/src/blender-mcp-bridge.ts +200 -19
- package/src/fulu-orca.ts +36 -6
- package/src/index.ts +686 -137
- package/src/printers/bambu-node-guard.ts +44 -0
- package/src/printers/bambu.ts +458 -164
- package/src/printers/creality.ts +40 -9
- package/src/printers/duet.ts +64 -53
- package/src/printers/generic-base.ts +286 -0
- package/src/printers/klipper.ts +83 -28
- package/src/printers/octoprint.ts +74 -21
- package/src/printers/printer-factory.ts +21 -11
- package/src/printers/prusa.ts +53 -10
- package/src/printers/repetier.ts +55 -17
- package/src/safety/archive.ts +181 -0
- package/src/safety/artifact.ts +58 -0
- package/src/safety/confirmation.ts +56 -0
- package/src/safety/expected-peaks.ts +47 -0
- package/src/safety/gcode-thermal.ts +404 -0
- package/src/safety/limits.ts +98 -0
- package/src/safety/print-file.ts +354 -0
- package/src/safety/printer-state.ts +339 -0
- package/src/slicer/bambu-cli.ts +633 -0
- package/src/slicer/profile-flatten.ts +830 -0
- package/src/slicer/run-slicer.ts +126 -0
- package/src/slicer/slicer-error.ts +72 -0
- package/src/slicer/templates.ts +288 -0
- package/src/stl/stl-manipulator.ts +247 -275
|
@@ -7,7 +7,11 @@ import path from 'path';
|
|
|
7
7
|
import { promisify } from 'util';
|
|
8
8
|
import { EventEmitter } from 'events';
|
|
9
9
|
import * as crypto from 'crypto';
|
|
10
|
-
import {
|
|
10
|
+
import { detectProfilesRoot } from '../slicer/profile-flatten.js';
|
|
11
|
+
import { BambuCliProfilePreparer, assertSlicedProjectOutput, buildBambuCliArgs, } from '../slicer/bambu-cli.js';
|
|
12
|
+
import { runSlicerProcess } from '../slicer/run-slicer.js';
|
|
13
|
+
import { SlicerError, asPreparationError } from '../slicer/slicer-error.js';
|
|
14
|
+
import { scanGcodeThermal } from '../safety/gcode-thermal.js';
|
|
11
15
|
const readFileAsync = promisify(fs.readFile);
|
|
12
16
|
const writeFileAsync = promisify(fs.writeFile);
|
|
13
17
|
const DEFAULT_SLICER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -27,6 +31,16 @@ export class STLManipulator extends EventEmitter {
|
|
|
27
31
|
generateOperationId() {
|
|
28
32
|
return crypto.randomUUID();
|
|
29
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Each operation writes into its own folder. Outputs named only after the
|
|
36
|
+
* input (for example bracket_extended.stl) would otherwise let two
|
|
37
|
+
* concurrent requests replace each other's file before it is used or printed.
|
|
38
|
+
*/
|
|
39
|
+
operationOutputPath(operationId, fileName) {
|
|
40
|
+
const directory = path.join(this.tempDir, `${operationId}-out`);
|
|
41
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
42
|
+
return path.join(directory, fileName);
|
|
43
|
+
}
|
|
30
44
|
validateSlicerExecutable(slicerPath) {
|
|
31
45
|
if (!slicerPath.trim()) {
|
|
32
46
|
throw new Error("Slicer executable path is required.");
|
|
@@ -84,28 +98,6 @@ export class STLManipulator extends EventEmitter {
|
|
|
84
98
|
}
|
|
85
99
|
return path.join(outputDir, plateOutputs[0]);
|
|
86
100
|
}
|
|
87
|
-
resolveBambuMachinePresetPath(slicerPath, printerPreset) {
|
|
88
|
-
if (fs.existsSync(printerPreset)) {
|
|
89
|
-
return printerPreset;
|
|
90
|
-
}
|
|
91
|
-
const presetFile = printerPreset.endsWith(".json") ? printerPreset : `${printerPreset}.json`;
|
|
92
|
-
const candidateDirs = [];
|
|
93
|
-
const macAppMarker = `${path.sep}Contents${path.sep}MacOS${path.sep}`;
|
|
94
|
-
const macAppIndex = slicerPath.indexOf(macAppMarker);
|
|
95
|
-
if (macAppIndex >= 0) {
|
|
96
|
-
candidateDirs.push(path.join(slicerPath.slice(0, macAppIndex + `${path.sep}Contents`.length), "Resources", "profiles", "BBL", "machine"));
|
|
97
|
-
}
|
|
98
|
-
const executableDir = path.dirname(slicerPath);
|
|
99
|
-
candidateDirs.push(path.join(executableDir, "..", "Resources", "profiles", "BBL", "machine"), path.join(executableDir, "..", "share", "OrcaSlicer", "resources", "profiles", "BBL", "machine"), path.join(executableDir, "..", "resources", "profiles", "BBL", "machine"), path.join(executableDir, "resources", "profiles", "BBL", "machine"));
|
|
100
|
-
for (const candidateDir of candidateDirs) {
|
|
101
|
-
const candidate = path.resolve(candidateDir, presetFile);
|
|
102
|
-
if (fs.existsSync(candidate)) {
|
|
103
|
-
return candidate;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
throw new Error(`Could not find Bambu machine preset "${presetFile}" near slicer executable ${slicerPath}. ` +
|
|
107
|
-
"Pass slicer_profile with an explicit machine/process settings file, or use a slicer bundle that includes Resources/profiles/BBL/machine.");
|
|
108
|
-
}
|
|
109
101
|
resolveSlicerTimeoutMs() {
|
|
110
102
|
const rawTimeout = process.env.SLICER_TIMEOUT_MS?.trim();
|
|
111
103
|
if (!rawTimeout) {
|
|
@@ -240,7 +232,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
240
232
|
}
|
|
241
233
|
// Generate output file path
|
|
242
234
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_scaled.stl';
|
|
243
|
-
const outputFilePath =
|
|
235
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
244
236
|
// Save the modified STL
|
|
245
237
|
await this.saveSTL(geometry, outputFilePath, progressCallback);
|
|
246
238
|
this.emit('operationComplete', {
|
|
@@ -298,7 +290,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
298
290
|
}
|
|
299
291
|
// Generate output file path
|
|
300
292
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_rotated.stl';
|
|
301
|
-
const outputFilePath =
|
|
293
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
302
294
|
// Save the modified STL
|
|
303
295
|
await this.saveSTL(geometry, outputFilePath, progressCallback);
|
|
304
296
|
this.emit('operationComplete', {
|
|
@@ -347,7 +339,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
347
339
|
}
|
|
348
340
|
// Generate output file path
|
|
349
341
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_translated.stl';
|
|
350
|
-
const outputFilePath =
|
|
342
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
351
343
|
// Save the modified STL
|
|
352
344
|
await this.saveSTL(geometry, outputFilePath, progressCallback);
|
|
353
345
|
this.emit('operationComplete', {
|
|
@@ -488,7 +480,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
488
480
|
progressCallback(90, "Saving visualization...");
|
|
489
481
|
// Write the SVG to file
|
|
490
482
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_visualization.svg';
|
|
491
|
-
const outputFilePath =
|
|
483
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
492
484
|
await writeFileAsync(outputFilePath, svgContent);
|
|
493
485
|
if (progressCallback)
|
|
494
486
|
progressCallback(100, "Visualization saved successfully");
|
|
@@ -630,7 +622,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
630
622
|
}
|
|
631
623
|
// Generate output file path
|
|
632
624
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_modified.stl';
|
|
633
|
-
const outputFilePath =
|
|
625
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
634
626
|
// Save the modified STL
|
|
635
627
|
await this.saveSTL(geometry, outputFilePath, progressCallback);
|
|
636
628
|
this.emit('operationComplete', {
|
|
@@ -666,7 +658,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
666
658
|
try {
|
|
667
659
|
if (progressCallback)
|
|
668
660
|
progressCallback(0, "Starting base extension operation...");
|
|
669
|
-
console.
|
|
661
|
+
console.error(`Extending base of ${stlFilePath} by ${extensionInches} inches`);
|
|
670
662
|
// Load the STL file
|
|
671
663
|
const { geometry, boundingBox } = await this.loadSTL(stlFilePath, progressCallback);
|
|
672
664
|
if (!this.activeOperations.get(operationId)) {
|
|
@@ -674,19 +666,19 @@ export class STLManipulator extends EventEmitter {
|
|
|
674
666
|
}
|
|
675
667
|
if (progressCallback)
|
|
676
668
|
progressCallback(60, "Creating extended base geometry...");
|
|
677
|
-
//
|
|
678
|
-
const
|
|
669
|
+
// STL files and slicers are Z-up: the base goes under the model.
|
|
670
|
+
const minZ = boundingBox.min.z;
|
|
679
671
|
// Convert inches to millimeters (STL files typically use mm)
|
|
680
672
|
const extensionMm = extensionInches * 25.4;
|
|
681
|
-
//
|
|
682
|
-
const matrix = new THREE.Matrix4().makeTranslation(0,
|
|
673
|
+
// Raise the model by the extension amount
|
|
674
|
+
const matrix = new THREE.Matrix4().makeTranslation(0, 0, extensionMm);
|
|
683
675
|
geometry.applyMatrix4(matrix);
|
|
684
|
-
//
|
|
676
|
+
// A box spanning the model's X/Y footprint fills the new space below it
|
|
685
677
|
const baseWidth = boundingBox.max.x - boundingBox.min.x;
|
|
686
|
-
const baseDepth = boundingBox.max.
|
|
687
|
-
const baseGeometry = new THREE.BoxGeometry(baseWidth,
|
|
678
|
+
const baseDepth = boundingBox.max.y - boundingBox.min.y;
|
|
679
|
+
const baseGeometry = new THREE.BoxGeometry(baseWidth, baseDepth, extensionMm);
|
|
688
680
|
// Position the base geometry
|
|
689
|
-
const baseMatrix = new THREE.Matrix4().makeTranslation((boundingBox.min.x + boundingBox.max.x) / 2,
|
|
681
|
+
const baseMatrix = new THREE.Matrix4().makeTranslation((boundingBox.min.x + boundingBox.max.x) / 2, (boundingBox.min.y + boundingBox.max.y) / 2, minZ + extensionMm / 2);
|
|
690
682
|
baseGeometry.applyMatrix4(baseMatrix);
|
|
691
683
|
if (!this.activeOperations.get(operationId)) {
|
|
692
684
|
throw new Error("Operation cancelled");
|
|
@@ -706,7 +698,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
706
698
|
const baseStl = exporter.parse(baseMesh);
|
|
707
699
|
// Generate output file path
|
|
708
700
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_extended.stl';
|
|
709
|
-
const outputFilePath =
|
|
701
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
710
702
|
if (progressCallback)
|
|
711
703
|
progressCallback(90, "Saving extended STL...");
|
|
712
704
|
// Write the combined STL data to file
|
|
@@ -719,7 +711,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
719
711
|
success: true,
|
|
720
712
|
output: outputFilePath
|
|
721
713
|
});
|
|
722
|
-
console.
|
|
714
|
+
console.error(`Modified STL saved to ${outputFilePath}`);
|
|
723
715
|
return outputFilePath;
|
|
724
716
|
}
|
|
725
717
|
catch (error) {
|
|
@@ -735,188 +727,184 @@ export class STLManipulator extends EventEmitter {
|
|
|
735
727
|
this.activeOperations.delete(operationId);
|
|
736
728
|
}
|
|
737
729
|
}
|
|
730
|
+
/**
|
|
731
|
+
* Bambu-compatible CLI slicing applies when the slicer is BambuStudio or
|
|
732
|
+
* FULU OrcaSlicer-bambulab, or when generic OrcaSlicer is given an explicit
|
|
733
|
+
* Bambu machine preset. Generic OrcaSlicer without a preset keeps its
|
|
734
|
+
* user-supplied machine/process profile path.
|
|
735
|
+
*/
|
|
736
|
+
static usesBambuCliPreparation(slicerType, printerPreset) {
|
|
737
|
+
return (slicerType === 'bambustudio' ||
|
|
738
|
+
slicerType === 'orcaslicer-bambulab' ||
|
|
739
|
+
(slicerType === 'orcaslicer' && Boolean(printerPreset)));
|
|
740
|
+
}
|
|
738
741
|
/**
|
|
739
742
|
* Slice an STL or 3MF file using the specified slicer
|
|
740
743
|
* @param stlFilePath Path to the input STL or 3MF file
|
|
741
744
|
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio)
|
|
742
745
|
* @param slicerPath Path to the slicer executable
|
|
743
|
-
* @param slicerProfile Optional
|
|
746
|
+
* @param slicerProfile Optional slicer profile/config file. For Bambu-compatible
|
|
747
|
+
* CLI slicing this is a process profile; the machine comes from printerPreset.
|
|
744
748
|
* @param progressCallback Optional callback for progress updates
|
|
749
|
+
* @param printerPreset Bambu machine preset name, e.g. "Bambu Lab P1S 0.4 nozzle"
|
|
750
|
+
* @param filamentProfile Optional filament profile path(s), ';'-separated
|
|
751
|
+
* @param bambuOptions Optional Bambu-compatible CLI options (added last for compatibility)
|
|
745
752
|
* @returns Path to the generated G-code or sliced 3MF file
|
|
746
753
|
*/
|
|
747
|
-
async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset, filamentProfile) {
|
|
754
|
+
async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset, filamentProfile, bambuOptions) {
|
|
748
755
|
const operationId = this.generateOperationId();
|
|
749
756
|
this.activeOperations.set(operationId, true);
|
|
750
|
-
|
|
751
|
-
const orcaProfiles = this.parseOrcaProfileArgs(slicerProfile, filamentProfile);
|
|
752
|
-
if (slicerType === 'orcaslicer') {
|
|
753
|
-
this.warnForMissingProfiles(orcaProfiles.settingsProfiles, 'Slicer profile');
|
|
754
|
-
this.warnForMissingProfiles(orcaProfiles.filamentProfiles, 'Filament profile');
|
|
755
|
-
}
|
|
756
|
-
else if (slicerProfile && !fs.existsSync(slicerProfile)) {
|
|
757
|
-
console.warn(`Slicer profile specified but not found: ${slicerProfile}. Slicer might use defaults.`);
|
|
758
|
-
// Allow proceeding without profile, slicer might handle it
|
|
759
|
-
}
|
|
760
|
-
if (slicerType !== 'orcaslicer' && filamentProfile && !fs.existsSync(filamentProfile)) {
|
|
761
|
-
console.warn(`Filament profile specified but not found: ${filamentProfile}. Slicer might use defaults.`);
|
|
762
|
-
}
|
|
763
|
-
const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
|
|
764
|
-
let outputFilePath = path.join(this.tempDir, outputFileName);
|
|
757
|
+
const bambuCli = STLManipulator.usesBambuCliPreparation(slicerType, printerPreset);
|
|
765
758
|
let orcaOutputDir;
|
|
766
|
-
let
|
|
759
|
+
let bambuOutputDir;
|
|
767
760
|
try {
|
|
761
|
+
try {
|
|
762
|
+
this.validateSlicerExecutable(slicerPath);
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
throw asPreparationError(error, { slicerType, slicerPath });
|
|
766
|
+
}
|
|
767
|
+
if (!fs.existsSync(stlFilePath)) {
|
|
768
|
+
throw new SlicerError('preparation', `Input model not found: ${stlFilePath}`, { slicerType, slicerPath });
|
|
769
|
+
}
|
|
770
|
+
const orcaProfiles = this.parseOrcaProfileArgs(slicerProfile, filamentProfile);
|
|
771
|
+
if (!bambuCli) {
|
|
772
|
+
if (slicerType === 'orcaslicer') {
|
|
773
|
+
this.warnForMissingProfiles(orcaProfiles.settingsProfiles, 'Slicer profile');
|
|
774
|
+
this.warnForMissingProfiles(orcaProfiles.filamentProfiles, 'Filament profile');
|
|
775
|
+
}
|
|
776
|
+
else if (slicerProfile && !fs.existsSync(slicerProfile)) {
|
|
777
|
+
console.warn(`Slicer profile specified but not found: ${slicerProfile}. Slicer might use defaults.`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
|
|
781
|
+
let outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
782
|
+
let args = [];
|
|
783
|
+
let bambuExport;
|
|
768
784
|
if (progressCallback)
|
|
769
785
|
progressCallback(0, `Starting slicing with ${slicerType}...`);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
fs.mkdirSync(orcaOutputDir, { recursive: true });
|
|
785
|
-
args = [
|
|
786
|
-
'--slice', '0',
|
|
787
|
-
'--outputdir', orcaOutputDir,
|
|
788
|
-
];
|
|
789
|
-
this.appendOrcaProfileArgs(args, orcaProfiles);
|
|
790
|
-
args.push(stlFilePath);
|
|
791
|
-
break;
|
|
792
|
-
case 'cura':
|
|
793
|
-
// CuraEngine CLI args are different, often requiring -s for settings
|
|
794
|
-
// Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
|
|
795
|
-
// This requires parsing the profile or passing individual settings.
|
|
796
|
-
// Keeping it simple for now, assuming profile contains necessary info.
|
|
797
|
-
args = [
|
|
798
|
-
'slice',
|
|
799
|
-
'-l', stlFilePath,
|
|
800
|
-
'-o', outputFilePath
|
|
801
|
-
];
|
|
802
|
-
if (slicerProfile) {
|
|
803
|
-
args.push('-j', slicerProfile); // Load settings from profile definition file
|
|
786
|
+
if (bambuCli) {
|
|
787
|
+
// Bambu-compatible CLI: resolve the exact machine preset and every
|
|
788
|
+
// profile dependency, then slice and export a 3MF with plate G-code.
|
|
789
|
+
const cliType = slicerType;
|
|
790
|
+
const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
|
|
791
|
+
const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
|
|
792
|
+
const exportName = `${outputBase}_sliced.3mf`;
|
|
793
|
+
try {
|
|
794
|
+
if (orcaProfiles.settingsProfiles.length > 1) {
|
|
795
|
+
throw new Error(`slicer_profile (or SLICER_PROFILE) lists ${orcaProfiles.settingsProfiles.length} settings files, but Bambu-compatible CLI slicing ` +
|
|
796
|
+
'takes one process profile. Set slicer_profile/SLICER_PROFILE to a process profile only (or leave it empty for the ' +
|
|
797
|
+
"preset's default process); the machine preset comes from bambu_model/BAMBU_MODEL and nozzle_diameter/NOZZLE_DIAMETER. " +
|
|
798
|
+
'Pass filaments with filament_profile/load_filaments. For a non-Bambu printer, use slicer_type orcaslicer without bambu_model ' +
|
|
799
|
+
'to keep the machine;process|filament format.');
|
|
804
800
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
801
|
+
const options = { ...bambuOptions };
|
|
802
|
+
if (orcaProfiles.filamentProfiles.length > 0) {
|
|
803
|
+
const legacyFilaments = orcaProfiles.filamentProfiles.join(';');
|
|
804
|
+
if (options.loadFilaments && this.splitProfileList(options.loadFilaments).join(';') !== legacyFilaments) {
|
|
805
|
+
throw new Error('Provide either load_filaments or filament_profile, not conflicting values.');
|
|
806
|
+
}
|
|
807
|
+
options.loadFilaments = legacyFilaments;
|
|
808
|
+
}
|
|
809
|
+
const profilesRoot = detectProfilesRoot(slicerPath, cliType);
|
|
810
|
+
const preparer = new BambuCliProfilePreparer(path.join(this.tempDir, 'slicer-profiles'));
|
|
811
|
+
const bundle = await preparer.prepare(stlFilePath, outputBase, cliType, profilesRoot, printerPreset, orcaProfiles.settingsProfiles[0], options);
|
|
812
|
+
bambuOutputDir = fs.mkdtempSync(path.join(this.tempDir, `${operationId}-bambu-output-`));
|
|
813
|
+
args = [...buildBambuCliArgs(bundle, bambuOutputDir, exportName, options), stlFilePath];
|
|
814
|
+
bambuExport = {
|
|
815
|
+
producedPath: path.join(bambuOutputDir, exportName),
|
|
816
|
+
// One folder per operation: concurrent slices of same-named inputs
|
|
817
|
+
// must never replace each other's result before it is printed.
|
|
818
|
+
finalPath: this.operationOutputPath(operationId, exportName),
|
|
819
|
+
};
|
|
820
|
+
outputFilePath = bambuExport.finalPath;
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
throw asPreparationError(error, { slicerType, slicerPath });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
switch (slicerType) {
|
|
828
|
+
case 'prusaslicer':
|
|
829
|
+
case 'slic3r': // PrusaSlicer and Slic3r share similar CLI args
|
|
816
830
|
args = [
|
|
817
|
-
'--slice',
|
|
818
|
-
'--
|
|
831
|
+
'--slice',
|
|
832
|
+
'--output', outputFilePath,
|
|
833
|
+
'--load', slicerProfile || '', // Pass profile path
|
|
834
|
+
stlFilePath
|
|
835
|
+
];
|
|
836
|
+
// Remove empty profile arg if not provided
|
|
837
|
+
args = args.filter(arg => arg !== '');
|
|
838
|
+
break;
|
|
839
|
+
case 'orcaslicer':
|
|
840
|
+
orcaOutputDir = path.join(this.tempDir, `${operationId}-orca-output`);
|
|
841
|
+
fs.mkdirSync(orcaOutputDir, { recursive: true });
|
|
842
|
+
args = [
|
|
843
|
+
'--slice', '0',
|
|
844
|
+
'--outputdir', orcaOutputDir,
|
|
845
|
+
];
|
|
846
|
+
this.appendOrcaProfileArgs(args, orcaProfiles);
|
|
847
|
+
args.push(stlFilePath);
|
|
848
|
+
break;
|
|
849
|
+
case 'cura':
|
|
850
|
+
// CuraEngine CLI args are different, often requiring -s for settings
|
|
851
|
+
// Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
|
|
852
|
+
args = [
|
|
853
|
+
'slice',
|
|
854
|
+
'-l', stlFilePath,
|
|
855
|
+
'-o', outputFilePath
|
|
819
856
|
];
|
|
820
|
-
// FULU Orca/Bambu Studio load machine presets as JSON settings files.
|
|
821
|
-
if (printerPreset && !slicerProfile) {
|
|
822
|
-
args.push('--load-settings', this.resolveBambuMachinePresetPath(slicerPath, printerPreset));
|
|
823
|
-
}
|
|
824
857
|
if (slicerProfile) {
|
|
825
|
-
args.push('
|
|
826
|
-
}
|
|
827
|
-
if (filamentProfile) {
|
|
828
|
-
args.push('--load-filaments', filamentProfile);
|
|
858
|
+
args.push('-j', slicerProfile); // Load settings from profile definition file
|
|
829
859
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
break;
|
|
835
|
-
default:
|
|
836
|
-
throw new Error(`Unsupported slicer type: ${slicerType}`);
|
|
860
|
+
break;
|
|
861
|
+
default:
|
|
862
|
+
throw new SlicerError('preparation', `Unsupported slicer type: ${slicerType}`, { slicerType, slicerPath });
|
|
863
|
+
}
|
|
837
864
|
}
|
|
865
|
+
// Never mistake a previous run's file for this run's result (or overwrite the input).
|
|
866
|
+
if (path.resolve(outputFilePath) === path.resolve(stlFilePath)) {
|
|
867
|
+
throw new SlicerError('preparation', `Slicer output would overwrite the input file: ${stlFilePath}`, { slicerType, slicerPath });
|
|
868
|
+
}
|
|
869
|
+
if (fs.existsSync(outputFilePath))
|
|
870
|
+
fs.rmSync(outputFilePath, { force: true });
|
|
838
871
|
if (progressCallback)
|
|
839
872
|
progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
|
|
840
|
-
console.
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const settle = (callback) => {
|
|
848
|
-
if (settled) {
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
settled = true;
|
|
852
|
-
if (timeoutHandle) {
|
|
853
|
-
clearTimeout(timeoutHandle);
|
|
854
|
-
}
|
|
855
|
-
callback();
|
|
856
|
-
};
|
|
857
|
-
const process = execFile(slicerPath, args, (error, stdout, stderr) => {
|
|
858
|
-
if (error) {
|
|
859
|
-
console.error(`Slicer Error: ${error.message}`);
|
|
860
|
-
console.error(`Slicer Stderr: ${stderr}`);
|
|
861
|
-
const timeoutMessage = slicerProcessKilledByTimeout
|
|
862
|
-
? ` Slicer exceeded timeout ${timeoutMs}ms.`
|
|
863
|
-
: "";
|
|
864
|
-
settle(() => reject(new Error(`Slicer failed: ${error.message}.${timeoutMessage} Stderr: ${stderr}`)));
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
console.log(`Slicer Stdout: ${stdout}`);
|
|
868
|
-
if (stderr) {
|
|
869
|
-
console.warn(`Slicer Stderr: ${stderr}`); // Log stderr even on success
|
|
870
|
-
}
|
|
871
|
-
settle(() => resolve());
|
|
872
|
-
}
|
|
873
|
-
});
|
|
874
|
-
timeoutHandle = setTimeout(() => {
|
|
875
|
-
slicerProcessKilledByTimeout = true;
|
|
876
|
-
try {
|
|
877
|
-
process.kill("SIGKILL");
|
|
878
|
-
}
|
|
879
|
-
catch (killError) {
|
|
880
|
-
console.error("Error attempting to kill timed-out slicer process:", killError);
|
|
881
|
-
}
|
|
882
|
-
settle(() => reject(new Error(`Slicer exceeded timeout ${timeoutMs}ms.`)));
|
|
883
|
-
}, timeoutMs);
|
|
884
|
-
// Optional: Add listeners for stdout/stderr for real-time progress if slicer provides it
|
|
885
|
-
// process.stdout?.on('data', (data) => { console.log(`Slicer stdout: ${data}`); });
|
|
886
|
-
// process.stderr?.on('data', (data) => { console.log(`Slicer stderr: ${data}`); });
|
|
887
|
-
// Check for cancellation periodically
|
|
888
|
-
const checkCancel = setInterval(() => {
|
|
889
|
-
if (!this.activeOperations.get(operationId)) {
|
|
890
|
-
clearInterval(checkCancel);
|
|
891
|
-
try {
|
|
892
|
-
process.kill(); // Attempt to kill the slicer process
|
|
893
|
-
settle(() => reject(new Error("Slicing operation cancelled")));
|
|
894
|
-
}
|
|
895
|
-
catch (killError) {
|
|
896
|
-
console.error("Error attempting to kill slicer process:", killError);
|
|
897
|
-
settle(() => reject(new Error("Slicing operation cancelled, but failed to kill process.")));
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}, 500);
|
|
901
|
-
process.on('exit', () => clearInterval(checkCancel));
|
|
902
|
-
process.on('error', () => clearInterval(checkCancel)); // Ensure interval cleared on process error too
|
|
873
|
+
console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
|
|
874
|
+
await runSlicerProcess({
|
|
875
|
+
command: slicerPath,
|
|
876
|
+
args,
|
|
877
|
+
timeoutMs: this.resolveSlicerTimeoutMs(),
|
|
878
|
+
slicerType,
|
|
879
|
+
isCancelled: () => !this.activeOperations.get(operationId),
|
|
903
880
|
});
|
|
904
881
|
if (!this.activeOperations.get(operationId)) {
|
|
905
|
-
|
|
906
|
-
throw new Error("Slicing operation cancelled after process finished");
|
|
882
|
+
throw new SlicerError('execution', 'Slicing operation cancelled after process finished', { slicerType, slicerPath });
|
|
907
883
|
}
|
|
908
|
-
|
|
909
|
-
if (
|
|
910
|
-
|
|
884
|
+
try {
|
|
885
|
+
if (bambuExport) {
|
|
886
|
+
if (!fs.existsSync(bambuExport.producedPath)) {
|
|
887
|
+
throw new Error(`Slicer exited successfully but did not write ${bambuExport.producedPath}.`);
|
|
888
|
+
}
|
|
889
|
+
await assertSlicedProjectOutput(bambuExport.producedPath);
|
|
890
|
+
fs.mkdirSync(path.dirname(bambuExport.finalPath), { recursive: true });
|
|
891
|
+
fs.renameSync(bambuExport.producedPath, bambuExport.finalPath);
|
|
892
|
+
}
|
|
893
|
+
else if (slicerType === 'orcaslicer') {
|
|
894
|
+
if (!orcaOutputDir) {
|
|
895
|
+
throw new Error("Internal error: OrcaSlicer output directory was not initialized.");
|
|
896
|
+
}
|
|
897
|
+
const orcaGcodePath = this.resolveOrcaGcodeOutput(orcaOutputDir);
|
|
898
|
+
fs.renameSync(orcaGcodePath, outputFilePath);
|
|
911
899
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
fs.unlinkSync(outputFilePath);
|
|
900
|
+
if (!fs.existsSync(outputFilePath)) {
|
|
901
|
+
throw new Error(`Slicer finished but output file not found: ${outputFilePath}`);
|
|
915
902
|
}
|
|
916
|
-
fs.renameSync(orcaGcodePath, outputFilePath);
|
|
917
903
|
}
|
|
918
|
-
|
|
919
|
-
|
|
904
|
+
catch (error) {
|
|
905
|
+
if (error instanceof SlicerError)
|
|
906
|
+
throw error;
|
|
907
|
+
throw new SlicerError('output', error instanceof Error ? error.message : String(error), { slicerType, slicerPath });
|
|
920
908
|
}
|
|
921
909
|
if (progressCallback)
|
|
922
910
|
progressCallback(100, "Slicing completed successfully");
|
|
@@ -929,7 +917,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
929
917
|
return outputFilePath;
|
|
930
918
|
}
|
|
931
919
|
catch (error) {
|
|
932
|
-
console.error(`Slicing failed for ${stlFilePath}:`, error);
|
|
920
|
+
console.error(`Slicing failed for ${stlFilePath}:`, error instanceof Error ? error.message : error);
|
|
933
921
|
this.emit('operationError', {
|
|
934
922
|
operationId,
|
|
935
923
|
type: 'slice',
|
|
@@ -938,8 +926,10 @@ export class STLManipulator extends EventEmitter {
|
|
|
938
926
|
throw error;
|
|
939
927
|
}
|
|
940
928
|
finally {
|
|
941
|
-
|
|
942
|
-
fs.
|
|
929
|
+
for (const dir of [orcaOutputDir, bambuOutputDir]) {
|
|
930
|
+
if (dir && fs.existsSync(dir)) {
|
|
931
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
932
|
+
}
|
|
943
933
|
}
|
|
944
934
|
this.activeOperations.delete(operationId);
|
|
945
935
|
}
|
|
@@ -965,45 +955,37 @@ export class STLManipulator extends EventEmitter {
|
|
|
965
955
|
progressCallback(20, "Reading G-code file...");
|
|
966
956
|
// Read the G-code file
|
|
967
957
|
const gcode = await readFileAsync(gcodePath, 'utf8');
|
|
968
|
-
const lines = gcode.split('\n');
|
|
969
958
|
if (!this.activeOperations.get(operationId)) {
|
|
970
959
|
throw new Error("Operation cancelled");
|
|
971
960
|
}
|
|
972
961
|
if (progressCallback)
|
|
973
962
|
progressCallback(50, "Analyzing temperature commands...");
|
|
974
|
-
// Extract
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const temp = parseInt(bedMatch[1], 10);
|
|
992
|
-
allTemperatures.bed.push(temp);
|
|
993
|
-
// Keep the first temperature for compatibility with original function
|
|
994
|
-
if (!actual.bed) {
|
|
995
|
-
actual.bed = temp;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
}
|
|
963
|
+
// Extract every heater target with the shared print-safety scanner: S and
|
|
964
|
+
// R forms, tool-addressed commands, RepRapFirmware G10/M568 and Klipper
|
|
965
|
+
// SET_HEATER_TEMPERATURE. Unparseable thermal syntax fails closed.
|
|
966
|
+
const scan = scanGcodeThermal(gcode);
|
|
967
|
+
const allTemperatures = {
|
|
968
|
+
extruder: scan.targets.filter((target) => target.heater === "nozzle").map((target) => target.value),
|
|
969
|
+
bed: scan.targets.filter((target) => target.heater === "bed").map((target) => target.value),
|
|
970
|
+
};
|
|
971
|
+
// First positive target, kept for compatibility with the original report.
|
|
972
|
+
const actual = {
|
|
973
|
+
extruder: allTemperatures.extruder.find((value) => value > 0),
|
|
974
|
+
bed: allTemperatures.bed.find((value) => value > 0),
|
|
975
|
+
};
|
|
976
|
+
const peak = {
|
|
977
|
+
extruder: allTemperatures.extruder.length ? Math.max(...allTemperatures.extruder) : undefined,
|
|
978
|
+
bed: allTemperatures.bed.length ? Math.max(...allTemperatures.bed) : undefined,
|
|
979
|
+
};
|
|
999
980
|
if (progressCallback)
|
|
1000
981
|
progressCallback(80, "Comparing temperatures...");
|
|
1001
|
-
//
|
|
982
|
+
// An expected temperature matches only when it is the job's highest
|
|
983
|
+
// target, so a later or R-form higher target is always a mismatch.
|
|
1002
984
|
let match = true;
|
|
1003
|
-
if (expected.extruder !== undefined &&
|
|
985
|
+
if (expected.extruder !== undefined && peak.extruder !== expected.extruder) {
|
|
1004
986
|
match = false;
|
|
1005
987
|
}
|
|
1006
|
-
if (expected.bed !== undefined &&
|
|
988
|
+
if (expected.bed !== undefined && peak.bed !== expected.bed) {
|
|
1007
989
|
match = false;
|
|
1008
990
|
}
|
|
1009
991
|
if (progressCallback)
|
|
@@ -1012,9 +994,9 @@ export class STLManipulator extends EventEmitter {
|
|
|
1012
994
|
operationId,
|
|
1013
995
|
type: 'confirmTemperatures',
|
|
1014
996
|
success: true,
|
|
1015
|
-
result: { match, actual, expected, allTemperatures }
|
|
997
|
+
result: { match, actual, peak, expected, allTemperatures }
|
|
1016
998
|
});
|
|
1017
|
-
return { match, actual, expected, allTemperatures };
|
|
999
|
+
return { match, actual, peak, expected, allTemperatures };
|
|
1018
1000
|
}
|
|
1019
1001
|
catch (error) {
|
|
1020
1002
|
console.error("Error confirming temperatures:", error);
|
|
@@ -1047,13 +1029,13 @@ export class STLManipulator extends EventEmitter {
|
|
|
1047
1029
|
const originalVertexCount = geometry.attributes.position.count;
|
|
1048
1030
|
const mergedGeometry = BufferGeometryUtils.mergeVertices(geometry, tolerance);
|
|
1049
1031
|
const newVertexCount = mergedGeometry.attributes.position.count;
|
|
1050
|
-
console.
|
|
1032
|
+
console.error(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`);
|
|
1051
1033
|
if (progressCallback)
|
|
1052
1034
|
progressCallback(70, `Vertices merged: ${originalVertexCount} -> ${newVertexCount}`);
|
|
1053
1035
|
if (!this.activeOperations.get(operationId))
|
|
1054
1036
|
throw new Error("Operation cancelled");
|
|
1055
1037
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_merged.stl';
|
|
1056
|
-
const outputFilePath =
|
|
1038
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
1057
1039
|
await this.saveSTL(mergedGeometry, outputFilePath, progressCallback); // 80-100% progress
|
|
1058
1040
|
this.emit('operationComplete', { operationId, type: 'mergeVertices', success: true, output: outputFilePath, verticesRemoved: originalVertexCount - newVertexCount });
|
|
1059
1041
|
return outputFilePath;
|
|
@@ -1085,7 +1067,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1085
1067
|
if (center.lengthSq() < 0.0001) { // Already centered (or very close)
|
|
1086
1068
|
if (progressCallback)
|
|
1087
1069
|
progressCallback(100, "Model is already centered.");
|
|
1088
|
-
console.
|
|
1070
|
+
console.error("Model already centered. No changes made.");
|
|
1089
1071
|
this.emit('operationComplete', { operationId, type: 'centerModel', success: true, output: stlFilePath, message: "Model already centered." });
|
|
1090
1072
|
return stlFilePath; // Return original path
|
|
1091
1073
|
}
|
|
@@ -1097,7 +1079,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1097
1079
|
if (!this.activeOperations.get(operationId))
|
|
1098
1080
|
throw new Error("Operation cancelled");
|
|
1099
1081
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_centered.stl';
|
|
1100
|
-
const outputFilePath =
|
|
1082
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
1101
1083
|
await this.saveSTL(geometry, outputFilePath, progressCallback); // 80-100% progress
|
|
1102
1084
|
this.emit('operationComplete', { operationId, type: 'centerModel', success: true, output: outputFilePath });
|
|
1103
1085
|
return outputFilePath;
|
|
@@ -1226,7 +1208,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1226
1208
|
if (!this.activeOperations.get(operationId))
|
|
1227
1209
|
throw new Error("Operation cancelled");
|
|
1228
1210
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_flat.stl';
|
|
1229
|
-
const outputFilePath =
|
|
1211
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
1230
1212
|
await this.saveSTL(geometry, outputFilePath, progressCallback); // 90-100% progress
|
|
1231
1213
|
this.emit('operationComplete', { operationId, type: 'layFlat', success: true, output: outputFilePath });
|
|
1232
1214
|
return outputFilePath;
|