Merge branch 'main' into CreateThumbnailBeginBlockOptions

This commit is contained in:
HellAholic 2025-06-16 10:23:10 +02:00 committed by GitHub
commit cda8c33c2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8282 changed files with 165234 additions and 83072 deletions

View file

@ -122,7 +122,7 @@ class Script:
if not key in line or (';' in line and line.find(key) > line.find(';')):
return default
sub_part = line[line.find(key) + 1:]
m = re.search('^-?[0-9]+\.?[0-9]*', sub_part)
m = re.search(r'^-?[0-9]+\.?[0-9]*', sub_part)
if m is None:
return default
try:

View file

@ -1,16 +1,21 @@
# Designed in January 2023 by GregValiant (Greg Foresi)
## My design intent was to make this as full featured and "industrial strength" as I could. People printing exotic materials on large custom printers may want to turn the fans off for certain layers, and then back on again later in the print. This script allows that.
# Functions:
## Remove all fan speed lines from the file (optional). This should be enabled for the first instance of the script. It is disabled by default in any following instances.
## "By Layer" allows the user to adjust the fan speed up, or down, or off, within the print. "By Feature" allows different fan speeds for different features (;TYPE:WALL-OUTER, etc.).
## If 'By Feature' then a Start Layer and/or an End Layer can be defined.
## Fan speeds are scaled PWM (0 - 255) or RepRap (0.0 - 1.0) depending on {machine_scale_fan_speed_zero_to_one}.
## A minimum fan speed of 12% is enforced. It is the slowest speed that my cooling fan will turn on so that's what I used. 'M106 S14' (as Cura might insert) was pretty useless.
## If multiple extruders have separate fan circuits the speeds are set at tool changes and conform to the layer or feature setting. There is support for up to 4 layer cooling fan circuits.
## My thanks to @5axes(@CUQ), @fieldOfView(@AHoeben), @Ghostkeeper, and @Torgeir. A special thanks to @RBurema for his patience in reviewing my 'non-pythonic' script.
## 9/14/23 (Greg Foresi) Added support for One-at-a-Time print sequence.
## 12/15/23 (Greg Foresi) Split off 'Single Fan By Layer', 'Multi-fan By Layer', 'Single Fan By Feature', and 'Multi-fan By Feature' from the main 'execute' script.
## 1/5/24 (Greg Foresi) Revised the regex replacements.
"""
Designed in January 2023 by GregValiant (Greg Foresi)
My design intent was to make this as full featured and "industrial strength" as I could. People printing exotic materials on large custom printers may want to turn the fans off for certain layers, and then back on again later in the print. This script allows that.
Functions:
Remove all fan speed lines from the file (optional). This should be enabled for the first instance of the script. It is disabled by default in any following instances.
"By Layer" allows the user to adjust the fan speed up, or down, or off, within the print. "By Feature" allows different fan speeds for different features (;TYPE:WALL-OUTER, etc.).
If 'By Feature' then a Start Layer and/or an End Layer can be defined.
Fan speeds are scaled PWM (0 - 255) or RepRap (0.0 - 1.0) depending on {machine_scale_fan_speed_zero_to_one}.
A minimum fan speed of 12% is enforced. It is the slowest speed that my cooling fan will turn on so that's what I used. 'M106 S14' (as Cura might insert) was pretty useless.
If multiple extruders have separate fan circuits the speeds are set at tool changes and conform to the layer or feature setting. There is support for up to 4 layer cooling fan circuits.
My thanks to @5axes(@CUQ), @fieldOfView(@AHoeben), @Ghostkeeper, and @Torgeir. A special thanks to @RBurema for his patience in reviewing my 'non-pythonic' script.
Changes:
09/14/23 (GV) Added support for One-at-a-Time print sequence.
12/15/23 (GV) Split off 'Single Fan By Layer', 'Multi-fan By Layer', 'Single Fan By Feature', and 'Multi-fan By Feature' from the main 'execute' script.
01/05/24 (GV) Revised the regex replacements.
12/11/24 (GV) Added 'off_fan_speed' for the idle nozzle layer cooling fan. It does not have to go to 0%.
03/22/25 (GV) Added 'Chamber Cooling Fan / Auxiliary Fan' control.
"""
from ..Script import Script
from UM.Application import Application
@ -43,7 +48,8 @@ class AddCoolingProfile(Script):
"type": "bool",
"enabled": true,
"value": true,
"default_value": true
"default_value": true,
"read_only": true
},
"feature_fan_start_layer":
{
@ -273,67 +279,180 @@ class AddCoolingProfile(Script):
"maximum_value": 100,
"unit": "% ",
"enabled": "fan_enable_raft"
},
"enable_off_fan_speed":
{
"label": "Enable 'Off speed' of the idle fan",
"description": "For machines with independent layer cooling fans. Leaving a fan running while the other nozzle is printing can help with oozing. You can pick the speed % for the idle nozzle layer cooling fan to hold at.",
"type": "bool",
"default_value": false,
"enabled": "enable_off_fan_speed_enable and self.extruder_count > 1"
},
"off_fan_speed":
{
"label": " 'Off' speed of idle nozzle fan",
"description": "This is the speed that the 'idle nozzle' layer cooling fan will maintain rather than being turned off completely.",
"type": "int",
"default_value": 35,
"minimum_value": 0,
"maximum_value": 100,
"unit": "% ",
"enabled": "enable_off_fan_speed_enable and enable_off_fan_speed and self.extruder_count > 1"
},
"enable_off_fan_speed_enable":
{
"label": "Hidden setting",
"description": "For dual extruder printers, this enables 'enable_off_fan_speed'.",
"type": "bool",
"default_value": false,
"enabled": false
},
"bv_fan_speed_control_enable":
{
"label": "Enable 'Chamber/Aux Fan' control",
"description": "Controls the 'Build Volume Fan' or an 'Auxiliary Fan' on printers with that hardware. Provides: 'On' layer, 'Off' layer, and PWM speed control of a secondary fan.",
"type": "bool",
"default_value": false,
"enabled": "enable_bv_fan"
},
"bv_fan_nr":
{
"label": " Chamber/Aux Fan Number",
"description": "The mainboard circuit number of the Chamber or Auxiliary Fan.",
"type": "int",
"unit": "# ",
"default_value": 0,
"minimum_value": 0,
"enabled": "enable_bv_fan and bv_fan_speed_control_enable"
},
"bv_fan_speed":
{
"label": " Chamber/Aux Fan Speed %",
"description": "The speed of the Chamber or Auxiliary Fan. This will be converted to PWM Duty Cycle (0-255) or (RepRap 0-1 if that is enabled in Cura). If your specified fan does not operate on variable speeds then set this to '100'.",
"type": "int",
"unit": "% ",
"default_value": 50,
"maximum_value": 100,
"minimum_value": 0,
"enabled": "enable_bv_fan and bv_fan_speed_control_enable"
},
"bv_fan_start_layer":
{
"label": " Chamber/Aux Fan Start Layer",
"description": "The layer to start the Chamber or Auxiliary Fan. Use the Cura preview layer number and the fan will start at the beginning of the layer.",
"type": "int",
"unit": "Layer# ",
"default_value": 1,
"minimum_value": 1,
"enabled": "enable_bv_fan and bv_fan_speed_control_enable"
},
"bv_fan_end_layer":
{
"label": " Chamber/Aux Fan End Layer",
"description": "The layer number for Chamber or Auxiliary Fan to turn off. Use the Cura preview layer number or '-1' to indicate the end of the print. The fan will run until the end of the layer",
"type": "int",
"unit": "Layer# ",
"default_value": -1,
"minimum_value": -1,
"enabled": "enable_bv_fan and bv_fan_speed_control_enable"
},
"enable_bv_fan":
{
"label": "Hidden setting",
"description": "This is enabled when machine_heated_bed is true, and in turn this enables 'bv_fan_speed_control_enable'.",
"type": "bool",
"default_value": false,
"enabled": false
}
}
}"""
def initialize(self) -> None:
super().initialize()
scripts = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("post_processing_scripts")
self.global_stack = Application.getInstance().getGlobalContainerStack()
self.extruder_list = self.global_stack.extruderList
self.extruder_count = self.global_stack.getProperty("machine_extruder_count", "value")
scripts = self.global_stack.getMetaDataEntry("post_processing_scripts")
if scripts != None:
script_count = scripts.count("AddCoolingProfile")
if script_count > 0:
## Set 'Remove M106 lines' to "false" if there is already an instance of this script running.
# Set 'Remove M106 lines' to "false" if there is already an instance of this script running.
self._instance.setProperty("delete_existing_m106", "value", False)
self._instance.setProperty("enable_off_fan_speed_enable", "value", False)
if self.extruder_count > 1:
if self.extruder_list[0].getProperty("machine_extruder_cooling_fan_number", "value") != self.extruder_list[1].getProperty("machine_extruder_cooling_fan_number", "value"):
self._instance.setProperty("enable_off_fan_speed_enable", "value", True)
if bool(self.global_stack.getProperty("machine_heated_bed", "value")):
self._instance.setProperty("enable_bv_fan", "value", True)
def execute(self, data):
#Initialize variables that are buried in if statements.
mycura = Application.getInstance().getGlobalContainerStack()
"""
Collect the settings from Cura and from this script
params:
t0_fan thru t3_fan: The fan numbers for up to 4 layer cooling circuits
fan_mode: Whether the fan scale will be 0-255 PWM (when true) or 0-1 RepRap (when false)
bed_adhesion: Is only important if a raft is enabled
print_seuence: Options are slightly different if in One-at-a-Time mode
is_multi-fan: Used to distinguish between a multi-extruder with a single fan for each nozzle, or one fan for both nozzles.
is_multi_extr_print: For the slight difference in handling a multi-extruder printer and a print that only uses one of the extruders.
fan_list: A list of fan speeds (even numbered items) and layer numbers (odd numbered items)
feature_speed_list: A list of the speeds for each ';TYPE:' in the gcode
feature_name_list: The list of each 'TYPE' in the gcode
off_fan_speed: The speed that will be maintained by the fan for the inactive extruder (for an anti-oozing effect)
init_fan: The fan number of the first extruder used in a print
delete_existing_m106: The first instance of the script in the post processing list should remove the CUra M106 lines. Following instances should not delete the changes made by the first instance.
feature_fan_combing: Whether or not to shut the cooling fan off during travel moves.
the_start_layer: When in By Feature this is the user selected start of the fan changes.
the_end_layer: When in By Feature this is the user selected end of the fan changes
the_end_is_enabled: When in By Feature, if the fan control ends before the print ends, then this will enable the Final Fan Speed to carry through to the print end.
"""
# Exit if the gcode has been previously post-processed.
if ";POSTPROCESSED" in data[0]:
return data
# Initialize variables that are buried in if statements.
t0_fan = " P0"; t1_fan = " P0"; t2_fan = " P0"; t3_fan = " P0"; is_multi_extr_print = True
#Get some information from Cura-----------------------------------
extruder = mycura.extruderList
#This will be true when fan scale is 0-255pwm and false when it's RepRap 0-1 (Cura 5.x)
# This will be true when fan scale is 0-255pwm and false when it's RepRap 0-1 (Cura 5.x)
fan_mode = True
##For 4.x versions that don't have the 0-1 option
# For 4.x versions that don't have the 0-1 option
try:
fan_mode = not bool(extruder[0].getProperty("machine_scale_fan_speed_zero_to_one", "value"))
except:
fan_mode = not bool(self.extruder_list[0].getProperty("machine_scale_fan_speed_zero_to_one", "value"))
except AttributeError:
pass
bed_adhesion = (extruder[0].getProperty("adhesion_type", "value"))
extruder_count = mycura.getProperty("machine_extruder_count", "value")
print_sequence = str(mycura.getProperty("print_sequence", "value"))
#Assign the fan numbers to the tools------------------------------
if extruder_count == 1:
bed_adhesion = (self.extruder_list[0].getProperty("adhesion_type", "value"))
print_sequence = str(self.global_stack.getProperty("print_sequence", "value"))
# Assign the fan numbers to the tools
if self.extruder_count == 1:
is_multi_fan = False
is_multi_extr_print = False
if int((extruder[0].getProperty("machine_extruder_cooling_fan_number", "value"))) > 0:
t0_fan = " P" + str((extruder[0].getProperty("machine_extruder_cooling_fan_number", "value")))
if int((self.extruder_list[0].getProperty("machine_extruder_cooling_fan_number", "value"))) > 0:
t0_fan = " P" + str((self.extruder_list[0].getProperty("machine_extruder_cooling_fan_number", "value")))
else:
#No P parameter if there is a single fan circuit------------------
# No P parameter if there is a single fan circuit
t0_fan = ""
#Get the cooling fan numbers for each extruder if the printer has multiple extruders
elif extruder_count > 1:
# Get the cooling fan numbers for each extruder if the printer has multiple extruders
elif self.extruder_count > 1:
is_multi_fan = True
t0_fan = " P" + str((extruder[0].getProperty("machine_extruder_cooling_fan_number", "value")))
t0_fan = " P" + str((self.extruder_list[0].getProperty("machine_extruder_cooling_fan_number", "value")))
if is_multi_fan:
if extruder_count > 1: t1_fan = " P" + str((extruder[1].getProperty("machine_extruder_cooling_fan_number", "value")))
if extruder_count > 2: t2_fan = " P" + str((extruder[2].getProperty("machine_extruder_cooling_fan_number", "value")))
if extruder_count > 3: t3_fan = " P" + str((extruder[3].getProperty("machine_extruder_cooling_fan_number", "value")))
if self.extruder_count > 1: t1_fan = " P" + str((self.extruder_list[1].getProperty("machine_extruder_cooling_fan_number", "value")))
if self.extruder_count > 2: t2_fan = " P" + str((self.extruder_list[2].getProperty("machine_extruder_cooling_fan_number", "value")))
if self.extruder_count > 3: t3_fan = " P" + str((self.extruder_list[3].getProperty("machine_extruder_cooling_fan_number", "value")))
#Initialize the fan_list with defaults----------------------------
# Initialize the fan_list with defaults
fan_list = ["z"] * 16
for num in range(0,15,2):
fan_list[num] = len(data)
fan_list[num + 1] = "M106 S0"
#Assign the variable values if "By Layer"-------------------------
# Assign the variable values if "By Layer"
by_layer_or_feature = self.getSettingValueByKey("fan_layer_or_feature")
if by_layer_or_feature == "by_layer":
## By layer doesn't do any feature search so there is no need to look for combing moves
# By layer doesn't do any feature search so there is no need to look for combing moves
feature_fan_combing = False
fan_list[0] = self.getSettingValueByKey("layer_fan_1")
fan_list[2] = self.getSettingValueByKey("layer_fan_2")
@ -343,25 +462,25 @@ class AddCoolingProfile(Script):
fan_list[10] = self.getSettingValueByKey("layer_fan_6")
fan_list[12] = self.getSettingValueByKey("layer_fan_7")
fan_list[14] = self.getSettingValueByKey("layer_fan_8")
## If there is no '/' delimiter then ignore the line else put the settings in a list
# If there is no '/' delimiter then ignore the line else put the settings in a list
for num in range(0,15,2):
if "/" in fan_list[num]:
fan_list[num + 1] = self._layer_checker(fan_list[num], "p", fan_mode)
fan_list[num] = self._layer_checker(fan_list[num], "l", fan_mode)
## Assign the variable values if "By Feature"
# Assign the variable values if "By Feature"
elif by_layer_or_feature == "by_feature":
the_start_layer = self.getSettingValueByKey("feature_fan_start_layer") - 1
the_end_layer = self.getSettingValueByKey("feature_fan_end_layer")
try:
if int(the_end_layer) != -1:
## Catch a possible input error.
# Catch a possible input error.
if the_end_layer < the_start_layer:
the_end_layer = the_start_layer
except:
the_end_layer = -1 ## If there is an input error default to the entire gcode file.
except ValueError:
the_end_layer = -1 # If there is an input error then default to the entire gcode file.
## Get the speed for each feature
# Get the speed for each feature
feature_name_list = []
feature_speed_list = []
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_skirt"), fan_mode)); feature_name_list.append(";TYPE:SKIRT")
@ -376,20 +495,29 @@ class AddCoolingProfile(Script):
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_feature_final"), fan_mode)); feature_name_list.append("FINAL_FAN")
feature_fan_combing = self.getSettingValueByKey("feature_fan_combing")
if the_end_layer > -1 and by_layer_or_feature == "by_feature":
## Required so the final speed input can be determined
# Required so the final speed input can be determined
the_end_is_enabled = True
else:
## There is no ending layer so do the whole file
# There is no ending layer so do the whole file
the_end_is_enabled = False
if the_end_layer == -1 or the_end_is_enabled == False:
the_end_layer = len(data) + 2
## Find the Layer0Index and the RaftIndex
# For multi-extruder printers with separate cooling fans the 'idle' nozzle fan can be left on for ooze control
off_fan_speed = 0
if self.extruder_count > 1:
if self.getSettingValueByKey("enable_off_fan_speed"):
if fan_mode:
off_fan_speed = round(int(self.getSettingValueByKey("off_fan_speed")) * 2.55)
else:
off_fan_speed = round(int(self.getSettingValueByKey("off_fan_speed")) * .01, 2)
# Find the Layer0Index and the RaftIndex
raft_start_index = 0
number_of_raft_layers = 0
layer_0_index = 0
## Catch the number of raft layers.
for l_num in range(1,10,1):
# Catch the number of raft layers.
for l_num in range(1,len(data) - 1):
layer = data[l_num]
if ";LAYER:-" in layer:
number_of_raft_layers += 1
@ -399,14 +527,14 @@ class AddCoolingProfile(Script):
layer_0_index = l_num
break
## Is this a single extruder print on a multi-extruder printer? - get the correct fan number for the extruder being used.
# Is this a single extruder print on a multi-extruder printer? - get the correct fan number for the extruder being used.
if is_multi_fan:
T0_used = False
T1_used = False
T2_used = False
T3_used = False
## Bypass the file header and ending gcode.
for num in range(1,len(data)-1,1):
# Bypass the file header and ending gcode.
for num in range(1,len(data)-1):
lines = data[num]
if "T0" in lines:
T0_used = True
@ -418,7 +546,7 @@ class AddCoolingProfile(Script):
T3_used = True
is_multi_extr_print = True if sum([T0_used, T1_used, T2_used, T3_used]) > 1 else False
## On a multi-extruder printer and single extruder print find out which extruder starts the file.
# On a multi-extruder printer and single extruder print find out which extruder starts the file.
init_fan = t0_fan
if not is_multi_extr_print:
startup = data[1]
@ -431,7 +559,7 @@ class AddCoolingProfile(Script):
elif line == "T3":
t0_fan = t3_fan
elif is_multi_extr_print:
## On a multi-extruder printer and multi extruder print find out which extruder starts the file.
# On a multi-extruder printer and multi extruder print find out which extruder starts the file.
startup = data[1]
lines = startup.split("\n")
for line in lines:
@ -445,23 +573,23 @@ class AddCoolingProfile(Script):
init_fan = t3_fan
else:
init_fan = ""
## Assign the variable values if "Raft Enabled"
# Assign the variable values if "Raft Enabled"
raft_enabled = self.getSettingValueByKey("fan_enable_raft")
if raft_enabled and bed_adhesion == "raft":
fan_sp_raft = self._feature_checker(self.getSettingValueByKey("fan_raft_percent"), fan_mode)
else:
fan_sp_raft = "M106 S0"
# Start to alter the data-----------------------------------------
## Strip the existing M106 lines from the file up to the end of the last layer. If a user wants to use more than one instance of this plugin then they won't want to erase the M106 lines that the preceding plugins inserted so 'delete_existing_m106' is an option.
# Start to alter the data
# Strip the existing M106 lines from the file up to the end of the last layer. If a user wants to use more than one instance of this plugin then they won't want to erase the M106 lines that the preceding plugins inserted so 'delete_existing_m106' is an option.
delete_existing_m106 = self.getSettingValueByKey("delete_existing_m106")
if delete_existing_m106:
## Start deleting from the beginning
# Start deleting from the beginning
start_from = int(raft_start_index)
else:
if by_layer_or_feature == "by_layer":
altered_start_layer = str(len(data))
## The fan list layers don't need to be in ascending order. Get the lowest.
# The fan list layers don't need to be in ascending order. Get the lowest.
for num in range(0,15,2):
try:
if int(fan_list[num]) < int(altered_start_layer):
@ -471,12 +599,12 @@ class AddCoolingProfile(Script):
elif by_layer_or_feature == "by_feature":
altered_start_layer = int(the_start_layer) - 1
start_from = int(layer_0_index) + int(altered_start_layer)
## Strip the M106 and M107 lines from the file
# Strip the M106 and M107 lines from the file
for l_index in range(int(start_from), len(data) - 1, 1):
data[l_index] = re.sub(re.compile("M106(.*)\n"), "", data[l_index])
data[l_index] = re.sub(re.compile("M107(.*)\n"), "", data[l_index])
## Deal with a raft and with One-At-A-Time print sequence
# Deal with a raft and with One-At-A-Time print sequence
if raft_enabled and bed_adhesion == "raft":
if print_sequence == "one_at_a_time":
for r_index in range(2,len(data)-2,1):
@ -486,9 +614,9 @@ class AddCoolingProfile(Script):
lines.insert(1, "M106 S0" + str(t0_fan))
if raft_enabled and bed_adhesion == "raft":
if ";LAYER:-" in data[r_index]:
## Turn the raft fan on
# Turn the raft fan on
lines.insert(1, fan_sp_raft + str(t0_fan))
## Shut the raft fan off at layer 0
# Shut the raft fan off at layer 0
if ";LAYER:0" in data[r_index]:
lines.insert(1,"M106 S0" + str(t0_fan))
data[r_index] = "\n".join(lines)
@ -496,13 +624,13 @@ class AddCoolingProfile(Script):
layer = data[raft_start_index]
lines = layer.split("\n")
if ";LAYER:-" in layer:
## Turn the raft fan on
# Turn the raft fan on
lines.insert(1, fan_sp_raft + str(init_fan))
layer = "\n".join(lines)
data[raft_start_index] = layer
layer = data[layer_0_index]
lines = layer.split("\n")
## Shut the raft fan off
# Shut the raft fan off
lines.insert(1, "M106 S0" + str(init_fan))
data[layer_0_index] = "\n".join(lines)
else:
@ -513,41 +641,44 @@ class AddCoolingProfile(Script):
lines.insert(1, "M106 S0" + str(t0_fan))
data[r_index] = "\n".join(lines)
## Turn off all fans at the end of data[1]. If more than one instance of this script is running then this will result in multiple M106 lines.
# Turn off all fans at the end of data[1]. If more than one instance of this script is running then this will result in multiple M106 lines.
temp_startup = data[1].split("\n")
temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t0_fan))
## If there are multiple cooling fans shut them all off
# If there are multiple cooling fans shut them all off
if is_multi_fan:
if extruder_count > 1 and t1_fan != t0_fan: temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t1_fan))
if extruder_count > 2 and t2_fan != t1_fan and t2_fan != t0_fan: temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t2_fan))
if extruder_count > 3 and t3_fan != t2_fan and t3_fan != t1_fan and t3_fan != t0_fan: temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t3_fan))
if self.extruder_count > 1 and t1_fan != t0_fan: temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t1_fan))
if self.extruder_count > 2 and t2_fan != t1_fan and t2_fan != t0_fan: temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t2_fan))
if self.extruder_count > 3 and t3_fan != t2_fan and t3_fan != t1_fan and t3_fan != t0_fan: temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t3_fan))
data[1] = "\n".join(temp_startup)
## If 'feature_fan_combing' is True then add additional 'MESH:NONMESH' lines for travel moves over 5 lines long
## For compatibility with 5.3.0 change any MESH:NOMESH to MESH:NONMESH.
# If 'feature_fan_combing' is True then add additional 'MESH:NONMESH' lines for travel moves over 5 lines long
# For compatibility with 5.3.0 change any MESH:NOMESH to MESH:NONMESH.
if feature_fan_combing:
for layer_num in range(2,len(data)):
layer = data[layer_num]
data[layer_num] = re.sub(";MESH:NOMESH", ";MESH:NONMESH", layer)
data = self._add_travel_comment(data, layer_0_index)
# Single Fan "By Layer"--------------------------------------------
if bool(self.getSettingValueByKey("bv_fan_speed_control_enable")):
data = self._control_bv_fan(data)
# Single Fan "By Layer"
if by_layer_or_feature == "by_layer" and not is_multi_fan:
return self._single_fan_by_layer(data, layer_0_index, fan_list, t0_fan)
# Multi-Fan "By Layer"---------------------------------------------
# Multi-Fan "By Layer"
if by_layer_or_feature == "by_layer" and is_multi_fan:
return self._multi_fan_by_layer(data, layer_0_index, fan_list, t0_fan, t1_fan, t2_fan, t3_fan)
return self._multi_fan_by_layer(data, layer_0_index, fan_list, t0_fan, t1_fan, t2_fan, t3_fan, fan_mode, off_fan_speed)
#Single Fan "By Feature"------------------------------------------
# Single Fan "By Feature"
if by_layer_or_feature == "by_feature" and (not is_multi_fan or not is_multi_extr_print):
return self._single_fan_by_feature(data, layer_0_index, the_start_layer, the_end_layer, the_end_is_enabled, fan_list, t0_fan, feature_speed_list, feature_name_list, feature_fan_combing)
#Multi Fan "By Feature"-------------------------------------------
# Multi Fan "By Feature"
if by_layer_or_feature == "by_feature" and is_multi_fan:
return self._multi_fan_by_feature(data, layer_0_index, the_start_layer, the_end_layer, the_end_is_enabled, fan_list, t0_fan, t1_fan, t2_fan, t3_fan, feature_speed_list, feature_name_list, feature_fan_combing)
return self._multi_fan_by_feature(data, layer_0_index, the_start_layer, the_end_layer, the_end_is_enabled, fan_list, t0_fan, t1_fan, t2_fan, t3_fan, feature_speed_list, feature_name_list, feature_fan_combing, fan_mode, off_fan_speed)
# The Single Fan "By Layer"----------------------------------------
# The Single Fan "By Layer"
def _single_fan_by_layer(self, data: str, layer_0_index: int, fan_list: str, t0_fan: str)->str:
layer_number = "0"
single_fan_data = data
@ -557,15 +688,15 @@ class AddCoolingProfile(Script):
for fan_line in fan_lines:
if ";LAYER:" in fan_line:
layer_number = str(fan_line.split(":")[1])
## If there is a match for the current layer number make the insertion
# If there is a match for the current layer number make the insertion
for num in range(0,15,2):
if layer_number == str(fan_list[num]):
layer = layer.replace(fan_lines[0],fan_lines[0] + "\n" + fan_list[num + 1] + str(t0_fan))
single_fan_data[l_index] = layer
return single_fan_data
# Multi-Fan "By Layer"-----------------------------------------
def _multi_fan_by_layer(self, data: str, layer_0_index: int, fan_list: str, t0_fan: str, t1_fan: str, t2_fan: str, t3_fan: str)->str:
# Multi-Fan "By Layer"
def _multi_fan_by_layer(self, data: str, layer_0_index: int, fan_list: str, t0_fan: str, t1_fan: str, t2_fan: str, t3_fan: str, fan_mode: bool, off_fan_speed: str)->str:
multi_fan_data = data
layer_number = "0"
current_fan_speed = "0"
@ -573,15 +704,16 @@ class AddCoolingProfile(Script):
this_fan = str(t0_fan)
start_index = str(len(multi_fan_data))
for num in range(0,15,2):
## The fan_list may not be in ascending order. Get the lowest layer number
# The fan_list may not be in ascending order. Get the lowest layer number
try:
if int(fan_list[num]) < int(start_index):
start_index = str(fan_list[num])
except:
except ValueError:
pass
## Move the start point if delete_existing_m106 is false
# Move the start point if delete_existing_m106 is false
start_index = int(start_index) + int(layer_0_index)
## Track the tool number
# Track the tool number
for num in range(1,int(start_index),1):
layer = multi_fan_data[num]
lines = layer.split("\n")
@ -598,18 +730,19 @@ class AddCoolingProfile(Script):
elif line == "T3":
prev_fan = this_fan
this_fan = t3_fan
# With Active Tool tracked - now the body of changes can start
for l_index in range(int(start_index),len(multi_fan_data)-1,1):
modified_data = ""
layer = multi_fan_data[l_index]
fan_lines = layer.split("\n")
for fan_line in fan_lines:
## Prepare to shut down the previous fan and start the next one.
# Prepare to turn off the previous fan and start the next one.
if fan_line.startswith("T"):
if fan_line == "T0": this_fan = str(t0_fan)
if fan_line == "T1": this_fan = str(t1_fan)
if fan_line == "T2": this_fan = str(t2_fan)
if fan_line == "T3": this_fan = str(t3_fan)
modified_data += "M106 S0" + prev_fan + "\n"
modified_data += f"M106 S{off_fan_speed}" + prev_fan + "\n"
modified_data += fan_line + "\n"
modified_data += "M106 S" + str(current_fan_speed) + this_fan + "\n"
prev_fan = this_fan
@ -620,19 +753,22 @@ class AddCoolingProfile(Script):
if layer_number == str(fan_list[num]):
modified_data += fan_list[num + 1] + this_fan + "\n"
current_fan_speed = str(fan_list[num + 1].split("S")[1])
current_fan_speed = str(current_fan_speed.split(" ")[0]) ## Just in case
current_fan_speed = str(current_fan_speed.split(" ")[0]) # Just in case
else:
modified_data += fan_line + "\n"
if modified_data.endswith("\n"): modified_data = modified_data[0:-1]
multi_fan_data[l_index] = modified_data
# Insure the fans get shut off if 'off_fan_speed' was enabled
if self.extruder_count > 1 and self.getSettingValueByKey("enable_off_fan_speed"):
multi_fan_data[-1] += "M106 S0 P1\nM106 S0 P0\n"
return multi_fan_data
# Single fan by feature-----------------------------------------------
# Single fan by feature
def _single_fan_by_feature(self, data: str, layer_0_index: int, the_start_layer: str, the_end_layer: str, the_end_is_enabled: str, fan_list: str, t0_fan: str, feature_speed_list: str, feature_name_list: str, feature_fan_combing: bool)->str:
single_fan_data = data
layer_number = "0"
index = 1
## Start with layer:0
# Start with layer:0
for l_index in range(layer_0_index,len(single_fan_data)-1,1):
modified_data = ""
layer = single_fan_data[l_index]
@ -652,15 +788,16 @@ class AddCoolingProfile(Script):
if feature_fan_combing == True:
modified_data += "M106 S0" + t0_fan + "\n"
modified_data += line + "\n"
## If an End Layer is defined and is less than the last layer then insert the Final Speed
# If an End Layer is defined and is less than the last layer then insert the Final Speed
if line == ";LAYER:" + str(the_end_layer) and the_end_is_enabled == True:
modified_data += feature_speed_list[len(feature_speed_list) - 1] + t0_fan + "\n"
if modified_data.endswith("\n"): modified_data = modified_data[0: - 1]
single_fan_data[l_index] = modified_data
return single_fan_data
# Multi-fan by feature------------------------------------------------
def _multi_fan_by_feature(self, data: str, layer_0_index: int, the_start_layer: str, the_end_layer: str, the_end_is_enabled: str, fan_list: str, t0_fan: str, t1_fan: str, t2_fan: str, t3_fan: str, feature_speed_list: str, feature_name_list: str, feature_fan_combing: bool)->str:
# Multi-fan by feature
def _multi_fan_by_feature(self, data: str, layer_0_index: int, the_start_layer: str, the_end_layer: str, the_end_is_enabled: str, fan_list: str, t0_fan: str, t1_fan: str, t2_fan: str, t3_fan: str, feature_speed_list: str, feature_name_list: str, feature_fan_combing: bool, fan_mode: bool, off_fan_speed: str)->str:
multi_fan_data = data
layer_number = "0"
start_index = 1
@ -673,7 +810,7 @@ class AddCoolingProfile(Script):
if ";LAYER:" + str(the_start_layer) + "\n" in layer:
start_index = int(my_index) - 1
break
## Track the previous tool changes
# Track the previous tool changes
for num in range(1,start_index,1):
layer = multi_fan_data[num]
lines = layer.split("\n")
@ -690,7 +827,7 @@ class AddCoolingProfile(Script):
elif line == "T3":
prev_fan = this_fan
this_fan = t3_fan
## Get the current tool.
# Get the current tool.
for l_index in range(start_index,start_index + 1,1):
layer = multi_fan_data[l_index]
lines = layer.split("\n")
@ -702,7 +839,7 @@ class AddCoolingProfile(Script):
if line == "T3": this_fan = t3_fan
prev_fan = this_fan
## Start to make insertions-------------------------------------
# Start to make insertions
for l_index in range(start_index+1,len(multi_fan_data)-1,1):
layer = multi_fan_data[l_index]
lines = layer.split("\n")
@ -712,10 +849,10 @@ class AddCoolingProfile(Script):
if line == "T1": this_fan = t1_fan
if line == "T2": this_fan = t2_fan
if line == "T3": this_fan = t3_fan
## Turn off the prev fan
modified_data += "M106 S0" + prev_fan + "\n"
# Turn off the prev fan
modified_data += f"M106 S{off_fan_speed}" + prev_fan + "\n"
modified_data += line + "\n"
## Turn on the current fan
# Turn on the current fan
modified_data += "M106 S" + str(current_fan_speed) + this_fan + "\n"
prev_fan = this_fan
if ";LAYER:" in line:
@ -725,34 +862,39 @@ class AddCoolingProfile(Script):
temp = line.split(" ")[0]
try:
name_index = feature_name_list.index(temp)
except:
except IndexError:
name_index = -1
if name_index != -1:
modified_data += line + "\n" + feature_speed_list[name_index] + this_fan + "\n"
#modified_data += feature_speed_list[name_index] + this_fan + "\n"
current_fan_speed = str(feature_speed_list[name_index].split("S")[1])
elif ";MESH:NONMESH" in line:
if feature_fan_combing == True:
modified_data += line + "\n"
modified_data += "M106 S0" + this_fan + "\n"
modified_data += f"M106 S{off_fan_speed}" + this_fan + "\n"
current_fan_speed = "0"
else:
modified_data += line + "\n"
## If an end layer is defined - Insert the final speed and set the other variables to Final Speed to finish the file
## There cannot be a break here because if there are multiple fan numbers they still need to be shut off and turned on.
# If an end layer is defined - Insert the final speed and set the other variables to Final Speed to finish the file
# There cannot be a 'break' here because if there are multiple fan numbers they still need to be shut off and turned on.
elif line == ";LAYER:" + str(the_end_layer):
modified_data += feature_speed_list[len(feature_speed_list) - 1] + this_fan + "\n"
for set_speed in range(0, len(feature_speed_list) - 2):
feature_speed_list[set_speed] = feature_speed_list[len(feature_speed_list) - 1]
else:
## Layer and Tool get inserted into modified_data above. All other lines go into modified_data here
# Layer and Tool get inserted into modified_data above. All other lines go into modified_data here
if not line.startswith("T") and not line.startswith(";LAYER:"): modified_data += line + "\n"
if modified_data.endswith("\n"): modified_data = modified_data[0: - 1]
multi_fan_data[l_index] = modified_data
modified_data = ""
# Insure the fans get shut off if 'off_fan_speed' was enabled
if self.extruder_count > 1 and self.getSettingValueByKey("enable_off_fan_speed"):
multi_fan_data[-1] += "M106 S0 P1\nM106 S0 P0\n"
return multi_fan_data
#Try to catch layer input errors, set the minimum speed to 12%, and put the strings together
# Try to catch layer input errors, set the minimum speed to 12%, and put the strings together
def _layer_checker(self, fan_string: str, ty_pe: str, fan_mode: bool) -> str:
fan_string_l = str(fan_string.split("/")[0])
try:
@ -768,7 +910,7 @@ class AddCoolingProfile(Script):
if int(fan_string_p) > 100: fan_string_p = "100"
except ValueError:
fan_string_p = "0"
## Set the minimum fan speed to 12%
# Set the minimum fan speed to 12%
if int(fan_string_p) < 12 and int(fan_string_p) != 0:
fan_string_p = "12"
fan_layer_line = str(fan_string_l)
@ -784,7 +926,7 @@ class AddCoolingProfile(Script):
#Try to catch feature input errors, set the minimum speed to 12%, and put the strings together when 'By Feature'
def _feature_checker(self, fan_feat_string: int, fan_mode: bool) -> str:
if fan_feat_string < 0: fan_feat_string = 0
## Set the minimum fan speed to 12%
# Set the minimum fan speed to 12%
if fan_feat_string > 0 and fan_feat_string < 12: fan_feat_string = 12
if fan_feat_string > 100: fan_feat_string = 100
if fan_mode:
@ -798,7 +940,7 @@ class AddCoolingProfile(Script):
for lay_num in range(int(lay_0_index), len(comment_data)-1,1):
layer = comment_data[lay_num]
lines = layer.split("\n")
## Copy the data to new_data and make the insertions there
# Copy the data to new_data and make the insertions there
new_data = lines
g0_count = 0
g0_index = -1
@ -818,12 +960,12 @@ class AddCoolingProfile(Script):
if g0_index == -1:
g0_index = lines.index(line)
elif not line.startswith("G0 ") and not is_travel:
## Add additional 'NONMESH' lines to shut the fan off during long combing moves--------
# Add additional 'NONMESH' lines to shut the fan off during long combing moves
if g0_count > 5:
if not is_travel:
new_data.insert(g0_index + insert_index, ";MESH:NONMESH")
insert_index += 1
## Add the feature_type at the end of the combing move to turn the fan back on
# Add the feature_type at the end of the combing move to turn the fan back on
new_data.insert(g0_index + g0_count + 1, feature_type)
insert_index += 1
g0_count = 0
@ -834,4 +976,35 @@ class AddCoolingProfile(Script):
g0_index = -1
is_travel = False
comment_data[lay_num] = "\n".join(new_data)
return comment_data
return comment_data
def _control_bv_fan(self, bv_data: str) -> str:
# Control any secondary fan. Can be used for an Auxilliary/Chamber fan
bv_start_layer = self.getSettingValueByKey("bv_fan_start_layer") - 1
bv_end_layer = self.getSettingValueByKey("bv_fan_end_layer")
bv_fan_nr = self.getSettingValueByKey("bv_fan_nr")
if bv_end_layer != -1:
bv_end_layer -= 1
# Get the PWM speed or if RepRap then the 0-1 speed
if self.extruder_list[0].getProperty("machine_scale_fan_speed_zero_to_one", "value"):
bv_fan_speed = round(self.getSettingValueByKey("bv_fan_speed") * .01, 1)
else:
bv_fan_speed = int(self.getSettingValueByKey("bv_fan_speed") * 2.55)
# Turn the chamber fan on
for index, layer in enumerate(bv_data):
if f";LAYER:{bv_start_layer}\n" in layer:
bv_data[index] = re.sub(f";LAYER:{bv_start_layer}", f";LAYER:{bv_start_layer}\nM106 S{bv_fan_speed} P{bv_fan_nr}",layer)
break
# Turn the chamber fan off
if bv_end_layer == -1:
bv_data[len(bv_data)-2] += f"M106 S0 P{bv_fan_nr}\n"
else:
for index, layer in enumerate(bv_data):
if f";LAYER:{bv_end_layer}\n" in layer:
lines = layer.split("\n")
for fdex, line in enumerate(lines):
if ";TIME_ELAPSED:" in line:
lines[fdex] = f"M106 S0 P{bv_fan_nr}\n" + line
bv_data[index] = "\n".join(lines)
break
return bv_data

View file

@ -92,7 +92,7 @@ class FilamentChange(Script):
"type": "float",
"default_value": 0,
"minimum_value": 0,
"enabled": "enabled"
"enabled": "enabled and not firmware_config"
},
"retract_method":
{

View file

@ -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"
},
"start_layer":
{
"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"
},
"end_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": -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 before or after layer change.",
"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": ""
},
"skip_layers":
{
"label": "Skip layers",
"description": "Number of layers to skip between insertions (0 for every layer).",
"type": "int",
"default_value": 0,
"minimum_value": 0
"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

View file

@ -338,7 +338,7 @@ class PauseAtHeight(Script):
nbr_negative_layers += 1
#Track the latest printing temperature in order to resume at the correct temperature.
if re.match("T(\d*)", line):
if re.match(r"T(\d*)", line):
current_t = self.getValue(line, "T")
m = self.getValue(line, "M")
if m is not None and (m == 104 or m == 109) and self.getValue(line, "S") is not None:

View file

@ -0,0 +1,988 @@
# August 2024 - Designed by: GregValiant (Greg Foresi). Straightened out by: Hellaholic
#
# NOTE: You may have purge lines in your startup, or you may use this script, you should not do both. The script will attempt to comment out existing StartUp purge lines.
# 'Add Purge Lines to StartUp' Allows the user to determine where the purge lines are on the build plate, or to not use purge lines if a print extends to the limits of the build surface.
# This script will attempt to recognize and comment out purge lines in the StartUp Gcode but they should be removed if using this script.
# The setting 'Purge Line Length' is only avaialble for rectangular beds because I was too lazy to calculate the 45° arcs.
# 'Move to Start' takes an orthogonal path around the periphery before moving in to the print start location. It eliminates strings across the print area.
# 'Adjust Starting E' is a correction in the E location before the skirt/brim starts. The user can make an adjustment so that the skirt / brim / raft starts where it should.
# 'Unload' adds code to the Ending Gcode that will unload the filament from the machine. The unlaod distance is broken into chunks to avoid overly long E distances.
# Added extra moves to account for Cura adding a "Travel to Prime Tower" move that can cross the middle of the build surface.
# Added ability to take 'disallowed areas' into account.
import math
from ..Script import Script
from UM.Application import Application
from UM.Message import Message
import re
from UM.Logger import Logger
from enum import Enum
class Location(str, Enum):
LEFT = "left"
RIGHT = "right"
REAR = "rear"
FRONT = "front"
class Position(tuple, Enum):
LEFT_FRONT = ("left", "front")
RIGHT_FRONT = ("right", "front")
LEFT_REAR = ("left", "rear")
RIGHT_REAR = ("right", "rear")
class PurgeLinesAndUnload(Script):
def __init__(self):
super().__init__()
self.global_stack = Application.getInstance().getGlobalContainerStack()
self.extruder = self.global_stack.extruderList
self.end_purge_location = None
self.speed_travel = None
# This will be True when there are more than 4 'machine_disallowed_areas'
self.show_warning = False
self.disallowed_areas = self.global_stack.getProperty("machine_disallowed_areas", "value")
self.extruder = self.global_stack.extruderList
self.extruder_count = self.global_stack.getProperty("machine_extruder_count", "value")
self.bed_shape = self.global_stack.getProperty("machine_shape", "value")
self.origin_at_center = self.global_stack.getProperty("machine_center_is_zero", "value")
self.machine_width = self.global_stack.getProperty("machine_width", "value")
self.machine_depth = self.global_stack.getProperty("machine_depth", "value")
self.machine_left = 1.0
self.machine_right = self.machine_width - 1.0
self.machine_front = 1.0
self.machine_back = self.machine_depth - 1.0
self.start_x = None
self.start_y = None
def initialize(self) -> None:
super().initialize()
# Get the StartUp Gcode from Cura and attempt to catch if it contains purge lines. Message the user if an extrusion is in the startup.
startup_gcode = self.global_stack.getProperty("machine_start_gcode", "value")
start_lines = startup_gcode.splitlines()
for line in start_lines:
if "G1" in line and " E" in line and (" X" in line or " Y" in line):
Message(title="[Purge Lines and Unload]",
text="It appears that there are 'purge lines' in the StartUp Gcode. Using the 'Add Purge Lines' function of this script will comment them out.").show()
break
# 'is_rectangular' is used to disable half-length purge lines for elliptic beds.
self._instance.setProperty("is_rectangular", "value", True if self.global_stack.getProperty("machine_shape", "value") == "rectangular" else False)
self._instance.setProperty("move_to_prime_tower", "value", True if self.global_stack.getProperty("machine_extruder_count", "value") > 1 else False)
# Set the default E adjustment
self._instance.setProperty("adjust_e_loc_to", "value", -abs(round(float(self.extruder[0].getProperty("retraction_amount", "value")), 1)))
def getSettingDataString(self):
return """{
"name": "Purge Lines and Unload Filament",
"key": "PurgeLinesAndUnload",
"metadata": {},
"version": 2,
"settings":
{
"add_purge_lines":
{
"label": "Add Purge Lines to StartUp",
"description": "The purge lines can be left, right, front or back. If there are purge lines present in the StartUp Gcode remove them or comment them out before using this script. You don't want to double dip.",
"type": "bool",
"default_value": false,
"value": false,
"enabled": true
},
"purge_line_location":
{
"label": " Purge Line Location",
"description": "What edge of the build plate should have the purge lines. If the printer is 'Elliptical' then it is assumed to be an 'Origin At Center' printer and the purge lines are 90° arcs.",
"type": "enum",
"options": {
"left": "On left edge (Xmin)",
"right": "On right edge (Xmax)",
"front": "On front edge (Ymin)",
"rear": "On back edge (Ymax)"},
"default_value": "left",
"enabled": "add_purge_lines"
},
"purge_line_length":
{
"label": " Purge Line Length",
"description": "Select 'Full' for the entire Height or Width of the build plate. Select 'Half' for shorter purge lines. NOTE: This has no effect on elliptic beds.",
"type": "enum",
"options": {
"purge_full": "Full",
"purge_half": "Half"},
"default_value": "purge_full",
"enabled": "add_purge_lines and is_rectangular"
},
"border_distance":
{
"label": " Border Distance",
"description": "This is the distance from the build plate edge to the first purge line. '0' works for most printers but you might want the lines further inboard. The allowable range is -12 to 12. ⚠️ Negative numbers are allowed for printers that have 'Disallowed Areas'. You must use due caution when using a negative value.",
"type": "int",
"unit": "mm ",
"default_value": 0,
"minimum_value": -12,
"maximum_value": 12,
"enabled": "add_purge_lines"
},
"prime_blob_enable":
{
"label": " Start with Prime Blob",
"description": "Enable a stationary purge before starting the purge lines. Available only when purge line location is 'left' or 'front'",
"type": "bool",
"default_value": false,
"enabled": "add_purge_lines and purge_line_location in ['front', 'left']"
},
"prime_blob_distance":
{
"label": " Blob Distance",
"description": "How many mm's of filament should be extruded for the blob.",
"type": "int",
"default_value": 0,
"unit": "mm ",
"enabled": "add_purge_lines and prime_blob_enable and purge_line_location in ['front', 'left']"
},
"prime_blob_loc_x":
{
"label": " Blob Location X",
"description": "The 'X' position to put the prime blob. 'Origin at Center' printers might require a negative value here. Keep in mind that purge lines always start in the left front, or the right rear. Pay attention or the nozzle can sit down into the prime blob.",
"type": "int",
"default_value": 0,
"unit": "mm ",
"enabled": "add_purge_lines and prime_blob_enable and purge_line_location in ['front', 'left']"
},
"prime_blob_loc_y":
{
"label": " Blob location Y",
"description": "The 'Y' position to put the prime blob. 'Origin at Center' printers might require a negative value here. Keep in mind that purge lines always start in the left front, or the right rear. Pay attention or the nozzle can sit down into the prime blob.",
"type": "int",
"default_value": 0,
"unit": "mm ",
"enabled": "add_purge_lines and prime_blob_enable and purge_line_location in ['front', 'left']"
},
"move_to_start":
{
"label": "Circle around to layer start ⚠️​",
"description": "Depending on where the 'Layer Start X' and 'Layer Start Y' are for the print, the opening travel move can pass across the print area and leave a string there. This option will generate an orthogonal path that moves the nozzle around the edges of the build plate and then comes in to the Start Point. || ⚠️​ || The nozzle can drop to Z0.0 and touch the build plate at each stop in order to 'nail down the string'. The nozzle always raises after the touch-down. It will not drag on the bed.",
"type": "bool",
"default_value": false,
"enabled": true
},
"move_to_start_min_z":
{
"label": " Minimum Z height ⚠️​",
"description": "When moving to the start position, the nozzle can touch down on the build plate at each stop (Z = 0.0). That will stick the string to the build plate at each direction change so it doesn't pull across the print area. Some printers may not respond well to Z=0.0. You may set a minimum Z height here (min is 0.0 and max is 0.50). The string must stick or it defeats the purpose of moving around the periphery.",
"type": "float",
"default_value": 0.0,
"minimum_value": 0.0,
"maximum_value": 0.5,
"enabled": "move_to_start"
},
"adjust_starting_e":
{
"label": "Adjust Starting E location",
"description": "If there is a retraction after the purge lines in the Startup Gcode (like the 'Add Purge Lines' script here does) then often the skirt does not start where the nozzle starts. It is because Cura always adds a retraction prior to the print starting which results in a double retraction. Enabling this will allow you to adjust the starting E location and tune it so the skirt/brim/model starts right where it should. To fix a blob enter a positive number. To fix a 'dry start' enter a negative number.",
"type": "bool",
"default_value": false,
"value": false,
"enabled": true
},
"adjust_e_loc_to":
{
"label": " Starting E location",
"description": "This is usually a negative amount and often equal to the '-Retraction Distance'. This 'G92 E' adjustment changes where the printer 'thinks' the end of the filament is in relation to the nozzle. It replaces the retraction that Cura adds prior to the start of 'LAYER:0'. If retraction is not enabled then this setting has no effect.",
"type": "float",
"unit": "mm ",
"default_value": -6.5,
"enabled": "adjust_starting_e"
},
"enable_unload":
{
"label": "Unload filament at print end",
"description": "Adds an unload script to the Ending Gcode section. It goes in just ahead of the M104 S0. This scripts always unloads the active extruder. If the unload distance is greater than 150mm it will be broken into chunks to avoid tripping the excessive extrusion warning in some firmware.",
"type": "bool",
"default_value": false,
"enabled": true
},
"unload_distance":
{
"label": " Unload Distance",
"description": "The amount of filament to unload. Bowden printers usually require a significant amount and direct drives not as much.",
"type": "int",
"default_value": 440,
"unit": "mm ",
"enabled": "enable_unload"
},
"unload_quick_purge":
{
"label": " Quick purge before unload",
"description": "When printing something fine that has a lot of retractions in a short space (like lettering or spires) right before the unload, the filament can get hung up in the hot end and unload can fail. A quick purge will soften the end of the filament so it will retract correctly. This 'quick purge' will take place at the last position of the nozzle.",
"type": "bool",
"default_value": false,
"enabled": "enable_unload"
},
"move_to_prime_tower":
{
"label": "Hidden setting",
"description": "Hidden setting that enables 'move_to_prime_tower' for multi extruder machines.",
"type": "bool",
"default_value": false,
"enabled": false
},
"is_rectangular":
{
"label": "Bed is rectangular",
"description": "Hidden setting that disables 'purge line length' for elliptical beds.",
"type": "bool",
"default_value": false,
"enabled": false
}
}
}"""
def execute(self, data):
# Exit if the Gcode has already been processed.
for num in range(0, len(data)):
layer = data[num].split("\n")
for line in layer:
if ";LAYER:" in line:
break
elif "PurgeLinesAndUnload" in line:
Logger.log("i", "[Add Purge Lines and Unload Filament] has already run on this gcode.")
return data
# The function also retrieves extruder settings used later in the script
# 't0_has_offsets' is used to exit 'Add Purge Lines' and 'Circle around...' because the script is not compatible with machines with the right nozzle as the primary nozzle.
self.t0_has_offsets = False
self.init_ext_nr = self._get_initial_tool()
# Adjust the usable size of the bed per any 'disallowed areas'
self._get_build_plate_extents()
# The start location changes according to which quadrant the nozzle is in at the beginning
self.end_purge_location = self._get_real_start_point(data[1])
self.border_distance = self.getSettingValueByKey("border_distance")
self.prime_blob_enable = self.getSettingValueByKey("prime_blob_enable")
if self.prime_blob_enable:
self.prime_blob_distance = self.getSettingValueByKey("prime_blob_distance")
else:
self.prime_blob_distance = 0
# Set the minimum Z to stick the string to the build plate when Move to Start is selected.
self.touchdown_z = self.getSettingValueByKey("move_to_start_min_z")
# Mapping settings to corresponding methods
procedures = {
"add_purge_lines": self._add_purge_lines,
"move_to_prime_tower": self._move_to_prime_tower,
"move_to_start": self._move_to_start,
"adjust_starting_e": self._adjust_starting_e,
"enable_unload": self._unload_filament
}
# Run selected procedures
for setting, method in procedures.items():
if self.getSettingValueByKey(setting):
method(data)
# Format the startup and ending gcodes
data[1] = self._format_string(data[1])
data[-1] = self._format_string(data[-1])
if self.getSettingValueByKey("add_purge_lines"):
if self.show_warning:
msg_text = ("The printer has ( " + str(len(self.disallowed_areas))
+ " ) 'disallowed areas'. That can cause the area available for the purge lines to be small.\nOpen the Gcode file for preview in Cura and check the purge line location to insure it is acceptable.")
else:
msg_text = "Open the Gcode file for preview in Cura. Make sure the 'Purge Lines' don't run underneath something else and are acceptable."
Message(title="[Purge Lines and Unload]", text=msg_text).show()
return data
def _get_real_start_point(self, first_section: str) -> tuple:
last_x, last_y = 0.0, 0.0
start_quadrant = Position.LEFT_FRONT
for line in first_section.split("\n"):
if line.startswith(";") and not line.startswith(";LAYER_COUNT") or not line:
continue
if line.startswith("G28"):
last_x, last_y = 0.0, 0.0
elif line[:3] in {"G0 ", "G1 "}:
last_x = self.getValue(line, "X") if " X" in line else last_x
last_y = self.getValue(line, "Y") if " Y" in line else last_y
elif "LAYER_COUNT" in line:
break
midpoint_x, midpoint_y = (0.0, 0.0) if self.origin_at_center else (
self.machine_width / 2, self.machine_depth / 2)
if last_x <= midpoint_x and last_y <= midpoint_y:
start_quadrant = Position.LEFT_FRONT
elif last_x > midpoint_x and last_y <= midpoint_y:
start_quadrant = Position.RIGHT_FRONT
elif last_x > midpoint_x and last_y > midpoint_y:
start_quadrant = Position.RIGHT_REAR
elif last_x <= midpoint_x and last_y > midpoint_y:
start_quadrant = Position.LEFT_REAR
return start_quadrant
"""
For some multi-extruder printers.
Takes into account a 'Move to Prime Tower' if there is one and adds orthogonal travel moves to get there.
'Move to Prime Tower' does not require that the prime tower is enabled,
only that 'machine_extruder_start_position_?' is in the definition file.
"""
def _move_to_prime_tower(self, first_section: str) -> str:
if self.extruder_count == 1:
return first_section
adjustment_lines = ""
move_to_prime_present = False
prime_tower_x = self.global_stack.getProperty("prime_tower_position_x", "value")
prime_tower_y = self.global_stack.getProperty("prime_tower_position_y", "value")
prime_tower_loc = self._prime_tower_quadrant(prime_tower_x, prime_tower_y)
# Shortstop an error if Start Location comes through as None
if self.end_purge_location is None:
self.end_purge_location = Position.LEFT_FRONT
if prime_tower_loc != self.end_purge_location:
startup = first_section[1].split("\n")
for index, line in enumerate(startup):
if ";LAYER_COUNT:" in line:
try:
if startup[index + 1].startswith("G0"):
prime_move = startup[index + 1] + " ; Move to Prime Tower"
adjustment_lines = self._move_to_location("Prime Tower", prime_tower_loc)
startup[index + 1] = adjustment_lines + prime_move + "\n;---------------------[End of Prime Tower moves]\n" + startup[index]
startup.pop(index)
first_section[1] = "\n".join(startup)
move_to_prime_present = True
except IndexError:
pass
# The start_location changes to the prime tower location in case 'Move to Start' is enabled.
if move_to_prime_present:
self.end_purge_location = prime_tower_loc
return first_section
# Determine the quadrant that the prime tower rests in so the orthogonal moves can be calculated
def _prime_tower_quadrant(self, prime_tower_x: float, prime_tower_y: float) -> tuple:
midpoint_x, midpoint_y = (0.0, 0.0) if self.origin_at_center else (
self.machine_width / 2, self.machine_depth / 2)
if prime_tower_x < midpoint_x and prime_tower_y < midpoint_y:
return Position.LEFT_FRONT
elif prime_tower_x > midpoint_x and prime_tower_y < midpoint_y:
return Position.RIGHT_FRONT
elif prime_tower_x > midpoint_x and prime_tower_y > midpoint_y:
return Position.RIGHT_REAR
elif prime_tower_x < midpoint_x and prime_tower_y > midpoint_y:
return Position.LEFT_REAR
else:
return Position.LEFT_FRONT # return Default in case of no match
def _move_to_location(self, location_name: str, location: tuple) -> str:
"""
Compare the input tuple (B) with the end purge location (A) and describe the move from A to B.
Parameters:
location_name (str): A descriptive name for the target location.
location (tuple): The target tuple (e.g., ("right", "front")).
Returns:
str: G-code for the move from A to B or an empty string if no move is required.
"""
# Validate input
if len(self.end_purge_location) != 2 or len(location) != 2:
raise ValueError("Both locations must be tuples of length 2.")
# Extract components
start_side, start_depth = self.end_purge_location
target_side, target_depth = location
# Start of the moves and a comment to highlight the move
moves = [f";MESH:NONMESH---------[Circle around to {location_name}] Start from: {str(start_side)} {str(start_depth)} Go to: {target_side} {target_depth}\nG0 F600 Z2 ; Move up\n"]
# Helper function to add G-code for moves
def add_move(axis: str, position: float) -> None:
moves.append(
f"G0 F{self.speed_travel} {axis}{position} ; Start move\n"
f"G0 F600 Z{self.touchdown_z} ; Nail down the string\n"
f"G0 F600 Z2 ; Move up\n"
)
# Move to a corner
if start_side == Location.LEFT:
moves.append(f"G0 F{self.speed_travel} X{self.machine_left + 6} ; Init move\n")
elif start_side == Location.RIGHT:
moves.append(f"G0 F{self.speed_travel} X{self.machine_right - 6} ; Init move\n")
if start_depth == Location.FRONT:
add_move("Y", self.machine_front + 6)
elif start_depth == Location.REAR:
add_move("Y", self.machine_back - 6)
# Compare sides
if start_side != target_side:
if target_side == Location.RIGHT:
add_move("X", self.machine_right)
else:
add_move("X", self.machine_left)
# Compare positions
if start_depth != target_depth:
if target_depth == Location.REAR:
add_move("Y", self.machine_back)
else:
add_move("Y", self.machine_front)
if len(moves) == 1:
moves.append(f"G0 F{self.speed_travel} Y{self.start_y} ; Move to start Y\n")
# Combine moves into a single G-code string and return
return "".join(moves)
def _get_build_plate_extents(self):
"""
Machine disallowed areas can be ordered at the whim of the definition author and cannot be counted on when parsed
This determines a simple rectangle that will be available for the purge lines. For some machines (Ex: UM3) it can be a small rectangle.
If there are "extruder offsets" then use them to adjust the 'machine_right' and 'machine_back' independent of any disallowed areas.
"""
if self.bed_shape == "rectangular":
if self.disallowed_areas:
if len(self.disallowed_areas) > 4:
self.show_warning = True
mid_x = 0
mid_y = 0
left_x = -(self.machine_width / 2)
right_x = (self.machine_width / 2)
front_y = (self.machine_depth / 2)
back_y = -(self.machine_depth / 2)
for rect in self.disallowed_areas:
for corner in rect:
x = corner[0]
if mid_x > x > left_x:
left_x = x
if mid_x < x < right_x:
right_x = x
y = corner[1]
if mid_y < y < front_y:
front_y = y
if mid_y > y > back_y:
back_y = y
if self.origin_at_center:
self.machine_left = round(left_x, 2)
self.machine_right = round(right_x, 2)
self.machine_front = round(front_y, 2)
self.machine_back = round(back_y, 2)
else:
self.machine_left = round(left_x + self.machine_width / 2, 2)
self.machine_right = round(right_x + self.machine_width / 2, 2)
self.machine_front = round((self.machine_depth / 2) - front_y, 2)
self.machine_back = round((self.machine_depth / 2) - back_y, 2)
else:
if self.origin_at_center:
self.machine_left = round(-(self.machine_width / 2), 2)
self.machine_right = round((self.machine_width / 2) - self.nozzle_offset_x, 2)
self.machine_front = round(-(self.machine_depth / 2) + self.nozzle_offset_y, 2)
self.machine_back = round((self.machine_depth / 2) - self.nozzle_offset_y, 2)
else:
self.machine_left = 0
self.machine_right = self.machine_width - self.nozzle_offset_x
if self.nozzle_offset_y >= 0:
self.machine_front = 0
self.machine_back = self.machine_depth - self.nozzle_offset_y
elif self.nozzle_offset_y < 0:
self.machine_front = abs(self.nozzle_offset_y)
self.machine_back = self.machine_depth
return
# Add Purge Lines to the user defined position on the build plate
def _add_purge_lines(self, data: str):
if self.t0_has_offsets:
data[0] += "; [Purge Lines and Unload] 'Add Purge Lines' did not run because the assumed primary nozzle (T0) has tool offsets.\n"
Message(title = "[Purge Lines and Unload]", text = "'Add Purge Lines' did not run because the assumed primary nozzle (T0) has tool offsets").show()
return data
def calculate_purge_volume(line_width, purge_length, volume_per_mm):
return round((line_width * 0.3 * purge_length) * 1.25 / volume_per_mm, 5)
def adjust_for_prime_blob_gcode(retract_speed, retract_distance):
"""Generates G-code lines for prime blob adjustment."""
gcode_lines = [
f"G1 F{retract_speed} E{retract_distance} ; Unretract",
"G92 E0 ; Reset extruder\n"
]
return "\n".join(gcode_lines)
purge_location = self.getSettingValueByKey("purge_line_location")
purge_extrusion_full = True if self.getSettingValueByKey("purge_line_length") == "purge_full" else False
purge_str = ";TYPE:CUSTOM----------[Purge Lines]\nG0 F600 Z2 ; Move up\nG92 E0 ; Reset extruder\n"
purge_str += self._get_blob_code()
# Normal cartesian printer with origin at the left front corner
if self.bed_shape == "rectangular" and not self.origin_at_center:
if purge_location == Location.LEFT:
purge_len = int(self.machine_back - 20) if purge_extrusion_full else int((self.machine_back - self.machine_front) / 2)
y_stop = int(self.machine_back - 10) if purge_extrusion_full else int(self.machine_depth / 2)
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
purge_str = purge_str.replace("Lines", "Lines at MinX")
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_left + self.border_distance} Y{self.machine_front + 10} ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
purge_str += f"G1 F{self.print_speed} X{self.machine_left + self.border_distance} Y{y_stop} E{purge_volume} ; First line\n"
purge_str += f"G0 X{self.machine_left + 3 + self.border_distance} Y{y_stop} ; Move over\n"
purge_str += f"G1 F{self.print_speed} X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 10} E{round(purge_volume * 2, 5)} ; Second line\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 20} Z0.3 ; Slide over and down\n"
purge_str += f"G0 X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 35} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.RIGHT:
purge_len = int(self.machine_depth - 20) if purge_extrusion_full else int((self.machine_back - self.machine_front) / 2)
y_stop = int(self.machine_front + 10) if purge_extrusion_full else int(self.machine_depth / 2)
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
purge_str = purge_str.replace("Lines", "Lines at MaxX")
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_right - self.border_distance} ; Move\nG0 Y{self.machine_back - 10} ; Move\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
purge_str += f"G1 F{self.print_speed} X{self.machine_right - self.border_distance} Y{y_stop} E{purge_volume} ; First line\n"
purge_str += f"G0 X{self.machine_right - 3 - self.border_distance} Y{y_stop} ; Move over\n"
purge_str += f"G1 F{self.print_speed} X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 10} E{purge_volume * 2} ; Second line\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 20} Z0.3 ; Slide over and down\n"
purge_str += f"G0 X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 35} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
elif purge_location == Location.FRONT:
purge_len = int(self.machine_width) - self.nozzle_offset_x - 20 if purge_extrusion_full else int(
(self.machine_right - self.machine_left) / 2)
x_stop = int(self.machine_right - 10) if purge_extrusion_full else int(self.machine_width / 2)
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
purge_str = purge_str.replace("Lines", "Lines at MinY")
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_left + 10} Y{self.machine_front + self.border_distance} ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
purge_str += f"G1 F{self.print_speed} X{x_stop} Y{self.machine_front + self.border_distance} E{purge_volume} ; First line\n"
purge_str += f"G0 X{x_stop} Y{self.machine_front + 3 + self.border_distance} ; Move over\n"
purge_str += f"G1 F{self.print_speed} X{self.machine_left + 10} Y{self.machine_front + 3 + self.border_distance} E{purge_volume * 2} ; Second line\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 20} Y{self.machine_front + 3 + self.border_distance} Z0.3 ; Slide over and down\n"
purge_str += f"G0 X{self.machine_left + 35} Y{self.machine_front + 3 + self.border_distance} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.REAR:
purge_len = int(self.machine_width - 20) if purge_extrusion_full else int(
(self.machine_right - self.machine_left) / 2)
x_stop = int(self.machine_left + 10) if purge_extrusion_full else int(self.machine_width / 2)
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
purge_str = purge_str.replace("Lines", "Lines at MaxY")
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} Y{self.machine_back - self.border_distance} ; Ortho Move to back\n"
purge_str += f"G0 X{self.machine_right - 10} ; Ortho move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
purge_str += f"G1 F{self.print_speed} X{x_stop} Y{self.machine_back - self.border_distance} E{purge_volume} ; First line\n"
purge_str += f"G0 X{x_stop} Y{self.machine_back - 3 - self.border_distance} ; Move over\n"
purge_str += f"G1 F{self.print_speed} X{self.machine_right - 10} Y{self.machine_back - 3 - self.border_distance} E{purge_volume * 2} ; Second line\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 20} Y{self.machine_back - 3 - self.border_distance} Z0.3 ; Slide over and down\n"
purge_str += f"G0 X{self.machine_right - 35} Y{self.machine_back - 3 - self.border_distance} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
# Some cartesian printers (BIBO, Weedo, MethodX, etc.) are Origin at Center
elif self.bed_shape == "rectangular" and self.origin_at_center:
if purge_location == Location.LEFT:
purge_len = int(self.machine_back - self.machine_front - 20) if purge_extrusion_full else abs(
int(self.machine_front - 10))
y_stop = int(self.machine_back - 10) if purge_extrusion_full else 0
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_left + self.border_distance} Y{self.machine_front + 10} ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
purge_str += f"G1 F{self.print_speed} X{self.machine_left + self.border_distance} Y{y_stop} E{purge_volume} ; First line\n"
purge_str += f"G0 X{self.machine_left + 3 + self.border_distance} Y{y_stop} ; Move over\n"
purge_str += f"G1 F{self.print_speed} X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 10} E{round(purge_volume * 2, 5)} ; Second line\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 20} Z0.3 ; Slide over and down\n"
purge_str += f"G0 X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 35} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.RIGHT:
purge_len = int(self.machine_back - 20) if purge_extrusion_full else int(
(self.machine_back - self.machine_front) / 2)
y_stop = int(self.machine_front + 10) if purge_extrusion_full else 0
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_right - self.border_distance} Z2 ; Move\nG0 Y{self.machine_back - 10} Z2 ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
purge_str += f"G1 F{self.print_speed} X{self.machine_right - self.border_distance} Y{y_stop} E{purge_volume} ; First line\n"
purge_str += f"G0 X{self.machine_right - 3 - self.border_distance} Y{y_stop} ; Move over\n"
purge_str += f"G1 F{self.print_speed} X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 10} E{purge_volume * 2} ; Second line\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 20} Z0.3 ; Slide over and down\n"
purge_str += f"G0 X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 35} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
elif purge_location == Location.FRONT:
purge_len = int(self.machine_right - self.machine_left - 20) if purge_extrusion_full else int(
(self.machine_right - self.machine_left) / 2)
x_stop = int(self.machine_right - 10) if purge_extrusion_full else 0
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_left + 10} Z2 ; Move\nG0 Y{self.machine_front + self.border_distance} Z2 ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
purge_str += f"G1 F{self.print_speed} X{x_stop} Y{self.machine_front + self.border_distance} E{purge_volume} ; First line\n"
purge_str += f"G0 X{x_stop} Y{self.machine_front + 3 + self.border_distance} ; Move over\n"
purge_str += f"G1 F{self.print_speed} X{self.machine_left + 10} Y{self.machine_front + 3 + self.border_distance} E{purge_volume * 2} ; Second line\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 20} Y{self.machine_front + 3 + self.border_distance} Z0.3 ; Slide over and down\n"
purge_str += f"G0 X{self.machine_left + 35} Y{self.machine_front + 3 + self.border_distance} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.REAR:
purge_len = int(self.machine_right - self.machine_left - 20) if purge_extrusion_full else abs(
int(self.machine_right - 10))
x_stop = int(self.machine_left + 10) if purge_extrusion_full else 0
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} Y{self.machine_back - self.border_distance} Z2; Ortho Move to back\n"
purge_str += f"G0 X{self.machine_right - 10} Z2 ; Ortho Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
purge_str += f"G1 F{self.print_speed} X{x_stop} Y{self.machine_back - self.border_distance} E{purge_volume} ; First line\n"
purge_str += f"G0 X{x_stop} Y{self.machine_back - 3 - self.border_distance} ; Move over\n"
purge_str += f"G1 F{self.print_speed} X{self.machine_right - 10} Y{self.machine_back - 3 - self.border_distance} E{purge_volume * 2} ; Second line\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 20} Y{self.machine_back - 3 - self.border_distance} Z0.3 ; Slide over and down\n"
purge_str += f"G0 X{self.machine_right - 35} Y{self.machine_back - 3 - self.border_distance} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
# Elliptic printers with Origin at Center
elif self.bed_shape == "elliptic":
if purge_location in [Location.LEFT, Location.RIGHT]:
radius_1 = round((self.machine_width / 2) - 1, 2)
else: # For purge_location in [Location.FRONT, Location.REAR]
radius_1 = round((self.machine_depth / 2) - 1, 2)
purge_len = int(radius_1) * math.pi / 4
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
if purge_location == Location.LEFT:
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X-{round(radius_1 * .707, 2)} Y-{round(radius_1 * .707, 2)} ; Travel\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
# Purge two arcs
purge_str += f"G2 F{self.print_speed} X-{round(radius_1 * .707, 2)} Y{round(radius_1 * .707, 2)} I{round(radius_1 * .707, 2)} J{round(radius_1 * .707, 2)} E{purge_volume} ; First Arc\n"
purge_str += f"G0 X-{round((radius_1 - 3) * .707, 2)} Y{round((radius_1 - 3) * .707, 2)} ; Move Over\n"
purge_str += f"G3 F{self.print_speed} X-{round((radius_1 - 3) * .707, 2)} Y-{round((radius_1 - 3) * .707, 2)} I{round((radius_1 - 3) * .707, 2)} J-{round((radius_1 - 3) * .707, 2)} E{purge_volume * 2} ; Second Arc\n"
purge_str += f"G1 X-{round((radius_1 - 3) * .707 - 25, 2)} E{round(purge_volume * 2 + 1, 5)} ; Move Over\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round((purge_volume * 2 + 1) - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z5 ; Move Up\nG4 S1 ; Wait 1 Second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X-{round((radius_1 - 3) * .707 - 15, 2)} Z0.3 ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} X-{round((radius_1 - 3) * .707, 2)} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.RIGHT:
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{round(radius_1 * .707, 2)} Y-{round(radius_1 * .707, 2)} ; Travel\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
# Purge two arcs
purge_str += f"G3 F{self.print_speed} X{round(radius_1 * .707, 2)} Y{round(radius_1 * .707, 2)} I-{round(radius_1 * .707, 2)} J{round(radius_1 * .707, 2)} E{purge_volume} ; First Arc\n"
purge_str += f"G0 X{round((radius_1 - 3) * .707, 2)} Y{round((radius_1 - 3) * .707, 2)} ; Move Over\n"
purge_str += f"G2 F{self.print_speed} X{round((radius_1 - 3) * .707, 2)} Y-{round((radius_1 - 3) * .707, 2)} I-{round((radius_1 - 3) * .707, 2)} J-{round((radius_1 - 3) * .707, 2)} E{purge_volume * 2} ; Second Arc\n"
purge_str += f"G1 X{round((radius_1 - 3) * .707 - 25, 2)} E{round(purge_volume * 2 + 1, 5)} ; Move Over\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round((purge_volume * 2 + 1) - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z5 ; Move Up\nG4 S1 ; Wait 1 Second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{round((radius_1 - 3) * .707 - 15, 2)} Z0.3 ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} X{round((radius_1 - 3) * .707, 2)} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
elif purge_location == Location.FRONT:
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X-{round(radius_1 * .707, 2)} Y-{round(radius_1 * .707, 2)} ; Travel\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
# Purge two arcs
purge_str += f"G3 F{self.print_speed} X{round(radius_1 * .707, 2)} Y-{round(radius_1 * .707, 2)} I{round(radius_1 * .707, 2)} J{round(radius_1 * .707, 2)} E{purge_volume} ; First Arc\n"
purge_str += f"G0 X{round((radius_1 - 3) * .707, 2)} Y-{round((radius_1 - 3) * .707, 2)} ; Move Over\n"
purge_str += f"G2 F{self.print_speed} X-{round((radius_1 - 3) * .707, 2)} Y-{round((radius_1 - 3) * .707, 2)} I-{round((radius_1 - 3) * .707, 2)} J{round((radius_1 - 3) * .707, 2)} E{purge_volume * 2} ; Second Arc\n"
purge_str += f"G1 Y-{round((radius_1 - 3) * .707 - 25, 2)} E{round(purge_volume * 2 + 1, 5)} ; Move Over\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round((purge_volume * 2 + 1) - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z5 ; Move Up\nG4 S1 ; Wait 1 Second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} Y-{round((radius_1 - 3) * .707 - 15, 2)} Z0.3 ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} Y-{round((radius_1 - 3) * .707, 2)} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.REAR:
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{round(radius_1 * .707, 2)} Y{round(radius_1 * .707, 2)} ; Travel\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
# Purge two arcs
purge_str += f"G3 F{self.print_speed} X-{round(radius_1 * .707, 2)} Y{round(radius_1 * .707, 2)} I-{round(radius_1 * .707, 2)} J-{round(radius_1 * .707, 2)} E{purge_volume} ; First Arc\n"
purge_str += f"G0 X-{round((radius_1 - 3) * .707, 2)} Y{round((radius_1 - 3) * .707, 2)} ; Move Over\n"
purge_str += f"G2 F{self.print_speed} X{round((radius_1 - 3) * .707, 2)} Y{round((radius_1 - 3) * .707, 2)} I{round((radius_1 - 3) * .707, 2)} J-{round((radius_1 - 3) * .707, 2)} E{purge_volume * 2} ; Second Arc\n"
purge_str += f"G1 Y{round((radius_1 - 3) * .707 - 25, 2)} E{round(purge_volume * 2 + 1, 5)} ; Move Over\n"
# Retract if enabled
purge_str += f"G1 F{int(self.retract_speed)} E{round((purge_volume * 2 + 1) - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z5\nG4 S1 ; Wait 1 Second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} Y{round((radius_1 - 3) * .707 - 15, 2)} Z0.3 ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} Y{round((radius_1 - 3) * .707, 2)} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
# Common ending for purge_str
purge_str += "G0 F600 Z2 ; Move Z\n;---------------------[End of Purge]"
# Comment out any existing purge lines in data
startup = data[1].split("\n")
for index, line in enumerate(startup):
if "G1" in line and " E" in line and (" X" in line or " Y" in line):
next_line = index
try:
while not startup[next_line].startswith("G92 E0"):
startup[next_line] = ";" + startup[next_line]
next_line += 1
except IndexError:
break
data[1] = "\n".join(startup)
# Find the insertion location in data
purge_str = self._format_string(purge_str)
startup_section = data[1].split("\n")
insert_index = len(startup_section) - 1
for num in range(len(startup_section) - 1, 0, -1):
# In Absolute Extrusion mode - insert above the last G92 E0 line
if "G92 E0" in startup_section[num]:
insert_index = num
break
# In Relative Extrusion mode - insert above the M83 line
elif "M83" in startup_section[num]:
insert_index = num
break
startup_section.insert(insert_index, purge_str)
data[1] = "\n".join(startup_section)
return data
# Travel moves around the bed periphery to keep strings from crossing the footprint of the model.
def _move_to_start(self, data: str) -> str:
if self.t0_has_offsets:
data[0] += "; [Purge Lines and Unload] 'Circle Around to Layer Start' did not run because the assumed primary nozzle (T0) has tool offsets.\n"
Message(title = "[Purge Lines and Unload]", text = "'Circle Around to Layer Start' did not run because the assumed primary nozzle (T0) has tool offsets.").show()
return data
self.start_x = None
self.start_y = None
move_str = None
layer = data[2].split("\n")
for line in layer:
if line.startswith("G0") and " X" in line and " Y" in line:
self.start_x = self.getValue(line, "X")
self.start_y = self.getValue(line, "Y")
break
self.start_x = self.start_x or 0
self.start_y = self.start_y or 0
if self.end_purge_location is None:
self.end_purge_location = Position.LEFT_FRONT
midpoint_x = self.machine_width / 2
midpoint_y = self.machine_depth / 2
if not self.origin_at_center:
if float(self.start_x) <= float(midpoint_x):
x_target = Location.LEFT
else:
x_target = Location.RIGHT
if float(self.start_y) <= float(midpoint_y):
y_target = Location.FRONT
else:
y_target = Location.REAR
else:
if float(self.start_x) <= 0:
x_target = Location.LEFT
else:
x_target = Location.RIGHT
if float(self.start_y) <= 0:
y_target = Location.FRONT
else:
y_target = Location.REAR
target_location = (x_target, y_target)
if self.bed_shape == "rectangular":
move_str = self._move_to_location("Layer Start", target_location)
elif self.bed_shape == "elliptic" and self.origin_at_center:
move_str = f";MESH:NONMESH---------[Travel to Layer Start]\nG0 F600 Z2 ; Move up\n"
radius = self.machine_width / 2
offset_sin = round(2 ** .5 / 2 * radius, 2)
if target_location == Position.LEFT_FRONT:
move_str += f"G0 F{self.speed_travel} X-{offset_sin} Z2 ; Move\nG0 Y-{offset_sin} Z2 ; Move to start\n"
elif target_location == Position.LEFT_REAR:
if self.end_purge_location == Position.LEFT_REAR:
move_str += f"G2 X0 Y{offset_sin} I{offset_sin} J{offset_sin} ; Move around to start\n"
else:
move_str += f"G0 F{self.speed_travel} X-{offset_sin} Z2 ; Ortho move\nG0 Y{offset_sin} Z2 ; Ortho move\n"
elif target_location == Position.RIGHT_FRONT:
move_str += f"G0 F{self.speed_travel} X{offset_sin} Z2 ; Ortho move\nG0 Y-{offset_sin} Z2 ; Ortho move\n"
elif target_location == Position.RIGHT_REAR:
move_str += f"G0 F{self.speed_travel} X{offset_sin} Z2 ; Ortho move\nG0 Y{offset_sin} Z2 ; Ortho move\n"
move_str += ";---------------------[End of layer start travels]"
# Add the move_str to the end of the StartUp section and move 'LAYER_COUNT' to the end.
startup = data[1].split("\n")
move_str = self._format_string(move_str)
if move_str.startswith("\n"):
move_str = move_str[1:]
startup.append(move_str)
# Move the 'LAYER_COUNT' line so it's at the end of data[1]
for index, line in enumerate(startup):
if "LAYER_COUNT" in line:
lay_count = startup.pop(index) + "\n"
startup.append(lay_count)
break
data[1] = "\n".join(startup)
# Remove any double-spaced lines
data[1] = data[1].replace("\n\n", "\n")
return data
# Unloading a large amount of filament in a single command can trip the 'Overlong Extrusion' warning in some firmware. Unloads longer than 150mm are split into individual 150mm segments.
def _unload_filament(self, data: str) -> str:
extrude_speed = 3000
quick_purge_speed = round(float(self.nozzle_size) * 500)
if self.material_diameter > 2: quick_purge_speed *= .38 # Adjustment for 2.85 filament
retract_amount = self.extruder[0].getProperty("retraction_amount", "value")
quick_purge_amount = retract_amount + 5 if retract_amount < 2.0 else retract_amount * 2
unload_distance = self.getSettingValueByKey("unload_distance")
quick_purge = self.getSettingValueByKey("unload_quick_purge")
lines = data[-1].split("\n")
for index, line in enumerate(lines):
# Unload the filament just before the hot end turns off.
if line.startswith("M104") and "S0" in line:
filament_str = (
"M83 ; [Unload] Relative extrusion\n"
"M400 ; Complete all moves\n"
)
if quick_purge:
filament_str += f"G1 F{quick_purge_speed} E{quick_purge_amount} ; Quick Purge before unload\n"
if unload_distance > 150:
filament_str += "".join(
f"G1 F{extrude_speed} E-150 ; Unload some\n"
for _ in range(unload_distance // 150)
)
remaining_unload = unload_distance % 150
if remaining_unload > 0:
filament_str += f"G1 F{extrude_speed} E-{remaining_unload} ; Unload the remainder\n"
else:
filament_str += f"G1 F{extrude_speed} E-{unload_distance} ; Unload\n"
filament_str += (
"M82 ; Absolute Extrusion\n"
"G92 E0 ; Reset Extruder\n"
)
lines[index] = filament_str + line
break
data[-1] = "\n".join(lines)
return data
# Make an adjustment to the starting E location so the skirt/brim/raft starts out when the nozzle starts out.
def _adjust_starting_e(self, data: str) -> str:
if not self.extruder[0].getProperty("retraction_enable", "value"):
return data
adjust_amount = self.getSettingValueByKey("adjust_e_loc_to")
lines = data[1].split("\n")
lines.reverse()
if self.global_stack.getProperty("machine_firmware_retract", "value"):
search_pattern = r"G10"
else:
search_pattern = r"G1 F(\d*) E-(\d.*)"
for index, line in enumerate(lines):
if re.search(search_pattern, line):
lines[index] = re.sub(search_pattern, f"G92 E{adjust_amount}", line)
lines.reverse()
data[1] = "\n".join(lines)
break
return data
# Format the purge or travel-to-start strings. No reason they shouldn't look nice.
def _format_string(self, any_gcode_str: str):
temp_lines = any_gcode_str.split("\n")
gap_len = 0
for temp_line in temp_lines:
if ";" in temp_line and not temp_line.startswith(";"):
if gap_len - len(temp_line.split(";")[0]) + 1 < 0:
gap_len = len(temp_line.split(";")[0]) + 1
if gap_len < 30: gap_len = 30
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(
" " * (gap_len - len(temp_line.split(";")[0]))), 1)
# This formats lines that are commented out but contain additional comments Ex: ;M420 ; leveling mesh
elif temp_line.startswith(";") and ";" in temp_line[1:]:
temp_lines[temp_index] = temp_line[1:].replace(temp_line[1:].split(";")[0],
";" + temp_line[1:].split(";")[0] + str(" " * (
gap_len - 1 - len(
temp_line[1:].split(";")[0]))), 1)
any_gcode_str = "\n".join(temp_lines)
return any_gcode_str
def _get_initial_tool(self) -> int:
# Get the Initial Extruder
num = Application.getInstance().getExtruderManager().getInitialExtruderNr()
if num is None or num == -1:
num = 0
# If there is an extruder offset X then it will be used to adjust the "machine_right" and a Y offset will adjust the "machine_back"
if self.extruder_count > 1 and bool(self.global_stack.getProperty("machine_use_extruder_offset_to_offset_coords", "value")):
self.nozzle_offset_x = self.extruder[1].getProperty("machine_nozzle_offset_x", "value")
self.nozzle_offset_y = self.extruder[1].getProperty("machine_nozzle_offset_y", "value")
else:
self.nozzle_offset_x = 0.0
self.nozzle_offset_y = 0.0
self.material_diameter = self.extruder[num].getProperty("material_diameter", "value")
self.nozzle_size = self.extruder[num].getProperty("machine_nozzle_size", "value")
self.init_line_width = self.extruder[num].getProperty("skirt_brim_line_width", "value")
self.print_speed = round(self.extruder[num].getProperty("speed_print", "value") * 60 * .75)
self.speed_travel = round(self.extruder[num].getProperty("speed_travel", "value") * 60)
self.retract_dist = self.extruder[num].getProperty("retraction_amount", "value")
self.retraction_enable = self.extruder[num].getProperty("retraction_enable", "value")
self.retract_speed = self.extruder[num].getProperty("retraction_retract_speed", "value") * 60
self.mm3_per_mm = (self.material_diameter / 2) ** 2 * math.pi
# Don't add purge lines if 'T0' has offsets.
t0_x_offset = self.extruder[0].getProperty("machine_nozzle_offset_x", "value")
t0_y_offset = self.extruder[0].getProperty("machine_nozzle_offset_y", "value")
if t0_x_offset or t0_y_offset:
self.t0_has_offsets = True
return num
def _get_blob_code(self) -> str:
if not self.prime_blob_enable or self.prime_blob_distance == 0 or self.getSettingValueByKey("purge_line_location") not in ["front", "left"]:
return ""
# Set extruder speed for 1.75 filament
speed_blob = round(float(self.nozzle_size) * 500)
# Adjust speed if 2.85 filament
if self.material_diameter > 2: speed_blob *= .4
blob_x = self.getSettingValueByKey("prime_blob_loc_x")
blob_y = self.getSettingValueByKey("prime_blob_loc_y")
blob_string = "G0 F1200 Z20 ; Move up\n"
blob_string += f"G0 F{self.speed_travel} X{blob_x} Y{blob_y} ; Move to blob location\n"
blob_string += f"G1 F{speed_blob} E{self.prime_blob_distance} ; Blob\n"
blob_string += f"G1 F{self.retract_speed} E{self.prime_blob_distance - self.retract_dist} ; Retract\n"
blob_string += "G92 E0 ; Reset extruder\n"
blob_string += "M300 P500 S600 ; Beep\n"
blob_string += "G4 S2 ; Wait\n"
return blob_string

View file

@ -1,20 +1,20 @@
# Copyright (c) 2017 Ghostkeeper
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
# Altered by GregValiant (Greg Foresi) February, 2025.
# Added option for "first instance only"
# Added option for a layer search with a Start Layer and an End layer.
# Added 'Ignore StartUp G-code' and 'Ignore Ending G-code' options
import re # To perform the search and replace.
import re
from ..Script import Script
from UM.Application import Application
class SearchAndReplace(Script):
"""Performs a search-and-replace on all g-code.
Due to technical limitations, the search can't cross the border between
layers.
"""Performs a search-and-replace on the g-code.
"""
def getSettingDataString(self):
return """{
return r"""{
"name": "Search and Replace",
"key": "SearchAndReplace",
"metadata": {},
@ -23,37 +23,145 @@ class SearchAndReplace(Script):
{
"search":
{
"label": "Search",
"description": "All occurrences of this text will get replaced by the replacement text.",
"label": "Search for:",
"description": "All occurrences of this text (within the search range) will be replaced by the 'Replace with' string. The search string is 'Case Sensitive' and 'Layer' is not the same as 'layer'.",
"type": "str",
"default_value": ""
},
"replace":
{
"label": "Replace",
"description": "The search text will get replaced by this text.",
"label": "Replace with:",
"description": "The 'Search For' text will get replaced by this text. For MultiLine insertions use the newline character '\\n' as the delimiter. If your Search term ends with a '\\n' remember to add '\\n' to the end of this Replace term.",
"type": "str",
"default_value": ""
},
"is_regex":
{
"label": "Use Regular Expressions",
"description": "When enabled, the search text will be interpreted as a regular expression.",
"description": "When disabled the search string is treated as a simple text string. When enabled, the search text will be interpreted as a Python regular expression.",
"type": "bool",
"default_value": false
},
"enable_layer_search":
{
"label": "Enable search within a Layer Range:",
"description": "When enabled, You can choose a Start and End layer for the search. When 'Layer Search' is enabled the StartUp and Ending gcodes are always ignored.",
"type": "bool",
"default_value": false,
"enabled": true
},
"search_start":
{
"label": "Start S&R at Layer:",
"description": "Use the Cura Preview layer numbering.",
"type": "int",
"default_value": 1,
"minimum_value": 1,
"enabled": "enable_layer_search"
},
"search_end":
{
"label": "Stop S&R at end of Layer:",
"description": "Use the Cura Preview layer numbering. The replacements will conclude at the end of this layer. If the End Layer is equal to the Start Layer then only that single layer is searched.",
"type": "int",
"default_value": 2,
"minimum_value": 1,
"enabled": "enable_layer_search"
},
"first_instance_only":
{
"label": "Replace first instance only:",
"description": "When enabled only the first instance is replaced.",
"type": "bool",
"default_value": false,
"enabled": true
},
"ignore_start":
{
"label": "Ignore StartUp G-code:",
"description": "When enabled the StartUp Gcode is unaffected. The StartUp Gcode is everything from ';generated with Cura...' to ';LAYER_COUNT:' inclusive.",
"type": "bool",
"default_value": true,
"enabled": "not enable_layer_search"
},
"ignore_end":
{
"label": "Ignore Ending G-code:",
"description": "When enabled the Ending Gcode is unaffected.",
"type": "bool",
"default_value": true,
"enabled": "not enable_layer_search"
}
}
}"""
def execute(self, data):
global_stack = Application.getInstance().getGlobalContainerStack()
extruder = global_stack.extruderList
retract_enabled = bool(extruder[0].getProperty("retraction_enable", "value"))
search_string = self.getSettingValueByKey("search")
if not self.getSettingValueByKey("is_regex"):
search_string = re.escape(search_string) #Need to search for the actual string, not as a regex.
search_regex = re.compile(search_string)
replace_string = self.getSettingValueByKey("replace")
is_regex = self.getSettingValueByKey("is_regex")
enable_layer_search = self.getSettingValueByKey("enable_layer_search")
start_layer = self.getSettingValueByKey("search_start")
end_layer = self.getSettingValueByKey("search_end")
ignore_start = self.getSettingValueByKey("ignore_start")
ignore_end = self.getSettingValueByKey("ignore_end")
if enable_layer_search:
ignore_start = True
ignore_end = True
first_instance_only = bool(self.getSettingValueByKey("first_instance_only"))
for layer_number, layer in enumerate(data):
data[layer_number] = re.sub(search_regex, replace_string, layer) #Replace all.
# Account for missing layer numbers when a raft is used
start_index = 1
end_index = len(data) - 1
data_list = [0,1]
layer_list = [-1,0]
lay_num = 1
for index, layer in enumerate(data):
if re.search(r";LAYER:(-?\d+)", layer):
data_list.append(index)
layer_list.append(lay_num)
lay_num += 1
return data
# Get the start and end indexes within the data
if not enable_layer_search:
if ignore_start:
start_index = 2
else:
start_index = 1
if ignore_end:
end_index = data_list[len(data_list) - 1]
else:
# Account for the extra data item when retraction is enabled
end_index = data_list[len(data_list) - 1] + (2 if retract_enabled else 1)
elif enable_layer_search:
for index, num in enumerate(layer_list):
if num == start_layer:
start_index = data_list[index]
if num == end_layer:
end_index = data_list[index]
# Make replacements
replace_one = False
if not is_regex:
search_string = re.escape(search_string)
search_regex = re.compile(search_string)
for num in range(start_index, end_index + 1, 1):
layer = data[num]
# First_instance only
if first_instance_only:
if re.search(search_regex, layer) and replace_one == False:
data[num] = re.sub(search_regex, replace_string, data[num], 1)
replace_one = True
break
# All instances
else:
if end_index > start_index:
data[num] = re.sub(search_regex, replace_string, layer)
elif end_index == start_index:
layer = data[start_index]
data[start_index] = re.sub(search_regex, replace_string, layer)
return data

View file

@ -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