Browse Source

Merge branch 'main' into PP-511-Method-5-9-Rebased

Erwan MATHIEU 5 months ago
parent
commit
fec3f3ec37

+ 1 - 1
cura/CuraApplication.py

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

+ 2 - 1
plugins/CuraEngineBackend/CuraEngineBackend.py

@@ -570,7 +570,8 @@ class CuraEngineBackend(QObject, Backend):
             return
 
         # Preparation completed, send it to the backend.
-        if not self._socket.sendMessage(job.getSliceMessage()):
+        immediate_success = self._socket.sendMessage(job.getSliceMessage())
+        if (not CuraApplication.getInstance().getUseExternalBackend()) and (not immediate_success):
             if self._last_socket_error is not None and self._last_socket_error.getErrorCode() == Arcus.ErrorCode.MessageTooBigError:
                 error_txt = catalog.i18nc("@info:status", "Unable to send the model data to the engine. Please try to use a less detailed model, or reduce the number of instances.")
             else:

+ 191 - 39
plugins/PostProcessingPlugin/scripts/InsertAtLayerChange.py

@@ -1,65 +1,217 @@
 # Copyright (c) 2020 Ultimaker B.V.
-# Cura is released under the terms of the LGPLv3 or higher.
 # Created by Wayne Porter
+# Re-write in April of 2024 by GregValiant (Greg Foresi)
+# Changes:
+#     Added an 'Enable' setting
+#     Added support for multi-line insertions (comma delimited)
+#     Added insertions in a range of layers or a single insertion at a layer.  Numbers are consistent with the Cura Preview (base1)
+#     Added frequency of Insertion (once only, every layer, every 2nd, 3rd, 5th, 10th, 25th, 50th, 100th)
+#     Added support for 'One at a Time' print sequence
+#     Rafts are allowed and accounted for but no insertions are made in raft layers
 
 from ..Script import Script
+import re
+from UM.Application import Application
 
 class InsertAtLayerChange(Script):
-    def __init__(self):
-        super().__init__()
 
     def getSettingDataString(self):
         return """{
-            "name": "Insert at layer change",
+            "name": "Insert at Layer Change",
             "key": "InsertAtLayerChange",
             "metadata": {},
             "version": 2,
             "settings":
             {
-                "insert_location":
+                "enabled":
                 {
-                    "label": "When to insert",
-                    "description": "Whether to insert code before or after layer change.",
+                    "label": "Enable this script",
+                    "description": "You must enable the script for it to run.",
+                    "type": "bool",
+                    "default_value": true,
+                    "enabled": true
+                },
+                "insert_frequency":
+                {
+                    "label": "How often to insert",
+                    "description": "Every so many layers starting with the Start Layer OR as single insertion at a specific layer.  If the print sequence is 'one_at_a_time' then the insertions will be made for every model.  Insertions are made at the beginning of a layer.",
                     "type": "enum",
-                    "options": {"before": "Before", "after": "After"},
-                    "default_value": "before"
+                    "options": {
+                        "once_only": "One insertion only",
+                        "every_layer": "Every Layer",
+                        "every_2nd": "Every 2nd",
+                        "every_3rd": "Every 3rd",
+                        "every_5th": "Every 5th",
+                        "every_10th": "Every 10th",
+                        "every_25th": "Every 25th",
+                        "every_50th": "Every 50th",
+                        "every_100th": "Every 100th"},
+                    "default_value": "every_layer",
+                    "enabled": "enabled"
                 },
-                "gcode_to_add":
+                "start_layer":
                 {
-                    "label": "G-code to insert",
-                    "description": "G-code to add before or after layer change.",
-                    "type": "str",
-                    "default_value": ""
+                    "label": "Starting Layer",
+                    "description": "The layer before which the first insertion will take place.  If the Print_Sequence is 'All at Once' then use the layer numbers from the Cura Preview.  Enter '1' to start at gcode LAYER:0. In 'One at a Time' mode use the layer numbers from the first model that prints AND all models will receive the same insertions.  NOTE: There is never an insertion for raft layers.",
+                    "type": "int",
+                    "default_value": 1,
+                    "minimum_value": 1,
+                    "enabled": "insert_frequency != 'once_only' and enabled"
                 },
-                "skip_layers":
+                "end_layer":
                 {
-                    "label": "Skip layers",
-                    "description": "Number of layers to skip between insertions (0 for every layer).",
+                    "label": "Ending Layer",
+                    "description": "The layer before which the last insertion will take place.  Enter '-1' to indicate the entire file.  Use layer numbers from the Cura Preview.",
                     "type": "int",
-                    "default_value": 0,
-                    "minimum_value": 0
+                    "default_value": -1,
+                    "minimum_value": -1,
+                    "enabled": "insert_frequency != 'once_only' and enabled"
+                },
+                "single_end_layer":
+                {
+                    "label": "Layer # for Single Insertion.",
+                    "description": "The layer before which the Gcode insertion will take place.  Use the layer numbers from the Cura Preview.",
+                    "type": "str",
+                    "default_value": "",
+                    "enabled": "insert_frequency == 'once_only' and enabled"
+                },
+                "gcode_to_add":
+                {
+                    "label": "G-code to insert.",
+                    "description": "G-code to add at start of the layer. Use a comma to delimit multi-line commands. EX: G28 X Y,M220 S100,M117 HELL0.  NOTE:  All inserted text will be converted to upper-case as some firmwares don't understand lower-case.",
+                    "type": "str",
+                    "default_value": "",
+                    "enabled": "enabled"
                 }
             }
         }"""
 
     def execute(self, data):
