Browse Source

Merge branch 'master' of github.com:Ultimaker/Cura

Jaime van Kessel 4 years ago
parent
commit
13105cfbd7

+ 1320 - 298
plugins/PostProcessingPlugin/scripts/ChangeAtZ.py

@@ -4,71 +4,86 @@
 # It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
 # This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
 
-#Authors of the ChangeAtZ plugin / script:
+# Authors of the ChangeAtZ plugin / script:
 # Written by Steven Morlock, smorloc@gmail.com
 # Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+
 # Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below)
 # Modified by Jaime van Kessel (Ultimaker), j.vankessel@ultimaker.com to make it work for 15.10 / 2.x
 # Modified by Ruben Dulek (Ultimaker), r.dulek@ultimaker.com, to debug.
+# Modified by Wes Hanney, https://github.com/novamxd, Retract Length + Speed, Clean up
 
-##history / changelog:
-##V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment)
-##V3.1:   Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at,
-##        extruder three temperature disabled by "#Ex3"
-##V3.1.1: Bugfix reset flow rate
-##V3.1.2: Bugfix disable TweakAtZ on Cool Head Lift
-##V3.2:   Flow rate for specific extruder added (only for 2 extruders), bugfix parser,
-##        added speed reset at the end of the print
-##V4.0:   Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option,
-##        extruder three code removed, tweaking print speed, save call of Publisher class,
-##        uses previous value from other plugins also on UltiGCode
-##V4.0.1: Bugfix for doubled G1 commands
-##V4.0.2: uses Cura progress bar instead of its own
-##V4.0.3: Bugfix for cool head lift (contributed by luisonoff)
-##V4.9.91: First version for Cura 15.06.x and PostProcessingPlugin
-##V4.9.92: Modifications for Cura 15.10
-##V4.9.93: Minor bugfixes (input settings) / documentation
-##V4.9.94: Bugfix Combobox-selection; remove logger
-##V5.0:   Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x
-##V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed'
-##V5.1:   API Changes included for use with Cura 2.2
-
-## Uses -
-## M220 S<factor in percent> - set speed factor override percentage
-## M221 S<factor in percent> - set flow factor override percentage
-## M221 S<factor in percent> T<0-#toolheads> - set flow factor override percentage for single extruder
-## M104 S<temp> T<0-#toolheads> - set extruder <T> to target temperature <S>
-## M140 S<temp> - set bed target temperature
-## M106 S<PWM> - set fan speed to target speed <S>
-## M605/606 to save and recall material settings on the UM2
+# history / changelog:
+# V3.0.1:   TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment)
+# V3.1:     Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at,
+# extruder three temperature disabled by "#Ex3"
+# V3.1.1:   Bugfix reset flow rate
+# V3.1.2:   Bugfix disable TweakAtZ on Cool Head Lift
+# V3.2:     Flow rate for specific extruder added (only for 2 extruders), bugfix parser,
+# added speed reset at the end of the print
+# V4.0:     Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option,
+# extruder three code removed, tweaking print speed, save call of Publisher class,
+# uses previous value from other plugins also on UltiGCode
+# V4.0.1:	Bugfix for doubled G1 commands
+# V4.0.2:	Uses Cura progress bar instead of its own
+# V4.0.3:	Bugfix for cool head lift (contributed by luisonoff)
+# V4.9.91:	First version for Cura 15.06.x and PostProcessingPlugin
+# V4.9.92:	Modifications for Cura 15.10
+# V4.9.93:	Minor bugfixes (input settings) / documentation
+# V4.9.94:	Bugfix Combobox-selection; remove logger
+# V5.0:		Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x
+# V5.0.1:	Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed'
+# V5.1:		API Changes included for use with Cura 2.2
+# V5.2.0:	Wes Hanney. Added support for changing Retract Length and Speed. Removed layer spread option. Fixed issue of cumulative ChangeZ
+# mods so they can now properly be stacked on top of each other. Applied code refactoring to clean up various coding styles. Added comments.
+# Broke up functions for clarity. Split up class so it can be debugged outside of Cura.
+# V5.2.1:	Wes Hanney. Added support for firmware based retractions. Fixed issue of properly restoring previous values in single layer option.
+# Added support for outputting changes to LCD (untested). Added type hints to most functions and variables. Added more comments. Created GCodeCommand
+# class for better detection of G1 vs G10 or G11 commands, and accessing arguments. Moved most GCode methods to GCodeCommand class. Improved wording
+# of Single Layer vs Keep Layer to better reflect what was happening.
 
+# Uses -
+# M220 S<factor in percent> - set speed factor override percentage
+# M221 S<factor in percent> - set flow factor override percentage
+# M221 S<factor in percent> T<0-#toolheads> - set flow factor override percentage for single extruder
+# M104 S<temp> T<0-#toolheads> - set extruder <T> to target temperature <S>
+# M140 S<temp> - set bed target temperature
+# M106 S<PWM> - set fan speed to target speed <S>
+# M207 S<mm> F<mm/m> - set the retract length <S> or feed rate <F>
+# M117 - output the current changes
+
+from typing import List, Optional, Dict
 from ..Script import Script
-#from UM.Logger import Logger
 import re
 
+
+# this was broken up into a separate class so the main ChangeZ script could be debugged outside of Cura
 class ChangeAtZ(Script):
