diff --git a/CMakeLists.txt b/CMakeLists.txt index ab08a4d624..8105b677ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,9 @@ add_custom_target(tests) add_custom_command(TARGET tests POST_BUILD COMMAND "PYTHONPATH=${CMAKE_SOURCE_DIR}/../Uranium/:${CMAKE_SOURCE_DIR}" ${PYTHON_EXECUTABLE} -m pytest -r a --junitxml=${CMAKE_BINARY_DIR}/junit.xml ${CMAKE_SOURCE_DIR} || exit 0) option(CURA_DEBUGMODE "Enable debug dialog and other debug features" OFF) +if(CURA_DEBUGMODE) + set(_cura_debugmode "ON") +endif() set(CURA_VERSION "master" CACHE STRING "Version name of Cura") set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'") diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 144c9533be..174e894087 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -126,6 +126,8 @@ class CuraApplication(QtApplication): SettingDefinition.addSettingType("extruder", None, str, Validator) + SettingDefinition.addSettingType("[int]", None, str, None) + SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues) SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue) SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue) diff --git a/cura/CuraVersion.py.in b/cura/CuraVersion.py.in index 8a4d13e526..fb66275395 100644 --- a/cura/CuraVersion.py.in +++ b/cura/CuraVersion.py.in @@ -3,4 +3,4 @@ CuraVersion = "@CURA_VERSION@" CuraBuildType = "@CURA_BUILDTYPE@" -CuraDebugMode = True if "@CURA_DEBUGMODE@" == "ON" else False +CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 7f0b7c4c07..f411190fd5 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -47,8 +47,8 @@ class PrinterOutputDevice(QObject, OutputDevice): self._job_name = "" self._error_text = "" self._accepts_commands = True - self._preheat_bed_timeout = 900 #Default time-out for pre-heating the bed, in seconds. - self._preheat_bed_timer = QTimer() #Timer that tracks how long to preheat still. + self._preheat_bed_timeout = 900 # Default time-out for pre-heating the bed, in seconds. + self._preheat_bed_timer = QTimer() # Timer that tracks how long to preheat still. self._preheat_bed_timer.setSingleShot(True) self._preheat_bed_timer.timeout.connect(self.cancelPreheatBed) @@ -232,11 +232,15 @@ class PrinterOutputDevice(QObject, OutputDevice): # \return The duration of the time-out to pre-heat the bed, formatted. @pyqtProperty(str, notify = preheatBedRemainingTimeChanged) def preheatBedRemainingTime(self): + if not self._preheat_bed_timer.isActive(): + return "" period = self._preheat_bed_timer.remainingTime() if period <= 0: return "" minutes, period = divmod(period, 60000) #60000 milliseconds in a minute. seconds, _ = divmod(period, 1000) #1000 milliseconds in a second. + if minutes <= 0 and seconds <= 0: + return "" return "%d:%02d" % (minutes, seconds) ## Time the print has been printing. diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index fde4d6ab21..38e7130070 100644 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -18,6 +18,10 @@ from cura.QualityManager import QualityManager from UM.Scene.SceneNode import SceneNode MYPY = False + +import Savitar +import numpy + try: if not MYPY: import xml.etree.cElementTree as ET @@ -38,98 +42,10 @@ class ThreeMFReader(MeshReader): self._base_name = "" self._unit = None - def _createNodeFromObject(self, object, name = ""): - node = SceneNode() - node.setName(name) - mesh_builder = MeshBuilder() - vertex_list = [] - - components = object.find(".//3mf:components", self._namespaces) - if components: - for component in components: - id = component.get("objectid") - new_object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces) - new_node = self._createNodeFromObject(new_object, self._base_name + "_" + str(id)) - node.addChild(new_node) - transform = component.get("transform") - if transform is not None: - new_node.setTransformation(self._createMatrixFromTransformationString(transform)) - - # for vertex in entry.mesh.vertices.vertex: - for vertex in object.findall(".//3mf:vertex", self._namespaces): - vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")]) - Job.yieldThread() - - xml_settings = list(object.findall(".//cura:setting", self._namespaces)) - - # Add the setting override decorator, so we can add settings to this node. - if xml_settings: - node.addDecorator(SettingOverrideDecorator()) - - global_container_stack = Application.getInstance().getGlobalContainerStack() - # Ensure the correct next container for the SettingOverride decorator is set. - if global_container_stack: - multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 - # Ensure that all extruder data is reset - if not multi_extrusion: - default_stack_id = global_container_stack.getId() - else: - default_stack = ExtruderManager.getInstance().getExtruderStack(0) - if default_stack: - default_stack_id = default_stack.getId() - else: - default_stack_id = global_container_stack.getId() - node.callDecoration("setActiveExtruder", default_stack_id) - - # Get the definition & set it - definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom()) - node.callDecoration("getStack").getTop().setDefinition(definition) - - setting_container = node.callDecoration("getStack").getTop() - for setting in xml_settings: - setting_key = setting.get("key") - setting_value = setting.text - - # Extruder_nr is a special case. - if setting_key == "extruder_nr": - extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value)) - if extruder_stack: - node.callDecoration("setActiveExtruder", extruder_stack.getId()) - else: - Logger.log("w", "Unable to find extruder in position %s", setting_value) - continue - setting_container.setProperty(setting_key,"value", setting_value) - - if len(node.getChildren()) > 0: - group_decorator = GroupDecorator() - node.addDecorator(group_decorator) - - triangles = object.findall(".//3mf:triangle", self._namespaces) - mesh_builder.reserveFaceCount(len(triangles)) - - for triangle in triangles: - v1 = int(triangle.get("v1")) - v2 = int(triangle.get("v2")) - v3 = int(triangle.get("v3")) - - mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2], - vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2], - vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2]) - - Job.yieldThread() - - # TODO: We currently do not check for normals and simply recalculate them. - mesh_builder.calculateNormals(fast=True) - mesh_builder.setFileName(name) - mesh_data = mesh_builder.build() - - if len(mesh_data.getVertices()): - node.setMeshData(mesh_data) - - node.setSelectable(True) - return node - def _createMatrixFromTransformationString(self, transformation): + if transformation == "": + return Matrix() + splitted_transformation = transformation.split() ## Transformation is saved as: ## M00 M01 M02 0.0 @@ -156,51 +72,96 @@ class ThreeMFReader(MeshReader): return temp_mat + + ## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scenenode. + # \returns Uranium Scenen node. + def _convertSavitarNodeToUMNode(self, savitar_node): + um_node = SceneNode() + transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation()) + um_node.setTransformation(transformation) + mesh_builder = MeshBuilder() + + data = numpy.fromstring(savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32) + + vertices = numpy.resize(data, (int(data.size / 3), 3)) + mesh_builder.setVertices(vertices) + mesh_builder.calculateNormals(fast=True) + mesh_data = mesh_builder.build() + + if len(mesh_data.getVertices()): + um_node.setMeshData(mesh_data) + + for child in savitar_node.getChildren(): + um_node.addChild(self._convertSavitarNodeToUMNode(child)) + + settings = savitar_node.getSettings() + + # Add the setting override decorator, so we can add settings to this node. + if settings: + um_node.addDecorator(SettingOverrideDecorator()) + + global_container_stack = Application.getInstance().getGlobalContainerStack() + # Ensure the correct next container for the SettingOverride decorator is set. + if global_container_stack: + multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 + + # Ensure that all extruder data is reset + if not multi_extrusion: + default_stack_id = global_container_stack.getId() + else: + default_stack = ExtruderManager.getInstance().getExtruderStack(0) + if default_stack: + default_stack_id = default_stack.getId() + else: + default_stack_id = global_container_stack.getId() + um_node.callDecoration("setActiveExtruder", default_stack_id) + + # Get the definition & set it + definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom()) + um_node.callDecoration("getStack").getTop().setDefinition(definition) + + setting_container = um_node.callDecoration("getStack").getTop() + + for key in settings: + setting_value = settings[key] + + # Extruder_nr is a special case. + if key == "extruder_nr": + extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value)) + if extruder_stack: + um_node.callDecoration("setActiveExtruder", extruder_stack.getId()) + else: + Logger.log("w", "Unable to find extruder in position %s", setting_value) + continue + setting_container.setProperty(key,"value", setting_value) + + if len(um_node.getChildren()) > 0: + group_decorator = GroupDecorator() + um_node.addDecorator(group_decorator) + um_node.setSelectable(True) + return um_node + def read(self, file_name): result = [] # The base object of 3mf is a zipped archive. - archive = zipfile.ZipFile(file_name, "r") - self._base_name = os.path.basename(file_name) try: - self._root = ET.parse(archive.open("3D/3dmodel.model")) - self._unit = self._root.getroot().get("unit") - - build_items = self._root.findall("./3mf:build/3mf:item", self._namespaces) - - for build_item in build_items: - id = build_item.get("objectid") - object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces) - if "type" in object.attrib: - if object.attrib["type"] == "support" or object.attrib["type"] == "other": - # Ignore support objects, as cura does not support these. - # We can't guarantee that they wont be made solid. - # We also ignore "other", as I have no idea what to do with them. - Logger.log("w", "3MF file contained an object of type %s which is not supported by Cura", object.attrib["type"]) - continue - elif object.attrib["type"] == "solidsupport" or object.attrib["type"] == "model": - pass # Load these as normal - else: - # We should technically fail at this point because it's an invalid 3MF, but try to continue anyway. - Logger.log("e", "3MF file contained an object of type %s which is not supported by the 3mf spec", - object.attrib["type"]) - continue - - build_item_node = self._createNodeFromObject(object, self._base_name + "_" + str(id)) - + archive = zipfile.ZipFile(file_name, "r") + self._base_name = os.path.basename(file_name) + parser = Savitar.ThreeMFParser() + scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read()) + self._unit = scene_3mf.getUnit() + for node in scene_3mf.getSceneNodes(): + um_node = self._convertSavitarNodeToUMNode(node) # compensate for original center position, if object(s) is/are not around its zero position + transform_matrix = Matrix() - mesh_data = build_item_node.getMeshData() + mesh_data = um_node.getMeshData() if mesh_data is not None: extents = mesh_data.getExtents() center_vector = Vector(extents.center.x, extents.center.y, extents.center.z) transform_matrix.setByTranslation(center_vector) - - # offset with transform from 3mf - transform = build_item.get("transform") - if transform is not None: - transform_matrix.multiply(self._createMatrixFromTransformationString(transform)) - - build_item_node.setTransformation(transform_matrix) + transform_matrix.multiply(um_node.getLocalTransformation()) + um_node.setTransformation(transform_matrix) global_container_stack = Application.getInstance().getGlobalContainerStack() @@ -215,9 +176,9 @@ class ThreeMFReader(MeshReader): # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: - translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2, - y = -global_container_stack.getProperty("machine_depth", "value") / 2, - z = 0) + translation_vector = Vector(x=-global_container_stack.getProperty("machine_width", "value") / 2, + y=-global_container_stack.getProperty("machine_depth", "value") / 2, + z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.multiply(translation_matrix) @@ -228,12 +189,13 @@ class ThreeMFReader(MeshReader): transformation_matrix.multiply(scale_matrix) # Pre multiply the transformation with the loaded transformation, so the data is handled correctly. - build_item_node.setTransformation(build_item_node.getLocalTransformation().preMultiply(transformation_matrix)) + um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix)) - result.append(build_item_node) + result.append(um_node) - except Exception as e: - Logger.log("e", "An exception occurred in 3mf reader: %s", e) + except Exception: + Logger.logException("e", "An exception occurred in 3mf reader.") + return [] return result diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index c907752137..7ce75bf60e 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -6,6 +6,11 @@ from UM.Math.Vector import Vector from UM.Logger import Logger from UM.Math.Matrix import Matrix from UM.Application import Application +import UM.Scene.SceneNode + +import Savitar + +import numpy MYPY = False try: @@ -35,18 +40,18 @@ class ThreeMFWriter(MeshWriter): def _convertMatrixToString(self, matrix): result = "" - result += str(matrix._data[0,0]) + " " - result += str(matrix._data[1,0]) + " " - result += str(matrix._data[2,0]) + " " - result += str(matrix._data[0,1]) + " " - result += str(matrix._data[1,1]) + " " - result += str(matrix._data[2,1]) + " " - result += str(matrix._data[0,2]) + " " - result += str(matrix._data[1,2]) + " " - result += str(matrix._data[2,2]) + " " - result += str(matrix._data[0,3]) + " " - result += str(matrix._data[1,3]) + " " - result += str(matrix._data[2,3]) + result += str(matrix._data[0, 0]) + " " + result += str(matrix._data[1, 0]) + " " + result += str(matrix._data[2, 0]) + " " + result += str(matrix._data[0, 1]) + " " + result += str(matrix._data[1, 1]) + " " + result += str(matrix._data[2, 1]) + " " + result += str(matrix._data[0, 2]) + " " + result += str(matrix._data[1, 2]) + " " + result += str(matrix._data[2, 2]) + " " + result += str(matrix._data[0, 3]) + " " + result += str(matrix._data[1, 3]) + " " + result += str(matrix._data[2, 3]) return result ## Should we store the archive @@ -55,6 +60,48 @@ class ThreeMFWriter(MeshWriter): def setStoreArchive(self, store_archive): self._store_archive = store_archive + ## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode + # \returns Uranium Scenen node. + def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()): + if type(um_node) is not UM.Scene.SceneNode.SceneNode: + return None + + savitar_node = Savitar.SceneNode() + + node_matrix = um_node.getLocalTransformation() + + matrix_string = self._convertMatrixToString(node_matrix.preMultiply(transformation)) + + savitar_node.setTransformation(matrix_string) + mesh_data = um_node.getMeshData() + if mesh_data is not None: + savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray()) + indices_array = mesh_data.getIndicesAsByteArray() + if indices_array is not None: + savitar_node.getMeshData().setFacesFromBytes(indices_array) + else: + savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tostring()) + + # Handle per object settings (if any) + stack = um_node.callDecoration("getStack") + if stack is not None: + changed_setting_keys = set(stack.getTop().getAllKeys()) + + # Ensure that we save the extruder used for this object. + if stack.getProperty("machine_extruder_count", "value") > 1: + changed_setting_keys.add("extruder_nr") + + # Get values for all changed settings & save them. + for key in changed_setting_keys: + savitar_node.setSetting(key, str(stack.getProperty(key, "value"))) + + for child_node in um_node.getChildren(): + savitar_child_node = self._convertUMNodeToSavitarNode(child_node) + if savitar_child_node is not None: + savitar_node.addChild(savitar_child_node) + + return savitar_node + def getArchive(self): return self._archive @@ -79,98 +126,7 @@ class ThreeMFWriter(MeshWriter): relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") - model = ET.Element("model", unit = "millimeter", xmlns = self._namespaces["3mf"]) - model.set("xmlns:cura", self._namespaces["cura"]) - - # Add the version of Cura this was created with. Since there is no "version" or similar metadata name we need - # to prefix it with the cura namespace, as specified by the 3MF specification. - version_metadata = ET.SubElement(model, "metadata", name = "cura:version") - version_metadata.text = Application.getInstance().getVersion() - - resources = ET.SubElement(model, "resources") - build = ET.SubElement(model, "build") - - added_nodes = [] - index = 0 # Ensure index always exists (even if there are no nodes to write) - # Write all nodes with meshData to the file as objects inside the resource tag - for index, n in enumerate(MeshWriter._meshNodes(nodes)): - added_nodes.append(n) # Save the nodes that have mesh data - object = ET.SubElement(resources, "object", id = str(index+1), type = "model") - mesh = ET.SubElement(object, "mesh") - - mesh_data = n.getMeshData() - vertices = ET.SubElement(mesh, "vertices") - verts = mesh_data.getVertices() - - if verts is None: - Logger.log("d", "3mf writer can't write nodes without mesh data. Skipping this node.") - continue # No mesh data, nothing to do. - if mesh_data.hasIndices(): - for face in mesh_data.getIndices(): - v1 = verts[face[0]] - v2 = verts[face[1]] - v3 = verts[face[2]] - xml_vertex1 = ET.SubElement(vertices, "vertex", x = str(v1[0]), y = str(v1[1]), z = str(v1[2])) - xml_vertex2 = ET.SubElement(vertices, "vertex", x = str(v2[0]), y = str(v2[1]), z = str(v2[2])) - xml_vertex3 = ET.SubElement(vertices, "vertex", x = str(v3[0]), y = str(v3[1]), z = str(v3[2])) - - triangles = ET.SubElement(mesh, "triangles") - for face in mesh_data.getIndices(): - triangle = ET.SubElement(triangles, "triangle", v1 = str(face[0]) , v2 = str(face[1]), v3 = str(face[2])) - else: - triangles = ET.SubElement(mesh, "triangles") - for idx, vert in enumerate(verts): - xml_vertex = ET.SubElement(vertices, "vertex", x = str(vert[0]), y = str(vert[1]), z = str(vert[2])) - - # If we have no faces defined, assume that every three subsequent vertices form a face. - if idx % 3 == 0: - triangle = ET.SubElement(triangles, "triangle", v1 = str(idx), v2 = str(idx + 1), v3 = str(idx + 2)) - - # Handle per object settings - stack = n.callDecoration("getStack") - if stack is not None: - changed_setting_keys = set(stack.getTop().getAllKeys()) - - # Ensure that we save the extruder used for this object. - if stack.getProperty("machine_extruder_count", "value") > 1: - changed_setting_keys.add("extruder_nr") - - settings_xml = ET.SubElement(object, "settings", xmlns=self._namespaces["cura"]) - - # Get values for all changed settings & save them. - for key in changed_setting_keys: - setting_xml = ET.SubElement(settings_xml, "setting", key = key) - setting_xml.text = str(stack.getProperty(key, "value")) - - # Add one to the index as we haven't incremented the last iteration. - index += 1 - nodes_to_add = set() - - for node in added_nodes: - # Check the parents of the nodes with mesh_data and ensure that they are also added. - parent_node = node.getParent() - while parent_node is not None: - if parent_node.callDecoration("isGroup"): - nodes_to_add.add(parent_node) - parent_node = parent_node.getParent() - else: - parent_node = None - - # Sort all the nodes by depth (so nodes with the highest depth are done first) - sorted_nodes_to_add = sorted(nodes_to_add, key=lambda node: node.getDepth(), reverse = True) - - # We have already saved the nodes with mesh data, but now we also want to save nodes required for the scene - for node in sorted_nodes_to_add: - object = ET.SubElement(resources, "object", id=str(index + 1), type="model") - components = ET.SubElement(object, "components") - for child in node.getChildren(): - if child in added_nodes: - component = ET.SubElement(components, "component", objectid = str(added_nodes.index(child) + 1), transform = self._convertMatrixToString(child.getLocalTransformation())) - index += 1 - added_nodes.append(node) - - # Create a transformation Matrix to convert from our worldspace into 3MF. - # First step: flip the y and z axis. + savitar_scene = Savitar.Scene() transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = -1 @@ -188,14 +144,23 @@ class ThreeMFWriter(MeshWriter): translation_matrix.setByTranslation(translation_vector) transformation_matrix.preMultiply(translation_matrix) - # Find out what the final build items are and add them. - for node in added_nodes: - if node.getParent().callDecoration("isGroup") is None: - node_matrix = node.getLocalTransformation() - ET.SubElement(build, "item", objectid = str(added_nodes.index(node) + 1), transform = self._convertMatrixToString(node_matrix.preMultiply(transformation_matrix))) + root_node = UM.Application.getInstance().getController().getScene().getRoot() + for node in nodes: + if node == root_node: + for root_child in node.getChildren(): + savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix) + if savitar_node: + savitar_scene.addSceneNode(savitar_node) + else: + savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix) + if savitar_node: + savitar_scene.addSceneNode(savitar_node) - archive.writestr(model_file, b' \n' + ET.tostring(model)) + parser = Savitar.ThreeMFParser() + scene_string = parser.sceneToString(savitar_scene) + + archive.writestr(model_file, scene_string) archive.writestr(content_types_file, b' \n' + ET.tostring(content_types)) archive.writestr(relations_file, b' \n' + ET.tostring(relations_element)) except Exception as e: diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index 9811316948..cb65da635b 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -209,6 +209,8 @@ Item { { case "int": return settingTextField + case "[int]": + return settingTextField case "float": return settingTextField case "enum": diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py index a7223128b4..ea8917ed9f 100644 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py @@ -558,7 +558,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._preheat_bed_timer.stop() self.preheatBedRemainingTimeChanged.emit() - def close(self): Logger.log("d", "Closing connection of printer %s with ip %s", self._key, self._address) self._updateJobState("") diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 4457ecfda4..d808984117 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -93,6 +93,7 @@ "description": "Whether to wait until the nozzle temperature is reached at the start.", "default_value": true, "type": "bool", + "enabled": "machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": false, "settable_per_meshgroup": false @@ -103,6 +104,7 @@ "description": "Whether to include nozzle temperature commands at the start of the gcode. When the start_gcode already contains nozzle temperature commands Cura frontend will automatically disable this setting.", "default_value": true, "type": "bool", + "enabled": "machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": false, "settable_per_meshgroup": false @@ -249,6 +251,17 @@ "settable_per_extruder": true, "settable_per_meshgroup": false }, + "machine_nozzle_temp_enabled": + { + "label": "Enable Nozzle Temperature Control", + "description": "Whether to control temperature from Cura. Turn this off to control nozzle temperature from outside of Cura.", + "default_value": true, + "value": "machine_gcode_flavor != \"UltiGCode\"", + "type": "bool", + "settable_per_mesh": false, + "settable_per_extruder": true, + "settable_per_meshgroup": false + }, "machine_nozzle_heat_up_speed": { "label": "Heat up speed", @@ -256,6 +269,7 @@ "default_value": 2.0, "unit": "°C/s", "type": "float", + "enabled": "machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -266,6 +280,7 @@ "default_value": 2.0, "unit": "°C/s", "type": "float", + "enabled": "machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -276,6 +291,7 @@ "default_value": 50.0, "unit": "s", "type": "float", + "enabled": "machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -908,6 +924,15 @@ "value": "top_bottom_pattern", "settable_per_mesh": true }, + "skin_angles": + { + "label": "Top/Bottom Line Directions", + "description": "A list of integer line directions to use when the top/bottom layers use the lines or zig zag pattern. Elements from the list are used sequentially as the layers progress and when the end of the list is reached, it starts at the beginning again. The list items are separated by commas and the whole list is contained in square brackets. Default is an empty list which means use the traditional default angles (45 and 135 degrees).", + "type": "[int]", + "default_value": "[ ]", + "enabled": "top_bottom_pattern != 'concentric'", + "settable_per_mesh": true + }, "wall_0_inset": { "label": "Outer Wall Inset", @@ -1089,6 +1114,15 @@ "value": "'lines' if infill_sparse_density > 25 else 'grid'", "settable_per_mesh": true }, + "infill_angles": + { + "label": "Infill Line Directions", + "description": "A list of integer line directions to use. Elements from the list are used sequentially as the layers progress and when the end of the list is reached, it starts at the beginning again. The list items are separated by commas and the whole list is contained in square brackets. Default is an empty list which means use the traditional default angles (45 and 135 degrees for the lines and zig zag patterns and 45 degrees for all other patterns).", + "type": "[int]", + "default_value": "[ ]", + "enabled": "infill_pattern != 'concentric' and infill_pattern != 'concentric_3d' and infill_pattern != 'cubicsubdiv'", + "settable_per_mesh": true + }, "sub_div_rad_mult": { "label": "Cubic Subdivision Radius", @@ -1243,6 +1277,74 @@ "minimum_value": "0", "default_value": 0, "settable_per_mesh": true + }, + "expand_skins_into_infill": + { + "label": "Expand Skins Into Infill", + "description": "Expand skin areas of top and/or bottom skin of flat surfaces. By default, skins stop under the wall lines that surround infill but this can lead to holes appearing when the infill density is low. This setting extends the skins beyond the wall lines so that the infill on the next layer rests on skin.", + "type": "bool", + "default_value": false, + "settable_per_mesh": true, + "children": + { + "expand_upper_skins": + { + "label": "Expand Upper Skins", + "description": "Expand upper skin areas (areas with air above) so that they support infill above.", + "type": "bool", + "default_value": false, + "value": "expand_skins_into_infill", + "settable_per_mesh": true + }, + "expand_lower_skins": + { + "label": "Expand Lower Skins", + "description": "Expand lower skin areas (areas with air below) so that they are anchored by the infill layers above and below.", + "type": "bool", + "default_value": false, + "settable_per_mesh": true + } + } + }, + "expand_skins_expand_distance": + { + "label": "Skin Expand Distance", + "description": "The distance the skins are expanded into the infill. The default distance is enough to bridge the gap between the infill lines and will stop holes appearing in the skin where it meets the wall when the infill density is low. A smaller distance will often be sufficient.", + "unit": "mm", + "type": "float", + "default_value": 2.8, + "value": "infill_line_distance * 1.4", + "minimum_value": "0", + "enabled": "expand_upper_skins or expand_lower_skins", + "settable_per_mesh": true + }, + "min_skin_angle_for_expansion": + { + "label": "Minimum Skin Angle for Expansion", + "description": "Top and or bottom surfaces of your object with an angle larger than this setting, won't have their top/bottom skin expanded. This avoids expanding the narrow skin areas that are created when the model surface has a near vertical slope.", + "unit": "°", + "type": "float", + "minimum_value": "0", + "maximum_value": "90", + "maximum_value_warning": "45", + "default_value": 20, + "enabled": "expand_upper_skins or expand_lower_skins", + "settable_per_mesh": true, + "children": + { + "min_skin_width_for_expansion": + { + "label": "Minimum Skin Width for Expansion", + "description": "Skin areas narrower than this are not expanded. This avoids expanding the narrow skin areas that are created when the model surface has a slope close to the vertical.", + "unit": "mm", + "type": "float", + "default_value": 2.24, + "value": "top_layers * layer_height / math.tan(math.radians(min_skin_angle_for_expansion))", + "minimum_value": "0", + "enabled": "expand_upper_skins or expand_lower_skins", + "settable_per_mesh": true + } + } } } }, @@ -1260,7 +1362,7 @@ "description": "Change the temperature for each layer automatically with the average flow speed of that layer.", "type": "bool", "default_value": false, - "enabled": "False", + "enabled": "machine_nozzle_temp_enabled and False", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -1273,14 +1375,14 @@ "default_value": 210, "minimum_value_warning": "0", "maximum_value_warning": "285", - "enabled": false, + "enabled": "machine_nozzle_temp_enabled", "settable_per_extruder": true, "minimum_value": "-273.15" }, "material_print_temperature": { "label": "Printing Temperature", - "description": "The temperature used for printing. If this is 0, the extruder will not heat up for this print.", + "description": "The temperature used for printing.", "unit": "°C", "type": "float", "default_value": 210, @@ -1288,7 +1390,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "285", - "enabled": "not (material_flow_dependent_temperature) and machine_gcode_flavor != \"UltiGCode\"", + "enabled": "machine_nozzle_temp_enabled and not (material_flow_dependent_temperature)", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -1303,7 +1405,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "285", - "enabled": "machine_gcode_flavor != \"UltiGCode\"", + "enabled": "machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -1318,7 +1420,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "material_standby_temperature", "maximum_value_warning": "material_print_temperature", - "enabled": "machine_gcode_flavor != \"UltiGCode\"", + "enabled": "machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -1333,7 +1435,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "material_standby_temperature", "maximum_value_warning": "material_print_temperature", - "enabled": "machine_gcode_flavor != \"UltiGCode\"", + "enabled": "machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -1344,8 +1446,7 @@ "unit": "[[mm³,°C]]", "type": "str", "default_value": "[[3.5,200],[7.0,240]]", - "enabled": "False", - "comments": "old enabled function: material_flow_dependent_temperature", + "enabled": "False and machine_nozzle_temp_enabled and material_flow_dependent_temperature", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -1359,8 +1460,7 @@ "minimum_value": "0", "maximum_value_warning": "10.0", "maximum_value": "machine_nozzle_heat_up_speed", - "enabled": "False", - "comments": "old enabled function: material_flow_dependent_temperature or machine_extruder_count > 1", + "enabled": "material_flow_dependent_temperature or (machine_extruder_count > 1 and material_final_print_temperature != material_print_temperature)", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -1567,7 +1667,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "260", - "enabled": "machine_extruder_count > 1 and machine_gcode_flavor != \"UltiGCode\"", + "enabled": "machine_extruder_count > 1 and machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true }, diff --git a/resources/definitions/ultimaker2.def.json b/resources/definitions/ultimaker2.def.json index 84e09113f3..a52075fe5e 100644 --- a/resources/definitions/ultimaker2.def.json +++ b/resources/definitions/ultimaker2.def.json @@ -100,6 +100,9 @@ }, "machine_acceleration": { "default_value": 3000 + }, + "machine_nozzle_temp_enabled": { + "default_value": false } } } diff --git a/resources/qml/DiscardOrKeepProfileChangesDialog.qml b/resources/qml/DiscardOrKeepProfileChangesDialog.qml index 4c88801bb0..26c343ad5c 100644 --- a/resources/qml/DiscardOrKeepProfileChangesDialog.qml +++ b/resources/qml/DiscardOrKeepProfileChangesDialog.qml @@ -64,7 +64,7 @@ UM.Dialog anchors.margins: UM.Theme.getSize("default_margin").width anchors.left: parent.left anchors.right: parent.right - height: base.height - 130 + height: base.height - 200 id: tableView Component { @@ -146,7 +146,25 @@ UM.Dialog ] width: 300 currentIndex: UM.Preferences.getValue("cura/choice_on_profile_override") - onCurrentIndexChanged: UM.Preferences.setValue("cura/choice_on_profile_override", currentIndex) + onCurrentIndexChanged: + { + UM.Preferences.setValue("cura/choice_on_profile_override", currentIndex) + if (currentIndex == 1) { + // 1 == "Discard and never ask again", so only enable the "Discard" button + discardButton.enabled = true + keepButton.enabled = false + } + else if (currentIndex == 2) { + // 2 == "Keep and never ask again", so only enable the "Keep" button + keepButton.enabled = true + discardButton.enabled = false + } + else { + // 0 == "Always ask me this", so show both + keepButton.enabled = true + discardButton.enabled = true + } + } } } diff --git a/resources/qml/Settings/SettingTextField.qml b/resources/qml/Settings/SettingTextField.qml index da24f0f521..ce376e1c77 100644 --- a/resources/qml/Settings/SettingTextField.qml +++ b/resources/qml/Settings/SettingTextField.qml @@ -98,9 +98,9 @@ SettingItem selectByMouse: true; - maximumLength: 10; + maximumLength: (definition.type == "[int]") ? 20 : 10; - validator: RegExpValidator { regExp: (definition.type == "int") ? /^-?[0-9]{0,10}$/ : /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } // definition.type property from parent loader used to disallow fractional number entry + validator: RegExpValidator { regExp: (definition.type == "[int]") ? /^\[?(\s*-?[0-9]{0,9}\s*,)*(\s*-?[0-9]{0,9})\s*\]?$/ : (definition.type == "int") ? /^-?[0-9]{0,10}$/ : /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } // definition.type property from parent loader used to disallow fractional number entry Binding { diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 24022891c3..7138d4acd3 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -217,6 +217,8 @@ Item { case "int": return "SettingTextField.qml" + case "[int]": + return "SettingTextField.qml" case "float": return "SettingTextField.qml" case "enum":