Browse Source

Merge branch 'master' of https://github.com/maukcc/Cura

resources/quality/ultimaker3/um3_aa0.8_TPU_Not_Supported_Quality.inst.cfg
resources/quality/ultimaker3/um3_aa0.8_TPU_Not_Supported_Superdraft_Quality.inst.cfg
maukcc 7 years ago
parent
commit
d57701c7ef

+ 10 - 8
cura/BuildVolume.py

@@ -433,7 +433,8 @@ class BuildVolume(SceneNode):
                 self._global_container_stack.getProperty("raft_interface_thickness", "value") +
                 self._global_container_stack.getProperty("raft_surface_layers", "value") *
                     self._global_container_stack.getProperty("raft_surface_thickness", "value") +
-                self._global_container_stack.getProperty("raft_airgap", "value"))
+                self._global_container_stack.getProperty("raft_airgap", "value") -
+                self._global_container_stack.getProperty("layer_0_z_overlap", "value"))
 
         # Rounding errors do not matter, we check if raft_thickness has changed at all
         if old_raft_thickness != self._raft_thickness:
@@ -562,7 +563,7 @@ class BuildVolume(SceneNode):
                 used_extruders = [self._global_container_stack]
 
         result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
-        prime_areas = self._computeDisallowedAreasPrime(disallowed_border_size, used_extruders)
+        prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
         prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
 
         #Check if prime positions intersect with disallowed areas.
@@ -658,7 +659,7 @@ class BuildVolume(SceneNode):
 
         return result
 
-    ##  Computes the disallowed areas for the prime locations.
+    ##  Computes the disallowed areas for the prime blobs.
     #
     #   These are special because they are not subject to things like brim or
     #   travel avoidance. They do get a dilute with the border size though
@@ -669,17 +670,18 @@ class BuildVolume(SceneNode):
     #   \param used_extruders The extruder stacks to generate disallowed areas
     #   for.
     #   \return A dictionary with for each used extruder ID the prime areas.
-    def _computeDisallowedAreasPrime(self, border_size, used_extruders):
+    def _computeDisallowedAreasPrimeBlob(self, border_size, used_extruders):
         result = {}
 
         machine_width = self._global_container_stack.getProperty("machine_width", "value")
         machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
         for extruder in used_extruders:
+            prime_blob_enabled = extruder.getProperty("prime_blob_enable", "value")
             prime_x = extruder.getProperty("extruder_prime_pos_x", "value")
             prime_y = - extruder.getProperty("extruder_prime_pos_y", "value")
 
-            #Ignore extruder prime position if it is not set
-            if prime_x == 0 and prime_y == 0:
+            #Ignore extruder prime position if it is not set or if blob is disabled
+            if (prime_x == 0 and prime_y == 0) or not prime_blob_enabled:
                 result[extruder.getId()] = []
                 continue
 
@@ -950,9 +952,9 @@ class BuildVolume(SceneNode):
         return max(min(value, max_value), min_value)
 
     _skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist"]
-    _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap"]
+    _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
     _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
-    _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z"]
+    _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
     _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
     _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
     _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]

+ 1 - 2
cura/ConvexHullDecorator.py

