mcp-3d-printer-server 1.2.28 → 1.2.30
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 +12 -0
- package/README.md +2 -0
- package/dist/bambu-network-bridge.d.ts +8 -2
- package/dist/bambu-network-bridge.js +25 -3
- package/dist/index.js +25 -10
- package/dist/slicer/print-quality.d.ts +26 -0
- package/dist/slicer/print-quality.js +53 -0
- package/dist/stl/stl-manipulator.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/bambu-network-bridge.ts +25 -3
- package/src/index.ts +25 -10
- package/src/slicer/print-quality.ts +55 -0
- package/src/stl/stl-manipulator.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.30] – 2026-11-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `slice_stl` and `slice_with_template` take `quality` (`draft`, `standard`, `fine`) and `strength` (`standard`, `strong`, `max`). The server turns them into the slicer's own settings: Draft and Fine set the layer height for the nozzle in use (0.28 and 0.12 mm on a 0.4 mm nozzle, scaled with it), Strong and Max set walls and infill (4 walls at 25%, 6 at 40%), and Standard leaves the resolved preset alone. An explicit `layer_height`, `wall_loops` or `sparse_infill_density` still wins. Clients such as Counterpart can now send the person's choice instead of carrying slicer numbers. Checked with stand-in slicers only. For PrusaSlicer, Slic3r and generic OrcaSlicer the layer height scales with `nozzle_diameter` (or `NOZZLE_DIAMETER`), which must match the profile's nozzle; Cura takes only Standard. Same change as bambu-printer-mcp 1.1.56.
|
|
8
|
+
|
|
9
|
+
## [1.2.29] – 2026-11-04
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- With no method given, a FULU print over Bambu Cloud goes the way Bambu Studio sends it to a cloud-bound printer it can reach on the LAN (`PrintJob.cpp`): `start_local_print_with_record`, which uploads to the printer over the LAN and records the task in the account, in the account's cloud session. When that fails before the print command (the plug-in's `-2010` to `-2140` preparation results, other than a missing or oversized file), the same checked job goes once through `start_print`, with the printer checked again first. Any other failure may have reached the printer and is not sent again. A printer that reports no storage goes straight to `start_print`, as in Studio. Same change as bambu-printer-mcp 1.1.55. On a P1S with firmware 01.10.00.00, `start_print` alone returns `-3120`, HTTP 403 "internal blocking", every time. Errors name `-2120`, `-2130` and `-3120`. Checked with the stand-in host and intercepted transports only; not yet on a printer.
|
|
14
|
+
|
|
3
15
|
## [1.2.28] – 2026-11-04
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -688,6 +688,8 @@ Options for the Bambu-compatible path:
|
|
|
688
688
|
| `scale`, `rotate`, `rotate_x`, `rotate_y` | Transform before slicing (uniform scale; rotations in degrees). For OrcaSlicer STL input the server applies them to a mesh copy in Orca's order to avoid a CLI crash; see [OrcaSlicer CLI limits](https://github.com/DMontgomery40/mcp-3D-printer-server/blob/main/docs/SLICING.md#bambu-compatible-slicing) |
|
|
689
689
|
| `enable_timelapse`, `allow_mix_temp` | Insert timelapse parking moves; allow filaments with different temperature needs on one plate |
|
|
690
690
|
|
|
691
|
+
`slice_stl` and `slice_with_template` take `quality` (`draft`, `standard`, `fine`) and `strength` (`standard`, `strong`, `max`), so a client can send the person's choice without slicer numbers. The server picks the layer height for the nozzle (0.28 and 0.12 mm on a 0.4 mm nozzle; it scales with `nozzle_diameter`, or `NOZZLE_DIAMETER` when that's not given, which for PrusaSlicer, Slic3r and generic OrcaSlicer must match the nozzle in `slicer_profile`) and the walls and infill (4 walls at 25%, 6 at 40%). Standard keeps the preset; explicit values win. Cura takes only Standard.
|
|
692
|
+
|
|
691
693
|
`layer_height` (mm), `wall_loops` (a whole number), and `sparse_infill_density` (such as `"15%"`) are written over the selected profile on every slicer: as the process keys `layer_height`, `wall_loops`, and `sparse_infill_density` for Bambu Studio and OrcaSlicer, and as `--layer-height`, `--perimeters`, and `--fill-density` for PrusaSlicer and Slic3r. They win over a template. Cura refuses them; set them in its definition file. Your profile files are never modified.
|
|
692
694
|
|
|
693
695
|
`enable_support` (true or false), `support_type` (`normal(auto)`, `tree(auto)`, `normal(manual)` or `tree(manual)`) and `support_threshold_angle` (0 to 90 degrees; 0 lets the slicer decide) are written over the selected process profile as the same-named Bambu Studio and OrcaSlicer keys. PrusaSlicer, Slic3r and Cura refuse them; set supports in their own profile.
|
|
@@ -186,10 +186,16 @@ export declare class BambuNetworkBridge {
|
|
|
186
186
|
* Shares the LAN session queue: one reader of the plug-in's events at a time.
|
|
187
187
|
*/
|
|
188
188
|
withCloudSession<T>(devId: string, options: BambuNetworkBridgeOptions, assertActive: () => void, timing: Readonly<CloudTiming>, run: (session: CloudSession) => Promise<T>): Promise<T>;
|
|
189
|
-
/**
|
|
190
|
-
|
|
189
|
+
/**
|
|
190
|
+
* A cloud print in a cloud session, so the plug-in has the printer's state. With a fallback this is
|
|
191
|
+
* Bambu Studio's order for a cloud-bound printer it can also reach on the LAN (PrintJob.cpp,
|
|
192
|
+
* SendAttemptRoute::LanWithRecord, then ::Cloud): start_local_print_with_record, then start_print
|
|
193
|
+
* when the first failed before its print command was sent. `firstFailure` is the first method's result.
|
|
194
|
+
*/
|
|
195
|
+
startCloudPrint(devId: string, method: string, payload: JsonObject, options?: BambuNetworkBridgeOptions, assertActive?: () => void, timing?: Readonly<CloudTiming>, fallbackMethod?: string): Promise<{
|
|
191
196
|
result: unknown;
|
|
192
197
|
jobUpdates: BridgeJobUpdate[];
|
|
198
|
+
firstFailure?: number;
|
|
193
199
|
}>;
|
|
194
200
|
/**
|
|
195
201
|
* A print control over Bambu Cloud: how Bambu Studio and FULU's slicer control a printer
|
|
@@ -21,8 +21,16 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 120000;
|
|
|
21
21
|
const LOCAL_CONNECT_STATUS = ["ok", "failed", "lost"];
|
|
22
22
|
// bambu_networking.hpp BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED.
|
|
23
23
|
const LOCAL_PRINT_PUBLISH_FAILED = -4030;
|
|
24
|
+
// A with-record send falls back to start_print only on its own preparation failures (the plug-in's
|
|
25
|
+
// PRINT_WR_* results, -2010 to -2140: project, upload, task record, FTPS), which come before the print
|
|
26
|
+
// command, so the printer has no job. Any other result may have reached the printer and is never
|
|
27
|
+
// replayed. Of those, Studio also stops on a file missing or too large (should_stop_without_fallback).
|
|
28
|
+
const WITH_RECORD_PREPARATION_FAILURE = (value) => value <= -2010 && value >= -2140 && value !== -2040 && value !== -2100;
|
|
24
29
|
// bambu_networking.hpp print errors that name a recoverable cause.
|
|
25
30
|
const BAMBU_NETWORK_PRINT_ERRORS = {
|
|
31
|
+
[-2120]: "Bambu Cloud refused to create the print task",
|
|
32
|
+
[-2130]: "FTPS upload to the printer failed",
|
|
33
|
+
[-3120]: "Bambu Cloud refused to create the print task",
|
|
26
34
|
[-4010]: "the file exceeds the 1 GB upload limit",
|
|
27
35
|
[-4020]: "FTPS upload to the printer failed",
|
|
28
36
|
[LOCAL_PRINT_PUBLISH_FAILED]: "the networking plug-in could not send the print command to the printer over MQTT",
|
|
@@ -703,9 +711,23 @@ export class BambuNetworkBridge {
|
|
|
703
711
|
release();
|
|
704
712
|
}
|
|
705
713
|
}
|
|
706
|
-
/**
|
|
707
|
-
|
|
708
|
-
|
|
714
|
+
/**
|
|
715
|
+
* A cloud print in a cloud session, so the plug-in has the printer's state. With a fallback this is
|
|
716
|
+
* Bambu Studio's order for a cloud-bound printer it can also reach on the LAN (PrintJob.cpp,
|
|
717
|
+
* SendAttemptRoute::LanWithRecord, then ::Cloud): start_local_print_with_record, then start_print
|
|
718
|
+
* when the first failed before its print command was sent. `firstFailure` is the first method's result.
|
|
719
|
+
*/
|
|
720
|
+
async startCloudPrint(devId, method, payload, options = {}, assertActive = () => { }, timing = CLOUD_PRINT_TIMING, fallbackMethod) {
|
|
721
|
+
return await this.withCloudSession(devId, options, assertActive, timing, async (session) => {
|
|
722
|
+
const result = await this.callWithAgent(method, payload, options);
|
|
723
|
+
const value = bridgeNumericResult(result);
|
|
724
|
+
if (!fallbackMethod || value === undefined || !WITH_RECORD_PREPARATION_FAILURE(value))
|
|
725
|
+
return { result, jobUpdates: session.jobUpdates };
|
|
726
|
+
options.trace?.({ step: "fallback", from: method, to: fallbackMethod, result: value });
|
|
727
|
+
const params = payload.params;
|
|
728
|
+
const fallbackPayload = params ? { ...payload, params: { ...params, comments: `failed(${value})` } } : payload;
|
|
729
|
+
return { result: await this.callWithAgent(fallbackMethod, fallbackPayload, options), jobUpdates: session.jobUpdates, firstFailure: value };
|
|
730
|
+
});
|
|
709
731
|
}
|
|
710
732
|
/**
|
|
711
733
|
* A print control over Bambu Cloud: how Bambu Studio and FULU's slicer control a printer
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import * as THREE from 'three';
|
|
|
19
19
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
20
20
|
import { STLManipulator, SUPPORT_TYPES } from "./stl/stl-manipulator.js";
|
|
21
21
|
import { parsePauseLayers, readPlateGcode, pausedLayers, machinePauseGcode } from "./slicer/layer-pauses.js";
|
|
22
|
+
import { PRINT_QUALITY_PROPERTIES, withPrintQuality } from "./slicer/print-quality.js";
|
|
22
23
|
import { CounterpartService } from "./counterpart/service.js";
|
|
23
24
|
import { CAMERA_STREAM_TOOLS } from "./counterpart/camera-tools.js";
|
|
24
25
|
import { annotateTools } from "./tool-annotations.js";
|
|
@@ -34,7 +35,7 @@ import { BambuNetworkBridge, describeBambuNetworkPrintError } from "./bambu-netw
|
|
|
34
35
|
import { PRINT_ROUTES, connectionForRoute, routeOf, routeOptions, writeEnvValues } from "./counterpart/route.js";
|
|
35
36
|
import { resolveStudioPrintOptions, usedNozzleCount } from "./studio-print-options.js";
|
|
36
37
|
import { inspectPrintFile } from "./safety/print-file.js";
|
|
37
|
-
import { validatePrinterState } from "./safety/printer-state.js";
|
|
38
|
+
import { externalStorageState, validatePrinterState } from "./safety/printer-state.js";
|
|
38
39
|
import { uniquePrintName, withPrinterOperation, withPrintSnapshot } from "./safety/artifact.js";
|
|
39
40
|
import { checkedFile } from "./printers/bambu.js";
|
|
40
41
|
import { CONFIRMATION_UNAVAILABLE, confirmationTimedOut, confirmationTimeoutMs } from "./safety/confirmation.js";
|
|
@@ -306,6 +307,7 @@ const SLICE_TOOL_PROPERTIES = {
|
|
|
306
307
|
min_save: { type: "boolean", description: "Bambu-compatible slicing: write a smaller output 3MF (--min-save)." },
|
|
307
308
|
skip_modified_gcodes: { type: "boolean", description: "Bambu-compatible slicing: ignore custom G-code embedded in an input 3MF (--skip-modified-gcodes)." },
|
|
308
309
|
slice_plate: { type: "number", description: "Bambu-compatible slicing: plate number to slice; 0 slices all plates (default)." },
|
|
310
|
+
...PRINT_QUALITY_PROPERTIES,
|
|
309
311
|
layer_height: {
|
|
310
312
|
type: "number", exclusiveMinimum: 0, maximum: 2,
|
|
311
313
|
description: "Layer height in mm, written over the selected process profile (Orca/Bambu Studio layer_height; PrusaSlicer --layer-height). Not mapped for Cura.",
|
|
@@ -748,7 +750,7 @@ class ThreeDPrinterMCPServer {
|
|
|
748
750
|
if (!args?.stl_path) {
|
|
749
751
|
throw new Error("Missing required parameter: stl_path");
|
|
750
752
|
}
|
|
751
|
-
const processOverrides = parseProcessOverrides(args);
|
|
753
|
+
const processOverrides = parseProcessOverrides(withPrintQuality(args, Number(args.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER)));
|
|
752
754
|
const slicerType = normalizeSlicerType(String(args.slicer_type || DEFAULT_SLICER_TYPE));
|
|
753
755
|
const explicitModel = typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
|
|
754
756
|
// Generic OrcaSlicer keeps its user-supplied machine profile unless the
|
|
@@ -881,8 +883,10 @@ class ThreeDPrinterMCPServer {
|
|
|
881
883
|
throw new Error("FULU BambuNetwork printing requires bambu_serial, the printer host and its LAN access code: fresh printer state is checked over MQTT before every bridge print, including cloud dispatch.");
|
|
882
884
|
}
|
|
883
885
|
const isLocalBridgePrint = connectionType === "lan";
|
|
884
|
-
|
|
885
|
-
|
|
886
|
+
// Bambu Studio sends a cloud-bound printer it can reach on the LAN with record first, in the cloud
|
|
887
|
+
// session, then start_print (PrintJob.cpp). This server always has the printer's IP and access code.
|
|
888
|
+
let bridgeMethod = isLocalBridgePrint ? "net.start_local_print" : "net.start_local_print_with_record";
|
|
889
|
+
let cloudFallbackMethod = isLocalBridgePrint ? undefined : "net.start_print";
|
|
886
890
|
const { plateIndex } = options;
|
|
887
891
|
const threeMfFilename = path.basename(options.threeMFPath);
|
|
888
892
|
const projectName = threeMfFilename.replace(/\.3mf$/i, "");
|
|
@@ -923,6 +927,11 @@ class ThreeDPrinterMCPServer {
|
|
|
923
927
|
validatePrinterState(await bambu.getSafetyStatus(devIp, devId, password), requirements);
|
|
924
928
|
const confirmedStatus = await bambu.getSafetyStatus(devIp, devId, password);
|
|
925
929
|
validatePrinterState(confirmedStatus, requirements);
|
|
930
|
+
if (cloudFallbackMethod && externalStorageState(confirmedStatus.raw) !== "normal") {
|
|
931
|
+
// Studio sends straight through the cloud when the printer has no storage for the LAN upload.
|
|
932
|
+
bridgeMethod = cloudFallbackMethod;
|
|
933
|
+
cloudFallbackMethod = undefined;
|
|
934
|
+
}
|
|
926
935
|
const wireMapping = mapping.map((value) => bridgeAmsMappingValue(value, dualNozzle));
|
|
927
936
|
const remoteFileName = uniquePrintName(options.threeMFPath);
|
|
928
937
|
const checkedName = remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, "");
|
|
@@ -979,23 +988,29 @@ class ThreeDPrinterMCPServer {
|
|
|
979
988
|
const bridgeOptions = {
|
|
980
989
|
// Trusted in-process preflight immediately before the print frame.
|
|
981
990
|
beforeDispatch: async (method) => {
|
|
982
|
-
if (method !== bridgeMethod)
|
|
991
|
+
if (method !== bridgeMethod && method !== cloudFallbackMethod)
|
|
983
992
|
return;
|
|
984
993
|
assertActive();
|
|
985
994
|
validatePrinterState(await bambu.getSafetyStatus(devIp, devId, password), requirements);
|
|
986
995
|
},
|
|
987
|
-
assertDispatchAllowed: (method) => { if (method === bridgeMethod)
|
|
996
|
+
assertDispatchAllowed: (method) => { if (method === bridgeMethod || method === cloudFallbackMethod)
|
|
988
997
|
assertActive(); },
|
|
989
|
-
onDispatched: (method) => { if (method === bridgeMethod)
|
|
998
|
+
onDispatched: (method) => { if (method === bridgeMethod || method === cloudFallbackMethod)
|
|
990
999
|
retainForDispatchedUpload(); },
|
|
991
1000
|
};
|
|
992
1001
|
let jobUpdates = [];
|
|
993
1002
|
let bridgeResult;
|
|
1003
|
+
let firstAttempt = "";
|
|
994
1004
|
if (isLocalBridgePrint) {
|
|
995
1005
|
({ result: bridgeResult, jobUpdates } = await this.bambuNetwork.startLocalPrint({ devId, devIp, username: "bblp", password, useSsl: true }, bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions, assertActive));
|
|
996
1006
|
}
|
|
997
1007
|
else {
|
|
998
|
-
|
|
1008
|
+
let firstFailure;
|
|
1009
|
+
({ result: bridgeResult, jobUpdates, firstFailure } = await this.bambuNetwork.startCloudPrint(devId, bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions, assertActive, undefined, cloudFallbackMethod));
|
|
1010
|
+
if (firstFailure !== undefined) {
|
|
1011
|
+
bridgeMethod = cloudFallbackMethod;
|
|
1012
|
+
firstAttempt = ` It first tried net.start_local_print_with_record, as Bambu Studio does, which returned ${firstFailure}${describeBambuNetworkPrintError(firstFailure)}.`;
|
|
1013
|
+
}
|
|
999
1014
|
}
|
|
1000
1015
|
if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
|
|
1001
1016
|
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
|
|
@@ -1005,14 +1020,14 @@ class ThreeDPrinterMCPServer {
|
|
|
1005
1020
|
const reported = jobUpdates.filter((update) => update.code !== 0 || update.msg).slice(-3)
|
|
1006
1021
|
.map((update) => `status ${update.status}, code ${update.code}${update.msg ? `: ${update.msg}` : ""}`).join("; ");
|
|
1007
1022
|
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}${describeBambuNetworkPrintError(value)}.` +
|
|
1008
|
-
(reported ? ` Plug-in reported: ${reported}.` : ""));
|
|
1023
|
+
(reported ? ` Plug-in reported: ${reported}.` : "") + firstAttempt);
|
|
1009
1024
|
}
|
|
1010
1025
|
assertActive();
|
|
1011
1026
|
// Bambu's plug-in placed the file, so a resume re-reads it if it can and otherwise asks a person.
|
|
1012
1027
|
bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements, resumeFile, false);
|
|
1013
1028
|
return {
|
|
1014
1029
|
status: "success",
|
|
1015
|
-
message: `FULU BambuNetwork ${
|
|
1030
|
+
message: `FULU BambuNetwork ${bridgeMethod.replace(/^net\./, "")} command for ${threeMfFilename} sent successfully.${firstAttempt}`,
|
|
1016
1031
|
connectionMode: "bambu_network",
|
|
1017
1032
|
connectionType,
|
|
1018
1033
|
bridgeMethod,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slice_stl's `quality` and `strength` (PRD §8.4): a client's print-sheet choice, mapped here to the
|
|
3
|
+
* slicer's own numbers so no client carries layer heights, wall counts or infill. Draft and Fine set
|
|
4
|
+
* the layer height for the nozzle in use (0.28 and 0.12 mm on a 0.4 mm nozzle, scaled with it); Strong
|
|
5
|
+
* and Max set walls and infill. Standard sends nothing: the resolved preset decides. An explicit
|
|
6
|
+
* layer_height, wall_loops or sparse_infill_density in the same call wins over these.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PRINT_QUALITIES: readonly ["draft", "standard", "fine"];
|
|
9
|
+
export declare const PRINT_STRENGTHS: readonly ["standard", "strong", "max"];
|
|
10
|
+
export type PrintQuality = (typeof PRINT_QUALITIES)[number];
|
|
11
|
+
export type PrintStrength = (typeof PRINT_STRENGTHS)[number];
|
|
12
|
+
/** The call's arguments with `quality` and `strength` written out as slicer settings. */
|
|
13
|
+
export declare function withPrintQuality(args: Record<string, unknown>, nozzleDiameter?: number): Record<string, unknown>;
|
|
14
|
+
/** slice_stl input-schema properties for both arguments. */
|
|
15
|
+
export declare const PRINT_QUALITY_PROPERTIES: {
|
|
16
|
+
readonly quality: {
|
|
17
|
+
readonly type: "string";
|
|
18
|
+
readonly enum: readonly ["draft", "standard", "fine"];
|
|
19
|
+
readonly description: "Print quality: draft (thicker layers, faster), standard (the preset's own layer height) or fine (thinner layers). The server picks the layer height for the nozzle in use; an explicit layer_height wins.";
|
|
20
|
+
};
|
|
21
|
+
readonly strength: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
readonly enum: readonly ["standard", "strong", "max"];
|
|
24
|
+
readonly description: "Part strength: standard (the preset's walls and infill), strong or max (more walls and denser infill). The server picks the values; explicit wall_loops or sparse_infill_density win.";
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slice_stl's `quality` and `strength` (PRD §8.4): a client's print-sheet choice, mapped here to the
|
|
3
|
+
* slicer's own numbers so no client carries layer heights, wall counts or infill. Draft and Fine set
|
|
4
|
+
* the layer height for the nozzle in use (0.28 and 0.12 mm on a 0.4 mm nozzle, scaled with it); Strong
|
|
5
|
+
* and Max set walls and infill. Standard sends nothing: the resolved preset decides. An explicit
|
|
6
|
+
* layer_height, wall_loops or sparse_infill_density in the same call wins over these.
|
|
7
|
+
*/
|
|
8
|
+
export const PRINT_QUALITIES = ["draft", "standard", "fine"];
|
|
9
|
+
export const PRINT_STRENGTHS = ["standard", "strong", "max"];
|
|
10
|
+
/** Layer heights on the reference 0.4 mm nozzle; other nozzles scale from it. */
|
|
11
|
+
const REFERENCE_NOZZLE_MM = 0.4;
|
|
12
|
+
const LAYER_MM_ON_REFERENCE = { draft: 0.28, fine: 0.12 };
|
|
13
|
+
const STRENGTH = {
|
|
14
|
+
strong: { wall_loops: 4, sparse_infill_density: "25%" },
|
|
15
|
+
max: { wall_loops: 6, sparse_infill_density: "40%" },
|
|
16
|
+
};
|
|
17
|
+
/** The call's arguments with `quality` and `strength` written out as slicer settings. */
|
|
18
|
+
export function withPrintQuality(args, nozzleDiameter) {
|
|
19
|
+
if (args.quality === undefined && args.strength === undefined)
|
|
20
|
+
return args;
|
|
21
|
+
const quality = args.quality === undefined ? "standard" : args.quality;
|
|
22
|
+
const strength = args.strength === undefined ? "standard" : args.strength;
|
|
23
|
+
if (typeof quality !== "string" || !PRINT_QUALITIES.includes(quality)) {
|
|
24
|
+
throw new Error(`quality must be one of ${PRINT_QUALITIES.join(", ")}.`);
|
|
25
|
+
}
|
|
26
|
+
if (typeof strength !== "string" || !PRINT_STRENGTHS.includes(strength)) {
|
|
27
|
+
throw new Error(`strength must be one of ${PRINT_STRENGTHS.join(", ")}.`);
|
|
28
|
+
}
|
|
29
|
+
const out = { ...args };
|
|
30
|
+
const scale = typeof nozzleDiameter === "number" && Number.isFinite(nozzleDiameter) && nozzleDiameter > 0 ? nozzleDiameter / REFERENCE_NOZZLE_MM : 1;
|
|
31
|
+
const layer = LAYER_MM_ON_REFERENCE[quality];
|
|
32
|
+
if (layer !== undefined && out.layer_height === undefined)
|
|
33
|
+
out.layer_height = Math.round(layer * scale * 100) / 100;
|
|
34
|
+
const walls = STRENGTH[strength];
|
|
35
|
+
if (walls) {
|
|
36
|
+
if (out.wall_loops === undefined)
|
|
37
|
+
out.wall_loops = walls.wall_loops;
|
|
38
|
+
if (out.sparse_infill_density === undefined)
|
|
39
|
+
out.sparse_infill_density = walls.sparse_infill_density;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** slice_stl input-schema properties for both arguments. */
|
|
44
|
+
export const PRINT_QUALITY_PROPERTIES = {
|
|
45
|
+
quality: {
|
|
46
|
+
type: "string", enum: [...PRINT_QUALITIES],
|
|
47
|
+
description: "Print quality: draft (thicker layers, faster), standard (the preset's own layer height) or fine (thinner layers). The server picks the layer height for the nozzle in use; an explicit layer_height wins.",
|
|
48
|
+
},
|
|
49
|
+
strength: {
|
|
50
|
+
type: "string", enum: [...PRINT_STRENGTHS],
|
|
51
|
+
description: "Part strength: standard (the preset's walls and infill), strong or max (more walls and denser infill). The server picks the values; explicit wall_loops or sparse_infill_density win.",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -1019,7 +1019,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1019
1019
|
break;
|
|
1020
1020
|
case 'cura':
|
|
1021
1021
|
if (Object.keys(orcaProcessOverrides(processOverrides)).length) {
|
|
1022
|
-
throw new SlicerError('preparation', 'layer_height, wall_loops, sparse_infill_density and the support settings are not mapped for Cura; set them in the Cura definition passed as slicer_profile.', { slicerType, slicerPath });
|
|
1022
|
+
throw new SlicerError('preparation', 'quality, strength, layer_height, wall_loops, sparse_infill_density and the support settings are not mapped for Cura; set them in the Cura definition passed as slicer_profile.', { slicerType, slicerPath });
|
|
1023
1023
|
}
|
|
1024
1024
|
// CuraEngine CLI args are different, often requiring -s for settings
|
|
1025
1025
|
// Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-3d-printer-server",
|
|
3
3
|
"mcpName": "io.github.DMontgomery40/mcp-3d-printer-server",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.30",
|
|
5
5
|
"description": "MCP server for connecting Claude with 3D printer management systems",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"repository": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.2.
|
|
10
|
+
"version": "1.2.30",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "mcp-3d-printer-server",
|
|
15
|
-
"version": "1.2.
|
|
15
|
+
"version": "1.2.30",
|
|
16
16
|
"runtimeHint": "npx",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
@@ -23,8 +23,16 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
|
23
23
|
const LOCAL_CONNECT_STATUS = ["ok", "failed", "lost"];
|
|
24
24
|
// bambu_networking.hpp BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED.
|
|
25
25
|
const LOCAL_PRINT_PUBLISH_FAILED = -4030;
|
|
26
|
+
// A with-record send falls back to start_print only on its own preparation failures (the plug-in's
|
|
27
|
+
// PRINT_WR_* results, -2010 to -2140: project, upload, task record, FTPS), which come before the print
|
|
28
|
+
// command, so the printer has no job. Any other result may have reached the printer and is never
|
|
29
|
+
// replayed. Of those, Studio also stops on a file missing or too large (should_stop_without_fallback).
|
|
30
|
+
const WITH_RECORD_PREPARATION_FAILURE = (value: number) => value <= -2010 && value >= -2140 && value !== -2040 && value !== -2100;
|
|
26
31
|
// bambu_networking.hpp print errors that name a recoverable cause.
|
|
27
32
|
const BAMBU_NETWORK_PRINT_ERRORS: Record<number, string> = {
|
|
33
|
+
[-2120]: "Bambu Cloud refused to create the print task",
|
|
34
|
+
[-2130]: "FTPS upload to the printer failed",
|
|
35
|
+
[-3120]: "Bambu Cloud refused to create the print task",
|
|
28
36
|
[-4010]: "the file exceeds the 1 GB upload limit",
|
|
29
37
|
[-4020]: "FTPS upload to the printer failed",
|
|
30
38
|
[LOCAL_PRINT_PUBLISH_FAILED]: "the networking plug-in could not send the print command to the printer over MQTT",
|
|
@@ -853,10 +861,24 @@ export class BambuNetworkBridge {
|
|
|
853
861
|
}
|
|
854
862
|
}
|
|
855
863
|
|
|
856
|
-
/**
|
|
864
|
+
/**
|
|
865
|
+
* A cloud print in a cloud session, so the plug-in has the printer's state. With a fallback this is
|
|
866
|
+
* Bambu Studio's order for a cloud-bound printer it can also reach on the LAN (PrintJob.cpp,
|
|
867
|
+
* SendAttemptRoute::LanWithRecord, then ::Cloud): start_local_print_with_record, then start_print
|
|
868
|
+
* when the first failed before its print command was sent. `firstFailure` is the first method's result.
|
|
869
|
+
*/
|
|
857
870
|
async startCloudPrint(devId: string, method: string, payload: JsonObject, options: BambuNetworkBridgeOptions = {},
|
|
858
|
-
assertActive: () => void = () => {}, timing: Readonly<CloudTiming> = CLOUD_PRINT_TIMING
|
|
859
|
-
|
|
871
|
+
assertActive: () => void = () => {}, timing: Readonly<CloudTiming> = CLOUD_PRINT_TIMING, fallbackMethod?: string
|
|
872
|
+
): Promise<{ result: unknown; jobUpdates: BridgeJobUpdate[]; firstFailure?: number }> {
|
|
873
|
+
return await this.withCloudSession(devId, options, assertActive, timing, async session => {
|
|
874
|
+
const result = await this.callWithAgent(method, payload, options);
|
|
875
|
+
const value = bridgeNumericResult(result);
|
|
876
|
+
if (!fallbackMethod || value === undefined || !WITH_RECORD_PREPARATION_FAILURE(value)) return { result, jobUpdates: session.jobUpdates };
|
|
877
|
+
options.trace?.({ step: "fallback", from: method, to: fallbackMethod, result: value });
|
|
878
|
+
const params = (payload as { params?: JsonObject }).params;
|
|
879
|
+
const fallbackPayload = params ? { ...payload, params: { ...params, comments: `failed(${value})` } } : payload;
|
|
880
|
+
return { result: await this.callWithAgent(fallbackMethod, fallbackPayload, options), jobUpdates: session.jobUpdates, firstFailure: value };
|
|
881
|
+
});
|
|
860
882
|
}
|
|
861
883
|
|
|
862
884
|
/**
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import * as THREE from 'three';
|
|
|
36
36
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
37
37
|
import { STLManipulator, SUPPORT_TYPES, type BambuSliceOptions, type ProcessOverrides, type SliceOutput } from "./stl/stl-manipulator.js";
|
|
38
38
|
import { parsePauseLayers, readPlateGcode, pausedLayers, machinePauseGcode } from "./slicer/layer-pauses.js";
|
|
39
|
+
import { PRINT_QUALITY_PROPERTIES, withPrintQuality } from "./slicer/print-quality.js";
|
|
39
40
|
import { CounterpartService, type BambuControlRoute, type PrinterTarget } from "./counterpart/service.js";
|
|
40
41
|
import {CAMERA_STREAM_TOOLS} from "./counterpart/camera-tools.js";
|
|
41
42
|
import { annotateTools } from "./tool-annotations.js";
|
|
@@ -75,7 +76,7 @@ import { BambuNetworkBridge, describeBambuNetworkPrintError, type BridgeJobUpdat
|
|
|
75
76
|
import { PRINT_ROUTES, connectionForRoute, routeOf, routeOptions, writeEnvValues, type PrintRoute } from "./counterpart/route.js";
|
|
76
77
|
import { resolveStudioPrintOptions, usedNozzleCount, type CallerPrintOptions } from "./studio-print-options.js";
|
|
77
78
|
import { inspectPrintFile } from "./safety/print-file.js";
|
|
78
|
-
import { validatePrinterState } from "./safety/printer-state.js";
|
|
79
|
+
import { externalStorageState, validatePrinterState } from "./safety/printer-state.js";
|
|
79
80
|
import { uniquePrintName, withPrinterOperation, withPrintSnapshot } from "./safety/artifact.js";
|
|
80
81
|
import { checkedFile } from "./printers/bambu.js";
|
|
81
82
|
import { CONFIRMATION_UNAVAILABLE, confirmationTimedOut, confirmationTimeoutMs } from "./safety/confirmation.js";
|
|
@@ -384,6 +385,7 @@ const SLICE_TOOL_PROPERTIES = {
|
|
|
384
385
|
min_save: { type: "boolean", description: "Bambu-compatible slicing: write a smaller output 3MF (--min-save)." },
|
|
385
386
|
skip_modified_gcodes: { type: "boolean", description: "Bambu-compatible slicing: ignore custom G-code embedded in an input 3MF (--skip-modified-gcodes)." },
|
|
386
387
|
slice_plate: { type: "number", description: "Bambu-compatible slicing: plate number to slice; 0 slices all plates (default)." },
|
|
388
|
+
...PRINT_QUALITY_PROPERTIES,
|
|
387
389
|
layer_height: {
|
|
388
390
|
type: "number", exclusiveMinimum: 0, maximum: 2,
|
|
389
391
|
description: "Layer height in mm, written over the selected process profile (Orca/Bambu Studio layer_height; PrusaSlicer --layer-height). Not mapped for Cura.",
|
|
@@ -904,7 +906,7 @@ class ThreeDPrinterMCPServer {
|
|
|
904
906
|
if (!args?.stl_path) {
|
|
905
907
|
throw new Error("Missing required parameter: stl_path");
|
|
906
908
|
}
|
|
907
|
-
const processOverrides = parseProcessOverrides(args);
|
|
909
|
+
const processOverrides = parseProcessOverrides(withPrintQuality(args, Number(args.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER)));
|
|
908
910
|
const slicerType = normalizeSlicerType(String(args.slicer_type || DEFAULT_SLICER_TYPE));
|
|
909
911
|
const explicitModel =
|
|
910
912
|
typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
|
|
@@ -1074,8 +1076,10 @@ class ThreeDPrinterMCPServer {
|
|
|
1074
1076
|
throw new Error("FULU BambuNetwork printing requires bambu_serial, the printer host and its LAN access code: fresh printer state is checked over MQTT before every bridge print, including cloud dispatch.");
|
|
1075
1077
|
}
|
|
1076
1078
|
const isLocalBridgePrint = connectionType === "lan";
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
+
// Bambu Studio sends a cloud-bound printer it can reach on the LAN with record first, in the cloud
|
|
1080
|
+
// session, then start_print (PrintJob.cpp). This server always has the printer's IP and access code.
|
|
1081
|
+
let bridgeMethod = isLocalBridgePrint ? "net.start_local_print" : "net.start_local_print_with_record";
|
|
1082
|
+
let cloudFallbackMethod = isLocalBridgePrint ? undefined : "net.start_print";
|
|
1079
1083
|
const { plateIndex } = options;
|
|
1080
1084
|
const threeMfFilename = path.basename(options.threeMFPath);
|
|
1081
1085
|
const projectName = threeMfFilename.replace(/\.3mf$/i, "");
|
|
@@ -1116,6 +1120,11 @@ class ThreeDPrinterMCPServer {
|
|
|
1116
1120
|
validatePrinterState(await bambu.getSafetyStatus(devIp, devId, password), requirements);
|
|
1117
1121
|
const confirmedStatus = await bambu.getSafetyStatus(devIp, devId, password);
|
|
1118
1122
|
validatePrinterState(confirmedStatus, requirements);
|
|
1123
|
+
if (cloudFallbackMethod && externalStorageState(confirmedStatus.raw) !== "normal") {
|
|
1124
|
+
// Studio sends straight through the cloud when the printer has no storage for the LAN upload.
|
|
1125
|
+
bridgeMethod = cloudFallbackMethod;
|
|
1126
|
+
cloudFallbackMethod = undefined;
|
|
1127
|
+
}
|
|
1119
1128
|
const wireMapping = mapping!.map((value) => bridgeAmsMappingValue(value, dualNozzle));
|
|
1120
1129
|
const remoteFileName = uniquePrintName(options.threeMFPath);
|
|
1121
1130
|
const checkedName = remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, "");
|
|
@@ -1172,22 +1181,28 @@ class ThreeDPrinterMCPServer {
|
|
|
1172
1181
|
const bridgeOptions = {
|
|
1173
1182
|
// Trusted in-process preflight immediately before the print frame.
|
|
1174
1183
|
beforeDispatch: async (method: string) => {
|
|
1175
|
-
if (method !== bridgeMethod) return;
|
|
1184
|
+
if (method !== bridgeMethod && method !== cloudFallbackMethod) return;
|
|
1176
1185
|
assertActive();
|
|
1177
1186
|
validatePrinterState(await bambu.getSafetyStatus(devIp, devId, password), requirements);
|
|
1178
1187
|
},
|
|
1179
|
-
assertDispatchAllowed: (method: string) => { if (method === bridgeMethod) assertActive(); },
|
|
1180
|
-
onDispatched: (method: string) => { if (method === bridgeMethod) retainForDispatchedUpload(); },
|
|
1188
|
+
assertDispatchAllowed: (method: string) => { if (method === bridgeMethod || method === cloudFallbackMethod) assertActive(); },
|
|
1189
|
+
onDispatched: (method: string) => { if (method === bridgeMethod || method === cloudFallbackMethod) retainForDispatchedUpload(); },
|
|
1181
1190
|
};
|
|
1182
1191
|
let jobUpdates: BridgeJobUpdate[] = [];
|
|
1183
1192
|
let bridgeResult: unknown;
|
|
1193
|
+
let firstAttempt = "";
|
|
1184
1194
|
if (isLocalBridgePrint) {
|
|
1185
1195
|
({ result: bridgeResult, jobUpdates } = await this.bambuNetwork.startLocalPrint(
|
|
1186
1196
|
{ devId, devIp, username: "bblp", password, useSsl: true },
|
|
1187
1197
|
bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions, assertActive
|
|
1188
1198
|
));
|
|
1189
1199
|
} else {
|
|
1190
|
-
|
|
1200
|
+
let firstFailure: number | undefined;
|
|
1201
|
+
({ result: bridgeResult, jobUpdates, firstFailure } = await this.bambuNetwork.startCloudPrint(devId, bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions, assertActive, undefined, cloudFallbackMethod));
|
|
1202
|
+
if (firstFailure !== undefined) {
|
|
1203
|
+
bridgeMethod = cloudFallbackMethod!;
|
|
1204
|
+
firstAttempt = ` It first tried net.start_local_print_with_record, as Bambu Studio does, which returned ${firstFailure}${describeBambuNetworkPrintError(firstFailure)}.`;
|
|
1205
|
+
}
|
|
1191
1206
|
}
|
|
1192
1207
|
if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
|
|
1193
1208
|
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String((bridgeResult as { error?: unknown }).error || "unknown bridge error")}`);
|
|
@@ -1197,14 +1212,14 @@ class ThreeDPrinterMCPServer {
|
|
|
1197
1212
|
const reported = jobUpdates.filter((update) => update.code !== 0 || update.msg).slice(-3)
|
|
1198
1213
|
.map((update) => `status ${update.status}, code ${update.code}${update.msg ? `: ${update.msg}` : ""}`).join("; ");
|
|
1199
1214
|
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}${describeBambuNetworkPrintError(value)}.` +
|
|
1200
|
-
(reported ? ` Plug-in reported: ${reported}.` : ""));
|
|
1215
|
+
(reported ? ` Plug-in reported: ${reported}.` : "") + firstAttempt);
|
|
1201
1216
|
}
|
|
1202
1217
|
assertActive();
|
|
1203
1218
|
// Bambu's plug-in placed the file, so a resume re-reads it if it can and otherwise asks a person.
|
|
1204
1219
|
bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements, resumeFile, false);
|
|
1205
1220
|
return {
|
|
1206
1221
|
status: "success",
|
|
1207
|
-
message: `FULU BambuNetwork ${
|
|
1222
|
+
message: `FULU BambuNetwork ${bridgeMethod.replace(/^net\./, "")} command for ${threeMfFilename} sent successfully.${firstAttempt}`,
|
|
1208
1223
|
connectionMode: "bambu_network",
|
|
1209
1224
|
connectionType,
|
|
1210
1225
|
bridgeMethod,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slice_stl's `quality` and `strength` (PRD §8.4): a client's print-sheet choice, mapped here to the
|
|
3
|
+
* slicer's own numbers so no client carries layer heights, wall counts or infill. Draft and Fine set
|
|
4
|
+
* the layer height for the nozzle in use (0.28 and 0.12 mm on a 0.4 mm nozzle, scaled with it); Strong
|
|
5
|
+
* and Max set walls and infill. Standard sends nothing: the resolved preset decides. An explicit
|
|
6
|
+
* layer_height, wall_loops or sparse_infill_density in the same call wins over these.
|
|
7
|
+
*/
|
|
8
|
+
export const PRINT_QUALITIES = ["draft", "standard", "fine"] as const;
|
|
9
|
+
export const PRINT_STRENGTHS = ["standard", "strong", "max"] as const;
|
|
10
|
+
|
|
11
|
+
export type PrintQuality = (typeof PRINT_QUALITIES)[number];
|
|
12
|
+
export type PrintStrength = (typeof PRINT_STRENGTHS)[number];
|
|
13
|
+
|
|
14
|
+
/** Layer heights on the reference 0.4 mm nozzle; other nozzles scale from it. */
|
|
15
|
+
const REFERENCE_NOZZLE_MM = 0.4;
|
|
16
|
+
const LAYER_MM_ON_REFERENCE: Partial<Record<PrintQuality, number>> = { draft: 0.28, fine: 0.12 };
|
|
17
|
+
const STRENGTH: Partial<Record<PrintStrength, { wall_loops: number; sparse_infill_density: string }>> = {
|
|
18
|
+
strong: { wall_loops: 4, sparse_infill_density: "25%" },
|
|
19
|
+
max: { wall_loops: 6, sparse_infill_density: "40%" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** The call's arguments with `quality` and `strength` written out as slicer settings. */
|
|
23
|
+
export function withPrintQuality(args: Record<string, unknown>, nozzleDiameter?: number): Record<string, unknown> {
|
|
24
|
+
if (args.quality === undefined && args.strength === undefined) return args;
|
|
25
|
+
const quality = args.quality === undefined ? "standard" : args.quality;
|
|
26
|
+
const strength = args.strength === undefined ? "standard" : args.strength;
|
|
27
|
+
if (typeof quality !== "string" || !(PRINT_QUALITIES as readonly string[]).includes(quality)) {
|
|
28
|
+
throw new Error(`quality must be one of ${PRINT_QUALITIES.join(", ")}.`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof strength !== "string" || !(PRINT_STRENGTHS as readonly string[]).includes(strength)) {
|
|
31
|
+
throw new Error(`strength must be one of ${PRINT_STRENGTHS.join(", ")}.`);
|
|
32
|
+
}
|
|
33
|
+
const out: Record<string, unknown> = { ...args };
|
|
34
|
+
const scale = typeof nozzleDiameter === "number" && Number.isFinite(nozzleDiameter) && nozzleDiameter > 0 ? nozzleDiameter / REFERENCE_NOZZLE_MM : 1;
|
|
35
|
+
const layer = LAYER_MM_ON_REFERENCE[quality as PrintQuality];
|
|
36
|
+
if (layer !== undefined && out.layer_height === undefined) out.layer_height = Math.round(layer * scale * 100) / 100;
|
|
37
|
+
const walls = STRENGTH[strength as PrintStrength];
|
|
38
|
+
if (walls) {
|
|
39
|
+
if (out.wall_loops === undefined) out.wall_loops = walls.wall_loops;
|
|
40
|
+
if (out.sparse_infill_density === undefined) out.sparse_infill_density = walls.sparse_infill_density;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** slice_stl input-schema properties for both arguments. */
|
|
46
|
+
export const PRINT_QUALITY_PROPERTIES = {
|
|
47
|
+
quality: {
|
|
48
|
+
type: "string", enum: [...PRINT_QUALITIES],
|
|
49
|
+
description: "Print quality: draft (thicker layers, faster), standard (the preset's own layer height) or fine (thinner layers). The server picks the layer height for the nozzle in use; an explicit layer_height wins.",
|
|
50
|
+
},
|
|
51
|
+
strength: {
|
|
52
|
+
type: "string", enum: [...PRINT_STRENGTHS],
|
|
53
|
+
description: "Part strength: standard (the preset's walls and infill), strong or max (more walls and denser infill). The server picks the values; explicit wall_loops or sparse_infill_density win.",
|
|
54
|
+
},
|
|
55
|
+
} as const;
|
|
@@ -1317,7 +1317,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1317
1317
|
case 'cura':
|
|
1318
1318
|
if (Object.keys(orcaProcessOverrides(processOverrides)).length) {
|
|
1319
1319
|
throw new SlicerError('preparation',
|
|
1320
|
-
'layer_height, wall_loops, sparse_infill_density and the support settings are not mapped for Cura; set them in the Cura definition passed as slicer_profile.',
|
|
1320
|
+
'quality, strength, layer_height, wall_loops, sparse_infill_density and the support settings are not mapped for Cura; set them in the Cura definition passed as slicer_profile.',
|
|
1321
1321
|
{ slicerType, slicerPath });
|
|
1322
1322
|
}
|
|
1323
1323
|
// CuraEngine CLI args are different, often requiring -s for settings
|