mcp-3d-printer-server 1.2.13 → 1.2.15
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 +31 -0
- package/CONTRIBUTORS.md +2 -0
- package/README.md +2 -2
- package/dist/fulu-orca.d.ts +8 -0
- package/dist/fulu-orca.js +107 -10
- package/dist/index.js +3 -3
- package/dist/printers/bambu.js +11 -4
- package/dist/safety/confirmation.js +9 -2
- package/dist/safety/printer-state.d.ts +2 -0
- package/dist/safety/printer-state.js +42 -1
- package/dist/slicer/bambu-cli.d.ts +21 -1
- package/dist/slicer/bambu-cli.js +86 -11
- package/dist/slicer/run-slicer.d.ts +2 -0
- package/dist/slicer/run-slicer.js +3 -2
- package/dist/stl/stl-manipulator.d.ts +8 -0
- package/dist/stl/stl-manipulator.js +31 -2
- package/dist/test/stl-manipulator-test.js +9 -3
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/fulu-orca.ts +102 -10
- package/src/index.ts +3 -3
- package/src/printers/bambu.ts +11 -3
- package/src/safety/confirmation.ts +8 -2
- package/src/safety/printer-state.ts +42 -1
- package/src/slicer/bambu-cli.ts +92 -7
- package/src/slicer/run-slicer.ts +5 -2
- package/src/stl/stl-manipulator.ts +29 -1
- package/src/test/stl-manipulator-test.ts +9 -3
package/src/slicer/bambu-cli.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface BambuSliceOptions {
|
|
|
33
33
|
orient?: boolean; // --orient 1: auto-orient for printability
|
|
34
34
|
arrange?: boolean; // --arrange 1: auto-arrange objects on plate
|
|
35
35
|
ensureOnBed?: boolean; // --ensure-on-bed: lift floating models onto bed
|
|
36
|
-
cloneObjects?: string; // --clone-objects
|
|
36
|
+
cloneObjects?: string; // --clone-objects N: total copies of the loaded model
|
|
37
37
|
skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
|
|
38
38
|
loadFilaments?: string; // "f1.json;f2.json": positional filament profiles
|
|
39
39
|
loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
|
|
@@ -567,12 +567,91 @@ const PROFILE_METADATA_KEYS = new Set([
|
|
|
567
567
|
"compatible_printers", "compatible_printers_condition", "compatible_prints", "compatible_prints_condition",
|
|
568
568
|
]);
|
|
569
569
|
|
|
570
|
+
/** How the CLI receives the model: Orca STL transforms are applied to a mesh copy first. */
|
|
571
|
+
export interface BambuCliInput {
|
|
572
|
+
slicerType: BambuCliSlicerType;
|
|
573
|
+
is3mf: boolean;
|
|
574
|
+
/** scale and rotations are already applied to the mesh passed to the CLI. */
|
|
575
|
+
meshTransformed: boolean;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Decide how the CLI receives the model. OrcaSlicer's CLI (upstream and FULU)
|
|
580
|
+
* crashes with SIGSEGV in PartPlate::check_outside, which needs the GUI's
|
|
581
|
+
* Plater, when --scale or --rotate-x/-y move an object below the bed while it
|
|
582
|
+
* arranges, the default for STL input. An Orca STL's transforms are therefore
|
|
583
|
+
* applied to a mesh copy; combinations the CLI cannot handle are refused.
|
|
584
|
+
*/
|
|
585
|
+
export function planBambuCliInput(slicerType: BambuCliSlicerType, is3mf: boolean, options: BambuSliceOptions): BambuCliInput {
|
|
586
|
+
const input = { slicerType, is3mf, meshTransformed: false };
|
|
587
|
+
if (slicerType === "bambustudio") return input;
|
|
588
|
+
const tilts = !!options.rotateX || !!options.rotateY;
|
|
589
|
+
if (is3mf) {
|
|
590
|
+
if (options.arrange === true && ((options.scale ?? 1) > 1 || tilts)) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
"OrcaSlicer's CLI crashes when scale above 1, rotate_x or rotate_y moves a 3MF object below the bed while arranging. " +
|
|
593
|
+
"Set arrange to false, or apply the transform in the slicer and export the project."
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return input;
|
|
597
|
+
}
|
|
598
|
+
if (options.repetitions !== undefined && options.cloneObjects) {
|
|
599
|
+
throw new Error("Use either repetitions or clone_objects for an STL with OrcaSlicer, not both.");
|
|
600
|
+
}
|
|
601
|
+
return { ...input, meshTransformed: options.scale !== undefined || tilts };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Bambu Studio and OrcaSlicer CLI errors (libslic3r/Utils.hpp). The process
|
|
606
|
+
* exits with the low byte of the code; on Linux result.json names the error.
|
|
607
|
+
*/
|
|
608
|
+
const BAMBU_CLI_ERRORS: Record<number, string> = {
|
|
609
|
+
[-2]: "invalid parameters to the slicer",
|
|
610
|
+
[-3]: "input files not found",
|
|
611
|
+
[-5]: "preset file cannot be parsed",
|
|
612
|
+
[-6]: "model file cannot be parsed",
|
|
613
|
+
[-9]: "copying objects failed",
|
|
614
|
+
[-17]: "printer is not compatible with the process preset",
|
|
615
|
+
[-21]: "auto-arranging objects failed",
|
|
616
|
+
[-22]: "auto-orienting objects failed",
|
|
617
|
+
[-50]: "a plate is empty or has no object fully inside it; arrange the model or place it on the plate",
|
|
618
|
+
[-51]: "incorrect slicing parameters",
|
|
619
|
+
[-52]: "objects extend over the bed boundary",
|
|
620
|
+
[-60]: "no printable objects remain after skipping",
|
|
621
|
+
[-61]: "filaments are not compatible with the plate type",
|
|
622
|
+
[-62]: "the filaments' temperatures differ too much",
|
|
623
|
+
[-63]: "objects collide in print-by-object mode",
|
|
624
|
+
[-64]: "objects collide",
|
|
625
|
+
[-100]: "slicing the model failed",
|
|
626
|
+
[-101]: "G-code conflicts after slicing",
|
|
627
|
+
[-102]: "G-code in an unprintable area",
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* The CLI's own reason for a failed run: result.json in the run's fresh output
|
|
632
|
+
* directory (written on Linux for handled errors), else the exit-code table.
|
|
633
|
+
*/
|
|
634
|
+
export function bambuCliFailureReason(exitCode: number | null, outputDir: string): string | undefined {
|
|
635
|
+
try {
|
|
636
|
+
const result = JSON.parse(fs.readFileSync(path.join(outputDir, "result.json"), "utf8"));
|
|
637
|
+
if (typeof result?.error_string === "string" && result.error_string.trim()) {
|
|
638
|
+
return `${result.error_string.trim()}${result.return_code !== undefined ? ` [${result.return_code}]` : ""}`;
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
// result.json is written only on Linux and only for handled errors.
|
|
642
|
+
}
|
|
643
|
+
if (exitCode === null) return undefined;
|
|
644
|
+
const code = Object.keys(BAMBU_CLI_ERRORS).map(Number).find((value) => value === exitCode || (value & 0xff) === exitCode);
|
|
645
|
+
return code === undefined ? undefined : `${BAMBU_CLI_ERRORS[code]} [${code}]`;
|
|
646
|
+
}
|
|
647
|
+
|
|
570
648
|
/** CLI flags for the prepared bundle and caller options (input path appended by the caller). */
|
|
571
649
|
export function buildBambuCliArgs(
|
|
572
650
|
bundle: BambuSettingsBundle,
|
|
573
651
|
outputDir: string,
|
|
574
652
|
exportName: string,
|
|
575
|
-
options: BambuSliceOptions
|
|
653
|
+
options: BambuSliceOptions,
|
|
654
|
+
input: BambuCliInput
|
|
576
655
|
): string[] {
|
|
577
656
|
const args = [
|
|
578
657
|
"--slice", String(options.slicePlate ?? 0),
|
|
@@ -590,11 +669,17 @@ export function buildBambuCliArgs(
|
|
|
590
669
|
if (options.skipModifiedGcodes) args.push("--skip-modified-gcodes");
|
|
591
670
|
if (options.orient !== undefined) args.push("--orient", options.orient ? "1" : "0");
|
|
592
671
|
if (options.arrange !== undefined) args.push("--arrange", options.arrange ? "1" : "0");
|
|
593
|
-
if (options.repetitions !== undefined)
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
672
|
+
if (options.repetitions !== undefined) {
|
|
673
|
+
// OrcaSlicer rejects --repetitions for one STL; it copies the loaded model with --clone-objects.
|
|
674
|
+
if (input.slicerType !== "bambustudio" && !input.is3mf) args.push("--clone-objects", String(options.repetitions));
|
|
675
|
+
else args.push("--repetitions", String(options.repetitions));
|
|
676
|
+
}
|
|
677
|
+
if (!input.meshTransformed) {
|
|
678
|
+
if (options.scale !== undefined) args.push("--scale", String(options.scale));
|
|
679
|
+
if (options.rotate !== undefined) args.push("--rotate", String(options.rotate));
|
|
680
|
+
if (options.rotateX !== undefined) args.push("--rotate-x", String(options.rotateX));
|
|
681
|
+
if (options.rotateY !== undefined) args.push("--rotate-y", String(options.rotateY));
|
|
682
|
+
}
|
|
598
683
|
if (options.cloneObjects) args.push("--clone-objects", options.cloneObjects);
|
|
599
684
|
if (options.skipObjects) args.push("--skip-objects", options.skipObjects);
|
|
600
685
|
if (bundle.filamentPaths.length > 0) {
|
package/src/slicer/run-slicer.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface SlicerRunOptions {
|
|
|
11
11
|
slicerType: string;
|
|
12
12
|
/** Polled while the slicer runs; returning true kills it. */
|
|
13
13
|
isCancelled?: () => boolean;
|
|
14
|
+
/** The slicer's own reason for a nonzero exit, read before its output directory is removed. */
|
|
15
|
+
failureReason?: (exitCode: number | null) => string | undefined;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface SlicerRunResult {
|
|
@@ -51,7 +53,7 @@ function describeOutput(stdoutTail: string, stderrTail: string): string {
|
|
|
51
53
|
* cancellation rejects with a SlicerError that includes the evidence.
|
|
52
54
|
*/
|
|
53
55
|
export function runSlicerProcess(options: SlicerRunOptions): Promise<SlicerRunResult> {
|
|
54
|
-
const { command, args, timeoutMs, slicerType, isCancelled } = options;
|
|
56
|
+
const { command, args, timeoutMs, slicerType, isCancelled, failureReason } = options;
|
|
55
57
|
return new Promise((resolve, reject) => {
|
|
56
58
|
const stdout = new TailBuffer();
|
|
57
59
|
const stderr = new TailBuffer();
|
|
@@ -107,9 +109,10 @@ export function runSlicerProcess(options: SlicerRunOptions): Promise<SlicerRunRe
|
|
|
107
109
|
: signal
|
|
108
110
|
? `Slicer was terminated by signal ${signal}`
|
|
109
111
|
: `Slicer exited with code ${code}`;
|
|
112
|
+
const cliReason = timedOut || cancelled ? undefined : failureReason?.(code);
|
|
110
113
|
settle(() => reject(new SlicerError(
|
|
111
114
|
"execution",
|
|
112
|
-
`${reason} (${slicerType}: ${command}).\n${describeOutput(stdoutTail, stderrTail)}`,
|
|
115
|
+
`${reason} (${slicerType}: ${command})${cliReason ? `: ${cliReason}` : ""}.\n${describeOutput(stdoutTail, stderrTail)}`,
|
|
113
116
|
{
|
|
114
117
|
slicerType,
|
|
115
118
|
slicerPath: command,
|
|
@@ -11,7 +11,9 @@ import { detectProfilesRoot } from '../slicer/profile-flatten.js';
|
|
|
11
11
|
import {
|
|
12
12
|
BambuCliProfilePreparer,
|
|
13
13
|
assertSlicedProjectOutput,
|
|
14
|
+
bambuCliFailureReason,
|
|
14
15
|
buildBambuCliArgs,
|
|
16
|
+
planBambuCliInput,
|
|
15
17
|
type BambuCliSlicerType,
|
|
16
18
|
type BambuSliceOptions,
|
|
17
19
|
} from '../slicer/bambu-cli.js';
|
|
@@ -182,6 +184,27 @@ export class STLManipulator extends EventEmitter {
|
|
|
182
184
|
return timeoutMs;
|
|
183
185
|
}
|
|
184
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Apply slice_stl's scale and rotations to a copy of the mesh as OrcaSlicer's
|
|
189
|
+
* CLI would, about the model's centre. Orca adds each --rotate* to the
|
|
190
|
+
* object's Euler angles (ModelVolume::rotate), so the result rotates about X,
|
|
191
|
+
* then Y, then Z whatever the argument order, after scaling. The slicer then
|
|
192
|
+
* places the result on the plate.
|
|
193
|
+
*/
|
|
194
|
+
private async transformMeshForCli(stlFilePath: string, options: BambuSliceOptions, outputDir: string): Promise<string> {
|
|
195
|
+
const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
|
|
196
|
+
const center = new THREE.Vector3();
|
|
197
|
+
boundingBox.getCenter(center);
|
|
198
|
+
const radians = (degrees: number) => degrees * Math.PI / 180;
|
|
199
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
|
|
200
|
+
if (options.scale !== undefined) geometry.applyMatrix4(new THREE.Matrix4().makeScale(options.scale, options.scale, options.scale));
|
|
201
|
+
if (options.rotateX) geometry.applyMatrix4(new THREE.Matrix4().makeRotationX(radians(options.rotateX)));
|
|
202
|
+
if (options.rotateY) geometry.applyMatrix4(new THREE.Matrix4().makeRotationY(radians(options.rotateY)));
|
|
203
|
+
if (options.rotate) geometry.applyMatrix4(new THREE.Matrix4().makeRotationZ(radians(options.rotate)));
|
|
204
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(center.x, center.y, center.z));
|
|
205
|
+
return this.saveSTL(geometry, path.join(outputDir, `${path.basename(stlFilePath, path.extname(stlFilePath))}_transformed.stl`));
|
|
206
|
+
}
|
|
207
|
+
|
|
185
208
|
/**
|
|
186
209
|
* Load STL file and return geometry and bounding box
|
|
187
210
|
*/
|
|
@@ -1060,6 +1083,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1060
1083
|
}
|
|
1061
1084
|
options.loadFilaments = legacyFilaments;
|
|
1062
1085
|
}
|
|
1086
|
+
const cliInput = planBambuCliInput(cliType, is3mf, options);
|
|
1063
1087
|
const profilesRoot = detectProfilesRoot(slicerPath, cliType);
|
|
1064
1088
|
const preparer = new BambuCliProfilePreparer(path.join(this.tempDir, 'slicer-profiles'));
|
|
1065
1089
|
const bundle = await preparer.prepare(
|
|
@@ -1072,7 +1096,9 @@ export class STLManipulator extends EventEmitter {
|
|
|
1072
1096
|
options
|
|
1073
1097
|
);
|
|
1074
1098
|
bambuOutputDir = fs.mkdtempSync(path.join(this.tempDir, `${operationId}-bambu-output-`));
|
|
1075
|
-
|
|
1099
|
+
// The transformed copy lives in the run's output directory, which is removed afterwards.
|
|
1100
|
+
const inputPath = cliInput.meshTransformed ? await this.transformMeshForCli(stlFilePath, options, bambuOutputDir) : stlFilePath;
|
|
1101
|
+
args = [...buildBambuCliArgs(bundle, bambuOutputDir, exportName, options, cliInput), inputPath];
|
|
1076
1102
|
bambuExport = {
|
|
1077
1103
|
producedPath: path.join(bambuOutputDir, exportName),
|
|
1078
1104
|
// One folder per operation: concurrent slices of same-named inputs
|
|
@@ -1135,12 +1161,14 @@ export class STLManipulator extends EventEmitter {
|
|
|
1135
1161
|
if (progressCallback) progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
|
|
1136
1162
|
console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
|
|
1137
1163
|
|
|
1164
|
+
const cliOutputDir = bambuOutputDir;
|
|
1138
1165
|
await runSlicerProcess({
|
|
1139
1166
|
command: slicerPath,
|
|
1140
1167
|
args,
|
|
1141
1168
|
timeoutMs: this.resolveSlicerTimeoutMs(),
|
|
1142
1169
|
slicerType,
|
|
1143
1170
|
isCancelled: () => !this.activeOperations.get(operationId),
|
|
1171
|
+
failureReason: cliOutputDir ? (exitCode) => bambuCliFailureReason(exitCode, cliOutputDir) : undefined,
|
|
1144
1172
|
});
|
|
1145
1173
|
|
|
1146
1174
|
if (!this.activeOperations.get(operationId)) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { STLManipulator, ProgressCallback } from '../stl/stl-manipulator.js';
|
|
4
4
|
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
import * as THREE from 'three';
|
|
7
8
|
|
|
@@ -11,6 +12,9 @@ if (!fs.existsSync(TEST_DIR)) {
|
|
|
11
12
|
fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
// Operation outputs go to a scratch folder removed after the run
|
|
16
|
+
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'stl-manipulator-test-'));
|
|
17
|
+
|
|
14
18
|
// Path for a sample STL file
|
|
15
19
|
const sampleStlPath = path.join(TEST_DIR, 'sample_cube.stl');
|
|
16
20
|
|
|
@@ -48,8 +52,8 @@ async function testSTLManipulator() {
|
|
|
48
52
|
// Create a sample STL for testing
|
|
49
53
|
const stlPath = await createSampleCube();
|
|
50
54
|
|
|
51
|
-
//
|
|
52
|
-
const manipulator = new STLManipulator(
|
|
55
|
+
// Write operation outputs to a scratch folder, not the tracked test directory
|
|
56
|
+
const manipulator = new STLManipulator(outputDir);
|
|
53
57
|
|
|
54
58
|
// Listen for events from the manipulator
|
|
55
59
|
manipulator.on('operationComplete', (data) => {
|
|
@@ -116,7 +120,9 @@ async function testSTLManipulator() {
|
|
|
116
120
|
console.log('\nAll tests completed successfully!');
|
|
117
121
|
} catch (error) {
|
|
118
122
|
console.error('An error occurred during testing:', error);
|
|
119
|
-
process.
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
} finally {
|
|
125
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
|