@@ -328,8 +328,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
         return self.__isDescendant(root, node.getParent())
 
     _affected_settings = [
-        "adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers",
-        "raft_surface_thickness", "raft_airgap", "raft_margin", "print_sequence",
+        "adhesion_type", "raft_margin", "print_sequence",
         "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]
 
     ##  Settings that change the convex hull.

+ 10 - 3
cura/CuraApplication.py

@@ -99,6 +99,11 @@ if not MYPY:
 
 
 class CuraApplication(QtApplication):
+    # SettingVersion represents the set of settings available in the machine/extruder definitions.
+    # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
+    # changes of the settings.
+    SettingVersion = 1
+
     class ResourceTypes:
         QmlFiles = Resources.UserType + 1
         Firmware = Resources.UserType + 2
@@ -169,11 +174,11 @@ class CuraApplication(QtApplication):
 
         UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
             {
-                ("quality", InstanceContainer.Version):    (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
+                ("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion):    (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
                 ("machine_stack", ContainerStack.Version): (self.ResourceTypes.MachineStack, "application/x-uranium-containerstack"),
                 ("extruder_train", ContainerStack.Version): (self.ResourceTypes.ExtruderStack, "application/x-uranium-extruderstack"),
                 ("preferences", Preferences.Version):               (Resources.Preferences, "application/x-uranium-preferences"),
-                ("user", InstanceContainer.Version):       (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer")
+                ("user", InstanceContainer.Version * 1000000 + self.SettingVersion):       (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer")
             }
         )
 
@@ -620,7 +625,9 @@ class CuraApplication(QtApplication):
         camera.lookAt(Vector(0, 0, 0))
         controller.getScene().setActiveCamera("3d")
 
-        self.getController().getTool("CameraTool").setOrigin(Vector(0, 100, 0))
+        camera_tool = self.getController().getTool("CameraTool")
+        camera_tool.setOrigin(Vector(0, 100, 0))
+        camera_tool.setZoomRange(0.1, 200000)
 
         self._camera_animation = CameraAnimation.CameraAnimation()
         self._camera_animation.setCameraTool(self.getController().getTool("CameraTool"))

+ 2 - 2
cura/PrintInformation.py

@@ -31,8 +31,8 @@ catalog = i18nCatalog("cura")
 #   - This triggers a new slice with the current settings - this is the "current settings pass".
 #   - When the slice is done, we update the current print time and material amount.
 #   - If the source of the slice was not a Setting change, we start the second slice pass, the "low quality settings pass". Otherwise we stop here.
-#   - When that is done, we update the minimum print time and start the final slice pass, the "high quality settings pass".
-#   - When the high quality pass is done, we update the maximum print time.
+#   - When that is done, we update the minimum print time and start the final slice pass, the "Extra Fine settings pass".
+#   - When the Extra Fine pass is done, we update the maximum print time.
 #
 #   This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
 #   This job name is requested by the JobSpecs qml file.

+ 38 - 36
cura/QualityManager.py

@@ -3,7 +3,7 @@
 
 # This collects a lot of quality and quality changes related code which was split between ContainerManager
 # and the MachineManager and really needs to usable from both.
-from typing import List
+from typing import List, Optional, Dict, TYPE_CHECKING
 
 from UM.Application import Application
 from UM.Settings.ContainerRegistry import ContainerRegistry
@@ -11,6 +11,10 @@ from UM.Settings.DefinitionContainer import DefinitionContainer
 from UM.Settings.InstanceContainer import InstanceContainer
 from cura.Settings.ExtruderManager import ExtruderManager
 
+if TYPE_CHECKING:
+    from cura.Settings.GlobalStack import GlobalStack
+    from cura.Settings.ExtruderStack import ExtruderStack
+    from UM.Settings.DefinitionContainer import DefinitionContainerInterface
 
 class QualityManager:
 
@@ -27,12 +31,12 @@ class QualityManager:
     ##  Find a quality by name for a specific machine definition and materials.
     #
     #   \param quality_name
-    #   \param machine_definition (Optional) \type{ContainerInstance} If nothing is
+    #   \param machine_definition (Optional) \type{DefinitionContainerInterface} If nothing is
     #                               specified then the currently selected machine definition is used.
-    #   \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
+    #   \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
     #                               the current set of selected materials is used.
-    #   \return the matching quality container \type{ContainerInstance}
-    def findQualityByName(self, quality_name, machine_definition=None, material_containers=None):
+    #   \return the matching quality container \type{InstanceContainer}
+    def findQualityByName(self, quality_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None) -> Optional[InstanceContainer]:
         criteria = {"type": "quality", "name": quality_name}
         result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
 
@@ -46,12 +50,10 @@ class QualityManager:
     ##  Find a quality changes container by name.
     #
     #   \param quality_changes_name \type{str} the name of the quality changes container.
-    #   \param machine_definition (Optional) \type{ContainerInstance} If nothing is
-    #                               specified then the currently selected machine definition is used.
-    #   \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
-    #                               the current set of selected materials is used.
-    #   \return the matching quality changes containers \type{List[ContainerInstance]}
-    def findQualityChangesByName(self, quality_changes_name, machine_definition=None):
+    #   \param machine_definition (Optional) \type{DefinitionContainer} If nothing is
+    #                               specified then the currently selected machine definition is used..
+    #   \return the matching quality changes containers \type{List[InstanceContainer]}
+    def findQualityChangesByName(self, quality_changes_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None):
         criteria = {"type": "quality_changes", "name": quality_changes_name}
         result = self._getFilteredContainersForStack(machine_definition, [], **criteria)
 
@@ -62,7 +64,7 @@ class QualityManager:
     #   \param machine_definition \type{DefinitionContainer}
     #   \param material_containers \type{List[InstanceContainer]}
     #   \return \type{List[str]}
-    def findAllQualityTypesForMachineAndMaterials(self, machine_definition, material_containers):
+    def findAllQualityTypesForMachineAndMaterials(self, machine_definition: "DefinitionContainerInterface", material_containers: List[InstanceContainer]) -> List[str]:
         # Determine the common set of quality types which can be
         # applied to all of the materials for this machine.
         quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_containers[0])
@@ -76,9 +78,9 @@ class QualityManager:
     ##  Fetches a dict of quality types names to quality profiles for a combination of machine and material.
     #
     #   \param machine_definition \type{DefinitionContainer} the machine definition.
-    #   \param material \type{ContainerInstance} the material.
-    #   \return \type{Dict[str, ContainerInstance]} the dict of suitable quality type names mapping to qualities.
-    def __fetchQualityTypeDictForMaterial(self, machine_definition, material):
+    #   \param material \type{InstanceContainer} the material.
+    #   \return \type{Dict[str, InstanceContainer]} the dict of suitable quality type names mapping to qualities.
+    def __fetchQualityTypeDictForMaterial(self, machine_definition: "DefinitionContainerInterface", material: InstanceContainer) -> Dict[str, InstanceContainer]:
         qualities = self.findAllQualitiesForMachineMaterial(machine_definition, material)
         quality_type_dict = {}
         for quality in qualities:
@@ -88,12 +90,12 @@ class QualityManager:
     ##  Find a quality container by quality type.
     #
     #   \param quality_type \type{str} the name of the quality type to search for.
-    #   \param machine_definition (Optional) \type{ContainerInstance} If nothing is
+    #   \param machine_definition (Optional) \type{InstanceContainer} If nothing is
     #                               specified then the currently selected machine definition is used.
-    #   \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
+    #   \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
     #                               the current set of selected materials is used.
-    #   \return the matching quality container \type{ContainerInstance}
-    def findQualityByQualityType(self, quality_type, machine_definition=None, material_containers=None, **kwargs):
+    #   \return the matching quality container \type{InstanceContainer}
+    def findQualityByQualityType(self, quality_type: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None, **kwargs) -> InstanceContainer:
         criteria = kwargs
         criteria["type"] = "quality"
         if quality_type:
@@ -110,9 +112,9 @@ class QualityManager:
     ##  Find all suitable qualities for a combination of machine and material.
     #
     #   \param machine_definition \type{DefinitionContainer} the machine definition.
-    #   \param material_container \type{ContainerInstance} the material.
-    #   \return \type{List[ContainerInstance]} the list of suitable qualities.
-    def findAllQualitiesForMachineMaterial(self, machine_definition, material_container):
+    #   \param material_container \type{InstanceContainer} the material.
+    #   \return \type{List[InstanceContainer]} the list of suitable qualities.
+    def findAllQualitiesForMachineMaterial(self, machine_definition: "DefinitionContainerInterface", material_container: InstanceContainer) -> List[InstanceContainer]:
         criteria = {"type": "quality" }
         result = self._getFilteredContainersForStack(machine_definition, [material_container], **criteria)
         if not result:
@@ -125,7 +127,7 @@ class QualityManager:
     #
     #   \param machine_definition \type{DefinitionContainer} the machine definition.
     #   \return \type{List[InstanceContainer]} the list of quality changes
-    def findAllQualityChangesForMachine(self, machine_definition: DefinitionContainer) -> List[InstanceContainer]:
+    def findAllQualityChangesForMachine(self, machine_definition: "DefinitionContainerInterface") -> List[InstanceContainer]:
         if machine_definition.getMetaDataEntry("has_machine_quality"):
             definition_id = machine_definition.getId()
         else:
@@ -141,19 +143,19 @@ class QualityManager:
     #   Only one quality per quality type is returned. i.e. if there are 2 qualities with quality_type=normal
     #   then only one of then is returned (at random).
     #
-    #   \param global_container_stack \type{ContainerStack} the global machine definition
-    #   \param extruder_stacks \type{List[ContainerStack]} the list of extruder stacks
+    #   \param global_container_stack \type{GlobalStack} the global machine definition
+    #   \param extruder_stacks \type{List[ExtruderStack]} the list of extruder stacks
     #   \return \type{List[InstanceContainer]} the list of the matching qualities. The quality profiles
     #       return come from the first extruder in the given list of extruders.
-    def findAllUsableQualitiesForMachineAndExtruders(self, global_container_stack, extruder_stacks):
+    def findAllUsableQualitiesForMachineAndExtruders(self, global_container_stack: "GlobalStack", extruder_stacks: List["ExtruderStack"]) -> List[InstanceContainer]:
         global_machine_definition = global_container_stack.getBottom()
 
         if extruder_stacks:
             # Multi-extruder machine detected.
-            materials = [stack.findContainer(type="material") for stack in extruder_stacks]
+            materials = [stack.material for stack in extruder_stacks]
         else:
             # Machine with one extruder.
-            materials = [global_container_stack.findContainer(type="material")]
+            materials = [global_container_stack.material]
 
         quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials)
 
@@ -170,7 +172,7 @@ class QualityManager:
     #   This tries to find a generic or basic version of the given material.
     #   \param material_container \type{InstanceContainer} the material
     #   \return \type{List[InstanceContainer]} a list of the basic materials or an empty list if one could not be found.
-    def _getBasicMaterials(self, material_container):
+    def _getBasicMaterials(self, material_container: InstanceContainer):
         base_material = material_container.getMetaDataEntry("material")
         material_container_definition = material_container.getDefinition()
         if material_container_definition and material_container_definition.getMetaDataEntry("has_machine_quality"):
@@ -192,7 +194,7 @@ class QualityManager:
     def _getFilteredContainers(self, **kwargs):
         return self._getFilteredContainersForStack(None, None, **kwargs)
 
-    def _getFilteredContainersForStack(self, machine_definition=None, material_containers=None, **kwargs):
+    def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_containers: List[InstanceContainer] = None, **kwargs):
         # Fill in any default values.
         if machine_definition is None:
             machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
@@ -202,7 +204,8 @@ class QualityManager:
 
         if material_containers is None:
             active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
-            material_containers = [stack.findContainer(type="material") for stack in active_stacks]
+            if active_stacks:
+                material_containers = [stack.material for stack in active_stacks]
 
         criteria = kwargs
         filter_by_material = False
@@ -216,12 +219,11 @@ class QualityManager:
             filter_by_material = whole_machine_definition.getMetaDataEntry("has_materials")
         else:
             criteria["definition"] = "fdmprinter"
-
+        material_ids = set()
         # Stick the material IDs in a set
         if material_containers is None or len(material_containers) == 0:
             filter_by_material = False
         else:
-            material_ids = set()
             for material_instance in material_containers:
                 if material_instance is not None:
                     # Add the parent material too.
@@ -245,7 +247,7 @@ class QualityManager:
     #               an extruder definition.
     #    \return  \type{DefinitionContainer} the parent machine definition. If the given machine
     #               definition doesn't have a parent then it is simply returned.
-    def getParentMachineDefinition(self, machine_definition: DefinitionContainer) -> DefinitionContainer:
+    def getParentMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
         container_registry = ContainerRegistry.getInstance()
 
         machine_entry = machine_definition.getMetaDataEntry("machine")
@@ -274,8 +276,8 @@ class QualityManager:
     #
     #    \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
     #               an extruder definition.
-    #    \return \type{DefinitionContainer}
-    def getWholeMachineDefinition(self, machine_definition):
+    #    \return \type{DefinitionContainerInterface}
+    def getWholeMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
         machine_entry = machine_definition.getMetaDataEntry("machine")
         if machine_entry is None:
             # This already is a 'global' machine definition.

+ 117 - 1
cura/Settings/ContainerManager.py

@@ -1,13 +1,15 @@
-# Copyright (c) 2016 Ultimaker B.V.
+# Copyright (c) 2017 Ultimaker B.V.
 # Cura is released under the terms of the AGPLv3 or higher.
 
 import os.path
 import urllib
+import uuid
 from typing import Dict, Union
 
 from PyQt5.QtCore import QObject, QUrl, QVariant
 from UM.FlameProfiler import pyqtSlot
 from PyQt5.QtWidgets import QMessageBox
+from UM.Util import parseBool
 
 from UM.PluginRegistry import PluginRegistry
 from UM.SaveFile import SaveFile
@@ -671,6 +673,9 @@ class ContainerManager(QObject):
 
         return new_change_instances
 
+    ##  Create a duplicate of a material, which has the same GUID and base_file metadata
+    #
+    #   \return \type{str} the id of the newly created container.
     @pyqtSlot(str, result = str)
     def duplicateMaterial(self, material_id: str) -> str:
         containers = self._container_registry.findInstanceContainers(id=material_id)
@@ -693,6 +698,115 @@ class ContainerManager(QObject):
             duplicated_container.deserialize(f.read())
         duplicated_container.setDirty(True)
         self._container_registry.addContainer(duplicated_container)
+        return self._getMaterialContainerIdForActiveMachine(new_id)
+
+    ##  Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue
+    #
+    #   \return \type{str} the id of the newly created container.
+    @pyqtSlot(result = str)
+    def createMaterial(self) -> str:
+        # Ensure all settings are saved.
+        Application.getInstance().saveSettings()
+
+        global_stack = Application.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            return ""
+
+        approximate_diameter = round(global_stack.getProperty("material_diameter", "value"))
+        containers = self._container_registry.findInstanceContainers(id = "generic_pla*", approximate_diameter = approximate_diameter)
+        if not containers:
+            Logger.log("d", "Unable to create a new material by cloning Generic PLA, because it cannot be found for the material diameter for this machine.")
+            return ""
+
+        base_file = containers[0].getMetaDataEntry("base_file")
+        containers = self._container_registry.findInstanceContainers(id = base_file)
+        if not containers:
+            Logger.log("d", "Unable to create a new material by cloning Generic PLA, because the base file for Generic PLA for this machine can not be found.")
+            return ""
+
+        # Create a new ID & container to hold the data.
+        new_id = self._container_registry.uniqueName("custom_material")
+        container_type = type(containers[0])  # Always XMLMaterialProfile, since we specifically clone the base_file
+        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())
+
+        duplicated_container.setMetaDataEntry("GUID", str(uuid.uuid4()))
+        duplicated_container.setMetaDataEntry("brand", catalog.i18nc("@label", "Custom"))
+        duplicated_container.setMetaDataEntry("material", catalog.i18nc("@label", "Custom"))
+        duplicated_container.setName(catalog.i18nc("@label", "Custom Material"))
+
+        self._container_registry.addContainer(duplicated_container)
+        return self._getMaterialContainerIdForActiveMachine(new_id)
+
+    ##  Find the id of a material container based on the new material
+    #   Utilty function that is shared between duplicateMaterial and createMaterial
+    #
+    #   \param base_file \type{str} the id of the created container.
+    def _getMaterialContainerIdForActiveMachine(self, base_file):
+        global_stack = Application.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            return base_file
+
+        has_machine_materials = parseBool(global_stack.getMetaDataEntry("has_machine_materials", default = False))
+        has_variant_materials = parseBool(global_stack.getMetaDataEntry("has_variant_materials", default = False))
+        has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False))
+        if has_machine_materials or has_variant_materials:
+            if has_variants:
+                materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId)
+            else:
+                materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId())
+
+            if materials:
+                return materials[0].getId()
+
+            Logger.log("w", "Unable to find a suitable container based on %s for the current machine .", base_file)
+            return "" # do not activate a new material if a container can not be found
+
+        return base_file
+
+    ##  Get a list of materials that have the same GUID as the reference material
+    #
+    #   \param material_id \type{str} the id of the material for which to get the linked materials.
+    #   \return \type{list} a list of names of materials with the same GUID
+    @pyqtSlot(str, result = "QStringList")
+    def getLinkedMaterials(self, material_id: str):
+        containers = self._container_registry.findInstanceContainers(id=material_id)
+        if not containers:
+            Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't exist.", material_id)
+            return []
+
+        material_container = containers[0]
+        material_base_file = material_container.getMetaDataEntry("base_file", "")
+        material_guid = material_container.getMetaDataEntry("GUID", "")
+        if not material_guid:
+            Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't have a GUID.", material_id)
+            return []
+
+        containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_guid)
+        linked_material_names = []
+        for container in containers:
+            if container.getId() in [material_id, material_base_file] or container.getMetaDataEntry("base_file") != container.getId():
+                continue
+
+            linked_material_names.append(container.getName())
+        return linked_material_names
+
+    ##  Unlink a material from all other materials by creating a new GUID
+    #   \param material_id \type{str} the id of the material to create a new GUID for.
+    @pyqtSlot(str)
+    def unlinkMaterial(self, material_id: str):
+        containers = self._container_registry.findInstanceContainers(id=material_id)
+        if not containers:
+            Logger.log("d", "Unable to make the material with id %s unique, because it doesn't exist.", material_id)
+            return ""
+
+        containers[0].setMetaDataEntry("GUID", str(uuid.uuid4()))
+
 
     ##  Get the singleton instance for this class.
     @classmethod