-        gcode_to_add = self.getSettingValueByKey("gcode_to_add") + "\n"
-        skip_layers = self.getSettingValueByKey("skip_layers")
-        count = 0
-        for layer in data:
-            # Check that a layer is being printed
-            lines = layer.split("\n")
-            for line in lines:
-                if ";LAYER:" in line:
-                    index = data.index(layer)
-                    if count == 0:
-                        if self.getSettingValueByKey("insert_location") == "before":
-                            layer = gcode_to_add + layer
-                        else:
-                            layer = layer + gcode_to_add
-
-                        data[index] = layer
-
-                    count = (count + 1) % (skip_layers + 1)
-                    break
-        return data
+        # Exit if the script is not enabled
+        if not bool(self.getSettingValueByKey("enabled")):
+            return data
+        #Initialize variables
+        mycode = self.getSettingValueByKey("gcode_to_add").upper()
+        start_layer = int(self.getSettingValueByKey("start_layer"))
+        end_layer = int(self.getSettingValueByKey("end_layer"))
+        when_to_insert = self.getSettingValueByKey("insert_frequency")
+        end_list = [0]
+        print_sequence = Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value")
+        # Get the topmost layer number and adjust the end_list
+        if end_layer == -1:
+            if print_sequence == "all_at_once":
+                for lnum in range(0, len(data) - 1):
+                    if ";LAYER:" in data[lnum]:
+                        the_top = int(data[lnum].split(";LAYER:")[1].split("\n")[0])
+                end_list[0] = the_top
+            # Get the topmost layer number for each model and append it to the end_list
+            if print_sequence == "one_at_a_time":
+                for lnum in range(0, 10):
+                    if ";LAYER:0" in data[lnum]:
+                        start_at = lnum + 1
+                        break
+                for lnum in range(start_at, len(data)-1, 1):
+                    if ";LAYER:" in data[lnum] and not ";LAYER:0" in data[lnum] and not ";LAYER:-" in data[lnum]:
+                        end_list[len(end_list) - 1] = int(data[lnum].split(";LAYER:")[1].split("\n")[0])
+                        continue
+                    if ";LAYER:0" in data[lnum]:
+                        end_list.append(0)
+        elif end_layer != -1:
+            if print_sequence == "all_at_once":
+                # Catch an error if the entered End_Layer > the top layer in the gcode
+                for e_num, layer in enumerate(data):
+                    if ";LAYER:" in layer:
+                        top_layer = int(data[e_num].split(";LAYER:")[1].split("\n")[0])
+                end_list[0] = end_layer - 1
+                if top_layer < end_layer - 1:
+                    end_list[0] = top_layer
+            elif print_sequence == "one_at_a_time":
+                # Find the index of the first Layer:0
+                for lnum in range(0, 10):
+                    if ";LAYER:0" in data[lnum]:
+                        start_at = lnum + 1
+                        break
+                # Get the top layer number for each model
+                for lnum in range(start_at, len(data)-1):
+                    if ";LAYER:" in data[lnum] and not ";LAYER:0" in data[lnum] and not ";LAYER:-" in data[lnum]:
+                        end_list[len(end_list) - 1] = int(data[lnum].split(";LAYER:")[1].split("\n")[0])
+                    if ";LAYER:0" in data[lnum]:
+                        end_list.append(0)
+                # Adjust the end list if an end layer was named
+                for index, num in enumerate(end_list):
+                    if num > end_layer - 1:
+                        end_list[index] = end_layer - 1
+        #If the gcode_to_enter is multi-line then replace the commas with newline characters
+        if mycode != "":
+            if "," in mycode:
+                mycode = re.sub(",", "\n",mycode)
+            gcode_to_add = mycode
+        #Get the insertion frequency
+        match when_to_insert:
+            case "every_layer":
+                freq = 1
+            case "every_2nd":
+                freq = 2
+            case "every_3rd":
+                freq = 3
+            case "every_5th":
+                freq = 5
+            case "every_10th":
+                freq = 10
+            case "every_25th":
+                freq = 25
+            case "every_50th":
+                freq = 50
+            case "every_100th":
+                freq = 100
+            case "once_only":
+                the_insert_layer = int(self.getSettingValueByKey("single_end_layer"))-1
+            case _:
+                raise ValueError(f"Unexpected insertion frequency {when_to_insert}")
+        #Single insertion
+        if when_to_insert == "once_only":
+            # For print sequence 'All at once'
+            if print_sequence == "all_at_once":
+                for index, layer in enumerate(data):
+                    if ";LAYER:" + str(the_insert_layer) + "\n" in layer:
+                        lines = layer.split("\n")
+                        lines.insert(1,gcode_to_add)
+                        data[index] = "\n".join(lines)
+                        return data
+            # For print sequence 'One at a time'
+            else:
+                for index, layer in enumerate(data):
+                    if ";LAYER:" + str(the_insert_layer) + "\n" in layer:
+                        lines = layer.split("\n")
+                        lines.insert(1,gcode_to_add)
+                        data[index] = "\n".join(lines)
+                return data
+        # For multiple insertions
+        if when_to_insert != "once_only":
+            # Search from the line after the first Layer:0 so we know when a model ends if in One at a Time mode.
+            first_0 = True
+            next_layer = start_layer - 1
+            end_layer = end_list.pop(0)
+            for index, layer in enumerate(data):
+                lines = layer.split("\n")
+                for l_index, line in enumerate(lines):
+                    if ";LAYER:" in line:
+                        layer_number = int(line.split(":")[1])
+                        if layer_number == next_layer and layer_number <= end_layer:
+                            lines.insert(l_index + 1,gcode_to_add)
+                            data[index] = "\n".join(lines)
+                            next_layer += freq
+                        # Reset the next_layer for one-at-a-time
+                        if next_layer > int(end_layer):
+                            next_layer = start_layer - 1
+                        # Index to the next end_layer when a Layer:0 is encountered
+                        try:
+                            if not first_0 and layer_number == 0:
+                                end_layer = end_list.pop(0)
+                        except:
+                            pass
+                        # Beyond the initial Layer:0 futher Layer:0's indicate the top layer of a model.
+                        if layer_number == 0:
+                            first_0 = False
+                        break
+        return data

