bambu-printer-mcp 1.1.27 → 1.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CONTRIBUTORS.md +7 -1
  2. package/README.md +29 -10
  3. package/dist/ams-mapping.d.ts +18 -2
  4. package/dist/ams-mapping.js +36 -5
  5. package/dist/bambu-native.d.ts +55 -0
  6. package/dist/bambu-native.js +212 -13
  7. package/dist/index.js +154 -71
  8. package/dist/printers/bambu.d.ts +29 -2
  9. package/dist/printers/bambu.js +59 -28
  10. package/dist/safety/hidden-hms.d.ts +32 -0
  11. package/dist/safety/hidden-hms.js +405 -0
  12. package/dist/safety/limits.d.ts +6 -0
  13. package/dist/safety/limits.js +9 -0
  14. package/dist/safety/print-file.d.ts +2 -0
  15. package/dist/safety/print-file.js +83 -7
  16. package/dist/safety/printer-state.d.ts +11 -0
  17. package/dist/safety/printer-state.js +115 -22
  18. package/dist/slicer/profile-flatten.js +25 -0
  19. package/dist/slicer/remote-slicer.d.ts +12 -0
  20. package/dist/slicer/remote-slicer.js +240 -0
  21. package/dist/stl/stl-manipulator.d.ts +2 -1
  22. package/dist/stl/stl-manipulator.js +71 -42
  23. package/native/bambu-native-print.cpp +100 -26
  24. package/package.json +7 -5
  25. package/scripts/build-bambu-native.sh +53 -0
  26. package/scripts/setup-bambu-native.mjs +372 -0
  27. package/src/ams-mapping.ts +38 -5
  28. package/src/bambu-native.ts +229 -14
  29. package/src/index.ts +156 -67
  30. package/src/printers/bambu.ts +65 -25
  31. package/src/safety/hidden-hms.ts +406 -0
  32. package/src/safety/limits.ts +9 -0
  33. package/src/safety/print-file.ts +71 -7
  34. package/src/safety/printer-state.ts +117 -22
  35. package/src/slicer/profile-flatten.ts +32 -0
  36. package/src/slicer/remote-slicer.ts +216 -0
  37. package/src/stl/stl-manipulator.ts +70 -43
  38. package/scripts/build-bambu-native.zsh +0 -33