@@ -815,6 +929,8 @@ class ContainerManager(QObject):
             quality_changes.setDefinition(self._container_registry.findContainers(id = "fdmprinter")[0])
         else:
             quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition))
+        from cura.CuraApplication import CuraApplication
+        quality_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
         return quality_changes
 
 

+ 3 - 1
cura/Settings/CuraContainerRegistry.py

@@ -22,6 +22,8 @@ from . import GlobalStack
 from .ContainerManager import ContainerManager
 from .ExtruderManager import ExtruderManager
 
+from cura.CuraApplication import CuraApplication
+
 from UM.i18n import i18nCatalog
 catalog = i18nCatalog("cura")
 
@@ -43,7 +45,7 @@ class CuraContainerRegistry(ContainerRegistry):
 
         if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
             #Check against setting version of the definition.
-            required_setting_version = int(container.getDefinition().getMetaDataEntry("setting_version"))
+            required_setting_version = CuraApplication.SettingVersion
             actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
             if required_setting_version != actual_setting_version:
                 Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))

+ 4 - 1
cura/Settings/CuraStackBuilder.py

@@ -9,7 +9,6 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
 
 from .GlobalStack import GlobalStack
 from .ExtruderStack import ExtruderStack
-from .CuraContainerStack import CuraContainerStack
 from typing import Optional
 
 