+ 178 - 71
plugins/PostProcessingPlugin/scripts/TimeLapse.py

@@ -1,9 +1,15 @@
-# Copyright (c) 2020 Ultimaker B.V.
-# Cura is released under the terms of the LGPLv3 or higher.
-# Created by Wayne Porter
+# Modified 5/15/2023 - Greg Valiant (Greg Foresi)
+#    Created by Wayne Porter
+#    Added insertion frequency
+#    Adjusted for use with Relative Extrusion
+#    Changed Retract to a boolean and when true use the regular Cura retract settings.
+#    Use the regular Cura settings for Travel Speed and Speed_Z instead of asking.
+#    Added code to check the E location to prevent retracts if the filament was already retracted.
+#    Added 'Pause before image' per LemanRus
 
 from ..Script import Script
-
+from UM.Application import Application
+from UM.Logger import Logger
 
 class TimeLapse(Script):
     def __init__(self):
@@ -11,7 +17,7 @@ class TimeLapse(Script):
 
     def getSettingDataString(self):
         return """{
-            "name": "Time Lapse",
+            "name": "Time Lapse Camera",
             "key": "TimeLapse",
             "metadata": {},
             "version": 2,
@@ -19,24 +25,49 @@ class TimeLapse(Script):
             {
                 "trigger_command":
                 {
-                    "label": "Trigger camera command",
-                    "description": "G-code command used to trigger camera.",
+                    "label": "Camera Trigger Command",
+                    "description": "G-code command used to trigger the camera.  The setting box will take any command and parameters.",
                     "type": "str",
                     "default_value": "M240"
                 },
+                "insert_frequency":
+                {
+                    "label": "How often (layers)",
+                    "description": "Every so many layers (always starts at the first layer whether it's the model or a raft).",
+                    "type": "enum",
+                    "options": {
+                        "every_layer": "Every Layer",
+                        "every_2nd": "Every 2nd",
+                        "every_3rd": "Every 3rd",
+                        "every_5th": "Every 5th",
+                        "every_10th": "Every 10th",
+                        "every_25th": "Every 25th",
+                        "every_50th": "Every 50th",
+                        "every_100th": "Every 100th"},
+                    "default_value": "every_layer"
+                },
+                "anti_shake_length":
+                {
+                    "label": "Pause before image",
+                    "description": "How long to wait (in ms) before capturing the image.  This is to allow the printer to 'settle down' after movement.  To disable set this to '0'.",
+                    "type": "int",
+                    "default_value": 0,
+                    "minimum_value": 0,
+                    "unit": "ms"
+                },
                 "pause_length":
                 {
-                    "label": "Pause length",
+                    "label": "Pause after image",
                     "description": "How long to wait (in ms) after camera was triggered.",
                     "type": "int",
-                    "default_value": 700,
+                    "default_value": 500,
                     "minimum_value": 0,
                     "unit": "ms"
                 },
                 "park_print_head":
                 {
                     "label": "Park Print Head",
-                    "description": "Park the print head out of the way. Assumes absolute positioning.",
+                    "description": "Park the print head out of the way.",
                     "type": "bool",
                     "default_value": true
                 },
@@ -55,90 +86,166 @@ class TimeLapse(Script):
                     "description": "What Y location does the head move to for photo.",
                     "unit": "mm",
                     "type": "float",
-                    "default_value": 190,
-                    "enabled": "park_print_head"
-                },
-                "park_feed_rate":
-                {
-                    "label": "Park Feed Rate",
-                    "description": "How fast does the head move to the park coordinates.",
-                    "unit": "mm/s",
-                    "type": "float",
-                    "default_value": 9000,
+                    "default_value": 0,
                     "enabled": "park_print_head"
                 },
                 "retract":
                 {
-                    "label": "Retraction Distance",
-                    "description": "Filament retraction distance for camera trigger.",
-                    "unit": "mm",
-                    "type": "int",
-                    "default_value": 0
+                    "label": "Retract when required",
+                    "description": "Retract if there isn't already a retraction.  If unchecked then there will be no retraction even if there is none in the gcode.  If retractions are not enabled in Cura there won't be a retraction. regardless of this setting.",
+                    "type": "bool",
+                    "default_value": true
                 },
                 "zhop":
                 {
                     "label": "Z-Hop Height When Parking",
-                    "description": "Z-hop length before parking",
+                    "description": "The height to lift the nozzle off the print before parking.",
                     "unit": "mm",
                     "type": "float",
-                    "default_value": 0
+                    "default_value": 2.0,
+                    "minimum_value": 0.0
+                },
+                "ensure_final_image":
+                {
+                    "label": "Ensure Final Image",
+                    "description": "Depending on how the layer numbers work out with the 'How Often' frequency there might not be an image taken at the end of the last layer.  This will ensure that one is taken.  There is no parking as the Ending Gcode comes right up.",
+                    "type": "bool",
+                    "default_value": false
                 }
             }
         }"""
 
     def execute(self, data):