@@ -0,0 +1,240 @@
1
+ /** AFKFelix/orca-slicer-api contract at a9a28f0a7b0c6f51b9be158509e9e9677532b868. */
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { inspectPrintFile } from '../safety/print-file.js';
6
+ import { normalizeMaterial, normalizeModel } from '../safety/limits.js';
7
+ const MAX_BYTES = 100000000;
8
+ const SUPPORTED_OPTIONS = new Set([
9
+ 'arrange', 'orient', 'loadFilaments', 'filamentColours', 'bedType',
10
+ 'nozzleType', 'nozzleVolumeType', 'slicePlate',
11
+ ]);
12
+ function configuration() {
13
+ let url;
14
+ try {
15
+ url = new URL(process.env.SLICER_API_URL ?? '');
16
+ }
17
+ catch {
18
+ throw new Error('Remote slicing requires a valid trusted SLICER_API_URL.');
19
+ }
20
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
21
+ throw new Error('SLICER_API_URL must use HTTP(S) without embedded credentials, query or fragment.');
22
+ }
23
+ const mode = process.env.SLICER_API_MODE || 'sync';
24
+ if (!['sync', 'async'].includes(mode))
25
+ throw new Error('SLICER_API_MODE must be sync or async.');
26
+ const timeout = Number(process.env.SLICER_API_TIMEOUT_MS ?? 600000);
27
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > 3600000) {
28
+ throw new Error('SLICER_API_TIMEOUT_MS must be an integer from 1 to 3600000.');
29
+ }
30
+ const headers = {};
31
+ const token = process.env.SLICER_API_TOKEN;
32
+ if (token) {
33
+ const header = process.env.SLICER_API_TOKEN_HEADER || 'Authorization';
34
+ if (!/^[a-zA-Z0-9!#$%&'*+.^_`|~-]+$/.test(header) ||
35
+ ['host', 'content-type', 'content-length', 'connection', 'transfer-encoding'].includes(header.toLowerCase()) ||
36
+ /[^\x20-\x7e]/.test(token)) {
37
+ throw new Error('Invalid remote slicing authentication configuration.');
38
+ }
39
+ headers[header] = header.toLowerCase() === 'authorization' ? `Bearer ${token}` : token;
40
+ }
41
+ return { base: url.href.replace(/\/+$/, ''), headers, mode, timeout };
42
+ }
43
+ export function validateRemoteSliceOptions(options = {}) {
44
+ for (const [key, value] of Object.entries(options)) {
45
+ if (value !== undefined && !SUPPORTED_OPTIONS.has(key)) {
46
+ throw new Error(`Remote slicing option ${key} is unsupported by orca-slicer-api; use a local backend for this workflow.`);
47
+ }
48
+ }
49
+ // Upstream evaluates multipart values by truthiness, so the string "false"
50
+ // becomes true. Never silently turn an explicit false into an enabled flag.
51
+ if (options.arrange === false || options.orient === false) {
52
+ throw new Error('Remote arrange=false and orient=false are unsupported by the upstream multipart API.');
53
+ }
54
+ const plate = options.slicePlate ?? 1;
55
+ if (!Number.isInteger(plate) || plate < 1)
56
+ throw new Error('Remote slicing requires one positive plate number; slicing all plates is unsupported.');
57
+ return plate;
58
+ }
59
+ /** The remote API forwards this display name to Orca's --curr-bed-type. */
60
+ function remoteBedType(value) {
61
+ const names = {
62
+ textured_plate: 'Textured PEI Plate', textured_pei_plate: 'Textured PEI Plate',
63
+ cool_plate: 'Cool Plate', engineering_plate: 'Engineering Plate',
64
+ hot_plate: 'High Temp Plate', high_temp_plate: 'High Temp Plate',
65
+ };
66
+ const key = typeof value === 'string' ? value.trim().toLowerCase().replace(/[\s-]+/g, '_') : '';
67
+ if (typeof names[key] !== 'string')
68
+ throw new Error('Unsupported remote bed type; use textured_plate, cool_plate, engineering_plate, or hot_plate.');
69
+ return names[key];
70
+ }
71
+ async function readResponse(response, limit) {
72
+ const size = Number(response.headers.get('content-length'));
73
+ if (size > limit) {
74
+ await response.body?.cancel();
75
+ throw new Error('Remote slicing response exceeds the size limit.');
76
+ }
77
+ if (!response.body)
78
+ throw new Error('Remote slicing returned an empty response.');
79
+ const reader = response.body.getReader();
80
+ const chunks = [];
81
+ let length = 0;
82
+ try {
83
+ for (;;) {
84
+ const { value, done } = await reader.read();
85
+ if (done)
86
+ break;
87
+ length += value.byteLength;
88
+ if (length > limit)
89
+ throw new Error('Remote slicing response exceeds the size limit.');
90
+ chunks.push(value);
91
+ }
92
+ }
93
+ finally {
94
+ await reader.cancel();
95
+ }
96
+ return Buffer.concat(chunks);
97
+ }
98
+ async function readJob(response) {
99
+ try {
100
+ const job = JSON.parse((await readResponse(response, 65536)).toString('utf8'));
101
+ if (job && typeof job === 'object' && !Array.isArray(job))
102
+ return job;
103
+ }
104
+ catch { /* Do not echo an untrusted response, which may contain credentials. */ }
105
+ throw new Error('Remote slicing returned invalid job metadata.');
106
+ }
107
+ /** Upload only locally prepared profiles, then inspect the exact downloaded 3MF. */
108
+ export async function sliceWithOrcaApi(input) {
109
+ const options = input.options ?? {};
110
+ const plate = validateRemoteSliceOptions(options);
111
+ if (input.filamentPaths.length !== 1 || (options.filamentColours && options.filamentColours.length !== 1)) {
112
+ throw new Error('orca-slicer-api supports exactly one filament slot; multi-slot projects require a local slicer.');
113
+ }
114
+ const config = configuration();
115
+ const machine = JSON.parse(await fs.readFile(input.machinePath, 'utf8'));
116
+ const filament = JSON.parse(await fs.readFile(input.filamentPaths[0], 'utf8'));
117
+ const processProfile = JSON.parse(await fs.readFile(input.processPath, 'utf8'));
118
+ const bedType = remoteBedType(options.bedType ?? processProfile.curr_bed_type ?? 'textured_plate');
119
+ const model = normalizeModel(machine.printer_model);
120
+ const nozzles = Array.isArray(machine.nozzle_diameter) ? machine.nozzle_diameter.map(Number) : [];
121
+ const materialValues = Array.isArray(filament.filament_type) ? filament.filament_type : [];
122
+ const materials = materialValues.map(normalizeMaterial);
123
+ if (!model || !nozzles.length || nozzles.some((n) => ![0.2, 0.4, 0.6, 0.8].includes(n)) ||
124
+ materials.length !== 1 || !materials[0]) {
125
+ throw new Error('Remote slicing requires explicit model, nozzle and single-material metadata in the prepared profiles.');
126
+ }
127
+ const controller = new AbortController();
128
+ let timedOut = false;
129
+ const deadline = setTimeout(() => { timedOut = true; controller.abort(); }, config.timeout);
130
+ const cancelCheck = setInterval(() => { if (input.isCancelled())
131
+ controller.abort(); }, 100);
132
+ let requestId;
133
+ let terminalJob = false;
134
+ let wroteOutput = false;
135
+ const request = async (route, init = {}) => {
136
+ if (input.isCancelled())
137
+ controller.abort();
138
+ // Do not pass multipart data to fetch after cancellation: undici may start
139
+ // its body producer before checking the signal and close it mid-enqueue.
140
+ controller.signal.throwIfAborted();
141
+ let response;
142
+ try {
143
+ response = await fetch(config.base + route, { ...init, headers: config.headers, signal: controller.signal, redirect: 'manual' });
144
+ }
145
+ catch {
146
+ throw new Error('Remote slicing HTTP request failed; check the trusted service configuration.');
147
+ }
148
+ if (!response.ok) {
149
+ await response.body?.cancel();
150
+ throw new Error(`Remote slicing HTTP ${response.status}.`);
151
+ }
152
+ return response;
153
+ };
154
+ try {
155
+ const ext = path.extname(input.inputPath).toLowerCase();
156
+ if (!['.stl', '.3mf'].includes(ext))
157
+ throw new Error('Remote slicing supports only STL or 3MF input.');
158
+ const bytes = await fs.readFile(input.inputPath);
159
+ if (bytes.length > MAX_BYTES)
160
+ throw new Error('Remote slicing input exceeds the 100 MB upstream limit.');
161
+ const form = new FormData();
162
+ form.set('file', new Blob([new Uint8Array(bytes)], { type: ext === '.3mf' ? 'model/3mf' : 'model/stl' }), `model${ext}`);
163
+ for (const [field, filePath, type] of [
164
+ ['printerProfile', input.machinePath, 'machine'],
165
+ ['presetProfile', input.processPath, 'process'],
166
+ ['filamentProfile', input.filamentPaths[0], 'filament'],
167
+ ]) {
168
+ const profile = JSON.parse(await fs.readFile(filePath, 'utf8'));
169
+ // The remote validator requires these metadata fields even for standalone
170
+ // user profiles. They do not change the resolved machine/process settings.
171
+ const data = { ...profile, type, from: profile.from || 'User' };
172
+ form.set(field, new Blob([JSON.stringify(data)], { type: 'application/json' }), `${field}.json`);
173
+ }
174
+ form.set('exportType', '3mf');
175
+ form.set('plate', String(plate));
176
+ form.set('bedType', bedType);
177
+ form.set('resolveProfileInheritance', 'false');
178
+ if (options.arrange === true)
179
+ form.set('arrange', 'true');
180
+ if (options.orient === true)
181
+ form.set('orient', 'true');
182
+ let response = await request(config.mode === 'async' ? '/slice-async' : '/slice', { method: 'POST', body: form });
183
+ if (config.mode === 'async') {
184
+ const submitted = await readJob(response);
185
+ if (typeof submitted.requestId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(submitted.requestId)) {
186
+ throw new Error('Remote slicing returned an invalid request ID.');
187
+ }
188
+ requestId = submitted.requestId;
189
+ for (;;) {
190
+ // Construct the documented same-service paths. Returned URLs cannot
191
+ // redirect authenticated requests to an arbitrary origin or route.
192
+ const job = await readJob(await request(`/slice-async/${requestId}`));
193
+ if (job.status === 'completed' || job.status === 'failed') {
194
+ terminalJob = true;
195
+ if (job.status === 'failed')
196
+ throw new Error('Remote slicing job failed; inspect the service logs.');
197
+ break;
198
+ }
199
+ if (job.status !== 'pending' && job.status !== 'processing')
200
+ throw new Error('Remote slicing returned an unknown job state.');
201
+ await delay(500, undefined, { signal: controller.signal });
202
+ }
203
+ response = await request(`/slice-async/${requestId}/result`);
204
+ }
205
+ const result = await readResponse(response, MAX_BYTES);
206
+ await fs.writeFile(input.outputPath, result, { flag: 'wx', mode: 0o600 });
207
+ wroteOutput = true;
208
+ const inspection = await inspectPrintFile(input.outputPath, {
209
+ model, nozzleDiameters: nozzles, plateIndex: plate - 1, bedType,
210
+ });
211
+ if (JSON.stringify(inspection.materials) !== JSON.stringify(materials)) {
212
+ throw new Error('Remote sliced file materials contradict the prepared filament profile.');
213
+ }
214
+ if (input.isCancelled())
215
+ throw new Error('Remote slicing cancelled.');
216
+ return input.outputPath;
217
+ }
218
+ catch (error) {
219
+ if (wroteOutput)
220
+ await fs.rm(input.outputPath, { force: true });
221
+ if (controller.signal.aborted) {
222
+ throw new Error(`Remote slicing ${timedOut ? 'timed out' : 'cancelled'}; the remote job may continue. No retry was sent.`);
223
+ }
224
+ throw error;
225
+ }
226
+ finally {
227
+ // DELETE only removes finished jobs; upstream has no active-job cancellation.
228
+ // Cleanup never retries submission or masks a valid result / original error.
229
+ if (requestId && terminalJob && !controller.signal.aborted) {
230
+ try {
231
+ await (await request(`/slice-async/${requestId}`, { method: 'DELETE' })).body?.cancel();
232
+ }
233
+ catch {
234
+ console.error('Remote slicing finished-job cleanup failed; service retention applies.');
235
+ }
236
+ }
237
+ clearTimeout(deadline);
238
+ clearInterval(cancelCheck);
239
+ }
240
+ }
@@ -22,7 +22,7 @@ export type TransformationParams = {
22
22
  relative?: boolean;
23
23
  selectionBounds?: THREE.Box3;
24
24
  };