@@ -76,6 +75,8 @@ class CuraStackBuilder:
         user_container = InstanceContainer(new_stack_id + "_user")
         user_container.addMetaDataEntry("type", "user")
         user_container.addMetaDataEntry("extruder", new_stack_id)
+        from cura.CuraApplication import CuraApplication
+        user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
         user_container.setDefinition(machine_definition)
 
         stack.setUserChanges(user_container)
@@ -124,6 +125,8 @@ class CuraStackBuilder:
         user_container = InstanceContainer(new_stack_id + "_user")
         user_container.addMetaDataEntry("type", "user")
         user_container.addMetaDataEntry("machine", new_stack_id)
+        from cura.CuraApplication import CuraApplication
+        user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
         user_container.setDefinition(definition)
 
         stack.setUserChanges(user_container)

+ 17 - 7
cura/Settings/ExtruderManager.py

@@ -1,4 +1,4 @@
-# Copyright (c) 2016 Ultimaker B.V.
+# Copyright (c) 2017 Ultimaker B.V.
 # Cura is released under the terms of the AGPLv3 or higher.
 
 from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant #For communicating data and events to Qt.
@@ -20,6 +20,7 @@ from typing import Optional, List, TYPE_CHECKING, Union
 
 if TYPE_CHECKING:
     from cura.Settings.ExtruderStack import ExtruderStack