-        feed_rate = self.getSettingValueByKey("park_feed_rate")
+        mycura = Application.getInstance().getGlobalContainerStack()
+        relative_extrusion = bool(mycura.getProperty("relative_extrusion", "value"))
+        extruder = mycura.extruderList
+        retract_speed = int(extruder[0].getProperty("retraction_speed", "value"))*60
+        retract_dist = round(float(extruder[0].getProperty("retraction_amount", "value")), 2)
+        retract_enabled = bool(extruder[0].getProperty("retraction_enable", "value"))
+        firmware_retract = bool(mycura.getProperty("machine_firmware_retract", "value"))
+        speed_z = int(extruder[0].getProperty("speed_z_hop", "value"))*60
+        if relative_extrusion:
+            rel_cmd = 83
+        else:
+            rel_cmd = 82
+        travel_speed = int(extruder[0].getProperty("speed_travel", "value"))*60
         park_print_head = self.getSettingValueByKey("park_print_head")
         x_park = self.getSettingValueByKey("head_park_x")
         y_park = self.getSettingValueByKey("head_park_y")
         trigger_command = self.getSettingValueByKey("trigger_command")
         pause_length = self.getSettingValueByKey("pause_length")
-        retract = int(self.getSettingValueByKey("retract"))
+        retract = bool(self.getSettingValueByKey("retract"))
         zhop = self.getSettingValueByKey("zhop")
