Merge branch 'main' into CURA-12814
Some checks failed
conan-package / conan-package (push) Waiting to run
unit-test / Run unit tests (push) Waiting to run
conan-package-resources / conan-package (push) Has been cancelled
printer-linter-format / Printer linter auto format (push) Has been cancelled
conan-package-resources / signal-curator (push) Has been cancelled

This commit is contained in:
HellAholic 2026-02-28 17:17:56 +01:00
commit e5facbe5cc
8243 changed files with 9893 additions and 8442 deletions

View file

@ -140,7 +140,7 @@ class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions.
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
# changes of the settings.
SettingVersion = 25
SettingVersion = 26
Created = False

View file

@ -51,6 +51,7 @@ class PaintTool(Tool):
self._view: PaintView = view
self._view.canUndoChanged.connect(self._onCanUndoChanged)
self._view.canRedoChanged.connect(self._onCanRedoChanged)
self._view.currentPaintedObjectMeshDataChanged.connect(self._updateState)
self._picking_pass: Optional[PickingPass] = None
self._faces_selection_pass: Optional[SelectionPass] = None
@ -198,40 +199,6 @@ class PaintTool(Tool):
self._view.clearPaint()
self._updateScene(update_node = True)
@staticmethod
def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float:
# compute the intersection of (param) A - pt with (param) B - (param) C
if all(a == pt) or all(b == c) or all(a == c) or all(a == b):
return 1.0
# compute unit vectors of directions of lines A and B
udir_a = a - pt
udir_a /= numpy.linalg.norm(udir_a)
udir_b = b - c
udir_b /= numpy.linalg.norm(udir_b)
# find unit direction vector for line C, which is perpendicular to lines A and B
udir_res = numpy.cross(udir_b, udir_a)
udir_res_len = numpy.linalg.norm(udir_res)
if udir_res_len == 0:
return 1.0
udir_res /= udir_res_len
# solve system of equations
rhs = b - a
lhs = numpy.array([udir_a, -udir_b, udir_res]).T
try:
solved = numpy.linalg.solve(lhs, rhs)
except numpy.linalg.LinAlgError:
return 1.0
# get the ratio
intersect = ((a + solved[0] * udir_a) + (b + solved[1] * udir_b)) * 0.5
a_intersect_dist = numpy.linalg.norm(a - intersect)
if a_intersect_dist == 0:
return 1.0
return numpy.linalg.norm(pt - intersect) / a_intersect_dist
def _nodeTransformChanged(self, *args) -> None:
self._cache_dirty = True
@ -453,7 +420,7 @@ class PaintTool(Tool):
def _updateState(self):
painted_object = self._view.getPaintedObject()
if painted_object is not None and self._controller.getActiveTool() == self:
if painted_object.callDecoration("getPaintTexture") is not None:
if painted_object.callDecoration("getPaintTexture") is not None and painted_object.getMeshData().hasUVCoordinates():
new_state = PaintTool.Paint.State.READY
else:
new_state = PaintTool.Paint.State.PREPARING_MODEL
@ -472,6 +439,7 @@ class PaintTool(Tool):
self._prepare_texture_job = None
self._state = PaintTool.Paint.State.READY
self.propertyChanged.emit()
self._updateScene()
def _updateIgnoreUnselectedObjects(self):
ignore_unselected_objects = self._controller.getActiveView().name == "PaintTool"

View file

@ -61,11 +61,13 @@ class PaintView(CuraView):
canUndoChanged = pyqtSignal(bool)
canRedoChanged = pyqtSignal(bool)
currentPaintedObjectMeshDataChanged = pyqtSignal()
def setPaintedObject(self, painted_object: Optional[SceneNode]):
if self._painted_object is not None:
texture_changed_signal = self._painted_object.callDecoration("getPaintTextureChangedSignal")
texture_changed_signal.disconnect(self._onCurrentPaintedObjectTextureChanged)
self._painted_object.meshDataChanged.disconnect(self._onCurrentPaintedObjectMesDataChanged)
self._paint_texture = None
self._cursor_texture = None
@ -78,6 +80,7 @@ class PaintView(CuraView):
if texture_changed_signal is not None:
texture_changed_signal.connect(self._onCurrentPaintedObjectTextureChanged)
self._onCurrentPaintedObjectTextureChanged()
self._painted_object.meshDataChanged.connect(self._onCurrentPaintedObjectMesDataChanged)
self._updateCurrentBitsRanges()
@ -99,6 +102,10 @@ class PaintView(CuraView):
else:
self._cursor_texture = None
def _onCurrentPaintedObjectMesDataChanged(self, object: SceneNode) -> None:
if object == self._painted_object:
self.currentPaintedObjectMeshDataChanged.emit()
def canUndo(self):
stack = self._getUndoStack()
return stack.canUndo() if stack is not None else False

View file