-    version = "5.1.1"
-    def __init__(self):
-        super().__init__()
+    version = "5.2.1"
 
     def getSettingDataString(self):
         return """{
-            "name":"ChangeAtZ """ + self.version + """ (Experimental)",
-            "key":"ChangeAtZ",
+            "name": "ChangeAtZ """ + self.version + """(Experimental)",
+            "key": "ChangeAtZ",
             "metadata": {},
             "version": 2,
-            "settings":
-            {
-                "a_trigger":
-                {
+            "settings": {
+                "caz_enabled": {
+                    "label": "Enabled",
+                    "description": "Allows adding multiple ChangeZ mods and disabling them as needed.",
+                    "type": "bool",
+                    "default_value": true
+                },             
+                "a_trigger": {
                     "label": "Trigger",
                     "description": "Trigger at height or at layer no.",
                     "type": "enum",
-                    "options": {"height":"Height","layer_no":"Layer No."},
+                    "options": {
+                        "height": "Height",
+                        "layer_no": "Layer No."
+                    },
                     "default_value": "height"
                 },
-                "b_targetZ":
-                {
+                "b_targetZ": {
                     "label": "Change Height",
                     "description": "Z height to change at",
                     "unit": "mm",
@@ -79,8 +94,7 @@ class ChangeAtZ(Script):
                     "maximum_value_warning": "230",
                     "enabled": "a_trigger == 'height'"
                 },
-                "b_targetL":
-                {
+                "b_targetL": {
                     "label": "Change Layer",
                     "description": "Layer no. to change at",
                     "unit": "",
@@ -90,34 +104,29 @@ class ChangeAtZ(Script):
                     "minimum_value_warning": "-1",
                     "enabled": "a_trigger == 'layer_no'"
                 },
-                "c_behavior":
-                {
-                    "label": "Behavior",
-                    "description": "Select behavior: Change value and keep it for the rest, Change value for single layer only",
+                "c_behavior": {
+                    "label": "Apply To",
+                    "description": "Target Layer + Subsequent Layers is good for testing changes between ranges of layers, ex: Layer 0 to 10 or 0mm to 5mm. Single layer is good for testing changes at a single layer, ex: at Layer 10 or 5mm only.",
                     "type": "enum",
-                    "options": {"keep_value":"Keep value","single_layer":"Single Layer"},
+                    "options": {
+                        "keep_value": "Target Layer + Subsequent Layers",
+                        "single_layer": "Target Layer Only"
+                    },
                     "default_value": "keep_value"
                 },
-                "d_twLayers":
-                {
-                    "label": "Layer Spread",
-                    "description": "The change will be gradual over this many layers. Enter 1 to make the change immediate.",
-                    "unit": "",
-                    "type": "int",
-                    "default_value": 1,
-                    "minimum_value": "1",
-                    "maximum_value_warning": "50",
-                    "enabled": "c_behavior == 'keep_value'"
-                },
-                "e1_Change_speed":
-                {
+                "caz_output_to_display": {
+                    "label": "Output to Display",
+                    "description": "Displays the current changes to the LCD",
+                    "type": "bool",
+                    "default_value": false
+                },                                                         
+                "e1_Change_speed": {
                     "label": "Change Speed",
                     "description": "Select if total speed (print and travel) has to be changed",
                     "type": "bool",
                     "default_value": false
                 },
-                "e2_speed":
-                {
+                "e2_speed": {
                     "label": "Speed",
                     "description": "New total speed (print and travel)",
                     "unit": "%",
@@ -128,15 +137,13 @@ class ChangeAtZ(Script):
                     "maximum_value_warning": "200",
                     "enabled": "e1_Change_speed"
                 },
-                "f1_Change_printspeed":
-                {
+                "f1_Change_printspeed": {
                     "label": "Change Print Speed",
                     "description": "Select if print speed has to be changed",
                     "type": "bool",
                     "default_value": false
                 },
-                "f2_printspeed":
-                {
+                "f2_printspeed": {
                     "label": "Print Speed",
                     "description": "New print speed",
                     "unit": "%",
@@ -147,15 +154,13 @@ class ChangeAtZ(Script):
                     "maximum_value_warning": "200",
                     "enabled": "f1_Change_printspeed"
                 },
-                "g1_Change_flowrate":
-                {
+                "g1_Change_flowrate": {
                     "label": "Change Flow Rate",
                     "description": "Select if flow rate has to be changed",
                     "type": "bool",
                     "default_value": false
                 },
-                "g2_flowrate":
-                {
+                "g2_flowrate": {
                     "label": "Flow Rate",
                     "description": "New Flow rate",
                     "unit": "%",
@@ -166,15 +171,13 @@ class ChangeAtZ(Script):
                     "maximum_value_warning": "200",
                     "enabled": "g1_Change_flowrate"
                 },
-                "g3_Change_flowrateOne":
-                {
+                "g3_Change_flowrateOne": {
                     "label": "Change Flow Rate 1",
                     "description": "Select if first extruder flow rate has to be changed",
                     "type": "bool",
                     "default_value": false
                 },
-                "g4_flowrateOne":
-                {
+                "g4_flowrateOne": {
                     "label": "Flow Rate One",
                     "description": "New Flow rate Extruder 1",
                     "unit": "%",
@@ -185,15 +188,13 @@ class ChangeAtZ(Script):
                     "maximum_value_warning": "200",
                     "enabled": "g3_Change_flowrateOne"
                 },
-                "g5_Change_flowrateTwo":
-                {
+                "g5_Change_flowrateTwo": {
                     "label": "Change Flow Rate 2",
                     "description": "Select if second extruder flow rate has to be changed",
                     "type": "bool",
                     "default_value": false
                 },
-                "g6_flowrateTwo":
-                {
+                "g6_flowrateTwo": {
                     "label": "Flow Rate two",
                     "description": "New Flow rate Extruder 2",
                     "unit": "%",
@@ -204,15 +205,13 @@ class ChangeAtZ(Script):
                     "maximum_value_warning": "200",
                     "enabled": "g5_Change_flowrateTwo"
                 },
-                "h1_Change_bedTemp":
-                {
+                "h1_Change_bedTemp": {
                     "label": "Change Bed Temp",
                     "description": "Select if Bed Temperature has to be changed",
                     "type": "bool",
                     "default_value": false
                 },
-                "h2_bedTemp":
-                {
+                "h2_bedTemp": {
                     "label": "Bed Temp",
                     "description": "New Bed Temperature",
                     "unit": "C",
@@ -223,15 +222,13 @@ class ChangeAtZ(Script):
                     "maximum_value_warning": "120",
                     "enabled": "h1_Change_bedTemp"
                 },
-                "i1_Change_extruderOne":
-                {
+                "i1_Change_extruderOne": {
                     "label": "Change Extruder 1 Temp",
                     "description": "Select if First Extruder Temperature has to be changed",
                     "type": "bool",
                     "default_value": false
                 },
-                "i2_extruderOne":
-                {
+                "i2_extruderOne": {
                     "label": "Extruder 1 Temp",
                     "description": "New First Extruder Temperature",
                     "unit": "C",
@@ -242,15 +239,13 @@ class ChangeAtZ(Script):
                     "maximum_value_warning": "250",
                     "enabled": "i1_Change_extruderOne"
                 },
-                "i3_Change_extruderTwo":
-                {
+                "i3_Change_extruderTwo": {
                     "label": "Change Extruder 2 Temp",
                     "description": "Select if Second Extruder Temperature has to be changed",
                     "type": "bool",
                     "default_value": false
                 },
-                "i4_extruderTwo":
-                {
+                "i4_extruderTwo": {
                     "label": "Extruder 2 Temp",
                     "description": "New Second Extruder Temperature",
                     "unit": "C",
@@ -261,239 +256,1266 @@ class ChangeAtZ(Script):
                     "maximum_value_warning": "250",
                     "enabled": "i3_Change_extruderTwo"
                 },
-                "j1_Change_fanSpeed":
-                {
+                "j1_Change_fanSpeed": {
                     "label": "Change Fan Speed",
                     "description": "Select if Fan Speed has to be changed",
                     "type": "bool",
                     "default_value": false
                 },
-                "j2_fanSpeed":
-                {
+                "j2_fanSpeed": {
                     "label": "Fan Speed",
-                    "description": "New Fan Speed (0-255)",
-                    "unit": "PWM",
+                    "description": "New Fan Speed (0-100)",
+                    "unit": "%",
                     "type": "int",
-                    "default_value": 255,
+                    "default_value": 100,
                     "minimum_value": "0",
-                    "minimum_value_warning": "15",
-                    "maximum_value_warning": "255",
+                    "minimum_value_warning": "0",
+                    "maximum_value_warning": "100",
                     "enabled": "j1_Change_fanSpeed"
-                }
+                },
+                "caz_change_retract": {
+                    "label": "Change Retraction",
+                    "description": "Indicates you would like to modify retraction properties.",
+                    "type": "bool",
+                    "default_value": false
+                },                  
+                "caz_retractstyle": {
+                    "label": "Retract Style",
+                    "description": "Specify if you're using firmware retraction or linear move based retractions. Check your printer settings to see which you're using.",
+                    "type": "enum",
+                    "options": {
+                        "linear": "Linear Move",                       
+                        "firmware": "Firmware"
+                    },
+                    "default_value": "linear",
+                    "enabled": "caz_change_retract"
+                },                  
+                "caz_change_retractfeedrate": {
+                    "label": "Change Retract Feed Rate",
+                    "description": "Changes the retraction feed rate during print",
+                    "type": "bool",
+                    "default_value": false,
+                    "enabled": "caz_change_retract"
+                },                
+                "caz_retractfeedrate": {
+                    "label": "Retract Feed Rate",
+                    "description": "New Retract Feed Rate (mm/s)",
+                    "unit": "mm/s",
+                    "type": "float",
+                    "default_value": 40,
+                    "minimum_value": "0",
+                    "minimum_value_warning": "0",
+                    "maximum_value_warning": "100",
+                    "enabled": "caz_change_retractfeedrate"
+                },
+                "caz_change_retractlength": {
+                    "label": "Change Retract Length",
+                    "description": "Changes the retraction length during print",
+                    "type": "bool",
+                    "default_value": false,
+                    "enabled": "caz_change_retract"
+                },
+                "caz_retractlength": {
+                    "label": "Retract Length",
+                    "description": "New Retract Length (mm)",
+                    "unit": "mm",
+                    "type": "float",
+                    "default_value": 6,
+                    "minimum_value": "0",
+                    "minimum_value_warning": "0",
+                    "maximum_value_warning": "20",
+                    "enabled": "caz_change_retractlength"
+                }            
             }
         }"""
 
-    def getValue(self, line, key, default = None): #replace default getvalue due to comment-reading feature
-        if not key in line or (";" in line and line.find(key) > line.find(";") and
-                                   not ";ChangeAtZ" in key and not ";LAYER:" in key):
+    def __init__(self):
+        super().__init__()
+
+    def execute(self, data):
+
+        caz_instance = ChangeAtZProcessor()
+
+        caz_instance.TargetValues = {}
+
+        # copy over our settings to our change z class
+        self.setIntSettingIfEnabled(caz_instance, "e1_Change_speed", "speed", "e2_speed")
+        self.setIntSettingIfEnabled(caz_instance, "f1_Change_printspeed", "printspeed", "f2_printspeed")
+        self.setIntSettingIfEnabled(caz_instance, "g1_Change_flowrate", "flowrate", "g2_flowrate")
+        self.setIntSettingIfEnabled(caz_instance, "g3_Change_flowrateOne", "flowrateOne", "g4_flowrateOne")
+        self.setIntSettingIfEnabled(caz_instance, "g5_Change_flowrateTwo", "flowrateTwo", "g6_flowrateTwo")
+        self.setFloatSettingIfEnabled(caz_instance, "h1_Change_bedTemp", "bedTemp", "h2_bedTemp")
+        self.setFloatSettingIfEnabled(caz_instance, "i1_Change_extruderOne", "extruderOne", "i2_extruderOne")
+        self.setFloatSettingIfEnabled(caz_instance, "i3_Change_extruderTwo", "extruderTwo", "i4_extruderTwo")
+        self.setIntSettingIfEnabled(caz_instance, "j1_Change_fanSpeed", "fanSpeed", "j2_fanSpeed")
+        self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractfeedrate", "retractfeedrate", "caz_retractfeedrate")
+        self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractlength", "retractlength", "caz_retractlength")
+
+        # is this mod enabled?
+        caz_instance.IsEnabled = self.getSettingValueByKey("caz_enabled")
+
+        # are we emitting data to the LCD?
+        caz_instance.IsDisplayingChangesToLcd = self.getSettingValueByKey("caz_output_to_display")
+
+        # are we doing linear move retractions?
+        caz_instance.IsLinearRetraction = self.getSettingValueByKey("caz_retractstyle") == "linear"
+
+        # see if we're applying to a single layer or to all layers hence forth
+        caz_instance.IsApplyToSingleLayer = self.getSettingValueByKey("c_behavior") == "single_layer"
+
+        # used for easy reference of layer or height targeting
+        caz_instance.IsTargetByLayer = self.getSettingValueByKey("a_trigger") == "layer_no"
+
+        # change our target based on what we're targeting
+        caz_instance.TargetLayer = self.getIntSettingByKey("b_targetL", None)
+        caz_instance.TargetZ = self.getFloatSettingByKey("b_targetZ", None)
+
+        # run our script
+        return caz_instance.execute(data)
+
+    # Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified
+    def setIntSettingIfEnabled(self, caz_instance, trigger, target, setting):
+
+        # stop here if our trigger isn't enabled
+        if not self.getSettingValueByKey(trigger):
+            return
+
+        # get our value from the settings
+        value = self.getIntSettingByKey(setting, None)
+
+        # skip if there's no value or we can't interpret it
+        if value is None:
+            return
+
+        # set our value in the target settings
+        caz_instance.TargetValues[target] = value
+
+    # Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified
+    def setFloatSettingIfEnabled(self, caz_instance, trigger, target, setting):
+
+        # stop here if our trigger isn't enabled
+        if not self.getSettingValueByKey(trigger):
+            return
+
+        # get our value from the settings
+        value = self.getFloatSettingByKey(setting, None)
+
+        # skip if there's no value or we can't interpret it
+        if value is None:
+            return
+
+        # set our value in the target settings
+        caz_instance.TargetValues[target] = value
+
+    # Returns the given settings value as an integer or the default if it cannot parse it
+    def getIntSettingByKey(self, key, default):
+
+        # change our target based on what we're targeting
+        try:
+            return int(self.getSettingValueByKey(key))
+        except:
+            return default
+
+    # Returns the given settings value as an integer or the default if it cannot parse it
+    def getFloatSettingByKey(self, key, default):
+
+        # change our target based on what we're targeting
+        try:
+            return float(self.getSettingValueByKey(key))
+        except:
+            return default
+
+
+# This is a utility class for getting details of gcodes from a given line
+class GCodeCommand:
+
+    # The GCode command itself (ex: G10)
+    Command = None,
+
+    # Contains any arguments passed to the command. The key is the argument name, the value is the value of the argument.
+    Arguments = {}
+
+    # Contains the components of the command broken into pieces
+    Components = []
+
+    # Constructor. Sets up defaults
+    def __init__(self):
+        self.reset()
+
+    # Gets a GCode Command from the given single line of GCode
+    @staticmethod
+    def getFromLine(line: str):
+
+        # obviously if we don't have a command, we can't return anything
+        if line is None or len(line) == 0:
+            return None
+
+        # we only support G or M commands
+        if line[0] != "G" and line[0] != "M":
+            return None
+
+        # remove any comments
+        line = re.sub(r";.*$", "", line)
+
+        # break into the individual components
+        command_pieces = line.strip().split(" ")
+
+        # our return command details
+        command = GCodeCommand()
+
+        # stop here if we don't even have something to interpret
+        if len(command_pieces) == 0:
+            return None
+
+        # stores all the components of the command within the class for later
+        command.Components = command_pieces
+
+        # set the actual command
+        command.Command = command_pieces[0]
+
+        # stop here if we don't have any parameters
+        if len(command_pieces) == 1:
+            return None
+
+        # return our indexed command
+        return command
+
+    # Handy function for reading a linear move command
+    @staticmethod
+    def getLinearMoveCommand(line: str):
+
+        # get our command from the line
+        linear_command = GCodeCommand.getFromLine(line)
+
+        # if it's not a linear move, we don't care
+        if linear_command is None or (linear_command.Command != "G0" and linear_command.Command != "G1"):
+            return None
+
+        # convert our values to floats (or defaults)
+        linear_command.Arguments["F"] = linear_command.getArgumentAsFloat("F", None)
+        linear_command.Arguments["X"] = linear_command.getArgumentAsFloat("X", None)
+        linear_command.Arguments["Y"] = linear_command.getArgumentAsFloat("Y", None)
+        linear_command.Arguments["Z"] = linear_command.getArgumentAsFloat("Z", None)
+        linear_command.Arguments["E"] = linear_command.getArgumentAsFloat("E", None)
+
+        # return our new command
+        return linear_command
+
+    # Gets the value of a parameter or returns the default if there is none
+    def getArgument(self, name: str, default: str = None) -> str:
+
+        # parse our arguments (only happens once)
+        self.parseArguments()
+
+        # if we don't have the parameter, return the default
+        if name not in self.Arguments:
+            return default
+
+        # otherwise return the value
+        return self.Arguments[name]
+
+    # Gets the value of a parameter as a float or returns the default
+    def getArgumentAsFloat(self, name: str, default: float = None) -> float:
+
+        # try to parse as a float, otherwise return the default
+        try:
+            return float(self.getArgument(name, default))
+        except:
             return default
