diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index ca4c8e7b34..a25604d165 100644 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -67,6 +67,9 @@ class BuildVolume(SceneNode): self._disallowed_areas = [] self._disallowed_area_mesh = None + self._prime_tower_area = None + self._prime_tower_area_mesh = None + self.setCalculateBoundingBox(False) self._volume_aabb = None @@ -82,6 +85,8 @@ class BuildVolume(SceneNode): ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged) self._onActiveExtruderStackChanged() + self._has_errors = False + def setWidth(self, width): if width: self._width = width @@ -110,6 +115,10 @@ class BuildVolume(SceneNode): if self._disallowed_area_mesh: renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9) + if self._prime_tower_area_mesh: + renderer.queueNode(self, mesh = self._prime_tower_area_mesh, shader = self._shader, transparent=True, + backface_cull=True, sort=-8) + return True ## Recalculates the build volume & disallowed areas. @@ -184,6 +193,24 @@ class BuildVolume(SceneNode): else: self._disallowed_area_mesh = None + if self._prime_tower_area: + mb = MeshBuilder() + color = Color(1.0, 0.0, 0.0, 0.5) + points = self._prime_tower_area.getPoints() + first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, + self._clamp(points[0][1], min_d, max_d)) + previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, + self._clamp(points[0][1], min_d, max_d)) + for point in points: + new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, + self._clamp(point[1], min_d, max_d)) + mb.addFace(first, previous_point, new_point, color=color) + previous_point = new_point + + self._prime_tower_area_mesh = mb.build() + else: + self._prime_tower_area_mesh = None + self._volume_aabb = AxisAlignedBox( minimum = Vector(min_w, min_h - 1.0, min_d), maximum = Vector(max_w, max_h - self._raft_thickness, max_d)) @@ -291,24 +318,34 @@ class BuildVolume(SceneNode): if rebuild_me: self.rebuild() + def hasErrors(self): + return self._has_errors + def _updateDisallowedAreas(self): if not self._global_container_stack: return - + self._has_errors = False # Reset. disallowed_areas = copy.deepcopy( self._global_container_stack.getProperty("machine_disallowed_areas", "value")) areas = [] machine_width = self._global_container_stack.getProperty("machine_width", "value") machine_depth = self._global_container_stack.getProperty("machine_depth", "value") - + self._prime_tower_area = None # Add prime tower location as disallowed area. if self._global_container_stack.getProperty("prime_tower_enable", "value") == True: prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value") prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") - machine_width / 2 prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") + machine_depth / 2 - disallowed_areas.append([ + '''disallowed_areas.append([ + [prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size], + [prime_tower_x, prime_tower_y - prime_tower_size], + [prime_tower_x, prime_tower_y], + [prime_tower_x - prime_tower_size, prime_tower_y], + ])''' + + self._prime_tower_area = Polygon([ [prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size], [prime_tower_x, prime_tower_y - prime_tower_size], [prime_tower_x, prime_tower_y], @@ -344,6 +381,9 @@ class BuildVolume(SceneNode): areas.append(poly) + if self._prime_tower_area: + self._prime_tower_area = self._prime_tower_area.getMinkowskiHull(Polygon(approximatedCircleVertices(bed_adhesion_size))) + # Add the skirt areas around the borders of the build plate. if bed_adhesion_size > 0: half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2 @@ -377,6 +417,19 @@ class BuildVolume(SceneNode): [half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size] ], numpy.float32))) + # Check if the prime tower area intersects with any of the other areas. + # If this is the case, keep the polygon seperate, so it can be drawn in red. + # If not, add it back to disallowed area's, so it's rendered as normal. + collision = False + if self._prime_tower_area: + for area in areas: + if self._prime_tower_area.intersectsPolygon(area) is not None: + collision = True + break + if not collision: + areas.append(self._prime_tower_area) + self._prime_tower_area = None + self._has_errors = collision self._disallowed_areas = areas ## Convenience function to calculate the size of the bed adhesion in directions x, y. diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 892440cba0..7651cad930 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -322,6 +322,7 @@ class CuraApplication(QtApplication): path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name) if path: + instance.setPath(path) with SaveFile(path, "wt", -1, "utf-8") as f: f.write(data) @@ -346,6 +347,7 @@ class CuraApplication(QtApplication): elif stack_type == "extruder_train": path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name) if path: + stack.setPath(path) with SaveFile(path, "wt", -1, "utf-8") as f: f.write(data) @@ -693,17 +695,17 @@ class CuraApplication(QtApplication): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) - nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: + # Ensure that the object is above the build platform node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) op.addOperation(SetTransformOperation(node, Vector(0, node.getWorldPosition().y - node.getBoundingBox().bottom, 0))) op.push() - - ## Reset all transformations on nodes with mesh data. + + ## Reset all transformations on nodes with mesh data. @pyqtSlot() def resetAll(self): Logger.log("i", "Resetting all scene transformations") @@ -719,15 +721,17 @@ class CuraApplication(QtApplication): if nodes: op = GroupedOperation() - for node in nodes: # Ensure that the object is above the build platform node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) - - op.addOperation(SetTransformOperation(node, Vector(0, node.getMeshData().getCenterPosition().y, 0), Quaternion(), Vector(1, 1, 1))) - + center_y = 0 + if node.callDecoration("isGroup"): + center_y = node.getWorldPosition().y - node.getBoundingBox().bottom + else: + center_y = node.getMeshData().getCenterPosition().y + op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.push() - + ## Reload all mesh data on the screen from file. @pyqtSlot() def reloadAll(self): diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index c5eb4b699e..d3da01e590 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -16,7 +16,7 @@ class LayerPolygon: MoveRetractionType = 9 SupportInterfaceType = 10 - __jump_map = numpy.logical_or( numpy.arange(11) == NoneType, numpy.arange(11) >= MoveRetractionType ) + __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(11) == NoneType, numpy.arange(11) == MoveCombingType), numpy.arange(11) == MoveRetractionType) def __init__(self, mesh, extruder, line_types, data, line_widths): self._mesh = mesh @@ -42,7 +42,7 @@ class LayerPolygon: # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType # Should be generated in better way, not hardcoded. - self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0], dtype=numpy.bool) + self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool) self._build_cache_line_mesh_mask = None self._build_cache_needed_points = None diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index d718f25b87..2a5bd4091c 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -15,6 +15,9 @@ from cura.ConvexHullDecorator import ConvexHullDecorator from . import PlatformPhysicsOperation from . import ZOffsetDecorator +import random # used for list shuffling + + class PlatformPhysics: def __init__(self, controller, volume): super().__init__() @@ -48,7 +51,12 @@ class PlatformPhysics: # same direction. transformed_nodes = [] - for node in BreadthFirstIterator(root): + group_nodes = [] + # We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A. + # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve. + nodes = list(BreadthFirstIterator(root)) + random.shuffle(nodes) + for node in nodes: if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: continue @@ -69,6 +77,9 @@ class PlatformPhysics: if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: node._outside_buildarea = True + if node.callDecoration("isGroup"): + group_nodes.append(node) # Keep list of affected group_nodes + # Move it downwards if bottom is above platform move_vector = Vector() if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down @@ -102,7 +113,6 @@ class PlatformPhysics: continue # Other node is already moving, wait for next pass. overlap = (0, 0) # Start loop with no overlap - move_vector = move_vector.set(x=overlap[0] * self._move_factor, z=overlap[1] * self._move_factor) current_overlap_checks = 0 # Continue to check the overlap until we no longer find one. while overlap and current_overlap_checks < self._max_overlap_checks: @@ -144,7 +154,6 @@ class PlatformPhysics: overlap = convex_hull.intersectsPolygon(area) if overlap is None: continue - node._outside_buildarea = True if not Vector.Null.equals(move_vector, epsilon=1e-5): @@ -152,6 +161,12 @@ class PlatformPhysics: op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op.push() + # Group nodes should override the _outside_buildarea property of their children. + for group_node in group_nodes: + for child_node in group_node.getAllChildren(): + child_node._outside_buildarea = group_node._outside_buildarea + + def _onToolOperationStarted(self, tool): self._enabled = False diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 40e036eba5..27a9bd90dd 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -167,6 +167,19 @@ class ContainerManager(QObject): return True + @pyqtSlot(str, str, result=str) + def getContainerMetaDataEntry(self, container_id, entry_name): + containers = self._container_registry.findContainers(None, id=container_id) + if not containers: + UM.Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id) + return False + + result = containers[0].getMetaDataEntry(entry_name) + if result: + return result + else: + return "" + ## Set a metadata entry of the specified container. # # This will set the specified entry of the container's metadata to the specified @@ -254,6 +267,10 @@ class ContainerManager(QObject): return True return False + @pyqtSlot(str, result = str) + def makeUniqueName(self, original_name): + return self._container_registry.uniqueName(original_name) + ## Get a list of string that can be used as name filters for a Qt File Dialog # # This will go through the list of available container types and generate a list of strings @@ -573,6 +590,28 @@ class ContainerManager(QObject): return new_name + @pyqtSlot(str, result = str) + def duplicateMaterial(self, material_id): + containers = self._container_registry.findInstanceContainers(id=material_id) + if not containers: + UM.Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id) + return "" + + # Ensure all settings are saved. + UM.Application.getInstance().saveSettings() + + # Create a new ID & container to hold the data. + new_id = self._container_registry.uniqueName(material_id) + container_type = type(containers[0]) # Could be either a XMLMaterialProfile or a InstanceContainer + duplicated_container = container_type(new_id) + + # Instead of duplicating we load the data from the basefile again. + # This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile + # are also correctly created. + with open(containers[0].getPath(), encoding="utf-8") as f: + duplicated_container.deserialize(f.read()) + self._container_registry.addContainer(duplicated_container) + # Factory function, used by QML @staticmethod def createContainerManager(engine, js_engine): diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index ea3ca30118..89fa5e3e88 100644 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -71,7 +71,7 @@ class ThreeMFReader(MeshReader): rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0)) # TODO: We currently do not check for normals and simply recalculate them. - mesh_builder.calculateNormals() + mesh_builder.calculateNormals(flip = True) mesh_builder.setFileName(file_name) node.setMeshData(mesh_builder.build().getTransformed(rotation)) node.setSelectable(True) diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 4d40da899d..fc26bd086b 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -78,6 +78,10 @@ class StartSliceJob(Job): self.setResult(StartJobResult.SettingError) return + if Application.getInstance().getBuildVolume().hasErrors(): + self.setResult(StartJobResult.SettingError) + return + # Don't slice if there is a per object setting with an error value. for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is not SceneNode or not node.isSelectable(): diff --git a/plugins/CuraProfileReader/CuraProfileReader.py b/plugins/CuraProfileReader/CuraProfileReader.py index 772b11890b..2bccb8e3cb 100644 --- a/plugins/CuraProfileReader/CuraProfileReader.py +++ b/plugins/CuraProfileReader/CuraProfileReader.py @@ -1,8 +1,8 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2016 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. +import configparser -import os.path - +from UM import PluginRegistry from UM.Logger import Logger from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make. from cura.ProfileReader import ProfileReader @@ -27,22 +27,76 @@ class CuraProfileReader(ProfileReader): # returned. def read(self, file_name): try: - archive = zipfile.ZipFile(file_name, "r") - except Exception: - # zipfile doesn't give proper exceptions, so we can only catch broad ones + with zipfile.ZipFile(file_name, "r") as archive: + results = [] + for profile_id in archive.namelist(): + with archive.open(profile_id) as f: + serialized = f.read() + profile = self._loadProfile(serialized.decode("utf-8"), profile_id) + if profile is not None: + results.append(profile) + return results + + except zipfile.BadZipFile: + # It must be an older profile from Cura 2.1. + with open(file_name, encoding="utf-8") as fhandle: + serialized = fhandle.read() + return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized, file_name)] + + ## Convert a profile from an old Cura to this Cura if needed. + # + # \param serialized \type{str} The profile data to convert in the serialized on-disk format. + # \param profile_id \type{str} The name of the profile. + # \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names. + def _upgradeProfile(self, serialized, profile_id): + parser = configparser.ConfigParser(interpolation=None) + parser.read_string(serialized) + + if not "general" in parser: + Logger.log("w", "Missing required section 'general'.") + return None + if not "version" in parser["general"]: + Logger.log("w", "Missing required 'version' property") + return None + + version = int(parser["general"]["version"]) + if InstanceContainer.Version != version: + name = parser["general"]["name"] + return self._upgradeProfileVersion(serialized, name, version) + else: + return [(serialized, profile_id)] + + ## Load a profile from a serialized string. + # + # \param serialized \type{str} The profile data to read. + # \param profile_id \type{str} The name of the profile. + # \return \type{InstanceContainer|None} + def _loadProfile(self, serialized, profile_id): + # Create an empty profile. + profile = InstanceContainer(profile_id) + profile.addMetaDataEntry("type", "quality_changes") + try: + profile.deserialize(serialized) + except Exception as e: # Parsing error. This is not a (valid) Cura profile then. + Logger.log("e", "Error while trying to parse profile: %s", str(e)) + return None + return profile + + ## Upgrade a serialized profile to the current profile format. + # + # \param serialized \type{str} The profile data to convert. + # \param profile_id \type{str} The name of the profile. + # \param source_version \type{int} The profile version of 'serialized'. + # \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names. + def _upgradeProfileVersion(self, serialized, profile_id, source_version): + converter_plugins = PluginRegistry.getInstance().getAllMetaData(filter={"version_upgrade": {} }, active_only=True) + + source_format = ("profile", source_version) + profile_convert_funcs = [plugin["version_upgrade"][source_format][2] for plugin in converter_plugins + if source_format in plugin["version_upgrade"] and plugin["version_upgrade"][source_format][1] == InstanceContainer.Version] + + if not profile_convert_funcs: return [] - results = [] - for profile_id in archive.namelist(): - # Create an empty profile. - profile = InstanceContainer(profile_id) - profile.addMetaDataEntry("type", "quality_changes") - serialized = "" - with archive.open(profile_id) as f: - serialized = f.read() - try: - profile.deserialize(serialized.decode("utf-8") ) - except Exception as e: # Parsing error. This is not a (valid) Cura profile then. - Logger.log("e", "Error while trying to parse profile: %s", str(e)) - continue - results.append(profile) - return results \ No newline at end of file + + filenames, outputs = profile_convert_funcs[0](serialized, profile_id) + return list(zip(outputs, filenames)) diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index c15b079097..95d6659dd1 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -66,7 +66,10 @@ class GCodeWriter(MeshWriter): ## Create a new container with container 2 as base and container 1 written over it. def _createFlattenedContainerInstance(self, instance_container1, instance_container2): flat_container = InstanceContainer(instance_container2.getName()) - flat_container.setDefinition(instance_container2.getDefinition()) + if instance_container1.getDefinition(): + flat_container.setDefinition(instance_container1.getDefinition()) + else: + flat_container.setDefinition(instance_container2.getDefinition()) flat_container.setMetaData(instance_container2.getMetaData()) for key in instance_container2.getAllKeys(): diff --git a/plugins/LegacyProfileReader/DictionaryOfDoom.json b/plugins/LegacyProfileReader/DictionaryOfDoom.json index db0d26b8e4..15ef792f83 100644 --- a/plugins/LegacyProfileReader/DictionaryOfDoom.json +++ b/plugins/LegacyProfileReader/DictionaryOfDoom.json @@ -70,7 +70,8 @@ "magic_spiralize": "spiralize", "prime_tower_enable": "wipe_tower", "prime_tower_size": "math.sqrt(float(wipe_tower_volume) / float(layer_height))", - "ooze_shield_enabled": "ooze_shield" + "ooze_shield_enabled": "ooze_shield", + "skin_overlap": "fill_overlap" }, "defaults": { diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py index b4086291ca..25c2290b37 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py @@ -69,7 +69,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0] else: stack = UM.Application.getInstance().getGlobalContainerStack() - new_instance.setProperty("value", stack.getProperty(item, "value")) + new_instance.setProperty("value", stack.getRawProperty(item, "value")) new_instance.resetState() # Ensure that the state is not seen as a user state. settings.addInstance(new_instance) visibility_changed = True diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py index b5c4c0f22c..953f60a33d 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py @@ -7,6 +7,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Application import Application from UM.Preferences import Preferences from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator +from cura.Settings.ExtruderManager import ExtruderManager ## This tool allows the user to add & change settings per node in the scene. @@ -71,11 +72,20 @@ class PerObjectSettingsTool(Tool): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 + + # Ensure that all extruder data is reset if not self._multi_extrusion: - # Ensure that all extruder data is reset - root_node = Application.getInstance().getController().getScene().getRoot() - for node in DepthFirstIterator(root_node): - node.callDecoration("setActiveExtruder", global_container_stack.getId()) + 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() + + root_node = Application.getInstance().getController().getScene().getRoot() + for node in DepthFirstIterator(root_node): + node.callDecoration("setActiveExtruder", default_stack_id) + self._updateEnabled() def _updateEnabled(self): diff --git a/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py index 37f4422a11..90adac73b1 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py @@ -49,7 +49,7 @@ class RemovableDrivePlugin(OutputDevicePlugin): message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(device.getName())) message.show() else: - message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Maybe it is still in use?").format(device.getName())) + message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Another program may be using the drive.").format(device.getName())) message.show() return result diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index 047a03575d..117ac2a178 100644 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -65,7 +65,7 @@ class SliceInfo(Extension): Preferences.getInstance().addPreference("info/asked_send_slice_info", False) if not Preferences.getInstance().getValue("info/asked_send_slice_info"): - self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura automatically sends slice info. You can disable this in preferences"), lifetime = 0, dismissable = False) + self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. You can disable this in preferences"), lifetime = 0, dismissable = False) self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "") self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered) self.send_slice_info_message.show() diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 3b56ac1881..fd9f106334 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -13,6 +13,7 @@ from UM.Settings.Validator import ValidatorState from UM.View.GL.OpenGL import OpenGL import cura.Settings +from cura.Settings.ExtruderManager import ExtruderManager import math @@ -45,8 +46,18 @@ class SolidView(View): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: + multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 + + if multi_extrusion: + support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value") + support_angle_stack = ExtruderManager.getInstance().getExtruderStack(support_extruder_nr) + if not support_angle_stack: + support_angle_stack = global_container_stack + else: + support_angle_stack = global_container_stack + if Preferences.getInstance().getValue("view/show_overhang"): - angle = global_container_stack.getProperty("support_angle", "value") + angle = support_angle_stack.getProperty("support_angle", "value") # Make sure the overhang angle is valid before passing it to the shader # Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None) if angle is not None and global_container_stack.getProperty("support_angle", "validationState") in [None, ValidatorState.Valid]: @@ -56,7 +67,6 @@ class SolidView(View): else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) - multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 920a71d40f..4838fe9b96 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -140,7 +140,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # \param gcode_list List with gcode (strings). def printGCode(self, gcode_list): if self._progress or self._connection_state != ConnectionState.connected: - self._error_message = Message(catalog.i18nc("@info:status", "Printer is busy or not connected. Unable to start a new job.")) + self._error_message = Message(catalog.i18nc("@info:status", "Unable to start a new job because the printer is busy or not connected.")) self._error_message.show() Logger.log("d", "Printer is busy or not connected, aborting print") self.writeError.emit(self) diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 248649f431..4dec2e3a06 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -105,9 +105,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): @pyqtSlot(str) def updateAllFirmware(self, file_name): - file_name = file_name.replace("file://", "") # File dialogs prepend the path with file://, which we don't need / want + if file_name.startswith("file://"): + file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want if not self._usb_output_devices: - Message(i18n_catalog.i18nc("@info", "Cannot update firmware, there were no connected printers found.")).show() + Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected.")).show() return for printer_connection in self._usb_output_devices: diff --git a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py index 64c9ae1180..b92dc30c68 100644 --- a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py +++ b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py @@ -31,7 +31,7 @@ class UMOUpgradeSelection(MachineAction): if variant: if variant.getId() == "empty_variant": variant_index = global_container_stack.getContainerIndex(variant) - self._createVariant(global_container_stack, variant_index) + variant = self._createVariant(global_container_stack, variant_index) variant.setProperty("machine_heated_bed", "value", heated_bed) self.heatedBedChanged.emit() @@ -41,4 +41,5 @@ class UMOUpgradeSelection(MachineAction): new_variant.addMetaDataEntry("type", "variant") new_variant.setDefinition(global_container_stack.getBottom()) UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant) - global_container_stack.replaceContainer(variant_index, new_variant) \ No newline at end of file + global_container_stack.replaceContainer(variant_index, new_variant) + return new_variant \ No newline at end of file diff --git a/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py b/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py index 0c3a4d1055..c8ec559702 100644 --- a/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py +++ b/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py @@ -279,6 +279,8 @@ class VersionUpgrade21to22(VersionUpgrade): elif key in _setting_name_translations: del settings[key] settings[_setting_name_translations[key]] = value + if "infill_overlap" in settings: # New setting, added in 2.3 + settings["skin_overlap"] = settings["infill_overlap"] return settings ## Translates a setting name for the change from Cura 2.1 to 2.2. diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index a586f45b21..3568f9bc14 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -10,6 +10,7 @@ "manufacturer": "Ultimaker", "file_formats": "text/x-gcode;application/x-stl-ascii;application/x-stl-binary;application/x-wavefront-obj;application/x3g", "visible": false, + "has_materials": true, "preferred_material": "*generic_pla*", "preferred_quality": "*normal*", "machine_extruder_trains": @@ -403,7 +404,7 @@ "description": "The maximum speed of the filament.", "unit": "mm/s", "type": "float", - "default_value": 25, + "default_value": 299792458000, "settable_per_mesh": false, "settable_per_extruder": false, "settable_per_meshgroup": false @@ -1161,7 +1162,7 @@ "default_value": 25, "minimum_value": "0", "maximum_value": "machine_max_feedrate_e", - "maximum_value_warning": "100", + "maximum_value_warning": "25", "enabled": "retraction_enable", "settable_per_mesh": false, "settable_per_extruder": true, @@ -1174,7 +1175,7 @@ "default_value": 25, "minimum_value": "0", "maximum_value": "machine_max_feedrate_e", - "maximum_value_warning": "100", + "maximum_value_warning": "25", "enabled": "retraction_enable", "value": "retraction_speed", "settable_per_mesh": false, @@ -1188,7 +1189,7 @@ "default_value": 25, "minimum_value": "0", "maximum_value": "machine_max_feedrate_e", - "maximum_value_warning": "100", + "maximum_value_warning": "25", "enabled": "retraction_enable", "value": "retraction_speed", "settable_per_mesh": false, @@ -3207,6 +3208,7 @@ "type": "category", "icon": "category_dual", "description": "Settings used for printing with multiple extruders.", + "enabled": "machine_extruder_count > 1", "children": { "adhesion_extruder_nr": @@ -3284,6 +3286,7 @@ "default_value": 15, "value": "15 if prime_tower_enable else 0", "minimum_value": "0", + "maximum_value": "min(0.5 * machine_width, 0.5 * machine_depth)", "maximum_value_warning": "20", "settable_per_mesh": false, "settable_per_extruder": false diff --git a/resources/definitions/ultimaker.def.json b/resources/definitions/ultimaker.def.json index dc52b00dcc..0a3729c766 100644 --- a/resources/definitions/ultimaker.def.json +++ b/resources/definitions/ultimaker.def.json @@ -9,6 +9,9 @@ "visible": false }, "overrides": { + "machine_max_feedrate_e": { + "default_value": 45 + }, "material_print_temperature": { "minimum_value": "0" }, diff --git a/resources/definitions/ultimaker2.def.json b/resources/definitions/ultimaker2.def.json index 81723ae10d..a0ead36bff 100644 --- a/resources/definitions/ultimaker2.def.json +++ b/resources/definitions/ultimaker2.def.json @@ -14,6 +14,7 @@ "platform": "ultimaker2_platform.obj", "platform_texture": "Ultimaker2backplate.png", "platform_offset": [9, 0, 0], + "has_materials": false, "supported_actions":["UpgradeFirmware"] }, "overrides": { diff --git a/resources/definitions/ultimaker_original_dual.def.json b/resources/definitions/ultimaker_original_dual.def.json index 6feaa07470..8d2d5d85d6 100644 --- a/resources/definitions/ultimaker_original_dual.def.json +++ b/resources/definitions/ultimaker_original_dual.def.json @@ -78,7 +78,7 @@ "default_value": 185 }, "prime_tower_position_y": { - "default_value": 175 + "default_value": 160 } } } diff --git a/resources/materials/generic_nylon.xml.fdm_material b/resources/materials/generic_nylon.xml.fdm_material index d7391e2984..02b2d5414b 100644 --- a/resources/materials/generic_nylon.xml.fdm_material +++ b/resources/materials/generic_nylon.xml.fdm_material @@ -14,11 +14,10 @@ Generic Nylon profile. Serves as an example file, data in this file is not corre #3DF266 - 1.19 + 1.14 2.85 - no 250 60 175 diff --git a/resources/materials/generic_pc.xml.fdm_material b/resources/materials/generic_pc.xml.fdm_material index 292b3fd098..77d7e60bf5 100644 --- a/resources/materials/generic_pc.xml.fdm_material +++ b/resources/materials/generic_pc.xml.fdm_material @@ -14,7 +14,7 @@ Generic PC profile. Serves as an example file, data in this file is not correct. #F29030 - 1.18 + 1.19 2.85 diff --git a/resources/materials/generic_tpu.xml.fdm_material b/resources/materials/generic_tpu.xml.fdm_material index 455e7b89be..5a0c793cda 100644 --- a/resources/materials/generic_tpu.xml.fdm_material +++ b/resources/materials/generic_tpu.xml.fdm_material @@ -14,7 +14,7 @@ Generic TPU 95A profile. Serves as an example file, data in this file is not cor #B22744 - 1.19 + 1.22 2.85 diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 42c2ad9cf4..005a0892a3 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -464,12 +464,11 @@ UM.MainWindow target: Cura.Actions.addProfile onTriggered: { - Cura.ContainerManager.createQualityChanges(null); preferences.setPage(4); preferences.show(); - // Show the renameDialog after a very short delay so the preference page has time to initiate - showProfileNameDialogTimer.start(); + // Create a new profile after a very short delay so the preference page has time to initiate + createProfileTimer.start(); } } @@ -516,11 +515,11 @@ UM.MainWindow Timer { - id: showProfileNameDialogTimer + id: createProfileTimer repeat: false interval: 1 - onTriggered: preferences.getCurrentItem().showProfileNameDialog() + onTriggered: preferences.getCurrentItem().createProfile() } // BlurSettings is a way to force the focus away from any of the setting items. diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index 3041115ad2..6115671563 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -115,7 +115,7 @@ TabView width: base.secondColumnWidth; value: properties.density; decimals: 2 - suffix: "g/cm" + suffix: "g/cm³" stepSize: 0.01 readOnly: !base.editingEnabled; @@ -128,7 +128,7 @@ TabView width: base.secondColumnWidth; value: properties.diameter; decimals: 2 - suffix: "mm³" + suffix: "mm" stepSize: 0.01 readOnly: !base.editingEnabled; diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index d2d7c1b0a2..b1f0afe52f 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -129,30 +129,24 @@ UM.ManagementPage enabled: base.currentItem != null && base.currentItem.id != Cura.MachineManager.activeMaterialId onClicked: Cura.MachineManager.setActiveMaterial(base.currentItem.id) }, - /* // apparently visible does not work on OS X - Button + // apparently visible does not work on OS X + /*Button { text: catalog.i18nc("@action:button", "Duplicate"); iconName: "list-add"; enabled: base.currentItem != null onClicked: { - var material_id = Cura.ContainerManager.duplicateContainer(base.currentItem.id) + var base_file = Cura.ContainerManager.getContainerMetaDataEntry(base.currentItem.id, "base_file") + // We need to copy the base container instead of the specific variant. + var material_id = base_file == "" ? Cura.ContainerManager.duplicateMaterial(base.currentItem.id): Cura.ContainerManager.duplicateMaterial(base_file) if(material_id == "") { return } - if(Cura.MachineManager.filterQualityByMachine) - { - var quality_id = Cura.ContainerManager.duplicateContainer(Cura.MachineManager.activeQualityId) - Cura.ContainerManager.setContainerMetaDataEntry(quality_id, "material", material_id) - Cura.MachineManager.setActiveQuality(quality_id) - } - Cura.MachineManager.setActiveMaterial(material_id) } - visible: false; }, */ Button diff --git a/resources/qml/Preferences/ProfilesPage.qml b/resources/qml/Preferences/ProfilesPage.qml index 56d8bd41a4..4e872bdc2b 100644 --- a/resources/qml/Preferences/ProfilesPage.qml +++ b/resources/qml/Preferences/ProfilesPage.qml @@ -84,7 +84,7 @@ UM.ManagementPage onClicked: { - newNameDialog.object = base.currentItem != null ? base.currentItem.name : ""; + newNameDialog.object = base.currentItem != null ? Cura.ContainerManager.makeUniqueName(base.currentItem.name) : ""; newNameDialog.open(); newNameDialog.selectText(); } @@ -100,7 +100,7 @@ UM.ManagementPage onClicked: { - newDuplicateNameDialog.object = base.currentItem.name; + newDuplicateNameDialog.object = Cura.ContainerManager.makeUniqueName(base.currentItem.name); newDuplicateNameDialog.open(); newDuplicateNameDialog.selectText(); } @@ -141,11 +141,12 @@ UM.ManagementPage scrollviewCaption: catalog.i18nc("@label %1 is printer name","Printer: %1").arg(Cura.MachineManager.activeMachineName) - signal showProfileNameDialog() - onShowProfileNameDialog: + signal createProfile() + onCreateProfile: { - renameDialog.open(); - renameDialog.selectText(); + newNameDialog.object = base.currentItem != null ? Cura.ContainerManager.makeUniqueName(base.currentItem.name) : ""; + newNameDialog.open(); + newNameDialog.selectText(); } signal selectContainer(string name) @@ -267,6 +268,7 @@ UM.ManagementPage UM.RenameDialog { + title: catalog.i18nc("@title:window", "Rename Profile") id: renameDialog; object: base.currentItem != null ? base.currentItem.name : "" onAccepted: @@ -279,6 +281,7 @@ UM.ManagementPage // Dialog to request a name when creating a new profile UM.RenameDialog { + title: catalog.i18nc("@title:window", "Create Profile") id: newNameDialog; object: ""; onAccepted: @@ -292,6 +295,7 @@ UM.ManagementPage // Dialog to request a name when duplicating a new profile UM.RenameDialog { + title: catalog.i18nc("@title:window", "Duplicate Profile") id: newDuplicateNameDialog; object: ""; onAccepted: diff --git a/resources/qml/Settings/SettingItem.qml b/resources/qml/Settings/SettingItem.qml index ed16b722dd..cdc557a089 100644 --- a/resources/qml/Settings/SettingItem.qml +++ b/resources/qml/Settings/SettingItem.qml @@ -138,7 +138,7 @@ Item { { id: linkedSettingIcon; - visible: Cura.MachineManager.activeStackId != Cura.MachineManager.activeMachineId && !definition.settable_per_extruder && base.showLinkedSettingIcon + visible: Cura.MachineManager.activeStackId != Cura.MachineManager.activeMachineId && (!definition.settable_per_extruder || definition.global_inherits_stack != "-1") && base.showLinkedSettingIcon height: parent.height; width: height; diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 7f8c1488ae..c50c2cc4e0 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -40,7 +40,7 @@ ScrollView id: delegate width: UM.Theme.getSize("sidebar").width; - height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : 0 + height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : - contents.spacing Behavior on height { NumberAnimation { duration: 100 } } opacity: provider.properties.enabled == "True" ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 100 } } diff --git a/resources/themes/cura/styles.qml b/resources/themes/cura/styles.qml index 3cfb4514ee..91a512b6db 100644 --- a/resources/themes/cura/styles.qml +++ b/resources/themes/cura/styles.qml @@ -199,18 +199,32 @@ QtObject { property Component progressbar: Component{ ProgressBarStyle { - background:Rectangle { + background: Rectangle { implicitWidth: Theme.getSize("message").width - (Theme.getSize("default_margin").width * 2) implicitHeight: Theme.getSize("progressbar").height radius: Theme.getSize("progressbar_radius").width - color: Theme.getColor("progressbar_background") + color: control.hasOwnProperty("backgroundColor") ? control.backgroundColor : Theme.getColor("progressbar_background") } progress: Rectangle { - color: control.indeterminate ? "transparent" : Theme.getColor("progressbar_control") + color: + { + if(control.indeterminate) + { + return "transparent"; + } + else if(control.hasOwnProperty("controlColor")) + { + return control.controlColor; + } + else + { + return Theme.getColor("progressbar_control"); + } + } radius: Theme.getSize("progressbar_radius").width Rectangle{ radius: Theme.getSize("progressbar_radius").width - color: Theme.getColor("progressbar_control") + color: control.hasOwnProperty("controlColor") ? control.controlColor : Theme.getColor("progressbar_control") width: Theme.getSize("progressbar_control").width height: Theme.getSize("progressbar_control").height visible: control.indeterminate diff --git a/resources/themes/cura/theme.json b/resources/themes/cura/theme.json index 69fc2c2c71..f90467c9e8 100644 --- a/resources/themes/cura/theme.json +++ b/resources/themes/cura/theme.json @@ -159,9 +159,17 @@ "tooltip": [12, 169, 227, 255], "tooltip_text": [255, 255, 255, 255], - "message_background": [255, 255, 255, 255], - "message_text": [32, 166, 219, 255], - "message_dismiss": [127, 127, 127, 255], + "message_background": [24, 41, 77, 255], + "message_text": [255, 255, 255, 255], + "message_border": [24, 41, 77, 255], + "message_button": [255, 255, 255, 255], + "message_button_hover": [12, 169, 227, 255], + "message_button_active": [32, 166, 219, 255], + "message_button_text": [24, 41, 77, 255], + "message_button_text_hover": [255, 255, 255, 255], + "message_button_text_active": [255, 255, 255, 255], + "message_progressbar_background": [255, 255, 255, 255], + "message_progressbar_control": [12, 169, 227, 255], "tool_panel_background": [255, 255, 255, 255],