@ -39,6 +39,42 @@ UM.Dialog
anchors.fill: parent
// Helper function to check if a setting should use multiline text area
// Supports "multiline" or "@[multiline]" or "@[multiline, other] comment"
function isMultilineSetting(definition)
{
if (!definition || !definition.comments)
{
return false;
}
var commentsLower = definition.comments.toLowerCase();
// Simple format: exact match
if (commentsLower === "multiline")
{
return true;
}
// Directive format: parse @[...] and check if multiline is in the list
var directiveStart = commentsLower.indexOf("@[");
var directiveEnd = commentsLower.indexOf("]", directiveStart);
if (directiveStart >= 0 && directiveEnd > directiveStart)
{
var directivesText = commentsLower.substring(directiveStart + 2, directiveEnd);
var directives = directivesText.split(",");
for (var i = 0; i < directives.length; i++)
{
if (directives[i].trim() === "multiline")
{
return true;
}
}
}
return false;
}
ButtonGroup
{
id: selectedScriptGroup
@ -301,6 +337,10 @@ UM.Dialog
{
if (provider.properties.enabled == "True" && model.type != undefined)
{
if (definition && definition.comments && definition.comments.toLowerCase() === "multiline")
{
return UM.Theme.getSize("standard_list_lineheight").height + UM.Theme.getSize("narrow_margin").height + (UM.Theme.getSize("setting_control").height * 3);
}
return UM.Theme.getSize("section").height;
}
else
@ -331,6 +371,19 @@ UM.Dialog
settingLoader.item.showLinkedSettingIcon = false
settingLoader.item.doDepthIndentation = false
settingLoader.item.doQualityUserSettingEmphasis = false
// Pass properties explicitly to custom components that don't extend SettingItem
if (settingLoader.item.hasOwnProperty("definition")) {
settingLoader.item.definition = settingLoader.definition
}
if (settingLoader.item.hasOwnProperty("settingDefinitionsModel")) {
settingLoader.item.settingDefinitionsModel = settingLoader.settingDefinitionsModel
}
if (settingLoader.item.hasOwnProperty("propertyProvider")) {
settingLoader.item.propertyProvider = settingLoader.propertyProvider
}
if (settingLoader.item.hasOwnProperty("globalPropertyProvider")) {
settingLoader.item.globalPropertyProvider = settingLoader.globalPropertyProvider
}
}
sourceComponent:
@ -348,7 +401,7 @@ UM.Dialog
case "bool":
return settingCheckBox
case "str":
return settingTextField
return base.isMultilineSetting(definition) ? settingTextArea : settingTextField
case "category":
return settingCategory
default:
@ -405,6 +458,13 @@ UM.Dialog
Cura.SettingTextField { }
}
Component
{
id: settingTextArea;
SettingTextArea { }
}
Component
{
id: settingComboBox;

View file

@ -0,0 +1,108 @@
// Copyright (c) 2025 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import UM 1.7 as UM
// Custom multiline text area component for post-processing script settings
// Triggered when setting has "comments": "multiline" property
Item
{
id: base
// Properties passed from the Loader
property var definition
property var settingDefinitionsModel
property var propertyProvider
property var globalPropertyProvider
// Standard setting item properties (required by loader but unused in this component)
property bool showRevertButton: false
property bool showInheritButton: false
property bool showLinkedSettingIcon: false
property bool doDepthIndentation: false
property bool doQualityUserSettingEmphasis: false
// Internal state tracking (unused but kept for potential future use)
property string textBeforeEdit
property bool textHasChanged
// Signals for tooltip support (required by Connections in loader)
// Note: These signals are declared but intentionally never emitted.
// This prevents tooltips from appearing on mouse-over, which would obstruct the text area
// while the user is typing or editing multiline content.
signal showTooltip(string text)
signal hideTooltip()
width: parent.width
// Height calculation: label height + spacing + text area (3x normal height for multiline editing)
height: UM.Theme.getSize("standard_list_lineheight").height + UM.Theme.getSize("narrow_margin").height + (UM.Theme.getSize("setting_control").height * 3)
UM.Label
{
id: labelText
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: UM.Theme.getSize("standard_list_lineheight").height
text: definition ? definition.label : ""
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold")
color: UM.Theme.getColor("text")
}
Flickable
{
id: flickable
anchors.top: labelText.bottom
anchors.topMargin: UM.Theme.getSize("narrow_margin").height
anchors.left: parent.left
anchors.right: parent.right
// Right margin to prevent overlap with parent ListView scrollbar
anchors.rightMargin: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
height: UM.Theme.getSize("setting_control").height * 3
clip: true
ScrollBar.vertical: UM.ScrollBar { }
TextArea.flickable: TextArea
{
id: textArea
enabled: propertyProvider && propertyProvider.properties ? propertyProvider.properties.enabled === "True" : true
// Explicit undefined check to prevent QML warnings about undefined QString assignment
text: (propertyProvider && propertyProvider.properties && propertyProvider.properties.value !== undefined) ? propertyProvider.properties.value : ""
background: Rectangle
{
color: UM.Theme.getColor("setting_control")
border.color: textArea.activeFocus ? UM.Theme.getColor("text_field_border_active") : UM.Theme.getColor("text_field_border")
border.width: UM.Theme.getSize("default_lining").width
}
onTextChanged:
{
// Save value on each keystroke when focused (live update)
if (activeFocus && propertyProvider)
{
propertyProvider.setPropertyValue("value", text);
}
}
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
selectionColor: UM.Theme.getColor("text_selection")
selectedTextColor: UM.Theme.getColor("text")
wrapMode: TextEdit.NoWrap
selectByMouse: true
// Allow Enter/Return to insert newlines instead of closing dialog
Keys.onReturnPressed: function(event) { event.accepted = false; }
Keys.onEnterPressed: function(event) { event.accepted = false; }
// Escape key removes focus from text area
Keys.onEscapePressed: function(event) { focus = false; event.accepted = true; }
}
}
}

View file

@ -1,7 +1,7 @@
{
"name": "Post Processing",
"author": "Ultimaker",
"version": "2.2.1",
"version": "2.3.0",
"api": 8,
"description": "Extension that allows for user created scripts for post processing",
"catalog": "cura"

View file

@ -15,14 +15,20 @@ Designed in January 2023 by GregValiant (Greg Foresi)
01/05/24 (GV) Revised the regex replacements.
12/11/24 (GV) Added 'off_fan_speed' for the idle nozzle layer cooling fan. It does not have to go to 0%.
03/22/25 (GV) Added 'Chamber Cooling Fan / Auxiliary Fan' control.
12/26/25 (GV) Added 'Enable Script' setting so it can be left installed, but not run.
12/26/25 (GV) Added 'Jump Start' option to get the fan spinning just before it is actually called.
"""
from ..Script import Script
from UM.Application import Application
import re
from UM.Logger import Logger
class AddCoolingProfile(Script):
jump_speed_marlin = 179 # ~70% of 255
jump_speed_reprap = 0.70 # used if fan_scale_0_to_1 is true
def getSettingDataString(self):
return """{
"name": "Advanced Cooling Fan Control",
@ -31,6 +37,14 @@ class AddCoolingProfile(Script):
"version": 2,
"settings":
{
"enable_cooling_profile":
{
"label": "Enable Advanced Cooling",
"description": "Enable the script so it will run.",
"type": "bool",
"default_value": true,
"enabled": true
},
"fan_layer_or_feature":
{
"label": "Cooling Control by:",
@ -39,14 +53,15 @@ class AddCoolingProfile(Script):
"options": {
"by_layer": "Layer Numbers",
"by_feature": "Feature Types"},
"default_value": "by_layer"
"default_value": "by_layer",
"enabled": "enable_cooling_profile"
},
"delete_existing_m106":
{
"label": "Remove M106 lines prior to inserting new.",
"description": "If you have 2 or more instances of 'Advanced Cooling Fan Control' running (to cool a portion of a print differently), then you must uncheck this box or the followup instances will remove all the lines inserted by the first instance. Pay attention to the Start and Stop layers. Regardless of this setting: The script always removes M106 lines starting with the lowest layer number (when 'By Layer') or the starting layer number (when 'By Feature'). If you want to keep the M106 lines that Cura inserted up to the point where this post-processor will start making insertions, then un-check the box.",
"type": "bool",
"enabled": true,
"enabled": "enable_cooling_profile",
"default_value": true
},
"feature_fan_start_layer":
@ -57,7 +72,7 @@ class AddCoolingProfile(Script):
"default_value": 5,
"minimum_value": 1,
"unit": "Lay# ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_end_layer":
{
@ -67,7 +82,7 @@ class AddCoolingProfile(Script):
"default_value": -1,
"minimum_value": -1,
"unit": "Lay# ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"layer_fan_1":
{
@ -76,7 +91,7 @@ class AddCoolingProfile(Script):
"type": "str",
"default_value": "5/30",
"unit": "L#/% ",
"enabled": "fan_layer_or_feature == 'by_layer'"
"enabled": "fan_layer_or_feature == 'by_layer' and enable_cooling_profile"
},
"layer_fan_2":
{
@ -85,7 +100,7 @@ class AddCoolingProfile(Script):
"type": "str",
"default_value": "",
"unit": "L#/% ",
"enabled": "fan_layer_or_feature == 'by_layer'"
"enabled": "fan_layer_or_feature == 'by_layer' and enable_cooling_profile"
},
"layer_fan_3":
{
@ -94,7 +109,7 @@ class AddCoolingProfile(Script):
"type": "str",
"default_value": "",
"unit": "L#/% ",
"enabled": "fan_layer_or_feature == 'by_layer'"
"enabled": "fan_layer_or_feature == 'by_layer' and enable_cooling_profile"
},
"layer_fan_4":
{
@ -103,7 +118,7 @@ class AddCoolingProfile(Script):
"type": "str",
"default_value": "",
"unit": "L#/% ",
"enabled": "fan_layer_or_feature == 'by_layer'"
"enabled": "fan_layer_or_feature == 'by_layer' and enable_cooling_profile"
},
"layer_fan_5":
{
@ -112,7 +127,7 @@ class AddCoolingProfile(Script):
"type": "str",
"default_value": "",
"unit": "L#/% ",
"enabled": "fan_layer_or_feature == 'by_layer'"
"enabled": "fan_layer_or_feature == 'by_layer' and enable_cooling_profile"
},
"layer_fan_6":
{
@ -121,7 +136,7 @@ class AddCoolingProfile(Script):
"type": "str",
"default_value": "",
"unit": "L#/% ",
"enabled": "fan_layer_or_feature == 'by_layer'"
"enabled": "fan_layer_or_feature == 'by_layer' and enable_cooling_profile"
},
"layer_fan_7":
{
@ -130,7 +145,7 @@ class AddCoolingProfile(Script):
"type": "str",
"default_value": "",
"unit": "L#/% ",
"enabled": "fan_layer_or_feature == 'by_layer'"
"enabled": "fan_layer_or_feature == 'by_layer' and enable_cooling_profile"
},
"layer_fan_8":
{
@ -139,7 +154,7 @@ class AddCoolingProfile(Script):
"type": "str",
"default_value": "",
"unit": "L#/% ",
"enabled": "fan_layer_or_feature == 'by_layer'"
"enabled": "fan_layer_or_feature == 'by_layer' and enable_cooling_profile"
},
"feature_fan_skirt":
{
@ -150,7 +165,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_wall_inner":
{
@ -161,7 +176,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_wall_outer":
{
@ -172,7 +187,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_fill":
{
@ -183,7 +198,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_skin":
{
@ -194,7 +209,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_support":
{
@ -205,7 +220,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_support_interface":
{
@ -216,7 +231,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_prime_tower":
{
@ -227,7 +242,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_bridge":
{
@ -238,14 +253,14 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_layer_or_feature == 'by_feature'"
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
},
"feature_fan_combing":
{
"label": "Fan 'OFF' during Combing:",
"description": "When checked will set the fan to 0% for combing moves over 5 lines long in the gcode. When un-checked the fan speed during combing is whatever the previous speed is set to.",
"type": "bool",
"enabled": "fan_layer_or_feature == 'by_feature'",
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile",
"default_value": true
},
"feature_fan_feature_final":
@ -257,7 +272,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "(int(feature_fan_end_layer) != -1) and (fan_layer_or_feature == 'by_feature')"
"enabled": "(int(feature_fan_end_layer) != -1) and (fan_layer_or_feature == 'by_feature') and enable_cooling_profile"
},
"fan_enable_raft":
{
@ -265,7 +280,7 @@ class AddCoolingProfile(Script):
"description": "Enable the fan for the raft layers. When enabled the Raft Fan Speed will continue until another Layer or Feature setting over-rides it.",
"type": "bool",
"default_value": false,
"enabled": true
"enabled": "enable_cooling_profile"
},
"fan_raft_percent":
{
@ -276,7 +291,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_enable_raft"
"enabled": "fan_enable_raft and enable_cooling_profile"
},
"enable_off_fan_speed_enable":
{
@ -292,7 +307,7 @@ class AddCoolingProfile(Script):
"description": "For machines with independent layer cooling fans. Leaving a fan running while the other nozzle is printing can help with oozing. You can pick the speed % for the idle nozzle layer cooling fan to hold at.",
"type": "bool",
"default_value": false,
"enabled": "enable_off_fan_speed_enable"
"enabled": "enable_off_fan_speed_enable and enable_cooling_profile"
},
"off_fan_speed":
{
@ -303,7 +318,7 @@ class AddCoolingProfile(Script):
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "enable_off_fan_speed_enable and enable_off_fan_speed"
"enabled": "enable_off_fan_speed_enable and enable_off_fan_speed and enable_cooling_profile"
},
"bv_fan_speed_control_enable":
{
@ -311,7 +326,7 @@ class AddCoolingProfile(Script):
"description": "Controls the 'Build Volume Fan' or an 'Auxiliary Fan' on printers with that hardware. Provides: 'On' layer, 'Off' layer, and PWM speed control of a secondary fan.",
"type": "bool",
"default_value": false,
"enabled": "enable_bv_fan"
"enabled": "enable_bv_fan and enable_cooling_profile"
},
"bv_fan_nr":
{
@ -321,7 +336,7 @@ class AddCoolingProfile(Script):
"unit": "# ",
"default_value": 0,
"minimum_value": 0,
"enabled": "enable_bv_fan and bv_fan_speed_control_enable"
"enabled": "enable_bv_fan and bv_fan_speed_control_enable and enable_cooling_profile"
},
"bv_fan_speed":
{
@ -332,7 +347,7 @@ class AddCoolingProfile(Script):
"default_value": 50,
"maximum_value": 100,
"minimum_value": 0,
"enabled": "enable_bv_fan and bv_fan_speed_control_enable"
"enabled": "enable_bv_fan and bv_fan_speed_control_enable and enable_cooling_profile"
},
"bv_fan_start_layer":
{
@ -342,7 +357,7 @@ class AddCoolingProfile(Script):
"unit": "Layer# ",
"default_value": 1,
"minimum_value": 1,
"enabled": "enable_bv_fan and bv_fan_speed_control_enable"
"enabled": "enable_bv_fan and bv_fan_speed_control_enable and enable_cooling_profile"
},
"bv_fan_end_layer":
{
@ -352,7 +367,7 @@ class AddCoolingProfile(Script):
"unit": "Layer# ",
"default_value": -1,
"minimum_value": -1,
"enabled": "enable_bv_fan and bv_fan_speed_control_enable"
"enabled": "enable_bv_fan and bv_fan_speed_control_enable and enable_cooling_profile"
},
"enable_bv_fan":
{
@ -361,6 +376,14 @@ class AddCoolingProfile(Script):
"type": "bool",
"default_value": false,
"enabled": false
},
"jump_start_enable":
{
"label": "Enable Jump Starting",
"description": "Some fans take a bit of time to get up to speed. This option will enter a 75% fan speed line 5 lines before the M106 fan setting line.",
"type": "bool",
"default_value": false,
"enabled": "fan_layer_or_feature == 'by_feature' and enable_cooling_profile"
}
}
}"""
@ -402,12 +425,17 @@ class AddCoolingProfile(Script):
feature_fan_combing: Whether or not to shut the cooling fan off during travel moves.
the_start_layer: When in By Feature this is the user selected start of the fan changes.
the_end_layer: When in By Feature this is the user selected end of the fan changes
the_end_is_enabled: When in By Feature, if the fan control ends before the print ends, then this will enable the Final Fan Speed to carry through to the print end.
the_end_is_enabled: When in By Feature, if the fan control ends before the print ends, then this will enable the Final Fan Speed to carry through to the print end.
"""
# Exit if the gcode has been previously post-processed.
if ";POSTPROCESSED" in data[0]:
return data
# Return if the script is not enabled.
if not bool(self.getSettingValueByKey("enable_cooling_profile")):
return data
# Initialize variables that are buried in if statements.
t0_fan = " P0"; t1_fan = " P0"; t2_fan = " P0"; t3_fan = " P0"; is_multi_extr_print = True
@ -419,9 +447,8 @@ class AddCoolingProfile(Script):
except AttributeError:
pass
bed_adhesion = (self.extruder_list[0].getProperty("adhesion_type", "value"))
bed_adhesion = self.extruder_list[0].getProperty("adhesion_type", "value")
print_sequence = str(self.global_stack.getProperty("print_sequence", "value"))
# Assign the fan numbers to the tools
if self.extruder_count == 1:
is_multi_fan = False
@ -452,6 +479,8 @@ class AddCoolingProfile(Script):
if by_layer_or_feature == "by_layer":
# By layer doesn't do any feature search so there is no need to look for combing moves
feature_fan_combing = False
# Don't need to jump start fans when By Layer
self._jump_start = False
fan_list[0] = self.getSettingValueByKey("layer_fan_1")
fan_list[2] = self.getSettingValueByKey("layer_fan_2")
fan_list[4] = self.getSettingValueByKey("layer_fan_3")
@ -468,6 +497,7 @@ class AddCoolingProfile(Script):
# Assign the variable values if "By Feature"
elif by_layer_or_feature == "by_feature":
self._jump_start = self.getSettingValueByKey("jump_start_enable")
the_start_layer = self.getSettingValueByKey("feature_fan_start_layer") - 1
the_end_layer = self.getSettingValueByKey("feature_fan_end_layer")
try:
@ -691,6 +721,8 @@ class AddCoolingProfile(Script):
if layer_number == str(fan_list[num]):
layer = layer.replace(fan_lines[0],fan_lines[0] + "\n" + fan_list[num + 1] + str(t0_fan))
single_fan_data[l_index] = layer
if self._jump_start:
single_fan_data = self._jump_start_layer_cooling_fan(single_fan_data, layer_0_index)
return single_fan_data
# Multi-Fan "By Layer"
@ -759,6 +791,8 @@ class AddCoolingProfile(Script):
# Insure the fans get shut off if 'off_fan_speed' was enabled
if self.extruder_count > 1 and self.getSettingValueByKey("enable_off_fan_speed"):
multi_fan_data[-1] += "M106 S0 P1\nM106 S0 P0\n"
if self._jump_start:
multi_fan_data = self._jump_start_layer_cooling_fan(multi_fan_data, layer_0_index)
return multi_fan_data
# Single fan by feature
@ -786,12 +820,14 @@ class AddCoolingProfile(Script):
if feature_fan_combing == True:
modified_data += "M106 S0" + t0_fan + "\n"
modified_data += line + "\n"
# If an End Layer is defined and is less than the last layer then insert the Final Speed
if line == ";LAYER:" + str(the_end_layer) and the_end_is_enabled == True:
modified_data += feature_speed_list[len(feature_speed_list) - 1] + t0_fan + "\n"
if modified_data.endswith("\n"): modified_data = modified_data[0: - 1]
single_fan_data[l_index] = modified_data
if self._jump_start:
single_fan_data = self._jump_start_layer_cooling_fan(single_fan_data, layer_0_index)
return single_fan_data
# Multi-fan by feature
@ -890,6 +926,8 @@ class AddCoolingProfile(Script):
# Insure the fans get shut off if 'off_fan_speed' was enabled
if self.extruder_count > 1 and self.getSettingValueByKey("enable_off_fan_speed"):
multi_fan_data[-1] += "M106 S0 P1\nM106 S0 P0\n"
if self._jump_start:
multi_fan_data = self._jump_start_layer_cooling_fan(multi_fan_data, layer_0_index)
return multi_fan_data
# Try to catch layer input errors, set the minimum speed to 12%, and put the strings together
@ -1005,4 +1043,33 @@ class AddCoolingProfile(Script):
lines[fdex] = f"M106 S0 P{bv_fan_nr}\n" + line
bv_data[index] = "\n".join(lines)
break
return bv_data
return bv_data
def _jump_start_layer_cooling_fan(self, data: str, layer_0_index: int):
"""
If the fan is off - run the speed up to ~70% prior to setting the actual speed to insure the fan starts spinning.
"""
fan_mode = not bool(self.extruder_list[0].getProperty("machine_scale_fan_speed_zero_to_one", "value"))
fan_is_on = False
for lay_index, layer in enumerate(data):
if lay_index < layer_0_index:
continue
lines = layer.split("\n")
for index, line in enumerate(lines):
if line.startswith("M106 S0") or line.startswith("M107"):
fan_is_on = False
elif line.startswith("M106 S") and float(self.getValue(line, 'S')) != 0:
if not fan_is_on:
# Insert a jump start command a few lines before the first non-zero fan speed,
# without modifying existing comment/header lines in place.
if index >= 5:
insert_index = index - 5
else:
# Keep any initial header/comment line (e.g. ";LAYER:5") intact by
# inserting after it when possible.
insert_index = 1 if len(lines) > 1 else 0
jump_start_cmd = f"M106 S{self.jump_speed_marlin} ; Jump start" if fan_mode else "M106 S{self.jump_speed_reprap} ; Jump start"
lines.insert(insert_index, jump_start_cmd)
fan_is_on = True
data[lay_index] = "\n".join(lines)
return data

View file

@ -19,6 +19,7 @@ Copyright (c) 2025 GregValiant (Greg Foresi)
from UM.Application import Application
from ..Script import Script
from UM.Message import Message
from UM.Logger import Logger
class AnnealingOrDrying(Script):
@ -304,7 +305,7 @@ class AnnealingOrDrying(Script):
data = self._anneal_print(add_messages, data, bed_temperature, chamber_temp, heated_chamber, heating_zone, lowest_temp, max_x, max_y, max_z, park_xy, park_z, speed_travel)
elif cycle_type == "dry_cycle":
data = self._dry_filament_only(data, bed_temperature, chamber_temp, heated_chamber, heating_zone, max_y, max_z, speed_travel)
return data
def _anneal_print(
@ -325,7 +326,7 @@ class AnnealingOrDrying(Script):
"""
The procedure disables the M140 (and M141) lines at the end of the print, and adds additional bed (and chamber) temperature commands to the end of the G-Code file.
The bed is allowed to cool down over a period of time.
:param add_messages: Whether to include M117 and M118 messages for LCD and print server
:param anneal_data: The G-code data to be modified with annealing commands
:param bed_temperature: Starting bed temperature in degrees Celsius
@ -483,7 +484,7 @@ class AnnealingOrDrying(Script):
This procedure turns the bed on, homes the printer, parks the head. After the time period the bed is turned off.
There is no actual print in the generated gcode, just a couple of moves to get the nozzle out of the way, and the bed heat (and possibly chamber heat) control.
It allows a user to use the bed to warm up and hopefully dry a filament roll.
:param bed_temperature: Bed temperature for drying in degrees Celsius
:param chamber_temp: Chamber/build volume temperature for drying in degrees Celsius
:param drydata: The G-code data to be replaced with filament drying commands
@ -494,9 +495,14 @@ class AnnealingOrDrying(Script):
:param speed_travel: Travel speed for positioning moves in mm/min as string
:return: Modified G-code data containing only filament drying sequence
"""
machine_name = str(self.global_stack.getProperty("machine_name", "value"))
machine_gcode_flavor = str(self.global_stack.getProperty("machine_gcode_flavor", "value"))
active_machine = ""
if machine_gcode_flavor in ["Ultigcode", "Cheetah", "Griffin"] or "Ultimaker" in machine_name:
active_machine = "UM"
for num in range(2, len(drydata)):
drydata[num] = ""
drydata[0] = drydata[0].split("\n")[0] + "\n"
add_messages = bool(self.getSettingValueByKey("add_messages"))
pause_cmd = self.getSettingValueByKey("pause_cmd")
if pause_cmd != "":
@ -567,5 +573,31 @@ class AnnealingOrDrying(Script):
if heated_chamber and heating_zone == "bed_chamber":
dry_txt += "; Chamber temperature ....... " + str(chamber_temp) + "°\n"
Message(title = "[Dry Filament]", text = dry_txt).show()
drydata[0] = "; <<< This is a filament drying file only. There is no actual print. >>>\n;\n" + dry_txt + ";\n"
# UM machines require the existing 'data[0]' with some alteration
if active_machine == "UM":
drydata[0] = self.prep_for_um_machines(drydata, bed_temperature, chamber_temp, heating_zone, dry_time)
drydata[0] += ";\n; <<< This is a filament drying file only. There is no actual print. >>>\n;\n" + dry_txt + ";\n"
return drydata
def prep_for_um_machines(self, drydata: str, bed_temperature, chamber_temp, heating_zone: str, dry_time):
Message(title = "​⚠️⚠️⚠️ [Dry Filament] ⚠️⚠️⚠️​", text = f"Your printer appears to be a 'UltiMaker' printer. 'Dry Filament' might bring warnings up on the LCD screen (ex: for 'material compatibility'). You should be able to 'Ignore' them and the gcode should run. In some cases, the machine may not want to run the gcode. Experimenting may help get past that blockage. Do not place anything on the Build Plate until the machine has settled down and the bed is heating.").show()
# This will alter the bed/chamber/hot end temperatures for drying
lines = drydata[0].split("\n")
for index, line in enumerate(lines):
if ";BUILD_PLATE.INITIAL_TEMPERATURE:" in line:
lines[index] = f";BUILD_PLATE.INITIAL_TEMPERATURE:{bed_temperature}"
if ";EXTRUDER_TRAIN.0.INITIAL_TEMPERATURE:" in line:
lines[index] = f";EXTRUDER_TRAIN.0.INITIAL_TEMPERATURE:40"
if ";EXTRUDER_TRAIN.1.INITIAL_TEMPERATURE:" in line:
lines[index] = f";EXTRUDER_TRAIN.1.INITIAL_TEMPERATURE:40"
if ";BUILD_VOLUME.TEMPERATURE:" in line:
if heating_zone == "bed_chamber":
lines[index] = f";BUILD_VOLUME.TEMPERATURE:{chamber_temp}"
if ";PRINT.TIME:" in line:
lines[index] = f";PRINT.TIME:{dry_time}"
drydata[0] = "\n".join(lines)
# This line will tell the printer sto skip these processes. Skipping requires the G28 to be added.
um_line = ";SKIP_PROCEDURES:PRE_PRINT_SETUP,PURGE_MATERIAL_MISP,LOAD_MATERIAL_MISP,UNLOAD_MATERIAL_MISP_0,UNLOAD_MATERIAL_MISP_1,PREPARE_MISP_MATERIALS,DEPRIME_FOR_MATERIAL_CHANGE_MISP,SEND_FOLLOW_COMMAND_MISP,BREAK_FAILED_WIZARD,LOADING_FAILURE_RECOVERY_WIZARD,START_COOLDOWN_HOTEND,SET_HOTEND_TEMPERATURE_WAIT\nG28 X Y Z"
lines.insert(len(lines)-2, um_line)
drydata[0] = "\n".join(lines)
return drydata[0]

View file

@ -29,7 +29,9 @@ Display Filename and Layer on the LCD by Amanda de Castilho on August 28, 2018
- 'Add M118 Line' is available with either option. M118 will bounce the message back to a remote print server through the USB connection.
- 'Add M73 Line' is used by 'Display Progress' only. There are options to incluse M73 P(percent) and M73 R(time remaining)
- Enable 'Finish-Time' Message - when enabled, takes the Print Time and calculates when the print will end. It uses the Time Fudge Factor. The user may enter a print start time.
Date: June 30, 2025 Cost of electricity added to the other print statistics in '_add_stats'.
Date: June 30, 2025 Cost of electricity added to the other print statistics in '_add_stats'. (GregValiant)
Date: Sept 24, 2025 Disabled countdown to pauses when in 'One-at-a-Time' mode. (GregValiant)
Date: Jan 20, 2026 Added "weight" to the stats inserted in the Gcode.
"""
from ..Script import Script
@ -41,19 +43,27 @@ import math
from UM.Message import Message
class DisplayInfoOnLCD(Script):
def initialize(self) -> None:
minimumCuraVersion = 5110
def initialize(self) -> None:
super().initialize()
cura_version = Application.getInstance().getVersion()
cura_version_str = cura_version.split("-")[0]
cura_version_int = int(cura_version_str.replace(".", ""))
try:
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "all_at_once":
enable_countdown = True
self._instance.setProperty("enable_countdown", "value", enable_countdown)
except AttributeError:
else:
enable_countdown = False
self._instance.setProperty("enable_countdown", "value", enable_countdown)
if cura_version_int > self.minimumCuraVersion:
cura_adjust_percent = Application.getInstance().getGlobalContainerStack().getProperty("machine_time_estimation_factor", "value")
self._instance.setProperty("time_adj_percentage", "value", cura_adjust_percent)
except (AttributeError, KeyError):
# Handle cases where the global container stack or its properties are not accessible
pass
except KeyError:
# Handle cases where the "print_sequence" property is missing
pass
cura_adjust_percent = 100
def getSettingDataString(self):
return """{
@ -186,10 +196,10 @@ class DisplayInfoOnLCD(Script):
"default_value": false,
"enabled": "add_m73_line and display_option == 'display_progress' and display_remaining_time"
},
"speed_factor":
"time_adj_percentage":
{
"label": "Time Fudge Factor %",
"description": "When using 'Display Progress' tweak this value to get better estimates. ([Actual Print Time]/[Cura Estimate]) x 100 = Time Fudge Factor. If Cura estimated 9hr and the print actually took 10hr30min then enter 117 here to adjust any estimate closer to reality. This Fudge Factor is also used to calculate the print finish time.",
"label": "Print Time Estimation Factor",
"description": "When using 'Display Progress' adjust this value to get better time estimates. '([Actual Print Time]/[Cura Estimate]) x 100 = Time Estimation Factor'. This number should match the entry in the Cura Machine Settings but it can be different.",
"type": "float",
"unit": "%",
"default_value": 100,
@ -200,14 +210,13 @@ class DisplayInfoOnLCD(Script):
"label": "Enable Countdown to Pauses",
"description": "If print sequence is 'one_at_a_time' this is false. This setting is always hidden.",
"type": "bool",
"default_value": false,
"value": false,
"enabled": false
},
"countdown_to_pause":
{
"label": "Countdown to Pauses",
"description": "This must run AFTER any script that adds a pause. Instead of the remaining print time the LCD will show the estimated time to the next layer that has a pause (TP). Countdown to Pause is not available when in One-at-a-Time' mode.",
"label": "Countdown to Pause(s)",
"description": "This must run AFTER any script that adds a pause. Instead of the remaining print time the LCD will show the estimated time to the next layer that has a pause (TP). Countdown to Pause does not work in 'One-at-a-Time' mode.",
"type": "bool",
"default_value": false,
"enabled": "display_option == 'display_progress' and enable_countdown and display_remaining_time"
@ -240,7 +249,7 @@ class DisplayInfoOnLCD(Script):
"electricity_cost":
{
"label": "Electricity Cost per kWh",
"description": "Cost of electricity per kilowatt-hour. This should be on your electric utility bill.",
"description": "Cost of electricity per kilowatt-hour. This should be on your electric utility bill. The 'Electricity Cost' will be added to the gcode file.",
"type": "float",
"default_value": 0.151,
"minimum_value": 0,
@ -260,6 +269,7 @@ class DisplayInfoOnLCD(Script):
}"""
def execute(self, data):
self.global_stack = Application.getInstance().getGlobalContainerStack()
display_option = self.getSettingValueByKey("display_option")
self.add_m117_line = self.getSettingValueByKey("add_m117_line")
self.add_m118_line = self.getSettingValueByKey("add_m118_line")
@ -340,7 +350,7 @@ class DisplayInfoOnLCD(Script):
final_lines = "\n".join(lines)
data[layer_index] = final_lines
if bool(self.getSettingValueByKey("enable_end_message")):
message_str = self._message_to_user(self.getSettingValueByKey("speed_factor") / 100)
message_str = self._message_to_user(self.getSettingValueByKey("time_adj_percentage") / 100)
Message(title = "Display Info on LCD - Estimated Finish Time", text = message_str[0] + "\n\n" + message_str[1] + "\n" + message_str[2] + "\n" + message_str[3]).show()
return data
@ -350,9 +360,10 @@ class DisplayInfoOnLCD(Script):
data[0] = self._add_stats(data)
# Get settings
print_sequence = Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value")
countdown_to_pause = bool(self.getSettingValueByKey("countdown_to_pause")) if print_sequence == "all_at_once" else False
display_total_layers = self.getSettingValueByKey("display_total_layers")
display_remaining_time = self.getSettingValueByKey("display_remaining_time")
speed_factor = self.getSettingValueByKey("speed_factor") / 100
time_adj_percentage = self.getSettingValueByKey("time_adj_percentage") / 100
m73_time = False
m73_percent = False
if self.add_m73_line and self.add_m73_time:
@ -375,7 +386,7 @@ class DisplayInfoOnLCD(Script):
if line.startswith(";TIME:"):
tindex = lines.index(line)
cura_time = int(line.split(":")[1])
print_time = cura_time * speed_factor
print_time = cura_time * time_adj_percentage
hhh = print_time/3600
hr = round(hhh // 1)
mmm = round((hhh % 1) * 60)
@ -396,7 +407,7 @@ class DisplayInfoOnLCD(Script):
lines.insert(tindex + 4, "M73" + self.m73_str)
# If Countdown to pause is enabled then count the pauses
pause_str = ""
if bool(self.getSettingValueByKey("countdown_to_pause")):
if countdown_to_pause:
pause_count = 0
pause_setting = self.getSettingValueByKey("pause_cmd").upper()
if pause_setting != "":
@ -420,7 +431,7 @@ class DisplayInfoOnLCD(Script):
if self.add_m117_line:
data[len(data)-1] += "M117 Orig Cura Est " + str(orig_hr) + "hr " + str(orig_mmm) + "min\n"
if self.add_m118_line:
data[len(data)-1] += "M118 Est w/FudgeFactor " + str(speed_factor * 100) + "% was " + str(hr) + "hr " + str(mmm) + "min\n"
data[len(data)-1] += "M118 Est w/FudgeFactor " + str(time_adj_percentage * 100) + "% was " + str(hr) + "hr " + str(mmm) + "min\n"
if not display_total_layers or not display_remaining_time:
base_display_text = "layer "
else:
@ -463,7 +474,7 @@ class DisplayInfoOnLCD(Script):
if display_remaining_time:
time_remaining_display = " | ET " # initialize the time display
m = (self.time_total - time_elapsed) // 60 # estimated time in minutes
m *= speed_factor # correct for printing time
m *= time_adj_percentage # correct for printing time
m = int(m)
h, m = divmod(m, 60) # convert to hours and minutes
# add the time remaining to the display_text
@ -523,7 +534,7 @@ class DisplayInfoOnLCD(Script):
lines = layer.split("\n")
for line in lines:
if line.startswith(";TIME_ELAPSED:"):
this_time = (float(line.split(":")[1]))*speed_factor
this_time = (float(line.split(":")[1]))*time_adj_percentage
time_list.append(str(this_time))
for p_cmd in pause_cmd:
if p_cmd in layer:
@ -533,7 +544,7 @@ class DisplayInfoOnLCD(Script):
break
# Make the adjustments to the M117 (and M118) lines that are prior to a pause
for num in range (2, len(data) - 1,1):
for num in range (2, len(data) - 1):
layer = data[num]
lines = layer.split("\n")
for line in lines:
@ -550,16 +561,19 @@ class DisplayInfoOnLCD(Script):
continue
data[num] = layer
if bool(self.getSettingValueByKey("enable_end_message")):
message_str = self._message_to_user(data, speed_factor, pause_cmd)
Message(title = "[Display Info on LCD] - Estimated Finish Time", text = message_str[0] + "\n\n" + message_str[1] + "\n" + message_str[2] + "\n" + message_str[3]).show()
message_str = self._message_to_user(data, time_adj_percentage, pause_cmd)
electric_use = self.getElectricCostLine()
Message(title = "[Display Info on LCD] - Estimated Finish Time", text = message_str[0] + "\n\n" + message_str[1] + "\n" + message_str[2] + "\n" + message_str[3] + "\n" + electric_use).show()
return data
def _message_to_user(self, data: str, speed_factor: float, pause_cmd: str) -> str:
def _message_to_user(self, data: str, time_adj_percentage: float, pause_cmd: str) -> str:
"""
Message the user of the projected finish time of the print and when any pauses might occur
"""
print_time = Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)
print_start_time = self.getSettingValueByKey("print_start_time")
if print_start_time == "":
print_start_time = datetime.datetime.now().strftime("%H:%M")
# If the user entered a print start time make sure it is in the correct format or ignore it.
if print_start_time == "" or print_start_time == "0" or len(print_start_time) != 5 or not ":" in print_start_time:
print_start_time = ""
@ -586,7 +600,7 @@ class DisplayInfoOnLCD(Script):
#Adjust the print time if none was entered
print_seconds = pr_hr*3600 + pr_min*60 + pr_sec
#Adjust the total seconds by the Fudge Factor
adjusted_print_time = print_seconds * speed_factor
adjusted_print_time = print_seconds * time_adj_percentage
#Break down the adjusted seconds back into hh:mm:ss
adj_hr = int(adjusted_print_time/3600)
print_seconds = adjusted_print_time - (adj_hr * 3600)
@ -618,7 +632,17 @@ class DisplayInfoOnLCD(Script):
else:
print_start_str = "Print Start Time.................Now"
estimate_str = "Cura Time Estimate.........." + str(print_time)
adjusted_str = "Adjusted Time Estimate..." + str(time_change)
# Set a default value for compatibility with earlier versions
try:
cura_adjust_percent = int(Application.getInstance().getGlobalContainerStack().getProperty("machine_time_estimation_factor", "value"))
except (NameError, ValueError, TypeError):
cura_adjust_percent = 100
if cura_adjust_percent == 100:
adjusted_str = "Adjusted Time Estimate..." + str(time_change)
else:
adjusted_str = f"(Time adjusted to {cura_adjust_percent}% per Cura Machine Settings)"
if time_adj_percentage != cura_adjust_percent:
adjusted_str += f"\n(Gcode was adjusted by the script setting of {time_adj_percentage * 100}%)"
finish_str = f"{week_day} {mo_str} {new_time.strftime('%d')}, {new_time.strftime('%Y')} at {show_hr}{new_time.strftime('%M')}{show_ampm}"
# If there are pauses and if countdown is enabled, then add the time-to-pause to the message.
@ -649,7 +673,6 @@ class DisplayInfoOnLCD(Script):
return time_to_go
def _add_stats(self, data: str) -> str:
global_stack = Application.getInstance().getGlobalContainerStack()
"""
Make a list of the models in the file.
Add some of the filament stats to the first section of the gcode.
@ -663,50 +686,62 @@ class DisplayInfoOnLCD(Script):
if not model_name in model_list:
model_list.append(model_name)
# Filament stats
extruder_count = global_stack.getProperty("machine_extruder_count", "value")
layheight_0 = global_stack.getProperty("layer_height_0", "value")
extruder_count = self.global_stack.getProperty("machine_extruder_count", "value")
layheight_0 = self.global_stack.getProperty("layer_height_0", "value")
init_layer_hgt_line = ";Initial Layer Height: " + f"{layheight_0:.2f}".format(layheight_0)
filament_line_t0 = ";Extruder 1 (T0)\n"
filament_amount = Application.getInstance().getPrintInformation().materialLengths
filament_line_t0 += f"; Filament used: {filament_amount[0]}m\n"
filament_line_t0 += f"; Filament Type: {global_stack.extruderList[0].material.getMetaDataEntry("material", "")}\n"
filament_line_t0 += f"; Filament Dia.: {global_stack.extruderList[0].getProperty("material_diameter", "value")}mm\n"
filament_line_t0 += f"; Nozzle Size : {global_stack.extruderList[0].getProperty("machine_nozzle_size", "value")}mm\n"
filament_line_t0 += f"; Print Temp. : {global_stack.extruderList[0].getProperty("material_print_temperature", "value")}°\n"
filament_line_t0 += f"; Bed Temp. : {global_stack.extruderList[0].getProperty("material_bed_temperature", "value")}°"
filament_weight = Application.getInstance().getPrintInformation().materialWeights
filament_line_t0 += f"; Filament used: {filament_amount[0]}m ({round(filament_weight[0], 2)}g)\n"
filament_line_t0 += f"; Filament Type: {self.global_stack.extruderList[0].material.getMetaDataEntry("material", "")}\n"
filament_line_t0 += f"; Filament Dia.: {self.global_stack.extruderList[0].getProperty("material_diameter", "value")}mm\n"
filament_line_t0 += f"; Nozzle Size : {self.global_stack.extruderList[0].getProperty("machine_nozzle_size", "value")}mm\n"
filament_line_t0 += f"; Print Temp. : {self.global_stack.extruderList[0].getProperty("material_print_temperature", "value")}°\n"
filament_line_t0 += f"; Bed Temp. : {self.global_stack.extruderList[0].getProperty("material_bed_temperature", "value")}°"
# if there is more than one extruder then get the stats for the second one.
filament_line_t1 = ""
if extruder_count > 1:
filament_line_t1 = "\n;Extruder 2 (T1)\n"
filament_line_t1 += f"; Filament used: {filament_amount[1]}m\n"
filament_line_t1 += f"; Filament Type: {global_stack.extruderList[1].material.getMetaDataEntry("material", "")}\n"
filament_line_t1 += f"; Filament Dia.: {global_stack.extruderList[1].getProperty("material_diameter", "value")}mm\n"
filament_line_t1 += f"; Nozzle Size : {global_stack.extruderList[1].getProperty("machine_nozzle_size", "value")}mm\n"
filament_line_t1 += f"; Print Temp. : {global_stack.extruderList[1].getProperty("material_print_temperature", "value")}°"
filament_line_t1 += f"; Filament used: {filament_amount[1]}m ({round(filament_weight[1], 2)}g)\n"
filament_line_t1 += f"; Filament Type: {self.global_stack.extruderList[1].material.getMetaDataEntry("material", "")}\n"
filament_line_t1 += f"; Filament Dia.: {self.global_stack.extruderList[1].getProperty("material_diameter", "value")}mm\n"
filament_line_t1 += f"; Nozzle Size : {self.global_stack.extruderList[1].getProperty("machine_nozzle_size", "value")}mm\n"
filament_line_t1 += f"; Print Temp. : {self.global_stack.extruderList[1].getProperty("material_print_temperature", "value")}°"
# Calculate the cost of electricity for the print
electricity_cost = self.getSettingValueByKey("electricity_cost")
printer_power_usage = self.getSettingValueByKey("printer_power_usage")
currency_unit = Application.getInstance().getPreferences().getValue("cura/currency")
total_cost_electricity = (printer_power_usage / 1000) * (self.time_total / 3600) * electricity_cost
electric_line = self.getElectricCostLine()
# Add the stats to the gcode file
lines = data[0].split("\n")
# This is a switch to avoid a 'double insertion' in a Marlin file.
lines_inserted = False
for index, line in enumerate(lines):
if line.startswith(";Layer height:") or line.startswith(";TARGET_MACHINE.NAME:"):
lines[index] = ";Layer height: " + f"{global_stack.getProperty("layer_height", "value")}"
lines[index] += f"\n{init_layer_hgt_line}"
lines[index] += f"\n;Base Quality Name : '{global_stack.quality.getMetaDataEntry("name", "")}'"
lines[index] += f"\n;Custom Quality Name: '{global_stack.qualityChanges.getMetaDataEntry("name")}'"
if not lines_inserted:
lines[index] = ";Layer height: " + f"{self.global_stack.getProperty("layer_height", "value")}"
lines[index] += f"\n{init_layer_hgt_line}"
lines[index] += f"\n;Base Quality Name : '{self.global_stack.quality.getMetaDataEntry("name", "")}'"
lines[index] += f"\n;Custom Quality Name: '{self.global_stack.qualityChanges.getMetaDataEntry("name")}'"
lines_inserted = True
if line.startswith(";Filament used"):
lines[index] = filament_line_t0 + filament_line_t1 + f"\n;Electric Cost: {currency_unit}{total_cost_electricity:.2f}".format(total_cost_electricity)
lines[index] = filament_line_t0 + filament_line_t1 + "\n;" + electric_line
# The target machine "machine_name" is actually the printer model. This adds the user defined printer name to the "TARGET_MACHINE" line.
if line.startswith(";TARGET_MACHINE"):
machine_model = str(global_stack.getProperty("machine_name", "value"))
machine_name = str(global_stack.getName())
machine_model = str(self.global_stack.getProperty("machine_name", "value"))
machine_name = str(self.global_stack.getName())
lines[index] += f" / {machine_name}"
if "MINX" in line or "MIN.X" in line:
# Add the Object List
lines[index - 1] += f"\n;Model List: {str(model_list)}"
return "\n".join(lines)
return "\n".join(lines)
def getElectricCostLine(self) -> str:
electricity_cost = self.getSettingValueByKey("electricity_cost")
printer_power_usage = self.getSettingValueByKey("printer_power_usage")
if electricity_cost == 0 or printer_power_usage == 0:
return ""
currency_unit = Application.getInstance().getPreferences().getValue("cura/currency")
total_cost_electricity = (printer_power_usage / 1000) * (self.time_total / 3600) * electricity_cost
electric_line = f"Electric Cost: {currency_unit}{total_cost_electricity:.2f}"
return electric_line

View file

@ -3,10 +3,13 @@
# Modification 06.09.2020
# add checkbox, now you can choose and use configuration from the firmware itself.
# Modification 01.13.2026 by @hobbe
# convert G-code Before and G-Code After inputs to text areas (#21298).
# Modification 01.17.2026 by @GregValiant
# Move the inserted text below the ";LAYER:??" line.
from typing import List
from ..Script import Script
from UM.Application import Application # To get the current printer's settings.
class FilamentChange(Script):
@ -24,21 +27,21 @@ class FilamentChange(Script):
"version": 2,
"settings":
{
"enabled":
"enable_filament_change":
{
"label": "Enable",
"description": "Uncheck to temporarily disable this feature.",
"label": "Enable 'Filament Change'",
"description": "Uncheck to disable this feature.",
"type": "bool",
"default_value": true
},
"layer_number":
{
"label": "Layer",
"description": "At what layer should color change occur. This will be before the layer starts printing. Specify multiple color changes with a comma.",
"unit": "",
"description": "The layer(s) where the color change occurs. Enter the layer number from the Cura Preview. The change will occur at the START of the layer(s). Delimit multiple 'Layer Numbers' with commas (Ex: 12,25,134).",
"unit": "Lay#",
"type": "str",
"default_value": "1",
"enabled": "enabled"
"enabled": "enable_filament_change"
},
"firmware_config":
{
@ -46,7 +49,7 @@ class FilamentChange(Script):
"description": "Use the settings in your firmware, or customise the parameters of the filament change here.",
"type": "bool",
"default_value": false,
"enabled": "enabled"
"enabled": "enable_filament_change"
},
"initial_retract":
{
@ -55,7 +58,7 @@ class FilamentChange(Script):
"unit": "mm",
"type": "float",
"default_value": 30.0,
"enabled": "enabled and not firmware_config"
"enabled": "enable_filament_change and not firmware_config"
},
"later_retract":
{
@ -64,7 +67,7 @@ class FilamentChange(Script):
"unit": "mm",
"type": "float",
"default_value": 300.0,
"enabled": "enabled and not firmware_config"
"enabled": "enable_filament_change and not firmware_config"
},
"x_position":
{
@ -73,7 +76,7 @@ class FilamentChange(Script):
"unit": "mm",
"type": "float",
"default_value": 0,
"enabled": "enabled and not firmware_config"
"enabled": "enable_filament_change and not firmware_config"
},
"y_position":
{
@ -82,7 +85,7 @@ class FilamentChange(Script):
"unit": "mm",
"type": "float",
"default_value": 0,
"enabled": "enabled and not firmware_config"
"enabled": "enable_filament_change and not firmware_config"
},
"z_position":
{
@ -92,7 +95,7 @@ class FilamentChange(Script):
"type": "float",
"default_value": 0,
"minimum_value": 0,
"enabled": "enabled and not firmware_config"
"enabled": "enable_filament_change and not firmware_config"
},
"retract_method":
{
@ -101,8 +104,8 @@ class FilamentChange(Script):
"type": "enum",
"options": {"U": "Marlin (M600 U)", "L": "Reprap (M600 L)"},
"default_value": "U",
"enabled": "enabled and not firmware_config"
},
"enabled": "enable_filament_change and not firmware_config"
},
"machine_gcode_flavor":
{
"label": "G-code flavor",
@ -129,16 +132,16 @@ class FilamentChange(Script):
"description": "Use this to insert a custom G-code macro before the filament change happens",
"type": "bool",
"default_value": false,
"enabled": "enabled"
"enabled": "enable_filament_change"
},
"before_macro":
{
"label": "G-code Before",
"description": "Any custom G-code to run before the filament change happens, for example, M300 S1000 P10000 for a long beep.",
"unit": "",
"type": "str",
"comments": "multiline",
"default_value": "M300 S1000 P10000",
"enabled": "enabled and enable_before_macro"
"enabled": "enable_filament_change and enable_before_macro"
},
"enable_after_macro":
{
@ -146,16 +149,16 @@ class FilamentChange(Script):
"description": "Use this to insert a custom G-code macro after the filament change",
"type": "bool",
"default_value": false,
"enabled": "enabled"
"enabled": "enable_filament_change"
},
"after_macro":
{
"label": "G-code After",
"description": "Any custom G-code to run after the filament has been changed right before continuing the print, for example, you can add a sequence to purge filament and wipe the nozzle.",
"unit": "",
"type": "str",
"comments": "multiline",
"default_value": "M300 S440 P500",
"enabled": "enabled and enable_after_macro"
"enabled": "enable_filament_change and enable_after_macro"
}
}
}"""
@ -171,20 +174,19 @@ class FilamentChange(Script):
for key in ["machine_gcode_flavor"]:
self._instance.setProperty(key, "value", global_container_stack.getProperty(key, "value"))
# Set retract method based on gcode flavor
gcode_flavor = global_container_stack.getProperty("machine_gcode_flavor", "value")
if gcode_flavor == "RepRap (RepRap)":
self._instance.setProperty("retract_method", "value", "L")
else:
self._instance.setProperty("retract_method", "value", "U")
def execute(self, data: List[str]):
"""Inserts the filament change g-code at specific layer numbers.
:param data: A list of layers of g-code.
:return: A similar list, with filament change commands inserted.
"""
enabled = self.getSettingValueByKey("enabled")
enabled = self.getSettingValueByKey("enable_filament_change")
# Return if the script is not enabled
if not enabled:
return data
# Initialize variables
layer_nums = self.getSettingValueByKey("layer_number")
initial_retract = self.getSettingValueByKey("initial_retract")
later_retract = self.getSettingValueByKey("later_retract")
@ -197,50 +199,50 @@ class FilamentChange(Script):
enable_after_macro = self.getSettingValueByKey("enable_after_macro")
after_macro = self.getSettingValueByKey("after_macro")
if not enabled:
return data
color_change = ";BEGIN FilamentChange plugin\n"
# Put together the insertion string
color_change = "\n;BEGIN FilamentChange\n"
if enable_before_macro:
color_change = color_change + before_macro + "\n"
color_change += before_macro + "\n"
color_change = color_change + "M600"
color_change += "M600"
if not firmware_config:
if initial_retract is not None and initial_retract > 0.:
color_change = color_change + (" E%.2f" % initial_retract)
color_change += (" E%.2f" % initial_retract)
if later_retract is not None and later_retract > 0.:
# Reprap uses 'L': https://reprap.org/wiki/G-code#M600:_Filament_change_pause
# Marlin uses 'U' https://marlinfw.org/docs/gcode/M600.html
retract_method = self.getSettingValueByKey("retract_method")
color_change = color_change + (" %s%.2f" % (retract_method, later_retract))
color_change += (" %s%.2f" % (retract_method, later_retract))
if x_pos is not None:
color_change = color_change + (" X%.2f" % x_pos)
color_change += (" X%.2f" % x_pos)
if y_pos is not None:
color_change = color_change + (" Y%.2f" % y_pos)
color_change += (" Y%.2f" % y_pos)
if z_pos is not None and z_pos > 0.:
color_change = color_change + (" Z%.2f" % z_pos)
color_change = color_change + "\n"
color_change += (" Z%.2f" % z_pos)
color_change += "\n"
if enable_after_macro:
color_change = color_change + after_macro + "\n"
color_change += after_macro + "\n"
color_change = color_change + ";END FilamentChange plugin\n"
color_change += ";END FilamentChange\n"
# Make the insertions
layer_targets = layer_nums.split(",")
if len(layer_targets) > 0:
for layer_num in layer_targets:
try:
layer_num = int(layer_num.strip()) + 1 #Needs +1 because the 1st layer is reserved for start g-code.
except ValueError: #Layer number is not an integer.
continue
if 0 < layer_num < len(data):
data[layer_num] = color_change + data[layer_num]
for index, layer in enumerate(data):
for layer_num in layer_targets:
try:
layer_str = f"{self._layer_keyword}{int(layer_num.strip()) - 1}\n"
except ValueError: #Layer number is not an integer.
continue
if layer_str in data[index]:
layer_of_interest = data[index].split("\n")
layer_of_interest.insert(1, color_change)
data[index] = "\n".join(layer_of_interest)
return data

View file

@ -12,8 +12,8 @@
~Added support for Relative Extrusion
~Added support for Firmware Retraction
~Added support for 'G2' and 'G3' moves.
~The script supports a maximum of 2 extruders.
~'One-at-a-Time' is not supported and a kick-out is added
~The script supports a maximum of 2 extruders. You can have more, but they are ignored.
(see change note below) ~'One-at-a-Time' is now supported
Previous contributions by:
Original Authors and contributors to the ChangeAtZ post-processing script and the earlier TweakAtZ:
@ -25,6 +25,7 @@
Modified by Wes Hanney, Retract Length + Speed, Clean up
Modified by Alex Jaxon, Added option to modify Build Volume Temperature
Re-write by GregValiant, to work with new variables in Cura 5.x and with the changes noted above
Dec 12, 2025 ~ Added support for 'One at a Time' print sequence.
"""
from UM.Application import Application
@ -34,7 +35,11 @@ from UM.Message import Message
from UM.Logger import Logger
class TweakAtZ(Script):
version = "2.0.0"
version = "2.1" # Updated from 2.0 on Dec 12, 2025
# These are for one-at-a-time mode
start_list = []
end_list = []
print_start_adj = 2 # Accounts for the first layer being in data[2]
def initialize(self) -> None:
"""
@ -63,7 +68,7 @@ class TweakAtZ(Script):
def getSettingDataString(self):
return """{
"name": "Tweak At Z (2.0)",
"name": "Tweak at Z (""" + self.version + """)",
"key": "TweakAtZ",
"metadata": {},
"version": 2,
@ -75,6 +80,13 @@ class TweakAtZ(Script):
"default_value": true,
"enabled": true
},
"enable_one_at_a_time_mode": {
"label": "Enable 'One at a Time' mode",
"description": "If your print sequence is 'One at a Time' enable this so each model will have its own adjustments. The models are treated as if they are all the same. If you have different models and model heights you will need to experiment. NOTE: This setting has no effect on 'All at Once' prints.",
"type": "bool",
"default_value": false,
"enabled": "taz_enabled"
},
"by_layer_or_height": {
"label": "'By Layer' or 'By Height'",
"description": "Which criteria to use to start and end the changes.",
@ -352,13 +364,6 @@ class TweakAtZ(Script):
Logger.log("i", "[Tweak at Z] is not enabled")
return data
# Message the user and exit if the print sequence is 'One at a Time'
if self.global_stack.getProperty("print_sequence", "value") == "one_at_a_time":
Message(title = "[Tweak at Z]", text = "One-at-a-Time mode is not supported. The script will exit without making any changes.").show()
data[0] += "; [Tweak at Z] Did not run (One at a Time mode is not supported)\n"
Logger.log("i", "TweakAtZ does not support 'One at a Time' mode")
return data
# Exit if the gcode has been previously post-processed.
if ";POSTPROCESSED" in data[0]:
return data
@ -375,6 +380,11 @@ class TweakAtZ(Script):
self.orig_bv_temp = self.global_stack.getProperty("build_volume_temperature", "value")
self.z_hop_enabled = bool(self.extruder_list[0].getProperty("retraction_hop_enabled", "value"))
self.raft_enabled = True if str(self.global_stack.getProperty("adhesion_type", "value")) == "raft" else False
# Script settings
self.print_sequence = self.global_stack.getProperty("print_sequence", "value")
self.by_layer_or_height = self.getSettingValueByKey("by_layer_or_height")
# The Start and end layer numbers are used when 'By Layer' is selected
self.start_layer = self.getSettingValueByKey("a_start_layer") - 1
end_layer = int(self.getSettingValueByKey("a_end_layer"))
@ -451,9 +461,34 @@ class TweakAtZ(Script):
}
# Run the selected procedures
for setting, method in procedures.items():
if self.getSettingValueByKey(setting):
method(data)
if self.print_sequence == "all_at_once":
for setting, method in procedures.items():
if self.getSettingValueByKey(setting):
method(data)
elif self.print_sequence == "one_at_a_time" and bool(self.getSettingValueByKey("enable_one_at_a_time_mode")):
# Put together a list of the starts and ends of each model on the build plate.
starts_and_ends = self.one_at_a_time_lists(data)
# To accommodate existing code the temp_start and temp_end were necessary
temp_start = self.start_index
temp_end = self.end_index
"""
Make adjustments to the start and end indexes based on the indexes of the LAYER:0's
This was required to get One at a Time to work with By Height and By Layer.
"""
for num in range(0, len(self.start_list)):
self.start_index = self.start_list[num] + temp_start - self.print_start_adj
if end_layer != -1 or self.by_layer_or_height == "by_height":
self.end_index = self.start_list[num] + temp_end - self.print_start_adj
elif end_layer == -1 and self.by_layer_or_height == "by_layer":
self.end_index = self.end_list[num]
# Run each procedure with different starts and ends
for setting, method in procedures.items():
if self.getSettingValueByKey(setting):
method(data)
self.start_index = temp_start
self.end_index = temp_end
data = self._format_lines(data)
return data
@ -744,10 +779,6 @@ class TweakAtZ(Script):
if not self.retract_enabled:
return
# Exit if neither child setting is checked.
if not (change_retract_amt or change_retract_speed):
return
speed_retract_0 = int(self.extruder_list[0].getProperty("retraction_speed", "value") * 60)
retract_amt_0 = self.extruder_list[0].getProperty("retraction_amount", "value")
change_retract_amt = self.getSettingValueByKey("g_change_retract_amount")
@ -755,6 +786,10 @@ class TweakAtZ(Script):
new_retract_speed = int(self.getSettingValueByKey("g_retract_speed") * 60)
new_retract_amt = self.getSettingValueByKey("g_retract_amount")
# Exit if neither child setting is checked.
if not (change_retract_amt or change_retract_speed):
return
# Use M207 and M208 to adjust firmware retraction when required
if self.firmware_retraction:
lines = data[self.start_index].splitlines()
@ -987,3 +1022,23 @@ class TweakAtZ(Script):
def _f_x_y_not_z(self, line):
return " F" in line and " X" in line and " Y" in line and not " Z" in line
def one_at_a_time_lists(self, data):
"""
This puts together a list of starts and their related list of ends.
This is required for One-at-a-Time mode.
"""
self.start_list = []
self.end_list = []
for index, layer in enumerate(data):
if index < 2 or index > len(data) - 1:
continue
if ";LAYER:0" in layer:
self.start_list.append(index)
self.end_list.append(index - 1)
if "M140 S0" in layer:
self.end_list.append(index-1)
break
# Delete the initial stop index as it is not required. This also leaves both lists the same length.
self.end_list.pop(0)
return True

View file

@ -30,7 +30,7 @@ from UM.Logger import Logger
class ZHopOnTravel(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name": "Z-Hop on Travel",
@ -40,7 +40,7 @@ class ZHopOnTravel(Script):
"settings": {
"zhop_travel_enabled": {
"label": "Enable script",
"description": "Enables the script so it will run. 'One-at-a-Time' is not supported. This script is slow running because it must check the length of all travel moves in your layer range. ",
"description": "Enables the script so it will run. 'One-at-a-Time' is not supported. This script is slow running because it must check the length of all travel moves in your layer range.",
"type": "bool",
"default_value": true,
"enabled": true
@ -112,10 +112,17 @@ class ZHopOnTravel(Script):
},
"infill_only": {
"label": "Add Z-hops to Infill Only",
"description": "Only add Z-hops to 'Infill' within the layer range. (NOTE: For technical reasons it is not possible to add Z-hops to travel moves that start somewhere and just 'cross infill'.)",
"description": "Only add Z-hops to 'Infill' within the layer range. (NOTE1: For technical reasons it is not possible to add Z-hops to travel moves that start somewhere and just 'move across infill'.) (NOTE2: You may use 'Infill Only' or 'Interface Only', but not both in a single instance of this post-processor. If you want both then add another instance.)",
"type": "bool",
"default_value": false,
"enabled": "zhop_travel_enabled"
"enabled": "zhop_travel_enabled and not interface_only"
},
"interface_only": {
"label": "Add Z-hops to Support Interface Only",
"description": "Only add Z-hops to 'Support Interface' within the layer range. (NOTE1: For technical reasons it is not possible to add Z-hops to travel moves that start somewhere and just 'cross the interface'.) (NOTE2: You may use 'Infill Only' or 'Interface Only', but not both in a single instance of this post-processor. If you want both then add another instance.)",
"type": "bool",
"default_value": false,
"enabled": "zhop_travel_enabled and not infill_only"
}
}
}"""
@ -176,6 +183,7 @@ class ZHopOnTravel(Script):
hop_height = round(self.getSettingValueByKey("hop_height"),2)
list_or_range = self.getSettingValueByKey("list_or_range")
infill_only = self.getSettingValueByKey("infill_only")
interface_only = self.getSettingValueByKey("interface_only")
layer_list = []
index_list = []
@ -250,6 +258,7 @@ class ZHopOnTravel(Script):
# Make the insertions
in_the_infill = False
in_the_interface = False
for num in range(start_index, len(data)-1):
# Leave if the num > highest index number to speed up the script.
if num > index_list[len(index_list)-1]:
@ -272,6 +281,10 @@ class ZHopOnTravel(Script):
in_the_infill = False
if line.startswith(";TYPE:FILL"):
in_the_infill = True
if line.startswith(";") and in_the_interface == True:
in_the_interface = False
if line.startswith(";TYPE:SUPPORT-INTERFACE"):
in_the_interface = True
if line.startswith("G92") and " E" in line:
self._cur_e = self.getValue(line, "E")
self._prev_e = self._cur_e
@ -313,6 +326,9 @@ class ZHopOnTravel(Script):
if infill_only and not in_the_infill:
hop_start = 0
hop_end = 0
if interface_only and not in_the_interface:
hop_start = 0
hop_end = 0
if hop_start > 0:
# For any lines that are XYZ moves right before layer change
if " Z" in line:

View file

@ -181,10 +181,16 @@ class CloudApiClient:
body = json.dumps({"data": data}).encode() if data else b""
url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/print_jobs/{cluster_job_id}/action/{action}"
def on_error(cloud_error: CloudError, error: "QNetworkReply.NetworkError", http_code: int):
cloud_error_str = f"{cloud_error.title} (id:{cloud_error.id} code:{cloud_error.code}) {cloud_error.detail} ({cloud_error.meta})"
Logger.warning(f"CloudApiClient.doPrintJobAction failed for {url} with {http_code}: {cloud_error_str}")
self._http.post(url,
scope=self._scope,
data=body,
timeout=self.DEFAULT_REQUEST_TIMEOUT)
timeout=self.DEFAULT_REQUEST_TIMEOUT,
error_callback=self._parseError(on_error))
def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
"""We override _createEmptyRequest in order to add the user credentials.

View file

@ -90,19 +90,22 @@ class ClusterApiClient:
"""Move a print job to the top of the queue."""
url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.post(self.createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode())
reply = self._manager.post(self.createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode())
self._trackReply(reply)
def forcePrintJob(self, print_job_uuid: str) -> None:
"""Override print job configuration and force it to be printed."""
url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.put(self.createEmptyRequest(url), json.dumps({"force": True}).encode())
reply = self._manager.put(self.createEmptyRequest(url), json.dumps({"force": True}).encode())
self._trackReply(reply)
def deletePrintJob(self, print_job_uuid: str) -> None:
"""Delete a print job from the queue."""
url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.deleteResource(self.createEmptyRequest(url))
reply = self._manager.deleteResource(self.createEmptyRequest(url))
self._trackReply(reply)
def setPrintJobState(self, print_job_uuid: str, state: str) -> None:
"""Set the state of a print job."""
@ -110,7 +113,8 @@ class ClusterApiClient:
url = "{}/print_jobs/{}/action".format(self.CLUSTER_API_PREFIX, print_job_uuid)
# We rewrite 'resume' to 'print' here because we are using the old print job action endpoints.
action = "print" if state == "resume" else state
self._manager.put(self.createEmptyRequest(url), json.dumps({"action": action}).encode())
reply = self._manager.put(self.createEmptyRequest(url), json.dumps({"action": action}).encode())
self._trackReply(reply)
def getPrintJobPreviewImage(self, print_job_uuid: str, on_finished: Callable) -> None:
"""Get the preview image data of a print job."""
@ -169,6 +173,25 @@ class ClusterApiClient:
except (JSONDecodeError, TypeError, ValueError):
Logger.log("e", "Could not parse response from network: %s", str(response))
def _trackReply(self, reply: QNetworkReply) -> None:
"""Track a reply to prevent garbage collection and ensure proper completion.
Use this for fire-and-forget requests that don't need response handling.
:param reply: The reply that should be tracked.
"""
def cleanup() -> None:
try:
self._anti_gc_callbacks.remove(cleanup)
except ValueError:
return
if reply.error() != QNetworkReply.NetworkError.NoError:
Logger.warning(f"Cluster API request error: {reply.errorString()}")
self._anti_gc_callbacks.append(cleanup)
reply.finished.connect(cleanup)
def _addCallback(self, reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any],
Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None,
) -> None:

View file

@ -0,0 +1,96 @@
# Copyright (c) 2026 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
import configparser
import io
from typing import Tuple, List
from UM.VersionUpgrade import VersionUpgrade
_REMOVED_SETTINGS = {
"skin_edge_support_thickness",
"skin_edge_support_layers",
"extra_infill_lines_to_support_skins",
"bridge_sparse_infill_max_density",
}
_HIGH_SPEED_PRINTERS = {
"ultimaker_factor4",
}
_OVERLAPPING_INFILLS = {
"grid",
"triangles",
"cubic"
}
_NEW_SETTING_VERSION = "26"
class VersionUpgrade511to512(VersionUpgrade):
def upgradePreferences(self, serialized: str, filename: str):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
# Remove deleted settings from the visible settings list.
if "general" in parser and "visible_settings" in parser["general"]:
visible_settings = set(parser["general"]["visible_settings"].split(";"))
for removed in _REMOVED_SETTINGS:
if removed in visible_settings:
visible_settings.remove(removed)
parser["general"]["visible_settings"] = ";".join(visible_settings)
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ())
parser.read_string(serialized)
# Update version number.
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
if "values" in parser:
# Enable the new skin_support based on the value of bridge_sparse_infill_max_density
if "bridge_sparse_infill_max_density" in parser["values"]:
parser["values"]["skin_support"] = f"=infill_sparse_density < {parser["values"]["bridge_sparse_infill_max_density"]}"
# Remove deleted settings from the instance containers.
for removed in _REMOVED_SETTINGS:
if removed in parser["values"]:
del parser["values"][removed]
# Force honeycomb infill pattern for high speed printers if using an overlapping pattern
printer_definition = ""
if "general" in parser and "definition" in parser["general"]:
printer_definition = parser["general"]["definition"]
infill_pattern = ""
if "infill_pattern" in parser["values"]:
infill_pattern = parser["values"]["infill_pattern"]
if printer_definition in _HIGH_SPEED_PRINTERS and infill_pattern in _OVERLAPPING_INFILLS:
parser["values"]["infill_pattern"] = "honeycomb"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
if "metadata" not in parser:
parser["metadata"] = {}
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]

View file

@ -0,0 +1,60 @@
# Copyright (c) 2026 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, Dict, TYPE_CHECKING
from . import VersionUpgrade511to512
if TYPE_CHECKING:
from UM.Application import Application
upgrade = VersionUpgrade511to512.VersionUpgrade511to512()
def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
("preferences", 7000025): ("preferences", 7000026, upgrade.upgradePreferences),
("machine_stack", 6000025): ("machine_stack", 6000026, upgrade.upgradeStack),
("extruder_train", 6000025): ("extruder_train", 6000026, upgrade.upgradeStack),
("definition_changes", 4000025): ("definition_changes", 4000026, upgrade.upgradeInstanceContainer),
("quality_changes", 4000025): ("quality_changes", 4000026, upgrade.upgradeInstanceContainer),
("quality", 4000025): ("quality", 4000026, upgrade.upgradeInstanceContainer),
("user", 4000025): ("user", 4000026, upgrade.upgradeInstanceContainer),
("intent", 4000025): ("intent", 4000026, upgrade.upgradeInstanceContainer),
},
"sources": {
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
},
"machine_stack": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"extruder_train": {
"get_version": upgrade.getCfgVersion,
"location": {"./extruders"}
},
"definition_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./definition_changes"}
},
"quality_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality_changes"}
},
"quality": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality"}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}
def register(app: "Application") -> Dict[str, Any]:
return {"version_upgrade": upgrade}

View file

@ -0,0 +1,8 @@
{
"name": "Version Upgrade 5.11 to 5.12",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 5.11 to Cura 5.12",
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -0,0 +1,126 @@
{
"version": 2,
"name": "ELEGOO Centauri Carbon",
"inherits": "elegoo_base",
"metadata":
{
"visible": true,
"author": "Steven Molen",
"manufacturer": "ELEGOO",
"file_formats": "text/x-gcode",
"platform": "elegoo_platform.3mf",
"has_machine_quality": true,
"has_materials": true,
"has_variants": true,
"machine_extruder_trains": { "0": "elegoo_centauri_carbon_extruder_0" },
"preferred_material": "generic_pla_175",
"preferred_quality_type": "elegoo_cc_layer_020",
"preferred_variant_name": "0.40mm_Elegoo_Nozzle",
"variants_name": "Nozzle Size"
},
"overrides":
{
"machine_name": { "default_value": "ELEGOO Centauri Carbon" },
"machine_width": { "default_value": 257 },
"machine_depth": { "default_value": 257 },
"machine_height": { "default_value": 257 },
"machine_heated_bed": { "default_value": true },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_head_with_fans_polygon":
{
"value": [
[-57, 68],
[-57, -57],
[57, 68],
[57, -57]
]
},
"gantry_height": { "value": 90 },
"machine_max_acceleration_x": { "value": 20000 },
"machine_max_acceleration_y": { "value": 20000 },
"machine_max_acceleration_z": { "value": 500 },
"machine_max_acceleration_e": { "value": 5000 },
"machine_acceleration": { "value": 10000 },
"machine_max_jerk_xy": { "value": 9.0 },
"machine_max_jerk_z": { "value": 3.0 },
"machine_max_jerk_e": { "value": 2.5 },
"acceleration_print":
{
"value": 10000,
"maximum_value_warning": 20000
},
"acceleration_wall": { "value": "acceleration_print / 2" },
"acceleration_layer_0": { "value": 1000 },
"speed_print":
{
"default_value": 200,
"maximum_value_warning": 500
},
"speed_wall_0":
{
"value": 160,
"maximum_value_warning": 500
},
"speed_wall_x":
{
"value": 200,
"maximum_value_warning": 500
},
"speed_infill":
{
"value": 200,
"maximum_value_warning": 500
},
"speed_topbottom":
{
"value": 150,
"maximum_value_warning": 500
},
"speed_travel":
{
"value": 400,
"maximum_value_warning": 500
},
"speed_layer_0":
{
"value": 50,
"maximum_value_warning": 500
},
"speed_travel_layer_0":
{
"value": 100,
"maximum_value_warning": 500
},
"speed_support_interface": { "value": 80, "maximum_value_warning": 500 },
"retraction_amount": { "default_value": 0.8 },
"retraction_speed": { "default_value": 30 },
"retraction_retract_speed": { "value": 30 },
"retraction_prime_speed": { "value": 30 },
"retraction_hop": { "value": 0.4 },
"retraction_hop_enabled": { "value": true },
"retraction_count_max": { "value": 100 },
"retraction_combing": { "value": "'no_outer_surfaces'" },
"infill_pattern": { "value": "'zigzag'" },
"top_layers": { "value": 5 },
"bottom_layers": { "value": 3 },
"top_bottom_thickness": { "value": 1.0 },
"support_angle": { "value": 40 },
"support_z_distance": { "value": 0.15 },
"support_xy_distance": { "value": 0.5 },
"support_interface_enable": { "value": true },
"adhesion_type": { "value": "'skirt'" },
"skirt_line_count": { "value": 2 },
"skirt_gap": { "value": 2 },
"machine_start_gcode": { "default_value": ";;===== Elegoo Centauri Carbon Start G-code =====\n;printer_model:ELEGOO Centauri Carbon\nM400 ; wait for buffer to clear\nM220 S100 ;Set the feed speed to 100%\nM221 S100 ;Set the flow rate to 100%\nM104 S140\nM140 S{material_bed_temperature_layer_0}\nG90\nG28 ;home\nM729 ;Clean Nozzle\nM106 P2 S255\nM190 S{material_bed_temperature_layer_0}\nM106 P2 S0\n\n;=============turn on fans to prevent PLA jamming=================\nM106 P3 S180\n\n;Enable pressure advance\nSET_PRESSURE_ADVANCE ADVANCE=0.024\nM400\nM204 S10000 ;Set acceleration\n\nG1 X128 Y-1.2 F20000\nG1 Z0.3 F900\nM109 S{material_print_temperature_layer_0}\nM83\nG92 E0 ;Reset Extruder\n;Purge line - values from official Elegoo config\nG1 X-1.2 E10.156 F6000 ;Draw the first line\nG1 Y98.8 E7.934\nG1 X-0.5 Y100 E0.1\nG1 Y-0.3 E7.934\nG1 X78 E6.284\nG3 I-1 J0 Z0.6 F1200.0 ;Move to side a little\nG1 F20000\nG92 E0 ;Reset Extruder\n" },
"machine_end_gcode": { "default_value": ";===== Elegoo Centauri Carbon End G-code =====\nM400 ; wait for buffer to clear\nM140 S0 ;Turn-off bed\nM106 S255 ;Cooling nozzle\nM83\nG92 E0 ; zero the extruder\nG1 E-1 F3000 ; retract\nG90\nG1 Z{max(layer_height * (layer_count - 1) + 50, 100)} F20000 ; Move print head up\nM204 S5000\nM400\nM83\nG1 X202 F20000\nM400\nG1 Y250 F20000\nG1 Y255 F1200\nM400\nG92 E0\nM104 S0 ;Turn-off hotend\nM140 S0 ;Turn-off bed\nM106 S0 ; turn off fan\nM106 P2 S0 ; turn off remote part cooling fan\nM106 P3 S0 ; turn off chamber cooling fan\nM84 ;Disable all steppers" }
}
}

View file

@ -7,7 +7,7 @@
"author": "Ultimaker",
"manufacturer": "Unknown",
"position": "0",
"setting_version": 25,
"setting_version": 26,
"type": "extruder"
},
"settings":

View file

@ -6,7 +6,7 @@
"type": "machine",
"author": "Unknown",
"manufacturer": "Unknown",
"setting_version": 25,
"setting_version": 26,
"file_formats": "text/x-gcode;model/stl;application/x-wavefront-obj;application/x3g",
"visible": false,
"has_materials": true,
@ -2189,7 +2189,7 @@
"default_value": 2,
"minimum_value": "0",
"minimum_value_warning": "infill_line_width",
"value": "0 if infill_sparse_density == 0 else (infill_line_width * 100) / infill_sparse_density * (2 if infill_pattern == 'grid' else (3 if infill_pattern == 'triangles' or infill_pattern == 'trihexagon' or infill_pattern == 'cubic' or infill_pattern == 'cubicsubdiv' else (2 if infill_pattern == 'tetrahedral' or infill_pattern == 'quarter_cubic' else (1 if infill_pattern == 'cross' or infill_pattern == 'cross_3d' else (1.6 if infill_pattern == 'lightning' else 1)))))",
"value": "0 if infill_sparse_density == 0 else (((infill_line_width * 100) / infill_sparse_density) * (((4/3) - (1/3) * (infill_sparse_density / 100.0)) if infill_pattern in ['honeycomb', 'octagon'] else (2 if infill_pattern == 'grid' else (3 if infill_pattern in ['triangles', 'trihexagon', 'cubic', 'cubicsubdiv'] else (2 if infill_pattern in ['tetrahedral', 'quarter_cubic'] else (1 if infill_pattern in ['cross', 'cross_3d'] else (1.6 if infill_pattern == 'lightning' else 1)))))))",
"limit_to_extruder": "infill_extruder_nr",
"settable_per_mesh": true
}
@ -2198,15 +2198,12 @@
"infill_pattern":
{
"label": "Infill Pattern",
"description": "The pattern of the infill material of the print. The line and zig zag infill swap direction on alternate layers, reducing material cost. The grid, triangle, tri-hexagon, cubic, octet, quarter cubic, cross and concentric patterns are fully printed every layer. Gyroid, cubic, quarter cubic and octet infill change with every layer to provide a more equal distribution of strength over each direction. Lightning infill tries to minimize the infill, by only supporting the ceiling of the object.",
"description": "<html>The pattern of the infill material of the print.<ul><li>The Line and Zig Zag infill swap direction on alternate layers, reducing material cost.</li><li>The Grid, Triangle, Tri-Hexagon, Cubic, Octet, Quarter Cubic, Cross, and Concentric patterns are fully printed every layer.</li><li>Gyroid, Honeycomb, Octagon, Cubic, Quarter Cubic, and Octet infill change with every layer to provide a more equal distribution of strength over each direction.</li><li>Lightning infill tries to minimize the infill, by only supporting the ceiling of the object.</li></ul>\u26a0 Grid, Triangles, and Cubic infill patterns contain intersecting lines, that may cause your nozzle to bump into printed lines, and your printer to vibrate. Use with caution.<br/></html>",
"type": "enum",
"options":
{
"grid": "Grid",
"lines": "Lines",
"triangles": "Triangles",
"trihexagon": "Tri-Hexagon",
"cubic": "Cubic",
"cubicsubdiv": "Cubic Subdivision",
"tetrahedral": "Octet",
"quarter_cubic": "Quarter Cubic",
@ -2215,7 +2212,12 @@
"cross": "Cross",
"cross_3d": "Cross 3D",
"gyroid": "Gyroid",
"lightning": "Lightning"
"lightning": "Lightning",
"honeycomb": "Honeycomb",
"octagon": "Octagon",
"grid": "Grid",
"cubic": "Cubic",
"triangles": "Triangles"
},
"default_value": "grid",
"enabled": "infill_line_distance > 0",
@ -2230,7 +2232,7 @@
"type": "bool",
"default_value": false,
"value": "infill_pattern == 'cross' or infill_pattern == 'cross_3d'",
"enabled": "infill_pattern == 'lines' or infill_pattern == 'grid' or infill_pattern == 'triangles' or infill_pattern == 'trihexagon' or infill_pattern == 'cubic' or infill_pattern == 'tetrahedral' or infill_pattern == 'quarter_cubic' or infill_pattern == 'cross' or infill_pattern == 'cross_3d' or infill_pattern == 'gyroid'",
"enabled": "infill_pattern == 'lines' or infill_pattern == 'grid' or infill_pattern == 'triangles' or infill_pattern == 'trihexagon' or infill_pattern == 'cubic' or infill_pattern == 'tetrahedral' or infill_pattern == 'quarter_cubic' or infill_pattern == 'cross' or infill_pattern == 'cross_3d' or infill_pattern == 'gyroid' or infill_pattern == 'honeycomb' or infill_pattern == 'octagon'",
"limit_to_extruder": "infill_extruder_nr",
"settable_per_mesh": true
},
@ -2251,7 +2253,8 @@
"description": "A list of integer line directions to use. Elements from the list are used sequentially as the layers progress and when the end of the list is reached, it starts at the beginning again. The list items are separated by commas and the whole list is contained in square brackets. Default is an empty list which means use the traditional default angles (45 and 135 degrees for the lines and zig zag patterns and 45 degrees for all other patterns).",
"type": "[int]",
"default_value": "[ ]",
"enabled": "infill_pattern not in ('concentric', 'cross', 'cross_3d', 'gyroid', 'lightning') and infill_sparse_density > 0",
"value": "[0, 90] if infill_pattern == 'octagon' else [0, 60, 120] if infill_pattern == 'honeycomb' else [0] if infill_pattern == 'gyroid' else [ ]",
"enabled": "infill_pattern not in ('concentric', 'cross', 'cross_3d', 'lightning') and infill_sparse_density > 0",
"limit_to_extruder": "infill_extruder_nr",
"settable_per_mesh": true
},
@ -2462,7 +2465,7 @@
"description": "Print a solid infill pattern just below skin to avoid printing the skin over air.",
"type": "bool",
"default_value": true,
"value": "infill_sparse_density < 50",
"value": "infill_sparse_density < 40",
"enabled": "infill_sparse_density > 0 and top_layers > 0",
"settable_per_mesh": true,
"settable_per_extruder": true
@ -4816,7 +4819,7 @@
"retraction_combing_avoid_distance":
{
"label": "Inside Travel Avoid Distance",
"description": "The distance between the nozzle and already printed outer walls when travelling inside a model.",
"description": "The distance between the nozzle and outer walls when travelling inside a model.",
"unit": "mm",
"type": "float",
"default_value": 0.6,
@ -5575,7 +5578,9 @@
"concentric": "Concentric",
"zigzag": "Zig Zag",
"cross": "Cross",
"gyroid": "Gyroid"
"gyroid": "Gyroid",
"honeycomb": "Honeycomb",
"octagon": "Octagon"
},
"default_value": "zigzag",
"enabled": "support_enable or support_meshes_present",
@ -5656,8 +5661,8 @@
"description": "Connect the ends of the support lines together. Enabling this setting can make your support more sturdy and reduce underextrusion, but it will cost more material.",
"type": "bool",
"default_value": false,
"value": "support_pattern == 'cross' or support_pattern == 'gyroid'",
"enabled": "(support_enable or support_meshes_present) and (support_pattern == 'lines' or support_pattern == 'grid' or support_pattern == 'triangles' or support_pattern == 'cross' or support_pattern == 'gyroid')",
"value": "support_pattern == 'cross' or support_pattern == 'gyroid' or support_pattern == 'honeycomb' or support_pattern == 'octagon'",
"enabled": "(support_enable or support_meshes_present) and (support_pattern == 'lines' or support_pattern == 'grid' or support_pattern == 'triangles' or support_pattern == 'cross' or support_pattern == 'gyroid' or support_pattern == 'honeycomb' or support_pattern == 'octagon')",
"limit_to_extruder": "support_infill_extruder_nr",
"settable_per_mesh": false,
"settable_per_extruder": true

View file

@ -85,7 +85,7 @@
"gantry_height": { "value": 35 },
"gradual_support_infill_steps": { "value": "3 if support_interface_enable and support_structure != 'tree' else 0" },
"group_outer_walls": { "value": "False" },
"infill_angles": { "value": "[-40, 50] if infill_pattern in ('grid', 'lines', 'zigzag') else [ ]" },
"infill_angles": { "value": "[-40, 50] if infill_pattern in ('grid', 'lines', 'zigzag') else [0, 60, 120] if infill_pattern == 'honeycomb' else [ ]" },
"infill_before_walls": { "value": "False if infill_sparse_density > 50 else True" },
"infill_enable_travel_optimization": { "value": "True" },
"infill_material_flow":
@ -94,7 +94,7 @@
"value": "(1 + (skin_material_flow-infill_sparse_density) / 100 if infill_sparse_density > skin_material_flow else 1) * material_flow"
},
"infill_overlap": { "value": "0" },
"infill_pattern": { "value": "'zigzag' if infill_sparse_density > 50 else 'gyroid' if 15 < speed_infill / ( infill_line_width * 300 / infill_sparse_density ) < 25 else 'triangles'" },
"infill_pattern": { "value": "'zigzag' if infill_sparse_density > 50 else 'honeycomb'" },
"infill_sparse_density": { "maximum_value": "100" },
"infill_wipe_dist": { "value": "0" },
"inset_direction": { "value": "'inside_out'" },

View file

@ -403,6 +403,7 @@
"machine_start_gcode": { "default_value": "G0 Z20" },
"machine_width": { "default_value": 283.3 },
"material_bed_temperature": { "enabled": "machine_heated_bed" },
"material_diameter": { "default_value": 1.75 },
"material_final_print_temperature":
{
"maximum_value": "material_print_temperature",

View file

@ -241,7 +241,7 @@
"infill_material_flow": { "value": "material_flow if infill_sparse_density < 95 else 95" },
"infill_move_inwards_length": { "value": "3*machine_nozzle_size" },
"infill_overlap": { "value": 10 },
"infill_pattern": { "value": "'zigzag' if infill_sparse_density > 50 else 'grid'" },
"infill_pattern": { "value": "'zigzag'" },
"infill_sparse_density": { "value": 15 },
"infill_wall_line_count": { "value": "1 if infill_sparse_density > 80 else 0" },
"initial_bottom_layers": { "value": "2 if extruderValueFromContainer(top_bottom_extruder_nr, 'bottom_layers', 2) == bottom_layers else bottom_layers" },
@ -425,7 +425,7 @@
"material_pressure_advance_factor":
{
"enabled": true,
"value": 0.5
"value": 0.75
},
"material_print_temperature": { "maximum_value_warning": 320 },
"material_print_temperature_layer_0": { "maximum_value_warning": 320 },
@ -456,6 +456,7 @@
"seam_overhang_angle": { "value": 35 },
"skin_material_flow": { "value": 93 },
"skin_outline_count": { "value": 0 },
"skin_preshrink": { "value": 0 },
"skin_support_speed": { "value": "speed_infill * infill_material_flow / skin_support_material_flow" },
"skirt_brim_minimal_length": { "value": 1000 },
"skirt_line_count": { "value": 5 },

View file

@ -0,0 +1,18 @@
{
"version": 2,
"name": "Extruder 1",
"inherits": "fdmextruder",
"metadata":
{
"machine": "elegoo_centauri_carbon",
"position": "0"
},
"overrides":
{
"extruder_nr": { "default_value": 0 },
"material_diameter": { "default_value": 1.75 },
"machine_nozzle_size": { "default_value": 0.4 },
"machine_nozzle_offset_x": { "default_value": 0 },
"machine_nozzle_offset_y": { "default_value": 0 }
}
}

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_abs
quality_type = D010
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_abs
quality_type = D015
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_abs
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_abs
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_abs
quality_type = D030
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_abs
quality_type = D010
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_abs
quality_type = D015
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_abs
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_abs
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_abs
quality_type = D030
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_petg
quality_type = D010
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_petg
quality_type = D015
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_petg
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_petg
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_petg
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_petg
quality_type = D030
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_petg
quality_type = D010
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_petg
quality_type = D015
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_petg
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_petg
quality_type = D015
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_petg
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_petg
quality_type = D030
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = D010
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = D015
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_pla
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_pla
quality_type = D030
setting_version = 25
setting_version = 26
type = intent
variant = DBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = D010
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = D015
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_pla
quality_type = D020
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_pla
quality_type = D030
setting_version = 25
setting_version = 26
type = intent
variant = FBE 0.40mm

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = Elegoo_layer_005
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = Elegoo_layer_015
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = Elegoo_layer_015
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = Elegoo_layer_010
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = Elegoo_layer_010
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_pla
quality_type = Elegoo_layer_020
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -8,7 +8,7 @@ intent_category = quick
is_experimental = True
material = generic_pla
quality_type = Elegoo_layer_030
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = Elegoo_layer_005
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -8,7 +8,7 @@ intent_category = engineering
is_experimental = True
material = generic_pla
quality_type = Elegoo_layer_015
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = Elegoo_layer_015
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -8,7 +8,7 @@ intent_category = engineering
is_experimental = True
material = generic_pla
quality_type = Elegoo_layer_010
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = Elegoo_layer_010
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -8,7 +8,7 @@ intent_category = engineering
is_experimental = True
material = generic_pla
quality_type = Elegoo_layer_020
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -8,7 +8,7 @@ intent_category = quick
is_experimental = True
material = generic_pla
quality_type = Elegoo_layer_020
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -8,7 +8,7 @@ intent_category = quick
is_experimental = True
material = generic_pla
quality_type = Elegoo_layer_030
setting_version = 25
setting_version = 26
type = intent
variant = 0.40mm_Elegoo_Nozzle

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_abs
quality_type = draft
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_abs
quality_type = fast
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_abs
quality_type = fast
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_abs
quality_type = high
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_abs
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_abs
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_cpe
quality_type = fast
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_cpe
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_nylon
quality_type = fast
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_nylon
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pc
quality_type = fast
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pc
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_petg
quality_type = draft
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_petg
quality_type = fast
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_petg
quality_type = fast
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_petg
quality_type = high
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_petg
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_petg
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = quick
material = generic_pla
quality_type = draft
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = fast
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = fast
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = high
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_pla
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = VO 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_petg
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = AA 0.25

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_pla
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = AA 0.25

View file

@ -7,7 +7,7 @@ version = 4
intent_category = visual
material = generic_tough_pla
quality_type = normal
setting_version = 25
setting_version = 26
type = intent
variant = AA 0.25

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_abs
quality_type = draft
setting_version = 25
setting_version = 26
type = intent
variant = AA 0.4

View file

@ -7,7 +7,7 @@ version = 4
intent_category = engineering
material = generic_asa
quality_type = draft
setting_version = 25
setting_version = 26
type = intent
variant = AA 0.4

Some files were not shown because too many files have changed in this diff Show more