-        subPart = line[line.find(key) + len(key):] #allows for string lengths larger than 1
+
+    # Gets the value of a parameter as an integer or returns the default
+    def getArgumentAsInt(self, name: str, default: int = None) -> int:
+
+        # try to parse as a integer, otherwise return the default
+        try:
+            return int(self.getArgument(name, default))
+        except:
+            return default
+
+    # Allows retrieving values from the given GCODE line
+    @staticmethod
+    def getDirectArgument(line: str, key: str, default: str = None) -> str:
+
+        if key not in line or (";" in line and line.find(key) > line.find(";") and ";ChangeAtZ" not in key and ";LAYER:" not in key):
+            return default
+
+        # allows for string lengths larger than 1
+        sub_part = line[line.find(key) + len(key):]
+
         if ";ChangeAtZ" in key:
-            m = re.search("^[0-4]", subPart)
+            m = re.search("^[0-4]", sub_part)
         elif ";LAYER:" in key:
-            m = re.search("^[+-]?[0-9]*", subPart)
+            m = re.search("^[+-]?[0-9]*", sub_part)
         else:
-            #the minus at the beginning allows for negative values, e.g. for delta printers
-            m = re.search("^[-]?[0-9]*\.?[0-9]*", subPart)
-        if m == None:
+            # the minus at the beginning allows for negative values, e.g. for delta printers
+            m = re.search(r"^[-]?[0-9]*\.?[0-9]*", sub_part)
+        if m is None:
             return default
+
         try:
-            return float(m.group(0))
+            return m.group(0)
         except:
             return default
 
-    def execute(self, data):
-        #Check which changes should apply
-        ChangeProp = {"speed": self.getSettingValueByKey("e1_Change_speed"),
-             "flowrate": self.getSettingValueByKey("g1_Change_flowrate"),
-             "flowrateOne": self.getSettingValueByKey("g3_Change_flowrateOne"),
-             "flowrateTwo": self.getSettingValueByKey("g5_Change_flowrateTwo"),
-             "bedTemp": self.getSettingValueByKey("h1_Change_bedTemp"),
-             "extruderOne": self.getSettingValueByKey("i1_Change_extruderOne"),
-             "extruderTwo": self.getSettingValueByKey("i3_Change_extruderTwo"),
-             "fanSpeed": self.getSettingValueByKey("j1_Change_fanSpeed")}
-        ChangePrintSpeed = self.getSettingValueByKey("f1_Change_printspeed")
-        ChangeStrings = {"speed": "M220 S%f\n",
-            "flowrate": "M221 S%f\n",
-            "flowrateOne": "M221 T0 S%f\n",
-            "flowrateTwo": "M221 T1 S%f\n",
-            "bedTemp": "M140 S%f\n",
-            "extruderOne": "M104 S%f T0\n",
-            "extruderTwo": "M104 S%f T1\n",
-            "fanSpeed": "M106 S%d\n"}
-        target_values = {"speed": self.getSettingValueByKey("e2_speed"),
-            "printspeed": self.getSettingValueByKey("f2_printspeed"),
-            "flowrate": self.getSettingValueByKey("g2_flowrate"),
-            "flowrateOne": self.getSettingValueByKey("g4_flowrateOne"),
-            "flowrateTwo": self.getSettingValueByKey("g6_flowrateTwo"),
-            "bedTemp": self.getSettingValueByKey("h2_bedTemp"),
-            "extruderOne": self.getSettingValueByKey("i2_extruderOne"),
-            "extruderTwo": self.getSettingValueByKey("i4_extruderTwo"),
-            "fanSpeed": self.getSettingValueByKey("j2_fanSpeed")}
-        old = {"speed": -1, "flowrate": 100, "flowrateOne": -1, "flowrateTwo": -1, "platformTemp": -1, "extruderOne": -1,
-            "extruderTwo": -1, "bedTemp": -1, "fanSpeed": -1, "state": -1}
-        twLayers = self.getSettingValueByKey("d_twLayers")
-        if self.getSettingValueByKey("c_behavior") == "single_layer":
-            behavior = 1
-        else:
-            behavior = 0
+    # Converts the command parameter to a int or returns the default
+    @staticmethod
+    def getDirectArgumentAsFloat(line: str, key: str, default: float = None) -> float:
+
+        # get the value from the command
+        value = GCodeCommand.getDirectArgument(line, key, default)
+
+        # stop here if it's the default
+        if value == default:
+            return value
+
         try:
-            twLayers = max(int(twLayers),1) #for the case someone entered something as "funny" as -1
+            return float(value)
         except:
-            twLayers = 1
-        pres_ext = 0
-        done_layers = 0
-        z = 0
-        x = None
-        y = None
-        layer = -100000 #layer no. may be negative (raft) but never that low
-        # state 0: deactivated, state 1: activated, state 2: active, but below z,
-        # state 3: active and partially executed (multi layer), state 4: active and passed z
-        state = 1
-        # IsUM2: Used for reset of values (ok for Marlin/Sprinter),
-        # has to be set to 1 for UltiGCode (work-around for missing default values)
-        IsUM2 = False
-        oldValueUnknown = False
-        TWinstances = 0
-
-        if self.getSettingValueByKey("a_trigger") == "layer_no":
-            targetL_i = int(self.getSettingValueByKey("b_targetL"))
-            targetZ = 100000
-        else:
-            targetL_i = -100000
-            targetZ = self.getSettingValueByKey("b_targetZ")
+            return default
+
+    # Converts the command parameter to a int or returns the default
+    @staticmethod
+    def getDirectArgumentAsInt(line: str, key: str, default: int = None) -> int:
+
+        # get the value from the command
+        value = GCodeCommand.getDirectArgument(line, key, default)
+
+        # stop here if it's the default
+        if value == default:
+            return value
+
+        try:
+            return int(value)
+        except:
+            return default
+
+    # Parses the arguments of the command on demand, only once
+    def parseArguments(self):
+
+        # stop here if we don't have any remaining components
+        if len(self.Components) <= 1:
+            return None
+
+        # iterate and index all of our parameters, skip the first component as it's the command
+        for i in range(1, len(self.Components)):
+
+            # get our component
+            component = self.Components[i]
+
+            # get the first character of the parameter, which is the name
+            component_name = component[0]
+
+            # get the value of the parameter (the rest of the string
+            component_value = None
+
+            # get our value if we have one
+            if len(component) > 1:
+                component_value = component[1:]
+
+            # index the argument
+            self.Arguments[component_name] = component_value
+
+        # clear the components to we don't process again
+        self.Components = []
+
+    # Easy function for replacing any GCODE parameter variable in a given GCODE command
+    @staticmethod
+    def replaceDirectArgument(line: str, key: str, value: str) -> str:
+        return re.sub(r"(^|\s)" + key + r"[\d\.]+(\s|$)", r"\1" + key + str(value) + r"\2", line)
+
+    # Resets the model back to defaults
+    def reset(self):
+        self.Command = None
+        self.Arguments = {}
+
+
+# The primary ChangeAtZ class that does all the gcode editing. This was broken out into an
+# independent class so it could be debugged using a standard IDE
+class ChangeAtZProcessor:
+
+    # Holds our current height
+    CurrentZ = None
+
+    # Holds our current layer number
+    CurrentLayer = None
+
+    # Indicates if we're only supposed to apply our settings to a single layer or multiple layers
+    IsApplyToSingleLayer = False
+
+    # Indicates if this should emit the changes as they happen to the LCD
+    IsDisplayingChangesToLcd = False
+
+    # Indicates that this mod is still enabled (or not)
+    IsEnabled = True
+
+    # Indicates if we're processing inside the target layer or not
+    IsInsideTargetLayer = False
+
+    # Indicates if we have restored the previous values from before we started our pass
+    IsLastValuesRestored = False
+
+    # Indicates if the user has opted for linear move retractions or firmware retractions
+    IsLinearRetraction = True
+
+    # Indicates if we're targetting by layer or height value
+    IsTargetByLayer = True
+
+    # Indicates if we have injected our changed values for the given layer yet
+    IsTargetValuesInjected = False
+
+    # Holds the last extrusion value, used with detecting when a retraction is made
+    LastE = None
+
+    # An index of our gcodes which we're monitoring
+    LastValues = {}
+
+    # The detected layer height from the gcode
+    LayerHeight = None
+
+    # The target layer
+    TargetLayer = None
+
+    # Holds the values the user has requested to change
+    TargetValues = {}
+
+    # The target height in mm
+    TargetZ = None
+
+    # Used to track if we've been inside our target layer yet
+    WasInsideTargetLayer = False
+
+    # boots up the class with defaults
+    def __init__(self):
+        self.reset()
+
+    # Modifies the given GCODE and injects the commands at the various targets
+    def execute(self, data):
+
+        # short cut the whole thing if we're not enabled
+        if not self.IsEnabled:
+            return data
+
+        # our layer cursor
         index = 0