+    from cura.Settings.GlobalStack import GlobalStack
 
 
 ##  Manages all existing extruder stacks.
@@ -76,8 +77,9 @@ class ExtruderManager(QObject):
     @pyqtProperty("QVariantMap", notify=extrudersChanged)
     def extruderIds(self):
         map = {}
-        for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
-            map[position] = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position].getId()
+        global_stack_id = Application.getInstance().getGlobalContainerStack().getId()
+        for position in self._extruder_trains[global_stack_id]:
+            map[position] = self._extruder_trains[global_stack_id][position].getId()
         return map
 
     @pyqtSlot(str, result = str)
@@ -85,7 +87,7 @@ class ExtruderManager(QObject):
         for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
             extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position]
             if extruder.getId() == id:
-                return extruder.findContainer(type = "quality_changes").getId()
+                return extruder.qualityChanges.getId()
 
     ##  The instance of the singleton pattern.
     #
@@ -235,6 +237,13 @@ class ExtruderManager(QObject):
         if machine_id not in self._extruder_trains:
             self._extruder_trains[machine_id] = {}
             changed = True
+
+        # do not register if an extruder has already been registered at the position on this machine
+        if any(item.getId() == extruder_train.getId() for item in self._extruder_trains[machine_id].values()):
+            Logger.log("w", "Extruder [%s] has already been registered on machine [%s], not doing anything",
+                       extruder_train.getId(), machine_id)
+            return
+
         if extruder_train:
             self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
             changed = True