25
- export declare const SLICER_TYPES: readonly ["bambustudio", "orcaslicer", "orcaslicer-bambulab", "prusaslicer", "cura", "slic3r"];
25
+ export declare const SLICER_TYPES: readonly ["bambustudio", "orcaslicer", "orcaslicer-bambulab", "orca-slicer-api", "prusaslicer", "cura", "slic3r"];
26
26
  export type SlicerType = typeof SLICER_TYPES[number];
27
27
  export declare function normalizeSlicerType(rawSlicerType: string): SlicerType;
28
28
  export declare function isBambuCompatibleSlicer(slicerType: SlicerType): boolean;
@@ -39,6 +39,7 @@ export interface BambuSliceOptions {
39
39
  filamentColours?: string[];
40
40
  bedType?: string;
41
41
  nozzleType?: string;
42
+ nozzleVolumeType?: 'Standard' | 'High Flow';
42
43
  enableTimelapse?: boolean;
43
44
  allowMixTemp?: boolean;
44
45
  scale?: number;
@@ -11,6 +11,7 @@ import * as crypto from 'crypto';
11
11
  import { execFile } from 'child_process';
12
12
  import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
13
13
  import { profileJsonFiles } from '../slicer/profile-files.js';
14
+ import { sliceWithOrcaApi, validateRemoteSliceOptions } from '../slicer/remote-slicer.js';
14
15
  const readFileAsync = promisify(fs.readFile);
15
16
  const writeFileAsync = promisify(fs.writeFile);
16
17
  const BAMBU_PROFILE_ROOTS = [
@@ -27,6 +28,7 @@ export const SLICER_TYPES = [
27
28
  'bambustudio',
28
29
  'orcaslicer',
29
30
  'orcaslicer-bambulab',
31
+ 'orca-slicer-api',
30
32
  'prusaslicer',
31
33
  'cura',
32
34
  'slic3r',
@@ -48,6 +50,8 @@ const SLICER_TYPE_ALIASES = {
48
50
  orcastudio: 'orcaslicer-bambulab',
49
51
  'orcaslicer-bmcu': 'orcaslicer-bambulab',
50
52
  'orca-bmcu': 'orcaslicer-bambulab',
53
+ 'orca-slicer-api': 'orca-slicer-api',
54
+ 'orcaslicer-api': 'orca-slicer-api',
51
55
  prusaslicer: 'prusaslicer',
52
56
  'prusa-slicer': 'prusaslicer',
53
57
  cura: 'cura',
@@ -65,7 +69,8 @@ export function normalizeSlicerType(rawSlicerType) {
65
69
  export function isBambuCompatibleSlicer(slicerType) {
66
70
  return (slicerType === 'bambustudio' ||
67
71
  slicerType === 'orcaslicer' ||
68
- slicerType === 'orcaslicer-bambulab');
72
+ slicerType === 'orcaslicer-bambulab' ||
73
+ slicerType === 'orca-slicer-api');
69
74
  }
70
75
  function isPathLikeExecutable(slicerPath) {
71
76
  return (path.isAbsolute(slicerPath) ||
@@ -405,6 +410,7 @@ export class STLManipulator extends EventEmitter {
405
410
  userProfileRoots: configuredBambuProfileDirs(),
406
411
  tempDir: this.tempDir,
407
412
  bedType: this.resolveBambuStudioBedType(bambuOptions?.bedType),
413
+ nozzleVolumeType: bambuOptions?.nozzleVolumeType,
408
414
  filamentColours: bambuOptions?.filamentColours ?? bundle.filamentColours,
409
415
  projectTowerPosition: bundle.projectTowerPosition,
410
416
  sourceProfiles: {
@@ -1224,7 +1230,7 @@ export class STLManipulator extends EventEmitter {
1224
1230
  async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset, bambuOptions) {
1225
1231
  const operationId = this.generateOperationId();
1226
1232
  this.activeOperations.set(operationId, true);
1227
- if (isPathLikeExecutable(slicerPath) && !fs.existsSync(slicerPath)) {
1233
+ if (slicerType !== 'orca-slicer-api' && isPathLikeExecutable(slicerPath) && !fs.existsSync(slicerPath)) {
1228
1234
  throw new Error(`Slicer executable not found at: ${slicerPath}`);
1229
1235
  }
1230
1236
  if (slicerProfile && !fs.existsSync(slicerProfile)) {
@@ -1266,6 +1272,7 @@ export class STLManipulator extends EventEmitter {
1266
1272
  case 'bambustudio':
1267
1273
  case 'orcaslicer':
1268
1274
  case 'orcaslicer-bambulab':
1275
+ case 'orca-slicer-api':
1269
1276
  // Bambu-compatible CLI: slice and export as 3MF with embedded gcode.
1270
1277
  // FULU's OrcaSlicer-bambulab fork keeps this Bambu-style path.
1271
1278
  // For 3MF input: slice in place and export sliced 3MF
@@ -1273,10 +1280,19 @@ export class STLManipulator extends EventEmitter {
1273
1280
  {
1274
1281
  const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
1275
1282
  const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
1276
- const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
1283
+ const remote = slicerType === 'orca-slicer-api';
1284
+ if (remote) {
1285
+ validateRemoteSliceOptions(bambuOptions);
1286
+ if (!process.env.BAMBU_PROFILES_ROOT)
1287
+ throw new Error('Remote slicing requires BAMBU_PROFILES_ROOT pointing to the matching Orca profile tree (a read-only mount is sufficient).');
1288
+ if (slicerProfile && !fs.existsSync(slicerProfile))
1289
+ throw new Error('Remote slicing process profile does not exist.');
1290
+ }
1291
+ const profileSlicerType = remote ? 'orcaslicer' : slicerType;
1292
+ const bambuOutputPath = path.join(this.tempDir, outputBase + (remote ? `_${operationId}` : '') + '_sliced.3mf');
1277
1293
  const outputDir = path.dirname(bambuOutputPath);
1278
- const profilesRoot = detectProfilesRoot(slicerPath, slicerType);
1279
- const rawBundle = await this.resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, profilesRoot);
1294
+ const profilesRoot = detectProfilesRoot(remote ? undefined : slicerPath, profileSlicerType);
1295
+ const rawBundle = await this.resolveBambuLikeSettingsBundle(outputBase, profileSlicerType, slicerProfile, printerPreset, bambuOptions, profilesRoot);
1280
1296
  // Resolve every BBL dependency before launching the slicer.
1281
1297
  const settingsBundle = await this.maybeFlattenBundle(await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, profilesRoot);
1282
1298
  // The machine preset assumes the stock hotend; record the installed one so
@@ -1296,7 +1312,7 @@ export class STLManipulator extends EventEmitter {
1296
1312
  // absolute-extrusion normalization after resolving those ancestors.
1297
1313
  // The machine owns the extrusion mode and layer G-code once process
1298
1314
  // files lose machine keys, so normalize it there as well.
1299
- if (slicerType === 'orcaslicer' && settingsBundle.settingsArg) {
1315
+ if (profileSlicerType === 'orcaslicer' && settingsBundle.settingsArg) {
1300
1316
  const [machinePath, processPath] = settingsBundle.settingsArg.split(';');
1301
1317
  settingsBundle.settingsArg = [
1302
1318
  this.writeTempJson(outputBase, 'machine_orca_resolved', this.useAbsoluteExtrusionForOrca(this.readJsonFile(machinePath))),
@@ -1304,6 +1320,17 @@ export class STLManipulator extends EventEmitter {
1304
1320
  ].join(';');
1305
1321
  }
1306
1322
  const cliBundle = this.isolateMachineSettings(settingsBundle);
1323
+ if (remote) {
1324
+ const [machinePath, processPath] = cliBundle.settingsArg?.split(';') ?? [];
1325
+ if (!machinePath || !processPath)
1326
+ throw new Error('Remote slicing requires complete prepared machine and process profiles.');
1327
+ outputFilePath = await sliceWithOrcaApi({
1328
+ inputPath: stlFilePath, machinePath, processPath,
1329
+ filamentPaths: cliBundle.filamentPaths, outputPath: bambuOutputPath,
1330
+ options: bambuOptions, isCancelled: () => !this.activeOperations.get(operationId),
1331
+ });
1332
+ break;
1333
+ }
1307
1334
  args = [
1308
1335
  '--slice', String(bambuOptions?.slicePlate ?? 0),
1309
1336
  '--outputdir', outputDir,
@@ -1359,45 +1386,47 @@ export class STLManipulator extends EventEmitter {
1359
1386
  default:
1360
1387
  throw new Error(`Unsupported slicer type: ${slicerType}`);
1361
1388
  }
1362
- if (progressCallback)
1363
- progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
1364
- console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
1365
- // Execute the slicer
1366
- await new Promise((resolve, reject) => {
1367
- const process = execFile(slicerPath, args, (error, stdout, stderr) => {
1368
- if (error) {
1369
- console.error(`Slicer Error: ${error.message}`);
1370
- console.error(`Slicer Stderr: ${stderr}`);
1371
- reject(new Error(`Slicer failed: ${error.message}. Stderr: ${stderr}`));
1372
- }
1373
- else {
1374
- console.error(`Slicer Stdout: ${stdout}`);
1375
- if (stderr) {
1376
- console.warn(`Slicer Stderr: ${stderr}`); // Log stderr even on success
1389
+ if (slicerType !== 'orca-slicer-api') {
1390
+ if (progressCallback)
1391
+ progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
1392
+ console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
1393
+ // Execute the slicer
1394
+ await new Promise((resolve, reject) => {
1395
+ const process = execFile(slicerPath, args, (error, stdout, stderr) => {
1396
+ if (error) {
1397
+ console.error(`Slicer Error: ${error.message}`);
1398
+ console.error(`Slicer Stderr: ${stderr}`);
1399
+ reject(new Error(`Slicer failed: ${error.message}. Stderr: ${stderr}`));
1377
1400
  }
1378
- resolve();
1379
- }
1380
- });
1381
- // Optional: Add listeners for stdout/stderr for real-time progress if slicer provides it
1382
- // process.stdout?.on('data', (data) => { console.error(`Slicer stdout: ${data}`); });
1383
- // process.stderr?.on('data', (data) => { console.error(`Slicer stderr: ${data}`); });
1384
- // Check for cancellation periodically
1385
- const checkCancel = setInterval(() => {
1386
- if (!this.activeOperations.get(operationId)) {
1387
- clearInterval(checkCancel);
1388
- try {
1389
- process.kill(); // Attempt to kill the slicer process
1390
- reject(new Error("Slicing operation cancelled"));
1401
+ else {
1402
+ console.error(`Slicer Stdout: ${stdout}`);
1403
+ if (stderr) {
1404
+ console.warn(`Slicer Stderr: ${stderr}`); // Log stderr even on success
1405
+ }
1406
+ resolve();
1391
1407
  }
1392
- catch (killError) {
1393
- console.error("Error attempting to kill slicer process:", killError);
1394
- reject(new Error("Slicing operation cancelled, but failed to kill process."));
1408
+ });
1409
+ // Optional: Add listeners for stdout/stderr for real-time progress if slicer provides it
1410
+ // process.stdout?.on('data', (data) => { console.error(`Slicer stdout: ${data}`); });
1411
+ // process.stderr?.on('data', (data) => { console.error(`Slicer stderr: ${data}`); });
1412
+ // Check for cancellation periodically
1413
+ const checkCancel = setInterval(() => {
1414
+ if (!this.activeOperations.get(operationId)) {
1415
+ clearInterval(checkCancel);
1416
+ try {
1417
+ process.kill(); // Attempt to kill the slicer process
1418
+ reject(new Error("Slicing operation cancelled"));
1419
+ }
1420
+ catch (killError) {
1421
+ console.error("Error attempting to kill slicer process:", killError);
1422
+ reject(new Error("Slicing operation cancelled, but failed to kill process."));
1423
+ }
1395
1424
  }
1396
- }
1397
- }, 500);
1398
- process.on('exit', () => clearInterval(checkCancel));
1399
- process.on('error', () => clearInterval(checkCancel)); // Ensure interval cleared on process error too
1400
- });
1425
+ }, 500);
1426
+ process.on('exit', () => clearInterval(checkCancel));
1427
+ process.on('error', () => clearInterval(checkCancel)); // Ensure interval cleared on process error too
1428
+ });
1429
+ }
1401
1430
  if (!this.activeOperations.get(operationId)) {
1402
1431
  // This check might be redundant if the promise rejected on kill, but good safety
1403
1432
  throw new Error("Slicing operation cancelled after process finished");