-        gcode_to_append = ";TimeLapse Begin\n"
+        ensure_final_image = bool(self.getSettingValueByKey("ensure_final_image"))
+        when_to_insert = self.getSettingValueByKey("insert_frequency")
         last_x = 0
         last_y = 0
         last_z = 0
-
+        last_e = 0
+        prev_e = 0
+        is_retracted = False
+        gcode_to_append = ""
         if park_print_head:
-            gcode_to_append += self.putValue(G=1, F=feed_rate,
-                                             X=x_park, Y=y_park) + " ;Park print head\n"
-        gcode_to_append += self.putValue(M=400) + " ;Wait for moves to finish\n"
-        gcode_to_append += trigger_command + " ;Snap Photo\n"
-        gcode_to_append += self.putValue(G=4, P=pause_length) + " ;Wait for camera\n"
-
-        for idx, layer in enumerate(data):
-            for line in layer.split("\n"):
-                if self.getValue(line, "G") in {0, 1}:  # Track X,Y,Z location.
-                    last_x = self.getValue(line, "X", last_x)
-                    last_y = self.getValue(line, "Y", last_y)
-                    last_z = self.getValue(line, "Z", last_z)
-            # Check that a layer is being printed
-            lines = layer.split("\n")
-            for line in lines:
-                if ";LAYER:" in line:
-                    if retract != 0: # Retract the filament so no stringing happens
-                        layer += self.putValue(M=83) + " ;Extrude Relative\n"
-                        layer += self.putValue(G=1, E=-retract, F=3000) + " ;Retract filament\n"
-                        layer += self.putValue(M=82) + " ;Extrude Absolute\n"
-                        layer += self.putValue(M=400) + " ;Wait for moves to finish\n" # Wait to fully retract before hopping
-
-                    if zhop != 0:
-                        layer += self.putValue(G=1, Z=last_z+zhop, F=3000) + " ;Z-Hop\n"
-
-                    layer += gcode_to_append
-
-                    if zhop != 0:
-                        layer += self.putValue(G=0, X=last_x, Y=last_y, Z=last_z) + "; Restore position \n"
-                    else:
-                        layer += self.putValue(G=0, X=last_x, Y=last_y) + "; Restore position \n"
-
-                    if retract != 0:
-                        layer += self.putValue(M=400) + " ;Wait for moves to finish\n"
-                        layer += self.putValue(M=83) + " ;Extrude Relative\n"
-                        layer += self.putValue(G=1, E=retract, F=3000) + " ;Retract filament\n"
-                        layer += self.putValue(M=82) + " ;Extrude Absolute\n"
-
-                    data[idx] = layer
-                    break
+            gcode_to_append += f"G0 F{travel_speed} X{x_park} Y{y_park} ;Park print head\n"
+        gcode_to_append += "M400 ;Wait for moves to finish\n"
+        anti_shake_length = self.getSettingValueByKey("anti_shake_length")
+        if anti_shake_length > 0:
+            gcode_to_append += f"G4 P{anti_shake_length} ;Wait for printer to settle down\n"
+        gcode_to_append += trigger_command + " ;Snap the Image\n"
+        gcode_to_append += f"G4 P{pause_length} ;Wait for camera to finish\n"
+        match when_to_insert:
+            case "every_layer":
+                step_freq = 1
+            case "every_2nd":
+                step_freq = 2
+            case "every_3rd":
+                step_freq = 3
+            case "every_5th":
+                step_freq = 5
+            case "every_10th":
+                step_freq = 10
+            case "every_25th":
+                step_freq = 25
+            case "every_50th":
+                step_freq = 50
+            case "every_100th":
+                step_freq = 100
+            case _:
+                step_freq = 1
+        # Use the step_freq to index through the layers----------------------------------------
+        for num in range(2,len(data)-1,step_freq):
+            layer = data[num]
+            try:
+                # Track X,Y,Z location.--------------------------------------------------------
+                for line in layer.split("\n"):
+                    if self.getValue(line, "G") in {0, 1}:
+                        last_x = self.getValue(line, "X", last_x)
+                        last_y = self.getValue(line, "Y", last_y)
+                        last_z = self.getValue(line, "Z", last_z)
+                #Track the E location so that if there is already a retraction we don't double dip.
+                        if rel_cmd == 82:
+                            if " E" in line:
+                                last_e = line.split("E")[1]
+                                if float(last_e) < float(prev_e):
+                                    is_retracted = True
+                                else:
+                                    is_retracted = False
+                                prev_e = last_e
+                        elif rel_cmd == 83:
+                            if " E" in line:
+                                last_e = line.split("E")[1]
+                                if float(last_e) < 0:
+                                    is_retracted = True
+                                else:
+                                    is_retracted = False
+                                prev_e = last_e
+                    if firmware_retract and self.getValue(line, "G") in {10, 11}:
+                        if self.getValue(line, "G") == 10:
+                            is_retracted = True
+                            last_e = float(prev_e) - float(retract_dist)
+                        if self.getValue(line, "G") == 11:
+                            is_retracted = False
+                            last_e = float(prev_e) + float(retract_dist)
+                        prev_e = last_e
+                lines = layer.split("\n")
+                # Insert the code----------------------------------------------------
+                camera_code = ""
+                for line in lines:
+                    if ";LAYER:" in line:
+                        if retract and not is_retracted and retract_enabled: # Retract unless already retracted
+                            camera_code += ";TYPE:CUSTOM-----------------TimeLapse Begin\n"
+                            camera_code += "M83 ;Extrude Relative\n"
+                            if not firmware_retract:
+                                camera_code += f"G1 F{retract_speed} E-{retract_dist} ;Retract filament\n"
+                            else:
+                                camera_code += "G10 ;Retract filament\n"
+                        else:
+                            camera_code += ";TYPE:CUSTOM-----------------TimeLapse Begin\n"
+                        if zhop != 0:
+                            camera_code += f"G1 F{speed_z} Z{round(last_z + zhop,2)} ;Z-Hop\n"
+                        camera_code += gcode_to_append
+                        camera_code += f"G0 F{travel_speed} X{last_x} Y{last_y} ;Restore XY position\n"
+                        if zhop != 0:
+                            camera_code += f"G0 F{speed_z} Z{last_z} ;Restore Z position\n"
+                        if retract and not is_retracted and retract_enabled:
+                            if not firmware_retract:
+                                camera_code += f"G1 F{retract_speed} E{retract_dist} ;Un-Retract filament\n"
+                            else:
+                                camera_code += "G11 ;Un-Retract filament\n"
+                            camera_code += f"M{rel_cmd} ;Extrude Mode\n"
+                        camera_code += f";{'-' * 28}TimeLapse End"
+                        # Format the camera code to be inserted
+                        temp_lines = camera_code.split("\n")
+                        for temp_index, temp_line in enumerate(temp_lines):
+                            if ";" in temp_line and not temp_line.startswith(";"):
+                                temp_lines[temp_index] = temp_line.replace(temp_line.split(";")[0], temp_line.split(";")[0] + str(" " * (29 - len(temp_line.split(";")[0]))),1)
+                        temp_lines = "\n".join(temp_lines)
+                        lines.insert(len(lines) - 2, temp_lines)
+                        data[num] = "\n".join(lines)
+                        break
+            except Exception as e:
+                Logger.log("w", "TimeLapse Error: " + repr(e))
+        # Take a final image if there was no camera shot at the end of the last layer.
+        if "TimeLapse Begin" not in data[len(data) - (3 if retract_enabled else 2)] and ensure_final_image:
+            data[len(data)-1] = "M400    ; Wait for all moves to finish\n" + trigger_command + "    ;Snap the final Image\n" + f"G4 P{pause_length} ;Wait for camera\n" + data[len(data)-1]
         return data