@@ -362,6 +371,8 @@ class ExtruderManager(QObject):
             user_profile = InstanceContainer(extruder_stack_id + "_current_settings")  # Add an empty user profile.
             user_profile.addMetaDataEntry("type", "user")
             user_profile.addMetaDataEntry("extruder", extruder_stack_id)
+            from cura.CuraApplication import CuraApplication
+            user_profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
             user_profile.setDefinition(machine_definition)
             container_registry.addContainer(user_profile)
         container_stack.addContainer(user_profile)
@@ -457,10 +468,9 @@ class ExtruderManager(QObject):
     #   \param machine_id The machine to remove the extruders for.
     def removeMachineExtruders(self, machine_id: str):
         for extruder in self.getMachineExtruders(machine_id):
-            containers = ContainerRegistry.getInstance().findInstanceContainers(type = "user", extruder = extruder.getId())
-            for container in containers:
-                ContainerRegistry.getInstance().removeContainer(container.getId())
+            ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId())
             ContainerRegistry.getInstance().removeContainer(extruder.getId())
+        del self._extruder_trains[machine_id]
 
     ##  Returns extruders for a specific machine.
     #

+ 15 - 6
cura/Settings/ExtruderStack.py

@@ -1,22 +1,21 @@
 # Copyright (c) 2017 Ultimaker B.V.
 # Cura is released under the terms of the AGPLv3 or higher.
 