+
         for active_layer in data:
+
+            # will hold our updated gcode
             modified_gcode = ""
+
+            # mark all the defaults for deletion
+            active_layer = self.markChangesForDeletion(active_layer)
+
+            # break apart the layer into commands
             lines = active_layer.split("\n")
+
+            # evaluate each command individually
             for line in lines:
-                if line.strip() == "":
+
+                # trim or command
+                line = line.strip()
+
+                # skip empty lines
+                if len(line) == 0:
                     continue
-                if ";Generated with Cura_SteamEngine" in line:
-                    TWinstances += 1
-                    modified_gcode += ";ChangeAtZ instances: %d\n" % TWinstances
-                if not ("M84" in line or "M25" in line or ("G1" in line and ChangePrintSpeed and (state==3 or state==4)) or
-                                ";ChangeAtZ instances:" in line):
-                    modified_gcode += line + "\n"
-                IsUM2 = ("FLAVOR:UltiGCode" in line) or IsUM2 #Flavor is UltiGCode!
-                if ";ChangeAtZ-state" in line: #checks for state change comment
-                    state = self.getValue(line, ";ChangeAtZ-state", state)
-                if ";ChangeAtZ instances:" in line:
-                    try:
-                        tempTWi = int(line[20:])
-                    except:
-                        tempTWi = TWinstances
-                    TWinstances = tempTWi
-                if ";Small layer" in line: #checks for begin of Cool Head Lift
-                    old["state"] = state
-                    state = 0
-                if ";LAYER:" in line: #new layer no. found
-                    if state == 0:
-                        state = old["state"]
-                    layer = self.getValue(line, ";LAYER:", layer)
-                    if targetL_i > -100000: #target selected by layer no.
-                        if (state == 2 or targetL_i == 0) and layer == targetL_i: #determine targetZ from layer no.; checks for change on layer 0
-                            state = 2
-                            targetZ = z + 0.001
-                if (self.getValue(line, "T", None) is not None) and (self.getValue(line, "M", None) is None): #looking for single T-cmd
-                    pres_ext = self.getValue(line, "T", pres_ext)
-                if "M190" in line or "M140" in line and state < 3: #looking for bed temp, stops after target z is passed
-                    old["bedTemp"] = self.getValue(line, "S", old["bedTemp"])
-                if "M109" in line or "M104" in line and state < 3: #looking for extruder temp, stops after target z is passed
-                    if self.getValue(line, "T", pres_ext) == 0:
-                        old["extruderOne"] = self.getValue(line, "S", old["extruderOne"])
-                    elif self.getValue(line, "T", pres_ext) == 1:
-                        old["extruderTwo"] = self.getValue(line, "S", old["extruderTwo"])
-                if "M107" in line: #fan is stopped; is always updated in order not to miss switch off for next object
-                    old["fanSpeed"] = 0
-                if "M106" in line and state < 3: #looking for fan speed
-                    old["fanSpeed"] = self.getValue(line, "S", old["fanSpeed"])
-                if "M221" in line and state < 3: #looking for flow rate
-                    tmp_extruder = self.getValue(line, "T", None)
-                    if tmp_extruder == None: #check if extruder is specified
-                        old["flowrate"] = self.getValue(line, "S", old["flowrate"])
-                        if old["flowrate"] == -1:
-                            old["flowrate"] = 100.0
-                    elif tmp_extruder == 0: #first extruder
-                        old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
-                    elif tmp_extruder == 1: #second extruder
-                        old["flowrateTwo"] = self.getValue(line, "S", old["flowrateTwo"])
-                if ("M84" in line or "M25" in line):
-                    if state>0 and ChangeProp["speed"]: #"finish" commands for UM Original and UM2
-                        modified_gcode += "M220 S100 ; speed reset to 100% at the end of print\n"
-                        modified_gcode += "M117                     \n"
-                    modified_gcode += line + "\n"
-                if "G1" in line or "G0" in line:
-                    newZ = self.getValue(line, "Z", z)
-                    x = self.getValue(line, "X", None)
-                    y = self.getValue(line, "Y", None)
-                    e = self.getValue(line, "E", None)
-                    f = self.getValue(line, "F", None)
-                    if 'G1' in line and ChangePrintSpeed and (state==3 or state==4):
-                        # check for pure print movement in target range:
-                        if x != None and y != None and f != None and e != None and newZ==z:
-                            modified_gcode += "G1 F%d X%1.3f Y%1.3f E%1.5f\n" % (int(f / 100.0 * float(target_values["printspeed"])), self.getValue(line, "X"),
-                                                                          self.getValue(line, "Y"), self.getValue(line, "E"))
-                        else: #G1 command but not a print movement
-                            modified_gcode += line + "\n"
-                    # no changing on retraction hops which have no x and y coordinate:
-                    if (newZ != z) and (x is not None) and (y is not None):
-                        z = newZ
-                        if z < targetZ and state == 1:
-                            state = 2
-                        if z >= targetZ and state == 2:
-                            state = 3
-                            done_layers = 0
-                            for key in ChangeProp:
-                                if ChangeProp[key] and old[key]==-1: #old value is not known
-                                    oldValueUnknown = True
-                            if oldValueUnknown: #the changing has to happen within one layer
-                                twLayers = 1
-                                if IsUM2: #Parameters have to be stored in the printer (UltiGCode=UM2)
-                                    modified_gcode += "M605 S%d;stores parameters before changing\n" % (TWinstances-1)
-                            if behavior == 1: #single layer change only and then reset
-                                twLayers = 1
-                            if ChangePrintSpeed and behavior == 0:
-                                twLayers = done_layers + 1
-                        if state==3:
-                            if twLayers-done_layers>0: #still layers to go?
-                                if targetL_i > -100000:
-                                    modified_gcode += ";ChangeAtZ V%s: executed at Layer %d\n" % (self.version,layer)
-                                    modified_gcode += "M117 Printing... ch@L%4d\n" % layer
-                                else:
-                                    modified_gcode += (";ChangeAtZ V%s: executed at %1.2f mm\n" % (self.version,z))
-                                    modified_gcode += "M117 Printing... ch@%5.1f\n" % z
-                                for key in ChangeProp:
-                                    if ChangeProp[key]:
-                                        modified_gcode += ChangeStrings[key] % float(old[key]+(float(target_values[key])-float(old[key]))/float(twLayers)*float(done_layers+1))
-                                done_layers += 1
-                            else:
-                                state = 4
-                                if behavior == 1: #reset values after one layer
-                                    if targetL_i > -100000:
-                                        modified_gcode += ";ChangeAtZ V%s: reset on Layer %d\n" % (self.version,layer)
-                                    else:
-                                        modified_gcode += ";ChangeAtZ V%s: reset at %1.2f mm\n" % (self.version,z)
-                                    if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
-                                        modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
-                                    else: #executes on RepRap, UM2 with Ultigcode and Cura setting
-                                        for key in ChangeProp:
-                                            if ChangeProp[key]:
-                                                modified_gcode += ChangeStrings[key] % float(old[key])
-                        # re-activates the plugin if executed by pre-print G-command, resets settings:
-                        if (z < targetZ or layer == 0) and state >= 3: #resets if below change level or at level 0
-                            state = 2
-                            done_layers = 0
-                            if targetL_i > -100000:
-                                modified_gcode += ";ChangeAtZ V%s: reset below Layer %d\n" % (self.version, targetL_i)
-                            else:
-                                modified_gcode += ";ChangeAtZ V%s: reset below %1.2f mm\n" % (self.version, targetZ)
-                            if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
-                                modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
-                            else: #executes on RepRap, UM2 with Ultigcode and Cura setting
-                                for key in ChangeProp:
-                                    if ChangeProp[key]:
-                                        modified_gcode += ChangeStrings[key] % float(old[key])
+
+                # update our layer number if applicable
+                self.processLayerNumber(line)
+
+                # update our layer height if applicable
+                self.processLayerHeight(line)
+
+                # check if we're at the target layer or not
+                self.processTargetLayer()
+
+                # process any changes to the gcode
+                modified_gcode += self.processLine(line)
+
+            # remove any marked defaults
+            modified_gcode = self.removeMarkedChanges(modified_gcode)
+
+            # append our modified line
             data[index] = modified_gcode
+
             index += 1
+
+        # return our modified gcode
         return data