+ 103 - 0
plugins/VersionUpgrade/VersionUpgrade58to59/VersionUpgrade58to59.py

@@ -0,0 +1,103 @@
+# Copyright (c) 2024 UltiMaker
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import configparser
+from typing import Dict, List, Tuple
+import io
+from UM.VersionUpgrade import VersionUpgrade
+
+# Just to be sure, since in my testing there were both 0.1.0 and 0.2.0 settings about.
+_PLUGIN_NAME = "_plugin__curaenginegradualflow"
+_FROM_PLUGINS_SETTINGS = {
+    "gradual_flow_enabled",
+    "max_flow_acceleration",
+    "layer_0_max_flow_acceleration",
+    "gradual_flow_discretisation_step_size",
+    "reset_flow_duration",
+}  # type: Set[str]
+
+_NEW_SETTING_VERSION = "24"
+
+
+class VersionUpgrade58to59(VersionUpgrade):
+    def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+        """
+        Upgrades preferences to remove from the visibility list the settings that were removed in this version.
+        It also changes the preferences to have the new version number.
+
+        This removes any settings that were removed in the new Cura version.
+        :param serialized: The original contents of the preferences file.
+        :param filename: The file name of the preferences file.
+        :return: A list of new file names, and a list of the new contents for
+        those files.
+        """
+        parser = configparser.ConfigParser(interpolation = None)
+        parser.read_string(serialized)
+
+        # Update version number.
+        parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
+
+        # Fix renamed settings for visibility
+        if "visible_settings" in parser["general"]:
+            all_setting_keys = parser["general"]["visible_settings"].strip().split(";")
+            if all_setting_keys:
+                for idx, key in enumerate(all_setting_keys):
+                    if key.startswith(_PLUGIN_NAME):
+                        all_setting_keys[idx] = key.split("__")[-1]
+                parser["general"]["visible_settings"] = ";".join(all_setting_keys)
+
+        result = io.StringIO()
+        parser.write(result)
+        return [filename], [result.getvalue()]
+
+    def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+        """
+        Upgrades instance containers to remove the settings that were removed in this version.
+        It also changes the instance containers to have the new version number.
+
+        This removes any settings that were removed in the new Cura version and updates settings that need to be updated
+        with a new value.
+
+        :param serialized: The original contents of the instance container.
+        :param filename: The original file name of the instance container.
+        :return: A list of new file names, and a list of the new contents for
+        those files.
+        """
+        parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ())
+        parser.read_string(serialized)
+
+        # Update version number.
+        parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
+
+        # Rename settings.
+        if "values" in parser:
+            for key, value in parser["values"].items():
+                if key.startswith(_PLUGIN_NAME):
+                    parser["values"][key.split("__")[-1]] = parser["values"][key]
+                    del parser["values"][key]
+
+        result = io.StringIO()
+        parser.write(result)
+        return [filename], [result.getvalue()]
+
+    def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+        """
+        Upgrades stacks to have the new version number.
+
+        :param serialized: The original contents of the stack.
+        :param filename: The original file name of the stack.
+        :return: A list of new file names, and a list of the new contents for
+        those files.
+        """
+        parser = configparser.ConfigParser(interpolation = None)
+        parser.read_string(serialized)
+
+        # Update version number.
+        if "metadata" not in parser:
+            parser["metadata"] = {}
+
+        parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
+
+        result = io.StringIO()
+        parser.write(result)
+        return [filename], [result.getvalue()]

