123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- # Copyright (c) 2022 Ultimaker B.V.
- # Cura is released under the terms of the LGPLv3 or higher.
- import re # For escaping characters in the settings.
- import json
- from UM.Mesh.MeshWriter import MeshWriter
- from UM.Logger import Logger
- from UM.Application import Application
- from UM.Settings.InstanceContainer import InstanceContainer
- from cura.Machines.ContainerTree import ContainerTree
- from UM.i18n import i18nCatalog
- from cura.Settings.CuraStackBuilder import CuraStackBuilder
- catalog = i18nCatalog("cura")
- class GCodeWriter(MeshWriter):
- """Writes g-code to a file.
- While this poses as a mesh writer, what this really does is take the g-code
- in the entire scene and write it to an output device. Since the g-code of a
- single mesh isn't separable from the rest what with rafts and travel moves
- and all, it doesn't make sense to write just a single mesh.
- So this plug-in takes the g-code that is stored in the root of the scene
- node tree, adds a bit of extra information about the profiles and writes
- that to the output device.
- """
- version = 3
- """The file format version of the serialised g-code.
- It can only read settings with the same version as the version it was
- written with. If the file format is changed in a way that breaks reverse
- compatibility, increment this version number!
- """
- escape_characters = {
- re.escape("\\"): "\\\\", # The escape character.
- re.escape("\n"): "\\n", # Newlines. They break off the comment.
- re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors.
- }
- """Dictionary that defines how characters are escaped when embedded in
- g-code.
- Note that the keys of this dictionary are regex strings. The values are
- not.
- """
- _setting_keyword = ";SETTING_"
- def __init__(self):
- super().__init__(add_to_recent_files = False)
- self._application = Application.getInstance()
- def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
- """Writes the g-code for the entire scene to a stream.
- Note that even though the function accepts a collection of nodes, the
- entire scene is always written to the file since it is not possible to
- separate the g-code for just specific nodes.
- :param stream: The stream to write the g-code to.
- :param nodes: This is ignored.
- :param mode: Additional information on how to format the g-code in the
- file. This must always be text mode.
- """
- if mode != MeshWriter.OutputMode.TextMode:
- Logger.log("e", "GCodeWriter does not support non-text mode.")
- self.setInformation(catalog.i18nc("@error:not supported", "GCodeWriter does not support non-text mode."))
- return False
- active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
- scene = Application.getInstance().getController().getScene()
- if not hasattr(scene, "gcode_dict"):
- self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
- return False
- gcode_dict = getattr(scene, "gcode_dict")
- gcode_list = gcode_dict.get(active_build_plate, None)
- if gcode_list is not None:
- has_settings = False
- for gcode in gcode_list:
- if gcode[:len(self._setting_keyword)] == self._setting_keyword:
- has_settings = True
- stream.write(gcode)
- # Serialise the current container stack and put it at the end of the file.
- if not has_settings:
- settings = self._serialiseSettings(Application.getInstance().getGlobalContainerStack())
- stream.write(settings)
- return True
- self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
- return False
- def _serialiseSettings(self, stack):
- """Serialises a container stack to prepare it for writing at the end of the g-code.
- The settings are serialised, and special characters (including newline)
- are escaped.
- :param stack: A container stack to serialise.
- :return: A serialised string of the settings.
- """
- container_registry = self._application.getContainerRegistry()
- prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line.
- prefix_length = len(prefix)
- quality_type = stack.quality.getMetaDataEntry("quality_type")
- container_with_profile = stack.qualityChanges
- machine_definition_id_for_quality = ContainerTree.getInstance().machines[stack.definition.getId()].quality_definition
- if container_with_profile.getId() == "empty_quality_changes":
- # If the global quality changes is empty, create a new one
- quality_name = container_registry.uniqueName(stack.quality.getName())
- quality_id = container_registry.uniqueName((stack.definition.getId() + "_" + quality_name).lower().replace(" ", "_"))
- container_with_profile = InstanceContainer(quality_id)
- container_with_profile.setName(quality_name)
- container_with_profile.setMetaDataEntry("type", "quality_changes")
- container_with_profile.setMetaDataEntry("quality_type", quality_type)
- if stack.getMetaDataEntry("position") is not None: # For extruder stacks, the quality changes should include an intent category.
- container_with_profile.setMetaDataEntry("intent_category", stack.intent.getMetaDataEntry("intent_category", "default"))
- container_with_profile.setDefinition(machine_definition_id_for_quality)
- container_with_profile.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
- merged_global_instance_container = InstanceContainer.createMergedInstanceContainer(stack.userChanges, container_with_profile)
- # If the quality changes is not set, we need to set type manually
- if merged_global_instance_container.getMetaDataEntry("type", None) is None:
- merged_global_instance_container.setMetaDataEntry("type", "quality_changes")
- # Ensure that quality_type is set. (Can happen if we have empty quality changes).
- if merged_global_instance_container.getMetaDataEntry("quality_type", None) is None:
- merged_global_instance_container.setMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
- # Get the machine definition ID for quality profiles
- merged_global_instance_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
- serialized = merged_global_instance_container.serialize()
- data = {"global_quality": serialized}
- all_setting_keys = merged_global_instance_container.getAllKeys()
- for extruder in stack.extruderList:
- extruder_quality = extruder.qualityChanges
- if extruder_quality.getId() == "empty_quality_changes":
- # Same story, if quality changes is empty, create a new one
- quality_name = container_registry.uniqueName(stack.quality.getName())
- quality_id = container_registry.uniqueName((stack.definition.getId() + "_" + quality_name).lower().replace(" ", "_"))
- extruder_quality = InstanceContainer(quality_id)
- extruder_quality.setName(quality_name)
- extruder_quality.setMetaDataEntry("type", "quality_changes")
- extruder_quality.setMetaDataEntry("quality_type", quality_type)
- extruder_quality.setDefinition(machine_definition_id_for_quality)
- extruder_quality.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
- flat_extruder_quality = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder_quality)
- # If the quality changes is not set, we need to set type manually
- if flat_extruder_quality.getMetaDataEntry("type", None) is None:
- flat_extruder_quality.setMetaDataEntry("type", "quality_changes")
- # Ensure that extruder is set. (Can happen if we have empty quality changes).
- if flat_extruder_quality.getMetaDataEntry("position", None) is None:
- flat_extruder_quality.setMetaDataEntry("position", extruder.getMetaDataEntry("position"))
- # Ensure that quality_type is set. (Can happen if we have empty quality changes).
- if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None:
- flat_extruder_quality.setMetaDataEntry("quality_type", extruder.quality.getMetaDataEntry("quality_type", "normal"))
- # Change the default definition
- flat_extruder_quality.setMetaDataEntry("definition", machine_definition_id_for_quality)
- extruder_serialized = flat_extruder_quality.serialize()
- data.setdefault("extruder_quality", []).append(extruder_serialized)
- all_setting_keys.update(flat_extruder_quality.getAllKeys())
- # Check if there is any profiles
- if not all_setting_keys:
- Logger.log("i", "No custom settings found, not writing settings to g-code.")
- return ""
- json_string = json.dumps(data)
- # Escape characters that have a special meaning in g-code comments.
- pattern = re.compile("|".join(GCodeWriter.escape_characters.keys()))
- # Perform the replacement with a regular expression.
- escaped_string = pattern.sub(lambda m: GCodeWriter.escape_characters[re.escape(m.group(0))], json_string)
- # Introduce line breaks so that each comment is no longer than 80 characters. Prepend each line with the prefix.
- result = ""
- # Lines have 80 characters, so the payload of each line is 80 - prefix.
- for pos in range(0, len(escaped_string), 80 - prefix_length):
- result += prefix + escaped_string[pos: pos + 80 - prefix_length] + "\n"
- return result
|