+
+    # Builds the restored layer settings based on the previous settings and returns the relevant GCODE lines
+    def getChangedLastValues(self) -> Dict[str, any]:
+
+        # capture the values that we've changed
+        changed = {}
+
+        # for each of our target values, get the value to restore
+        # no point in restoring values we haven't changed
+        for key in self.TargetValues:
+
+            # skip target values we can't restore
+            if key not in self.LastValues:
+                continue
+
+            # save into our changed
+            changed[key] = self.LastValues[key]
+
+        # return our collection of changed values
+        return changed
+
+    # Builds the relevant display feedback for each of the values
+    def getDisplayChangesFromValues(self, values: Dict[str, any]) -> str:
+
+        # stop here if we're not outputting data
+        if not self.IsDisplayingChangesToLcd:
+            return ""
+
+        # will hold all the default settings for the target layer
+        codes = []
+
+        # looking for wait for bed temp
+        if "bedTemp" in values:
+            codes.append("BedTemp: " + str(values["bedTemp"]))
+
+        # set our extruder one temp (if specified)
+        if "extruderOne" in values:
+            codes.append("Extruder 1 Temp: " + str(values["extruderOne"]))
+
+        # set our extruder two temp (if specified)
+        if "extruderTwo" in values:
+            codes.append("Extruder 2 Temp: " + str(values["extruderTwo"]))
+
+        # set global flow rate
+        if "flowrate" in values:
+            codes.append("Extruder A Flow Rate: " + str(values["flowrate"]))
+
+        # set extruder 0 flow rate
+        if "flowrateOne" in values:
+            codes.append("Extruder 1 Flow Rate: " + str(values["flowrateOne"]))
+
+        # set second extruder flow rate
+        if "flowrateTwo" in values:
+            codes.append("Extruder 2 Flow Rate: " + str(values["flowrateTwo"]))
+
+        # set our fan speed
+        if "fanSpeed" in values:
+            codes.append("Fan Speed: " + str(values["fanSpeed"]))
+
+        # set feedrate percentage
+        if "speed" in values:
+            codes.append("Print Speed: " + str(values["speed"]))
+
+        # set print rate percentage
+        if "printspeed" in values:
+            codes.append("Linear Print Speed: " + str(values["printspeed"]))
+
+        # set retract rate
+        if "retractfeedrate" in values:
+            codes.append("Retract Feed Rate: " + str(values["retractfeedrate"]))
+
+        # set retract length
+        if "retractlength" in values:
+            codes.append("Retract Length: " + str(values["retractlength"]))
+
+        # stop here if there's nothing to output
+        if len(codes) == 0:
+            return ""
+
+        # output our command to display the data
+        return "M117 " + ", ".join(codes) + "\n"
+
+    # Converts the last values to something that can be output on the LCD
+    def getLastDisplayValues(self) -> str:
+
+        # convert our last values to something we can output
+        return self.getDisplayChangesFromValues(self.getChangedLastValues())
+
+    # Converts the target values to something that can be output on the LCD
+    def getTargetDisplayValues(self) -> str:
+
+        # convert our target values to something we can output
+        return self.getDisplayChangesFromValues(self.TargetValues)
+
+    # Builds the the relevant GCODE lines from the given collection of values
+    def getCodeFromValues(self, values: Dict[str, any]) -> str:
+
+        # will hold all the desired settings for the target layer
+        codes = self.getCodeLinesFromValues(values)
+
+        # stop here if there are no values that require changing
+        if len(codes) == 0:
+            return ""
+
+        # return our default block for this layer
+        return ";[CAZD:\n" + "\n".join(codes) + "\n;:CAZD]"
+
+    # Builds the relevant GCODE lines from the given collection of values
+    def getCodeLinesFromValues(self, values: Dict[str, any]) -> List[str]:
+
+        # will hold all the default settings for the target layer
+        codes = []
+
+        # looking for wait for bed temp
+        if "bedTemp" in values:
+            codes.append("M140 S" + str(values["bedTemp"]))
+
+        # set our extruder one temp (if specified)
+        if "extruderOne" in values:
+            codes.append("M104 S" + str(values["extruderOne"]) + " T0")
+
+        # set our extruder two temp (if specified)
+        if "extruderTwo" in values:
+            codes.append("M104 S" + str(values["extruderTwo"]) + " T1")
+
+        # set our fan speed
+        if "fanSpeed" in values:
+
+            # convert our fan speed percentage to PWM
+            fan_speed = int((float(values["fanSpeed"]) / 100.0) * 255)
+
+            # add our fan speed to the defaults
+            codes.append("M106 S" + str(fan_speed))
+
+        # set global flow rate
+        if "flowrate" in values:
+            codes.append("M221 S" + str(values["flowrate"]))
+
+        # set extruder 0 flow rate
+        if "flowrateOne" in values:
+            codes.append("M221 S" + str(values["flowrateOne"]) + " T0")
+
+        # set second extruder flow rate
+        if "flowrateTwo" in values:
+            codes.append("M221 S" + str(values["flowrateTwo"]) + " T1")
+
+        # set feedrate percentage
+        if "speed" in values:
+            codes.append("M220 S" + str(values["speed"]) + " T1")
+
+        # set print rate percentage
+        if "printspeed" in values:
+            codes.append(";PRINTSPEED " + str(values["printspeed"]) + "")
+
+        # set retract rate
+        if "retractfeedrate" in values:
+
+            if self.IsLinearRetraction:
+                codes.append(";RETRACTFEEDRATE " + str(values["retractfeedrate"] * 60) + "")
+            else:
+                codes.append("M207 F" + str(values["retractfeedrate"] * 60) + "")
+
+        # set retract length
+        if "retractlength" in values:
+
+            if self.IsLinearRetraction:
+                codes.append(";RETRACTLENGTH " + str(values["retractlength"]) + "")
+            else:
+                codes.append("M207 S" + str(values["retractlength"]) + "")
+
+        return codes
+
+    # Builds the restored layer settings based on the previous settings and returns the relevant GCODE lines
+    def getLastValues(self) -> str:
+
+        # build the gcode to restore our last values
+        return self.getCodeFromValues(self.getChangedLastValues())
+
+    # Builds the gcode to inject either the changed values we want or restore the previous values
+    def getInjectCode(self) -> str:
+
+        # if we're now outside of our target layer and haven't restored our last values, do so now
+        if not self.IsInsideTargetLayer and self.WasInsideTargetLayer and not self.IsLastValuesRestored:
+
+            # mark that we've injected the last values
+            self.IsLastValuesRestored = True
+
+            # inject the defaults
+            return self.getLastValues() + "\n" + self.getLastDisplayValues()
+
+        # if we're inside our target layer but haven't added our values yet, do so now
+        if self.IsInsideTargetLayer and not self.IsTargetValuesInjected:
+
+            # mark that we've injected the target values
+            self.IsTargetValuesInjected = True
+
+            # inject the defaults
+            return self.getTargetValues() + "\n" + self.getTargetDisplayValues()
+
+        # nothing to do
+        return ""
+
+    # Returns the unmodified GCODE line from previous ChangeZ edits
+    @staticmethod
+    def getOriginalLine(line: str) -> str:
+
+        # get the change at z original (cazo) details
+        original_line = re.search(r"\[CAZO:(.*?):CAZO\]", line)
+
+        # if we didn't get a hit, this is the original line
+        if original_line is None:
+            return line
+
+        return original_line.group(1)
+
+    # Builds the target layer settings based on the specified values and returns the relevant GCODE lines
+    def getTargetValues(self) -> str:
+
+        # build the gcode to change our current values
+        return self.getCodeFromValues(self.TargetValues)
+
+    # Determines if the current line is at or below the target required to start modifying
+    def isTargetLayerOrHeight(self) -> bool:
+
+        # target selected by layer no.
+        if self.IsTargetByLayer:
+
+            # if we don't have a current layer, we're not there yet
+            if self.CurrentLayer is None:
+                return False
+
+            # if we're applying to a single layer, stop if our layer is not identical
+            if self.IsApplyToSingleLayer:
+                return self.CurrentLayer == self.TargetLayer
+            else:
+                return self.CurrentLayer >= self.TargetLayer
+
+        else:
+
+            # if we don't have a current Z, we're not there yet
+            if self.CurrentZ is None:
+                return False
+
+            # if we're applying to a single layer, stop if our Z is not identical
+            if self.IsApplyToSingleLayer:
+                return self.CurrentZ == self.TargetZ
+            else:
+                return self.CurrentZ >= self.TargetZ
+
+    # Marks any current ChangeZ layer defaults in the layer for deletion
+    @staticmethod
+    def markChangesForDeletion(layer: str):
+        return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer)
+
+    # Grabs the current height
+    def processLayerHeight(self, line: str):
+
+        # stop here if we haven't entered a layer yet
+        if self.CurrentLayer is None:
+            return
+
+        # get our gcode command
+        command = GCodeCommand.getFromLine(line)
+
+        # skip if it's not a command we're interested in
+        if command is None:
+            return
+
+        # stop here if this isn't a linear move command
+        if command.Command != "G0" and command.Command != "G1":
+            return
+
+        # get our value from the command
+        current_z = command.getArgumentAsFloat("Z", None)
+
+        # stop here if we don't have a Z value defined, we can't get the height from this command
+        if current_z is None:
+            return
+
+        # stop if there's no change
+        if current_z == self.CurrentZ:
+            return
+
+        # set our current Z value
+        self.CurrentZ = current_z
+
+        # if we don't have a layer height yet, set it based on the current Z value
+        if self.LayerHeight is None:
+            self.LayerHeight = self.CurrentZ
+
+    # Grabs the current layer number
+    def processLayerNumber(self, line: str):
+
+        # if this isn't a layer comment, stop here, nothing to update
+        if ";LAYER:" not in line:
+            return
+
+        # get our current layer number
+        current_layer = GCodeCommand.getDirectArgumentAsInt(line, ";LAYER:", None)
+
+        # this should never happen, but if our layer number hasn't changed, stop here
+        if current_layer == self.CurrentLayer:
+            return
+
+        # update our current layer
+        self.CurrentLayer = current_layer
+
+    # Makes any linear move changes and also injects either target or restored values depending on the plugin state
+    def processLine(self, line: str) -> str:
+
+        # used to change the given line of code
+        modified_gcode = ""
+
+        # track any values that we may be interested in
+        self.trackChangeableValues(line)
+
+        # if we're not inside the target layer, simply read the any
+        # settings we can and revert any ChangeAtZ deletions
+        if not self.IsInsideTargetLayer:
+
+            # read any settings if we haven't hit our target layer yet
+            if not self.WasInsideTargetLayer:
+                self.processSetting(line)
+
+            # if we haven't hit our target yet, leave the defaults as is (unmark them for deletion)
+            if "[CAZD:DELETE:" in line:
+                line = line.replace("[CAZD:DELETE:", "[CAZD:")
+
+        # if we're targeting by Z, we want to add our values before the first linear move
+        if "G1 " in line or "G0 " in line:
+            modified_gcode += self.getInjectCode()
+
+        # modify our command if we're still inside our target layer, otherwise pass unmodified
+        if self.IsInsideTargetLayer:
+            modified_gcode += self.processLinearMove(line) + "\n"
+        else:
+            modified_gcode += line + "\n"
+
+        # if we're targetting by layer we want to add our values just after the layer label
+        if ";LAYER:" in line:
+            modified_gcode += self.getInjectCode()
+
+        # return our changed code
+        return modified_gcode
+
+    # Handles any linear moves in the current line
+    def processLinearMove(self, line: str) -> str:
+
+        # if it's not a linear motion command we're not interested
+        if not ("G1 " in line or "G0 " in line):
+            return line
+
+        # always get our original line, otherwise the effect will be cumulative
+        line = self.getOriginalLine(line)
+
+        # get our command from the line
+        linear_command = GCodeCommand.getLinearMoveCommand(line)
+
+        # if it's not a linear move, we don't care
+        if linear_command is None:
+            return
+
+        # get our linear move parameters
+        feed_rate = linear_command.Arguments["F"]
+        x_coord = linear_command.Arguments["X"]
+        y_coord = linear_command.Arguments["Y"]
+        z_coord = linear_command.Arguments["Z"]
+        extrude_length = linear_command.Arguments["E"]
+
+        # set our new line to our old line
+        new_line = line
+
+        # handle retract length
+        new_line = self.processRetractLength(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
+
+        # handle retract feed rate
+        new_line = self.processRetractFeedRate(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
+
+        # handle print speed adjustments
+        new_line = self.processPrintSpeed(feed_rate, new_line)
+
+        # set our current extrude position
+        self.LastE = extrude_length if extrude_length is not None else self.LastE
+
+        # if no changes have been made, stop here
+        if new_line == line:
+            return line
+
+        # return our updated command
+        return self.setOriginalLine(new_line, line)
+
+    # Handles any changes to print speed for the given linear motion command
+    def processPrintSpeed(self, feed_rate: float, new_line: str) -> str:
+
+        # if we're not setting print speed or we don't have a feed rate, stop here
+        if "printspeed" not in self.TargetValues or feed_rate is None:
+            return new_line
+
+        # get our requested print speed
+        print_speed = int(self.TargetValues["printspeed"])
+
+        # if they requested no change to print speed (ie: 100%), stop here
+        if print_speed == 100:
+            return new_line
+
+        # get our feed rate from the command
+        feed_rate = GCodeCommand.getDirectArgumentAsFloat(new_line, "F") * (float(print_speed) / 100.0)
+
+        # change our feed rate
+        return GCodeCommand.replaceDirectArgument(new_line, "F", feed_rate)
+
+    # Handles any changes to retraction length for the given linear motion command
+    def processRetractLength(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
+
+        # if we don't have a retract length in the file we can't add one
+        if "retractlength" not in self.LastValues or self.LastValues["retractlength"] == 0:
+            return new_line
+
+        # if we're not changing retraction length, stop here
+        if "retractlength" not in self.TargetValues:
+            return new_line
+
+        # retractions are only F (feed rate) and E (extrude), at least in cura
+        if x_coord is not None or y_coord is not None or z_coord is not None:
+            return new_line
+
+        # since retractions require both F and E, and we don't have either, we can't process
+        if feed_rate is None or extrude_length is None:
+            return new_line
+
+        # stop here if we don't know our last extrude value
+        if self.LastE is None:
+            return new_line
+
+        # if there's no change in extrude we have nothing to change
+        if self.LastE == extrude_length:
+            return new_line
+
+        # if our last extrude was lower than our current, we're restoring, so skip
+        if self.LastE < extrude_length:
+            return new_line
+
+        # get our desired retract length
+        retract_length = float(self.TargetValues["retractlength"])
+
+        # subtract the difference between the default and the desired
+        extrude_length -= (retract_length - self.LastValues["retractlength"])
+
+        # replace our extrude amount
+        return GCodeCommand.replaceDirectArgument(new_line, "E", extrude_length)
+
+    # Used for picking out the retract length set by Cura
+    def processRetractLengthSetting(self, line: str):
+
+        # skip if we're not doing linear retractions
+        if not self.IsLinearRetraction:
+            return
+
+        # get our command from the line
+        linear_command = GCodeCommand.getLinearMoveCommand(line)
+
+        # if it's not a linear move, we don't care
+        if linear_command is None:
+            return
+
+        # get our linear move parameters
+        feed_rate = linear_command.Arguments["F"]
+        x_coord = linear_command.Arguments["X"]
+        y_coord = linear_command.Arguments["Y"]
+        z_coord = linear_command.Arguments["Z"]
+        extrude_length = linear_command.Arguments["E"]
+
+        # the command we're looking for only has extrude and feed rate
+        if x_coord is not None or y_coord is not None or z_coord is not None:
+            return
+
+        # if either extrude or feed is missing we're likely looking at the wrong command
+        if extrude_length is None or feed_rate is None:
+            return
+
+        # cura stores the retract length as a negative E just before it starts printing
+        extrude_length = extrude_length * -1
+
+        # if it's a negative extrude after being inverted, it's not our retract length
+        if extrude_length < 0:
+            return
+
+        # what ever the last negative retract length is it wins
+        self.LastValues["retractlength"] = extrude_length
+
+    # Handles any changes to retraction feed rate for the given linear motion command
+    def processRetractFeedRate(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
+
+        # skip if we're not doing linear retractions
+        if not self.IsLinearRetraction:
+            return new_line
+
+        # if we're not changing retraction length, stop here
+        if "retractfeedrate" not in self.TargetValues:
+            return new_line
+
+        # retractions are only F (feed rate) and E (extrude), at least in cura
+        if x_coord is not None or y_coord is not None or z_coord is not None:
+            return new_line
+
+        # since retractions require both F and E, and we don't have either, we can't process
+        if feed_rate is None or extrude_length is None:
+            return new_line
+
+        # get our desired retract feed rate
+        retract_feed_rate = float(self.TargetValues["retractfeedrate"])
+
+        # convert to units/min
+        retract_feed_rate *= 60
+
+        # replace our feed rate
+        return GCodeCommand.replaceDirectArgument(new_line, "F", retract_feed_rate)
+
+    # Used for finding settings in the print file before we process anything else
+    def processSetting(self, line: str):
+
+        # if we're in layers already we're out of settings
+        if self.CurrentLayer is not None:
+            return
+
+        # check our retract length
+        self.processRetractLengthSetting(line)
+
+    # Sets the flags if we're at the target layer or not
+    def processTargetLayer(self):
+
+        # skip this line if we're not there yet
+        if not self.isTargetLayerOrHeight():
+
+            # flag that we're outside our target layer
+            self.IsInsideTargetLayer = False
+
+            # skip to the next line
+            return
+
+        # flip if we hit our target layer
+        self.WasInsideTargetLayer = True
+
+        # flag that we're inside our target layer
+        self.IsInsideTargetLayer = True
+
+    # Removes all the ChangeZ layer defaults from the given layer
+    @staticmethod
+    def removeMarkedChanges(layer: str) -> str:
+        return re.sub(r";\[CAZD:DELETE:[\s\S]+?:CAZD\](\n|$)", "", layer)
+
+    # Resets the class contents to defaults
+    def reset(self):
+
+        self.TargetValues = {}
+        self.IsApplyToSingleLayer = False
+        self.LastE = None
+        self.CurrentZ = None
+        self.CurrentLayer = None
+        self.IsTargetByLayer = True
+        self.TargetLayer = None
+        self.TargetZ = None
+        self.LayerHeight = None
+        self.LastValues = {}
+        self.IsLinearRetraction = True
+        self.IsInsideTargetLayer = False
+        self.IsTargetValuesInjected = False
+        self.IsLastValuesRestored = False
+        self.WasInsideTargetLayer = False
+        self.IsEnabled = True
+
+    # Sets the original GCODE line in a given GCODE command
+    @staticmethod
+    def setOriginalLine(line, original) -> str:
+        return line + ";[CAZO:" + original + ":CAZO]"
+
+    # Tracks the change in gcode values we're interested in
+    def trackChangeableValues(self, line: str):
+
+        # simulate a print speed command
+        if ";PRINTSPEED" in line:
+            line = line.replace(";PRINTSPEED ", "M220 S")
+
+        # simulate a retract feedrate command
+        if ";RETRACTFEEDRATE" in line:
+            line = line.replace(";RETRACTFEEDRATE ", "M207 F")
+
+        # simulate a retract length command
+        if ";RETRACTLENGTH" in line:
+            line = line.replace(";RETRACTLENGTH ", "M207 S")
+
+        # get our gcode command
+        command = GCodeCommand.getFromLine(line)
+
+        # stop here if it isn't a G or M command
+        if command is None:
+            return
+
+        # handle retract length changes
+        if command.Command == "M207":
+
+            # get our retract length if provided
+            if "S" in command.Arguments:
+                self.LastValues["retractlength"] = command.getArgumentAsFloat("S")
+
+            # get our retract feedrate if provided, convert from mm/m to mm/s
+            if "F" in command.Arguments:
+                self.LastValues["retractfeedrate"] = command.getArgumentAsFloat("F") / 60.0
+
+            # move to the next command
+            return
+
+        # handle bed temp changes
+        if command.Command == "M140" or command.Command == "M190":
+
+            # get our bed temp if provided
+            if "S" in command.Arguments:
+                self.LastValues["bedTemp"] = command.getArgumentAsFloat("S")
+
+            # move to the next command
+            return
+
+        # handle extruder temp changes
+        if command.Command == "M104" or command.Command == "M109":
+
+            # get our tempurature
+            tempurature = command.getArgumentAsFloat("S")
+
+            # don't bother if we don't have a tempurature
+            if tempurature is None:
+                return
+
+            # get our extruder, default to extruder one
+            extruder = command.getArgumentAsInt("T", None)
+
+            # set our extruder temp based on the extruder
+            if extruder is None or extruder == 0:
+                self.LastValues["extruderOne"] = tempurature
+
+            if extruder is None or extruder == 1:
+                self.LastValues["extruderTwo"] = tempurature
+
+            # move to the next command
+            return
+
+        # handle fan speed changes
+        if command.Command == "M106":
+
+            # get our bed temp if provided
+            if "S" in command.Arguments:
+                self.LastValues["fanSpeed"] = (command.getArgumentAsInt("S") / 255.0) * 100
+
+            # move to the next command
+            return
+
+        # handle flow rate changes
+        if command.Command == "M221":
+
+            # get our flow rate
+            tempurature = command.getArgumentAsFloat("S")
+
+            # don't bother if we don't have a flow rate (for some reason)
+            if tempurature is None:
+                return
+
+            # get our extruder, default to global
+            extruder = command.getArgumentAsInt("T", None)
+
+            # set our extruder temp based on the extruder
+            if extruder is None:
+                self.LastValues["flowrate"] = tempurature
+            elif extruder == 1:
+                self.LastValues["flowrateOne"] = tempurature
+            elif extruder == 1:
+                self.LastValues["flowrateTwo"] = tempurature
+
+            # move to the next command
+            return
+
+        # handle print speed changes
+        if command.Command == "M220":
+
+            # get our speed if provided
+            if "S" in command.Arguments:
+                self.LastValues["speed"] = command.getArgumentAsInt("S")
+
+            # move to the next command
+            return
+
+
+def debug():
+    # get our input file
+    file = r"C:\Users\Wes\Desktop\Archive\gcode test\emit + single layer\AC_Retraction.gcode"
+
+    # read the whole thing
+    f = open(file, "r")
+    gcode = f.read()
+    f.close()
+
+    # boot up change
+    caz_instance = ChangeAtZProcessor()
+    caz_instance.IsTargetByLayer = False
+    caz_instance.TargetZ = 5
+    caz_instance.TargetValues["printspeed"] = 100
+    caz_instance.TargetValues["retractfeedrate"] = 60
+
+    # process gcode
+    gcode = debug_iteration(gcode, caz_instance)
+
+    # write our file
+    debug_write(gcode, file + ".1.modified")
+
+    caz_instance.reset()
+    caz_instance.IsTargetByLayer = False
+    caz_instance.IsDisplayingChangesToLcd = True
+    caz_instance.IsApplyToSingleLayer = False
+    caz_instance.TargetZ = 10.6
+    caz_instance.TargetValues["bedTemp"] = 75.111
+    caz_instance.TargetValues["printspeed"] = 150
+    caz_instance.TargetValues["retractfeedrate"] = 40.555
+    caz_instance.TargetValues["retractlength"] = 10.3333
+
+    # and again
+    gcode = debug_iteration(gcode, caz_instance)
+
+    # write our file
+    debug_write(gcode, file + ".2.modified")
+
+    caz_instance.reset()
+    caz_instance.IsTargetByLayer = False
+    caz_instance.TargetZ = 15
+    caz_instance.IsApplyToSingleLayer = True
+    caz_instance.TargetValues["bedTemp"] = 80
+    caz_instance.TargetValues["printspeed"] = 100
+    caz_instance.TargetValues["retractfeedrate"] = 10
+    caz_instance.TargetValues["retractlength"] = 0
+    caz_instance.TargetValues["extruderOne"] = 100
+    caz_instance.TargetValues["extruderTwo"] = 200
+
+    # and again
+    gcode = debug_iteration(gcode, caz_instance)
+
+    # write our file
+    debug_write(gcode, file + ".3.modified")
+
+
+def debug_write(gcode, file):
+    # write our file
+    f = open(file, "w")
+    f.write(gcode)
+    f.close()
+
+
+def debug_iteration(gcode, caz_instance):
+    index = 0
+
+    # break apart the GCODE like cura
+    layers = re.split(r"^;LAYER:\d+\n", gcode)
+
+    # add the layer numbers back
+    for layer in layers:
+
+        # if this is the first layer, skip it, basically
+        if index == 0:
+            # leave our first layer as is
+            layers[index] = layer
+
+            # move the cursor
+            index += 1
+
+            # skip to the next layer
+            continue
+
+        layers[index] = ";LAYER:" + str(index - 1) + ";\n" + layer
+
+    return "".join(caz_instance.execute(layers))
+
+# debug()

+ 115 - 32
plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py

@@ -1,26 +1,29 @@
 # Copyright (c) 2019 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
-
+import os
 from typing import Dict, List, Optional
+
 from PyQt5.QtCore import QTimer
 
 from UM import i18nCatalog
 from UM.Logger import Logger  # To log errors talking to the API.
+from UM.Message import Message
 from UM.Signal import Signal
 from cura.API import Account
 from cura.CuraApplication import CuraApplication
 from cura.Settings.CuraStackBuilder import CuraStackBuilder
 from cura.Settings.GlobalStack import GlobalStack
-
 from .CloudApiClient import CloudApiClient
 from .CloudOutputDevice import CloudOutputDevice
 from ..Models.Http.CloudClusterResponse import CloudClusterResponse
 
 
-## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
-#  Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
-#  API spec is available on https://api.ultimaker.com/docs/connect/spec/.
 class CloudOutputDeviceManager:
+    """The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
+    
+    Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
+    API spec is available on https://api.ultimaker.com/docs/connect/spec/.
+    """
 
     META_CLUSTER_ID = "um_cloud_cluster_id"
     META_NETWORK_KEY = "um_network_key"
@@ -44,14 +47,16 @@ class CloudOutputDeviceManager:
         # Create a timer to update the remote cluster list
         self._update_timer = QTimer()
         self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000))
-        self._update_timer.setSingleShot(False)
+        # The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates
+        self._update_timer.setSingleShot(True)
         self._update_timer.timeout.connect(self._getRemoteClusters)
 
         # Ensure we don't start twice.
         self._running = False
 
-    ## Starts running the cloud output device manager, thus periodically requesting cloud data.
     def start(self):
+        """Starts running the cloud output device manager, thus periodically requesting cloud data."""
+
         if self._running:
             return
         if not self._account.isLoggedIn:
@@ -61,8 +66,9 @@ class CloudOutputDeviceManager:
             self._update_timer.start()
         self._getRemoteClusters()
 
-    ## Stops running the cloud output device manager.
     def stop(self):
+        """Stops running the cloud output device manager."""
+
         if not self._running:
             return
         self._running = False
@@ -70,47 +76,121 @@ class CloudOutputDeviceManager:
             self._update_timer.stop()
         self._onGetRemoteClustersFinished([])  # Make sure we remove all cloud output devices.
 
-    ## Force refreshing connections.
     def refreshConnections(self) -> None:
+        """Force refreshing connections."""
+
         self._connectToActiveMachine()
 
-    ## Called when the uses logs in or out
     def _onLoginStateChanged(self, is_logged_in: bool) -> None:
+        """Called when the uses logs in or out"""
+
         if is_logged_in:
             self.start()
         else:
             self.stop()
 
-    ## Gets all remote clusters from the API.
     def _getRemoteClusters(self) -> None:
+        """Gets all remote clusters from the API."""
+
         self._api.getClusters(self._onGetRemoteClustersFinished)
 
-    ## Callback for when the request for getting the clusters is finished.
     def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None:
+        """Callback for when the request for getting the clusters is finished."""
+
+        new_clusters = []
         online_clusters = {c.cluster_id: c for c in clusters if c.is_online}  # type: Dict[str, CloudClusterResponse]
         for device_id, cluster_data in online_clusters.items():
             if device_id not in self._remote_clusters:
-                self._onDeviceDiscovered(cluster_data)
+                new_clusters.append(cluster_data)
             else:
                 self._onDiscoveredDeviceUpdated(cluster_data)
 
+        self._onDevicesDiscovered(new_clusters)
+
         removed_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys())
         for device_id in removed_device_keys:
             self._onDiscoveredDeviceRemoved(device_id)
 
-    def _onDeviceDiscovered(self, cluster_data: CloudClusterResponse) -> None:
-        device = CloudOutputDevice(self._api, cluster_data)
-        CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter(
-            ip_address=device.key,
-            key=device.getId(),
-            name=device.getName(),
-            create_callback=self._createMachineFromDiscoveredDevice,
-            machine_type=device.printerType,
-            device=device
+        if new_clusters or removed_device_keys:
+            self.discoveredDevicesChanged.emit()
+        if removed_device_keys:
+            # If the removed device was active we should connect to the new active device
+            self._connectToActiveMachine()
+        # Schedule a new update
+        self._update_timer.start()
+
+    def _onDevicesDiscovered(self, clusters: List[CloudClusterResponse]) -> None:
+        """**Synchronously** create machines for discovered devices
+
+        Any new machines are made available to the user.
+        May take a long time to complete. As this code needs access to the Application
+        and blocks the GIL, creating a Job for this would not make sense.
+        Shows a Message informing the user of progress.
+        """
+        new_devices = []
+        for cluster_data in clusters:
+            device = CloudOutputDevice(self._api, cluster_data)
+            # Create a machine if we don't already have it. Do not make it the active machine.
+            machine_manager = CuraApplication.getInstance().getMachineManager()
+
+            # We only need to add it if it wasn't already added by "local" network or by cloud.
+            if machine_manager.getMachine(device.printerType, {self.META_CLUSTER_ID: device.key}) is None \
+                    and machine_manager.getMachine(device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None:  # The host name is part of the network key.
+                new_devices.append(device)
+
+        if not new_devices:
+            return
+
+        new_devices.sort(key = lambda x: x.name.lower())
+
+        image_path = os.path.join(
+            CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "",
+            "resources", "svg", "cloud-flow-completed.svg"
         )
-        self._remote_clusters[device.getId()] = device
-        self.discoveredDevicesChanged.emit()
-        self._connectToActiveMachine()
+
+        message = Message(
+            title = self.I18N_CATALOG.i18ncp(
+                "info:status",
+                "New printer detected from your Ultimaker account",
+                "New printers detected from your Ultimaker account",
+                len(new_devices)
+            ),
+            progress = 0,
+            lifetime = 0,
+            image_source = image_path
+        )
+        message.show()
+
+        for idx, device in enumerate(new_devices):
+            message_text = self.I18N_CATALOG.i18nc(
+                "info:status", "Adding printer {} ({}) from your account",
+                device.name,
+                device.printerTypeName
+            )
+            message.setText(message_text)
+            if len(new_devices) > 1:
+                message.setProgress((idx / len(new_devices)) * 100)
+            CuraApplication.getInstance().processEvents()
+            self._remote_clusters[device.getId()] = device
+            self._createMachineFromDiscoveredDevice(device.getId(), activate = False)
+
+        message.setProgress(None)
+
+        max_disp_devices = 3
+        if len(new_devices) > max_disp_devices:
+            num_hidden = len(new_devices) - max_disp_devices + 1
+            device_name_list = ["- {} ({})".format(device.name, device.printerTypeName) for device in new_devices[0:num_hidden]]
+            device_name_list.append(self.I18N_CATALOG.i18nc("info:hidden list items", "- and {} others", num_hidden))
+            device_names = "\n".join(device_name_list)
+        else:
+            device_names = "\n".join(["- {} ({})".format(device.name, device.printerTypeName) for device in new_devices])
+
+        message_text = self.I18N_CATALOG.i18nc(
+            "info:status",
+            "Cloud printers added from your account:\n{}",
+            device_names
+        )
+        message.setText(message_text)
 
     def _onDiscoveredDeviceUpdated(self, cluster_data: CloudClusterResponse) -> None:
         device = self._remote_clusters.get(cluster_data.cluster_id)
@@ -128,29 +208,31 @@ class CloudOutputDeviceManager:
         if not device:
             return
         device.close()
-        CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key)
         output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
         if device.key in output_device_manager.getOutputDeviceIds():
             output_device_manager.removeOutputDevice(device.key)
-        self.discoveredDevicesChanged.emit()
 
-    def _createMachineFromDiscoveredDevice(self, key: str) -> None:
+    def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> None:
         device = self._remote_clusters[key]
         if not device:
             return
 
-        # Create a new machine and activate it.
+        # Create a new machine.
         # We do not use use MachineManager.addMachine here because we need to set the cluster ID before activating it.
         new_machine = CuraStackBuilder.createMachine(device.name, device.printerType)
         if not new_machine:
             Logger.log("e", "Failed creating a new machine")
             return
         new_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
-        CuraApplication.getInstance().getMachineManager().setActiveMachine(new_machine.getId())
+
+        if activate:
+            CuraApplication.getInstance().getMachineManager().setActiveMachine(new_machine.getId())
+
         self._connectToOutputDevice(device, new_machine)
 
-    ##  Callback for when the active machine was changed by the user or a new remote cluster was found.
     def _connectToActiveMachine(self) -> None:
+        """Callback for when the active machine was changed by the user"""
+
         active_machine = CuraApplication.getInstance().getGlobalContainerStack()
         if not active_machine:
             return
@@ -169,8 +251,9 @@ class CloudOutputDeviceManager:
                 # Remove device if it is not meant for the active machine.
                 output_device_manager.removeOutputDevice(device.key)
 
-    ## Connects to an output device and makes sure it is registered in the output device manager.
     def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None:
+        """Connects to an output device and makes sure it is registered in the output device manager."""
+
         machine.setName(device.name)
         machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
         machine.setMetaDataEntry("group_name", device.name)

+ 1 - 1
resources/definitions/flsun_qq_s.def.json

@@ -42,7 +42,7 @@
             "default_value": 1.75
         },
         "machine_start_gcode": {
-            "default_value": "G21\nG90\nM82\nM107 T0\nM190 S{material_bed_temperature}\nM109 S{material_print_temperature} T0\nG28\nG92 E0\nG0 E3 F200\nG92 E0\n"
+            "default_value": "G21\nG90\nM82\nM107 T0\nM190 S{material_bed_temperature_layer_0}\nM109 S{material_print_temperature_layer_0} T0\nG28\nG92 E0\nG0 E3 F200\nG92 E0\n"
         },
         "machine_end_gcode": {
             "default_value": "M107 T0\nM104 S0\nM104 S0 T1\nM140 S0\nG92 E0\nG91\nG1 E-1 F300 \nG1 Z+0.5 E-5 X-20 Y-20 F9000\nG28 X0 Y0\nM84 ;steppers off\nG90 ;absolute positioning\n"

+ 8 - 7
resources/definitions/hms434.def.json

@@ -69,7 +69,7 @@
         "material_bed_temp_wait":     {"default_value": false },
         "machine_max_feedrate_z":     {"default_value": 10 },
         "machine_acceleration":       {"default_value": 180 },
-        "machine_start_gcode":        {"default_value": "\n;Neither Hybrid AM Systems nor any of Hybrid AM Systems representatives has any liabilities or gives any warranties on this .gcode file, or on any or all objects made with this .gcode file.\n\nM140 S{material_bed_temperature_layer_0}\n\nM117 Homing Y ......\nG28 Y\nM117 Homing X ......\nG28 X\nM117 Homing Z ......\nG28 Z F100\n\nG1 Z10 F900\nG1 X-30 Y100 F12000\n\nM190 S{material_bed_temperature_layer_0}\nM117 HMS434 Printing ...\n\nM42 P10 S255 ; chamberfans on" },
+        "machine_start_gcode":        {"default_value": "\n;Neither Hybrid AM Systems nor any of Hybrid AM Systems representatives has any liabilities or gives any warranties on this .gcode file, or on any or all objects made with this .gcode file.\n\nM140 S{material_bed_temperature_layer_0}\n\nM117 Homing Y ......\nG28 Y\nM117 Homing X ......\nG28 X\nM117 Homing Z ......\nG28 Z F100\n\nG1 Z10 F900\nG1 X-25 Y20 F12000\n\nM190 S{material_bed_temperature_layer_0}\nM117 HMS434 Printing ...\n\nM42 P10 S255 ; chamberfans on" },
         "machine_end_gcode":          {"default_value": "" },
 
         "retraction_extra_prime_amount":      {"minimum_value_warning": "-2.0" },
@@ -81,18 +81,19 @@
         "layer_height":                 {"maximum_value": "(0.8 * min(extruderValues('machine_nozzle_size')))" },
         "layer_height_0":               {"maximum_value": "(0.8 * min(extruderValues('machine_nozzle_size')))" },
         "line_width":                      {"value": "(machine_nozzle_size + 0.2)" },
-        "wall_line_width_0":               {"value": "(machine_nozzle_size - 0.05)" },
+        "wall_line_width_0":               {"value": "(machine_nozzle_size)" },
         "infill_line_width":               {"value": "(line_width)" },
         "initial_layer_line_width_factor": {"value": 110 },
 
         "wall_thickness":            {"value": "(line_width * 3) if infill_sparse_density < 95 else line_width" },
-        "roofing_layer_count":       {"value": "4" },
-        "top_bottom_thickness":      {"value": "(layer_height_0 + (layer_height * 3))" },
-        "top_layers":                {"value": "4" },
+        "roofing_layer_count":       {"value": "0" },
+        "top_bottom_thickness":      {"value": "(layer_height_0 + (layer_height * (top_layers - 1)))" },
+        "top_layers":                {"value": "4 if infill_sparse_density < 95 else 1" },
         "bottom_layers":             {"value": "(top_layers)" },
         "wall_0_inset":              {"value": "0" },
         "outer_inset_first":         {"value": true },
         "alternate_extra_perimeter": {"value": false },
+        "travel_compensate_overlapping_walls_enabled": {"value": false },
         "filter_out_tiny_gaps":      {"value": true },
         "fill_outline_gaps":         {"value": true },
         "z_seam_type":               {"value": "'shortest'"},
@@ -107,7 +108,7 @@
  
         "infill_sparse_density": {"value": 30},
         "infill_pattern":        {"value": "'lines'"},
-        "infill_before_walls":   {"value": false},
+        "infill_before_walls":   {"value": true},
 
         "material_print_temperature_layer_0": {"value": "material_print_temperature"},
         "material_initial_print_temperature": {"value": "material_print_temperature",
@@ -116,7 +117,7 @@
         "material_bed_temperature_layer_0":   {"value": "material_bed_temperature"},
         "material_flow_layer_0":              {"value": "material_flow"},
         "retraction_enable":                  {"value": true },
-        "retract_at_layer_change":            {"value": true },
+        "retract_at_layer_change":            {"value": false },
         "retraction_min_travel":              {"value": "(round(line_width * 10))"},
         "switch_extruder_retraction_speeds":  {"value": "(retraction_speed)"},
         "switch_extruder_prime_speed":        {"value": "(retraction_prime_speed)"},

+ 2 - 2
resources/extruders/hms434_tool_1.def.json

@@ -16,10 +16,10 @@
         "machine_nozzle_offset_y": { "default_value": 0.0 },
         "material_diameter": { "default_value": 1.75 },
         "machine_extruder_start_code": {
-          "default_value": "\n;changing to tool1\nM83\nM109 T0 S{material_print_temperature}\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y120 F3000\nG1 X10 F12000\n\n"
+          "default_value": "\n;changing to tool1\nM83\nM109 T0 S{material_print_temperature}\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y40 F3000\nG1 X10 F12000\n\n"
         },
         "machine_extruder_end_code": {
-           "default_value": "\nG1 X10 Y120 F12000\nG1 X-40 F12000\nM109 T0 R{material_standby_temperature}\nG1 Y100 F3000\n; ending tool1\n\n"
+           "default_value": "\nG1 X10 Y120 F12000\nG1 X-25 F12000\nM109 T0 R{material_standby_temperature}\nG1 Y20 F3000\n; ending tool1\n\n"
         }
     }
 }

+ 2 - 2
resources/extruders/hms434_tool_2.def.json

@@ -16,10 +16,10 @@
         "machine_nozzle_offset_y": { "default_value": 0.0 },
         "material_diameter": { "default_value": 1.75 },
         "machine_extruder_start_code": {
-          "default_value": "\n;changing to tool2\nM83\nM109 T1 S{material_print_temperature}\nG1 E{switch_retraction_prime_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y120 F3000\nG1 X10 F12000\n\n"
+          "default_value": "\n;changing to tool2\nM83\nM109 T1 S{material_print_temperature}\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y40 F3000\nG1 X10 F12000\n\n"
         },
         "machine_extruder_end_code": {
-           "default_value": "\nG1 X10 Y120 F12000\nG1 X-40 F12000\nM109 T1 R{material_standby_temperature}\nG1 Y100 F3000\n; ending tool2\n\n"
+           "default_value": "\nG1 X10 Y120 F12000\nG1 X-25 F12000\nM109 T1 R{material_standby_temperature}\nG1 Y20 F3000\n; ending tool2\n\n"
         }
     }
 }