+ 61 - 0
plugins/VersionUpgrade/VersionUpgrade58to59/__init__.py

@@ -0,0 +1,61 @@
+# Copyright (c) 2024 UltiMaker
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from typing import Any, Dict, TYPE_CHECKING
+
+from . import VersionUpgrade58to59
+
+if TYPE_CHECKING:
+    from UM.Application import Application
+
+upgrade = VersionUpgrade58to59.VersionUpgrade58to59()
+
+
+def getMetaData() -> Dict[str, Any]:
+    return {
+        "version_upgrade": {
+            # From                           To                              Upgrade function
+            ("preferences", 7000023):        ("preferences", 7000024,        upgrade.upgradePreferences),
+            ("machine_stack", 6000023):      ("machine_stack", 6000024,      upgrade.upgradeStack),
+            ("extruder_train", 6000023):     ("extruder_train", 6000024,     upgrade.upgradeStack),
+            ("definition_changes", 4000023): ("definition_changes", 4000024, upgrade.upgradeInstanceContainer),
+            ("quality_changes", 4000023):    ("quality_changes", 4000024,    upgrade.upgradeInstanceContainer),
+            ("quality", 4000023):            ("quality", 4000024,            upgrade.upgradeInstanceContainer),
+            ("user", 4000023):               ("user", 4000024,               upgrade.upgradeInstanceContainer),
+            ("intent", 4000023):             ("intent", 4000024,             upgrade.upgradeInstanceContainer),
+        },
+        "sources": {
+            "preferences": {
+                "get_version": upgrade.getCfgVersion,
+                "location": {"."}
+            },
+            "machine_stack": {
+                "get_version": upgrade.getCfgVersion,
+                "location": {"./machine_instances"}
+            },
+            "extruder_train": {
+                "get_version": upgrade.getCfgVersion,
+                "location": {"./extruders"}
+            },
+            "definition_changes": {
+                "get_version": upgrade.getCfgVersion,
+                "location": {"./definition_changes"}
+            },
+            "quality_changes": {
+                "get_version": upgrade.getCfgVersion,
+                "location": {"./quality_changes"}
+            },
+            "quality": {
+                "get_version": upgrade.getCfgVersion,
+                "location": {"./quality"}
+            },
+            "user": {
+                "get_version": upgrade.getCfgVersion,
+                "location": {"./user"}
+            }
+        }
+    }
+
+
+def register(app: "Application") -> Dict[str, Any]:
+    return {"version_upgrade": upgrade}

+ 8 - 0
plugins/VersionUpgrade/VersionUpgrade58to59/plugin.json

@@ -0,0 +1,8 @@
+{
+    "name": "Version Upgrade 5.8 to 5.9",
+    "author": "UltiMaker",
+    "version": "1.0.0",
+    "description": "Upgrades configurations from Cura 5.8 to Cura 5.9.",
+    "api": 8,
+    "i18n-catalog": "cura"
+}

+ 1 - 1
resources/intent/deltacomb/ABS/deltacomb_DBE0.40_ABS_accurate_B.inst.cfg

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

+ 1 - 1
resources/intent/deltacomb/ABS/deltacomb_DBE0.40_ABS_accurate_C.inst.cfg

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

+ 1 - 1
resources/intent/deltacomb/ABS/deltacomb_DBE0.40_ABS_accurate_D.inst.cfg

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

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