mcp-3d-printer-server 1.2.5 → 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 +228 -0
- package/CONTRIBUTORS.md +49 -0
- package/README.md +529 -739
- package/dist/3mf_parser.js +8 -8
- package/dist/blender-mcp-bridge.d.ts +11 -0
- package/dist/blender-mcp-bridge.js +661 -0
- package/dist/fulu-orca.js +29 -5
- package/dist/index.js +752 -212
- 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 -120
- 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 +30 -14
- package/server.json +29 -0
- package/src/3mf_parser.ts +8 -8
- package/src/blender-mcp-bridge.ts +611 -0
- package/src/fulu-orca.ts +36 -6
- package/src/index.ts +888 -255
- package/src/printers/bambu-node-guard.ts +44 -0
- package/src/printers/bambu.ts +458 -158
- 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
- package/dist/index.js.map +0 -1
|
@@ -7,7 +7,20 @@ 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 { detectProfilesRoot } from '../slicer/profile-flatten.js';
|
|
11
|
+
import {
|
|
12
|
+
BambuCliProfilePreparer,
|
|
13
|
+
assertSlicedProjectOutput,
|
|
14
|
+
buildBambuCliArgs,
|
|
15
|
+
type BambuCliSlicerType,
|
|
16
|
+
type BambuSliceOptions,
|
|
17
|
+
} from '../slicer/bambu-cli.js';
|
|
18
|
+
import { runSlicerProcess } from '../slicer/run-slicer.js';
|
|
19
|
+
import { SlicerError, asPreparationError } from '../slicer/slicer-error.js';
|
|
20
|
+
|
|
21
|
+
export type { BambuSliceOptions } from '../slicer/bambu-cli.js';
|
|
10
22
|
import { execFile } from 'child_process';
|
|
23
|
+
import { scanGcodeThermal } from '../safety/gcode-thermal.js';
|
|
11
24
|
|
|
12
25
|
const readFileAsync = promisify(fs.readFile);
|
|
13
26
|
const writeFileAsync = promisify(fs.writeFile);
|
|
@@ -69,6 +82,17 @@ export class STLManipulator extends EventEmitter {
|
|
|
69
82
|
return crypto.randomUUID();
|
|
70
83
|
}
|
|
71
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Each operation writes into its own folder. Outputs named only after the
|
|
87
|
+
* input (for example bracket_extended.stl) would otherwise let two
|
|
88
|
+
* concurrent requests replace each other's file before it is used or printed.
|
|
89
|
+
*/
|
|
90
|
+
private operationOutputPath(operationId: string, fileName: string): string {
|
|
91
|
+
const directory = path.join(this.tempDir, `${operationId}-out`);
|
|
92
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
93
|
+
return path.join(directory, fileName);
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
private validateSlicerExecutable(slicerPath: string): void {
|
|
73
97
|
if (!slicerPath.trim()) {
|
|
74
98
|
throw new Error("Slicer executable path is required.");
|
|
@@ -143,49 +167,6 @@ export class STLManipulator extends EventEmitter {
|
|
|
143
167
|
return path.join(outputDir, plateOutputs[0]);
|
|
144
168
|
}
|
|
145
169
|
|
|
146
|
-
private resolveBambuMachinePresetPath(slicerPath: string, printerPreset: string): string {
|
|
147
|
-
if (fs.existsSync(printerPreset)) {
|
|
148
|
-
return printerPreset;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const presetFile = printerPreset.endsWith(".json") ? printerPreset : `${printerPreset}.json`;
|
|
152
|
-
const candidateDirs: string[] = [];
|
|
153
|
-
const macAppMarker = `${path.sep}Contents${path.sep}MacOS${path.sep}`;
|
|
154
|
-
const macAppIndex = slicerPath.indexOf(macAppMarker);
|
|
155
|
-
|
|
156
|
-
if (macAppIndex >= 0) {
|
|
157
|
-
candidateDirs.push(
|
|
158
|
-
path.join(
|
|
159
|
-
slicerPath.slice(0, macAppIndex + `${path.sep}Contents`.length),
|
|
160
|
-
"Resources",
|
|
161
|
-
"profiles",
|
|
162
|
-
"BBL",
|
|
163
|
-
"machine"
|
|
164
|
-
)
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const executableDir = path.dirname(slicerPath);
|
|
169
|
-
candidateDirs.push(
|
|
170
|
-
path.join(executableDir, "..", "Resources", "profiles", "BBL", "machine"),
|
|
171
|
-
path.join(executableDir, "..", "share", "OrcaSlicer", "resources", "profiles", "BBL", "machine"),
|
|
172
|
-
path.join(executableDir, "..", "resources", "profiles", "BBL", "machine"),
|
|
173
|
-
path.join(executableDir, "resources", "profiles", "BBL", "machine")
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
for (const candidateDir of candidateDirs) {
|
|
177
|
-
const candidate = path.resolve(candidateDir, presetFile);
|
|
178
|
-
if (fs.existsSync(candidate)) {
|
|
179
|
-
return candidate;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
throw new Error(
|
|
184
|
-
`Could not find Bambu machine preset "${presetFile}" near slicer executable ${slicerPath}. ` +
|
|
185
|
-
"Pass slicer_profile with an explicit machine/process settings file, or use a slicer bundle that includes Resources/profiles/BBL/machine."
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
170
|
private resolveSlicerTimeoutMs(): number {
|
|
190
171
|
const rawTimeout = process.env.SLICER_TIMEOUT_MS?.trim();
|
|
191
172
|
if (!rawTimeout) {
|
|
@@ -360,7 +341,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
360
341
|
|
|
361
342
|
// Generate output file path
|
|
362
343
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_scaled.stl';
|
|
363
|
-
const outputFilePath =
|
|
344
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
364
345
|
|
|
365
346
|
// Save the modified STL
|
|
366
347
|
await this.saveSTL(geometry, outputFilePath, progressCallback);
|
|
@@ -434,7 +415,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
434
415
|
|
|
435
416
|
// Generate output file path
|
|
436
417
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_rotated.stl';
|
|
437
|
-
const outputFilePath =
|
|
418
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
438
419
|
|
|
439
420
|
// Save the modified STL
|
|
440
421
|
await this.saveSTL(geometry, outputFilePath, progressCallback);
|
|
@@ -494,7 +475,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
494
475
|
|
|
495
476
|
// Generate output file path
|
|
496
477
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_translated.stl';
|
|
497
|
-
const outputFilePath =
|
|
478
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
498
479
|
|
|
499
480
|
// Save the modified STL
|
|
500
481
|
await this.saveSTL(geometry, outputFilePath, progressCallback);
|
|
@@ -662,7 +643,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
662
643
|
|
|
663
644
|
// Write the SVG to file
|
|
664
645
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_visualization.svg';
|
|
665
|
-
const outputFilePath =
|
|
646
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
666
647
|
|
|
667
648
|
await writeFileAsync(outputFilePath, svgContent);
|
|
668
649
|
|
|
@@ -846,7 +827,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
846
827
|
|
|
847
828
|
// Generate output file path
|
|
848
829
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_modified.stl';
|
|
849
|
-
const outputFilePath =
|
|
830
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
850
831
|
|
|
851
832
|
// Save the modified STL
|
|
852
833
|
await this.saveSTL(geometry, outputFilePath, progressCallback);
|
|
@@ -889,7 +870,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
889
870
|
|
|
890
871
|
try {
|
|
891
872
|
if (progressCallback) progressCallback(0, "Starting base extension operation...");
|
|
892
|
-
console.
|
|
873
|
+
console.error(`Extending base of ${stlFilePath} by ${extensionInches} inches`);
|
|
893
874
|
|
|
894
875
|
// Load the STL file
|
|
895
876
|
const { geometry, boundingBox } = await this.loadSTL(stlFilePath, progressCallback);
|
|
@@ -900,30 +881,30 @@ export class STLManipulator extends EventEmitter {
|
|
|
900
881
|
|
|
901
882
|
if (progressCallback) progressCallback(60, "Creating extended base geometry...");
|
|
902
883
|
|
|
903
|
-
//
|
|
904
|
-
const
|
|
884
|
+
// STL files and slicers are Z-up: the base goes under the model.
|
|
885
|
+
const minZ = boundingBox.min.z;
|
|
905
886
|
|
|
906
887
|
// Convert inches to millimeters (STL files typically use mm)
|
|
907
888
|
const extensionMm = extensionInches * 25.4;
|
|
908
889
|
|
|
909
|
-
//
|
|
910
|
-
const matrix = new THREE.Matrix4().makeTranslation(0,
|
|
890
|
+
// Raise the model by the extension amount
|
|
891
|
+
const matrix = new THREE.Matrix4().makeTranslation(0, 0, extensionMm);
|
|
911
892
|
geometry.applyMatrix4(matrix);
|
|
912
893
|
|
|
913
|
-
//
|
|
894
|
+
// A box spanning the model's X/Y footprint fills the new space below it
|
|
914
895
|
const baseWidth = boundingBox.max.x - boundingBox.min.x;
|
|
915
|
-
const baseDepth = boundingBox.max.
|
|
896
|
+
const baseDepth = boundingBox.max.y - boundingBox.min.y;
|
|
916
897
|
const baseGeometry = new THREE.BoxGeometry(
|
|
917
898
|
baseWidth,
|
|
918
|
-
|
|
919
|
-
|
|
899
|
+
baseDepth,
|
|
900
|
+
extensionMm
|
|
920
901
|
);
|
|
921
902
|
|
|
922
903
|
// Position the base geometry
|
|
923
904
|
const baseMatrix = new THREE.Matrix4().makeTranslation(
|
|
924
905
|
(boundingBox.min.x + boundingBox.max.x) / 2,
|
|
925
|
-
|
|
926
|
-
|
|
906
|
+
(boundingBox.min.y + boundingBox.max.y) / 2,
|
|
907
|
+
minZ + extensionMm / 2
|
|
927
908
|
);
|
|
928
909
|
baseGeometry.applyMatrix4(baseMatrix);
|
|
929
910
|
|
|
@@ -950,7 +931,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
950
931
|
|
|
951
932
|
// Generate output file path
|
|
952
933
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_extended.stl';
|
|
953
|
-
const outputFilePath =
|
|
934
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
954
935
|
|
|
955
936
|
if (progressCallback) progressCallback(90, "Saving extended STL...");
|
|
956
937
|
|
|
@@ -966,7 +947,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
966
947
|
output: outputFilePath
|
|
967
948
|
});
|
|
968
949
|
|
|
969
|
-
console.
|
|
950
|
+
console.error(`Modified STL saved to ${outputFilePath}`);
|
|
970
951
|
return outputFilePath;
|
|
971
952
|
} catch (error) {
|
|
972
953
|
console.error("Error extending STL base:", error);
|
|
@@ -983,13 +964,31 @@ export class STLManipulator extends EventEmitter {
|
|
|
983
964
|
}
|
|
984
965
|
}
|
|
985
966
|
|
|
967
|
+
/**
|
|
968
|
+
* Bambu-compatible CLI slicing applies when the slicer is BambuStudio or
|
|
969
|
+
* FULU OrcaSlicer-bambulab, or when generic OrcaSlicer is given an explicit
|
|
970
|
+
* Bambu machine preset. Generic OrcaSlicer without a preset keeps its
|
|
971
|
+
* user-supplied machine/process profile path.
|
|
972
|
+
*/
|
|
973
|
+
static usesBambuCliPreparation(slicerType: SlicerType, printerPreset?: string): boolean {
|
|
974
|
+
return (
|
|
975
|
+
slicerType === 'bambustudio' ||
|
|
976
|
+
slicerType === 'orcaslicer-bambulab' ||
|
|
977
|
+
(slicerType === 'orcaslicer' && Boolean(printerPreset))
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
986
981
|
/**
|
|
987
982
|
* Slice an STL or 3MF file using the specified slicer
|
|
988
983
|
* @param stlFilePath Path to the input STL or 3MF file
|
|
989
984
|
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio)
|
|
990
985
|
* @param slicerPath Path to the slicer executable
|
|
991
|
-
* @param slicerProfile Optional
|
|
986
|
+
* @param slicerProfile Optional slicer profile/config file. For Bambu-compatible
|
|
987
|
+
* CLI slicing this is a process profile; the machine comes from printerPreset.
|
|
992
988
|
* @param progressCallback Optional callback for progress updates
|
|
989
|
+
* @param printerPreset Bambu machine preset name, e.g. "Bambu Lab P1S 0.4 nozzle"
|
|
990
|
+
* @param filamentProfile Optional filament profile path(s), ';'-separated
|
|
991
|
+
* @param bambuOptions Optional Bambu-compatible CLI options (added last for compatibility)
|
|
993
992
|
* @returns Path to the generated G-code or sliced 3MF file
|
|
994
993
|
*/
|
|
995
994
|
async sliceSTL(
|
|
@@ -999,199 +998,181 @@ export class STLManipulator extends EventEmitter {
|
|
|
999
998
|
slicerProfile?: string,
|
|
1000
999
|
progressCallback?: ProgressCallback,
|
|
1001
1000
|
printerPreset?: string,
|
|
1002
|
-
filamentProfile?: string
|
|
1001
|
+
filamentProfile?: string,
|
|
1002
|
+
bambuOptions?: BambuSliceOptions
|
|
1003
1003
|
): Promise<string> {
|
|
1004
1004
|
const operationId = this.generateOperationId();
|
|
1005
1005
|
this.activeOperations.set(operationId, true);
|
|
1006
|
+
const bambuCli = STLManipulator.usesBambuCliPreparation(slicerType, printerPreset);
|
|
1006
1007
|
|
|
1007
|
-
this.validateSlicerExecutable(slicerPath);
|
|
1008
|
-
const orcaProfiles = this.parseOrcaProfileArgs(slicerProfile, filamentProfile);
|
|
1009
|
-
if (slicerType === 'orcaslicer') {
|
|
1010
|
-
this.warnForMissingProfiles(orcaProfiles.settingsProfiles, 'Slicer profile');
|
|
1011
|
-
this.warnForMissingProfiles(orcaProfiles.filamentProfiles, 'Filament profile');
|
|
1012
|
-
} else if (slicerProfile && !fs.existsSync(slicerProfile)) {
|
|
1013
|
-
console.warn(`Slicer profile specified but not found: ${slicerProfile}. Slicer might use defaults.`);
|
|
1014
|
-
// Allow proceeding without profile, slicer might handle it
|
|
1015
|
-
}
|
|
1016
|
-
if (slicerType !== 'orcaslicer' && filamentProfile && !fs.existsSync(filamentProfile)) {
|
|
1017
|
-
console.warn(`Filament profile specified but not found: ${filamentProfile}. Slicer might use defaults.`);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
|
|
1021
|
-
let outputFilePath = path.join(this.tempDir, outputFileName);
|
|
1022
1008
|
let orcaOutputDir: string | undefined;
|
|
1023
|
-
|
|
1024
|
-
let args: string[] = [];
|
|
1009
|
+
let bambuOutputDir: string | undefined;
|
|
1025
1010
|
|
|
1026
1011
|
try {
|
|
1027
|
-
|
|
1012
|
+
try {
|
|
1013
|
+
this.validateSlicerExecutable(slicerPath);
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
throw asPreparationError(error, { slicerType, slicerPath });
|
|
1016
|
+
}
|
|
1017
|
+
if (!fs.existsSync(stlFilePath)) {
|
|
1018
|
+
throw new SlicerError('preparation', `Input model not found: ${stlFilePath}`, { slicerType, slicerPath });
|
|
1019
|
+
}
|
|
1028
1020
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1021
|
+
const orcaProfiles = this.parseOrcaProfileArgs(slicerProfile, filamentProfile);
|
|
1022
|
+
if (!bambuCli) {
|
|
1023
|
+
if (slicerType === 'orcaslicer') {
|
|
1024
|
+
this.warnForMissingProfiles(orcaProfiles.settingsProfiles, 'Slicer profile');
|
|
1025
|
+
this.warnForMissingProfiles(orcaProfiles.filamentProfiles, 'Filament profile');
|
|
1026
|
+
} else if (slicerProfile && !fs.existsSync(slicerProfile)) {
|
|
1027
|
+
console.warn(`Slicer profile specified but not found: ${slicerProfile}. Slicer might use defaults.`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
|
|
1032
|
+
let outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
1033
|
+
let args: string[] = [];
|
|
1034
|
+
let bambuExport: { producedPath: string; finalPath: string } | undefined;
|
|
1041
1035
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
'-l', stlFilePath,
|
|
1061
|
-
'-o', outputFilePath
|
|
1062
|
-
];
|
|
1063
|
-
if (slicerProfile) {
|
|
1064
|
-
args.push('-j', slicerProfile); // Load settings from profile definition file
|
|
1036
|
+
if (progressCallback) progressCallback(0, `Starting slicing with ${slicerType}...`);
|
|
1037
|
+
|
|
1038
|
+
if (bambuCli) {
|
|
1039
|
+
// Bambu-compatible CLI: resolve the exact machine preset and every
|
|
1040
|
+
// profile dependency, then slice and export a 3MF with plate G-code.
|
|
1041
|
+
const cliType = slicerType as BambuCliSlicerType;
|
|
1042
|
+
const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
|
|
1043
|
+
const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
|
|
1044
|
+
const exportName = `${outputBase}_sliced.3mf`;
|
|
1045
|
+
try {
|
|
1046
|
+
if (orcaProfiles.settingsProfiles.length > 1) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`slicer_profile (or SLICER_PROFILE) lists ${orcaProfiles.settingsProfiles.length} settings files, but Bambu-compatible CLI slicing ` +
|
|
1049
|
+
'takes one process profile. Set slicer_profile/SLICER_PROFILE to a process profile only (or leave it empty for the ' +
|
|
1050
|
+
"preset's default process); the machine preset comes from bambu_model/BAMBU_MODEL and nozzle_diameter/NOZZLE_DIAMETER. " +
|
|
1051
|
+
'Pass filaments with filament_profile/load_filaments. For a non-Bambu printer, use slicer_type orcaslicer without bambu_model ' +
|
|
1052
|
+
'to keep the machine;process|filament format.'
|
|
1053
|
+
);
|
|
1065
1054
|
}
|
|
1066
|
-
|
|
1055
|
+
const options: BambuSliceOptions = { ...bambuOptions };
|
|
1056
|
+
if (orcaProfiles.filamentProfiles.length > 0) {
|
|
1057
|
+
const legacyFilaments = orcaProfiles.filamentProfiles.join(';');
|
|
1058
|
+
if (options.loadFilaments && this.splitProfileList(options.loadFilaments).join(';') !== legacyFilaments) {
|
|
1059
|
+
throw new Error('Provide either load_filaments or filament_profile, not conflicting values.');
|
|
1060
|
+
}
|
|
1061
|
+
options.loadFilaments = legacyFilaments;
|
|
1062
|
+
}
|
|
1063
|
+
const profilesRoot = detectProfilesRoot(slicerPath, cliType);
|
|
1064
|
+
const preparer = new BambuCliProfilePreparer(path.join(this.tempDir, 'slicer-profiles'));
|
|
1065
|
+
const bundle = await preparer.prepare(
|
|
1066
|
+
stlFilePath,
|
|
1067
|
+
outputBase,
|
|
1068
|
+
cliType,
|
|
1069
|
+
profilesRoot,
|
|
1070
|
+
printerPreset,
|
|
1071
|
+
orcaProfiles.settingsProfiles[0],
|
|
1072
|
+
options
|
|
1073
|
+
);
|
|
1074
|
+
bambuOutputDir = fs.mkdtempSync(path.join(this.tempDir, `${operationId}-bambu-output-`));
|
|
1075
|
+
args = [...buildBambuCliArgs(bundle, bambuOutputDir, exportName, options), stlFilePath];
|
|
1076
|
+
bambuExport = {
|
|
1077
|
+
producedPath: path.join(bambuOutputDir, exportName),
|
|
1078
|
+
// One folder per operation: concurrent slices of same-named inputs
|
|
1079
|
+
// must never replace each other's result before it is printed.
|
|
1080
|
+
finalPath: this.operationOutputPath(operationId, exportName),
|
|
1081
|
+
};
|
|
1082
|
+
outputFilePath = bambuExport.finalPath;
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
throw asPreparationError(error, { slicerType, slicerPath });
|
|
1085
|
+
}
|
|
1086
|
+
} else {
|
|
1087
|
+
switch (slicerType) {
|
|
1088
|
+
case 'prusaslicer':
|
|
1089
|
+
case 'slic3r': // PrusaSlicer and Slic3r share similar CLI args
|
|
1090
|
+
args = [
|
|
1091
|
+
'--slice',
|
|
1092
|
+
'--output', outputFilePath,
|
|
1093
|
+
'--load', slicerProfile || '', // Pass profile path
|
|
1094
|
+
stlFilePath
|
|
1095
|
+
];
|
|
1096
|
+
// Remove empty profile arg if not provided
|
|
1097
|
+
args = args.filter(arg => arg !== '');
|
|
1098
|
+
break;
|
|
1067
1099
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
// orcaslicer-bambulab is the FULU fork that restores BambuNetwork support.
|
|
1072
|
-
// For 3MF input: slice in place and export sliced 3MF
|
|
1073
|
-
// For STL input: slice and export as 3MF
|
|
1074
|
-
{
|
|
1075
|
-
const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
|
|
1076
|
-
const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
|
|
1077
|
-
const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
|
|
1100
|
+
case 'orcaslicer':
|
|
1101
|
+
orcaOutputDir = path.join(this.tempDir, `${operationId}-orca-output`);
|
|
1102
|
+
fs.mkdirSync(orcaOutputDir, { recursive: true });
|
|
1078
1103
|
args = [
|
|
1079
|
-
'--slice', '0',
|
|
1080
|
-
'--
|
|
1104
|
+
'--slice', '0',
|
|
1105
|
+
'--outputdir', orcaOutputDir,
|
|
1106
|
+
];
|
|
1107
|
+
this.appendOrcaProfileArgs(args, orcaProfiles);
|
|
1108
|
+
args.push(stlFilePath);
|
|
1109
|
+
break;
|
|
1110
|
+
|
|
1111
|
+
case 'cura':
|
|
1112
|
+
// CuraEngine CLI args are different, often requiring -s for settings
|
|
1113
|
+
// Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
|
|
1114
|
+
args = [
|
|
1115
|
+
'slice',
|
|
1116
|
+
'-l', stlFilePath,
|
|
1117
|
+
'-o', outputFilePath
|
|
1081
1118
|
];
|
|
1082
|
-
// FULU Orca/Bambu Studio load machine presets as JSON settings files.
|
|
1083
|
-
if (printerPreset && !slicerProfile) {
|
|
1084
|
-
args.push('--load-settings', this.resolveBambuMachinePresetPath(slicerPath, printerPreset));
|
|
1085
|
-
}
|
|
1086
1119
|
if (slicerProfile) {
|
|
1087
|
-
args.push('
|
|
1120
|
+
args.push('-j', slicerProfile); // Load settings from profile definition file
|
|
1088
1121
|
}
|
|
1089
|
-
|
|
1090
|
-
args.push('--load-filaments', filamentProfile);
|
|
1091
|
-
}
|
|
1092
|
-
args.push(stlFilePath);
|
|
1093
|
-
// Override outputFilePath for bambustudio since it produces 3MF, not gcode
|
|
1094
|
-
outputFilePath = bambuOutputPath;
|
|
1095
|
-
}
|
|
1096
|
-
break;
|
|
1122
|
+
break;
|
|
1097
1123
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1124
|
+
default:
|
|
1125
|
+
throw new SlicerError('preparation', `Unsupported slicer type: ${slicerType}`, { slicerType, slicerPath });
|
|
1126
|
+
}
|
|
1100
1127
|
}
|
|
1101
1128
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const timeoutMs = this.resolveSlicerTimeoutMs();
|
|
1108
|
-
let settled = false;
|
|
1109
|
-
let slicerProcessKilledByTimeout = false;
|
|
1110
|
-
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
1111
|
-
|
|
1112
|
-
const settle = (callback: () => void) => {
|
|
1113
|
-
if (settled) {
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
settled = true;
|
|
1117
|
-
if (timeoutHandle) {
|
|
1118
|
-
clearTimeout(timeoutHandle);
|
|
1119
|
-
}
|
|
1120
|
-
callback();
|
|
1121
|
-
};
|
|
1122
|
-
|
|
1123
|
-
const process = execFile(slicerPath, args, (error, stdout, stderr) => {
|
|
1124
|
-
if (error) {
|
|
1125
|
-
console.error(`Slicer Error: ${error.message}`);
|
|
1126
|
-
console.error(`Slicer Stderr: ${stderr}`);
|
|
1127
|
-
const timeoutMessage = slicerProcessKilledByTimeout
|
|
1128
|
-
? ` Slicer exceeded timeout ${timeoutMs}ms.`
|
|
1129
|
-
: "";
|
|
1130
|
-
settle(() => reject(new Error(`Slicer failed: ${error.message}.${timeoutMessage} Stderr: ${stderr}`)));
|
|
1131
|
-
} else {
|
|
1132
|
-
console.log(`Slicer Stdout: ${stdout}`);
|
|
1133
|
-
if (stderr) {
|
|
1134
|
-
console.warn(`Slicer Stderr: ${stderr}`); // Log stderr even on success
|
|
1135
|
-
}
|
|
1136
|
-
settle(() => resolve());
|
|
1137
|
-
}
|
|
1138
|
-
});
|
|
1139
|
-
|
|
1140
|
-
timeoutHandle = setTimeout(() => {
|
|
1141
|
-
slicerProcessKilledByTimeout = true;
|
|
1142
|
-
try {
|
|
1143
|
-
process.kill("SIGKILL");
|
|
1144
|
-
} catch (killError) {
|
|
1145
|
-
console.error("Error attempting to kill timed-out slicer process:", killError);
|
|
1146
|
-
}
|
|
1147
|
-
settle(() => reject(new Error(`Slicer exceeded timeout ${timeoutMs}ms.`)));
|
|
1148
|
-
}, timeoutMs);
|
|
1149
|
-
|
|
1150
|
-
// Optional: Add listeners for stdout/stderr for real-time progress if slicer provides it
|
|
1151
|
-
// process.stdout?.on('data', (data) => { console.log(`Slicer stdout: ${data}`); });
|
|
1152
|
-
// process.stderr?.on('data', (data) => { console.log(`Slicer stderr: ${data}`); });
|
|
1153
|
-
|
|
1154
|
-
// Check for cancellation periodically
|
|
1155
|
-
const checkCancel = setInterval(() => {
|
|
1156
|
-
if (!this.activeOperations.get(operationId)) {
|
|
1157
|
-
clearInterval(checkCancel);
|
|
1158
|
-
try {
|
|
1159
|
-
process.kill(); // Attempt to kill the slicer process
|
|
1160
|
-
settle(() => reject(new Error("Slicing operation cancelled")));
|
|
1161
|
-
} catch (killError) {
|
|
1162
|
-
console.error("Error attempting to kill slicer process:", killError);
|
|
1163
|
-
settle(() => reject(new Error("Slicing operation cancelled, but failed to kill process.")));
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
}, 500);
|
|
1167
|
-
|
|
1168
|
-
process.on('exit', () => clearInterval(checkCancel));
|
|
1169
|
-
process.on('error', () => clearInterval(checkCancel)); // Ensure interval cleared on process error too
|
|
1129
|
+
// Never mistake a previous run's file for this run's result (or overwrite the input).
|
|
1130
|
+
if (path.resolve(outputFilePath) === path.resolve(stlFilePath)) {
|
|
1131
|
+
throw new SlicerError('preparation', `Slicer output would overwrite the input file: ${stlFilePath}`, { slicerType, slicerPath });
|
|
1132
|
+
}
|
|
1133
|
+
if (fs.existsSync(outputFilePath)) fs.rmSync(outputFilePath, { force: true });
|
|
1170
1134
|
|
|
1135
|
+
if (progressCallback) progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
|
|
1136
|
+
console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
|
|
1137
|
+
|
|
1138
|
+
await runSlicerProcess({
|
|
1139
|
+
command: slicerPath,
|
|
1140
|
+
args,
|
|
1141
|
+
timeoutMs: this.resolveSlicerTimeoutMs(),
|
|
1142
|
+
slicerType,
|
|
1143
|
+
isCancelled: () => !this.activeOperations.get(operationId),
|
|
1171
1144
|
});
|
|
1172
1145
|
|
|
1173
1146
|
if (!this.activeOperations.get(operationId)) {
|
|
1174
|
-
|
|
1175
|
-
throw new Error("Slicing operation cancelled after process finished");
|
|
1147
|
+
throw new SlicerError('execution', 'Slicing operation cancelled after process finished', { slicerType, slicerPath });
|
|
1176
1148
|
}
|
|
1177
1149
|
|
|
1178
|
-
|
|
1179
|
-
if (
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
fs.
|
|
1150
|
+
try {
|
|
1151
|
+
if (bambuExport) {
|
|
1152
|
+
if (!fs.existsSync(bambuExport.producedPath)) {
|
|
1153
|
+
throw new Error(`Slicer exited successfully but did not write ${bambuExport.producedPath}.`);
|
|
1154
|
+
}
|
|
1155
|
+
await assertSlicedProjectOutput(bambuExport.producedPath);
|
|
1156
|
+
fs.mkdirSync(path.dirname(bambuExport.finalPath), { recursive: true });
|
|
1157
|
+
fs.renameSync(bambuExport.producedPath, bambuExport.finalPath);
|
|
1158
|
+
} else if (slicerType === 'orcaslicer') {
|
|
1159
|
+
if (!orcaOutputDir) {
|
|
1160
|
+
throw new Error("Internal error: OrcaSlicer output directory was not initialized.");
|
|
1161
|
+
}
|
|
1162
|
+
const orcaGcodePath = this.resolveOrcaGcodeOutput(orcaOutputDir);
|
|
1163
|
+
fs.renameSync(orcaGcodePath, outputFilePath);
|
|
1185
1164
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
if (!fs.existsSync(outputFilePath)) {
|
|
1165
|
+
|
|
1166
|
+
if (!fs.existsSync(outputFilePath)) {
|
|
1190
1167
|
throw new Error(`Slicer finished but output file not found: ${outputFilePath}`);
|
|
1168
|
+
}
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
if (error instanceof SlicerError) throw error;
|
|
1171
|
+
throw new SlicerError('output', error instanceof Error ? error.message : String(error), { slicerType, slicerPath });
|
|
1191
1172
|
}
|
|
1192
1173
|
|
|
1193
1174
|
if (progressCallback) progressCallback(100, "Slicing completed successfully");
|
|
1194
|
-
|
|
1175
|
+
|
|
1195
1176
|
this.emit('operationComplete', {
|
|
1196
1177
|
operationId,
|
|
1197
1178
|
type: 'slice',
|
|
@@ -1201,16 +1182,18 @@ export class STLManipulator extends EventEmitter {
|
|
|
1201
1182
|
|
|
1202
1183
|
return outputFilePath;
|
|
1203
1184
|
} catch (error) {
|
|
1204
|
-
console.error(`Slicing failed for ${stlFilePath}:`, error);
|
|
1185
|
+
console.error(`Slicing failed for ${stlFilePath}:`, error instanceof Error ? error.message : error);
|
|
1205
1186
|
this.emit('operationError', {
|
|
1206
1187
|
operationId,
|
|
1207
1188
|
type: 'slice',
|
|
1208
1189
|
error: error instanceof Error ? error.message : String(error)
|
|
1209
1190
|
});
|
|
1210
|
-
throw error;
|
|
1191
|
+
throw error;
|
|
1211
1192
|
} finally {
|
|
1212
|
-
|
|
1213
|
-
fs.
|
|
1193
|
+
for (const dir of [orcaOutputDir, bambuOutputDir]) {
|
|
1194
|
+
if (dir && fs.existsSync(dir)) {
|
|
1195
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1196
|
+
}
|
|
1214
1197
|
}
|
|
1215
1198
|
this.activeOperations.delete(operationId);
|
|
1216
1199
|
}
|
|
@@ -1233,6 +1216,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1233
1216
|
): Promise<{
|
|
1234
1217
|
match: boolean;
|
|
1235
1218
|
actual: { extruder?: number; bed?: number };
|
|
1219
|
+
peak: { extruder?: number; bed?: number };
|
|
1236
1220
|
expected: { extruder?: number; bed?: number };
|
|
1237
1221
|
allTemperatures: { extruder: number[]; bed: number[] };
|
|
1238
1222
|
}> {
|
|
@@ -1251,7 +1235,6 @@ export class STLManipulator extends EventEmitter {
|
|
|
1251
1235
|
|
|
1252
1236
|
// Read the G-code file
|
|
1253
1237
|
const gcode = await readFileAsync(gcodePath, 'utf8');
|
|
1254
|
-
const lines = gcode.split('\n');
|
|
1255
1238
|
|
|
1256
1239
|
if (!this.activeOperations.get(operationId)) {
|
|
1257
1240
|
throw new Error("Operation cancelled");
|
|
@@ -1259,57 +1242,46 @@ export class STLManipulator extends EventEmitter {
|
|
|
1259
1242
|
|
|
1260
1243
|
if (progressCallback) progressCallback(50, "Analyzing temperature commands...");
|
|
1261
1244
|
|
|
1262
|
-
// Extract
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const bedMatch = line.match(/M1[49]0 S(\d+)/);
|
|
1281
|
-
if (bedMatch) {
|
|
1282
|
-
const temp = parseInt(bedMatch[1], 10);
|
|
1283
|
-
allTemperatures.bed.push(temp);
|
|
1284
|
-
|
|
1285
|
-
// Keep the first temperature for compatibility with original function
|
|
1286
|
-
if (!actual.bed) {
|
|
1287
|
-
actual.bed = temp;
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1245
|
+
// Extract every heater target with the shared print-safety scanner: S and
|
|
1246
|
+
// R forms, tool-addressed commands, RepRapFirmware G10/M568 and Klipper
|
|
1247
|
+
// SET_HEATER_TEMPERATURE. Unparseable thermal syntax fails closed.
|
|
1248
|
+
const scan = scanGcodeThermal(gcode);
|
|
1249
|
+
const allTemperatures: { extruder: number[]; bed: number[] } = {
|
|
1250
|
+
extruder: scan.targets.filter((target) => target.heater === "nozzle").map((target) => target.value),
|
|
1251
|
+
bed: scan.targets.filter((target) => target.heater === "bed").map((target) => target.value),
|
|
1252
|
+
};
|
|
1253
|
+
// First positive target, kept for compatibility with the original report.
|
|
1254
|
+
const actual: { extruder?: number; bed?: number } = {
|
|
1255
|
+
extruder: allTemperatures.extruder.find((value) => value > 0),
|
|
1256
|
+
bed: allTemperatures.bed.find((value) => value > 0),
|
|
1257
|
+
};
|
|
1258
|
+
const peak = {
|
|
1259
|
+
extruder: allTemperatures.extruder.length ? Math.max(...allTemperatures.extruder) : undefined,
|
|
1260
|
+
bed: allTemperatures.bed.length ? Math.max(...allTemperatures.bed) : undefined,
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1292
1263
|
if (progressCallback) progressCallback(80, "Comparing temperatures...");
|
|
1293
|
-
|
|
1294
|
-
//
|
|
1264
|
+
|
|
1265
|
+
// An expected temperature matches only when it is the job's highest
|
|
1266
|
+
// target, so a later or R-form higher target is always a mismatch.
|
|
1295
1267
|
let match = true;
|
|
1296
|
-
if (expected.extruder !== undefined &&
|
|
1268
|
+
if (expected.extruder !== undefined && peak.extruder !== expected.extruder) {
|
|
1297
1269
|
match = false;
|
|
1298
1270
|
}
|
|
1299
|
-
if (expected.bed !== undefined &&
|
|
1271
|
+
if (expected.bed !== undefined && peak.bed !== expected.bed) {
|
|
1300
1272
|
match = false;
|
|
1301
1273
|
}
|
|
1302
|
-
|
|
1274
|
+
|
|
1303
1275
|
if (progressCallback) progressCallback(100, "Temperature verification complete");
|
|
1304
|
-
|
|
1276
|
+
|
|
1305
1277
|
this.emit('operationComplete', {
|
|
1306
1278
|
operationId,
|
|
1307
1279
|
type: 'confirmTemperatures',
|
|
1308
1280
|
success: true,
|
|
1309
|
-
result: { match, actual, expected, allTemperatures }
|
|
1281
|
+
result: { match, actual, peak, expected, allTemperatures }
|
|
1310
1282
|
});
|
|
1311
|
-
|
|
1312
|
-
return { match, actual, expected, allTemperatures };
|
|
1283
|
+
|
|
1284
|
+
return { match, actual, peak, expected, allTemperatures };
|
|
1313
1285
|
} catch (error) {
|
|
1314
1286
|
console.error("Error confirming temperatures:", error);
|
|
1315
1287
|
|
|
@@ -1348,13 +1320,13 @@ export class STLManipulator extends EventEmitter {
|
|
|
1348
1320
|
const mergedGeometry = BufferGeometryUtils.mergeVertices(geometry, tolerance);
|
|
1349
1321
|
const newVertexCount = mergedGeometry.attributes.position.count;
|
|
1350
1322
|
|
|
1351
|
-
console.
|
|
1323
|
+
console.error(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`);
|
|
1352
1324
|
if (progressCallback) progressCallback(70, `Vertices merged: ${originalVertexCount} -> ${newVertexCount}`);
|
|
1353
1325
|
|
|
1354
1326
|
if (!this.activeOperations.get(operationId)) throw new Error("Operation cancelled");
|
|
1355
1327
|
|
|
1356
1328
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_merged.stl';
|
|
1357
|
-
const outputFilePath =
|
|
1329
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
1358
1330
|
|
|
1359
1331
|
await this.saveSTL(mergedGeometry, outputFilePath, progressCallback); // 80-100% progress
|
|
1360
1332
|
|
|
@@ -1392,7 +1364,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1392
1364
|
|
|
1393
1365
|
if (center.lengthSq() < 0.0001) { // Already centered (or very close)
|
|
1394
1366
|
if (progressCallback) progressCallback(100, "Model is already centered.");
|
|
1395
|
-
console.
|
|
1367
|
+
console.error("Model already centered. No changes made.");
|
|
1396
1368
|
this.emit('operationComplete', { operationId, type: 'centerModel', success: true, output: stlFilePath, message: "Model already centered." });
|
|
1397
1369
|
return stlFilePath; // Return original path
|
|
1398
1370
|
}
|
|
@@ -1406,7 +1378,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1406
1378
|
if (!this.activeOperations.get(operationId)) throw new Error("Operation cancelled");
|
|
1407
1379
|
|
|
1408
1380
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_centered.stl';
|
|
1409
|
-
const outputFilePath =
|
|
1381
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
1410
1382
|
|
|
1411
1383
|
await this.saveSTL(geometry, outputFilePath, progressCallback); // 80-100% progress
|
|
1412
1384
|
|
|
@@ -1553,7 +1525,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1553
1525
|
if (!this.activeOperations.get(operationId)) throw new Error("Operation cancelled");
|
|
1554
1526
|
|
|
1555
1527
|
const outputFileName = path.basename(stlFilePath, '.stl') + '_flat.stl';
|
|
1556
|
-
const outputFilePath =
|
|
1528
|
+
const outputFilePath = this.operationOutputPath(operationId, outputFileName);
|
|
1557
1529
|
|
|
1558
1530
|
await this.saveSTL(geometry, outputFilePath, progressCallback); // 90-100% progress
|
|
1559
1531
|
|