+ 5 - 10
resources/shaders/overhang.shader

@@ -68,11 +68,9 @@ fragment =
 
         finalColor = (-normal.y > u_overhangAngle) ? u_overhangColor : finalColor;
 
-        if(u_renderError > 0.5)
-        {
-            vec3 grid = vec3(f_vertex.x - round(f_vertex.x), f_vertex.y - round(f_vertex.y), f_vertex.z - round(f_vertex.z));
-            finalColor.a = dot(grid, grid) < 0.245 ? 0.667 : 1.0;
-        }
+        vec3 grid = vec3(f_vertex.x - floor(f_vertex.x - 0.5), f_vertex.y - floor(f_vertex.y - 0.5), f_vertex.z - floor(f_vertex.z - 0.5));
+        finalColor.a = (u_renderError > 0.5) && dot(grid, grid) < 0.245 ? 0.667 : 1.0;
+
         gl_FragColor = finalColor;
     }
 
@@ -144,11 +142,8 @@ fragment41core =
         finalColor = (u_faceId != gl_PrimitiveID) ? ((-normal.y > u_overhangAngle) ? u_overhangColor : finalColor) : u_faceColor;
 
         frag_color = finalColor;
-        if(u_renderError > 0.5)
-        {
-            vec3 grid = f_vertex - round(f_vertex);
-            frag_color.a = dot(grid, grid) < 0.245 ? 0.667 : 1.0;
-        }
+        vec3 grid = f_vertex - round(f_vertex);
+        frag_color.a = (u_renderError > 0.5) && dot(grid, grid) < 0.245 ? 0.667 : 1.0;
     }
 
 [defaults]