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.
- package/CONTRIBUTORS.md +7 -1
- package/README.md +29 -10
- package/dist/ams-mapping.d.ts +18 -2
- package/dist/ams-mapping.js +36 -5
- package/dist/bambu-native.d.ts +55 -0
- package/dist/bambu-native.js +212 -13
- package/dist/index.js +154 -71
- package/dist/printers/bambu.d.ts +29 -2
- package/dist/printers/bambu.js +59 -28
- package/dist/safety/hidden-hms.d.ts +32 -0
- package/dist/safety/hidden-hms.js +405 -0
- package/dist/safety/limits.d.ts +6 -0
- package/dist/safety/limits.js +9 -0
- package/dist/safety/print-file.d.ts +2 -0
- package/dist/safety/print-file.js +83 -7
- package/dist/safety/printer-state.d.ts +11 -0
- package/dist/safety/printer-state.js +115 -22
- package/dist/slicer/profile-flatten.js +25 -0
- package/dist/slicer/remote-slicer.d.ts +12 -0
- package/dist/slicer/remote-slicer.js +240 -0
- package/dist/stl/stl-manipulator.d.ts +2 -1
- package/dist/stl/stl-manipulator.js +71 -42
- package/native/bambu-native-print.cpp +100 -26
- package/package.json +7 -5
- package/scripts/build-bambu-native.sh +53 -0
- package/scripts/setup-bambu-native.mjs +372 -0
- package/src/ams-mapping.ts +38 -5
- package/src/bambu-native.ts +229 -14
- package/src/index.ts +156 -67
- package/src/printers/bambu.ts +65 -25
- package/src/safety/hidden-hms.ts +406 -0
- package/src/safety/limits.ts +9 -0
- package/src/safety/print-file.ts +71 -7
- package/src/safety/printer-state.ts +117 -22
- package/src/slicer/profile-flatten.ts +32 -0
- package/src/slicer/remote-slicer.ts +216 -0
- package/src/stl/stl-manipulator.ts +70 -43
- 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
|
|
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,
|
|
1279
|
-
const rawBundle = await this.resolveBambuLikeSettingsBundle(outputBase,
|
|
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 (
|
|
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 (
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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");
|