def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool: """Check if a setting has an inheritance function that is overwritten""" has_setting_function = False if not stack: stack = self._active_container_stack if not stack: # No active container stack yet! return False if self._active_container_stack is None: return False all_keys = self._active_container_stack.getAllKeys() containers = [] # type: List[ContainerInterface] has_user_state = stack.getProperty(key, "state") == InstanceState.User """Check if the setting has a user state. If not, it is never overwritten.""" if not has_user_state: return False # If a setting is not enabled, don't label it as overwritten (It's never visible anyway). if not stack.getProperty(key, "enabled"): return False user_container = stack.getTop() """Also check if the top container is not a setting function (this happens if the inheritance is restored).""" if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction): return False ## Mash all containers for all the stacks together. while stack: containers.extend(stack.getContainers()) stack = stack.getNextStack() has_non_function_value = False for container in containers: try: value = container.getProperty(key, "value") except AttributeError: continue if value is not None: # If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value has_setting_function = isinstance(value, SettingFunction) if has_setting_function: for setting_key in value.getUsedSettingKeys(): if setting_key in all_keys: break # We found an actual setting. So has_setting_function can remain true else: # All of the setting_keys turned out to not be setting keys at all! # This can happen due enum keys also being marked as settings. has_setting_function = False if has_setting_function is False: has_non_function_value = True continue if has_setting_function: break # There is a setting function somewhere, stop looking deeper. return has_setting_function and has_non_function_value
def _buildExtruderMessage(self, stack: ContainerStack) -> None: """Create extruder message from stack""" message = self._slice_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) if not self._all_extruders_settings: self._cacheAllExtruderSettings() if self._all_extruders_settings is None: return extruder_nr = stack.getProperty("extruder_nr", "value") settings = self._all_extruders_settings[str(extruder_nr)].copy() # Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it. settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "") # Replace the setting tokens in start and end g-code. extruder_nr = stack.getProperty("extruder_nr", "value") settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr) settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr) global_definition = cast(ContainerInterface, cast(ContainerStack, stack.getNextStack()).getBottom()) own_definition = cast(ContainerInterface, stack.getBottom()) for key, value in settings.items(): # Do not send settings that are not settable_per_extruder. # Since these can only be set in definition files, we only have to ask there. if not global_definition.getProperty(key, "settable_per_extruder") and \ not own_definition.getProperty(key, "settable_per_extruder"): continue setting = message.getMessage("settings").addRepeatedMessage("settings") setting.name = key setting.value = str(value).encode("utf-8") Job.yieldThread()
def _buildExtruderMessage(self, stack: ContainerStack) -> None: message = self._arcus_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) settings = self._buildReplacementTokens(stack) # Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it. settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "") # Replace the setting tokens in start and end g-code. extruder_nr = stack.getProperty("extruder_nr", "value") settings["machine_extruder_start_code"] = self._expandGcodeTokens( settings["machine_extruder_start_code"], extruder_nr) settings["machine_extruder_end_code"] = self._expandGcodeTokens( settings["machine_extruder_end_code"], extruder_nr) settings["machine_fiber_cut_code"] = self._expandGcodeTokens( settings["machine_fiber_cut_code"], extruder_nr) settings["machine_fiber_prime_code"] = self._expandGcodeTokens( settings["machine_fiber_prime_code"], extruder_nr) for key, value in settings.items(): # Do not send settings that are not settable_per_extruder. if not stack.getProperty(key, "settable_per_extruder"): continue setting = message.getMessage("settings").addRepeatedMessage( "settings") setting.name = key setting.value = str(value).encode("utf-8") Job.yieldThread()
def _buildExtruderMessage(self, stack: ContainerStack) -> None: message = self._slice_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) if not self._all_extruders_settings: self._cacheAllExtruderSettings() if self._all_extruders_settings is None: return extruder_nr = stack.getProperty("extruder_nr", "value") settings = self._all_extruders_settings[str(extruder_nr)].copy() # Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it. settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "") # Replace the setting tokens in start and end g-code. extruder_nr = stack.getProperty("extruder_nr", "value") settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr) settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr) for key, value in settings.items(): # Do not send settings that are not settable_per_extruder. if not stack.getProperty(key, "settable_per_extruder"): continue setting = message.getMessage("settings").addRepeatedMessage("settings") setting.name = key setting.value = str(value).encode("utf-8") Job.yieldThread()
def _checkStackForErrors(self, stack: ContainerStack) -> bool: """Check if a stack has any errors.""" """returns true if it has errors, false otherwise.""" top_of_stack = cast(InstanceContainer, stack.getTop()) # Cache for efficiency. changed_setting_keys = top_of_stack.getAllKeys() # Add all relations to changed settings as well. for key in top_of_stack.getAllKeys(): instance = top_of_stack.getInstance(key) if instance is None: continue self._addRelations(changed_setting_keys, instance.definition.relations) Job.yieldThread() for changed_setting_key in changed_setting_keys: if not stack.getProperty(changed_setting_key, "enabled"): continue validation_state = stack.getProperty(changed_setting_key, "validationState") if validation_state is None: definition = cast(SettingDefinition, stack.getSettingDefinition(changed_setting_key)) validator_type = SettingDefinition.getValidatorForType(definition.type) if validator_type: validator = validator_type(changed_setting_key) validation_state = validator(stack) if validation_state in ( ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid): Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", changed_setting_key, validation_state) return True Job.yieldThread() return False
def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool: has_setting_function = False if not stack: stack = self._active_container_stack if not stack: #No active container stack yet! return False containers = [] ## Check if the setting has a user state. If not, it is never overwritten. has_user_state = stack.getProperty(key, "state") == InstanceState.User if not has_user_state: return False ## If a setting is not enabled, don't label it as overwritten (It's never visible anyway). if not stack.getProperty(key, "enabled"): return False ## Also check if the top container is not a setting function (this happens if the inheritance is restored). if isinstance(stack.getTop().getProperty(key, "value"), SettingFunction): return False ## Mash all containers for all the stacks together. while stack: containers.extend(stack.getContainers()) stack = stack.getNextStack() has_non_function_value = False for container in containers: try: value = container.getProperty(key, "value") except AttributeError: continue if value is not None: # If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value has_setting_function = isinstance(value, SettingFunction) if has_setting_function: for setting_key in value.getUsedSettingKeys(): if setting_key in self._active_container_stack.getAllKeys(): break # We found an actual setting. So has_setting_function can remain true else: # All of the setting_keys turned out to not be setting keys at all! # This can happen due enum keys also being marked as settings. has_setting_function = False if has_setting_function is False: has_non_function_value = True continue if has_setting_function: break # There is a setting function somewhere, stop looking deeper. return has_setting_function and has_non_function_value
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() result["print_bed_temperature"] = result[ "material_bed_temperature"] # Renamed settings. result["print_bed_temperature_layer_0"] = result[ "material_bed_temperature_layer_0"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["print_chamber_temperature"] = result[ "default_material_chamber_temperature"] result["travel_speed"] = result["speed_travel"] result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] if (result["support_solubile"] == True): print("SUPPORT TRUE!!!!") result["support_bottom_distance"] = "0.0" result["support_top_distance"] = "0.0" initial_extruder_stack = CuraApplication.getInstance( ).getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty( "extruder_nr", "value") result["initial_extruder_nr"] = initial_extruder_nr return result
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: """Creates a dictionary of tokens to replace in g-code pieces. This indicates what should be replaced in the start and end g-codes. :param stack: The stack to get the settings from to replace the tokens with. :return: A dictionary of replacement tokens to the values they should be replaced with. """ result = {} for key in stack.getAllKeys(): result[key] = stack.getProperty(key, "value") Job.yieldThread() # Material identification in addition to non-human-readable GUID result["material_id"] = stack.material.getMetaDataEntry( "base_file", "") result["material_type"] = stack.material.getMetaDataEntry( "material", "") result["material_name"] = stack.material.getMetaDataEntry("name", "") result["material_brand"] = stack.material.getMetaDataEntry("brand", "") # Renamed settings. result["print_bed_temperature"] = result["material_bed_temperature"] result["print_temperature"] = result["material_print_temperature"] result["travel_speed"] = result["speed_travel"] #Some extra settings. result["time"] = time.strftime("%H:%M:%S") result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] result["initial_extruder_nr"] = CuraApplication.getInstance( ).getExtruderManager().getInitialExtruderNr() return result
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() result["print_bed_temperature"] = result[ "material_bed_temperature"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["time"] = time.strftime("%H:%M:%S") # Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] printing_mode = result["printing_mode"] if printing_mode in ["cylindrical_full", "cylindrical"]: result["cylindrical_rotate"] = "G0 A%.2f" % ( 90 * result["machine_a_axis_multiplier"] / result["machine_a_axis_divider"]) result["coordinate_system"] = "G56" elif printing_mode in ["spherical_full", "spherical"]: result["cylindrical_rotate"] = "G0 A0" result["coordinate_system"] = "G55" initial_extruder_stack = SteSlicerApplication.getInstance( ).getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty( "extruder_nr", "value") result["initial_extruder_nr"] = initial_extruder_nr return result
def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None: for key in stack.getAllKeys(): extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder")))) if extruder_position >= 0: # Set to a specific extruder. setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder") setting_extruder.name = key setting_extruder.extruder = extruder_position Job.yieldThread()
def _checkStackForErrors(self, stack: ContainerStack) -> bool: if stack is None: return False for key in stack.getAllKeys(): validation_state = stack.getProperty(key, "validationState") if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid): Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state) return True Job.yieldThread() return False
def _checkStackForErrors(self, stack: ContainerStack) -> bool: if stack is None: return False for key in stack.getAllKeys(): validation_state = stack.getProperty(key, "validationState") if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError): Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state) return True Job.yieldThread() return False
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["travel_speed"] = result["speed_travel"] result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] result["initial_extruder_nr"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr() return result
def _checkStackForErrors(self, stack: ContainerStack) -> bool: if stack is None: return False # if there are no per-object settings we don't need to check the other settings here stack_top = stack.getTop() if stack_top is None or not stack_top.getAllKeys(): return False for key in stack.getAllKeys(): validation_state = stack.getProperty(key, "validationState") if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid): Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state) return True Job.yieldThread() return False
def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None: """Sends for some settings which extruder they should fallback to if not set. This is only set for settings that have the limit_to_extruder property. :param stack: The global stack with all settings, from which to read the limit_to_extruder property. """ for key in stack.getAllKeys(): extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder")))) if extruder_position >= 0: # Set to a specific extruder. setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder") setting_extruder.name = key setting_extruder.extruder = extruder_position Job.yieldThread()
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["travel_speed"] = result["speed_travel"] result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value") result["initial_extruder_nr"] = initial_extruder_nr return result
class Script: def __init__(self) -> None: super().__init__() self._stack = None # type: Optional[ContainerStack] self._definition = None # type: Optional[DefinitionContainerInterface] self._instance = None # type: Optional[InstanceContainer] def initialize(self) -> None: setting_data = self.getSettingData() self._stack = ContainerStack(stack_id=str(id(self))) self._stack.setDirty(False) # This stack does not need to be saved. # Check if the definition of this script already exists. If not, add it to the registry. if "key" in setting_data: definitions = ContainerRegistry.getInstance( ).findDefinitionContainers(id=setting_data["key"]) if definitions: # Definition was found self._definition = definitions[0] else: self._definition = DefinitionContainer(setting_data["key"]) try: self._definition.deserialize(json.dumps(setting_data)) ContainerRegistry.getInstance().addContainer( self._definition) except ContainerFormatError: self._definition = None return if self._definition is None: return self._stack.addContainer(self._definition) self._instance = InstanceContainer( container_id="ScriptInstanceContainer") self._instance.setDefinition(self._definition.getId()) self._instance.setMetaDataEntry( "setting_version", self._definition.getMetaDataEntry("setting_version", default=0)) self._stack.addContainer(self._instance) self._stack.propertyChanged.connect(self._onPropertyChanged) ContainerRegistry.getInstance().addContainer(self._stack) settingsLoaded = Signal() valueChanged = Signal( ) # Signal emitted whenever a value of a setting is changed def _onPropertyChanged(self, key: str, property_name: str) -> None: if property_name == "value": self.valueChanged.emit() # Property changed: trigger reslice # To do this we use the global container stack propertyChanged. # Re-slicing is necessary for setting changes in this plugin, because the changes # are applied only once per "fresh" gcode global_container_stack = Application.getInstance( ).getGlobalContainerStack() if global_container_stack is not None: global_container_stack.propertyChanged.emit(key, property_name) # Needs to return a dict that can be used to construct a settingcategory file. # See the example script for an example. # It follows the same style / guides as the Uranium settings. # Scripts can either override getSettingData directly, or use getSettingDataString # to return a string that will be parsed as json. The latter has the benefit over # returning a dict in that the order of settings is maintained. def getSettingData(self) -> Dict[str, Any]: setting_data_as_string = self.getSettingDataString() setting_data = json.loads(setting_data_as_string, object_pairs_hook=collections.OrderedDict) return setting_data def getSettingDataString(self) -> str: raise NotImplementedError() def getDefinitionId(self) -> Optional[str]: if self._stack: bottom = self._stack.getBottom() if bottom is not None: return bottom.getId() return None def getStackId(self) -> Optional[str]: if self._stack: return self._stack.getId() return None # Convenience function that retrieves value of a setting from the stack. def getSettingValueByKey(self, key: str) -> Any: if self._stack is not None: return self._stack.getProperty(key, "value") return None # Convenience function that finds the value in a line of g-code. # When requesting key = x from line "G1 X100" the value 100 is returned. def getValue(self, line: str, key: str, default=None) -> Any: if key not 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) if m is None: return default try: return int(m.group(0)) except ValueError: # Not an integer. try: return float(m.group(0)) except ValueError: # Not a number at all. return default # Convenience function to produce a line of g-code. # # You can put in an original g-code line and it will re-use all the values in that line. # All other keyword parameters are put in the result in g-code's format. # For instance, if you put "G=1" in the parameters, it will output "G1". # If you put "G=1", X=100" in the parameters, it will output "G1 X100". # The parameters will be put in this order "G", "M", "T", "S", "F", "X", "Y", "Z", "E" # followed by any other parameters # \param line The original g-code line that must be modified. If not provided, an entirely new g-code line will be produced. # \return A line of g-code with the desired parameters filled in. def putValue(self, line: str = "", **kwargs) -> str: # Strip the comment. comment = "" if ";" in line: comment = line[line.find(";"):] line = line[:line.find(";")] # Strip the comment. # Parse the original g-code line and add them to kwargs. for part in line.split(" "): if part == "": continue parameter = part[0] if parameter not in kwargs: value = part[1:] kwargs[parameter] = value # Start writing the new g-code line. line_parts = list() # First add these parameters in order for parameter in ["G", "M", "T", "S", "F", "X", "Y", "Z", "E"]: if parameter in kwargs: line_parts.append(parameter + str(kwargs.pop(parameter))) # Then add the rest of the parameters for parameter, value in kwargs.items(): line_parts.append(parameter + str(value)) # If there was a comment, put it back in. if comment != "": line_parts.append(comment) # Construct the new line return " ".join(line_parts) # This is called when the script is executed. # It gets a list of g-code strings and needs to return a (modified) list. def execute(self, data: List[str]) -> List[str]: raise NotImplementedError()
class Script: def __init__(self) -> None: super().__init__() self._stack = None # type: Optional[ContainerStack] self._definition = None # type: Optional[DefinitionContainerInterface] self._instance = None # type: Optional[InstanceContainer] def initialize(self) -> None: setting_data = self.getSettingData() self._stack = ContainerStack(stack_id=str(id(self))) self._stack.setDirty(False) # This stack does not need to be saved. ## Check if the definition of this script already exists. If not, add it to the registry. if "key" in setting_data: definitions = ContainerRegistry.getInstance().findDefinitionContainers(id=setting_data["key"]) if definitions: # Definition was found self._definition = definitions[0] else: self._definition = DefinitionContainer(setting_data["key"]) try: self._definition.deserialize(json.dumps(setting_data)) ContainerRegistry.getInstance().addContainer(self._definition) except ContainerFormatError: self._definition = None return if self._definition is None: return self._stack.addContainer(self._definition) self._instance = InstanceContainer(container_id="ScriptInstanceContainer") self._instance.setDefinition(self._definition.getId()) self._instance.setMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default=0)) self._stack.addContainer(self._instance) self._stack.propertyChanged.connect(self._onPropertyChanged) ContainerRegistry.getInstance().addContainer(self._stack) settingsLoaded = Signal() valueChanged = Signal() # Signal emitted whenever a value of a setting is changed def _onPropertyChanged(self, key: str, property_name: str) -> None: if property_name == "value": self.valueChanged.emit() # Property changed: trigger reslice # To do this we use the global container stack propertyChanged. # Re-slicing is necessary for setting changes in this plugin, because the changes # are applied only once per "fresh" gcode global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is not None: global_container_stack.propertyChanged.emit(key, property_name) ## Needs to return a dict that can be used to construct a settingcategory file. # See the example script for an example. # It follows the same style / guides as the Uranium settings. # Scripts can either override getSettingData directly, or use getSettingDataString # to return a string that will be parsed as json. The latter has the benefit over # returning a dict in that the order of settings is maintained. def getSettingData(self) -> Dict[str, Any]: setting_data_as_string = self.getSettingDataString() setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict) return setting_data def getSettingDataString(self) -> str: raise NotImplementedError() def getDefinitionId(self) -> Optional[str]: if self._stack: bottom = self._stack.getBottom() if bottom is not None: return bottom.getId() return None def getStackId(self) -> Optional[str]: if self._stack: return self._stack.getId() return None ## Convenience function that retrieves value of a setting from the stack. def getSettingValueByKey(self, key: str) -> Any: if self._stack is not None: return self._stack.getProperty(key, "value") return None ## Convenience function that finds the value in a line of g-code. # When requesting key = x from line "G1 X100" the value 100 is returned. def getValue(self, line: str, key: str, default = None) -> Any: 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) if m is None: return default try: return int(m.group(0)) except ValueError: #Not an integer. try: return float(m.group(0)) except ValueError: #Not a number at all. return default ## Convenience function to produce a line of g-code. # # You can put in an original g-code line and it'll re-use all the values # in that line. # All other keyword parameters are put in the result in g-code's format. # For instance, if you put ``G=1`` in the parameters, it will output # ``G1``. If you put ``G=1, X=100`` in the parameters, it will output # ``G1 X100``. The parameters G and M will always be put first. The # parameters T and S will be put second (or first if there is no G or M). # The rest of the parameters will be put in arbitrary order. # \param line The original g-code line that must be modified. If not # provided, an entirely new g-code line will be produced. # \return A line of g-code with the desired parameters filled in. def putValue(self, line: str = "", **kwargs) -> str: #Strip the comment. comment = "" if ";" in line: comment = line[line.find(";"):] line = line[:line.find(";")] #Strip the comment. #Parse the original g-code line. for part in line.split(" "): if part == "": continue parameter = part[0] if parameter in kwargs: continue #Skip this one. The user-provided parameter overwrites the one in the line. value = part[1:] kwargs[parameter] = value #Write the new g-code line. result = "" priority_parameters = ["G", "M", "T", "S", "F", "X", "Y", "Z", "E"] #First some parameters that get priority. In order of priority! for priority_key in priority_parameters: if priority_key in kwargs: if result != "": result += " " result += priority_key + str(kwargs[priority_key]) del kwargs[priority_key] for key, value in kwargs.items(): if result != "": result += " " result += key + str(value) #Put the comment back in. if comment != "": if result != "": result += " " result += ";" + comment return result ## This is called when the script is executed. # It gets a list of g-code strings and needs to return a (modified) list. def execute(self, data: List[str]) -> List[str]: raise NotImplementedError()
class Script: def __init__(self): super().__init__() self._settings = None self._stack = None setting_data = self.getSettingData() self._stack = ContainerStack(stack_id=str(id(self))) self._stack.setDirty(False) # This stack does not need to be saved. ## Check if the definition of this script already exists. If not, add it to the registry. if "key" in setting_data: definitions = ContainerRegistry.getInstance( ).findDefinitionContainers(id=setting_data["key"]) if definitions: # Definition was found self._definition = definitions[0] else: self._definition = DefinitionContainer(setting_data["key"]) try: self._definition.deserialize(json.dumps(setting_data)) ContainerRegistry.getInstance().addContainer( self._definition) except ContainerFormatError: self._definition = None return self._stack.addContainer(self._definition) self._instance = InstanceContainer( container_id="ScriptInstanceContainer") self._instance.setDefinition(self._definition.getId()) self._instance.addMetaDataEntry( "setting_version", self._definition.getMetaDataEntry("setting_version", default=0)) self._stack.addContainer(self._instance) self._stack.propertyChanged.connect(self._onPropertyChanged) ContainerRegistry.getInstance().addContainer(self._stack) settingsLoaded = Signal() valueChanged = Signal( ) # Signal emitted whenever a value of a setting is changed def _onPropertyChanged(self, key, property_name): if property_name == "value": self.valueChanged.emit() # Property changed: trigger reslice # To do this we use the global container stack propertyChanged. # Reslicing is necessary for setting changes in this plugin, because the changes # are applied only once per "fresh" gcode global_container_stack = Application.getInstance( ).getGlobalContainerStack() global_container_stack.propertyChanged.emit(key, property_name) ## Needs to return a dict that can be used to construct a settingcategory file. # See the example script for an example. # It follows the same style / guides as the Uranium settings. # Scripts can either override getSettingData directly, or use getSettingDataString # to return a string that will be parsed as json. The latter has the benefit over # returning a dict in that the order of settings is maintained. def getSettingData(self): setting_data = self.getSettingDataString() if type(setting_data) == str: setting_data = json.loads( setting_data, object_pairs_hook=collections.OrderedDict) return setting_data def getSettingDataString(self): raise NotImplementedError() def getDefinitionId(self): if self._stack: return self._stack.getBottom().getId() def getStackId(self): if self._stack: return self._stack.getId() ## Convenience function that retrieves value of a setting from the stack. def getSettingValueByKey(self, key): return self._stack.getProperty(key, "value") ## Convenience function that finds the value in a line of g-code. # When requesting key = x from line "G1 X100" the value 100 is returned. def getValue(self, line, key, default=None): 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) if m is None: return default try: return float(m.group(0)) except: return default ## Convenience function to produce a line of g-code. # # You can put in an original g-code line and it'll re-use all the values # in that line. # All other keyword parameters are put in the result in g-code's format. # For instance, if you put ``G=1`` in the parameters, it will output # ``G1``. If you put ``G=1, X=100`` in the parameters, it will output # ``G1 X100``. The parameters G and M will always be put first. The # parameters T and S will be put second (or first if there is no G or M). # The rest of the parameters will be put in arbitrary order. # \param line The original g-code line that must be modified. If not # provided, an entirely new g-code line will be produced. # \return A line of g-code with the desired parameters filled in. def putValue(self, line="", **kwargs): #Strip the comment. comment = "" if ";" in line: comment = line[line.find(";"):] line = line[:line.find(";")] #Strip the comment. #Parse the original g-code line. for part in line.split(" "): if part == "": continue parameter = part[0] if parameter in kwargs: continue #Skip this one. The user-provided parameter overwrites the one in the line. value = part[1:] kwargs[parameter] = value #Write the new g-code line. result = "" priority_parameters = [ "G", "M", "T", "S", "F", "X", "Y", "Z", "E" ] #First some parameters that get priority. In order of priority! for priority_key in priority_parameters: if priority_key in kwargs: if result != "": result += " " result += priority_key + str(kwargs[priority_key]) del kwargs[priority_key] for key, value in kwargs.items(): if result != "": result += " " result += key + str(value) #Put the comment back in. if comment != "": if result != "": result += " " result += ";" + comment return result ## This is called when the script is executed. # It gets a list of g-code strings and needs to return a (modified) list. def execute(self, data): raise NotImplementedError()
class CuraSettingsGuide(Extension, QObject): """ The main manager and entry point for the Cura Settings Guide. This adds a menu item to the extensions menu and to the context menu of every article. It creates a dialogue window to display the guide in, and makes the menu items open said dialogue window. This class is also exposed to the QML code, which can use it to request the articles and metadata about the article. """ def __init__(self, parent=None) -> None: """ Executed during registration of the plug-in. This adds a menu item to the extensions menu and the context menu of the settings. :param parent: The parent QObject this is located in. Unused by this particular object. """ QObject.__init__(self, parent) Extension.__init__(self) self.setMenuName("Ѕettings Guide") # Using Cyrillic Ѕ instead of normal S to prevent MacOS detecting the word "setting" and pulling this menu item out of context. self.addMenuItem("Ѕettings Guide", self.startWelcomeGuide) self._dialog = None # Cached instance of the dialogue window. self._container_stack = None # Stack that provides not only the normal settings but also the extra articles added by this guide. self._markdown_per_folder = {} # For each directory containing Markdown files, create one renderer that correctly dereferences images relatively. self.articles = {} # type: Dict[str, Dict[str, List[List[str]]]] # All of the articles by key and language. Key: article ID, value: Dict with language as key and lists of items in each article as value. self.load_definitions() self.article_locations = self.find_articles() self._selected_article_id = "" # Which article is currently shown for the user. Empty string indicates it's the welcome screen. # Add context menu item to the settings list to open the guide for that setting. application = CuraApplication.getInstance() self._selected_language = application.getPreferences().getValue("general/language") application.getCuraAPI().interface.settings.addContextMenuItem({ "name": "Settings Guide", "icon_name": "help-contents", "actions": ["sidebarMenuItemOnClickHandler"], "menu_item": MenuItemHandler.MenuItemHandler(self) }) application.getPreferences().addPreference("settings_guide/show+articles+in+setting+tooltips+%28requires+restart%29", False) self.adjust_theme() application.initializationFinished.connect(self.load_all_in_background) def adjust_theme(self): """ Makes the tooltips wider, if displaying articles in the tooltips. """ application = CuraApplication.getInstance() preferences = application.getPreferences() if preferences.getValue("settings_guide/show+articles+in+setting+tooltips+%28requires+restart%29"): preferences.addPreference("general/theme", application.default_theme) theme_name = preferences.getValue("general/theme") if theme_name.endswith("_settingsguideadjust"): return # Already adjusted. theme_path = Resources.getPath(Resources.Themes, theme_name) # Create a new theme where we can adjust the tooltip. new_theme_name = theme_name + "_settingsguideadjust" new_theme_path = Resources.getStoragePath(Resources.Themes, new_theme_name) try: shutil.copytree(theme_path, new_theme_path) except OSError: # Already exists. Happens when the user manually adjusted the theme back in the preferences screen. try: os.removedirs(new_theme_path) shutil.copytree(theme_path, new_theme_path) except OSError: pass # Perhaps no rights? # Adjust the tooltip width. with open(os.path.join(theme_path, "theme.json")) as f: adjusted_theme = json.load(f) if "sizes" not in adjusted_theme: adjusted_theme["sizes"] = {} adjusted_theme["sizes"]["tooltip"] = [50.0, 10.0] with open(os.path.join(new_theme_path, "theme.json"), "w") as f: json.dump(adjusted_theme, f) # Enable the new theme. preferences.setValue("general/theme", new_theme_name) application.default_theme = new_theme_name def load_all_in_background(self): """ Runs the load_all() function as a background task. """ class ArticleLoadJob(Job): """ A background task that loads all articles of the guide. """ def __init__(self, guide): """ Creates the background task. :param guide: The CuraSettingsGuide object which has the function to call. """ self.guide = guide def run(self): """ Runs the background task. Cura will call this function from a different thread. """ self.guide.load_all() JobQueue.getInstance().add(ArticleLoadJob(self)) def load_all(self): """ Pre-cache all articles. This is meant to run as a background task. This makes sure all setting descriptions are loaded so that they can load faster next time the article is requested. It also makes sure that the setting description is correctly displayed (after a while). """ for article_id in self._container_stack.getAllKeys(): self._getArticle(article_id) # Load articles one by one. Logger.log("i", "Finished loading Settings Guide articles.") def load_definitions(self): """ Load all the setting definitions into a custom container stack. This container stack also contains extra entries for the articles that are not settings. The result is stored in self._container_stack. """ if self._container_stack: return # Already done earlier. Don't re-load. with open(os.path.join(os.path.dirname(__file__), "resources", "settings_guide_definitions.def.json")) as f: definitions_serialised = f.read() definition_container = DefinitionContainer("settings_guide_definitions") definition_container.deserialize(definitions_serialised) ContainerRegistry.getInstance().addContainer(definition_container) self._container_stack = ContainerStack("settings_guide_stack") self._container_stack.addContainer(definition_container) ContainerRegistry.getInstance().addContainer(self._container_stack) def find_articles(self): """ For each article and language, find where the Markdown file is located. :return: A nested dictionary mapping article ID to language to file path. """ result = {} # Find the English translations first. for root, _, files in os.walk(os.path.join(os.path.dirname(__file__), "resources", "articles")): for filename in files: base_name, extension = os.path.splitext(filename) if extension != ".md": continue # Only interested in article files. result[base_name] = {"en_US": os.path.join(root, filename)} # Find the translated articles in the translations folder. for language in os.listdir(os.path.join(os.path.dirname(__file__), "resources", "translations")): language_path = os.path.join(os.path.dirname(__file__), "resources", "translations", language) if not os.path.isdir(language_path): continue # Not a translation folder. for root, _, files in os.walk(language_path): for filename in files: base_name, extension = os.path.splitext(filename) if extension != ".md": continue # Only interested in article files. if base_name not in result: continue # Translation for an article that doesn't exist in English? result[base_name][language] = os.path.join(root, filename) return result def load_window(self): """ Create the GUI window for the guide. The window's QML element is stored in a field. There can only be one instance at a time. """ self.load_definitions() if self._dialog: return # Dialogue already open. Logger.log("d", "Creating Settings Guide window.") plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if plugin_path is None: plugin_path = os.path.dirname(__file__) path = os.path.join(plugin_path, "resources", "qml", "SettingsGuide.qml") self._dialog = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) if not self._dialog: Logger.log("e", "Unable to create settings guide dialogue.") def startWelcomeGuide(self) -> None: """ Opens the guide in the welcome page. """ self.load_window() if not self._dialog: return self.setSelectedArticleId("") # Display welcome page. self._dialog.show() def startWelcomeGuideAndSelectArticle(self, article_key: str) -> None: """ Opens the guide and immediately selects a certain article. :param article_key: The key of the article to show the guide of. """ self.load_window() if not self._dialog: return self.currentArticleReset.emit() self.setSelectedArticleId(article_key) self._dialog.show() currentArticleReset = pyqtSignal() #Signal to indicate that the article list should reset its current index. def _getArticle(self, article_id, language="en_US") -> List[List[str]]: """ Gets the rich text of a specified article. This function lazily loads an article from a file. If it's never been loaded before the article gets parsed and stored. Otherwise it'll get taken from the cache. :param article_id: The ID of the article to get. :param language: The language to get the article in. """ if article_id in self.articles and language in self.articles[article_id]: return self.articles[article_id][language] images_path = os.path.join(os.path.dirname(__file__), "resources", "articles", "images") try: if language not in self.article_locations[article_id]: language = "en_US" # Fall back to English if the preferred language is not available. markdown_file = self.article_locations[article_id][language] with open(markdown_file, encoding="utf-8") as f: markdown_str = f.read() images_path = os.path.dirname(markdown_file) except (OSError, KeyError): # File doesn't exist or is otherwise not readable. if self._container_stack and article_id in self._container_stack.getAllKeys(): markdown_str = "*" + self._container_stack.getProperty(article_id, "description") + "*" # Use the setting description as fallback. else: markdown_str = "There is no article on this topic." if images_path not in self._markdown_per_folder: renderer = QtMarkdownRenderer.QtMarkdownRenderer(images_path) self._markdown_per_folder[images_path] = mistune.Markdown(renderer=renderer) # Renders the Markdown articles into the subset of HTML supported by Qt. preferences = CuraApplication.getInstance().getPreferences() find_images = re.compile(r"!\[(.*)\]\(([^\)]+)\)") find_checkboxes = re.compile(r"\[ \]\s*([^\n]+)") image_description = None parts = [] # type: List[List[str]] # List of items in the article. Each item starts with a type ID, and then a variable number of data items. for index, part_between_images in enumerate(find_images.split(markdown_str)): # The parts of the regex split alternate between text, image description and image URL. if index % 3 == 0: part_between_images = part_between_images.strip() if part_between_images or index == 0: parts_between_checkboxes = find_checkboxes.split(part_between_images) for index2, part_between_checkboxes in enumerate(parts_between_checkboxes): part_between_checkboxes = part_between_checkboxes.strip() # The parts of the regex split alternate between text and checkbox description. if index2 % 2 == 0: if part_between_checkboxes: rich_text = self._markdown_per_folder[images_path](part_between_checkboxes) parts.append(["rich_text", rich_text]) else: # if index2 == 1: preference_key = "settings_guide/" + urllib.parse.quote_plus(part_between_checkboxes).lower() parts.append(["checkbox", preference_key, part_between_checkboxes]) elif index % 3 == 1: image_description = mistune.markdown(part_between_images) else: # if index % 3 == 2: if image_description is not None: if parts[-1][0] != "images": # List of images. parts.append(["images"]) image_url = os.path.join(images_path, part_between_images) parts[-1].append(QUrl.fromLocalFile(image_url).toString() + "|" + image_description) image_description = None if article_id not in self.articles: self.articles[article_id] = {} self.articles[article_id][language] = parts if preferences.getValue("settings_guide/show+articles+in+setting+tooltips+%28requires+restart%29"): # Load the article into the actual setting description as well. global_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_stack and article_id in global_stack.getAllKeys(): complete_article = self._markdown_per_folder[images_path](markdown_str) definition = global_stack.definition.findDefinitions(key=article_id)[0] definition._SettingDefinition__property_values["description"] = complete_article return self.articles[article_id][language] @pyqtSlot(str, result=bool) def isArticleFile(self, filename: str) -> bool: """ Tests whether a file name is the file name of an existing article. This test is used to determine if a link should refer to a different article or to the internet. :param filename: The file name to test for. :return: True if the file name is the file name of an existing article, or False if it isn't. """ return os.path.exists(filename) @pyqtProperty(QObject, constant=True) def containerStack(self) -> Optional[ContainerStack]: """ The specialised container stack containing setting definitions for all of the articles in the guide. :return: A container stack with extra definitions for all articles in the guide. """ return self._container_stack @pyqtProperty(str, constant=True) def pluginVersion(self) -> str: """ Get the version number of this plug-in. This plug-in version is a semantic version number in three parts ("1.2.3"). :return: The version number of this plug-in. """ return self.getVersion() selectedArticleChanged = pyqtSignal() @pyqtSlot(str) def setSelectedArticleId(self, article_key: str) -> None: """ Show a certain article in the dialogue. :param article_key: The key of the article to display. """ if self._selected_article_id != article_key: self._selected_article_id = article_key self.selectedArticleChanged.emit() @pyqtProperty(str, fset=setSelectedArticleId, notify=selectedArticleChanged) def selectedArticleId(self) -> str: """ Returns the key of the article that is currently selected. If no article has been selected (the logo is being shown), an empty string is returned. :return: The key of the article that is currently selected. """ return self._selected_article_id @pyqtProperty("QVariantList", notify=selectedArticleChanged) def selectedArticle(self) -> List[List[str]]: """ Returns the currently selected article. This article data is a rich text document, properly formatted from the Markdown files in the article directory. Each article is a list of items, some of which are text and some of which are image lists. :return: The the currently selected article. """ return self._getArticle(self._selected_article_id, self._selected_language)
class Script: def __init__(self): super().__init__() self._settings = None self._stack = None setting_data = self.getSettingData() self._stack = ContainerStack(stack_id=id(self)) self._stack.setDirty(False) # This stack does not need to be saved. ## Check if the definition of this script already exists. If not, add it to the registry. if "key" in setting_data: definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"]) if definitions: # Definition was found self._definition = definitions[0] else: self._definition = DefinitionContainer(setting_data["key"]) self._definition.deserialize(json.dumps(setting_data)) ContainerRegistry.getInstance().addContainer(self._definition) self._stack.addContainer(self._definition) self._instance = InstanceContainer(container_id="ScriptInstanceContainer") self._instance.setDefinition(self._definition) self._stack.addContainer(self._instance) ContainerRegistry.getInstance().addContainer(self._stack) settingsLoaded = Signal() ## Needs to return a dict that can be used to construct a settingcategory file. # See the example script for an example. # It follows the same style / guides as the Uranium settings. def getSettingData(self): raise NotImplementedError() def getDefinitionId(self): if self._stack: return self._stack.getBottom().getId() def getStackId(self): if self._stack: return self._stack.getId() ## Convenience function that retrieves value of a setting from the stack. def getSettingValueByKey(self, key): return self._stack.getProperty(key, "value") ## Convenience function that finds the value in a line of g-code. # When requesting key = x from line "G1 X100" the value 100 is returned. def getValue(self, line, key, default = None): 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) if m is None: return default try: return float(m.group(0)) except: return default ## This is called when the script is executed. # It gets a list of g-code strings and needs to return a (modified) list. def execute(self, data): raise NotImplementedError()
class Script: def __init__(self): super().__init__() self._settings = None self._stack = None setting_data = self.getSettingData() self._stack = ContainerStack(stack_id = str(id(self))) self._stack.setDirty(False) # This stack does not need to be saved. ## Check if the definition of this script already exists. If not, add it to the registry. if "key" in setting_data: definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"]) if definitions: # Definition was found self._definition = definitions[0] else: self._definition = DefinitionContainer(setting_data["key"]) self._definition.deserialize(json.dumps(setting_data)) ContainerRegistry.getInstance().addContainer(self._definition) self._stack.addContainer(self._definition) self._instance = InstanceContainer(container_id="ScriptInstanceContainer") self._instance.setDefinition(self._definition.getId()) self._instance.addMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default = 0)) self._stack.addContainer(self._instance) self._stack.propertyChanged.connect(self._onPropertyChanged) ContainerRegistry.getInstance().addContainer(self._stack) settingsLoaded = Signal() valueChanged = Signal() # Signal emitted whenever a value of a setting is changed def _onPropertyChanged(self, key, property_name): if property_name == "value": self.valueChanged.emit() # Property changed: trigger reslice # To do this we use the global container stack propertyChanged. # Reslicing is necessary for setting changes in this plugin, because the changes # are applied only once per "fresh" gcode global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack.propertyChanged.emit(key, property_name) ## Needs to return a dict that can be used to construct a settingcategory file. # See the example script for an example. # It follows the same style / guides as the Uranium settings. # Scripts can either override getSettingData directly, or use getSettingDataString # to return a string that will be parsed as json. The latter has the benefit over # returning a dict in that the order of settings is maintained. def getSettingData(self): setting_data = self.getSettingDataString() if type(setting_data) == str: setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict) return setting_data def getSettingDataString(self): raise NotImplementedError() def getDefinitionId(self): if self._stack: return self._stack.getBottom().getId() def getStackId(self): if self._stack: return self._stack.getId() ## Convenience function that retrieves value of a setting from the stack. def getSettingValueByKey(self, key): return self._stack.getProperty(key, "value") ## Convenience function that finds the value in a line of g-code. # When requesting key = x from line "G1 X100" the value 100 is returned. def getValue(self, line, key, default = None): 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) if m is None: return default try: return float(m.group(0)) except: return default ## This is called when the script is executed. # It gets a list of g-code strings and needs to return a (modified) list. def execute(self, data): raise NotImplementedError()