-from typing import Any
-
-from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
+from typing import Any, TYPE_CHECKING, Optional
 
 from UM.Decorators import override
 from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
-from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError
+from UM.Settings.ContainerStack import ContainerStack
 from UM.Settings.ContainerRegistry import ContainerRegistry
-from UM.Settings.InstanceContainer import InstanceContainer
-from UM.Settings.DefinitionContainer import DefinitionContainer
 from UM.Settings.Interfaces import ContainerInterface
 
 from . import Exceptions
 from .CuraContainerStack import CuraContainerStack
 from .ExtruderManager import ExtruderManager
 
+if TYPE_CHECKING:
+    from cura.Settings.GlobalStack import GlobalStack
+
 ##  Represents an Extruder and its related containers.
 #
 #
@@ -38,6 +37,10 @@ class ExtruderStack(CuraContainerStack):
         # For backward compatibility: Register the extruder with the Extruder Manager
         ExtruderManager.getInstance().registerExtruder(self, stack.id)
 
+    @override(ContainerStack)
+    def getNextStack(self) -> Optional["GlobalStack"]:
+        return super().getNextStack()
+
     @classmethod
     def getLoadingPriority(cls) -> int:
         return 3
@@ -59,6 +62,12 @@ class ExtruderStack(CuraContainerStack):
         if not super().getProperty(key, "settable_per_extruder"):
             return self.getNextStack().getProperty(key, property_name)
 
+        limit_to_extruder = super().getProperty(key, "limit_to_extruder")
+        if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder):
+            result = self.getNextStack().extruders[str(limit_to_extruder)].getProperty(key, property_name)
+            if result is not None:
+                return result
+
         return super().getProperty(key, property_name)
 
     @override(CuraContainerStack)

Some files were not shown because too many files changed in this diff